aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--xA/xA.go156
-rw-r--r--xF.c1088
-rw-r--r--xM/main.swift3
-rw-r--r--xP/public/xP.js2
-rw-r--r--xW/xW.cpp181
5 files changed, 1241 insertions, 189 deletions
diff --git a/xA/xA.go b/xA/xA.go
index 5bd3975..f501622 100644
--- a/xA/xA.go
+++ b/xA/xA.go
@@ -281,7 +281,11 @@ func beep() {
}
go func() {
<-otoReady
- otoContext.NewPlayer(bytes.NewReader(beepSample)).Play()
+ p := otoContext.NewPlayer(bytes.NewReader(beepSample))
+ p.Play()
+ for p.IsPlaying() {
+ time.Sleep(time.Second)
+ }
}()
}
@@ -363,16 +367,18 @@ func bufferByName(name string) *buffer {
return nil
}
-func bufferActivate(name string) {
- relaySend(RelayCommandData{
- Variant: &RelayCommandDataBufferActivate{BufferName: name},
- }, nil)
+func bufferAtBottom() bool {
+ return wRichScroll.Offset.Y >=
+ wRichScroll.Content.Size().Height-wRichScroll.Size().Height
}
-func bufferToggleUnimportant(name string) {
- relaySend(RelayCommandData{
- Variant: &RelayCommandDataBufferToggleUnimportant{BufferName: name},
- }, nil)
+func bufferScrollToBottom() {
+ // XXX: Doing it once is not reliable, something's amiss.
+ // (In particular, nothing happens when we switch from an empty buffer
+ // to a buffer than needs scrolling.)
+ wRichScroll.ScrollToBottom()
+ wRichScroll.ScrollToBottom()
+ refreshStatus()
}
func bufferPushLine(b *buffer, line bufferLine) {
@@ -386,68 +392,20 @@ func bufferPushLine(b *buffer, line bufferLine) {
}
}
-// --- Current buffer ----------------------------------------------------------
-
-func bufferToggleLogFinish(err string, response *RelayResponseDataBufferLog) {
- if response == nil {
- showErrorMessage(err)
- return
- }
-
- wLog.SetText(string(response.Log))
- wLog.Show()
- wRichScroll.Hide()
-}
-
-func bufferToggleLog() {
- if wLog.Visible() {
- wRichScroll.Show()
- wLog.Hide()
- wLog.SetText("")
- return
- }
-
- name := bufferCurrent
- relaySend(RelayCommandData{Variant: &RelayCommandDataBufferLog{
- BufferName: name,
- }}, func(err string, response *RelayResponseData) {
- if bufferCurrent == name {
- bufferToggleLogFinish(
- err, response.Variant.(*RelayResponseDataBufferLog))
- }
- })
-}
-
-func bufferAtBottom() bool {
- return wRichScroll.Offset.Y >=
- wRichScroll.Content.Size().Height-wRichScroll.Size().Height
-}
-
-func bufferScrollToBottom() {
- // XXX: Doing it once is not reliable, something's amiss.
- // (In particular, nothing happens when we switch from an empty buffer
- // to a buffer than needs scrolling.)
- wRichScroll.ScrollToBottom()
- wRichScroll.ScrollToBottom()
- refreshStatus()
-}
-
// --- UI state refresh --------------------------------------------------------
func refreshIcon() {
- highlighted := false
+ resource := resourceIconNormal
for _, b := range buffers {
if b.highlighted {
- highlighted = true
+ resource = resourceIconHighlighted
break
}
}
- if highlighted {
- wWindow.SetIcon(resourceIconHighlighted)
- } else {
- wWindow.SetIcon(resourceIconNormal)
- }
+ // Prevent deadlocks (though it might have a race condition).
+ // https://github.com/fyne-io/fyne/issues/5266
+ go func() { wWindow.SetIcon(resource) }()
}
func refreshTopic(topic []bufferLineItem) {
@@ -515,6 +473,63 @@ func refreshStatus() {
wStatus.SetText(status)
}
+func recheckHighlighted() {
+ // Corresponds to the logic toggling the bool on.
+ if b := bufferByName(bufferCurrent); b != nil &&
+ b.highlighted && bufferAtBottom() &&
+ inForeground && !wLog.Visible() {
+ b.highlighted = false
+ refreshIcon()
+ refreshBufferList()
+ }
+}
+
+// --- Buffer actions ----------------------------------------------------------
+
+func bufferActivate(name string) {
+ relaySend(RelayCommandData{
+ Variant: &RelayCommandDataBufferActivate{BufferName: name},
+ }, nil)
+}
+
+func bufferToggleUnimportant(name string) {
+ relaySend(RelayCommandData{
+ Variant: &RelayCommandDataBufferToggleUnimportant{BufferName: name},
+ }, nil)
+}
+
+func bufferToggleLogFinish(err string, response *RelayResponseDataBufferLog) {
+ if response == nil {
+ showErrorMessage(err)
+ return
+ }
+
+ wLog.SetText(string(response.Log))
+ wLog.Show()
+ wRichScroll.Hide()
+}
+
+func bufferToggleLog() {
+ if wLog.Visible() {
+ wRichScroll.Show()
+ wLog.Hide()
+ wLog.SetText("")
+
+ recheckHighlighted()
+ return
+ }
+
+ name := bufferCurrent
+ relaySend(RelayCommandData{Variant: &RelayCommandDataBufferLog{
+ BufferName: name,
+ }}, func(err string, response *RelayResponseData) {
+ if bufferCurrent == name {
+ bufferToggleLogFinish(
+ err, response.Variant.(*RelayResponseDataBufferLog))
+ }
+ })
+}
+
// --- RichText formatting -----------------------------------------------------
func defaultBufferLineItem() bufferLineItem { return bufferLineItem{} }
@@ -756,6 +771,7 @@ func refreshBuffer(b *buffer) {
bufferPrintAndWatchTrailingDateChanges()
wRichText.Refresh()
bufferScrollToBottom()
+ recheckHighlighted()
}
// --- Event processing --------------------------------------------------------
@@ -921,11 +937,11 @@ func relayProcessMessage(m *RelayEventMessage) {
b.bufferName = data.New
- refreshBufferList()
if data.BufferName == bufferCurrent {
bufferCurrent = data.New
refreshStatus()
}
+ refreshBufferList()
if data.BufferName == bufferLast {
bufferLast = data.New
}
@@ -1374,6 +1390,9 @@ func (l *customLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
}
if toBottom {
bufferScrollToBottom()
+ } else {
+ recheckHighlighted()
+ refreshStatus()
}
}
@@ -1490,16 +1509,14 @@ func main() {
a := app.New()
a.Settings().SetTheme(&customTheme{})
+ a.SetIcon(resourceIconNormal)
wWindow = a.NewWindow(projectName)
wWindow.Resize(fyne.NewSize(640, 480))
a.Lifecycle().SetOnEnteredForeground(func() {
// TODO(p): Does this need locking?
inForeground = true
- if b := bufferByName(bufferCurrent); b != nil {
- b.highlighted = false
- refreshIcon()
- }
+ recheckHighlighted()
})
a.Lifecycle().SetOnExitedForeground(func() {
inForeground = false
@@ -1540,7 +1557,10 @@ func main() {
wRichText = widget.NewRichText()
wRichText.Wrapping = fyne.TextWrapWord
wRichScroll = container.NewVScroll(wRichText)
- wRichScroll.OnScrolled = func(position fyne.Position) { refreshStatus() }
+ wRichScroll.OnScrolled = func(position fyne.Position) {
+ recheckHighlighted()
+ refreshStatus()
+ }
wLog = newLogEntry()
wLog.Wrapping = fyne.TextWrapWord
wLog.Hide()
diff --git a/xF.c b/xF.c
index 054871d..98dfbf8 100644
--- a/xF.c
+++ b/xF.c
@@ -1,7 +1,7 @@
/*
* xF.c: a toothless IRC client frontend
*
- * Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
+ * Copyright (c) 2022 - 2024, 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.
@@ -22,21 +22,556 @@
#include "common.c"
#include "xC-proto.c"
+#define LIBERTY_XDG_WANT_X11
+#define LIBERTY_XDG_WANT_ICONS
+#include "liberty/liberty-xdg.c"
+
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/XKBlib.h>
#include <X11/Xft/Xft.h>
+// --- Frontend ----------------------------------------------------------------
+
+// See: struct relay_event_data_buffer_line
+struct buffer_line
+{
+ LIST_HEADER (struct buffer_line)
+
+ /// Leaked from another buffer, but temporarily staying in another one.
+ bool leaked;
+
+ bool is_unimportant;
+ bool is_highlight;
+ enum relay_rendition rendition;
+ uint64_t when;
+ char text[];
+};
+
+// See: struct relay_event_data_buffer_update
+struct buffer
+{
+ LIST_HEADER (struct buffer)
+
+ char *buffer_name;
+ enum relay_buffer_kind kind;
+ char *server_name;
+ struct buffer_line *lines;
+ struct buffer_line *lines_tail;
+
+ // Stats:
+
+ uint32_t new_messages;
+ uint32_t new_unimportant_messages;
+ bool highlighted;
+};
+
+static void
+buffer_destroy (struct buffer *self)
+{
+ free (self->buffer_name);
+ free (self->server_name);
+ LIST_FOR_EACH (struct buffer_line, iter, self->lines)
+ free (iter);
+ free (self);
+}
+
+// See: struct relay_event_data_server_update
+struct server
+{
+ enum relay_server_state state;
+ char *user;
+ char *user_modes;
+};
+
+static void
+server_destroy (struct server *self)
+{
+ free (self->user);
+ free (self->user_modes);
+ free (self);
+}
+
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+/// Wraps Xft fonts into a linked list with fallbacks.
+struct x11_font_link
+{
+ struct x11_font_link *next;
+ XftFont *font;
+};
+
+enum
+{
+ X11_FONT_BOLD = 1 << 0,
+ X11_FONT_ITALIC = 1 << 1,
+ X11_FONT_MONOSPACE = 1 << 2,
+};
+
+struct x11_font
+{
+ struct x11_font *next; ///< Next in a linked list
+
+ struct x11_font_link *list; ///< Fonts of varying Unicode coverage
+ unsigned style; ///< X11_FONT_* flags
+ FcPattern *pattern; ///< Original unsubstituted pattern
+ FcCharSet *unavailable; ///< Couldn't find a font for these
+};
+
static struct
{
- bool polling;
+ struct poller poller; ///< Poller
+ bool polling; ///< The event loop is running
+
+ // Relay plumbing:
+
struct connector connector;
- int socket;
+ int socket_fd; ///< Backend TCP socket
+ struct poller_fd socket_event; ///< The socket can be read/written to
+ struct str read_buffer; ///< Unprocessed input
+ struct str write_buffer; ///< Output yet to be sent out
+
+ uint32_t command_seq; ///< Outgoing message counter
+
+ // Relay state:
+
+ struct buffer *buffers; ///< Ordered list of all buffers
+ struct buffer *buffers_tail; ///< The tail of all buffers
+ struct buffer *buffer_current; ///< The current buffer
+ struct buffer *buffer_last; ///< Last used buffer
+
+ struct str_map servers; ///< All servers
+
+ // User interface:
+
+ int ui_width; ///< Window width
+ int ui_height; ///< Window height
+ bool ui_focused; ///< Whether the window has focus
+
+ XIM x11_im; ///< Input method
+ XIC x11_ic; ///< Input method context
+ 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
+ Region x11_clip; ///< Invalidated region
+ Picture x11_pixmap_picture; ///< XRender wrap for x11_pixmap
+ XftDraw *xft_draw; ///< Xft rendering context
+ struct x11_font *xft_fonts; ///< Font collection
+ char *x11_selection; ///< CLIPBOARD selection
+
+ const char *x11_fontname; ///< Fontconfig font name
+ const char *x11_fontname_monospace; ///< Fontconfig monospace font name
+
+ struct poller_idle refresh_event; ///< Refresh the window's contents
+ struct poller_idle flip_event; ///< Draw rendered widgets on screen
+}
+g =
+{
+ .x11_fontname = "sans\\-serif-11",
+ .x11_fontname_monospace = "monospace-11",
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+app_init_context (void)
+{
+ poller_init (&g.poller);
+
+ g.socket_fd = -1;
+ g.read_buffer = str_make ();
+ g.write_buffer = str_make ();
+
+ g.servers = str_map_make ((str_map_free_fn) server_destroy);
+
+ // Presumably, although not necessarily; unsure if queryable at all
+ g.ui_focused = true;
+}
+
+static void
+app_quit (void)
+{
+ // So far there's nothing for us to wait on, so let's just stop looping.
+ g.polling = false;
+}
+
+static void
+app_invalidate (void)
+{
+ poller_idle_set (&g.refresh_event);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static struct buffer *
+buffer_find (const char *name)
+{
+ LIST_FOR_EACH (struct buffer, buffer, g.buffers)
+ if (!strcmp (buffer->buffer_name, name))
+ return buffer;
+ return NULL;
+}
+
+static struct buffer_line *
+buffer_line_new (struct relay_event_data_buffer_line *m)
+{
+ struct str s = str_make ();
+ for (uint32_t i = 0; i < m->items_len; i++)
+ if (m->items[i].kind == RELAY_ITEM_TEXT)
+ str_append_str (&s, &m->items[i].text.text);
+
+ struct buffer_line *self = xcalloc (1, sizeof *self + s.len + 1);
+ memcpy (self->text, s.str, s.len + 1);
+ str_free (&s);
+
+ self->is_unimportant = m->is_unimportant;
+ self->is_highlight = m->is_highlight;
+ self->rendition = m->rendition;
+ self->when = m->when;
+ return self;
+}
+
+static void
+relay_process_buffer_line (struct buffer *buffer,
+ struct relay_event_data_buffer_line *m)
+{
+ // Initial sync: skip all other processing, let highlights be.
+ if (!g.buffer_current)
+ {
+ struct buffer_line *line = buffer_line_new (m);
+ LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line);
+ return;
+ }
+
+ // TODO: Track window on-screen visibility.
+ bool visible = buffer == g.buffer_current || m->leak_to_active;
+
+ // TODO: Port the rest from xP.js.
+ struct buffer_line *line = buffer_line_new (m);
+ LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line);
+ if (!(visible || m->leak_to_active)
+ || buffer->new_messages || buffer->new_unimportant_messages)
+ {
+ if (line->is_unimportant || m->leak_to_active)
+ buffer->new_unimportant_messages++;
+ else
+ buffer->new_messages++;
+ }
+
+ if (m->leak_to_active)
+ {
+ struct buffer *bc = g.buffer_current;
+ struct buffer_line *line = buffer_line_new (m);
+ line->leaked = true;
+ LIST_APPEND_WITH_TAIL (bc->lines, bc->lines_tail, line);
+ if (!visible || bc->new_messages || bc->new_unimportant_messages)
+ {
+ if (line->is_unimportant)
+ bc->new_unimportant_messages++;
+ else
+ bc->new_messages++;
+ }
+ }
+
+ if (line->is_highlight || (!visible && !line->is_unimportant
+ && buffer->kind == RELAY_BUFFER_KIND_PRIVATE_MESSAGE))
+ {
+ // TODO: Play a beep sample.
+ if (!visible)
+ buffer->highlighted = true;
+ }
+}
+
+static const char *
+relay_message_buffer_name (const struct relay_event_message *m)
+{
+ switch (m->data.event)
+ {
+ case RELAY_EVENT_BUFFER_LINE:
+ return m->data.buffer_line.buffer_name.str;
+ case RELAY_EVENT_BUFFER_UPDATE:
+ return m->data.buffer_update.buffer_name.str;
+ case RELAY_EVENT_BUFFER_STATS:
+ return m->data.buffer_stats.buffer_name.str;
+ case RELAY_EVENT_BUFFER_RENAME:
+ return m->data.buffer_rename.buffer_name.str;
+ case RELAY_EVENT_BUFFER_REMOVE:
+ return m->data.buffer_remove.buffer_name.str;
+ case RELAY_EVENT_BUFFER_ACTIVATE:
+ return m->data.buffer_activate.buffer_name.str;
+ case RELAY_EVENT_BUFFER_INPUT:
+ return m->data.buffer_input.buffer_name.str;
+ case RELAY_EVENT_BUFFER_CLEAR:
+ return m->data.buffer_clear.buffer_name.str;
+ default:
+ return NULL;
+ }
+}
+
+static const char *
+relay_message_server_name (const struct relay_event_message *m)
+{
+ switch (m->data.event)
+ {
+ case RELAY_EVENT_SERVER_UPDATE:
+ return m->data.server_update.server_name.str;
+ case RELAY_EVENT_SERVER_RENAME:
+ return m->data.server_rename.server_name.str;
+ case RELAY_EVENT_SERVER_REMOVE:
+ return m->data.server_remove.server_name.str;
+ default:
+ return NULL;
+ }
+}
+
+static const char *
+relay_buffer_update_server_name (const struct relay_event_data_buffer_update *e)
+{
+ switch (e->context.kind)
+ {
+ case RELAY_BUFFER_KIND_SERVER:
+ return e->context.server.server_name.str;
+ case RELAY_BUFFER_KIND_CHANNEL:
+ return e->context.channel.server_name.str;
+ case RELAY_BUFFER_KIND_PRIVATE_MESSAGE:
+ return e->context.private_message.server_name.str;
+ default:
+ return NULL;
+ }
+}
+
+static bool
+relay_process_message (struct msg_unpacker *r, struct relay_event_message *m)
+{
+ if (!relay_event_message_deserialize (m, r)
+ || msg_unpacker_get_available (r))
+ {
+ print_error ("deserialization failed");
+ return false;
+ }
+
+ const char *buffer_name = relay_message_buffer_name (m);
+ struct buffer *buffer = NULL;
+ if (buffer_name && !(buffer = buffer_find (buffer_name)))
+ {
+ // TODO: Maybe handle BUFFER_ACTIVATE the way xP does.
+ if (m->data.event != RELAY_EVENT_BUFFER_UPDATE)
+ {
+ print_warning ("unknown buffer: %s", buffer_name);
+ return true;
+ }
+
+ buffer = xcalloc (1, sizeof *buffer);
+ buffer->buffer_name = xstrdup (buffer_name);
+ LIST_APPEND_WITH_TAIL (g.buffers, g.buffers_tail, buffer);
+ }
+
+ const char *server_name = relay_message_server_name (m);
+ struct server *server = NULL;
+ if (server_name && !(server = str_map_find (&g.servers, server_name)))
+ {
+ if (m->data.event != RELAY_EVENT_SERVER_UPDATE)
+ {
+ print_warning ("unknown server: %s", server_name);
+ return true;
+ }
+
+ server = xcalloc (1, sizeof *server);
+ str_map_set (&g.servers, server_name, server);
+ }
+
+ switch (m->data.event)
+ {
+ case RELAY_EVENT_PING:
+ // TODO: While not important, we should implement it.
+ return true;
+
+ case RELAY_EVENT_BUFFER_LINE:
+ relay_process_buffer_line (buffer, &m->data.buffer_line);
+ break;
+ case RELAY_EVENT_BUFFER_UPDATE:
+ buffer->kind = m->data.buffer_update.context.kind;
+
+ server_name = relay_buffer_update_server_name (&m->data.buffer_update);
+ cstr_set (&buffer->server_name, NULL);
+ if (server_name)
+ buffer->server_name = xstrdup (server_name);
+
+ // TODO: More kind-dependent state.
+ break;
+ case RELAY_EVENT_BUFFER_STATS:
+ buffer->new_messages
+ = m->data.buffer_stats.new_messages;
+ buffer->new_unimportant_messages
+ = m->data.buffer_stats.new_unimportant_messages;
+ buffer->highlighted
+ = m->data.buffer_stats.highlighted;
+ break;
+ case RELAY_EVENT_BUFFER_RENAME:
+ free (buffer->buffer_name);
+ buffer->buffer_name = xstrdup (m->data.buffer_rename.new.str);
+ break;
+ case RELAY_EVENT_BUFFER_REMOVE:
+ LIST_UNLINK_WITH_TAIL (g.buffers, g.buffers_tail, buffer);
+ if (g.buffer_current == buffer)
+ g.buffer_current = NULL;
+ if (g.buffer_last == buffer)
+ g.buffer_last = NULL;
+ buffer_destroy (buffer);
+ break;
+ case RELAY_EVENT_BUFFER_ACTIVATE:
+ if (g.buffer_current)
+ {
+ g.buffer_current->new_messages = 0;
+ g.buffer_current->new_unimportant_messages = 0;
+ g.buffer_current->highlighted = false;
+ }
+
+ g.buffer_last = g.buffer_current;
+ g.buffer_current = buffer;
+ // TODO: Switch the input line.
+ break;
+ case RELAY_EVENT_BUFFER_CLEAR:
+ LIST_FOR_EACH (struct buffer_line, iter, buffer->lines)
+ free (iter);
+ buffer->lines = NULL;
+ break;
+
+ case RELAY_EVENT_SERVER_UPDATE:
+ server->state = m->data.server_update.data.state;
+
+ cstr_set (&server->user, NULL);
+ cstr_set (&server->user_modes, NULL);
+ if (server->state == RELAY_SERVER_STATE_REGISTERED)
+ {
+ server->user =
+ xstrdup (m->data.server_update.data.registered.user.str);
+ server->user_modes =
+ xstrdup (m->data.server_update.data.registered.user_modes.str);
+ }
+ break;
+ case RELAY_EVENT_SERVER_RENAME:
+ str_map_set (&g.servers, m->data.server_rename.new.str,
+ str_map_steal (&g.servers, server_name));
+ break;
+ case RELAY_EVENT_SERVER_REMOVE:
+ str_map_set (&g.servers, server_name, NULL);
+ break;
+
+ default:
+ return true;
+ }
+ app_invalidate ();
+ return true;
+}
+
+static bool
+relay_process_buffer (void)
+{
+ struct str *buf = &g.read_buffer;
+ size_t offset = 0;
+ while (true)
+ {
+ uint32_t frame_len = 0;
+ struct msg_unpacker r =
+ msg_unpacker_make (buf->str + offset, buf->len - offset);
+ if (!msg_unpacker_u32 (&r, &frame_len))
+ break;
+
+ r.len = MIN (r.len, sizeof frame_len + frame_len);
+ if (msg_unpacker_get_available (&r) < frame_len)
+ break;
+
+ struct relay_event_message m = {};
+ bool ok = relay_process_message (&r, &m);
+ relay_event_message_free (&m);
+ if (!ok)
+ return false;
+
+ offset += r.offset;
+ }
+
+ str_remove_slice (buf, 0, offset);
+ return true;
+}
+
+static bool
+relay_try_read (void)
+{
+ struct str *buf = &g.read_buffer;
+ ssize_t n_read;
+
+ while ((n_read = read (g.socket_fd, buf->str + buf->len,
+ buf->alloc - buf->len - 1 /* null byte */)) > 0)
+ {
+ buf->len += n_read;
+ if (!relay_process_buffer ())
+ break;
+ str_reserve (buf, 512);
+ }
+
+ if (n_read < 0)
+ {
+ if (errno == EAGAIN || errno == EINTR)
+ return true;
+
+ print_debug ("%s: %s: %s", __func__, "read", strerror (errno));
+ }
+ return false;
}
-g;
+
+static bool
+relay_try_write (void)
+{
+ struct str *buf = &g.write_buffer;
+ ssize_t n_written;
+
+ while (buf->len)
+ {
+ n_written = write (g.socket_fd, buf->str, buf->len);
+ if (n_written >= 0)
+ {
+ str_remove_slice (buf, 0, n_written);
+ continue;
+ }
+ if (errno == EAGAIN || errno == EINTR)
+ return true;
+
+ print_debug ("%s: %s: %s", __func__, "write", strerror (errno));
+ return false;
+ }
+ return true;
+}
+
+static void
+relay_update_poller (const struct pollfd *pfd)
+{
+ int new_events = POLLIN;
+ if (g.write_buffer.len)
+ new_events |= POLLOUT;
+
+ hard_assert (new_events != 0);
+ if (!pfd || pfd->events != new_events)
+ poller_fd_set (&g.socket_event, new_events);
+}
+
+static void
+on_relay_ready (const struct pollfd *pfd, void *user_data)
+{
+ if (relay_try_read () && relay_try_write ())
+ relay_update_poller (pfd);
+ else
+ {
+ // TODO: Probably autoreconnect.
+ exit_fatal ("disconnected");
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
on_connector_connecting (void *user_data, const char *address)
@@ -64,65 +599,526 @@ on_connector_connected (void *user_data, int socket, const char *hostname)
{
(void) user_data;
(void) hostname;
- g.polling = false;
- g.socket = socket;
+ connector_free (&g.connector);
+
+ set_blocking (socket, false);
+ set_cloexec (socket);
+
+ // We already buffer our output, so reduce latencies.
+ int yes = 1;
+ soft_assert (setsockopt (socket, IPPROTO_TCP, TCP_NODELAY,
+ &yes, sizeof yes) != -1);
+
+ g.socket_fd = socket;
+ g.socket_event = poller_fd_make (&g.poller, g.socket_fd);
+ g.socket_event.dispatcher = (poller_fd_fn) on_relay_ready;
+
+ str_reset (&g.read_buffer);
+ str_reset (&g.write_buffer);
+
+ struct str *s = &g.write_buffer;
+ str_pack_u32 (s, 0);
+ struct relay_command_message m = {};
+ m.command_seq = ++g.command_seq;
+ m.data.hello.command = RELAY_COMMAND_HELLO;
+ m.data.hello.version = RELAY_VERSION;
+ if (!relay_command_message_serialize (&m, s))
+ exit_fatal ("serialization failed");
+
+ uint32_t len = htonl (s->len - sizeof len);
+ memcpy (s->str, &len, sizeof len);
+
+ relay_update_poller (NULL);
}
static void
-protocol_test (const char *host, const char *port)
+relay_connect (const char *host, const char *port)
{
- struct poller poller = {};
- poller_init (&poller);
-
- connector_init (&g.connector, &poller);
+ connector_init (&g.connector, &g.poller);
g.connector.on_connecting = on_connector_connecting;
g.connector.on_error = on_connector_error;
g.connector.on_connected = on_connector_connected;
g.connector.on_failure = on_connector_failure;
connector_add_target (&g.connector, host, port);
+}
- g.polling = true;
- while (g.polling)
- poller_run (&poller);
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- connector_free (&g.connector);
+static XRenderColor x11_default_fg = { .alpha = 0xffff };
+static XRenderColor x11_default_bg = { 0xffff, 0xffff, 0xffff, 0xffff };
+static XErrorHandler x11_default_error_handler;
- struct str s = str_make ();
- str_pack_u32 (&s, 0);
- struct relay_command_message m = {};
- m.data.hello.command = RELAY_COMMAND_HELLO;
- m.data.hello.version = RELAY_VERSION;
- if (!relay_command_message_serialize (&m, &s))
- exit_fatal ("serialization failed");
+static struct x11_font_link *
+x11_font_link_new (XftFont *font)
+{
+ struct x11_font_link *self = xcalloc (1, sizeof *self);
+ self->font = font;
+ return self;
+}
- uint32_t len = htonl (s.len - sizeof len);
- memcpy (s.str, &len, sizeof len);
- if (errno = 0, write (g.socket, s.str, s.len) != (ssize_t) s.len)
- exit_fatal ("short send or error: %s", strerror (errno));
+static void
+x11_font_link_destroy (struct x11_font_link *self)
+{
+ XftFontClose (g.dpy, self->font);
+ free (self);
+}
- char buf[1 << 20] = "";
- while (errno = 0, read (g.socket, &len, sizeof len) == sizeof len)
+static struct x11_font_link *
+x11_font_link_open (FcPattern *pattern)
+{
+ XftFont *font = XftFontOpenPattern (g.dpy, pattern);
+ if (!font)
{
- len = ntohl (len);
- if (errno = 0, read (g.socket, buf, MIN (len, sizeof buf)) != len)
- exit_fatal ("short read or error: %s", strerror (errno));
+ FcPatternDestroy (pattern);
+ return NULL;
+ }
+ return x11_font_link_new (font);
+}
- struct msg_unpacker r = msg_unpacker_make (buf, len);
- struct relay_event_message m = {};
- if (!relay_event_message_deserialize (&m, &r))
- exit_fatal ("deserialization failed");
- if (msg_unpacker_get_available (&r))
- exit_fatal ("trailing data");
+static struct x11_font *
+x11_font_open (unsigned style)
+{
+ FcPattern *pattern = (style & X11_FONT_MONOSPACE)
+ ? FcNameParse ((const FcChar8 *) g.x11_fontname_monospace)
+ : FcNameParse ((const FcChar8 *) g.x11_fontname);
+ if (style & X11_FONT_BOLD)
+ FcPatternAdd (pattern, FC_STYLE, (FcValue) {
+ .type = FcTypeString, .u.s = (FcChar8 *) "Bold" }, FcFalse);
+ if (style & X11_FONT_ITALIC)
+ FcPatternAdd (pattern, FC_STYLE, (FcValue) {
+ .type = FcTypeString, .u.s = (FcChar8 *) "Italic" }, FcFalse);
- printf ("event: %d\n", m.data.event);
- relay_event_message_free (&m);
+ FcPattern *substituted = FcPatternDuplicate (pattern);
+ FcConfigSubstitute (NULL, substituted, FcMatchPattern);
+
+ FcResult result = 0;
+ FcPattern *match = XftFontMatch (g.dpy,
+ DefaultScreen (g.dpy), substituted, &result);
+ FcPatternDestroy (substituted);
+ struct x11_font_link *link = NULL;
+ if (!match || !(link = x11_font_link_open (match)))
+ {
+ FcPatternDestroy (pattern);
+ return NULL;
}
- exit_fatal ("short read or error: %s", strerror (errno));
+
+ struct x11_font *self = xcalloc (1, sizeof *self);
+ self->list = link;
+ self->style = style;
+ self->pattern = pattern;
+ self->unavailable = FcCharSetCreate ();
+ return self;
+}
+
+static void
+x11_font_destroy (struct x11_font *self)
+{
+ FcPatternDestroy (self->pattern);
+ FcCharSetDestroy (self->unavailable);
+ LIST_FOR_EACH (struct x11_font_link, iter, self->list)
+ x11_font_link_destroy (iter);
+ free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+static void
+x11_init_pixmap (void)
+{
+ int screen = DefaultScreen (g.dpy);
+ g.x11_pixmap = XCreatePixmap (g.dpy, g.x11_window,
+ MAX (1, g.ui_width), MAX (1, 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_selection_request (XSelectionRequestEvent *ev)
+{
+ Atom xa_targets = XInternAtom (g.dpy, "TARGETS", False);
+ Atom xa_compound_text = XInternAtom (g.dpy, "COMPOUND_TEXT", False);
+ Atom xa_utf8 = XInternAtom (g.dpy, "UTF8_STRING", False);
+ Atom targets[] = { xa_targets, XA_STRING, xa_compound_text, xa_utf8 };
+
+ XEvent response = {};
+ bool ok = false;
+ Atom property = ev->property ? ev->property : ev->target;
+ if (!g.x11_selection)
+ goto out;
+
+ XICCEncodingStyle style = 0;
+ if ((ok = ev->target == xa_targets))
+ {
+ XChangeProperty (g.dpy, ev->requestor, property,
+ XA_ATOM, 32, PropModeReplace,
+ (const unsigned char *) targets, N_ELEMENTS (targets));
+ goto out;
+ }
+ else if (ev->target == XA_STRING)
+ style = XStringStyle;
+ else if (ev->target == xa_compound_text)
+ style = XCompoundTextStyle;
+ else if (ev->target == xa_utf8)
+ style = XUTF8StringStyle;
+ else
+ goto out;
+
+ // XXX: We let it crash us with BadLength, but we may, e.g., use INCR.
+ XTextProperty text = {};
+ if ((ok = !Xutf8TextListToTextProperty
+ (g.dpy, &g.x11_selection, 1, style, &text)))
+ {
+ XChangeProperty (g.dpy, ev->requestor, property,
+ text.encoding, text.format, PropModeReplace,
+ text.value, text.nitems);
+ }
+ XFree (text.value);
+
+out:
+ response.xselection.type = SelectionNotify;
+ // XXX: We should check it against the event causing XSetSelectionOwner().
+ response.xselection.time = ev->time;
+ response.xselection.requestor = ev->requestor;
+ response.xselection.selection = ev->selection;
+ response.xselection.target = ev->target;
+ response.xselection.property = ok ? property : None;
+ XSendEvent (g.dpy, ev->requestor, False, 0, &response);
+}
+
+static bool
+on_x11_input_event (XEvent *e)
+{
+ if (e->type != KeyPress)
+ return false;
+
+ // A kibibyte long buffer will have to suffice for anyone.
+ XKeyEvent *ev = &e->xkey;
+ char buf[1 << 10] = {}, *p = buf;
+ KeySym keysym = None;
+ Status status = 0;
+ int len = Xutf8LookupString
+ (g.x11_ic, ev, buf, sizeof buf, &keysym, &status);
+ if (status == XBufferOverflow)
+ print_warning ("input method overflow");
+
+ // TODO: Implement an input line.
+ switch (keysym)
+ {
+ case XK_q:
+ app_quit ();
+ return true;
+ }
+ return false;
+}
+
+static void
+on_x11_event (XEvent *ev)
+{
+ switch (ev->type)
+ {
+ case Expose:
+ {
+ XRectangle r = { ev->xexpose.x, ev->xexpose.y,
+ ev->xexpose.width, ev->xexpose.height };
+ XUnionRectWithRegion (&r, g.x11_clip, g.x11_clip);
+ poller_idle_set (&g.flip_event);
+ 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 ();
+ break;
+ case SelectionRequest:
+ on_x11_selection_request (&ev->xselectionrequest);
+ break;
+ case SelectionClear:
+ cstr_set (&g.x11_selection, NULL);
+ break;
+ // UnmapNotify can be received when restarting the window manager.
+ // Should this turn out to be unreliable (window not destroyed by WM
+ // upon closing), opt for the WM_DELETE_WINDOW protocol as well.
+ case DestroyNotify:
+ app_quit ();
+ break;
+ case FocusIn:
+ g.ui_focused = true;
+ app_invalidate ();
+ break;
+ case FocusOut:
+ g.ui_focused = false;
+ app_invalidate ();
+ break;
+ case KeyPress:
+ case ButtonPress:
+ case ButtonRelease:
+ case MotionNotify:
+ if (!on_x11_input_event (ev))
+ XkbBell (g.dpy, ev->xany.window, 0, None);
+ }
+}
+
+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");
+ if (XFilterEvent (&ev.core, None))
+ continue;
+
+ 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;
+
+ // XXX: The simplest possible way of discarding selection management errors.
+ // XCB would be a small win here, but it is a curse at the same time.
+ if (event->error_code == BadWindow && event->resourceid != g.x11_window)
+ return 0;
+
+ return x11_default_error_handler (dpy, event);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+x11_init (void)
+{
+ // https://tedyin.com/posts/a-brief-intro-to-linux-input-method-framework/
+ if (!XSupportsLocale ())
+ print_warning ("locale not supported by Xlib");
+ XSetLocaleModifiers ("");
+
+ 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");
+ if (!(g.x11_im = XOpenIM (g.dpy, NULL, NULL, NULL)))
+ exit_fatal ("failed to open an input method");
+
+ 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);
+
+ struct xdg_xsettings settings = xdg_xsettings_make ();
+ xdg_xsettings_update (&settings, g.dpy);
+
+ if (!FcInit ())
+ print_warning ("Fontconfig initialization failed");
+ if (!(g.xft_fonts = x11_font_open (0)))
+ exit_fatal ("cannot open a font");
+
+ 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
+ | Button1MotionMask,
+ .bit_gravity = NorthWestGravity,
+ .background_pixel = default_bg.pixel,
+ };
+
+ // 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.xft_fonts->list->font->height;
+ g.ui_width = g.ui_height * 4 / 3;
+
+ long im_event_mask = 0;
+ if (!XGetIMValues (g.x11_im, XNFilterEvents, &im_event_mask, NULL))
+ attrs.event_mask |= im_event_mask;
+
+ 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_clip = XCreateRegion ();
+
+ XTextProperty prop = {};
+ char *name = PROGRAM_NAME;
+ if (!Xutf8TextListToTextProperty (g.dpy, &name, 1, XUTF8StringStyle, &prop))
+ XSetWMName (g.dpy, g.x11_window, &prop);
+ XFree (prop.value);
+
+ // This is a rather GNOME-centric mechanism, but it's better than nothing.
+ const char *icon_theme_name = NULL;
+ const struct xdg_xsettings_setting *setting =
+ str_map_find (&settings.settings, "Net/IconThemeName");
+ if (setting != NULL && setting->type == XDG_XSETTINGS_STRING)
+ icon_theme_name = setting->string.str;
+ icon_theme_set_window_icon (g.dpy, g.x11_window, icon_theme_name, name);
+ xdg_xsettings_free (&settings);
+
+ // TODO: It is possible to do, e.g., on-the-spot.
+ XIMStyle im_style = XIMPreeditNothing | XIMStatusNothing;
+ XIMStyles *im_styles = NULL;
+ bool im_style_found = false;
+ if (!XGetIMValues (g.x11_im, XNQueryInputStyle, &im_styles, NULL)
+ && im_styles)
+ {
+ for (unsigned i = 0; i < im_styles->count_styles; i++)
+ im_style_found |= im_styles->supported_styles[i] == im_style;
+ XFree (im_styles);
+ }
+ if (!im_style_found)
+ print_warning ("failed to find the desired input method style");
+ if (!(g.x11_ic = XCreateIC (g.x11_im,
+ XNInputStyle, im_style,
+ XNClientWindow, g.x11_window,
+ NULL)))
+ exit_fatal ("failed to open an input context");
+
+ XSetICFocus (g.x11_ic);
+
+ x11_init_pixmap ();
+ g.xft_draw = XftDrawCreate (g.dpy, g.x11_pixmap, visual, cmap);
+
+ XMapWindow (g.dpy, g.x11_window);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+render_line (int *bottom, const char *text)
+{
+ XftFont *font = g.xft_fonts->list->font;
+ XftColor color = { .color = x11_default_fg };
+ XftDrawStringUtf8 (g.xft_draw, &color, font, 0, *bottom - font->descent,
+ (const FcChar8 *) text, strlen (text));
+ return (*bottom -= font->height) > 0;
+}
+
+static void
+x11_render (void)
+{
+ XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture,
+ &x11_default_bg, 0, 0, g.ui_width, g.ui_height);
+
+ int bottom = g.ui_height;
+ render_line (&bottom, "xF");
+ if (!g.buffer_current)
+ render_line (&bottom, "-");
+ else
+ {
+ struct buffer *buffer = g.buffer_current;
+ render_line (&bottom, buffer->buffer_name);
+ struct buffer_line *line = buffer->lines_tail;
+ for (; line; line = line->prev)
+ if (!render_line (&bottom, line->text))
+ break;
+ }
+
+ XRectangle r = { 0, 0, g.ui_width, g.ui_height };
+ XUnionRectWithRegion (&r, g.x11_clip, g.x11_clip);
+ poller_idle_set (&g.xpending_event);
+}
+
+static void
+x11_flip (void)
+{
+ // This exercise in futility doesn't seem to affect CPU usage much.
+ XRectangle r = {};
+ XClipBox (g.x11_clip, &r);
+ XCopyArea (g.dpy, g.x11_pixmap, g.x11_window,
+ DefaultGC (g.dpy, DefaultScreen (g.dpy)),
+ r.x, r.y, r.width, r.height, r.x, r.y);
+
+ XSubtractRegion (g.x11_clip, g.x11_clip, g.x11_clip);
+ poller_idle_set (&g.xpending_event);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+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)
+ x11_flip ();
+}
+
+static void
+app_on_refresh (void *user_data)
+{
+ (void) user_data;
+ poller_idle_reset (&g.refresh_event);
+
+ x11_render ();
+ poller_idle_set (&g.flip_event);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+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;
+}
+
int
main (int argc, char *argv[])
{
@@ -166,7 +1162,15 @@ main (int argc, char *argv[])
if (!port)
exit_fatal ("missing port number/service name");
- // TODO: Actually implement an X11-based user interface.
- protocol_test (host, port);
+ app_init_context ();
+ app_init_poller_events ();
+ x11_init ();
+
+ relay_connect (host, port);
+ free (address);
+
+ g.polling = true;
+ while (g.polling)
+ poller_run (&g.poller);
return 0;
}
diff --git a/xM/main.swift b/xM/main.swift
index 91e3499..48f26c4 100644
--- a/xM/main.swift
+++ b/xM/main.swift
@@ -842,11 +842,11 @@ relayRPC.onEvent = { message in
b.bufferName = data.new
- refreshBufferList()
if b.bufferName == relayBufferCurrent {
relayBufferCurrent = data.new
refreshStatus()
}
+ refreshBufferList()
if b.bufferName == relayBufferLast {
relayBufferLast = data.new
}
@@ -1203,6 +1203,7 @@ class WindowDelegate: NSObject, NSWindowDelegate {
b.highlighted = false
refreshIcon()
+ refreshBufferList()
}
// Buffer indexes rotated to start after the current buffer.
diff --git a/xP/public/xP.js b/xP/public/xP.js
index 5436a65..6035db3 100644
--- a/xP/public/xP.js
+++ b/xP/public/xP.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2022 - 2023, Přemysl Eric Janouch <p@janouch.name>
+// Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
// SPDX-License-Identifier: 0BSD
import * as Relay from './proto.js'
diff --git a/xW/xW.cpp b/xW/xW.cpp
index b05eb37..7fd8950 100644
--- a/xW/xW.cpp
+++ b/xW/xW.cpp
@@ -1,7 +1,7 @@
/*
* xW.cpp: Win32 frontend for xC
*
- * Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
+ * Copyright (c) 2023 - 2024, 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.
@@ -255,73 +255,6 @@ buffer_by_name(const std::wstring &name)
return nullptr;
}
-static void
-buffer_activate(const std::wstring &name)
-{
- auto activate = new Relay::CommandData_BufferActivate();
- activate->buffer_name = name;
- relay_send(activate);
-}
-
-static void
-buffer_toggle_unimportant(const std::wstring &name)
-{
- auto toggle = new Relay::CommandData_BufferToggleUnimportant();
- toggle->buffer_name = name;
- relay_send(toggle);
-}
-
-// --- Current buffer ----------------------------------------------------------
-
-static void
-buffer_toggle_log(
- const std::wstring &error, const Relay::ResponseData_BufferLog *response)
-{
- if (!response) {
- show_error_message(error.c_str());
- return;
- }
-
- std::wstring log;
- if (!LibertyXDR::utf8_to_wstring(
- response->log.data(), response->log.size(), log)) {
- show_error_message(L"Invalid encoding.");
- return;
- }
-
- std::wstring filtered;
- for (auto wch : log) {
- if (wch == L'\n')
- filtered += L"\r\n";
- else
- filtered += wch;
- }
-
- SetWindowText(g.hwndBufferLog, filtered.c_str());
- ShowWindow(g.hwndBuffer, SW_HIDE);
- ShowWindow(g.hwndBufferLog, SW_SHOW);
-}
-
-static void
-buffer_toggle_log()
-{
- if (IsWindowVisible(g.hwndBufferLog)) {
- ShowWindow(g.hwndBufferLog, SW_HIDE);
- ShowWindow(g.hwndBuffer, SW_SHOW);
- SetWindowText(g.hwndBufferLog, L"");
- return;
- }
-
- auto log = new Relay::CommandData_BufferLog();
- log->buffer_name = g.buffer_current;
- relay_send(log, [name = g.buffer_current](auto error, auto response) {
- if (g.buffer_current != name)
- return;
- buffer_toggle_log(error,
- dynamic_cast<const Relay::ResponseData_BufferLog *>(response));
- });
-}
-
static bool
buffer_at_bottom()
{
@@ -354,6 +287,7 @@ refresh_icon()
if (b.highlighted)
icon = g.hiconHighlighted;
+ // XXX: This may not change the taskbar icon.
SendMessage(g.hwndMain, WM_SETICON, ICON_SMALL, (LPARAM) icon);
SendMessage(g.hwndMain, WM_SETICON, ICON_BIG, (LPARAM) icon);
}
@@ -430,6 +364,88 @@ refresh_status()
SetWindowText(g.hwndStatus, status.c_str());
}
+static void
+recheck_highlighted()
+{
+ // Corresponds to the logic toggling the bool on.
+ auto b = buffer_by_name(g.buffer_current);
+ if (b && b->highlighted && buffer_at_bottom() &&
+ !IsIconic(g.hwndMain) && !IsWindowVisible(g.hwndBufferLog)) {
+ b->highlighted = false;
+ refresh_icon();
+ refresh_buffer_list();
+ }
+}
+
+// --- Buffer actions ----------------------------------------------------------
+
+static void
+buffer_activate(const std::wstring &name)
+{
+ auto activate = new Relay::CommandData_BufferActivate();
+ activate->buffer_name = name;
+ relay_send(activate);
+}
+
+static void
+buffer_toggle_unimportant(const std::wstring &name)
+{
+ auto toggle = new Relay::CommandData_BufferToggleUnimportant();
+ toggle->buffer_name = name;
+ relay_send(toggle);
+}
+
+static void
+buffer_toggle_log(
+ const std::wstring &error, const Relay::ResponseData_BufferLog *response)
+{
+ if (!response) {
+ show_error_message(error.c_str());
+ return;
+ }
+
+ std::wstring log;
+ if (!LibertyXDR::utf8_to_wstring(
+ response->log.data(), response->log.size(), log)) {
+ show_error_message(L"Invalid encoding.");
+ return;
+ }
+
+ std::wstring filtered;
+ for (auto wch : log) {
+ if (wch == L'\n')
+ filtered += L"\r\n";
+ else
+ filtered += wch;
+ }
+
+ SetWindowText(g.hwndBufferLog, filtered.c_str());
+ ShowWindow(g.hwndBuffer, SW_HIDE);
+ ShowWindow(g.hwndBufferLog, SW_SHOW);
+}
+
+static void
+buffer_toggle_log()
+{
+ if (IsWindowVisible(g.hwndBufferLog)) {
+ ShowWindow(g.hwndBufferLog, SW_HIDE);
+ ShowWindow(g.hwndBuffer, SW_SHOW);
+ SetWindowText(g.hwndBufferLog, L"");
+
+ recheck_highlighted();
+ return;
+ }
+
+ auto log = new Relay::CommandData_BufferLog();
+ log->buffer_name = g.buffer_current;
+ relay_send(log, [name = g.buffer_current](auto error, auto response) {
+ if (g.buffer_current != name)
+ return;
+ buffer_toggle_log(error,
+ dynamic_cast<const Relay::ResponseData_BufferLog *>(response));
+ });
+}
+
// --- Rich Edit formatting ----------------------------------------------------
static COLORREF
@@ -695,7 +711,7 @@ buffer_print_line(std::vector<BufferLine>::const_iterator begin,
static void
buffer_print_separator()
{
- bool sameline = !GetWindowTextLength(g.hwndBuffer);
+ bool sameline = !buffer_reset_selection();
CHARFORMAT2 format = default_charformat();
format.dwEffects &= ~CFE_AUTOCOLOR;
@@ -728,6 +744,7 @@ refresh_buffer(const Buffer &b)
buffer_print_and_watch_trailing_date_changes();
buffer_scroll_to_bottom();
+ // We will get a scroll event, so no need to recheck_highlighted() here.
SendMessage(g.hwndBuffer, WM_SETREDRAW, (WPARAM) TRUE, 0);
InvalidateRect(g.hwndBuffer, NULL, TRUE);
@@ -749,8 +766,9 @@ relay_process_buffer_line(Buffer &b, Relay::EventData_BufferLine &m)
// Retained mode is complicated.
bool display = (!m.is_unimportant || !bc->hide_unimportant) &&
(b.buffer_name == g.buffer_current || m.leak_to_active);
+ // XXX: It would be great if it didn't autoscroll when focused.
bool to_bottom = display &&
- buffer_at_bottom();
+ (buffer_at_bottom() || GetFocus() == g.hwndBuffer);
bool visible = display &&
to_bottom &&
!IsIconic(g.hwndMain) &&
@@ -914,11 +932,11 @@ relay_process_message(const Relay::EventMessage &m)
b->buffer_name = data.new_;
- refresh_buffer_list();
if (data.buffer_name == g.buffer_current) {
g.buffer_current = data.new_;
refresh_status();
}
+ refresh_buffer_list();
if (data.buffer_name == g.buffer_last)
g.buffer_last = data.new_;
break;
@@ -1465,6 +1483,7 @@ richedit_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
{
// Dragging the scrollbar doesn't result in EN_VSCROLL.
LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam);
+ recheck_highlighted();
refresh_status();
return lResult;
}
@@ -1522,8 +1541,12 @@ process_resize(UINT w, UINT h)
MoveWindow(g.hwndBufferList, 3, top, 150, h - top - bottom, FALSE);
MoveWindow(g.hwndBuffer, 156, top, w - 159, h - top - bottom, FALSE);
MoveWindow(g.hwndBufferLog, 156, top, w - 159, h - top - bottom, FALSE);
- if (to_bottom)
+ if (to_bottom) {
buffer_scroll_to_bottom();
+ } else {
+ recheck_highlighted();
+ refresh_status();
+ }
InvalidateRect(g.hwndMain, NULL, TRUE);
}
@@ -1685,8 +1708,10 @@ window_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
}
case WM_SYSCOMMAND:
{
+ // We're not deiconified yet, so duplicate recheck_highlighted().
auto b = buffer_by_name(g.buffer_current);
- if (b && wParam == SC_RESTORE) {
+ if (wParam == SC_RESTORE && b && b->highlighted && buffer_at_bottom() &&
+ !IsWindowVisible(g.hwndBufferLog)) {
b->highlighted = false;
refresh_icon();
}
@@ -1694,13 +1719,15 @@ window_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
break;
}
case WM_COMMAND:
- if (!lParam)
+ if (!lParam) {
process_accelerator(LOWORD(wParam));
- else if (lParam == (LPARAM) g.hwndBufferList)
+ } else if (lParam == (LPARAM) g.hwndBufferList) {
process_bufferlist_notification(HIWORD(wParam));
- else if (lParam == (LPARAM) g.hwndBuffer &&
- HIWORD(wParam) == EN_VSCROLL)
+ } else if (lParam == (LPARAM) g.hwndBuffer &&
+ HIWORD(wParam) == EN_VSCROLL) {
+ recheck_highlighted();
refresh_status();
+ }
return 0;
case WM_NOTIFY:
switch (((LPNMHDR) lParam)->code) {