aboutsummaryrefslogtreecommitdiff
path: root/src/sdtui.c
diff options
context:
space:
mode:
authorPřemysl Janouch <p@janouch.name>2018-09-22 14:09:42 +0200
committerPřemysl Janouch <p@janouch.name>2018-09-23 06:38:03 +0200
commit942bda7db4b7b9e5bae6ad1ad81295b1f346a9f6 (patch)
tree2602530b371ee3449bfc9233709bb6fe9f82e5fa /src/sdtui.c
parentb0d3b2dcb5c02fedb0f4d95e8337f7829015a7bd (diff)
downloadtdv-942bda7db4b7b9e5bae6ad1ad81295b1f346a9f6.tar.gz
tdv-942bda7db4b7b9e5bae6ad1ad81295b1f346a9f6.tar.xz
tdv-942bda7db4b7b9e5bae6ad1ad81295b1f346a9f6.zip
Use XCB and SelectSelectionInput instead of GTK+
As it happens, there is no real need to constantly poll for changes, since XFixes can inform us of updates as they happen. With GTK+ gone we've got dependencies and error handling under control. XCB is a truly awful thing to learn, though. Our method will never work on Wayland or Windows, so we don't miss out on anything by abandoning the huge toolkit.
Diffstat (limited to 'src/sdtui.c')
-rw-r--r--src/sdtui.c340
1 files changed, 239 insertions, 101 deletions
diff --git a/src/sdtui.c b/src/sdtui.c
index 5ccef5d..2b49373 100644
--- a/src/sdtui.c
+++ b/src/sdtui.c
@@ -1,7 +1,7 @@
/*
* StarDict terminal UI
*
- * Copyright (c) 2013 - 2016, Přemysl Janouch <p@janouch.name>
+ * Copyright (c) 2013 - 2018, Přemysl Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
@@ -43,10 +43,6 @@
#include "stardict.h"
#include "utils.h"
-#ifdef WITH_GTK
-#include <gtk/gtk.h>
-#endif // WITH_GTK
-
#define CTRL_KEY(x) ((x) - 'A' + 1)
#define TOP_BAR_CUTOFF 2 ///< How many lines are reserved on top
@@ -213,6 +209,7 @@ struct application
guint center_search : 1; ///< Whether to center the search
guint underline_last : 1; ///< Underline the last definition
guint hl_prefix : 1; ///< Highlight the common prefix
+ guint watch_x11_sel : 1; ///< Requested X11 selection watcher
guint32 top_position; ///< Index of the topmost dict. entry
guint top_offset; ///< Offset into the top entry
@@ -226,10 +223,6 @@ struct application
gfloat division; ///< Position of the division column
- guint selection_timer; ///< Selection watcher timeout timer
- gint selection_interval; ///< Selection watcher timer interval
- gchar * selection_contents; ///< Selection contents
-
struct attrs attrs[ATTRIBUTE_COUNT];
};
@@ -398,18 +391,6 @@ app_reload_view (Application *self)
g_object_unref (iterator);
}
-#ifdef WITH_GTK
-static gboolean on_selection_timer (gpointer data);
-
-static void
-rearm_selection_watcher (Application *self)
-{
- if (self->selection_interval > 0)
- self->selection_timer = g_timeout_add
- (self->selection_interval, on_selection_timer, self);
-}
-#endif // WITH_GTK
-
/// Load configuration for a color using a subset of git config colors.
static void
app_load_color (Application *self, GKeyFile *kf, const gchar *name, int id)
@@ -468,14 +449,8 @@ app_load_config_values (Application *self, GKeyFile *kf)
app_load_bool (kf, "underline-last", self->underline_last);
self->hl_prefix =
app_load_bool (kf, "hl-common-prefix", self->hl_prefix);
-
- guint64 timer;
- const gchar *watch_selection = "watch-selection";
- if (app_load_bool (kf, watch_selection, FALSE))
- self->selection_interval = 500;
- else if ((timer = g_key_file_get_uint64
- (kf, "Settings", watch_selection, NULL)) && timer <= G_MAXINT)
- self->selection_interval = timer;
+ self->watch_x11_sel =
+ app_load_bool (kf, "watch-selection", self->watch_x11_sel);
#define XX(name, config, fg_, bg_, attrs_) \
app_load_color (self, kf, config, ATTRIBUTE_ ## name);
@@ -613,9 +588,6 @@ static void
app_init (Application *self, char **filenames)
{
self->loop = NULL;
- self->selection_interval = -1;
- self->selection_timer = 0;
- self->selection_contents = NULL;
self->tk = NULL;
self->tk_timer = 0;
@@ -624,6 +596,7 @@ app_init (Application *self, char **filenames)
self->center_search = TRUE;
self->underline_last = TRUE;
self->hl_prefix = TRUE;
+ self->watch_x11_sel = FALSE;
self->top_position = 0;
self->top_offset = 0;
@@ -660,18 +633,7 @@ app_init (Application *self, char **filenames)
exit (EXIT_FAILURE);
}
- // Now we have settings for the clipboard watcher, we can arm the timer
-#ifdef WITH_GTK
- if (gtk_init_check (0, NULL))
- {
- // So that we set the input only when it actually changes
- GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
- self->selection_contents = gtk_clipboard_wait_for_text (clipboard);
- rearm_selection_watcher (self);
- }
- else
-#endif // WITH_GTK
- self->loop = g_main_loop_new (NULL, FALSE);
+ self->loop = g_main_loop_new (NULL, FALSE);
// Dictionaries given on the command line override the configuration
if (*filenames)
@@ -744,10 +706,6 @@ app_destroy (Application *self)
if (self->tk_timer)
g_source_remove (self->tk_timer);
- if (self->selection_timer)
- g_source_remove (self->selection_timer);
- g_free (self->selection_contents);
-
g_ptr_array_free (self->entries, TRUE);
g_free (self->search_label);
g_array_free (self->input, TRUE);
@@ -760,24 +718,14 @@ app_destroy (Application *self)
static void
app_run (Application *self)
{
- if (self->loop)
- g_main_loop_run (self->loop);
-#ifdef WITH_GTK
- else
- gtk_main ();
-#endif // WITH_GTK
+ g_main_loop_run (self->loop);
}
/// Quit the main event dispatch loop.
static void
app_quit (Application *self)
{
- if (self->loop)
- g_main_loop_quit (self->loop);
-#ifdef WITH_GTK
- else
- gtk_main_quit ();
-#endif // WITH_GTK
+ g_main_loop_quit (self->loop);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -1023,7 +971,7 @@ app_show_help (Application *self)
{
PROJECT_NAME " " PROJECT_VERSION,
_("Terminal UI for StarDict dictionaries"),
- "Copyright (c) 2013 - 2016, Přemysl Janouch",
+ "Copyright (c) 2013 - 2018, Přemysl Janouch",
"",
_("Type to search")
};
@@ -1835,6 +1783,215 @@ install_winch_handler (void)
sigaction (SIGWINCH, &act, &oldact);
}
+// --- X11 selection watcher ---------------------------------------------------
+
+#ifdef WITH_X11
+
+#include <xcb/xcb.h>
+#include <xcb/xfixes.h>
+
+/// Data relating to one entry within the dictionary.
+typedef struct selection_watch SelectionWatch;
+
+struct selection_watch
+{
+ Application *app;
+ xcb_connection_t *X;
+ const xcb_query_extension_reply_t *xfixes;
+
+ guint watch; ///< X11 connection watcher
+ xcb_window_t wid; ///< Withdrawn communications window
+ xcb_atom_t atom_utf8_string; ///< UTF8_STRING
+ xcb_timestamp_t in_progress; ///< Timestamp of last processed event
+};
+
+static gboolean
+is_xcb_ok (xcb_connection_t *X)
+{
+ int xcb_error = xcb_connection_has_error (X);
+ if (xcb_error)
+ {
+ g_warning (_("X11 connection failed (error code %d)"), xcb_error);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static xcb_atom_t
+resolve_atom (xcb_connection_t *X, const char *atom)
+{
+ xcb_intern_atom_reply_t *iar = xcb_intern_atom_reply (X,
+ xcb_intern_atom (X, false, strlen (atom), atom), NULL);
+ xcb_atom_t result = iar ? iar->atom : XCB_NONE;
+ free (iar);
+ return result;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+app_set_input (Application *self, const gchar *text, gsize text_len);
+
+static void
+on_selection_text_received (SelectionWatch *self, const gchar *text);
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static gboolean
+read_utf8_property (SelectionWatch *self, xcb_window_t wid, xcb_atom_t property,
+ GString *buf)
+{
+ guint32 offset = 0;
+ gboolean loop = TRUE, ok = TRUE;
+ while (ok && loop)
+ {
+ xcb_get_property_reply_t *gpr = xcb_get_property_reply (self->X,
+ xcb_get_property (self->X, FALSE /* delete */, wid,
+ property, XCB_GET_PROPERTY_TYPE_ANY, offset, 0x7fff), NULL);
+
+ if (!gpr || gpr->type != self->atom_utf8_string || gpr->format != 8)
+ ok = FALSE;
+ else
+ {
+ int len = xcb_get_property_value_length (gpr);
+ g_string_append_len (buf, xcb_get_property_value (gpr), len);
+ offset += len >> 2;
+ loop = gpr->bytes_after > 0;
+ }
+
+ free (gpr);
+ }
+ return ok;
+}
+
+static void
+process_x11_event (SelectionWatch *self, xcb_generic_event_t *event)
+{
+ xcb_generic_error_t *err = NULL;
+ int event_code = event->response_type & 0x7f;
+ if (event_code == 0)
+ {
+ err = (xcb_generic_error_t *) event;
+ g_warning (_("X11 request error (%d, major %d, minor %d)"),
+ err->error_code, err->major_code, err->minor_code);
+ }
+ else if (event_code ==
+ self->xfixes->first_event + XCB_XFIXES_SELECTION_NOTIFY)
+ {
+ xcb_xfixes_selection_notify_event_t *e =
+ (xcb_xfixes_selection_notify_event_t *) event;
+
+ // Not checking whether we should give up when this interrupts our
+ // current retrieval attempt--the timeout solves this
+ if (e->owner == XCB_NONE)
+ return;
+
+ // Don't try to process two things at once. Each request gets a few
+ // seconds to finish, then we move on, hoping that a property race
+ // doesn't commence. Ideally we'd set up a separate queue for these
+ // skipped requests and process them later.
+ if (self->in_progress != 0 && e->timestamp - self->in_progress < 5000)
+ return;
+
+ // ICCCM says we should ensure the named property doesn't exist
+ (void) xcb_delete_property (self->X, self->wid, XCB_ATOM_PRIMARY);
+
+ (void) xcb_convert_selection (self->X, self->wid, e->selection,
+ self->atom_utf8_string, XCB_ATOM_PRIMARY, e->timestamp);
+ self->in_progress = e->timestamp;
+ }
+ else if (event_code == XCB_SELECTION_NOTIFY)
+ {
+ xcb_selection_notify_event_t *e =
+ (xcb_selection_notify_event_t *) event;
+ if (e->time != self->in_progress)
+ return;
+
+ self->in_progress = 0;
+ if (e->property == XCB_ATOM_NONE)
+ return;
+
+ GString *buf = g_string_new (NULL);
+ if (read_utf8_property (self, e->requestor, e->property, buf))
+ on_selection_text_received (self, buf->str);
+ g_string_free (buf, TRUE);
+
+ (void) xcb_delete_property (self->X, self->wid, e->property);
+ }
+}
+
+static gboolean
+process_x11 (G_GNUC_UNUSED GIOChannel *source,
+ G_GNUC_UNUSED GIOCondition condition, gpointer data)
+{
+ SelectionWatch *self = data;
+
+ xcb_generic_event_t *event;
+ while ((event = xcb_poll_for_event (self->X)))
+ {
+ process_x11_event (self, event);
+ free (event);
+ }
+ (void) xcb_flush (self->X);
+ return is_xcb_ok (self->X);
+}
+
+static void
+selection_watch_init (SelectionWatch *self, Application *app)
+{
+ memset (self, 0, sizeof *self);
+ if (!app->watch_x11_sel)
+ return;
+ self->app = app;
+
+ int which_screen = -1;
+ self->X = xcb_connect (NULL, &which_screen);
+ if (!is_xcb_ok (self->X))
+ return;
+
+ // Most modern applications support this, though an XCB_ATOM_STRING
+ // fallback might be good to add (COMPOUND_TEXT is complex)
+ g_return_if_fail
+ ((self->atom_utf8_string = resolve_atom (self->X, "UTF8_STRING")));
+
+ self->xfixes = xcb_get_extension_data (self->X, &xcb_xfixes_id);
+ g_return_if_fail (self->xfixes->present);
+
+ (void) xcb_xfixes_query_version_unchecked (self->X,
+ XCB_XFIXES_MAJOR_VERSION, XCB_XFIXES_MINOR_VERSION);
+
+ const xcb_setup_t *setup = xcb_get_setup (self->X);
+ xcb_screen_iterator_t setup_iter = xcb_setup_roots_iterator (setup);
+ while (which_screen--)
+ xcb_screen_next (&setup_iter);
+
+ xcb_screen_t *screen = setup_iter.data;
+ self->wid = xcb_generate_id (self->X);
+ (void) xcb_create_window (self->X, screen->root_depth, self->wid,
+ screen->root, 0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
+ screen->root_visual, 0, NULL);
+
+ (void) xcb_xfixes_select_selection_input (self->X, self->wid,
+ XCB_ATOM_PRIMARY, XCB_XFIXES_SELECTION_EVENT_MASK_SET_SELECTION_OWNER |
+ XCB_XFIXES_SELECTION_EVENT_MASK_SELECTION_WINDOW_DESTROY |
+ XCB_XFIXES_SELECTION_EVENT_MASK_SELECTION_CLIENT_CLOSE);
+
+ (void) xcb_flush (self->X);
+ self->watch = g_io_add_watch (g_io_channel_unix_new
+ (xcb_get_file_descriptor (self->X)), G_IO_IN, process_x11, self);
+}
+
+static void
+selection_watch_destroy (SelectionWatch *self)
+{
+ if (self->X)
+ xcb_disconnect (self->X);
+ if (self->watch)
+ g_source_remove (self->watch);
+}
+
+#endif // WITH_X11
+
// --- Initialisation, event handling ------------------------------------------
static gboolean on_stdin_input_timeout (gpointer data);
@@ -1903,7 +2060,7 @@ on_terminated (gpointer user_data)
return TRUE;
}
-#ifdef WITH_GTK
+#ifdef WITH_X11
static void
app_set_input (Application *self, const gchar *text, gsize text_len)
{
@@ -1935,47 +2092,19 @@ app_set_input (Application *self, const gchar *text, gsize text_len)
}
static void
-on_selection_text_received (G_GNUC_UNUSED GtkClipboard *clipboard,
- const gchar *text, gpointer data)
-{
- Application *app = data;
- rearm_selection_watcher (app);
-
- if (text)
- {
- // Strip ASCII whitespace: this is compatible with UTF-8
- while (g_ascii_isspace (*text))
- text++;
- gsize text_len = strlen (text);
- while (text_len && g_ascii_isspace (text[text_len - 1]))
- text_len--;
-
- if (app->selection_contents &&
- !strncmp (app->selection_contents, text, text_len))
- return;
-
- g_free (app->selection_contents);
- app->selection_contents = g_strndup (text, text_len);
- app_set_input (app, text, text_len);
- }
- else if (app->selection_contents)
- {
- g_free (app->selection_contents);
- app->selection_contents = NULL;
- }
-}
-
-static gboolean
-on_selection_timer (gpointer data)
+on_selection_text_received (SelectionWatch *self, const gchar *text)
{
- Application *app = data;
- GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
- gtk_clipboard_request_text (clipboard, on_selection_text_received, app);
+ // Strip ASCII whitespace: this is compatible with UTF-8
+ while (g_ascii_isspace (*text))
+ text++;
+ gsize text_len = strlen (text);
+ while (text_len && g_ascii_isspace (text[text_len - 1]))
+ text_len--;
- app->selection_timer = 0;
- return FALSE;
+ if (text_len)
+ app_set_input (self->app, text, text_len);
}
-#endif // WITH_GTK
+#endif // WITH_X11
static void
log_handler_curses (Application *self, const gchar *message)
@@ -2091,7 +2220,7 @@ G_GNUC_END_IGNORE_DEPRECATIONS
// g_unix_signal_add() cannot handle SIGWINCH
install_winch_handler ();
- // GtkClipboard can internally issue some rather disruptive warnings
+ // Avoid disruptive warnings
g_log_set_default_handler (log_handler, &app);
// Message loop
@@ -2102,8 +2231,17 @@ G_GNUC_END_IGNORE_DEPRECATIONS
guint watch_winch = g_io_add_watch (g_io_channel_unix_new (g_winch_pipe[0]),
G_IO_IN, process_winch_input, &app);
+#ifdef WITH_X11
+ SelectionWatch sw;
+ selection_watch_init (&sw, &app);
+#endif // WITH_X11
+
app_run (&app);
+#ifdef WITH_X11
+ selection_watch_destroy (&sw);
+#endif // WITH_X11
+
g_source_remove (watch_term);
g_source_remove (watch_int);
g_source_remove (watch_stdin);