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, print_text};
17use parser::{parse_pipeline, Redirect};
18
19use crate::{shell_print, shell_println, sync::FixedQueue, 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::string::{String, ToString};
35use core::sync::atomic::{AtomicBool, Ordering};
36
37const SHELL_HISTORY_CAPACITY: usize = 50;
38
39/// Global flag set by Ctrl+C. Long-running commands should poll this
40/// via [`is_interrupted`] and abort early when it returns `true`.
41pub static SHELL_INTERRUPTED: AtomicBool = AtomicBool::new(false);
42
43/// Returns `true` if Ctrl+C was pressed, and clears the flag.
44///
45/// Commands that loop (e.g. `top`, `watch`) should call this each
46/// iteration to support cancellation.
47pub fn is_interrupted() -> bool {
48    SHELL_INTERRUPTED.swap(false, Ordering::Relaxed)
49}
50
51/// Execute one shell line without prompt/history handling.
52///
53/// This helper is used by commands such as `watch` to run another
54/// command through the same parser/executor pipeline.
55pub fn run_line(line: &str) {
56    let registry = CommandRegistry::new();
57    execute_line(line, &registry);
58}
59
60/// Returns whether continuation byte.
61#[inline]
62fn is_continuation_byte(b: u8) -> bool {
63    (b & 0b1100_0000) == 0b1000_0000
64}
65
66/// Performs the prev char boundary operation.
67fn 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
78/// Performs the next char boundary operation.
79fn 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
90/// Performs the char count operation.
91fn char_count(input: &[u8]) -> usize {
92    core::str::from_utf8(input)
93        .map(|s| s.chars().count())
94        .unwrap_or(input.len())
95}
96
97/// Performs the print bytes operation.
98fn 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
110/// Performs the move cursor left chars operation.
111fn 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
122/// Performs the clear visible line operation.
123fn 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
141/// Redraw the current shell input line after the prompt
142fn 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
174/// Performs the redraw full line operation.
175fn 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
214/// Performs the insert bytes at cursor operation.
215fn 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
240/// Performs the delete prev char at cursor operation.
241fn 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    // Backspace behavior: visual cursor moves left by one character first.
259    move_cursor_left_chars(1);
260    redraw_line(&input_buf[*cursor_pos..*input_len], 0);
261    true
262}
263
264/// Performs the delete next char at cursor operation.
265fn 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    // Delete behavior: cursor stays at the same logical position.
282    redraw_line(&input_buf[*cursor_pos..*input_len], 0);
283    true
284}
285
286/// Main shell loop
287///
288/// This function never returns. It continuously reads keyboard input,
289/// parses commands, and executes them.
290pub extern "C" fn shell_main() -> ! {
291    // E9 'Q' (0x51) = first instruction, before any lock/serial. Confirms we reached shell.
292    let q: u8 = 0x51;
293    unsafe { core::arch::asm!("out 0xe9, al", in("al") q) }
294    // E9 'S' = shell entered (no serial_force_println here - avoids FORCE_LOCK contention with timer)
295    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    // E9 'L' = init complete, entering main loop
302    unsafe { core::arch::asm!("mov al, 0x4C; out 0xe9, al") }
303
304    // Command history
305    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    // Mouse state
313    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    // Display welcome message using ASCII for robust terminal rendering.
325    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    // Cap per-loop mouse work to avoid starving timer ticks when dragging.
338    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        // Handle cursor blinking (graphics only)
345        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) // Cyan
354                } else {
355                    crate::arch::x86_64::vga::RgbColor::new(0x12, 0x16, 0x1E) // Background
356                };
357                crate::arch::x86_64::vga::draw_text_cursor(color);
358            }
359        }
360
361        // Poll USB HID and drain events into unified keyboard/mouse buffers.
362        if crate::hardware::usb::hid::is_available() {
363            crate::hardware::usb::hid::poll_all();
364        }
365
366        // Read from keyboard buffer
367        if let Some(ch) = crate::arch::x86_64::keyboard::read_char() {
368            // Any keypress returns the view to live output.
369            if crate::arch::x86_64::vga::is_available() {
370                crate::arch::x86_64::vga::scroll_to_live();
371            }
372
373            // Hide cursor before any action
374            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, &registry);
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, &registry);
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                        // stay in escape sequence
541                    } 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            // Reset blink state on input
589            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                        // Prevent monopolizing the CPU under heavy mouse input
616                        // (e.g. rapid drag on scrollbar). Remaining events are
617                        // processed on next loop iteration after yield_task().
618                        break;
619                    }
620                }
621
622                // Under heavy mouse input, give the scheduler a chance to run
623                // other tasks (including timer tick handlers driving UI).
624                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                            // Inverted wheel: wheel up (dz>0) -> scroll down (history forward)
693                            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
740/// Execute a command line, handling scripting, pipes and redirections.
741fn 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
809/// Execute a single pipeline (no scripting).
810fn execute_pipeline(line: &str, registry: &CommandRegistry) {
811    // Ensure stale pipe input from a previous command cannot leak.
812    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        // A stage may ignore stdin pipe input; clear any leftovers before next stage.
876        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
888/// Tab completion for command names and VFS paths.
889///
890/// If the cursor is on the first token, completes against registered commands.
891/// Otherwise completes against VFS directory entries.
892fn 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
998/// Replace the word starting at `word_start` (byte offset) with `replacement`.
999fn 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
1041/// Find the longest common prefix of a set of strings.
1042fn 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
1060/// Write captured output to a file (truncate or append).
1061fn 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}