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