diff options
Diffstat (limited to 'src')
67 files changed, 1537 insertions, 444 deletions
diff --git a/src/cmdline.c b/src/cmdline.c index 816816c..dde096d 100644 --- a/src/cmdline.c +++ b/src/cmdline.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -614,7 +614,7 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t * */ if (teco_interface_popup_is_shown()) { /* cycle through popup pages */ - teco_interface_popup_show(); + teco_interface_popup_scroll(); return TRUE; } @@ -645,6 +645,16 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t * } gboolean +teco_state_stringbuilding_insert_completion(teco_machine_stringbuilding_t *ctx, const teco_string_t *str, GError **error) +{ + g_auto(teco_string_t) str_escaped; + teco_machine_stringbuilding_escape(ctx, str->data, str->len, &str_escaped); + if (!str->len || !G_IS_DIR_SEPARATOR(str->data[str->len-1])) + teco_string_append_c(&str_escaped, ' '); + return teco_cmdline_insert(str_escaped.data, str_escaped.len, error); +} + +gboolean teco_state_stringbuilding_escaped_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error) { @@ -684,6 +694,14 @@ teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_ } gboolean +teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current; + return stringbuilding_current->insert_completion_cb(&stringbuilding_ctx->parent, str, error); +} + +gboolean teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; @@ -778,7 +796,7 @@ teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t if (teco_interface_popup_is_shown()) { /* cycle through popup pages */ - teco_interface_popup_show(); + teco_interface_popup_scroll(); return TRUE; } @@ -801,6 +819,20 @@ teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t } gboolean +teco_state_expectfile_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + + g_auto(teco_string_t) str_escaped; + teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped); + if ((!str->len || !G_IS_DIR_SEPARATOR(str->data[str->len-1])) && + ctx->expectstring.nesting == 1) + teco_string_append_wc(&str_escaped, + ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char); + return teco_cmdline_insert(str_escaped.data, str_escaped.len, error); +} + +gboolean teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; @@ -820,7 +852,7 @@ teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t if (teco_interface_popup_is_shown()) { /* cycle through popup pages */ - teco_interface_popup_show(); + teco_interface_popup_scroll(); return TRUE; } @@ -855,6 +887,21 @@ teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t } gboolean +teco_state_expectglob_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + + g_autofree gchar *pattern_escaped = teco_globber_escape_pattern(str->data); + g_auto(teco_string_t) str_escaped; + teco_machine_stringbuilding_escape(stringbuilding_ctx, pattern_escaped, strlen(pattern_escaped), &str_escaped); + if ((!str->len || !G_IS_DIR_SEPARATOR(str->data[str->len-1])) && + ctx->expectstring.nesting == 1) + teco_string_append_wc(&str_escaped, + ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char); + return teco_cmdline_insert(str_escaped.data, str_escaped.len, error); +} + +gboolean teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; @@ -874,7 +921,7 @@ teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t * if (teco_interface_popup_is_shown()) { /* cycle through popup pages */ - teco_interface_popup_show(); + teco_interface_popup_scroll(); return TRUE; } @@ -898,6 +945,19 @@ teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t * } gboolean +teco_state_expectdir_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + + /* + * FIXME: We might terminate the command in case of leaf directories. + */ + g_auto(teco_string_t) str_escaped; + teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped); + return teco_cmdline_insert(str_escaped.data, str_escaped.len, error); +} + +gboolean teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error) { g_assert(ctx->expectqreg != NULL); @@ -910,6 +970,18 @@ teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t } gboolean +teco_state_expectqreg_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + g_assert(ctx->expectqreg != NULL); + /* + * NOTE: teco_machine_qregspec_t is private, so we downcast to teco_machine_t. + * Otherwise, we'd have to move this callback into qreg.c. + */ + teco_state_t *expectqreg_current = ((teco_machine_t *)ctx->expectqreg)->current; + return expectqreg_current->insert_completion_cb((teco_machine_t *)ctx->expectqreg, str, error); +} + +gboolean teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error) { switch (key) { @@ -919,7 +991,7 @@ teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_ if (teco_interface_popup_is_shown()) { /* cycle through popup pages */ - teco_interface_popup_show(); + teco_interface_popup_scroll(); return TRUE; } @@ -945,6 +1017,12 @@ teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_ } gboolean +teco_state_qregspec_insert_completion(teco_machine_qregspec_t *ctx, const teco_string_t *str, GError **error) +{ + return teco_cmdline_insert(str->data, str->len, error); +} + +gboolean teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = teco_machine_qregspec_get_stringbuilding(ctx); @@ -967,7 +1045,7 @@ teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_m if (teco_interface_popup_is_shown()) { /* cycle through popup pages */ - teco_interface_popup_show(); + teco_interface_popup_scroll(); return TRUE; } @@ -985,6 +1063,17 @@ teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_m } gboolean +teco_state_qregspec_string_insert_completion(teco_machine_qregspec_t *ctx, const teco_string_t *str, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = teco_machine_qregspec_get_stringbuilding(ctx); + + g_auto(teco_string_t) str_escaped; + teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped); + teco_string_append_c(&str_escaped, ']'); + return teco_cmdline_insert(str_escaped.data, str_escaped.len, error); +} + +gboolean teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; @@ -1003,14 +1092,14 @@ teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa break; /* - * In the EC command, <TAB> completes files just like ^T + * In the EC command, <TAB> completes files just like ^G<TAB>. * * TODO: Implement shell-command completion by iterating * executables in $PATH */ if (teco_interface_popup_is_shown()) { /* cycle through popup pages */ - teco_interface_popup_show(); + teco_interface_popup_scroll(); return TRUE; } @@ -1049,7 +1138,7 @@ teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_mac if (teco_interface_popup_is_shown()) { /* cycle through popup pages */ - teco_interface_popup_show(); + teco_interface_popup_scroll(); return TRUE; } @@ -1075,6 +1164,17 @@ teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_mac } gboolean +teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + + g_auto(teco_string_t) str_escaped; + teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped); + teco_string_append_c(&str_escaped, ','); + return teco_cmdline_insert(str_escaped.data, str_escaped.len, error); +} + +gboolean teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; @@ -1094,7 +1194,7 @@ teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *paren if (teco_interface_popup_is_shown()) { /* cycle through popup pages */ - teco_interface_popup_show(); + teco_interface_popup_scroll(); return TRUE; } @@ -1122,6 +1222,20 @@ teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *paren } gboolean +teco_state_goto_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + + g_auto(teco_string_t) str_escaped; + teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped); + /* + * FIXME: This does not escape `,`. Cannot be escaped via ^Q currently? + */ + teco_string_append_c(&str_escaped, ','); + return teco_cmdline_insert(str_escaped.data, str_escaped.len, error); +} + +gboolean teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; @@ -1141,7 +1255,7 @@ teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *paren if (teco_interface_popup_is_shown()) { /* cycle through popup pages */ - teco_interface_popup_show(); + teco_interface_popup_scroll(); return TRUE; } @@ -1163,6 +1277,19 @@ teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *paren return stringbuilding_current->process_edit_cmd_cb(&stringbuilding_ctx->parent, &ctx->parent, key, error); } +gboolean +teco_state_help_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; + + g_auto(teco_string_t) str_escaped; + teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped); + if (ctx->expectstring.nesting == 1) + teco_string_append_wc(&str_escaped, + ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char); + return teco_cmdline_insert(str_escaped.data, str_escaped.len, error); +} + /* * Command states */ diff --git a/src/cmdline.h b/src/cmdline.h index f4b84e4..ebdf1e1 100644 --- a/src/cmdline.h +++ b/src/cmdline.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/core-commands.c b/src/core-commands.c index 4ee0c5c..0d23adb 100644 --- a/src/core-commands.c +++ b/src/core-commands.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -2202,51 +2202,6 @@ TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_escape, .style = SCE_SCITECO_COMMAND ); -/*$ EF close - * [bool]EF -- Remove buffer from ring - * -EF - * - * Removes buffer from buffer ring, effectively - * closing it. - * If the buffer is dirty (modified), EF will yield - * an error. - * <bool> may be a specified to enforce closing dirty - * buffers. - * If it is a Failure condition boolean (negative), - * the buffer will be closed unconditionally. - * If <bool> is absent, the sign prefix (1 or -1) will - * be implied, so \(lq-EF\(rq will always close the buffer. - * - * It is noteworthy that EF will be executed immediately in - * interactive mode but can be rubbed out at a later time - * to reopen the file. - * Closed files are kept in memory until the command line - * is terminated. - */ -static void -teco_state_ecommand_close(teco_machine_main_t *ctx, GError **error) -{ - if (teco_qreg_current) { - const teco_string_t *name = &teco_qreg_current->head.name; - g_autofree gchar *name_printable = teco_string_echo(name->data, name->len); - g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, - "Q-Register \"%s\" currently edited", name_printable); - return; - } - - teco_int_t v; - if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) - return; - if (teco_is_failure(v) && teco_ring_current->dirty) { - g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, - "Buffer \"%s\" is dirty", - teco_ring_current->filename ? : "(Unnamed)"); - return; - } - - teco_ring_close(error); -} - /*$ ED flags * flags ED -- Set and get ED-flags * [off,]on ED @@ -2266,37 +2221,50 @@ teco_state_ecommand_close(teco_machine_main_t *ctx, GError **error) * Without any argument ED returns the current flags. * * Currently, the following flags are used by \*(ST: - * - 4: If enabled, prefer raw single-byte ANSI encoding - * for all new buffers and registers. - * This does not change the encoding of any existing - * buffers and any initialized default register when set via - * \fBED\fP, so you might want to launch \*(ST with \fB--8bit\fP. - * - 8: Enable/disable automatic folding of case-insensitive - * command characters during interactive key translation. - * The case of letter keys is inverted, so one or two - * character commands will typically be inserted upper-case, - * but you can still press Shift to insert lower-case letters. - * Case-insensitive Q-Register specifications are not - * case folded. - * This is thought to improve the readability of the command - * line macro. - * - 16: Enable/disable automatic translation of end of - * line sequences to and from line feed. - * Disabling this flag allows 8-bit clean loading and saving - * of files. - * - 32: Enable/Disable buffer editing hooks - * (via execution of macro in global Q-Register \(lqED\(rq) - * - 128: Enable/Disable enforcement of UNIX98 - * \(lq/bin/sh\(rq emulation for operating system command - * executions - * - 256: Enable/Disable OSC-52 clipboard support. - * Must only be enabled if the terminal emulator is configured - * properly. - * - 512: Enable/Disable Unicode icons in the Curses UI. - * This requires a capable font, like the ones provided - * by the \(lqNerd Fonts\(rq project. - * Changes to this flag in interactive mode may not become - * effective immediately. + * .IP 4: 5 + * If enabled, prefer raw single-byte ANSI encoding + * for all new buffers and registers. + * This does not change the encoding of any existing + * buffers and any initialized default register when set via + * \fBED\fP, so you might want to launch \*(ST with \fB--8bit\fP. + * .IP 8: + * Enable/disable automatic folding of case-insensitive + * command characters during interactive key translation. + * The case of letter keys is inverted, so one or two + * character commands will typically be inserted upper-case, + * but you can still press Shift to insert lower-case letters. + * Case-insensitive Q-Register specifications are not + * case folded. + * This is thought to improve the readability of the command + * line macro. + * .IP 16: + * Enable/disable automatic translation of end of + * line sequences to and from line feed. + * Disabling this flag allows 8-bit clean loading and saving + * of files. + * .IP 32: + * Enable/Disable buffer editing hooks + * (via execution of macro in global Q-Register \(lqED\(rq) + * .IP 64: + * .SCITECO_TOPIC mouse + * Enable/Disable processing and delivery of mouse events in + * the Curses UI. + * If enabled, the terminal emulator's default mouse behavior + * may be inhibited. + * .IP 128: + * Enable/Disable enforcement of UNIX98 + * \(lq/bin/sh\(rq emulation for operating system command + * executions + * .IP 256: + * Enable/Disable OSC-52 clipboard support. + * Must only be enabled if the terminal emulator is configured + * properly. + * .IP 512: + * Enable/Disable Unicode icons in the Curses UI. + * This requires a capable font, like the ones provided + * by the \(lqNerd Fonts\(rq project. + * Changes to this flag in interactive mode may not become + * effective immediately. * * The features controlled thus are discribed in other sections * of this manual. @@ -2322,9 +2290,10 @@ teco_state_ecommand_flags(teco_machine_main_t *ctx, GError **error) /*$ EJ properties * [key]EJ -> value -- Get and set system properties - * -EJ -> value * value,keyEJ * rgb,color,3EJ + * -EJ -> event + * -2EJ -> y, x * * This command may be used to get and set system * properties. @@ -2338,16 +2307,16 @@ teco_state_ecommand_flags(teco_machine_main_t *ctx, GError **error) * write-only or read-only. * * The following property keys are defined: - * .IP 0 4 + * .IP 0: 4 * The current user interface: 1 for Curses, 2 for GTK * (\fBread-only\fP) - * .IP 1 + * .IP 1: * The current numbfer of buffers: Also the numeric id * of the last buffer in the ring. This is implied if * no argument is given, so \(lqEJ\(rq returns the number * of buffers in the ring. * (\fBread-only\fP) - * .IP 2 + * .IP 2: * The current memory limit in bytes. * This limit helps to prevent dangerous out-of-memory * conditions (e.g. resulting from infinite loops) by @@ -2373,7 +2342,7 @@ teco_state_ecommand_flags(teco_machine_main_t *ctx, GError **error) * this happens you may have to clear your command-line * first. * Memory limiting is enabled by default. - * .IP 3 + * .IP 3: * This \fBwrite-only\fP property allows redefining the * first 16 entries of the terminal color palette \(em a * feature required by some @@ -2414,17 +2383,60 @@ teco_state_ecommand_flags(teco_machine_main_t *ctx, GError **error) * on exit the author is aware of is \fBxterm\fP(1) and * the Linux console driver. * You have been warned. Good luck. - * .IP 4 + * .IP 4: * The column after the last horizontal movement. * This is only used by \fBfnkeys.tes\fP and is similar to the Scintilla-internal * setting \fBSCI_CHOOSECARETX\fP. * Unless most other settings, this is on purpose not restored on rubout, * so it "survives" command line replacements. + * . + * .IP -1: + * Type of the last mouse event (\fBread-only\fP). + * One of the following values will be returned: + * .RS + * . IP 1: 4 + * Some button has been pressed + * . IP 2: + * Some button has been released + * . IP 3: + * Scroll up + * . IP 4: + * Scroll down + * .RE + * .IP -2: + * Coordinates of the mouse pointer relative to the Scintilla view + * at the time of the last mouse event. + * This is in pixels or cells depending on the UI. + * First the Y coordinate is pushed, followed by the X coordinate, + * allowing you to pass them on directly to the \fBSCI_POSITIONFROMPOINT\fP + * and similar Scintilla messages using the \fBES\fP command. + * (\fBread-only\fP) + * .IP -3: + * Number of the mouse button involved in the last mouse event, beginning with 1. + * Can be -1 if the button cannot be determined or is irrelevant. + * (\fBread-only\fP) + * .IP -4: + * Bit mask describing the key modifiers at the time of the last + * mouse event (\fBread-only\fP). + * Currently, the following flags are used: + * .RS + * . IP 1: 4 + * Shift key + * . IP 2: + * Control key (CTRL) + * . IP 4: + * Alt key + * .RE */ static void teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error) { enum { + EJ_MOUSE_MODS = -4, + EJ_MOUSE_BUTTON, + EJ_MOUSE_COORD, + EJ_MOUSE_TYPE, + EJ_USER_INTERFACE = 0, EJ_BUFFERS, EJ_MEMORY_LIMIT, @@ -2487,6 +2499,21 @@ teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error) * Get property */ switch (property) { + case EJ_MOUSE_TYPE: + teco_expressions_push(teco_mouse.type); + break; + case EJ_MOUSE_COORD: + /* can be passed down to @ES/POSITIONFROMPOINT// */ + teco_expressions_push(teco_mouse.y); + teco_expressions_push(teco_mouse.x); + break; + case EJ_MOUSE_BUTTON: + teco_expressions_push(teco_mouse.button); + break; + case EJ_MOUSE_MODS: + teco_expressions_push(teco_mouse.mods); + break; + case EJ_USER_INTERFACE: /* * FIXME: Replace INTERFACE_* macros with @@ -2767,7 +2794,7 @@ teco_state_ecommand_encoding(teco_machine_main_t *ctx, GError **error) } } - teco_int_t dot_glyphs; + teco_int_t dot_glyphs = 0; if (colon_modified) { sptr_t dot_bytes = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); dot_glyphs = teco_interface_bytes2glyphs(dot_bytes); diff --git a/src/core-commands.h b/src/core-commands.h index fbb67fa..8ce7be7 100644 --- a/src/core-commands.h +++ b/src/core-commands.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/error.c b/src/error.c index afa2ac1..6326984 100644 --- a/src/error.c +++ b/src/error.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/error.h b/src/error.h index 021f759..b672024 100644 --- a/src/error.h +++ b/src/error.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/expressions.c b/src/expressions.c index 63d3b2f..c48e7b0 100644 --- a/src/expressions.c +++ b/src/expressions.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/expressions.h b/src/expressions.h index caea1d7..631c867 100644 --- a/src/expressions.h +++ b/src/expressions.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/file-utils.c b/src/file-utils.c index 7839a38..75bcb48 100644 --- a/src/file-utils.c +++ b/src/file-utils.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -241,6 +241,60 @@ teco_file_get_program_path(void) #endif +#ifdef G_OS_WIN32 + +/* + * Definitions from the DDK's ntifs.h. + */ +#define FileCaseSensitiveInformation 71 + +static gboolean +teco_file_is_case_sensitive(const gchar *path) +{ + g_autofree gunichar2 *path_utf16 = g_utf8_to_utf16(path, -1, NULL, NULL, NULL); + HANDLE hnd = CreateFileW(path_utf16, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, NULL, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (hnd == INVALID_HANDLE_VALUE) + return FALSE; + + /* + * NOTE: This requires Windows 10, version 1803 or later. + * FIXME: But even then, this is relying on undocumented behavior! + * If unavailable we just assume the platform-default case-insensitivity. + */ + FILE_CASE_SENSITIVE_INFORMATION info = {0}; + GetFileInformationByHandleEx(hnd, FileCaseSensitiveInformation, &info, sizeof(info)); + CloseHandle(hnd); + return info.Flags & FILE_CS_FLAG_CASE_SENSITIVE_DIR; +} + +#elif defined(G_OS_UNIX) && defined(_PC_CASE_SENSITIVE) + +/* + * This is supported at least on Mac OS. + * + * NOTE: If the selector is not supported, -1 is returned and we also assume case-sensitivity. + */ +static inline gboolean +teco_file_is_case_sensitive(const gchar *path) +{ + return pathconf(path, _PC_CASE_SENSITIVE); +} + +#else /* !G_OS_WIN32 && (!G_OS_UNIX || !_PC_CASE_SENSITIVE) */ + +/* + * FIXME: The only way to query this on Linux and FreeBSD would be to + * hardcode "case-insensitive" file systems. + */ +static inline gboolean +teco_file_is_case_sensitive(const gchar *path) +{ + return TRUE; +} + +#endif + /** * Get the datadir. * @@ -334,11 +388,16 @@ teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_ gsize dirname_len = teco_file_get_dirname_len(filename_expanded); g_autofree gchar *dirname = g_strndup(filename_expanded, dirname_len); gchar *basename = filename_expanded + dirname_len; + gsize basename_len = strlen(basename); g_autoptr(GDir) dir = g_dir_open(dirname_len ? dirname : ".", 0, NULL); if (!dir) return FALSE; + /* Whether the directory has case-sensitive entries */ + gboolean case_sensitive = teco_file_is_case_sensitive(dirname_len ? dirname : "."); + teco_string_diff_t string_diff = case_sensitive ? teco_string_diff : teco_string_casediff; + /* * On Windows, both forward and backslash * directory separators are allowed in directory @@ -356,9 +415,12 @@ teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_ guint files_len = 0; gsize prefix_len = 0; - const gchar *cur_basename; - while ((cur_basename = g_dir_read_name(dir))) { - if (!g_str_has_prefix(cur_basename, basename)) + teco_string_t cur_basename; + while ((cur_basename.data = (gchar *)g_dir_read_name(dir))) { + cur_basename.len = strlen(cur_basename.data); + + if (string_diff(&cur_basename, basename, basename_len) != basename_len) + /* basename is not a prefix of cur_basename */ continue; /* @@ -366,8 +428,8 @@ teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_ * Reserving one byte at the end of the filename ensures we can easily * append the directory separator without reallocations. */ - gchar *cur_filename = g_malloc(strlen(dirname)+strlen(cur_basename)+2); - strcat(strcpy(cur_filename, dirname), cur_basename); + gchar *cur_filename = g_malloc(strlen(dirname)+cur_basename.len+2); + strcat(strcpy(cur_filename, dirname), cur_basename.data); /* * NOTE: This avoids g_file_test() for G_FILE_TEST_EXISTS @@ -391,8 +453,8 @@ teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_ other_file.data = (gchar *)g_slist_next(files)->data + filename_len; other_file.len = strlen(other_file.data); - gsize len = teco_string_diff(&other_file, cur_filename + filename_len, - strlen(cur_filename) - filename_len); + gsize len = string_diff(&other_file, cur_filename + filename_len, + strlen(cur_filename) - filename_len); if (len < prefix_len) prefix_len = len; } else { @@ -421,7 +483,7 @@ teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_ strlen((gchar *)file->data), is_buffer); } - teco_interface_popup_show(); + teco_interface_popup_show(filename_len); } /* diff --git a/src/file-utils.h b/src/file-utils.h index e974e2f..12a9b83 100644 --- a/src/file-utils.h +++ b/src/file-utils.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -48,6 +48,7 @@ GRegex *teco_globber_compile_pattern(const gchar *pattern); /* in cmdline.c */ gboolean teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); +gboolean teco_state_expectglob_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error); /** * @interface TECO_DEFINE_STATE_EXPECTGLOB @@ -58,6 +59,8 @@ gboolean teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_m TECO_DEFINE_STATE_EXPECTFILE(NAME, \ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ teco_state_expectglob_process_edit_cmd, \ + .insert_completion_cb = (teco_state_insert_completion_cb_t) \ + teco_state_expectglob_insert_completion, \ ##__VA_ARGS__ \ ) diff --git a/src/goto-commands.c b/src/goto-commands.c index 2035277..99288c1 100644 --- a/src/goto-commands.c +++ b/src/goto-commands.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -30,6 +30,7 @@ #include "lexer.h" #include "core-commands.h" #include "undo.h" +#include "interface.h" #include "goto.h" #include "goto-commands.h" @@ -62,15 +63,20 @@ teco_state_label_input(teco_machine_main_t *ctx, gunichar chr, GError **error) } if (chr == '!') { - /* - * NOTE: If the label already existed, its PC will be restored - * on rubout. - * Otherwise, the label will be removed (PC == -1). - */ gssize existing_pc = teco_goto_table_set(&ctx->goto_table, ctx->goto_label.data, ctx->goto_label.len, ctx->macro_pc); + if (existing_pc == ctx->macro_pc) + /* encountered the same label again */ + return &teco_state_start; + if (existing_pc >= 0) { + g_autofree gchar *label_printable = teco_string_echo(ctx->goto_label.data, + ctx->goto_label.len); + teco_interface_msg(TECO_MSG_WARNING, "Ignoring goto label \"%s\" redefinition", + label_printable); + return &teco_state_start; + } if (ctx->parent.must_undo) - teco_goto_table_undo_set(&ctx->goto_table, ctx->goto_label.data, ctx->goto_label.len, existing_pc); + teco_goto_table_undo_remove(&ctx->goto_table, ctx->goto_label.data, ctx->goto_label.len); if (teco_goto_skip_label.len > 0 && !teco_string_cmp(&ctx->goto_label, teco_goto_skip_label.data, teco_goto_skip_label.len)) { @@ -151,7 +157,10 @@ teco_state_goto_done(teco_machine_main_t *ctx, const teco_string_t *str, GError } /* in cmdline.c */ -gboolean teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar chr, GError **error); +gboolean teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, + gunichar chr, GError **error); +gboolean teco_state_goto_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, + GError **error); /*$ O * Olabel$ -- Go to label @@ -180,7 +189,8 @@ gboolean teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine * terminate the command-line. */ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_goto, - .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_goto_process_edit_cmd + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_goto_process_edit_cmd, + .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_goto_insert_completion ); /* diff --git a/src/goto-commands.h b/src/goto-commands.h index 03773c0..f4f52d5 100644 --- a/src/goto-commands.h +++ b/src/goto-commands.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -78,53 +78,26 @@ teco_goto_table_dump(teco_goto_table_t *ctx) } #endif -/** @memberof teco_goto_table_t */ -gssize +/** + * Remove label from goto table. + * + * @param ctx Goto table + * @param name Label name + * @param len Length of label name + * @return TRUE if the label existed and was removed + * + * @memberof teco_goto_table_t + */ +gboolean teco_goto_table_remove(teco_goto_table_t *ctx, const gchar *name, gsize len) { - gssize existing_pc = -1; - teco_goto_label_t *label = (teco_goto_label_t *)teco_rb3str_find(&ctx->tree, TRUE, name, len); - if (label) { - existing_pc = label->pc; - rb3_unlink_and_rebalance(&label->head.head); - teco_goto_label_free(label); - } + if (!label) + return FALSE; - return existing_pc; -} - -/** @memberof teco_goto_table_t */ -gssize -teco_goto_table_find(teco_goto_table_t *ctx, const gchar *name, gsize len) -{ - teco_goto_label_t *label = (teco_goto_label_t *)teco_rb3str_find(&ctx->tree, TRUE, name, len); - return label ? label->pc : -1; -} - -/** @memberof teco_goto_table_t */ -gssize -teco_goto_table_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gssize pc) -{ - if (pc < 0) - return teco_goto_table_remove(ctx, name, len); - - gssize existing_pc = -1; - - teco_goto_label_t *label = (teco_goto_label_t *)teco_rb3str_find(&ctx->tree, TRUE, name, len); - if (label) { - existing_pc = label->pc; - label->pc = pc; - } else { - label = teco_goto_label_new(name, len, pc); - teco_rb3str_insert(&ctx->tree, TRUE, &label->head); - } - -#ifdef DEBUG - teco_goto_table_dump(ctx); -#endif - - return existing_pc; + rb3_unlink_and_rebalance(&label->head.head); + teco_goto_label_free(label); + return TRUE; } /* @@ -135,35 +108,35 @@ teco_goto_table_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gssize */ typedef struct { teco_goto_table_t *table; - gssize pc; gsize len; gchar name[]; -} teco_goto_table_undo_set_t; +} teco_goto_table_undo_remove_t; static void -teco_goto_table_undo_set_action(teco_goto_table_undo_set_t *ctx, gboolean run) +teco_goto_table_undo_remove_action(teco_goto_table_undo_remove_t *ctx, gboolean run) { - if (run) { - teco_goto_table_set(ctx->table, ctx->name, ctx->len, ctx->pc); + if (!run) + return; + + G_GNUC_UNUSED gboolean removed = teco_goto_table_remove(ctx->table, ctx->name, ctx->len); + g_assert(removed == TRUE); #ifdef DEBUG - teco_goto_table_dump(ctx->table); + teco_goto_table_dump(ctx->table); #endif - } } /** @memberof teco_goto_table_t */ void -teco_goto_table_undo_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gssize pc) +teco_goto_table_undo_remove(teco_goto_table_t *ctx, const gchar *name, gsize len) { if (!ctx->must_undo) return; - teco_goto_table_undo_set_t *token; - token = teco_undo_push_size((teco_undo_action_t)teco_goto_table_undo_set_action, + teco_goto_table_undo_remove_t *token; + token = teco_undo_push_size((teco_undo_action_t)teco_goto_table_undo_remove_action, sizeof(*token) + len); if (token) { token->table = ctx; - token->pc = pc; token->len = len; if (name) memcpy(token->name, name, len); @@ -171,6 +144,44 @@ teco_goto_table_undo_set(teco_goto_table_t *ctx, const gchar *name, gsize len, g } /** @memberof teco_goto_table_t */ +gssize +teco_goto_table_find(teco_goto_table_t *ctx, const gchar *name, gsize len) +{ + teco_goto_label_t *label = (teco_goto_label_t *)teco_rb3str_find(&ctx->tree, TRUE, name, len); + return label ? label->pc : -1; +} + +/** + * Insert label into goto table. + * + * @param ctx Goto table + * @param name Label name + * @param len Length of label name + * @param pc Program counter of the new label + * @return The program counter of any label of the same name + * or -1. The label is inserted only if there is no label in the + * table already. + * + * @memberof teco_goto_table_t + */ +gssize +teco_goto_table_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gsize pc) +{ + gssize existing_pc = teco_goto_table_find(ctx, name, len); + if (existing_pc >= 0) + return existing_pc; + + teco_goto_label_t *label = teco_goto_label_new(name, len, pc); + teco_rb3str_insert(&ctx->tree, TRUE, &label->head); + +#ifdef DEBUG + teco_goto_table_dump(ctx); +#endif + + return -1; +} + +/** @memberof teco_goto_table_t */ void teco_goto_table_clear(teco_goto_table_t *ctx) { @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -40,12 +40,12 @@ teco_goto_table_init(teco_goto_table_t *ctx, gboolean must_undo) ctx->must_undo = must_undo; } -gssize teco_goto_table_remove(teco_goto_table_t *ctx, const gchar *name, gsize len); +gboolean teco_goto_table_remove(teco_goto_table_t *ctx, const gchar *name, gsize len); +void teco_goto_table_undo_remove(teco_goto_table_t *ctx, const gchar *name, gsize len); gssize teco_goto_table_find(teco_goto_table_t *ctx, const gchar *name, gsize len); -gssize teco_goto_table_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gssize pc); -void teco_goto_table_undo_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gssize pc); +gssize teco_goto_table_set(teco_goto_table_t *ctx, const gchar *name, gsize len, gsize pc); /** @memberof teco_goto_table_t */ static inline gboolean @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -289,13 +289,13 @@ teco_state_help_done(teco_machine_main_t *ctx, const teco_string_t *str, GError return NULL; } - teco_ring_undo_edit(); /* * ED hooks with the default lexer framework * will usually load the styling SciTECO script * when editing the buffer for the first time. */ - if (!teco_ring_edit(topic->filename, error)) + if (!teco_current_doc_undo_edit(error) || + !teco_ring_edit(topic->filename, error)) return NULL; /* @@ -314,7 +314,10 @@ teco_state_help_done(teco_machine_main_t *ctx, const teco_string_t *str, GError } /* in cmdline.c */ -gboolean teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar chr, GError **error); +gboolean teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, + gunichar chr, GError **error); +gboolean teco_state_help_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, + GError **error); /*$ "?" help * ?[topic]$ -- Get help for topic @@ -384,5 +387,6 @@ gboolean teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine TECO_DEFINE_STATE_EXPECTSTRING(teco_state_help, .initial_cb = (teco_state_initial_cb_t)teco_state_help_initial, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_help_process_edit_cmd, + .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_help_insert_completion, .expectstring.string_building = FALSE ); @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/interface-curses/curses-icons.c b/src/interface-curses/curses-icons.c index e2e4256..3e63d02 100644 --- a/src/interface-curses/curses-icons.c +++ b/src/interface-curses/curses-icons.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/interface-curses/curses-icons.h b/src/interface-curses/curses-icons.h index c1be06f..933241d 100644 --- a/src/interface-curses/curses-icons.h +++ b/src/interface-curses/curses-icons.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/interface-curses/curses-info-popup.c b/src/interface-curses/curses-info-popup.c index e6e1549..dffbcf8 100644 --- a/src/interface-curses/curses-info-popup.c +++ b/src/interface-curses/curses-info-popup.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -200,6 +200,65 @@ teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr) wmove(ctx->window, bar_y, cols-1); wattron(ctx->window, A_REVERSE); wvline(ctx->window, ' ', bar_height); +} + +/** + * Find the entry at the given character coordinates. + * + * @param ctx The popup widget to look up + * @param y The pointer's Y position, relative to the popup's window + * @param x The pointer's X position, relative to the popup's window + * @return Pointer to the entry's string under the pointer or NULL. + * This string is owned by the popup and is only valid until the + * popup is cleared. + * + * @note This must match the calculations in teco_curses_info_popup_init_pad(). + * But we could perhaps also cache these values. + */ +const teco_string_t * +teco_curses_info_popup_getentry(teco_curses_info_popup_t *ctx, gint y, gint x) +{ + int cols = getmaxx(stdscr); /**! screen width */ + gint pad_cols; /**! entry columns */ + gint pad_colwidth; /**! width per entry column */ + + if (y == 0) + return NULL; + + /* + * With Unicode icons enabled, we reserve 2 characters at the beginning and one + * after the filename/directory. + * Otherwise 2 characters after the entry. + */ + gint reserve = teco_ed & TECO_ED_ICONS ? 2+1 : 2; + pad_colwidth = MIN(ctx->longest + reserve, cols - 2); + + /* pad_cols = floor((cols - 2) / pad_colwidth) */ + pad_cols = (cols - 2) / pad_colwidth; + + gint cur_col = 0; + for (teco_stailq_entry_t *cur = ctx->list.first; cur != NULL; cur = cur->next) { + teco_popup_entry_t *entry = (teco_popup_entry_t *)cur; + gint cur_line = cur_col/pad_cols + 1; + + if (cur_line > ctx->pad_first_line+y) + break; + if (cur_line == ctx->pad_first_line+y && + x > (cur_col % pad_cols)*pad_colwidth && x <= ((cur_col % pad_cols)+1)*pad_colwidth) + return &entry->name; + + cur_col++; + } + + return NULL; +} + +void +teco_curses_info_popup_scroll_page(teco_curses_info_popup_t *ctx) +{ + gint lines = getmaxy(stdscr); + gint pad_lines = getmaxy(ctx->pad); + gint popup_lines = MIN(pad_lines + 1, lines - 1); /* progress scroll position */ ctx->pad_first_line += popup_lines - 1; @@ -211,6 +270,19 @@ teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr) } void +teco_curses_info_popup_scroll(teco_curses_info_popup_t *ctx, gint delta) +{ + gint lines = getmaxy(stdscr); + gint pad_lines = getmaxy(ctx->pad); + gint popup_lines = MIN(pad_lines + 1, lines - 1); + + ctx->pad_first_line = MAX(ctx->pad_first_line+delta, 0); + if (pad_lines - ctx->pad_first_line < popup_lines - 1) + /* show last page */ + ctx->pad_first_line = pad_lines - (popup_lines - 1); +} + +void teco_curses_info_popup_clear(teco_curses_info_popup_t *ctx) { if (ctx->window) diff --git a/src/interface-curses/curses-info-popup.h b/src/interface-curses/curses-info-popup.h index a6c28a5..d845b29 100644 --- a/src/interface-curses/curses-info-popup.h +++ b/src/interface-curses/curses-info-popup.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,6 +23,7 @@ #include <curses.h> #include "list.h" +#include "string-utils.h" #include "interface.h" typedef struct { @@ -49,6 +50,10 @@ void teco_curses_info_popup_add(teco_curses_info_popup_t *ctx, teco_popup_entry_ const gchar *name, gsize name_len, gboolean highlight); void teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr); +const teco_string_t *teco_curses_info_popup_getentry(teco_curses_info_popup_t *ctx, gint y, gint x); +void teco_curses_info_popup_scroll_page(teco_curses_info_popup_t *ctx); +void teco_curses_info_popup_scroll(teco_curses_info_popup_t *ctx, gint delta); + static inline bool teco_curses_info_popup_is_shown(teco_curses_info_popup_t *ctx) { @@ -58,8 +63,17 @@ teco_curses_info_popup_is_shown(teco_curses_info_popup_t *ctx) static inline void teco_curses_info_popup_noutrefresh(teco_curses_info_popup_t *ctx) { - if (ctx->window) - wnoutrefresh(ctx->window); + if (!ctx->window) + return; + /* + * NOTE: Scinterm always redraws its window, which is + * equivalent to touching it, even if it didn't change. + * Consequently, wnoutrefresh() will always copy it to newscr. + * We must therefore always redraw the popup as well, so it + * will still overlap the Scintilla view. + */ + touchwin(ctx->window); + wnoutrefresh(ctx->window); } void teco_curses_info_popup_clear(teco_curses_info_popup_t *ctx); diff --git a/src/interface-curses/curses-utils.c b/src/interface-curses/curses-utils.c index c751afd..f362424 100644 --- a/src/interface-curses/curses-utils.c +++ b/src/interface-curses/curses-utils.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/interface-curses/curses-utils.h b/src/interface-curses/curses-utils.h index 2c819ee..9f2e8f3 100644 --- a/src/interface-curses/curses-utils.h +++ b/src/interface-curses/curses-utils.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/interface-curses/interface.c b/src/interface-curses/interface.c index f713bc1..42ffdc6 100644 --- a/src/interface-curses/interface.c +++ b/src/interface-curses/interface.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -44,6 +44,10 @@ #include <glib/gprintf.h> #include <glib/gstdio.h> +#ifdef G_OS_UNIX +#include <sys/wait.h> +#endif + #include <curses.h> #ifdef HAVE_TIGETSTR @@ -53,6 +57,7 @@ * Some macros in term.h interfere with our code. */ #undef lines +#undef buttons #endif #include <Scintilla.h> @@ -355,6 +360,7 @@ static struct { GQueue *input_queue; teco_curses_info_popup_t popup; + gsize popup_prefix_len; /** * GError "thrown" by teco_interface_event_loop_iter(). @@ -688,6 +694,15 @@ teco_interface_init_interactive(GError **error) #endif /* + * Disables click-detection. + * If we'd want to discern PRESSED and CLICKED events, + * we'd have to emulate the same feature on GTK. + */ +#if NCURSES_MOUSE_VERSION >= 2 + mouseinterval(0); +#endif + + /* * We always have a CTRL handler on Windows, but doing it * here again, ensures that we have a higher precedence * than the one installed by PDCurses. @@ -700,6 +715,11 @@ teco_interface_init_interactive(GError **error) noecho(); /* Scintilla draws its own cursor */ curs_set(0); + /* + * This has also been observed to reduce flickering + * in teco_interface_refresh(). + */ + leaveok(stdscr, TRUE); teco_interface.info_window = newwin(1, 0, 0, 0); teco_interface.msg_window = newwin(1, 0, LINES - 2, 0); @@ -781,11 +801,11 @@ teco_interface_restore_batch(void) */ #ifdef CURSES_TTY if (teco_interface.stdout_orig >= 0) { - int fd = dup2(teco_interface.stdout_orig, 1); + G_GNUC_UNUSED int fd = dup2(teco_interface.stdout_orig, 1); g_assert(fd == 1); } if (teco_interface.stderr_orig >= 0) { - int fd = dup2(teco_interface.stderr_orig, 2); + G_GNUC_UNUSED int fd = dup2(teco_interface.stderr_orig, 2); g_assert(fd == 2); } #endif @@ -1247,39 +1267,7 @@ teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError return TRUE; } -#elif defined(CURSES_TTY) - -static void -teco_interface_init_clipboard(void) -{ - /* - * At least on XTerm, there are escape sequences - * for modifying the clipboard (OSC-52). - * This is not standardized in terminfo, so we add special - * XTerm support here. Unfortunately, it is pretty hard to find out - * whether clipboard operations will actually work. - * XTerm must be at least at v203 and the corresponding window operations - * must be enabled. - * There is no way to find out if they are but we must - * not register the clipboard registers if they aren't. - * Still, XTerm clipboards are broken with Unicode characters. - * Also, there are other terminal emulators supporting OSC-52, - * so the XTerm version is only checked if the terminal identifies as XTerm. - * Also, a special clipboard ED flag must be set by the user. - * - * NOTE: Apparently there is also a terminfo entry Ms, but it's probably - * not worth using it since it won't always be set and even if set, does not - * tell you whether the terminal will actually answer to the escape sequence or not. - */ - if (!(teco_ed & TECO_ED_OSC52) || - (teco_xterm_version() >= 0 && teco_xterm_version() < 203)) - return; - - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("P")); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("S")); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("C")); -} +#elif defined(G_OS_UNIX) && defined(CURSES_TTY) static inline gchar get_selection_by_name(const gchar *name) @@ -1294,9 +1282,48 @@ get_selection_by_name(const gchar *name) return g_ascii_tolower(*name) ? : 'c'; } -gboolean -teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, - GError **error) +/* + * OSC-52 clipboard implementation. + * + * At least on XTerm, there are escape sequences + * for modifying the clipboard (OSC-52). + * This is not standardized in terminfo, so we add special + * XTerm support here. Unfortunately, it is pretty hard to find out + * whether clipboard operations will actually work. + * XTerm must be at least at v203 and the corresponding window operations + * must be enabled. + * There is no way to find out if they are but we must + * not register the clipboard registers if they aren't. + * Still, XTerm clipboards are broken with Unicode characters. + * Also, there are other terminal emulators supporting OSC-52, + * so the XTerm version is only checked if the terminal identifies as XTerm. + * Also, a special clipboard ED flag must be set by the user. + * + * NOTE: Apparently there is also a terminfo entry Ms, but it's probably + * not worth using it since it won't always be set and even if set, does not + * tell you whether the terminal will actually answer to the escape sequence or not. + * + * This is a rarely used feature and could theoretically also be handled + * by the $SCITECO_CLIPBOARD_SET/GET feature. + * Unfortunately, there is no readily available command-line utility allowing both + * copying and pasting via OSC-52. + * That's really the only reason we keep built-in OSC-52 clipboard support. + * + * FIXME: This is the only thing here requiring CURSES_TTY. + * On the other hand, there is hardly any non-PDCurses on UNIX, which is not + * on a TTY, so we shouldn't be loosing much by requiring both. + */ + +static inline gboolean +teco_interface_osc52_is_enabled(void) +{ + return teco_ed & TECO_ED_OSC52 && + (teco_xterm_version() < 0 || teco_xterm_version() >= 203); +} + +static gboolean +teco_interface_osc52_set_clipboard(const gchar *name, const gchar *str, gsize str_len, + GError **error) { fputs("\e]52;", teco_interface.screen_tty); fputc(get_selection_by_name(name), teco_interface.screen_tty); @@ -1335,8 +1362,8 @@ teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, return TRUE; } -gboolean -teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error) +static gboolean +teco_interface_osc52_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error) { gboolean ret = TRUE; @@ -1430,7 +1457,167 @@ cleanup: return ret; } -#else /* !PDCURSES && !CURSES_TTY */ +/* + * Implementation using external processes. + * + * NOTE: This could be done with the portable GSpawn API as well, + * but this implementation is much simpler. + * We don't really need it on Windows anyway as long as we are using + * only PDCurses. + * This might only be of interest on Windows if building for the Win32 version + * of ncurses. + * As a downside, compared to GSpawn, this cannot inherit the environment + * variables from the global Q-Register table. + */ + +static void +teco_interface_init_clipboard(void) +{ + if (!teco_interface_osc52_is_enabled() && + (!teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECO_CLIPBOARD_SET", 22) || + !teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECO_CLIPBOARD_GET", 22))) + return; + + teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); + teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("P")); + teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("S")); + teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("C")); +} + +gboolean +teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, + GError **error) +{ + if (teco_interface_osc52_is_enabled()) + return teco_interface_osc52_set_clipboard(name, str, str_len, error); + + static const gchar *reg_name = "$SCITECO_CLIPBOARD_SET"; + + teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, reg_name, strlen(reg_name)); + if (!reg) { + /* Q-Register could have been removed in the meantime */ + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Cannot set clipboard. %s is undefined.", reg_name); + return FALSE; + } + + g_auto(teco_string_t) command; + if (!reg->vtable->get_string(reg, &command.data, &command.len, NULL, error)) + return FALSE; + if (teco_string_contains(&command, '\0')) { + teco_error_qregcontainsnull_set(error, reg_name, strlen(reg_name), FALSE); + return FALSE; + } + + gchar *sel = g_strstr_len(command.data, command.len, "{}"); + if (sel) { + *sel++ = ' '; + *sel = get_selection_by_name(name); + } + + FILE *pipe = popen(command.data, "w"); + if (!pipe) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Cannot spawn process from %s", reg_name); + return FALSE; + } + + size_t len = fwrite(str, 1, str_len, pipe); + + int status = pclose(pipe); + if (status < 0 || !WIFEXITED(status)) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Error reaping process from %s", reg_name); + return FALSE; + } + if (WEXITSTATUS(status) != 0) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Process from %s returned with exit code %d", + reg_name, WEXITSTATUS(status)); + return FALSE; + } + + if (len < str_len) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Error writing to process from %s", reg_name); + return FALSE; + } + + return TRUE; +} + +gboolean +teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError **error) +{ + if (teco_interface_osc52_is_enabled()) + return teco_interface_osc52_get_clipboard(name, str, len, error); + + static const gchar *reg_name = "$SCITECO_CLIPBOARD_GET"; + + teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, reg_name, strlen(reg_name)); + if (!reg) { + /* Q-Register could have been removed in the meantime */ + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Cannot get clipboard. %s is undefined.", reg_name); + return FALSE; + } + + g_auto(teco_string_t) command; + if (!reg->vtable->get_string(reg, &command.data, &command.len, NULL, error)) + return FALSE; + if (teco_string_contains(&command, '\0')) { + teco_error_qregcontainsnull_set(error, reg_name, strlen(reg_name), FALSE); + return FALSE; + } + + gchar *sel = g_strstr_len(command.data, command.len, "{}"); + if (sel) { + *sel++ = ' '; + *sel = get_selection_by_name(name); + } + + FILE *pipe = popen(command.data, "r"); + if (!pipe) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Cannot spawn process from %s", reg_name); + return FALSE; + } + + gchar buffer[1024]; + size_t read_len; + + g_auto(teco_string_t) ret = {NULL, 0}; + + do { + read_len = fread(buffer, 1, sizeof(buffer), pipe); + teco_string_append(&ret, buffer, read_len); + } while (read_len == sizeof(buffer)); + + int status = pclose(pipe); + if (status < 0 || !WIFEXITED(status)) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Error reaping process from %s", reg_name); + return FALSE; + } + /* + * You may have to add a `|| true` for instance to xclip if it + * could fail for empty selections. + */ + if (WEXITSTATUS(status) != 0) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Process from %s returned with exit code %d", + reg_name, WEXITSTATUS(status)); + return FALSE; + } + + *str = ret.data; + *len = ret.len; + memset(&ret, 0, sizeof(ret)); + + return TRUE; +} + +#else /* !PDCURSES && !G_OS_UNIX && !CURSES_TTY */ static void teco_interface_init_clipboard(void) @@ -1470,7 +1657,7 @@ teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize } void -teco_interface_popup_show(void) +teco_interface_popup_show(gsize prefix_len) { if (!teco_interface.cmdline_window) /* batch mode */ @@ -1479,9 +1666,21 @@ teco_interface_popup_show(void) short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_CALLTIP, 0)); short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_CALLTIP, 0)); + teco_interface.popup_prefix_len = prefix_len; teco_curses_info_popup_show(&teco_interface.popup, SCI_COLOR_ATTR(fg, bg)); } +void +teco_interface_popup_scroll(void) +{ + if (!teco_interface.cmdline_window) + /* batch mode */ + return; + + teco_curses_info_popup_scroll_page(&teco_interface.popup); + teco_interface_popup_show(teco_interface.popup_prefix_len); +} + gboolean teco_interface_popup_is_shown(void) { @@ -1496,8 +1695,7 @@ teco_interface_popup_clear(void) * PDCurses will not redraw all windows that may be * overlapped by the popup window correctly - at least * not the info window. - * The Scintilla window is apparently always touched by - * scintilla_noutrefresh(). + * The Scintilla window is always touched by scintilla_noutrefresh(). * Actually we would expect this to be necessary on any curses, * but ncurses doesn't require this. */ @@ -1563,13 +1761,6 @@ static void teco_interface_refresh(void) { /* - * Scintilla has been patched to avoid any automatic scrolling since that - * has been benchmarked to be a very costly operation. - * Instead we do it only once after every keypress. - */ - teco_interface_ssm(SCI_SCROLLCARET, 0, 0); - - /* * Info window is updated very often which is very * costly, especially when using PDC_set_title(), * so we redraw it here, where the overhead does @@ -1584,9 +1775,131 @@ teco_interface_refresh(void) doupdate(); } +#if NCURSES_MOUSE_VERSION >= 2 + +#define BUTTON_NUM(X) \ + (BUTTON##X##_PRESSED | BUTTON##X##_RELEASED | \ + BUTTON##X##_CLICKED | BUTTON##X##_DOUBLE_CLICKED | BUTTON##X##_TRIPLE_CLICKED) +#define BUTTON_EVENT(X) \ + (BUTTON1_##X | BUTTON2_##X | BUTTON3_##X | BUTTON4_##X | BUTTON5_##X) + +static gboolean +teco_interface_getmouse(GError **error) +{ + MEVENT event; + + if (getmouse(&event) != OK) + return TRUE; + + if (teco_curses_info_popup_is_shown(&teco_interface.popup) && + wmouse_trafo(teco_interface.popup.window, &event.y, &event.x, FALSE)) { + /* + * NOTE: Not all curses variants report the RELEASED event, + * but may also return REPORT_MOUSE_POSITION. + * So we might react to all button presses as well. + */ + if (event.bstate & (BUTTON1_RELEASED | REPORT_MOUSE_POSITION)) { + teco_machine_t *machine = &teco_cmdline.machine.parent; + const teco_string_t *insert = teco_curses_info_popup_getentry(&teco_interface.popup, event.y, event.x); + + if (insert && machine->current->insert_completion_cb) { + /* successfully clicked popup item */ + const teco_string_t insert_suffix = {insert->data + teco_interface.popup_prefix_len, + insert->len - teco_interface.popup_prefix_len}; + if (!machine->current->insert_completion_cb(machine, &insert_suffix, error)) + return FALSE; + + teco_interface_popup_clear(); + teco_interface_msg_clear(); + teco_interface_cmdline_update(&teco_cmdline); + } + + return TRUE; + } + if (event.bstate & BUTTON_NUM(4)) + teco_curses_info_popup_scroll(&teco_interface.popup, -1); + else if (event.bstate & BUTTON_NUM(5)) + teco_curses_info_popup_scroll(&teco_interface.popup, +1); + + short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_CALLTIP, 0)); + short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_CALLTIP, 0)); + teco_curses_info_popup_show(&teco_interface.popup, SCI_COLOR_ATTR(fg, bg)); + + return TRUE; + } + + /* + * Return mouse coordinates relative to the view. + * They will be in characters, but that's what SCI_POSITIONFROMPOINT + * expects on Scinterm anyway. + */ + WINDOW *current = teco_view_get_window(teco_interface_current_view); + if (!wmouse_trafo(current, &event.y, &event.x, FALSE)) + /* no event inside of current view */ + return TRUE; + + /* + * NOTE: There will only be one of the button bits + * set in bstate, so we don't loose information translating + * them to enums. + * + * At least on ncurses, we don't always get a RELEASED event. + * It instead sends only REPORT_MOUSE_POSITION, + * so make sure not to overwrite teco_mouse.button in this case. + */ + if (event.bstate & BUTTON_NUM(4)) + /* scroll up - there will be no RELEASED event */ + teco_mouse.type = TECO_MOUSE_SCROLLUP; + else if (event.bstate & BUTTON_NUM(5)) + /* scroll down - there will be no RELEASED event */ + teco_mouse.type = TECO_MOUSE_SCROLLDOWN; + else if (event.bstate & BUTTON_EVENT(RELEASED)) + teco_mouse.type = TECO_MOUSE_RELEASED; + else if (event.bstate & BUTTON_EVENT(PRESSED)) + teco_mouse.type = TECO_MOUSE_PRESSED; + else + /* can also be REPORT_MOUSE_POSITION */ + teco_mouse.type = TECO_MOUSE_RELEASED; + + teco_mouse.x = event.x; + teco_mouse.y = event.y; + + if (event.bstate & BUTTON_NUM(1)) + teco_mouse.button = 1; + else if (event.bstate & BUTTON_NUM(2)) + teco_mouse.button = 2; + else if (event.bstate & BUTTON_NUM(3)) + teco_mouse.button = 3; + else if (!(event.bstate & REPORT_MOUSE_POSITION)) + teco_mouse.button = -1; + + teco_mouse.mods = 0; + if (event.bstate & BUTTON_SHIFT) + teco_mouse.mods |= TECO_MOUSE_SHIFT; + if (event.bstate & BUTTON_CTRL) + teco_mouse.mods |= TECO_MOUSE_CTRL; + if (event.bstate & BUTTON_ALT) + teco_mouse.mods |= TECO_MOUSE_ALT; + + return teco_cmdline_keymacro("MOUSE", -1, error); +} + +#endif /* NCURSES_MOUSE_VERSION >= 2 */ + static gint teco_interface_blocking_getch(void) { +#if NCURSES_MOUSE_VERSION >= 2 + /* + * FIXME: REPORT_MOUSE_POSITION is necessary at least on + * ncurses, so that BUTTONX_RELEASED events are reported. + * It does NOT report every cursor movement, though. + * What does PDCurses do? + */ + mousemask(teco_ed & TECO_ED_MOUSEKEY + ? ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION : 0, NULL); +#endif + /* no special <CTRL/C> handling */ raw(); nodelay(teco_interface.input_pad, FALSE); @@ -1630,6 +1943,9 @@ teco_interface_event_loop_iter(void) ? teco_interface_blocking_getch() : GPOINTER_TO_INT(g_queue_pop_head(teco_interface.input_queue)); + const teco_view_t *last_view = teco_interface_current_view; + sptr_t last_pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + switch (key) { case ERR: /* shouldn't really happen */ @@ -1697,6 +2013,14 @@ teco_interface_event_loop_iter(void) #undef FNS #undef FN +#if NCURSES_MOUSE_VERSION >= 2 + case KEY_MOUSE: + /* ANY of the mouse events */ + if (!teco_interface_getmouse(error)) + return; + break; +#endif + /* * Control keys and keys with printable representation */ @@ -1740,6 +2064,14 @@ teco_interface_event_loop_iter(void) } } + /* + * Scintilla has been patched to avoid any automatic scrolling since that + * has been benchmarked to be a very costly operation. + * Instead we do it only once after every keypress. + */ + if (teco_interface_current_view != last_view || + last_pos != teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0)) + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); teco_interface_refresh(); } @@ -1755,6 +2087,7 @@ teco_interface_event_loop(GError **error) static const teco_cmdline_t empty_cmdline; // FIXME teco_interface_cmdline_update(&empty_cmdline); teco_interface_msg_clear(); + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); teco_interface_refresh(); #ifdef EMCURSES diff --git a/src/interface-gtk/Makefile.am b/src/interface-gtk/Makefile.am index 50e5311..e731a21 100644 --- a/src/interface-gtk/Makefile.am +++ b/src/interface-gtk/Makefile.am @@ -4,7 +4,7 @@ AM_CPPFLAGS += -I$(top_srcdir)/contrib/rb3ptr \ AM_CFLAGS = -std=gnu11 -Wall -Wno-initializer-overrides -Wno-unused-value noinst_LTLIBRARIES = libsciteco-interface.la -libsciteco_interface_la_SOURCES = interface.c \ +libsciteco_interface_la_SOURCES = view.c interface.c \ gtk-info-popup.c gtk-info-popup.h \ gtk-label.c gtk-label.h diff --git a/src/interface-gtk/gtk-info-popup.c b/src/interface-gtk/gtk-info-popup.c index 4e25224..aaa0a65 100644 --- a/src/interface-gtk/gtk-info-popup.c +++ b/src/interface-gtk/gtk-info-popup.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -47,12 +47,15 @@ struct _TecoGtkInfoPopup { GtkAdjustment *hadjustment, *vadjustment; GtkWidget *flow_box; + GdkCursor *cursor; /*< pointer/hand cursor */ GStringChunk *chunk; teco_stailq_head_t list; guint idle_id; gboolean frozen; }; +static guint teco_gtk_info_popup_clicked_signal; + static gboolean teco_gtk_info_popup_scroll_event(GtkWidget *widget, GdkEventScroll *event); static void teco_gtk_info_popup_show(GtkWidget *widget); static void teco_gtk_info_popup_vadjustment_changed(GtkAdjustment *vadjustment, GtkWidget *scrollbar); @@ -72,6 +75,9 @@ teco_gtk_info_popup_finalize(GObject *obj_self) while ((entry = teco_stailq_remove_head(&self->list))) g_free(entry); + if (self->cursor) + g_object_unref(self->cursor); + /* chain up to parent class */ G_OBJECT_CLASS(teco_gtk_info_popup_parent_class)->finalize(obj_self); } @@ -82,6 +88,31 @@ teco_gtk_info_popup_class_init(TecoGtkInfoPopupClass *klass) GTK_WIDGET_CLASS(klass)->scroll_event = teco_gtk_info_popup_scroll_event; GTK_WIDGET_CLASS(klass)->show = teco_gtk_info_popup_show; G_OBJECT_CLASS(klass)->finalize = teco_gtk_info_popup_finalize; + + teco_gtk_info_popup_clicked_signal = + g_signal_new("clicked", G_TYPE_FROM_CLASS(klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_ULONG); +} + +static void +teco_gtk_info_popup_activated_cb(GtkFlowBox *box, GtkFlowBoxChild *child, gpointer user_data) +{ + TecoGtkInfoPopup *popup = TECO_GTK_INFO_POPUP(user_data); + + /* + * Find the TecoGtkLabel in the flow box child. + */ + GtkWidget *hbox = gtk_bin_get_child(GTK_BIN(child)); + g_autoptr(GList) child_list = gtk_container_get_children(GTK_CONTAINER(hbox)); + GList *entry; + for (entry = child_list; entry != NULL && !TECO_IS_GTK_LABEL(entry->data); entry = g_list_next(entry)); + g_assert(entry != NULL); + const teco_string_t *str = teco_gtk_label_get_text(TECO_GTK_LABEL(entry->data)); + + g_signal_emit(popup, teco_gtk_info_popup_clicked_signal, 0, + str->data, (gulong)str->len); } static void @@ -106,6 +137,8 @@ teco_gtk_info_popup_init(TecoGtkInfoPopup *self) G_CALLBACK(teco_gtk_info_popup_vadjustment_changed), scrollbar); self->flow_box = gtk_flow_box_new(); + g_signal_connect(self->flow_box, "child-activated", + G_CALLBACK(teco_gtk_info_popup_activated_cb), self); /* take as little height as necessary */ gtk_orientable_set_orientation(GTK_ORIENTABLE(self->flow_box), GTK_ORIENTATION_HORIZONTAL); @@ -311,12 +344,6 @@ teco_gtk_info_popup_idle_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t typ gtk_widget_set_halign(label, GTK_ALIGN_START); gtk_widget_set_valign(label, GTK_ALIGN_CENTER); - /* - * FIXME: This makes little sense once we've got mouse support. - * But for the time being, it's a useful setting. - */ - gtk_label_set_selectable(GTK_LABEL(label), TRUE); - switch (type) { case TECO_POPUP_PLAIN: gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_START); @@ -331,6 +358,16 @@ teco_gtk_info_popup_idle_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t typ gtk_widget_show_all(hbox); gtk_container_add(GTK_CONTAINER(self->flow_box), hbox); + + GtkWidget *flow_box_child = gtk_widget_get_parent(hbox); + g_assert(GTK_IS_FLOW_BOX_CHILD(flow_box_child)); + GdkWindow *window = gtk_widget_get_window(flow_box_child); + g_assert(window != NULL); + + if (G_UNLIKELY(!self->cursor)) + /* we only initialize it now after guaranteed widget realization */ + self->cursor = gdk_cursor_new_from_name(gdk_window_get_display(window), "pointer"); + gdk_window_set_cursor(window, self->cursor); } static gboolean @@ -417,12 +454,10 @@ teco_gtk_info_popup_scroll_page(TecoGtkInfoPopup *self) * Adjust this so only complete entries are shown. * Effectively, this rounds down to the line height. */ - GList *child_list = gtk_container_get_children(GTK_CONTAINER(self->flow_box)); - if (child_list) { + g_autoptr(GList) child_list = gtk_container_get_children(GTK_CONTAINER(self->flow_box)); + if (child_list) new_value -= (gint)new_value % gtk_widget_get_allocated_height(GTK_WIDGET(child_list->data)); - g_list_free(child_list); - } /* clip to the maximum possible value */ new_value = MIN(new_value, gtk_adjustment_get_upper(adj)); diff --git a/src/interface-gtk/gtk-info-popup.h b/src/interface-gtk/gtk-info-popup.h index c3a62ec..ad79b84 100644 --- a/src/interface-gtk/gtk-info-popup.h +++ b/src/interface-gtk/gtk-info-popup.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/interface-gtk/gtk-label.c b/src/interface-gtk/gtk-label.c index 50cd345..ef370a2 100644 --- a/src/interface-gtk/gtk-label.c +++ b/src/interface-gtk/gtk-label.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -269,3 +269,9 @@ teco_gtk_label_set_text(TecoGtkLabel *self, const gchar *str, gssize len) gtk_label_set_text(GTK_LABEL(self), plaintext); } + +const teco_string_t * +teco_gtk_label_get_text(TecoGtkLabel *self) +{ + return &self->string; +} diff --git a/src/interface-gtk/gtk-label.h b/src/interface-gtk/gtk-label.h index bed6642..c52d073 100644 --- a/src/interface-gtk/gtk-label.h +++ b/src/interface-gtk/gtk-label.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,6 +27,7 @@ G_DECLARE_FINAL_TYPE(TecoGtkLabel, teco_gtk_label, TECO, GTK_LABEL, GtkLabel) GtkWidget *teco_gtk_label_new(const gchar *str, gssize len); void teco_gtk_label_set_text(TecoGtkLabel *self, const gchar *str, gssize len); +const teco_string_t *teco_gtk_label_get_text(TecoGtkLabel *self); void teco_gtk_label_parse_string(const gchar *str, gssize len, PangoColor *fg, guint16 fg_alpha, diff --git a/src/interface-gtk/interface.c b/src/interface-gtk/interface.c index 0dbd2ba..7f58c45 100644 --- a/src/interface-gtk/interface.c +++ b/src/interface-gtk/interface.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -60,16 +60,17 @@ //#define DEBUG +static gboolean teco_interface_busy_timeout_cb(gpointer user_data); +static void teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data); static void teco_interface_cmdline_size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation, gpointer user_data); static void teco_interface_cmdline_commit_cb(GtkIMContext *context, gchar *str, gpointer user_data); -static void teco_interface_size_allocate_cb(GtkWidget *widget, - GdkRectangle *allocation, +static gboolean teco_interface_input_cb(GtkWidget *widget, GdkEvent *event, + gpointer user_data); +static void teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong len, gpointer user_data); -static gboolean teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, - gpointer user_data); 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; @@ -102,73 +103,6 @@ teco_bgr2rgb(guint32 bgr) return GUINT32_SWAP_LE_BE(bgr) >> 8; } -/* - * NOTE: The teco_view_t pointer is reused to directly - * point to the ScintillaObject. - * This saves one heap object per view. - */ - -static void -teco_view_scintilla_notify(ScintillaObject *sci, gint iMessage, - SCNotification *notify, gpointer user_data) -{ - teco_view_process_notify((teco_view_t *)sci, notify); -} - -teco_view_t * -teco_view_new(void) -{ - ScintillaObject *sci = SCINTILLA(scintilla_new()); - /* - * We don't want the object to be destroyed - * when it is removed from the vbox. - */ - g_object_ref_sink(sci); - - scintilla_set_id(sci, 0); - - gtk_widget_set_size_request(GTK_WIDGET(sci), 500, 300); - - /* - * This disables mouse and key events on this view. - * For some strange reason, masking events on - * the event box does NOT work. - * - * NOTE: Scroll events are still allowed - scrolling - * is currently not under direct control of SciTECO - * (i.e. it is OK the side effects of scrolling are not - * tracked). - */ - gtk_widget_set_can_focus(GTK_WIDGET(sci), FALSE); - gint events = gtk_widget_get_events(GTK_WIDGET(sci)); - events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK); - events &= ~(GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK); - gtk_widget_set_events(GTK_WIDGET(sci), events); - - g_signal_connect(sci, SCINTILLA_NOTIFY, - G_CALLBACK(teco_view_scintilla_notify), NULL); - - return (teco_view_t *)sci; -} - -static inline GtkWidget * -teco_view_get_widget(teco_view_t *ctx) -{ - return GTK_WIDGET(ctx); -} - -sptr_t -teco_view_ssm(teco_view_t *ctx, unsigned int iMessage, uptr_t wParam, sptr_t lParam) -{ - return scintilla_send_message(SCINTILLA(ctx), iMessage, wParam, lParam); -} - -void -teco_view_free(teco_view_t *ctx) -{ - g_object_unref(teco_view_get_widget(ctx)); -} - static struct { GtkCssProvider *css_var_provider; @@ -198,6 +132,7 @@ static struct { GtkIMContext *input_method; GtkWidget *popup_widget; + gsize popup_prefix_len; GtkWidget *current_view_widget; @@ -240,7 +175,7 @@ teco_interface_init(void) G_CALLBACK(teco_interface_window_delete_cb), NULL); g_signal_connect(teco_interface.window, "key-press-event", - G_CALLBACK(teco_interface_key_pressed_cb), NULL); + G_CALLBACK(teco_interface_input_cb), NULL); GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); @@ -313,8 +248,20 @@ teco_interface_init(void) gtk_box_pack_start(GTK_BOX(overlay_vbox), teco_interface.event_box_widget, TRUE, TRUE, 0); - g_signal_connect(teco_interface.event_box_widget, "size-allocate", - G_CALLBACK(teco_interface_size_allocate_cb), NULL); + g_signal_connect(teco_interface.event_box_widget, "realize", + G_CALLBACK(teco_interface_event_box_realized_cb), NULL); + + gint events = gtk_widget_get_events(teco_interface.event_box_widget); + events |= GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_SCROLL_MASK; + gtk_widget_set_events(teco_interface.event_box_widget, events); + + g_signal_connect(teco_interface.event_box_widget, "button-press-event", + G_CALLBACK(teco_interface_input_cb), NULL); + g_signal_connect(teco_interface.event_box_widget, "button-release-event", + G_CALLBACK(teco_interface_input_cb), NULL); + g_signal_connect(teco_interface.event_box_widget, "scroll-event", + G_CALLBACK(teco_interface_input_cb), NULL); teco_interface.message_bar_widget = gtk_info_bar_new(); gtk_widget_set_name(teco_interface.message_bar_widget, "sciteco-message-bar"); @@ -350,7 +297,7 @@ teco_interface_init(void) /* we will forward key events, so the view should only react to text insertion */ teco_view_ssm(teco_interface.cmdline_view, SCI_CLEARALLCMDKEYS, 0, 0); - GtkWidget *cmdline_widget = teco_view_get_widget(teco_interface.cmdline_view); + GtkWidget *cmdline_widget = GTK_WIDGET(teco_interface.cmdline_view); gtk_widget_set_name(cmdline_widget, "sciteco-cmdline"); g_signal_connect(cmdline_widget, "size-allocate", G_CALLBACK(teco_interface_cmdline_size_allocate_cb), NULL); @@ -373,6 +320,8 @@ teco_interface_init(void) */ teco_interface.popup_widget = teco_gtk_info_popup_new(); gtk_widget_set_name(teco_interface.popup_widget, "sciteco-info-popup"); + g_signal_connect(teco_interface.popup_widget, "clicked", + G_CALLBACK(teco_interface_popup_clicked_cb), NULL); gtk_overlay_add_overlay(GTK_OVERLAY(overlay_widget), teco_interface.popup_widget); g_signal_connect(overlay_widget, "get-child-position", G_CALLBACK(teco_gtk_info_popup_get_position_in_overlay), NULL); @@ -389,6 +338,17 @@ teco_interface_init(void) teco_interface_cmdline_update(&empty_cmdline); } +static void +teco_interface_set_cursor(GtkWidget *widget, const gchar *name) +{ + GdkWindow *window = gtk_widget_get_window(widget); + g_assert(window != NULL); + GdkDisplay *display = gdk_window_get_display(window); + + g_autoptr(GdkCursor) cursor = name ? gdk_cursor_new_from_name(display, name) : NULL; + gdk_window_set_cursor(window, cursor); +} + GOptionGroup * teco_interface_get_options(void) { @@ -746,12 +706,16 @@ teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize } void -teco_interface_popup_show(void) +teco_interface_popup_show(gsize prefix_len) { - if (gtk_widget_get_visible(teco_interface.popup_widget)) - teco_gtk_info_popup_scroll_page(TECO_GTK_INFO_POPUP(teco_interface.popup_widget)); - else - gtk_widget_show(teco_interface.popup_widget); + teco_interface.popup_prefix_len = prefix_len; + gtk_widget_show(teco_interface.popup_widget); +} + +void +teco_interface_popup_scroll(void) +{ + teco_gtk_info_popup_scroll_page(TECO_GTK_INFO_POPUP(teco_interface.popup_widget)); } gboolean @@ -876,7 +840,7 @@ teco_interface_set_css_variables(teco_view_t *view) * This cannot be done via CSS or Scintilla messages. * Currently, it is always exactly one line high in order to mimic the Curses UI. */ - gtk_widget_set_size_request(teco_view_get_widget(teco_interface.cmdline_view), -1, text_height); + gtk_widget_set_size_request(GTK_WIDGET(teco_interface.cmdline_view), -1, text_height); } static void @@ -910,19 +874,12 @@ teco_interface_refresh(gboolean current_view_changed) gtk_container_remove(GTK_CONTAINER(teco_interface.event_box_widget), teco_interface.current_view_widget); - teco_interface.current_view_widget = teco_view_get_widget(teco_interface_current_view); + teco_interface.current_view_widget = GTK_WIDGET(teco_interface_current_view); gtk_container_add(GTK_CONTAINER(teco_interface.event_box_widget), teco_interface.current_view_widget); gtk_widget_show(teco_interface.current_view_widget); } - - /* - * Scintilla has been patched to avoid any automatic scrolling since that - * has been benchmarked to be a very costly operation. - * Instead we do it only once after every keypress. - */ - teco_interface_ssm(SCI_SCROLLCARET, 0, 0); } static void @@ -977,7 +934,7 @@ teco_interface_get_ansi_key(GdkEventKey *event) static gboolean teco_interface_handle_key_press(GdkEventKey *event, GError **error) { - const teco_view_t *last_view = teco_interface_current_view; + g_assert(event->type == GDK_KEY_PRESS); switch (event->keyval) { case GDK_KEY_Escape: @@ -1105,10 +1062,81 @@ teco_interface_handle_key_press(GdkEventKey *event, GError **error) gtk_im_context_filter_keypress(teco_interface.input_method, event); } - teco_interface_refresh(teco_interface_current_view != last_view); return TRUE; } +static gboolean +teco_interface_handle_mouse_button(GdkEventButton *event, GError **error) +{ + switch (event->type) { + case GDK_BUTTON_PRESS: + teco_mouse.type = TECO_MOUSE_PRESSED; + break; + case GDK_BUTTON_RELEASE: + teco_mouse.type = TECO_MOUSE_RELEASED; + break; + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + default: + /* delivered in addition to GDK_BUTTON_PRESS */ + return TRUE; + } + + teco_mouse.x = event->x; + teco_mouse.y = event->y; + teco_mouse.button = event->button; + + teco_mouse.mods = 0; + if (event->state & GDK_SHIFT_MASK) + teco_mouse.mods |= TECO_MOUSE_SHIFT; + if (event->state & GDK_CONTROL_MASK) + teco_mouse.mods |= TECO_MOUSE_CTRL; + /* + * NOTE: GTK returns MOD1 *without* SHIFT for ALT. + */ + if ((event->state & (GDK_MOD1_MASK | GDK_SHIFT_MASK)) == GDK_MOD1_MASK) + teco_mouse.mods |= TECO_MOUSE_ALT; + + return teco_cmdline_keymacro("MOUSE", -1, error); +} + +static gboolean +teco_interface_handle_scroll(GdkEventScroll *event, GError **error) +{ + g_assert(event->type == GDK_SCROLL); + + /* + * FIXME: Do we have to support GDK_SCROLL_SMOOTH? + */ + switch (event->direction) { + case GDK_SCROLL_UP: + teco_mouse.type = TECO_MOUSE_SCROLLUP; + break; + case GDK_SCROLL_DOWN: + teco_mouse.type = TECO_MOUSE_SCROLLDOWN; + break; + default: + return TRUE; + } + + teco_mouse.x = event->x; + teco_mouse.y = event->y; + teco_mouse.button = -1; + + teco_mouse.mods = 0; + if (event->state & GDK_SHIFT_MASK) + teco_mouse.mods |= TECO_MOUSE_SHIFT; + if (event->state & GDK_CONTROL_MASK) + teco_mouse.mods |= TECO_MOUSE_CTRL; + /* + * NOTE: GTK returns MOD1 *without* SHIFT for ALT. + */ + if ((event->state & (GDK_MOD1_MASK | GDK_SHIFT_MASK)) == GDK_MOD1_MASK) + teco_mouse.mods |= TECO_MOUSE_ALT; + + return teco_cmdline_keymacro("MOUSE", -1, error); +} + gboolean teco_interface_event_loop(GError **error) { @@ -1251,6 +1279,29 @@ teco_interface_cleanup(void) */ /** + * Called some time after processing an input event in order to show + * business. + * + * The delay avoids cursor flickering during normal typing. + * + * @fixme It would be nicer to set the cursor for the entire window, + * but that would apparently require another GtkEventBox, spanning everything. + */ +static gboolean +teco_interface_busy_timeout_cb(gpointer user_data) +{ + teco_interface_set_cursor(teco_interface.event_box_widget, "wait"); + return G_SOURCE_REMOVE; +} + +static void +teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data) +{ + /* It's only now safe to get the GdkWindow. */ + teco_interface_set_cursor(widget, "text"); +} + +/** * Called when the commandline widget is resized. * This should ensure that the caret jumps to the middle of the command line, * imitating the behaviour of the current Curses command line. @@ -1263,26 +1314,16 @@ teco_interface_cmdline_size_allocate_cb(GtkWidget *widget, CARET_SLOP | CARET_EVEN, allocation->width/2); } -static void -teco_interface_size_allocate_cb(GtkWidget *widget, - GdkRectangle *allocation, gpointer user_data) -{ - /* - * This especially ensures that the caret is visible after startup. - */ - teco_interface_ssm(SCI_SCROLLCARET, 0, 0); -} - static gboolean -teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data) +teco_interface_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data) { static gboolean recursed = FALSE; - g_autoptr(GError) error = NULL; #ifdef DEBUG - g_printf("KEY \"%s\" (%d) SHIFT=%d CNTRL=%d\n", - event->string, *event->string, - event->state & GDK_SHIFT_MASK, event->state & GDK_CONTROL_MASK); + if (event->type == GDK_KEY_PRESS) + g_printf("KEY \"%s\" (%d) SHIFT=%d CNTRL=%d\n", + event->key.string, *event->key.string, + event->key.state & GDK_SHIFT_MASK, event->key.state & GDK_CONTROL_MASK); #endif if (recursed) { @@ -1295,8 +1336,9 @@ teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer us * during execution, but the current implementation is * probably easier. */ - if (event->state & GDK_CONTROL_MASK && - gdk_keyval_to_upper(event->keyval) == GDK_KEY_C) + if (event->type == GDK_KEY_PRESS && + event->key.state & GDK_CONTROL_MASK && + gdk_keyval_to_upper(event->key.keyval) == GDK_KEY_C) /* * Handle asynchronous interruptions if CTRL+C is pressed. * If the execution thread is currently blocking, @@ -1305,20 +1347,26 @@ teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer us teco_interrupted = TRUE; else g_queue_push_tail(teco_interface.event_queue, - gdk_event_copy((GdkEvent *)event)); + gdk_event_copy(event)); return TRUE; } recursed = TRUE; + GSource *busy_timeout = g_timeout_source_new(500); /* ms */ + g_source_set_callback(busy_timeout, teco_interface_busy_timeout_cb, NULL, NULL); + g_source_attach(busy_timeout, NULL); + teco_memory_start_limiting(); - g_queue_push_tail(teco_interface.event_queue, gdk_event_copy((GdkEvent *)event)); + g_queue_push_tail(teco_interface.event_queue, gdk_event_copy(event)); GdkWindow *top_window = gdk_window_get_toplevel(gtk_widget_get_window(teco_interface.window)); do { + g_autoptr(GError) error = NULL; + /* * The event queue might be filled when pressing keys when SciTECO * is busy executing code. @@ -1334,10 +1382,37 @@ teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer us */ gdk_window_freeze_updates(top_window); + const teco_view_t *last_view = teco_interface_current_view; + sptr_t last_pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + teco_interrupted = FALSE; - teco_interface_handle_key_press(&event->key, &error); + switch (event->type) { + case GDK_KEY_PRESS: + teco_interface_handle_key_press(&event->key, &error); + break; + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + teco_interface_handle_mouse_button(&event->button, &error); + break; + case GDK_SCROLL: + teco_interface_handle_scroll(&event->scroll, &error); + break; + default: + g_assert_not_reached(); + } teco_interrupted = FALSE; + teco_interface_refresh(teco_interface_current_view != last_view); + /* + * Scintilla has been patched to avoid any automatic scrolling since that + * has been benchmarked to be a very costly operation. + * Instead we do it only once after every keypress. + */ + if (last_pos != teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0)) + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); + gdk_window_thaw_updates(top_window); if (g_error_matches(error, TECO_ERROR, TECO_ERROR_QUIT)) { @@ -1355,10 +1430,36 @@ teco_interface_key_pressed_cb(GtkWidget *widget, GdkEventKey *event, gpointer us teco_memory_stop_limiting(); + g_source_destroy(busy_timeout); + g_source_unref(busy_timeout); + teco_interface_set_cursor(teco_interface.event_box_widget, "text"); + recursed = FALSE; return TRUE; } +static void +teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong len, gpointer user_data) +{ + g_assert(len >= teco_interface.popup_prefix_len); + const teco_string_t insert = {str+teco_interface.popup_prefix_len, len-teco_interface.popup_prefix_len}; + teco_machine_t *machine = &teco_cmdline.machine.parent; + + const teco_view_t *last_view = teco_interface_current_view; + + /* + * NOTE: It shouldn't really be necessary to catch TECO_ERROR_QUIT here. + * A auto completion should never result in program termination. + */ + if (machine->current->insert_completion_cb && + !machine->current->insert_completion_cb(machine, &insert, NULL)) + return; + teco_interface_popup_clear(); + teco_interface_cmdline_update(&teco_cmdline); + + teco_interface_refresh(teco_interface_current_view != last_view); +} + static gboolean teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer user_data) { @@ -1374,7 +1475,7 @@ teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer close_event->key.window = gtk_widget_get_parent_window(widget); close_event->key.keyval = GDK_KEY_Close; - return teco_interface_key_pressed_cb(widget, &close_event->key, NULL); + return teco_interface_input_cb(widget, close_event, NULL); } static gboolean @@ -1386,5 +1487,5 @@ teco_interface_sigterm_handler(gpointer user_data) g_autoptr(GdkEvent) close_event = gdk_event_new(GDK_KEY_PRESS); close_event->key.keyval = GDK_KEY_Close; - return teco_interface_key_pressed_cb(teco_interface.window, &close_event->key, NULL); + return teco_interface_input_cb(teco_interface.window, close_event, NULL); } diff --git a/src/interface-gtk/view.c b/src/interface-gtk/view.c new file mode 100644 index 0000000..ef839d6 --- /dev/null +++ b/src/interface-gtk/view.c @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2012-2025 Robin Haberkorn + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <string.h> + +#include <glib.h> + +#include <gtk/gtk.h> + +#include <Scintilla.h> +#include <ScintillaWidget.h> + +#include "view.h" + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(ScintillaObject, g_object_unref) + +#define TECO_TYPE_VIEW teco_view_get_type() +G_DECLARE_FINAL_TYPE(TecoView, teco_view, TECO, VIEW, ScintillaObject) + +struct _TecoView { + ScintillaObject parent_instance; + /** current size allocation */ + GdkRectangle allocation; +}; + +G_DEFINE_TYPE(TecoView, teco_view, SCINTILLA_TYPE_OBJECT) + +static void +teco_view_scintilla_notify_cb(ScintillaObject *sci, gint iMessage, SCNotification *notify) +{ + teco_view_process_notify((teco_view_t *)TECO_VIEW(sci), notify); +} + +/** + * Called when the view is size allocated. + * + * This especially ensures that the caret is visible after startup and when + * opening files on specific lines. + * It's important to scroll the caret only when the size actually changes, + * so we do not interfere with mouse scrolling. + * That callback is invoked even if the size does not change, so that's why + * we have to store the current allocation in teco_view_t. + * Calling it once is unfortunately not sufficient since the window size + * can change during startup. + */ +static void +teco_view_size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation) +{ + /* chain to parent class */ + GTK_WIDGET_CLASS(teco_view_parent_class)->size_allocate(widget, allocation); + + TecoView *view = TECO_VIEW(widget); + + if (allocation->width == view->allocation.width && allocation->height == view->allocation.height) + return; + teco_view_ssm((teco_view_t *)view, SCI_SCROLLCARET, 0, 0); + memcpy(&view->allocation, allocation, sizeof(view->allocation)); +} + +teco_view_t * +teco_view_new(void) +{ + TecoView *ctx = TECO_VIEW(g_object_new(TECO_TYPE_VIEW, NULL)); + /* + * We don't want the object to be destroyed + * when it is removed from the vbox. + */ + g_object_ref_sink(ctx); + + scintilla_set_id(SCINTILLA(ctx), 0); + + gtk_widget_set_size_request(GTK_WIDGET(ctx), 500, 300); + + /* + * This disables mouse and key events on this view. + * For some strange reason, masking events on + * the event box does NOT work. + */ + gtk_widget_set_can_focus(GTK_WIDGET(ctx), FALSE); + gint events = gtk_widget_get_events(GTK_WIDGET(ctx)); + events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_SCROLL_MASK | + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK); + gtk_widget_set_events(GTK_WIDGET(ctx), events); + + return (teco_view_t *)ctx; +} + +static void +teco_view_class_init(TecoViewClass *klass) +{ + SCINTILLA_CLASS(klass)->notify = teco_view_scintilla_notify_cb; + GTK_WIDGET_CLASS(klass)->size_allocate = teco_view_size_allocate_cb; +} + +static void teco_view_init(TecoView *self) {} + +sptr_t +teco_view_ssm(teco_view_t *ctx, unsigned int iMessage, uptr_t wParam, sptr_t lParam) +{ + return scintilla_send_message(SCINTILLA(ctx), iMessage, wParam, lParam); +} + +void +teco_view_free(teco_view_t *ctx) +{ + g_object_unref(ctx); +} diff --git a/src/interface.c b/src/interface.c index 2973dd2..9ec1bed 100644 --- a/src/interface.c +++ b/src/interface.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -43,6 +43,9 @@ TECO_DEFINE_UNDO_CALL(teco_interface_ssm, unsigned int, uptr_t, sptr_t); TECO_DEFINE_UNDO_CALL(teco_interface_info_update_qreg, const teco_qreg_t *); TECO_DEFINE_UNDO_CALL(teco_interface_info_update_buffer, const teco_buffer_t *); +/** Last mouse event */ +teco_mouse_t teco_mouse = {0}; + typedef struct { teco_string_t str; gchar name[]; diff --git a/src/interface.h b/src/interface.h index 80da8d9..33b094b 100644 --- a/src/interface.h +++ b/src/interface.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -132,7 +132,9 @@ typedef enum { void teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize name_len, gboolean highlight); /** @pure */ -void teco_interface_popup_show(void); +void teco_interface_popup_show(gsize prefix_len); +/** @pure */ +void teco_interface_popup_scroll(void); /** @pure */ gboolean teco_interface_popup_is_shown(void); /** @pure */ @@ -141,6 +143,28 @@ void teco_interface_popup_clear(void); /** @pure */ gboolean teco_interface_is_interrupted(void); +typedef struct { + enum { + TECO_MOUSE_PRESSED = 1, + TECO_MOUSE_RELEASED, + TECO_MOUSE_SCROLLUP, + TECO_MOUSE_SCROLLDOWN + } type; + + guint x; /*< X-coordinate relative to view */ + guint y; /*< Y-coordinate relative to view */ + + gint button; /*< number of pressed mouse button or -1 */ + + enum { + TECO_MOUSE_SHIFT = (1 << 0), + TECO_MOUSE_CTRL = (1 << 1), + TECO_MOUSE_ALT = (1 << 2) + } mods; +} teco_mouse_t; + +extern teco_mouse_t teco_mouse; + /** @pure main entry point */ gboolean teco_interface_event_loop(GError **error); diff --git a/src/lexer.c b/src/lexer.c index c0c7847..1124b99 100644 --- a/src/lexer.c +++ b/src/lexer.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/lexer.h b/src/lexer.h index 87b0d0f..2b011be 100644 --- a/src/lexer.h +++ b/src/lexer.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -141,7 +141,7 @@ teco_process_options(gchar ***argv) g_autoptr(GError) error = NULL; - g_autoptr(GOptionContext) options = g_option_context_new("[--] [SCRIPT] [ARGUMENT...]"); + g_autoptr(GOptionContext) options = g_option_context_new("[--|-S] [SCRIPT] [ARGUMENT...]"); g_option_context_set_summary( options, @@ -168,27 +168,48 @@ teco_process_options(gchar ***argv) * in many situations. * It is also strictly required to make hash-bang lines like * #!/usr/bin/sciteco -m - * work. + * work (without additional --). */ g_option_context_set_strict_posix(options, TRUE); + /* + * The first unknown parameter will be left in argv and + * terminates option parsing (see above). + * This means we can use "-S" as an alternative to "--", + * that is always preserved and passed down to the macro. + */ + g_option_context_set_ignore_unknown_options(options, TRUE); + if (!g_option_context_parse_strv(options, argv, &error)) { g_fprintf(stderr, "Option parsing failed: %s\n", error->message); exit(EXIT_FAILURE); } - /* - * GOption will NOT remove "--" if followed by an - * option-argument, which may interfer with scripts - * doing their own option handling and interpreting "--". - * - * NOTE: This is still true if we're parsing in GNU-mode - * and "--" is not the first non-option argument as in - * sciteco foo -- -C bar. - */ - if ((*argv)[0] && !g_strcmp0((*argv)[1], "--")) + if ((*argv)[0] && !g_strcmp0((*argv)[1], "-S")) { + /* translate -S to --, this is always passed down */ + (*argv)[1][1] = '-'; + } else if ((*argv)[0] && !g_strcmp0((*argv)[1], "--")) { + /* + * GOption will NOT remove "--" if followed by an + * option-argument, which may interfer with scripts + * doing their own option handling and interpreting "--". + * Otherwise, GOption will always remove "--". + * + * NOTE: This is still true if we're parsing in GNU-mode + * and "--" is not the first non-option argument as in + * sciteco foo -- -C bar. + */ g_free(teco_strv_remove(*argv, 1)); + } else if ((*argv)[0] && (*argv)[1] && *(*argv)[1] == '-') { + /* + * GOption does not remove "--" if it is followed by "-", + * so if the first parameter starts with "-", we know it's + * not a known built-in parameter. + */ + g_fprintf(stderr, "Unknown option \"%s\"\n", (*argv)[1]); + exit(EXIT_FAILURE); + } gchar *mung_filename = NULL; diff --git a/src/memory.c b/src/memory.c index 653d1ef..ea056bc 100644 --- a/src/memory.c +++ b/src/memory.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/memory.h b/src/memory.h index 39f8319..ae7b506 100644 --- a/src/memory.h +++ b/src/memory.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/parser.c b/src/parser.c index 018e35f..295c635 100644 --- a/src/parser.c +++ b/src/parser.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -447,11 +447,14 @@ teco_state_stringbuilding_start_input(teco_machine_stringbuilding_t *ctx, gunich /* in cmdline.c */ gboolean teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); +gboolean teco_state_stringbuilding_insert_completion(teco_machine_stringbuilding_t *ctx, const teco_string_t *str, GError **error); TECO_DEFINE_STATE(teco_state_stringbuilding_start, .is_start = TRUE, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) - teco_state_stringbuilding_start_process_edit_cmd + teco_state_stringbuilding_start_process_edit_cmd, + .insert_completion_cb = (teco_state_insert_completion_cb_t) + teco_state_stringbuilding_insert_completion ); static teco_state_t * diff --git a/src/parser.h b/src/parser.h index 7ca5ab3..fe8e764 100644 --- a/src/parser.h +++ b/src/parser.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -110,6 +110,7 @@ typedef gboolean (*teco_state_refresh_cb_t)(teco_machine_t *ctx, GError **error) typedef gboolean (*teco_state_end_of_macro_cb_t)(teco_machine_t *ctx, GError **error); typedef gboolean (*teco_state_process_edit_cmd_cb_t)(teco_machine_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); +typedef gboolean (*teco_state_insert_completion_cb_t)(teco_machine_t *ctx, const teco_string_t *str, GError **error); typedef enum { TECO_KEYMACRO_MASK_START = (1 << 0), @@ -187,6 +188,19 @@ struct teco_state_t { teco_state_process_edit_cmd_cb_t process_edit_cmd_cb; /** + * Insert completion after clicking an entry in the popup + * window. + * + * All implementations of this method are currently + * defined in cmdline.c. + * + * It can be NULL if not required. + * + * @fixme Perhaps move all implementations to interface.c. + */ + teco_state_insert_completion_cb_t insert_completion_cb; + + /** * Whether this state is a start state (i.e. not within any * escape sequence etc.). * This is separate of TECO_KEYMACRO_MASK_START which is set @@ -241,11 +255,12 @@ gboolean teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent #define TECO_DEFINE_STATE(NAME, ...) \ /** @ingroup states */ \ teco_state_t NAME = { \ - .initial_cb = NULL, /* do nothing */ \ + .initial_cb = NULL, /* do nothing */ \ .input_cb = (teco_state_input_cb_t)NAME##_input, /* always required */ \ - .refresh_cb = NULL, /* do nothing */ \ + .refresh_cb = NULL, /* do nothing */ \ .end_of_macro_cb = teco_state_end_of_macro, \ .process_edit_cmd_cb = teco_state_process_edit_cmd, \ + .insert_completion_cb = NULL, /* do nothing */ \ .is_start = FALSE, \ .keymacro_mask = TECO_KEYMACRO_MASK_DEFAULT, \ .style = SCE_SCITECO_DEFAULT, \ @@ -552,7 +567,10 @@ teco_state_t *teco_state_expectstring_input(teco_machine_main_t *ctx, gunichar c gboolean teco_state_expectstring_refresh(teco_machine_main_t *ctx, GError **error); /* in cmdline.c */ -gboolean teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); +gboolean teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, + gunichar key, GError **error); +gboolean teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, + GError **error); /** * @interface TECO_DEFINE_STATE_EXPECTSTRING @@ -577,6 +595,8 @@ gboolean teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco .refresh_cb = (teco_state_refresh_cb_t)teco_state_expectstring_refresh, \ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ teco_state_expectstring_process_edit_cmd, \ + .insert_completion_cb = (teco_state_insert_completion_cb_t) \ + teco_state_expectstring_insert_completion, \ .keymacro_mask = TECO_KEYMACRO_MASK_STRING, \ .style = SCE_SCITECO_STRING, \ .expectstring.string_building = TRUE, \ @@ -591,6 +611,7 @@ gboolean teco_state_expectfile_process(teco_machine_main_t *ctx, const teco_stri /* in cmdline.c */ gboolean teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); +gboolean teco_state_expectfile_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error); /** * @interface TECO_DEFINE_STATE_EXPECTFILE @@ -601,12 +622,15 @@ gboolean teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_m TECO_DEFINE_STATE_EXPECTSTRING(NAME, \ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ teco_state_expectfile_process_edit_cmd, \ + .insert_completion_cb = (teco_state_insert_completion_cb_t) \ + teco_state_expectfile_insert_completion, \ .expectstring.process_cb = teco_state_expectfile_process, \ ##__VA_ARGS__ \ ) /* in cmdline.c */ gboolean teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); +gboolean teco_state_expectdir_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error); /** * @interface TECO_DEFINE_STATE_EXPECTDIR @@ -617,5 +641,7 @@ gboolean teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_ma TECO_DEFINE_STATE_EXPECTFILE(NAME, \ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ teco_state_expectdir_process_edit_cmd, \ + .insert_completion_cb = (teco_state_insert_completion_cb_t) \ + teco_state_expectdir_insert_completion, \ ##__VA_ARGS__ \ ) diff --git a/src/qreg-commands.c b/src/qreg-commands.c index 0a64b2f..ebf6caa 100644 --- a/src/qreg-commands.c +++ b/src/qreg-commands.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/qreg-commands.h b/src/qreg-commands.h index d999587..6dbd1c4 100644 --- a/src/qreg-commands.h +++ b/src/qreg-commands.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -37,7 +37,10 @@ gboolean teco_state_expectqreg_initial(teco_machine_main_t *ctx, GError **error) teco_state_t *teco_state_expectqreg_input(teco_machine_main_t *ctx, gunichar chr, GError **error); /* in cmdline.c */ -gboolean teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); +gboolean teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, + gunichar key, GError **error); +gboolean teco_state_expectqreg_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, + GError **error); /** * @interface TECO_DEFINE_STATE_EXPECTQREG @@ -56,6 +59,8 @@ gboolean teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_m .initial_cb = (teco_state_initial_cb_t)teco_state_expectqreg_initial, \ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ teco_state_expectqreg_process_edit_cmd, \ + .insert_completion_cb = (teco_state_insert_completion_cb_t) \ + teco_state_expectqreg_insert_completion, \ .style = SCE_SCITECO_QREG, \ .expectqreg.type = TECO_QREG_REQUIRED, \ .expectqreg.got_register_cb = NAME##_got_register, /* always required */ \ @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1445,11 +1445,15 @@ teco_state_qregspec_start_input(teco_machine_qregspec_t *ctx, gunichar chr, GErr } /* in cmdline.c */ -gboolean teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); +gboolean teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, + gunichar key, GError **error); +gboolean teco_state_qregspec_insert_completion(teco_machine_qregspec_t *ctx, const teco_string_t *str, + GError **error); TECO_DEFINE_STATE(teco_state_qregspec_start, .is_start = TRUE, - .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd, + .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_qregspec_insert_completion ); static teco_state_t * @@ -1585,9 +1589,12 @@ teco_state_qregspec_string_input(teco_machine_qregspec_t *ctx, gunichar chr, GEr /* in cmdline.c */ gboolean teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); +gboolean teco_state_qregspec_string_insert_completion(teco_machine_qregspec_t *ctx, const teco_string_t *str, + GError **error); TECO_DEFINE_STATE(teco_state_qregspec_string, - .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_string_process_edit_cmd + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_string_process_edit_cmd, + .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_qregspec_string_insert_completion ); /** @static @memberof teco_machine_qregspec_t */ @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/rb3str.c b/src/rb3str.c index d51ac5d..276b624 100644 --- a/src/rb3str.c +++ b/src/rb3str.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -143,7 +143,7 @@ teco_rb3str_auto_complete(teco_rb3str_tree_t *tree, gboolean case_sensitive, cur->key.data, cur->key.len, FALSE); } - teco_interface_popup_show(); + teco_interface_popup_show(str_len); } return prefixed_entries == 1; diff --git a/src/rb3str.h b/src/rb3str.h index adf5f89..466cf90 100644 --- a/src/rb3str.h +++ b/src/rb3str.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -413,7 +413,7 @@ teco_state_edit_file_initial(teco_machine_main_t *ctx, GError **error) strlen(filename), cur == teco_ring_current); } - teco_interface_popup_show(); + teco_interface_popup_show(0); } else if (id > 0) { allow_filename = FALSE; if (!teco_current_doc_undo_edit(error) || @@ -480,6 +480,10 @@ teco_state_edit_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GE * area. * Naturally this only has any effect in interactive * mode. + * Note that you can also click on these entries \(em + * if mouse support is enabled \(em to immediately switch + * to any file in the buffer ring just like with any + * other popup. * * <file> may also be a glob pattern, in which case * all regular files matching the pattern are opened/edited. @@ -577,3 +581,61 @@ teco_state_save_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GE * characters are enabled by default. */ TECO_DEFINE_STATE_EXPECTFILE(teco_state_save_file); + +/*$ EF close + * [bool]EF -- Remove buffer from ring + * -EF + * :EF + * + * Removes buffer from buffer ring, effectively + * closing it. + * If the buffer is dirty (modified), EF will yield + * an error. + * <bool> may be a specified to enforce closing dirty + * buffers. + * If it is a Failure condition boolean (negative), + * the buffer will be closed unconditionally. + * If <bool> is absent, the sign prefix (1 or -1) will + * be implied, so \(lq-EF\(rq will always close the buffer. + * + * When colon-modified, <bool> is ignored and \fBEF\fP + * will save the buffer before closing. + * The file is always written, unlike \(lq:EX\(rq which + * saves only dirty buffers. + * This can fail of course, e.g. when called on the unnamed + * buffer. + * + * It is noteworthy that EF will be executed immediately in + * interactive mode but can be rubbed out at a later time + * to reopen the file. + * Closed files are kept in memory until the command line + * is terminated. + */ +void +teco_state_ecommand_close(teco_machine_main_t *ctx, GError **error) +{ + if (teco_qreg_current) { + const teco_string_t *name = &teco_qreg_current->head.name; + g_autofree gchar *name_printable = teco_string_echo(name->data, name->len); + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Q-Register \"%s\" currently edited", name_printable); + return; + } + + if (teco_machine_main_eval_colon(ctx) > 0) { + if (!teco_buffer_save(teco_ring_current, NULL, error)) + return; + } else { + teco_int_t v; + if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) + return; + if (teco_is_failure(v) && teco_ring_current->dirty) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Buffer \"%s\" is dirty", + teco_ring_current->filename ? : "(Unnamed)"); + return; + } + } + + teco_ring_close(error); +} @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -100,6 +100,8 @@ void teco_ring_cleanup(void); TECO_DECLARE_STATE(teco_state_edit_file); TECO_DECLARE_STATE(teco_state_save_file); +void teco_state_ecommand_close(teco_machine_main_t *ctx, GError **error); + /* * Helper functions applying to any current * document (whether a buffer or QRegister). diff --git a/src/sciteco.h b/src/sciteco.h index 7fe09d4..4868303 100644 --- a/src/sciteco.h +++ b/src/sciteco.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -89,7 +89,7 @@ enum { TECO_ED_AUTOCASEFOLD = (1 << 3), TECO_ED_AUTOEOL = (1 << 4), TECO_ED_HOOKS = (1 << 5), - //TECO_ED_MOUSEKEY = (1 << 6), + TECO_ED_MOUSEKEY = (1 << 6), TECO_ED_SHELLEMU = (1 << 7), TECO_ED_OSC52 = (1 << 8), TECO_ED_ICONS = (1 << 9) diff --git a/src/search.c b/src/search.c index 1945f5c..3b2ebe3 100644 --- a/src/search.c +++ b/src/search.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/search.h b/src/search.h index 40ab4d8..621fdd1 100644 --- a/src/search.h +++ b/src/search.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/spawn.c b/src/spawn.c index fb7a946..e44ecc4 100644 --- a/src/spawn.c +++ b/src/spawn.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -129,11 +129,13 @@ teco_parse_shell_command_line(const gchar *cmdline, GError **error) teco_string_t comspec; if (!reg->vtable->get_string(reg, &comspec.data, &comspec.len, NULL, error)) return NULL; + if (teco_string_contains(&comspec, '\0')) { + teco_string_clear(&comspec); + teco_error_qregcontainsnull_set(error, "$COMSPEC", 8, FALSE); + return NULL; + } argv = g_new(gchar *, 5); - /* - * FIXME: What if $COMSPEC contains null-bytes? - */ argv[0] = comspec.data; argv[1] = g_strdup("/q"); argv[2] = g_strdup("/c"); @@ -148,11 +150,13 @@ teco_parse_shell_command_line(const gchar *cmdline, GError **error) teco_string_t shell; if (!reg->vtable->get_string(reg, &shell.data, &shell.len, NULL, error)) return NULL; + if (teco_string_contains(&shell, '\0')) { + teco_string_clear(&shell); + teco_error_qregcontainsnull_set(error, "$SHELL", 6, FALSE); + return NULL; + } argv = g_new(gchar *, 4); - /* - * FIXME: What if $SHELL contains null-bytes? - */ argv[0] = shell.data; argv[1] = g_strdup("-c"); argv[2] = g_strdup(cmdline); diff --git a/src/spawn.h b/src/spawn.h index 312de6e..ef210e9 100644 --- a/src/spawn.h +++ b/src/spawn.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/string-utils.c b/src/string-utils.c index b284760..10e34a8 100644 --- a/src/string-utils.c +++ b/src/string-utils.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/string-utils.h b/src/string-utils.h index ebe25d5..2491d07 100644 --- a/src/string-utils.h +++ b/src/string-utils.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/symbols.c b/src/symbols.c index 7198639..7dcc601 100644 --- a/src/symbols.c +++ b/src/symbols.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -155,7 +155,7 @@ teco_symbol_list_auto_complete(teco_symbol_list_t *ctx, const gchar *symbol, tec strlen(entry->data), FALSE); } - teco_interface_popup_show(); + teco_interface_popup_show(symbol_len); } return glist_len == 1; @@ -252,7 +252,10 @@ teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, const teco_string_t } /* in cmdline.c */ -gboolean teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); +gboolean teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, + gunichar key, GError **error); +gboolean teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, + GError **error); /*$ ES scintilla message * -- Send Scintilla message @@ -332,6 +335,7 @@ gboolean teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, */ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_scintilla_symbols, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_scintilla_symbols_process_edit_cmd, + .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_scintilla_symbols_insert_completion, .expectstring.last = FALSE ); diff --git a/src/symbols.h b/src/symbols.h index 0325d9d..1d0af12 100644 --- a/src/symbols.h +++ b/src/symbols.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2024 Robin Haberkorn + * Copyright (C) 2012-2025 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by |