prompt.cpp
1 #include "prompt.h" 2 #include "ansi.h" 3 #include "icons.h" 4 #include <identity.h> 5 #include "../networking/sntp.h" 6 #include <config.h> 7 8 #include <Arduino.h> 9 #include <stdio.h> 10 #include <string.h> 11 12 namespace { 13 14 uint16_t g_terminal_width = 0; 15 16 // Probe terminal width using VT100 escape sequences: 17 // 1. Save cursor position 18 // 2. Move cursor to column 999 (far right — terminal clamps to last column) 19 // 3. Query cursor position (DSR → CPR response: \x1b[row;colR) 20 // 4. Restore cursor position 21 // Returns 0 on failure (no response / timeout). 22 uint16_t probe_terminal_width() { 23 while (Serial.available()) Serial.read(); 24 25 Serial.print("\x1b[s\x1b[999C\x1b[6n\x1b[u"); 26 Serial.flush(); 27 28 char buf[16]; 29 size_t pos = 0; 30 int timeout_ms = 500; 31 const int poll_ms = 5; 32 33 while (timeout_ms > 0 && pos < sizeof(buf) - 1) { 34 if (Serial.available()) { 35 char c = Serial.read(); 36 buf[pos++] = c; 37 if (c == 'R') break; 38 } else { 39 delay(poll_ms); 40 timeout_ms -= poll_ms; 41 } 42 } 43 buf[pos] = '\0'; 44 45 // Parse \x1b[row;colR 46 char *semi = strchr(buf, ';'); 47 if (!semi) return 0; 48 uint16_t col = (uint16_t)atoi(semi + 1); 49 return col > 0 ? col : 0; 50 } 51 52 // Prompt buffer — large enough for two-line powerline with ANSI escapes. 53 // ANSI sequences are invisible but consume bytes. Each segment has ~20 bytes 54 // of escape codes. With 8 segments + frame + fill, 1024 is comfortable. 55 char g_prompt[2048]; 56 57 const char *cwd_glyph(const char *cwd) { 58 if (strcmp(cwd, "/") == 0) return NF_FA_LOCK; 59 if (strcmp(cwd, "~") == 0 || strncmp(cwd, "~/", 2) == 0) return NF_FA_HOME; 60 return NF_FA_FOLDER_OPEN; 61 } 62 63 int visible_width(const char *s) { 64 int w = 0; 65 bool in_esc = false; 66 while (*s) { 67 if (*s == '\x1b') { 68 in_esc = true; 69 s++; 70 continue; 71 } 72 if (in_esc) { 73 if ((*s >= 'A' && *s <= 'Z') || (*s >= 'a' && *s <= 'z')) 74 in_esc = false; 75 s++; 76 continue; 77 } 78 unsigned char c = (unsigned char)*s; 79 if (c < 0x80) { 80 w++; 81 s++; 82 } else if ((c & 0xE0) == 0xC0) { 83 w++; s += 2; 84 } else if ((c & 0xF0) == 0xE0) { 85 w++; s += 3; 86 } else if ((c & 0xF8) == 0xF0) { 87 w++; s += 4; 88 } else { 89 s++; 90 } 91 } 92 return w; 93 } 94 95 const char *last_path_component(const char *cwd) { 96 const char *p = strrchr(cwd, '/'); 97 if (!p || *(p + 1) == '\0') return cwd; 98 return p + 1; 99 } 100 101 } // namespace 102 103 void console::prompt::detect_width() { 104 uint16_t w = probe_terminal_width(); 105 if (w > 0) { 106 g_terminal_width = w; 107 Serial.printf("[console] terminal width: %u\n", w); 108 } else { 109 g_terminal_width = 80; 110 Serial.println("[console] width probe failed, defaulting to 80"); 111 } 112 } 113 114 void console::prompt::set_terminal_width(uint16_t w) { 115 g_terminal_width = w; 116 } 117 118 uint16_t console::prompt::terminal_width() { 119 if (g_terminal_width == 0) g_terminal_width = 80; 120 return g_terminal_width; 121 } 122 123 const char *console::prompt::build(const char *cwd) { 124 const char *display = last_path_component(cwd); 125 const char *glyph = cwd_glyph(cwd); 126 const char *hostname = services::identity::access_hostname(); 127 128 // Time string 129 char time_str[24]; 130 if (networking::sntp::isSynced()) { 131 uint32_t epoch = networking::sntp::accessUTCEpoch(); 132 uint32_t secs_of_day = epoch % 86400; 133 uint32_t hour24 = secs_of_day / 3600; 134 uint32_t minute = (secs_of_day % 3600) / 60; 135 uint32_t second = secs_of_day % 60; 136 uint32_t hour12; 137 const char *ampm; 138 if (hour24 == 0) { hour12 = 12; ampm = "AM"; } 139 else if (hour24 < 12) { hour12 = hour24; ampm = "AM"; } 140 else if (hour24 == 12) { hour12 = 12; ampm = "PM"; } 141 else { hour12 = hour24 - 12; ampm = "PM"; } 142 snprintf(time_str, sizeof(time_str), "%02lu:%02lu:%02lu %s", 143 (unsigned long)hour12, (unsigned long)minute, 144 (unsigned long)second, ampm); 145 } else { 146 unsigned long uptime = millis() / 1000; 147 snprintf(time_str, sizeof(time_str), "%lum%lus", uptime / 60, uptime % 60); 148 } 149 150 // RAM 151 uint32_t heap_free = ESP.getFreeHeap(); 152 uint32_t heap_total = ESP.getHeapSize(); 153 uint32_t heap_pct = heap_total > 0 ? ((heap_total - heap_free) * 100) / heap_total : 0; 154 char ram_str[16]; 155 if (heap_free >= 1024 * 1024) 156 snprintf(ram_str, sizeof(ram_str), "%.1fM", heap_free / (1024.0f * 1024.0f)); 157 else 158 snprintf(ram_str, sizeof(ram_str), "%.1fK", heap_free / 1024.0f); 159 char ram_pct[8]; 160 snprintf(ram_pct, sizeof(ram_pct), "%lu%%", (unsigned long)heap_pct); 161 162 // Context 163 char context[80]; 164 snprintf(context, sizeof(context), "%s@%s", CONFIG_SSH_USER, hostname); 165 166 // Build left segment 167 char left[256]; 168 snprintf(left, sizeof(left), 169 ANSI_DIM FRAME_TOP_LEFT ANSI_RESET 170 PROMPT_OS_BG PROMPT_OS_FG " " NF_FA_MICROCHIP " " 171 PROMPT_DIR_BG PROMPT_OS_BG_AS_FG NF_PLE_LEFT_HARD 172 PROMPT_DIR_BG PROMPT_DIR_FG " %s %s " 173 ANSI_RESET PROMPT_DIR_BG_AS_FG NF_PLE_LEFT_HARD ANSI_RESET, 174 glyph, display); 175 176 // Build right segment 177 char right[384]; 178 snprintf(right, sizeof(right), 179 PROMPT_CTX_BG_AS_FG NF_PLE_RIGHT_HARD 180 PROMPT_CTX_BG PROMPT_CTX_FG " %s " 181 PROMPT_RAM_BG PROMPT_RAM_FG NF_PLE_RIGHT_HARD " " 182 PROMPT_RAM_BG PROMPT_RAM_FG "%s " NF_MD_RAM " %s " 183 NF_PLE_RIGHT_SOFT " xtensa " NF_MD_ARCH " " 184 PROMPT_ARCH_BG PROMPT_CLOCK_BG_AS_FG NF_PLE_RIGHT_HARD 185 PROMPT_CLOCK_BG PROMPT_CLOCK_FG " %s " NF_FA_CLOCK " " ANSI_RESET 186 ANSI_DIM "\xe2\x94\x80\xe2\x95\xae" ANSI_RESET, 187 context, ram_pct, ram_str, time_str); 188 189 int left_vis = visible_width(left); 190 int right_vis = visible_width(right); 191 192 int fill = (int)g_terminal_width - left_vis - right_vis; 193 if (fill < 1) fill = 1; 194 195 // Assemble directly into g_prompt — no intermediate fill buffer 196 int pos = snprintf(g_prompt, sizeof(g_prompt), "%s" ANSI_DIM, left); 197 for (int i = 0; i < fill && pos < (int)sizeof(g_prompt) - 4; i++) { 198 g_prompt[pos++] = '\xe2'; 199 g_prompt[pos++] = '\x94'; 200 g_prompt[pos++] = '\x80'; 201 } 202 pos += snprintf(g_prompt + pos, sizeof(g_prompt) - pos, 203 ANSI_RESET "%s\r\n" ANSI_DIM FRAME_BOT_LEFT ANSI_RESET " ", right); 204 205 return g_prompt; 206 } 207 208 const char *console::prompt::build_motd(const char *remote_ip) { 209 static char motd[640]; 210 const char *hostname = services::identity::access_hostname(); 211 int pos = 0; 212 213 if (remote_ip && networking::sntp::isSynced()) { 214 pos += snprintf(motd + pos, sizeof(motd) - pos, 215 "Last login: %s from %s\r\n", 216 networking::sntp::accessLocalTimeString(), remote_ip); 217 } 218 219 pos += snprintf(motd + pos, sizeof(motd) - pos, 220 "\r\n" 221 "Welcome to %s!\r\n" 222 "\r\n" 223 "System information: microfetch\r\n" 224 "Hardware sensors: sensors\r\n" 225 "Network interfaces: ip\r\n" 226 "Memory usage: free\r\n" 227 "Disk usage: df\r\n" 228 "Show all commands: help\r\n" 229 "\r\n", 230 hostname); 231 232 return motd; 233 }