aboutsummaryrefslogtreecommitdiff
path: root/nncmpp.c
diff options
context:
space:
mode:
Diffstat (limited to 'nncmpp.c')
-rw-r--r--nncmpp.c2528
1 files changed, 1948 insertions, 580 deletions
diff --git a/nncmpp.c b/nncmpp.c
index 6faad48..19f1903 100644
--- a/nncmpp.c
+++ b/nncmpp.c
@@ -1,7 +1,7 @@
/*
* nncmpp -- the MPD client you never knew you needed
*
- * Copyright (c) 2016 - 2021, Přemysl Eric Janouch <p@janouch.name>
+ * Copyright (c) 2016 - 2022, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
@@ -107,30 +107,20 @@ enum
#include <pulse/sample.h>
#endif // WITH_PULSE
+// Elementary port of the TUI to X11.
+#ifdef WITH_X11
+#include <X11/Xlib.h>
+#include <X11/keysym.h>
+#include <X11/XKBlib.h>
+#include <X11/Xft/Xft.h>
+#include <xkbcommon/xkbcommon.h>
+#endif // WITH_X11
+
#define APP_TITLE PROGRAM_NAME ///< Left top corner
-// --- Utilities ---------------------------------------------------------------
+#include "nncmpp-actions.h"
-// The standard endwin/refresh sequence makes the terminal flicker
-static void
-update_curses_terminal_size (void)
-{
-#if defined HAVE_RESIZETERM && defined TIOCGWINSZ
- struct winsize size;
- if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
- {
- char *row = getenv ("LINES");
- char *col = getenv ("COLUMNS");
- unsigned long tmp;
- resizeterm (
- (row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row,
- (col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col);
- }
-#else // HAVE_RESIZETERM && TIOCGWINSZ
- endwin ();
- refresh ();
-#endif // HAVE_RESIZETERM && TIOCGWINSZ
-}
+// --- Utilities ---------------------------------------------------------------
static int64_t
clock_msec (clockid_t clock)
@@ -600,7 +590,8 @@ struct spectrum
int samples; ///< Number of windows to average
float accumulator_scale; ///< Scaling factor for accum. values
int *top_bins; ///< Top DFT bin index for each bar
- char *spectrum; ///< String buffer for the "render"
+ char *rendered; ///< String buffer for the "render"
+ float *spectrum; ///< The "render" as normalized floats
void *buffer; ///< Input buffer
size_t buffer_len; ///< Input buffer fill level
@@ -722,7 +713,7 @@ spectrum_sample (struct spectrum *s)
}
int last_bin = 0;
- char *p = s->spectrum;
+ char *p = s->rendered;
for (int bar = 0; bar < s->bars; bar++)
{
int top_bin = s->top_bins[bar];
@@ -740,9 +731,13 @@ spectrum_sample (struct spectrum *s)
db = 0;
// Assuming decibels are always negative (i.e., properly normalized).
- // The division defines the cutoff: 9 * 7 = 63 dB of range.
+ // The division defines the cutoff: 8 * 7 = 56 dB of range.
int height = N_ELEMENTS (spectrum_bars) - 1 + (int) (db / 7);
p += strlen (strcpy (p, spectrum_bars[MAX (height, 0)]));
+
+ // Even with slightly the higher display resolutions provided by X11,
+ // 60 dB roughly covers the useful range.
+ s->spectrum[bar] = MAX (0, 1 + db / 60);
}
}
@@ -814,7 +809,8 @@ spectrum_init (struct spectrum *s, char *format, int bars, struct error **e)
s->useful_bins = s->bins / 2;
int used_bins = necessary_bins / 2;
- s->spectrum = xcalloc (sizeof *s->spectrum, s->bars * 3 + 1);
+ s->rendered = xcalloc (sizeof *s->rendered, s->bars * 3 + 1);
+ s->spectrum = xcalloc (sizeof *s->spectrum, s->bars);
s->top_bins = xcalloc (sizeof *s->top_bins, s->bars);
for (int bar = 0; bar < s->bars; bar++)
{
@@ -867,6 +863,7 @@ spectrum_free (struct spectrum *s)
free (s->data);
free (s->window);
+ free (s->rendered);
free (s->spectrum);
free (s->top_bins);
free (s->buffer);
@@ -1128,43 +1125,136 @@ pulse_volume_status (struct pulse *self, struct str *s)
// --- Application -------------------------------------------------------------
-// Function names are prefixed mostly because of curses which clutters the
+// Function names are prefixed mostly because of curses, which clutters the
// global namespace and makes it harder to distinguish what functions relate to.
// The user interface is focused on conceptual simplicity. That is important
-// since we're not using any TUI framework (which are mostly a lost cause to me
-// in the post-Unicode era and not worth pursuing), and the code would get
-// bloated and incomprehensible fast. We mostly rely on "row_buffer" to write
-// text from left to right row after row while keeping track of cells.
+// since we use a custom toolkit, so code would get bloated rather fast--
+// especially given our TUI/GUI duality.
//
// There is an independent top pane displaying general status information,
// followed by a tab bar and a listview served by a per-tab event handler.
//
// For simplicity, the listview can only work with items that are one row high.
+// Widget identification, mostly for mouse events.
+enum
+{
+ WIDGET_NONE = 0, WIDGET_BUTTON, WIDGET_GAUGE, WIDGET_TAB, WIDGET_SPECTRUM,
+ WIDGET_LIST, WIDGET_SCROLLBAR,
+};
+
+struct widget;
+
+/// Draw a widget on the window
+typedef void (*widget_render_fn) (struct widget *self);
+
+/// A minimal abstraction appropriate for both TUI and GUI widgets.
+/// Units for the widget's region are frontend-specific.
+/// Having this as a linked list simplifies layouting and memory management.
+struct widget
+{
+ LIST_HEADER (struct widget)
+
+ int x; ///< X coordinate
+ int y; ///< Y coordinate
+ int width; ///< Width, initialized by UI methods
+ int height; ///< Height, initialized by UI methods
+
+ widget_render_fn on_render; ///< Render callback
+ chtype attrs; ///< Rendition, in Curses terms
+
+ short id; ///< Post-layouting identification
+ short subid; ///< Action ID/Tab index/...
+ char text[]; ///< Any text label
+};
+
+struct layout
+{
+ struct widget *head;
+ struct widget *tail;
+};
+
+struct ui
+{
+ struct widget *(*padding) (chtype attrs, float width, float height);
+ struct widget *(*label) (chtype attrs, const char *label);
+ struct widget *(*button) (chtype attrs, const char *label, enum action a);
+ struct widget *(*gauge) (chtype attrs);
+ struct widget *(*spectrum) (chtype attrs, int width);
+ struct widget *(*scrollbar) (chtype attrs);
+ struct widget *(*list) (void);
+ struct widget *(*editor) (chtype attrs);
+
+ void (*render) (void);
+ void (*flip) (void);
+ void (*winch) (void);
+ void (*destroy) (void);
+
+ bool have_icons;
+};
+
+/// Replaces negative widths amongst widgets in the sublist by redistributing
+/// any width remaining after all positive claims are satisfied from "width".
+/// Also unifies heights to the maximum value of the run, and returns it.
+/// Then the widths are taken as final, and used to initialize X coordinates.
+static int
+widget_redistribute (struct widget *head, int width)
+{
+ int parts = 0, max_height = 0;
+ LIST_FOR_EACH (struct widget, w, head)
+ {
+ max_height = MAX (max_height, w->height);
+ if (w->width < 0)
+ parts -= w->width;
+ else
+ width -= w->width;
+ }
+
+ int remaining = MAX (width, 0), part_width = parts ? remaining / parts : 0;
+ struct widget *last = NULL;
+ LIST_FOR_EACH (struct widget, w, head)
+ {
+ w->height = max_height;
+ if (w->width < 0)
+ {
+ remaining -= (w->width *= -part_width);
+ last = w;
+ }
+ }
+ if (last)
+ last->width += remaining;
+
+ int x = 0;
+ LIST_FOR_EACH (struct widget, w, head)
+ {
+ w->x = x;
+ x += w->width;
+ }
+ return max_height;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
struct tab;
-enum action;
/// Try to handle an action in the tab
typedef bool (*tab_action_fn) (enum action action);
-/// Draw an item to the screen using the row buffer API
-typedef void (*tab_item_draw_fn)
- (size_t item_index, struct row_buffer *buffer, int width);
+/// Return a line of widgets for the row
+typedef struct layout (*tab_item_layout_fn) (size_t item_index);
struct tab
{
LIST_HEADER (struct tab)
char *name; ///< Visible identifier
- size_t name_width; ///< Visible width of the name
-
char *header; ///< The header, should there be any
// Implementation:
tab_action_fn on_action; ///< User action handler callback
- tab_item_draw_fn on_item_draw; ///< Item draw callback
+ tab_item_layout_fn on_item_layout; ///< Item layout callback
// Provided by tab owner:
@@ -1213,7 +1303,6 @@ static struct app_context
int64_t elapsed_since; ///< Last tick ts or last elapsed time
bool elapsed_poll; ///< Poll MPD for the elapsed time?
- // TODO: initialize these to -1
int song; ///< Current song index
int song_elapsed; ///< Song elapsed in seconds
int song_duration; ///< Song duration in seconds
@@ -1234,19 +1323,19 @@ static struct app_context
struct tab *active_tab; ///< Active tab
struct tab *last_tab; ///< Previous tab
- // Emulated widgets:
+ // User interface:
- int header_height; ///< Height of the header
-
- int tabs_offset; ///< Offset to tabs or -1
- int controls_offset; ///< Offset to player controls or -1
- int gauge_offset; ///< Offset to the gauge or -1
- int gauge_width; ///< Width of the gauge, if present
+ struct ui *ui; ///< User interface interface
+ struct layout widgets; ///< Layouted widgets
+ int ui_width; ///< Window width
+ int ui_height; ///< Window height
+ int ui_hunit; ///< Horizontal unit
+ int ui_vunit; ///< Vertical unit
+ bool ui_focused; ///< Whether the window has focus
#ifdef WITH_FFTW
struct spectrum spectrum; ///< Spectrum analyser
int spectrum_fd; ///< FIFO file descriptor (non-blocking)
- int spectrum_column, spectrum_row; ///< Position for fast refresh
struct poller_fd spectrum_event; ///< FIFO watcher
#endif // WITH_FFTW
@@ -1255,16 +1344,32 @@ static struct app_context
#endif // WITH_PULSE
bool pulse_control_requested; ///< PulseAudio control desired by user
+#ifdef WITH_X11
+ Display *dpy; ///< X display handle
+ struct poller_fd x11_event; ///< X11 events on wire
+ struct poller_idle xpending_event; ///< X11 events possibly in I/O queues
+ int xkb_base_event_code; ///< Xkb base event code
+ Window x11_window; ///< Application window
+ Pixmap x11_pixmap; ///< Off-screen bitmap
+ Picture x11_pixmap_picture; ///< XRender wrap for x11_pixmap
+ XftDraw *xft_draw; ///< Xft rendering context
+ XftFont *xft_regular; ///< Regular font
+ XftFont *xft_bold; ///< Bold font
+
+ XRenderColor x_fg[ATTRIBUTE_COUNT]; ///< Foreground per attribute
+ XRenderColor x_bg[ATTRIBUTE_COUNT]; ///< Background per attribute
+#endif // WITH_X11
+
struct line_editor editor; ///< Line editor
- struct poller_idle refresh_event; ///< Refresh the screen
+ struct poller_idle refresh_event; ///< Refresh the window's contents
+ struct poller_idle flip_event; ///< Draw rendered widgets on screen
// Terminal:
- termo_t *tk; ///< termo handle
+ termo_t *tk; ///< termo handle (TUI/X11)
struct poller_timer tk_timer; ///< termo timeout timer
bool locale_is_utf8; ///< The locale is Unicode
bool use_partial_boxes; ///< Use Unicode box drawing chars
- bool focused; ///< Whether the terminal has focus
struct attrs attrs[ATTRIBUTE_COUNT];
}
@@ -1282,9 +1387,6 @@ tab_init (struct tab *self, const char *name)
// Add some padding for decorative purposes
self->name = xstrdup_printf (" %s ", name);
- // Assuming tab names are pure ASCII, otherwise this would be inaccurate
- // and we'd need to filter it first to replace invalid chars with '?'
- self->name_width = u8_strwidth ((uint8_t *) self->name, locale_charset ());
self->item_selected = 0;
self->item_mark = -1;
}
@@ -1362,6 +1464,13 @@ static struct config_schema g_config_settings[] =
.default_ = "off" },
#endif // WITH_PULSE
+#ifdef WITH_X11
+ { .name = "x11_font",
+ .comment = "Fontconfig name/pattern for the X11 font to use",
+ .type = CONFIG_ITEM_STRING,
+ .default_ = "`sans\\-serif-11`" },
+#endif // WITH_X11
+
// Disabling this minimises MPD traffic and has the following caveats:
// - when MPD stalls on retrieving audio data, we keep ticking
// - when the "play" succeeds in ACTION_MPD_REPLACE for the same item as
@@ -1510,23 +1619,27 @@ app_init_context (void)
poller_init (&g.poller);
hard_assert (poller_curl_init (&g.poller_curl, &g.poller, NULL));
g.client = mpd_client_make (&g.poller);
+ g.song_elapsed = g.song_duration = g.volume = g.song = -1;
+ g.playlist = item_list_make ();
g.config = config_make ();
g.streams = strv_make ();
g.enqueue = strv_make ();
- g.playlist = item_list_make ();
g.playback_info = str_map_make (free);
g.playback_info.key_xfrm = tolower_ascii_strxfrm;
#ifdef WITH_FFTW
g.spectrum_fd = -1;
- g.spectrum_row = g.spectrum_column = -1;
#endif // WITH_FFTW
#ifdef WITH_PULSE
pulse_init (&g.pulse, NULL);
#endif // WITH_PULSE
+ TERMO_CHECK_VERSION;
+ if (!(g.tk = termo_new (STDIN_FILENO, NULL, TERMO_FLAG_NOSTART)))
+ exit_fatal ("failed to initialize termo");
+
// This is also approximately what libunistring does internally,
// since the locale name is canonicalized by locale_charset().
// Note that non-Unicode locales are handled pretty inefficiently.
@@ -1537,43 +1650,12 @@ app_init_context (void)
g.use_partial_boxes = g.locale_is_utf8;
// Presumably, although not necessarily; unsure if queryable at all
- g.focused = true;
+ g.ui_focused = true;
app_init_attributes ();
}
static void
-app_init_terminal (void)
-{
- TERMO_CHECK_VERSION;
- if (!(g.tk = termo_new (STDIN_FILENO, NULL, 0)))
- exit_fatal ("failed to set up the terminal");
- if (!initscr () || nonl () == ERR)
- exit_fatal ("failed to set up the terminal");
-
- // By default we don't use any colors so they're not required...
- if (start_color () == ERR
- || use_default_colors () == ERR
- || COLOR_PAIRS <= ATTRIBUTE_COUNT)
- return;
-
- for (int a = 0; a < ATTRIBUTE_COUNT; a++)
- {
- // ...thus we can reset back to defaults even after initializing some
- // FIXME: that's a lie now, MULTISELECT requires a colour
- if (g.attrs[a].fg >= COLORS || g.attrs[a].fg < -1
- || g.attrs[a].bg >= COLORS || g.attrs[a].bg < -1)
- {
- app_init_attributes ();
- return;
- }
-
- init_pair (a + 1, g.attrs[a].fg, g.attrs[a].bg);
- g.attrs[a].attrs |= COLOR_PAIR (a + 1);
- }
-}
-
-static void
app_free_context (void)
{
mpd_client_free (&g.client);
@@ -1634,7 +1716,7 @@ app_is_character_in_locale (ucs4_t ch)
return true;
}
-// --- Rendering ---------------------------------------------------------------
+// --- Layouting ---------------------------------------------------------------
static void
app_invalidate (void)
@@ -1643,50 +1725,75 @@ app_invalidate (void)
}
static void
-app_flush_buffer (struct row_buffer *buf, int width, chtype attrs)
+app_flush_layout (struct layout *l)
{
- row_buffer_align (buf, width, attrs);
- row_buffer_flush (buf);
- row_buffer_free (buf);
+ hard_assert (l != NULL && l->head != NULL);
+ widget_redistribute (l->head, g.ui_width);
+
+ struct widget *last = g.widgets.tail;
+ if (!last)
+ g.widgets = *l;
+ else
+ {
+ // Assuming there is no unclaimed vertical space.
+ LIST_FOR_EACH (struct widget, w, l->head)
+ w->y = last->y + last->height;
+
+ last->next = l->head;
+ l->head->prev = last;
+ g.widgets.tail = l->tail;
+ }
}
-/// Write the given UTF-8 string padded with spaces.
-/// @param[in] attrs Text attributes for the text, including padding.
-static void
-app_write_line (const char *str, chtype attrs)
+static struct widget *
+app_push (struct layout *l, struct widget *w)
{
- struct row_buffer buf = row_buffer_make ();
- row_buffer_append (&buf, str, attrs);
- app_flush_buffer (&buf, COLS, attrs);
+ LIST_APPEND_WITH_TAIL (l->head, l->tail, w);
+ return w;
}
-// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+static struct widget *
+app_push_fill (struct layout *l, struct widget *w)
+{
+ w->width = -1;
+ LIST_APPEND_WITH_TAIL (l->head, l->tail, w);
+ return w;
+}
+/// Write the given UTF-8 string padded with spaces.
+/// @param[in] attrs Text attributes for the text, including padding.
static void
-app_flush_header (struct row_buffer *buf, chtype attrs)
+app_layout_text (const char *str, chtype attrs)
{
- move (g.header_height++, 0);
- app_flush_buffer (buf, COLS, attrs);
+ struct layout l = {};
+ app_push (&l, g.ui->padding (attrs, 0.25, 1));
+ app_push_fill (&l, g.ui->label (attrs, str));
+ app_push (&l, g.ui->padding (attrs, 0.25, 1));
+ app_flush_layout (&l);
}
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
static void
-app_draw_song_info (void)
+app_layout_song_info (void)
{
compact_map_t map;
if (!(map = item_list_get (&g.playlist, g.song)))
return;
- chtype attr_normal = APP_ATTR (NORMAL);
- chtype attr_highlight = APP_ATTR (HIGHLIGHT);
+ chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) };
char *title;
if ((title = compact_map_find (map, "title"))
|| (title = compact_map_find (map, "name"))
|| (title = compact_map_find (map, "file")))
{
- struct row_buffer buf = row_buffer_make ();
- row_buffer_append (&buf, title, attr_highlight);
- app_flush_header (&buf, attr_normal);
+ struct layout l = {};
+ app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
+ app_push (&l, g.ui->label (attrs[1], title));
+ app_push_fill (&l, g.ui->padding (attrs[0], 0, 1));
+ app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
+ app_flush_layout (&l);
}
char *artist = compact_map_find (map, "artist");
@@ -1694,14 +1801,23 @@ app_draw_song_info (void)
if (!artist && !album)
return;
- struct row_buffer buf = row_buffer_make ();
+ struct layout l = {};
+ app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
+
if (artist)
- row_buffer_append_args (&buf, " by " + !buf.total_width, attr_normal,
- artist, attr_highlight, NULL);
+ {
+ app_push (&l, g.ui->label (attrs[0], "by "));
+ app_push (&l, g.ui->label (attrs[1], artist));
+ }
if (album)
- row_buffer_append_args (&buf, " from " + !buf.total_width, attr_normal,
- album, attr_highlight, NULL);
- app_flush_header (&buf, attr_normal);
+ {
+ app_push (&l, g.ui->label (attrs[0], " from " + !artist));
+ app_push (&l, g.ui->label (attrs[1], album));
+ }
+
+ app_push_fill (&l, g.ui->padding (attrs[0], 0, 1));
+ app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
+ app_flush_layout (&l);
}
static char *
@@ -1721,196 +1837,169 @@ app_time_string (int seconds)
}
static void
-app_write_time (struct row_buffer *buf, int seconds, chtype attrs)
-{
- char *s = app_time_string (seconds);
- row_buffer_append (buf, s, attrs);
- free (s);
-}
-
-static void
-app_write_gauge (struct row_buffer *buf, float ratio, int width)
+app_layout_status (void)
{
- if (ratio < 0) ratio = 0;
- if (ratio > 1) ratio = 1;
-
- // Always compute it in exactly eight times the resolution,
- // because sometimes Unicode is even useful
- int len_left = ratio * width * 8 + 0.5;
-
- static const char *partials[] = { " ", "▏", "▎", "▍", "▌", "▋", "▊", "▉" };
- int remainder = len_left % 8;
- len_left /= 8;
-
- const char *partial = NULL;
- if (g.use_partial_boxes)
- partial = partials[remainder];
- else
- len_left += remainder >= (int) 4;
-
- int len_right = width - len_left;
- row_buffer_space (buf, len_left, APP_ATTR (ELAPSED));
- if (partial && len_right-- > 0)
- row_buffer_append (buf, partial, APP_ATTR (REMAINS));
- row_buffer_space (buf, len_right, APP_ATTR (REMAINS));
-}
-
-static void
-app_draw_status (void)
-{
- if (g.state != PLAYER_STOPPED)
- app_draw_song_info ();
-
- chtype attr_normal = APP_ATTR (NORMAL);
- chtype attr_highlight = APP_ATTR (HIGHLIGHT);
-
- struct row_buffer buf = row_buffer_make ();
bool stopped = g.state == PLAYER_STOPPED;
- chtype attr_song_action = stopped ? attr_normal : attr_highlight;
+ if (!stopped)
+ app_layout_song_info ();
+ chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) };
+ struct layout l = {};
+
+ app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
+ app_push (&l, g.ui->button (attrs[!stopped], "<<", ACTION_MPD_PREVIOUS));
+ app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
const char *toggle = g.state == PLAYER_PLAYING ? "||" : "|>";
- row_buffer_append_args (&buf,
- "<<", attr_song_action, " ", attr_normal,
- toggle, attr_highlight, " ", attr_normal,
- "[]", attr_song_action, " ", attr_normal,
- ">>", attr_song_action, " ", attr_normal,
- NULL);
+ app_push (&l, g.ui->button (attrs[1], toggle, ACTION_MPD_TOGGLE));
+ app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
+ app_push (&l, g.ui->button (attrs[!stopped], "[]", ACTION_MPD_STOP));
+ app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
+ app_push (&l, g.ui->button (attrs[!stopped], ">>", ACTION_MPD_NEXT));
+ app_push (&l, g.ui->padding (attrs[0], 1, 1));
if (stopped)
- row_buffer_append (&buf, "Stopped", attr_normal);
+ app_push_fill (&l, g.ui->label (attrs[0], "Stopped"));
else
{
if (g.song_elapsed >= 0)
{
- app_write_time (&buf, g.song_elapsed, attr_normal);
- row_buffer_append (&buf, " ", attr_normal);
+ char *s = app_time_string (g.song_elapsed);
+ app_push (&l, g.ui->label (attrs[0], s));
+ free (s);
}
if (g.song_duration >= 1)
{
- row_buffer_append (&buf, "/ ", attr_normal);
- app_write_time (&buf, g.song_duration, attr_normal);
- row_buffer_append (&buf, " ", attr_normal);
+ char *s = app_time_string (g.song_duration);
+ app_push (&l, g.ui->label (attrs[0], " / "));
+ app_push (&l, g.ui->label (attrs[0], s));
+ free (s);
}
- row_buffer_append (&buf, " ", attr_normal);
+
+ app_push (&l, g.ui->padding (attrs[0], 1, 1));
}
- // It gets a bit complicated due to the only right-aligned item on the row
struct str volume = str_make ();
#ifdef WITH_PULSE
if (g.pulse_control_requested)
{
- struct str buf = str_make ();
- if (pulse_volume_status (&g.pulse, &buf))
+ if (pulse_volume_status (&g.pulse, &volume))
{
if (g.volume >= 0 && g.volume != 100)
- str_append_printf (&buf, " (%d%%)", g.volume);
+ str_append_printf (&volume, " (%d%%)", g.volume);
}
else
{
if (g.volume >= 0)
- str_append_printf (&buf, "(%d%%)", g.volume);
+ str_append_printf (&volume, "(%d%%)", g.volume);
}
- if (buf.len)
- str_append_printf (&volume, " %s", buf.str);
-
- str_free (&buf);
}
else
#endif // WITH_PULSE
if (g.volume >= 0)
- str_append_printf (&volume, " %3d%%", g.volume);
+ str_append_printf (&volume, "%3d%%", g.volume);
- int remaining = COLS - buf.total_width - volume.len;
- if (!stopped && g.song_elapsed >= 0 && g.song_duration >= 1
- && remaining > 0)
- {
- g.gauge_offset = buf.total_width;
- g.gauge_width = remaining;
- app_write_gauge (&buf,
- (float) g.song_elapsed / g.song_duration, remaining);
- }
+ if (!stopped && g.song_elapsed >= 0 && g.song_duration >= 1)
+ app_push (&l, g.ui->gauge (attrs[0]))
+ ->id = WIDGET_GAUGE;
else
- row_buffer_space (&buf, remaining, attr_normal);
+ app_push_fill (&l, g.ui->padding (attrs[0], 0, 1));
if (volume.len)
- row_buffer_append (&buf, volume.str, attr_normal);
+ {
+ app_push (&l, g.ui->padding (attrs[0], 1, 1));
+ app_push (&l, g.ui->label (attrs[0], volume.str));
+ }
str_free (&volume);
- g.controls_offset = g.header_height;
- app_flush_header (&buf, attr_normal);
+ app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
+ app_flush_layout (&l);
}
static void
-app_draw_header (void)
+app_layout_tabs (void)
{
- g.header_height = 0;
-
- g.tabs_offset = -1;
- g.controls_offset = -1;
- g.gauge_offset = -1;
- g.gauge_width = 0;
-
- switch (g.client.state)
- {
- case MPD_CONNECTED:
- app_draw_status ();
- break;
- case MPD_CONNECTING:
- move (g.header_height++, 0);
- app_write_line ("Connecting to MPD...", APP_ATTR (NORMAL));
- break;
- case MPD_DISCONNECTED:
- move (g.header_height++, 0);
- app_write_line ("Disconnected", APP_ATTR (NORMAL));
- }
-
chtype attrs[2] = { APP_ATTR (TAB_BAR), APP_ATTR (TAB_ACTIVE) };
+ struct layout l = {};
// The help tab is disguised so that it's not too intruding
- struct row_buffer buf = row_buffer_make ();
- row_buffer_append (&buf, APP_TITLE, attrs[g.active_tab == g.help_tab]);
- row_buffer_append (&buf, " ", attrs[false]);
+ app_push (&l, g.ui->padding (attrs[g.active_tab == g.help_tab], 0.25, 1))
+ ->id = WIDGET_TAB;
+ app_push (&l, g.ui->label (attrs[g.active_tab == g.help_tab], APP_TITLE))
+ ->id = WIDGET_TAB;
- g.tabs_offset = g.header_height;
+ // XXX: attrs[0]?
+ app_push (&l, g.ui->padding (attrs[g.active_tab == g.help_tab], 0.5, 1))
+ ->id = WIDGET_TAB;
+
+ int i = 0;
LIST_FOR_EACH (struct tab, iter, g.tabs)
- row_buffer_append (&buf, iter->name, attrs[iter == g.active_tab]);
+ {
+ struct widget *w = app_push (&l,
+ g.ui->label (attrs[iter == g.active_tab], iter->name));
+ w->id = WIDGET_TAB;
+ w->subid = ++i;
+ }
+
+ app_push_fill (&l, g.ui->padding (attrs[0], 1, 1));
#ifdef WITH_FFTW
// This seems like the most reasonable, otherwise unoccupied space
if (g.spectrum_fd != -1)
{
- // Find some space and remember where it was, for fast refreshes
- row_buffer_ellipsis (&buf, COLS - g.spectrum.bars - 1);
- row_buffer_align (&buf, COLS - g.spectrum.bars, attrs[false]);
- g.spectrum_row = g.header_height;
- g.spectrum_column = buf.total_width;
-
- row_buffer_append (&buf, g.spectrum.spectrum, attrs[false]);
+ app_push (&l, g.ui->spectrum (attrs[0], g.spectrum.bars))
+ ->id = WIDGET_SPECTRUM;
}
#endif // WITH_FFTW
- app_flush_header (&buf, attrs[false]);
+ app_flush_layout (&l);
+}
- const char *header = g.active_tab->header;
- if (header)
+static void
+app_layout_header (void)
+{
{
- buf = row_buffer_make ();
- row_buffer_append (&buf, header, APP_ATTR (HEADER));
- app_flush_header (&buf, APP_ATTR (HEADER));
+ struct layout l = {};
+ app_push_fill (&l, g.ui->padding (APP_ATTR (NORMAL), 0, 0.125));
+ app_flush_layout (&l);
}
-}
-static int
-app_fitting_items (void)
-{
- // The raw number of items that would have fit on the terminal
- return LINES - g.header_height - 1 /* status bar */;
+ switch (g.client.state)
+ {
+ case MPD_CONNECTED:
+ app_layout_status ();
+ break;
+ case MPD_CONNECTING:
+ app_layout_text ("Connecting to MPD...", APP_ATTR (NORMAL));
+ break;
+ case MPD_DISCONNECTED:
+ app_layout_text ("Disconnected", APP_ATTR (NORMAL));
+ }
+
+ {
+ struct layout l = {};
+ app_push_fill (&l, g.ui->padding (APP_ATTR (NORMAL), 0, 0.125));
+ app_flush_layout (&l);
+ }
+
+ app_layout_tabs ();
+
+ const char *header = g.active_tab->header;
+ if (header)
+ app_layout_text (header, APP_ATTR (HEADER));
}
static int
app_visible_items (void)
{
- return MAX (0, app_fitting_items ());
+ struct widget *list = NULL;
+ LIST_FOR_EACH (struct widget, w, g.widgets.head)
+ if (w->id == WIDGET_LIST)
+ list = w;
+
+ hard_assert (list != NULL);
+
+ // The raw number of items that would have fit on the terminal
+ return MAX (0, list->height / g.ui_vunit);
}
/// Figure out scrollbar appearance. @a s is the minimal slider length as well
@@ -1941,116 +2030,67 @@ app_compute_scrollbar (struct tab *tab, long visible, long s)
return (struct scrollbar) { length, offset };
}
-static void
-app_draw_scrollbar (void)
+static struct widget *
+app_layout_row (struct tab *tab, int item_index)
{
- // This assumes that we can write to the one-before-last column,
- // i.e. that it's not covered by any double-wide character (and that
- // ncurses comes to the right results when counting characters).
- //
- // We could also precompute the scrollbar and append it to each row
- // as we render them, plus all the unoccupied rows.
- struct tab *tab = g.active_tab;
- int visible_items = app_visible_items ();
-
- hard_assert (tab->item_count != 0);
- if (!g.use_partial_boxes)
- {
- struct scrollbar bar = app_compute_scrollbar (tab, visible_items, 1);
- for (int row = 0; row < visible_items; row++)
- {
- move (g.header_height + row, COLS - 1);
- if (row < bar.start || row >= bar.start + bar.length)
- addch (' ' | APP_ATTR (SCROLLBAR));
- else
- addch (' ' | APP_ATTR (SCROLLBAR) | A_REVERSE);
- }
- return;
- }
+ int row_attrs = (item_index & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN);
- struct scrollbar bar = app_compute_scrollbar (tab, visible_items, 8);
- bar.length += bar.start;
-
- int start_part = bar.start % 8; bar.start /= 8;
- int end_part = bar.length % 8; bar.length /= 8;
+ bool override_colors = true;
+ if (item_index == tab->item_selected)
+ row_attrs = g.ui_focused
+ ? APP_ATTR (SELECTION) : APP_ATTR (DEFOCUSED);
+ else if (tab->item_mark > -1 &&
+ ((item_index >= tab->item_mark && item_index <= tab->item_selected)
+ || (item_index >= tab->item_selected && item_index <= tab->item_mark)))
+ row_attrs = g.ui_focused
+ ? APP_ATTR (MULTISELECT) : APP_ATTR (DEFOCUSED);
+ else
+ override_colors = false;
- // Even with this, the solid part must be at least one character high
- static const char *partials[] = { "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁" };
+ // The padding must be added before the recoloring below.
+ struct layout l = tab->on_item_layout (item_index);
+ struct widget *w = g.ui->padding (0, 0.25, 1);
+ LIST_PREPEND (l.head, w);
+ app_push (&l, g.ui->padding (0, 0.25, 1));
- for (int row = 0; row < visible_items; row++)
+ // Combine attributes used by the handler with the defaults.
+ LIST_FOR_EACH (struct widget, w, l.head)
{
- chtype attrs = APP_ATTR (SCROLLBAR);
- if (row > bar.start && row <= bar.length)
- attrs ^= A_REVERSE;
-
- const char *c = " ";
- if (row == bar.start) c = partials[start_part];
- if (row == bar.length) c = partials[end_part];
-
- move (g.header_height + row, COLS - 1);
-
- struct row_buffer buf = row_buffer_make ();
- row_buffer_append (&buf, c, attrs);
- row_buffer_flush (&buf);
- row_buffer_free (&buf);
+ chtype *attrs = &w->attrs;
+ if (override_colors)
+ *attrs = (*attrs & ~(A_COLOR | A_REVERSE)) | row_attrs;
+ else if ((*attrs & A_COLOR) && (row_attrs & A_COLOR))
+ *attrs |= (row_attrs & ~A_COLOR);
+ else
+ *attrs |= row_attrs;
}
+ return l.head;
}
static void
-app_draw_view (void)
+app_layout_view (void)
{
- move (g.header_height, 0);
- clrtobot ();
+ // XXX: Expecting the status bar to always be there, one row tall.
+ struct widget *last = g.widgets.tail;
+ int unavailable_height = last->y + last->height + g.ui_vunit;
- struct tab *tab = g.active_tab;
- bool want_scrollbar = (int) tab->item_count > app_visible_items ();
- int view_width = COLS - want_scrollbar;
+ struct layout l = {};
+ struct widget *w = app_push_fill (&l, g.ui->list ());
+ w->id = WIDGET_LIST;
+ w->height = g.ui_height - unavailable_height;
- int to_show =
- MIN (app_fitting_items (), (int) tab->item_count - tab->item_top);
- for (int row = 0; row < to_show; row++)
+ struct tab *tab = g.active_tab;
+ if ((int) tab->item_count * g.ui_vunit > w->height)
{
- int item_index = tab->item_top + row;
- int row_attrs = (item_index & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN);
-
- bool override_colors = true;
- if (item_index == tab->item_selected)
- row_attrs = g.focused
- ? APP_ATTR (SELECTION) : APP_ATTR (DEFOCUSED);
- else if (tab->item_mark > -1 &&
- ((item_index >= tab->item_mark && item_index <= tab->item_selected)
- || (item_index >= tab->item_selected && item_index <= tab->item_mark)))
- row_attrs = g.focused
- ? APP_ATTR (MULTISELECT) : APP_ATTR (DEFOCUSED);
- else
- override_colors = false;
-
- struct row_buffer buf = row_buffer_make ();
- tab->on_item_draw (item_index, &buf, view_width);
-
- // Combine attributes used by the handler with the defaults.
- // Avoiding attrset() because of row_buffer_flush().
- for (size_t i = 0; i < buf.chars_len; i++)
- {
- chtype *attrs = &buf.chars[i].attrs;
- if (override_colors)
- *attrs = (*attrs & ~(A_COLOR | A_REVERSE)) | row_attrs;
- else if ((*attrs & A_COLOR) && (row_attrs & A_COLOR))
- *attrs |= (row_attrs & ~A_COLOR);
- else
- *attrs |= row_attrs;
- }
-
- move (g.header_height + row, 0);
- app_flush_buffer (&buf, view_width, row_attrs);
+ app_push (&l, g.ui->scrollbar (APP_ATTR (SCROLLBAR)))
+ ->id = WIDGET_SCROLLBAR;
}
- if (want_scrollbar)
- app_draw_scrollbar ();
+ app_flush_layout (&l);
}
-static void
-app_write_mpd_status_playlist (struct row_buffer *buf)
+static char *
+app_mpd_status_playlist (void)
{
struct str stats = str_make ();
if (g.playlist.len == 1)
@@ -2074,28 +2114,35 @@ app_write_mpd_status_playlist (struct row_buffer *buf)
else if (minutes)
str_append_printf (&stats, " %d minutes", minutes);
}
- row_buffer_append (buf, stats.str, APP_ATTR (NORMAL));
- str_free (&stats);
+ return str_steal (&stats);
}
static void
-app_write_mpd_status (struct row_buffer *buf)
+app_layout_mpd_status (void)
{
+ struct layout l = {};
+ chtype attrs[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) };
+ app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
+
struct str_map *map = &g.playback_info;
if (g.active_tab->item_mark > -1)
{
struct tab_range r = tab_selection_range (g.active_tab);
char *msg = xstrdup_printf (r.from == r.upto
? "Selected %d item" : "Selected %d items", r.upto - r.from + 1);
- row_buffer_append (buf, msg, APP_ATTR (HIGHLIGHT));
+ app_push_fill (&l, g.ui->label (attrs[0], msg));
free (msg);
}
else if (g.poller_curl.registered)
- row_buffer_append (buf, "Downloading...", APP_ATTR (NORMAL));
+ app_push_fill (&l, g.ui->label (attrs[0], "Downloading..."));
else if (str_map_find (map, "updating_db"))
- row_buffer_append (buf, "Updating database...", APP_ATTR (NORMAL));
+ app_push_fill (&l, g.ui->label (attrs[0], "Updating database..."));
else
- app_write_mpd_status_playlist (buf);
+ {
+ char *status = app_mpd_status_playlist ();
+ app_push_fill (&l, g.ui->label (attrs[0], status));
+ free (status);
+ }
const char *s;
bool repeat = (s = str_map_find (map, "repeat")) && strcmp (s, "0");
@@ -2103,46 +2150,51 @@ app_write_mpd_status (struct row_buffer *buf)
bool single = (s = str_map_find (map, "single")) && strcmp (s, "0");
bool consume = (s = str_map_find (map, "consume")) && strcmp (s, "0");
- struct row_buffer right = row_buffer_make ();
- chtype a[2] = { APP_ATTR (NORMAL), APP_ATTR (HIGHLIGHT) };
- if (repeat) row_buffer_append_args (&right,
- " ", APP_ATTR (NORMAL), "repeat", a[repeat], NULL);
- if (random) row_buffer_append_args (&right,
- " ", APP_ATTR (NORMAL), "random", a[random], NULL);
- if (single) row_buffer_append_args (&right,
- " ", APP_ATTR (NORMAL), "single", a[single], NULL);
- if (consume) row_buffer_append_args (&right,
- " ", APP_ATTR (NORMAL), "consume", a[consume], NULL);
+ if (g.ui->have_icons || repeat)
+ {
+ app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
+ app_push (&l,
+ g.ui->button (attrs[repeat], "repeat", ACTION_MPD_REPEAT));
+ }
+ if (g.ui->have_icons || random)
+ {
+ app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
+ app_push (&l,
+ g.ui->button (attrs[random], "random", ACTION_MPD_RANDOM));
+ }
+ if (g.ui->have_icons || single)
+ {
+ app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
+ app_push (&l,
+ g.ui->button (attrs[single], "single", ACTION_MPD_SINGLE));
+ }
+ if (g.ui->have_icons || consume)
+ {
+ app_push (&l, g.ui->padding (attrs[0], 0.5, 1));
+ app_push (&l,
+ g.ui->button (attrs[consume], "consume", ACTION_MPD_CONSUME));
+ }
- row_buffer_space (buf,
- MAX (0, COLS - buf->total_width - right.total_width),
- APP_ATTR (NORMAL));
- row_buffer_append_buffer (buf, &right);
- row_buffer_free (&right);
+ app_push (&l, g.ui->padding (attrs[0], 0.25, 1));
+ app_flush_layout (&l);
}
static void
-app_draw_statusbar (void)
+app_layout_statusbar (void)
{
- int caret = -1;
- struct row_buffer buf = row_buffer_make ();
if (g.message)
- row_buffer_append (&buf, g.message, APP_ATTR (HIGHLIGHT));
+ app_layout_text (g.message, APP_ATTR (HIGHLIGHT));
else if (g.editor.line)
- caret = line_editor_write (&g.editor, &buf, COLS, APP_ATTR (HIGHLIGHT));
- else if (g.client.state == MPD_CONNECTED)
- app_write_mpd_status (&buf);
-
- move (LINES - 1, 0);
- app_flush_buffer (&buf, COLS, APP_ATTR (NORMAL));
-
- curs_set (0);
- if (caret != -1)
{
- move (LINES - 1, caret);
- curs_set (1);
+ struct layout l = {};
+ app_push (&l, g.ui->padding (APP_ATTR (NORMAL), 0.25, 1));
+ app_push (&l, g.ui->editor (APP_ATTR (HIGHLIGHT)));
+ app_push (&l, g.ui->padding (APP_ATTR (NORMAL), 0.25, 1));
+ app_flush_layout (&l);
}
+ else if (g.client.state == MPD_CONNECTED)
+ app_layout_mpd_status ();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -2172,17 +2224,35 @@ app_fix_view_range (void)
}
static void
+app_on_flip (void *user_data)
+{
+ (void) user_data;
+ poller_idle_reset (&g.flip_event);
+
+ // Waste of time, and may cause X11 to render uninitialised pixmaps.
+ if (g.polling && !g.refresh_event.active)
+ g.ui->flip ();
+}
+
+static void
app_on_refresh (void *user_data)
{
(void) user_data;
poller_idle_reset (&g.refresh_event);
- app_draw_header ();
+ LIST_FOR_EACH (struct widget, w, g.widgets.head)
+ free (w);
+
+ g.widgets = (struct layout) {};
+
+ app_layout_header ();
+ app_layout_view ();
+ app_layout_statusbar ();
+
app_fix_view_range();
- app_draw_view ();
- app_draw_statusbar ();
- refresh ();
+ g.ui->render ();
+ poller_idle_set (&g.flip_event);
}
// --- Actions -----------------------------------------------------------------
@@ -2277,8 +2347,6 @@ app_goto_tab (int tab_index)
// --- Actions -----------------------------------------------------------------
-#include "nncmpp-actions.h"
-
static int
action_resolve (const char *name)
{
@@ -2362,15 +2430,15 @@ app_on_mpd_command_editor_end (bool confirmed)
static size_t
incremental_search_match (const ucs4_t *needle, size_t len,
- const struct row_buffer *row)
+ const ucs4_t *chars, size_t chars_len)
{
// XXX: this is slow and simplistic, but unistring is awkward to use
size_t best = 0;
- for (size_t start = 0; start < row->chars_len; start++)
+ for (size_t start = 0; start < chars_len; start++)
{
size_t i = 0;
- for (; i < len && start + i < row->chars_len; i++)
- if (uc_tolower (needle[i]) != uc_tolower (row->chars[start + i].c))
+ for (; i < len && start + i < chars_len; i++)
+ if (uc_tolower (needle[i]) != uc_tolower (chars[start + i]))
break;
best = MAX (best, i);
}
@@ -2387,10 +2455,19 @@ incremental_search_on_changed (void)
size_t best = 0, current = 0, index = MAX (tab->item_selected, 0), i = 0;
while (i++ < tab->item_count)
{
- struct row_buffer buf = row_buffer_make ();
- tab->on_item_draw (index, &buf, COLS);
- current = incremental_search_match (g.editor.line, g.editor.len, &buf);
- row_buffer_free (&buf);
+ struct str s = str_make ();
+ LIST_FOR_EACH (struct widget, w, tab->on_item_layout (index).head)
+ {
+ str_append (&s, w->text);
+ free (w);
+ }
+
+ size_t len;
+ ucs4_t *text = u8_to_u32 ((const uint8_t *) s.str, s.len, NULL, &len);
+ str_free (&s);
+ current = incremental_search_match
+ (g.editor.line, g.editor.len, text, len);
+ free (text);
if (best < current)
{
best = current;
@@ -2615,92 +2692,103 @@ app_editor_process_action (enum action action)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
-app_process_left_mouse_click (int line, int column, bool double_click)
+app_process_left_mouse_click (struct widget *w, int x, int y, bool double_click)
{
- if (line == g.controls_offset)
+ switch (w->id)
{
- // XXX: there could be a push_widget(buf, text, attrs, handler)
- // function to help with this but it might not be worth it
- enum action action = ACTION_NONE;
- if (column >= 0 && column <= 1) action = ACTION_MPD_PREVIOUS;
- if (column >= 3 && column <= 4) action = ACTION_MPD_TOGGLE;
- if (column >= 6 && column <= 7) action = ACTION_MPD_STOP;
- if (column >= 9 && column <= 10) action = ACTION_MPD_NEXT;
-
- if (action)
- return app_process_action (action);
-
- int gauge_offset = column - g.gauge_offset;
- if (g.gauge_offset < 0
- || gauge_offset < 0 || gauge_offset >= g.gauge_width)
- return false;
-
- float position = (float) gauge_offset / g.gauge_width;
+ case WIDGET_BUTTON:
+ app_process_action (w->subid);
+ break;
+ case WIDGET_GAUGE:
+ {
+ float position = (float) x / w->width;
if (g.song_duration >= 1)
{
char *where = xstrdup_printf ("%f", position * g.song_duration);
MPD_SIMPLE ("seekcur", where);
free (where);
}
+ break;
}
- else if (line == g.tabs_offset)
+ case WIDGET_TAB:
{
- struct tab *winner = NULL;
- int indent = strlen (APP_TITLE);
- if (column < indent)
- {
- app_switch_tab (g.help_tab);
- return true;
- }
- for (struct tab *iter = g.tabs; !winner && iter; iter = iter->next)
- {
- if (column < (indent += iter->name_width))
- winner = iter;
- }
- if (!winner)
- return false;
+ struct tab *tab = g.help_tab;
+ int i = 0;
+ LIST_FOR_EACH (struct tab, iter, g.tabs)
+ if (++i == w->subid)
+ tab = iter;
- app_switch_tab (winner);
+ app_switch_tab (tab);
+ break;
}
- else if (line >= g.header_height)
+ case WIDGET_LIST:
{
struct tab *tab = g.active_tab;
- int row_index = line - g.header_height;
+ int row_index = y / g.ui_vunit;
if (row_index < 0
|| row_index >= (int) tab->item_count - tab->item_top)
return false;
- // TODO: handle the scrollbar a bit better than this
- int visible_items = app_visible_items ();
- if ((int) tab->item_count > visible_items && column == COLS - 1)
- tab->item_top = (float) row_index / visible_items
- * (int) tab->item_count - visible_items / 2;
- else
- tab->item_selected = row_index + tab->item_top;
+ // TODO: Probably will need to fix up item->top
+ // for partially visible items in X11.
+ tab->item_selected = row_index + tab->item_top;
app_invalidate ();
if (double_click)
app_process_action (ACTION_CHOOSE);
+ break;
+ }
+ case WIDGET_SCROLLBAR:
+ {
+ struct tab *tab = g.active_tab;
+ int visible_items = app_visible_items ();
+ tab->item_top = (float) y / w->height
+ * (int) tab->item_count - visible_items / 2;
+ app_invalidate ();
+ }
}
return true;
}
static bool
-app_process_mouse (termo_mouse_event_t type, int line, int column, int button,
+app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
bool double_click)
{
+ // XXX: Terminals don't let us know which button has been released,
+ // so we can't press buttons at that point. We'd need a special "click"
+ // event handler that could be handled better under X11.
if (type != TERMO_MOUSE_PRESS)
return true;
if (g.editor.line)
+ {
line_editor_abort (&g.editor, false);
+ app_invalidate ();
+ }
+
+ struct widget *target = NULL;
+ LIST_FOR_EACH (struct widget, w, g.widgets.head)
+ if (x >= w->x && x < w->x + w->width
+ && y >= w->y && y < w->y + w->height)
+ target = w;
+ if (!target)
+ return false;
- if (button == 1)
- return app_process_left_mouse_click (line, column, double_click);
- else if (button == 4)
- return app_process_action (ACTION_SCROLL_UP);
- else if (button == 5)
- return app_process_action (ACTION_SCROLL_DOWN);
+ x -= target->x;
+ y -= target->y;
+ switch (button)
+ {
+ case 1:
+ return app_process_left_mouse_click (target, x, y, double_click);
+ case 4:
+ if (target->id == WIDGET_LIST)
+ return app_process_action (ACTION_SCROLL_UP);
+ break;
+ case 5:
+ if (target->id == WIDGET_LIST)
+ return app_process_action (ACTION_SCROLL_DOWN);
+ break;
+ }
return false;
}
@@ -2892,7 +2980,7 @@ app_process_termo_event (termo_key_t *event)
bool handled = false;
if ((handled = event->type == TERMO_TYPE_FOCUS))
{
- g.focused = !!event->code.focused;
+ g.ui_focused = !!event->code.focused;
app_invalidate ();
// Senseless fall-through
}
@@ -2930,11 +3018,8 @@ app_process_termo_event (termo_key_t *event)
static struct tab g_current_tab;
-#define DURATION_MAX_LEN (1 /*separator */ + 2 /* h */ + 3 /* m */+ 3 /* s */)
-
-static void
-current_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
- int width)
+static struct layout
+current_tab_on_item_layout (size_t item_index)
{
// TODO: configurable output, maybe dynamically sized columns
compact_map_t map = item_list_get (&g.playlist, item_index);
@@ -2942,23 +3027,26 @@ current_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
const char *title = compact_map_find (map, "title");
chtype attrs = (int) item_index == g.song ? A_BOLD : 0;
+ struct layout l = {};
if (artist && title)
- row_buffer_append_args (buffer,
- artist, attrs, " - ", attrs, title, attrs, NULL);
+ {
+ char *joined = xstrdup_printf ("%s - %s", artist, title);
+ app_push_fill (&l, g.ui->label (attrs, joined));
+ free (joined);
+ }
else
- row_buffer_append (buffer, compact_map_find (map, "file"), attrs);
-
- row_buffer_align (buffer, width - DURATION_MAX_LEN, attrs);
+ app_push_fill (&l, g.ui->label (attrs, compact_map_find (map, "file")));
int duration = -1;
mpd_read_time (compact_map_find (map, "duration"), &duration, NULL);
mpd_read_time (compact_map_find (map, "time"), &duration, NULL);
char *s = duration < 0 ? xstrdup ("-") : app_time_string (duration);
- char *right_aligned = xstrdup_printf ("%*s", DURATION_MAX_LEN, s);
- row_buffer_append (buffer, right_aligned, attrs);
- free (right_aligned);
+ app_push (&l, g.ui->padding (attrs, 1, 1));
+ app_push (&l, g.ui->label (attrs, s));
free (s);
+
+ return l;
}
static void
@@ -3073,7 +3161,7 @@ current_tab_init (void)
tab_init (super, "Current");
super->can_multiselect = true;
super->on_action = current_tab_on_action;
- super->on_item_draw = current_tab_on_item_draw;
+ super->on_item_layout = current_tab_on_item_layout;
return super;
}
@@ -3139,11 +3227,9 @@ library_tab_add (int type, int duration, const char *name, const char *path)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-static void
-library_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
- int width)
+static struct layout
+library_tab_on_item_layout (size_t item_index)
{
- (void) width;
hard_assert (item_index < g_library_tab.items_len);
struct library_tab_item *x = &g_library_tab.items[item_index];
@@ -3156,15 +3242,21 @@ library_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
case LIBRARY_FILE: prefix = " "; name = x->name; break;
default: hard_assert (!"invalid item type");
}
+
chtype attrs = x->type != LIBRARY_FILE ? APP_ATTR (DIRECTORY) : 0;
- row_buffer_append_args (buffer, prefix, attrs, name, attrs, NULL);
- if (x->duration < 0)
- return;
+ struct layout l = {};
- char *s = app_time_string (x->duration);
- row_buffer_align (buffer, width - 2 /* gap */ - strlen (s), 0);
- row_buffer_append_args (buffer, " " /* gap */, 0, s, 0, NULL);
- free (s);
+ app_push (&l, g.ui->label (attrs, prefix));
+ app_push_fill (&l, g.ui->label (attrs, name));
+
+ if (x->duration >= 0)
+ {
+ char *s = app_time_string (x->duration);
+ app_push (&l, g.ui->padding (0, 1, 1));
+ app_push (&l, g.ui->label (attrs, s));
+ free (s);
+ }
+ return l;
}
static char
@@ -3533,7 +3625,7 @@ library_tab_init (void)
tab_init (super, "Library");
super->can_multiselect = true;
super->on_action = library_tab_on_action;
- super->on_item_draw = library_tab_on_item_draw;
+ super->on_item_layout = library_tab_on_item_layout;
return super;
}
@@ -3792,12 +3884,12 @@ streams_tab_on_action (enum action action)
return true;
}
-static void
-streams_tab_on_item_draw (size_t item_index, struct row_buffer *buffer,
- int width)
+static struct layout
+streams_tab_on_item_layout (size_t item_index)
{
- (void) width;
- row_buffer_append (buffer, g.streams.vector[item_index], 0);
+ struct layout l = {};
+ app_push_fill (&l, g.ui->label (0, g.streams.vector[item_index]));
+ return l;
}
static struct tab *
@@ -3806,7 +3898,7 @@ streams_tab_init (void)
static struct tab super;
tab_init (&super, "Streams");
super.on_action = streams_tab_on_action;
- super.on_item_draw = streams_tab_on_item_draw;
+ super.on_item_layout = streams_tab_on_item_layout;
super.item_count = g.streams.len;
return &super;
}
@@ -3821,23 +3913,19 @@ static struct
}
g_info_tab;
-static void
-info_tab_on_item_draw (size_t item_index, struct row_buffer *buffer, int width)
+static struct layout
+info_tab_on_item_layout (size_t item_index)
{
- (void) width;
-
- // It looks like we could do with a generic list structure that just
- // stores formatted row_buffers. Let's see for other tabs:
- // - Current -- unusable, has dynamic column alignment
- // - Library -- could work for the "icons"
- // - Streams -- useless
- // - Debug -- it'd take up considerably more space
- // However so far we're only showing show key-value pairs.
+ const char *key = g_info_tab.keys.vector[item_index];
+ const char *value = g_info_tab.values.vector[item_index];
+ struct layout l = {};
- row_buffer_append_args (buffer,
- g_info_tab.keys.vector[item_index], A_BOLD, ":", A_BOLD, NULL);
- row_buffer_space (buffer, 8 - buffer->total_width, 0);
- row_buffer_append (buffer, g_info_tab.values.vector[item_index], 0);
+ char *prefix = xstrdup_printf ("%s:", key);
+ app_push (&l, g.ui->label (A_BOLD, prefix))
+ ->width = 8 * g.ui_hunit;
+ app_push (&l, g.ui->padding (0, 0.5, 1));
+ app_push_fill (&l, g.ui->label (0, value));
+ return l;
}
static void
@@ -3879,7 +3967,7 @@ info_tab_init (void)
struct tab *super = &g_info_tab.super;
tab_init (super, "Info");
- super->on_item_draw = info_tab_on_item_draw;
+ super->on_item_layout = info_tab_on_item_layout;
return super;
}
@@ -3949,7 +4037,7 @@ help_tab_group (struct binding *keys, size_t len, struct strv *out,
{
char *joined = strv_join (&ass, ", ");
strv_append_owned (out, xstrdup_printf
- (" %-30s %s", g_action_descriptions[i], joined));
+ (" %s%c%s", g_action_descriptions[i], 0, joined));
free (joined);
bound[i] = true;
@@ -3966,19 +4054,27 @@ help_tab_unbound (struct strv *out, bool bound[ACTION_COUNT])
if (!bound[i])
{
strv_append_owned (out,
- xstrdup_printf (" %-30s", g_action_descriptions[i]));
+ xstrdup_printf (" %s%c", g_action_descriptions[i], 0));
help_tab_assign_action (i);
}
}
-static void
-help_tab_on_item_draw (size_t item_index, struct row_buffer *buffer, int width)
+static struct layout
+help_tab_on_item_layout (size_t item_index)
{
- (void) width;
-
hard_assert (item_index < g_help_tab.lines.len);
const char *line = g_help_tab.lines.vector[item_index];
- row_buffer_append (buffer, line, *line == ' ' ? 0 : A_BOLD);
+
+ struct layout l = {};
+ app_push_fill (&l, g.ui->label (*line == ' ' ? 0 : A_BOLD, line));
+
+ const char *definition = strchr (line, 0) + 1;
+ if (*line == ' ' && *definition)
+ {
+ app_push (&l, g.ui->padding (0, 0.5, 1));
+ app_push_fill (&l, g.ui->label (0, definition));
+ }
+ return l;
}
static struct tab *
@@ -4013,7 +4109,7 @@ help_tab_init (void)
struct tab *super = &g_help_tab.super;
tab_init (super, "Help");
super->on_action = help_tab_on_action;
- super->on_item_draw = help_tab_on_item_draw;
+ super->on_item_layout = help_tab_on_item_layout;
super->item_count = lines->len;
return super;
}
@@ -4035,8 +4131,8 @@ static struct
}
g_debug_tab;
-static void
-debug_tab_on_item_draw (size_t item_index, struct row_buffer *buffer, int width)
+static struct layout
+debug_tab_on_item_layout (size_t item_index)
{
hard_assert (item_index < g_debug_tab.items_len);
struct debug_item *item = &g_debug_tab.items[item_index];
@@ -4048,14 +4144,13 @@ debug_tab_on_item_draw (size_t item_index, struct row_buffer *buffer, int width)
char *prefix = xstrdup_printf
("%s.%03d", buf, (int) (item->timestamp % 1000));
- row_buffer_append (buffer, prefix, 0);
- free (prefix);
-
- row_buffer_append (buffer, " ", item->attrs);
- row_buffer_append (buffer, item->text, item->attrs);
- // We override the formatting including colors -- do it for the whole line
- row_buffer_align (buffer, width, item->attrs);
+ struct layout l = {};
+ app_push (&l, g.ui->label (0, prefix));
+ app_push (&l, g.ui->padding (item->attrs, 0.5, 1));
+ app_push_fill (&l, g.ui->label (item->attrs, item->text));
+ free (prefix);
+ return l;
}
static void
@@ -4079,7 +4174,7 @@ debug_tab_init (void)
struct tab *super = &g_debug_tab.super;
tab_init (super, "Debug");
- super->on_item_draw = debug_tab_on_item_draw;
+ super->on_item_layout = debug_tab_on_item_layout;
return super;
}
@@ -4092,21 +4187,14 @@ spectrum_redraw (void)
{
// A full refresh would be too computationally expensive,
// let's hack around it in this case
- if (g.spectrum_row != -1)
- {
- // Don't mess up the line editor caret, when it's shown
- int last_x, last_y;
- getyx (stdscr, last_y, last_x);
+ struct widget *spectrum = NULL;
+ LIST_FOR_EACH (struct widget, w, g.widgets.head)
+ if (w->id == WIDGET_SPECTRUM)
+ spectrum = w;
+ if (spectrum)
+ spectrum->on_render (spectrum);
- attrset (APP_ATTR (TAB_BAR));
- mvaddstr (g.spectrum_row, g.spectrum_column, g.spectrum.spectrum);
- attrset (0);
-
- move (last_y, last_x);
- refresh ();
- }
- else
- app_invalidate ();
+ poller_idle_set (&g.flip_event);
}
// When any problem occurs with the FIFO, we'll just give up on it completely
@@ -4120,7 +4208,6 @@ spectrum_discard_fifo (void)
g.spectrum_fd = -1;
spectrum_free (&g.spectrum);
- g.spectrum_row = g.spectrum_column = -1;
app_invalidate ();
}
}
@@ -4746,75 +4833,340 @@ app_on_reconnect (void *user_data)
free (address);
}
-// --- Signals -----------------------------------------------------------------
+// --- TUI ---------------------------------------------------------------------
-static int g_signal_pipe[2]; ///< A pipe used to signal... signals
+static void
+tui_flush_buffer (struct widget *self, struct row_buffer *buf)
+{
+ move (self->y, self->x);
-/// Program termination has been requested by a signal
-static volatile sig_atomic_t g_termination_requested;
-/// The window has changed in size
-static volatile sig_atomic_t g_winch_received;
+ int space = MIN (self->width, g.ui_width - self->x);
+ row_buffer_align (buf, space, self->attrs);
+ row_buffer_flush (buf);
+ row_buffer_free (buf);
+}
static void
-signals_postpone_handling (char id)
+tui_render_padding (struct widget *self)
{
- int original_errno = errno;
- if (write (g_signal_pipe[1], &id, 1) == -1)
- soft_assert (errno == EAGAIN);
- errno = original_errno;
+ struct row_buffer buf = row_buffer_make ();
+ tui_flush_buffer (self, &buf);
+}
+
+static struct widget *
+tui_make_padding (chtype attrs, float width, float height)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 2);
+ w->text[0] = ' ';
+ w->on_render = tui_render_padding;
+ w->attrs = attrs;
+ w->width = width * 2;
+ w->height = height;
+ return w;
}
static void
-signals_superhandler (int signum)
+tui_render_label (struct widget *self)
{
- switch (signum)
+ struct row_buffer buf = row_buffer_make ();
+ row_buffer_append (&buf, self->text, self->attrs);
+ tui_flush_buffer (self, &buf);
+}
+
+static struct widget *
+tui_make_label (chtype attrs, const char *label)
+{
+ size_t len = strlen (label);
+ struct widget *w = xcalloc (1, sizeof *w + len + 1);
+ w->on_render = tui_render_label;
+ w->attrs = attrs;
+ memcpy (w + 1, label, len);
+
+ struct row_buffer buf = row_buffer_make ();
+ row_buffer_append (&buf, w->text, w->attrs);
+ w->width = buf.total_width;
+ w->height = 1;
+ row_buffer_free (&buf);
+ return w;
+}
+
+static struct widget *
+tui_make_button (chtype attrs, const char *label, enum action a)
+{
+ struct widget *w = tui_make_label (attrs, label);
+ w->id = WIDGET_BUTTON;
+ w->subid = a;
+ return w;
+}
+
+static void
+tui_render_gauge (struct widget *self)
+{
+ struct row_buffer buf = row_buffer_make ();
+ if (g.state == PLAYER_STOPPED || g.song_elapsed < 0 || g.song_duration < 1)
+ goto out;
+
+ float ratio = (float) g.song_elapsed / g.song_duration;
+ if (ratio < 0) ratio = 0;
+ if (ratio > 1) ratio = 1;
+
+ // Always compute it in exactly eight times the resolution,
+ // because sometimes Unicode is even useful
+ int len_left = ratio * self->width * 8 + 0.5;
+
+ static const char *partials[] = { " ", "▏", "▎", "▍", "▌", "▋", "▊", "▉" };
+ int remainder = len_left % 8;
+ len_left /= 8;
+
+ const char *partial = NULL;
+ if (g.use_partial_boxes)
+ partial = partials[remainder];
+ else
+ len_left += remainder >= (int) 4;
+
+ int len_right = self->width - len_left;
+ row_buffer_space (&buf, len_left, APP_ATTR (ELAPSED));
+ if (partial && len_right-- > 0)
+ row_buffer_append (&buf, partial, APP_ATTR (REMAINS));
+ row_buffer_space (&buf, len_right, APP_ATTR (REMAINS));
+
+out:
+ tui_flush_buffer (self, &buf);
+}
+
+// TODO: Perhaps it should save the number within.
+static struct widget *
+tui_make_gauge (chtype attrs)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = tui_render_gauge;
+ w->attrs = attrs;
+ w->width = -1;
+ w->height = 1;
+ return w;
+}
+
+static void
+tui_render_spectrum (struct widget *self)
+{
+ // Don't mess up the line editor caret, when it's shown
+ int last_x, last_y;
+ getyx (stdscr, last_y, last_x);
+
+ struct row_buffer buf = row_buffer_make ();
+#ifdef WITH_FFTW
+ row_buffer_append (&buf, g.spectrum.rendered, self->attrs);
+#endif // WITH_FFTW
+ tui_flush_buffer (self, &buf);
+
+ move (last_y, last_x);
+}
+
+static struct widget *
+tui_make_spectrum (chtype attrs, int width)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = tui_render_spectrum;
+ w->attrs = attrs;
+ w->width = width;
+ w->height = 1;
+ return w;
+}
+
+static void
+tui_render_scrollbar (struct widget *self)
+{
+ // This assumes that we can write to the one-before-last column,
+ // i.e. that it's not covered by any double-wide character (and that
+ // ncurses comes to the right results when counting characters).
+ struct tab *tab = g.active_tab;
+ int visible_items = app_visible_items ();
+
+ hard_assert (tab->item_count != 0);
+ if (!g.use_partial_boxes)
{
- case SIGWINCH:
- g_winch_received = true;
- signals_postpone_handling ('w');
- break;
- case SIGINT:
- case SIGTERM:
- g_termination_requested = true;
- signals_postpone_handling ('t');
- break;
- default:
- hard_assert (!"unhandled signal");
+ struct scrollbar bar = app_compute_scrollbar (tab, visible_items, 1);
+ for (int row = 0; row < visible_items; row++)
+ {
+ move (self->y + row, self->x);
+ if (row < bar.start || row >= bar.start + bar.length)
+ addch (' ' | self->attrs);
+ else
+ addch (' ' | self->attrs | A_REVERSE);
+ }
+ return;
+ }
+
+ struct scrollbar bar = app_compute_scrollbar (tab, visible_items, 8);
+ bar.length += bar.start;
+
+ int start_part = bar.start % 8; bar.start /= 8;
+ int end_part = bar.length % 8; bar.length /= 8;
+
+ // Even with this, the solid part must be at least one character high
+ static const char *partials[] = { "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁" };
+
+ for (int row = 0; row < visible_items; row++)
+ {
+ chtype attrs = self->attrs;
+ if (row > bar.start && row <= bar.length)
+ attrs ^= A_REVERSE;
+
+ const char *c = " ";
+ if (row == bar.start) c = partials[start_part];
+ if (row == bar.length) c = partials[end_part];
+
+ move (self->y + row, self->x);
+
+ struct row_buffer buf = row_buffer_make ();
+ row_buffer_append (&buf, c, attrs);
+ row_buffer_flush (&buf);
+ row_buffer_free (&buf);
}
}
+static struct widget *
+tui_make_scrollbar (chtype attrs)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = tui_render_scrollbar;
+ w->attrs = attrs;
+ w->width = 1;
+ return w;
+}
+
static void
-signals_setup_handlers (void)
+tui_render_list (struct widget *self)
{
- if (pipe (g_signal_pipe) == -1)
- exit_fatal ("%s: %s", "pipe", strerror (errno));
+ struct tab *tab = g.active_tab;
+ int to_show =
+ MIN (app_visible_items (), (int) tab->item_count - tab->item_top);
+ for (int row = 0; row < to_show; row++)
+ {
+ int item_index = tab->item_top + row;
+ struct widget *head = app_layout_row (tab, item_index);
+ widget_redistribute (head, self->width);
- set_cloexec (g_signal_pipe[0]);
- set_cloexec (g_signal_pipe[1]);
+ int x = self->x;
+ int y = self->y + row * g.ui_vunit;
+ LIST_FOR_EACH (struct widget, w, head)
+ {
+ w->x += x;
+ w->y += y;
+ }
- // So that the pipe cannot overflow; it would make write() block within
- // the signal handler, which is something we really don't want to happen.
- // The same holds true for read().
- set_blocking (g_signal_pipe[0], false);
- set_blocking (g_signal_pipe[1], false);
+ LIST_FOR_EACH (struct widget, w, head)
+ {
+ w->on_render (w);
+ free (w);
+ }
+ }
+}
- signal (SIGPIPE, SIG_IGN);
+static struct widget *
+tui_make_list (void)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->width = -1;
+ w->height = g.active_tab->item_count;
+ w->on_render = tui_render_list;
+ return w;
+}
- struct sigaction sa;
- sa.sa_flags = SA_RESTART;
- sa.sa_handler = signals_superhandler;
- sigemptyset (&sa.sa_mask);
+static void
+tui_render_editor (struct widget *self)
+{
+ struct row_buffer buf = row_buffer_make ();
+ int caret = line_editor_write (&g.editor, &buf, self->width, self->attrs);
+ tui_flush_buffer (self, &buf);
- if (sigaction (SIGWINCH, &sa, NULL) == -1
- || sigaction (SIGINT, &sa, NULL) == -1
- || sigaction (SIGTERM, &sa, NULL) == -1)
- exit_fatal ("sigaction: %s", strerror (errno));
+ // FIXME: This should be at the end of of tui_render().
+ move (self->y, self->x + caret);
+ curs_set (1);
}
-// --- Initialisation, event handling ------------------------------------------
+static struct widget *
+tui_make_editor (chtype attrs)
+{
+ // TODO: This should ideally measure the text, and copy it to w->text.
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = tui_render_editor;
+ w->attrs = attrs;
+ w->width = -1;
+ w->height = 1;
+ return w;
+}
+
+static void
+tui_render (void)
+{
+ erase ();
+ curs_set (0);
+
+ LIST_FOR_EACH (struct widget, w, g.widgets.head)
+ if (w->width >= 0 && w->height >= 0)
+ w->on_render (w);
+}
+
+static void
+tui_flip (void)
+{
+ // Curses handles double-buffering for us automatically.
+ refresh ();
+}
static void
-app_on_tty_event (termo_key_t *event, int64_t event_ts)
+tui_winch (void)
+{
+ // The standard endwin/refresh sequence makes the terminal flicker
+#if defined HAVE_RESIZETERM && defined TIOCGWINSZ
+ struct winsize size;
+ if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
+ {
+ char *row = getenv ("LINES");
+ char *col = getenv ("COLUMNS");
+ unsigned long tmp;
+ resizeterm (
+ (row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row,
+ (col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col);
+ }
+#else // HAVE_RESIZETERM && TIOCGWINSZ
+ endwin ();
+ refresh ();
+#endif // HAVE_RESIZETERM && TIOCGWINSZ
+
+ g.ui_width = COLS;
+ g.ui_height = LINES;
+ app_invalidate ();
+}
+
+static void
+tui_destroy (void)
+{
+ endwin ();
+}
+
+static struct ui tui_ui =
+{
+ .padding = tui_make_padding,
+ .label = tui_make_label,
+ .button = tui_make_button,
+ .gauge = tui_make_gauge,
+ .spectrum = tui_make_spectrum,
+ .scrollbar = tui_make_scrollbar,
+ .list = tui_make_list,
+ .editor = tui_make_editor,
+
+ .render = tui_render,
+ .flip = tui_flip,
+ .winch = tui_winch,
+ .destroy = tui_destroy,
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+tui_on_tty_event (termo_key_t *event, int64_t event_ts)
{
// Simple double click detection via release--press delay, only a bit
// complicated by the fact that we don't know what's being released
@@ -4831,7 +5183,7 @@ app_on_tty_event (termo_key_t *event, int64_t event_ts)
&& event_ts - last_event_ts < 500
&& type_last == TERMO_MOUSE_RELEASE && type == TERMO_MOUSE_PRESS
&& y_last == y && x_last == x && last_button == button;
- if (!app_process_mouse (type, y, x, button, double_click))
+ if (!app_process_mouse (type, x, y, button, double_click))
beep ();
// Prevent interpreting triple clicks as two double clicks
@@ -4848,7 +5200,7 @@ app_on_tty_event (termo_key_t *event, int64_t event_ts)
}
static void
-app_on_tty_readable (const struct pollfd *fd, void *user_data)
+tui_on_tty_readable (const struct pollfd *fd, void *user_data)
{
(void) user_data;
if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
@@ -4861,7 +5213,7 @@ app_on_tty_readable (const struct pollfd *fd, void *user_data)
int64_t event_ts = clock_msec (CLOCK_BEST);
termo_result_t res;
while ((res = termo_getkey (g.tk, &event)) == TERMO_RES_KEY)
- app_on_tty_event (&event, event_ts);
+ tui_on_tty_event (&event, event_ts);
if (res == TERMO_RES_AGAIN)
poller_timer_set (&g.tk_timer, termo_get_waittime (g.tk));
@@ -4870,7 +5222,7 @@ app_on_tty_readable (const struct pollfd *fd, void *user_data)
}
static void
-app_on_key_timer (void *user_data)
+tui_on_key_timer (void *user_data)
{
(void) user_data;
@@ -4881,6 +5233,1006 @@ app_on_key_timer (void *user_data)
}
static void
+tui_init (void)
+{
+ poller_fd_set (&g.tty_event, POLLIN);
+ if (!termo_start (g.tk) || !initscr () || nonl () == ERR)
+ exit_fatal ("failed to set up the terminal");
+
+ g.ui = &tui_ui;
+ g.ui_width = COLS;
+ g.ui_height = LINES;
+ g.ui_vunit = 1;
+ g.ui_hunit = 1;
+
+ // By default we don't use any colors so they're not required...
+ if (start_color () == ERR
+ || use_default_colors () == ERR
+ || COLOR_PAIRS <= ATTRIBUTE_COUNT)
+ return;
+
+ for (int a = 0; a < ATTRIBUTE_COUNT; a++)
+ {
+ // ...thus we can reset back to defaults even after initializing some
+ // FIXME: that's a lie now, MULTISELECT requires a colour
+ if (g.attrs[a].fg >= COLORS || g.attrs[a].fg < -1
+ || g.attrs[a].bg >= COLORS || g.attrs[a].bg < -1)
+ {
+ app_init_attributes ();
+ return;
+ }
+
+ init_pair (a + 1, g.attrs[a].fg, g.attrs[a].bg);
+ g.attrs[a].attrs |= COLOR_PAIR (a + 1);
+ }
+}
+
+// --- X11 ---------------------------------------------------------------------
+
+#ifdef WITH_X11
+
+static XRenderColor x11_default_fg = { .alpha = 0xffff };
+static XRenderColor x11_default_bg = { 0xffff, 0xffff, 0xffff, 0xffff };
+static XErrorHandler x11_default_error_handler;
+
+static XftFont *
+x11_font (struct widget *self)
+{
+ return (self->attrs & A_BOLD) ? g.xft_bold : g.xft_regular;
+}
+
+static XRenderColor *
+x11_fg_attrs (chtype attrs)
+{
+ int pair = PAIR_NUMBER (attrs);
+ if (!pair--)
+ return &x11_default_fg;
+ return (attrs & A_REVERSE) ? &g.x_bg[pair] : &g.x_fg[pair];
+}
+
+static XRenderColor *
+x11_fg (struct widget *self)
+{
+ return x11_fg_attrs (self->attrs);
+}
+
+static XRenderColor *
+x11_bg_attrs (chtype attrs)
+{
+ int pair = PAIR_NUMBER (attrs);
+ if (!pair--)
+ return &x11_default_bg;
+ return (attrs & A_REVERSE) ? &g.x_fg[pair] : &g.x_bg[pair];
+}
+
+static XRenderColor *
+x11_bg (struct widget *self)
+{
+ return x11_bg_attrs (self->attrs);
+}
+
+static void
+x11_render_padding (struct widget *self)
+{
+ if (PAIR_NUMBER (self->attrs))
+ {
+ XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+ x11_bg (self), self->x, self->y, self->width, self->height);
+ }
+ if (self->attrs & A_UNDERLINE)
+ {
+ XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+ x11_fg (self), self->x, self->y + self->height - 1, self->width, 1);
+ }
+}
+
+static struct widget *
+x11_make_padding (chtype attrs, float width, float height)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 2);
+ w->text[0] = ' ';
+ w->on_render = x11_render_padding;
+ w->attrs = attrs;
+ w->width = g.ui_vunit * width;
+ w->height = g.ui_vunit * height;
+ return w;
+}
+
+static void
+x11_render_label (struct widget *self)
+{
+ x11_render_padding (self);
+
+ int space = MIN (self->width, g.ui_width - self->x);
+ if (space <= 0)
+ return;
+
+ // TODO: Try to avoid re-measuring on each render.
+ XftFont *font = x11_font (self);
+ XGlyphInfo extents = {};
+ XftTextExtentsUtf8 (g.dpy, font,
+ (const FcChar8 *) self->text, strlen (self->text), &extents);
+ if (extents.xOff <= space)
+ {
+ XftColor color = { .color = *x11_fg (self) };
+ XftDrawStringUtf8 (g.xft_draw, &color, font,
+ self->x, self->y + font->ascent,
+ (const FcChar8 *) self->text, strlen (self->text));
+ return;
+ }
+
+ // XRender doesn't extend gradients beyond their end stops.
+ XRenderColor solid = *x11_fg (self), colors[3] = { solid, solid, solid };
+ colors[2].alpha = 0;
+
+ double portion = MIN (1, 2.0 * font->height / space);
+ XFixed stops[3] = { 0, XDoubleToFixed (1 - portion), XDoubleToFixed (1) };
+ XLinearGradient gradient = { {}, { XDoubleToFixed (space), 0 } };
+
+ // Note that this masking is a very expensive operation.
+ Picture source =
+ XRenderCreateLinearGradient (g.dpy, &gradient, stops, colors, 3);
+ XftTextRenderUtf8 (g.dpy, PictOpOver, source, font, g.x11_pixmap_picture,
+ -self->x, 0, self->x, self->y + font->ascent,
+ (const FcChar8 *) self->text, strlen (self->text));
+ XRenderFreePicture (g.dpy, source);
+}
+
+static struct widget *
+x11_make_label (chtype attrs, const char *label)
+{
+ size_t len = strlen (label);
+ struct widget *w = xcalloc (1, sizeof *w + len + 1);
+ w->on_render = x11_render_label;
+ w->attrs = attrs;
+ memcpy (w + 1, label, len);
+
+ XftFont *font = x11_font (w);
+ XGlyphInfo extents = {};
+ XftTextExtentsUtf8 (g.dpy, font, (const FcChar8 *) label, len, &extents);
+ w->width = extents.xOff;
+ w->height = font->height;
+ return w;
+}
+
+// On a 20x20 raster to make it feasible to design on paper.
+static const XPointDouble x11_stop = {INFINITY, INFINITY},
+ x11_icon_previous[] =
+ {
+ {10, 0}, {0, 10}, {10, 20}, x11_stop,
+ {20, 0}, {10, 10}, {20, 20}, x11_stop, x11_stop,
+ },
+ x11_icon_pause[] =
+ {
+ {1, 0}, {7, 0}, {7, 20}, {1, 20}, x11_stop,
+ {13, 0}, {19, 0}, {19, 20}, {13, 20}, x11_stop, x11_stop,
+ },
+ x11_icon_play[] =
+ {
+ {0, 0}, {20, 10}, {0, 20}, x11_stop, x11_stop,
+ },
+ x11_icon_stop[] =
+ {
+ {0, 0}, {20, 0}, {20, 20}, {0, 20}, x11_stop, x11_stop,
+ },
+ x11_icon_next[] =
+ {
+ {0, 0}, {10, 10}, {0, 20}, x11_stop,
+ {10, 0}, {20, 10}, {10, 20}, x11_stop, x11_stop,
+ },
+ x11_icon_repeat[] =
+ {
+ {0, 12}, {0, 6}, {3, 3}, {13, 3}, {13, 0}, {20, 4.5},
+ {13, 9}, {13, 6}, {3, 6}, {3, 10}, x11_stop,
+ {0, 15.5}, {7, 11}, {7, 14}, {17, 14}, {17, 10}, {20, 8},
+ {20, 14}, {17, 17}, {7, 17}, {7, 20}, x11_stop, x11_stop,
+ },
+ x11_icon_random[] =
+ {
+ {0, 6}, {0, 3}, {5, 3}, {6, 4.5}, {4, 7.5}, {3, 6}, x11_stop,
+ {9, 15.5}, {11, 12.5}, {12, 14}, {13, 14}, {13, 11}, {20, 15.5},
+ {13, 20}, {13, 17}, {10, 17}, x11_stop,
+ {0, 17}, {0, 14}, {3, 14}, {10, 3}, {13, 3}, {13, 0}, {20, 4.5},
+ {13, 9}, {13, 6}, {12, 6}, {5, 17}, x11_stop, x11_stop,
+ },
+ x11_icon_single[] =
+ {
+ {7, 6}, {7, 4}, {9, 2}, {12, 2}, {12, 15}, {14, 15}, {14, 18},
+ {7, 18}, {7, 15}, {9, 15}, {9, 6}, x11_stop, x11_stop,
+ },
+ x11_icon_consume[] =
+ {
+ {0, 13}, {0, 7}, {4, 3}, {10, 3}, {14, 7}, {5, 10}, {14, 13},
+ {10, 17}, {4, 17}, x11_stop,
+ {16, 12}, {16, 8}, {20, 8}, {20, 12}, x11_stop, x11_stop,
+ };
+
+static const XPointDouble *
+x11_icon_for_action (enum action action)
+{
+ switch (action)
+ {
+ case ACTION_MPD_PREVIOUS:
+ return x11_icon_previous;
+ case ACTION_MPD_TOGGLE:
+ return g.state == PLAYER_PLAYING ? x11_icon_pause : x11_icon_play;
+ case ACTION_MPD_STOP:
+ return x11_icon_stop;
+ case ACTION_MPD_NEXT:
+ return x11_icon_next;
+ case ACTION_MPD_REPEAT:
+ return x11_icon_repeat;
+ case ACTION_MPD_RANDOM:
+ return x11_icon_random;
+ case ACTION_MPD_SINGLE:
+ return x11_icon_single;
+ case ACTION_MPD_CONSUME:
+ return x11_icon_consume;
+ default:
+ return NULL;
+ }
+}
+
+static void
+x11_render_button (struct widget *self)
+{
+ x11_render_padding (self);
+
+ const XPointDouble *icon = x11_icon_for_action (self->subid);
+ if (!icon)
+ {
+ x11_render_label (self);
+ return;
+ }
+
+ size_t total = 0;
+ for (size_t i = 0; icon[i].x != INFINITY || icon[i - 1].x != INFINITY; i++)
+ total++;
+
+ // TODO: There should be an attribute for buttons, to handle this better.
+ XRenderColor color = *x11_fg (self);
+ if (!(self->attrs & A_BOLD))
+ {
+ color.alpha /= 2;
+ color.red /= 2;
+ color.green /= 2;
+ color.blue /= 2;
+ }
+
+ Picture source = XRenderCreateSolidFill (g.dpy, &color);
+ const XRenderPictFormat *format
+ = XRenderFindStandardFormat (g.dpy, PictStandardA8);
+
+ int x = self->x, y = self->y + (self->height - self->width) / 2;
+ XPointDouble buffer[total], *p = buffer;
+ for (size_t i = 0; i < total; i++)
+ if (icon[i].x != INFINITY)
+ {
+ p->x = x + icon[i].x / 20.0 * self->width;
+ p->y = y + icon[i].y / 20.0 * self->width;
+ p++;
+ }
+ else if (p != buffer)
+ {
+ XRenderCompositeDoublePoly (g.dpy, PictOpOver,
+ source, g.x11_pixmap_picture, format,
+ 0, 0, 0, 0, buffer, p - buffer, EvenOddRule);
+ p = buffer;
+ }
+ XRenderFreePicture (g.dpy, source);
+}
+
+static struct widget *
+x11_make_button (chtype attrs, const char *label, enum action a)
+{
+ struct widget *w = x11_make_label (attrs, label);
+ w->id = WIDGET_BUTTON;
+ w->subid = a;
+
+ if (x11_icon_for_action (a))
+ {
+ w->on_render = x11_render_button;
+
+ // It should be padded by the caller horizontally.
+ w->height = g.ui_vunit;
+ w->width = w->height * 3 / 4;
+ }
+ return w;
+}
+
+static void
+x11_render_gauge (struct widget *self)
+{
+ x11_render_padding (self);
+ if (g.state == PLAYER_STOPPED || g.song_elapsed < 0 || g.song_duration < 1)
+ return;
+
+ int part = (float) g.song_elapsed / g.song_duration * self->width;
+ XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+ x11_bg_attrs (APP_ATTR (ELAPSED)),
+ self->x,
+ self->y + self->height / 8,
+ part,
+ self->height * 3 / 4);
+ XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+ x11_bg_attrs (APP_ATTR (REMAINS)),
+ self->x + part,
+ self->y + self->height / 8,
+ self->width - part,
+ self->height * 3 / 4);
+}
+
+// TODO: Perhaps it should save the number within.
+static struct widget *
+x11_make_gauge (chtype attrs)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = x11_render_gauge;
+ w->attrs = attrs;
+ w->width = -1;
+ w->height = g.ui_vunit;
+ return w;
+}
+
+static void
+x11_render_spectrum (struct widget *self)
+{
+ x11_render_padding (self);
+
+#ifdef WITH_FFTW
+ int step = self->width / g.spectrum.bars;
+ for (int i = 0; i < g.spectrum.bars; i++)
+ {
+ float value = g.spectrum.spectrum[i];
+ int height = round ((self->height - 2) * value);
+ XRenderFillRectangle (g.dpy, PictOpSrc,
+ g.x11_pixmap_picture, x11_fg (self),
+ self->x + i * step,
+ self->y + self->height - 1 - height,
+ step,
+ height);
+ }
+#endif // WITH_FFTW
+}
+
+static struct widget *
+x11_make_spectrum (chtype attrs, int width)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = x11_render_spectrum;
+ w->attrs = attrs;
+ w->width = width * g.ui_vunit / 2;
+ w->height = g.ui_vunit;
+ return w;
+}
+
+static void
+x11_render_scrollbar (struct widget *self)
+{
+ x11_render_padding (self);
+
+ struct tab *tab = g.active_tab;
+ // FIXME: This isn't an integer number in this case.
+ int visible_items = app_visible_items ();
+ struct scrollbar bar =
+ app_compute_scrollbar (tab, visible_items, g.ui_vunit);
+
+ XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+ x11_fg_attrs (self->attrs),
+ self->x,
+ self->y + bar.start,
+ self->width,
+ bar.length);
+}
+
+static struct widget *
+x11_make_scrollbar (chtype attrs)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = x11_render_scrollbar;
+ w->attrs = attrs;
+ w->width = g.ui_vunit / 2;
+ return w;
+}
+
+// TODO: Handle partial items, otherwise this is the same as tui_render_list().
+static void
+x11_render_list (struct widget *self)
+{
+ x11_render_padding (self);
+
+ struct tab *tab = g.active_tab;
+ int to_show =
+ MIN (app_visible_items (), (int) tab->item_count - tab->item_top);
+ for (int row = 0; row < to_show; row++)
+ {
+ int item_index = tab->item_top + row;
+ struct widget *head = app_layout_row (tab, item_index);
+ widget_redistribute (head, self->width);
+
+ int x = self->x;
+ int y = self->y + row * g.ui_vunit;
+ LIST_FOR_EACH (struct widget, w, head)
+ {
+ w->x += x;
+ w->y += y;
+ }
+
+ LIST_FOR_EACH (struct widget, w, head)
+ {
+ w->on_render (w);
+ free (w);
+ }
+ }
+}
+
+static struct widget *
+x11_make_list (void)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = x11_render_list;
+ return w;
+}
+
+static void
+x11_render_editor (struct widget *self)
+{
+ x11_render_padding (self);
+
+ XftFont *font = x11_font (self);
+ XftColor color = { .color = *x11_fg (self) };
+
+ // A simplistic adaptation of line_editor_write() follows.
+ int x = self->x, y = self->y + font->ascent;
+ XGlyphInfo extents = {};
+ if (g.editor.prompt)
+ {
+ FT_UInt i = XftCharIndex (g.dpy, font, g.editor.prompt);
+ XftDrawGlyphs (g.xft_draw, &color, font, x, y, &i, 1);
+ XftGlyphExtents (g.dpy, font, &i, 1, &extents);
+ x += extents.xOff + g.ui_vunit / 4;
+ }
+
+ // TODO: Make this scroll around the caret, and fade like labels.
+ XftDrawString32 (g.xft_draw, &color, font, x, y,
+ g.editor.line, g.editor.len);
+
+ XftTextExtents32 (g.dpy, font, g.editor.line, g.editor.point, &extents);
+ XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+ &color.color, x + extents.xOff, self->y, 2, self->height);
+}
+
+static struct widget *
+x11_make_editor (chtype attrs)
+{
+ // TODO: This should ideally measure the text, and copy it to w->text.
+ struct widget *w = xcalloc (1, sizeof *w + 1);
+ w->on_render = x11_render_editor;
+ w->attrs = attrs;
+ w->width = -1;
+ w->height = g.ui_vunit;
+ return w;
+}
+
+static void
+x11_render (void)
+{
+ XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+ &x11_default_bg, 0, 0, g.ui_width, g.ui_height);
+
+ // TODO: Consider setting clip rectangles (not particularly needed).
+ LIST_FOR_EACH (struct widget, w, g.widgets.head)
+ if (w->width && w->height)
+ w->on_render (w);
+ poller_idle_set (&g.xpending_event);
+}
+
+static void
+x11_flip (void)
+{
+ XCopyArea (g.dpy, g.x11_pixmap, g.x11_window,
+ DefaultGC (g.dpy, DefaultScreen (g.dpy)),
+ 0, 0, g.ui_width, g.ui_height, 0, 0);
+ poller_idle_set (&g.xpending_event);
+}
+
+static void
+x11_destroy (void)
+{
+ XDestroyWindow (g.dpy, g.x11_window);
+ XRenderFreePicture (g.dpy, g.x11_pixmap_picture);
+ XFreePixmap (g.dpy, g.x11_pixmap);
+ XftDrawDestroy (g.xft_draw);
+ XftFontClose (g.dpy, g.xft_regular);
+ XftFontClose (g.dpy, g.xft_bold);
+
+ poller_fd_reset (&g.x11_event);
+ XCloseDisplay (g.dpy);
+}
+
+static struct ui x11_ui =
+{
+ .padding = x11_make_padding,
+ .label = x11_make_label,
+ .button = x11_make_button,
+ .gauge = x11_make_gauge,
+ .spectrum = x11_make_spectrum,
+ .scrollbar = x11_make_scrollbar,
+ .list = x11_make_list,
+ .editor = x11_make_editor,
+
+ .render = x11_render,
+ .flip = x11_flip,
+ .destroy = x11_destroy,
+ .have_icons = true,
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static termo_sym_t
+x11_convert_keysym (KeySym keysym)
+{
+ // Leaving out TERMO_TYPE_FUNCTION, TERMO_SYM_DEL (N/A),
+ // and TERMO_SYM_SPACE (governed by TERMO_FLAG_SPACESYMBOL, not in use).
+ switch (keysym)
+ {
+ case XK_BackSpace: return TERMO_SYM_BACKSPACE;
+ case XK_Tab: return TERMO_SYM_TAB;
+ case XK_Return: return TERMO_SYM_ENTER;
+ case XK_Escape: return TERMO_SYM_ESCAPE;
+
+ case XK_Up: return TERMO_SYM_UP;
+ case XK_Down: return TERMO_SYM_DOWN;
+ case XK_Left: return TERMO_SYM_LEFT;
+ case XK_Right: return TERMO_SYM_RIGHT;
+ case XK_Begin: return TERMO_SYM_BEGIN;
+ case XK_Find: return TERMO_SYM_FIND;
+ case XK_Insert: return TERMO_SYM_INSERT;
+ case XK_Delete: return TERMO_SYM_DELETE;
+ case XK_Select: return TERMO_SYM_SELECT;
+ case XK_Page_Up: return TERMO_SYM_PAGEUP;
+ case XK_Page_Down: return TERMO_SYM_PAGEDOWN;
+ case XK_Home: return TERMO_SYM_HOME;
+ case XK_End: return TERMO_SYM_END;
+
+ case XK_Cancel: return TERMO_SYM_CANCEL;
+ case XK_Clear: return TERMO_SYM_CLEAR;
+ // TERMO_SYM_CLOSE
+ // TERMO_SYM_COMMAND
+ // TERMO_SYM_COPY
+ // TERMO_SYM_EXIT
+ case XK_Help: return TERMO_SYM_HELP;
+ // TERMO_SYM_MARK
+ // TERMO_SYM_MESSAGE
+ // TERMO_SYM_MOVE
+ // TERMO_SYM_OPEN
+ // TERMO_SYM_OPTIONS
+ case XK_Print: return TERMO_SYM_PRINT;
+ case XK_Redo: return TERMO_SYM_REDO;
+ // TERMO_SYM_REFERENCE
+ // TERMO_SYM_REFRESH
+ // TERMO_SYM_REPLACE
+ // TERMO_SYM_RESTART
+ // TERMO_SYM_RESUME
+ // TERMO_SYM_SAVE
+ // TERMO_SYM_SUSPEND
+ case XK_Undo: return TERMO_SYM_UNDO;
+
+ case XK_KP_0: return TERMO_SYM_KP0;
+ case XK_KP_1: return TERMO_SYM_KP1;
+ case XK_KP_2: return TERMO_SYM_KP2;
+ case XK_KP_3: return TERMO_SYM_KP3;
+ case XK_KP_4: return TERMO_SYM_KP4;
+ case XK_KP_5: return TERMO_SYM_KP5;
+ case XK_KP_6: return TERMO_SYM_KP6;
+ case XK_KP_7: return TERMO_SYM_KP7;
+ case XK_KP_8: return TERMO_SYM_KP8;
+ case XK_KP_9: return TERMO_SYM_KP9;
+ case XK_KP_Enter: return TERMO_SYM_KPENTER;
+ case XK_KP_Add: return TERMO_SYM_KPPLUS;
+ case XK_KP_Subtract: return TERMO_SYM_KPMINUS;
+ case XK_KP_Multiply: return TERMO_SYM_KPMULT;
+ case XK_KP_Divide: return TERMO_SYM_KPDIV;
+ case XK_KP_Separator: return TERMO_SYM_KPCOMMA;
+ case XK_KP_Decimal: return TERMO_SYM_KPPERIOD;
+ case XK_KP_Equal: return TERMO_SYM_KPEQUALS;
+ }
+ return TERMO_SYM_UNKNOWN;
+}
+
+static void
+on_x11_keypress (XEvent *e)
+{
+ XKeyEvent *ev = &e->xkey;
+ unsigned unconsumed_mods = 0;
+ KeySym keysym = None;
+ if (!XkbLookupKeySym (g.dpy,
+ (KeyCode) ev->keycode, ev->state, &unconsumed_mods, &keysym))
+ return;
+
+ termo_key_t key = {};
+ if (ev->state & ShiftMask)
+ key.modifiers |= TERMO_KEYMOD_SHIFT;
+ if (ev->state & ControlMask)
+ key.modifiers |= TERMO_KEYMOD_CTRL;
+ if (ev->state & Mod1Mask)
+ key.modifiers |= TERMO_KEYMOD_ALT;
+
+ if (keysym >= XK_F1 && keysym <= XK_F35)
+ {
+ key.type = TERMO_TYPE_FUNCTION;
+ key.code.number = 1 + keysym - XK_F1;
+ }
+ else if ((key.code.sym = x11_convert_keysym (keysym)) != TERMO_SYM_UNKNOWN)
+ key.type = TERMO_TYPE_KEYSYM;
+ else if ((key.code.codepoint = xkb_keysym_to_utf32 (keysym)))
+ {
+ // Not filling in UTF-8, but xkb_keysym_to_utf8() exists.
+ key.type = TERMO_TYPE_KEY;
+ key.modifiers &= ~TERMO_KEYMOD_SHIFT;
+ }
+ else
+ return;
+
+ app_process_termo_event (&key);
+}
+
+static void
+x11_init_pixmap (void)
+{
+ int screen = DefaultScreen (g.dpy);
+ g.x11_pixmap = XCreatePixmap (g.dpy, g.x11_window,
+ g.ui_width, g.ui_height, DefaultDepth (g.dpy, screen));
+
+ Visual *visual = DefaultVisual (g.dpy, screen);
+ XRenderPictFormat *format = XRenderFindVisualFormat (g.dpy, visual);
+ g.x11_pixmap_picture
+ = XRenderCreatePicture (g.dpy, g.x11_pixmap, format, 0, NULL);
+}
+
+static void
+on_x11_input_event (XEvent *ev)
+{
+ static XEvent last_button_event;
+ if (ev->type == KeyPress)
+ {
+ last_button_event = (XEvent) {};
+ on_x11_keypress (ev);
+ return;
+ }
+
+ // See tui_on_tty_event(). Just here we know the button on button release.
+ int x = ev->xbutton.x, y = ev->xbutton.y;
+ unsigned int button = ev->xbutton.button;
+ bool double_click = ev->xbutton.time - last_button_event.xbutton.time < 500
+ && last_button_event.type == ButtonRelease && ev->type == ButtonPress
+ && abs (last_button_event.xbutton.x - x) < 5
+ && abs (last_button_event.xbutton.y - y) < 5
+ && last_button_event.xbutton.button == button;
+
+ if (ev->type == ButtonPress)
+ app_process_mouse (TERMO_MOUSE_PRESS, x, y, button, double_click);
+ if (ev->type == ButtonRelease)
+ app_process_mouse (TERMO_MOUSE_RELEASE, x, y, button, double_click);
+
+ // Prevent interpreting triple clicks as two double clicks.
+ last_button_event = (XEvent) {};
+ if (!double_click)
+ last_button_event = *ev;
+}
+
+static void
+on_x11_event (XEvent *ev)
+{
+ termo_key_t key = {};
+ switch (ev->type)
+ {
+ case Expose:
+ if (!ev->xexpose.count)
+ poller_idle_set (&g.flip_event);
+ break;
+ case FocusIn:
+ key.type = TERMO_TYPE_FOCUS;
+ key.code.focused = true;
+ app_process_termo_event (&key);
+ break;
+ case FocusOut:
+ key.type = TERMO_TYPE_FOCUS;
+ key.code.focused = false;
+ app_process_termo_event (&key);
+ break;
+ case KeyPress:
+ case ButtonPress:
+ case ButtonRelease:
+ on_x11_input_event (ev);
+ break;
+ case UnmapNotify:
+ app_quit ();
+ break;
+ case ConfigureNotify:
+ if (g.ui_width == ev->xconfigure.width
+ && g.ui_height == ev->xconfigure.height)
+ break;
+
+ g.ui_width = ev->xconfigure.width;
+ g.ui_height = ev->xconfigure.height;
+
+ XRenderFreePicture (g.dpy, g.x11_pixmap_picture);
+ XFreePixmap (g.dpy, g.x11_pixmap);
+ x11_init_pixmap ();
+ XftDrawChange (g.xft_draw, g.x11_pixmap);
+ app_invalidate ();
+ }
+}
+
+static void
+on_x11_pending (void *user_data)
+{
+ (void) user_data;
+
+ XkbEvent ev;
+ while (XPending (g.dpy))
+ {
+ if (XNextEvent (g.dpy, &ev.core))
+ exit_fatal ("XNextEvent returned non-zero");
+ on_x11_event (&ev.core);
+ }
+
+ poller_idle_reset (&g.xpending_event);
+}
+
+static void
+on_x11_ready (const struct pollfd *pfd, void *user_data)
+{
+ (void) pfd;
+ on_x11_pending (user_data);
+}
+
+static int
+on_x11_error (Display *dpy, XErrorEvent *event)
+{
+ // Without opting for WM_DELETE_WINDOW, this window can become destroyed
+ // and hence invalid at any time. We don't use the Window much,
+ // so we should be fine ignoring these errors.
+ if ((event->error_code == BadWindow && event->resourceid == g.x11_window)
+ || (event->error_code == BadDrawable && event->resourceid == g.x11_window))
+ return app_quit (), 0;
+
+ return x11_default_error_handler (dpy, event);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static XRenderColor
+x11_convert_color (int color)
+{
+ hard_assert (color >= 0 && color <= 255);
+
+ static const uint16_t base[16] =
+ {
+ 0x000, 0x800, 0x080, 0x880, 0x008, 0x808, 0x088, 0xccc,
+ 0x888, 0xf00, 0x0f0, 0xff0, 0x00f, 0xf0f, 0x0ff, 0xfff,
+ };
+
+ XRenderColor c = { .alpha = 0xffff };
+ if (color < 16)
+ {
+ c.red = 0x1111 * (base[color] >> 8);
+ c.green = 0x1111 * (0xf & (base[color] >> 4));
+ c.blue = 0x1111 * (0xf & (base[color]));
+ }
+ else if (color >= 232)
+ c.red = c.green = c.blue = 0x0101 * (8 + (color - 232) * 10);
+ else
+ {
+ color -= 16;
+
+ int r = color / 36;
+ int g = (color / 6) % 6;
+ int b = (color % 6);
+ c.red = 0x0101 * !!r * (55 + 40 * r);
+ c.green = 0x0101 * !!g * (55 + 40 * g);
+ c.blue = 0x0101 * !!b * (55 + 40 * b);
+ }
+ return c;
+}
+
+static void
+x11_init_attributes (void)
+{
+ for (int a = 0; a < ATTRIBUTE_COUNT; a++)
+ {
+ g.x_fg[a] = x11_default_fg;
+ g.x_bg[a] = x11_default_bg;
+ if (g.attrs[a].fg >= 256 || g.attrs[a].fg < -1
+ || g.attrs[a].bg >= 256 || g.attrs[a].bg < -1)
+ continue;
+
+ if (g.attrs[a].fg != -1)
+ g.x_fg[a] = x11_convert_color (g.attrs[a].fg);
+ if (g.attrs[a].bg != -1)
+ g.x_bg[a] = x11_convert_color (g.attrs[a].bg);
+
+ g.attrs[a].attrs |= COLOR_PAIR (a + 1);
+ }
+}
+
+static void
+x11_init_fonts (void)
+{
+ // TODO: Try to use Gtk/FontName from the _XSETTINGS_S%d selection,
+ // as well as Net/DoubleClick*. See the XSETTINGS proposal for details.
+ // https://www.freedesktop.org/wiki/Specifications/XSettingsRegistry/
+ const char *name = get_config_string (g.config.root, "settings.x11_font");
+ int screen = DefaultScreen (g.dpy);
+ FcResult result = 0;
+
+ FcPattern *query_regular = FcNameParse ((const FcChar8 *) name);
+ FcPattern *query_bold = FcPatternDuplicate (query_regular);
+ FcPatternAdd (query_bold, FC_STYLE,
+ (FcValue) { .type = FcTypeString, .u.s = (FcChar8 *) "Bold" }, FcFalse);
+
+ FcPattern *regular = XftFontMatch (g.dpy, screen, query_regular, &result);
+ FcPatternDestroy (query_regular);
+ if (!regular)
+ exit_fatal ("cannot open font: %s (%d)", name, result);
+ if (!(g.xft_regular = XftFontOpenPattern (g.dpy, regular)))
+ {
+ FcPatternDestroy (regular);
+ exit_fatal ("cannot open font: %s", name);
+ }
+
+ FcPattern *bold = XftFontMatch (g.dpy, screen, query_bold, &result);
+ FcPatternDestroy (query_bold);
+ if (bold && !(g.xft_bold = XftFontOpenPattern (g.dpy, bold)))
+ FcPatternDestroy (bold);
+ if (!g.xft_bold)
+ g.xft_bold = XftFontCopy (g.dpy, g.xft_regular);
+}
+
+static void
+x11_init (void)
+{
+ if (!(g.dpy = XkbOpenDisplay
+ (NULL, &g.xkb_base_event_code, NULL, NULL, NULL, NULL)))
+ exit_fatal ("cannot open display");
+ if (!XftDefaultHasRender (g.dpy))
+ exit_fatal ("XRender is not supported");
+
+ x11_default_error_handler = XSetErrorHandler (on_x11_error);
+
+ set_cloexec (ConnectionNumber (g.dpy));
+ g.x11_event = poller_fd_make (&g.poller, ConnectionNumber (g.dpy));
+ g.x11_event.dispatcher = on_x11_ready;
+ poller_fd_set (&g.x11_event, POLLIN);
+
+ // Whenever something causes Xlib to read its socket, it can make
+ // the I/O event above fail to trigger for whatever might have ended up
+ // in its queue. So always use this instead of XSync:
+ g.xpending_event = poller_idle_make (&g.poller);
+ g.xpending_event.dispatcher = on_x11_pending;
+ poller_idle_set (&g.xpending_event);
+
+ x11_init_attributes ();
+ x11_init_fonts ();
+
+ int screen = DefaultScreen (g.dpy);
+ Colormap cmap = DefaultColormap (g.dpy, screen);
+ XColor default_bg =
+ {
+ .red = x11_default_bg.red,
+ .green = x11_default_bg.green,
+ .blue = x11_default_bg.blue,
+ };
+ if (!XAllocColor (g.dpy, cmap, &default_bg))
+ exit_fatal ("X11 setup failed");
+
+ XSetWindowAttributes attrs =
+ {
+ .event_mask = StructureNotifyMask | ExposureMask | FocusChangeMask
+ | KeyPressMask | ButtonPressMask | ButtonReleaseMask,
+ .bit_gravity = NorthWestGravity,
+ .background_pixel = default_bg.pixel,
+ };
+
+ // Approximate the average width of a character to half of the em unit.
+ g.ui_vunit = g.xft_regular->height;
+ g.ui_hunit = g.ui_vunit / 2;
+ // Base the window's size on the regular font size.
+ // Roughly trying to match the 80x24 default dimensions of terminals.
+ g.ui_height = 24 * g.ui_vunit;
+ g.ui_width = g.ui_height * 4 / 3;
+
+ Visual *visual = DefaultVisual (g.dpy, screen);
+ g.x11_window = XCreateWindow (g.dpy, RootWindow (g.dpy, screen), 100, 100,
+ g.ui_width, g.ui_height, 0, CopyFromParent, InputOutput, visual,
+ CWEventMask | CWBackPixel | CWBitGravity, &attrs);
+ g.x11_pixmap = XCreatePixmap (g.dpy, g.x11_window, g.ui_width, g.ui_height,
+ DefaultDepth (g.dpy, screen));
+
+ x11_init_pixmap ();
+ g.xft_draw = XftDrawCreate (g.dpy, g.x11_pixmap, visual, cmap);
+ g.ui = &x11_ui;
+
+ XTextProperty prop = {};
+ char *name = PROGRAM_NAME;
+ if (!Xutf8TextListToTextProperty (g.dpy, &name, 1, XUTF8StringStyle, &prop))
+ XSetWMName (g.dpy, g.x11_window, &prop);
+ XFree (prop.value);
+
+ XMapWindow (g.dpy, g.x11_window);
+}
+
+#endif // WITH_X11
+
+// --- Signals -----------------------------------------------------------------
+
+static int g_signal_pipe[2]; ///< A pipe used to signal... signals
+
+/// Program termination has been requested by a signal
+static volatile sig_atomic_t g_termination_requested;
+/// The window has changed in size
+static volatile sig_atomic_t g_winch_received;
+
+static void
+signals_postpone_handling (char id)
+{
+ int original_errno = errno;
+ if (write (g_signal_pipe[1], &id, 1) == -1)
+ soft_assert (errno == EAGAIN);
+ errno = original_errno;
+}
+
+static void
+signals_superhandler (int signum)
+{
+ switch (signum)
+ {
+ case SIGWINCH:
+ g_winch_received = true;
+ signals_postpone_handling ('w');
+ break;
+ case SIGINT:
+ case SIGTERM:
+ g_termination_requested = true;
+ signals_postpone_handling ('t');
+ break;
+ default:
+ hard_assert (!"unhandled signal");
+ }
+}
+
+static void
+signals_setup_handlers (void)
+{
+ if (pipe (g_signal_pipe) == -1)
+ exit_fatal ("%s: %s", "pipe", strerror (errno));
+
+ set_cloexec (g_signal_pipe[0]);
+ set_cloexec (g_signal_pipe[1]);
+
+ // So that the pipe cannot overflow; it would make write() block within
+ // the signal handler, which is something we really don't want to happen.
+ // The same holds true for read().
+ set_blocking (g_signal_pipe[0], false);
+ set_blocking (g_signal_pipe[1], false);
+
+ signal (SIGPIPE, SIG_IGN);
+
+ struct sigaction sa;
+ sa.sa_flags = SA_RESTART;
+ sa.sa_handler = signals_superhandler;
+ sigemptyset (&sa.sa_mask);
+
+ if (sigaction (SIGWINCH, &sa, NULL) == -1
+ || sigaction (SIGINT, &sa, NULL) == -1
+ || sigaction (SIGTERM, &sa, NULL) == -1)
+ exit_fatal ("sigaction: %s", strerror (errno));
+}
+
+// --- Initialisation, event handling ------------------------------------------
+
+static void
app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data)
{
(void) user_data;
@@ -4891,11 +6243,13 @@ app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data)
if (g_termination_requested && !g.quitting)
app_quit ();
+ // It would be awkward to set up SIGWINCH conditionally,
+ // so have it as a handler within UIs.
if (g_winch_received)
{
g_winch_received = false;
- update_curses_terminal_size ();
- app_invalidate ();
+ if (g.ui->winch)
+ g.ui->winch ();
}
}
@@ -4948,15 +6302,14 @@ app_init_poller_events (void)
g.signal_event.dispatcher = app_on_signal_pipe_readable;
poller_fd_set (&g.signal_event, POLLIN);
- g.tty_event = poller_fd_make (&g.poller, STDIN_FILENO);
- g.tty_event.dispatcher = app_on_tty_readable;
- poller_fd_set (&g.tty_event, POLLIN);
-
g.message_timer = poller_timer_make (&g.poller);
g.message_timer.dispatcher = app_on_message_timer;
+ // Always initialized, but only activated with the TUI.
+ g.tty_event = poller_fd_make (&g.poller, STDIN_FILENO);
+ g.tty_event.dispatcher = tui_on_tty_readable;
g.tk_timer = poller_timer_make (&g.poller);
- g.tk_timer.dispatcher = app_on_key_timer;
+ g.tk_timer.dispatcher = tui_on_key_timer;
g.connect_event = poller_timer_make (&g.poller);
g.connect_event.dispatcher = app_on_reconnect;
@@ -4969,6 +6322,9 @@ app_init_poller_events (void)
g.refresh_event = poller_idle_make (&g.poller);
g.refresh_event.dispatcher = app_on_refresh;
+
+ g.flip_event = poller_idle_make (&g.poller);
+ g.flip_event.dispatcher = app_on_flip;
}
static void
@@ -4999,14 +6355,17 @@ main (int argc, char *argv[])
static const struct opt opts[] =
{
{ 'd', "debug", NULL, 0, "run in debug mode" },
+#ifdef WITH_X11
+ { 'x', "x11", NULL, 0, "use X11 even when run from a terminal" },
+#endif // WITH_X11
{ 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 0, NULL, NULL, 0, NULL }
};
- struct opt_handler oh =
- opt_handler_make (argc, argv, opts,
- "[URL | PATH]...", "Terminal-based MPD client.");
+ bool requested_x11 = false;
+ struct opt_handler oh
+ = opt_handler_make (argc, argv, opts, "[URL | PATH]...", "MPD client.");
int c;
while ((c = opt_handler_get (&oh)) != -1)
@@ -5021,6 +6380,9 @@ main (int argc, char *argv[])
case 'V':
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
exit (EXIT_SUCCESS);
+ case 'x':
+ requested_x11 = true;
+ break;
default:
print_error ("wrong options");
opt_handler_usage (&oh, stderr);
@@ -5040,7 +6402,13 @@ main (int argc, char *argv[])
app_load_configuration ();
signals_setup_handlers ();
app_init_poller_events ();
- app_init_terminal ();
+
+#ifdef WITH_X11
+ if (requested_x11 || (!isatty (STDIN_FILENO) && getenv ("DISPLAY")))
+ x11_init ();
+ else
+#endif // WITH_X11
+ tui_init ();
g_normal_keys = app_init_bindings ("normal",
g_normal_defaults, N_ELEMENTS (g_normal_defaults), &g_normal_keys_len);
@@ -5069,7 +6437,7 @@ main (int argc, char *argv[])
while (g.polling)
poller_run (&g.poller);
- endwin ();
+ g.ui->destroy ();
g_log_message_real = log_message_stdio;
app_free_context ();
return 0;