/ firmware / src / console / remote.cpp
remote.cpp
  1  #include "remote.h"
  2  #include "path.h"
  3  #include "prompt.h"
  4  #include "../programs/shell/microfetch.h"
  5  
  6  #include <Console.h>
  7  #include <SD.h>
  8  #include <stdio.h>
  9  #include <string.h>
 10  
 11  extern char g_cwd[];
 12  
 13  namespace {
 14  
 15  struct RedirectCtx {
 16    console::remote::flush_fn flush;
 17    void *ctx;
 18  };
 19  
 20  int redirect_write(void *cookie, const char *buf, int len) {
 21    RedirectCtx *r = (RedirectCtx *)cookie;
 22    const char *start = buf;
 23    for (int i = 0; i < len; i++) {
 24      if (buf[i] == '\n') {
 25        if (&buf[i] > start)
 26          r->flush(start, &buf[i] - start, r->ctx);
 27        r->flush("\r\n", 2, r->ctx);
 28        start = &buf[i + 1];
 29      }
 30    }
 31    if (start < buf + len)
 32      r->flush(start, (buf + len) - start, r->ctx);
 33    return len;
 34  }
 35  
 36  } // namespace
 37  
 38  //------------------------------------------
 39  //  Shell construction / reset
 40  //------------------------------------------
 41  console::remote::Shell::Shell(char *ring_buf, uint16_t ring_cap,
 42                                char *write_buf, size_t write_cap,
 43                                char *line_buf, size_t line_cap,
 44                                flush_fn flush, void *flush_ctx)
 45      : terminal_(line_buf, line_cap), history_(), cwd_{"/"},
 46        flush_fn_(flush), flush_ctx_(flush_ctx) {
 47    ring_.data = ring_buf;
 48    ring_.capacity = ring_cap;
 49    ring_.head.store(0);
 50    ring_.tail.store(0);
 51    write_.data = write_buf;
 52    write_.capacity = write_cap;
 53    write_.position = 0;
 54  }
 55  
 56  void console::remote::Shell::reset() {
 57    programs::shell::session::reset(&ring_);
 58    programs::shell::session::reset(&write_);
 59    terminal_.clear_buffer();
 60    history_.clear();
 61    history_.load("/.MSH_HIST");
 62    strlcpy(cwd_, console::path::home_dir(), sizeof(cwd_));
 63  }
 64  
 65  void console::remote::Shell::save_history() {
 66    history_.save("/.MSH_HIST");
 67  }
 68  
 69  //------------------------------------------
 70  //  Input — push raw bytes to ring buffer
 71  //------------------------------------------
 72  void console::remote::Shell::push_input(char ch) {
 73    programs::shell::session::push(&ring_, ch);
 74  }
 75  
 76  void console::remote::Shell::push_input(const char *data, size_t len) {
 77    for (size_t i = 0; i < len; i++)
 78      programs::shell::session::push(&ring_, data[i]);
 79  }
 80  
 81  //------------------------------------------
 82  //  Output helpers
 83  //------------------------------------------
 84  void console::remote::Shell::write(const char *data, size_t len) {
 85    for (size_t i = 0; i < len; i++) {
 86      if (!programs::shell::session::push(&write_, data[i])) {
 87        flush();
 88        programs::shell::session::push(&write_, data[i]);
 89      }
 90    }
 91  }
 92  
 93  void console::remote::Shell::flush() {
 94    if (write_.position > 0) {
 95      flush_fn_(write_.data, write_.position, flush_ctx_);
 96      programs::shell::session::reset(&write_);
 97    }
 98  }
 99  
