mirror of
https://github.com/flipperdevices/flipperzero-firmware.git
synced 2025-12-12 20:59:50 +04:00
[FL-3965] Separate cli_shell into toolbox (#4175)
* cli_shell: separate into toolbox * fix: cmd flags * fix formatting * cli: increase default stack depth * cli_shell: fix loader lock logic * cli: fix command flags * fix f18 * speaker_debug: fix * cli_registry: fix docs * ufbt: rename cli target back * cli: rename app and record * cli: fix and simplify help command * cli_master_shell: fix ext commands * fix formatting * cli: rename master to main * fix formatting --------- Co-authored-by: hedger <hedger@users.noreply.github.com>
This commit is contained in:
366
lib/toolbox/cli/shell/cli_shell_completions.c
Normal file
366
lib/toolbox/cli/shell/cli_shell_completions.c
Normal file
@@ -0,0 +1,366 @@
|
||||
#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);
|
||||
CliCommandTree_t* commands = cli_registry_get_commands(registry);
|
||||
for
|
||||
M_EACH(registered_command, *commands, CliCommandTree_t) {
|
||||
FuriString* command_name = *registered_command->key_ptr;
|
||||
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},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user