Share completion and add SSH line editing
This commit is contained in:
+114
-14
@@ -49,6 +49,7 @@ typedef struct {
|
||||
admin_ssh_console_token_t token;
|
||||
user_principal_t principal;
|
||||
size_t input_length;
|
||||
size_t input_cursor;
|
||||
uint8_t input[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
|
||||
uint8_t history[ADMIN_SSH_CONSOLE_HISTORY_DEPTH]
|
||||
[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
|
||||
@@ -57,6 +58,8 @@ typedef struct {
|
||||
uint8_t draft[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
|
||||
size_t draft_length;
|
||||
uint8_t escape_state;
|
||||
uint8_t escape_parameters[4];
|
||||
size_t escape_parameter_length;
|
||||
bool discard_next_lf;
|
||||
admin_prompt_state_t prompt_state;
|
||||
bool prompt_hidden;
|
||||
@@ -185,12 +188,25 @@ static void print_prompt(const admin_ssh_console_token_t *token)
|
||||
static bool redraw_line_locked(admin_session_t *session)
|
||||
{
|
||||
static const char prefix[] = "\r\x1b[2Kadmin@serial-tool> ";
|
||||
size_t required = sizeof(prefix) - 1U + session->input_length;
|
||||
char cursor_back[16] = {0};
|
||||
size_t tail_length = session->input_length - session->input_cursor;
|
||||
size_t cursor_back_length = 0U;
|
||||
if (tail_length > 0U) {
|
||||
int written = snprintf(cursor_back, sizeof(cursor_back), "\x1b[%uD",
|
||||
(unsigned int)tail_length);
|
||||
if (written < 0 || (size_t)written >= sizeof(cursor_back)) {
|
||||
return false;
|
||||
}
|
||||
cursor_back_length = (size_t)written;
|
||||
}
|
||||
size_t required = sizeof(prefix) - 1U + session->input_length + cursor_back_length;
|
||||
if (required > ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - session->output_length) {
|
||||
return append_output_locked(session, (const uint8_t *)"\a", 1U);
|
||||
}
|
||||
(void)append_output_locked(session, (const uint8_t *)prefix, sizeof(prefix) - 1U);
|
||||
return append_output_locked(session, session->input, session->input_length);
|
||||
(void)append_output_locked(session, session->input, session->input_length);
|
||||
return cursor_back_length == 0U ||
|
||||
append_output_locked(session, (const uint8_t *)cursor_back, cursor_back_length);
|
||||
}
|
||||
|
||||
static void history_commit_locked(admin_session_t *session)
|
||||
@@ -210,6 +226,35 @@ static void history_commit_locked(admin_session_t *session)
|
||||
}
|
||||
}
|
||||
|
||||
static void history_move_locked(admin_session_t *session, bool older);
|
||||
|
||||
static void editor_key_locked(admin_session_t *session, uint8_t key)
|
||||
{
|
||||
if (key == 'A' || key == 'B') {
|
||||
history_move_locked(session, key == 'A');
|
||||
return;
|
||||
}
|
||||
if (key == 'C' && session->input_cursor < session->input_length) {
|
||||
++session->input_cursor;
|
||||
} else if (key == 'D' && session->input_cursor > 0U) {
|
||||
--session->input_cursor;
|
||||
} else if (key == 'H') {
|
||||
session->input_cursor = 0U;
|
||||
} else if (key == 'F') {
|
||||
session->input_cursor = session->input_length;
|
||||
} else if (key == 'X' && session->input_cursor < session->input_length) {
|
||||
memmove(session->input + session->input_cursor,
|
||||
session->input + session->input_cursor + 1U,
|
||||
session->input_length - session->input_cursor);
|
||||
--session->input_length;
|
||||
session->history_position = -1;
|
||||
} else {
|
||||
(void)append_output_locked(session, (const uint8_t *)"\a", 1U);
|
||||
return;
|
||||
}
|
||||
(void)redraw_line_locked(session);
|
||||
}
|
||||
|
||||
static void history_move_locked(admin_session_t *session, bool older)
|
||||
{
|
||||
if (older) {
|
||||
@@ -226,6 +271,7 @@ static void history_move_locked(admin_session_t *session, bool older)
|
||||
memcpy(session->input, session->history[session->history_position],
|
||||
sizeof(session->input));
|
||||
session->input_length = strlen((const char *)session->input);
|
||||
session->input_cursor = session->input_length;
|
||||
} else {
|
||||
if (session->history_position < 0) {
|
||||
(void)append_output_locked(session, (const uint8_t *)"\a", 1U);
|
||||
@@ -235,10 +281,12 @@ static void history_move_locked(admin_session_t *session, bool older)
|
||||
if (session->history_position < 0) {
|
||||
memcpy(session->input, session->draft, sizeof(session->input));
|
||||
session->input_length = session->draft_length;
|
||||
session->input_cursor = session->input_length;
|
||||
} else {
|
||||
memcpy(session->input, session->history[session->history_position],
|
||||
sizeof(session->input));
|
||||
session->input_length = strlen((const char *)session->input);
|
||||
session->input_cursor = session->input_length;
|
||||
}
|
||||
}
|
||||
(void)redraw_line_locked(session);
|
||||
@@ -855,13 +903,45 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
|
||||
}
|
||||
session->discard_next_lf = false;
|
||||
if (session->escape_state != 0U) {
|
||||
if (session->escape_state == 1U && (value == '[' || value == 'O')) {
|
||||
session->escape_state = 2U;
|
||||
} else if (session->escape_state == 2U) {
|
||||
if (value == 'A' || value == 'B') {
|
||||
history_move_locked(session, value == 'A');
|
||||
if (session->escape_state == 1U) {
|
||||
if (value == '[') {
|
||||
session->escape_state = 2U;
|
||||
session->escape_parameter_length = 0U;
|
||||
} else if (value == 'O') {
|
||||
session->escape_state = 3U;
|
||||
} else {
|
||||
session->escape_state = 0U;
|
||||
}
|
||||
} else if (session->escape_state == 3U) {
|
||||
editor_key_locked(session, value);
|
||||
session->escape_state = 0U;
|
||||
} else if (value >= 'A' && value <= 'Z') {
|
||||
editor_key_locked(session, value);
|
||||
session->escape_state = 0U;
|
||||
} else if (value == '~') {
|
||||
uint8_t key = 0U;
|
||||
if (session->escape_parameter_length > 0U) {
|
||||
switch (session->escape_parameters[0]) {
|
||||
case '1':
|
||||
case '7':
|
||||
key = 'H';
|
||||
break;
|
||||
case '3':
|
||||
key = 'X';
|
||||
break;
|
||||
case '4':
|
||||
case '8':
|
||||
key = 'F';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
editor_key_locked(session, key);
|
||||
session->escape_state = 0U;
|
||||
} else if ((value == ';' || (value >= '0' && value <= '9')) &&
|
||||
session->escape_parameter_length < sizeof(session->escape_parameters)) {
|
||||
session->escape_parameters[session->escape_parameter_length++] = value;
|
||||
} else {
|
||||
session->escape_state = 0U;
|
||||
}
|
||||
@@ -876,6 +956,12 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
|
||||
continue;
|
||||
}
|
||||
if (value == '\t') {
|
||||
if (session->input_cursor != session->input_length) {
|
||||
(void)append_output_locked(session, (const uint8_t *)"\a", 1U);
|
||||
++*consumed;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
continue;
|
||||
}
|
||||
char current[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
|
||||
memcpy(current, session->input, sizeof(current));
|
||||
++*consumed;
|
||||
@@ -890,6 +976,7 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
|
||||
if (expanded) {
|
||||
strlcpy((char *)session->input, completed, sizeof(session->input));
|
||||
session->input_length = strlen((const char *)session->input);
|
||||
session->input_cursor = session->input_length;
|
||||
session->history_position = -1;
|
||||
(void)redraw_line_locked(session);
|
||||
} else {
|
||||
@@ -910,6 +997,7 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
|
||||
request.principal = session->principal;
|
||||
secure_wipe(session->input, sizeof(session->input));
|
||||
session->input_length = 0U;
|
||||
session->input_cursor = 0U;
|
||||
session->history_position = -1;
|
||||
session->command_pending = true;
|
||||
(void)append_output_locked(session, (const uint8_t *)"\r\n",
|
||||
@@ -918,29 +1006,41 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
|
||||
} else if (value == 0x03U) {
|
||||
secure_wipe(session->input, sizeof(session->input));
|
||||
session->input_length = 0U;
|
||||
session->input_cursor = 0U;
|
||||
session->history_position = -1;
|
||||
(void)append_output_locked(session, (const uint8_t *)"^C\r\n",
|
||||
sizeof("^C\r\n") - 1U);
|
||||
(void)append_output_locked(session, (const uint8_t *)"admin@serial-tool> ",
|
||||
sizeof("admin@serial-tool> ") - 1U);
|
||||
} else if (value == 0x08U || value == 0x7fU) {
|
||||
if (session->input_length > 0U) {
|
||||
session->input[--session->input_length] = 0U;
|
||||
if (session->input_cursor > 0U) {
|
||||
memmove(session->input + session->input_cursor - 1U,
|
||||
session->input + session->input_cursor,
|
||||
session->input_length - session->input_cursor + 1U);
|
||||
--session->input_cursor;
|
||||
--session->input_length;
|
||||
session->history_position = -1;
|
||||
(void)append_output_locked(session, (const uint8_t *)"\b \b",
|
||||
sizeof("\b \b") - 1U);
|
||||
(void)redraw_line_locked(session);
|
||||
}
|
||||
} else if (value >= 0x20U && value <= 0x7eU) {
|
||||
if (session->input_length >= ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY) {
|
||||
session->input_length = 0U;
|
||||
session->input_cursor = 0U;
|
||||
(void)append_output_locked(session, (const uint8_t *)
|
||||
"\r\nCommand too long; discarded.\r\nadmin@serial-tool> ",
|
||||
sizeof("\r\nCommand too long; discarded.\r\nadmin@serial-tool> ") - 1U);
|
||||
} else {
|
||||
memmove(session->input + session->input_cursor + 1U,
|
||||
session->input + session->input_cursor,
|
||||
session->input_length - session->input_cursor + 1U);
|
||||
session->input[session->input_cursor++] = value;
|
||||
++session->input_length;
|
||||
session->history_position = -1;
|
||||
session->input[session->input_length++] = value;
|
||||
session->input[session->input_length] = 0U;
|
||||
(void)append_output_locked(session, &value, 1U);
|
||||
if (session->input_cursor == session->input_length) {
|
||||
(void)append_output_locked(session, &value, 1U);
|
||||
} else {
|
||||
(void)redraw_line_locked(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
++*consumed;
|
||||
|
||||
+57
-44
@@ -7,7 +7,6 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "esp_console.h"
|
||||
#include "linenoise/linenoise.h"
|
||||
|
||||
static const char *const s_root_candidates[] = {
|
||||
@@ -144,6 +143,7 @@ static const char *const s_completion_candidates[] = {
|
||||
"wifi start",
|
||||
"wifi stop",
|
||||
"wifi reconnect",
|
||||
"wifi next-profile",
|
||||
"wifi save",
|
||||
"wifi load",
|
||||
"wifi defaults",
|
||||
@@ -202,10 +202,12 @@ static const char *const s_completion_candidates[] = {
|
||||
"ssh reset --force",
|
||||
};
|
||||
|
||||
bool console_completion_expand(const char *line, char *completed, size_t capacity)
|
||||
void console_completion_visit(const char *line,
|
||||
console_completion_visitor_t visitor,
|
||||
void *context)
|
||||
{
|
||||
if (line == NULL || completed == NULL || capacity == 0U) {
|
||||
return false;
|
||||
if (line == NULL || visitor == NULL) {
|
||||
return;
|
||||
}
|
||||
size_t line_length = strlen(line);
|
||||
const char *const *candidates = strchr(line, ' ') == NULL
|
||||
@@ -215,33 +217,56 @@ bool console_completion_expand(const char *line, char *completed, size_t capacit
|
||||
? sizeof(s_root_candidates) / sizeof(s_root_candidates[0])
|
||||
: sizeof(s_completion_candidates) /
|
||||
sizeof(s_completion_candidates[0]);
|
||||
const char *first = NULL;
|
||||
size_t common_length = 0U;
|
||||
for (size_t index = 0U; index < candidate_count; ++index) {
|
||||
const char *candidate = candidates[index];
|
||||
if (strncmp(candidate, line, line_length) != 0) {
|
||||
continue;
|
||||
if (strlen(candidate) > line_length &&
|
||||
strncmp(candidate, line, line_length) == 0 &&
|
||||
!visitor(candidate, context)) {
|
||||
return;
|
||||
}
|
||||
if (first == NULL) {
|
||||
first = candidate;
|
||||
common_length = strlen(candidate);
|
||||
continue;
|
||||
}
|
||||
size_t candidate_length = strlen(candidate);
|
||||
if (common_length > candidate_length) {
|
||||
common_length = candidate_length;
|
||||
}
|
||||
size_t offset = line_length;
|
||||
while (offset < common_length && first[offset] == candidate[offset]) {
|
||||
++offset;
|
||||
}
|
||||
common_length = offset;
|
||||
}
|
||||
if (first == NULL || common_length <= line_length || common_length >= capacity) {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
memcpy(completed, first, common_length);
|
||||
completed[common_length] = '\0';
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -261,27 +286,15 @@ static ssize_t console_read_with_late_terminal_upgrade(int file_descriptor,
|
||||
return received;
|
||||
}
|
||||
|
||||
static bool add_linenoise_completion(const char *candidate, void *context)
|
||||
{
|
||||
linenoiseAddCompletion(context, candidate);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void console_completion_callback(const char *buffer, linenoiseCompletions *completions)
|
||||
{
|
||||
/* Preserve ESP-IDF completion for registered root command names. */
|
||||
if (strchr(buffer, ' ') == NULL) {
|
||||
esp_console_get_completion(buffer, completions);
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t buffer_length = strlen(buffer);
|
||||
for (size_t index = 0;
|
||||
index < sizeof(s_completion_candidates) / sizeof(s_completion_candidates[0]);
|
||||
++index) {
|
||||
const char *const candidate = s_completion_candidates[index];
|
||||
const size_t candidate_length = strlen(candidate);
|
||||
|
||||
/* linenoise expects the complete replacement line, not only its suffix. */
|
||||
if (candidate_length > buffer_length &&
|
||||
strncmp(candidate, buffer, buffer_length) == 0) {
|
||||
linenoiseAddCompletion(completions, candidate);
|
||||
}
|
||||
}
|
||||
console_completion_visit(buffer, add_linenoise_completion, completions);
|
||||
}
|
||||
|
||||
void console_completion_install(void)
|
||||
|
||||
@@ -12,6 +12,13 @@ extern "C" {
|
||||
/* Install late-terminal upgrade handling and project-specific completion. */
|
||||
void console_completion_install(void);
|
||||
|
||||
typedef bool (*console_completion_visitor_t)(const char *candidate, void *context);
|
||||
|
||||
/* Visit the same matching hint candidates used by both UART0 and admin SSH. */
|
||||
void console_completion_visit(const char *line,
|
||||
console_completion_visitor_t visitor,
|
||||
void *context);
|
||||
|
||||
/* Bounded longest-prefix completion shared by the UART and admin SSH frontends. */
|
||||
bool console_completion_expand(const char *line, char *completed, size_t capacity);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user