100  void console::remote::Shell::redraw_line() {
101    write("\r\x1b[K", 4);
102    const char *buf = terminal_.buffer_str();
103    size_t len = terminal_.buffer_length();
104    size_t cursor = terminal_.cursor_position();
105  
106    if (len > 0)
107      write(buf, len);
108  
109    if (cursor < len) {
110      char esc[16];
111      int n = snprintf(esc, sizeof(esc), "\x1b[%uD", (unsigned)(len - cursor));
112      write(esc, n);
113    }
114  
115    flush();
116  }
117  
118  //------------------------------------------
119  //  MOTD / Prompt
120  //------------------------------------------
121  void console::remote::Shell::send_motd(const char *transport, const char *remote_ip) {
122    const char *banner = console::prompt::build_motd(remote_ip);
123    write(banner, strlen(banner));
124    const char *motd = programs::shell::microfetch::generate(transport);
125    write(motd, strlen(motd));
126  }
127  
128  void console::remote::Shell::send_prompt() {
129    const char *p = console::prompt::build(cwd_);
130    write(p, strlen(p));
131    flush();
132  }
133  
134  //------------------------------------------
135  //  Built-in commands (cd, pwd, clear)
136  //------------------------------------------
137  bool console::remote::Shell::handle_builtin(const char *cmd) {
138    if (strcmp(cmd, "pwd") == 0) {
139      write(cwd_, strlen(cwd_));
140      write("\r\n", 2);
141      return true;
142    }
143  
144    if (strcmp(cmd, "clear") == 0) {
145      write("\x1b[2J\x1b[H", 7);
146      return true;
147    }
148  
149    if (strcmp(cmd, "cd") == 0) {
150      strlcpy(cwd_, console::path::home_dir(), sizeof(cwd_));
151      return true;
152    }
153  
154    if (strncmp(cmd, "cd ", 3) == 0) {
155      const char *arg = cmd + 3;
156      while (*arg == ' ') arg++;
157  
158      char prev[128];
159      strlcpy(prev, cwd_, sizeof(prev));
160      console::path::apply_cd(cwd_, sizeof(cwd_), arg);
161  
162      if (!SD.exists(cwd_)) {
163        strlcpy(cwd_, prev, sizeof(cwd_));
164        write("no such directory\r\n", 19);
165      }
166      return true;
167    }
168  
169    return false;
170  }
171  
172  //------------------------------------------
173  //  Service loop — terminal-aware
174  //------------------------------------------
175  void console::remote::Shell::service() {
176    char raw;
177    while (programs::shell::session::pop(&ring_, &raw)) {
178      console::KeyCode key = terminal_.process_byte((uint8_t)raw);
179      if (key == console::KeyCode::None) continue;
180  
181      console::TerminalEvent event = terminal_.handle_key(key);
182  
183      switch (event) {
184      case console::TerminalEvent::BufferChanged:
185        redraw_line();
186        break;
187  
188      case console::TerminalEvent::CursorMoved:
189        if (key == console::KeyCode::ArrowLeft || key == console::KeyCode::CtrlB)
190          write("\x1b[D", 3);
191        else
192          write("\x1b[C", 3);
193        flush();
194        break;
195  
196      case console::TerminalEvent::CursorHome:
197      case console::TerminalEvent::CursorEnd:
198        redraw_line();
199        break;
200  
201      case console::TerminalEvent::CommandReady: {
202        write("\r\n", 2);
203        const char *cmd = terminal_.take_command();
204  
205        if (strcmp(cmd, "exit") == 0 || strcmp(cmd, "quit") == 0) {
206          write("\x1b[33mgoodbye!\x1b[0m\r\n", 22);
207          flush();
208          return;
209        }
210  
211        history_.add(cmd);
212        history_.reset_position();
213  
214        if (!handle_builtin(cmd)) {
215          char saved_cwd[128];
216          strlcpy(saved_cwd, g_cwd, sizeof(saved_cwd));
217          strlcpy(g_cwd, cwd_, sizeof(saved_cwd));
218          run_command(cmd, flush_fn_, flush_ctx_);
219          strlcpy(cwd_, g_cwd, sizeof(cwd_)); // cd may have changed it
220          strlcpy(g_cwd, saved_cwd, sizeof(saved_cwd));
221        }
222  
223        send_prompt();
224        break;
225      }
226  
227      case console::TerminalEvent::EmptyCommand:
228        write("\r\n", 2);
229        send_prompt();
230        break;
231  
232      case console::TerminalEvent::Interrupt:
233        terminal_.clear_buffer();
234        write("^C\r\n", 4);
235        send_prompt();
236        break;
237  
238      case console::TerminalEvent::EndOfFile:
239        write("logout\r\n", 8);
240        flush();
241        return;
242  
243      case console::TerminalEvent::ClearScreen:
244        terminal_.clear_buffer();
245        write("\x1b[2J\x1b[H", 7);
246        send_prompt();
247        break;
248  
249      case console::TerminalEvent::DeleteWord: {
250        const char *buf = terminal_.buffer_str();
251        size_t len = terminal_.buffer_length();
252        if (len == 0) break;
253  
254        size_t pos = len;
255        while (pos > 0 && buf[pos - 1] == ' ') pos--;
256        while (pos > 0 && buf[pos - 1] != ' ') pos--;
257  
258        char trimmed[256];
259        if (pos >= sizeof(trimmed)) pos = sizeof(trimmed) - 1;
260        memcpy(trimmed, buf, pos);
261        trimmed[pos] = '\0';
262        terminal_.set_buffer(trimmed);
263        redraw_line();
264        break;
265      }
266  
267      case console::TerminalEvent::ClearLine:
268        terminal_.clear_buffer();
269        redraw_line();
270        break;
271  
272      case console::TerminalEvent::KillToEnd:
273        redraw_line();
274        break;
275  
276      case console::TerminalEvent::SwapChars:
277        redraw_line();
278        break;
279  
280      case console::TerminalEvent::Redraw:
281        redraw_line();
282        break;
283  
284      case console::TerminalEvent::HistoryPrevious: {
285        const char *entry = history_.previous();
286        if (entry) {
287          terminal_.set_buffer(entry);
288          redraw_line();
289        }
290        break;
291      }
292  
293      case console::TerminalEvent::HistoryNext: {
294        const char *entry = history_.next();
295        if (entry)
296          terminal_.set_buffer(entry);
297        else
298          terminal_.clear_buffer();
299        redraw_line();
300        break;
301      }
302  
303      default:
304        break;
305      }
306    }
307  }
308  
309  //------------------------------------------
310  //  Command execution with stdout redirect
311  //------------------------------------------
312  int console::remote::run_command(const char *line, flush_fn flush, void *ctx) {
313    RedirectCtx rctx = {flush, ctx};
314  
315    FILE *capture = funopen(&rctx, NULL, redirect_write, NULL, NULL);
316    if (!capture) return -1;
317  
318    setvbuf(capture, NULL, _IONBF, 0);
319  
320    FILE *saved = stdout;
321    stdout = capture;
322  
323    int ret = Console.run(line);
324  
325    fflush(capture);
326    stdout = saved;
327    fclose(capture);
328  
329    return ret;
330  }