diff --git a/CHANGELOG.md b/CHANGELOG.md index ebde54991..8effc6289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,7 +93,10 @@ - Infrared: - OFW: Add Fujitsu ASTG12LVCC to AC Universal Remote (by @KereruA0i) - OFW: Increase max carrier limit to 1000000 (by @skotopes) -- OFW: CLI: New CLI architecture, some text formatting, better stability and less RAM usage (by @portasynthinca3) +- CLI: + - OFW: New CLI architecture, some text formatting, better stability and less RAM usage (by @portasynthinca3) + - OFW: Autocomplete and more keyboard shortcuts (by @portasynthinca3) + - OFW: Improved loading of CLI commands from SD card with fals and threads (by @portasynthinca3) - OFW: Power: Added OTG controls to Power service, remembers OTG when unplugging USB (by @Astrrra & @skotopes) - OFW: GUI: Updated Button Panel with more options for button handling (by @Akiva-Cohen) - Furi: diff --git a/applications/debug/unit_tests/application.fam b/applications/debug/unit_tests/application.fam index f92d7e66f..05e834402 100644 --- a/applications/debug/unit_tests/application.fam +++ b/applications/debug/unit_tests/application.fam @@ -4,7 +4,7 @@ App( entry_point="unit_tests_on_system_start", sources=["unit_tests.c", "test_runner.c", "unit_test_api_table.cpp"], cdefines=["APP_UNIT_TESTS"], - requires=["system_settings", "subghz_start"], + requires=["system_settings", "cli_subghz"], provides=["delay_test"], resources="resources", order=100, diff --git a/applications/debug/unit_tests/tests/pipe/pipe_test.c b/applications/debug/unit_tests/tests/pipe/pipe_test.c index 4eae39636..f0227b353 100644 --- a/applications/debug/unit_tests/tests/pipe/pipe_test.c +++ b/applications/debug/unit_tests/tests/pipe/pipe_test.c @@ -70,10 +70,9 @@ static void on_data_arrived(PipeSide* pipe, void* context) { } static void on_space_freed(PipeSide* pipe, void* context) { + UNUSED(pipe); AncillaryThreadContext* ctx = context; ctx->flag |= TestFlagSpaceFreed; - const char* message = "Hi!"; - pipe_send(pipe, message, strlen(message)); } static void on_became_broken(PipeSide* pipe, void* context) { @@ -119,16 +118,10 @@ MU_TEST(pipe_test_event_loop) { size_t size = pipe_receive(alice, buffer_1, strlen(message)); buffer_1[size] = 0; - char buffer_2[16]; - const char* expected_reply = "Hi!"; - size = pipe_receive(alice, buffer_2, strlen(expected_reply)); - buffer_2[size] = 0; - pipe_free(alice); furi_thread_join(thread); mu_assert_string_eq(message, buffer_1); - mu_assert_string_eq(expected_reply, buffer_2); mu_assert_int_eq( TestFlagDataArrived | TestFlagSpaceFreed | TestFlagBecameBroken, furi_thread_get_return_code(thread)); diff --git a/applications/main/application.fam b/applications/main/application.fam index 6859909de..82c297c28 100644 --- a/applications/main/application.fam +++ b/applications/main/application.fam @@ -23,12 +23,6 @@ App( apptype=FlipperAppType.METAPACKAGE, provides=[ "cli", - "ibutton_start", - "onewire_start", - "subghz_start", "subghz_load_extended_settings", - "infrared_start", - "lfrfid_start", - "nfc_start", ], ) diff --git a/applications/main/bad_usb/application.fam b/applications/main/bad_usb/application.fam index 5847f1100..737e6b52b 100644 --- a/applications/main/bad_usb/application.fam +++ b/applications/main/bad_usb/application.fam @@ -9,6 +9,7 @@ App( icon="A_BadUsb_14", order=70, resources="resources", + fap_libs=["assets"], fap_icon="icon.png", fap_category="Tools", ) diff --git a/applications/main/gpio/application.fam b/applications/main/gpio/application.fam index 5bae286b2..607d97a27 100644 --- a/applications/main/gpio/application.fam +++ b/applications/main/gpio/application.fam @@ -6,6 +6,7 @@ App( stack_size=2 * 1024, icon="A_GPIO_14", order=50, + fap_libs=["assets"], fap_icon="icon.png", fap_category="GPIO", ) diff --git a/applications/main/ibutton/application.fam b/applications/main/ibutton/application.fam index 8fe147968..84afe0f02 100644 --- a/applications/main/ibutton/application.fam +++ b/applications/main/ibutton/application.fam @@ -7,24 +7,16 @@ App( icon="A_iButton_14", stack_size=2 * 1024, order=60, + fap_libs=["assets"], fap_icon="icon.png", fap_category="iButton", ) App( - appid="ibutton_cli", + appid="cli_ikey", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="ibutton_cli_plugin_ep", + entry_point="cli_ikey_ep", requires=["cli"], sources=["ibutton_cli.c"], ) - -App( - appid="ibutton_start", - apptype=FlipperAppType.STARTUP, - targets=["f7"], - entry_point="ibutton_on_system_start", - sources=["ibutton_cli.c"], - order=60, -) diff --git a/applications/main/ibutton/ibutton_cli.c b/applications/main/ibutton/ibutton_cli.c index 7dbc20281..2ff0860bb 100644 --- a/applications/main/ibutton/ibutton_cli.c +++ b/applications/main/ibutton/ibutton_cli.c @@ -204,7 +204,7 @@ void ibutton_cli_emulate(PipeSide* pipe, FuriString* args) { while(!cli_is_pipe_broken_or_is_etx_next_char(pipe)) { furi_delay_ms(100); - } + }; } while(false); @@ -216,8 +216,7 @@ void ibutton_cli_emulate(PipeSide* pipe, FuriString* args) { ibutton_protocols_free(protocols); } -void ibutton_cli(PipeSide* pipe, FuriString* args, void* context) { - UNUSED(pipe); +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -241,15 +240,4 @@ void ibutton_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -#include -CLI_PLUGIN_WRAPPER("ibutton", ibutton_cli) - -void ibutton_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "ikey", CliCommandFlagDefault, ibutton_cli_wrapper, cli); - furi_record_close(RECORD_CLI); -#else - UNUSED(ibutton_cli); -#endif -} +CLI_COMMAND_INTERFACE(ikey, execute, CliCommandFlagDefault, 1024); diff --git a/applications/main/infrared/application.fam b/applications/main/infrared/application.fam index 8c86c78f3..79b3fdbfa 100644 --- a/applications/main/infrared/application.fam +++ b/applications/main/infrared/application.fam @@ -9,15 +9,16 @@ App( order=40, sources=["*.c", "!infrared_cli.c"], resources="resources", + fap_libs=["assets"], fap_icon="icon.png", fap_category="Infrared", ) App( - appid="infrared_cli", + appid="cli_ir", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="infrared_cli_start_ir_plugin_ep", + entry_point="cli_ir_ep", requires=["cli"], sources=[ "infrared_cli.c", @@ -25,12 +26,3 @@ App( "infrared_signal.c", ], ) - -App( - appid="infrared_start", - apptype=FlipperAppType.STARTUP, - targets=["f7"], - entry_point="infrared_on_system_start", - sources=["infrared_cli.c"], - order=20, -) diff --git a/applications/main/infrared/infrared_cli.c b/applications/main/infrared/infrared_cli.c index fd78af0a3..0a57d9990 100644 --- a/applications/main/infrared/infrared_cli.c +++ b/applications/main/infrared/infrared_cli.c @@ -526,7 +526,7 @@ static void infrared_cli_process_universal(PipeSide* pipe, FuriString* args) { furi_string_free(arg2); } -static void infrared_cli_start_ir(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); if(furi_hal_infrared_is_busy()) { printf("INFRARED is busy. Exiting."); @@ -554,15 +554,4 @@ static void infrared_cli_start_ir(PipeSide* pipe, FuriString* args, void* contex furi_string_free(command); } -#include -CLI_PLUGIN_WRAPPER("infrared", infrared_cli_start_ir) - -void infrared_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = (Cli*)furi_record_open(RECORD_CLI); - cli_add_command(cli, "ir", CliCommandFlagDefault, infrared_cli_start_ir_wrapper, NULL); - furi_record_close(RECORD_CLI); -#else - UNUSED(infrared_cli_start_ir); -#endif -} +CLI_COMMAND_INTERFACE(ir, execute, CliCommandFlagDefault, 2048); diff --git a/applications/main/lfrfid/application.fam b/applications/main/lfrfid/application.fam index d2abf300d..d6fca74f4 100644 --- a/applications/main/lfrfid/application.fam +++ b/applications/main/lfrfid/application.fam @@ -7,24 +7,16 @@ App( icon="A_125khz_14", stack_size=2 * 1024, order=20, + fap_libs=["assets"], fap_icon="icon.png", fap_category="RFID", ) App( - appid="lfrfid_cli", + appid="cli_rfid", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="lfrfid_cli_plugin_ep", + entry_point="cli_rfid_ep", requires=["cli"], sources=["lfrfid_cli.c"], ) - -App( - appid="lfrfid_start", - targets=["f7"], - apptype=FlipperAppType.STARTUP, - entry_point="lfrfid_on_system_start", - sources=["lfrfid_cli.c"], - order=50, -) diff --git a/applications/main/lfrfid/lfrfid_cli.c b/applications/main/lfrfid/lfrfid_cli.c index 4d8d8760c..cefc55f65 100644 --- a/applications/main/lfrfid/lfrfid_cli.c +++ b/applications/main/lfrfid/lfrfid_cli.c @@ -536,7 +536,7 @@ static void lfrfid_cli_raw_emulate(PipeSide* pipe, FuriString* args) { furi_string_free(filepath); } -static void lfrfid_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -566,11 +566,4 @@ static void lfrfid_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -#include -CLI_PLUGIN_WRAPPER("lfrfid", lfrfid_cli) - -void lfrfid_on_system_start(void) { - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "rfid", CliCommandFlagDefault, lfrfid_cli_wrapper, NULL); - furi_record_close(RECORD_CLI); -} +CLI_COMMAND_INTERFACE(rfid, execute, CliCommandFlagDefault, 2048); diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index a07e0946f..576c66008 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -350,19 +350,10 @@ App( ) App( - appid="nfc_cli", + appid="cli_nfc", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="nfc_cli_plugin_ep", + entry_point="cli_nfc_ep", requires=["cli"], sources=["nfc_cli.c"], ) - -App( - appid="nfc_start", - targets=["f7"], - apptype=FlipperAppType.STARTUP, - entry_point="nfc_on_system_start", - sources=["nfc_cli.c"], - order=30, -) diff --git a/applications/main/nfc/nfc_cli.c b/applications/main/nfc/nfc_cli.c index c1bfd5a8d..fd5598fc6 100644 --- a/applications/main/nfc/nfc_cli.c +++ b/applications/main/nfc/nfc_cli.c @@ -42,7 +42,7 @@ static void nfc_cli_field(PipeSide* pipe, FuriString* args) { furi_hal_nfc_release(); } -static void nfc_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -65,15 +65,4 @@ static void nfc_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -#include -CLI_PLUGIN_WRAPPER("nfc", nfc_cli) - -void nfc_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "nfc", CliCommandFlagDefault, nfc_cli_wrapper, NULL); - furi_record_close(RECORD_CLI); -#else - UNUSED(nfc_cli); -#endif -} +CLI_COMMAND_INTERFACE(nfc, execute, CliCommandFlagDefault, 1024); diff --git a/applications/main/onewire/application.fam b/applications/main/onewire/application.fam index ef756ba56..e38bcdfef 100644 --- a/applications/main/onewire/application.fam +++ b/applications/main/onewire/application.fam @@ -1,16 +1,8 @@ App( - appid="onewire_cli", + appid="cli_onewire", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="onewire_cli_plugin_ep", + entry_point="cli_onewire_ep", requires=["cli"], sources=["onewire_cli.c"], ) - -App( - appid="onewire_start", - apptype=FlipperAppType.STARTUP, - entry_point="onewire_on_system_start", - sources=["onewire_cli.c"], - order=60, -) diff --git a/applications/main/onewire/onewire_cli.c b/applications/main/onewire/onewire_cli.c index 59c1ef083..83bbc6770 100644 --- a/applications/main/onewire/onewire_cli.c +++ b/applications/main/onewire/onewire_cli.c @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -45,7 +46,7 @@ static void onewire_cli_search(PipeSide* pipe) { furi_record_close(RECORD_POWER); } -static void onewire_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -63,15 +64,4 @@ static void onewire_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -#include -CLI_PLUGIN_WRAPPER("onewire", onewire_cli) - -void onewire_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "onewire", CliCommandFlagDefault, onewire_cli_wrapper, cli); - furi_record_close(RECORD_CLI); -#else - UNUSED(onewire_cli); -#endif -} +CLI_COMMAND_INTERFACE(onewire, execute, CliCommandFlagDefault, 1024); diff --git a/applications/main/subghz/application.fam b/applications/main/subghz/application.fam index 3c9625ed3..fab4ec3e8 100644 --- a/applications/main/subghz/application.fam +++ b/applications/main/subghz/application.fam @@ -18,7 +18,7 @@ App( # ], requires=["region"], resources="resources", - fap_libs=["hwdrivers"], + fap_libs=["assets", "hwdrivers"], fap_icon="icon.png", fap_category="Sub-GHz", sdk_headers=["subghz_fap.h"], @@ -45,24 +45,14 @@ App( ) App( - appid="subghz_cli", + appid="cli_subghz", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="subghz_cli_command_plugin_ep", + entry_point="cli_subghz_ep", requires=["cli"], sources=["subghz_cli.c", "helpers/subghz_chat.c"], ) - -App( - appid="subghz_start", - targets=["f7"], - apptype=FlipperAppType.STARTUP, - entry_point="subghz_on_system_start", - # sources=["subghz_cli.c"], - order=40, -) - App( appid="subghz_load_extended_settings", targets=["f7"], diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index 396db7c21..31541ac8f 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -1115,7 +1115,7 @@ static void subghz_cli_command_chat(PipeSide* pipe, FuriString* args) { printf("\r\nExit chat\r\n"); } -static void subghz_cli_command(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { FuriString* cmd = furi_string_alloc(); do { @@ -1182,24 +1182,4 @@ static void subghz_cli_command(PipeSide* pipe, FuriString* args, void* context) furi_string_free(cmd); } -#include -CLI_PLUGIN_WRAPPER("subghz", subghz_cli_command) - -static void subghz_cli_command_chat_wrapper(PipeSide* pipe, FuriString* args, void* context) { - furi_string_replace_at(args, 0, 0, "chat "); - subghz_cli_command_wrapper(pipe, args, context); -} - -void subghz_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - - cli_add_command(cli, "subghz", CliCommandFlagDefault, subghz_cli_command_wrapper, NULL); - cli_add_command(cli, "chat", CliCommandFlagDefault, subghz_cli_command_chat_wrapper, NULL); - - furi_record_close(RECORD_CLI); -#else - UNUSED(subghz_cli_command); - UNUSED(subghz_cli_command_chat_wrapper); -#endif -} +CLI_COMMAND_INTERFACE(subghz, execute, CliCommandFlagDefault, 2048); diff --git a/applications/main/u2f/application.fam b/applications/main/u2f/application.fam index f1b454e5c..5e0cde736 100644 --- a/applications/main/u2f/application.fam +++ b/applications/main/u2f/application.fam @@ -7,7 +7,7 @@ App( icon="A_U2F_14", order=80, resources="resources", - fap_libs=["mbedtls"], + fap_libs=["assets", "mbedtls"], fap_category="USB", fap_icon="icon.png", ) diff --git a/applications/services/application.fam b/applications/services/application.fam index 0c61d717a..f007b2ffe 100644 --- a/applications/services/application.fam +++ b/applications/services/application.fam @@ -4,7 +4,6 @@ App( apptype=FlipperAppType.METAPACKAGE, provides=[ "cli_vcp", - "crypto_start", "rpc_start", "expansion_start", "bt", diff --git a/applications/services/bt/application.fam b/applications/services/bt/application.fam index 5e77b2a84..fe3a3c7a1 100644 --- a/applications/services/bt/application.fam +++ b/applications/services/bt/application.fam @@ -9,7 +9,6 @@ App( "dialogs", ], provides=[ - "bt_start", "bt_settings", ], stack_size=1 * 1024, @@ -18,17 +17,10 @@ App( ) App( - appid="bt_cli", + appid="cli_bt", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="bt_cli_plugin_ep", + entry_point="cli_bt_ep", requires=["cli"], sources=["bt_cli.c", "bt_service/bt_settings_api.c"], ) - -App( - appid="bt_start", - apptype=FlipperAppType.STARTUP, - entry_point="bt_on_system_start", - order=70, -) diff --git a/applications/services/bt/bt_cli.c b/applications/services/bt/bt_cli.c index e44d31737..b58ec3d77 100644 --- a/applications/services/bt/bt_cli.c +++ b/applications/services/bt/bt_cli.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -180,7 +181,7 @@ static void bt_cli_print_usage(void) { } } -static void bt_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); Bt* bt = furi_record_open(RECORD_BT); @@ -228,15 +229,4 @@ static void bt_cli(PipeSide* pipe, FuriString* args, void* context) { furi_record_close(RECORD_BT); } -#include -CLI_PLUGIN_WRAPPER("bt", bt_cli) - -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_wrapper, NULL); - furi_record_close(RECORD_CLI); -#else - UNUSED(bt_cli); -#endif -} +CLI_COMMAND_INTERFACE(bt, execute, CliCommandFlagDefault, 1024); diff --git a/applications/services/cli/application.fam b/applications/services/cli/application.fam index aca39ffc0..36f2eb09c 100644 --- a/applications/services/cli/application.fam +++ b/applications/services/cli/application.fam @@ -6,6 +6,7 @@ App( sources=[ "cli.c", "shell/cli_shell.c", + "shell/cli_shell_completions.c", "shell/cli_shell_line.c", "cli_commands.c", "cli_command_gpio.c", @@ -34,82 +35,107 @@ App( ) App( - appid="src_cli", + appid="cli_hello_world", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_hello_world_ep", + requires=["cli"], + sources=["commands/hello_world.c"], +) + +App( + appid="cli_neofetch", + apptype=FlipperAppType.PLUGIN, + entry_point="cli_neofetch_ep", + requires=["cli"], + sources=["commands/neofetch.c"], +) + +App( + appid="cli_src", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="cli_command_src_plugin_ep", + entry_point="cli_src_ep", requires=["cli"], sources=["cli_commands.c"], ) App( - appid="neofetch_cli", + appid="cli_uptime", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="cli_command_neofetch_plugin_ep", + entry_point="cli_uptime_ep", requires=["cli"], sources=["cli_commands.c"], ) App( - appid="uptime_cli", + appid="cli_date", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="cli_command_uptime_plugin_ep", + entry_point="cli_date_ep", requires=["cli"], sources=["cli_commands.c"], ) App( - appid="date_cli", + appid="cli_sysctl", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="cli_command_date_plugin_ep", + entry_point="cli_sysctl_ep", requires=["cli"], sources=["cli_commands.c"], ) App( - appid="sysctl_cli", + appid="cli_top", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="cli_command_sysctl_plugin_ep", + entry_point="cli_top_ep", requires=["cli"], sources=["cli_commands.c"], ) App( - appid="vibro_cli", + appid="cli_vibro", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="cli_command_vibro_plugin_ep", + entry_point="cli_vibro_ep", requires=["cli"], sources=["cli_commands.c"], ) App( - appid="led_cli", + appid="cli_led", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="cli_command_led_plugin_ep", + entry_point="cli_led_ep", requires=["cli"], sources=["cli_commands.c"], ) App( - appid="gpio_cli", + appid="cli_gpio", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="cli_command_gpio_plugin_ep", + entry_point="cli_gpio_ep", requires=["cli"], sources=["cli_commands.c", "cli_command_gpio.c"], ) App( - appid="i2c_cli", + appid="cli_i2c", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="cli_command_i2c_plugin_ep", + entry_point="cli_i2c_ep", + requires=["cli"], + sources=["cli_commands.c"], +) + +App( + appid="cli_clear", + targets=["f7"], + apptype=FlipperAppType.PLUGIN, + entry_point="cli_clear_ep", requires=["cli"], sources=["cli_commands.c"], ) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index 34da32596..2bfce3a63 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -4,11 +4,6 @@ #include "cli_ansi.h" #include -#include -#include -#include -#include - #define TAG "cli" struct Cli { @@ -19,7 +14,7 @@ struct Cli { Cli* cli_alloc(void) { Cli* cli = malloc(sizeof(Cli)); CliCommandTree_init(cli->commands); - cli->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + cli->mutex = furi_mutex_alloc(FuriMutexTypeRecursive); return cli; } @@ -43,6 +38,9 @@ void cli_add_command_ex( 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 @@ -91,18 +89,75 @@ bool cli_get_command(Cli* cli, FuriString* command, CliCommand* result) { 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_assert(cli); + furi_check(cli); furi_check(furi_mutex_acquire(cli->mutex, FuriWaitForever) == FuriStatusOk); } void cli_unlock_commands(Cli* cli) { - furi_assert(cli); - furi_mutex_release(cli->mutex); + furi_check(cli); + furi_check(furi_mutex_release(cli->mutex) == FuriStatusOk); } CliCommandTree_t* cli_get_commands(Cli* cli) { - furi_assert(cli); + furi_check(cli); + furi_check(furi_mutex_get_owner(cli->mutex) == furi_thread_get_current_id()); return &cli->commands; } @@ -124,24 +179,6 @@ void cli_print_usage(const char* cmd, const char* usage, const char* arg) { 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); } - -void cli_plugin_wrapper(const char* name, PipeSide* pipe, FuriString* args, void* context) { - PluginManager* manager = - plugin_manager_alloc(CLI_PLUGIN_APP_ID, CLI_PLUGIN_API_VERSION, firmware_api_interface); - FuriString* path = - furi_string_alloc_printf(EXT_PATH("apps_data/cli/plugins/%s_cli.fal"), name); - PluginManagerError error = plugin_manager_load_single(manager, furi_string_get_cstr(path)); - if(error == PluginManagerErrorNone) { - const CliExecuteCallback handler = plugin_manager_get_ep(manager, 0); - handler(pipe, args, context); - } else { - printf( - "CLI plugin '%s' failed (code %" PRIu16 "), reinstall firmware or check logs\r\n", - name, - error); - } - furi_string_free(path); - plugin_manager_free(manager); -} diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h index 211e89d88..2352e1806 100644 --- a/applications/services/cli/cli.h +++ b/applications/services/cli/cli.h @@ -20,6 +20,13 @@ typedef enum { 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 */ @@ -87,6 +94,20 @@ void cli_add_command_ex( */ 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 * diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index fb8a641a3..7b73f8a6f 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -14,7 +14,6 @@ #include #include #include -#include // Close to ISO, `date +'%Y-%m-%d %H:%M:%S %u'` #define CLI_DATE_FORMAT "%.4d-%.2d-%.2d %.2d:%.2d:%.2d %d" @@ -57,158 +56,6 @@ void cli_command_info(PipeSide* pipe, FuriString* args, void* context) { } } -// Lil Easter egg :> -void cli_command_neofetch(PipeSide* pipe, FuriString* args, void* context) { - UNUSED(pipe); - UNUSED(args); - UNUSED(context); - - static const char* const neofetch_logo[] = { - " _.-------.._ -,", - " .-\"```\"--..,,_/ /`-, -, \\ ", - " .:\" /:/ /'\\ \\ ,_..., `. | |", - " / ,----/:/ /`\\ _\\~`_-\"` _;", - " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ ", - " | | | 0 | | .-' ,/` /", - " | ,..\\ \\ ,.-\"` ,/` /", - "; : `/`\"\"\\` ,/--==,/-----,", - "| `-...| -.___-Z:_______J...---;", - ": ` _-'", - }; -#define NEOFETCH_COLOR ANSI_FLIPPER_BRAND_ORANGE - - // Determine logo parameters - size_t logo_height = COUNT_OF(neofetch_logo), logo_width = 0; - for(size_t i = 0; i < logo_height; i++) - logo_width = MAX(logo_width, strlen(neofetch_logo[i])); - logo_width += 4; // space between logo and info - - // Format hostname delimiter - const size_t size_of_hostname = 4 + strlen(furi_hal_version_get_name_ptr()); - char delimiter[64]; - memset(delimiter, '-', size_of_hostname); - delimiter[size_of_hostname] = '\0'; - - // Get heap info - size_t heap_total = memmgr_get_total_heap(); - size_t heap_used = heap_total - memmgr_get_free_heap(); - uint16_t heap_percent = (100 * heap_used) / heap_total; - - // Get storage info - Storage* storage = furi_record_open(RECORD_STORAGE); - uint64_t ext_total, ext_free, ext_used, ext_percent; - storage_common_fs_info(storage, "/ext", &ext_total, &ext_free); - ext_used = ext_total - ext_free; - ext_percent = (100 * ext_used) / ext_total; - ext_used /= 1024 * 1024; - ext_total /= 1024 * 1024; - furi_record_close(RECORD_STORAGE); - - // Get battery info - uint16_t charge_percent = furi_hal_power_get_pct(); - const char* charge_state; - if(furi_hal_power_is_charging()) { - if((charge_percent < 100) && (!furi_hal_power_is_charging_done())) { - charge_state = "charging"; - } else { - charge_state = "charged"; - } - } else { - charge_state = "discharging"; - } - - // Get misc info - uint32_t uptime = furi_get_tick() / furi_kernel_get_tick_frequency(); - const Version* version = version_get(); - uint16_t major, minor; - furi_hal_info_get_api_version(&major, &minor); - - // Print ASCII art with info - const size_t info_height = 16; - for(size_t i = 0; i < MAX(logo_height, info_height); i++) { - printf(NEOFETCH_COLOR "%-*s", logo_width, (i < logo_height) ? neofetch_logo[i] : ""); - switch(i) { - case 0: // you@ - printf("you" ANSI_RESET "@" NEOFETCH_COLOR "%s", furi_hal_version_get_name_ptr()); - break; - case 1: // delimiter - printf(ANSI_RESET "%s", delimiter); - break; - case 2: // OS: FURI (SDK .) - printf( - "OS" ANSI_RESET ": FURI %s %s %s %s (SDK %hu.%hu)", - version_get_version(version), - version_get_gitbranch(version), - version_get_version(version), - version_get_githash(version), - major, - minor); - break; - case 3: // Host: - printf( - "Host" ANSI_RESET ": %s %s", - furi_hal_version_get_model_code(), - furi_hal_version_get_device_name_ptr()); - break; - case 4: // Kernel: FreeRTOS .. - printf( - "Kernel" ANSI_RESET ": FreeRTOS %d.%d.%d", - tskKERNEL_VERSION_MAJOR, - tskKERNEL_VERSION_MINOR, - tskKERNEL_VERSION_BUILD); - break; - case 5: // Uptime: ?h?m?s - printf( - "Uptime" ANSI_RESET ": %luh%lum%lus", - uptime / 60 / 60, - uptime / 60 % 60, - uptime % 60); - break; - case 6: // ST7567 128x64 @ 1 bpp in 1.4" - printf("Display" ANSI_RESET ": ST7567 128x64 @ 1 bpp in 1.4\""); - break; - case 7: // DE: GuiSrv - printf("DE" ANSI_RESET ": GuiSrv"); - break; - case 8: // Shell: CliSrv - printf("Shell" ANSI_RESET ": CliSrv"); - break; - case 9: // CPU: STM32WB55RG @ 64 MHz - printf("CPU" ANSI_RESET ": STM32WB55RG @ 64 MHz"); - break; - case 10: // Memory: / B (??%) - printf( - "Memory" ANSI_RESET ": %zu / %zu B (%hu%%)", heap_used, heap_total, heap_percent); - break; - case 11: // Disk (/ext): / MiB (??%) - printf( - "Disk (/ext)" ANSI_RESET ": %llu / %llu MiB (%llu%%)", - ext_used, - ext_total, - ext_percent); - break; - case 12: // Battery: ??% () - printf("Battery" ANSI_RESET ": %hu%% (%s)" ANSI_RESET, charge_percent, charge_state); - break; - case 13: // empty space - break; - case 14: // Colors (line 1) - for(size_t j = 30; j <= 37; j++) - printf("\e[%dm███", j); - break; - case 15: // Colors (line 2) - for(size_t j = 90; j <= 97; j++) - printf("\e[%dm███", j); - break; - default: - break; - } - printf("\r\n"); - } - printf(ANSI_RESET); -#undef NEOFETCH_COLOR -} - void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { UNUSED(pipe); UNUSED(args); @@ -244,6 +91,8 @@ void cli_command_help(PipeSide* pipe, FuriString* args, void* context) { } } + 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); @@ -718,6 +567,16 @@ 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) */ @@ -746,44 +605,29 @@ void cli_command_clear(PipeSide* pipe, FuriString* args, void* context) { printf("\e[2J\e[H"); } -CLI_PLUGIN_WRAPPER("src", cli_command_src) -CLI_PLUGIN_WRAPPER("neofetch", cli_command_neofetch) -CLI_PLUGIN_WRAPPER("uptime", cli_command_uptime) -CLI_PLUGIN_WRAPPER("date", cli_command_date) -CLI_PLUGIN_WRAPPER("sysctl", cli_command_sysctl) -CLI_PLUGIN_WRAPPER("vibro", cli_command_vibro) -CLI_PLUGIN_WRAPPER("led", cli_command_led) -CLI_PLUGIN_WRAPPER("gpio", cli_command_gpio) -CLI_PLUGIN_WRAPPER("i2c", cli_command_i2c) -CLI_PLUGIN_WRAPPER("clear", cli_command_clear) - 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, "source", CliCommandFlagParallelSafe, cli_command_src_wrapper, NULL); - cli_add_command(cli, "src", CliCommandFlagParallelSafe, cli_command_src_wrapper, NULL); cli_add_command( - cli, "neofetch", CliCommandFlagParallelSafe, cli_command_neofetch_wrapper, NULL); + cli, "reload_ext_cmds", CliCommandFlagDefault, cli_command_reload_external, NULL); cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help, NULL); cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL); - cli_add_command(cli, "uptime", CliCommandFlagDefault, cli_command_uptime_wrapper, NULL); - cli_add_command(cli, "date", CliCommandFlagParallelSafe, cli_command_date_wrapper, NULL); cli_add_command(cli, "log", CliCommandFlagParallelSafe, cli_command_log, NULL); - cli_add_command(cli, "l", CliCommandFlagParallelSafe, cli_command_log, NULL); - cli_add_command(cli, "sysctl", CliCommandFlagDefault, cli_command_sysctl_wrapper, 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_wrapper, NULL); - cli_add_command(cli, "led", CliCommandFlagDefault, cli_command_led_wrapper, NULL); - cli_add_command(cli, "gpio", CliCommandFlagDefault, cli_command_gpio_wrapper, NULL); - cli_add_command(cli, "i2c", CliCommandFlagDefault, cli_command_i2c_wrapper, NULL); - - cli_add_command(cli, "clear", CliCommandFlagParallelSafe, cli_command_clear, NULL); - cli_add_command(cli, "cls", CliCommandFlagParallelSafe, cli_command_clear, NULL); } + +CLI_COMMAND_INTERFACE(src, cli_command_src, CliCommandFlagParallelSafe, 768); +CLI_COMMAND_INTERFACE(uptime, cli_command_uptime, CliCommandFlagDefault, 768); +CLI_COMMAND_INTERFACE(date, cli_command_date, CliCommandFlagParallelSafe, 768); +CLI_COMMAND_INTERFACE(sysctl, cli_command_sysctl, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(top, cli_command_top, CliCommandFlagParallelSafe, 1024); +CLI_COMMAND_INTERFACE(vibro, cli_command_vibro, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(led, cli_command_led, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(gpio, cli_command_gpio, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(i2c, cli_command_i2c, CliCommandFlagDefault, 1024); +CLI_COMMAND_INTERFACE(clear, cli_command_clear, CliCommandFlagParallelSafe, 768); diff --git a/applications/services/cli/cli_commands.h b/applications/services/cli/cli_commands.h index 77d9930af..0066dd647 100644 --- a/applications/services/cli/cli_commands.h +++ b/applications/services/cli/cli_commands.h @@ -23,12 +23,12 @@ typedef struct { stack_depth, \ }; \ \ - static const FlipperAppPluginDescriptor plugin_descriptor = { \ + static const FlipperAppPluginDescriptor plugin_descriptor_##name = { \ .appid = PLUGIN_APP_ID, \ .ep_api_version = PLUGIN_API_VERSION, \ .entry_point = &cli_##name##_desc, \ }; \ \ const FlipperAppPluginDescriptor* cli_##name##_ep(void) { \ - return &plugin_descriptor; \ + return &plugin_descriptor_##name; \ } diff --git a/applications/services/cli/cli_i.h b/applications/services/cli/cli_i.h index 9e4311206..3e948c345 100644 --- a/applications/services/cli/cli_i.h +++ b/applications/services/cli/cli_i.h @@ -14,6 +14,7 @@ extern "C" { #endif #define CLI_BUILTIN_COMMAND_STACK_SIZE (3 * 1024U) +#define CLI_COMMANDS_PATH "/ext/apps_data/cli/plugins" typedef struct { void* context; // -#define CLI_PLUGIN_APP_ID "cli" -#define CLI_PLUGIN_API_VERSION 1 -#define CLI_PLUGIN_WRAPPER(plugin_name_without_cli_suffix, cli_command_callback) \ - void cli_command_callback##_wrapper(PipeSide* pipe, FuriString* args, void* context) { \ - cli_plugin_wrapper(plugin_name_without_cli_suffix, pipe, args, context); \ - } \ - static const FlipperAppPluginDescriptor cli_command_callback##_plugin_descriptor = { \ - .appid = CLI_PLUGIN_APP_ID, \ - .ep_api_version = CLI_PLUGIN_API_VERSION, \ - .entry_point = &cli_command_callback, \ - }; \ - const FlipperAppPluginDescriptor* cli_command_callback##_plugin_ep(void) { \ - UNUSED(cli_command_callback##_wrapper); \ - return &cli_command_callback##_plugin_descriptor; \ - } - #ifdef __cplusplus } #endif diff --git a/applications/services/cli/commands/hello_world.c b/applications/services/cli/commands/hello_world.c new file mode 100644 index 000000000..81be97298 --- /dev/null +++ b/applications/services/cli/commands/hello_world.c @@ -0,0 +1,10 @@ +#include "../cli_commands.h" + +static void execute(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + UNUSED(context); + puts("Hello, World!"); +} + +CLI_COMMAND_INTERFACE(hello_world, execute, CliCommandFlagDefault, 768); diff --git a/applications/services/cli/commands/neofetch.c b/applications/services/cli/commands/neofetch.c new file mode 100644 index 000000000..e652212eb --- /dev/null +++ b/applications/services/cli/commands/neofetch.c @@ -0,0 +1,159 @@ +#include "../cli_commands.h" +#include +#include +#include +#include +#include + +static void execute(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(pipe); + UNUSED(args); + UNUSED(context); + + static const char* const neofetch_logo[] = { + " _.-------.._ -,", + " .-\"```\"--..,,_/ /`-, -, \\ ", + " .:\" /:/ /'\\ \\ ,_..., `. | |", + " / ,----/:/ /`\\ _\\~`_-\"` _;", + " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ ", + " | | | 0 | | .-' ,/` /", + " | ,..\\ \\ ,.-\"` ,/` /", + "; : `/`\"\"\\` ,/--==,/-----,", + "| `-...| -.___-Z:_______J...---;", + ": ` _-'", + }; +#define NEOFETCH_COLOR ANSI_FLIPPER_BRAND_ORANGE + + // Determine logo parameters + size_t logo_height = COUNT_OF(neofetch_logo), logo_width = 0; + for(size_t i = 0; i < logo_height; i++) + logo_width = MAX(logo_width, strlen(neofetch_logo[i])); + logo_width += 4; // space between logo and info + + // Format hostname delimiter + const size_t size_of_hostname = 4 + strlen(furi_hal_version_get_name_ptr()); + char delimiter[64]; + memset(delimiter, '-', size_of_hostname); + delimiter[size_of_hostname] = '\0'; + + // Get heap info + size_t heap_total = memmgr_get_total_heap(); + size_t heap_used = heap_total - memmgr_get_free_heap(); + uint16_t heap_percent = (100 * heap_used) / heap_total; + + // Get storage info + Storage* storage = furi_record_open(RECORD_STORAGE); + uint64_t ext_total, ext_free, ext_used, ext_percent; + storage_common_fs_info(storage, "/ext", &ext_total, &ext_free); + ext_used = ext_total - ext_free; + ext_percent = (100 * ext_used) / ext_total; + ext_used /= 1024 * 1024; + ext_total /= 1024 * 1024; + furi_record_close(RECORD_STORAGE); + + // Get battery info + uint16_t charge_percent = furi_hal_power_get_pct(); + const char* charge_state; + if(furi_hal_power_is_charging()) { + if((charge_percent < 100) && (!furi_hal_power_is_charging_done())) { + charge_state = "charging"; + } else { + charge_state = "charged"; + } + } else { + charge_state = "discharging"; + } + + // Get misc info + uint32_t uptime = furi_get_tick() / furi_kernel_get_tick_frequency(); + const Version* version = version_get(); + uint16_t major, minor; + furi_hal_info_get_api_version(&major, &minor); + + // Print ASCII art with info + const size_t info_height = 16; + for(size_t i = 0; i < MAX(logo_height, info_height); i++) { + printf(NEOFETCH_COLOR "%-*s", logo_width, (i < logo_height) ? neofetch_logo[i] : ""); + switch(i) { + case 0: // you@ + printf("you" ANSI_RESET "@" NEOFETCH_COLOR "%s", furi_hal_version_get_name_ptr()); + break; + case 1: // delimiter + printf(ANSI_RESET "%s", delimiter); + break; + case 2: // OS: FURI (SDK .) + printf( + "OS" ANSI_RESET ": FURI %s %s %s %s (SDK %hu.%hu)", + version_get_version(version), + version_get_gitbranch(version), + version_get_version(version), + version_get_githash(version), + major, + minor); + break; + case 3: // Host: + printf( + "Host" ANSI_RESET ": %s %s", + furi_hal_version_get_model_code(), + furi_hal_version_get_device_name_ptr()); + break; + case 4: // Kernel: FreeRTOS .. + printf( + "Kernel" ANSI_RESET ": FreeRTOS %d.%d.%d", + tskKERNEL_VERSION_MAJOR, + tskKERNEL_VERSION_MINOR, + tskKERNEL_VERSION_BUILD); + break; + case 5: // Uptime: ?h?m?s + printf( + "Uptime" ANSI_RESET ": %luh%lum%lus", + uptime / 60 / 60, + uptime / 60 % 60, + uptime % 60); + break; + case 6: // ST7567 128x64 @ 1 bpp in 1.4" + printf("Display" ANSI_RESET ": ST7567 128x64 @ 1 bpp in 1.4\""); + break; + case 7: // DE: GuiSrv + printf("DE" ANSI_RESET ": GuiSrv"); + break; + case 8: // Shell: CliSrv + printf("Shell" ANSI_RESET ": CliShell"); + break; + case 9: // CPU: STM32WB55RG @ 64 MHz + printf("CPU" ANSI_RESET ": STM32WB55RG @ 64 MHz"); + break; + case 10: // Memory: / B (??%) + printf( + "Memory" ANSI_RESET ": %zu / %zu B (%hu%%)", heap_used, heap_total, heap_percent); + break; + case 11: // Disk (/ext): / MiB (??%) + printf( + "Disk (/ext)" ANSI_RESET ": %llu / %llu MiB (%llu%%)", + ext_used, + ext_total, + ext_percent); + break; + case 12: // Battery: ??% () + printf("Battery" ANSI_RESET ": %hu%% (%s)" ANSI_RESET, charge_percent, charge_state); + break; + case 13: // empty space + break; + case 14: // Colors (line 1) + for(size_t j = 30; j <= 37; j++) + printf("\e[%dm███", j); + break; + case 15: // Colors (line 2) + for(size_t j = 90; j <= 97; j++) + printf("\e[%dm███", j); + break; + default: + break; + } + printf("\r\n"); + } + printf(ANSI_RESET); +#undef NEOFETCH_COLOR +} + +CLI_COMMAND_INTERFACE(neofetch, execute, CliCommandFlagDefault, 2048); diff --git a/applications/services/cli/shell/cli_shell.c b/applications/services/cli/shell/cli_shell.c index 62cbbd403..554d36682 100644 --- a/applications/services/cli/shell/cli_shell.c +++ b/applications/services/cli/shell/cli_shell.c @@ -4,6 +4,7 @@ #include "../cli_i.h" #include "../cli_commands.h" #include "cli_shell_line.h" +#include "cli_shell_completions.h" #include #include #include @@ -11,21 +12,29 @@ #include #include #include +#include #define TAG "CliShell" #define ANSI_TIMEOUT_MS 10 typedef enum { + CliShellComponentCompletions, CliShellComponentLine, CliShellComponentMAX, //pipe); + pipe_attach_to_event_loop(cli_shell->pipe, cli_shell->event_loop); + pipe_set_callback_context(cli_shell->pipe, cli_shell); + pipe_set_data_arrived_callback(cli_shell->pipe, cli_shell_data_available, 0); + pipe_set_broken_callback(cli_shell->pipe, cli_shell_pipe_broken, 0); +} + +static void cli_shell_detach_pipe(CliShell* cli_shell) { + pipe_detach_from_event_loop(cli_shell->pipe); + furi_thread_set_stdin_callback(NULL, NULL); + furi_thread_set_stdout_callback(NULL, NULL); +} + // ========= // Execution // ========= +static int32_t cli_command_thread(void* context) { + CliCommandThreadData* thread_data = context; + if(!(thread_data->command->flags & CliCommandFlagDontAttachStdio)) + pipe_install_as_stdio(thread_data->pipe); + + thread_data->command->execute_callback( + thread_data->pipe, thread_data->args, thread_data->command->context); + + fflush(stdout); + return 0; +} + +static size_t cli_shell_string_distance(const char* s1, const char* s2) { + size_t distance = 0; + + while(*s1 && *s2) { + if(*s1++ != *s2++) distance++; + } + while(*s1++) + distance++; + while(*s2++) + distance++; + + return distance; +} + +static void + cli_shell_find_similar_command(CliShell* cli_shell, const char* input, FuriString* suggestion) { + size_t min_distance = (size_t)-1; + const size_t max_allowed = (strlen(input) + 1) / 2; + furi_string_reset(suggestion); + + cli_lock_commands(cli_shell->cli); + CliCommandTree_t* commands = cli_get_commands(cli_shell->cli); + for + M_EACH(registered_command, *commands, CliCommandTree_t) { + const char* command_name = furi_string_get_cstr(*registered_command->key_ptr); + const size_t distance = cli_shell_string_distance(input, command_name); + if(distance < min_distance && distance <= max_allowed) { + min_distance = distance; + furi_string_set(suggestion, command_name); + } + } + cli_unlock_commands(cli_shell->cli); +} + void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { // split command into command and args size_t space = furi_string_search_char(command, ' '); @@ -56,18 +132,59 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { FuriString* args = furi_string_alloc_set(command); furi_string_right(args, space + 1); + PluginManager* plugin_manager = NULL; Loader* loader = NULL; CliCommand command_data; do { // find handler if(!cli_get_command(cli_shell->cli, command_name, &command_data)) { - printf( - ANSI_FG_RED "could not find command `%s`, try `help`" ANSI_RESET, - furi_string_get_cstr(command_name)); + FuriString* suggestion = furi_string_alloc(); + cli_shell_find_similar_command( + cli_shell, furi_string_get_cstr(command_name), suggestion); + if(furi_string_empty(suggestion)) { + printf( + ANSI_FG_RED "could not find command `%s`, try `help`" ANSI_RESET, + furi_string_get_cstr(command_name)); + } else { + printf( + ANSI_FG_RED + "could not find command `%s`, did you mean `%s`? Use `help` to list all available commands" ANSI_RESET, + furi_string_get_cstr(command_name), + furi_string_get_cstr(suggestion)); + } + furi_string_free(suggestion); break; } + // load external command + if(command_data.flags & CliCommandFlagExternal) { + plugin_manager = + plugin_manager_alloc(PLUGIN_APP_ID, PLUGIN_API_VERSION, firmware_api_interface); + FuriString* path = furi_string_alloc_printf( + "%s/cli_%s.fal", CLI_COMMANDS_PATH, furi_string_get_cstr(command_name)); + uint32_t plugin_cnt_last = plugin_manager_get_count(plugin_manager); + PluginManagerError error = + plugin_manager_load_single(plugin_manager, furi_string_get_cstr(path)); + furi_string_free(path); + + if(error != PluginManagerErrorNone) { + printf(ANSI_FG_RED "failed to load external command" ANSI_RESET); + break; + } + + const CliCommandDescriptor* plugin = + plugin_manager_get_ep(plugin_manager, plugin_cnt_last); + furi_assert(plugin); + furi_check(furi_string_cmp_str(command_name, plugin->name) == 0); + command_data.execute_callback = plugin->execute_callback; + command_data.flags = plugin->flags | CliCommandFlagExternal; + command_data.stack_depth = plugin->stack_depth; + + // external commands have to run in an external thread + furi_check(!(command_data.flags & CliCommandFlagUseShellThread)); + } + // lock loader if(!(command_data.flags & CliCommandFlagParallelSafe)) { loader = furi_record_open(RECORD_LOADER); @@ -79,7 +196,27 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { } } - command_data.execute_callback(cli_shell->pipe, args, command_data.context); + if(command_data.flags & CliCommandFlagUseShellThread) { + // run command in this thread + command_data.execute_callback(cli_shell->pipe, args, command_data.context); + } else { + // run command in separate thread + cli_shell_detach_pipe(cli_shell); + CliCommandThreadData thread_data = { + .command = &command_data, + .pipe = cli_shell->pipe, + .args = args, + }; + FuriThread* thread = furi_thread_alloc_ex( + furi_string_get_cstr(command_name), + command_data.stack_depth, + cli_command_thread, + &thread_data); + furi_thread_start(thread); + furi_thread_join(thread); + furi_thread_free(thread); + cli_shell_install_pipe(cli_shell); + } } while(0); furi_string_free(command_name); @@ -88,13 +225,51 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) { // unlock loader if(loader) loader_unlock(loader); furi_record_close(RECORD_LOADER); + + // unload external command + if(plugin_manager) plugin_manager_free(plugin_manager); } // ============== // Event handlers // ============== -static void cli_shell_process_key(CliShell* cli_shell, CliKeyCombo key_combo) { +static void cli_shell_storage_event(const void* message, void* context) { + CliShell* cli_shell = context; + const StorageEvent* event = message; + + if(event->type == StorageEventTypeCardMount) { + CliShellStorageEvent cli_event = CliShellStorageEventMount; + furi_check( + furi_message_queue_put(cli_shell->storage_event_queue, &cli_event, 0) == FuriStatusOk); + } else if(event->type == StorageEventTypeCardUnmount) { + CliShellStorageEvent cli_event = CliShellStorageEventUnmount; + furi_check( + furi_message_queue_put(cli_shell->storage_event_queue, &cli_event, 0) == FuriStatusOk); + } +} + +static void cli_shell_storage_internal_event(FuriEventLoopObject* object, void* context) { + CliShell* cli_shell = context; + FuriMessageQueue* queue = object; + CliShellStorageEvent event; + furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk); + + if(event == CliShellStorageEventMount) { + cli_enumerate_external_commands(cli_shell->cli); + } else if(event == CliShellStorageEventUnmount) { + cli_remove_external_commands(cli_shell->cli); + } else { + furi_crash(); + } +} + +static void + cli_shell_process_parser_result(CliShell* cli_shell, CliAnsiParserResult parse_result) { + if(!parse_result.is_done) return; + CliKeyCombo key_combo = parse_result.result; + if(key_combo.key == CliKeyUnrecognized) return; + for(size_t i = 0; i < CliShellComponentMAX; i++) { // -V1008 CliShellKeyComboSet* set = component_key_combo_sets[i]; void* component_context = cli_shell->components[i]; @@ -127,22 +302,13 @@ static void cli_shell_data_available(PipeSide* pipe, void* context) { // process ANSI escape sequences int c = getchar(); furi_assert(c >= 0); - CliAnsiParserResult parse_result = cli_ansi_parser_feed(cli_shell->ansi_parser, c); - if(!parse_result.is_done) return; - CliKeyCombo key_combo = parse_result.result; - if(key_combo.key == CliKeyUnrecognized) return; - - cli_shell_process_key(cli_shell, key_combo); + cli_shell_process_parser_result(cli_shell, cli_ansi_parser_feed(cli_shell->ansi_parser, c)); } static void cli_shell_timer_expired(void* context) { CliShell* cli_shell = context; - CliAnsiParserResult parse_result = cli_ansi_parser_feed_timeout(cli_shell->ansi_parser); - if(!parse_result.is_done) return; - CliKeyCombo key_combo = parse_result.result; - if(key_combo.key == CliKeyUnrecognized) return; - - cli_shell_process_key(cli_shell, key_combo); + cli_shell_process_parser_result( + cli_shell, cli_ansi_parser_feed_timeout(cli_shell->ansi_parser)); } // ======= @@ -155,26 +321,42 @@ static CliShell* cli_shell_alloc(PipeSide* pipe) { cli_shell->cli = furi_record_open(RECORD_CLI); cli_shell->ansi_parser = cli_ansi_parser_alloc(); cli_shell->pipe = pipe; - pipe_install_as_stdio(cli_shell->pipe); cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell); + cli_shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc( + cli_shell->cli, cli_shell, cli_shell->components[CliShellComponentLine]); cli_shell->event_loop = furi_event_loop_alloc(); cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc( cli_shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, cli_shell); - pipe_attach_to_event_loop(cli_shell->pipe, cli_shell->event_loop); - pipe_set_callback_context(cli_shell->pipe, cli_shell); - pipe_set_data_arrived_callback(cli_shell->pipe, cli_shell_data_available, 0); - pipe_set_broken_callback(cli_shell->pipe, cli_shell_pipe_broken, 0); + cli_shell_install_pipe(cli_shell); + + cli_shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent)); + furi_event_loop_subscribe_message_queue( + cli_shell->event_loop, + cli_shell->storage_event_queue, + FuriEventLoopEventIn, + cli_shell_storage_internal_event, + cli_shell); + cli_shell->storage = furi_record_open(RECORD_STORAGE); + cli_shell->storage_subscription = furi_pubsub_subscribe( + storage_get_pubsub(cli_shell->storage), cli_shell_storage_event, cli_shell); return cli_shell; } static void cli_shell_free(CliShell* cli_shell) { + furi_pubsub_unsubscribe( + storage_get_pubsub(cli_shell->storage), cli_shell->storage_subscription); + furi_record_close(RECORD_STORAGE); + furi_event_loop_unsubscribe(cli_shell->event_loop, cli_shell->storage_event_queue); + furi_message_queue_free(cli_shell->storage_event_queue); + + cli_shell_completions_free(cli_shell->components[CliShellComponentCompletions]); cli_shell_line_free(cli_shell->components[CliShellComponentLine]); - pipe_detach_from_event_loop(cli_shell->pipe); + cli_shell_detach_pipe(cli_shell); furi_event_loop_timer_free(cli_shell->ansi_parsing_timer); furi_event_loop_free(cli_shell->event_loop); pipe_free(cli_shell->pipe); diff --git a/applications/services/cli/shell/cli_shell_completions.c b/applications/services/cli/shell/cli_shell_completions.c new file mode 100644 index 000000000..0b32c18a2 --- /dev/null +++ b/applications/services/cli/shell/cli_shell_completions.c @@ -0,0 +1,364 @@ +#include "cli_shell_completions.h" + +ARRAY_DEF(CommandCompletions, FuriString*, FURI_STRING_OPLIST); // -V524 +#define M_OPL_CommandCompletions_t() ARRAY_OPLIST(CommandCompletions) + +struct CliShellCompletions { + Cli* cli; + CliShell* shell; + CliShellLine* line; + CommandCompletions_t variants; + size_t selected; + bool is_displaying; +}; + +#define COMPLETION_COLUMNS 3 +#define COMPLETION_COLUMN_WIDTH "30" +#define COMPLETION_COLUMN_WIDTH_I 30 + +/** + * @brief Update for the completions menu + */ +typedef enum { + CliShellCompletionsActionOpen, + CliShellCompletionsActionClose, + CliShellCompletionsActionUp, + CliShellCompletionsActionDown, + CliShellCompletionsActionLeft, + CliShellCompletionsActionRight, + CliShellCompletionsActionSelect, + CliShellCompletionsActionSelectNoClose, +} CliShellCompletionsAction; + +typedef enum { + CliShellCompletionSegmentTypeCommand, + CliShellCompletionSegmentTypeArguments, +} CliShellCompletionSegmentType; + +typedef struct { + CliShellCompletionSegmentType type; + size_t start; + size_t length; +} CliShellCompletionSegment; + +// ========== +// Public API +// ========== + +CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line) { + CliShellCompletions* completions = malloc(sizeof(CliShellCompletions)); + + completions->cli = cli; + completions->shell = shell; + completions->line = line; + CommandCompletions_init(completions->variants); + + return completions; +} + +void cli_shell_completions_free(CliShellCompletions* completions) { + CommandCompletions_clear(completions->variants); + free(completions); +} + +// ======= +// Helpers +// ======= + +CliShellCompletionSegment cli_shell_completions_segment(CliShellCompletions* completions) { + furi_assert(completions); + CliShellCompletionSegment segment; + + FuriString* input = furi_string_alloc_set(cli_shell_line_get_editing(completions->line)); + furi_string_left(input, cli_shell_line_get_line_position(completions->line)); + + // find index of first non-space character + size_t first_non_space = 0; + while(1) { + size_t ret = furi_string_search_char(input, ' ', first_non_space); + if(ret == FURI_STRING_FAILURE) break; + if(ret - first_non_space > 1) break; + first_non_space++; + } + + size_t first_space_in_command = furi_string_search_char(input, ' ', first_non_space); + + if(first_space_in_command == FURI_STRING_FAILURE) { + segment.type = CliShellCompletionSegmentTypeCommand; + segment.start = first_non_space; + segment.length = furi_string_size(input) - first_non_space; + } else { + segment.type = CliShellCompletionSegmentTypeArguments; + segment.start = 0; + segment.length = 0; + // support removed, might reimplement in the future + } + + furi_string_free(input); + return segment; +} + +void cli_shell_completions_fill_variants(CliShellCompletions* completions) { + furi_assert(completions); + CommandCompletions_reset(completions->variants); + + CliShellCompletionSegment segment = cli_shell_completions_segment(completions); + FuriString* input = furi_string_alloc_set(cli_shell_line_get_editing(completions->line)); + furi_string_right(input, segment.start); + furi_string_left(input, segment.length); + + if(segment.type == CliShellCompletionSegmentTypeCommand) { + cli_lock_commands(completions->cli); + CliCommandTree_t* commands = cli_get_commands(completions->cli); + for + M_EACH(registered_command, *commands, CliCommandTree_t) { + FuriString* command_name = *registered_command->key_ptr; + if(furi_string_start_with(command_name, input)) { + CommandCompletions_push_back(completions->variants, command_name); + } + } + cli_unlock_commands(completions->cli); + + } else { + // support removed, might reimplement in the future + } + + furi_string_free(input); +} + +static size_t cli_shell_completions_rows_at_column(CliShellCompletions* completions, size_t x) { + size_t completions_size = CommandCompletions_size(completions->variants); + size_t n_full_rows = completions_size / COMPLETION_COLUMNS; + size_t n_cols_in_last_row = completions_size % COMPLETION_COLUMNS; + size_t n_rows_at_x = n_full_rows + ((x >= n_cols_in_last_row) ? 0 : 1); + return n_rows_at_x; +} + +void cli_shell_completions_render( + CliShellCompletions* completions, + CliShellCompletionsAction action) { + furi_assert(completions); + if(action == CliShellCompletionsActionOpen) furi_check(!completions->is_displaying); + if(action == CliShellCompletionsActionClose) furi_check(completions->is_displaying); + + char prompt[64]; + cli_shell_line_format_prompt(completions->line, prompt, sizeof(prompt)); + + if(action == CliShellCompletionsActionOpen) { + cli_shell_completions_fill_variants(completions); + completions->selected = 0; + + if(CommandCompletions_size(completions->variants) == 1) { + cli_shell_completions_render(completions, CliShellCompletionsActionSelectNoClose); + return; + } + + // show completions menu (full re-render) + printf("\n\r"); + size_t position = 0; + for + M_EACH(completion, completions->variants, CommandCompletions_t) { + if(position == completions->selected) printf(ANSI_INVERT); + printf("%-" COMPLETION_COLUMN_WIDTH "s", furi_string_get_cstr(*completion)); + if(position == completions->selected) printf(ANSI_RESET); + if((position % COMPLETION_COLUMNS == COMPLETION_COLUMNS - 1) && + position != CommandCompletions_size(completions->variants)) { + printf("\r\n"); + } + position++; + } + + if(!position) { + printf(ANSI_FG_RED "no completions" ANSI_RESET); + } + + size_t total_rows = (position / COMPLETION_COLUMNS) + 1; + printf( + ANSI_ERASE_DISPLAY(ANSI_ERASE_FROM_CURSOR_TO_END) ANSI_CURSOR_UP_BY("%zu") + ANSI_CURSOR_HOR_POS("%zu"), + total_rows, + strlen(prompt) + cli_shell_line_get_line_position(completions->line) + 1); + + completions->is_displaying = true; + + } else if(action == CliShellCompletionsActionClose) { + // clear completions menu + printf( + ANSI_CURSOR_HOR_POS("%zu") ANSI_ERASE_DISPLAY(ANSI_ERASE_FROM_CURSOR_TO_END) + ANSI_CURSOR_HOR_POS("%zu"), + strlen(prompt) + furi_string_size(cli_shell_line_get_selected(completions->line)) + 1, + strlen(prompt) + cli_shell_line_get_line_position(completions->line) + 1); + completions->is_displaying = false; + + } else if( + action == CliShellCompletionsActionUp || action == CliShellCompletionsActionDown || + action == CliShellCompletionsActionLeft || action == CliShellCompletionsActionRight) { + if(CommandCompletions_empty_p(completions->variants)) return; + + // move selection + size_t completions_size = CommandCompletions_size(completions->variants); + size_t old_selection = completions->selected; + int n_columns = (completions_size >= COMPLETION_COLUMNS) ? COMPLETION_COLUMNS : + completions_size; + int selection_unclamped = old_selection; + if(action == CliShellCompletionsActionLeft) { + selection_unclamped--; + } else if(action == CliShellCompletionsActionRight) { + selection_unclamped++; + } else { + int selection_x = old_selection % COMPLETION_COLUMNS; + int selection_y_unclamped = old_selection / COMPLETION_COLUMNS; + if(action == CliShellCompletionsActionUp) selection_y_unclamped--; + if(action == CliShellCompletionsActionDown) selection_y_unclamped++; + size_t selection_y = 0; + if(selection_y_unclamped < 0) { + selection_x = CLAMP_WRAPAROUND(selection_x - 1, n_columns - 1, 0); + selection_y = + cli_shell_completions_rows_at_column(completions, selection_x) - 1; // -V537 + } else if( + (size_t)selection_y_unclamped > + cli_shell_completions_rows_at_column(completions, selection_x) - 1) { + selection_x = CLAMP_WRAPAROUND(selection_x + 1, n_columns - 1, 0); + selection_y = 0; + } else { + selection_y = selection_y_unclamped; + } + selection_unclamped = (selection_y * COMPLETION_COLUMNS) + selection_x; + } + size_t new_selection = CLAMP_WRAPAROUND(selection_unclamped, (int)completions_size - 1, 0); + completions->selected = new_selection; + + if(new_selection != old_selection) { + // determine selection coordinates relative to top-left of suggestion menu + size_t old_x = (old_selection % COMPLETION_COLUMNS) * COMPLETION_COLUMN_WIDTH_I; + size_t old_y = old_selection / COMPLETION_COLUMNS; + size_t new_x = (new_selection % COMPLETION_COLUMNS) * COMPLETION_COLUMN_WIDTH_I; + size_t new_y = new_selection / COMPLETION_COLUMNS; + printf("\n\r"); + + // print old selection in normal colors + if(old_y) printf(ANSI_CURSOR_DOWN_BY("%zu"), old_y); + printf(ANSI_CURSOR_HOR_POS("%zu"), old_x + 1); + printf( + "%-" COMPLETION_COLUMN_WIDTH "s", + furi_string_get_cstr( + *CommandCompletions_cget(completions->variants, old_selection))); + if(old_y) printf(ANSI_CURSOR_UP_BY("%zu"), old_y); + printf(ANSI_CURSOR_HOR_POS("1")); + + // print new selection in inverted colors + if(new_y) printf(ANSI_CURSOR_DOWN_BY("%zu"), new_y); + printf(ANSI_CURSOR_HOR_POS("%zu"), new_x + 1); + printf( + ANSI_INVERT "%-" COMPLETION_COLUMN_WIDTH "s" ANSI_RESET, + furi_string_get_cstr( + *CommandCompletions_cget(completions->variants, new_selection))); + + // return cursor + printf(ANSI_CURSOR_UP_BY("%zu"), new_y + 1); + printf( + ANSI_CURSOR_HOR_POS("%zu"), + strlen(prompt) + furi_string_size(cli_shell_line_get_selected(completions->line)) + + 1); + } + + } else if(action == CliShellCompletionsActionSelectNoClose) { + // insert selection into prompt + CliShellCompletionSegment segment = cli_shell_completions_segment(completions); + FuriString* input = cli_shell_line_get_selected(completions->line); + FuriString* completion = + *CommandCompletions_cget(completions->variants, completions->selected); + furi_string_replace_at( + input, segment.start, segment.length, furi_string_get_cstr(completion)); + printf( + ANSI_CURSOR_HOR_POS("%zu") "%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END), + strlen(prompt) + 1, + furi_string_get_cstr(input)); + + int position_change = (int)furi_string_size(completion) - (int)segment.length; + cli_shell_line_set_line_position( + completions->line, + MAX(0, (int)cli_shell_line_get_line_position(completions->line) + position_change)); + + } else if(action == CliShellCompletionsActionSelect) { + cli_shell_completions_render(completions, CliShellCompletionsActionSelectNoClose); + cli_shell_completions_render(completions, CliShellCompletionsActionClose); + + } else { + furi_crash(); + } + + fflush(stdout); +} + +// ============== +// Input handlers +// ============== + +static bool hide_if_open_and_continue_handling(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellCompletions* completions = context; + if(completions->is_displaying) + cli_shell_completions_render(completions, CliShellCompletionsActionClose); + return false; // process other home events +} + +static bool key_combo_cr(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellCompletions* completions = context; + if(!completions->is_displaying) return false; + cli_shell_completions_render(completions, CliShellCompletionsActionSelect); + return true; +} + +static bool key_combo_up_down(CliKeyCombo combo, void* context) { + CliShellCompletions* completions = context; + if(!completions->is_displaying) return false; + cli_shell_completions_render( + completions, + (combo.key == CliKeyUp) ? CliShellCompletionsActionUp : CliShellCompletionsActionDown); + return true; +} + +static bool key_combo_left_right(CliKeyCombo combo, void* context) { + CliShellCompletions* completions = context; + if(!completions->is_displaying) return false; + cli_shell_completions_render( + completions, + (combo.key == CliKeyLeft) ? CliShellCompletionsActionLeft : + CliShellCompletionsActionRight); + return true; +} + +static bool key_combo_tab(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellCompletions* completions = context; + cli_shell_completions_render( + completions, + completions->is_displaying ? CliShellCompletionsActionRight : + CliShellCompletionsActionOpen); + return true; +} + +static bool key_combo_esc(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellCompletions* completions = context; + if(!completions->is_displaying) return false; + cli_shell_completions_render(completions, CliShellCompletionsActionClose); + return true; +} + +CliShellKeyComboSet cli_shell_completions_key_combo_set = { + .fallback = hide_if_open_and_continue_handling, + .count = 7, + .records = + { + {{CliModKeyNo, CliKeyCR}, key_combo_cr}, + {{CliModKeyNo, CliKeyUp}, key_combo_up_down}, + {{CliModKeyNo, CliKeyDown}, key_combo_up_down}, + {{CliModKeyNo, CliKeyLeft}, key_combo_left_right}, + {{CliModKeyNo, CliKeyRight}, key_combo_left_right}, + {{CliModKeyNo, CliKeyTab}, key_combo_tab}, + {{CliModKeyNo, CliKeyEsc}, key_combo_esc}, + }, +}; diff --git a/applications/services/cli/shell/cli_shell_completions.h b/applications/services/cli/shell/cli_shell_completions.h new file mode 100644 index 000000000..6353bde71 --- /dev/null +++ b/applications/services/cli/shell/cli_shell_completions.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include "cli_shell_i.h" +#include "cli_shell_line.h" +#include "../cli.h" +#include "../cli_i.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct CliShellCompletions CliShellCompletions; + +CliShellCompletions* cli_shell_completions_alloc(Cli* cli, CliShell* shell, CliShellLine* line); + +void cli_shell_completions_free(CliShellCompletions* completions); + +extern CliShellKeyComboSet cli_shell_completions_key_combo_set; + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/shell/cli_shell_line.c b/applications/services/cli/shell/cli_shell_line.c index 45bc19d9d..959cd0b3b 100644 --- a/applications/services/cli/shell/cli_shell_line.c +++ b/applications/services/cli/shell/cli_shell_line.c @@ -65,6 +65,64 @@ void cli_shell_line_ensure_not_overwriting_history(CliShellLine* line) { } } +size_t cli_shell_line_get_line_position(CliShellLine* line) { + return line->line_position; +} + +void cli_shell_line_set_line_position(CliShellLine* line, size_t position) { + line->line_position = position; +} + +// ======= +// Helpers +// ======= + +typedef enum { + CliCharClassWord, + CliCharClassSpace, + CliCharClassOther, +} CliCharClass; + +typedef enum { + CliSkipDirectionLeft, + CliSkipDirectionRight, +} CliSkipDirection; + +CliCharClass cli_shell_line_char_class(char c) { + if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') { + return CliCharClassWord; + } else if(c == ' ') { + return CliCharClassSpace; + } else { + return CliCharClassOther; + } +} + +size_t + cli_shell_line_skip_run(FuriString* string, size_t original_pos, CliSkipDirection direction) { + if(furi_string_size(string) == 0) return original_pos; + if(direction == CliSkipDirectionLeft && original_pos == 0) return original_pos; + if(direction == CliSkipDirectionRight && original_pos == furi_string_size(string)) + return original_pos; + + int8_t look_offset = (direction == CliSkipDirectionLeft) ? -1 : 0; + int8_t increment = (direction == CliSkipDirectionLeft) ? -1 : 1; + int32_t position = original_pos; + CliCharClass start_class = + cli_shell_line_char_class(furi_string_get_char(string, position + look_offset)); + + while(true) { + position += increment; + if(position < 0) break; + if(position >= (int32_t)furi_string_size(string)) break; + if(cli_shell_line_char_class(furi_string_get_char(string, position + look_offset)) != + start_class) + break; + } + + return MAX(0, position); +} + // ============== // Input handlers // ============== @@ -87,13 +145,32 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) { FuriString* command = cli_shell_line_get_selected(line); furi_string_trim(command); + if(furi_string_empty(command)) { + cli_shell_line_prompt(line); + return true; + } + FuriString* command_copy = furi_string_alloc_set(command); + if(line->history_position == 0) { + for(size_t i = 1; i < line->history_entries; i++) { + if(furi_string_cmp(line->history[i], command) == 0) { + line->history_position = i; + command = cli_shell_line_get_selected(line); + furi_string_trim(command); + break; + } + } + } + + // move selected command to the front if(line->history_position > 0) { - // move selected command to the front + size_t pos = line->history_position; + size_t len = line->history_entries; memmove( - &line->history[1], &line->history[0], line->history_position * sizeof(FuriString*)); - line->history[0] = command; + &line->history[pos], &line->history[pos + 1], (len - pos - 1) * sizeof(FuriString*)); + furi_string_move(line->history[0], command); + line->history_entries--; } // insert empty command @@ -109,7 +186,7 @@ static bool cli_shell_line_input_cr(CliKeyCombo combo, void* context) { // execute command printf("\r\n"); - if(!furi_string_empty(command_copy)) cli_shell_execute_command(line->shell, command_copy); + cli_shell_execute_command(line->shell, command_copy); furi_string_free(command_copy); cli_shell_line_prompt(line); @@ -199,7 +276,57 @@ static bool cli_shell_line_input_bksp(CliKeyCombo combo, void* context) { return true; } -static bool cli_shell_line_input_fallback(CliKeyCombo combo, void* context) { +static bool cli_shell_line_input_ctrl_l(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // clear screen + FuriString* command = cli_shell_line_get_selected(line); + char prompt[64]; + cli_shell_line_format_prompt(line, prompt, sizeof(prompt)); + printf( + ANSI_ERASE_DISPLAY(ANSI_ERASE_ENTIRE) ANSI_ERASE_SCROLLBACK_BUFFER ANSI_CURSOR_POS( + "1", "1") "%s%s" ANSI_CURSOR_HOR_POS("%zu"), + prompt, + furi_string_get_cstr(command), + strlen(prompt) + line->line_position + 1 /* 1-based column indexing */); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_ctrl_left_right(CliKeyCombo combo, void* context) { + CliShellLine* line = context; + // skip run of similar chars to the left or right + FuriString* selected_line = cli_shell_line_get_selected(line); + CliSkipDirection direction = (combo.key == CliKeyLeft) ? CliSkipDirectionLeft : + CliSkipDirectionRight; + line->line_position = cli_shell_line_skip_run(selected_line, line->line_position, direction); + printf( + ANSI_CURSOR_HOR_POS("%zu"), cli_shell_line_prompt_length(line) + line->line_position + 1); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_ctrl_bksp(CliKeyCombo combo, void* context) { + UNUSED(combo); + CliShellLine* line = context; + // delete run of similar chars to the left + cli_shell_line_ensure_not_overwriting_history(line); + FuriString* selected_line = cli_shell_line_get_selected(line); + size_t run_start = + cli_shell_line_skip_run(selected_line, line->line_position, CliSkipDirectionLeft); + furi_string_replace_at(selected_line, run_start, line->line_position - run_start, ""); + line->line_position = run_start; + printf( + ANSI_CURSOR_HOR_POS("%zu") "%s" ANSI_ERASE_LINE(ANSI_ERASE_FROM_CURSOR_TO_END) + ANSI_CURSOR_HOR_POS("%zu"), + cli_shell_line_prompt_length(line) + line->line_position + 1, + furi_string_get_cstr(selected_line) + run_start, + cli_shell_line_prompt_length(line) + run_start + 1); + fflush(stdout); + return true; +} + +static bool cli_shell_line_input_normal(CliKeyCombo combo, void* context) { CliShellLine* line = context; if(combo.modifiers != CliModKeyNo) return false; if(combo.key < CliKeySpace || combo.key >= CliKeyDEL) return false; @@ -220,8 +347,8 @@ static bool cli_shell_line_input_fallback(CliKeyCombo combo, void* context) { } CliShellKeyComboSet cli_shell_line_key_combo_set = { - .fallback = cli_shell_line_input_fallback, - .count = 10, + .fallback = cli_shell_line_input_normal, + .count = 14, .records = { {{CliModKeyNo, CliKeyETX}, cli_shell_line_input_ctrl_c}, @@ -234,5 +361,9 @@ CliShellKeyComboSet cli_shell_line_key_combo_set = { {{CliModKeyNo, CliKeyEnd}, cli_shell_line_input_end}, {{CliModKeyNo, CliKeyBackspace}, cli_shell_line_input_bksp}, {{CliModKeyNo, CliKeyDEL}, cli_shell_line_input_bksp}, + {{CliModKeyNo, CliKeyFF}, cli_shell_line_input_ctrl_l}, + {{CliModKeyCtrl, CliKeyLeft}, cli_shell_line_input_ctrl_left_right}, + {{CliModKeyCtrl, CliKeyRight}, cli_shell_line_input_ctrl_left_right}, + {{CliModKeyNo, CliKeyETB}, cli_shell_line_input_ctrl_bksp}, }, }; diff --git a/applications/services/cli/shell/cli_shell_line.h b/applications/services/cli/shell/cli_shell_line.h index c1c810ee4..1e4b9e32a 100644 --- a/applications/services/cli/shell/cli_shell_line.h +++ b/applications/services/cli/shell/cli_shell_line.h @@ -24,6 +24,10 @@ void cli_shell_line_format_prompt(CliShellLine* line, char* buf, size_t length); void cli_shell_line_prompt(CliShellLine* line); +size_t cli_shell_line_get_line_position(CliShellLine* line); + +void cli_shell_line_set_line_position(CliShellLine* line, size_t position); + /** * @brief If a line from history has been selected, moves it into the active line */ diff --git a/applications/services/crypto/application.fam b/applications/services/crypto/application.fam index e42f4e0f7..f038aee76 100644 --- a/applications/services/crypto/application.fam +++ b/applications/services/crypto/application.fam @@ -1,16 +1,8 @@ App( - appid="crypto_cli", + appid="cli_crypto", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="crypto_cli_plugin_ep", + entry_point="cli_crypto_ep", requires=["cli"], sources=["crypto_cli.c"], ) - -App( - appid="crypto_start", - apptype=FlipperAppType.STARTUP, - entry_point="crypto_on_system_start", - sources=["crypto_cli.c"], - order=10, -) diff --git a/applications/services/crypto/crypto_cli.c b/applications/services/crypto/crypto_cli.c index b509db61d..40ffebbe8 100644 --- a/applications/services/crypto/crypto_cli.c +++ b/applications/services/crypto/crypto_cli.c @@ -4,6 +4,7 @@ #include #include #include +#include void crypto_cli_print_usage(void) { printf("Usage:\r\n"); @@ -280,7 +281,7 @@ void crypto_cli_store_key(PipeSide* pipe, FuriString* args) { furi_string_free(key_type); } -static void crypto_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -317,15 +318,4 @@ static void crypto_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -#include -CLI_PLUGIN_WRAPPER("crypto", crypto_cli) - -void crypto_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "crypto", CliCommandFlagDefault, crypto_cli_wrapper, NULL); - furi_record_close(RECORD_CLI); -#else - UNUSED(crypto_cli); -#endif -} +CLI_COMMAND_INTERFACE(crypto, execute, CliCommandFlagDefault, 1024); diff --git a/applications/services/input/application.fam b/applications/services/input/application.fam index efca47405..c87a84d37 100644 --- a/applications/services/input/application.fam +++ b/applications/services/input/application.fam @@ -11,10 +11,10 @@ App( ) App( - appid="input_cli", + appid="cli_input", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="input_cli_plugin_ep", + entry_point="cli_input_ep", requires=["cli"], sources=["input_cli.c"], ) diff --git a/applications/services/input/input.c b/applications/services/input/input.c index 0b7e7fc57..7bf6e1a59 100644 --- a/applications/services/input/input.c +++ b/applications/services/input/input.c @@ -30,9 +30,6 @@ typedef struct { volatile uint32_t counter; } InputPinState; -/** Input CLI command handler */ -void input_cli_wrapper(PipeSide* pipe, FuriString* args, void* context); - // #define INPUT_DEBUG #define GPIO_Read(input_pin) (furi_hal_gpio_read(input_pin.pin->gpio) ^ (input_pin.pin->inverted)) @@ -104,11 +101,6 @@ int32_t input_srv(void* p) { furi_hal_gpio_init_simple(&gpio_ext_pa4, GpioModeOutputPushPull); #endif -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - cli_add_command(cli, "input", CliCommandFlagParallelSafe, input_cli_wrapper, event_pubsub); -#endif - InputPinState pin_states[input_pins_count]; for(size_t i = 0; i < input_pins_count; i++) { diff --git a/applications/services/input/input_cli.c b/applications/services/input/input_cli.c index 8e7abbf64..79927fc0a 100644 --- a/applications/services/input/input_cli.c +++ b/applications/services/input/input_cli.c @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -195,9 +196,9 @@ static void input_cli_send(PipeSide* pipe, FuriString* args, FuriPubSub* event_p furi_string_free(key_str); } -void input_cli(PipeSide* pipe, FuriString* args, void* context) { - furi_assert(context); - FuriPubSub* event_pubsub = context; +static void execute(PipeSide* pipe, FuriString* args, void* context) { + UNUSED(context); + FuriPubSub* event_pubsub = furi_record_open(RECORD_INPUT_EVENTS); FuriString* cmd; cmd = furi_string_alloc(); @@ -223,7 +224,7 @@ void input_cli(PipeSide* pipe, FuriString* args, void* context) { } while(false); furi_string_free(cmd); + furi_record_close(RECORD_INPUT_EVENTS); } -#include -CLI_PLUGIN_WRAPPER("input", input_cli) +CLI_COMMAND_INTERFACE(input, execute, CliCommandFlagParallelSafe, 1024); diff --git a/applications/services/loader/application.fam b/applications/services/loader/application.fam index b3035dee4..b72fe696e 100644 --- a/applications/services/loader/application.fam +++ b/applications/services/loader/application.fam @@ -5,7 +5,6 @@ App( entry_point="loader_srv", cdefines=["SRV_LOADER"], requires=["gui"], - provides=["loader_start"], stack_size=2 * 1024, order=90, sdk_headers=[ @@ -15,18 +14,10 @@ App( ) App( - appid="loader_cli", + appid="cli_loader", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="loader_cli_plugin_ep", + entry_point="cli_loader_ep", requires=["cli"], sources=["loader_cli.c"], ) - -App( - appid="loader_start", - apptype=FlipperAppType.STARTUP, - entry_point="loader_on_system_start", - requires=["loader"], - order=90, -) diff --git a/applications/services/loader/loader_cli.c b/applications/services/loader/loader_cli.c index 1e18f3ff5..4f01278b0 100644 --- a/applications/services/loader/loader_cli.c +++ b/applications/services/loader/loader_cli.c @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -114,7 +115,7 @@ static void loader_cli_signal(FuriString* args, Loader* loader) { } } -static void loader_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(pipe); UNUSED(context); Loader* loader = furi_record_open(RECORD_LOADER); @@ -142,15 +143,4 @@ static void loader_cli(PipeSide* pipe, FuriString* args, void* context) { furi_record_close(RECORD_LOADER); } -#include -CLI_PLUGIN_WRAPPER("loader", loader_cli) - -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_wrapper, NULL); - furi_record_close(RECORD_CLI); -#else - UNUSED(loader_cli); -#endif -} +CLI_COMMAND_INTERFACE(loader, execute, CliCommandFlagParallelSafe, 1024); diff --git a/applications/services/power/application.fam b/applications/services/power/application.fam index f973ab2b6..ccd00a416 100644 --- a/applications/services/power/application.fam +++ b/applications/services/power/application.fam @@ -10,7 +10,6 @@ App( ], provides=[ "power_settings", - "power_start", ], stack_size=1 * 1024, order=110, @@ -18,18 +17,10 @@ App( ) App( - appid="power_cli", + appid="cli_power", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="power_cli_plugin_ep", + entry_point="cli_power_ep", requires=["cli"], sources=["power_cli.c"], ) - -App( - appid="power_start", - apptype=FlipperAppType.STARTUP, - entry_point="power_on_system_start", - requires=["power"], - order=80, -) diff --git a/applications/services/power/power_cli.c b/applications/services/power/power_cli.c index 2f84ff439..f3a8fde3e 100644 --- a/applications/services/power/power_cli.c +++ b/applications/services/power/power_cli.c @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -68,7 +69,7 @@ static void power_cli_command_print_usage(void) { } } -void power_cli(PipeSide* pipe, FuriString* args, void* context) { +static void execute(PipeSide* pipe, FuriString* args, void* context) { UNUSED(context); FuriString* cmd; cmd = furi_string_alloc(); @@ -112,17 +113,4 @@ void power_cli(PipeSide* pipe, FuriString* args, void* context) { furi_string_free(cmd); } -#include -CLI_PLUGIN_WRAPPER("power", power_cli) - -void power_on_system_start(void) { -#ifdef SRV_CLI - Cli* cli = furi_record_open(RECORD_CLI); - - cli_add_command(cli, "power", CliCommandFlagParallelSafe, power_cli_wrapper, NULL); - - furi_record_close(RECORD_CLI); -#else - UNUSED(power_cli); -#endif -} +CLI_COMMAND_INTERFACE(power, execute, CliCommandFlagParallelSafe, 1024); diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index b2f4a7a27..22466b1db 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -321,9 +321,11 @@ static void storage_cli_write_chunk(PipeSide* pipe, FuriString* path, FuriString uint8_t* buffer = malloc(buffer_size); while(need_to_read) { - size_t read_this_time = pipe_receive(pipe, buffer, MIN(buffer_size, need_to_read)); - size_t wrote_this_time = storage_file_write(file, buffer, read_this_time); + size_t to_read_this_time = MIN(buffer_size, need_to_read); + size_t read_this_time = pipe_receive(pipe, buffer, to_read_this_time); + if(read_this_time != to_read_this_time) break; + size_t wrote_this_time = storage_file_write(file, buffer, read_this_time); if(wrote_this_time != read_this_time) { storage_cli_print_error(storage_file_get_error(file)); break; @@ -720,7 +722,13 @@ 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, "storage", CliCommandFlagParallelSafe, storage_cli, NULL, 512); + cli_add_command_ex( + cli, + "storage", + CliCommandFlagParallelSafe | CliCommandFlagUseShellThread, + storage_cli, + NULL, + 512); cli_add_command( cli, "factory_reset", CliCommandFlagParallelSafe, storage_cli_factory_reset, NULL); furi_record_close(RECORD_CLI); diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 0f792ef44..ee47ce2ac 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -10,7 +10,6 @@ App( stack_size=2 * 1024, resources="examples", order=0, - provides=["js_app_start"], sources=[ "js_app.c", "js_modules.c", @@ -24,10 +23,10 @@ App( ) App( - appid="js_cli", + appid="cli_js", targets=["f7"], apptype=FlipperAppType.PLUGIN, - entry_point="js_cli_execute_plugin_ep", + entry_point="cli_js_ep", requires=["cli"], sources=[ "js_app.c", @@ -40,14 +39,6 @@ App( fap_libs=["assets"], ) -App( - appid="js_app_start", - apptype=FlipperAppType.STARTUP, - entry_point="js_app_on_system_start", - order=160, - sources=["js_app.c"], -) - App( appid="js_event_loop", apptype=FlipperAppType.PLUGIN, diff --git a/applications/system/js_app/js_app.c b/applications/system/js_app/js_app.c index a67f469b8..dec97842e 100644 --- a/applications/system/js_app/js_app.c +++ b/applications/system/js_app/js_app.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #define TAG "JS app" @@ -207,13 +208,4 @@ void js_cli_execute(PipeSide* pipe, FuriString* args, void* context) { furi_record_close(RECORD_STORAGE); } -#include -CLI_PLUGIN_WRAPPER("js", js_cli_execute) - -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_wrapper, NULL); - furi_record_close(RECORD_CLI); -#endif -} +CLI_COMMAND_INTERFACE(js, js_cli_execute, CliCommandFlagDefault, 1024); diff --git a/documentation/file_formats/BadUsbScriptFormat.md b/documentation/file_formats/BadUsbScriptFormat.md index 77008db07..d32a73652 100644 --- a/documentation/file_formats/BadUsbScriptFormat.md +++ b/documentation/file_formats/BadUsbScriptFormat.md @@ -57,19 +57,17 @@ Pause script execution by a defined time. ### Modifier keys -Can be combined with a special key command or a single character. -| Command | Notes | -| -------------- | ---------- | -| CONTROL / CTRL | | -| SHIFT | | -| ALT | | -| WINDOWS / GUI | | -| CTRL-ALT | CTRL+ALT | -| CTRL-SHIFT | CTRL+SHIFT | -| ALT-SHIFT | ALT+SHIFT | -| ALT-GUI | ALT+WIN | -| GUI-SHIFT | WIN+SHIFT | -| GUI-CTRL | WIN+CTRL | +The following modifier keys are recognized: +| Command | Notes | +| ------- | ------------ | +| CTRL | | +| CONTROL | Same as CTRL | +| SHIFT | | +| ALT | | +| GUI | | +| WINDOWS | Same as GUI | + +You can chain multiple modifier keys together using hyphens (`-`) or spaces. ## Key hold and release diff --git a/furi/core/memmgr_heap.c b/furi/core/memmgr_heap.c index c8a72bc8c..3ce0558a3 100644 --- a/furi/core/memmgr_heap.c +++ b/furi/core/memmgr_heap.c @@ -295,10 +295,12 @@ void memmgr_heap_printf_free_blocks(void) { //can be enabled once we can do printf with a locked scheduler //vTaskSuspendAll(); - pxBlock = xStart.pxNextFreeBlock; - while(pxBlock->pxNextFreeBlock != NULL) { + pxBlock = heapPROTECT_BLOCK_POINTER(xStart.pxNextFreeBlock); + heapVALIDATE_BLOCK_POINTER(pxBlock); + while(pxBlock->pxNextFreeBlock != heapPROTECT_BLOCK_POINTER(NULL)) { printf("A %p S %lu\r\n", (void*)pxBlock, (uint32_t)pxBlock->xBlockSize); - pxBlock = pxBlock->pxNextFreeBlock; + pxBlock = heapPROTECT_BLOCK_POINTER(pxBlock->pxNextFreeBlock); + heapVALIDATE_BLOCK_POINTER(pxBlock); } //xTaskResumeAll(); diff --git a/scripts/flipper/storage.py b/scripts/flipper/storage.py index 40af5cebc..0182cf45f 100644 --- a/scripts/flipper/storage.py +++ b/scripts/flipper/storage.py @@ -109,6 +109,8 @@ class FlipperStorage: def start(self): self.port.open() + time.sleep(0.5) + self.read.until(self.CLI_PROMPT) self.port.reset_input_buffer() # Send a command with a known syntax to make sure the buffer is flushed self.send("device_info\r") diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index a3da6feeb..372b656dd 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -786,8 +786,10 @@ 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_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 9c3ae0d51..5e23ddd06 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -883,8 +883,10 @@ 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_vcp_disable,void,CliVcp* Function,+,cli_vcp_enable,void,CliVcp* Function,+,composite_api_resolver_add,void,"CompositeApiResolver*, const ElfApiInterface*" @@ -3572,10 +3574,10 @@ Function,+,subghz_file_encoder_worker_start,_Bool,"SubGhzFileEncoderWorker*, con Function,+,subghz_file_encoder_worker_stop,void,SubGhzFileEncoderWorker* Function,+,subghz_keystore_alloc,SubGhzKeystore*, Function,+,subghz_keystore_free,void,SubGhzKeystore* -Function,-,subghz_keystore_get_data,SubGhzKeyArray_t*,SubGhzKeystore* +Function,+,subghz_keystore_get_data,SubGhzKeyArray_t*,SubGhzKeystore* Function,+,subghz_keystore_load,_Bool,"SubGhzKeystore*, const char*" Function,+,subghz_keystore_raw_encrypted_save,_Bool,"const char*, const char*, uint8_t*" -Function,-,subghz_keystore_raw_get_data,_Bool,"const char*, size_t, uint8_t*, size_t" +Function,+,subghz_keystore_raw_get_data,_Bool,"const char*, size_t, uint8_t*, size_t" Function,-,subghz_keystore_reset_kl,void,SubGhzKeystore* Function,+,subghz_keystore_save,_Bool,"SubGhzKeystore*, const char*, uint8_t*" Function,+,subghz_protocol_alutech_at_4n_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*"