1
mirror of https://github.com/DarkFlippers/unleashed-firmware.git synced 2025-12-12 04:34:43 +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:
Anna Antonenko
2025-04-05 02:58:58 +04:00
committed by GitHub
parent 6f852e646c
commit 7192c9e68b
60 changed files with 994 additions and 683 deletions

View File

@@ -12,6 +12,10 @@ env.Append(
],
SDK_HEADERS=[
File("api_lock.h"),
File("cli/cli_ansi.h"),
File("cli/cli_command.h"),
File("cli/cli_registry.h"),
File("cli/shell/cli_shell.h"),
File("compress.h"),
File("manchester_decoder.h"),
File("manchester_encoder.h"),

123
lib/toolbox/cli/cli_ansi.c Normal file
View File

@@ -0,0 +1,123 @@
#include "cli_ansi.h"
typedef enum {
CliAnsiParserStateInitial,
CliAnsiParserStateEscape,
CliAnsiParserStateEscapeBrace,
CliAnsiParserStateEscapeBraceOne,
CliAnsiParserStateEscapeBraceOneSemicolon,
CliAnsiParserStateEscapeBraceOneSemicolonModifiers,
} CliAnsiParserState;
struct CliAnsiParser {
CliAnsiParserState state;
CliModKey modifiers;
};
CliAnsiParser* cli_ansi_parser_alloc(void) {
CliAnsiParser* parser = malloc(sizeof(CliAnsiParser));
return parser;
}
void cli_ansi_parser_free(CliAnsiParser* parser) {
free(parser);
}
/**
* @brief Converts a single character representing a special key into the enum
* representation
*/
static CliKey cli_ansi_key_from_mnemonic(char c) {
switch(c) {
case 'A':
return CliKeyUp;
case 'B':
return CliKeyDown;
case 'C':
return CliKeyRight;
case 'D':
return CliKeyLeft;
case 'F':
return CliKeyEnd;
case 'H':
return CliKeyHome;
default:
return CliKeyUnrecognized;
}
}
#define PARSER_RESET_AND_RETURN(parser, modifiers_val, key_val) \
do { \
parser->state = CliAnsiParserStateInitial; \
return (CliAnsiParserResult){ \
.is_done = true, \
.result = (CliKeyCombo){ \
.modifiers = modifiers_val, \
.key = key_val, \
}}; \
} while(0);
CliAnsiParserResult cli_ansi_parser_feed(CliAnsiParser* parser, char c) {
switch(parser->state) {
case CliAnsiParserStateInitial:
// <key> -> <key>
if(c != CliKeyEsc) PARSER_RESET_AND_RETURN(parser, CliModKeyNo, c); // -V1048
// <ESC> ...
parser->state = CliAnsiParserStateEscape;
break;
case CliAnsiParserStateEscape:
// <ESC> <ESC> -> <ESC>
if(c == CliKeyEsc) PARSER_RESET_AND_RETURN(parser, CliModKeyNo, c);
// <ESC> <key> -> Alt + <key>
if(c != '[') PARSER_RESET_AND_RETURN(parser, CliModKeyAlt, c);
// <ESC> [ ...
parser->state = CliAnsiParserStateEscapeBrace;
break;
case CliAnsiParserStateEscapeBrace:
// <ESC> [ <key mnemonic> -> <key>
if(c != '1') PARSER_RESET_AND_RETURN(parser, CliModKeyNo, cli_ansi_key_from_mnemonic(c));
// <ESC> [ 1 ...
parser->state = CliAnsiParserStateEscapeBraceOne;
break;
case CliAnsiParserStateEscapeBraceOne:
// <ESC> [ 1 <non-;> -> error
if(c != ';') PARSER_RESET_AND_RETURN(parser, CliModKeyNo, CliKeyUnrecognized);
// <ESC> [ 1 ; ...
parser->state = CliAnsiParserStateEscapeBraceOneSemicolon;
break;
case CliAnsiParserStateEscapeBraceOneSemicolon:
// <ESC> [ 1 ; <modifiers> ...
parser->modifiers = (c - '0');
parser->modifiers &= ~1;
parser->state = CliAnsiParserStateEscapeBraceOneSemicolonModifiers;
break;
case CliAnsiParserStateEscapeBraceOneSemicolonModifiers:
// <ESC> [ 1 ; <modifiers> <key mnemonic> -> <modifiers> + <key>
PARSER_RESET_AND_RETURN(parser, parser->modifiers, cli_ansi_key_from_mnemonic(c));
}
return (CliAnsiParserResult){.is_done = false};
}
CliAnsiParserResult cli_ansi_parser_feed_timeout(CliAnsiParser* parser) {
CliAnsiParserResult result = {.is_done = false};
if(parser->state == CliAnsiParserStateEscape) {
result.is_done = true;
result.result.key = CliKeyEsc;
result.result.modifiers = CliModKeyNo;
}
parser->state = CliAnsiParserStateInitial;
return result;
}

153
lib/toolbox/cli/cli_ansi.h Normal file
View File

@@ -0,0 +1,153 @@
#pragma once
#include <furi.h>
#ifdef __cplusplus
extern "C" {
#endif
// text styling
#define ANSI_RESET "\e[0m"
#define ANSI_BOLD "\e[1m"
#define ANSI_FAINT "\e[2m"
#define ANSI_INVERT "\e[7m"
#define ANSI_FG_BLACK "\e[30m"
#define ANSI_FG_RED "\e[31m"
#define ANSI_FG_GREEN "\e[32m"
#define ANSI_FG_YELLOW "\e[33m"
#define ANSI_FG_BLUE "\e[34m"
#define ANSI_FG_MAGENTA "\e[35m"
#define ANSI_FG_CYAN "\e[36m"
#define ANSI_FG_WHITE "\e[37m"
#define ANSI_FG_BR_BLACK "\e[90m"
#define ANSI_FG_BR_RED "\e[91m"
#define ANSI_FG_BR_GREEN "\e[92m"
#define ANSI_FG_BR_YELLOW "\e[93m"
#define ANSI_FG_BR_BLUE "\e[94m"
#define ANSI_FG_BR_MAGENTA "\e[95m"
#define ANSI_FG_BR_CYAN "\e[96m"
#define ANSI_FG_BR_WHITE "\e[97m"
#define ANSI_BG_BLACK "\e[40m"
#define ANSI_BG_RED "\e[41m"
#define ANSI_BG_GREEN "\e[42m"
#define ANSI_BG_YELLOW "\e[43m"
#define ANSI_BG_BLUE "\e[44m"
#define ANSI_BG_MAGENTA "\e[45m"
#define ANSI_BG_CYAN "\e[46m"
#define ANSI_BG_WHITE "\e[47m"
#define ANSI_BG_BR_BLACK "\e[100m"
#define ANSI_BG_BR_RED "\e[101m"
#define ANSI_BG_BR_GREEN "\e[102m"
#define ANSI_BG_BR_YELLOW "\e[103m"
#define ANSI_BG_BR_BLUE "\e[104m"
#define ANSI_BG_BR_MAGENTA "\e[105m"
#define ANSI_BG_BR_CYAN "\e[106m"
#define ANSI_BG_BR_WHITE "\e[107m"
#define ANSI_FLIPPER_BRAND_ORANGE "\e[38;2;255;130;0m"
// cursor positioning
#define ANSI_CURSOR_UP_BY(rows) "\e[" rows "A"
#define ANSI_CURSOR_DOWN_BY(rows) "\e[" rows "B"
#define ANSI_CURSOR_RIGHT_BY(cols) "\e[" cols "C"
#define ANSI_CURSOR_LEFT_BY(cols) "\e[" cols "D"
#define ANSI_CURSOR_DOWN_BY_AND_FIRST_COLUMN(rows) "\e[" rows "E"
#define ANSI_CURSOR_UP_BY_AND_FIRST_COLUMN(rows) "\e[" rows "F"
#define ANSI_CURSOR_HOR_POS(pos) "\e[" pos "G"
#define ANSI_CURSOR_POS(row, col) "\e[" row ";" col "H"
// erasing
#define ANSI_ERASE_FROM_CURSOR_TO_END "0"
#define ANSI_ERASE_FROM_START_TO_CURSOR "1"
#define ANSI_ERASE_ENTIRE "2"
#define ANSI_ERASE_DISPLAY(portion) "\e[" portion "J"
#define ANSI_ERASE_LINE(portion) "\e[" portion "K"
#define ANSI_ERASE_SCROLLBACK_BUFFER ANSI_ERASE_DISPLAY("3")
// misc
#define ANSI_INSERT_MODE_ENABLE "\e[4h"
#define ANSI_INSERT_MODE_DISABLE "\e[4l"
typedef enum {
CliKeyUnrecognized = 0,
CliKeySOH = 0x01,
CliKeyETX = 0x03,
CliKeyEOT = 0x04,
CliKeyBell = 0x07,
CliKeyBackspace = 0x08,
CliKeyTab = 0x09,
CliKeyLF = 0x0A,
CliKeyFF = 0x0C,
CliKeyCR = 0x0D,
CliKeyETB = 0x17,
CliKeyEsc = 0x1B,
CliKeyUS = 0x1F,
CliKeySpace = 0x20,
CliKeyDEL = 0x7F,
CliKeySpecial = 0x80,
CliKeyLeft,
CliKeyRight,
CliKeyUp,
CliKeyDown,
CliKeyHome,
CliKeyEnd,
} CliKey;
typedef enum {
CliModKeyNo = 0,
CliModKeyAlt = 2,
CliModKeyCtrl = 4,
CliModKeyMeta = 8,
} CliModKey;
typedef struct {
CliModKey modifiers;
CliKey key;
} CliKeyCombo;
typedef struct CliAnsiParser CliAnsiParser;
typedef struct {
bool is_done;
CliKeyCombo result;
} CliAnsiParserResult;
/**
* @brief Allocates an ANSI parser
*/
CliAnsiParser* cli_ansi_parser_alloc(void);
/**
* @brief Frees an ANSI parser
*/
void cli_ansi_parser_free(CliAnsiParser* parser);
/**
* @brief Feeds an ANSI parser a character
*/
CliAnsiParserResult cli_ansi_parser_feed(CliAnsiParser* parser, char c);
/**
* @brief Feeds an ANSI parser a timeout event
*
* As a user of the ANSI parser API, you are responsible for calling this
* function some time after the last character was fed into the parser. The
* recommended timeout is about 10 ms. The exact value does not matter as long
* as it is small enough for the user not notice a delay, but big enough that
* when a terminal is sending an escape sequence, this function does not get
* called in between the characters of the sequence.
*/
CliAnsiParserResult cli_ansi_parser_feed_timeout(CliAnsiParser* parser);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,17 @@
#include "cli_command.h"
#include "cli_ansi.h"
bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side) {
if(pipe_state(side) == PipeStateBroken) return true;
if(!pipe_bytes_available(side)) return false;
char c = getchar();
return c == CliKeyETX;
}
void cli_print_usage(const char* cmd, const char* usage, const char* arg) {
furi_check(cmd);
furi_check(arg);
furi_check(usage);
printf("%s: illegal option -- %s\r\nusage: %s %s", cmd, arg, cmd, usage);
}

View File

@@ -0,0 +1,103 @@
/**
* @file cli_command.h
* Command metadata and helpers
*/
#pragma once
#include <furi.h>
#include <toolbox/pipe.h>
#include <lib/flipper_application/flipper_application.h>
#ifdef __cplusplus
extern "C" {
#endif
#define CLI_PLUGIN_API_VERSION 1
typedef enum {
CliCommandFlagDefault = 0, /**< Default */
CliCommandFlagParallelSafe = (1 << 0), /**< Safe to run in parallel with other apps */
CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */
CliCommandFlagDontAttachStdio = (1 << 2), /**< Do no attach I/O pipe to thread stdio */
CliCommandFlagUseShellThread =
(1
<< 3), /**< Don't start a separate thread to run the command in. Incompatible with DontAttachStdio */
// internal flags (do not set them yourselves!)
CliCommandFlagExternal = (1 << 4), /**< The command comes from a .fal file */
} CliCommandFlag;
/**
* @brief CLI command execution callback pointer
*
* This callback will be called from a separate thread spawned just for your
* command. The pipe will be installed as the thread's stdio, so you can use
* `printf`, `getchar` and other standard functions to communicate with the
* user.
*
* @param [in] pipe Pipe that can be used to send and receive data. If
* `CliCommandFlagDontAttachStdio` was not set, you can
* also use standard C functions (printf, getc, etc.) to
* access this pipe.
* @param [in] args String with what was passed after the command
* @param [in] context Whatever you provided to `cli_add_command`
*/
typedef void (*CliCommandExecuteCallback)(PipeSide* pipe, FuriString* args, void* context);
typedef struct {
char* name;
CliCommandExecuteCallback execute_callback;
CliCommandFlag flags;
size_t stack_depth;
} CliCommandDescriptor;
/**
* @brief Configuration for locating external commands
*/
typedef struct {
const char* search_directory; //<! The directory to look in
const char* fal_prefix; //<! File name prefix that commands should have
const char* appid; //<! Expected plugin-reported appid
} CliCommandExternalConfig;
/**
* @brief Detects if Ctrl+C has been pressed or session has been terminated
*
* @param [in] side Pointer to pipe side given to the command thread
* @warning This function also assumes that the pipe is installed as the
* thread's stdio
* @warning This function will consume 0 or 1 bytes from the pipe
*/
bool cli_is_pipe_broken_or_is_etx_next_char(PipeSide* side);
/** Print unified cmd usage tip
*
* @param cmd cmd name
* @param usage usage tip
* @param arg arg passed by user
*/
void cli_print_usage(const char* cmd, const char* usage, const char* arg);
#define CLI_COMMAND_INTERFACE(name, execute_callback, flags, stack_depth, app_id) \
static const CliCommandDescriptor cli_##name##_desc = { \
#name, \
&execute_callback, \
flags, \
stack_depth, \
}; \
\
static const FlipperAppPluginDescriptor plugin_descriptor = { \
.appid = app_id, \
.ep_api_version = CLI_PLUGIN_API_VERSION, \
.entry_point = &cli_##name##_desc, \
}; \
\
const FlipperAppPluginDescriptor* cli_##name##_ep(void) { \
return &plugin_descriptor; \
}
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,178 @@
#include "cli_registry.h"
#include "cli_registry_i.h"
#include <toolbox/pipe.h>
#include <storage/storage.h>
#define TAG "cli"
struct CliRegistry {
CliCommandTree_t commands;
FuriMutex* mutex;
};
CliRegistry* cli_registry_alloc(void) {
CliRegistry* registry = malloc(sizeof(CliRegistry));
CliCommandTree_init(registry->commands);
registry->mutex = furi_mutex_alloc(FuriMutexTypeRecursive);
return registry;
}
void cli_registry_free(CliRegistry* registry) {
furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk);
furi_mutex_free(registry->mutex);
CliCommandTree_clear(registry->commands);
free(registry);
}
void cli_registry_add_command(
CliRegistry* registry,
const char* name,
CliCommandFlag flags,
CliCommandExecuteCallback callback,
void* context) {
cli_registry_add_command_ex(
registry, name, flags, callback, context, CLI_BUILTIN_COMMAND_STACK_SIZE);
}
void cli_registry_add_command_ex(
CliRegistry* registry,
const char* name,
CliCommandFlag flags,
CliCommandExecuteCallback callback,
void* context,
size_t stack_size) {
furi_check(registry);
furi_check(name);
furi_check(callback);
// the shell always attaches the pipe to the stdio, thus both flags can't be used at once
if(flags & CliCommandFlagUseShellThread) furi_check(!(flags & CliCommandFlagDontAttachStdio));
FuriString* name_str;
name_str = furi_string_alloc_set(name);
// command cannot contain spaces
furi_check(furi_string_search_char(name_str, ' ') == FURI_STRING_FAILURE);
CliRegistryCommand command = {
.context = context,
.execute_callback = callback,
.flags = flags,
.stack_depth = stack_size,
};
furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk);
CliCommandTree_set_at(registry->commands, name_str, command);
furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk);
furi_string_free(name_str);
}
void cli_registry_delete_command(CliRegistry* registry, const char* name) {
furi_check(registry);
FuriString* name_str;
name_str = furi_string_alloc_set(name);
furi_string_trim(name_str);
size_t name_replace;
do {
name_replace = furi_string_replace(name_str, " ", "_");
} while(name_replace != FURI_STRING_FAILURE);
furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk);
CliCommandTree_erase(registry->commands, name_str);
furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk);
furi_string_free(name_str);
}
bool cli_registry_get_command(
CliRegistry* registry,
FuriString* command,
CliRegistryCommand* result) {
furi_assert(registry);
furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk);
CliRegistryCommand* data = CliCommandTree_get(registry->commands, command);
if(data) *result = *data;
furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk);
return !!data;
}
void cli_registry_remove_external_commands(CliRegistry* registry) {
furi_check(registry);
furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk);
// FIXME FL-3977: memory leak somewhere within this function
CliCommandTree_t internal_cmds;
CliCommandTree_init(internal_cmds);
for
M_EACH(item, registry->commands, CliCommandTree_t) {
if(!(item->value_ptr->flags & CliCommandFlagExternal))
CliCommandTree_set_at(internal_cmds, *item->key_ptr, *item->value_ptr);
}
CliCommandTree_move(registry->commands, internal_cmds);
furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk);
}
void cli_registry_reload_external_commands(
CliRegistry* registry,
const CliCommandExternalConfig* config) {
furi_check(registry);
furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk);
FURI_LOG_D(TAG, "Reloading ext commands");
cli_registry_remove_external_commands(registry);
// iterate over files in plugin directory
Storage* storage = furi_record_open(RECORD_STORAGE);
File* plugin_dir = storage_file_alloc(storage);
if(storage_dir_open(plugin_dir, config->search_directory)) {
char plugin_filename[64];
FuriString* plugin_name = furi_string_alloc();
while(storage_dir_read(plugin_dir, NULL, plugin_filename, sizeof(plugin_filename))) {
FURI_LOG_T(TAG, "Plugin: %s", plugin_filename);
furi_string_set_str(plugin_name, plugin_filename);
furi_check(furi_string_end_with_str(plugin_name, ".fal"));
furi_string_replace_all_str(plugin_name, ".fal", "");
furi_check(furi_string_start_with_str(plugin_name, config->fal_prefix));
furi_string_replace_at(plugin_name, 0, strlen(config->fal_prefix), "");
CliRegistryCommand command = {
.context = NULL,
.execute_callback = NULL,
.flags = CliCommandFlagExternal,
};
CliCommandTree_set_at(registry->commands, plugin_name, command);
}
furi_string_free(plugin_name);
}
storage_dir_close(plugin_dir);
storage_file_free(plugin_dir);
furi_record_close(RECORD_STORAGE);
FURI_LOG_D(TAG, "Done reloading ext commands");
furi_check(furi_mutex_release(registry->mutex) == FuriStatusOk);
}
void cli_registry_lock(CliRegistry* registry) {
furi_assert(registry);
furi_check(furi_mutex_acquire(registry->mutex, FuriWaitForever) == FuriStatusOk);
}
void cli_registry_unlock(CliRegistry* registry) {
furi_assert(registry);
furi_mutex_release(registry->mutex);
}
CliCommandTree_t* cli_registry_get_commands(CliRegistry* registry) {
furi_assert(registry);
return &registry->commands;
}

