Compare commits
	
		
			16 Commits
		
	
	
		
			9c9776bacd
			...
			v2.1.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						a8575ab875
	
				 | 
					
					
						|||
| 
						
						
							
						
						081525f5be
	
				 | 
					
					
						|||
| 
						
						
							
						
						6ac2ac5511
	
				 | 
					
					
						|||
| 
						
						
							
						
						f6483489c2
	
				 | 
					
					
						|||
| 
						
						
							
						
						ed5ac1815b
	
				 | 
					
					
						|||
| 
						
						
							
						
						509cb9f4dd
	
				 | 
					
					
						|||
| 
						
						
							
						
						b3684c4d9f
	
				 | 
					
					
						|||
| 
						
						
							
						
						918c589c65
	
				 | 
					
					
						|||
| 
						
						
							
						
						21095a11d6
	
				 | 
					
					
						|||
| 
						
						
							
						
						a22baa4b55
	
				 | 
					
					
						|||
| 
						
						
							
						
						b3e545e0bb
	
				 | 
					
					
						|||
| 
						
						
							
						
						cd76702ab2
	
				 | 
					
					
						|||
| 
						
						
							
						
						977b073b58
	
				 | 
					
					
						|||
| 
						
						
							
						
						46be4836df
	
				 | 
					
					
						|||
| 
						
						
							
						
						05a41b2629
	
				 | 
					
					
						|||
| 
						
						
							
						
						a62ed5bbac
	
				 | 
					
					
						
							
								
								
									
										12
									
								
								NEWS
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								NEWS
									
									
									
									
									
								
							@@ -1,7 +1,17 @@
 | 
			
		||||
Unreleased
 | 
			
		||||
2.1.0 (2024-12-19) "Bunnyrific"
 | 
			
		||||
 | 
			
		||||
 * xC: fixed a crash when the channel topic had too many formatting items
 | 
			
		||||
 | 
			
		||||
 * xC: fixed keyboard EOF behaviour with Readline >= 8.0
 | 
			
		||||
 | 
			
		||||
 * xC: made it possible to stream commands into the binary
 | 
			
		||||
 | 
			
		||||
 * xM/xW: various bugfixes
 | 
			
		||||
 | 
			
		||||
 * Added a Fyne frontend for xC called xA
 | 
			
		||||
 | 
			
		||||
 * Added a Qt Widgets frontend for xC called xT
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
2.0.0 (2024-07-28) "Perfect Is the Enemy of Good"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								README.adoc
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								README.adoc
									
									
									
									
									
								
							@@ -33,9 +33,10 @@ including link:xC.adoc#_key_bindings[keyboard shortcuts].
 | 
			
		||||
 | 
			
		||||
image::xP.webp[align="center"]
 | 
			
		||||
 | 
			
		||||
xA, xW, xM
 | 
			
		||||
----------
 | 
			
		||||
The native frontends for 'xC'.  Using them is not recommended.
 | 
			
		||||
xA, xT, xW, xM
 | 
			
		||||
--------------
 | 
			
		||||
Fyne, Qt Widgets, Win32, Cocoa frontends for 'xC'.
 | 
			
		||||
Using them is not recommended.
 | 
			
		||||
 | 
			
		||||
xD
 | 
			
		||||
--
 | 
			
		||||
@@ -150,7 +151,13 @@ xA
 | 
			
		||||
~~
 | 
			
		||||
The Fyne frontend supports all of Linux, FreeBSD, Windows, macOS, Android, and
 | 
			
		||||
iOS natively, albeit somewhat poorly.  Only use `fyne` or `fyne-cross` after
 | 
			
		||||
running `make` first.
 | 
			
		||||
running `make generate` first.
 | 
			
		||||
 | 
			
		||||
xT
 | 
			
		||||
~~
 | 
			
		||||
The Qt Widgets frontend is a separate CMake subproject.  It generally supports
 | 
			
		||||
all desktop operating systems.  To avoid having to specify the relay address
 | 
			
		||||
each time you run it, pass it on the command line.
 | 
			
		||||
 | 
			
		||||
xW
 | 
			
		||||
