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