diff --git a/applications/debug/speaker_debug/speaker_debug.c b/applications/debug/speaker_debug/speaker_debug.c index d2a40a2a4..6a9956b07 100644 --- a/applications/debug/speaker_debug/speaker_debug.c +++ b/applications/debug/speaker_debug/speaker_debug.c @@ -1,9 +1,10 @@ #include #include #include -#include #include #include +#include +#include #define TAG "SpeakerDebug" @@ -20,14 +21,14 @@ typedef struct { typedef struct { MusicWorker* music_worker; FuriMessageQueue* message_queue; - Cli* cli; + CliRegistry* cli_registry; } SpeakerDebugApp; static SpeakerDebugApp* speaker_app_alloc(void) { SpeakerDebugApp* app = (SpeakerDebugApp*)malloc(sizeof(SpeakerDebugApp)); app->music_worker = music_worker_alloc(); app->message_queue = furi_message_queue_alloc(8, sizeof(SpeakerDebugAppMessage)); - app->cli = furi_record_open(RECORD_CLI); + app->cli_registry = furi_record_open(RECORD_CLI); return app; } @@ -96,7 +97,8 @@ static void speaker_app_run(SpeakerDebugApp* app, const char* arg) { return; } - cli_add_command(app->cli, CLI_COMMAND, CliCommandFlagParallelSafe, speaker_app_cli, app); + cli_registry_add_command( + app->cli_registry, CLI_COMMAND, CliCommandFlagParallelSafe, speaker_app_cli, app); SpeakerDebugAppMessage message; FuriStatus status; @@ -111,7 +113,7 @@ static void speaker_app_run(SpeakerDebugApp* app, const char* arg) { } } - cli_delete_command(app->cli, CLI_COMMAND); + cli_registry_delete_command(app->cli_registry, CLI_COMMAND); } int32_t speaker_debug_app(void* arg) { diff --git a/applications/debug/unit_tests/test_runner.c b/applications/debug/unit_tests/test_runner.c index 4de051125..ad7d31b02 100644 --- a/applications/debug/unit_tests/test_runner.c +++ b/applications/debug/unit_tests/test_runner.c @@ -2,7 +2,7 @@ #include "tests/test_api.h" -#include +#include #include #include #include diff --git a/applications/debug/unit_tests/tests/rpc/rpc_test.c b/applications/debug/unit_tests/tests/rpc/rpc_test.c index 5d26bdb30..82ab872ce 100644 --- a/applications/debug/unit_tests/tests/rpc/rpc_test.c +++ b/applications/debug/unit_tests/tests/rpc/rpc_test.c @@ -3,7 +3,6 @@ #include #include -#include #include #include #include diff --git a/applications/debug/unit_tests/unit_tests.c b/applications/debug/unit_tests/unit_tests.c index db78d8ced..8605cb781 100644 --- a/applications/debug/unit_tests/unit_tests.c +++ b/applications/debug/unit_tests/unit_tests.c @@ -1,6 +1,8 @@ #include -#include #include +#include +#include +#include #include "test_runner.h" @@ -14,8 +16,9 @@ void unit_tests_cli(PipeSide* pipe, FuriString* args, void* context) { void unit_tests_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "unit_tests", CliCommandFlagParallelSafe, unit_tests_cli, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, "unit_tests", CliCommandFlagParallelSafe, unit_tests_cli, NULL); furi_record_close(RECORD_CLI); #endif } diff --git a/applications/main/gpio/usb_uart_bridge.c b/applications/main/gpio/usb_uart_bridge.c index 15170f0d0..77cd02f63 100644 --- a/applications/main/gpio/usb_uart_bridge.c +++ b/applications/main/gpio/usb_uart_bridge.c @@ -1,7 +1,6 @@ #include "usb_uart_bridge.h" #include "usb_cdc.h" #include -#include #include #include #include diff --git a/applications/main/ibutton/ibutton_cli.c b/applications/main/ibutton/ibutton_cli.c index 2ff0860bb..0b9a59586 100644 --- a/applications/main/ibutton/ibutton_cli.c +++ b/applications/main/ibutton/ibutton_cli.c @@ -1,7 +1,7 @@ #include #include -#include +#include #include #include @@ -240,4 +240,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(ikey, execute, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(ikey, execute, CliCommandFlagDefault, 1024, CLI_APPID); diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index 22d916d15..eb13bcd79 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -554,4 +554,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(command); } -CLI_COMMAND_INTERFACE(ir, execute, CliCommandFlagDefault, 2048); +CLI_COMMAND_INTERFACE(ir, execute, CliCommandFlagDefault, 2048, CLI_APPID); diff --git a/applications/main/lfrfid/lfrfid_cli.c b/applications/main/lfrfid/lfrfid_cli.c index cefc55f65..63ca046b9 100644 --- a/applications/main/lfrfid/lfrfid_cli.c +++ b/applications/main/lfrfid/lfrfid_cli.c @@ -1,7 +1,7 @@ #include #include #include -#include +#include #include #include #include @@ -566,4 +566,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(rfid, execute, CliCommandFlagDefault, 2048); +CLI_COMMAND_INTERFACE(rfid, execute, CliCommandFlagDefault, 2048, CLI_APPID); diff --git a/applications/main/lfrfid/lfrfid_i.h b/applications/main/lfrfid/lfrfid_i.h index 2fcedcd7f..29a5eb902 100644 --- a/applications/main/lfrfid/lfrfid_i.h +++ b/applications/main/lfrfid/lfrfid_i.h @@ -8,7 +8,6 @@ #include #include #include -#include #include #include diff --git a/applications/main/nfc/nfc_app_i.h b/applications/main/nfc/nfc_app_i.h index 14e484622..1101b7bb2 100644 --- a/applications/main/nfc/nfc_app_i.h +++ b/applications/main/nfc/nfc_app_i.h @@ -10,7 +10,6 @@ #include #include #include -#include #include #include diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c index fd5598fc6..af3fd62eb 100644 --- a/applications/main/nfc/nfc_cli.c +++ b/applications/main/nfc/nfc_cli.c @@ -1,7 +1,6 @@ #include #include -#include -#include +#include #include #include #include @@ -65,4 +64,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(nfc, execute, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(nfc, execute, CliCommandFlagDefault, 1024, CLI_APPID); diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index 83bbc6770..193de76e4 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -1,9 +1,9 @@ #include #include -#include +#include #include -#include +#include #include #include @@ -64,4 +64,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(onewire, execute, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(onewire, execute, CliCommandFlagDefault, 1024, CLI_APPID); diff --git a/applications/main/subghz/helpers/subghz_chat.h b/applications/main/subghz/helpers/subghz_chat.h index 0d1497506..25fce0ecf 100644 --- a/applications/main/subghz/helpers/subghz_chat.h +++ b/applications/main/subghz/helpers/subghz_chat.h @@ -1,7 +1,6 @@ #pragma once #include "../subghz_i.h" #include -#include #include typedef struct SubGhzChatWorker SubGhzChatWorker; diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index 3c29aeeaf..674738851 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -4,7 +4,8 @@ #include #include -#include +#include +#include #include #include @@ -1184,4 +1185,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -CLI_COMMAND_INTERFACE(subghz, execute, CliCommandFlagDefault, 2048); +CLI_COMMAND_INTERFACE(subghz, execute, CliCommandFlagDefault, 2048, CLI_APPID); diff --git a/applications/main/subghz/subghz_cli.h b/applications/main/subghz/subghz_cli.h index f6388218f..275135581 100644 --- a/applications/main/subghz/subghz_cli.h +++ b/applications/main/subghz/subghz_cli.h @@ -1,5 +1,3 @@ #pragma once -#include - void subghz_on_system_start(void); diff --git a/applications/services/bt/bt_cli.c b/applications/services/bt/bt_cli.c index 9ef3ef8a2..d81bc0505 100644 --- a/applications/services/bt/bt_cli.c +++ b/applications/services/bt/bt_cli.c @@ -1,8 +1,9 @@ #include #include -#include #include #include +#include +#include #include #include "bt_settings.h" @@ -230,8 +231,8 @@ static void bt_cli(PipeSide* pipe, FuriString* args, void* context) { void bt_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, RECORD_BT, CliCommandFlagDefault, bt_cli, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "bt", CliCommandFlagDefault, bt_cli, NULL); furi_record_close(RECORD_CLI); #else UNUSED(bt_cli); diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index 6e2e393e0..e1e79f43d 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -4,17 +4,9 @@ App( entry_point="cli_on_system_start", cdefines=["SRV_CLI"], sources=[ - "cli.c", - "shell/cli_shell.c", - "shell/cli_shell_completions.c", - "shell/cli_shell_line.c", - "cli_commands.c", "cli_command_gpio.c", - "cli_ansi.c", - ], - sdk_headers=[ - "cli.h", - "cli_ansi.h", + "cli_main_commands.c", + "cli_main_shell.c", ], # This STARTUP has to be processed before those that depend on the "cli" record. # "cli" used to be a SERVICE, but it's been converted into a STARTUP in order to @@ -49,3 +41,11 @@ App( requires=["cli"], sources=["commands/neofetch.c"], ) + +App( + appid="cli_subshell_demo", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_subshell_demo_ep", + requires=["cli"], + sources=["commands/subshell_demo.c"], +) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c deleted file mode 100644 index 2bfce3a63..000000000 --- a/applications/services/cli/cli.c +++ /dev/null @@ -1,184 +0,0 @@ -#include "cli.h" -#include "cli_i.h" -#include "cli_commands.h" -#include "cli_ansi.h" -#include - -#define TAG "cli" - -struct Cli { - CliCommandTree_t commands; - FuriMutex* mutex; -}; - -Cli* cli_alloc(void) { - Cli* cli = malloc(sizeof(Cli)); - CliCommandTree_init(cli->commands); - cli->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); - return cli; -} - -void cli_add_command( - Cli* cli, - const char* name, - CliCommandFlag flags, - CliExecuteCallback callback, - void* context) { - cli_add_command_ex(cli, name, flags, callback, context, CLI_BUILTIN_COMMAND_STACK_SIZE); -} - -void cli_add_command_ex( - Cli* cli, - const char* name, - CliCommandFlag flags, - CliExecuteCallback callback, - void* context, - size_t stack_size) { - furi_check(cli); - 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); - - CliCommand command = { - .context = context, - .execute_callback = callback, - .flags = flags, - .stack_depth = stack_size, - }; - - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommandTree_set_at(cli->commands, name_str, command); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - - furi_string_free(name_str); -} - -void cli_delete_command(Cli* cli, const char* name) { - furi_check(cli); - 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(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommandTree_erase(cli->commands, name_str); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - - furi_string_free(name_str); -} - -bool cli_get_command(Cli* cli, FuriString* command, CliCommand* result) { - furi_assert(cli); - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - CliCommand* data = CliCommandTree_get(cli->commands, command); - if(data) *result = *data; - - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); - - return !!data; -} - -void cli_remove_external_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - - // FIXME FL-3977: memory leak somewhere within this function - - CliCommandTree_t internal_cmds; - CliCommandTree_init(internal_cmds); - for - M_EACH(item, cli->commands, CliCommandTree_t) { - if(!(item->value_ptr->flags & CliCommandFlagExternal)) - CliCommandTree_set_at(internal_cmds, *item->key_ptr, *item->value_ptr); - } - CliCommandTree_move(cli->commands, internal_cmds); - - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); -} - -void cli_enumerate_external_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); - FURI_LOG_D(TAG, "Enumerating external commands"); - - cli_remove_external_commands(cli); - - // 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, CLI_COMMANDS_PATH)) { - 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_string_replace_all_str(plugin_name, ".fal", ""); - furi_string_replace_at(plugin_name, 0, 4, ""); // remove "cli_" in the beginning - CliCommand command = { - .context = NULL, - .execute_callback = NULL, - .flags = CliCommandFlagExternal, - }; - CliCommandTree_set_at(cli->commands, plugin_name, command); - } - - furi_string_free(plugin_name); - } - - storage_file_free(plugin_dir); - furi_record_close(RECORD_STORAGE); - - FURI_LOG_D(TAG, "Finished enumerating external commands"); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); -} - -void cli_lock_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); -} - -void cli_unlock_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); -} - -CliCommandTree_t* cli_get_commands(Cli* cli) { - furi_check(cli); - furi_check(furi_mutex_get_owner(cli->mutex) == furi_thread_get_current_id()); - return &cli->commands; -} - -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); -} - -void cli_on_system_start(void) { - Cli* cli = cli_alloc(); - cli_commands_init(cli); - cli_enumerate_external_commands(cli); - furi_record_create(RECORD_CLI, cli); -} diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h deleted file mode 100644 index 2352e1806..000000000 --- a/applications/services/cli/cli.h +++ /dev/null @@ -1,131 +0,0 @@ -/** - * @file cli.h - * API for registering commands with the CLI - */ - -#pragma once -#include -#include -#include "cli_ansi.h" -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define RECORD_CLI "cli" - -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; - -/** Cli type anonymous structure */ -typedef struct Cli Cli; - -/** - * @brief CLI execution callback pointer. Implement this interface and use - * `add_cli_command`. - * - * 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 (*CliExecuteCallback)(PipeSide* pipe, FuriString* args, void* context); - -/** - * @brief Registers a command with the CLI. Provides less options than the `_ex` - * counterpart. - * - * @param [in] cli Pointer to CLI instance - * @param [in] name Command name - * @param [in] flags CliCommandFlag - * @param [in] callback Callback function - * @param [in] context Custom context - */ -void cli_add_command( - Cli* cli, - const char* name, - CliCommandFlag flags, - CliExecuteCallback callback, - void* context); - -/** - * @brief Registers a command with the CLI. Provides more options than the - * non-`_ex` counterpart. - * - * @param [in] cli Pointer to CLI instance - * @param [in] name Command name - * @param [in] flags CliCommandFlag - * @param [in] callback Callback function - * @param [in] context Custom context - * @param [in] stack_size Thread stack size - */ -void cli_add_command_ex( - Cli* cli, - const char* name, - CliCommandFlag flags, - CliExecuteCallback callback, - void* context, - size_t stack_size); - -/** - * @brief Deletes a cli command - * - * @param [in] cli pointer to cli instance - * @param [in] name command name - */ -void cli_delete_command(Cli* cli, const char* name); - -/** - * @brief Unregisters all external commands - * - * @param [in] cli pointer to the cli instance - */ -void cli_remove_external_commands(Cli* cli); - -/** - * @brief Reloads the list of externally available commands - * - * @param [in] cli pointer to cli instance - */ -void cli_enumerate_external_commands(Cli* cli); - -/** - * @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 1 byte 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); - -#ifdef __cplusplus -} -#endif diff --git a/applications/services/cli/cli_command_gpio.c b/applications/services/cli/cli_command_gpio.c index f37e6387f..f6337265d 100644 --- a/applications/services/cli/cli_command_gpio.c +++ b/applications/services/cli/cli_command_gpio.c @@ -4,6 +4,7 @@ #include #include #include +#include void cli_command_gpio_print_usage(void) { printf("Usage:\r\n"); diff --git a/applications/services/cli/cli_command_gpio.h b/applications/services/cli/cli_command_gpio.h index 0290949e0..c1911fb65 100644 --- a/applications/services/cli/cli_command_gpio.h +++ b/applications/services/cli/cli_command_gpio.h @@ -1,6 +1,5 @@ #pragma once -#include "cli_i.h" #include void cli_command_gpio(PipeSide* pipe, FuriString* args, void* context); diff --git a/applications/services/cli/cli_commands.h b/applications/services/cli/cli_commands.h deleted file mode 100644 index 77d9930af..000000000 --- a/applications/services/cli/cli_commands.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "cli.h" -#include - -void cli_commands_init(Cli* cli); - -#define PLUGIN_APP_ID "cli" -#define PLUGIN_API_VERSION 1 - -typedef struct { - char* name; - CliExecuteCallback execute_callback; - CliCommandFlag flags; - size_t stack_depth; -} CliCommandDescriptor; - -#define CLI_COMMAND_INTERFACE(name, execute_callback, flags, stack_depth) \ - static const CliCommandDescriptor cli_##name##_desc = { \ - #name, \ - &execute_callback, \ - flags, \ - stack_depth, \ - }; \ - \ - static const FlipperAppPluginDescriptor plugin_descriptor = { \ - .appid = PLUGIN_APP_ID, \ - .ep_api_version = PLUGIN_API_VERSION, \ - .entry_point = &cli_##name##_desc, \ - }; \ - \ - const FlipperAppPluginDescriptor* cli_##name##_ep(void) { \ - return &plugin_descriptor; \ - } diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_main_commands.c similarity index 82% rename from applications/services/cli/cli_commands.c rename to applications/services/cli/cli_main_commands.c index 723a4d556..f84c03d8a 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_main_commands.c @@ -1,7 +1,6 @@ -#include "cli_commands.h" +#include "cli_main_commands.h" #include "cli_command_gpio.h" -#include "cli_ansi.h" -#include "cli.h" +#include #include #include @@ -56,49 +55,6 @@ void cli_command_info(PipeSide* pipe, FuriString* args, void* context) { } } -void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { - UNUSED(pipe); - UNUSED(args); - UNUSED(context); - printf("Available commands:" ANSI_FG_GREEN); - - // count non-hidden commands - Cli* cli = furi_record_open(RECORD_CLI); - cli_lock_commands(cli); - CliCommandTree_t* commands = cli_get_commands(cli); - size_t commands_count = CliCommandTree_size(*commands); - - // create iterators starting at different positions - const size_t columns = 3; - const size_t commands_per_column = (commands_count / columns) + (commands_count % columns); - CliCommandTree_it_t iterators[columns]; - for(size_t c = 0; c < columns; c++) { - CliCommandTree_it(iterators[c], *commands); - for(size_t i = 0; i < c * commands_per_column; i++) - CliCommandTree_next(iterators[c]); - } - - // print commands - for(size_t r = 0; r < commands_per_column; r++) { - printf("\r\n"); - - for(size_t c = 0; c < columns; c++) { - if(!CliCommandTree_end_p(iterators[c])) { - const CliCommandTree_itref_t* item = CliCommandTree_cref(iterators[c]); - printf("%-30s", furi_string_get_cstr(*item->key_ptr)); - CliCommandTree_next(iterators[c]); - } - } - } - - printf(ANSI_RESET - "\r\nIf you just added a new 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_unlock_commands(cli); - furi_record_close(RECORD_CLI); -} - void cli_command_uptime(PipeSide* pipe, FuriString* args, void* context) { UNUSED(pipe); UNUSED(args); @@ -514,16 +470,6 @@ void cli_command_i2c(PipeSide* pipe, FuriString* args, void* context) { furi_hal_i2c_release(&furi_hal_i2c_handle_external); } -void cli_command_reload_external(PipeSide* pipe, FuriString* args, void* context) { - UNUSED(pipe); - UNUSED(args); - UNUSED(context); - Cli* cli = furi_record_open(RECORD_CLI); - cli_enumerate_external_commands(cli); - furi_record_close(RECORD_CLI); - printf("OK!"); -} - /** * Echoes any bytes it receives except ASCII ETX (0x03, Ctrl+C) */ @@ -545,27 +491,32 @@ void cli_command_echo(PipeSide* pipe, FuriString* args, void* context) { } } -void cli_commands_init(Cli* cli) { - cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); - cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); - cli_add_command(cli, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true); - cli_add_command( - cli, "reload_ext_cmds", CliCommandFlagDefault, cli_command_reload_external, NULL); +void cli_main_commands_init(CliRegistry* registry) { + cli_registry_add_command( + registry, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); + cli_registry_add_command(registry, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); + cli_registry_add_command( + registry, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true); - cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help, NULL); - cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL); + cli_registry_add_command( + registry, "uptime", CliCommandFlagParallelSafe, cli_command_uptime, NULL); + cli_registry_add_command(registry, "date", CliCommandFlagParallelSafe, cli_command_date, NULL); + cli_registry_add_command(registry, "log", CliCommandFlagParallelSafe, cli_command_log, NULL); + cli_registry_add_command(registry, "sysctl", CliCommandFlagDefault, cli_command_sysctl, NULL); + cli_registry_add_command(registry, "top", CliCommandFlagParallelSafe, cli_command_top, NULL); + cli_registry_add_command(registry, "free", CliCommandFlagParallelSafe, cli_command_free, NULL); + cli_registry_add_command( + registry, "free_blocks", CliCommandFlagDefault, cli_command_free_blocks, NULL); + cli_registry_add_command(registry, "echo", CliCommandFlagParallelSafe, cli_command_echo, NULL); - cli_add_command(cli, "uptime", CliCommandFlagDefault, cli_command_uptime, NULL); - cli_add_command(cli, "date", CliCommandFlagParallelSafe, cli_command_date, NULL); - cli_add_command(cli, "log", CliCommandFlagParallelSafe, cli_command_log, NULL); - cli_add_command(cli, "sysctl", CliCommandFlagDefault, cli_command_sysctl, NULL); - cli_add_command(cli, "top", CliCommandFlagParallelSafe, cli_command_top, NULL); - cli_add_command(cli, "free", CliCommandFlagParallelSafe, cli_command_free, NULL); - cli_add_command(cli, "free_blocks", CliCommandFlagParallelSafe, cli_command_free_blocks, NULL); - cli_add_command(cli, "echo", CliCommandFlagParallelSafe, cli_command_echo, NULL); - - cli_add_command(cli, "vibro", CliCommandFlagDefault, cli_command_vibro, NULL); - cli_add_command(cli, "led", CliCommandFlagDefault, cli_command_led, NULL); - cli_add_command(cli, "gpio", CliCommandFlagDefault, cli_command_gpio, NULL); - cli_add_command(cli, "i2c", CliCommandFlagDefault, cli_command_i2c, NULL); + cli_registry_add_command(registry, "vibro", CliCommandFlagDefault, cli_command_vibro, NULL); + cli_registry_add_command(registry, "led", CliCommandFlagDefault, cli_command_led, NULL); + cli_registry_add_command(registry, "gpio", CliCommandFlagDefault, cli_command_gpio, NULL); + cli_registry_add_command(registry, "i2c", CliCommandFlagDefault, cli_command_i2c, NULL); +} + +void cli_on_system_start(void) { + CliRegistry* registry = cli_registry_alloc(); + cli_main_commands_init(registry); + furi_record_create(RECORD_CLI, registry); } diff --git a/applications/services/cli/cli_main_commands.h b/applications/services/cli/cli_main_commands.h new file mode 100644 index 000000000..d8058f467 --- /dev/null +++ b/applications/services/cli/cli_main_commands.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +#define RECORD_CLI "cli" +#define CLI_APPID "cli" + +void cli_main_commands_init(CliRegistry* registry); diff --git a/applications/services/cli/cli_main_shell.c b/applications/services/cli/cli_main_shell.c new file mode 100644 index 000000000..7550bef04 --- /dev/null +++ b/applications/services/cli/cli_main_shell.c @@ -0,0 +1,46 @@ +#include "cli_main_shell.h" +#include "cli_main_commands.h" +#include +#include +#include + +void cli_main_motd(void* context) { + UNUSED(context); + printf(ANSI_FLIPPER_BRAND_ORANGE + "\r\n" + " _.-------.._ -,\r\n" + " .-\"```\"--..,,_/ /`-, -, \\ \r\n" + " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" + " / ,----/:/ /`\\ _\\~`_-\"` _;\r\n" + " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ \r\n" + " | | | 0 | | .-' ,/` /\r\n" + " | ,..\\ \\ ,.-\"` ,/` /\r\n" + " ; : `/`\"\"\\` ,/--==,/-----,\r\n" + " | `-...| -.___-Z:_______J...---;\r\n" + " : ` _-'\r\n" + " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" + "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" + "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" + "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" + "\r\n" ANSI_FG_BR_WHITE "Welcome to Flipper Zero Command Line Interface!\r\n" + "Read the manual: https://docs.flipper.net/development/cli\r\n" + "Run `help` or `?` to list available commands\r\n" + "\r\n" ANSI_RESET); + + const Version* firmware_version = furi_hal_version_get_firmware_version(); + if(firmware_version) { + printf( + "Firmware version: %s %s (%s%s built on %s)\r\n", + version_get_gitbranch(firmware_version), + version_get_version(firmware_version), + version_get_githash(firmware_version), + version_get_dirty_flag(firmware_version) ? "-dirty" : "", + version_get_builddate(firmware_version)); + } +} + +const CliCommandExternalConfig cli_main_ext_config = { + .search_directory = "/ext/apps_data/cli/plugins", + .fal_prefix = "cli_", + .appid = CLI_APPID, +}; diff --git a/applications/services/cli/cli_main_shell.h b/applications/services/cli/cli_main_shell.h new file mode 100644 index 000000000..576839990 --- /dev/null +++ b/applications/services/cli/cli_main_shell.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +void cli_main_motd(void* context); + +extern const CliCommandExternalConfig cli_main_ext_config; diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index 03ad6c610..f4b539e26 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -1,10 +1,12 @@ #include "cli_vcp.h" -#include "shell/cli_shell.h" #include #include #include #include #include +#include +#include "cli_main_shell.h" +#include "cli_main_commands.h" #define TAG "CliVcp" @@ -47,10 +49,12 @@ struct CliVcp { FuriHalUsbInterface* previous_interface; PipeSide* own_pipe; + PipeSide* shell_pipe; bool is_currently_transmitting; size_t previous_tx_length; - FuriThread* shell; + CliRegistry* main_registry; + CliShell* shell; }; // ============ @@ -210,13 +214,15 @@ static void cli_vcp_internal_event_happened(void* context) { // wait for previous shell to stop furi_check(!cli_vcp->own_pipe); if(cli_vcp->shell) { - furi_thread_join(cli_vcp->shell); - furi_thread_free(cli_vcp->shell); + cli_shell_join(cli_vcp->shell); + cli_shell_free(cli_vcp->shell); + pipe_free(cli_vcp->shell_pipe); } // start shell thread PipeSideBundle bundle = pipe_alloc(VCP_BUF_SIZE, 1); cli_vcp->own_pipe = bundle.alices_side; + cli_vcp->shell_pipe = bundle.bobs_side; pipe_attach_to_event_loop(cli_vcp->own_pipe, cli_vcp->event_loop); pipe_set_callback_context(cli_vcp->own_pipe, cli_vcp); pipe_set_data_arrived_callback( @@ -224,7 +230,9 @@ static void cli_vcp_internal_event_happened(void* context) { pipe_set_space_freed_callback( cli_vcp->own_pipe, cli_vcp_shell_ready, FuriEventLoopEventFlagEdge); furi_delay_ms(33); // we are too fast, minicom isn't ready yet - cli_vcp->shell = cli_shell_start(bundle.bobs_side); + cli_vcp->shell = cli_shell_alloc( + cli_main_motd, NULL, cli_vcp->shell_pipe, cli_vcp->main_registry, &cli_main_ext_config); + cli_shell_start(cli_vcp->shell); } if(event & CliVcpInternalEventRx) { @@ -260,6 +268,8 @@ static CliVcp* cli_vcp_alloc(void) { furi_event_loop_subscribe_thread_flags( cli_vcp->event_loop, cli_vcp_internal_event_happened, cli_vcp); + cli_vcp->main_registry = furi_record_open(RECORD_CLI); + return cli_vcp; } diff --git a/applications/services/cli/commands/hello_world.c b/applications/services/cli/commands/hello_world.c index 81be97298..b77f3e663 100644 --- a/applications/services/cli/commands/hello_world.c +++ b/applications/services/cli/commands/hello_world.c @@ -1,4 +1,4 @@ -#include "../cli_commands.h" +#include "../cli_main_commands.h" static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(pipe); @@ -7,4 +7,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { puts("Hello, World!"); } -CLI_COMMAND_INTERFACE(hello_world, execute, CliCommandFlagDefault, 768); +CLI_COMMAND_INTERFACE(hello_world, execute, CliCommandFlagParallelSafe, 768, CLI_APPID); diff --git a/applications/services/cli/commands/neofetch.c b/applications/services/cli/commands/neofetch.c index e652212eb..0e50a0d8d 100644 --- a/applications/services/cli/commands/neofetch.c +++ b/applications/services/cli/commands/neofetch.c @@ -1,4 +1,5 @@ -#include "../cli_commands.h" +#include "../cli_main_commands.h" +#include #include #include #include @@ -156,4 +157,4 @@ static void execute(PipeSide* pipe, FuriString* args, void* context) { #undef NEOFETCH_COLOR } -CLI_COMMAND_INTERFACE(neofetch, execute, CliCommandFlagDefault, 2048); +CLI_COMMAND_INTERFACE(neofetch, execute, CliCommandFlagParallelSafe, 2048, CLI_APPID); diff --git a/applications/services/cli/commands/subshell_demo.c b/applications/services/cli/commands/subshell_demo.c new file mode 100644 index 000000000..f0013c4a0 --- /dev/null +++ b/applications/services/cli/commands/subshell_demo.c @@ -0,0 +1,43 @@ +#include "../cli_main_commands.h" +#include +#include +#include + +#define RAINBOW_SUBCOMMAND \ + ANSI_FG_RED "s" ANSI_FG_YELLOW "u" ANSI_FG_BLUE "b" ANSI_FG_GREEN "c" ANSI_FG_MAGENTA \ + "o" ANSI_FG_RED "m" ANSI_FG_YELLOW "m" ANSI_FG_BLUE "a" ANSI_FG_GREEN \ + "n" ANSI_FG_MAGENTA "d" + +static void subcommand(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + UNUSED(context); + printf("This is a ✨" RAINBOW_SUBCOMMAND ANSI_RESET "✨!"); +} + +static void motd(void* context) { + UNUSED(context); + printf("\r\n"); + printf("+------------------------------------+\r\n"); + printf("| Hello world! |\r\n"); + printf("| This is the " ANSI_FG_GREEN "MOTD" ANSI_RESET " for our " ANSI_FG_BLUE + "subshell" ANSI_RESET "! |\r\n"); + printf("+------------------------------------+\r\n"); +} + +static void execute(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(args); + UNUSED(context); + CliRegistry* registry = cli_registry_alloc(); + cli_registry_add_command(registry, "subcommand", CliCommandFlagParallelSafe, subcommand, NULL); + + CliShell* shell = cli_shell_alloc(motd, NULL, pipe, registry, NULL); + cli_shell_set_prompt(shell, "subshell"); + cli_shell_start(shell); + cli_shell_join(shell); + + cli_shell_free(shell); + cli_registry_free(registry); +} + +CLI_COMMAND_INTERFACE(subshell_demo, execute, CliCommandFlagParallelSafe, 2048, CLI_APPID); diff --git a/applications/services/cli/shell/cli_shell.h b/applications/services/cli/shell/cli_shell.h deleted file mode 100644 index e60eefc77..000000000 --- a/applications/services/cli/shell/cli_shell.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define CLI_SHELL_STACK_SIZE (4 * 1024U) - -FuriThread* cli_shell_start(PipeSide* pipe); - -#ifdef __cplusplus -} -#endif diff --git a/applications/services/crypto/crypto_cli.c b/applications/services/crypto/crypto_cli.c index bfefaf0f1..32b4199f5 100644 --- a/applications/services/crypto/crypto_cli.c +++ b/applications/services/crypto/crypto_cli.c @@ -3,7 +3,9 @@ #include #include -#include +#include +#include +#include void crypto_cli_print_usage(void) { printf("Usage:\r\n"); @@ -319,8 +321,8 @@ static void crypto_cli(PipeSide* pipe, FuriString* args, void* context) { void crypto_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "crypto", CliCommandFlagDefault, crypto_cli, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "crypto", CliCommandFlagDefault, crypto_cli, NULL); furi_record_close(RECORD_CLI); #else UNUSED(crypto_cli); diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 0f6304823..36536b99f 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -1,6 +1,5 @@ #include "desktop_i.h" -#include #include #include diff --git a/applications/services/input/input.c b/applications/services/input/input.c index c77771f3e..d7e9f3d31 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -4,8 +4,9 @@ #include #include #include -#include #include +#include +#include #include #define INPUT_DEBOUNCE_TICKS_HALF (INPUT_DEBOUNCE_TICKS / 2) @@ -93,8 +94,10 @@ int32_t input_srv(void* p) { #endif #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "input", CliCommandFlagParallelSafe, input_cli, event_pubsub); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, "input", CliCommandFlagParallelSafe, input_cli, event_pubsub); + furi_record_close(RECORD_CLI); #endif InputPinState pin_states[input_pins_count]; diff --git a/applications/services/input/input_cli.c b/applications/services/input/input_cli.c index a34ec3c36..d5ea57418 100644 --- a/applications/services/input/input_cli.c +++ b/applications/services/input/input_cli.c @@ -1,7 +1,7 @@ #include "input.h" #include -#include +#include #include #include diff --git a/applications/services/loader/loader_cli.c b/applications/services/loader/loader_cli.c index 40312d8b3..265779e8f 100644 --- a/applications/services/loader/loader_cli.c +++ b/applications/services/loader/loader_cli.c @@ -1,7 +1,8 @@ #include "loader.h" #include -#include +#include +#include #include #include #include @@ -141,8 +142,9 @@ static void loader_cli(PipeSide* pipe, FuriString* args, void* context) { void loader_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, RECORD_LOADER, CliCommandFlagParallelSafe, loader_cli, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, RECORD_LOADER, CliCommandFlagParallelSafe, loader_cli, NULL); furi_record_close(RECORD_CLI); #else UNUSED(loader_cli); diff --git a/applications/services/power/power_cli.c b/applications/services/power/power_cli.c index f1771d9f1..f630cb2e8 100644 --- a/applications/services/power/power_cli.c +++ b/applications/services/power/power_cli.c @@ -1,7 +1,8 @@ #include "power_cli.h" #include -#include +#include +#include #include #include #include @@ -114,10 +115,8 @@ void power_cli(PipeSide* pipe, FuriString* args, void* context) { void power_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - - cli_add_command(cli, "power", CliCommandFlagParallelSafe, power_cli, NULL); - + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "power", CliCommandFlagParallelSafe, power_cli, NULL); furi_record_close(RECORD_CLI); #else UNUSED(power_cli); diff --git a/applications/services/rpc/rpc.c b/applications/services/rpc/rpc.c index 41d55841e..1a19348ff 100644 --- a/applications/services/rpc/rpc.c +++ b/applications/services/rpc/rpc.c @@ -9,7 +9,8 @@ #include -#include +#include +#include #include #include #include @@ -435,9 +436,14 @@ void rpc_on_system_start(void* p) { rpc->busy_mutex = furi_mutex_alloc(FuriMutexTypeNormal); - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command( - cli, "start_rpc_session", CliCommandFlagParallelSafe, rpc_cli_command_start_session, rpc); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, + "start_rpc_session", + CliCommandFlagParallelSafe, + rpc_cli_command_start_session, + rpc); + furi_record_close(RECORD_CLI); furi_record_create(RECORD_RPC, rpc); } diff --git a/applications/services/rpc/rpc_cli.c b/applications/services/rpc/rpc_cli.c index 3280fe702..fda059ec8 100644 --- a/applications/services/rpc/rpc_cli.c +++ b/applications/services/rpc/rpc_cli.c @@ -1,4 +1,5 @@ -#include +#include +#include #include #include #include diff --git a/applications/services/rpc/rpc_i.h b/applications/services/rpc/rpc_i.h index 51903a276..df1f17de4 100644 --- a/applications/services/rpc/rpc_i.h +++ b/applications/services/rpc/rpc_i.h @@ -5,7 +5,6 @@ #include #include #include -#include #include #ifdef __cplusplus diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 58b851926..2dab63e53 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -1,7 +1,9 @@ #include #include -#include +#include +#include +#include #include #include #include @@ -695,16 +697,15 @@ static void storage_cli_factory_reset(PipeSide* pipe, FuriString* args, void* co void storage_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command_ex( - cli, + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command( + registry, "storage", CliCommandFlagParallelSafe | CliCommandFlagUseShellThread, storage_cli, - NULL, - 512); - cli_add_command( - cli, "factory_reset", CliCommandFlagParallelSafe, storage_cli_factory_reset, NULL); + NULL); + cli_registry_add_command( + registry, "factory_reset", CliCommandFlagParallelSafe, storage_cli_factory_reset, NULL); furi_record_close(RECORD_CLI); #else UNUSED(storage_cli_factory_reset); diff --git a/applications/system/js_app/js_app.c b/applications/system/js_app/js_app.c index bdb2ccc28..3084b9b2a 100644 --- a/applications/system/js_app/js_app.c +++ b/applications/system/js_app/js_app.c @@ -4,7 +4,8 @@ #include "js_app_i.h" #include #include -#include +#include +#include #include #define TAG "JS app" @@ -209,8 +210,8 @@ void js_cli_execute(PipeSide* pipe, FuriString* args, void* context) { void js_app_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "js", CliCommandFlagDefault, js_cli_execute, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "js", CliCommandFlagDefault, js_cli_execute, NULL); furi_record_close(RECORD_CLI); #endif } diff --git a/applications/system/updater/cli/updater_cli.c b/applications/system/updater/cli/updater_cli.c index 01949269f..bad8f0cd6 100644 --- a/applications/system/updater/cli/updater_cli.c +++ b/applications/system/updater/cli/updater_cli.c @@ -1,7 +1,8 @@ #include #include -#include +#include +#include #include #include #include @@ -106,8 +107,8 @@ static void updater_start_app(void* context, uint32_t arg) { void updater_on_system_start(void) { #ifdef SRV_CLI - Cli* cli = (Cli*)furi_record_open(RECORD_CLI); - cli_add_command(cli, "update", CliCommandFlagDefault, updater_cli_ep, NULL); + CliRegistry* registry = furi_record_open(RECORD_CLI); + cli_registry_add_command(registry, "update", CliCommandFlagDefault, updater_cli_ep, NULL); furi_record_close(RECORD_CLI); #else UNUSED(updater_cli_ep); diff --git a/lib/toolbox/SConscript b/lib/toolbox/SConscript index 8a1c4a8c5..ad368e2a1 100644 --- a/lib/toolbox/SConscript +++ b/lib/toolbox/SConscript @@ -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"), diff --git a/applications/services/cli/cli_ansi.c b/lib/toolbox/cli/cli_ansi.c similarity index 100% rename from applications/services/cli/cli_ansi.c rename to lib/toolbox/cli/cli_ansi.c diff --git a/applications/services/cli/cli_ansi.h b/lib/toolbox/cli/cli_ansi.h similarity index 99% rename from applications/services/cli/cli_ansi.h rename to lib/toolbox/cli/cli_ansi.h index 20bf33d8e..04b6d7759 100644 --- a/applications/services/cli/cli_ansi.h +++ b/lib/toolbox/cli/cli_ansi.h @@ -1,6 +1,6 @@ #pragma once -#include "cli.h" +#include #ifdef __cplusplus extern "C" { diff --git a/lib/toolbox/cli/cli_command.c b/lib/toolbox/cli/cli_command.c new file mode 100644 index 000000000..a3c9ff292 --- /dev/null +++ b/lib/toolbox/cli/cli_command.c @@ -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); +} diff --git a/lib/toolbox/cli/cli_command.h b/lib/toolbox/cli/cli_command.h new file mode 100644 index 000000000..2d1d851d6 --- /dev/null +++ b/lib/toolbox/cli/cli_command.h @@ -0,0 +1,103 @@ +/** + * @file cli_command.h + * Command metadata and helpers + */ + +#pragma once + +#include +#include +#include + +#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; // +#include + +#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 ®istry->commands; +} diff --git a/lib/toolbox/cli/cli_registry.h b/lib/toolbox/cli/cli_registry.h new file mode 100644 index 000000000..44650e79b --- /dev/null +++ b/lib/toolbox/cli/cli_registry.h @@ -0,0 +1,92 @@ +/** + * @file cli_registry.h + * API for registering commands with a CLI shell + */ + +#pragma once + +#include +#include +#include +#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 diff --git a/applications/services/cli/cli_i.h b/lib/toolbox/cli/cli_registry_i.h similarity index 51% rename from applications/services/cli/cli_i.h rename to lib/toolbox/cli/cli_registry_i.h index 3e948c345..95b7c55da 100644 --- a/applications/services/cli/cli_i.h +++ b/lib/toolbox/cli/cli_registry_i.h @@ -1,5 +1,5 @@ /** - * @file cli_i.h + * @file cli_registry_i.h * Internal API for getting commands registered with the CLI */ @@ -7,21 +7,20 @@ #include #include -#include "cli.h" +#include "cli_registry.h" #ifdef __cplusplus extern "C" { #endif -#define CLI_BUILTIN_COMMAND_STACK_SIZE (3 * 1024U) -#define CLI_COMMANDS_PATH "/ext/apps_data/cli/plugins" +#define CLI_BUILTIN_COMMAND_STACK_SIZE (4 * 1024U) typedef struct { void* context; // @@ -36,10 +36,17 @@ typedef enum { } CliShellStorageEvent; struct CliShell { - Cli* cli; - FuriEventLoop* event_loop; + // 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; @@ -51,7 +58,7 @@ struct CliShell { }; typedef struct { - CliCommand* command; + CliRegistryCommand* command; PipeSide* pipe; FuriString* args; } CliCommandThreadData; @@ -73,9 +80,62 @@ static void cli_shell_detach_pipe(CliShell* cli_shell) { furi_thread_set_stdout_callback(NULL, NULL); } -// ========= -// Execution -// ========= +// ================= +// 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; @@ -99,12 +159,13 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { furi_string_right(args, space + 1); PluginManager* plugin_manager = NULL; - Loader* loader = NULL; - CliCommand command_data; + Loader* loader = furi_record_open(RECORD_LOADER); + bool loader_locked = false; + CliRegistryCommand command_data; do { // find handler - if(!cli_get_command(cli_shell->cli, command_name, &command_data)) { + 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)); @@ -113,10 +174,14 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { // load external command if(command_data.flags & CliCommandFlagExternal) { - plugin_manager = - plugin_manager_alloc(PLUGIN_APP_ID, PLUGIN_API_VERSION, firmware_api_interface); + 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/cli_%s.fal", CLI_COMMANDS_PATH, furi_string_get_cstr(command_name)); + "%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)); @@ -141,9 +206,8 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { // lock loader if(!(command_data.flags & CliCommandFlagParallelSafe)) { - loader = furi_record_open(RECORD_LOADER); - bool success = loader_lock(loader); - if(!success) { + 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; @@ -177,13 +241,17 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { furi_string_free(args); // unlock loader - if(loader) loader_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 // ============== @@ -210,9 +278,9 @@ static void cli_shell_storage_internal_event(FuriEventLoopObject* object, void* furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk); if(event == CliShellStorageEventMount) { - cli_enumerate_external_commands(cli_shell->cli); + cli_registry_reload_external_commands(cli_shell->registry, cli_shell->ext_config); } else if(event == CliShellStorageEventUnmount) { - cli_remove_external_commands(cli_shell->cli); + cli_registry_remove_external_commands(cli_shell->registry); } else { furi_crash(); } @@ -265,113 +333,97 @@ static void cli_shell_timer_expired(void* context) { cli_shell, cli_ansi_parser_feed_timeout(cli_shell->ansi_parser)); } -// ======= -// Helpers -// ======= +// =========== +// Thread code +// =========== -static CliShell* cli_shell_alloc(PipeSide* pipe) { - CliShell* cli_shell = malloc(sizeof(CliShell)); +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); - cli_shell->cli = furi_record_open(RECORD_CLI); - cli_shell->ansi_parser = cli_ansi_parser_alloc(); - cli_shell->pipe = pipe; + 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); + } - cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell); - cli_shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc( - cli_shell->cli, cli_shell, cli_shell->components[CliShellComponentLine]); + shell->components[CliShellComponentLine] = cli_shell_line_alloc(shell); + shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc( + shell->registry, shell, shell->components[CliShellComponentLine]); - cli_shell->event_loop = furi_event_loop_alloc(); - cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc( - cli_shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, cli_shell); + shell->ansi_parser = cli_ansi_parser_alloc(); - cli_shell_install_pipe(cli_shell); + 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); - cli_shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent)); + shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent)); furi_event_loop_subscribe_message_queue( - cli_shell->event_loop, - cli_shell->storage_event_queue, + shell->event_loop, + shell->storage_event_queue, FuriEventLoopEventIn, cli_shell_storage_internal_event, - cli_shell); - cli_shell->storage = furi_record_open(RECORD_STORAGE); - cli_shell->storage_subscription = furi_pubsub_subscribe( - storage_get_pubsub(cli_shell->storage), cli_shell_storage_event, cli_shell); + shell); + shell->storage = furi_record_open(RECORD_STORAGE); + shell->storage_subscription = + furi_pubsub_subscribe(storage_get_pubsub(shell->storage), cli_shell_storage_event, shell); - return cli_shell; + cli_shell_install_pipe(shell); } -static void cli_shell_free(CliShell* cli_shell) { - furi_pubsub_unsubscribe( - storage_get_pubsub(cli_shell->storage), cli_shell->storage_subscription); +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(cli_shell->event_loop, cli_shell->storage_event_queue); - furi_message_queue_free(cli_shell->storage_event_queue); + furi_event_loop_unsubscribe(shell->event_loop, shell->storage_event_queue); + furi_message_queue_free(shell->storage_event_queue); - cli_shell_completions_free(cli_shell->components[CliShellComponentCompletions]); - cli_shell_line_free(cli_shell->components[CliShellComponentLine]); + cli_shell_completions_free(shell->components[CliShellComponentCompletions]); + cli_shell_line_free(shell->components[CliShellComponentLine]); - cli_shell_detach_pipe(cli_shell); - furi_event_loop_timer_free(cli_shell->ansi_parsing_timer); - furi_event_loop_free(cli_shell->event_loop); - pipe_free(cli_shell->pipe); - cli_ansi_parser_free(cli_shell->ansi_parser); - furi_record_close(RECORD_CLI); - free(cli_shell); -} - -static void cli_shell_motd(void) { - printf(ANSI_FLIPPER_BRAND_ORANGE - "\r\n" - " _.-------.._ -,\r\n" - " .-\"```\"--..,,_/ /`-, -, \\ \r\n" - " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" - " / ,----/:/ /`\\ _\\~`_-\"` _;\r\n" - " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ \r\n" - " | | | 0 | | .-' ,/` /\r\n" - " | ,..\\ \\ ,.-\"` ,/` /\r\n" - " ; : `/`\"\"\\` ,/--==,/-----,\r\n" - " | `-...| -.___-Z:_______J...---;\r\n" - " : ` _-'\r\n" - " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" - "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" - "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" - "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" - "\r\n" ANSI_FG_BR_WHITE "Welcome to Flipper Zero Command Line Interface!\r\n" - "Read the manual: https://docs.flipper.net/development/cli\r\n" - "Run `help` or `?` to list available commands\r\n" - "\r\n" ANSI_RESET); - - const Version* firmware_version = furi_hal_version_get_firmware_version(); - if(firmware_version) { - printf( - "Firmware version: %s %s (%s%s built on %s)\r\n", - version_get_gitbranch(firmware_version), - version_get_version(firmware_version), - version_get_githash(firmware_version), - version_get_dirty_flag(firmware_version) ? "-dirty" : "", - version_get_builddate(firmware_version)); - } + 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) { - PipeSide* pipe = 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(pipe) == PipeStateBroken) return 0; - - CliShell* cli_shell = cli_shell_alloc(pipe); + if(pipe_state(shell->pipe) == PipeStateBroken) return 0; + cli_shell_init(shell); FURI_LOG_D(TAG, "Started"); - cli_shell_motd(); - cli_shell_line_prompt(cli_shell->components[CliShellComponentLine]); - furi_event_loop_run(cli_shell->event_loop); + 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_free(cli_shell); + cli_shell_deinit(shell); return 0; } @@ -379,9 +431,48 @@ static int32_t cli_shell_thread(void* context) { // Public API // ========== -FuriThread* cli_shell_start(PipeSide* pipe) { - FuriThread* thread = - furi_thread_alloc_ex("CliShell", CLI_SHELL_STACK_SIZE, cli_shell_thread, pipe); - furi_thread_start(thread); - return thread; +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; } diff --git a/lib/toolbox/cli/shell/cli_shell.h b/lib/toolbox/cli/shell/cli_shell.h new file mode 100644 index 000000000..74f71273e --- /dev/null +++ b/lib/toolbox/cli/shell/cli_shell.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#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 diff --git a/applications/services/cli/shell/cli_shell_completions.c b/lib/toolbox/cli/shell/cli_shell_completions.c similarity index 97% rename from applications/services/cli/shell/cli_shell_completions.c rename to lib/toolbox/cli/shell/cli_shell_completions.c index 0b32c18a2..823f91fb9 100644 --- a/applications/services/cli/shell/cli_shell_completions.c +++ b/lib/toolbox/cli/shell/cli_shell_completions.c @@ -4,7 +4,7 @@ ARRAY_DEF(CommandCompletions, FuriString*, FURI_STRING_OPLIST); // -V524 #define M_OPL_CommandCompletions_t() ARRAY_OPLIST(CommandCompletions) struct CliShellCompletions { - Cli* cli; + CliRegistry* registry; CliShell* shell; CliShellLine* line; CommandCompletions_t variants; @@ -45,10 +45,11 @@ typedef struct { // Public API // ========== -CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line) { +CliShellCompletions* + cli_shell_completions_alloc(CliRegistry* registry, CliShell* shell, CliShellLine* line) { CliShellCompletions* completions = malloc(sizeof(CliShellCompletions)); - completions->cli = cli; + completions->registry = registry; completions->shell = shell; completions->line = line; CommandCompletions_init(completions->variants); @@ -108,8 +109,9 @@ void cli_shell_completions_fill_variants(CliShellCompletions* completions) { furi_string_left(input, segment.length); if(segment.type == CliShellCompletionSegmentTypeCommand) { - cli_lock_commands(completions->cli); - CliCommandTree_t* commands = cli_get_commands(completions->cli); + 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; @@ -117,7 +119,7 @@ void cli_shell_completions_fill_variants(CliShellCompletions* completions) { CommandCompletions_push_back(completions->variants, command_name); } } - cli_unlock_commands(completions->cli); + cli_registry_unlock(registry); } else { // support removed, might reimplement in the future diff --git a/applications/services/cli/shell/cli_shell_completions.h b/lib/toolbox/cli/shell/cli_shell_completions.h similarity index 67% rename from applications/services/cli/shell/cli_shell_completions.h rename to lib/toolbox/cli/shell/cli_shell_completions.h index 6353bde71..d49a1982d 100644 --- a/applications/services/cli/shell/cli_shell_completions.h +++ b/lib/toolbox/cli/shell/cli_shell_completions.h @@ -4,8 +4,8 @@ #include #include "cli_shell_i.h" #include "cli_shell_line.h" -#include "../cli.h" -#include "../cli_i.h" +#include "../cli_registry.h" +#include "../cli_registry_i.h" #ifdef __cplusplus extern "C" { @@ -13,7 +13,8 @@ extern "C" { typedef struct CliShellCompletions CliShellCompletions; -CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line); +CliShellCompletions* + cli_shell_completions_alloc(CliRegistry* registry, CliShell* shell, CliShellLine* line); void cli_shell_completions_free(CliShellCompletions* completions); diff --git a/applications/services/cli/shell/cli_shell_i.h b/lib/toolbox/cli/shell/cli_shell_i.h similarity index 91% rename from applications/services/cli/shell/cli_shell_i.h rename to lib/toolbox/cli/shell/cli_shell_i.h index e8eae92c6..0b676b7de 100644 --- a/applications/services/cli/shell/cli_shell_i.h +++ b/lib/toolbox/cli/shell/cli_shell_i.h @@ -27,6 +27,8 @@ typedef struct { void cli_shell_execute_command(CliShell* cli_shell, FuriString* command); +const char* cli_shell_get_prompt(CliShell* cli_shell); + #ifdef __cplusplus } #endif diff --git a/applications/services/cli/shell/cli_shell_line.c b/lib/toolbox/cli/shell/cli_shell_line.c similarity index 95% rename from applications/services/cli/shell/cli_shell_line.c rename to lib/toolbox/cli/shell/cli_shell_line.c index 959cd0b3b..4826ba252 100644 --- a/applications/services/cli/shell/cli_shell_line.c +++ b/lib/toolbox/cli/shell/cli_shell_line.c @@ -8,6 +8,7 @@ struct CliShellLine { FuriString* history[HISTORY_DEPTH]; size_t history_entries; CliShell* shell; + bool about_to_exit; }; // ========== @@ -39,14 +40,16 @@ FuriString* cli_shell_line_get_editing(CliShellLine* line) { return line->history[0]; } -size_t cli_shell_line_prompt_length(CliShellLine* line) { - UNUSED(line); - return strlen(">: "); -} - void cli_shell_line_format_prompt(CliShellLine* line, char* buf, size_t length) { UNUSED(line); - snprintf(buf, length - 1, ">: "); + 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) { @@ -65,6 +68,10 @@ void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line) { } } +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; } @@ -188,8 +195,7 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) { printf("\r\n"); cli_shell_execute_command(line->shell, command_copy); furi_string_free(command_copy); - - cli_shell_line_prompt(line); + if(!line->about_to_exit) cli_shell_line_prompt(line); return true; } @@ -202,10 +208,13 @@ static bool cli_shell_line_input_up_down(CliKeyCombo combo, void* context) { // 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" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END), + 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); diff --git a/applications/services/cli/shell/cli_shell_line.h b/lib/toolbox/cli/shell/cli_shell_line.h similarity index 94% rename from applications/services/cli/shell/cli_shell_line.h rename to lib/toolbox/cli/shell/cli_shell_line.h index 1e4b9e32a..e40a12bd6 100644 --- a/applications/services/cli/shell/cli_shell_line.h +++ b/lib/toolbox/cli/shell/cli_shell_line.h @@ -33,6 +33,8 @@ void cli_shell_line_set_line_position(CliShellLine* line, size_t position); */ 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 diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 72512a46f..ce47fe5c2 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,9 +1,7 @@ entry,status,name,type,params -Version,+,84.1,, +Version,+,85.0,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, -Header,+,applications/services/cli/cli.h,, -Header,+,applications/services/cli/cli_ansi.h,, Header,+,applications/services/cli/cli_vcp.h,, Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, @@ -153,6 +151,10 @@ Header,+,lib/stm32wb_hal/Inc/stm32wbxx_ll_wwdg.h,, Header,+,lib/toolbox/api_lock.h,, Header,+,lib/toolbox/args.h,, Header,+,lib/toolbox/bit_buffer.h,, +Header,+,lib/toolbox/cli/cli_ansi.h,, +Header,+,lib/toolbox/cli/cli_command.h,, +Header,+,lib/toolbox/cli/cli_registry.h,, +Header,+,lib/toolbox/cli/shell/cli_shell.h,, Header,+,lib/toolbox/compress.h,, Header,+,lib/toolbox/crc32_calc.h,, Header,+,lib/toolbox/dir_walk.h,, @@ -779,17 +781,24 @@ Function,-,ceill,long double,long double Function,-,cfree,void,void* Function,-,clearerr,void,FILE* Function,-,clearerr_unlocked,void,FILE* -Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*" -Function,+,cli_add_command_ex,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*, size_t" Function,+,cli_ansi_parser_alloc,CliAnsiParser*, Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* Function,+,cli_ansi_parser_free,void,CliAnsiParser* -Function,+,cli_delete_command,void,"Cli*, const char*" -Function,+,cli_enumerate_external_commands,void,Cli* Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" -Function,+,cli_remove_external_commands,void,Cli* +Function,+,cli_registry_add_command,void,"CliRegistry*, const char*, CliCommandFlag, CliCommandExecuteCallback, void*" +Function,+,cli_registry_add_command_ex,void,"CliRegistry*, const char*, CliCommandFlag, CliCommandExecuteCallback, void*, size_t" +Function,+,cli_registry_alloc,CliRegistry*, +Function,+,cli_registry_delete_command,void,"CliRegistry*, const char*" +Function,+,cli_registry_free,void,CliRegistry* +Function,+,cli_registry_reload_external_commands,void,"CliRegistry*, const CliCommandExternalConfig*" +Function,+,cli_registry_remove_external_commands,void,CliRegistry* +Function,+,cli_shell_alloc,CliShell*,"CliShellMotd, void*, PipeSide*, CliRegistry*, const CliCommandExternalConfig*" +Function,+,cli_shell_free,void,CliShell* +Function,+,cli_shell_join,void,CliShell* +Function,+,cli_shell_set_prompt,void,"CliShell*, const char*" +Function,+,cli_shell_start,void,CliShell* Function,+,cli_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 73ad2dcd5..e19281e2a 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,10 +1,8 @@ entry,status,name,type,params -Version,+,84.1,, +Version,+,85.0,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, -Header,+,applications/services/cli/cli.h,, -Header,+,applications/services/cli/cli_ansi.h,, Header,+,applications/services/cli/cli_vcp.h,, Header,+,applications/services/dialogs/dialogs.h,, Header,+,applications/services/dolphin/dolphin.h,, @@ -225,6 +223,10 @@ Header,+,lib/subghz/transmitter.h,, Header,+,lib/toolbox/api_lock.h,, Header,+,lib/toolbox/args.h,, Header,+,lib/toolbox/bit_buffer.h,, +Header,+,lib/toolbox/cli/cli_ansi.h,, +Header,+,lib/toolbox/cli/cli_command.h,, +Header,+,lib/toolbox/cli/cli_registry.h,, +Header,+,lib/toolbox/cli/shell/cli_shell.h,, Header,+,lib/toolbox/compress.h,, Header,+,lib/toolbox/crc32_calc.h,, Header,+,lib/toolbox/dir_walk.h,, @@ -856,17 +858,24 @@ Function,-,ceill,long double,long double Function,-,cfree,void,void* Function,-,clearerr,void,FILE* Function,-,clearerr_unlocked,void,FILE* -Function,+,cli_add_command,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*" -Function,+,cli_add_command_ex,void,"Cli*, const char*, CliCommandFlag, CliExecuteCallback, void*, size_t" Function,+,cli_ansi_parser_alloc,CliAnsiParser*, Function,+,cli_ansi_parser_feed,CliAnsiParserResult,"CliAnsiParser*, char" Function,+,cli_ansi_parser_feed_timeout,CliAnsiParserResult,CliAnsiParser* Function,+,cli_ansi_parser_free,void,CliAnsiParser* -Function,+,cli_delete_command,void,"Cli*, const char*" -Function,+,cli_enumerate_external_commands,void,Cli* Function,+,cli_is_pipe_broken_or_is_etx_next_char,_Bool,PipeSide* Function,+,cli_print_usage,void,"const char*, const char*, const char*" -Function,+,cli_remove_external_commands,void,Cli* +Function,+,cli_registry_add_command,void,"CliRegistry*, const char*, CliCommandFlag, CliCommandExecuteCallback, void*" +Function,+,cli_registry_add_command_ex,void,"CliRegistry*, const char*, CliCommandFlag, CliCommandExecuteCallback, void*, size_t" +Function,+,cli_registry_alloc,CliRegistry*, +Function,+,cli_registry_delete_command,void,"CliRegistry*, const char*" +Function,+,cli_registry_free,void,CliRegistry* +Function,+,cli_registry_reload_external_commands,void,"CliRegistry*, const CliCommandExternalConfig*" +Function,+,cli_registry_remove_external_commands,void,CliRegistry* +Function,+,cli_shell_alloc,CliShell*,"CliShellMotd, void*, PipeSide*, CliRegistry*, const CliCommandExternalConfig*" +Function,+,cli_shell_free,void,CliShell* +Function,+,cli_shell_join,void,CliShell* +Function,+,cli_shell_set_prompt,void,"CliShell*, const char*" +Function,+,cli_shell_start,void,CliShell* Function,+,cli_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*"