Skip to content

Commit 0f83141

Browse files
[FL-3909] CLI improvements, part I (#3928)
* fix: cli top blinking * feat: clear prompt on down key * feat: proper-er ansi escape sequence handling * ci: fix compact build error * Make PVS happy * style: remove magic numbers * style: review suggestions Co-authored-by: あく <[email protected]>
1 parent d9d3867 commit 0f83141

File tree

9 files changed

+528
-101
lines changed

9 files changed

+528
-101
lines changed

applications/main/subghz/subghz_cli.c

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -999,13 +999,12 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) {
999999
chat_event = subghz_chat_worker_get_event_chat(subghz_chat);
10001000
switch(chat_event.event) {
10011001
case SubGhzChatEventInputData:
1002-
if(chat_event.c == CliSymbolAsciiETX) {
1002+
if(chat_event.c == CliKeyETX) {
10031003
printf("\r\n");
10041004
chat_event.event = SubGhzChatEventUserExit;
10051005
subghz_chat_worker_put_event_chat(subghz_chat, &chat_event);
10061006
break;
1007-
} else if(
1008-
(chat_event.c == CliSymbolAsciiBackspace) || (chat_event.c == CliSymbolAsciiDel)) {
1007+
} else if((chat_event.c == CliKeyBackspace) || (chat_event.c == CliKeyDEL)) {
10091008
size_t len = furi_string_utf8_length(input);
10101009
if(len > furi_string_utf8_length(name)) {
10111010
printf("%s", "\e[D\e[1P");
@@ -1027,7 +1026,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) {
10271026
}
10281027
furi_string_set(input, sysmsg);
10291028
}
1030-
} else if(chat_event.c == CliSymbolAsciiCR) {
1029+
} else if(chat_event.c == CliKeyCR) {
10311030
printf("\r\n");
10321031
furi_string_push_back(input, '\r');
10331032
furi_string_push_back(input, '\n');
@@ -1041,7 +1040,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) {
10411040
furi_string_printf(input, "%s", furi_string_get_cstr(name));
10421041
printf("%s", furi_string_get_cstr(input));
10431042
fflush(stdout);
1044-
} else if(chat_event.c == CliSymbolAsciiLF) {
1043+
} else if(chat_event.c == CliKeyLF) {
10451044
//cut out the symbol \n
10461045
} else {
10471046
putc(chat_event.c, stdout);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

33
#include <cli/cli.h>
4+
#include <cli/cli_ansi.h>
45

56
void subghz_on_system_start(void);

applications/services/cli/cli.c

Lines changed: 146 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
#include "cli_i.h"
22
#include "cli_commands.h"
33
#include "cli_vcp.h"
4+
#include "cli_ansi.h"
45
#include <furi_hal_version.h>
56
#include <loader/loader.h>
67

78
#define TAG "CliSrv"
89

910
#define CLI_INPUT_LEN_LIMIT 256
11+
#define CLI_PROMPT ">: " // qFlipper does not recognize us if we use escape sequences :(
12+
#define CLI_PROMPT_LENGTH 3 // printable characters
1013

1114
Cli* cli_alloc(void) {
1215
Cli* cli = malloc(sizeof(Cli));
@@ -85,7 +88,7 @@ bool cli_cmd_interrupt_received(Cli* cli) {
8588
char c = '\0';
8689
if(cli_is_connected(cli)) {
8790
if(cli->session->rx((uint8_t*)&c, 1, 0) == 1) {
88-
return c == CliSymbolAsciiETX;
91+
return c == CliKeyETX;
8992
}
9093
} else {
9194
return true;
@@ -102,7 +105,8 @@ void cli_print_usage(const char* cmd, const char* usage, const char* arg) {
102105
}
103106

104107
void cli_motd(void) {
105-
printf("\r\n"
108+
printf(ANSI_FLIPPER_BRAND_ORANGE
109+
"\r\n"
106110
" _.-------.._ -,\r\n"
107111
" .-\"```\"--..,,_/ /`-, -, \\ \r\n"
108112
" .:\" /:/ /'\\ \\ ,_..., `. | |\r\n"
@@ -116,12 +120,11 @@ void cli_motd(void) {
116120
" _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n"
117121
"| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n"
118122
"| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n"
119-
"|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n"
120-
"\r\n"
121-
"Welcome to Flipper Zero Command Line Interface!\r\n"
123+
"|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" ANSI_RESET
124+
"\r\n" ANSI_FG_BR_WHITE "Welcome to " ANSI_FLIPPER_BRAND_ORANGE
125+
"Flipper Zero" ANSI_FG_BR_WHITE " Command Line Interface!\r\n"
122126
"Read the manual: https://docs.flipper.net/development/cli\r\n"
123-
"Run `help` or `?` to list available commands\r\n"
124-
"\r\n");
127+
"Run `help` or `?` to list available commands\r\n" ANSI_RESET "\r\n");
125128

126129
const Version* firmware_version = furi_hal_version_get_firmware_version();
127130
if(firmware_version) {
@@ -142,7 +145,7 @@ void cli_nl(Cli* cli) {
142145

143146
void cli_prompt(Cli* cli) {
144147
UNUSED(cli);
145-
printf("\r\n>: %s", furi_string_get_cstr(cli->line));
148+
printf("\r\n" CLI_PROMPT "%s", furi_string_get_cstr(cli->line));
146149
fflush(stdout);
147150
}
148151

@@ -165,7 +168,7 @@ static void cli_handle_backspace(Cli* cli) {
165168

166169
cli->cursor_position--;
167170
} else {
168-
cli_putc(cli, CliSymbolAsciiBell);
171+
cli_putc(cli, CliKeyBell);
169172
}
170173
}
171174

@@ -241,7 +244,7 @@ static void cli_handle_enter(Cli* cli) {
241244
printf(
242245
"`%s` command not found, use `help` or `?` to list all available commands",
243246
furi_string_get_cstr(command));
244-
cli_putc(cli, CliSymbolAsciiBell);
247+
cli_putc(cli, CliKeyBell);
245248
}
246249

247250
cli_reset(cli);
@@ -305,8 +308,85 @@ static void cli_handle_autocomplete(Cli* cli) {
305308
cli_prompt(cli);
306309
}
307310

308-
static void cli_handle_escape(Cli* cli, char c) {
309-
if(c == 'A') {
311+
typedef enum {
312+
CliCharClassWord,
313+
CliCharClassSpace,
314+
CliCharClassOther,
315+
} CliCharClass;
316+
317+
/**
318+
* @brief Determines the class that a character belongs to
319+
*
320+
* The return value of this function should not be used on its own; it should
321+
* only be used for comparing it with other values returned by this function.
322+
* This function is used internally in `cli_skip_run`.
323+
*/
324+
static CliCharClass cli_char_class(char c) {
325+
if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') {
326+
return CliCharClassWord;
327+
} else if(c == ' ') {
328+
return CliCharClassSpace;
329+
} else {
330+
return CliCharClassOther;
331+
}
332+
}
333+
334+
typedef enum {
335+
CliSkipDirectionLeft,
336+
CliSkipDirectionRight,
337+
} CliSkipDirection;
338+
339+
/**
340+
* @brief Skips a run of a class of characters
341+
*
342+
* @param string Input string
343+
* @param original_pos Position to start the search at
344+
* @param direction Direction in which to perform the search
345+
* @returns The position at which the run ends
346+
*/
347+
static size_t cli_skip_run(FuriString* string, size_t original_pos, CliSkipDirection direction) {
348+
if(furi_string_size(string) == 0) return original_pos;
349+
if(direction == CliSkipDirectionLeft && original_pos == 0) return original_pos;
350+
if(direction == CliSkipDirectionRight && original_pos == furi_string_size(string))
351+
return original_pos;
352+
353+
int8_t look_offset = (direction == CliSkipDirectionLeft) ? -1 : 0;
354+
int8_t increment = (direction == CliSkipDirectionLeft) ? -1 : 1;
355+
int32_t position = original_pos;
356+
CliCharClass start_class =
357+
cli_char_class(furi_string_get_char(string, position + look_offset));
358+
359+
while(true) {
360+
position += increment;
361+
if(position < 0) break;
362+
if(position >= (int32_t)furi_string_size(string)) break;
363+
if(cli_char_class(furi_string_get_char(string, position + look_offset)) != start_class)
364+
break;
365+
}
366+
367+
return MAX(0, position);
368+
}
369+
370+
void cli_process_input(Cli* cli) {
371+
CliKeyCombo combo = cli_read_ansi_key_combo(cli);
372+
FURI_LOG_T(TAG, "code=0x%02x, mod=0x%x\r\n", combo.key, combo.modifiers);
373+
374+
if(combo.key == CliKeyTab) {
375+
cli_handle_autocomplete(cli);
376+
377+
} else if(combo.key == CliKeySOH) {
378+
furi_delay_ms(33); // We are too fast, Minicom is not ready yet
379+
cli_motd();
380+
cli_prompt(cli);
381+
382+
} else if(combo.key == CliKeyETX) {
383+
cli_reset(cli);
384+
cli_prompt(cli);
385+
386+
} else if(combo.key == CliKeyEOT) {
387+
cli_reset(cli);
388+
389+
} else if(combo.key == CliKeyUp && combo.modifiers == CliModKeyNo) {
310390
// Use previous command if line buffer is empty
311391
if(furi_string_size(cli->line) == 0 && furi_string_cmp(cli->line, cli->last_line) != 0) {
312392
// Set line buffer and cursor position
@@ -315,67 +395,85 @@ static void cli_handle_escape(Cli* cli, char c) {
315395
// Show new line to user
316396
printf("%s", furi_string_get_cstr(cli->line));
317397
}
318-
} else if(c == 'B') {
319-
} else if(c == 'C') {
398+
399+
} else if(combo.key == CliKeyDown && combo.modifiers == CliModKeyNo) {
400+
// Clear input buffer
401+
furi_string_reset(cli->line);
402+
cli->cursor_position = 0;
403+
printf("\r" CLI_PROMPT "\e[0K");
404+
405+
} else if(combo.key == CliKeyRight && combo.modifiers == CliModKeyNo) {
406+
// Move right
320407
if(cli->cursor_position < furi_string_size(cli->line)) {
321408
cli->cursor_position++;
322409
printf("\e[C");
323410
}
324-
} else if(c == 'D') {
411+
412+
} else if(combo.key == CliKeyLeft && combo.modifiers == CliModKeyNo) {
413+
// Move left
325414
if(cli->cursor_position > 0) {
326415
cli->cursor_position--;
327416
printf("\e[D");
328417
}
329-
}
330-
fflush(stdout);
331-
}
332418

333-
void cli_process_input(Cli* cli) {
334-
char in_chr = cli_getc(cli);
335-
size_t rx_len;
419+
} else if(combo.key == CliKeyHome && combo.modifiers == CliModKeyNo) {
420+
// Move to beginning of line
421+
cli->cursor_position = 0;
422+
printf("\e[%uG", CLI_PROMPT_LENGTH + 1); // columns start at 1 \(-_-)/
336423

337-
if(in_chr == CliSymbolAsciiTab) {
338-
cli_handle_autocomplete(cli);
339-
} else if(in_chr == CliSymbolAsciiSOH) {
340-
furi_delay_ms(33); // We are too fast, Minicom is not ready yet
341-
cli_motd();
342-
cli_prompt(cli);
343-
} else if(in_chr == CliSymbolAsciiETX) {
344-
cli_reset(cli);
345-
cli_prompt(cli);
346-
} else if(in_chr == CliSymbolAsciiEOT) {
347-
cli_reset(cli);
348-
} else if(in_chr == CliSymbolAsciiEsc) {
349-
rx_len = cli_read(cli, (uint8_t*)&in_chr, 1);
350-
if((rx_len > 0) && (in_chr == '[')) {
351-
cli_read(cli, (uint8_t*)&in_chr, 1);
352-
cli_handle_escape(cli, in_chr);
353-
} else {
354-
cli_putc(cli, CliSymbolAsciiBell);
355-
}
356-
} else if(in_chr == CliSymbolAsciiBackspace || in_chr == CliSymbolAsciiDel) {
424+
} else if(combo.key == CliKeyEnd && combo.modifiers == CliModKeyNo) {
425+
// Move to end of line
426+
cli->cursor_position = furi_string_size(cli->line);
427+
printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1);
428+
429+
} else if(
430+
combo.modifiers == CliModKeyCtrl &&
431+
(combo.key == CliKeyLeft || combo.key == CliKeyRight)) {
432+
// Skip run of similar chars to the left or right
433+
CliSkipDirection direction = (combo.key == CliKeyLeft) ? CliSkipDirectionLeft :
434+
CliSkipDirectionRight;
435+
cli->cursor_position = cli_skip_run(cli->line, cli->cursor_position, direction);
436+
printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1);
437+
438+
} else if(combo.key == CliKeyBackspace || combo.key == CliKeyDEL) {
357439
cli_handle_backspace(cli);
358-
} else if(in_chr == CliSymbolAsciiCR) {
440+
441+
} else if(combo.key == CliKeyETB) { // Ctrl + Backspace
442+
// Delete run of similar chars to the left
443+
size_t run_start = cli_skip_run(cli->line, cli->cursor_position, CliSkipDirectionLeft);
444+
furi_string_replace_at(cli->line, run_start, cli->cursor_position - run_start, "");
445+
cli->cursor_position = run_start;
446+
printf(
447+
"\e[%zuG%s\e[0K\e[%zuG", // move cursor, print second half of line, erase remains, move cursor again
448+
CLI_PROMPT_LENGTH + cli->cursor_position + 1,
449+
furi_string_get_cstr(cli->line) + run_start,
450+
CLI_PROMPT_LENGTH + run_start + 1);
451+
452+
} else if(combo.key == CliKeyCR) {
359453
cli_handle_enter(cli);
454+
360455
} else if(
361-
(in_chr >= 0x20 && in_chr < 0x7F) && //-V560
456+
(combo.key >= 0x20 && combo.key < 0x7F) && //-V560
362457
(furi_string_size(cli->line) < CLI_INPUT_LEN_LIMIT)) {
363458
if(cli->cursor_position == furi_string_size(cli->line)) {
364-
furi_string_push_back(cli->line, in_chr);
365-
cli_putc(cli, in_chr);
459+
furi_string_push_back(cli->line, combo.key);
460+
cli_putc(cli, combo.key);
366461
} else {
367462
// Insert character to line buffer
368-
const char in_str[2] = {in_chr, 0};
463+
const char in_str[2] = {combo.key, 0};
369464
furi_string_replace_at(cli->line, cli->cursor_position, 0, in_str);
370465

371466
// Print character in replace mode
372-
printf("\e[4h%c\e[4l", in_chr);
467+
printf("\e[4h%c\e[4l", combo.key);
373468
fflush(stdout);
374469
}
375470
cli->cursor_position++;
471+
376472
} else {
377-
cli_putc(cli, CliSymbolAsciiBell);
473+
cli_putc(cli, CliKeyBell);
378474
}
475+
476+
fflush(stdout);
379477
}
380478

381479
void cli_add_command(

applications/services/cli/cli.h

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,12 @@
1010
extern "C" {
1111
#endif
1212

13-
typedef enum {
14-
CliSymbolAsciiSOH = 0x01,
15-
CliSymbolAsciiETX = 0x03,
16-
CliSymbolAsciiEOT = 0x04,
17-
CliSymbolAsciiBell = 0x07,
18-
CliSymbolAsciiBackspace = 0x08,
19-
CliSymbolAsciiTab = 0x09,
20-
CliSymbolAsciiLF = 0x0A,
21-
CliSymbolAsciiCR = 0x0D,
22-
CliSymbolAsciiEsc = 0x1B,
23-
CliSymbolAsciiUS = 0x1F,
24-
CliSymbolAsciiSpace = 0x20,
25-
CliSymbolAsciiDel = 0x7F,
26-
} CliSymbols;
27-
2813
typedef enum {
2914
CliCommandFlagDefault = 0, /**< Default, loader lock is used */
3015
CliCommandFlagParallelSafe =
3116
(1 << 0), /**< Safe to run in parallel with other apps, loader lock is not used */
3217
CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */
18+
CliCommandFlagHidden = (1 << 2), /**< Not shown in `help` */
3319
} CliCommandFlag;
3420

3521
#define RECORD_CLI "cli"

0 commit comments

Comments
 (0)