1use super::*;
4
5pub(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 let dst_ip = match crate::kernel::net::parse_ipv4(&ip_s) {
16 Some(ip) => ip,
17 None => {
18 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
43pub(crate) struct WindowLaunchConfig {
45 state: &'static str,
47 width: u32,
49 height: u32,
51 def_x: i32,
53 def_y: i32,
55}
56
57pub(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"; 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"; } 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
124pub(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) }
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 (0, 0, sw, sh, false, false)
159 }
160 };
161
162 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 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 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 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 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 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 let sandbox = crate::kernel::app_sandbox::AppSandbox::new(0, manifest);
694 let mut app_env = super::super::eval::Env::with_sandbox(sandbox);
695 if let Some(cwd_val) = env.get("cwd") {
697 app_env.set(alloc::string::String::from("cwd"), cwd_val);
698 }
699
700 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 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
757pub(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 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 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
850pub(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
989pub(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