Skip to main content

strat9_kernel/shell/
mod.rs

1//! Chevron shell - Minimal interactive kernel shell
2//!
3//! Provides a simple interactive command-line interface for kernel management.
4//! Prompt: >>>
5
6// TODO UTF8
7//- clavier/layout renvoie des codepoints Unicode (pas seulement u8), puis conversion UTF‑8 pour l’édition.
8//- plus tard seulement, gestion graphemes/combinaisons complexes.
9
10pub 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/// Shell error types
23#[derive(Debug)]
24pub enum ShellError {
25    /// Unknown command
26    UnknownCommand,
27    /// Invalid arguments
28    InvalidArguments,
29    /// Command execution failed
30    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
40/// Global flag set by Ctrl+C. Long-running commands should poll this
41/// via [`is_interrupted`] and abort early when it returns `true`.
42pub static SHELL_INTERRUPTED: AtomicBool = AtomicBool::new(false);
43
44/// Returns `true` if Ctrl+C was pressed, and clears the flag.
45///
46/// Commands that loop (e.g. `top`, `watch`) should call this each
47/// iteration to support cancellation.
48pub fn is_interrupted() -> bool {
49    SHELL_INTERRUPTED.swap(false, Ordering::Relaxed)
50}
51
52/// Execute one shell line without prompt/history handling.
53///
54/// This helper is used by commands such as `watch` to run another
55/// command through the same parser/executor pipeline.
56pub fn run_line(line: &str) {
57    let registry = CommandRegistry::new();
58    execute_line(line, &registry);
59}
60
61/// Returns whether continuation byte.
62#[inline]
63fn is_continuation_byte(b: u8) -> bool {
64    (b & 0b1100_0000) == 0b1000_0000
65}
66
67/// Performs the prev char boundary operation.
68fn 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
79/// Performs the next char boundary operation.
80fn 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
91/// Performs the char count operation.
92fn char_count(input: &[u8]) -> usize {
93    core::str::from_utf8(input)
94        .map(|s| s.chars().count())
95        .unwrap_or(input.len())
96}
97
98/// Performs the print bytes operation.
99fn 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
111/// Performs the move cursor left chars operation.
112fn move_cursor_left_chars(n: usize) {
113    for _ in 0..n {
114        print_char('\x08');
115    }
116}
117
118/// Performs the clear visible line operation.
119fn 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
128/// Redraw the current shell input line after the prompt
129fn redraw_line(input: &[u8], cursor_pos: usize) {
130    print_bytes(input);
131
132    // Print a trailing space to clear any leftover char from a longer previous line
133    print_char(' ');
134    print_char('\x08');
135
136    // Move visual cursor back to its logical position
137    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
154/// Performs the redraw full line operation.
155fn 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
167/// Performs the insert bytes at cursor operation.
168fn 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
193/// Performs the delete prev char at cursor operation.
194fn 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    // Backspace behavior: visual cursor moves left by one character first.
212    move_cursor_left_chars(1);
213    redraw_line(&input_buf[*cursor_pos..*input_len], 0);
214    true
215}
216
217/// Performs the delete next char at cursor operation.
218fn 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    // Delete behavior: cursor stays at the same logical position.
235    redraw_line(&input_buf[*cursor_pos..*input_len], 0);
236    true
237}
238
239/// Main shell loop
240///
241/// This function never returns. It continuously reads keyboard input,
242/// parses commands, and executes them.
243pub 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    // Command history
251    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    // Mouse state
259    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    // Display welcome message using ASCII for robust terminal rendering.
268    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    // Cap per-loop mouse work to avoid starving timer ticks when dragging.
280    const MAX_MOUSE_EVENTS_PER_TURN: usize = 16;
281    const SCROLLBAR_DRAG_MIN_TICKS: u64 = 1;
282
283    loop {
284        // Handle cursor blinking (graphics only)
285        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) // Cyan
294                } else {
295                    crate::arch::x86_64::vga::RgbColor::new(0x12, 0x16, 0x1E) // Background
296                };
297                crate::arch::x86_64::vga::draw_text_cursor(color);
298            }
299        }
300
301        // Read from keyboard buffer
302        if let Some(ch) = crate::arch::x86_64::keyboard::read_char() {
303            // Any keypress returns the view to live output.
304            if crate::arch::x86_64::vga::is_available() {
305                crate::arch::x86_64::vga::scroll_to_live();
306            }
307
308            // Hide cursor before any action
309            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, &registry);
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, &registry);
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                        // stay in escape sequence
470                    } 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            // Reset blink state on input
518            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                        // Prevent monopolizing the CPU under heavy mouse input
545                        // (e.g. rapid drag on scrollbar). Remaining events are
546                        // processed on next loop iteration after yield_task().
547                        break;
548                    }
549                }
550
551                // Under heavy mouse input, give the scheduler a chance to run
552                // other tasks (including timer tick handlers driving UI).
553                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                        // Inverted wheel: wheel up (dz>0) → scroll down (history forward)
565                        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
622/// Execute a command line, handling scripting, pipes and redirections.
623fn 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
691/// Execute a single pipeline (no scripting).
692fn execute_pipeline(line: &str, registry: &CommandRegistry) {
693    // Ensure stale pipe input from a previous command cannot leak.
694    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        // A stage may ignore stdin pipe input; clear any leftovers before next stage.
758        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
770/// Tab completion for command names and VFS paths.
771///
772/// If the cursor is on the first token, completes against registered commands.
773/// Otherwise completes against VFS directory entries.
774fn 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
880/// Replace the word starting at `word_start` (byte offset) with `replacement`.
881fn 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
923/// Find the longest common prefix of a set of strings.
924fn 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
942/// Write captured output to a file (truncate or append).
943fn 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}