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