View File

@@ -0,0 +1,92 @@
/**
* @file cli_registry.h
* API for registering commands with a CLI shell
*/
#pragma once
#include <furi.h>
#include <m-array.h>
#include <toolbox/pipe.h>
#include "cli_command.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct CliRegistry CliRegistry;
/**
* @brief Allocates a `CliRegistry`.
*/
CliRegistry* cli_registry_alloc(void);
/**
* @brief Frees a `CliRegistry`.
*/
void cli_registry_free(CliRegistry* registry);
/**
* @brief Registers a command with the registry. Provides less options than the
* `_ex` counterpart.
*
* @param [in] registry Pointer to registry instance
* @param [in] name Command name
* @param [in] flags see CliCommandFlag
* @param [in] callback Callback function
* @param [in] context Custom context
*/
void cli_registry_add_command(
CliRegistry* registry,
const char* name,
CliCommandFlag flags,
CliCommandExecuteCallback callback,
void* context);
/**
* @brief Registers a command with the registry. Provides more options than the
* non-`_ex` counterpart.
*
* @param [in] registry Pointer to registry instance
* @param [in] name Command name
* @param [in] flags see CliCommandFlag
* @param [in] callback Callback function
* @param [in] context Custom context
* @param [in] stack_size Thread stack size
*/
void cli_registry_add_command_ex(
CliRegistry* registry,
const char* name,
CliCommandFlag flags,
CliCommandExecuteCallback callback,
void* context,
size_t stack_size);
/**
* @brief Deletes a cli command
*
* @param [in] registry Pointer to registry instance
* @param [in] name Command name
*/
void cli_registry_delete_command(CliRegistry* registry, const char* name);
/**
* @brief Unregisters all external commands
*
* @param [in] registry Pointer to registry instance
*/
void cli_registry_remove_external_commands(CliRegistry* registry);
/**
* @brief Reloads the list of externally available commands
*
* @param [in] registry Pointer to registry instance
* @param [in] config See `CliCommandExternalConfig`
*/
void cli_registry_reload_external_commands(
CliRegistry* registry,
const CliCommandExternalConfig* config);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,55 @@
/**
* @file cli_registry_i.h
* Internal API for getting commands registered with the CLI
*/
#pragma once
#include <furi.h>
#include <m-bptree.h>
#include "cli_registry.h"
#ifdef __cplusplus
extern "C" {
#endif
#define CLI_BUILTIN_COMMAND_STACK_SIZE (4 * 1024U)
typedef struct {
void* context; //<! Context passed to callbacks
CliCommandExecuteCallback execute_callback; //<! Callback for command execution
CliCommandFlag flags;
size_t stack_depth;
} CliRegistryCommand;
#define CLI_COMMANDS_TREE_RANK 4
// -V:BPTREE_DEF2:1103
// -V:BPTREE_DEF2:524
BPTREE_DEF2(
CliCommandTree,
CLI_COMMANDS_TREE_RANK,
FuriString*,
FURI_STRING_OPLIST,
CliRegistryCommand,
M_POD_OPLIST);
#define M_OPL_CliCommandTree_t() BPTREE_OPLIST2(CliCommandTree, FURI_STRING_OPLIST, M_POD_OPLIST)
bool cli_registry_get_command(
CliRegistry* registry,
FuriString* command,
CliRegistryCommand* result);
void cli_registry_lock(CliRegistry* registry);
void cli_registry_unlock(CliRegistry* registry);
/**
* @warning Surround calls to this function with `cli_registry_[un]lock`
*/
CliCommandTree_t* cli_registry_get_commands(CliRegistry* registry);
#ifdef __cplusplus
}
#endif

View 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;
}

View 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

View 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},
},
};

View 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

View 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

View 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},
},
};

View 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