aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/interface-curses/interface.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/interface-curses/interface.c')
-rw-r--r--src/interface-curses/interface.c437
1 files changed, 385 insertions, 52 deletions
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