mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2025-10-05 11:58:44 +02:00
* cli_vcp: handle tx/rx before connext/disconnect * cli_vcp: disable trace * cli_perf: advanced error reporting * cli_vcp: reset tx flag directly in event handler * fix formatting * cli_vcp: make tx flag volatile * storage_settings: fix scene ids * cli_shell: add safety check to set_prompt * cli_registry: move from bptree to dict, fix memory leak * cli_vcp: go back to message queue for event signaling * loader: move BeforeLoad event even earlier * fix formatting
367 lines
14 KiB
C
367 lines
14 KiB
C
#include "cli_shell_completions.h"
|
|
|
|
ARRAY_DEF(CommandCompletions, FuriString*, FURI_STRING_OPLIST); // -V524
|
|
#define M_OPL_CommandCompletions_t() ARRAY_OPLIST(CommandCompletions)
|
|
|
|
struct CliShellCompletions {
|
|
CliRegistry* registry;
|
|
CliShell* shell;
|
|
CliShellLine* line;
|
|
CommandCompletions_t variants;
|
|
size_t selected;
|
|
bool is_displaying;
|
|
};
|
|
|
|
#define COMPLETION_COLUMNS 3
|
|
#define COMPLETION_COLUMN_WIDTH "30"
|
|
#define COMPLETION_COLUMN_WIDTH_I 30
|
|
|
|
/**
|
|
* @brief Update for the completions menu
|
|
*/
|
|
typedef enum {
|
|
CliShellCompletionsActionOpen,
|
|
CliShellCompletionsActionClose,
|
|
CliShellCompletionsActionUp,
|
|
CliShellCompletionsActionDown,
|
|
CliShellCompletionsActionLeft,
|
|
CliShellCompletionsActionRight,
|
|
CliShellCompletionsActionSelect,
|
|
CliShellCompletionsActionSelectNoClose,
|
|
} CliShellCompletionsAction;
|
|
|
|
typedef enum {
|
|
CliShellCompletionSegmentTypeCommand,
|
|
CliShellCompletionSegmentTypeArguments,
|
|
} CliShellCompletionSegmentType;
|
|
|
|
typedef struct {
|
|
CliShellCompletionSegmentType type;
|
|
size_t start;
|
|
size_t length;
|
|
} CliShellCompletionSegment;
|
|
|
|
// ==========
|
|
// Public API
|
|
// ==========
|
|
|
|
CliShellCompletions*
|
|
cli_shell_completions_alloc(CliRegistry* registry, CliShell* shell, CliShellLine* line) {
|
|
CliShellCompletions* completions = malloc(sizeof(CliShellCompletions));
|
|
|
|
completions->registry = registry;
|
|
completions->shell = shell;
|
|
completions->line = line;
|
|
CommandCompletions_init(completions->variants);
|
|
|
|
return completions;
|
|
}
|
|
|
|
void cli_shell_completions_free(CliShellCompletions* completions) {
|
|
CommandCompletions_clear(completions->variants);
|
|
free(completions);
|
|
}
|
|
|
|
// =======
|
|
// Helpers
|
|
// =======
|
|
|
|
CliShellCompletionSegment cli_shell_completions_segment(CliShellCompletions* completions) {
|
|
furi_assert(completions);
|
|
CliShellCompletionSegment segment;
|
|
|
|
FuriString* input = furi_string_alloc_set(cli_shell_line_get_editing(completions->line));
|
|
furi_string_left(input, cli_shell_line_get_line_position(completions->line));
|
|
|
|
// find index of first non-space character
|
|
size_t first_non_space = 0;
|
|
while(1) {
|
|
size_t ret = furi_string_search_char(input, ' ', first_non_space);
|
|
if(ret == FURI_STRING_FAILURE) break;
|
|
if(ret - first_non_space > 1) break;
|
|
first_non_space++;
|
|
}
|
|
|
|
size_t first_space_in_command = furi_string_search_char(input, ' ', first_non_space);
|
|
|
|
if(first_space_in_command == FURI_STRING_FAILURE) {
|
|
segment.type = CliShellCompletionSegmentTypeCommand;
|
|
segment.start = first_non_space;
|
|
segment.length = furi_string_size(input) - first_non_space;
|
|
} else {
|
|
segment.type = CliShellCompletionSegmentTypeArguments;
|
|
segment.start = 0;
|
|
segment.length = 0;
|
|
// support removed, might reimplement in the future
|
|
}
|
|
|
|
furi_string_free(input);
|
|
return segment;
|
|
}
|
|
|
|
void cli_shell_completions_fill_variants(CliShellCompletions* completions) {
|
|
furi_assert(completions);
|
|
CommandCompletions_reset(completions->variants);
|
|
|
|
CliShellCompletionSegment segment = cli_shell_completions_segment(completions);
|
|
FuriString* input = furi_string_alloc_set(cli_shell_line_get_editing(completions->line));
|
|
furi_string_right(input, segment.start);
|
|
furi_string_left(input, segment.length);
|
|
|
|
if(segment.type == CliShellCompletionSegmentTypeCommand) {
|
|
CliRegistry* registry = completions->registry;
|
|
cli_registry_lock(registry);
|
|
CliCommandDict_t* commands = cli_registry_get_commands(registry);
|
|
for
|
|
M_EACH(registered_command, *commands, CliCommandDict_t) {
|
|
FuriString* command_name = registered_command->key;
|
|
if(furi_string_start_with(command_name, input)) {
|
|
CommandCompletions_push_back(completions->variants, command_name);
|
|
}
|
|
}
|
|
cli_registry_unlock(registry);
|
|
|
|
} else {
|
|
// support removed, might reimplement in the future
|
|
}
|
|
|
|
furi_string_free(input);
|
|
}
|
|
|
|
static size_t cli_shell_completions_rows_at_column(CliShellCompletions* completions, size_t x) {
|
|
size_t completions_size = CommandCompletions_size(completions->variants);
|
|
size_t n_full_rows = completions_size / COMPLETION_COLUMNS;
|
|
size_t n_cols_in_last_row = completions_size % COMPLETION_COLUMNS;
|
|
size_t n_rows_at_x = n_full_rows + ((x >= n_cols_in_last_row) ? 0 : 1);
|
|
return n_rows_at_x;
|
|
}
|
|
|
|
void cli_shell_completions_render(
|
|
CliShellCompletions* completions,
|
|
CliShellCompletionsAction action) {
|
|
furi_assert(completions);
|
|
if(action == CliShellCompletionsActionOpen) furi_check(!completions->is_displaying);
|
|
if(action == CliShellCompletionsActionClose) furi_check(completions->is_displaying);
|
|
|
|
char prompt[64];
|
|
cli_shell_line_format_prompt(completions->line, prompt, sizeof(prompt));
|
|
|
|
if(action == CliShellCompletionsActionOpen) {
|
|
cli_shell_completions_fill_variants(completions);
|
|
completions->selected = 0;
|
|
|
|
if(CommandCompletions_size(completions->variants) == 1) {
|
|
cli_shell_completions_render(completions, CliShellCompletionsActionSelectNoClose);
|
|
return;
|
|
}
|
|
|
|
// show completions menu (full re-render)
|
|
printf("\n\r");
|
|
size_t position = 0;
|
|
for
|
|
M_EACH(completion, completions->variants, CommandCompletions_t) {
|
|
if(position == completions->selected) printf(ANSI_INVERT);
|
|
printf("%-" COMPLETION_COLUMN_WIDTH "s", furi_string_get_cstr(*completion));
|
|
if(position == completions->selected) printf(ANSI_RESET);
|
|
if((position % COMPLETION_COLUMNS == COMPLETION_COLUMNS - 1) &&
|
|
position != CommandCompletions_size(completions->variants)) {
|
|
printf("\r\n");
|
|
}
|
|
position++;
|
|
}
|
|
|
|
if(!position) {
|
|
printf(ANSI_FG_RED "no completions" ANSI_RESET);
|
|
}
|
|
|
|
size_t total_rows = (position / COMPLETION_COLUMNS) + 1;
|
|
printf(
|
|
ANSI_ERASE_DISPLAY(ANSI_ERASE_FROM_CURSOR_TO_END) ANSI_CURSOR_UP_BY("%zu")
|
|
ANSI_CURSOR_HOR_POS("%zu"),
|
|
total_rows,
|
|
strlen(prompt) + cli_shell_line_get_line_position(completions->line) + 1);
|
|
|
|
completions->is_displaying = true;
|
|
|
|
} else if(action == CliShellCompletionsActionClose) {
|
|
// clear completions menu
|
|
printf(
|
|
ANSI_CURSOR_HOR_POS("%zu") ANSI_ERASE_DISPLAY(ANSI_ERASE_FROM_CURSOR_TO_END)
|
|
ANSI_CURSOR_HOR_POS("%zu"),
|
|
strlen(prompt) + furi_string_size(cli_shell_line_get_selected(completions->line)) + 1,
|
|
strlen(prompt) + cli_shell_line_get_line_position(completions->line) + 1);
|
|
completions->is_displaying = false;
|
|
|
|
} else if(
|
|
action == CliShellCompletionsActionUp || action == CliShellCompletionsActionDown ||
|
|
action == CliShellCompletionsActionLeft || action == CliShellCompletionsActionRight) {
|
|
if(CommandCompletions_empty_p(completions->variants)) return;
|
|
|
|
// move selection
|
|
size_t completions_size = CommandCompletions_size(completions->variants);
|
|
size_t old_selection = completions->selected;
|
|
int n_columns = (completions_size >= COMPLETION_COLUMNS) ? COMPLETION_COLUMNS :
|
|
completions_size;
|
|
int selection_unclamped = old_selection;
|
|
if(action == CliShellCompletionsActionLeft) {
|
|
selection_unclamped--;
|
|
} else if(action == CliShellCompletionsActionRight) {
|
|
selection_unclamped++;
|
|
} else {
|
|
int selection_x = old_selection % COMPLETION_COLUMNS;
|
|
int selection_y_unclamped = old_selection / COMPLETION_COLUMNS;
|
|
if(action == CliShellCompletionsActionUp) selection_y_unclamped--;
|
|
if(action == CliShellCompletionsActionDown) selection_y_unclamped++;
|
|
size_t selection_y = 0;
|
|
if(selection_y_unclamped < 0) {
|
|
selection_x = CLAMP_WRAPAROUND(selection_x - 1, n_columns - 1, 0);
|
|
selection_y =
|
|
cli_shell_completions_rows_at_column(completions, selection_x) - 1; // -V537
|
|
} else if(
|
|
(size_t)selection_y_unclamped >
|
|
cli_shell_completions_rows_at_column(completions, selection_x) - 1) {
|
|
selection_x = CLAMP_WRAPAROUND(selection_x + 1, n_columns - 1, 0);
|
|
selection_y = 0;
|
|
} else {
|
|
selection_y = selection_y_unclamped;
|
|
}
|
|
selection_unclamped = (selection_y * COMPLETION_COLUMNS) + selection_x;
|
|
}
|
|
size_t new_selection = CLAMP_WRAPAROUND(selection_unclamped, (int)completions_size - 1, 0);
|
|
completions->selected = new_selection;
|
|
|
|
if(new_selection != old_selection) {
|
|
// determine selection coordinates relative to top-left of suggestion menu
|
|
size_t old_x = (old_selection % COMPLETION_COLUMNS) * COMPLETION_COLUMN_WIDTH_I;
|
|
size_t old_y = old_selection / COMPLETION_COLUMNS;
|
|
size_t new_x = (new_selection % COMPLETION_COLUMNS) * COMPLETION_COLUMN_WIDTH_I;
|
|
size_t new_y = new_selection / COMPLETION_COLUMNS;
|
|
printf("\n\r");
|
|
|
|
// print old selection in normal colors
|
|
if(old_y) printf(ANSI_CURSOR_DOWN_BY("%zu"), old_y);
|
|
printf(ANSI_CURSOR_HOR_POS("%zu"), old_x + 1);
|
|
printf(
|
|
"%-" COMPLETION_COLUMN_WIDTH "s",
|
|
furi_string_get_cstr(
|
|
*CommandCompletions_cget(completions->variants, old_selection)));
|
|
if(old_y) printf(ANSI_CURSOR_UP_BY("%zu"), old_y);
|
|
printf(ANSI_CURSOR_HOR_POS("1"));
|
|
|
|
// print new selection in inverted colors
|
|
if(new_y) printf(ANSI_CURSOR_DOWN_BY("%zu"), new_y);
|
|
printf(ANSI_CURSOR_HOR_POS("%zu"), new_x + 1);
|
|
printf(
|
|
ANSI_INVERT "%-" COMPLETION_COLUMN_WIDTH "s" ANSI_RESET,
|
|
furi_string_get_cstr(
|
|
*CommandCompletions_cget(completions->variants, new_selection)));
|
|
|
|
// return cursor
|
|
printf(ANSI_CURSOR_UP_BY("%zu"), new_y + 1);
|
|
printf(
|
|
ANSI_CURSOR_HOR_POS("%zu"),
|
|
strlen(prompt) + furi_string_size(cli_shell_line_get_selected(completions->line)) +
|
|
1);
|
|
}
|
|
|
|
} else if(action == CliShellCompletionsActionSelectNoClose) {
|
|
// insert selection into prompt
|
|
CliShellCompletionSegment segment = cli_shell_completions_segment(completions);
|
|
FuriString* input = cli_shell_line_get_selected(completions->line);
|
|
FuriString* completion =
|
|
*CommandCompletions_cget(completions->variants, completions->selected);
|
|
furi_string_replace_at(
|
|
input, segment.start, segment.length, furi_string_get_cstr(completion));
|
|
printf(
|
|
ANSI_CURSOR_HOR_POS("%zu") "%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END),
|
|
strlen(prompt) + 1,
|
|
furi_string_get_cstr(input));
|
|
|
|
int position_change = (int)furi_string_size(completion) - (int)segment.length;
|
|
cli_shell_line_set_line_position(
|
|
completions->line,
|
|
MAX(0, (int)cli_shell_line_get_line_position(completions->line) + position_change));
|
|
|
|
} else if(action == CliShellCompletionsActionSelect) {
|
|
cli_shell_completions_render(completions, CliShellCompletionsActionSelectNoClose);
|
|
cli_shell_completions_render(completions, CliShellCompletionsActionClose);
|
|
|
|
} else {
|
|
furi_crash();
|
|
}
|
|
|
|
fflush(stdout);
|
|
}
|
|
|
|
// ==============
|
|
// Input handlers
|
|
// ==============
|
|
|
|
static bool hide_if_open_and_continue_handling(CliKeyCombo combo, void* context) {
|
|
UNUSED(combo);
|
|
CliShellCompletions* completions = context;
|
|
if(completions->is_displaying)
|
|
cli_shell_completions_render(completions, CliShellCompletionsActionClose);
|
|
return false; // process other home events
|
|
}
|
|
|
|
static bool key_combo_cr(CliKeyCombo combo, void* context) {
|
|
UNUSED(combo);
|
|
CliShellCompletions* completions = context;
|
|
if(!completions->is_displaying) return false;
|
|
cli_shell_completions_render(completions, CliShellCompletionsActionSelect);
|
|
return true;
|
|
}
|
|
|
|
static bool key_combo_up_down(CliKeyCombo combo, void* context) {
|
|
CliShellCompletions* completions = context;
|
|
if(!completions->is_displaying) return false;
|
|
cli_shell_completions_render(
|
|
completions,
|
|
(combo.key == CliKeyUp) ? CliShellCompletionsActionUp : CliShellCompletionsActionDown);
|
|
return true;
|
|
}
|
|
|
|
static bool key_combo_left_right(CliKeyCombo combo, void* context) {
|
|
CliShellCompletions* completions = context;
|
|
if(!completions->is_displaying) return false;
|
|
cli_shell_completions_render(
|
|
completions,
|
|
(combo.key == CliKeyLeft) ? CliShellCompletionsActionLeft :
|
|
CliShellCompletionsActionRight);
|
|
return true;
|
|
}
|
|
|
|
static bool key_combo_tab(CliKeyCombo combo, void* context) {
|
|
UNUSED(combo);
|
|
CliShellCompletions* completions = context;
|
|
cli_shell_completions_render(
|
|
completions,
|
|
completions->is_displaying ? CliShellCompletionsActionRight :
|
|
CliShellCompletionsActionOpen);
|
|
return true;
|
|
}
|
|
|
|
static bool key_combo_esc(CliKeyCombo combo, void* context) {
|
|
UNUSED(combo);
|
|
CliShellCompletions* completions = context;
|
|
if(!completions->is_displaying) return false;
|
|
cli_shell_completions_render(completions, CliShellCompletionsActionClose);
|
|
return true;
|
|
}
|
|
|
|
CliShellKeyComboSet cli_shell_completions_key_combo_set = {
|
|
.fallback = hide_if_open_and_continue_handling,
|
|
.count = 7,
|
|
.records =
|
|
{
|
|
{{CliModKeyNo, CliKeyCR}, key_combo_cr},
|
|
{{CliModKeyNo, CliKeyUp}, key_combo_up_down},
|
|
{{CliModKeyNo, CliKeyDown}, key_combo_up_down},
|
|
{{CliModKeyNo, CliKeyLeft}, key_combo_left_right},
|
|
{{CliModKeyNo, CliKeyRight}, key_combo_left_right},
|
|
{{CliModKeyNo, CliKeyTab}, key_combo_tab},
|
|
{{CliModKeyNo, CliKeyEsc}, key_combo_esc},
|
|
},
|
|
};
|