/* SPDX-License-Identifier: GPL-3.0-only */ /* Full-line linenoise completion for nested project console commands. */ #include "console_completion.h" #include #include #include #include #include "linenoise/linenoise.h" static const char *const s_root_candidates[] = { "help", "exit", "debug", "display", "serial", "broker", "usb", "user", "wifi", "mdns", "web", "ssh", "ping", "nslookup", "traceroute", "reboot", "memory", }; /* Keep full-line candidate strings grouped by their registered root command. */ static const char *const s_completion_candidates[] = { /* Hardware debug commands and safe fixed arguments. */ "debug status", "debug transceiver", "debug transceiver enable", "debug transceiver disable", "debug drivers", "debug loopback-a", "debug loopback-b", "debug valid-test", "debug uart-loopback", "debug uart-suite", "debug cts-flow-test", "debug rts-flow-test", "debug display", "debug display status", "debug display probe", "debug display scan", "debug display scan --force", "debug display init", "debug display init 0x3c", "debug display init 0x78", "debug display init 0x79", "debug display init 0x3d", "debug display init 0x7a", "debug display init 0x7b", "debug display off", "debug display pattern", "debug display pattern clear", "debug display pattern fill", "debug display pattern checker", "debug display pattern grid", "debug display pattern corners", "debug display pattern layout", "debug display row", "debug display contrast", "debug display invert", "debug display invert on", "debug display invert off", "debug buttons", "debug buttons status", "debug buttons test", /* Persistent local OLED aging settings. */ "display status", "display set", "display set dim-seconds", "display set off-seconds", "display save", "display load", "display defaults", "display reset", /* Serial service lifecycle, persistence, counters, and settings. */ "serial status", "serial start", "serial stop", "serial set", "serial set baud", "serial set data-bits", "serial set data-bits 7", "serial set data-bits 8", "serial set parity", "serial set parity none", "serial set parity even", "serial set parity odd", "serial set stop-bits", "serial set stop-bits 1", "serial set stop-bits 2", "serial set flow", "serial set flow none", "serial set flow rts-cts", "serial set dtr", "serial set dtr inactive", "serial set dtr active", "serial set dtr on-connect", "serial set rts-threshold", "serial save", "serial load", "serial defaults", "serial reset", "serial counters", "serial clear-counters", /* Session broker inspection, ownership, and data operations. */ "broker status", "broker clients", "broker counters", "broker clear-counters", "broker connect", "broker disconnect", "broker request-writer", "broker release-writer", "broker force-writer", "broker send-hex", "broker read", "broker events", /* Native USB CDC status and writer ownership. */ "usb help", "usb status", "usb counters", "usb clear-counters", "usb request-writer", "usb release-writer", /* Physical role-based user, password, and SSH-key administration. */ "user status", "user list", "user show", "user bootstrap", "user bootstrap --generate", "user recover --force", "user add", "user delete", "user role", "user password", "user key add", "user key delete", "user key clear", /* Wi-Fi lifecycle, persistence, profiles, AP policy, and diagnostics. */ "wifi status", "wifi profiles", "wifi counters", "wifi clear-counters", "wifi start", "wifi stop", "wifi reconnect", "wifi next-profile", "wifi save", "wifi load", "wifi defaults", "wifi reset", "wifi profile", "wifi profile set", "wifi profile secret", "wifi profile enable", "wifi profile disable", "wifi profile delete", "wifi ap", "wifi ap policy", "wifi ap policy off", "wifi ap policy fallback", "wifi ap policy always", "wifi ap ssid", "wifi ap channel", "wifi ap secret", "wifi ap show-secret", "wifi ping", "wifi nslookup", "wifi traceroute", /* Station mDNS hostname configuration. */ "mdns status", "mdns suffix", "mdns save", "mdns load", "mdns defaults", "mdns reset", /* Authenticated HTTPS lifecycle and physical-admin recovery operations. */ "web help", "web status", "web start", "web stop", "web counters", "web clear-counters", "web credentials", "web credentials show", "web credentials rotate", "web credentials rotate --force", "web certificate", "web certificate info", "web certificate rotate", "web certificate rotate --force", "web reset", "web reset --force", /* Authenticated SSH serial transport and independent host identity. */ "ssh help", "ssh status", "ssh start", "ssh stop", "ssh sessions", "ssh disconnect", "ssh counters", "ssh clear-counters", "ssh host-key", "ssh host-key info", "ssh host-key rotate", "ssh host-key rotate --force", "ssh reset", "ssh reset --force", }; void console_completion_visit(const char *line, console_completion_visitor_t visitor, void *context) { if (line == NULL || visitor == NULL) { return; } size_t line_length = strlen(line); const char *const *candidates = strchr(line, ' ') == NULL ? s_root_candidates : s_completion_candidates; size_t candidate_count = strchr(line, ' ') == NULL ? sizeof(s_root_candidates) / sizeof(s_root_candidates[0]) : sizeof(s_completion_candidates) / sizeof(s_completion_candidates[0]); for (size_t index = 0U; index < candidate_count; ++index) { const char *candidate = candidates[index]; if (strlen(candidate) > line_length && strncmp(candidate, line, line_length) == 0 && !visitor(candidate, context)) { return; } } } typedef struct { const char *line; const char *first; size_t common_length; } completion_expand_context_t; static bool collect_common_prefix(const char *candidate, void *context) { completion_expand_context_t *result = context; if (result->first == NULL) { result->first = candidate; result->common_length = strlen(candidate); return true; } size_t candidate_length = strlen(candidate); if (result->common_length > candidate_length) { result->common_length = candidate_length; } size_t offset = strlen(result->line); while (offset < result->common_length && result->first[offset] == candidate[offset]) { ++offset; } result->common_length = offset; return true; } bool console_completion_expand(const char *line, char *completed, size_t capacity) { if (line == NULL || completed == NULL || capacity == 0U) { return false; } completion_expand_context_t result = {.line = line}; console_completion_visit(line, collect_common_prefix, &result); size_t line_length = strlen(line); if (result.first == NULL || result.common_length <= line_length || result.common_length >= capacity) { return false; } memcpy(completed, result.first, result.common_length); completed[result.common_length] = '\0'; return true; } typedef struct { char *output; size_t capacity; size_t length; bool complete; } completion_format_context_t; static bool format_completion_candidate(const char *candidate, void *context) { completion_format_context_t *result = context; size_t candidate_length = strlen(candidate); if (candidate_length + 2U > result->capacity - result->length) { result->complete = false; return false; } memcpy(result->output + result->length, candidate, candidate_length); result->length += candidate_length; result->output[result->length++] = '\r'; result->output[result->length++] = '\n'; return true; } bool console_completion_format_matches(const char *line, char *output, size_t capacity, size_t *output_length) { if (line == NULL || output == NULL || output_length == NULL || capacity == 0U) { return false; } completion_format_context_t result = { .output = output, .capacity = capacity, .complete = true, }; console_completion_visit(line, format_completion_candidate, &result); if (!result.complete) { return false; } *output_length = result.length; return true; } static ssize_t console_read_with_late_terminal_upgrade(int file_descriptor, void *buffer, size_t size) { ssize_t received = read(file_descriptor, buffer, size); if (received > 0 && linenoiseIsDumbMode()) { /* * The current line remains in the safe dumb reader. The next call to * linenoise() starts enhanced mode only after a terminal has proven it * can send data, so its cursor-position query cannot block unattended. */ linenoiseSetDumbMode(0); } return received; } /* The UART frontend is the sole caller of linenoise's completion callback. */ static char s_uart_completion_output[CONSOLE_COMPLETION_OUTPUT_CAPACITY]; static void console_completion_callback(const char *buffer, linenoiseCompletions *completions) { char completed[257U] = {0}; if (console_completion_expand(buffer, completed, sizeof(completed))) { linenoiseAddCompletion(completions, completed); return; } size_t output_length = 0U; if (!console_completion_format_matches(buffer, s_uart_completion_output, sizeof(s_uart_completion_output), &output_length) || output_length == 0U) { return; } /* * Linenoise cycles every completion it receives. Print the shared list * ourselves, then return the unchanged line as its one completion so its * normal refresh restores the prompt without selecting a candidate. */ fputs("\r\n", stdout); (void)fwrite(s_uart_completion_output, 1U, output_length, stdout); fflush(stdout); linenoiseAddCompletion(completions, buffer); } void console_completion_install(void) { /* Preserve ESP-IDF's safe boot-time mode until actual UART input arrives. */ linenoiseSetReadFunction(&console_read_with_late_terminal_upgrade); linenoiseSetCompletionCallback(&console_completion_callback); }