376 lines
11 KiB
C
376 lines
11 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
/* Full-line linenoise completion for nested project console commands. */
|
|
|
|
#include "console_completion.h"
|
|
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#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);
|
|
}
|