1pub mod shell;
8
9use crate::kernel::draw::{Color, Screen};
10use alloc::collections::VecDeque;
11use alloc::string::String;
12use alloc::sync::Arc;
13use alloc::vec::Vec;
14use core::sync::atomic::{AtomicBool, Ordering};
15use spin::Mutex;
16
17pub struct SafeQueue<T> {
18 queue: Mutex<VecDeque<T>>,
19}
20
21impl<T> SafeQueue<T> {
22 pub fn new() -> Self {
23 Self {
24 queue: Mutex::new(VecDeque::new()),
25 }
26 }
27 pub fn push(&self, value: T) {
28 self.queue.lock().push_back(value);
29 }
30 pub fn pop(&self) -> Option<T> {
31 self.queue.lock().pop_front()
32 }
33 pub fn is_empty(&self) -> bool {
35 self.queue.lock().is_empty()
36 }
37}
38
39impl<T> Default for SafeQueue<T> {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45pub struct AuraThreadContext {
46 pub cmd_queue: SafeQueue<String>,
47 pub result_queue: SafeQueue<String>,
48 pub exit_flag: AtomicBool,
49}
50
51impl AuraThreadContext {
52 pub fn new() -> Self {
53 Self {
54 cmd_queue: SafeQueue::new(),
55 result_queue: SafeQueue::new(),
56 exit_flag: AtomicBool::new(false),
57 }
58 }
59}
60
61impl Default for AuraThreadContext {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67pub struct Terminal {
68 logs: Vec<String>,
69 input_buffer: String,
70 bg_color: Color,
71 fg_color: Color,
72 pub(super) history: Vec<String>,
73 pub(super) history_index: usize,
74 pub dirty: bool,
75 pub input_dirty: bool,
76 pub cursor_char_idx: usize,
77 pub env: Option<crate::os_lib::aura::eval::Env>,
78 pub scroll_offset: usize,
79 pub context: Option<Arc<AuraThreadContext>>,
80 pub launcher_regions: Vec<(i32, i32, u32, u32, String)>,
82}
83
84impl Default for Terminal {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90fn aura_thread_entry(ctx_ptr: usize) {
91 let ctx = unsafe { Arc::from_raw(ctx_ptr as *const AuraThreadContext) };
92 let mut env = crate::os_lib::aura::eval::setup_global_env(core::ptr::null_mut());
93
94 while !ctx.exit_flag.load(Ordering::Relaxed) {
95 if let Some(cmd) = ctx.cmd_queue.pop() {
96 let res = crate::os_lib::aura::eval_string(&cmd, &mut env);
97 ctx.result_queue.push(res);
98 }
99 crate::kernel::scheduler::yield_now();
100 }
101}
102
103impl Drop for Terminal {
104 fn drop(&mut self) {
105 if let Some(ctx) = &self.context {
106 ctx.exit_flag.store(true, Ordering::Relaxed);
107 }
108 }
109}
110
111impl Terminal {
112 pub fn new() -> Self {
113 let context = Arc::new(AuraThreadContext::new());
114 let ctx_clone = context.clone();
115 let raw_ptr = Arc::into_raw(ctx_clone) as usize;
116
117 let pid = crate::kernel::scheduler::spawn_with_arg(
118 aura_thread_entry,
119 raw_ptr,
120 crate::kernel::scheduler::Priority::Normal,
121 "aura_shell",
122 );
123 crate::info!("[Terminal] Aura shell thread spawned pid={}", pid);
124
125 Self {
126 logs: Vec::new(),
127 input_buffer: String::new(),
128 launcher_regions: Vec::new(),
129 bg_color: Color(crate::kernel::config::get_config().theme.terminal_bg),
130 fg_color: Color(crate::kernel::config::get_config().theme.terminal_fg),
131 history: Vec::new(),
132 history_index: 0,
133 dirty: true,
134 input_dirty: true,
135 cursor_char_idx: 0,
136 env: None,
137 scroll_offset: 0,
138 context: Some(context),
139 }
140 }
141
142 pub fn scroll(&mut self, lines: i32) {
143 if lines > 0 {
144 self.scroll_offset = self.scroll_offset.saturating_sub(lines as usize);
146 } else if lines < 0 {
147 let max_offset = self.logs.len();
149 self.scroll_offset = self
150 .scroll_offset
151 .saturating_add((-lines) as usize)
152 .min(max_offset);
153 }
154 self.dirty = true;
155 }
156
157 pub fn input_char(&mut self, c: char) {
158 self.scroll_offset = 0; self.dirty = true;
160
161 if c == '\x03' {
162 self.logs.push(alloc::format!("> {}^C", self.input_buffer));
164 self.input_buffer.clear();
165 self.cursor_char_idx = 0;
166 self.input_dirty = true;
167 return;
168 }
169
170 let chars: Vec<char> = self.input_buffer.chars().collect();
171 let count = chars.len();
172
173 if c == '\x08' {
174 if self.cursor_char_idx > 0 && self.cursor_char_idx <= count {
175 let mut new_chars = chars;
176 new_chars.remove(self.cursor_char_idx - 1);
177 self.input_buffer = new_chars.into_iter().collect();
178 self.cursor_char_idx -= 1;
179 }
180 } else if c.is_ascii_control() {
181 match c {
183 '\x01' => self.move_cursor_to_start(),
184 '\x05' => self.move_cursor_to_end(),
185 '\x10' => self.history_up(),
186 '\x0E' => self.history_down(),
187 '\x0B' => {
188 let chars: Vec<char> = self.input_buffer.chars().collect();
190 if self.cursor_char_idx <= chars.len() {
191 self.input_buffer = chars[0..self.cursor_char_idx].iter().collect();
192 self.input_dirty = true;
193 }
194 }
195 _ => {}
196 }
197 } else {
198 let mut new_chars = chars;
199 if self.cursor_char_idx <= count {
200 new_chars.insert(self.cursor_char_idx, c);
201 self.input_buffer = new_chars.into_iter().collect();
202 self.cursor_char_idx += 1;
203 }
204 }
205 self.input_dirty = true;
206 }
207
208 pub fn delete_char(&mut self) {
209 let chars: Vec<char> = self.input_buffer.chars().collect();
210 let count = chars.len();
211 if self.cursor_char_idx < count {
212 let mut new_chars = chars;
213 new_chars.remove(self.cursor_char_idx);
214 self.input_buffer = new_chars.into_iter().collect();
215 self.input_dirty = true;
216 }
217 }
218
219 pub fn move_cursor_left(&mut self) {
220 if self.cursor_char_idx > 0 {
221 self.cursor_char_idx -= 1;
222 self.input_dirty = true;
223 }
224 }
225
226 pub fn move_cursor_right(&mut self) {
227 let count = self.input_buffer.chars().count();
228 if self.cursor_char_idx < count {
229 self.cursor_char_idx += 1;
230 self.input_dirty = true;
231 }
232 }
233
234 pub fn move_cursor_to_start(&mut self) {
235 self.cursor_char_idx = 0;
236 self.input_dirty = true;
237 }
238
239 pub fn kill_line(&mut self) {
241 let chars: Vec<char> = self.input_buffer.chars().collect();
242 let mut end = self.cursor_char_idx;
243 while end < chars.len() && chars[end] != '\n' {
244 end += 1;
245 }
246 if end > self.cursor_char_idx {
247 let mut new_chars = chars;
248 new_chars.drain(self.cursor_char_idx..end);
249 self.input_buffer = new_chars.into_iter().collect();
250 self.input_dirty = true;
251 }
252 }
253
254 pub fn move_cursor_to_end(&mut self) {
255 self.cursor_char_idx = self.input_buffer.chars().count();
256 self.input_dirty = true;
257 }
258
259 pub fn submit(&mut self) -> Option<String> {
260 if !self.input_buffer.is_empty() {
261 let mut log_line = String::from("> ");
264 log_line.push_str(&self.input_buffer);
265 if let Some(t) = crate::kernel::timer::format_time_hms_jst() {
266 log_line.push('\x00');
267 log_line.push_str(&t);
268 }
269 self.logs.push(log_line);
270
271 let command = self.input_buffer.clone();
272
273 if self.history.last() != Some(&command) {
274 self.history.push(command.clone());
275 }
276 self.history_index = self.history.len();
277
278 self.input_buffer.clear();
279 self.cursor_char_idx = 0;
280 self.dirty = true;
281 Some(command)
282 } else {
283 None
284 }
285 }
286
287 pub fn history_up(&mut self) {
288 self.scroll_offset = 0;
289 if self.history_index > 0 {
290 self.history_index -= 1;
291 self.input_buffer = self.history[self.history_index].clone();
292 self.cursor_char_idx = self.input_buffer.chars().count();
293 self.dirty = true;
294 self.input_dirty = true;
295 }
296 }
297
298 pub fn history_down(&mut self) {
299 self.scroll_offset = 0;
300 if self.history_index < self.history.len() {
301 self.history_index += 1;
302 if self.history_index == self.history.len() {
303 self.input_buffer.clear();
304 } else {
305 self.input_buffer = self.history[self.history_index].clone();
306 }
307 self.cursor_char_idx = self.input_buffer.chars().count();
308 self.dirty = true;
309 self.input_dirty = true;
310 }
311 }
312
313 pub fn print_line(&mut self, msg: String) {
314 self.logs.push(msg);
315 self.dirty = true;
316 }
317
318 pub fn clear_logs(&mut self) {
319 self.logs.clear();
320 self.dirty = true;
321 }
322
323 pub fn has_pending_results(&self) -> bool {
327 self.context
328 .as_ref()
329 .map(|c| !c.result_queue.is_empty())
330 .unwrap_or(false)
331 }
332
333 pub fn show_history(&mut self) {
334 let history = self.history.clone();
335 for (i, cmd) in history.iter().enumerate() {
336 self.print_line(alloc::format!(" {} {}", i + 1, cmd));
337 }
338 }
339
340 pub fn handle_click(&self, x: i32, y: i32) -> Option<String> {
342 for (rx, ry, rw, rh, name) in &self.launcher_regions {
343 if x >= *rx && x < rx + *rw as i32 && y >= *ry && y < ry + *rh as i32 {
344 return Some(name.clone());
345 }
346 }
347 None
348 }
349
350 pub fn draw(&mut self, screen: &Screen, wx: i32, wy: i32, ww: u32, wh: u32) {
351 if let Some(ctx) = self.context.clone() {
352 while let Some(res) = ctx.result_queue.pop() {
353 if !res.is_empty() {
354 for line in res.split('\n') {
355 self.print_line(String::from(line));
356 }
357 }
358 }
359 }
360
361 let config = crate::kernel::config::get_config();
362 self.bg_color = Color(config.theme.terminal_bg);
363 self.fg_color = Color(config.theme.terminal_fg);
364
365 let ux0 = wx.max(0) as u32;
366 let uy0 = wy.max(0) as u32;
367 let ux1 = (wx + ww as i32).max(0) as u32;
368 let uy1 = (wy + wh as i32).max(0) as u32;
369
370 screen.boxfill(ux0, uy0, ux1, uy1, self.bg_color);
372
373 let line_height = 20; let padding_x = 10;
375 let max_width = ((ww - padding_x * 2 - 12) / 8) as usize; let mut display_lines: Vec<String> = Vec::new();
379 let mut current_line = String::from("> ");
380 let mut current_width = 2; let mut target_line_idx = 0;
383 let mut cursor_found = false;
384
385 let chars_list: Vec<char> = self.input_buffer.chars().collect();
386
387 let mut target_string = String::new();
388
389 if self.cursor_char_idx == 0 {
390 target_line_idx = 0;
391 target_string = String::from("> ");
392 cursor_found = true;
393 }
394
395 for (idx, &c) in chars_list.iter().enumerate() {
396 let char_w = if (c as u32) < 128 { 1 } else { 2 };
397 if c == '\n' {
398 display_lines.push(current_line);
399 current_line = String::from(" "); current_width = 2;
401 } else {
402 if current_width + char_w > max_width {
403 display_lines.push(current_line);
404 current_line = String::from(" "); current_width = 2;
406 }
407 current_line.push(c);
408 current_width += char_w;
409 }
410
411 if idx + 1 == self.cursor_char_idx {
412 target_line_idx = display_lines.len();
413 target_string = current_line.clone();
414 cursor_found = true;
415 }
416 }
417 display_lines.push(current_line.clone());
418
419 if !cursor_found {
420 target_line_idx = display_lines.len() - 1;
421 target_string = current_line.clone();
422 }
423
424 self.launcher_regions.clear();
426
427 let mut display_logs: alloc::vec::Vec<(String, Color, Option<String>, Option<String>)> =
431 alloc::vec::Vec::new();
432 for raw_log in &self.logs {
433 if raw_log.starts_with('\x02') {
435 let inner = raw_log.trim_matches('\x02');
436 if let Some(rest) = inner.strip_prefix("LAUNCHER:") {
437 if let Some((app_name, display)) = rest.split_once(':') {
438 display_logs.push((
439 String::from(display),
440 Color(config.theme.fn_bar_key_fg), None,
442 Some(String::from(app_name)),
443 ));
444 }
445 }
446 continue;
447 }
448
449 let is_input = raw_log.starts_with("> ");
450 let (log, mut timestamp): (&str, Option<String>) = match raw_log.split_once('\x00') {
452 Some((cmd, ts)) => (cmd, Some(String::from(ts))),
453 None => (raw_log.as_str(), None),
454 };
455 let is_error = log.to_lowercase().contains("error");
456
457 let color = if is_input {
458 Color(config.theme.success_fg) } else if is_error {
460 Color(config.theme.error_fg) } else {
462 self.fg_color };
464
465 let mut current_line = String::new();
466 let mut current_width = 0;
467 for c in log.chars() {
468 let char_w = if (c as u32) < 128 { 1 } else { 2 };
469 if c == '\n' {
470 display_logs.push((current_line.clone(), color, timestamp.take(), None));
471 current_line.clear();
472 current_width = 0;
473 } else {
474 if current_width + char_w > max_width {
475 display_logs.push((current_line.clone(), color, timestamp.take(), None));
476 current_line.clear();
477 current_width = 0;
478 }
479 current_line.push(c);
480 current_width += char_w;
481 }
482 }
483 display_logs.push((current_line, color, timestamp.take(), None));
484 }
485
486 let total_lines = display_lines.len();
487 let visible_lines_count = total_lines.min(5);
488
489 let input_area_height = visible_lines_count as u32 * line_height + 15;
490 let log_area_height = wh.saturating_sub(input_area_height);
491 let max_log_lines = log_area_height / line_height;
492
493 let max_possible_start = if display_logs.len() as u32 > max_log_lines {
495 display_logs.len() - max_log_lines as usize
496 } else {
497 0
498 };
499 let start_idx = max_possible_start.saturating_sub(self.scroll_offset);
500 let end_idx = (start_idx + max_log_lines as usize).min(display_logs.len());
501
502 let lines_to_draw = end_idx - start_idx;
504
505 let sep_y = wy + wh as i32 - input_area_height as i32;
507
508 let mut current_y = sep_y - (lines_to_draw as i32 * line_height as i32) - 5;
510
511 static DEFAULT_APP_ICON: &[u8] =
512 include_bytes!("../../../icons/ic_app_default.svg");
513
514 for i in start_idx..end_idx {
515 let (text, color, ts, launcher_app) = &display_logs[i];
516
517 if let Some(app_name) = launcher_app {
518 let icon_size = 16u32;
520 let icon_x = (wx + padding_x as i32) as u32;
521 let icon_y = if current_y >= 0 { current_y as u32 } else { 0 };
522 let text_x = icon_x + icon_size + 6;
523 let row_h = line_height;
524
525 let hl_y = if current_y >= 0 { current_y as u32 } else { 0 };
527 screen.boxfill(
528 (wx + 2) as u32,
529 hl_y,
530 (wx + ww as i32 - 20) as u32,
531 hl_y + row_h,
532 Color(config.theme.ui_overlay_bg),
533 );
534
535 let icon_path = alloc::format!("/apps/{}/icon.svg", app_name);
537 let fs = crate::kernel::fs::get_fs();
538 let icon_bytes_owned: Option<alloc::vec::Vec<u8>> =
539 fs.read_file(&icon_path).map(|(_m, b)| b);
540 let icon_data: &[u8] = match &icon_bytes_owned {
541 Some(b) => b.as_slice(),
542 None => DEFAULT_APP_ICON,
543 };
544 if icon_y + icon_size < (wy + wh as i32) as u32 {
545 screen.draw_svg_icon(icon_x, icon_y, icon_data, icon_size, *color);
546 }
547
548 screen.draw_string_monospace_bg(
550 text_x,
551 icon_y,
552 text,
553 *color,
554 Color(config.theme.ui_overlay_bg),
555 );
556
557 self.launcher_regions.push((
559 wx + 2,
560 current_y,
561 ww - 22,
562 row_h,
563 app_name.clone(),
564 ));
565 } else {
566 screen.draw_string_monospace_bg(
567 (wx + padding_x as i32) as u32,
568 current_y as u32,
569 text,
570 *color,
571 self.bg_color,
572 );
573 if let Some(t) = ts {
575 let tw = screen.measure_string_monospace(t);
576 let tx = (wx + ww as i32 - padding_x as i32 - tw as i32).max(wx + padding_x as i32);
577 screen.draw_string_monospace_bg(
578 tx as u32,
579 current_y as u32,
580 t,
581 Color(config.theme.fn_bar_dim_fg),
582 self.bg_color,
583 );
584 }
585 }
586 current_y += line_height as i32;
587 }
588
589 let clock_str = crate::kernel::timer::format_datetime_jst()
591 .unwrap_or_else(|| alloc::string::String::from("NTP Syncing..."));
592
593 let clock_w = screen.measure_string_monospace(&clock_str);
594 let clock_x =
595 (wx + ww as i32 - padding_x as i32 - clock_w as i32).max(wx + padding_x as i32);
596 let clock_y = sep_y - line_height as i32 + 2;
597 screen.draw_string_monospace_bg(
598 clock_x as u32,
599 clock_y as u32,
600 &clock_str,
601 Color(config.theme.fn_bar_dim_fg),
602 self.bg_color,
603 );
604
605 let total_log_lines = display_logs.len();
607 if total_log_lines > max_log_lines as usize {
608 let bar_w = 6u32;
609 let bar_padding = 2u32;
610 let rail_x = wx + ww as i32 - bar_w as i32 - bar_padding as i32;
611 let rail_y = wy + 5;
612 let rail_h = log_area_height.saturating_sub(10);
613
614 screen.boxfill(
616 rail_x as u32,
617 rail_y as u32,
618 (rail_x + bar_w as i32) as u32,
619 (rail_y + rail_h as i32) as u32,
620 Color(config.theme.tab_area_bg),
621 );
622
623 let thumb_h = ((rail_h * max_log_lines) / total_log_lines as u32).max(15);
625 let max_possible_start = total_log_lines.saturating_sub(max_log_lines as usize);
626 if max_possible_start > 0 {
627 let scroll_y_offset =
628 ((rail_h - thumb_h) * start_idx as u32) / max_possible_start as u32;
629 let thumb_y = rail_y + scroll_y_offset as i32;
630 screen.boxfill(
632 rail_x as u32,
633 thumb_y as u32,
634 (rail_x + bar_w as i32) as u32,
635 (thumb_y + thumb_h as i32) as u32,
636 Color(config.theme.tab_area_border),
637 );
638 }
639 }
640
641 let sep_y = wy + wh as i32 - input_area_height as i32;
643 screen.boxfill(
644 ux0,
645 sep_y as u32,
646 ux1,
647 (sep_y + 1) as u32,
648 Color(config.theme.tab_area_border),
649 );
650
651 let start_line_idx = total_lines.saturating_sub(5);
653 let mut cur_y = sep_y + 10;
654
655 for i in start_line_idx..total_lines {
656 let line = &display_lines[i];
657
658 if i == 0 {
660 screen.draw_string_monospace_bg(
661 (wx + padding_x as i32) as u32,
662 cur_y as u32,
663 "> ",
664 Color(config.theme.success_fg),
665 self.bg_color,
666 );
667 if line.len() > 2 {
668 let text = line.get(2..).unwrap_or("");
669 screen.draw_string_monospace_bg(
670 (wx + padding_x as i32 + 16) as u32,
671 cur_y as u32,
672 text,
673 self.fg_color,
674 self.bg_color,
675 );
676 }
677 } else {
678 screen.draw_string_monospace_bg(
679 (wx + padding_x as i32) as u32,
680 cur_y as u32,
681 line,
682 self.fg_color,
683 self.bg_color,
684 );
685 }
686
687 cur_y += line_height as i32;
688 }
689
690 if target_line_idx >= start_line_idx {
692 let relative_line_idx = target_line_idx - start_line_idx;
693
694 let mut prefix_w = 0;
696 if target_line_idx == 0 {
697 if target_string.len() >= 2 {
698 prefix_w += 16; let text = target_string.get(2..).unwrap_or("");
700 prefix_w += screen.measure_string_monospace(text);
701 } else {
702 prefix_w += screen.measure_string_monospace(&target_string);
703 }
704 } else {
705 prefix_w += screen.measure_string_monospace(&target_string);
706 }
707
708 let cursor_x = wx + padding_x as i32 + prefix_w as i32;
709 let cursor_y = sep_y + 10 + (relative_line_idx as i32) * line_height as i32;
710
711 if cursor_x + 2 <= wx + ww as i32 - padding_x as i32 {
712 screen.boxfill(
714 cursor_x as u32,
715 cursor_y as u32 + 2,
716 (cursor_x + 2) as u32,
717 (cursor_y + 14) as u32,
718 Color(crate::kernel::ime::cursor_color()),
719 );
720 }
721 }
722
723 self.dirty = false;
724 self.input_dirty = false;
725 }
726}