~~
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								liberty
									
									
									
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								liberty
									
									
									
									
									
								
							 Submodule liberty updated: 492815c8fc...1930f138d4
									
								
							
							
								
								
									
										158
									
								
								xA/xA.go
									
									
									
									
									
								
							
							
						
						
									
										158
									
								
								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,61 +367,6 @@ func bufferByName(name string) *buffer {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func bufferActivate(name string) {
 | 
			
		||||
	relaySend(RelayCommandData{
 | 
			
		||||
		Variant: &RelayCommandDataBufferActivate{BufferName: name},
 | 
			
		||||
	}, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func bufferToggleUnimportant(name string) {
 | 
			
		||||
	relaySend(RelayCommandData{
 | 
			
		||||
		Variant: &RelayCommandDataBufferToggleUnimportant{BufferName: name},
 | 
			
		||||
	}, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func bufferPushLine(b *buffer, line bufferLine) {
 | 
			
		||||
	b.lines = append(b.lines, line)
 | 
			
		||||
 | 
			
		||||
	// Fyne's text layouting is extremely slow.
 | 
			
		||||
	// The limit could be made configurable,
 | 
			
		||||
	// and we could use a ring buffer approach to storing the lines.
 | 
			
		||||
	if len(b.lines) > 100 {
 | 
			
		||||
		b.lines = slices.Delete(b.lines, 0, 1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 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
 | 
			
		||||
@@ -432,22 +381,31 @@ func bufferScrollToBottom() {
 | 
			
		||||
	refreshStatus()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func bufferPushLine(b *buffer, line bufferLine) {
 | 
			
		||||
	b.lines = append(b.lines, line)
 | 
			
		||||
 | 
			
		||||
	// Fyne's text layouting is extremely slow.
 | 
			
		||||
	// The limit could be made configurable,
 | 
			
		||||
	// and we could use a ring buffer approach to storing the lines.
 | 
			
		||||
	if len(b.lines) > 100 {
 | 
			
		||||
		b.lines = slices.Delete(b.lines, 0, 1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 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()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										777
									
								
								xC.c
									
									
									
									
									
								
							
							
						
						
									
										777
									
								
								xC.c
									
									
									
									
									
								
							@@ -474,6 +474,10 @@ input_rl_start (void *input, const char *program_name)
 | 
			
		||||
	// autofilter, and we don't generally want alphabetic ordering at all
 | 
			
		||||
	rl_sort_completion_matches = false;
 | 
			
		||||
 | 
			
		||||
	// Readline >= 8.0 otherwise prints spurious newlines on EOF.
 | 
			
		||||
	if (RL_VERSION_MAJOR >= 8)
 | 
			
		||||
		rl_variable_bind ("enable-bracketed-paste", "off");
 | 
			
		||||
 | 
			
		||||
	hard_assert (self->prompt != NULL);
 | 
			
		||||
	// The inputrc is read before any callbacks are called, so we need to
 | 
			
		||||
	// register all functions that our user may want to map up front
 | 
			
		||||
@@ -2891,390 +2895,6 @@ serialize_configuration (struct config_item *root, struct str *output)
 | 
			
		||||
	config_item_write (root, true, output);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Relay output ------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
client_kill (struct client *c)
 | 
			
		||||
{
 | 
			
		||||
	struct app_context *ctx = c->ctx;
 | 
			
		||||
	poller_fd_reset (&c->socket_event);
 | 
			
		||||
	xclose (c->socket_fd);
 | 
			
		||||
	c->socket_fd = -1;
 | 
			
		||||
 | 
			
		||||
	LIST_UNLINK (ctx->clients, c);
 | 
			
		||||
	client_destroy (c);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
client_update_poller (struct client *c, const struct pollfd *pfd)
 | 
			
		||||
{
 | 
			
		||||
	int new_events = POLLIN;
 | 
			
		||||
	if (c->write_buffer.len)
 | 
			
		||||
		new_events |= POLLOUT;
 | 
			
		||||
 | 
			
		||||
	hard_assert (new_events != 0);
 | 
			
		||||
	if (!pfd || pfd->events != new_events)
 | 
			
		||||
		poller_fd_set (&c->socket_event, new_events);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_send (struct client *c)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = &c->ctx->relay_message;
 | 
			
		||||
	m->event_seq = c->event_seq++;
 | 
			
		||||
 | 
			
		||||
	// TODO: Also don't try sending anything if half-closed.
 | 
			
		||||
	if (!c->initialized || c->socket_fd == -1)
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	// liberty has msg_{reader,writer} already, but they use 8-byte lengths.
 | 
			
		||||
	size_t frame_len_pos = c->write_buffer.len, frame_len = 0;
 | 
			
		||||
	str_pack_u32 (&c->write_buffer, 0);
 | 
			
		||||
	if (!relay_event_message_serialize (m, &c->write_buffer)
 | 
			
		||||
	 || (frame_len = c->write_buffer.len - frame_len_pos - 4) > UINT32_MAX)
 | 
			
		||||
	{
 | 
			
		||||
		print_error ("serialization failed, killing client");
 | 
			
		||||
		client_kill (c);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uint32_t len = htonl (frame_len);
 | 
			
		||||
	memcpy (c->write_buffer.str + frame_len_pos, &len, sizeof len);
 | 
			
		||||
	client_update_poller (c, NULL);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_broadcast_except (struct app_context *ctx, struct client *exception)
 | 
			
		||||
{
 | 
			
		||||
	LIST_FOR_EACH (struct client, c, ctx->clients)
 | 
			
		||||
		if (c != exception)
 | 
			
		||||
			relay_send (c);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define relay_broadcast(ctx) relay_broadcast_except ((ctx), NULL)
 | 
			
		||||
 | 
			
		||||
static struct relay_event_message *
 | 
			
		||||
relay_prepare (struct app_context *ctx)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = &ctx->relay_message;
 | 
			
		||||
	relay_event_message_free (m);
 | 
			
		||||
	memset (m, 0, sizeof *m);
 | 
			
		||||
	return m;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_ping (struct app_context *ctx)
 | 
			
		||||
{
 | 
			
		||||
	relay_prepare (ctx)->data.event = RELAY_EVENT_PING;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static union relay_item_data *
 | 
			
		||||
relay_translate_formatter (struct app_context *ctx, union relay_item_data *p,
 | 
			
		||||
	const struct formatter_item *i)
 | 
			
		||||
{
 | 
			
		||||
	// XXX: See attr_printer_decode_color(), this is a footgun.
 | 
			
		||||
	int16_t c16  = i->color;
 | 
			
		||||
	int16_t c256 = i->color >> 16;
 | 
			
		||||
 | 
			
		||||
	unsigned attrs = i->attribute;
 | 
			
		||||
	switch (i->type)
 | 
			
		||||
	{
 | 
			
		||||
	case FORMATTER_ITEM_TEXT:
 | 
			
		||||
		p->text.text = str_from_cstr (i->text);
 | 
			
		||||
		(p++)->kind = RELAY_ITEM_TEXT;
 | 
			
		||||
		break;
 | 
			
		||||
	case FORMATTER_ITEM_FG_COLOR:
 | 
			
		||||
		p->fg_color.color = c256 <= 0 ? c16 : c256;
 | 
			
		||||
		(p++)->kind = RELAY_ITEM_FG_COLOR;
 | 
			
		||||
		break;
 | 
			
		||||
	case FORMATTER_ITEM_BG_COLOR:
 | 
			
		||||
		p->bg_color.color = c256 <= 0 ? c16 : c256;
 | 
			
		||||
		(p++)->kind = RELAY_ITEM_BG_COLOR;
 | 
			
		||||
		break;
 | 
			
		||||
	case FORMATTER_ITEM_ATTR:
 | 
			
		||||
		(p++)->kind = RELAY_ITEM_RESET;
 | 
			
		||||
		if ((c256 = ctx->theme[i->attribute].fg) >= 0)
 | 
			
		||||
		{
 | 
			
		||||
			p->fg_color.color = c256;
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FG_COLOR;
 | 
			
		||||
		}
 | 
			
		||||
		if ((c256 = ctx->theme[i->attribute].bg) >= 0)
 | 
			
		||||
		{
 | 
			
		||||
			p->bg_color.color = c256;
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_BG_COLOR;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		attrs = ctx->theme[i->attribute].attrs;
 | 
			
		||||
		// Fall-through
 | 
			
		||||
	case FORMATTER_ITEM_SIMPLE:
 | 
			
		||||
		if (attrs & TEXT_BOLD)
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FLIP_BOLD;
 | 
			
		||||
		if (attrs & TEXT_ITALIC)
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FLIP_ITALIC;
 | 
			
		||||
		if (attrs & TEXT_UNDERLINE)
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FLIP_UNDERLINE;
 | 
			
		||||
		if (attrs & TEXT_INVERSE)
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FLIP_INVERSE;
 | 
			
		||||
		if (attrs & TEXT_CROSSED_OUT)
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FLIP_CROSSED_OUT;
 | 
			
		||||
		if (attrs & TEXT_MONOSPACE)
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FLIP_MONOSPACE;
 | 
			
		||||
		break;
 | 
			
		||||
	default:
 | 
			
		||||
		break;
 | 
			
		||||
	}
 | 
			
		||||
	return p;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static union relay_item_data *
 | 
			
		||||
relay_items (struct app_context *ctx, const struct formatter_item *items,
 | 
			
		||||
	uint32_t *len)
 | 
			
		||||
{
 | 
			
		||||
	size_t items_len = 0;
 | 
			
		||||
	for (size_t i = 0; items[i].type; i++)
 | 
			
		||||
		items_len++;
 | 
			
		||||
 | 
			
		||||
	// Beware of the upper bound, currently dominated by FORMATTER_ITEM_ATTR.
 | 
			
		||||
	union relay_item_data *a = xcalloc (items_len * 9, sizeof *a), *p = a;
 | 
			
		||||
	for (const struct formatter_item *i = items; items_len--; i++)
 | 
			
		||||
		p = relay_translate_formatter (ctx, p, i);
 | 
			
		||||
 | 
			
		||||
	*len = p - a;
 | 
			
		||||
	return a;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer,
 | 
			
		||||
	struct buffer_line *line, bool leak_to_active)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_line *e = &m->data.buffer_line;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_LINE;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
	e->is_unimportant = !!(line->flags & BUFFER_LINE_UNIMPORTANT);
 | 
			
		||||
	e->is_highlight = !!(line->flags & BUFFER_LINE_HIGHLIGHT);
 | 
			
		||||
	e->rendition = 1 + line->r;
 | 
			
		||||
	e->when = line->when * 1000;
 | 
			
		||||
	e->leak_to_active = leak_to_active;
 | 
			
		||||
	e->items = relay_items (ctx, line->items, &e->items_len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: Consider pushing this whole block of code much further down.
 | 
			
		||||
static void formatter_add (struct formatter *self, const char *format, ...);
 | 
			
		||||
static char *irc_to_utf8 (const char *text);
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_channel_buffer_update (struct app_context *ctx,
 | 
			
		||||
	struct buffer *buffer, struct relay_buffer_context_channel *e)
 | 
			
		||||
{
 | 
			
		||||
	struct channel *channel = buffer->channel;
 | 
			
		||||
	struct formatter f = formatter_make (ctx, buffer->server);
 | 
			
		||||
	if (channel->topic)
 | 
			
		||||
		formatter_add (&f, "#m", channel->topic);
 | 
			
		||||
	e->topic = relay_items (ctx, f.items, &e->topic_len);
 | 
			
		||||
	formatter_free (&f);
 | 
			
		||||
 | 
			
		||||
	// As in make_prompt(), conceal the last known channel modes.
 | 
			
		||||
	// XXX: This should use irc_channel_is_joined().
 | 
			
		||||
	if (!channel->users_len)
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	struct str modes = str_make ();
 | 
			
		||||
	str_append_str (&modes, &channel->no_param_modes);
 | 
			
		||||
 | 
			
		||||
	struct str params = str_make ();
 | 
			
		||||
	struct str_map_iter iter = str_map_iter_make (&channel->param_modes);
 | 
			
		||||
	const char *param;
 | 
			
		||||
	while ((param = str_map_iter_next (&iter)))
 | 
			
		||||
	{
 | 
			
		||||
		str_append_c (&modes, iter.link->key[0]);
 | 
			
		||||
		str_append_c (¶ms, ' ');
 | 
			
		||||
		str_append (¶ms, param);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	str_append_str (&modes, ¶ms);
 | 
			
		||||
	str_free (¶ms);
 | 
			
		||||
 | 
			
		||||
	char *modes_utf8 = irc_to_utf8 (modes.str);
 | 
			
		||||
	str_free (&modes);
 | 
			
		||||
	e->modes = str_from_cstr (modes_utf8);
 | 
			
		||||
	free (modes_utf8);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_update (struct app_context *ctx, struct buffer *buffer)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_update *e = &m->data.buffer_update;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_UPDATE;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
	e->hide_unimportant = buffer->hide_unimportant;
 | 
			
		||||
 | 
			
		||||
	struct str *server_name = NULL;
 | 
			
		||||
	switch (buffer->type)
 | 
			
		||||
	{
 | 
			
		||||
	case BUFFER_GLOBAL:
 | 
			
		||||
		e->context.kind = RELAY_BUFFER_KIND_GLOBAL;
 | 
			
		||||
		break;
 | 
			
		||||
	case BUFFER_SERVER:
 | 
			
		||||
		e->context.kind = RELAY_BUFFER_KIND_SERVER;
 | 
			
		||||
		server_name = &e->context.server.server_name;
 | 
			
		||||
		break;
 | 
			
		||||
	case BUFFER_CHANNEL:
 | 
			
		||||
		e->context.kind = RELAY_BUFFER_KIND_CHANNEL;
 | 
			
		||||
		server_name = &e->context.channel.server_name;
 | 
			
		||||
		relay_prepare_channel_buffer_update (ctx, buffer, &e->context.channel);
 | 
			
		||||
		break;
 | 
			
		||||
	case BUFFER_PM:
 | 
			
		||||
		e->context.kind = RELAY_BUFFER_KIND_PRIVATE_MESSAGE;
 | 
			
		||||
		server_name = &e->context.private_message.server_name;
 | 
			
		||||
		break;
 | 
			
		||||
	}
 | 
			
		||||
	if (server_name)
 | 
			
		||||
		*server_name = str_from_cstr (buffer->server->name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_stats (struct app_context *ctx, struct buffer *buffer)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_stats *e = &m->data.buffer_stats;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_STATS;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
	e->new_messages = MIN (UINT32_MAX,
 | 
			
		||||
		buffer->new_messages_count - buffer->new_unimportant_count);
 | 
			
		||||
	e->new_unimportant_messages = MIN (UINT32_MAX,
 | 
			
		||||
		buffer->new_unimportant_count);
 | 
			
		||||
	e->highlighted = buffer->highlighted;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_rename (struct app_context *ctx, struct buffer *buffer,
 | 
			
		||||
	const char *new_name)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_rename *e = &m->data.buffer_rename;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_RENAME;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
	e->new = str_from_cstr (new_name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_remove (struct app_context *ctx, struct buffer *buffer)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_remove *e = &m->data.buffer_remove;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_REMOVE;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_activate (struct app_context *ctx, struct buffer *buffer)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_activate *e = &m->data.buffer_activate;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_ACTIVATE;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_input (struct app_context *ctx, struct buffer *buffer,
 | 
			
		||||
	const char *input)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_input *e = &m->data.buffer_input;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_INPUT;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
	e->text = str_from_cstr (input);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_clear (struct app_context *ctx,
 | 
			
		||||
	struct buffer *buffer)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_clear *e = &m->data.buffer_clear;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_CLEAR;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum relay_server_state
 | 
			
		||||
relay_server_state_for_server (struct server *s)
 | 
			
		||||
{
 | 
			
		||||
	switch (s->state)
 | 
			
		||||
	{
 | 
			
		||||
	case IRC_DISCONNECTED: return RELAY_SERVER_STATE_DISCONNECTED;
 | 
			
		||||
	case IRC_CONNECTING:   return RELAY_SERVER_STATE_CONNECTING;
 | 
			
		||||
	case IRC_CONNECTED:    return RELAY_SERVER_STATE_CONNECTED;
 | 
			
		||||
	case IRC_REGISTERED:   return RELAY_SERVER_STATE_REGISTERED;
 | 
			
		||||
	case IRC_CLOSING:
 | 
			
		||||
	case IRC_HALF_CLOSED:  return RELAY_SERVER_STATE_DISCONNECTING;
 | 
			
		||||
	}
 | 
			
		||||
	return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_server_update (struct app_context *ctx, struct server *s)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_server_update *e = &m->data.server_update;
 | 
			
		||||
	e->event = RELAY_EVENT_SERVER_UPDATE;
 | 
			
		||||
	e->server_name = str_from_cstr (s->name);
 | 
			
		||||
	e->data.state = relay_server_state_for_server (s);
 | 
			
		||||
	if (s->state == IRC_REGISTERED)
 | 
			
		||||
	{
 | 
			
		||||
		char *user_utf8 = irc_to_utf8 (s->irc_user->nickname);
 | 
			
		||||
		e->data.registered.user = str_from_cstr (user_utf8);
 | 
			
		||||
		free (user_utf8);
 | 
			
		||||
 | 
			
		||||
		char *user_modes_utf8 = irc_to_utf8 (s->irc_user_modes.str);
 | 
			
		||||
		e->data.registered.user_modes = str_from_cstr (user_modes_utf8);
 | 
			
		||||
		free (user_modes_utf8);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_server_rename (struct app_context *ctx, struct server *s,
 | 
			
		||||
	const char *new_name)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_server_rename *e = &m->data.server_rename;
 | 
			
		||||
	e->event = RELAY_EVENT_SERVER_RENAME;
 | 
			
		||||
	e->server_name = str_from_cstr (s->name);
 | 
			
		||||
	e->new = str_from_cstr (new_name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_server_remove (struct app_context *ctx, struct server *s)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_server_remove *e = &m->data.server_remove;
 | 
			
		||||
	e->event = RELAY_EVENT_SERVER_REMOVE;
 | 
			
		||||
	e->server_name = str_from_cstr (s->name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_error (struct app_context *ctx, uint32_t seq, const char *message)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_error *e = &m->data.error;
 | 
			
		||||
	e->event = RELAY_EVENT_ERROR;
 | 
			
		||||
	e->command_seq = seq;
 | 
			
		||||
	e->error = str_from_cstr (message);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static struct relay_event_data_response *
 | 
			
		||||
relay_prepare_response (struct app_context *ctx, uint32_t seq)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_response *e = &m->data.response;
 | 
			
		||||
	e->event = RELAY_EVENT_RESPONSE;
 | 
			
		||||
	e->command_seq = seq;
 | 
			
		||||
	return e;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Terminal output ---------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
/// Default colour pair
 | 
			
		||||
@@ -4515,6 +4135,387 @@ formatter_flush (struct formatter *self, FILE *stream, int flush_opts)
 | 
			
		||||
	attr_printer_reset (&state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Relay output ------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
client_kill (struct client *c)
 | 
			
		||||
{
 | 
			
		||||
	struct app_context *ctx = c->ctx;
 | 
			
		||||
	poller_fd_reset (&c->socket_event);
 | 
			
		||||
	xclose (c->socket_fd);
 | 
			
		||||
	c->socket_fd = -1;
 | 
			
		||||
 | 
			
		||||
	LIST_UNLINK (ctx->clients, c);
 | 
			
		||||
	client_destroy (c);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
client_update_poller (struct client *c, const struct pollfd *pfd)
 | 
			
		||||
{
 | 
			
		||||
	int new_events = POLLIN;
 | 
			
		||||
	if (c->write_buffer.len)
 | 
			
		||||
		new_events |= POLLOUT;
 | 
			
		||||
 | 
			
		||||
	hard_assert (new_events != 0);
 | 
			
		||||
	if (!pfd || pfd->events != new_events)
 | 
			
		||||
		poller_fd_set (&c->socket_event, new_events);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_send (struct client *c)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = &c->ctx->relay_message;
 | 
			
		||||
	m->event_seq = c->event_seq++;
 | 
			
		||||
 | 
			
		||||
	// TODO: Also don't try sending anything if half-closed.
 | 
			
		||||
	if (!c->initialized || c->socket_fd == -1)
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	// liberty has msg_{reader,writer} already, but they use 8-byte lengths.
 | 
			
		||||
	size_t frame_len_pos = c->write_buffer.len, frame_len = 0;
 | 
			
		||||
	str_pack_u32 (&c->write_buffer, 0);
 | 
			
		||||
	if (!relay_event_message_serialize (m, &c->write_buffer)
 | 
			
		||||
	 || (frame_len = c->write_buffer.len - frame_len_pos - 4) > UINT32_MAX)
 | 
			
		||||
	{
 | 
			
		||||
		print_error ("serialization failed, killing client");
 | 
			
		||||
		client_kill (c);
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	uint32_t len = htonl (frame_len);
 | 
			
		||||
	memcpy (c->write_buffer.str + frame_len_pos, &len, sizeof len);
 | 
			
		||||
	client_update_poller (c, NULL);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_broadcast_except (struct app_context *ctx, struct client *exception)
 | 
			
		||||
{
 | 
			
		||||
	LIST_FOR_EACH (struct client, c, ctx->clients)
 | 
			
		||||
		if (c != exception)
 | 
			
		||||
			relay_send (c);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define relay_broadcast(ctx) relay_broadcast_except ((ctx), NULL)
 | 
			
		||||
 | 
			
		||||
static struct relay_event_message *
 | 
			
		||||
relay_prepare (struct app_context *ctx)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = &ctx->relay_message;
 | 
			
		||||
	relay_event_message_free (m);
 | 
			
		||||
	memset (m, 0, sizeof *m);
 | 
			
		||||
	return m;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_ping (struct app_context *ctx)
 | 
			
		||||
{
 | 
			
		||||
	relay_prepare (ctx)->data.event = RELAY_EVENT_PING;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static union relay_item_data *
 | 
			
		||||
relay_translate_formatter (struct app_context *ctx, union relay_item_data *p,
 | 
			
		||||
	const struct formatter_item *i)
 | 
			
		||||
{
 | 
			
		||||
	// XXX: See attr_printer_decode_color(), this is a footgun.
 | 
			
		||||
	int16_t c16  = i->color;
 | 
			
		||||
	int16_t c256 = i->color >> 16;
 | 
			
		||||
 | 
			
		||||
	unsigned attrs = i->attribute;
 | 
			
		||||
	switch (i->type)
 | 
			
		||||
	{
 | 
			
		||||
	case FORMATTER_ITEM_TEXT:
 | 
			
		||||
		p->text.text = str_from_cstr (i->text);
 | 
			
		||||
		(p++)->kind = RELAY_ITEM_TEXT;
 | 
			
		||||
		break;
 | 
			
		||||
	case FORMATTER_ITEM_FG_COLOR:
 | 
			
		||||
		p->fg_color.color = c256 <= 0 ? c16 : c256;
 | 
			
		||||
		(p++)->kind = RELAY_ITEM_FG_COLOR;
 | 
			
		||||
		break;
 | 
			
		||||
	case FORMATTER_ITEM_BG_COLOR:
 | 
			
		||||
		p->bg_color.color = c256 <= 0 ? c16 : c256;
 | 
			
		||||
		(p++)->kind = RELAY_ITEM_BG_COLOR;
 | 
			
		||||
		break;
 | 
			
		||||
	case FORMATTER_ITEM_ATTR:
 | 
			
		||||
		(p++)->kind = RELAY_ITEM_RESET;
 | 
			
		||||
		if ((c256 = ctx->theme[i->attribute].fg) >= 0)
 | 
			
		||||
		{
 | 
			
		||||
			p->fg_color.color = c256;
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FG_COLOR;
 | 
			
		||||
		}
 | 
			
		||||
		if ((c256 = ctx->theme[i->attribute].bg) >= 0)
 | 
			
		||||
		{
 | 
			
		||||
			p->bg_color.color = c256;
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_BG_COLOR;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		attrs = ctx->theme[i->attribute].attrs;
 | 
			
		||||
		// Fall-through
 | 
			
		||||
	case FORMATTER_ITEM_SIMPLE:
 | 
			
		||||
		if (attrs & TEXT_BOLD)
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FLIP_BOLD;
 | 
			
		||||
		if (attrs & TEXT_ITALIC)
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FLIP_ITALIC;
 | 
			
		||||
		if (attrs & TEXT_UNDERLINE)
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FLIP_UNDERLINE;
 | 
			
		||||
		if (attrs & TEXT_INVERSE)
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FLIP_INVERSE;
 | 
			
		||||
		if (attrs & TEXT_CROSSED_OUT)
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FLIP_CROSSED_OUT;
 | 
			
		||||
		if (attrs & TEXT_MONOSPACE)
 | 
			
		||||
			(p++)->kind = RELAY_ITEM_FLIP_MONOSPACE;
 | 
			
		||||
		break;
 | 
			
		||||
	default:
 | 
			
		||||
		break;
 | 
			
		||||
	}
 | 
			
		||||
	return p;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static union relay_item_data *
 | 
			
		||||
relay_items (struct app_context *ctx, const struct formatter_item *items,
 | 
			
		||||
	uint32_t *len)
 | 
			
		||||
{
 | 
			
		||||
	size_t items_len = 0;
 | 
			
		||||
	for (size_t i = 0; items[i].type; i++)
 | 
			
		||||
		items_len++;
 | 
			
		||||
 | 
			
		||||
	// Beware of the upper bound, currently dominated by FORMATTER_ITEM_ATTR.
 | 
			
		||||
	union relay_item_data *a = xcalloc (items_len * 9, sizeof *a), *p = a;
 | 
			
		||||
	for (const struct formatter_item *i = items; items_len--; i++)
 | 
			
		||||
		p = relay_translate_formatter (ctx, p, i);
 | 
			
		||||
 | 
			
		||||
	*len = p - a;
 | 
			
		||||
	return a;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer,
 | 
			
		||||
	struct buffer_line *line, bool leak_to_active)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_line *e = &m->data.buffer_line;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_LINE;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
	e->is_unimportant = !!(line->flags & BUFFER_LINE_UNIMPORTANT);
 | 
			
		||||
	e->is_highlight = !!(line->flags & BUFFER_LINE_HIGHLIGHT);
 | 
			
		||||
	e->rendition = 1 + line->r;
 | 
			
		||||
	e->when = line->when * 1000;
 | 
			
		||||
	e->leak_to_active = leak_to_active;
 | 
			
		||||
	e->items = relay_items (ctx, line->items, &e->items_len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_channel_buffer_update (struct app_context *ctx,
 | 
			
		||||
	struct buffer *buffer, struct relay_buffer_context_channel *e)
 | 
			
		||||
{
 | 
			
		||||
	struct channel *channel = buffer->channel;
 | 
			
		||||
	struct formatter f = formatter_make (ctx, buffer->server);
 | 
			
		||||
	if (channel->topic)
 | 
			
		||||
		formatter_add (&f, "#m", channel->topic);
 | 
			
		||||
	FORMATTER_ADD_ITEM (&f, END);
 | 
			
		||||
	e->topic = relay_items (ctx, f.items, &e->topic_len);
 | 
			
		||||
	formatter_free (&f);
 | 
			
		||||
 | 
			
		||||
	// As in make_prompt(), conceal the last known channel modes.
 | 
			
		||||
	// XXX: This should use irc_channel_is_joined().
 | 
			
		||||
	if (!channel->users_len)
 | 
			
		||||
		return;
 | 
			
		||||
 | 
			
		||||
	struct str modes = str_make ();
 | 
			
		||||
	str_append_str (&modes, &channel->no_param_modes);
 | 
			
		||||
 | 
			
		||||
	struct str params = str_make ();
 | 
			
		||||
	struct str_map_iter iter = str_map_iter_make (&channel->param_modes);
 | 
			
		||||
	const char *param;
 | 
			
		||||
	while ((param = str_map_iter_next (&iter)))
 | 
			
		||||
	{
 | 
			
		||||
		str_append_c (&modes, iter.link->key[0]);
 | 
			
		||||
		str_append_c (¶ms, ' ');
 | 
			
		||||
		str_append (¶ms, param);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	str_append_str (&modes, ¶ms);
 | 
			
		||||
	str_free (¶ms);
 | 
			
		||||
 | 
			
		||||
	char *modes_utf8 = irc_to_utf8 (modes.str);
 | 
			
		||||
	str_free (&modes);
 | 
			
		||||
	e->modes = str_from_cstr (modes_utf8);
 | 
			
		||||
	free (modes_utf8);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_update (struct app_context *ctx, struct buffer *buffer)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_update *e = &m->data.buffer_update;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_UPDATE;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
	e->hide_unimportant = buffer->hide_unimportant;
 | 
			
		||||
 | 
			
		||||
	struct str *server_name = NULL;
 | 
			
		||||
	switch (buffer->type)
 | 
			
		||||
	{
 | 
			
		||||
	case BUFFER_GLOBAL:
 | 
			
		||||
		e->context.kind = RELAY_BUFFER_KIND_GLOBAL;
 | 
			
		||||
		break;
 | 
			
		||||
	case BUFFER_SERVER:
 | 
			
		||||
		e->context.kind = RELAY_BUFFER_KIND_SERVER;
 | 
			
		||||
		server_name = &e->context.server.server_name;
 | 
			
		||||
		break;
 | 
			
		||||
	case BUFFER_CHANNEL:
 | 
			
		||||
		e->context.kind = RELAY_BUFFER_KIND_CHANNEL;
 | 
			
		||||
		server_name = &e->context.channel.server_name;
 | 
			
		||||
		relay_prepare_channel_buffer_update (ctx, buffer, &e->context.channel);
 | 
			
		||||
		break;
 | 
			
		||||
	case BUFFER_PM:
 | 
			
		||||
		e->context.kind = RELAY_BUFFER_KIND_PRIVATE_MESSAGE;
 | 
			
		||||
		server_name = &e->context.private_message.server_name;
 | 
			
		||||
		break;
 | 
			
		||||
	}
 | 
			
		||||
	if (server_name)
 | 
			
		||||
		*server_name = str_from_cstr (buffer->server->name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_stats (struct app_context *ctx, struct buffer *buffer)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_stats *e = &m->data.buffer_stats;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_STATS;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
	e->new_messages = MIN (UINT32_MAX,
 | 
			
		||||
		buffer->new_messages_count - buffer->new_unimportant_count);
 | 
			
		||||
	e->new_unimportant_messages = MIN (UINT32_MAX,
 | 
			
		||||
		buffer->new_unimportant_count);
 | 
			
		||||
	e->highlighted = buffer->highlighted;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_rename (struct app_context *ctx, struct buffer *buffer,
 | 
			
		||||
	const char *new_name)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_rename *e = &m->data.buffer_rename;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_RENAME;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
	e->new = str_from_cstr (new_name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_remove (struct app_context *ctx, struct buffer *buffer)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_remove *e = &m->data.buffer_remove;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_REMOVE;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_activate (struct app_context *ctx, struct buffer *buffer)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_activate *e = &m->data.buffer_activate;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_ACTIVATE;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_input (struct app_context *ctx, struct buffer *buffer,
 | 
			
		||||
	const char *input)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_input *e = &m->data.buffer_input;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_INPUT;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
	e->text = str_from_cstr (input);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_buffer_clear (struct app_context *ctx,
 | 
			
		||||
	struct buffer *buffer)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_buffer_clear *e = &m->data.buffer_clear;
 | 
			
		||||
	e->event = RELAY_EVENT_BUFFER_CLEAR;
 | 
			
		||||
	e->buffer_name = str_from_cstr (buffer->name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum relay_server_state
 | 
			
		||||
relay_server_state_for_server (struct server *s)
 | 
			
		||||
{
 | 
			
		||||
	switch (s->state)
 | 
			
		||||
	{
 | 
			
		||||
	case IRC_DISCONNECTED: return RELAY_SERVER_STATE_DISCONNECTED;
 | 
			
		||||
	case IRC_CONNECTING:   return RELAY_SERVER_STATE_CONNECTING;
 | 
			
		||||
	case IRC_CONNECTED:    return RELAY_SERVER_STATE_CONNECTED;
 | 
			
		||||
	case IRC_REGISTERED:   return RELAY_SERVER_STATE_REGISTERED;
 | 
			
		||||
	case IRC_CLOSING:
 | 
			
		||||
	case IRC_HALF_CLOSED:  return RELAY_SERVER_STATE_DISCONNECTING;
 | 
			
		||||
	}
 | 
			
		||||
	return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_server_update (struct app_context *ctx, struct server *s)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_server_update *e = &m->data.server_update;
 | 
			
		||||
	e->event = RELAY_EVENT_SERVER_UPDATE;
 | 
			
		||||
	e->server_name = str_from_cstr (s->name);
 | 
			
		||||
	e->data.state = relay_server_state_for_server (s);
 | 
			
		||||
	if (s->state == IRC_REGISTERED)
 | 
			
		||||
	{
 | 
			
		||||
		char *user_utf8 = irc_to_utf8 (s->irc_user->nickname);
 | 
			
		||||
		e->data.registered.user = str_from_cstr (user_utf8);
 | 
			
		||||
		free (user_utf8);
 | 
			
		||||
 | 
			
		||||
		char *user_modes_utf8 = irc_to_utf8 (s->irc_user_modes.str);
 | 
			
		||||
		e->data.registered.user_modes = str_from_cstr (user_modes_utf8);
 | 
			
		||||
		free (user_modes_utf8);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_server_rename (struct app_context *ctx, struct server *s,
 | 
			
		||||
	const char *new_name)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_server_rename *e = &m->data.server_rename;
 | 
			
		||||
	e->event = RELAY_EVENT_SERVER_RENAME;
 | 
			
		||||
	e->server_name = str_from_cstr (s->name);
 | 
			
		||||
	e->new = str_from_cstr (new_name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_server_remove (struct app_context *ctx, struct server *s)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_server_remove *e = &m->data.server_remove;
 | 
			
		||||
	e->event = RELAY_EVENT_SERVER_REMOVE;
 | 
			
		||||
	e->server_name = str_from_cstr (s->name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
relay_prepare_error (struct app_context *ctx, uint32_t seq, const char *message)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_error *e = &m->data.error;
 | 
			
		||||
	e->event = RELAY_EVENT_ERROR;
 | 
			
		||||
	e->command_seq = seq;
 | 
			
		||||
	e->error = str_from_cstr (message);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static struct relay_event_data_response *
 | 
			
		||||
relay_prepare_response (struct app_context *ctx, uint32_t seq)
 | 
			
		||||
{
 | 
			
		||||
	struct relay_event_message *m = relay_prepare (ctx);
 | 
			
		||||
	struct relay_event_data_response *e = &m->data.response;
 | 
			
		||||
	e->event = RELAY_EVENT_RESPONSE;
 | 
			
		||||
	e->command_seq = seq;
 | 
			
		||||
	return e;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Buffers -----------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
static void
 | 
			
		||||
@@ -14603,21 +14604,23 @@ on_readline_input (char *line)
 | 
			
		||||
		if (*line)
 | 
			
		||||
			add_history (line);
 | 
			
		||||
 | 
			
		||||
		// readline always erases the input line after returning from here,
 | 
			
		||||
		// Readline always erases the input line after returning from here,
 | 
			
		||||
		// but we don't want that to happen if the command to be executed
 | 
			
		||||
		// would switch the buffer (we'd keep the already executed command in
 | 
			
		||||
		// the old buffer and delete any input restored from the new buffer)
 | 
			
		||||
		strv_append_owned (&ctx->pending_input, line);
 | 
			
		||||
		poller_idle_set (&ctx->input_event);
 | 
			
		||||
	}
 | 
			
		||||
	else
 | 
			
		||||
	else if (isatty (STDIN_FILENO))
 | 
			
		||||
	{
 | 
			
		||||
		// Prevent readline from showing the prompt twice for w/e reason
 | 
			
		||||
		// Prevent Readline from showing the prompt twice for w/e reason
 | 
			
		||||
		CALL (ctx->input, hide);
 | 
			
		||||
		input_rl__restore (self);
 | 
			
		||||
 | 
			
		||||
		CALL (ctx->input, ding);
 | 
			
		||||
	}
 | 
			
		||||
	else
 | 
			
		||||
		request_quit (ctx, NULL);
 | 
			
		||||
 | 
			
		||||
	if (self->active)
 | 
			
		||||
		// Readline automatically redisplays it
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
2.0.0
 | 
			
		||||
2.1.0
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,9 @@
 | 
			
		||||
// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
			
		||||
// SPDX-License-Identifier: 0BSD
 | 
			
		||||
//
 | 
			
		||||
// As an odd regression, AppKit may be necessary for JIT linking.
 | 
			
		||||
import AppKit
 | 
			
		||||
 | 
			
		||||
// NSGraphicsContext mostly just weirdly wraps over Quartz,
 | 
			
		||||
// so we do it all in Quartz directly.
 | 
			
		||||
import CoreGraphics
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										164
									
								
								xT/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								xT/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,164 @@
 | 
			
		||||
# As per Qt 6.8 documentation, at least 3.16 is necessary
 | 
			
		||||
cmake_minimum_required (VERSION 3.21.1)
 | 
			
		||||
 | 
			
		||||
file (READ ../xK-version project_version)
 | 
			
		||||
configure_file (../xK-version xK-version.tag COPYONLY)
 | 
			
		||||
string (STRIP "${project_version}" project_version)
 | 
			
		||||
 | 
			
		||||
# This is an entirely separate CMake project.
 | 
			
		||||
project (xT VERSION "${project_version}"
 | 
			
		||||
	DESCRIPTION "Qt frontend for xC" LANGUAGES CXX)
 | 
			
		||||
 | 
			
		||||
set (CMAKE_CXX_STANDARD 17)
 | 
			
		||||
set (CMAKE_CXX_STANDARD_REQUIRED ON)
 | 
			
		||||
 | 
			
		||||
find_package (Qt6 REQUIRED COMPONENTS Widgets Network Multimedia)
 | 
			
		||||
qt_standard_project_setup ()
 | 
			
		||||
 | 
			
		||||
add_compile_options ("$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
 | 
			
		||||
add_compile_options ("$<$<CXX_COMPILER_ID:GNU>:-Wall;-Wextra>")
 | 
			
		||||
add_compile_options ("$<$<CXX_COMPILER_ID:Clang>:-Wall;-Wextra>")
 | 
			
		||||
 | 
			
		||||
set (project_config "${PROJECT_BINARY_DIR}/config.h")
 | 
			
		||||
configure_file ("${PROJECT_SOURCE_DIR}/config.h.in" "${project_config}")
 | 
			
		||||
include_directories ("${PROJECT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}")
 | 
			
		||||
 | 
			
		||||
# Produce a beep sample
 | 
			
		||||
find_program (sox_EXECUTABLE sox REQUIRED)
 | 
			
		||||
set (beep "${PROJECT_BINARY_DIR}/beep.wav")
 | 
			
		||||
add_custom_command (OUTPUT "${beep}"
 | 
			
		||||
	COMMAND ${sox_EXECUTABLE} -b 16 -Dr 44100 -n "${beep}"
 | 
			
		||||
		synth 0.1 0 25 triangle 800 vol 0.5 fade t 0 -0 0.005 pad 0 0.05
 | 
			
		||||
	COMMENT "Generating a beep sample" VERBATIM)
 | 
			
		||||
set_property (SOURCE "${beep}" APPEND PROPERTY QT_RESOURCE_ALIAS beep.wav)
 | 
			
		||||
 | 
			
		||||
# Rasterize SVG icons
 | 
			
		||||
set (root "${PROJECT_SOURCE_DIR}/..")
 | 
			
		||||
set (CMAKE_MODULE_PATH "${root}/liberty/cmake")
 | 
			
		||||
include (IconUtils)
 | 
			
		||||
 | 
			
		||||
# It might generally be better to use QtSvg, though it is an extra dependency.
 | 
			
		||||
# The icon_to_png macro is not intended to be used like this.
 | 
			
		||||
foreach (icon xT xT-highlighted)
 | 
			
		||||
	icon_to_png (${icon} "${PROJECT_SOURCE_DIR}/${icon}.svg"
 | 
			
		||||
		48 "${PROJECT_BINARY_DIR}/resources" icon_png)
 | 
			
		||||
	set_property (SOURCE "${icon_png}"
 | 
			
		||||
		APPEND PROPERTY QT_RESOURCE_ALIAS "${icon}.png")
 | 
			
		||||
	list (APPEND icon_rsrc_list "${icon_png}")
 | 
			
		||||
endforeach ()
 | 
			
		||||
 | 
			
		||||
if (APPLE)
 | 
			
		||||
	set (MACOSX_BUNDLE_ICON_FILE xT.icns)
 | 
			
		||||
	icon_to_icns ("${PROJECT_SOURCE_DIR}/xT.svg"
 | 
			
		||||
		"${MACOSX_BUNDLE_ICON_FILE}" icon_icns)
 | 
			
		||||
else ()
 | 
			
		||||
	# The largest size is mainly for an appropriately sized Windows icon
 | 
			
		||||
	set (icon_base "${PROJECT_BINARY_DIR}/icons")
 | 
			
		||||
	set (icon_png_list)
 | 
			
		||||
	foreach (icon_size 16 32 48 256)
 | 
			
		||||
		icon_to_png (xT "${PROJECT_SOURCE_DIR}/xT.svg"
 | 
			
		||||
			${icon_size} "${icon_base}" icon_png)
 | 
			
		||||
		list (APPEND icon_png_list "${icon_png}")
 | 
			
		||||
	endforeach ()
 | 
			
		||||
	add_custom_target (icons ALL DEPENDS ${icon_png_list})
 | 
			
		||||
	if (WIN32)
 | 
			
		||||
		list (REMOVE_ITEM icon_png_list "${icon_png}")
 | 
			
		||||
		set (icon_ico "${PROJECT_BINARY_DIR}/xT.ico")
 | 
			
		||||
		icon_for_win32 ("${icon_ico}" "${icon_png_list}" "${icon_png}")
 | 
			
		||||
 | 
			
		||||
		set (resource_file "${PROJECT_BINARY_DIR}/xT.rc")
 | 
			
		||||
		list (APPEND project_sources "${resource_file}")
 | 
			
		||||
		add_custom_command (OUTPUT "${resource_file}"
 | 
			
		||||
			COMMAND ${CMAKE_COMMAND} -E echo "1 ICON \"xT.ico\""
 | 
			
		||||
				> ${resource_file} VERBATIM)
 | 
			
		||||
		set_property (SOURCE "${resource_file}"
 | 
			
		||||
			APPEND PROPERTY OBJECT_DEPENDS ${icon_ico})
 | 
			
		||||
	endif ()
 | 
			
		||||
endif ()
 | 
			
		||||
 | 
			
		||||
# Build the main executable and link it
 | 
			
		||||
find_program (awk_EXECUTABLE awk ${find_program_REQUIRE})
 | 
			
		||||
add_custom_command (OUTPUT xC-proto.cpp
 | 
			
		||||
	COMMAND ${CMAKE_COMMAND} -E env LC_ALL=C ${awk_EXECUTABLE}
 | 
			
		||||
		-f ${root}/liberty/tools/lxdrgen.awk
 | 
			
		||||
		-f ${root}/liberty/tools/lxdrgen-cpp.awk
 | 
			
		||||
		-v PrefixCamel=Relay
 | 
			
		||||
		${root}/xC.lxdr > xC-proto.cpp
 | 
			
		||||
	DEPENDS
 | 
			
		||||
		${root}/liberty/tools/lxdrgen.awk
 | 
			
		||||
		${root}/liberty/tools/lxdrgen-cpp.awk
 | 
			
		||||
		${root}/xC.lxdr
 | 
			
		||||
	COMMENT "Generating xC relay protocol code" VERBATIM)
 | 
			
		||||
add_custom_target (xC-proto DEPENDS ${PROJECT_BINARY_DIR}/xC-proto.cpp)
 | 
			
		||||
 | 
			
		||||
list (APPEND project_sources "${root}/liberty/tools/lxdrgen-cpp-qt.cpp")
 | 
			
		||||
qt_add_executable (xT
 | 
			
		||||
	xT.cpp ${project_config} ${project_sources} "${icon_icns}")
 | 
			
		||||
add_dependencies (xT xC-proto)
 | 
			
		||||
qt_add_resources (xT "rsrc" PREFIX / FILES "${beep}" ${icon_rsrc_list})
 | 
			
		||||
target_link_libraries (xT PRIVATE Qt6::Widgets Qt6::Network Qt6::Multimedia)
 | 
			
		||||
set_target_properties (xT PROPERTIES WIN32_EXECUTABLE ON MACOSX_BUNDLE ON
 | 
			
		||||
	MACOSX_BUNDLE_GUI_IDENTIFIER name.janouch.xT)
 | 
			
		||||
 | 
			
		||||
# https://stackoverflow.com/questions/79079161 and resolved in Qt Creator 16.
 | 
			
		||||
set (QT_QML_GENERATE_QMLLS_INI ON)
 | 
			
		||||
 | 
			
		||||
# The files to be installed
 | 
			
		||||
include (GNUInstallDirs)
 | 
			
		||||
 | 
			
		||||
if (ANDROID)
 | 
			
		||||
	install (TARGETS xT DESTINATION .)
 | 
			
		||||
elseif (APPLE OR WIN32)
 | 
			
		||||
	install (TARGETS xT
 | 
			
		||||
		BUNDLE DESTINATION .
 | 
			
		||||
		RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
 | 
			
		||||
 | 
			
		||||
	# XXX: QTBUG-127075, which can be circumvented by manually running
 | 
			
		||||
	# macdeployqt on xT.app before the install.
 | 
			
		||||
	qt_generate_deploy_app_script (TARGET xT OUTPUT_SCRIPT deploy_xT)
 | 
			
		||||
	install (SCRIPT "${deploy_xT}")
 | 
			
		||||
else ()
 | 
			
		||||
	install (TARGETS xT DESTINATION ${CMAKE_INSTALL_BINDIR})
 | 
			
		||||
	install (FILES ../LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
 | 
			
		||||
 | 
			
		||||
	install (FILES xT.svg
 | 
			
		||||
		DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps)
 | 
			
		||||
	install (DIRECTORY ${icon_base}
 | 
			
		||||
		DESTINATION ${CMAKE_INSTALL_DATADIR})
 | 
			
		||||
	install (FILES xT.desktop
 | 
			
		||||
		DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
 | 
			
		||||
endif ()
 | 
			
		||||
 | 
			
		||||
# Within MSYS2, windeployqt doesn't copy the compiler runtime,
 | 
			
		||||
# which is always linked dynamically by the Qt binaries.
 | 
			
		||||
# TODO(p): Consider whether or not to use MSYS2 to cross-compile, and how.
 | 
			
		||||
if (WIN32)
 | 
			
		||||
	install (CODE [=[
 | 
			
		||||
	set (bindir "${CMAKE_INSTALL_PREFIX}/bin")
 | 
			
		||||
	execute_process (COMMAND cygpath -m /
 | 
			
		||||
		OUTPUT_VARIABLE cygroot OUTPUT_STRIP_TRAILING_WHITESPACE)
 | 
			
		||||
	if (cygroot)
 | 
			
		||||
		execute_process (COMMAND ldd "${bindir}/xT.exe"
 | 
			
		||||
			OUTPUT_VARIABLE ldd_output OUTPUT_STRIP_TRAILING_WHITESPACE)
 | 
			
		||||
		string (REGEX MATCHALL " /mingw64/bin/[^ ]+ " libs "${ldd_output}")
 | 
			
		||||
		foreach (lib ${libs})
 | 
			
		||||
			string (STRIP "${lib}" lib)
 | 
			
		||||
			file (COPY "${cygroot}${lib}" DESTINATION "${bindir}")
 | 
			
		||||
		endforeach()
 | 
			
		||||
	endif ()
 | 
			
		||||
	]=])
 | 
			
		||||
endif ()
 | 
			
		||||
 | 
			
		||||
# CPack
 | 
			
		||||
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
 | 
			
		||||
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
 | 
			
		||||
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/../LICENSE")
 | 
			
		||||
set (CPACK_GENERATOR "TGZ;ZIP")
 | 
			
		||||
set (CPACK_PACKAGE_FILE_NAME
 | 
			
		||||
	"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
 | 
			
		||||
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME} ${PROJECT_VERSION}")
 | 
			
		||||
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
 | 
			
		||||
set (CPACK_SOURCE_IGNORE_FILES "/build;/CMakeLists.txt.user")
 | 
			
		||||
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
 | 
			
		||||
 | 
			
		||||
include (CPack)
 | 
			
		||||
							
								
								
									
										7
									
								
								xT/config.h.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								xT/config.h.in
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
#ifndef CONFIG_H
 | 
			
		||||
#define CONFIG_H
 | 
			
		||||
 | 
			
		||||
#define PROJECT_NAME        "${PROJECT_NAME}"
 | 
			
		||||
#define PROJECT_VERSION     "${project_version}"
 | 
			
		||||
 | 
			
		||||
#endif  // ! CONFIG_H
 | 
			
		||||
							
								
								
									
										29
									
								
								xT/xT-highlighted.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								xT/xT-highlighted.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
 | 
			
		||||
  <defs>
 | 
			
		||||
    <radialGradient id="green-x">
 | 
			
		||||
      <stop stop-color="hsl(66, 100%, 80%)" offset="0" />
 | 
			
		||||
      <stop stop-color="hsl(66, 100%, 50%)" offset="1" />
 | 
			
		||||
    </radialGradient>
 | 
			
		||||
    <radialGradient id="orange">
 | 
			
		||||
      <stop stop-color="hsl(36, 100%, 60%)" offset="0" />
 | 
			
		||||
      <stop stop-color="hsl(23, 100%, 60%)" offset="1" />
 | 
			
		||||
    </radialGradient>
 | 
			
		||||
    <filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
 | 
			
		||||
      <feDropShadow dx="0" dy="0" stdDeviation="0.05"
 | 
			
		||||
         flood-color="rgba(0, 0, 0, .5)" />
 | 
			
		||||
    </filter>
 | 
			
		||||
  </defs>
 | 
			
		||||
 | 
			
		||||
  <!-- XXX: librsvg screws up shadows on rotated objects. -->
 | 
			
		||||
  <g filter="url(#shadow)" transform="translate(24 3) scale(16)">
 | 
			
		||||
    <path fill="url(#orange)" stroke="hsl(36, 100%, 20%)" stroke-width="0.1"
 | 
			
		||||
       d="M-.8 0 H.8 V.5 H.25 V2.625 H-.25 V.5 H-.8 Z" />
 | 
			
		||||
  </g>
 | 
			
		||||
  <g filter="url(#shadow)" transform="translate(24 28) rotate(-45) scale(16)">
 | 
			
		||||
    <path fill="url(#green-x)" stroke="hsl(66, 100%, 20%)" stroke-width="0.1"
 | 
			
		||||
       d="M-.25 -1 H.25 V-.25 H1 V.25 H.25 V1 H-.25 V.25 H-1 V-.25 H-.25 Z" />
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.2 KiB  | 
							
								
								
									
										8
									
								
								xT/xT.desktop
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								xT/xT.desktop
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
[Desktop Entry]
 | 
			
		||||
Type=Application
 | 
			
		||||
Name=xT
 | 
			
		||||
GenericName=IRC Client
 | 
			
		||||
Icon=xT
 | 
			
		||||
Exec=xT
 | 
			
		||||
StartupNotify=false
 | 
			
		||||
Categories=Network;Chat;IRCClient;
 | 
			
		||||
							
								
								
									
										29
									
								
								xT/xT.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								xT/xT.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
 | 
			
		||||
  <defs>
 | 
			
		||||
    <radialGradient id="grey-x">
 | 
			
		||||
      <stop stop-color="hsl(66, 0%, 90%)" offset="0" />
 | 
			
		||||
      <stop stop-color="hsl(66, 0%, 80%)" offset="1" />
 | 
			
		||||
    </radialGradient>
 | 
			
		||||
    <radialGradient id="orange">
 | 
			
		||||
      <stop stop-color="hsl(36, 100%, 60%)" offset="0" />
 | 
			
		||||
      <stop stop-color="hsl(23, 100%, 60%)" offset="1" />
 | 
			
		||||
    </radialGradient>
 | 
			
		||||
    <filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
 | 
			
		||||
      <feDropShadow dx="0" dy="0" stdDeviation="0.05"
 | 
			
		||||
         flood-color="rgba(0, 0, 0, .5)" />
 | 
			
		||||
    </filter>
 | 
			
		||||
  </defs>
 | 
			
		||||
 | 
			
		||||
  <!-- XXX: librsvg screws up shadows on rotated objects. -->
 | 
			
		||||
  <g filter="url(#shadow)" transform="translate(24 28) rotate(-45) scale(16)">
 | 
			
		||||
    <path fill="url(#grey-x)" stroke="hsl(66, 0%, 30%)" stroke-width="0.1"
 | 
			
		||||
       d="M-.25 -1 H.25 V-.25 H1 V.25 H.25 V1 H-.25 V.25 H-1 V-.25 H-.25 Z" />
 | 
			
		||||
  </g>
 | 
			
		||||
  <g filter="url(#shadow)" transform="translate(24 3) scale(16)">
 | 
			
		||||
    <path fill="url(#orange)" stroke="hsl(36, 100%, 20%)" stroke-width="0.1"
 | 
			
		||||
       d="M-.8 0 H.8 V.5 H.25 V2.625 H-.25 V.5 H-.8 Z" />
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.2 KiB  | 
							
								
								
									
										181
									
								
								xW/xW.cpp
									
									
									
									
									
								
							
							
						
						
									
										181
									
								
								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) {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user