aboutsummaryrefslogtreecommitdiff
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
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.
-rw-r--r--CMakeLists.txt25
-rw-r--r--LICENSE2
-rw-r--r--README.adoc8
-rw-r--r--config.h.in2
-rw-r--r--src/sdtui.c340
5 files changed, 257 insertions, 120 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4990a51..c13754f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -74,19 +74,18 @@ else (USE_SYSTEM_TERMO)
set (Termo_LIBRARIES termo-static)
endif (USE_SYSTEM_TERMO)
-# We actually don't care about the specific version
-pkg_search_module (gtk gtk+-3.0 gtk+-2.0)
-option (WITH_GTK "Compile with GTK+ support" ${gtk_FOUND})
-
-if (WITH_GTK)
- if (NOT gtk_FOUND)
- message (FATAL_ERROR "GTK+ library not found")
- endif (NOT gtk_FOUND)
-
- list (APPEND dependencies_INCLUDE_DIRS ${gtk_INCLUDE_DIRS})
- list (APPEND dependencies_LIBRARY_DIRS ${gtk_LIBRARY_DIRS})
- list (APPEND dependencies_LIBRARIES ${gtk_LIBRARIES})
-endif (WITH_GTK)
+pkg_check_modules (xcb xcb xcb-xfixes)
+option (WITH_X11 "Compile with X11 selection support using XCB" ${xcb_FOUND})
+
+if (WITH_X11)
+ if (NOT xcb_FOUND)
+ message (FATAL_ERROR "XCB not found")
+ endif (NOT xcb_FOUND)
+
+ list (APPEND dependencies_INCLUDE_DIRS ${xcb_INCLUDE_DIRS})
+ list (APPEND dependencies_LIBRARY_DIRS ${xcb_LIBRARY_DIRS})
+ list (APPEND dependencies_LIBRARIES ${xcb_LIBRARIES})
+endif (WITH_X11)
link_directories (${dependencies_LIBRARY_DIRS})
include_directories (${ZLIB_INCLUDE_DIRS} ${icu_INCLUDE_DIRS}
diff --git a/LICENSE b/LICENSE
index 24a508c..5f72054 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-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.
diff --git a/README.adoc b/README.adoc
index 3cbd303..cfad569 100644
--- a/README.adoc
+++ b/README.adoc
@@ -7,7 +7,7 @@ dictionary software of this kind, GUI or not, and thus decided to write my own.
The project is covered by a permissive license, unlike vast majority of other
similar projects, and can serve as a base for implementing other dictionary
-software. I wasn't able to reuse _anything_.
+software. I wasn't able to reuse _anything_ for StarDict.
Further Development
-------------------
@@ -32,12 +32,12 @@ Building and Running
--------------------
Build dependencies: CMake, pkg-config, xsltproc, docbook-xsl +
Runtime dependencies: ncursesw, zlib, ICU, termo (included),
- glib-2.0, pango, gtk+ (optional, any version)
+ glib-2.0, pango, xcb and xcb-xfixes (optional)
$ git clone --recursive https://git.janouch.name/p/sdtui.git
$ mkdir sdtui/build
$ cd sdtui/build
- $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug -DWITH_GTK=ON
+ $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug -DWITH_X11=ON
$ make
To install the application, you can do either the usual:
@@ -85,7 +85,7 @@ odd = 16 255
The `watch-selection` option makes the application watch the X11 primary
selection for changes and automatically search for selected text.
-This feature requires GTK+ and it will never work on Wayland by its design.
+This feature requires XCB and it will never work on Wayland by its design.
You can also set up some dictionaries to be loaded at startup automatically:
diff --git a/config.h.in b/config.h.in
index 40c9d79..4a263f2 100644
--- a/config.h.in
+++ b/config.h.in
@@ -8,7 +8,7 @@
#define GETTEXT_PACKAGE PROJECT_NAME
#define GETTEXT_DIRNAME "${CMAKE_INSTALL_PREFIX}/share/locale"
-#cmakedefine WITH_GTK
+#cmakedefine WITH_X11
#cmakedefine HAVE_RESIZETERM
#endif // ! CONFIG_H
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);