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