ove/
shell.rs

1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! Interactive shell subsystem for oveRTOS.
8//!
9//! Provides [`init`], [`process_char`], and [`register_cmd`] for building a
10//! simple command-line interface over a serial console. Commands are registered
11//! as safe Rust `fn(args: &[&[u8]])` callbacks; the FFI trampoline handles the
12//! C/Rust boundary automatically.
13
14use crate::bindings;
15use crate::error::{Error, Result};
16
17/// Shell command handler signature.
18///
19/// `args` contains the parsed arguments as byte slices (argv\[0\] is the command name).
20pub type CmdFn = fn(args: &[&[u8]]);
21
22const MAX_CMDS: usize = 16;
23const MAX_ARGS: usize = 8;
24
25struct CmdEntry {
26    name: &'static [u8],
27    handler: CmdFn,
28}
29
30static mut CMD_TABLE: [Option<CmdEntry>; MAX_CMDS] = {
31    const NONE: Option<CmdEntry> = None;
32    [NONE; MAX_CMDS]
33};
34static mut CMD_COUNT: usize = 0;
35
36/// Initialize the shell subsystem.
37pub fn init() -> Result<()> {
38    let rc = unsafe { bindings::ove_shell_init() };
39    Error::from_code(rc)
40}
41
42/// Process a single input character through the shell.
43pub fn process_char(c: i32) {
44    unsafe { bindings::ove_shell_process_char(c) }
45}
46
47/// Register a shell command with a safe Rust handler.
48///
49/// `name` and `help` must be `\0`-terminated `&'static` byte slices.
50/// A single trampoline dispatches to the correct handler by matching `argv[0]`.
51pub fn register_cmd(name: &'static [u8], help: &'static [u8], handler: CmdFn) -> Result<()> {
52    let idx = unsafe { CMD_COUNT };
53    if idx >= MAX_CMDS {
54        return Err(Error::NoMemory);
55    }
56
57    unsafe {
58        CMD_TABLE[idx] = Some(CmdEntry { name, handler });
59        CMD_COUNT = idx + 1;
60    }
61
62    let cmd = bindings::ove_shell_cmd {
63        name: name.as_ptr() as *const _,
64        help: help.as_ptr() as *const _,
65        handler: Some(trampoline),
66    };
67    let rc = unsafe { bindings::ove_shell_register_cmd(&cmd) };
68    Error::from_code(rc)
69}
70
71unsafe extern "C" fn trampoline(argc: core::ffi::c_int, argv: *mut *const core::ffi::c_char) {
72    if argc <= 0 || argv.is_null() {
73        return;
74    }
75
76    // Build safe arg slices on the stack
77    let argc = (argc as usize).min(MAX_ARGS);
78    let mut args: [&[u8]; MAX_ARGS] = [&[]; MAX_ARGS];
79    for i in 0..argc {
80        let ptr = unsafe { *argv.add(i) } as *const u8;
81        if !ptr.is_null() {
82            args[i] = unsafe { cstr_ptr_to_slice(ptr) };
83        }
84    }
85    let args = &args[..argc];
86
87    // Match argv[0] against registered command names
88    if args.is_empty() {
89        return;
90    }
91    let cmd_name = args[0];
92
93    let count = unsafe { CMD_COUNT };
94    for i in 0..count {
95        if let Some(entry) = unsafe { &CMD_TABLE[i] } {
96            // Compare without trailing \0
97            let entry_name = strip_nul(entry.name);
98            if cmd_name == entry_name {
99                (entry.handler)(args);
100                return;
101            }
102        }
103    }
104}
105
106fn strip_nul(s: &[u8]) -> &[u8] {
107    if s.last() == Some(&0) {
108        &s[..s.len() - 1]
109    } else {
110        s
111    }
112}
113
114unsafe fn cstr_ptr_to_slice<'a>(ptr: *const u8) -> &'a [u8] {
115    let mut len = 0;
116    while unsafe { *ptr.add(len) } != 0 {
117        len += 1;
118    }
119    unsafe { core::slice::from_raw_parts(ptr, len) }
120}
121
122/// Process a complete input line through the shell.
123///
124/// `line` must be a null-terminated byte string (e.g. `b"help\0"`).
125pub fn process_line(line: &[u8]) {
126    unsafe { bindings::ove_shell_process_line(line.as_ptr() as *const _) }
127}
128
129/// Set a hook to capture shell output.
130///
131/// Pass `None` to remove a previously set hook.
132pub fn set_output_hook(hook: bindings::ove_shell_output_hook_t) {
133    unsafe { bindings::ove_shell_set_output_hook(hook) }
134}