Compare commits
22 Commits
1635a730e8
...
v2.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
a8575ab875
|
|||
|
081525f5be
|
|||
|
6ac2ac5511
|
|||
|
f6483489c2
|
|||
|
ed5ac1815b
|
|||
|
509cb9f4dd
|
|||
|
b3684c4d9f
|
|||
|
918c589c65
|
|||
|
21095a11d6
|
|||
|
a22baa4b55
|
|||
|
b3e545e0bb
|
|||
|
cd76702ab2
|
|||
|
977b073b58
|
|||
|
46be4836df
|
|||
|
05a41b2629
|
|||
|
a62ed5bbac
|
|||
|
9c9776bacd
|
|||
|
086b879ab8
|
|||
|
214c349869
|
|||
|
3d975c9437
|
|||
|
fce8fd40cc
|
|||
|
3597ab9420
|
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"
|
||||
|
||||
|
||||
19
README.adoc
19
README.adoc
@@ -2,9 +2,9 @@ xK
|
||||
==
|
||||
|
||||
'xK' (chat kit) is an IRC software suite consisting of a daemon, bot, notifier,
|
||||
terminal client, and web/Windows/macOS frontends for the client. It's all
|
||||
you're ever going to need for chatting, so long as you can make do with slightly
|
||||
minimalist software.
|
||||
terminal client, and web/Windows/macOS/Linux/FreeBSD/Android/iOS frontends
|
||||
for the client. It's all you're ever going to need for chatting, so long as
|
||||
you can make do with slightly minimalist software.
|
||||
|
||||
They're all lean on dependencies, and offer a maximally permissive licence.
|
||||
|
||||
@@ -33,6 +33,11 @@ including link:xC.adoc#_key_bindings[keyboard shortcuts].
|
||||
|
||||
image::xP.webp[align="center"]
|
||||
|
||||
xA, xT, xW, xM
|
||||
--------------
|
||||
Fyne, Qt Widgets, Win32, Cocoa frontends for 'xC'.
|
||||
Using them is not recommended.
|
||||
|
||||
xD
|
||||
--
|
||||
The IRC daemon. It is designed for use as a regular user application rather
|
||||
@@ -146,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
@@ -4,8 +4,10 @@
|
||||
AWK = env LC_ALL=C awk
|
||||
|
||||
tools = ../liberty/tools
|
||||
outputs = FyneApp.toml xA proto.go xA.png xA-highlighted.png beep.raw
|
||||
generated = FyneApp.toml xA.png xA-highlighted.png beep.raw proto.go
|
||||
outputs = xA $(generated)
|
||||
all: $(outputs)
|
||||
generate: $(generated)
|
||||
|
||||
FyneApp.toml: ../xK-version
|
||||
printf "\
|
||||
@@ -27,7 +29,7 @@ beep.raw:
|
||||
proto.go: $(tools)/lxdrgen.awk $(tools)/lxdrgen-go.awk ../xC.lxdr
|
||||
$(AWK) -f $(tools)/lxdrgen.awk -f $(tools)/lxdrgen-go.awk \
|
||||
-v PrefixCamel=Relay ../xC.lxdr > $@
|
||||
xA: xA.go proto.go ../xK-version xA.png xA-highlighted.png beep.raw
|
||||
xA: xA.go ../xK-version $(generated)
|
||||
go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -o $@ \
|
||||
-gcflags=all="-N -l"
|
||||
clean:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module janouch.name/xK/xA
|
||||
|
||||
go 1.23
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.5.2
|
||||
|
||||
162
xA/xA.go
162
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,50 +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)
|
||||
}
|
||||
|
||||
// --- 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
|
||||
@@ -421,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) {
|
||||
@@ -504,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{} }
|
||||
@@ -745,6 +771,7 @@ func refreshBuffer(b *buffer) {
|
||||
bufferPrintAndWatchTrailingDateChanges()
|
||||
wRichText.Refresh()
|
||||
bufferScrollToBottom()
|
||||
recheckHighlighted()
|
||||
}
|
||||
|
||||
// --- Event processing --------------------------------------------------------
|
||||
@@ -755,7 +782,7 @@ func relayProcessBufferLine(b *buffer, m *RelayEventDataBufferLine) {
|
||||
// Initial sync: skip all other processing, let highlights be.
|
||||
bc := bufferByName(bufferCurrent)
|
||||
if bc == nil {
|
||||
b.lines = append(b.lines, line)
|
||||
bufferPushLine(b, line)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -767,7 +794,7 @@ func relayProcessBufferLine(b *buffer, m *RelayEventDataBufferLine) {
|
||||
separate := display &&
|
||||
!visible && bc.newMessages == 0 && bc.newUnimportantMessages == 0
|
||||
|
||||
b.lines = append(b.lines, line)
|
||||
bufferPushLine(b, line)
|
||||
if !(visible || m.LeakToActive) ||
|
||||
b.newMessages != 0 || b.newUnimportantMessages != 0 {
|
||||
if line.isUnimportant || m.LeakToActive {
|
||||
@@ -780,7 +807,7 @@ func relayProcessBufferLine(b *buffer, m *RelayEventDataBufferLine) {
|
||||
if m.LeakToActive {
|
||||
leakedLine := line
|
||||
leakedLine.leaked = true
|
||||
bc.lines = append(bc.lines, leakedLine)
|
||||
bufferPushLine(bc, leakedLine)
|
||||
|
||||
if !visible || bc.newMessages != 0 || bc.newUnimportantMessages != 0 {
|
||||
if line.isUnimportant {
|
||||
@@ -910,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
|
||||
}
|
||||
@@ -1311,7 +1338,8 @@ func (e *inputEntry) SetText(text string) {
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
type logEntry struct {
|
||||
// XXX: Sadly, we can't seem to make it read-only in any way.
|
||||
// XXX: Sadly, we can't seem to make it actually read-only.
|
||||
// https://github.com/fyne-io/fyne/issues/5263
|
||||
widget.Entry
|
||||
}
|
||||
|
||||
@@ -1323,6 +1351,12 @@ func newLogEntry() *logEntry {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *logEntry) SetText(text string) {
|
||||
e.OnChanged = nil
|
||||
e.Entry.SetText(text)
|
||||
e.OnChanged = func(string) { e.Entry.SetText(text) }
|
||||
}
|
||||
|
||||
func (e *logEntry) AcceptsTab() bool {
|
||||
return false
|
||||
}
|
||||
@@ -1356,6 +1390,9 @@ func (l *customLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
||||
}
|
||||
if toBottom {
|
||||
bufferScrollToBottom()
|
||||
} else {
|
||||
recheckHighlighted()
|
||||
refreshStatus()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1472,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
|
||||
@@ -1522,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,10 +1,7 @@
|
||||
module janouch.name/xK/xP
|
||||
|
||||
go 1.18
|
||||
go 1.21
|
||||
|
||||
require nhooyr.io/websocket v1.8.7
|
||||
toolchain go1.23.2
|
||||
|
||||
require (
|
||||
github.com/klauspost/compress v1.15.9 // indirect
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
|
||||
)
|
||||
require nhooyr.io/websocket v1.8.17
|
||||
|
||||
64
xP/go.sum
64
xP/go.sum
@@ -1,62 +1,2 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
|
||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
|
||||
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||
|
||||
@@ -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