Skip to main content

atmos/os_lib/aura/builtins/
system.rs

1// 分割: aura/builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ6)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5/// ICMP Echo Request (Ping) を送信するビルトイン関数。
6/// IPアドレスの他、ホスト名(ドメイン名)が指定された場合は自動的に DNS 解決を試みます。
7pub(crate) fn builtin_ping(env: &mut Env, args: &[AST]) -> Result<Value, String> {
8    let vals = eval_args(env, args)?;
9    if vals.is_empty() {
10        return Err(String::from("USAGE: ping(\"host_or_ip\")"));
11    }
12    let ip_s = unquote(&vals[0]);
13
14    // まず指定された文字列をIPv4アドレスとしてパースを試みる
15    let dst_ip = match crate::kernel::net::parse_ipv4(&ip_s) {
16        Some(ip) => ip,
17        None => {
18            // パースに失敗した場合(ドメイン名が指定されたと判断)、DNS Aレコードの解決(UDP経由)を試みる
19            match crate::kernel::net::dns_query_a_via_udp(&ip_s) {
20                Ok(ip) => ip,
21                Err(e) => {
22                    return Err(alloc::format!(
23                        "ERROR: invalid IPv4 address or DNS query failed: {}",
24                        e
25                    ))
26                }
27            }
28        }
29    };
30
31    match crate::kernel::net::ping_once_real(dst_ip) {
32        Ok((seq, rtt_ms, bytes)) => Ok(Value::Str(alloc::format!(
33            "PING {}: seq={} bytes={} time={}ms",
34            crate::kernel::net::format_ipv4(dst_ip),
35            seq,
36            bytes,
37            rtt_ms
38        ))),
39        Err(e) => Err(alloc::format!("PING error: {}", e)),
40    }
41}
42
43/// アプリケーション起動時のウィンドウ初期化用設定パラメータ
44pub(crate) struct WindowLaunchConfig {
45    /// ウィンドウ表示状態 ("normal" / "maximized" / "minimized" / "fullscreen")
46    state: &'static str,
47    /// 通常(normal)表示時のデフォルトの幅
48    width: u32,
49    /// 通常(normal)表示時のデフォルトの高さ
50    height: u32,
51    /// デフォルトの左上X座標 (通常表示時の原点位置)
52    def_x: i32,
53    /// デフォルトの左上Y座標 (通常表示時の原点位置)
54    def_y: i32,
55}
56
57/// アプリ起動引数からウィンドウ状態およびサイズに関するオプションをパースするヘルパー関数
58///
59/// 状態オプション(normal 等)やサイズ指定(800x600 等)はアプリ本体への引数から除去されます。
60pub(crate) fn parse_launch_options(
61    app_args: &[Value],
62    raw_name: &str,
63    manifest: &crate::kernel::app_sandbox::AppManifest,
64) -> (Vec<Value>, WindowLaunchConfig) {
65    let mut state = "maximized"; // デフォルトは最大化
66    let mut custom_size = None;
67    let mut cleaned_args = alloc::vec![];
68
69    for val in app_args {
70        let s = unquote(val);
71        if s == "normal" || s == "maximized" || s == "minimized" || s == "fullscreen" {
72            state = if s == "normal" {
73                "normal"
74            } else if s == "maximized" {
75                "maximized"
76            } else if s == "minimized" {
77                "minimized"
78            } else {
79                "fullscreen"
80            };
81        } else if let Some((left, right)) = s.split_once('x') {
82            if let (Ok(w), Ok(h)) = (left.parse::<u32>(), right.parse::<u32>()) {
83                custom_size = Some((w, h));
84                state = "normal"; // サイズ直接指定があれば自動的に通常状態
85            } else {
86                cleaned_args.push(val.clone());
87            }
88        } else {
89            cleaned_args.push(val.clone());
90        }
91    }
92
93    let (def_w, def_h) = if let Some((w, h)) = custom_size {
94        (w, h)
95    } else if let (Some(w), Some(h)) = (manifest.width, manifest.height) {
96        (w, h)
97    } else {
98        (500, 600)
99    };
100
101    let (def_x, def_y) = match raw_name {
102        "browser" => (100, 100),
103        "note" => (150, 150),
104        "settings" => (150, 100),
105        "music" => (180, 120),
106        "files" => (140, 90),
107        "drive" => (160, 110),
108        "onedrive" => (170, 120),
109        _ => (100, 100),
110    };
111
112    (
113        cleaned_args,
114        WindowLaunchConfig {
115            state,
116            width: def_w,
117            height: def_h,
118            def_x,
119            def_y,
120        },
121    )
122}
123
124/// 解析された `WindowLaunchConfig` に従ってウィンドウオブジェクトを生成し、WindowManager に登録する
125pub(crate) fn create_and_register_window(
126    config: WindowLaunchConfig,
127    title: &str,
128    app: Box<dyn crate::kernel::window_mgr::App>,
129) {
130    let (sw, sh) = unsafe {
131        if let Some(screen) = crate::CURRENT_SCREEN.as_ref() {
132            (screen.width, screen.height)
133        } else {
134            (1024, 768) // 画面が取得できない場合の安全なデフォルトフォールバック
135        }
136    };
137
138    let (win_x, win_y, win_w, win_h, is_fs, is_min) = match config.state {
139        "normal" => (
140            config.def_x,
141            config.def_y,
142            config.width,
143            config.height,
144            false,
145            false,
146        ),
147        "fullscreen" => (0, 0, sw, sh, true, false),
148        "minimized" => (
149            config.def_x,
150            config.def_y,
151            config.width,
152            config.height,
153            false,
154            true,
155        ),
156        _ => {
157            // maximized: 画面全体を使う(タスクバー等の予約領域は無い)。
158            (0, 0, sw, sh, false, false)
159        }
160    };
161
162    // ProxyApp を使用して、アプリケーションスレッドを Core 2 または 3 で非同期に立ち上げる
163    use alloc::sync::Arc;
164    use spin::Mutex;
165    use core::sync::atomic::AtomicBool;
166    use crate::kernel::window_mgr::{ProxyApp, AppRunnerParams, app_runner_entry};
167
168    let shared_buf = Arc::new(Mutex::new(alloc::vec![0u32; (win_w * win_h) as usize]));
169    let event_q = Arc::new(Mutex::new(alloc::vec![]));
170    let is_alive = Arc::new(AtomicBool::new(true));
171    let active_tab = Arc::new(Mutex::new(0));
172    let tabs = Arc::new(Mutex::new(app.tabs()));
173    let needs_timer_redraw = Arc::new(AtomicBool::new(true));
174    // app_runner 側が shared_buffer を更新した際に Core 0 の dirty 判定へ伝達するフラグ
175    let buffer_updated = Arc::new(AtomicBool::new(false));
176    let app_name = app.name();
177
178    let proxy = ProxyApp::new(
179        app_name,
180        shared_buf.clone(),
181        event_q.clone(),
182        is_alive.clone(),
183        active_tab.clone(),
184        tabs.clone(),
185        needs_timer_redraw.clone(),
186        buffer_updated.clone(),
187    );
188
189    let params = Box::new(AppRunnerParams {
190        app,
191        shared_buffer: shared_buf,
192        event_queue: event_q,
193        is_alive,
194        active_tab,
195        tabs,
196        needs_timer_redraw,
197        buffer_updated,
198        width: win_w,
199        height: win_h,
200    });
201    let params_ptr = Box::into_raw(params) as usize;
202
203    static mut NEXT_APP_CORE: usize = 2;
204    let target_core = unsafe {
205        let c = NEXT_APP_CORE;
206        NEXT_APP_CORE = if c == 2 { 3 } else { 2 };
207        c
208    };
209
210    crate::kernel::scheduler::spawn_with_arg_on_core(
211        app_runner_entry,
212        params_ptr,
213        crate::kernel::scheduler::Priority::Normal,
214        "app_runner",
215        target_core,
216    );
217
218    let mut win = crate::kernel::window_mgr::Window::new(win_x, win_y, win_w, win_h, title, Box::new(proxy));
219    if config.state == "maximized" || config.state == "fullscreen" {
220        win.saved_x = config.def_x;
221        win.saved_y = config.def_y;
222        win.saved_w = config.width;
223        win.saved_h = config.height;
224    }
225    win.is_fullscreen = is_fs;
226    win.is_minimized = is_min;
227    crate::kernel::window_mgr::get_instance().add_window(win);
228}
229
230pub(crate) fn builtin_sys_launch_gui(env: &mut Env, args: &[AST]) -> Result<Value, String> {
231    let vals = eval_args(env, args)?;
232    if vals.is_empty() {
233        return Err(String::from(
234            "USAGE: sys_launch_gui(\"app_name\" [\"args\"])",
235        ));
236    }
237    let raw_name = unquote(&vals[0]);
238
239    // unpack arguments if passed as a single list (which ARGS would be)
240    let app_args = if vals.len() > 1 {
241        if let Value::List(l) = &vals[1] {
242            l.clone()
243        } else {
244            vals[1..].to_vec()
245        }
246    } else {
247        alloc::vec![]
248    };
249
250    let manifest_path = alloc::format!("/apps/{}/manifest.json", raw_name);
251    let fs = crate::kernel::fs::get_fs();
252    let manifest = if let Some((_meta, manifest_bytes)) = fs.read_file(&manifest_path) {
253        core::str::from_utf8(&manifest_bytes)
254            .ok()
255            .and_then(crate::kernel::app_sandbox::AppManifest::parse)
256            .unwrap_or_default()
257    } else {
258        crate::kernel::app_sandbox::AppManifest::default()
259    };
260
261    let (cleaned_args, config) = parse_launch_options(&app_args, &raw_name, &manifest);
262
263    if raw_name == "browser" {
264        let mut app = crate::apps::browser::WebBrowser::new();
265        let top = app.top_offset();
266        let win_title = if cleaned_args.is_empty() {
267            String::from("Browser - AtmOS")
268        } else {
269            let url_arg = unquote(&cleaned_args[0]);
270            if url_arg.starts_with('/') {
271                if let Some((_meta, content)) = fs.read_file(&url_arg) {
272                    if let Ok(html) = core::str::from_utf8(&content) {
273                        app.engine.load_html(html, &url_arg, top);
274                    } else {
275                        app.engine.load_html(
276                            "<html><body><h1>Invalid UTF-8 in local file</h1></body></html>",
277                            &url_arg,
278                            top,
279                        );
280                    }
281                } else {
282                    app.engine.load_html(
283                        "<html><body><h1>File not found</h1></body></html>",
284                        &url_arg,
285                        top,
286                    );
287                }
288            } else {
289                let mut is_https = false;
290                let clean_url = if let Some(rest) = url_arg.strip_prefix("http://") {
291                    rest
292                } else if let Some(rest) = url_arg.strip_prefix("https://") {
293                    is_https = true;
294                    rest
295                } else {
296                    &url_arg
297                };
298                let parts: Vec<&str> = clean_url.splitn(2, '/').collect();
299                let host = parts[0];
300                let path = if parts.len() > 1 {
301                    alloc::format!("/{}", parts[1])
302                } else {
303                    String::from("/")
304                };
305                app.engine.load_page(is_https, host, &path, false, top);
306            }
307            alloc::format!("Browser - {}", url_arg)
308        };
309        create_and_register_window(config, &win_title, Box::new(app));
310        return Ok(Value::Str(String::from("Launched Browser")));
311    } else if raw_name == "note" {
312        let file_arg = if !cleaned_args.is_empty() {
313            unquote(&cleaned_args[0])
314        } else {
315            String::from("Untitled")
316        };
317        let app = if file_arg != "Untitled" {
318            let content = fs
319                .read_file(&file_arg)
320                .and_then(|(_meta, bytes)| core::str::from_utf8(&bytes).ok().map(String::from))
321                .unwrap_or_default();
322            crate::apps::note::NoteApp::open_file(&file_arg, &content)
323        } else {
324            crate::apps::note::NoteApp::new()
325        };
326        create_and_register_window(
327            config,
328            &alloc::format!("Note - {}", file_arg),
329            Box::new(app),
330        );
331        return Ok(Value::Str(String::from("Launched Note")));
332    } else if raw_name == "settings" {
333        let app = crate::apps::settings::SettingsApp::new();
334        create_and_register_window(config, "OS Settings", Box::new(app));
335        return Ok(Value::Str(String::from("Launched OS Settings App")));
336    } else if raw_name == "music" {
337        let app = crate::apps::music::MusicApp::new();
338        create_and_register_window(config, "Music Player", Box::new(app));
339        return Ok(Value::Str(String::from("Launched Music Player App")));
340    } else if raw_name == "files" {
341        let app = crate::apps::files::FilesApp::new();
342        create_and_register_window(config, "Files", Box::new(app));
343        return Ok(Value::Str(String::from("Launched Files App")));
344    } else if raw_name == "drive" {
345        let app = crate::apps::drive::DriveApp::new();
346        create_and_register_window(config, "Google Drive", Box::new(app));
347        return Ok(Value::Str(String::from("Launched Google Drive App")));
348    } else if raw_name == "onedrive" {
349        let app = crate::apps::onedrive::OneDriveApp::new();
350        create_and_register_window(config, "OneDrive", Box::new(app));
351        return Ok(Value::Str(String::from("Launched OneDrive App")));
352    } else if raw_name == "eldemo" {
353        crate::kernel::scheduler::spawn_user_on_core(
354            crate::user_pixel_demo,
355            crate::kernel::scheduler::Priority::Normal,
356            "el0_pixel_demo",
357            0,
358        );
359        return Ok(Value::Str(String::from(
360            "Launched EL0 Pixel Demo (sandboxed)",
361        )));
362    }
363
364    Err(alloc::format!("Unknown native GUI app: {}", raw_name))
365}
366
367pub(crate) fn builtin_settings(env: &mut Env, args: &[AST]) -> Result<Value, String> {
368    let vals = eval_args(env, args)?;
369    let manifest = crate::kernel::app_sandbox::AppManifest::default();
370    let (_cleaned, config) = parse_launch_options(&vals, "settings", &manifest);
371    let app = crate::apps::settings::SettingsApp::new();
372    create_and_register_window(config, "OS Settings", Box::new(app));
373    Ok(Value::Str(String::from("Launched OS Settings App")))
374}
375
376pub(crate) fn builtin_music(env: &mut Env, args: &[AST]) -> Result<Value, String> {
377    let vals = eval_args(env, args)?;
378    let manifest = crate::kernel::app_sandbox::AppManifest::default();
379    let (_cleaned, config) = parse_launch_options(&vals, "music", &manifest);
380    let app = crate::apps::music::MusicApp::new();
381    create_and_register_window(config, "Music Player", Box::new(app));
382    Ok(Value::Str(String::from("Launched Music Player App")))
383}
384
385pub(crate) fn builtin_files(env: &mut Env, args: &[AST]) -> Result<Value, String> {
386    let vals = eval_args(env, args)?;
387    let manifest = crate::kernel::app_sandbox::AppManifest::default();
388    let (_cleaned, config) = parse_launch_options(&vals, "files", &manifest);
389    let app = crate::apps::files::FilesApp::new();
390    create_and_register_window(config, "Files", Box::new(app));
391    let cwd = if let Some(Value::Str(c)) = env.get("cwd") {
392        c.clone()
393    } else {
394        String::from("/user/")
395    };
396    Ok(Value::Str(alloc::format!(
397        "FILES: Opened Files window in {}",
398        cwd
399    )))
400}
401
402pub(crate) fn builtin_help(env: &mut Env, args: &[AST]) -> Result<Value, String> {
403    // help("net") のようにカテゴリを指定すると詳細を表示する
404    let topic = if !args.is_empty() {
405        let vals = eval_args(env, args)?;
406        unquote(&vals[0]).to_lowercase()
407    } else {
408        String::new()
409    };
410
411    let mut out = String::new();
412    match topic.as_str() {
413        "net" | "network" => {
414            out.push_str("Network (net.*)\n");
415            out.push_str("  net.info                  show interface / IP / link state\n");
416            out.push_str("  net.nic                   NIC + DHCP debug info\n");
417            out.push_str("  net.dhcp(\"show\"|\"start\"|\"renew\"|\"release\")  DHCP client\n");
418            out.push_str("  net.ping(\"ip\")            send ICMP echo\n");
419            out.push_str("  net.dns(\"host\")           resolve hostname via DNS\n");
420            out.push_str("  net.ntp                   sync wall clock via NTP\n");
421            out.push_str("  net.http(\"get\" host [path])      HTTP GET\n");
422            out.push_str("  net.http(\"post\" host path ct body)  HTTP POST\n");
423            out.push_str("  net.https(\"get\" host [path])     HTTPS GET (TLS 1.3)\n");
424            out.push_str("  net.tcp(...)  net.udp(...)  net.arp(...)  low-level\n");
425        }
426        "fs" | "file" | "files" => {
427            out.push_str("Files (top-level + fs.*)\n");
428            out.push_str("  ls [dir]   cd <dir>   pwd   mkdir <dir>\n");
429            out.push_str("  cp <src> <dst>   mv <src> <dst>   rm <path>\n");
430            out.push_str("  cat <file>   head <file>   tail <file>   grep \"pat\" <file>\n");
431            out.push_str("  fs.save(name \"data\" \"labels\")   save labelled file\n");
432            out.push_str("  fs.find(\"label\")                find files by label\n");
433            out.push_str("  os.addlabel(name \"label\")       add labels to a file\n");
434            out.push_str("  (these also exist under fs.*, e.g. fs.ls, fs.cat)\n");
435        }
436        "math" => {
437            out.push_str("Math (math.*) — arbitrary-precision\n");
438            out.push_str("  abs sign sqrt exp log2 log10  factorial random\n");
439            out.push_str("  sin cos tan  arc-sin arc-cos arc-tan  hyp-sin ...\n");
440            out.push_str("  greatest-com-div  least-com-mul  odd even\n");
441            out.push_str("  complex real-part imag-part  numerator denominator\n");
442            out.push_str("  PI EULER   bit.and/or/xor/not/l-shift/r-shift\n");
443        }
444        "cloud" | "drive" | "gh" | "github" => {
445            out.push_str("Cloud (Phase 7 — Google Drive / GitHub)\n");
446            out.push_str("  drive ls [folder_id]      list Drive files (root, or a folder id)\n");
447            out.push_str("  drive cat <file_id>       print a Drive file's text content\n");
448            out.push_str("  drive download <id> <name>  save a Drive file's raw bytes locally\n");
449            out.push_str("  drive rm <file_id>        delete a Drive file\n");
450            out.push_str("  drive.upload(name content)  create a new text file (no shell alias)\n");
451            out.push_str("  (Drive login is device-flow via the 'drive' app: run(\"drive\"))\n");
452            out.push_str("  gh login <client_id>      request a GitHub device code\n");
453            out.push_str("  gh poll                   finish login after authorizing in browser\n");
454            out.push_str("  gh user                   show the authenticated GitHub user\n");
455            out.push_str("  gh repos                  list the authenticated user's repos\n");
456            out.push_str("  gh logout                 discard the stored GitHub token\n");
457        }
458        "script" | "lang" | "aura" => {
459            out.push_str("Aura basics\n");
460            out.push_str("  set x 10        define / assign a variable\n");
461            out.push_str("  fn(a b ...)     lambda;  func name fn(...)  named\n");
462            out.push_str("  print x   type x   if(c t e)   while(c body)   for(...)\n");
463            out.push_str("  list/cons/car/cdr/map/filter/fold/range/take\n");
464            out.push_str("  str.split/join/substring/length/replace\n");
465            out.push_str("  load(\"f.aura\")  require(\"f.aura\")   run a script file\n");
466            out.push_str("  arithmetic: + - * /   compare: = != < <= > >=   and or not\n");
467        }
468        "" => {
469            out.push_str("AtmOS Terminal — type a command, then Enter.\n");
470            out.push_str("Strings need quotes. `help(\"net\")` for category detail.\n");
471            out.push_str("---------------------------------------------------------------\n");
472            out.push_str("Apps     browser  note  files  music  settings  (type the name)\n");
473            out.push_str("Files    ls cd pwd mkdir cp mv rm cat head tail grep   help(\"fs\")\n");
474            out.push_str("Network  net.info  net.ping  net.dns  net.dhcp  net.https\n");
475            out.push_str("                                                     help(\"net\")\n");
476            out.push_str("System   os.apps  os.run(\"app\")  os.keymap(\"us\"|\"jis\")\n");
477            out.push_str("         os.settings  os.keyrepeat  date  ntp\n");
478            out.push_str("Media    sound.play(freq ms)  sound.play_mml(\"mml\")\n");
479            out.push_str("         draw.boxfill  draw.draw_string  draw.clear_screen\n");
480            out.push_str(
481                "Script   set print if while for fn  list/map/filter   help(\"script\")\n",
482            );
483            out.push_str("Math     math.sin math.sqrt ...  str.* bit.*   help(\"math\")\n");
484            out.push_str("Cloud    drive ls  drive cat <id>  gh user  gh repos   help(\"cloud\")\n");
485            out.push_str("---------------------------------------------------------------\n");
486            out.push_str("Tip: Esc exits the browser. F1 focuses the terminal.");
487        }
488        other => {
489            out.push_str(&alloc::format!("help: unknown topic '{}'.\n", other));
490            out.push_str("Try: help  |  help(\"net\")  |  help(\"fs\")  |  help(\"math\")  |  help(\"script\")  |  help(\"cloud\")");
491        }
492    }
493    Ok(Value::Str(out))
494}
495
496pub(crate) fn get_num_u32(val: &Value) -> Result<u32, String> {
497    match val {
498        Value::Num(n) => {
499            let s = n.to_string();
500            Ok(s.parse::<u32>().unwrap_or(0))
501        }
502        _ => Err(String::from("expected number")),
503    }
504}
505
506pub(crate) fn builtin_create_canvas(env: &mut Env, args: &[AST]) -> Result<Value, String> {
507    let vals = eval_args(env, args)?;
508    let title = if !vals.is_empty() {
509        unquote(&vals[0])
510    } else {
511        String::from("Canvas")
512    };
513
514    let id_num = {
515        let mut counter = crate::apps::canvas::CANVAS_ID_COUNTER.lock();
516        let current = *counter;
517        *counter += 1;
518        current
519    };
520    let id = alloc::format!("canvas_{}", id_num);
521
522    crate::apps::canvas::CANVAS_QUEUES
523        .lock()
524        .insert(id.clone(), alloc::collections::VecDeque::new());
525
526    let app = crate::apps::canvas::CanvasApp::new(id.clone(), title.clone(), 400, 300);
527    let win = crate::kernel::window_mgr::Window::new(200, 150, 400, 300, &title, Box::new(app));
528    crate::kernel::window_mgr::get_instance().add_window(win);
529    crate::kernel::window_mgr::get_instance().dirty = true;
530
531    Ok(Value::Str(id))
532}
533
534pub(crate) fn builtin_boxfill(env: &mut Env, args: &[AST]) -> Result<Value, String> {
535    let vals = eval_args(env, args)?;
536    if vals.len() < 6 {
537        return Err(String::from(
538            "USAGE: boxfill(canvas_id, x0, y0, x1, y1, color)",
539        ));
540    }
541    let canvas_id = unquote(&vals[0]);
542    let x0 = get_num_u32(&vals[1])?;
543    let y0 = get_num_u32(&vals[2])?;
544    let x1 = get_num_u32(&vals[3])?;
545    let y1 = get_num_u32(&vals[4])?;
546    let color = get_num_u32(&vals[5])?;
547
548    {
549        let mut queues = crate::apps::canvas::CANVAS_QUEUES.lock();
550        if let Some(q) = queues.get_mut(&canvas_id) {
551            q.push_back(crate::apps::canvas::DrawCommand::BoxFill {
552                x0,
553                y0,
554                x1,
555                y1,
556                color,
557            });
558        } else {
559            return Err(alloc::format!("Canvas ID not found: {}", canvas_id));
560        }
561    }
562    crate::kernel::window_mgr::get_instance().dirty = true;
563
564    Ok(Value::Str(alloc::format!(
565        "boxfill('{}', {}, {}, {}, {}, 0x{:08X})",
566        canvas_id,
567        x0,
568        y0,
569        x1,
570        y1,
571        color
572    )))
573}
574
575pub(crate) fn builtin_draw_string(env: &mut Env, args: &[AST]) -> Result<Value, String> {
576    let vals = eval_args(env, args)?;
577    if vals.len() < 6 {
578        return Err(String::from(
579            "USAGE: draw_string(canvas_id, x, y, text, color, size)",
580        ));
581    }
582    let canvas_id = unquote(&vals[0]);
583    let x = get_num_u32(&vals[1])?;
584    let y = get_num_u32(&vals[2])?;
585    let text = unquote(&vals[3]);
586    let color = get_num_u32(&vals[4])?;
587    let size = get_num_u32(&vals[5])?;
588
589    {
590        let mut queues = crate::apps::canvas::CANVAS_QUEUES.lock();
591        if let Some(q) = queues.get_mut(&canvas_id) {
592            q.push_back(crate::apps::canvas::DrawCommand::DrawString {
593                x,
594                y,
595                text: text.clone(),
596                color,
597                size,
598            });
599        } else {
600            return Err(alloc::format!("Canvas ID not found: {}", canvas_id));
601        }
602    }
603    crate::kernel::window_mgr::get_instance().dirty = true;
604
605    Ok(Value::Str(alloc::format!(
606        "draw_string('{}', {}, {}, '{}', 0x{:08X}, {})",
607        canvas_id,
608        x,
609        y,
610        text,
611        color,
612        size
613    )))
614}
615
616pub(crate) fn builtin_clear_screen(env: &mut Env, args: &[AST]) -> Result<Value, String> {
617    let vals = eval_args(env, args)?;
618    if vals.len() < 2 {
619        return Err(String::from("USAGE: clear_screen(canvas_id, color)"));
620    }
621    let canvas_id = unquote(&vals[0]);
622    let color = get_num_u32(&vals[1])?;
623
624    {
625        let mut queues = crate::apps::canvas::CANVAS_QUEUES.lock();
626        if let Some(q) = queues.get_mut(&canvas_id) {
627            q.push_back(crate::apps::canvas::DrawCommand::ClearScreen { color });
628        } else {
629            return Err(alloc::format!("Canvas ID not found: {}", canvas_id));
630        }
631    }
632    crate::kernel::window_mgr::get_instance().dirty = true;
633
634    Ok(Value::Str(alloc::format!(
635        "clear_screen('{}', 0x{:08X})",
636        canvas_id,
637        color
638    )))
639}
640
641pub fn builtin_run(env: &mut Env, args: &[AST]) -> Result<Value, String> {
642    let vals = eval_args(env, args)?;
643    if vals.is_empty() {
644        return Err(String::from(
645            "USAGE: run(\"app_name_or_filename\" [\"arg\"])",
646        ));
647    }
648    let raw_name = unquote(&vals[0]);
649
650    // --- Step 2: /apps/{name}/ パッケージを自動検索 ---
651    let manifest_path = alloc::format!("/apps/{}/manifest.json", raw_name);
652    let fs = crate::kernel::fs::get_fs();
653    if let Some((_meta, manifest_bytes)) = fs.read_file(&manifest_path) {
654        // manifest.json をパース(失敗時はデフォルト値を使用)
655        let manifest = core::str::from_utf8(&manifest_bytes)
656            .ok()
657            .and_then(crate::kernel::app_sandbox::AppManifest::parse)
658            .unwrap_or_default();
659
660        let entry_path = alloc::format!("/apps/{}/{}", raw_name, manifest.entry);
661        let display = if manifest.display_name.is_empty() {
662            manifest.name.clone()
663        } else {
664            manifest.display_name.clone()
665        };
666
667        if let Some((_meta2, entry_bytes)) = fs.read_file(&entry_path) {
668            let script = match core::str::from_utf8(&entry_bytes) {
669                Ok(s) => s,
670                Err(_) => {
671                    return Err(alloc::format!(
672                        "ERROR: App '{}' entry file contains invalid UTF-8",
673                        raw_name
674                    ))
675                }
676            };
677
678            let tokens = super::super::lexer::tokenize(script);
679            if tokens.is_empty() {
680                return Ok(Value::Str(alloc::format!(
681                    "App '{}' ({}): entry script is empty",
682                    raw_name,
683                    display
684                )));
685            }
686
687            let asts = match super::super::parser::parse(&tokens) {
688                Ok(a) => a,
689                Err(e) => return Err(alloc::format!("Syntax Error in app '{}': {}", raw_name, e)),
690            };
691
692            // サンドボックス付き環境で実行
693            let sandbox = crate::kernel::app_sandbox::AppSandbox::new(0, manifest);
694            let mut app_env = super::super::eval::Env::with_sandbox(sandbox);
695            // 親環境から cwd を引き継ぎ
696            if let Some(cwd_val) = env.get("cwd") {
697                app_env.set(alloc::string::String::from("cwd"), cwd_val);
698            }
699
700            // 引数 ARGS の設定
701            let mut args_list = Vec::new();
702            for v in &vals[1..] {
703                args_list.push(v.clone());
704            }
705            app_env.set(alloc::string::String::from("ARGS"), Value::List(args_list));
706
707            super::super::builtins::register(&mut app_env);
708
709            let mut last_val = Value::Str(String::from(""));
710            for ast in asts {
711                let val = eval(&mut app_env, &ast)?;
712                last_val = force_eval(&mut app_env, val)?;
713            }
714            return Ok(last_val);
715        } else {
716            return Err(alloc::format!(
717                "ERROR: App '{}' entry file '{}' not found in /apps/",
718                raw_name,
719                entry_path
720            ));
721        }
722    }
723
724    // --- Step 3: 従来のパス解決によるスクリプト実行 ---
725    let filename = resolve_path(env, &raw_name)?;
726    if let Some((_meta, content)) = fs.read_file(&filename) {
727        let script = match core::str::from_utf8(&content) {
728            Ok(s) => s,
729            Err(_) => return Err(String::from("ERROR: Script file contains invalid UTF-8")),
730        };
731
732        let tokens = super::super::lexer::tokenize(script);
733        if tokens.is_empty() {
734            return Ok(Value::Str(String::from("RUN: Script is empty")));
735        }
736
737        let asts = match super::super::parser::parse(&tokens) {
738            Ok(a) => a,
739            Err(e) => return Err(alloc::format!("Syntax Error in '{}': {}", filename, e)),
740        };
741
742        let mut last_val = Value::Str(String::from(""));
743        for ast in asts {
744            let val = eval(env, &ast)?;
745            last_val = force_eval(env, val)?;
746        }
747
748        Ok(last_val)
749    } else {
750        Err(alloc::format!(
751            "ERROR: '{}' not found. Check /apps/ or file path.",
752            raw_name
753        ))
754    }
755}
756
757/// apps() - /apps/ 配下のインストール済みアプリを一覧表示
758pub(crate) fn builtin_apps(_env: &mut Env, _args: &[AST]) -> Result<Value, String> {
759    use alloc::collections::BTreeSet;
760    let fs = crate::kernel::fs::get_fs();
761    let entries = fs.list_dir("/apps/");
762
763    // /apps/ 配下の全ファイルからアプリ名(最初のパスコンポーネント)を一意に集める。
764    // 旧実装はファイル毎に 1 行出していたため、同一アプリが複数回表示される不具合があった。
765    let mut names: BTreeSet<String> = BTreeSet::new();
766    for entry in &entries {
767        let filepath = entry.get_filename();
768        let without_prefix = filepath
769            .trim_start_matches("/apps/")
770            .trim_start_matches("apps/");
771        let app_name = without_prefix
772            .split('/')
773            .next()
774            .unwrap_or("")
775            .trim_matches('/');
776        if !app_name.is_empty() {
777            names.insert(String::from(app_name));
778        }
779    }
780
781    if names.is_empty() {
782        return Ok(Value::Str(String::from(
783            "Installed Apps (/apps/)\n-----------------------\n  (no apps installed)\n\nInstall: copy an app directory to /apps/<name>/"
784        )));
785    }
786
787    let mut out = String::from("Installed Apps (/apps/)  [click to launch]\n");
788    out.push_str("---------------------------------------------------------------------\n");
789    out.push_str("   Name             Version   Display name\n");
790    out.push_str("---------------------------------------------------------------------\n");
791
792    for app_name in &names {
793        let manifest_path = alloc::format!("/apps/{}/manifest.json", app_name);
794        let (manifest, _has_manifest) = if let Some((_meta, bytes)) = fs.read_file(&manifest_path) {
795            let m = core::str::from_utf8(&bytes)
796                .ok()
797                .and_then(crate::kernel::app_sandbox::AppManifest::parse)
798                .unwrap_or_default();
799            (m, true)
800        } else {
801            let m = crate::kernel::app_sandbox::AppManifest {
802                name: app_name.clone(),
803                ..Default::default()
804            };
805            (m, false)
806        };
807
808        let name = if manifest.name.is_empty() {
809            app_name.clone()
810        } else {
811            manifest.name.clone()
812        };
813        let version = if manifest.version.is_empty() {
814            String::from("-")
815        } else {
816            manifest.version.clone()
817        };
818        let display = if manifest.display_name.is_empty() {
819            name.clone()
820        } else {
821            manifest.display_name.clone()
822        };
823
824        // マーカー行: \x02LAUNCHER:<appname>:<name_col>/<version_col>/<display_col>\x02
825        // ターミナルの draw() がアイコン + テキストとして描画し、クリック可能にする。
826        let row_text = alloc::format!(
827            "   {:<16} {:<9} {}",
828            truncate_cols(&name, 16),
829            truncate_cols(&version, 9),
830            truncate_cols(&display, 22),
831        );
832        out.push('\x02');
833        out.push_str("LAUNCHER:");
834        out.push_str(app_name);
835        out.push(':');
836        out.push_str(&row_text);
837        out.push('\x02');
838        out.push('\n');
839    }
840
841    out.push_str("---------------------------------------------------------------------\n");
842    out.push_str(&alloc::format!("{} app(s) installed.\n", names.len()));
843    out.push_str("Run     : os.run(\"<name>\") or click a row above\n");
844    out.push_str("Install : copy an app dir to /apps/<name>/  (no installer needed)\n");
845    out.push_str("Remove  : delete the /apps/<name>/ directory");
846
847    Ok(Value::Str(out))
848}
849
850/// 表の桁あふれを防ぐため、文字数で切り詰める(マルチバイト安全)。
851pub(crate) fn truncate_cols(s: &str, max_chars: usize) -> String {
852    if s.chars().count() <= max_chars {
853        String::from(s)
854    } else {
855        let mut t: String = s.chars().take(max_chars.saturating_sub(1)).collect();
856        t.push('…');
857        t
858    }
859}
860
861pub(crate) fn builtin_play(env: &mut Env, args: &[AST]) -> Result<Value, String> {
862    let vals = eval_args(env, args)?;
863    if vals.len() < 2 {
864        return Err(String::from("USAGE: play(frequency, duration_ms)"));
865    }
866    let freq = get_num_u32(&vals[0])?;
867    let duration = get_num_u32(&vals[1])?;
868
869    crate::kernel::audio::Audio::play_tone_async(freq, duration);
870    Ok(Value::Str(alloc::format!("play({}, {})", freq, duration)))
871}
872
873pub(crate) fn builtin_play_mml(env: &mut Env, args: &[AST]) -> Result<Value, String> {
874    let vals = eval_args(env, args)?;
875    if vals.is_empty() {
876        return Err(String::from("USAGE: play_mml(\"mml_string\")"));
877    }
878    let mml = unquote(&vals[0]);
879
880    crate::kernel::audio::Audio::play_mml(&mml);
881    Ok(Value::Str(alloc::format!("play_mml('{}')", mml)))
882}
883
884pub(crate) fn builtin_keyrepeat(env: &mut Env, args: &[AST]) -> Result<Value, String> {
885    let vals = eval_args(env, args)?;
886
887    if vals.is_empty() {
888        unsafe {
889            let params = &crate::REPEAT_PARAMS;
890            let out = alloc::format!(
891                "Keyboard Repeat Configurations:\n\
892                 - First Repeat Delay:    {} ms\n\
893                 - First Repeat Interval: {} ms\n\
894                 - Second Repeat Count:   {} chars/loop\n\
895                 - Second Repeat Delay:   {} ms\n\
896                 - Second Repeat Interval: {} ms",
897                params.first_delay_ms,
898                params.first_interval_ms,
899                params.second_repeat_count,
900                params.second_delay_ms,
901                params.second_interval_ms
902            );
903            return Ok(Value::Str(out));
904        }
905    }
906
907    if vals.len() < 5 {
908        return Err(String::from("USAGE: keyrepeat(first_delay, first_interval, second_count, second_delay, second_interval)"));
909    }
910
911    let first_delay = get_num_u32(&vals[0])?;
912    let first_interval = get_num_u32(&vals[1])?;
913    let second_count = get_num_u32(&vals[2])?;
914    let second_delay = get_num_u32(&vals[3])?;
915    let second_interval = get_num_u32(&vals[4])?;
916
917    unsafe {
918        crate::REPEAT_PARAMS.first_delay_ms = first_delay;
919        crate::REPEAT_PARAMS.first_interval_ms = first_interval;
920        crate::REPEAT_PARAMS.second_repeat_count = second_count;
921        crate::REPEAT_PARAMS.second_delay_ms = second_delay;
922        crate::REPEAT_PARAMS.second_interval_ms = second_interval;
923    }
924
925    Ok(Value::Str(alloc::format!(
926        "SUCCESS: Keyboard repeat parameters updated:\n\
927         Delay1: {}ms, Interval1: {}ms, Repeat2 Count: {} chars, Delay2: {}ms, Interval2: {}ms",
928        first_delay,
929        first_interval,
930        second_count,
931        second_delay,
932        second_interval
933    )))
934}
935
936pub(crate) fn builtin_dom_get_value(env: &mut Env, args: &[AST]) -> Result<Value, String> {
937    let vals = eval_args(env, args)?;
938    if vals.len() != 1 {
939        return Err(String::from("dom_get_value requires 1 argument (id)"));
940    }
941    if let Value::Str(id) = &vals[0] {
942        let wm = crate::kernel::window_mgr::get_instance();
943        if !wm.windows.is_empty() {
944            let last_idx = wm.windows.len() - 1;
945            if let Some(val) = wm.windows[last_idx].app.get_dom_value(id) {
946                return Ok(Value::Str(val));
947            }
948        }
949        let browser = crate::apps::browser::get_instance();
950        if let Some(val) = browser.engine.form_values.get(id) {
951            Ok(Value::Str(val.clone()))
952        } else {
953            Ok(Value::Str(String::new()))
954        }
955    } else {
956        Err(String::from("dom_get_value id must be string"))
957    }
958}
959
960pub(crate) fn builtin_dom_set_value(env: &mut Env, args: &[AST]) -> Result<Value, String> {
961    let vals = eval_args(env, args)?;
962    if vals.len() != 2 {
963        return Err(String::from(
964            "dom_set_value requires 2 arguments (id, value)",
965        ));
966    }
967    let id = match &vals[0] {
968        Value::Str(s) => s.clone(),
969        _ => return Err(String::from("dom_set_value id must be string")),
970    };
971    let val = unquote(&vals[1]);
972
973    let mut wm = crate::kernel::window_mgr::get_instance();
974    let mut handled = false;
975    if !wm.windows.is_empty() {
976        let last_idx = wm.windows.len() - 1;
977        wm.windows[last_idx].app.set_dom_value(&id, &val);
978        handled = true;
979    }
980
981    if !handled {
982        let browser = crate::apps::browser::get_instance();
983        browser.engine.form_values.insert(id, val);
984        browser.engine.dirty = true;
985    }
986    Ok(Value::Nil)
987}
988
989/// 【2026-07-30】ブラウザを指定量スクロールする。
990///
991/// スクリーンショットでファーストビューより下のセクションを確認したかったが、
992/// キー入力が届かなかった(シリアルのエスケープシーケンスも QEMU の
993/// `sendkey pgdn` も効かない。ブラウザのキー処理は
994/// `apps::browser::is_active()` が真のときだけ動く)。
995/// 検証手段として、スクリプトから直接スクロールできるようにする。
996///
997/// `browser.scroll(600)` で 600px 下へ。負値で上へ。
998pub(crate) fn builtin_browser_scroll(env: &mut Env, args: &[AST]) -> Result<Value, String> {
999    let vals = eval_args(env, args)?;
1000    if vals.is_empty() {
1001        return Err(String::from("USAGE: browser.scroll(dy)"));
1002    }
1003    let dy: i32 = unquote(&vals[0])
1004        .trim()
1005        .parse()
1006        .map_err(|_| String::from("scroll: dy は整数で指定してください"))?;
1007    let browser = crate::apps::browser::get_instance();
1008    browser.engine.scroll(dy);
1009    browser.engine.dirty = true;
1010    Ok(Value::Str(alloc::format!("scrolled dy={}", dy)))
1011}
1012
1013pub(crate) fn builtin_dom_get_content(env: &mut Env, args: &[AST]) -> Result<Value, String> {
1014    let vals = eval_args(env, args)?;
1015    if vals.len() != 1 {
1016        return Err(String::from("dom_get_content requires 1 argument (id)"));
1017    }
1018    if let Value::Str(id) = &vals[0] {
1019        let wm = crate::kernel::window_mgr::get_instance();
1020        if !wm.windows.is_empty() {
1021            let last_idx = wm.windows.len() - 1;
1022            if let Some(val) = wm.windows[last_idx].app.get_dom_content(id) {
1023                return Ok(Value::Str(val));
1024            }
1025        }
1026        let browser = crate::apps::browser::get_instance();
1027        if let Some(val) = browser.engine.dom_contents.get(id) {
1028            Ok(Value::Str(val.clone()))
1029        } else {
1030            Ok(Value::Str(String::new()))
1031        }
1032    } else {
1033        Err(String::from("dom_get_content id must be string"))
1034    }
1035}
1036
1037pub(crate) fn builtin_dom_set_content(env: &mut Env, args: &[AST]) -> Result<Value, String> {
1038    let vals = eval_args(env, args)?;
1039    if vals.len() != 2 {
1040        return Err(String::from(
1041            "dom_set_content requires 2 arguments (id, value)",
1042        ));
1043    }
1044    let id = match &vals[0] {
1045        Value::Str(s) => s.clone(),
1046        _ => return Err(String::from("dom_set_content id must be string")),
1047    };
1048    let val = unquote(&vals[1]);
1049
1050    let mut wm = crate::kernel::window_mgr::get_instance();
1051    let mut handled = false;
1052    if !wm.windows.is_empty() {
1053        let last_idx = wm.windows.len() - 1;
1054        wm.windows[last_idx].app.set_dom_content(&id, &val);
1055        handled = true;
1056    }
1057
1058    if !handled {
1059        let browser = crate::apps::browser::get_instance();
1060        browser.engine.set_dom_content(&id, &val);
1061    }
1062    Ok(Value::Nil)
1063}
1064
1065// ---- load / require ----
1066