From 586b429658f6e8b4f74840c4671ec33ac5f24c6e Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Tue, 1 Oct 2024 20:18:46 +0300 Subject: [PATCH 1/7] fix: cli top blinking --- applications/services/cli/cli_commands.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index c3539813b1c..0ad58565552 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -391,16 +391,18 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { int interval = 1000; args_read_int_and_trim(args, &interval); + if(interval) printf("\e[2J\e[?25l"); // Clear display, hide cursor + FuriThreadList* thread_list = furi_thread_list_alloc(); while(!cli_cmd_interrupt_received(cli)) { uint32_t tick = furi_get_tick(); furi_thread_enumerate(thread_list); - if(interval) printf("\e[2J\e[0;0f"); // Clear display and return to 0 + if(interval) printf("\e[0;0f"); // Return to 0,0 uint32_t uptime = tick / furi_kernel_get_tick_frequency(); printf( - "Threads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus\r\n", + "\rThreads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus\e[0K\r\n", furi_thread_list_size(thread_list), (double)furi_thread_list_get_isr_time(thread_list), uptime / 60 / 60, @@ -408,14 +410,14 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { uptime % 60); printf( - "Heap: total %zu, free %zu, minimum %zu, max block %zu\r\n\r\n", + "\rHeap: total %zu, free %zu, minimum %zu, max block %zu\e[0K\r\n\r\n", memmgr_get_total_heap(), memmgr_get_free_heap(), memmgr_get_minimum_free_heap(), memmgr_heap_get_max_free_block()); printf( - "%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s\r\n", + "\r%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s\e[0K\r\n", "AppID", "Name", "State", @@ -429,7 +431,7 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { for(size_t i = 0; i < furi_thread_list_size(thread_list); i++) { const FuriThreadListItem* item = furi_thread_list_get_at(thread_list, i); printf( - "%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f\r\n", + "\r%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f\e[0K\r\n", item->app_id, item->name, item->state, @@ -448,6 +450,8 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { } } furi_thread_list_free(thread_list); + + if(interval) printf("\e[?25h"); // Show cursor } void cli_command_free(Cli* cli, FuriString* args, void* context) { From 3400a1993da40165a661af14e53f56c8a6c595c4 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Tue, 1 Oct 2024 22:58:17 +0300 Subject: [PATCH 2/7] feat: clear prompt on down key --- applications/services/cli/cli.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index 0d8f52c04ec..8eafb9962ed 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -316,6 +316,10 @@ static void cli_handle_escape(Cli* cli, char c) { printf("%s", furi_string_get_cstr(cli->line)); } } else if(c == 'B') { + // Clear input + furi_string_reset(cli->line); + cli->cursor_position = 0; + printf("\r>: \e[0K"); } else if(c == 'C') { if(cli->cursor_position < furi_string_size(cli->line)) { cli->cursor_position++; From f0e2e4efa48da79b8700e4ca5e2a1571a946da68 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Wed, 2 Oct 2024 20:25:32 +0300 Subject: [PATCH 3/7] feat: proper-er ansi escape sequence handling --- applications/main/subghz/subghz_cli.c | 9 +- applications/main/subghz/subghz_cli.h | 1 + applications/services/cli/cli.c | 180 +++++++++++----- applications/services/cli/cli.h | 16 +- applications/services/cli/cli_ansi.c | 76 +++++++ applications/services/cli/cli_ansi.h | 94 +++++++++ applications/services/cli/cli_commands.c | 217 +++++++++++++++++--- applications/services/crypto/crypto_cli.c | 9 +- applications/services/storage/storage_cli.c | 3 +- 9 files changed, 507 insertions(+), 98 deletions(-) create mode 100644 applications/services/cli/cli_ansi.c create mode 100644 applications/services/cli/cli_ansi.h diff --git a/applications/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index 6375f2eee4d..bce88b7a354 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -999,13 +999,12 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { chat_event = subghz_chat_worker_get_event_chat(subghz_chat); switch(chat_event.event) { case SubGhzChatEventInputData: - if(chat_event.c == CliSymbolAsciiETX) { + if(chat_event.c == CliKeyETX) { printf("\r\n"); chat_event.event = SubGhzChatEventUserExit; subghz_chat_worker_put_event_chat(subghz_chat, &chat_event); break; - } else if( - (chat_event.c == CliSymbolAsciiBackspace) || (chat_event.c == CliSymbolAsciiDel)) { + } else if((chat_event.c == CliKeyBackspace) || (chat_event.c == CliKeyDEL)) { size_t len = furi_string_utf8_length(input); if(len > furi_string_utf8_length(name)) { printf("%s", "\e[D\e[1P"); @@ -1027,7 +1026,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { } furi_string_set(input, sysmsg); } - } else if(chat_event.c == CliSymbolAsciiCR) { + } else if(chat_event.c == CliKeyCR) { printf("\r\n"); furi_string_push_back(input, '\r'); furi_string_push_back(input, '\n'); @@ -1041,7 +1040,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { furi_string_printf(input, "%s", furi_string_get_cstr(name)); printf("%s", furi_string_get_cstr(input)); fflush(stdout); - } else if(chat_event.c == CliSymbolAsciiLF) { + } else if(chat_event.c == CliKeyLF) { //cut out the symbol \n } else { putc(chat_event.c, stdout); diff --git a/applications/main/subghz/subghz_cli.h b/applications/main/subghz/subghz_cli.h index f6388218f46..18c84c3e0f4 100644 --- a/applications/main/subghz/subghz_cli.h +++ b/applications/main/subghz/subghz_cli.h @@ -1,5 +1,6 @@ #pragma once #include +#include void subghz_on_system_start(void); diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index 8eafb9962ed..95e549ac605 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -1,12 +1,15 @@ #include "cli_i.h" #include "cli_commands.h" #include "cli_vcp.h" +#include "cli_ansi.h" #include #include #define TAG "CliSrv" #define CLI_INPUT_LEN_LIMIT 256 +#define CLI_PROMPT ">: " // qFlipper does not recognize us if we use escape sequences :( +#define CLI_PROMPT_LENGTH 3 // printable characters Cli* cli_alloc(void) { Cli* cli = malloc(sizeof(Cli)); @@ -85,7 +88,7 @@ bool cli_cmd_interrupt_received(Cli* cli) { char c = '\0'; if(cli_is_connected(cli)) { if(cli->session->rx((uint8_t*)&c, 1, 0) == 1) { - return c == CliSymbolAsciiETX; + return c == CliKeyETX; } } else { return true; @@ -102,7 +105,8 @@ void cli_print_usage(const char* cmd, const char* usage, const char* arg) { } void cli_motd(void) { - printf("\r\n" + printf(ANSI_FLIPPER_BRAND_ORANGE + "\r\n" " _.-------.._ -,\r\n" " .-\"```\"--..,,_/ /`-, -, \\ \r\n" " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" @@ -116,12 +120,11 @@ void cli_motd(void) { " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" - "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" - "\r\n" - "Welcome to Flipper Zero Command Line Interface!\r\n" + "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" ANSI_RESET + "\r\n" ANSI_FG_BR_WHITE "Welcome to " ANSI_FLIPPER_BRAND_ORANGE + "Flipper Zero" ANSI_FG_BR_WHITE " 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"); + "Run `help` or `?` to list available commands\r\n" ANSI_RESET "\r\n"); const Version* firmware_version = furi_hal_version_get_firmware_version(); if(firmware_version) { @@ -142,7 +145,7 @@ void cli_nl(Cli* cli) { void cli_prompt(Cli* cli) { UNUSED(cli); - printf("\r\n>: %s", furi_string_get_cstr(cli->line)); + printf("\r\n" CLI_PROMPT "%s", furi_string_get_cstr(cli->line)); fflush(stdout); } @@ -165,7 +168,7 @@ static void cli_handle_backspace(Cli* cli) { cli->cursor_position--; } else { - cli_putc(cli, CliSymbolAsciiBell); + cli_putc(cli, CliKeyBell); } } @@ -241,7 +244,7 @@ static void cli_handle_enter(Cli* cli) { printf( "`%s` command not found, use `help` or `?` to list all available commands", furi_string_get_cstr(command)); - cli_putc(cli, CliSymbolAsciiBell); + cli_putc(cli, CliKeyBell); } cli_reset(cli); @@ -305,8 +308,72 @@ static void cli_handle_autocomplete(Cli* cli) { cli_prompt(cli); } -static void cli_handle_escape(Cli* cli, char c) { - if(c == 'A') { +/** + * @brief Determines the class that a character belongs to + * + * The return value of this function does not make sense on its own; it's only + * useful for comparing it with other values returned by this function. This + * function is used internally in `cli_skip_run` + */ +static uint8_t cli_char_class(char c) { + if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') { + return 0; + } else if(c == ' ') { + return 1; + } else { + return 255; + } +} + +/** + * @brief Skips a run of a class of characters + * + * @param string Input string + * @param original_pos Position to start the search at + * @param direction Direction in which to perform the search: + * left (`-1`) or right (`1`) + * @returns The position at which the run ends + */ +static size_t cli_skip_run(FuriString* string, size_t original_pos, int8_t direction) { + if(furi_string_size(string) == 0) return original_pos; + if(direction == -1 && original_pos == 0) return original_pos; + if(direction == 1 && original_pos == furi_string_size(string)) return original_pos; + + int8_t look_offset = direction == -1 ? -1 : 0; + int32_t position = original_pos; + uint8_t start_class = cli_char_class(furi_string_get_char(string, position + look_offset)); + + while(true) { + position += direction; + if(position < 0) break; + if(position >= (int32_t)furi_string_size(string)) break; + if(cli_char_class(furi_string_get_char(string, position + look_offset)) != start_class) + break; + } + + return MAX(0, position); +} + +void cli_process_input(Cli* cli) { + CliKeyCombo combo = cli_read_ansi_key_combo(cli); + FURI_LOG_T(TAG, "code=0x%02x, mod=0x%x\r\n", combo.key, combo.modifiers); + + if(combo.key == CliKeyTab) { + cli_handle_autocomplete(cli); + + } else if(combo.key == CliKeySOH) { + furi_delay_ms(33); // We are too fast, Minicom is not ready yet + cli_motd(); + cli_prompt(cli); + + } else if(combo.key == CliKeyETX) { + cli_reset(cli); + cli_prompt(cli); + + } else if(combo.key == CliKeyEOT) { + cli_reset(cli); + + } else if(combo.key == CliKeyUp && combo.modifiers == CliModKeyNo) { // Use previous command if line buffer is empty if(furi_string_size(cli->line) == 0 && furi_string_cmp(cli->line, cli->last_line) != 0) { // Set line buffer and cursor position @@ -315,71 +382,84 @@ static void cli_handle_escape(Cli* cli, char c) { // Show new line to user printf("%s", furi_string_get_cstr(cli->line)); } - } else if(c == 'B') { - // Clear input + + } else if(combo.key == CliKeyDown && combo.modifiers == CliModKeyNo) { + // Clear input buffer furi_string_reset(cli->line); cli->cursor_position = 0; - printf("\r>: \e[0K"); - } else if(c == 'C') { + printf("\r" CLI_PROMPT "\e[0K"); + + } else if(combo.key == CliKeyRight && combo.modifiers == CliModKeyNo) { + // Move right if(cli->cursor_position < furi_string_size(cli->line)) { cli->cursor_position++; printf("\e[C"); } - } else if(c == 'D') { + + } else if(combo.key == CliKeyLeft && combo.modifiers == CliModKeyNo) { + // Move left if(cli->cursor_position > 0) { cli->cursor_position--; printf("\e[D"); } - } - fflush(stdout); -} -void cli_process_input(Cli* cli) { - char in_chr = cli_getc(cli); - size_t rx_len; + } else if(combo.key == CliKeyHome && combo.modifiers == CliModKeyNo) { + // Move to beginning of line + cli->cursor_position = 0; + printf("\e[%dG", CLI_PROMPT_LENGTH + 1); // columns start at 1 \(-_-)/ - if(in_chr == CliSymbolAsciiTab) { - cli_handle_autocomplete(cli); - } else if(in_chr == CliSymbolAsciiSOH) { - furi_delay_ms(33); // We are too fast, Minicom is not ready yet - cli_motd(); - cli_prompt(cli); - } else if(in_chr == CliSymbolAsciiETX) { - cli_reset(cli); - cli_prompt(cli); - } else if(in_chr == CliSymbolAsciiEOT) { - cli_reset(cli); - } else if(in_chr == CliSymbolAsciiEsc) { - rx_len = cli_read(cli, (uint8_t*)&in_chr, 1); - if((rx_len > 0) && (in_chr == '[')) { - cli_read(cli, (uint8_t*)&in_chr, 1); - cli_handle_escape(cli, in_chr); - } else { - cli_putc(cli, CliSymbolAsciiBell); - } - } else if(in_chr == CliSymbolAsciiBackspace || in_chr == CliSymbolAsciiDel) { + } else if(combo.key == CliKeyEnd && combo.modifiers == CliModKeyNo) { + // Move to end of line + cli->cursor_position = furi_string_size(cli->line); + printf("\e[%dG", CLI_PROMPT_LENGTH + cli->cursor_position + 1); + + } else if( + combo.modifiers == CliModKeyCtrl && + (combo.key == CliKeyLeft || combo.key == CliKeyRight)) { + // Skip run of similar chars to the left or right + int32_t direction = (combo.key == CliKeyLeft) ? -1 : 1; + cli->cursor_position = cli_skip_run(cli->line, cli->cursor_position, direction); + printf("\e[%dG", CLI_PROMPT_LENGTH + cli->cursor_position + 1); + + } else if(combo.key == CliKeyBackspace || combo.key == CliKeyDEL) { cli_handle_backspace(cli); - } else if(in_chr == CliSymbolAsciiCR) { + + } else if(combo.key == CliKeyETB) { // Ctrl + Backspace + // Delete run of similar chars to the left + size_t run_start = cli_skip_run(cli->line, cli->cursor_position, -1); + furi_string_replace_at(cli->line, run_start, cli->cursor_position - run_start, ""); + cli->cursor_position = run_start; + printf( + "\e[%dG%s\e[0K\e[%dG", // move cursor, print second half of line, erase remains, move cursor again + CLI_PROMPT_LENGTH + cli->cursor_position + 1, + furi_string_get_cstr(cli->line) + run_start, + CLI_PROMPT_LENGTH + run_start + 1); + + } else if(combo.key == CliKeyCR) { cli_handle_enter(cli); + } else if( - (in_chr >= 0x20 && in_chr < 0x7F) && //-V560 + (combo.key >= 0x20 && combo.key < 0x7F) && //-V560 (furi_string_size(cli->line) < CLI_INPUT_LEN_LIMIT)) { if(cli->cursor_position == furi_string_size(cli->line)) { - furi_string_push_back(cli->line, in_chr); - cli_putc(cli, in_chr); + furi_string_push_back(cli->line, combo.key); + cli_putc(cli, combo.key); } else { // Insert character to line buffer - const char in_str[2] = {in_chr, 0}; + const char in_str[2] = {combo.key, 0}; furi_string_replace_at(cli->line, cli->cursor_position, 0, in_str); // Print character in replace mode - printf("\e[4h%c\e[4l", in_chr); + printf("\e[4h%c\e[4l", combo.key); fflush(stdout); } cli->cursor_position++; + } else { - cli_putc(cli, CliSymbolAsciiBell); + cli_putc(cli, CliKeyBell); } + + fflush(stdout); } void cli_add_command( diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h index bb84670a739..c91f71c4474 100644 --- a/applications/services/cli/cli.h +++ b/applications/services/cli/cli.h @@ -10,26 +10,12 @@ extern "C" { #endif -typedef enum { - CliSymbolAsciiSOH = 0x01, - CliSymbolAsciiETX = 0x03, - CliSymbolAsciiEOT = 0x04, - CliSymbolAsciiBell = 0x07, - CliSymbolAsciiBackspace = 0x08, - CliSymbolAsciiTab = 0x09, - CliSymbolAsciiLF = 0x0A, - CliSymbolAsciiCR = 0x0D, - CliSymbolAsciiEsc = 0x1B, - CliSymbolAsciiUS = 0x1F, - CliSymbolAsciiSpace = 0x20, - CliSymbolAsciiDel = 0x7F, -} CliSymbols; - typedef enum { CliCommandFlagDefault = 0, /**< Default, loader lock is used */ CliCommandFlagParallelSafe = (1 << 0), /**< Safe to run in parallel with other apps, loader lock is not used */ CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ + CliCommandFlagHidden = (1 << 2), /**< Not shown in `help` */ } CliCommandFlag; #define RECORD_CLI "cli" diff --git a/applications/services/cli/cli_ansi.c b/applications/services/cli/cli_ansi.c new file mode 100644 index 00000000000..d27c20bad09 --- /dev/null +++ b/applications/services/cli/cli_ansi.c @@ -0,0 +1,76 @@ +#include "cli_ansi.h" + +/** + * @brief Converts a single character representing a special key into the enum + * representation + */ +static CliKey cli_ansi_key_from_mnemonic(char c) { + switch(c) { + case 'A': + return CliKeyUp; + case 'B': + return CliKeyDown; + case 'C': + return CliKeyRight; + case 'D': + return CliKeyLeft; + case 'F': + return CliKeyEnd; + case 'H': + return CliKeyHome; + default: + return CliKeyUnrecognized; + } +} + +CliKeyCombo cli_read_ansi_key_combo(Cli* cli) { + char ch = cli_getc(cli); + + if(ch != CliKeyEsc) + return (CliKeyCombo){ + .modifiers = CliModKeyNo, + .key = ch, + }; + + ch = cli_getc(cli); + + // ESC ESC -> ESC + if(ch == '\e') + return (CliKeyCombo){ + .modifiers = CliModKeyNo, + .key = '\e', + }; + + // ESC -> Alt + + if(ch != '[') + return (CliKeyCombo){ + .modifiers = CliModKeyAlt, + .key = cli_getc(cli), + }; + + ch = cli_getc(cli); + + // ESC [ 1 + if(ch == '1') { + // ESC [ 1 ; + if(cli_getc(cli) == ';') { + CliModKey modifiers = (cli_getc(cli) - '0'); // convert following digit to a number + modifiers &= ~1; + return (CliKeyCombo){ + .modifiers = modifiers, + .key = cli_ansi_key_from_mnemonic(cli_getc(cli)), + }; + } + + return (CliKeyCombo){ + .modifiers = CliModKeyNo, + .key = CliKeyUnrecognized, + }; + } + + // ESC [ + return (CliKeyCombo){ + .modifiers = CliModKeyNo, + .key = cli_ansi_key_from_mnemonic(ch), + }; +} diff --git a/applications/services/cli/cli_ansi.h b/applications/services/cli/cli_ansi.h new file mode 100644 index 00000000000..110d8a5fcdb --- /dev/null +++ b/applications/services/cli/cli_ansi.h @@ -0,0 +1,94 @@ +#pragma once + +#include "cli.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define ANSI_RESET "\e[0m" +#define ANSI_BOLD "\e[1m" +#define ANSI_FAINT "\e[2m" + +#define ANSI_FG_BLACK "\e[30m" +#define ANSI_FG_RED "\e[31m" +#define ANSI_FG_GREEN "\e[32m" +#define ANSI_FG_YELLOW "\e[33m" +#define ANSI_FG_BLUE "\e[34m" +#define ANSI_FG_MAGENTA "\e[35m" +#define ANSI_FG_CYAN "\e[36m" +#define ANSI_FG_WHITE "\e[37m" +#define ANSI_FG_BR_BLACK "\e[90m" +#define ANSI_FG_BR_RED "\e[91m" +#define ANSI_FG_BR_GREEN "\e[92m" +#define ANSI_FG_BR_YELLOW "\e[93m" +#define ANSI_FG_BR_BLUE "\e[94m" +#define ANSI_FG_BR_MAGENTA "\e[95m" +#define ANSI_FG_BR_CYAN "\e[96m" +#define ANSI_FG_BR_WHITE "\e[97m" + +#define ANSI_BG_BLACK "\e[40m" +#define ANSI_BG_RED "\e[41m" +#define ANSI_BG_GREEN "\e[42m" +#define ANSI_BG_YELLOW "\e[43m" +#define ANSI_BG_BLUE "\e[44m" +#define ANSI_BG_MAGENTA "\e[45m" +#define ANSI_BG_CYAN "\e[46m" +#define ANSI_BG_WHITE "\e[47m" +#define ANSI_BG_BR_BLACK "\e[100m" +#define ANSI_BG_BR_RED "\e[101m" +#define ANSI_BG_BR_GREEN "\e[102m" +#define ANSI_BG_BR_YELLOW "\e[103m" +#define ANSI_BG_BR_BLUE "\e[104m" +#define ANSI_BG_BR_MAGENTA "\e[105m" +#define ANSI_BG_BR_CYAN "\e[106m" +#define ANSI_BG_BR_WHITE "\e[107m" + +#define ANSI_FLIPPER_BRAND_ORANGE "\e[38;2;255;130;0m" + +typedef enum { + CliKeyUnrecognized = 0, + + CliKeySOH = 0x01, + CliKeyETX = 0x03, + CliKeyEOT = 0x04, + CliKeyBell = 0x07, + CliKeyBackspace = 0x08, + CliKeyTab = 0x09, + CliKeyLF = 0x0A, + CliKeyCR = 0x0D, + CliKeyETB = 0x17, + CliKeyEsc = 0x1B, + CliKeyUS = 0x1F, + CliKeySpace = 0x20, + CliKeyDEL = 0x7F, + + CliKeySpecial = 0x80, + CliKeyLeft, + CliKeyRight, + CliKeyUp, + CliKeyDown, + CliKeyHome, + CliKeyEnd, +} CliKey; + +typedef enum { + CliModKeyNo = 0, + CliModKeyAlt = 2, + CliModKeyCtrl = 4, + CliModKeyMeta = 8, +} CliModKey; + +typedef struct { + CliModKey modifiers; + CliKey key; +} CliKeyCombo; + +/** + * @brief Reads a key or key combination + */ +CliKeyCombo cli_read_ansi_key_combo(Cli* cli); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index 0ad58565552..47f238bce95 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -1,5 +1,6 @@ #include "cli_commands.h" #include "cli_command_gpio.h" +#include "cli_ansi.h" #include #include @@ -10,6 +11,7 @@ #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" @@ -52,37 +54,200 @@ void cli_command_info(Cli* cli, FuriString* args, void* context) { } } -void cli_command_help(Cli* cli, FuriString* args, void* context) { +// Lil Easter egg :> +void cli_command_neofetch(Cli* cli, FuriString* args, void* context) { + UNUSED(cli); 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, + 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(Cli* cli, FuriString* args, void* context) { + UNUSED(context); printf("Commands available:"); - // Command count - const size_t commands_count = CliCommandTree_size(cli->commands); - const size_t commands_count_mid = commands_count / 2 + commands_count % 2; + // Count non-hidden commands + CliCommandTree_it_t it_count; + CliCommandTree_it(it_count, cli->commands); + size_t commands_count = 0; + while(!CliCommandTree_end_p(it_count)) { + if(!(CliCommandTree_cref(it_count)->value_ptr->flags & CliCommandFlagHidden)) + commands_count++; + CliCommandTree_next(it_count); + } - // Use 2 iterators from start and middle to show 2 columns - CliCommandTree_it_t it_left; - CliCommandTree_it(it_left, cli->commands); - CliCommandTree_it_t it_right; - CliCommandTree_it(it_right, cli->commands); - for(size_t i = 0; i < commands_count_mid; i++) - CliCommandTree_next(it_right); + // 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], cli->commands); + for(size_t i = 0; i < c * commands_per_column; i++) + CliCommandTree_next(iterators[c]); + } - // Iterate throw tree - for(size_t i = 0; i < commands_count_mid; i++) { + // Print commands + for(size_t r = 0; r < commands_per_column; r++) { printf("\r\n"); - // Left Column - if(!CliCommandTree_end_p(it_left)) { - printf("%-30s", furi_string_get_cstr(*CliCommandTree_ref(it_left)->key_ptr)); - CliCommandTree_next(it_left); - } - // Right Column - if(!CliCommandTree_end_p(it_right)) { - printf("%s", furi_string_get_cstr(*CliCommandTree_ref(it_right)->key_ptr)); - CliCommandTree_next(it_right); + + for(size_t c = 0; c < columns; c++) { + if(!CliCommandTree_end_p(iterators[c])) { + const CliCommandTree_itref_t* item = CliCommandTree_cref(iterators[c]); + if(!(item->value_ptr->flags & CliCommandFlagHidden)) { + printf("%-30s", furi_string_get_cstr(*item->key_ptr)); + } + CliCommandTree_next(iterators[c]); + } } - }; + } if(furi_string_size(args) > 0) { cli_nl(cli); @@ -503,6 +668,12 @@ 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, + "neofetch", + CliCommandFlagParallelSafe | CliCommandFlagHidden, + cli_command_neofetch, + NULL); cli_add_command(cli, "?", CliCommandFlagParallelSafe, cli_command_help, NULL); cli_add_command(cli, "help", CliCommandFlagParallelSafe, cli_command_help, NULL); diff --git a/applications/services/crypto/crypto_cli.c b/applications/services/crypto/crypto_cli.c index 744fa7151d1..90746801aae 100644 --- a/applications/services/crypto/crypto_cli.c +++ b/applications/services/crypto/crypto_cli.c @@ -3,6 +3,7 @@ #include #include +#include void crypto_cli_print_usage(void) { printf("Usage:\r\n"); @@ -45,14 +46,14 @@ void crypto_cli_encrypt(Cli* cli, FuriString* args) { input = furi_string_alloc(); char c; while(cli_read(cli, (uint8_t*)&c, 1) == 1) { - if(c == CliSymbolAsciiETX) { + if(c == CliKeyETX) { printf("\r\n"); break; } else if(c >= 0x20 && c < 0x7F) { putc(c, stdout); fflush(stdout); furi_string_push_back(input, c); - } else if(c == CliSymbolAsciiCR) { + } else if(c == CliKeyCR) { printf("\r\n"); furi_string_cat(input, "\r\n"); } @@ -120,14 +121,14 @@ void crypto_cli_decrypt(Cli* cli, FuriString* args) { hex_input = furi_string_alloc(); char c; while(cli_read(cli, (uint8_t*)&c, 1) == 1) { - if(c == CliSymbolAsciiETX) { + if(c == CliKeyETX) { printf("\r\n"); break; } else if(c >= 0x20 && c < 0x7F) { putc(c, stdout); fflush(stdout); furi_string_push_back(hex_input, c); - } else if(c == CliSymbolAsciiCR) { + } else if(c == CliKeyCR) { printf("\r\n"); } } diff --git a/applications/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 441b58da66b..17bbc02a5b4 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -224,7 +225,7 @@ static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) { while(true) { uint8_t symbol = cli_getc(cli); - if(symbol == CliSymbolAsciiETX) { + if(symbol == CliKeyETX) { size_t write_size = read_index % buffer_size; if(write_size > 0) { From 2053347eaaf78576ba0f1581c5058879361c0a24 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Wed, 2 Oct 2024 20:58:09 +0300 Subject: [PATCH 4/7] ci: fix compact build error --- applications/services/cli/cli_commands.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index 47f238bce95..cb813840a47 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -123,11 +123,7 @@ void cli_command_neofetch(Cli* cli, FuriString* args, void* context) { // 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, - logo_width, - (i < logo_height) ? neofetch_logo[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()); From 44c09fd7708bbc8f497ac8bd9c30ba2689e7a114 Mon Sep 17 00:00:00 2001 From: Aleksandr Kutuzov Date: Sun, 6 Oct 2024 20:49:13 +0100 Subject: [PATCH 5/7] Make PVS happy --- applications/services/cli/cli.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index 95e549ac605..e847da95fe5 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -406,12 +406,12 @@ void cli_process_input(Cli* cli) { } else if(combo.key == CliKeyHome && combo.modifiers == CliModKeyNo) { // Move to beginning of line cli->cursor_position = 0; - printf("\e[%dG", CLI_PROMPT_LENGTH + 1); // columns start at 1 \(-_-)/ + printf("\e[%uG", CLI_PROMPT_LENGTH + 1); // columns start at 1 \(-_-)/ } else if(combo.key == CliKeyEnd && combo.modifiers == CliModKeyNo) { // Move to end of line cli->cursor_position = furi_string_size(cli->line); - printf("\e[%dG", CLI_PROMPT_LENGTH + cli->cursor_position + 1); + printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1); } else if( combo.modifiers == CliModKeyCtrl && @@ -419,7 +419,7 @@ void cli_process_input(Cli* cli) { // Skip run of similar chars to the left or right int32_t direction = (combo.key == CliKeyLeft) ? -1 : 1; cli->cursor_position = cli_skip_run(cli->line, cli->cursor_position, direction); - printf("\e[%dG", CLI_PROMPT_LENGTH + cli->cursor_position + 1); + printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1); } else if(combo.key == CliKeyBackspace || combo.key == CliKeyDEL) { cli_handle_backspace(cli); @@ -430,7 +430,7 @@ void cli_process_input(Cli* cli) { furi_string_replace_at(cli->line, run_start, cli->cursor_position - run_start, ""); cli->cursor_position = run_start; printf( - "\e[%dG%s\e[0K\e[%dG", // move cursor, print second half of line, erase remains, move cursor again + "\e[%zuG%s\e[0K\e[%zuG", // move cursor, print second half of line, erase remains, move cursor again CLI_PROMPT_LENGTH + cli->cursor_position + 1, furi_string_get_cstr(cli->line) + run_start, CLI_PROMPT_LENGTH + run_start + 1); From 9f14d1ef2c71a0fcd8972a7a921ad820c17db9cb Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 7 Oct 2024 14:06:05 +0300 Subject: [PATCH 6/7] style: remove magic numbers --- applications/services/cli/cli.c | 48 +++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index e847da95fe5..097769262f3 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -308,43 +308,56 @@ static void cli_handle_autocomplete(Cli* cli) { cli_prompt(cli); } +typedef enum { + CliCharClassWord, + CliCharClassSpace, + CliCharClassOther, +} CliCharClass; + /** * @brief Determines the class that a character belongs to * - * The return value of this function does not make sense on its own; it's only - * useful for comparing it with other values returned by this function. This - * function is used internally in `cli_skip_run` + * The return value of this function should not be used on its own; it should + * only be used for comparing it with other values returned by this function. + * This function is used internally in `cli_skip_run`. */ -static uint8_t cli_char_class(char c) { +static CliCharClass cli_char_class(char c) { if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') { - return 0; + return CliCharClassWord; } else if(c == ' ') { - return 1; + return CliCharClassSpace; } else { - return 255; + return CliCharClassOther; } } +typedef enum { + CliSkipToTheLeft, + CliSkipToTheRight, +} CliSkipDirection; + /** * @brief Skips a run of a class of characters * * @param string Input string * @param original_pos Position to start the search at - * @param direction Direction in which to perform the search: - * left (`-1`) or right (`1`) + * @param direction Direction in which to perform the search * @returns The position at which the run ends */ -static size_t cli_skip_run(FuriString* string, size_t original_pos, int8_t direction) { +static size_t cli_skip_run(FuriString* string, size_t original_pos, CliSkipDirection direction) { if(furi_string_size(string) == 0) return original_pos; - if(direction == -1 && original_pos == 0) return original_pos; - if(direction == 1 && original_pos == furi_string_size(string)) return original_pos; + if(direction == CliSkipToTheLeft && original_pos == 0) return original_pos; + if(direction == CliSkipToTheRight && original_pos == furi_string_size(string)) + return original_pos; - int8_t look_offset = direction == -1 ? -1 : 0; + int8_t look_offset = (direction == CliSkipToTheLeft) ? -1 : 0; + int8_t increment = (direction == CliSkipToTheLeft) ? -1 : 1; int32_t position = original_pos; - uint8_t start_class = cli_char_class(furi_string_get_char(string, position + look_offset)); + CliCharClass start_class = + cli_char_class(furi_string_get_char(string, position + look_offset)); while(true) { - position += direction; + position += increment; if(position < 0) break; if(position >= (int32_t)furi_string_size(string)) break; if(cli_char_class(furi_string_get_char(string, position + look_offset)) != start_class) @@ -417,7 +430,8 @@ void cli_process_input(Cli* cli) { combo.modifiers == CliModKeyCtrl && (combo.key == CliKeyLeft || combo.key == CliKeyRight)) { // Skip run of similar chars to the left or right - int32_t direction = (combo.key == CliKeyLeft) ? -1 : 1; + CliSkipDirection direction = (combo.key == CliKeyLeft) ? CliSkipToTheLeft : + CliSkipToTheRight; cli->cursor_position = cli_skip_run(cli->line, cli->cursor_position, direction); printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1); @@ -426,7 +440,7 @@ void cli_process_input(Cli* cli) { } else if(combo.key == CliKeyETB) { // Ctrl + Backspace // Delete run of similar chars to the left - size_t run_start = cli_skip_run(cli->line, cli->cursor_position, -1); + size_t run_start = cli_skip_run(cli->line, cli->cursor_position, CliSkipToTheLeft); furi_string_replace_at(cli->line, run_start, cli->cursor_position - run_start, ""); cli->cursor_position = run_start; printf( From 7bd746ee67e60fe8391ba21b414de366227e35fd Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Fri, 11 Oct 2024 15:38:11 +0300 Subject: [PATCH 7/7] style: review suggestions --- applications/services/cli/cli.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index 097769262f3..6f18ee97316 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -332,8 +332,8 @@ static CliCharClass cli_char_class(char c) { } typedef enum { - CliSkipToTheLeft, - CliSkipToTheRight, + CliSkipDirectionLeft, + CliSkipDirectionRight, } CliSkipDirection; /** @@ -346,12 +346,12 @@ typedef enum { */ static size_t cli_skip_run(FuriString* string, size_t original_pos, CliSkipDirection direction) { if(furi_string_size(string) == 0) return original_pos; - if(direction == CliSkipToTheLeft && original_pos == 0) return original_pos; - if(direction == CliSkipToTheRight && original_pos == furi_string_size(string)) + 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 == CliSkipToTheLeft) ? -1 : 0; - int8_t increment = (direction == CliSkipToTheLeft) ? -1 : 1; + int8_t look_offset = (direction == CliSkipDirectionLeft) ? -1 : 0; + int8_t increment = (direction == CliSkipDirectionLeft) ? -1 : 1; int32_t position = original_pos; CliCharClass start_class = cli_char_class(furi_string_get_char(string, position + look_offset)); @@ -430,8 +430,8 @@ void cli_process_input(Cli* cli) { combo.modifiers == CliModKeyCtrl && (combo.key == CliKeyLeft || combo.key == CliKeyRight)) { // Skip run of similar chars to the left or right - CliSkipDirection direction = (combo.key == CliKeyLeft) ? CliSkipToTheLeft : - CliSkipToTheRight; + CliSkipDirection direction = (combo.key == CliKeyLeft) ? CliSkipDirectionLeft : + CliSkipDirectionRight; cli->cursor_position = cli_skip_run(cli->line, cli->cursor_position, direction); printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1); @@ -440,7 +440,7 @@ void cli_process_input(Cli* cli) { } else if(combo.key == CliKeyETB) { // Ctrl + Backspace // Delete run of similar chars to the left - size_t run_start = cli_skip_run(cli->line, cli->cursor_position, CliSkipToTheLeft); + size_t run_start = cli_skip_run(cli->line, cli->cursor_position, CliSkipDirectionLeft); furi_string_replace_at(cli->line, run_start, cli->cursor_position - run_start, ""); cli->cursor_position = run_start; printf(