diff options
author | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2025-07-30 23:58:32 +0300 |
---|---|---|
committer | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2025-07-31 00:33:43 +0300 |
commit | 2ec568579823c991b919fa3a2c8583a8db21cb81 (patch) | |
tree | 5ee30e3cde1df8b284aec73380c34b79afdbc8ab | |
parent | 86fbf212de71a83e7bb4d83a4b33e54bed52dff9 (diff) | |
download | sciteco-2ec568579823c991b919fa3a2c8583a8db21cb81.tar.gz |
implemented ^T command: allows typing by code and getting characters from stdin or the user
* n:^T always prints bytes (cf. :^A)
* ^T without arguments returns a codepoint or byte from stdin.
In interactive mode, this currentply places a cursor in the message line and waits for a keypress.
-rw-r--r-- | src/core-commands.c | 4 | ||||
-rw-r--r-- | src/error.h | 2 | ||||
-rw-r--r-- | src/interface-curses/interface.c | 54 | ||||
-rw-r--r-- | src/interface-gtk/gtk-label.c | 24 | ||||
-rw-r--r-- | src/interface-gtk/gtk-label.h | 2 | ||||
-rw-r--r-- | src/interface-gtk/interface.c | 98 | ||||
-rw-r--r-- | src/interface.c | 29 | ||||
-rw-r--r-- | src/interface.h | 6 | ||||
-rw-r--r-- | src/stdio-commands.c | 82 | ||||
-rw-r--r-- | src/stdio-commands.h | 8 | ||||
-rw-r--r-- | tests/testsuite.at | 7 |
11 files changed, 308 insertions, 8 deletions
diff --git a/src/core-commands.c b/src/core-commands.c index 015865d..213d9ed 100644 --- a/src/core-commands.c +++ b/src/core-commands.c @@ -1628,7 +1628,9 @@ teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error) .modifier_colon = 1}, ['X'] = {&teco_state_start, teco_state_control_search_mode}, ['Y'] = {&teco_state_start, teco_state_control_last_range}, - ['S'] = {&teco_state_start, teco_state_control_last_length} + ['S'] = {&teco_state_start, teco_state_control_last_length}, + ['T'] = {&teco_state_start, teco_state_control_typeout, + .modifier_colon = 1} }; /* diff --git a/src/error.h b/src/error.h index 04bb988..b4f92ad 100644 --- a/src/error.h +++ b/src/error.h @@ -95,7 +95,7 @@ static inline void teco_error_codepoint_set(GError **error, const gchar *cmd) { g_set_error(error, TECO_ERROR, TECO_ERROR_CODEPOINT, - "Invalid Unicode codepoint for <%s>", cmd); + "Invalid codepoint for <%s>", cmd); } static inline void diff --git a/src/interface-curses/interface.c b/src/interface-curses/interface.c index e461b3c..d92eade 100644 --- a/src/interface-curses/interface.c +++ b/src/interface-curses/interface.c @@ -134,6 +134,9 @@ teco_console_ctrl_handler(DWORD type) static gint teco_xterm_version(void) G_GNUC_UNUSED; +static void teco_interface_refresh(void); +static gint teco_interface_blocking_getch(void); + #define UNNAMED_FILE "(Unnamed)" /** @@ -915,6 +918,50 @@ teco_interface_msg_clear(void) teco_curses_clrtobot(teco_interface.msg_window); } +teco_int_t +teco_interface_getch(gboolean widechar) +{ + if (!teco_interface.cmdline_window) /* batch mode */ + return teco_interface_stdio_getch(widechar); + + teco_interface_refresh(); + + /* + * Signal that we accept input by drawing a real cursor in the message bar. + */ + wmove(teco_interface.msg_window, 0, 0); + curs_set(1); + wrefresh(teco_interface.msg_window); + + gchar buf[4]; + gint i = 0; + gint32 cp; + + do { + cp = teco_interface_blocking_getch(); + if (cp == TECO_CTL_KEY('C')) + teco_interrupted = TRUE; + if (cp == TECO_CTL_KEY('C') || cp == TECO_CTL_KEY('D')) { + cp = -1; + break; + } + if (cp < 0 || cp > 0xFF) + continue; + + if (!widechar || !cp) + break; + + /* doesn't work as expected when passed a null byte */ + buf[i] = cp; + cp = g_utf8_get_char_validated(buf, ++i); + if (i >= sizeof(buf) || cp != -2) + i = 0; + } while (cp < 0); + + curs_set(0); + return cp; +} + void teco_interface_show_view(teco_view_t *view) { @@ -1933,6 +1980,9 @@ teco_interface_getmouse(GError **error) static gint teco_interface_blocking_getch(void) { + if (!g_queue_is_empty(teco_interface.input_queue)) + return GPOINTER_TO_INT(g_queue_pop_head(teco_interface.input_queue)); + #if NCURSES_MOUSE_VERSION >= 2 #ifdef __PDCURSES__ /* @@ -1991,9 +2041,7 @@ teco_interface_event_loop_iter(void) GError **error = &teco_interface.event_loop_error; - gint key = g_queue_is_empty(teco_interface.input_queue) - ? teco_interface_blocking_getch() - : GPOINTER_TO_INT(g_queue_pop_head(teco_interface.input_queue)); + gint key = teco_interface_blocking_getch(); const teco_view_t *last_view = teco_interface_current_view; sptr_t last_vpos = teco_interface_ssm(SCI_GETFIRSTVISIBLELINE, 0, 0); diff --git a/src/interface-gtk/gtk-label.c b/src/interface-gtk/gtk-label.c index ef370a2..9fc9e76 100644 --- a/src/interface-gtk/gtk-label.c +++ b/src/interface-gtk/gtk-label.c @@ -275,3 +275,27 @@ teco_gtk_label_get_text(TecoGtkLabel *self) { return &self->string; } + +/** + * Signal that a keypress is expected (after executing ^T) + * by printing the first character in reverse. + * + * @fixme This mimics the current Curses implementation. + * Perhaps better show an icon? + */ +void +teco_gtk_label_highlight_getch(TecoGtkLabel *self) +{ + const gchar *plaintext = gtk_label_get_text(GTK_LABEL(self)); + g_assert(plaintext != NULL); + if (!*plaintext || !strcmp(plaintext, "\u258C")) { + gtk_label_set_text(GTK_LABEL(self), "\u258C"); + } else { + PangoAttrList *attribs = gtk_label_get_attributes(GTK_LABEL(self)); + teco_gtk_label_add_highlight_attribs(attribs, + &self->fg, self->fg_alpha, + &self->bg, self->bg_alpha, + 0, 1); + gtk_label_set_attributes(GTK_LABEL(self), attribs); + } +} diff --git a/src/interface-gtk/gtk-label.h b/src/interface-gtk/gtk-label.h index c52d073..3cd4cb9 100644 --- a/src/interface-gtk/gtk-label.h +++ b/src/interface-gtk/gtk-label.h @@ -33,3 +33,5 @@ void teco_gtk_label_parse_string(const gchar *str, gssize len, PangoColor *fg, guint16 fg_alpha, PangoColor *bg, guint16 bg_alpha, PangoAttrList **attribs, gchar **text); + +void teco_gtk_label_highlight_getch(TecoGtkLabel *self); diff --git a/src/interface-gtk/interface.c b/src/interface-gtk/interface.c index 544ff22..ae1dd74 100644 --- a/src/interface-gtk/interface.c +++ b/src/interface-gtk/interface.c @@ -75,6 +75,7 @@ static void teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong static gboolean teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer user_data); static gboolean teco_interface_sigterm_handler(gpointer user_data) G_GNUC_UNUSED; +static gchar teco_interface_get_ansi_key(GdkEventKey *event); /** * Interval between polling for keypresses. @@ -423,6 +424,103 @@ teco_interface_msg_clear(void) teco_gtk_label_set_text(TECO_GTK_LABEL(teco_interface.message_widget), "", 0); } +static void +teco_interface_getch_commit_cb(GtkIMContext *context, gchar *str, gpointer user_data) +{ + teco_int_t *cp = user_data; + + /* + * FIXME: What if str contains several characters? + */ + *cp = g_utf8_get_char_validated(str, -1); + g_assert(*cp >= 0); + gtk_main_quit(); +} + +/* + * FIXME: Redundancies with teco_interface_handle_keypress() + * FIXME: Report function keys + */ +static gboolean +teco_interface_getch_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data) +{ + teco_int_t *cp = user_data; + + g_assert(event->type == GDK_KEY_PRESS); + + switch (event->key.keyval) { + case GDK_KEY_Escape: *cp = '\e'; break; + case GDK_KEY_BackSpace: *cp = TECO_CTL_KEY('H'); break; + case GDK_KEY_Tab: *cp = '\t'; break; + case GDK_KEY_Return: *cp = '\n'; break; + default: + /* + * NOTE: Alt-Gr key-combinations are sometimes reported as + * Ctrl+Alt, so we filter those out. + */ + if ((event->key.state & (GDK_CONTROL_MASK | GDK_MOD1_MASK)) == GDK_CONTROL_MASK && + (*cp = teco_interface_get_ansi_key(&event->key))) { + *cp = TECO_CTL_KEY(g_ascii_toupper(*cp)); + switch (*cp) { + case TECO_CTL_KEY('C'): + teco_interrupted = TRUE; + /* fall through */ + case TECO_CTL_KEY('D'): + *cp = -1; + } + break; + } + + gtk_im_context_filter_keypress(teco_interface.input_method, &event->key); + return TRUE; + } + + gtk_main_quit(); + return TRUE; +} + +teco_int_t +teco_interface_getch(gboolean widechar) +{ + if (!gtk_main_level()) + /* batch mode */ + return teco_interface_stdio_getch(widechar); + + teco_int_t cp = -1; + gulong key_handler, commit_handler; + + /* temporarily replace the "key-press-event" and "commit" handlers */ + g_signal_handlers_block_by_func(teco_interface.window, + teco_interface_input_cb, NULL); + key_handler = g_signal_connect(teco_interface.window, "key-press-event", + G_CALLBACK(teco_interface_getch_input_cb), &cp); + g_signal_handlers_block_by_func(teco_interface.input_method, + teco_interface_cmdline_commit_cb, NULL); + commit_handler = g_signal_connect(teco_interface.input_method, "commit", + G_CALLBACK(teco_interface_getch_commit_cb), &cp); + + /* + * Highlights the first character in the label. + * This mimics what the Curses UI does. + * Is there a better way to signal that we expect input? + */ + teco_gtk_label_highlight_getch(TECO_GTK_LABEL(teco_interface.message_widget)); + + GdkWindow *top_window = gdk_window_get_toplevel(gtk_widget_get_window(teco_interface.window)); + gdk_window_thaw_updates(top_window); + + gtk_main(); + + gdk_window_freeze_updates(top_window); + + g_signal_handler_disconnect(teco_interface.input_method, commit_handler); + g_signal_handlers_unblock_by_func(teco_interface.input_method, teco_interface_cmdline_commit_cb, NULL); + g_signal_handler_disconnect(teco_interface.window, key_handler); + g_signal_handlers_unblock_by_func(teco_interface.window, teco_interface_input_cb, NULL); + + return cp; +} + void teco_interface_show_view(teco_view_t *view) { diff --git a/src/interface.c b/src/interface.c index cf8f1ca..c3103fd 100644 --- a/src/interface.c +++ b/src/interface.c @@ -130,3 +130,32 @@ teco_interface_stdio_msg(teco_msg_t type, const gchar *str, gsize len) break; } } + +/** + * Get character from stdin. + * + * @param widechar If TRUE reads one glyph encoded in UTF-8. + * If FALSE, returns exactly one byte. + * @return Codepoint or -1 in case of EOF. + */ +teco_int_t +teco_interface_stdio_getch(gboolean widechar) +{ + gchar buf[4]; + gint i = 0; + gint32 cp; + + do { + if (G_UNLIKELY(fread(buf+i, 1, 1, stdin) < 1)) + return -1; /* EOF */ + if (!widechar || !buf[i]) + return (guchar)buf[i]; + + /* doesn't work as expected when passed a null byte */ + cp = g_utf8_get_char_validated(buf, ++i); + if (i >= sizeof(buf) || cp != -2) + i = 0; + } while (cp < 0); + + return cp; +} diff --git a/src/interface.h b/src/interface.h index 0e03a98..f22c023 100644 --- a/src/interface.h +++ b/src/interface.h @@ -66,6 +66,9 @@ void teco_interface_msg_literal(teco_msg_t type, const gchar *str, gsize len); void teco_interface_msg(teco_msg_t type, const gchar *fmt, ...) G_GNUC_PRINTF(2, 3); /** @pure */ +teco_int_t teco_interface_getch(gboolean widechar); + +/** @pure */ void teco_interface_msg_clear(void); /** @pure */ @@ -173,6 +176,9 @@ gboolean teco_interface_event_loop(GError **error); /** @protected */ void teco_interface_stdio_msg(teco_msg_t type, const gchar *str, gsize len); +/** @protected */ +teco_int_t teco_interface_stdio_getch(gboolean widechar); + /** @pure */ void teco_interface_cleanup(void); diff --git a/src/stdio-commands.c b/src/stdio-commands.c index 2d5da62..3a1b320 100644 --- a/src/stdio-commands.c +++ b/src/stdio-commands.c @@ -292,7 +292,7 @@ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_print_string, * * Type out the next or previous number of <lines> from the buffer * as a message, i.e. in the message line in interactive mode - * and if possible on the terminal (stdout) as well.. + * and if possible on the terminal (stdout) as well. * If <lines> is omitted, the sign prefix is implied. * If two arguments are specified, the characters beginning * at position <from> up to the character at position <to> @@ -320,3 +320,83 @@ teco_state_start_typeout(teco_machine_main_t *ctx, GError **error) const gchar *str = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER, from, len); teco_interface_msg_literal(TECO_MSG_USER, str, len); } + +/*$ "^T" ":^T" "typeout glyph" "get char" + * <c1,c2,...>^T -- Type out the numeric arguments as a message or get character from user + * <c1,c2,...>:^T + * ^T -> codepoint + * :^T -> byte + * + * Types out characters for all the values + * on the argument stack (interpreted as codepoints) as messages, + * i.e. in the message line in interactive mode + * and if possible on the terminal (stdout) as well. + * It does so in the order of the arguments, i.e. + * <c1> is inserted before <c2>, ecetera. + * By default the codepoints are expected to be in the default + * codepage, but you can force raw ANSI encoding (for arbitrary + * bytes) by colon-modifying the command. + * + * When called without any argument, \fB^T\fP reads a key from the + * user (or from stdin) and returns the corresponding codepoint. + * If the default encoding is UTF-8, this will not work + * for function keys. + * If the default encoding is raw ANSI or if the command is + * colon-modified, \fB^T\fP returns raw bytes. + * When run in batch mode, this will return whatever byte is + * delivered by the attached terminal. + * In case stdin is closed, -1 is returned. + * In interactive mode, pressing CTRL+D or CTRL+C will also + * return -1. + */ +void +teco_state_control_typeout(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return; + + gboolean utf8 = !teco_machine_main_eval_colon(ctx) && + teco_default_codepage() == SC_CP_UTF8; + + guint args = teco_expressions_args(); + if (!args) { + teco_expressions_push(teco_interface_getch(utf8)); + return; + } + + if (!utf8) { + /* assume raw ANSI byte output */ + g_autofree gchar *buf = g_malloc(args); + gchar *p = buf+args; + + for (gint i = 0; i < args; i++) { + teco_int_t chr = teco_expressions_pop_num(0); + if (chr < 0 || chr > 0xFF) { + teco_error_codepoint_set(error, "^T"); + return; + } + *--p = chr; + } + + teco_interface_msg_literal(TECO_MSG_USER, p, args); + return; + } + + /* 4 bytes should be enough for UTF-8, but we better follow the documentation */ + g_autofree gchar *buf = g_malloc(args*6); + gchar *p = buf; + + for (gint i = args; i > 0; i--) { + teco_int_t chr = teco_expressions_peek_num(i-1); + if (chr < 0 || !g_unichar_validate(chr)) { + teco_error_codepoint_set(error, "^T"); + return; + } + p += g_unichar_to_utf8(chr, p); + } + /* we pop only now since we had to peek in reverse order */ + for (gint i = 0; i < args; i++) + teco_expressions_pop_num(0); + + teco_interface_msg_literal(TECO_MSG_USER, buf, p-buf); +} diff --git a/src/stdio-commands.h b/src/stdio-commands.h index b6870e7..985d1ac 100644 --- a/src/stdio-commands.h +++ b/src/stdio-commands.h @@ -20,7 +20,11 @@ #include "parser.h" +void teco_state_start_typeout(teco_machine_main_t *ctx, GError **error); +void teco_state_control_typeout(teco_machine_main_t *ctx, GError **error); + +/* + * Command states + */ TECO_DECLARE_STATE(teco_state_print_decimal); TECO_DECLARE_STATE(teco_state_print_string); - -void teco_state_start_typeout(teco_machine_main_t *ctx, GError **error); diff --git a/tests/testsuite.at b/tests/testsuite.at index 9f84a23..a5390d6 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -204,6 +204,13 @@ TE_CHECK([[@EB'test.txt' 2T]], 0, stdout, ignore) AT_FAIL_IF([test `$GREP -v "^Info:" stdout | wc -l` -ne 2], 0, ignore, ignore) AT_CLEANUP +AT_SETUP([Type out and get char]) +TE_CHECK([[1058,1045,1057,1058^T]], 0, stdout, ignore) +AT_FAIL_IF([test "`$GREP -v "^Info:" stdout`" != "ТЕСТ"], 0, ignore, ignore) +AT_CHECK([[echo -n "ТЕСТ" | $SCITECO -e '<^TUa Qa:; Qa=>']], 0, stdout, ignore) +AT_FAIL_IF([test `$GREP -v "^Info:" stdout | wc -l` -ne 4], 0, ignore, ignore) +AT_CLEANUP + AT_SETUP([Convert between line and glyph positions]) TE_CHECK([[@I/1^J2^J3/J 2^QC :^Q-3"N(0/0)']], 0, ignore, ignore) AT_CLEANUP |