1pub mod commands;
11pub mod output;
12pub mod parser;
13pub mod scripting;
14
15use commands::CommandRegistry;
16use output::{print_char, print_prompt};
17use parser::{parse_pipeline, Redirect};
18
19use crate::{shell_print, shell_println, vfs};
20use strat9_abi::flag::OpenFlags;
21
22#[derive(Debug)]
24pub enum ShellError {
25 UnknownCommand,
27 InvalidArguments,
29 ExecutionFailed,
31}
32
33use crate::arch::x86_64::keyboard::{KEY_DOWN, KEY_END, KEY_HOME, KEY_LEFT, KEY_RIGHT, KEY_UP};
34use alloc::{
35 collections::VecDeque,
36 string::{String, ToString},
37};
38use core::sync::atomic::{AtomicBool, Ordering};
39
40pub static SHELL_INTERRUPTED: AtomicBool = AtomicBool::new(false);
43
44pub fn is_interrupted() -> bool {
49 SHELL_INTERRUPTED.swap(false, Ordering::Relaxed)
50}
51
52pub fn run_line(line: &str) {
57 let registry = CommandRegistry::new();
58 execute_line(line, ®istry);
59}
60
61#[inline]
63fn is_continuation_byte(b: u8) -> bool {
64 (b & 0b1100_0000) == 0b1000_0000
65}
66
67fn prev_char_boundary(input: &[u8], mut idx: usize) -> usize {
69 if idx == 0 {
70 return 0;
71 }
72 idx -= 1;
73 while idx > 0 && is_continuation_byte(input[idx]) {
74 idx -= 1;
75 }
76 idx
77}
78
79fn next_char_boundary(input: &[u8], mut idx: usize) -> usize {
81 if idx >= input.len() {
82 return input.len();
83 }
84 idx += 1;
85 while idx < input.len() && is_continuation_byte(input[idx]) {
86 idx += 1;
87 }
88 idx
89}
90
91fn char_count(input: &[u8]) -> usize {
93 core::str::from_utf8(input)
94 .map(|s| s.chars().count())
95 .unwrap_or(input.len())
96}
97
98fn print_bytes(input: &[u8]) {
100 if let Ok(s) = core::str::from_utf8(input) {
101 for ch in s.chars() {
102 print_char(ch);
103 }
104 } else {
105 for &b in input {
106 print_char(if b.is_ascii() { b as char } else { '?' });
107 }
108 }
109}
110
111fn move_cursor_left_chars(n: usize) {
113 for _ in 0..n {
114 print_char('\x08');
115 }
116}
117
118fn clear_visible_line(line: &[u8]) {
120 let n = char_count(line);
121 move_cursor_left_chars(n);
122 for _ in 0..n {
123 print_char(' ');
124 }
125 move_cursor_left_chars(n);
126}
127
128fn redraw_line(input: &[u8], cursor_pos: usize) {
130 print_bytes(input);
131
132 print_char(' ');
134 print_char('\x08');
135
136 let back_moves = if cursor_pos <= input.len() {
138 if let (Ok(full), Ok(prefix)) = (
139 core::str::from_utf8(input),
140 core::str::from_utf8(&input[..cursor_pos]),
141 ) {
142 full.chars().count().saturating_sub(prefix.chars().count())
143 } else {
144 input.len().saturating_sub(cursor_pos)
145 }
146 } else {
147 0
148 };
149 for _ in 0..back_moves {
150 print_char('\x08');
151 }
152}
153
154fn redraw_full_line(input: &[u8], cursor_pos: usize) {
156 clear_visible_line(input);
157 print_bytes(input);
158 if cursor_pos <= input.len() {
159 if let Ok(sfx) = core::str::from_utf8(&input[cursor_pos..]) {
160 move_cursor_left_chars(sfx.chars().count());
161 } else {
162 move_cursor_left_chars(input.len().saturating_sub(cursor_pos));
163 }
164 }
165}
166
167fn insert_bytes_at_cursor(
169 input_buf: &mut [u8],
170 input_len: &mut usize,
171 cursor_pos: &mut usize,
172 bytes: &[u8],
173) -> bool {
174 if bytes.is_empty() {
175 return true;
176 }
177 if *input_len + bytes.len() > input_buf.len() {
178 return false;
179 }
180 let old_cursor = *cursor_pos;
181 if old_cursor < *input_len {
182 for i in (old_cursor..*input_len).rev() {
183 input_buf[i + bytes.len()] = input_buf[i];
184 }
185 }
186 input_buf[old_cursor..old_cursor + bytes.len()].copy_from_slice(bytes);
187 *input_len += bytes.len();
188 *cursor_pos += bytes.len();
189 redraw_line(&input_buf[old_cursor..*input_len], bytes.len());
190 true
191}
192
193fn delete_prev_char_at_cursor(
195 input_buf: &mut [u8],
196 input_len: &mut usize,
197 cursor_pos: &mut usize,
198) -> bool {
199 if *cursor_pos == 0 {
200 return false;
201 }
202
203 let prev = prev_char_boundary(&input_buf[..*input_len], *cursor_pos);
204 let removed = *cursor_pos - prev;
205 for i in *cursor_pos..*input_len {
206 input_buf[i - removed] = input_buf[i];
207 }
208 *input_len -= removed;
209 *cursor_pos = prev;
210
211 move_cursor_left_chars(1);
213 redraw_line(&input_buf[*cursor_pos..*input_len], 0);
214 true
215}
216
217fn delete_next_char_at_cursor(
219 input_buf: &mut [u8],
220 input_len: &mut usize,
221 cursor_pos: &mut usize,
222) -> bool {
223 if *cursor_pos >= *input_len {
224 return false;
225 }
226
227 let next = next_char_boundary(&input_buf[..*input_len], *cursor_pos);
228 let removed = next - *cursor_pos;
229 for i in next..*input_len {
230 input_buf[i - removed] = input_buf[i];
231 }
232 *input_len -= removed;
233
234 redraw_line(&input_buf[*cursor_pos..*input_len], 0);
236 true
237}
238
239pub extern "C" fn shell_main() -> ! {
244 let registry = CommandRegistry::new();
245 commands::util::init_shell_env();
246 let mut input_buf = [0u8; 256];
247 let mut input_len = 0;
248 let mut cursor_pos = 0;
249
250 let mut history = VecDeque::new();
252 let mut history_idx: isize = -1;
253 let mut current_input_saved = String::new();
254 let mut utf8_pending = [0u8; 4];
255 let mut utf8_pending_len = 0usize;
256 let mut in_escape_seq = false;
257
258 let mut prev_left = false;
260 let mut selecting = false;
261 let mut scrollbar_dragging = false;
262 let mut last_scrollbar_drag_tick = 0u64;
263 let mut pending_scrollbar_drag_y: Option<usize> = None;
264 let mut mouse_x: i32 = 0;
265 let mut mouse_y: i32 = 0;
266
267 shell_println!("");
269 shell_println!("+--------------------------------------------------------------+");
270 shell_println!("| Strat9-OS chevron shell v0.1.0 (Bedrock) |");
271 shell_println!("| Type 'help' for available commands |");
272 shell_println!("+--------------------------------------------------------------+");
273 shell_println!("");
274
275 print_prompt();
276
277 let mut last_blink_tick = 0;
278 let mut cursor_visible = false;
279 const MAX_MOUSE_EVENTS_PER_TURN: usize = 16;
281 const SCROLLBAR_DRAG_MIN_TICKS: u64 = 1;
282
283 loop {
284 let ticks = crate::process::scheduler::ticks();
286
287 if ticks / 50 != last_blink_tick {
288 last_blink_tick = ticks / 50;
289 cursor_visible = !cursor_visible;
290
291 if crate::arch::x86_64::vga::is_available() {
292 let color = if cursor_visible {
293 crate::arch::x86_64::vga::RgbColor::new(0x4F, 0xB3, 0xB3) } else {
295 crate::arch::x86_64::vga::RgbColor::new(0x12, 0x16, 0x1E) };
297 crate::arch::x86_64::vga::draw_text_cursor(color);
298 }
299 }
300
301 if let Some(ch) = crate::arch::x86_64::keyboard::read_char() {
303 if crate::arch::x86_64::vga::is_available() {
305 crate::arch::x86_64::vga::scroll_to_live();
306 }
307
308 if crate::arch::x86_64::vga::is_available() {
310 crate::arch::x86_64::vga::draw_text_cursor(
311 crate::arch::x86_64::vga::RgbColor::new(0x12, 0x16, 0x1E),
312 );
313 }
314
315 match ch {
316 b'\r' | b'\n' => {
317 in_escape_seq = false;
318 utf8_pending_len = 0;
319 shell_println!();
320
321 if input_len > 0 {
322 let line = core::str::from_utf8(&input_buf[..input_len]).unwrap_or("");
323
324 if !line.is_empty() {
325 if history.is_empty()
326 || history.back().map(|s: &String| s.as_str()) != Some(line)
327 {
328 history.push_back(line.to_string());
329 if history.len() > 50 {
330 history.pop_front();
331 }
332 }
333 }
334
335 execute_line(line, ®istry);
336 input_len = 0;
337 cursor_pos = 0;
338 history_idx = -1;
339 }
340
341 print_prompt();
342 }
343 b'\x08' | b'\x7f' => {
344 in_escape_seq = false;
345 utf8_pending_len = 0;
346 let _ =
347 delete_prev_char_at_cursor(&mut input_buf, &mut input_len, &mut cursor_pos);
348 }
349 b'\x03' => {
350 in_escape_seq = false;
351 utf8_pending_len = 0;
352 shell_println!("^C");
353 input_len = 0;
354 cursor_pos = 0;
355 history_idx = -1;
356 SHELL_INTERRUPTED.store(false, Ordering::Relaxed);
357 print_prompt();
358 }
359 b'\t' => {
360 in_escape_seq = false;
361 utf8_pending_len = 0;
362 tab_complete(&mut input_buf, &mut input_len, &mut cursor_pos, ®istry);
363 }
364 b'\x04' => {
365 in_escape_seq = false;
366 utf8_pending_len = 0;
367 let _ =
368 delete_next_char_at_cursor(&mut input_buf, &mut input_len, &mut cursor_pos);
369 }
370 KEY_LEFT => {
371 in_escape_seq = false;
372 utf8_pending_len = 0;
373 if cursor_pos > 0 {
374 cursor_pos = prev_char_boundary(&input_buf[..input_len], cursor_pos);
375 print_char('\x08');
376 }
377 }
378 KEY_RIGHT => {
379 in_escape_seq = false;
380 utf8_pending_len = 0;
381 if cursor_pos < input_len {
382 let next = next_char_boundary(&input_buf[..input_len], cursor_pos);
383 print_bytes(&input_buf[cursor_pos..next]);
384 cursor_pos = next;
385 }
386 }
387 KEY_HOME => {
388 in_escape_seq = false;
389 utf8_pending_len = 0;
390 while cursor_pos > 0 {
391 cursor_pos = prev_char_boundary(&input_buf[..input_len], cursor_pos);
392 print_char('\x08');
393 }
394 }
395 KEY_END => {
396 in_escape_seq = false;
397 utf8_pending_len = 0;
398 while cursor_pos < input_len {
399 let next = next_char_boundary(&input_buf[..input_len], cursor_pos);
400 print_bytes(&input_buf[cursor_pos..next]);
401 cursor_pos = next;
402 }
403 }
404 KEY_UP => {
405 in_escape_seq = false;
406 utf8_pending_len = 0;
407 if !history.is_empty() && history_idx < (history.len() as isize - 1) {
408 if history_idx == -1 {
409 current_input_saved = core::str::from_utf8(&input_buf[..input_len])
410 .unwrap_or("")
411 .to_string();
412 }
413
414 while cursor_pos < input_len {
415 let next = next_char_boundary(&input_buf[..input_len], cursor_pos);
416 print_bytes(&input_buf[cursor_pos..next]);
417 cursor_pos = next;
418 }
419 clear_visible_line(&input_buf[..input_len]);
420
421 history_idx += 1;
422 let hist_str = &history[history.len() - 1 - history_idx as usize];
423 let bytes = hist_str.as_bytes();
424 let copy_len = bytes.len().min(input_buf.len());
425 input_buf[..copy_len].copy_from_slice(&bytes[..copy_len]);
426 input_len = copy_len;
427 cursor_pos = input_len;
428
429 redraw_full_line(&input_buf[..input_len], cursor_pos);
430 }
431 }
432 KEY_DOWN => {
433 in_escape_seq = false;
434 utf8_pending_len = 0;
435 if history_idx >= 0 {
436 while cursor_pos < input_len {
437 let next = next_char_boundary(&input_buf[..input_len], cursor_pos);
438 print_bytes(&input_buf[cursor_pos..next]);
439 cursor_pos = next;
440 }
441 clear_visible_line(&input_buf[..input_len]);
442
443 history_idx -= 1;
444 if history_idx == -1 {
445 let bytes = current_input_saved.as_bytes();
446 let copy_len = bytes.len().min(input_buf.len());
447 input_buf[..copy_len].copy_from_slice(&bytes[..copy_len]);
448 input_len = copy_len;
449 } else {
450 let hist_str = &history[history.len() - 1 - history_idx as usize];
451 let bytes = hist_str.as_bytes();
452 let copy_len = bytes.len().min(input_buf.len());
453 input_buf[..copy_len].copy_from_slice(&bytes[..copy_len]);
454 input_len = copy_len;
455 }
456 cursor_pos = input_len;
457
458 redraw_full_line(&input_buf[..input_len], cursor_pos);
459 }
460 }
461 b'\x1b' => {
462 utf8_pending_len = 0;
463 in_escape_seq = true;
464 }
465 _ if in_escape_seq => {
466 if (0x40..=0x7E).contains(&ch) {
467 in_escape_seq = false;
468 } else if ch == b'[' || ch == b';' || ch == b'?' || ch.is_ascii_digit() {
469 } else {
471 in_escape_seq = false;
472 }
473 }
474 _ if ch >= 0x20 => {
475 in_escape_seq = false;
476 if ch < 0x80 {
477 utf8_pending_len = 0;
478 if insert_bytes_at_cursor(
479 &mut input_buf,
480 &mut input_len,
481 &mut cursor_pos,
482 core::slice::from_ref(&ch),
483 ) {
484 history_idx = -1;
485 }
486 } else {
487 if utf8_pending_len >= utf8_pending.len() {
488 utf8_pending_len = 0;
489 }
490 utf8_pending[utf8_pending_len] = ch;
491 utf8_pending_len += 1;
492 match core::str::from_utf8(&utf8_pending[..utf8_pending_len]) {
493 Ok(s) => {
494 if insert_bytes_at_cursor(
495 &mut input_buf,
496 &mut input_len,
497 &mut cursor_pos,
498 s.as_bytes(),
499 ) {
500 history_idx = -1;
501 }
502 utf8_pending_len = 0;
503 }
504 Err(err) => {
505 if err.error_len().is_some() {
506 utf8_pending_len = 0;
507 }
508 }
509 }
510 }
511 }
512 _ => {
513 in_escape_seq = false;
514 utf8_pending_len = 0;
515 }
516 }
517 last_blink_tick = ticks / 50;
519 cursor_visible = true;
520 } else {
521 if crate::arch::x86_64::mouse::MOUSE_READY.load(core::sync::atomic::Ordering::Relaxed) {
522 let mut scroll_delta: i32 = 0;
523 let mut left_pressed = false;
524 let mut left_released = false;
525 let mut left_held = false;
526 let mut had_events = false;
527
528 let mut mouse_events_seen = 0usize;
529 while let Some(ev) = crate::arch::x86_64::mouse::read_event() {
530 had_events = true;
531 scroll_delta += ev.dz as i32;
532 if ev.left && !prev_left {
533 left_pressed = true;
534 }
535 if !ev.left && prev_left {
536 left_released = true;
537 }
538 if ev.left && prev_left {
539 left_held = true;
540 }
541 prev_left = ev.left;
542 mouse_events_seen += 1;
543 if mouse_events_seen >= MAX_MOUSE_EVENTS_PER_TURN {
544 break;
548 }
549 }
550
551 if mouse_events_seen >= MAX_MOUSE_EVENTS_PER_TURN {
554 crate::process::scheduler::yield_task();
555 }
556
557 if had_events || left_held {
558 let (new_mx, new_my) = crate::arch::x86_64::mouse::mouse_pos();
559 let moved = new_mx != mouse_x || new_my != mouse_y;
560 mouse_x = new_mx;
561 mouse_y = new_my;
562
563 if crate::arch::x86_64::vga::is_available() {
564 if scroll_delta > 0 {
566 crate::arch::x86_64::vga::scroll_view_down((scroll_delta as usize) * 3);
567 } else if scroll_delta < 0 {
568 crate::arch::x86_64::vga::scroll_view_up((-scroll_delta as usize) * 3);
569 }
570
571 if left_pressed {
572 let (mx, my) = (new_mx as usize, new_my as usize);
573 if crate::arch::x86_64::vga::scrollbar_hit_test(mx, my) {
574 crate::arch::x86_64::vga::scrollbar_click(mx, my);
575 crate::arch::x86_64::vga::clear_selection();
576 selecting = false;
577 scrollbar_dragging = true;
578 } else {
579 crate::arch::x86_64::vga::start_selection(mx, my);
580 selecting = true;
581 scrollbar_dragging = false;
582 }
583 } else if left_held && scrollbar_dragging && moved {
584 pending_scrollbar_drag_y = Some(new_my as usize);
585 if ticks.saturating_sub(last_scrollbar_drag_tick)
586 >= SCROLLBAR_DRAG_MIN_TICKS
587 {
588 if let Some(py) = pending_scrollbar_drag_y.take() {
589 crate::arch::x86_64::vga::scrollbar_drag_to(py);
590 last_scrollbar_drag_tick = ticks;
591 }
592 }
593 } else if left_held && selecting && moved {
594 crate::arch::x86_64::vga::update_selection(
595 new_mx as usize,
596 new_my as usize,
597 );
598 } else if left_released {
599 if selecting {
600 crate::arch::x86_64::vga::end_selection();
601 selecting = false;
602 }
603 if scrollbar_dragging {
604 if let Some(py) = pending_scrollbar_drag_y.take() {
605 crate::arch::x86_64::vga::scrollbar_drag_to(py);
606 }
607 }
608 scrollbar_dragging = false;
609 }
610
611 if moved {
612 crate::arch::x86_64::vga::update_mouse_cursor(new_mx, new_my);
613 }
614 }
615 }
616 }
617 crate::process::yield_task();
618 }
619 }
620}
621
622fn execute_line(line: &str, registry: &CommandRegistry) {
624 let expanded = scripting::expand_vars(line);
625
626 match scripting::parse_script(&expanded) {
627 scripting::ScriptConstruct::SetVar { key, val } => {
628 let expanded_val = scripting::expand_vars(&val);
629 scripting::set_var(&key, &expanded_val);
630 scripting::set_last_exit(0);
631 return;
632 }
633 scripting::ScriptConstruct::UnsetVar(key) => {
634 scripting::unset_var(&key);
635 scripting::set_last_exit(0);
636 return;
637 }
638 scripting::ScriptConstruct::ForLoop { var, items, body } => {
639 for item in &items {
640 scripting::set_var(&var, item);
641 for cmd in &body {
642 let exp = scripting::expand_vars(cmd);
643 execute_pipeline(&exp, registry);
644 }
645 }
646 return;
647 }
648 scripting::ScriptConstruct::WhileLoop { cond, body } => {
649 let mut iters = 0u32;
650 loop {
651 if iters > 10000 || is_interrupted() {
652 break;
653 }
654 let cond_expanded = scripting::expand_vars(&cond);
655 execute_pipeline(&cond_expanded, registry);
656 if scripting::last_exit() != 0 {
657 break;
658 }
659 for cmd in &body {
660 let exp = scripting::expand_vars(cmd);
661 execute_pipeline(&exp, registry);
662 }
663 iters += 1;
664 }
665 return;
666 }
667 scripting::ScriptConstruct::IfElse {
668 cond,
669 then_body,
670 else_body,
671 } => {
672 let cond_expanded = scripting::expand_vars(&cond);
673 execute_pipeline(&cond_expanded, registry);
674 let branch = if scripting::last_exit() == 0 {
675 &then_body
676 } else {
677 &else_body
678 };
679 for cmd in branch {
680 let exp = scripting::expand_vars(cmd);
681 execute_pipeline(&exp, registry);
682 }
683 return;
684 }
685 scripting::ScriptConstruct::Simple(s) => {
686 execute_pipeline(&s, registry);
687 }
688 }
689}
690
691fn execute_pipeline(line: &str, registry: &CommandRegistry) {
693 output::clear_pipe_input();
695
696 let pipeline = match parse_pipeline(line) {
697 Some(p) => p,
698 None => return,
699 };
700
701 let stage_count = pipeline.stages.len();
702 let mut pipe_data: Option<alloc::vec::Vec<u8>> = None;
703
704 for (i, stage) in pipeline.stages.iter().enumerate() {
705 let is_last = i == stage_count - 1;
706 let needs_capture = !is_last || stage.stdout_redirect.is_some();
707
708 if let Some(ref stdin_path) = stage.stdin_redirect {
709 match vfs::open(stdin_path, vfs::OpenFlags::READ) {
710 Ok(fd) => {
711 let data = vfs::read_all(fd).unwrap_or_default();
712 let _ = vfs::close(fd);
713 output::set_pipe_input(data);
714 }
715 Err(e) => {
716 shell_println!("shell: cannot open '{}': {:?}", stdin_path, e);
717 return;
718 }
719 }
720 } else if let Some(data) = pipe_data.take() {
721 output::set_pipe_input(data);
722 }
723
724 if needs_capture {
725 output::start_capture();
726 }
727
728 let result = registry.execute(&stage.command);
729
730 let captured = if needs_capture {
731 output::take_capture()
732 } else {
733 alloc::vec::Vec::new()
734 };
735
736 match result {
737 Ok(()) => {
738 scripting::set_last_exit(0);
739 }
740 Err(ShellError::UnknownCommand) => {
741 scripting::set_last_exit(127);
742 shell_println!("Error: unknown command '{}'", stage.command.name);
743 return;
744 }
745 Err(ShellError::InvalidArguments) => {
746 scripting::set_last_exit(2);
747 shell_println!("Error: invalid arguments for '{}'", stage.command.name);
748 return;
749 }
750 Err(ShellError::ExecutionFailed) => {
751 scripting::set_last_exit(1);
752 shell_println!("Error: '{}' execution failed", stage.command.name);
753 return;
754 }
755 }
756
757 output::clear_pipe_input();
759
760 if let Some(ref redirect) = stage.stdout_redirect {
761 apply_redirect(redirect, &captured);
762 }
763
764 if !is_last {
765 pipe_data = Some(captured);
766 }
767 }
768}
769
770fn tab_complete(
775 input_buf: &mut [u8],
776 input_len: &mut usize,
777 cursor_pos: &mut usize,
778 registry: &CommandRegistry,
779) {
780 let text = match core::str::from_utf8(&input_buf[..*input_len]) {
781 Ok(s) => s,
782 Err(_) => return,
783 };
784
785 let before_cursor = &text[..*cursor_pos];
786 let has_space = before_cursor.contains(' ');
787
788 if !has_space {
789 let prefix = before_cursor;
790 let names = registry.command_names();
791 let matches: alloc::vec::Vec<&str> = names
792 .iter()
793 .copied()
794 .filter(|n| n.starts_with(prefix))
795 .collect();
796
797 if matches.len() == 1 {
798 complete_replace_word(input_buf, input_len, cursor_pos, 0, matches[0], true);
799 } else if matches.len() > 1 {
800 let common = longest_common_prefix(&matches);
801 if common.len() > prefix.len() {
802 complete_replace_word(input_buf, input_len, cursor_pos, 0, &common, false);
803 } else {
804 shell_println!();
805 for m in &matches {
806 shell_print!("{} ", m);
807 }
808 shell_println!();
809 output::print_prompt();
810 print_bytes(&input_buf[..*input_len]);
811 let back = char_count(&input_buf[*cursor_pos..*input_len]);
812 move_cursor_left_chars(back);
813 }
814 }
815 } else {
816 let last_space = before_cursor.rfind(' ').unwrap_or(0);
817 let partial = &before_cursor[last_space + 1..];
818 let (dir, file_prefix) = if let Some(slash_pos) = partial.rfind('/') {
819 (&partial[..=slash_pos], &partial[slash_pos + 1..])
820 } else {
821 ("/", partial)
822 };
823
824 if let Ok(fd) = vfs::open(dir, OpenFlags::READ | OpenFlags::DIRECTORY) {
825 let entries = vfs::getdents(fd).unwrap_or_default();
826 let _ = vfs::close(fd);
827
828 let matches: alloc::vec::Vec<alloc::string::String> = entries
829 .iter()
830 .filter(|e| e.name != "." && e.name != ".." && e.name.starts_with(file_prefix))
831 .map(|e| {
832 let mut s = alloc::string::String::from(dir);
833 s.push_str(&e.name);
834 if e.file_type == strat9_abi::data::DT_DIR {
835 s.push('/');
836 }
837 s
838 })
839 .collect();
840
841 if matches.len() == 1 {
842 let add_space = !matches[0].ends_with('/');
843 complete_replace_word(
844 input_buf,
845 input_len,
846 cursor_pos,
847 last_space + 1,
848 &matches[0],
849 add_space,
850 );
851 } else if matches.len() > 1 {
852 let refs: alloc::vec::Vec<&str> = matches.iter().map(|s| s.as_str()).collect();
853 let common = longest_common_prefix(&refs);
854 if common.len() > partial.len() {
855 complete_replace_word(
856 input_buf,
857 input_len,
858 cursor_pos,
859 last_space + 1,
860 &common,
861 false,
862 );
863 } else {
864 shell_println!();
865 for m in &matches {
866 let name = m.rsplit('/').next().unwrap_or(m);
867 shell_print!("{} ", name);
868 }
869 shell_println!();
870 output::print_prompt();
871 print_bytes(&input_buf[..*input_len]);
872 let back = char_count(&input_buf[*cursor_pos..*input_len]);
873 move_cursor_left_chars(back);
874 }
875 }
876 }
877 }
878}
879
880fn complete_replace_word(
882 buf: &mut [u8],
883 len: &mut usize,
884 cursor: &mut usize,
885 word_start: usize,
886 replacement: &str,
887 add_trailing_space: bool,
888) {
889 let mut new_line = alloc::string::String::new();
890 if let Ok(prefix) = core::str::from_utf8(&buf[..word_start]) {
891 new_line.push_str(prefix);
892 }
893 new_line.push_str(replacement);
894 if add_trailing_space {
895 new_line.push(' ');
896 }
897 let new_cursor = new_line.len();
898 if let Ok(suffix) = core::str::from_utf8(&buf[*cursor..*len]) {
899 new_line.push_str(suffix);
900 }
901
902 let bytes = new_line.as_bytes();
903 if bytes.len() > buf.len() {
904 return;
905 }
906
907 let old_visible = char_count(&buf[..*len]);
908 move_cursor_left_chars(char_count(&buf[..*cursor]));
909
910 buf[..bytes.len()].copy_from_slice(bytes);
911 *len = bytes.len();
912 *cursor = new_cursor;
913
914 for _ in 0..old_visible {
915 print_char(' ');
916 }
917 move_cursor_left_chars(old_visible);
918 print_bytes(&buf[..*len]);
919 let back = char_count(&buf[*cursor..*len]);
920 move_cursor_left_chars(back);
921}
922
923fn longest_common_prefix(strings: &[&str]) -> alloc::string::String {
925 if strings.is_empty() {
926 return alloc::string::String::new();
927 }
928 let first = strings[0];
929 let mut end = first.len();
930 for s in &strings[1..] {
931 end = end.min(s.len());
932 for (i, (a, b)) in first.bytes().zip(s.bytes()).enumerate() {
933 if a != b {
934 end = end.min(i);
935 break;
936 }
937 }
938 }
939 alloc::string::String::from(&first[..end])
940}
941
942fn apply_redirect(redirect: &Redirect, data: &[u8]) {
944 match redirect {
945 Redirect::Truncate(path) => {
946 let flags = OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE;
947 match vfs::open(path, flags) {
948 Ok(fd) => {
949 let _ = vfs::write(fd, data);
950 let _ = vfs::close(fd);
951 }
952 Err(e) => shell_println!("shell: cannot write '{}': {:?}", path, e),
953 }
954 }
955 Redirect::Append(path) => {
956 let flags = OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::APPEND;
957 match vfs::open(path, flags) {
958 Ok(fd) => {
959 let _ = vfs::write(fd, data);
960 let _ = vfs::close(fd);
961 }
962 Err(e) => shell_println!("shell: cannot append '{}': {:?}", path, e),
963 }
964 }
965 }
966}