diff --git a/src/configfile.c b/src/configfile.c index d9323a7..a8ac75d 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -250,6 +250,13 @@ static void config_parse_keys(GKeyFile *key_file, char *group) g_free((void *)string); string = NULL; } + config_get_string(key_file, group, "keymap", &string, NULL); + if (string != NULL) + { + option_parse_key_mappings(string); + g_free((void *)string); + string = NULL; + } config_get_string(key_file, group, "color", &string, NULL); if (string != NULL) { diff --git a/src/misc.c b/src/misc.c index eff7e3d..62a21f1 100644 --- a/src/misc.c +++ b/src/misc.c @@ -57,6 +57,16 @@ int ctrl_key_code(unsigned char key) return -1; } +int ctrl_key_char(int key_code) +{ + if (key_code >= ('a' & ~0x60) && key_code <= ('z' & ~0x60)) + { + return key_code | 0x60; + } + + return -1; +} + bool regex_match(const char *string, const char *pattern) { regex_t regex; diff --git a/src/misc.h b/src/misc.h index 6edbb9c..f501e82 100644 --- a/src/misc.h +++ b/src/misc.h @@ -26,10 +26,15 @@ #define POLL_NOWAIT (0) #define POLL_FOREVER (-1) + +#define TOSTRING_(x) #x +#define TOSTR(x) TOSTRING_(x) + #define UNUSED(expr) do { (void)(expr); } while (0) void delay(long ms); int ctrl_key_code(unsigned char key); +int ctrl_key_char(int key_code); bool regex_match(const char *string, const char *pattern); unsigned long djb2_hash(const unsigned char *str); void *base62_encode(unsigned long num, char *output); diff --git a/src/options.c b/src/options.c index 7e7eecf..eb07c7c 100644 --- a/src/options.c +++ b/src/options.c @@ -63,6 +63,7 @@ enum opt_t OPT_EXEC, OPT_RAW, OPT_RAW_INTERACTIVE, + OPT_KEYMAP, }; // clang-format off @@ -134,9 +135,12 @@ struct option_t option = .map_o_ign_cr = false, .raw = RAW_ON_DELAY, .raw_interactive = RAW_OFF, + .keymap = NULL, }; // clang-format on +struct keymap_t keymaps[KEYMAP_MAX] = {0}; + void option_print_help(char *argv[]) { UNUSED(argv); @@ -174,6 +178,7 @@ void option_print_help(char *argv[]) printf(" --log-append Append to log file\n"); printf(" --log-strip Strip control characters and escape sequences\n"); printf(" -m, --map Map characters\n"); + printf(" --keymap Set key-script mappings\n"); printf(" -c, --color 0..255|bold|none|list Colorize tio text (default: bold)\n"); printf(" -S, --socket Redirect I/O to socket\n"); printf(" --raw off|on|on-nodelay Select raw mode for non-interactive use (default: on)\n"); @@ -945,6 +950,11 @@ void options_print() tio_printf(" Script file: %s", option.script_filename); tio_printf(" Script run: %s", script_run_state_to_string(option.script_run)); } + if (option.script != NULL) + { + tio_printf(" Script command: %s", option.script); + tio_printf(" Script run: %s", script_run_state_to_string(option.script_run)); + } } void options_parse(int argc, char *argv[]) @@ -1003,6 +1013,7 @@ void options_parse(int argc, char *argv[]) {"log-strip", no_argument, 0, OPT_LOG_STRIP }, {"socket", required_argument, 0, 'S' }, {"map", required_argument, 0, 'm' }, + {"keymap", required_argument, 0, OPT_KEYMAP }, {"color", required_argument, 0, 'c' }, {"input-mode", required_argument, 0, OPT_INPUT_MODE }, {"output-mode", required_argument, 0, OPT_OUTPUT_MODE }, @@ -1215,6 +1226,11 @@ void options_parse(int argc, char *argv[]) option_parse_raw(optarg, &option.raw_interactive); break; + case OPT_KEYMAP: + option.keymap = optarg; + option_parse_key_mappings(optarg); + break; + case 'v': printf("tio %s\n", VERSION); exit(EXIT_SUCCESS); @@ -1299,3 +1315,169 @@ void options_parse_final(int argc, char *argv[]) // clang-format on #endif } + +int keymap_set(char *key_str, int key_len, char *func_str, int func_len) +{ + char func_str_r[KEYMAP_FUNC_STR_MAX + 1]; + char *srcp; + int dst_ofs; + int key_ofs; + int empty_idx, matched_idx, idx; + bool found_empty = false; + bool found_matched = false; + bool unset_requested = false; + + if (key_str[key_len] != '\0' || func_str[func_len] != '\0') + { + return -1; + } + + /* key_str should not include spaces */ + key_ofs = 0; + for (key_ofs = 0; key_ofs < key_len; key_ofs++) + { + if (key_str[key_ofs] == ' ') + { + tio_error_printf("Key should not include space"); + return -1; + } + } + + /* check disallowed key_str */ + if (strcmp(key_str, "q") == 0) + { + tio_error_printf("Key %s is immutable", key_str); + return -1; + } + + /* remove prefix spaces and postfix spaces from func_str */ + for (srcp = func_str; *srcp != '\0'; srcp++) + { + if (*srcp != ' ') + { + break; + } + } + strncpy(func_str_r, srcp, KEYMAP_KEY_STR_MAX); + func_str_r[KEYMAP_KEY_STR_MAX] = '\0'; + for (dst_ofs = strlen(func_str_r) - 1; dst_ofs >= 0; dst_ofs--) + { + if (func_str_r[dst_ofs] != ' ') + { + func_str_r[dst_ofs + 1] = '\0'; + break; + } + } + if (strcmp(func_str_r, "nil") == 0 || func_str_r[0] == '\0') + { + unset_requested = true; + } + + /* search for entry which key matched or is empty */ + for (idx = 0; idx < KEYMAP_MAX; idx++) + { + if (found_empty == false && keymaps[idx].key[0] == '\0') + { + empty_idx = idx; + found_empty = true; + } + if (found_matched == false && strcmp(keymaps[idx].key, key_str) == 0) + { + matched_idx = idx; + found_matched = true; + } + if (found_empty && found_matched) + { + break; + } + } + + /* update entry */ + if (unset_requested) + { + if (found_matched) + { + keymaps[matched_idx].key[0] = '\0'; + keymaps[matched_idx].func[0] = '\0'; + } + } + else /* set requested */ + { + if (found_matched) + { + strcpy(keymaps[matched_idx].key, key_str); + strcpy(keymaps[matched_idx].func, func_str); + } + else if (found_empty) + { + strcpy(keymaps[empty_idx].key, key_str); + strcpy(keymaps[empty_idx].func, func_str); + } + else + { + tio_error_printf("Too many keymaps", key_str); + return -1; + } + } + return 0; +} + +void keymaps_print(const char *title, int indent) +{ + int idx; + bool keymap_title_done = false; + + for (idx = 0; idx < KEYMAP_MAX; idx++) + { + if (keymaps[idx].key[0] == '\0') + { + continue; + } + if (!keymap_title_done) + { + if (title[0] != '\0') + { + tio_printf("%s", title); + } + keymap_title_done = true; + } + tio_printf("%*sctrl-%c %s : %s", indent, " ", option.prefix_key, keymaps[idx].key, keymaps[idx].func); + } +} + +void option_parse_key_mappings(const char *keymap) +{ + char key_str[KEYMAP_KEY_STR_MAX + 1]; + char func_str[KEYMAP_FUNC_STR_MAX + 1]; + int key_len, func_len; + char *buffer; + char *cp; + + if (keymap == NULL) + { + return; + } + + /* Parse specified key mappings */ + buffer = strdup(keymap); + cp = strchr(buffer, '@'); + if (cp == NULL) + { + tio_error_print("Can't find keymap top character '@'"); + goto parse_end; + } + + while (sscanf(cp, "@%" TOSTR(KEYMAP_KEY_STR_MAX) "[^=]=%" TOSTR(KEYMAP_FUNC_STR_MAX) "[^@]", key_str, func_str) == 2) + { + key_len = strlen(key_str); + func_len = strlen(func_str); + keymap_set(key_str, key_len, func_str, func_len); + cp = strchr(cp + key_len + func_len + 2, '@'); + if (cp == NULL) + { + break; + } + } + parse_end: + free(buffer); +} diff --git a/src/options.h b/src/options.h index 6589c16..1ab12ea 100644 --- a/src/options.h +++ b/src/options.h @@ -105,6 +105,7 @@ struct option_t int hex_n_value; bool vt100; char *exec; + char *keymap; bool map_i_nl_cr; bool map_i_cr_nl; bool map_ign_cr; @@ -120,7 +121,18 @@ struct option_t bool map_o_ign_cr; }; +#define KEYMAP_MAX 32 +#define KEYMAP_KEY_STR_MAX 7 +#define KEYMAP_FUNC_STR_MAX 127 + +struct keymap_t +{ + char key[KEYMAP_KEY_STR_MAX + 1]; + char func[KEYMAP_FUNC_STR_MAX + 1]; +}; + extern struct option_t option; +extern struct keymap_t keymaps[KEYMAP_MAX]; void options_print(); void options_parse(int argc, char *argv[]); @@ -147,5 +159,8 @@ void option_parse_timestamp(const char *arg, timestamp_t *timestamp); const char* option_timestamp_format_to_string(timestamp_t timestamp); void option_parse_mappings(const char *map); +void option_parse_key_mappings(const char *keymap); const char* option_raw_to_string(raw_t raw); + +void keymaps_print(const char *title, int indent); diff --git a/src/tty.c b/src/tty.c index 4c3d40c..4d5e136 100644 --- a/src/tty.c +++ b/src/tty.c @@ -119,6 +119,7 @@ #define KEY_I 0x69 #define KEY_J 0x6A #define KEY_SHIFT_J 0x4A +#define KEY_K 0x6B #define KEY_L 0x6C #define KEY_SHIFT_L 0x4C #define KEY_M 0x6D @@ -1099,6 +1100,26 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) return; } + // Handle user keymapped commands + for (int idx = 0; idx < KEYMAP_MAX; idx++) + { + if ((input_char >= 0x00) && (input_char <= 0x1f)) + { + int ctrl_key_ch = ctrl_key_char(input_char); + if ((ctrl_key_ch >= 0) && + (strncmp("ctrl-", keymaps[idx].key, 5) == 0) && (keymaps[idx].key[5] == ctrl_key_ch)) + { + script_run(keymaps[idx].func); + goto handle_commands_end; + } + } + else if (input_char == keymaps[idx].key[0] && keymaps[idx].key[1] == '\0') + { + script_run(keymaps[idx].func); + goto handle_commands_end; + } + } + // Handle commands switch (input_char) { @@ -1128,6 +1149,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) tio_printf(" ctrl-%c x Send/Receive file via Xmodem", option.prefix_key); tio_printf(" ctrl-%c y Send file via Ymodem", option.prefix_key); tio_printf(" ctrl-%c ctrl-%c Send ctrl-%c character", option.prefix_key, option.prefix_key, option.prefix_key); + keymaps_print("User key commands:", 1); break; case KEY_SHIFT_L: @@ -1203,6 +1225,7 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) rs485_print_config(); } mappings_print(); + keymaps_print(" Keymaps:", 4); break; case KEY_E: @@ -1290,6 +1313,12 @@ void handle_command_sequence(char input_char, char *output_char, bool *forward) } break; + case KEY_K: + /* Set keymap */ + tio_subcmd_readln("Enter keymap @=|!