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 let avail_h = sh.saturating_sub(crate::kernel::fn_bar::BAR_HEIGHT);
159 (0, 0, sw, avail_h, false, false)
160 }
161 };
162
163 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 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 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 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 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 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 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 let sandbox = crate::kernel::app_sandbox::AppSandbox::new(0, manifest);
698 let mut app_env = super::super::eval::Env::with_sandbox(sandbox);
699 if let Some(cwd_val) = env.get("cwd") {
701 app_env.set(alloc::string::String::from("cwd"), cwd_val);
702 }
703
704 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 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
761pub(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 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 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
854pub(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
993pub(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