Compare commits

..

No commits in common. "fcc0c3ef2d90603776c915335ed8bacf678cb278" and "b0d3b2dcb5c02fedb0f4d95e8337f7829015a7bd" have entirely different histories.

5 changed files with 157 additions and 368 deletions

View File

@ -74,18 +74,19 @@ else (USE_SYSTEM_TERMO)
set (Termo_LIBRARIES termo-static)
endif (USE_SYSTEM_TERMO)
pkg_check_modules (xcb xcb xcb-xfixes)
option (WITH_X11 "Compile with X11 selection support using XCB" ${xcb_FOUND})
# 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_X11)
if (NOT xcb_FOUND)
message (FATAL_ERROR "XCB not found")
endif (NOT xcb_FOUND)
if (WITH_GTK)
if (NOT gtk_FOUND)
message (FATAL_ERROR "GTK+ library not found")
endif (NOT gtk_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)
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)
link_directories (${dependencies_LIBRARY_DIRS})
include_directories (${ZLIB_INCLUDE_DIRS} ${icu_INCLUDE_DIRS}

View File

@ -1,4 +1,4 @@
Copyright (c) 2013 - 2018, Přemysl Janouch <p@janouch.name>
Copyright (c) 2013 - 2016, 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.

View File

@ -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_ for StarDict.
software. I wasn't able to reuse _anything_.
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, xcb and xcb-xfixes (optional)
glib-2.0, pango, gtk+ (optional, any version)
$ 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_X11=ON
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug -DWITH_GTK=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 XCB and it will never work on Wayland by its design.
This feature requires GTK+ and it will never work on Wayland by its design.
You can also set up some dictionaries to be loaded at startup automatically:

View File

@ -8,7 +8,7 @@
#define GETTEXT_PACKAGE PROJECT_NAME
#define GETTEXT_DIRNAME "${CMAKE_INSTALL_PREFIX}/share/locale"
#cmakedefine WITH_X11
#cmakedefine WITH_GTK
#cmakedefine HAVE_RESIZETERM
#endif // ! CONFIG_H

View File

@ -1,7 +1,7 @@
/*
* StarDict terminal UI
*
* Copyright (c) 2013 - 2018, Přemysl Janouch <p@janouch.name>
* Copyright (c) 2013 - 2016, 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,6 +43,10 @@
#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
@ -58,15 +62,6 @@ unichar_width (gunichar ch)
return 1 + g_unichar_iswide (ch);
}
static guint
add_read_watch (int fd, GIOFunc func, gpointer user_data)
{
GIOChannel *channel = g_io_channel_unix_new (fd);
guint res = g_io_add_watch (channel, G_IO_IN, func, user_data);
g_io_channel_unref (channel);
return res;
}
// At times, GLib even with its sheer size is surprisingly useless,
// and I need to port some code over from "liberty".
@ -218,7 +213,6 @@ 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
@ -232,6 +226,10 @@ 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];
};
@ -400,6 +398,18 @@ 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)
@ -458,8 +468,14 @@ 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);
self->watch_x11_sel =
app_load_bool (kf, "watch-selection", self->watch_x11_sel);
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;
#define XX(name, config, fg_, bg_, attrs_) \
app_load_color (self, kf, config, ATTRIBUTE_ ## name);
@ -597,6 +613,9 @@ 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;
@ -605,7 +624,6 @@ 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;
@ -642,7 +660,18 @@ app_init (Application *self, char **filenames)
exit (EXIT_FAILURE);
}
self->loop = g_main_loop_new (NULL, FALSE);
// 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);
// Dictionaries given on the command line override the configuration
if (*filenames)
@ -715,6 +744,10 @@ 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);
@ -727,14 +760,24 @@ app_destroy (Application *self)
static void
app_run (Application *self)
{
g_main_loop_run (self->loop);
if (self->loop)
g_main_loop_run (self->loop);
#ifdef WITH_GTK
else
gtk_main ();
#endif // WITH_GTK
}
/// Quit the main event dispatch loop.
static void
app_quit (Application *self)
{
g_main_loop_quit (self->loop);
if (self->loop)
g_main_loop_quit (self->loop);
#ifdef WITH_GTK
else
gtk_main_quit ();
#endif // WITH_GTK
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -980,7 +1023,7 @@ app_show_help (Application *self)
{
PROJECT_NAME " " PROJECT_VERSION,
_("Terminal UI for StarDict dictionaries"),
"Copyright (c) 2013 - 2018, Přemysl Janouch",
"Copyright (c) 2013 - 2016, Přemysl Janouch",
"",
_("Type to search")
};
@ -1792,326 +1835,6 @@ install_winch_handler (void)
sigaction (SIGWINCH, &act, &oldact);
}
// --- X11 selection watcher ---------------------------------------------------
#ifdef WITH_X11
static void
app_set_input (Application *self, const gchar *text, gsize text_len)
{
glong size;
gunichar *output = g_utf8_to_ucs4 (text, text_len, NULL, &size, NULL);
// XXX: signal invalid data?
if (!output)
return;
g_array_free (self->input, TRUE);
self->input = g_array_new (TRUE, FALSE, sizeof (gunichar));
self->input_pos = 0;
gunichar *p = output;
while (size--)
{
// XXX: skip?
if (!g_unichar_isprint (*p))
break;
g_array_insert_val (self->input, self->input_pos++, *p++);
}
g_free (output);
self->input_confirmed = FALSE;
app_search_for_entry (self);
app_redraw_top (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#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_incr; ///< INCR
xcb_atom_t atom_utf8_string; ///< UTF8_STRING
xcb_timestamp_t in_progress; ///< Timestamp of last processed event
GString * buffer; ///< UTF-8 text buffer
gboolean incr; ///< INCR running
gboolean incr_failure; ///< INCR failure indicator
};
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
on_selection_text_received (SelectionWatch *self, const gchar *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 (text_len)
app_set_input (self->app, text, text_len);
}
static gboolean
read_utf8_property (SelectionWatch *self, xcb_window_t wid, xcb_atom_t property,
gboolean *empty)
{
guint32 offset = 0;
gboolean more_data = TRUE, ok = TRUE;
xcb_get_property_reply_t *gpr;
while (ok && more_data)
{
if (!(gpr = xcb_get_property_reply (self->X,
xcb_get_property (self->X, FALSE /* delete */, wid,
property, XCB_GET_PROPERTY_TYPE_ANY, offset, 0x8000), NULL)))
return FALSE;
int len = xcb_get_property_value_length (gpr);
if (offset == 0 && len == 0 && empty)
*empty = TRUE;
ok = gpr->type == self->atom_utf8_string && gpr->format == 8;
more_data = gpr->bytes_after != 0;
if (ok)
{
offset += len >> 2;
g_string_append_len (self->buffer,
xcb_get_property_value (gpr), len);
}
free (gpr);
}
return ok;
}
static void
on_x11_selection_change (SelectionWatch *self,
xcb_xfixes_selection_notify_event_t *e)
{
// Not checking whether we should give up when this interrupts our
// current retrieval attempt--the timeout mostly solves this for all cases
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;
self->incr = FALSE;
}
static void
on_x11_selection_receive (SelectionWatch *self,
xcb_selection_notify_event_t *e)
{
if (e->requestor != self->wid
|| e->time != self->in_progress)
return;
self->in_progress = 0;
if (e->property == XCB_ATOM_NONE)
return;
xcb_get_property_reply_t *gpr = xcb_get_property_reply (self->X,
xcb_get_property (self->X, FALSE /* delete */, e->requestor,
e->property, XCB_GET_PROPERTY_TYPE_ANY, 0, 0), NULL);
if (!gpr)
return;
// Garbage collection, GString only ever expands in size
g_string_free (self->buffer, TRUE);
self->buffer = g_string_new (NULL);
// When you select a lot of text in VIM, it starts the ICCCM INCR mechanism,
// from which there is no opt-out
if (gpr->type == self->atom_incr)
{
self->in_progress = e->time;
self->incr = TRUE;
self->incr_failure = FALSE;
}
else if (read_utf8_property (self, e->requestor, e->property, NULL))
on_selection_text_received (self, self->buffer->str);
free (gpr);
(void) xcb_delete_property (self->X, self->wid, e->property);
}
static void
on_x11_property_notify (SelectionWatch *self, xcb_property_notify_event_t *e)
{
if (!self->incr
|| e->window != self->wid
|| e->state != XCB_PROPERTY_NEW_VALUE
|| e->atom != XCB_ATOM_PRIMARY)
return;
gboolean empty = FALSE;
if (!read_utf8_property (self, e->window, e->atom, &empty))
// We need to keep deleting the property
self->incr_failure = TRUE;
// Once it's empty, we've consumed everything and can move on undisturbed
if (empty)
{
if (!self->incr_failure)
on_selection_text_received (self, self->buffer->str);
self->in_progress = 0;
self->incr = FALSE;
}
(void) xcb_delete_property (self->X, e->window, e->atom);
}
static void
process_x11_event (SelectionWatch *self, xcb_generic_event_t *event)
{
int event_code = event->response_type & 0x7f;
if (event_code == 0)
{
xcb_generic_error_t *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)
on_x11_selection_change (self,
(xcb_xfixes_selection_notify_event_t *) event);
else if (event_code == XCB_SELECTION_NOTIFY)
on_x11_selection_receive (self,
(xcb_selection_notify_event_t *) event);
else if (event_code == XCB_PROPERTY_NOTIFY)
on_x11_property_notify (self,
(xcb_property_notify_event_t *) event);
}
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")));
g_return_if_fail
((self->atom_incr = resolve_atom (self->X, "INCR")));
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);
const uint32_t values[] = {XCB_EVENT_MASK_PROPERTY_CHANGE};
(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, XCB_CW_EVENT_MASK, values);
(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 = add_read_watch
(xcb_get_file_descriptor (self->X), process_x11, self);
// Never NULL so that we don't need to care about pointer validity
self->buffer = g_string_new (NULL);
}
static void
selection_watch_destroy (SelectionWatch *self)
{
if (self->X)
xcb_disconnect (self->X);
if (self->watch)
g_source_remove (self->watch);
if (self->buffer)
g_string_free (self->buffer, TRUE);
}
#endif // WITH_X11
// --- Initialisation, event handling ------------------------------------------
static gboolean on_stdin_input_timeout (gpointer data);
@ -2180,6 +1903,80 @@ on_terminated (gpointer user_data)
return TRUE;
}
#ifdef WITH_GTK
static void
app_set_input (Application *self, const gchar *text, gsize text_len)
{
glong size;
gunichar *output = g_utf8_to_ucs4 (text, text_len, NULL, &size, NULL);
// XXX: signal invalid data?
if (!output)
return;
g_array_free (self->input, TRUE);
self->input = g_array_new (TRUE, FALSE, sizeof (gunichar));
self->input_pos = 0;
gunichar *p = output;
while (size--)
{
// XXX: skip?
if (!g_unichar_isprint (*p))
break;
g_array_insert_val (self->input, self->input_pos++, *p++);
}
g_free (output);
self->input_confirmed = FALSE;
app_search_for_entry (self);
app_redraw_top (self);
}
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)
{
Application *app = data;
GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
gtk_clipboard_request_text (clipboard, on_selection_text_received, app);
app->selection_timer = 0;
return FALSE;
}
#endif // WITH_GTK
static void
log_handler_curses (Application *self, const gchar *message)
{
@ -2294,28 +2091,19 @@ G_GNUC_END_IGNORE_DEPRECATIONS
// g_unix_signal_add() cannot handle SIGWINCH
install_winch_handler ();
// Avoid disruptive warnings
// GtkClipboard can internally issue some rather disruptive warnings
g_log_set_default_handler (log_handler, &app);
// Message loop
guint watch_term = g_unix_signal_add (SIGTERM, on_terminated, &app);
guint watch_int = g_unix_signal_add (SIGINT, on_terminated, &app);
guint watch_stdin = add_read_watch
(STDIN_FILENO, process_stdin_input, &app);
guint watch_winch = add_read_watch
(g_winch_pipe[0], process_winch_input, &app);
#ifdef WITH_X11
SelectionWatch sw;
selection_watch_init (&sw, &app);
#endif // WITH_X11
guint watch_stdin = g_io_add_watch (g_io_channel_unix_new (STDIN_FILENO),
G_IO_IN, process_stdin_input, &app);
guint watch_winch = g_io_add_watch (g_io_channel_unix_new (g_winch_pipe[0]),
G_IO_IN, process_winch_input, &app);
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);