/ firmware / src / programs / shell.rs
shell.rs
  1  //! Shell: command dispatch plus prompt/path/server submodules.
  2  
  3  use core::fmt::Write;
  4  use core::sync::atomic::{AtomicU32, Ordering};
  5  
  6  use alloc::string::String as AllocString;
  7  
  8  use crate::programs::coreutils;
  9  
 10  mod path;
 11  mod prompt;
 12  mod server;
 13  
 14  pub use path::{apply_cd, display_cwd, ensure_filesystem_hierarchy, home_dir, resolve_path};
 15  pub use prompt::{build_motd, build_prompt};
 16  pub use server::{spawn, task};
 17  
 18  const CTRL_L: u8 = 0x0c;
 19  const CTRL_P: u8 = 0x10;
 20  const CTRL_N: u8 = 0x0e;
 21  const CTRL_W: u8 = 0x17;
 22  const CTRL_U: u8 = 0x15;
 23  
 24  static TERM_WIDTH: AtomicU32 = AtomicU32::new(80);
 25  
 26  pub fn set_terminal_width(width: u32) {
 27      TERM_WIDTH.store(width, Ordering::Relaxed);
 28  }
 29  
 30  pub fn terminal_width() -> u32 {
 31      TERM_WIDTH.load(Ordering::Relaxed)
 32  }
 33  
 34  pub fn dispatch(cmd: &str, cwd: &mut AllocString) -> (AllocString, bool) {
 35      let cmd = cmd.trim();
 36  
 37      if cmd == "exit" || cmd == "quit" {
 38          let mut out = AllocString::new();
 39          let _ = write!(out, "\x1b[33mgoodbye!\x1b[0m\r\n");
 40          return (out, true);
 41      }
 42  
 43      if let Some(argument) = cmd.strip_prefix("cd ") {
 44          let previous = cwd.clone();
 45          apply_cd(cwd, argument);
 46          if !crate::filesystems::sd::directory_exists(cwd.as_str()) {
 47              *cwd = previous;
 48              return (coreutils::fmt_error(&"no such directory"), false);
 49          }
 50          return (AllocString::new(), false);
 51      }
 52  
 53      if cmd == "cd" {
 54          *cwd = home_dir();
 55          return (AllocString::new(), false);
 56      }
 57  
 58      let (name, args) = match cmd.find(' ') {
 59          Some(position) => (&cmd[..position], cmd[position + 1..].trim()),
 60          None => (cmd, ""),
 61      };
 62  
 63      let out = match name {
 64          "help" | "h" => coreutils::help::run(),
 65          "ls" => {
 66              if args.is_empty() {
 67                  coreutils::ls::run(cwd)
 68              } else {
 69                  let mut target = cwd.clone();
 70                  apply_cd(&mut target, args);
 71                  coreutils::ls::run(&target)
 72              }
 73          }
 74          "pwd" => {
 75              let mut output = AllocString::new();
 76              let _ = write!(output, "{}\r\n", cwd);
 77              output
 78          }
 79          "mkdir" => coreutils::mkdir::run(cwd, args),
 80          "cp" => coreutils::cp::run(args),
 81          "mv" => coreutils::mv::run(args),
 82          "rm" => coreutils::rm::run(cwd, args),
 83          "cat" => coreutils::cat::run(cwd, args),
 84          "touch" => coreutils::touch::run(cwd, args),
 85          "uptime" => coreutils::uptime::run(),
 86          "free" => coreutils::free::run(),
 87          "date" => coreutils::date::run(),
 88          "df" => coreutils::df::run(),
 89          "whoami" => coreutils::whoami::run(),
 90          "hostname" => coreutils::hostname::run(),
 91          "ifconfig" => coreutils::ifconfig::run(),
 92          "sensors" => coreutils::sensors::run(),
 93          "wakecause" => coreutils::wakecause::run(),
 94          "logstatus" => coreutils::logstatus::run(),
 95          "microfetch" | "fetch" => crate::programs::microfetch::run(),
 96          "microtop" | "top" => crate::programs::microtop::render_frame(terminal_width() as u16, 24),
 97          "reboot" => esp_hal::system::software_reset(),
 98          "clear" => {
 99              let mut output = AllocString::new();
100              let _ = write!(output, "\x1b[2J\x1b[H");
101              output
102          }
103          "" => AllocString::new(),
104          unknown => {
105              let mut output = AllocString::new();
106              let _ = write!(output, "\x1b[31mcommand not found: {}\x1b[0m\r\n", unknown);
107              output
108          }
109      };
110  
111      (out, false)
112  }
113  
114  const HISTORY_FILE: &str = ".MSH_HIST";
115  
116  pub(super) fn load_history(history: &mut crate::services::ssh::history::History<256>) {
117      let home = home_dir();
118      if let Ok(contents) = crate::filesystems::sd::read_file_at::<4096>(home.as_str(), HISTORY_FILE)
119      {
120          if let Ok(text) = core::str::from_utf8(contents.as_slice()) {
121              for line in text.lines() {
122                  let line = line.trim();
123                  if !line.is_empty() {
124                      let _ = history.add(line);
125                  }
126              }
127          }
128      }
129  }
130  
131  pub(super) fn save_history(history: &crate::services::ssh::history::History<256>) {
132      let home = home_dir();
133      let mut buf = AllocString::new();
134      for entry in history.iter() {
135          buf.push_str(entry);
136          buf.push('\n');
137      }
138      let path = resolve_path(home.as_str(), HISTORY_FILE);
139      let _ = crate::filesystems::sd::write_file_chunk(path.as_str(), 0, buf.as_bytes());
140  }