Merge remote-tracking branch 'ofw/dev' into mntm-dev

This commit is contained in:
Willy-JL
2025-04-04 07:37:27 +01:00
52 changed files with 1170 additions and 606 deletions
+205 -23
View File
@@ -4,6 +4,7 @@
#include "../cli_i.h"
#include "../cli_commands.h"
#include "cli_shell_line.h"
#include "cli_shell_completions.h"
#include <stdio.h>
#include <furi_hal_version.h>
#include <m-array.h>
@@ -11,21 +12,29 @@
#include <toolbox/pipe.h>
#include <flipper_application/plugins/plugin_manager.h>
#include <loader/firmware_api/firmware_api.h>
#include <storage/storage.h>
#define TAG "CliShell"
#define ANSI_TIMEOUT_MS 10
typedef enum {
CliShellComponentCompletions,
CliShellComponentLine,
CliShellComponentMAX, //<! do not use
} CliShellComponent;
CliShellKeyComboSet* component_key_combo_sets[] = {
[CliShellComponentCompletions] = &cli_shell_completions_key_combo_set,
[CliShellComponentLine] = &cli_shell_line_key_combo_set,
};
static_assert(CliShellComponentMAX == COUNT_OF(component_key_combo_sets));
typedef enum {
CliShellStorageEventMount,
CliShellStorageEventUnmount,
} CliShellStorageEvent;
struct CliShell {
Cli* cli;
FuriEventLoop* event_loop;
@@ -34,6 +43,10 @@ struct CliShell {
CliAnsiParser* ansi_parser;
FuriEventLoopTimer* ansi_parsing_timer;
Storage* storage;
FuriPubSubSubscription* storage_subscription;
FuriMessageQueue* storage_event_queue;
void* components[CliShellComponentMAX];
};
@@ -43,10 +56,73 @@ typedef struct {
FuriString* args;
} CliCommandThreadData;
static void cli_shell_data_available(PipeSide* pipe, void* context);
static void cli_shell_pipe_broken(PipeSide* pipe, void* context);
static void cli_shell_install_pipe(CliShell* cli_shell) {
pipe_install_as_stdio(cli_shell->pipe);
pipe_attach_to_event_loop(cli_shell->pipe, cli_shell->event_loop);
pipe_set_callback_context(cli_shell->pipe, cli_shell);
pipe_set_data_arrived_callback(cli_shell->pipe, cli_shell_data_available, 0);
pipe_set_broken_callback(cli_shell->pipe, cli_shell_pipe_broken, 0);
}
static void cli_shell_detach_pipe(CliShell* cli_shell) {
pipe_detach_from_event_loop(cli_shell->pipe);
furi_thread_set_stdin_callback(NULL, NULL);
furi_thread_set_stdout_callback(NULL, NULL);
}
// =========
// Execution
// =========
static int32_t cli_command_thread(void* context) {
CliCommandThreadData* thread_data = context;
if(!(thread_data->command->flags & CliCommandFlagDontAttachStdio))
pipe_install_as_stdio(thread_data->pipe);
thread_data->command->execute_callback(
thread_data->pipe, thread_data->args, thread_data->command->context);
fflush(stdout);
return 0;
}
static size_t cli_shell_string_distance(const char* s1, const char* s2) {
size_t distance = 0;
while(*s1 && *s2) {
if(*s1++ != *s2++) distance++;
}
while(*s1++)
distance++;
while(*s2++)
distance++;
return distance;
}
static void
cli_shell_find_similar_command(CliShell* cli_shell, const char* input, FuriString* suggestion) {
size_t min_distance = (size_t)-1;
const size_t max_allowed = (strlen(input) + 1) / 2;
furi_string_reset(suggestion);
cli_lock_commands(cli_shell->cli);
CliCommandTree_t* commands = cli_get_commands(cli_shell->cli);
for
M_EACH(registered_command, *commands, CliCommandTree_t) {
const char* command_name = furi_string_get_cstr(*registered_command->key_ptr);
const size_t distance = cli_shell_string_distance(input, command_name);
if(distance < min_distance && distance <= max_allowed) {
min_distance = distance;
furi_string_set(suggestion, command_name);
}
}
cli_unlock_commands(cli_shell->cli);
}
void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) {
// split command into command and args
size_t space = furi_string_search_char(command, ' ');
@@ -56,18 +132,59 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) {
FuriString* args = furi_string_alloc_set(command);
furi_string_right(args, space + 1);
PluginManager* plugin_manager = NULL;
Loader* loader = NULL;
CliCommand command_data;
do {
// find handler
if(!cli_get_command(cli_shell->cli, command_name, &command_data)) {
printf(
ANSI_FG_RED "could not find command `%s`, try `help`" ANSI_RESET,
furi_string_get_cstr(command_name));
FuriString* suggestion = furi_string_alloc();
cli_shell_find_similar_command(
cli_shell, furi_string_get_cstr(command_name), suggestion);
if(furi_string_empty(suggestion)) {
printf(
ANSI_FG_RED "could not find command `%s`, try `help`" ANSI_RESET,
furi_string_get_cstr(command_name));
} else {
printf(
ANSI_FG_RED
"could not find command `%s`, did you mean `%s`? Use `help` to list all available commands" ANSI_RESET,
furi_string_get_cstr(command_name),
furi_string_get_cstr(suggestion));
}
furi_string_free(suggestion);
break;
}
// load external command
if(command_data.flags & CliCommandFlagExternal) {
plugin_manager =
plugin_manager_alloc(PLUGIN_APP_ID, PLUGIN_API_VERSION, firmware_api_interface);
FuriString* path = furi_string_alloc_printf(
"%s/cli_%s.fal", CLI_COMMANDS_PATH, furi_string_get_cstr(command_name));
uint32_t plugin_cnt_last = plugin_manager_get_count(plugin_manager);
PluginManagerError error =
plugin_manager_load_single(plugin_manager, furi_string_get_cstr(path));
furi_string_free(path);
if(error != PluginManagerErrorNone) {
printf(ANSI_FG_RED "failed to load external command" ANSI_RESET);
break;
}
const CliCommandDescriptor* plugin =
plugin_manager_get_ep(plugin_manager, plugin_cnt_last);
furi_assert(plugin);
furi_check(furi_string_cmp_str(command_name, plugin->name) == 0);
command_data.execute_callback = plugin->execute_callback;
command_data.flags = plugin->flags | CliCommandFlagExternal;
command_data.stack_depth = plugin->stack_depth;
// external commands have to run in an external thread
furi_check(!(command_data.flags & CliCommandFlagUseShellThread));
}
// lock loader
if(!(command_data.flags & CliCommandFlagParallelSafe)) {
loader = furi_record_open(RECORD_LOADER);
@@ -79,7 +196,27 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) {
}
}
command_data.execute_callback(cli_shell->pipe, args, command_data.context);
if(command_data.flags & CliCommandFlagUseShellThread) {
// run command in this thread
command_data.execute_callback(cli_shell->pipe, args, command_data.context);
} else {
// run command in separate thread
cli_shell_detach_pipe(cli_shell);
CliCommandThreadData thread_data = {
.command = &command_data,
.pipe = cli_shell->pipe,
.args = args,
};
FuriThread* thread = furi_thread_alloc_ex(
furi_string_get_cstr(command_name),
command_data.stack_depth,
cli_command_thread,
&thread_data);
furi_thread_start(thread);
furi_thread_join(thread);
furi_thread_free(thread);
cli_shell_install_pipe(cli_shell);
}
} while(0);
furi_string_free(command_name);
@@ -88,13 +225,51 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) {
// unlock loader
if(loader) loader_unlock(loader);
furi_record_close(RECORD_LOADER);
// unload external command
if(plugin_manager) plugin_manager_free(plugin_manager);
}
// ==============
// Event handlers
// ==============
static void cli_shell_process_key(CliShell* cli_shell, CliKeyCombo key_combo) {
static void cli_shell_storage_event(const void* message, void* context) {
CliShell* cli_shell = context;
const StorageEvent* event = message;
if(event->type == StorageEventTypeCardMount) {
CliShellStorageEvent cli_event = CliShellStorageEventMount;
furi_check(
furi_message_queue_put(cli_shell->storage_event_queue, &cli_event, 0) == FuriStatusOk);
} else if(event->type == StorageEventTypeCardUnmount) {
CliShellStorageEvent cli_event = CliShellStorageEventUnmount;
furi_check(
furi_message_queue_put(cli_shell->storage_event_queue, &cli_event, 0) == FuriStatusOk);
}
}
static void cli_shell_storage_internal_event(FuriEventLoopObject* object, void* context) {
CliShell* cli_shell = context;
FuriMessageQueue* queue = object;
CliShellStorageEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
if(event == CliShellStorageEventMount) {
cli_enumerate_external_commands(cli_shell->cli);
} else if(event == CliShellStorageEventUnmount) {
cli_remove_external_commands(cli_shell->cli);
} else {
furi_crash();
}
}
static void
cli_shell_process_parser_result(CliShell* cli_shell, CliAnsiParserResult parse_result) {
if(!parse_result.is_done) return;
CliKeyCombo key_combo = parse_result.result;
if(key_combo.key == CliKeyUnrecognized) return;
for(size_t i = 0; i < CliShellComponentMAX; i++) { // -V1008
CliShellKeyComboSet* set = component_key_combo_sets[i];
void* component_context = cli_shell->components[i];
@@ -127,22 +302,13 @@ static void cli_shell_data_available(PipeSide* pipe, void* context) {
// process ANSI escape sequences
int c = getchar();
furi_assert(c >= 0);
CliAnsiParserResult parse_result = cli_ansi_parser_feed(cli_shell->ansi_parser, c);
if(!parse_result.is_done) return;
CliKeyCombo key_combo = parse_result.result;
if(key_combo.key == CliKeyUnrecognized) return;
cli_shell_process_key(cli_shell, key_combo);
cli_shell_process_parser_result(cli_shell, cli_ansi_parser_feed(cli_shell->ansi_parser, c));
}
static void cli_shell_timer_expired(void* context) {
CliShell* cli_shell = context;
CliAnsiParserResult parse_result = cli_ansi_parser_feed_timeout(cli_shell->ansi_parser);
if(!parse_result.is_done) return;
CliKeyCombo key_combo = parse_result.result;
if(key_combo.key == CliKeyUnrecognized) return;
cli_shell_process_key(cli_shell, key_combo);
cli_shell_process_parser_result(
cli_shell, cli_ansi_parser_feed_timeout(cli_shell->ansi_parser));
}
// =======
@@ -155,26 +321,42 @@ static CliShell* cli_shell_alloc(PipeSide* pipe) {
cli_shell->cli = furi_record_open(RECORD_CLI);
cli_shell->ansi_parser = cli_ansi_parser_alloc();
cli_shell->pipe = pipe;
pipe_install_as_stdio(cli_shell->pipe);
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->ansi_parsing_timer = furi_event_loop_timer_alloc(
cli_shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, cli_shell);
pipe_attach_to_event_loop(cli_shell->pipe, cli_shell->event_loop);
pipe_set_callback_context(cli_shell->pipe, cli_shell);
pipe_set_data_arrived_callback(cli_shell->pipe, cli_shell_data_available, 0);
pipe_set_broken_callback(cli_shell->pipe, cli_shell_pipe_broken, 0);
cli_shell_install_pipe(cli_shell);
cli_shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent));
furi_event_loop_subscribe_message_queue(
cli_shell->event_loop,
cli_shell->storage_event_queue,
FuriEventLoopEventIn,
cli_shell_storage_internal_event,
cli_shell);
cli_shell->storage = furi_record_open(RECORD_STORAGE);
cli_shell->storage_subscription = furi_pubsub_subscribe(
storage_get_pubsub(cli_shell->storage), cli_shell_storage_event, cli_shell);
return cli_shell;
}
static void cli_shell_free(CliShell* cli_shell) {
furi_pubsub_unsubscribe(
storage_get_pubsub(cli_shell->storage), cli_shell->storage_subscription);
furi_record_close(RECORD_STORAGE);
furi_event_loop_unsubscribe(cli_shell->event_loop, cli_shell->storage_event_queue);
furi_message_queue_free(cli_shell->storage_event_queue);
cli_shell_completions_free(cli_shell->components[CliShellComponentCompletions]);
cli_shell_line_free(cli_shell->components[CliShellComponentLine]);
pipe_detach_from_event_loop(cli_shell->pipe);
cli_shell_detach_pipe(cli_shell);
furi_event_loop_timer_free(cli_shell->ansi_parsing_timer);
furi_event_loop_free(cli_shell->event_loop);
pipe_free(cli_shell->pipe);
@@ -0,0 +1,364 @@
#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) {
cli_lock_commands(completions->cli);
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);
}
}
cli_unlock_commands(completions->cli);
} 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},
},
};
@@ -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
// ==============
@@ -87,13 +145,32 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) {
FuriString* command = cli_shell_line_get_selected(line);
furi_string_trim(command);
if(furi_string_empty(command)) {
cli_shell_line_prompt(line);
return true;
}
FuriString* command_copy = furi_string_alloc_set(command);
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
if(line->history_position > 0) {
// move selected command to the front
size_t pos = line->history_position;
size_t len = line->history_entries;
memmove(
&line->history[1], &line->history[0], line->history_position * sizeof(FuriString*));
line->history[0] = command;
&line->history[pos], &line->history[pos + 1], (len - pos - 1) * sizeof(FuriString*));
furi_string_move(line->history[0], command);
line->history_entries--;
}
// insert empty command
@@ -109,7 +186,7 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) {
// execute command
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);
cli_shell_line_prompt(line);
@@ -199,7 +276,57 @@ static bool cli_shell_line_input_bksp(CliKeyCombo combo, void* context) {
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;
if(combo.modifiers != CliModKeyNo) 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 = {
.fallback = cli_shell_line_input_fallback,
.count = 10,
.fallback = cli_shell_line_input_normal,
.count = 14,
.records =
{
{{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, CliKeyBackspace}, 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);
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
*/