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 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"
|
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"]
|
image::xP.webp[align="center"]
|
||||||
|
|
||||||
xA, xW, xM
|
xA, xT, xW, xM
|
||||||
----------
|
--------------
|
||||||
The native frontends for 'xC'. Using them is not recommended.
|
Fyne, Qt Widgets, Win32, Cocoa frontends for 'xC'.
|
||||||
|
Using them is not recommended.
|
||||||
|
|
||||||
xD
|
xD
|
||||||
--
|
--
|
||||||
@@ -150,7 +151,13 @@ xA
|
|||||||
~~
|
~~
|
||||||
The Fyne frontend supports all of Linux, FreeBSD, Windows, macOS, Android, and
|
The Fyne frontend supports all of Linux, FreeBSD, Windows, macOS, Android, and
|
||||||
iOS natively, albeit somewhat poorly. Only use `fyne` or `fyne-cross` after
|
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
|
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() {
|
go func() {
|
||||||
<-otoReady
|
<-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
|
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 {
|
func bufferAtBottom() bool {
|
||||||
return wRichScroll.Offset.Y >=
|
return wRichScroll.Offset.Y >=
|
||||||
wRichScroll.Content.Size().Height-wRichScroll.Size().Height
|
wRichScroll.Content.Size().Height-wRichScroll.Size().Height
|
||||||
@@ -432,22 +381,31 @@ func bufferScrollToBottom() {
|
|||||||
refreshStatus()
|
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 --------------------------------------------------------
|
// --- UI state refresh --------------------------------------------------------
|
||||||
|
|
||||||
func refreshIcon() {
|
func refreshIcon() {
|
||||||
highlighted := false
|
resource := resourceIconNormal
|
||||||
for _, b := range buffers {
|
for _, b := range buffers {
|
||||||
if b.highlighted {
|
if b.highlighted {
|
||||||
highlighted = true
|
resource = resourceIconHighlighted
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if highlighted {
|
// Prevent deadlocks (though it might have a race condition).
|
||||||
wWindow.SetIcon(resourceIconHighlighted)
|
// https://github.com/fyne-io/fyne/issues/5266
|
||||||
} else {
|
go func() { wWindow.SetIcon(resource) }()
|
||||||
wWindow.SetIcon(resourceIconNormal)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshTopic(topic []bufferLineItem) {
|
func refreshTopic(topic []bufferLineItem) {
|
||||||
@@ -515,6 +473,63 @@ func refreshStatus() {
|
|||||||
wStatus.SetText(status)
|
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 -----------------------------------------------------
|
// --- RichText formatting -----------------------------------------------------
|
||||||
|
|
||||||
func defaultBufferLineItem() bufferLineItem { return bufferLineItem{} }
|
func defaultBufferLineItem() bufferLineItem { return bufferLineItem{} }
|
||||||
@@ -756,6 +771,7 @@ func refreshBuffer(b *buffer) {
|
|||||||
bufferPrintAndWatchTrailingDateChanges()
|
bufferPrintAndWatchTrailingDateChanges()
|
||||||
wRichText.Refresh()
|
wRichText.Refresh()
|
||||||
bufferScrollToBottom()
|
bufferScrollToBottom()
|
||||||
|
recheckHighlighted()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Event processing --------------------------------------------------------
|
// --- Event processing --------------------------------------------------------
|
||||||
@@ -921,11 +937,11 @@ func relayProcessMessage(m *RelayEventMessage) {
|
|||||||
|
|
||||||
b.bufferName = data.New
|
b.bufferName = data.New
|
||||||
|
|
||||||
refreshBufferList()
|
|
||||||
if data.BufferName == bufferCurrent {
|
if data.BufferName == bufferCurrent {
|
||||||
bufferCurrent = data.New
|
bufferCurrent = data.New
|
||||||
refreshStatus()
|
refreshStatus()
|
||||||
}
|
}
|
||||||
|
refreshBufferList()
|
||||||
if data.BufferName == bufferLast {
|
if data.BufferName == bufferLast {
|
||||||
bufferLast = data.New
|
bufferLast = data.New
|
||||||
}
|
}
|
||||||
@@ -1374,6 +1390,9 @@ func (l *customLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
|||||||
}
|
}
|
||||||
if toBottom {
|
if toBottom {
|
||||||
bufferScrollToBottom()
|
bufferScrollToBottom()
|
||||||
|
} else {
|
||||||
|
recheckHighlighted()
|
||||||
|
refreshStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1490,16 +1509,14 @@ func main() {
|
|||||||
|
|
||||||
a := app.New()
|
a := app.New()
|
||||||
a.Settings().SetTheme(&customTheme{})
|
a.Settings().SetTheme(&customTheme{})
|
||||||
|
a.SetIcon(resourceIconNormal)
|
||||||
wWindow = a.NewWindow(projectName)
|
wWindow = a.NewWindow(projectName)
|
||||||
wWindow.Resize(fyne.NewSize(640, 480))
|
wWindow.Resize(fyne.NewSize(640, 480))
|
||||||
|
|
||||||
a.Lifecycle().SetOnEnteredForeground(func() {
|
a.Lifecycle().SetOnEnteredForeground(func() {
|
||||||
// TODO(p): Does this need locking?
|
// TODO(p): Does this need locking?
|
||||||
inForeground = true
|
inForeground = true
|
||||||
if b := bufferByName(bufferCurrent); b != nil {
|
recheckHighlighted()
|
||||||
b.highlighted = false
|
|
||||||
refreshIcon()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
a.Lifecycle().SetOnExitedForeground(func() {
|
a.Lifecycle().SetOnExitedForeground(func() {
|
||||||
inForeground = false
|
inForeground = false
|
||||||
@@ -1540,7 +1557,10 @@ func main() {
|
|||||||
wRichText = widget.NewRichText()
|
wRichText = widget.NewRichText()
|
||||||
wRichText.Wrapping = fyne.TextWrapWord
|
wRichText.Wrapping = fyne.TextWrapWord
|
||||||
wRichScroll = container.NewVScroll(wRichText)
|
wRichScroll = container.NewVScroll(wRichText)
|
||||||
wRichScroll.OnScrolled = func(position fyne.Position) { refreshStatus() }
|
wRichScroll.OnScrolled = func(position fyne.Position) {
|
||||||
|
recheckHighlighted()
|
||||||
|
refreshStatus()
|
||||||
|
}
|
||||||
wLog = newLogEntry()
|
wLog = newLogEntry()
|
||||||
wLog.Wrapping = fyne.TextWrapWord
|
wLog.Wrapping = fyne.TextWrapWord
|
||||||
wLog.Hide()
|
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
|
// autofilter, and we don't generally want alphabetic ordering at all
|
||||||
rl_sort_completion_matches = false;
|
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);
|
hard_assert (self->prompt != NULL);
|
||||||
// The inputrc is read before any callbacks are called, so we need to
|
// 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
|
// 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);
|
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 ---------------------------------------------------------
|
// --- Terminal output ---------------------------------------------------------
|
||||||
|
|
||||||
/// Default colour pair
|
/// Default colour pair
|
||||||
@@ -4515,6 +4135,387 @@ formatter_flush (struct formatter *self, FILE *stream, int flush_opts)
|
|||||||
attr_printer_reset (&state);
|
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 -----------------------------------------------------------------
|
// --- Buffers -----------------------------------------------------------------
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -14603,21 +14604,23 @@ on_readline_input (char *line)
|
|||||||
if (*line)
|
if (*line)
|
||||||
add_history (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
|
// 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
|
// would switch the buffer (we'd keep the already executed command in
|
||||||
// the old buffer and delete any input restored from the new buffer)
|
// the old buffer and delete any input restored from the new buffer)
|
||||||
strv_append_owned (&ctx->pending_input, line);
|
strv_append_owned (&ctx->pending_input, line);
|
||||||
poller_idle_set (&ctx->input_event);
|
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);
|
CALL (ctx->input, hide);
|
||||||
input_rl__restore (self);
|
input_rl__restore (self);
|
||||||
|
|
||||||
CALL (ctx->input, ding);
|
CALL (ctx->input, ding);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
request_quit (ctx, NULL);
|
||||||
|
|
||||||
if (self->active)
|
if (self->active)
|
||||||
// Readline automatically redisplays it
|
// 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>
|
// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
|
||||||
// SPDX-License-Identifier: 0BSD
|
// SPDX-License-Identifier: 0BSD
|
||||||
//
|
//
|
||||||
|
// As an odd regression, AppKit may be necessary for JIT linking.
|
||||||
|
import AppKit
|
||||||
|
|
||||||
// NSGraphicsContext mostly just weirdly wraps over Quartz,
|
// NSGraphicsContext mostly just weirdly wraps over Quartz,
|
||||||
// so we do it all in Quartz directly.
|
// so we do it all in Quartz directly.
|
||||||
import CoreGraphics
|
import CoreGraphics
|
||||||
|
|||||||
@@ -842,11 +842,11 @@ relayRPC.onEvent = { message in
|
|||||||
|
|
||||||
b.bufferName = data.new
|
b.bufferName = data.new
|
||||||
|
|
||||||
refreshBufferList()
|
|
||||||
if b.bufferName == relayBufferCurrent {
|
if b.bufferName == relayBufferCurrent {
|
||||||
relayBufferCurrent = data.new
|
relayBufferCurrent = data.new
|
||||||
refreshStatus()
|
refreshStatus()
|
||||||
}
|
}
|
||||||
|
refreshBufferList()
|
||||||
if b.bufferName == relayBufferLast {
|
if b.bufferName == relayBufferLast {
|
||||||
relayBufferLast = data.new
|
relayBufferLast = data.new
|
||||||
}
|
}
|
||||||
@@ -1203,6 +1203,7 @@ class WindowDelegate: NSObject, NSWindowDelegate {
|
|||||||
|
|
||||||
b.highlighted = false
|
b.highlighted = false
|
||||||
refreshIcon()
|
refreshIcon()
|
||||||
|
refreshBufferList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer indexes rotated to start after the current buffer.
|
// 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
|
// SPDX-License-Identifier: 0BSD
|
||||||
import * as Relay from './proto.js'
|
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
|
* 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
|
* Permission to use, copy, modify, and/or distribute this software for any
|
||||||
* purpose with or without fee is hereby granted.
|
* purpose with or without fee is hereby granted.
|
||||||
@@ -255,73 +255,6 @@ buffer_by_name(const std::wstring &name)
|
|||||||
return nullptr;
|
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
|
static bool
|
||||||
buffer_at_bottom()
|
buffer_at_bottom()
|
||||||
{
|
{
|
||||||
@@ -354,6 +287,7 @@ refresh_icon()
|
|||||||
if (b.highlighted)
|
if (b.highlighted)
|
||||||
icon = g.hiconHighlighted;
|
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_SMALL, (LPARAM) icon);
|
||||||
SendMessage(g.hwndMain, WM_SETICON, ICON_BIG, (LPARAM) icon);
|
SendMessage(g.hwndMain, WM_SETICON, ICON_BIG, (LPARAM) icon);
|
||||||
}
|
}
|
||||||
@@ -430,6 +364,88 @@ refresh_status()
|
|||||||
SetWindowText(g.hwndStatus, status.c_str());
|
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 ----------------------------------------------------
|
// --- Rich Edit formatting ----------------------------------------------------
|
||||||
|
|
||||||
static COLORREF
|
static COLORREF
|
||||||
@@ -695,7 +711,7 @@ buffer_print_line(std::vector<BufferLine>::const_iterator begin,
|
|||||||
static void
|
static void
|
||||||
buffer_print_separator()
|
buffer_print_separator()
|
||||||
{
|
{
|
||||||
bool sameline = !GetWindowTextLength(g.hwndBuffer);
|
bool sameline = !buffer_reset_selection();
|
||||||
|
|
||||||
CHARFORMAT2 format = default_charformat();
|
CHARFORMAT2 format = default_charformat();
|
||||||
format.dwEffects &= ~CFE_AUTOCOLOR;
|
format.dwEffects &= ~CFE_AUTOCOLOR;
|
||||||
@@ -728,6 +744,7 @@ refresh_buffer(const Buffer &b)
|
|||||||
|
|
||||||
buffer_print_and_watch_trailing_date_changes();
|
buffer_print_and_watch_trailing_date_changes();
|
||||||
buffer_scroll_to_bottom();
|
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);
|
SendMessage(g.hwndBuffer, WM_SETREDRAW, (WPARAM) TRUE, 0);
|
||||||
InvalidateRect(g.hwndBuffer, NULL, TRUE);
|
InvalidateRect(g.hwndBuffer, NULL, TRUE);
|
||||||
@@ -749,8 +766,9 @@ relay_process_buffer_line(Buffer &b, Relay::EventData_BufferLine &m)
|
|||||||
// Retained mode is complicated.
|
// Retained mode is complicated.
|
||||||
bool display = (!m.is_unimportant || !bc->hide_unimportant) &&
|
bool display = (!m.is_unimportant || !bc->hide_unimportant) &&
|
||||||
(b.buffer_name == g.buffer_current || m.leak_to_active);
|
(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 &&
|
bool to_bottom = display &&
|
||||||
buffer_at_bottom();
|
(buffer_at_bottom() || GetFocus() == g.hwndBuffer);
|
||||||
bool visible = display &&
|
bool visible = display &&
|
||||||
to_bottom &&
|
to_bottom &&
|
||||||
!IsIconic(g.hwndMain) &&
|
!IsIconic(g.hwndMain) &&
|
||||||
@@ -914,11 +932,11 @@ relay_process_message(const Relay::EventMessage &m)
|
|||||||
|
|
||||||
b->buffer_name = data.new_;
|
b->buffer_name = data.new_;
|
||||||
|
|
||||||
refresh_buffer_list();
|
|
||||||
if (data.buffer_name == g.buffer_current) {
|
if (data.buffer_name == g.buffer_current) {
|
||||||
g.buffer_current = data.new_;
|
g.buffer_current = data.new_;
|
||||||
refresh_status();
|
refresh_status();
|
||||||
}
|
}
|
||||||
|
refresh_buffer_list();
|
||||||
if (data.buffer_name == g.buffer_last)
|
if (data.buffer_name == g.buffer_last)
|
||||||
g.buffer_last = data.new_;
|
g.buffer_last = data.new_;
|
||||||
break;
|
break;
|
||||||
@@ -1465,6 +1483,7 @@ richedit_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
|
|||||||
{
|
{
|
||||||
// Dragging the scrollbar doesn't result in EN_VSCROLL.
|
// Dragging the scrollbar doesn't result in EN_VSCROLL.
|
||||||
LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam);
|
LRESULT lResult = DefSubclassProc(hWnd, uMsg, wParam, lParam);
|
||||||
|
recheck_highlighted();
|
||||||
refresh_status();
|
refresh_status();
|
||||||
return lResult;
|
return lResult;
|
||||||
}
|
}
|
||||||
@@ -1522,8 +1541,12 @@ process_resize(UINT w, UINT h)
|
|||||||
MoveWindow(g.hwndBufferList, 3, top, 150, h - top - bottom, FALSE);
|
MoveWindow(g.hwndBufferList, 3, top, 150, h - top - bottom, FALSE);
|
||||||
MoveWindow(g.hwndBuffer, 156, top, w - 159, 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);
|
MoveWindow(g.hwndBufferLog, 156, top, w - 159, h - top - bottom, FALSE);
|
||||||
if (to_bottom)
|
if (to_bottom) {
|
||||||
buffer_scroll_to_bottom();
|
buffer_scroll_to_bottom();
|
||||||
|
} else {
|
||||||
|
recheck_highlighted();
|
||||||
|
refresh_status();
|
||||||
|
}
|
||||||
|
|
||||||
InvalidateRect(g.hwndMain, NULL, TRUE);
|
InvalidateRect(g.hwndMain, NULL, TRUE);
|
||||||
}
|
}
|
||||||
@@ -1685,8 +1708,10 @@ window_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
case WM_SYSCOMMAND:
|
case WM_SYSCOMMAND:
|
||||||
{
|
{
|
||||||
|
// We're not deiconified yet, so duplicate recheck_highlighted().
|
||||||
auto b = buffer_by_name(g.buffer_current);
|
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;
|
b->highlighted = false;
|
||||||
refresh_icon();
|
refresh_icon();
|
||||||
}
|
}
|
||||||
@@ -1694,13 +1719,15 @@ window_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WM_COMMAND:
|
case WM_COMMAND:
|
||||||
if (!lParam)
|
if (!lParam) {
|
||||||
process_accelerator(LOWORD(wParam));
|
process_accelerator(LOWORD(wParam));
|
||||||
else if (lParam == (LPARAM) g.hwndBufferList)
|
} else if (lParam == (LPARAM) g.hwndBufferList) {
|
||||||
process_bufferlist_notification(HIWORD(wParam));
|
process_bufferlist_notification(HIWORD(wParam));
|
||||||
else if (lParam == (LPARAM) g.hwndBuffer &&
|
} else if (lParam == (LPARAM) g.hwndBuffer &&
|
||||||
HIWORD(wParam) == EN_VSCROLL)
|
HIWORD(wParam) == EN_VSCROLL) {
|
||||||
|
recheck_highlighted();
|
||||||
refresh_status();
|
refresh_status();
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
case WM_NOTIFY:
|
case WM_NOTIFY:
|
||||||
switch (((LPNMHDR) lParam)->code) {
|
switch (((LPNMHDR) lParam)->code) {
|
||||||
|
|||||||
Reference in New Issue
Block a user