mirror of
https://github.com/DarkFlippers/unleashed-firmware.git
synced 2025-12-12 12:42:30 +04:00
[FL-3956] CLI autocomplete and other sugar (#4115)
* feat: FuriThread stdin * ci: fix f18 * feat: stdio callback context * feat: FuriPipe * POTENTIALLY EXPLOSIVE pipe welding * fix: non-explosive welding * Revert welding * docs: furi_pipe * feat: pipe event loop integration * update f18 sdk * f18 * docs: make doxygen happy * fix: event loop not triggering when pipe attached to stdio * fix: partial stdout in pipe * allow simultaneous in and out subscription in event loop * feat: vcp i/o * feat: cli ansi stuffs and history * feat: more line editing * working but slow cli rewrite * restore previous speed after 4 days of debugging 🥲 * fix: cli_app_should_stop * fix: cli and event_loop memory leaks * style: remove commented out code * ci: fix pvs warnings * fix: unit tests, event_loop crash * ci: fix build * ci: silence pvs warning * feat: cli gpio * ci: fix formatting * Fix memory leak during event loop unsubscription * Event better memory leak fix * feat: cli completions * Merge remote-tracking branch 'origin/dev' into portasynthinca3/3928-cli-threads * merge fixups * temporarily exclude speaker_debug app * pvs and unit tests fixups * feat: commands in fals * move commands out of flash, code cleanup * ci: fix errors * fix: run commands in buffer when stopping session * speedup cli file transfer * fix f18 * separate cli_shell into modules * fix pvs warning * fix qflipper refusing to connect * remove temp debug logs * remove erroneous conclusion * Fix memory leak during event loop unsubscription * Event better memory leak fix * unit test for the fix * improve thread stdio callback signatures * pipe stdout timeout * update api symbols * fix f18, formatting * fix pvs warnings * increase stack size, hope to fix unit tests * cli completions * more key combos * cli: revert flag changes * cli: fix formatting * cli, fbt: loopback perf benchmark * thread, event_loop: subscribing to thread flags * cli: signal internal events using thread flags, improve performance * fix f18, formatting * event_loop: fix crash * storage_cli: increase write_chunk buffer size again * cli: explanation for order=0 * thread, event_loop: thread flags callback refactor * cli: increase stack size * cli: rename cli_app_should_stop -> cli_is_pipe_broken_or_is_etx_next_char * cli: use plain array instead of mlib for history * cli: prepend file name to static fns * cli: fix formatting * cli_shell: increase stack size * fix merge * fix merge * cli_shell: fix autocomplete up/down logic * cli_shell: don't add empty line to history * cli_shell: fix history recall * cli_shell: find manually typed command in history * cli_shell: different up/down completions navigation * fix formatting * cli_shell: fix memory leak * cli_shell: silence pvs warning * test_pipe: fix race condition * storage_cli: terminate on pipe broken --------- Co-authored-by: Georgii Surkov <georgii.surkov@outlook.com> Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
@@ -70,10 +70,9 @@ static void on_data_arrived(PipeSide* pipe, void* context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void on_space_freed(PipeSide* pipe, void* context) {
|
static void on_space_freed(PipeSide* pipe, void* context) {
|
||||||
|
UNUSED(pipe);
|
||||||
AncillaryThreadContext* ctx = context;
|
AncillaryThreadContext* ctx = context;
|
||||||
ctx->flag |= TestFlagSpaceFreed;
|
ctx->flag |= TestFlagSpaceFreed;
|
||||||
const char* message = "Hi!";
|
|
||||||
pipe_send(pipe, message, strlen(message));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void on_became_broken(PipeSide* pipe, void* context) {
|
static void on_became_broken(PipeSide* pipe, void* context) {
|
||||||
@@ -119,16 +118,10 @@ MU_TEST(pipe_test_event_loop) {
|
|||||||
size_t size = pipe_receive(alice, buffer_1, strlen(message));
|
size_t size = pipe_receive(alice, buffer_1, strlen(message));
|
||||||
buffer_1[size] = 0;
|
buffer_1[size] = 0;
|
||||||
|
|
||||||
char buffer_2[16];
|
|
||||||
const char* expected_reply = "Hi!";
|
|
||||||
size = pipe_receive(alice, buffer_2, strlen(expected_reply));
|
|
||||||
buffer_2[size] = 0;
|
|
||||||
|
|
||||||
pipe_free(alice);
|
pipe_free(alice);
|
||||||
furi_thread_join(thread);
|
furi_thread_join(thread);
|
||||||
|
|
||||||
mu_assert_string_eq(message, buffer_1);
|
mu_assert_string_eq(message, buffer_1);
|
||||||
mu_assert_string_eq(expected_reply, buffer_2);
|
|
||||||
mu_assert_int_eq(
|
mu_assert_int_eq(
|
||||||
TestFlagDataArrived | TestFlagSpaceFreed | TestFlagBecameBroken,
|
TestFlagDataArrived | TestFlagSpaceFreed | TestFlagBecameBroken,
|
||||||
furi_thread_get_return_code(thread));
|
furi_thread_get_return_code(thread));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ App(
|
|||||||
sources=[
|
sources=[
|
||||||
"cli.c",
|
"cli.c",
|
||||||
"shell/cli_shell.c",
|
"shell/cli_shell.c",
|
||||||
|
"shell/cli_shell_completions.c",
|
||||||
"shell/cli_shell_line.c",
|
"shell/cli_shell_line.c",
|
||||||
"cli_commands.c",
|
"cli_commands.c",
|
||||||
"cli_command_gpio.c",
|
"cli_command_gpio.c",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "../cli_i.h"
|
#include "../cli_i.h"
|
||||||
#include "../cli_commands.h"
|
#include "../cli_commands.h"
|
||||||
#include "cli_shell_line.h"
|
#include "cli_shell_line.h"
|
||||||
|
#include "cli_shell_completions.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <furi_hal_version.h>
|
#include <furi_hal_version.h>
|
||||||
#include <m-array.h>
|
#include <m-array.h>
|
||||||
@@ -17,11 +18,13 @@
|
|||||||
#define ANSI_TIMEOUT_MS 10
|
#define ANSI_TIMEOUT_MS 10
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
CliShellComponentCompletions,
|
||||||
CliShellComponentLine,
|
CliShellComponentLine,
|
||||||
CliShellComponentMAX, //<! do not use
|
CliShellComponentMAX, //<! do not use
|
||||||
} CliShellComponent;
|
} CliShellComponent;
|
||||||
|
|
||||||
CliShellKeyComboSet* component_key_combo_sets[] = {
|
CliShellKeyComboSet* component_key_combo_sets[] = {
|
||||||
|
[CliShellComponentCompletions] = &cli_shell_completions_key_combo_set,
|
||||||
[CliShellComponentLine] = &cli_shell_line_key_combo_set,
|
[CliShellComponentLine] = &cli_shell_line_key_combo_set,
|
||||||
};
|
};
|
||||||
static_assert(CliShellComponentMAX == COUNT_OF(component_key_combo_sets));
|
static_assert(CliShellComponentMAX == COUNT_OF(component_key_combo_sets));
|
||||||
@@ -158,6 +161,8 @@ static CliShell* cli_shell_alloc(PipeSide* pipe) {
|
|||||||
pipe_install_as_stdio(cli_shell->pipe);
|
pipe_install_as_stdio(cli_shell->pipe);
|
||||||
|
|
||||||
cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell);
|
cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell);
|
||||||
|
cli_shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc(
|
||||||
|
cli_shell->cli, cli_shell, cli_shell->components[CliShellComponentLine]);
|
||||||
|
|
||||||
cli_shell->event_loop = furi_event_loop_alloc();
|
cli_shell->event_loop = furi_event_loop_alloc();
|
||||||
cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc(
|
cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc(
|
||||||
@@ -172,6 +177,7 @@ static CliShell* cli_shell_alloc(PipeSide* pipe) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void cli_shell_free(CliShell* cli_shell) {
|
static void cli_shell_free(CliShell* cli_shell) {
|
||||||
|
cli_shell_completions_free(cli_shell->components[CliShellComponentCompletions]);
|
||||||
cli_shell_line_free(cli_shell->components[CliShellComponentLine]);
|
cli_shell_line_free(cli_shell->components[CliShellComponentLine]);
|
||||||
|
|
||||||
pipe_detach_from_event_loop(cli_shell->pipe);
|
pipe_detach_from_event_loop(cli_shell->pipe);
|
||||||
|
|||||||
362
applications/services/cli/shell/cli_shell_completions.c
Normal file
362
applications/services/cli/shell/cli_shell_completions.c
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
#include "cli_shell_completions.h"
|
||||||
|
|
||||||
|
ARRAY_DEF(CommandCompletions, FuriString*, FURI_STRING_OPLIST); // -V524
|
||||||
|
#define M_OPL_CommandCompletions_t() ARRAY_OPLIST(CommandCompletions)
|
||||||
|
|
||||||
|
struct CliShellCompletions {
|
||||||
|
Cli* cli;
|
||||||
|
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(Cli* cli, CliShell* shell, CliShellLine* line) {
|
||||||
|
CliShellCompletions* completions = malloc(sizeof(CliShellCompletions));
|
||||||
|
|
||||||
|
completions->cli = cli;
|
||||||
|
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) {
|
||||||
|
CliCommandTree_t* commands = cli_get_commands(completions->cli);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} 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},
|
||||||
|
},
|
||||||
|
};
|
||||||
24
applications/services/cli/shell/cli_shell_completions.h
Normal file
24
applications/services/cli/shell/cli_shell_completions.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <furi.h>
|
||||||
|
#include <m-array.h>
|
||||||
|
#include "cli_shell_i.h"
|
||||||
|
#include "cli_shell_line.h"
|
||||||
|
#include "../cli.h"
|
||||||
|
#include "../cli_i.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct CliShellCompletions CliShellCompletions;
|
||||||
|
|
||||||
|
CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line);
|
||||||
|
|
||||||
|
void cli_shell_completions_free(CliShellCompletions* completions);
|
||||||
|
|
||||||
|
extern CliShellKeyComboSet cli_shell_completions_key_combo_set;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -65,6 +65,64 @@ void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t cli_shell_line_get_line_position(CliShellLine* line) {
|
||||||
|
return line->line_position;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cli_shell_line_set_line_position(CliShellLine* line, size_t position) {
|
||||||
|
line->line_position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =======
|
||||||
|
// Helpers
|
||||||
|
// =======
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CliCharClassWord,
|
||||||
|
CliCharClassSpace,
|
||||||
|
CliCharClassOther,
|
||||||
|
} CliCharClass;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CliSkipDirectionLeft,
|
||||||
|
CliSkipDirectionRight,
|
||||||
|
} CliSkipDirection;
|
||||||
|
|
||||||
|
CliCharClass cli_shell_line_char_class(char c) {
|
||||||
|
if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') {
|
||||||
|
return CliCharClassWord;
|
||||||
|
} else if(c == ' ') {
|
||||||
|
return CliCharClassSpace;
|
||||||
|
} else {
|
||||||
|
return CliCharClassOther;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t
|
||||||
|
cli_shell_line_skip_run(FuriString* string, size_t original_pos, CliSkipDirection direction) {
|
||||||
|
if(furi_string_size(string) == 0) return original_pos;
|
||||||
|
if(direction == CliSkipDirectionLeft && original_pos == 0) return original_pos;
|
||||||
|
if(direction == CliSkipDirectionRight && original_pos == furi_string_size(string))
|
||||||
|
return original_pos;
|
||||||
|
|
||||||
|
int8_t look_offset = (direction == CliSkipDirectionLeft) ? -1 : 0;
|
||||||
|
int8_t increment = (direction == CliSkipDirectionLeft) ? -1 : 1;
|
||||||
|
int32_t position = original_pos;
|
||||||
|
CliCharClass start_class =
|
||||||
|
cli_shell_line_char_class(furi_string_get_char(string, position + look_offset));
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
position += increment;
|
||||||
|
if(position < 0) break;
|
||||||
|
if(position >= (int32_t)furi_string_size(string)) break;
|
||||||
|
if(cli_shell_line_char_class(furi_string_get_char(string, position + look_offset)) !=
|
||||||
|
start_class)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MAX(0, position);
|
||||||
|
}
|
||||||
|
|
||||||
// ==============
|
// ==============
|
||||||
// Input handlers
|
// Input handlers
|
||||||
// ==============
|
// ==============
|
||||||
@@ -87,13 +145,32 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) {
|
|||||||
|
|
||||||
FuriString* command = cli_shell_line_get_selected(line);
|
FuriString* command = cli_shell_line_get_selected(line);
|
||||||
furi_string_trim(command);
|
furi_string_trim(command);
|
||||||
|
if(furi_string_empty(command)) {
|
||||||
|
cli_shell_line_prompt(line);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
FuriString* command_copy = furi_string_alloc_set(command);
|
FuriString* command_copy = furi_string_alloc_set(command);
|
||||||
|
|
||||||
if(line->history_position > 0) {
|
if(line->history_position == 0) {
|
||||||
|
for(size_t i = 1; i < line->history_entries; i++) {
|
||||||
|
if(furi_string_cmp(line->history[i], command) == 0) {
|
||||||
|
line->history_position = i;
|
||||||
|
command = cli_shell_line_get_selected(line);
|
||||||
|
furi_string_trim(command);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// move selected command to the front
|
// move selected command to the front
|
||||||
|
if(line->history_position > 0) {
|
||||||
|
size_t pos = line->history_position;
|
||||||
|
size_t len = line->history_entries;
|
||||||
memmove(
|
memmove(
|
||||||
&line->history[1], &line->history[0], line->history_position * sizeof(FuriString*));
|
&line->history[pos], &line->history[pos + 1], (len - pos - 1) * sizeof(FuriString*));
|
||||||
line->history[0] = command;
|
furi_string_move(line->history[0], command);
|
||||||
|
line->history_entries--;
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert empty command
|
// insert empty command
|
||||||
@@ -109,7 +186,7 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) {
|
|||||||
|
|
||||||
// execute command
|
// execute command
|
||||||
printf("\r\n");
|
printf("\r\n");
|
||||||
if(!furi_string_empty(command_copy)) cli_shell_execute_command(line->shell, command_copy);
|
cli_shell_execute_command(line->shell, command_copy);
|
||||||
furi_string_free(command_copy);
|
furi_string_free(command_copy);
|
||||||
|
|
||||||
cli_shell_line_prompt(line);
|
cli_shell_line_prompt(line);
|
||||||
@@ -199,7 +276,57 @@ static bool cli_shell_line_input_bksp(CliKeyCombo combo, void* context) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool cli_shell_line_input_fallback(CliKeyCombo combo, void* context) {
|
static bool cli_shell_line_input_ctrl_l(CliKeyCombo combo, void* context) {
|
||||||
|
UNUSED(combo);
|
||||||
|
CliShellLine* line = context;
|
||||||
|
// clear screen
|
||||||
|
FuriString* command = cli_shell_line_get_selected(line);
|
||||||
|
char prompt[64];
|
||||||
|
cli_shell_line_format_prompt(line, prompt, sizeof(prompt));
|
||||||
|
printf(
|
||||||
|
ANSI_ERASE_DISPLAY(ANSI_ERASE_ENTIRE) ANSI_ERASE_SCROLLBACK_BUFFER ANSI_CURSOR_POS(
|
||||||
|
"1", "1") "%s%s" ANSI_CURSOR_HOR_POS("%zu"),
|
||||||
|
prompt,
|
||||||
|
furi_string_get_cstr(command),
|
||||||
|
strlen(prompt) + line->line_position + 1 /* 1-based column indexing */);
|
||||||
|
fflush(stdout);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool cli_shell_line_input_ctrl_left_right(CliKeyCombo combo, void* context) {
|
||||||
|
CliShellLine* line = context;
|
||||||
|
// skip run of similar chars to the left or right
|
||||||
|
FuriString* selected_line = cli_shell_line_get_selected(line);
|
||||||
|
CliSkipDirection direction = (combo.key == CliKeyLeft) ? CliSkipDirectionLeft :
|
||||||
|
CliSkipDirectionRight;
|
||||||
|
line->line_position = cli_shell_line_skip_run(selected_line, line->line_position, direction);
|
||||||
|
printf(
|
||||||
|
ANSI_CURSOR_HOR_POS("%zu"), cli_shell_line_prompt_length(line) + line->line_position + 1);
|
||||||
|
fflush(stdout);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool cli_shell_line_input_ctrl_bksp(CliKeyCombo combo, void* context) {
|
||||||
|
UNUSED(combo);
|
||||||
|
CliShellLine* line = context;
|
||||||
|
// delete run of similar chars to the left
|
||||||
|
cli_shell_line_ensure_not_overwriting_history(line);
|
||||||
|
FuriString* selected_line = cli_shell_line_get_selected(line);
|
||||||
|
size_t run_start =
|
||||||
|
cli_shell_line_skip_run(selected_line, line->line_position, CliSkipDirectionLeft);
|
||||||
|
furi_string_replace_at(selected_line, run_start, line->line_position - run_start, "");
|
||||||
|
line->line_position = run_start;
|
||||||
|
printf(
|
||||||
|
ANSI_CURSOR_HOR_POS("%zu") "%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END)
|
||||||
|
ANSI_CURSOR_HOR_POS("%zu"),
|
||||||
|
cli_shell_line_prompt_length(line) + line->line_position + 1,
|
||||||
|
furi_string_get_cstr(selected_line) + run_start,
|
||||||
|
cli_shell_line_prompt_length(line) + run_start + 1);
|
||||||
|
fflush(stdout);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool cli_shell_line_input_normal(CliKeyCombo combo, void* context) {
|
||||||
CliShellLine* line = context;
|
CliShellLine* line = context;
|
||||||
if(combo.modifiers != CliModKeyNo) return false;
|
if(combo.modifiers != CliModKeyNo) return false;
|
||||||
if(combo.key < CliKeySpace || combo.key >= CliKeyDEL) return false;
|
if(combo.key < CliKeySpace || combo.key >= CliKeyDEL) return false;
|
||||||
@@ -220,8 +347,8 @@ static bool cli_shell_line_input_fallback(CliKeyCombo combo, void* context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CliShellKeyComboSet cli_shell_line_key_combo_set = {
|
CliShellKeyComboSet cli_shell_line_key_combo_set = {
|
||||||
.fallback = cli_shell_line_input_fallback,
|
.fallback = cli_shell_line_input_normal,
|
||||||
.count = 10,
|
.count = 14,
|
||||||
.records =
|
.records =
|
||||||
{
|
{
|
||||||
{{CliModKeyNo, CliKeyETX}, cli_shell_line_input_ctrl_c},
|
{{CliModKeyNo, CliKeyETX}, cli_shell_line_input_ctrl_c},
|
||||||
@@ -234,5 +361,9 @@ CliShellKeyComboSet cli_shell_line_key_combo_set = {
|
|||||||
{{CliModKeyNo, CliKeyEnd}, cli_shell_line_input_end},
|
{{CliModKeyNo, CliKeyEnd}, cli_shell_line_input_end},
|
||||||
{{CliModKeyNo, CliKeyBackspace}, cli_shell_line_input_bksp},
|
{{CliModKeyNo, CliKeyBackspace}, cli_shell_line_input_bksp},
|
||||||
{{CliModKeyNo, CliKeyDEL}, cli_shell_line_input_bksp},
|
{{CliModKeyNo, CliKeyDEL}, cli_shell_line_input_bksp},
|
||||||
|
{{CliModKeyNo, CliKeyFF}, cli_shell_line_input_ctrl_l},
|
||||||
|
{{CliModKeyCtrl, CliKeyLeft}, cli_shell_line_input_ctrl_left_right},
|
||||||
|
{{CliModKeyCtrl, CliKeyRight}, cli_shell_line_input_ctrl_left_right},
|
||||||
|
{{CliModKeyNo, CliKeyETB}, cli_shell_line_input_ctrl_bksp},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ void cli_shell_line_format_prompt(CliShellLine* line, char* buf, size_t length);
|
|||||||
|
|
||||||
void cli_shell_line_prompt(CliShellLine* line);
|
void cli_shell_line_prompt(CliShellLine* line);
|
||||||
|
|
||||||
|
size_t cli_shell_line_get_line_position(CliShellLine* line);
|
||||||
|
|
||||||
|
void cli_shell_line_set_line_position(CliShellLine* line, size_t position);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief If a line from history has been selected, moves it into the active line
|
* @brief If a line from history has been selected, moves it into the active line
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -321,9 +321,11 @@ static void storage_cli_write_chunk(PipeSide* pipe, FuriString* path, FuriString
|
|||||||
uint8_t* buffer = malloc(buffer_size);
|
uint8_t* buffer = malloc(buffer_size);
|
||||||
|
|
||||||
while(need_to_read) {
|
while(need_to_read) {
|
||||||
size_t read_this_time = pipe_receive(pipe, buffer, MIN(buffer_size, need_to_read));
|
size_t to_read_this_time = MIN(buffer_size, need_to_read);
|
||||||
size_t wrote_this_time = storage_file_write(file, buffer, read_this_time);
|
size_t read_this_time = pipe_receive(pipe, buffer, to_read_this_time);
|
||||||
|
if(read_this_time != to_read_this_time) break;
|
||||||
|
|
||||||
|
size_t wrote_this_time = storage_file_write(file, buffer, read_this_time);
|
||||||
if(wrote_this_time != read_this_time) {
|
if(wrote_this_time != read_this_time) {
|
||||||
storage_cli_print_error(storage_file_get_error(file));
|
storage_cli_print_error(storage_file_get_error(file));
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user