xC/xP: mark highlights and buffer activity

And more or less finalize out the protocol for this use case.
This commit is contained in:
Přemysl Eric Janouch 2022-09-07 19:42:18 +02:00
parent 45aa0e8dfb
commit 4ba28c6ed3
Signed by: p
GPG Key ID: A0420B94F92B9493
5 changed files with 114 additions and 19 deletions

View File

@ -26,7 +26,7 @@ As a unique bonus, you can launch a full text editor from within.
xP xP
-- --
The web frontend for 'xC', making use of its networked relay interface. The web frontend for 'xC', making use of its networked relay interface.
So far it's quite basic, yet usable. So far it's somewhat basic, yet usable.
xF xF
-- --

View File

@ -3,6 +3,8 @@ const VERSION = 1;
// From the frontend to the relay. // From the frontend to the relay.
struct CommandMessage { struct CommandMessage {
// The command sequence number will be repeated in responses
// in the respective fields.
u32 command_seq; u32 command_seq;
union CommandData switch (enum Command { union CommandData switch (enum Command {
HELLO, HELLO,
@ -15,6 +17,9 @@ struct CommandMessage {
} command) { } command) {
case HELLO: case HELLO:
u32 version; u32 version;
// If the version check succeeds, the client will receive
// an initial stream of BUFFER_UPDATE, BUFFER_LINE,
// and finally a BUFFER_ACTIVATE message.
case PING: case PING:
void; void;
case ACTIVE: case ACTIVE:
@ -51,6 +56,15 @@ struct EventMessage {
void; void;
case BUFFER_UPDATE: case BUFFER_UPDATE:
string buffer_name; string buffer_name;
// These are cumulative, even for lines flushed out from buffers.
// Updates to these values aren't broadcasted, thus handle:
// - BUFFER_LINE by bumping/setting them as appropriate,
// - BUFFER_ACTIVATE by clearing them for the previous buffer
// (this way, they can be used to mark unread messages).
// Any updates received after the initial sync should be ignored.
u32 new_messages;
u32 new_unimportant_messages;
bool highlighted;
case BUFFER_RENAME: case BUFFER_RENAME:
string buffer_name; string buffer_name;
string new; string new;
@ -60,6 +74,7 @@ struct EventMessage {
string buffer_name; string buffer_name;
case BUFFER_LINE: case BUFFER_LINE:
string buffer_name; string buffer_name;
// Whether the line should also be displayed in the active buffer.
bool leak_to_active; bool leak_to_active;
bool is_unimportant; bool is_unimportant;
bool is_highlight; bool is_highlight;
@ -106,7 +121,8 @@ struct EventMessage {
case BUFFER_CLEAR: case BUFFER_CLEAR:
string buffer_name; string buffer_name;
// Restriction: command_seq is strictly increasing, across both of these. // Restriction: command_seq strictly follows the sequence received
// by the relay, across both of these replies.
case ERROR: case ERROR:
u32 command_seq; u32 command_seq;
string error; string error;

13
xC.c
View File

@ -3068,6 +3068,11 @@ relay_prepare_buffer_update (struct app_context *ctx, struct buffer *buffer)
struct relay_event_data_buffer_update *e = &m->data.buffer_update; struct relay_event_data_buffer_update *e = &m->data.buffer_update;
e->event = RELAY_EVENT_BUFFER_UPDATE; e->event = RELAY_EVENT_BUFFER_UPDATE;
e->buffer_name = str_from_cstr (buffer->name); 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 static void
@ -5015,8 +5020,16 @@ buffer_merge (struct app_context *ctx,
buffer->lines_count += n; buffer->lines_count += n;
// And since there is no log_*() call, send them to relays manually // And since there is no log_*() call, send them to relays manually
buffer->highlighted |= merged->highlighted;
LIST_FOR_EACH (struct buffer_line, line, start) LIST_FOR_EACH (struct buffer_line, line, start)
{ {
if (buffer->new_messages_count)
{
buffer->new_messages_count++;
if (line->flags & BUFFER_LINE_UNIMPORTANT)
buffer->new_unimportant_count++;
}
relay_prepare_buffer_line (ctx, buffer, line, false); relay_prepare_buffer_line (ctx, buffer, line, false);
relay_broadcast (ctx); relay_broadcast (ctx);
} }

View File

@ -58,7 +58,13 @@ body {
padding: .05rem .3rem; padding: .05rem .3rem;
cursor: default; cursor: default;
} }
.item.active { .item.highlighted {
color: #ff5f00;
}
.item.activity {
font-weight: bold;
}
.item.current {
font-style: italic; font-style: italic;
background: #f8f8f8; background: #f8f8f8;
border-top: 1px solid #eee; border-top: 1px solid #eee;
@ -97,6 +103,11 @@ body {
grid-column: span 2; grid-column: span 2;
font-weight: bold; font-weight: bold;
} }
.unread {
height: 1px;
grid-column: span 2;
background: #ff5f00;
}
.time { .time {
padding: .1rem .3rem; padding: .1rem .3rem;
background: #f8f8f8; background: #f8f8f8;

View File

@ -156,10 +156,13 @@ rpc.addEventListener('close', event => {
rpc.addEventListener('BufferUpdate', event => { rpc.addEventListener('BufferUpdate', event => {
let e = event.detail, b = buffers.get(e.bufferName) let e = event.detail, b = buffers.get(e.bufferName)
if (b === undefined) { if (b === undefined) {
b = {lines: []} buffers.set(e.bufferName, {
buffers.set(e.bufferName, b) lines: [],
newMessages: e.newMessages,
newUnimportantMessages: e.newUnimportantMessages,
highlighted: e.highlighted,
})
} }
// TODO: Update any buffer properties.
}) })
rpc.addEventListener('BufferRename', event => { rpc.addEventListener('BufferRename', event => {
@ -174,8 +177,14 @@ rpc.addEventListener('BufferRemove', event => {
}) })
rpc.addEventListener('BufferActivate', event => { rpc.addEventListener('BufferActivate', event => {
let e = event.detail, b = buffers.get(e.bufferName)
let old = buffers.get(bufferCurrent) let old = buffers.get(bufferCurrent)
if (old !== undefined) {
old.newMessages = 0
old.newUnimportantMessages = 0
old.highlighted = false
}
let e = event.detail, b = buffers.get(e.bufferName)
bufferCurrent = e.bufferName bufferCurrent = e.bufferName
bufferLog = undefined bufferLog = undefined
bufferAutoscroll = true bufferAutoscroll = true
@ -195,12 +204,41 @@ rpc.addEventListener('BufferActivate', event => {
}) })
rpc.addEventListener('BufferLine', event => { rpc.addEventListener('BufferLine', event => {
let e = event.detail, b = buffers.get(e.bufferName), let e = event.detail, b = buffers.get(e.bufferName), line = {...e}
line = {when: e.when, rendition: e.rendition, items: e.items} delete line.event
if (b !== undefined) delete line.leakToActive
if (b === undefined)
return
// Initial sync: skip all other processing, let highlights be.
if (bufferCurrent === undefined) {
b.lines.push(line)
return
}
let visible = e.bufferName == bufferCurrent || e.leakToActive
b.lines.push({...line}) b.lines.push({...line})
if (e.leakToActive && (b = buffers.get(bufferCurrent)) !== undefined) if (!visible || b.newMessages || b.newUnimportantMessages) {
b.lines.push({leaked: true, ...line}) if (line.isUnimportant)
b.newUnimportantMessages++
else
b.newMessages++
}
if (e.leakToActive) {
let bc = buffers.get(bufferCurrent)
bc.lines.push({...line, leaked: true})
if (bc.newMessages || bc.newUnimportantMessages) {
if (line.isUnimportant)
bc.newUnimportantMessages++
else
bc.newMessages++
}
}
// TODO: Find some way of highlighting the tab in a browser.
// TODO: Also highlight on unseen private messages, like xC does.
if (!visible && line.isHighlight)
b.highlighted = true
}) })
rpc.addEventListener('BufferClear', event => { rpc.addEventListener('BufferClear', event => {
@ -267,10 +305,21 @@ let BufferList = {
view: vnode => { view: vnode => {
let items = Array.from(buffers, ([name, b]) => { let items = Array.from(buffers, ([name, b]) => {
let attrs = {onclick: event => BufferList.activate(name)} let classes = [], displayName = name
if (name == bufferCurrent) if (name == bufferCurrent) {
attrs.class = 'active' classes.push('current')
return m('.item', attrs, name) } else {
if (b.highlighted)
classes.push('highlighted')
if (b.newMessages) {
classes.push('activity')
displayName += ` (${b.newMessages})`
}
}
return m('.item', {
onclick: event => BufferList.activate(name),
class: classes.join(' '),
}, displayName)
}) })
return m('.list', {}, items) return m('.list', {}, items)
}, },
@ -387,7 +436,9 @@ let Buffer = {
return return
let lastDateMark = undefined let lastDateMark = undefined
b.lines.forEach(line => { let markBefore = b.lines.length
- b.newMessages - b.newUnimportantMessages
b.lines.forEach((line, i) => {
let date = new Date(line.when) let date = new Date(line.when)
let dateMark = date.toLocaleDateString() let dateMark = date.toLocaleDateString()
if (dateMark !== lastDateMark) { if (dateMark !== lastDateMark) {
@ -395,16 +446,20 @@ let Buffer = {
lastDateMark = dateMark lastDateMark = dateMark
} }
if (i == markBefore)
lines.push(m('.unread'))
let attrs = {} let attrs = {}
if (line.leaked) if (line.leaked)
attrs.class = 'leaked' attrs.class = 'leaked'
// TODO: Make use of isUnimportant.
lines.push(m('.time', {...attrs}, date.toLocaleTimeString())) lines.push(m('.time', {...attrs}, date.toLocaleTimeString()))
lines.push(m(Content, {...attrs}, line)) lines.push(m(Content, {...attrs}, line))
}) })
let dateMark = new Date().toLocaleDateString() let dateMark = new Date().toLocaleDateString()
if (dateMark !== lastDateMark) if (dateMark !== lastDateMark && lastDateMark !== undefined)
lines.push(m('.date', {}, dateMark)) lines.push(m('.date', {}, dateMark))
return m('.buffer', {}, lines) return m('.buffer', {}, lines)
}, },