mirror of
https://github.com/DarkFlippers/unleashed-firmware.git
synced 2025-12-12 20:49:49 +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:
478
lib/toolbox/cli/shell/cli_shell.c
Normal file
478
lib/toolbox/cli/shell/cli_shell.c
Normal file
@@ -0,0 +1,478 @@
|
||||
#include "cli_shell.h"
|
||||
#include "cli_shell_i.h"
|
||||
#include "../cli_ansi.h"
|
||||
#include "../cli_registry_i.h"
|
||||
#include "../cli_command.h"
|
||||
#include "cli_shell_line.h"
|
||||
#include "cli_shell_completions.h"
|
||||
#include <stdio.h>
|
||||
#include <furi_hal_version.h>
|
||||
#include <m-array.h>
|
||||
#include <loader/loader.h>
|
||||
#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 {
|
||||
// Set and freed by external thread
|
||||
CliShellMotd motd;
|
||||
void* callback_context;
|
||||
PipeSide* pipe;
|
||||
CliRegistry* registry;
|
||||
const CliCommandExternalConfig* ext_config;
|
||||
FuriThread* thread;
|
||||
const char* prompt;
|
||||
|
||||
// Set and freed by shell thread
|
||||
FuriEventLoop* event_loop;
|
||||
CliAnsiParser* ansi_parser;
|
||||
FuriEventLoopTimer* ansi_parsing_timer;
|
||||
|
||||
Storage* storage;
|
||||
FuriPubSubSubscription* storage_subscription;
|
||||
FuriMessageQueue* storage_event_queue;
|
||||
|
||||
void* components[CliShellComponentMAX];
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
CliRegistryCommand* command;
|
||||
PipeSide* pipe;
|
||||
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);
|
||||
}
|
||||
|
||||
// =================
|
||||
// Built-in commands
|
||||
// =================
|
||||
|
||||
void cli_command_reload_external(PipeSide* pipe, FuriString* args, void* context) {
|
||||
UNUSED(pipe);
|
||||
UNUSED(args);
|
||||
CliShell* shell = context;
|
||||
furi_check(shell->ext_config);
|
||||
cli_registry_reload_external_commands(shell->registry, shell->ext_config);
|
||||
printf("OK!");
|
||||
}
|
||||
|
||||
void cli_command_help(PipeSide* pipe, FuriString* args, void* context) {
|
||||
UNUSED(pipe);
|
||||
UNUSED(args);
|
||||
CliShell* shell = context;
|
||||
CliRegistry* registry = shell->registry;
|
||||
|
||||
const size_t columns = 3;
|
||||
|
||||
printf("Available commands:\r\n" ANSI_FG_GREEN);
|
||||
cli_registry_lock(registry);
|
||||
CliCommandTree_t* commands = cli_registry_get_commands(registry);
|
||||
size_t commands_count = CliCommandTree_size(*commands);
|
||||
|
||||
CliCommandTree_it_t iterator;
|
||||
CliCommandTree_it(iterator, *commands);
|
||||
for(size_t i = 0; i < commands_count; i++) {
|
||||
const CliCommandTree_itref_t* item = CliCommandTree_cref(iterator);
|
||||
printf("%-30s", furi_string_get_cstr(*item->key_ptr));
|
||||
CliCommandTree_next(iterator);
|
||||
|
||||
if(i % columns == columns - 1) printf("\r\n");
|
||||
}
|
||||
|
||||
if(shell->ext_config)
|
||||
printf(
|
||||
ANSI_RESET
|
||||
"\r\nIf you added a new external command and can't see it above, run `reload_ext_cmds`");
|
||||
printf(ANSI_RESET "\r\nFind out more: https://docs.flipper.net/development/cli");
|
||||
|
||||
cli_registry_unlock(registry);
|
||||
}
|
||||
|
||||
void cli_command_exit(PipeSide* pipe, FuriString* args, void* context) {
|
||||
UNUSED(pipe);
|
||||
UNUSED(args);
|
||||
CliShell* shell = context;
|
||||
cli_shell_line_set_about_to_exit(shell->components[CliShellComponentLine]);
|
||||
furi_event_loop_stop(shell->event_loop);
|
||||
}
|
||||
|
||||
// ==================
|
||||
// Internal functions
|
||||
// ==================
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) {
|
||||
// split command into command and args
|
||||
size_t space = furi_string_search_char(command, ' ');
|
||||
if(space == FURI_STRING_FAILURE) space = furi_string_size(command);
|
||||
FuriString* command_name = furi_string_alloc_set(command);
|
||||
furi_string_left(command_name, space);
|
||||
FuriString* args = furi_string_alloc_set(command);
|
||||
furi_string_right(args, space + 1);
|
||||
|
||||
PluginManager* plugin_manager = NULL;
|
||||
Loader* loader = furi_record_open(RECORD_LOADER);
|
||||
bool loader_locked = false;
|
||||
CliRegistryCommand command_data;
|
||||
|
||||
do {
|
||||
// find handler
|
||||
if(!cli_registry_get_command(cli_shell->registry, command_name, &command_data)) {
|
||||
printf(
|
||||
ANSI_FG_RED "could not find command `%s`, try `help`" ANSI_RESET,
|
||||
furi_string_get_cstr(command_name));
|
||||
break;
|
||||
}
|
||||
|
||||
// load external command
|
||||
if(command_data.flags & CliCommandFlagExternal) {
|
||||
const CliCommandExternalConfig* ext_config = cli_shell->ext_config;
|
||||
plugin_manager = plugin_manager_alloc(
|
||||
ext_config->appid, CLI_PLUGIN_API_VERSION, firmware_api_interface);
|
||||
FuriString* path = furi_string_alloc_printf(
|
||||
"%s/%s%s.fal",
|
||||
ext_config->search_directory,
|
||||
ext_config->fal_prefix,
|
||||
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_locked = loader_lock(loader);
|
||||
if(!loader_locked) {
|
||||
printf(ANSI_FG_RED
|
||||
"this command cannot be run while an application is open" ANSI_RESET);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
furi_string_free(args);
|
||||
|
||||
// unlock loader
|
||||
if(loader_locked) loader_unlock(loader);
|
||||
furi_record_close(RECORD_LOADER);
|
||||
|
||||
// unload external command
|
||||
if(plugin_manager) plugin_manager_free(plugin_manager);
|
||||
}
|
||||
|
||||
const char* cli_shell_get_prompt(CliShell* cli_shell) {
|
||||
return cli_shell->prompt;
|
||||
}
|
||||
|
||||
// ==============
|
||||
// Event handlers
|
||||
// ==============
|
||||
|
||||
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_registry_reload_external_commands(cli_shell->registry, cli_shell->ext_config);
|
||||
} else if(event == CliShellStorageEventUnmount) {
|
||||
cli_registry_remove_external_commands(cli_shell->registry);
|
||||
} 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];
|
||||
|
||||
for(size_t j = 0; j < set->count; j++) {
|
||||
if(set->records[j].combo.modifiers == key_combo.modifiers &&
|
||||
set->records[j].combo.key == key_combo.key)
|
||||
if(set->records[j].action(key_combo, component_context)) return;
|
||||
}
|
||||
|
||||
if(set->fallback)
|
||||
if(set->fallback(key_combo, component_context)) return;
|
||||
}
|
||||
}
|
||||
|
||||
static void cli_shell_pipe_broken(PipeSide* pipe, void* context) {
|
||||
// allow commands to be processed before we stop the shell
|
||||
if(pipe_bytes_available(pipe)) return;
|
||||
|
||||
CliShell* cli_shell = context;
|
||||
furi_event_loop_stop(cli_shell->event_loop);
|
||||
}
|
||||
|
||||
static void cli_shell_data_available(PipeSide* pipe, void* context) {
|
||||
UNUSED(pipe);
|
||||
CliShell* cli_shell = context;
|
||||
|
||||
furi_event_loop_timer_start(cli_shell->ansi_parsing_timer, furi_ms_to_ticks(ANSI_TIMEOUT_MS));
|
||||
|
||||
// process ANSI escape sequences
|
||||
int c = getchar();
|
||||
furi_assert(c >= 0);
|
||||
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;
|
||||
cli_shell_process_parser_result(
|
||||
cli_shell, cli_ansi_parser_feed_timeout(cli_shell->ansi_parser));
|
||||
}
|
||||
|
||||
// ===========
|
||||
// Thread code
|
||||
// ===========
|
||||
|
||||
static void cli_shell_init(CliShell* shell) {
|
||||
cli_registry_add_command(
|
||||
shell->registry,
|
||||
"help",
|
||||
CliCommandFlagUseShellThread | CliCommandFlagParallelSafe,
|
||||
cli_command_help,
|
||||
shell);
|
||||
cli_registry_add_command(
|
||||
shell->registry,
|
||||
"?",
|
||||
CliCommandFlagUseShellThread | CliCommandFlagParallelSafe,
|
||||
cli_command_help,
|
||||
shell);
|
||||
cli_registry_add_command(
|
||||
shell->registry,
|
||||
"exit",
|
||||
CliCommandFlagUseShellThread | CliCommandFlagParallelSafe,
|
||||
cli_command_exit,
|
||||
shell);
|
||||
|
||||
if(shell->ext_config) {
|
||||
cli_registry_add_command(
|
||||
shell->registry,
|
||||
"reload_ext_cmds",
|
||||
CliCommandFlagUseShellThread,
|
||||
cli_command_reload_external,
|
||||
shell);
|
||||
cli_registry_reload_external_commands(shell->registry, shell->ext_config);
|
||||
}
|
||||
|
||||
shell->components[CliShellComponentLine] = cli_shell_line_alloc(shell);
|
||||
shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc(
|
||||
shell->registry, shell, shell->components[CliShellComponentLine]);
|
||||
|
||||
shell->ansi_parser = cli_ansi_parser_alloc();
|
||||
|
||||
shell->event_loop = furi_event_loop_alloc();
|
||||
shell->ansi_parsing_timer = furi_event_loop_timer_alloc(
|
||||
shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, shell);
|
||||
|
||||
shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent));
|
||||
furi_event_loop_subscribe_message_queue(
|
||||
shell->event_loop,
|
||||
shell->storage_event_queue,
|
||||
FuriEventLoopEventIn,
|
||||
cli_shell_storage_internal_event,
|
||||
shell);
|
||||
shell->storage = furi_record_open(RECORD_STORAGE);
|
||||
shell->storage_subscription =
|
||||
furi_pubsub_subscribe(storage_get_pubsub(shell->storage), cli_shell_storage_event, shell);
|
||||
|
||||
cli_shell_install_pipe(shell);
|
||||
}
|
||||
|
||||
static void cli_shell_deinit(CliShell* shell) {
|
||||
furi_pubsub_unsubscribe(storage_get_pubsub(shell->storage), shell->storage_subscription);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
furi_event_loop_unsubscribe(shell->event_loop, shell->storage_event_queue);
|
||||
furi_message_queue_free(shell->storage_event_queue);
|
||||
|
||||
cli_shell_completions_free(shell->components[CliShellComponentCompletions]);
|
||||
cli_shell_line_free(shell->components[CliShellComponentLine]);
|
||||
|
||||
cli_shell_detach_pipe(shell);
|
||||
furi_event_loop_timer_free(shell->ansi_parsing_timer);
|
||||
furi_event_loop_free(shell->event_loop);
|
||||
cli_ansi_parser_free(shell->ansi_parser);
|
||||
}
|
||||
|
||||
static int32_t cli_shell_thread(void* context) {
|
||||
CliShell* shell = context;
|
||||
|
||||
// Sometimes, the other side closes the pipe even before our thread is started. Although the
|
||||
// rest of the code will eventually find this out if this check is removed, there's no point in
|
||||
// wasting time.
|
||||
if(pipe_state(shell->pipe) == PipeStateBroken) return 0;
|
||||
|
||||
cli_shell_init(shell);
|
||||
FURI_LOG_D(TAG, "Started");
|
||||
|
||||
shell->motd(shell->callback_context);
|
||||
cli_shell_line_prompt(shell->components[CliShellComponentLine]);
|
||||
|
||||
furi_event_loop_run(shell->event_loop);
|
||||
|
||||
FURI_LOG_D(TAG, "Stopped");
|
||||
cli_shell_deinit(shell);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ==========
|
||||
// Public API
|
||||
// ==========
|
||||
|
||||
CliShell* cli_shell_alloc(
|
||||
CliShellMotd motd,
|
||||
void* context,
|
||||
PipeSide* pipe,
|
||||
CliRegistry* registry,
|
||||
const CliCommandExternalConfig* ext_config) {
|
||||
furi_check(motd);
|
||||
furi_check(pipe);
|
||||
furi_check(registry);
|
||||
|
||||
CliShell* shell = malloc(sizeof(CliShell));
|
||||
*shell = (CliShell){
|
||||
.motd = motd,
|
||||
.callback_context = context,
|
||||
.pipe = pipe,
|
||||
.registry = registry,
|
||||
.ext_config = ext_config,
|
||||
};
|
||||
|
||||
shell->thread =
|
||||
furi_thread_alloc_ex("CliShell", CLI_SHELL_STACK_SIZE, cli_shell_thread, shell);
|
||||
|
||||
return shell;
|
||||
}
|
||||
|
||||
void cli_shell_free(CliShell* shell) {
|
||||
furi_check(shell);
|
||||
furi_thread_free(shell->thread);
|
||||
free(shell);
|
||||
}
|
||||
|
||||
void cli_shell_start(CliShell* shell) {
|
||||
furi_check(shell);
|
||||
furi_thread_start(shell->thread);
|
||||
}
|
||||
|
||||
void cli_shell_join(CliShell* shell) {
|
||||
furi_check(shell);
|
||||
furi_thread_join(shell->thread);
|
||||
}
|
||||
|
||||
void cli_shell_set_prompt(CliShell* shell, const char* prompt) {
|
||||
furi_check(shell);
|
||||
shell->prompt = prompt;
|
||||
}
|
||||
75
lib/toolbox/cli/shell/cli_shell.h
Normal file
75
lib/toolbox/cli/shell/cli_shell.h
Normal file
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <toolbox/pipe.h>
|
||||
#include "../cli_registry.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define CLI_SHELL_STACK_SIZE (4 * 1024U)
|
||||
|
||||
typedef struct CliShell CliShell;
|
||||
|
||||
/**
|
||||
* Called from the shell thread to print the Message of the Day when the shell
|
||||
* is started.
|
||||
*/
|
||||
typedef void (*CliShellMotd)(void* context);
|
||||
|
||||
/**
|
||||
* @brief Allocates a shell
|
||||
*
|
||||
* @param [in] motd Message of the Day callback
|
||||
* @param [in] context Callback context
|
||||
* @param [in] pipe Pipe side to be used by the shell
|
||||
* @param [in] registry Command registry
|
||||
* @param [in] ext_config External command configuration. See
|
||||
* `CliCommandExternalConfig`. May be NULL if support for
|
||||
* external commands is not required.
|
||||
*
|
||||
* @return Shell instance
|
||||
*/
|
||||
CliShell* cli_shell_alloc(
|
||||
CliShellMotd motd,
|
||||
void* context,
|
||||
PipeSide* pipe,
|
||||
CliRegistry* registry,
|
||||
const CliCommandExternalConfig* ext_config);
|
||||
|
||||
/**
|
||||
* @brief Frees a shell
|
||||
*
|
||||
* @param [in] shell Shell instance
|
||||
*/
|
||||
void cli_shell_free(CliShell* shell);
|
||||
|
||||
/**
|
||||
* @brief Starts a shell
|
||||
*
|
||||
* The shell runs in a separate thread. This call is non-blocking.
|
||||
*
|
||||
* @param [in] shell Shell instance
|
||||
*/
|
||||
void cli_shell_start(CliShell* shell);
|
||||
|
||||
/**
|
||||
* @brief Joins the shell thread
|
||||
*
|
||||
* @warning This call is blocking.
|
||||
*
|
||||
* @param [in] shell Shell instance
|
||||
*/
|
||||
void cli_shell_join(CliShell* shell);
|
||||
|
||||
/**
|
||||
* @brief Sets optional text before prompt (`>:`)
|
||||
*
|
||||
* @param [in] shell Shell instance
|
||||
*/
|
||||
void cli_shell_set_prompt(CliShell* shell, const char* prompt);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
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},
|
||||
},
|
||||
};
|
||||
25
lib/toolbox/cli/shell/cli_shell_completions.h
Normal file
25
lib/toolbox/cli/shell/cli_shell_completions.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <m-array.h>
|
||||
#include "cli_shell_i.h"
|
||||
#include "cli_shell_line.h"
|
||||
#include "../cli_registry.h"
|
||||
#include "../cli_registry_i.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct CliShellCompletions CliShellCompletions;
|
||||
|
||||
CliShellCompletions*
|
||||
cli_shell_completions_alloc(CliRegistry* registry, CliShell* shell, CliShellLine* line);
|
||||
|
||||
void cli_shell_completions_free(CliShellCompletions* completions);
|
||||
|
||||
extern CliShellKeyComboSet cli_shell_completions_key_combo_set;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
34
lib/toolbox/cli/shell/cli_shell_i.h
Normal file
34
lib/toolbox/cli/shell/cli_shell_i.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "../cli_ansi.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct CliShell CliShell;
|
||||
|
||||
/**
|
||||
* @brief Key combo handler
|
||||
* @return true if the event was handled, false otherwise
|
||||
*/
|
||||
typedef bool (*CliShellKeyComboAction)(CliKeyCombo combo, void* context);
|
||||
|
||||
typedef struct {
|
||||
CliKeyCombo combo;
|
||||
CliShellKeyComboAction action;
|
||||
} CliShellKeyComboRecord;
|
||||
|
||||
typedef struct {
|
||||
CliShellKeyComboAction fallback;
|
||||
size_t count;
|
||||
CliShellKeyComboRecord records[];
|
||||
} CliShellKeyComboSet;
|
||||
|
||||
void cli_shell_execute_command(CliShell* cli_shell, FuriString* command);
|
||||
|
||||
const char* cli_shell_get_prompt(CliShell* cli_shell);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
378
lib/toolbox/cli/shell/cli_shell_line.c
Normal file
378
lib/toolbox/cli/shell/cli_shell_line.c
Normal file
@@ -0,0 +1,378 @@
|
||||
#include "cli_shell_line.h"
|
||||
|
||||
#define HISTORY_DEPTH 10
|
||||
|
||||
struct CliShellLine {
|
||||
size_t history_position;
|
||||
size_t line_position;
|
||||
FuriString* history[HISTORY_DEPTH];
|
||||
size_t history_entries;
|
||||
CliShell* shell;
|
||||
bool about_to_exit;
|
||||
};
|
||||
|
||||
// ==========
|
||||
// Public API
|
||||
// ==========
|
||||
|
||||
CliShellLine* cli_shell_line_alloc(CliShell* shell) {
|
||||
CliShellLine* line = malloc(sizeof(CliShellLine));
|
||||
line->shell = shell;
|
||||
|
||||
line->history[0] = furi_string_alloc();
|
||||
line->history_entries = 1;
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
void cli_shell_line_free(CliShellLine* line) {
|
||||
for(size_t i = 0; i < line->history_entries; i++)
|
||||
furi_string_free(line->history[i]);
|
||||
|
||||
free(line);
|
||||
}
|
||||
|
||||
FuriString* cli_shell_line_get_selected(CliShellLine* line) {
|
||||
return line->history[line->history_position];
|
||||
}
|
||||
|
||||
FuriString* cli_shell_line_get_editing(CliShellLine* line) {
|
||||
return line->history[0];
|
||||
}
|
||||
|
||||
void cli_shell_line_format_prompt(CliShellLine* line, char* buf, size_t length) {
|
||||
UNUSED(line);
|
||||
const char* prompt = cli_shell_get_prompt(line->shell);
|
||||
snprintf(buf, length - 1, "%s>: ", prompt ? prompt : "");
|
||||
}
|
||||
|
||||
size_t cli_shell_line_prompt_length(CliShellLine* line) {
|
||||
char buffer[128];
|
||||
cli_shell_line_format_prompt(line, buffer, sizeof(buffer));
|
||||
return strlen(buffer);
|
||||
}
|
||||
|
||||
void cli_shell_line_prompt(CliShellLine* line) {
|
||||
char buffer[32];
|
||||
cli_shell_line_format_prompt(line, buffer, sizeof(buffer));
|
||||
printf("\r\n%s", buffer);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line) {
|
||||
if(line->history_position > 0) {
|
||||
FuriString* source = cli_shell_line_get_selected(line);
|
||||
FuriString* destination = cli_shell_line_get_editing(line);
|
||||
furi_string_set(destination, source);
|
||||
line->history_position = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void cli_shell_line_set_about_to_exit(CliShellLine* line) {
|
||||
line->about_to_exit = true;
|
||||
}
|
||||
|
||||
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
|
||||
// ==============
|
||||
|
||||
static bool cli_shell_line_input_ctrl_c(CliKeyCombo combo, void* context) {
|
||||
UNUSED(combo);
|
||||
CliShellLine* line = context;
|
||||
// reset input
|
||||
furi_string_reset(cli_shell_line_get_editing(line));
|
||||
line->line_position = 0;
|
||||
line->history_position = 0;
|
||||
printf("^C");
|
||||
cli_shell_line_prompt(line);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) {
|
||||
UNUSED(combo);
|
||||
CliShellLine* line = 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) {
|
||||
size_t pos = line->history_position;
|
||||
size_t len = line->history_entries;
|
||||
memmove(
|
||||
&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
|
||||
if(line->history_entries == HISTORY_DEPTH) {
|
||||
furi_string_free(line->history[HISTORY_DEPTH - 1]);
|
||||
line->history_entries--;
|
||||
}
|
||||
memmove(&line->history[1], &line->history[0], line->history_entries * sizeof(FuriString*));
|
||||
line->history[0] = furi_string_alloc();
|
||||
line->history_entries++;
|
||||
line->line_position = 0;
|
||||
line->history_position = 0;
|
||||
|
||||
// execute command
|
||||
printf("\r\n");
|
||||
cli_shell_execute_command(line->shell, command_copy);
|
||||
furi_string_free(command_copy);
|
||||
if(!line->about_to_exit) cli_shell_line_prompt(line);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cli_shell_line_input_up_down(CliKeyCombo combo, void* context) {
|
||||
CliShellLine* line = context;
|
||||
// go up and down in history
|
||||
int increment = (combo.key == CliKeyUp) ? 1 : -1;
|
||||
size_t new_pos =
|
||||
CLAMP((int)line->history_position + increment, (int)line->history_entries - 1, 0);
|
||||
|
||||
// print prompt with selected command
|
||||
if(new_pos != line->history_position) {
|
||||
char prompt[64];
|
||||
cli_shell_line_format_prompt(line, prompt, sizeof(prompt));
|
||||
line->history_position = new_pos;
|
||||
FuriString* command = cli_shell_line_get_selected(line);
|
||||
printf(
|
||||
ANSI_CURSOR_HOR_POS("1") "%s%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END),
|
||||
prompt,
|
||||
furi_string_get_cstr(command));
|
||||
fflush(stdout);
|
||||
line->line_position = furi_string_size(command);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cli_shell_line_input_left_right(CliKeyCombo combo, void* context) {
|
||||
CliShellLine* line = context;
|
||||
// go left and right in the current line
|
||||
FuriString* command = cli_shell_line_get_selected(line);
|
||||
int increment = (combo.key == CliKeyRight) ? 1 : -1;
|
||||
size_t new_pos =
|
||||
CLAMP((int)line->line_position + increment, (int)furi_string_size(command), 0);
|
||||
|
||||
// move cursor
|
||||
if(new_pos != line->line_position) {
|
||||
line->line_position = new_pos;
|
||||
printf("%s", (increment == 1) ? ANSI_CURSOR_RIGHT_BY("1") : ANSI_CURSOR_LEFT_BY("1"));
|
||||
fflush(stdout);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cli_shell_line_input_home(CliKeyCombo combo, void* context) {
|
||||
UNUSED(combo);
|
||||
CliShellLine* line = context;
|
||||
// go to the start
|
||||
line->line_position = 0;
|
||||
printf(ANSI_CURSOR_HOR_POS("%zu"), cli_shell_line_prompt_length(line) + 1);
|
||||
fflush(stdout);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cli_shell_line_input_end(CliKeyCombo combo, void* context) {
|
||||
UNUSED(combo);
|
||||
CliShellLine* line = context;
|
||||
// go to the end
|
||||
line->line_position = furi_string_size(cli_shell_line_get_selected(line));
|
||||
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_bksp(CliKeyCombo combo, void* context) {
|
||||
UNUSED(combo);
|
||||
CliShellLine* line = context;
|
||||
// erase one character
|
||||
cli_shell_line_ensure_not_overwriting_history(line);
|
||||
FuriString* editing_line = cli_shell_line_get_editing(line);
|
||||
if(line->line_position == 0) {
|
||||
putc(CliKeyBell, stdout);
|
||||
fflush(stdout);
|
||||
return true;
|
||||
}
|
||||
line->line_position--;
|
||||
furi_string_replace_at(editing_line, line->line_position, 1, "");
|
||||
|
||||
// move cursor, print the rest of the line, restore cursor
|
||||
printf(
|
||||
ANSI_CURSOR_LEFT_BY("1") "%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END),
|
||||
furi_string_get_cstr(editing_line) + line->line_position);
|
||||
size_t left_by = furi_string_size(editing_line) - line->line_position;
|
||||
if(left_by) // apparently LEFT_BY("0") still shifts left by one ._ .
|
||||
printf(ANSI_CURSOR_LEFT_BY("%zu"), left_by);
|
||||
fflush(stdout);
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
// insert character
|
||||
cli_shell_line_ensure_not_overwriting_history(line);
|
||||
FuriString* editing_line = cli_shell_line_get_editing(line);
|
||||
if(line->line_position == furi_string_size(editing_line)) {
|
||||
furi_string_push_back(editing_line, combo.key);
|
||||
printf("%c", combo.key);
|
||||
} else {
|
||||
const char in_str[2] = {combo.key, 0};
|
||||
furi_string_replace_at(editing_line, line->line_position, 0, in_str);
|
||||
printf(ANSI_INSERT_MODE_ENABLE "%c" ANSI_INSERT_MODE_DISABLE, combo.key);
|
||||
}
|
||||
fflush(stdout);
|
||||
line->line_position++;
|
||||
return true;
|
||||
}
|
||||
|
||||
CliShellKeyComboSet cli_shell_line_key_combo_set = {
|
||||
.fallback = cli_shell_line_input_normal,
|
||||
.count = 14,
|
||||
.records =
|
||||
{
|
||||
{{CliModKeyNo, CliKeyETX}, cli_shell_line_input_ctrl_c},
|
||||
{{CliModKeyNo, CliKeyCR}, cli_shell_line_input_cr},
|
||||
{{CliModKeyNo, CliKeyUp}, cli_shell_line_input_up_down},
|
||||
{{CliModKeyNo, CliKeyDown}, cli_shell_line_input_up_down},
|
||||
{{CliModKeyNo, CliKeyLeft}, cli_shell_line_input_left_right},
|
||||
{{CliModKeyNo, CliKeyRight}, cli_shell_line_input_left_right},
|
||||
{{CliModKeyNo, CliKeyHome}, cli_shell_line_input_home},
|
||||
{{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},
|
||||
},
|
||||
};
|
||||
42
lib/toolbox/cli/shell/cli_shell_line.h
Normal file
42
lib/toolbox/cli/shell/cli_shell_line.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
|
||||
#include "cli_shell_i.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct CliShellLine CliShellLine;
|
||||
|
||||
CliShellLine* cli_shell_line_alloc(CliShell* shell);
|
||||
|
||||
void cli_shell_line_free(CliShellLine* line);
|
||||
|
||||
FuriString* cli_shell_line_get_selected(CliShellLine* line);
|
||||
|
||||
FuriString* cli_shell_line_get_editing(CliShellLine* line);
|
||||
|
||||
size_t cli_shell_line_prompt_length(CliShellLine* line);
|
||||
|
||||
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
|
||||
*/
|
||||
void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line);
|
||||
|
||||
void cli_shell_line_set_about_to_exit(CliShellLine* line);
|
||||
|
||||
extern CliShellKeyComboSet cli_shell_line_key_combo_set;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user