Compare commits

..

No commits in common. "0bc2c12eecfb5b035c498272556f8fc6a39059a9" and "8c8e06b0157c97ee1771f848ed363c6a0ed398be" have entirely different histories.

7 changed files with 96 additions and 192 deletions

View File

@ -110,11 +110,11 @@ function codegen_enum(name, cg, ctype) {
# XXX: This should also check if it isn't out-of-range for any reason,
# but our usage of sprintf() stands in the way a bit.
CodegenSerialize[name] = "\tstr_pack_i8(w, %s);\n"
CodegenSerialize[name] = "\tstr_pack_i32(w, %s);\n"
CodegenDeserialize[name] = \
"\t{\n" \
"\t\tint8_t v = 0;\n" \
"\t\tif (!msg_unpacker_i8(r, &v) || !v)\n" \
"\t\tint32_t v = 0;\n" \
"\t\tif (!msg_unpacker_i32(r, &v) || !v)\n" \
"\t\t\treturn false;\n" \
"\t\t%s = v;\n" \
"\t}\n"

View File

@ -161,7 +161,7 @@ function codegen_begin() {
print "\tvar n int64"
print "\tif err := json.Unmarshal(data, &n); err != nil {"
print "\t\treturn 0, err"
print "\t} else if n > math.MaxInt8 || n < math.MinInt8 {"
print "\t} else if n > math.MaxInt32 || n < math.MinInt32 {"
print "\t\treturn 0, errors.New(`integer out of range`)"
print "\t} else {"
print "\t\treturn n, nil"
@ -191,7 +191,7 @@ function codegen_enum_value(name, subname, value, cg, goname) {
function codegen_enum(name, cg, gotype, fields) {
gotype = PrefixCamel name
print "type " gotype " int8"
print "type " gotype " int"
print ""
print "const ("
@ -239,10 +239,12 @@ function codegen_enum(name, cg, gotype, fields) {
# XXX: This should also check if it isn't out-of-range for any reason,
# but our usage of sprintf() stands in the way a bit.
CodegenSerialize[name] = "\tdata = append(data, uint8(%s))\n"
CodegenSerialize[name] = \
"\tdata = binary.BigEndian.AppendUint32(data, uint32(%s))\n"
CodegenDeserialize[name] = \
"\tif len(data) >= 1 {\n" \
"\t\t%s, data = " gotype "(data[0]), data[1:]\n" \
"\tif len(data) >= 4 {\n" \
"\t\t%s = " gotype "(int32(binary.BigEndian.Uint32(data)))\n" \
"\t\tdata = data[4:]\n" \
"\t} else {\n" \
"\t\treturn nil, false\n" \
"\t}\n"

View File

@ -15,7 +15,7 @@
# Booleans are one byte each.
# Strings must be valid UTF-8, use u8<> to lift that restriction.
# String and array lengths are encoded as u32.
# Enumeration values automatically start at 1, and are encoded as i8.
# Enumeration values automatically start at 1, and are encoded as i32.
# Any struct or union field may be a variable-length array.
#
# Message framing is done externally, but also happens to prefix u32 lengths.
@ -189,8 +189,6 @@ function defenum( name, ident, value, cg) {
value = readnumber()
if (!value)
fatal("enumeration values cannot be zero")
if (value < -128 || value > 127)
fatal("enumeration value out of range")
expect(accept(","))
append(EnumValues, name, SUBSEP ident)
if (EnumValues[name, ident]++)

View File

@ -19,7 +19,7 @@ struct CommandMessage {
case HELLO:
u32 version;
// If the version check succeeds, the client will receive
// an initial stream of BUFFER_UPDATE, BUFFER_STATS, BUFFER_LINE,
// an initial stream of BUFFER_UPDATE, BUFFER_LINE,
// and finally a BUFFER_ACTIVATE message.
case ACTIVE:
void;
@ -50,7 +50,6 @@ struct EventMessage {
union EventData switch (enum Event {
PING,
BUFFER_UPDATE,
BUFFER_STATS,
BUFFER_RENAME,
BUFFER_REMOVE,
BUFFER_ACTIVATE,
@ -62,15 +61,13 @@ struct EventMessage {
case PING:
void;
case BUFFER_UPDATE:
string buffer_name;
bool hide_unimportant;
case BUFFER_STATS:
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;

93
xC.c
View File

@ -3068,16 +3068,6 @@ relay_prepare_buffer_update (struct app_context *ctx, struct buffer *buffer)
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;
}
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,
@ -8181,7 +8171,7 @@ irc_try_parse_welcome_for_userhost (struct server *s, const char *m)
strv_free (&v);
}
static bool process_input_line
static bool process_input_utf8
(struct app_context *, struct buffer *, const char *, int);
static void on_autoaway_timer (struct app_context *ctx);
@ -8210,7 +8200,7 @@ irc_on_registered (struct server *s, const char *nickname)
if (command)
{
log_server_debug (s, "Executing \"#s\"", command);
(void) process_input_line (s->ctx, s->buffer, command, 0);
(void) process_input_utf8 (s->ctx, s->buffer, command, 0);
}
int64_t command_delay = get_config_integer (s->config, "command_delay");
@ -9076,21 +9066,12 @@ irc_autosplit_message (struct server *s, const char *message,
- 1 - (int) strlen (s->irc_user_host)
- 1 - fixed_part;
// Multiline messages can be triggered through hooks and plugins.
struct strv lines = strv_make ();
cstr_split (message, "\r\n", false, &lines);
bool success = true;
for (size_t i = 0; i < lines.len; i++)
{
// We don't always have the full info for message splitting.
if (!space_in_one_message)
strv_append (output, lines.vector[i]);
else if (!(success =
wrap_message (lines.vector[i], space_in_one_message, output, e)))
break;
}
strv_free (&lines);
return success;
// However we don't always have the full info for message splitting
if (!space_in_one_message)
strv_append (output, message);
else if (!wrap_message (message, space_in_one_message, output, e))
return false;
return true;
}
static void
@ -9099,11 +9080,12 @@ send_autosplit_message (struct server *s,
const char *prefix, const char *suffix)
{
struct buffer *buffer = str_map_find (&s->irc_buffer_map, target);
// "COMMAND target * :prefix*suffix"
int fixed_part = strlen (command) + 1 + strlen (target) + 1 + 1
+ strlen (prefix) + strlen (suffix);
// We might also want to preserve attributes across splits but
// that would make this code a lot more complicated
struct strv lines = strv_make ();
struct error *e = NULL;
if (!irc_autosplit_message (s, message, fixed_part, &lines, &e))
@ -9849,7 +9831,7 @@ lua_buffer_execute (lua_State *L)
struct lua_weak *wrapper = lua_weak_deref (L, &lua_buffer_info);
struct buffer *buffer = wrapper->object;
const char *line = lua_plugin_check_utf8 (L, 2);
(void) process_input_line (wrapper->plugin->ctx, buffer, line, 0);
(void) process_input_utf8 (wrapper->plugin->ctx, buffer, line, 0);
return 0;
}
@ -13279,13 +13261,13 @@ process_alias (struct app_context *ctx, struct buffer *buffer,
log_global_debug (ctx, "Alias expanded to: ###d: \"#s\"",
(int) i, commands->vector[i]);
for (size_t i = 0; i < commands->len; i++)
if (!process_input_line (ctx, buffer, commands->vector[i], ++level))
if (!process_input_utf8 (ctx, buffer, commands->vector[i], ++level))
return false;
return true;
}
static bool
process_input_line_posthook (struct app_context *ctx, struct buffer *buffer,
process_input_utf8_posthook (struct app_context *ctx, struct buffer *buffer,
char *input, int alias_level)
{
if (*input != '/' || *++input == '/')
@ -13334,27 +13316,35 @@ process_input_hooks (struct app_context *ctx, struct buffer *buffer,
}
static bool
process_input_line (struct app_context *ctx, struct buffer *buffer,
process_input_utf8 (struct app_context *ctx, struct buffer *buffer,
const char *input, int alias_level)
{
// Note that this also gets called on expanded aliases,
// which might or might not be desirable (we can forward "alias_level")
char *processed = process_input_hooks (ctx, buffer, xstrdup (input));
bool result = !processed
|| process_input_line_posthook (ctx, buffer, processed, alias_level);
|| process_input_utf8_posthook (ctx, buffer, processed, alias_level);
free (processed);
return result;
}
static void
process_input (struct app_context *ctx, struct buffer *buffer,
const char *input)
process_input (struct app_context *ctx, char *user_input)
{
struct strv lines = strv_make ();
cstr_split (input, "\r\n", false, &lines);
for (size_t i = 0; i < lines.len; i++)
(void) process_input_line (ctx, buffer, lines.vector[i], 0);
strv_free (&lines);
char *input;
if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, NULL)))
print_error ("character conversion failed for: %s", "user input");
else
{
struct strv lines = strv_make ();
cstr_split (input, "\r\n", false, &lines);
for (size_t i = 0; i < lines.len; i++)
(void) process_input_utf8 (ctx,
ctx->current_buffer, lines.vector[i], 0);
strv_free (&lines);
}
free (input);
}
// --- Word completion ---------------------------------------------------------
@ -14203,10 +14193,6 @@ on_toggle_unimportant (int count, int key, void *user_data)
(void) key;
struct app_context *ctx = user_data;
ctx->current_buffer->hide_unimportant ^= true;
relay_prepare_buffer_update (ctx, ctx->current_buffer);
relay_broadcast (ctx);
buffer_print_backlog (ctx, ctx->current_buffer);
return true;
}
@ -14594,7 +14580,7 @@ on_editline_return (EditLine *editline, int key)
}
free (line);
// on_pending_input() expects a multibyte string
// process_input() expects a multibyte string
const LineInfo *info_mb = el_line (editline);
strv_append_owned (&ctx->pending_input,
xstrndup (info_mb->buffer, info_mb->lastchar - info_mb->buffer));
@ -15184,15 +15170,7 @@ on_pending_input (struct app_context *ctx)
{
poller_idle_reset (&ctx->input_event);
for (size_t i = 0; i < ctx->pending_input.len; i++)
{
char *input = iconv_xstrdup
(ctx->term_to_utf8, ctx->pending_input.vector[i], -1, NULL);
if (input)
process_input (ctx, ctx->current_buffer, input);
else
print_error ("character conversion failed for: %s", "user input");
free (input);
}
process_input (ctx, ctx->pending_input.vector[i]);
strv_reset (&ctx->pending_input);
}
@ -15245,8 +15223,6 @@ client_resync (struct client *c)
{
relay_prepare_buffer_update (c->ctx, buffer);
relay_send (c);
relay_prepare_buffer_stats (c->ctx, buffer);
relay_send (c);
LIST_FOR_EACH (struct buffer_line, line, buffer->lines)
{
@ -15396,7 +15372,8 @@ client_process_message (struct client *c,
&m->data.buffer_complete);
break;
case RELAY_COMMAND_BUFFER_INPUT:
process_input (c->ctx, buffer, m->data.buffer_input.text.str);
(void) process_input_utf8 (c->ctx,
buffer, m->data.buffer_input.text.str, 0);
break;
case RELAY_COMMAND_BUFFER_ACTIVATE:
buffer_activate (c->ctx, buffer);

View File

@ -89,18 +89,14 @@ body {
}
.buffer {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
grid-template-columns: max-content auto;
overflow-y: auto;
}
.log {
font-family: monospace;
overflow-y: auto;
}
.log, .content {
padding: .1em .3em;
/* Note: https://bugs.chromium.org/p/chromium/issues/detail?id=1261435 */
white-space: break-spaces;
overflow-wrap: break-word;
font-family: monospace;
white-space: pre-wrap;
overflow-y: auto;
}
.leaked {
@ -140,6 +136,10 @@ body {
.mark.action {
color: darkred;
}
.content {
padding: .1em .3em;
white-space: pre-wrap;
}
.content .b {
font-weight: bold;
}
@ -162,7 +162,6 @@ textarea {
margin: 0;
border: 2px inset #eee;
flex-shrink: 0;
resize: vertical;
}
textarea:focus {
outline: none;

View File

@ -110,9 +110,8 @@ class RelayRpc extends EventTarget {
if (typeof params !== 'object')
throw "Method parameters must be an object"
// Left shifts in Javascript convert to a 32-bit signed number.
let seq = ++this.commandSeq
if ((seq << 0) != seq)
if (seq >= 1 << 32)
seq = this.commandSeq = 0
this.ws.send(JSON.stringify({commandSeq: seq, data: params}))
@ -132,25 +131,13 @@ class RelayRpc extends EventTarget {
let rpc = new RelayRpc(proxy)
let buffers = new Map()
let bufferLast = undefined
let bufferCurrent = undefined
let bufferLog = undefined
let bufferAutoscroll = true
function resetBufferStats(b) {
b.newMessages = 0
b.newUnimportantMessages = 0
b.highlighted = false
}
function bufferActivate(name) {
rpc.send({command: 'BufferActivate', bufferName: name})
}
let connecting = true
rpc.connect().then(result => {
buffers.clear()
bufferLast = undefined
bufferCurrent = undefined
bufferLog = undefined
bufferAutoscroll = true
@ -174,20 +161,13 @@ rpc.addEventListener('Ping', event => {
rpc.addEventListener('BufferUpdate', event => {
let e = event.detail, b = buffers.get(e.bufferName)
if (b === undefined) {
buffers.set(e.bufferName, (b = {lines: []}))
resetBufferStats(b)
buffers.set(e.bufferName, {
lines: [],
newMessages: e.newMessages,
newUnimportantMessages: e.newUnimportantMessages,
highlighted: e.highlighted,
})
}
b.hideUnimportant = e.hideUnimportant
})
rpc.addEventListener('BufferStats', event => {
let e = event.detail, b = buffers.get(e.bufferName)
if (b === undefined)
return
b.newMessages = e.newMessages,
b.newUnimportantMessages = e.newUnimportantMessages
b.highlighted = e.highlighted
})
rpc.addEventListener('BufferRename', event => {
@ -199,16 +179,16 @@ rpc.addEventListener('BufferRename', event => {
rpc.addEventListener('BufferRemove', event => {
let e = event.detail
buffers.delete(e.bufferName)
if (e.bufferName === bufferLast)
bufferLast = undefined
})
rpc.addEventListener('BufferActivate', event => {
let old = buffers.get(bufferCurrent)
if (old !== undefined)
resetBufferStats(old)
if (old !== undefined) {
old.newMessages = 0
old.newUnimportantMessages = 0
old.highlighted = false
}
bufferLast = bufferCurrent
let e = event.detail, b = buffers.get(e.bufferName)
bufferCurrent = e.bufferName
bufferLog = undefined
@ -241,21 +221,18 @@ rpc.addEventListener('BufferLine', event => {
return
}
let visible = !document.hidden && bufferLog === undefined &&
(e.bufferName == bufferCurrent || e.leakToActive)
let visible = e.bufferName == bufferCurrent || e.leakToActive
b.lines.push({...line})
if (!(visible || e.leakToActive) ||
b.newMessages || b.newUnimportantMessages) {
if (!visible || b.newMessages || b.newUnimportantMessages) {
if (line.isUnimportant)
b.newUnimportantMessages++
else
b.newMessages++
}
if (e.leakToActive) {
let bc = buffers.get(bufferCurrent)
bc.lines.push({...line, leaked: true})
if (!visible || bc.newMessages || bc.newUnimportantMessages) {
if (bc.newMessages || bc.newUnimportantMessages) {
if (line.isUnimportant)
bc.newUnimportantMessages++
else
@ -296,12 +273,6 @@ for (let i = 0; i < 24; i++) {
// ---- UI ---------------------------------------------------------------------
let linkRE = [
/https?:\/\//,
/([^\[\](){}<>"'\s]|\([^\[\](){}<>"'\s]*\))+/,
/[^\[\](){}<>"'\s,.:]/,
].map(r => r.source).join('')
let Toolbar = {
toggleAutoscroll: () => {
bufferAutoscroll = !bufferAutoscroll
@ -333,6 +304,10 @@ let Toolbar = {
}
let BufferList = {
activate: name => {
rpc.send({command: 'BufferActivate', bufferName: name})
},
view: vnode => {
let items = Array.from(buffers, ([name, b]) => {
let classes = [], displayName = name
@ -347,7 +322,7 @@ let BufferList = {
}
}
return m('.item', {
onclick: event => bufferActivate(name),
onclick: event => BufferList.activate(name),
class: classes.join(' '),
}, displayName)
})
@ -370,11 +345,17 @@ let Content = {
},
linkify: (text, attrs) => {
let re = new RegExp(linkRE, 'g'), a = [], end = 0, match
let re = new RegExp([
/https?:\/\//,
/([^\[\](){}<>"'\s]|\([^\[\](){}<>"'\s]*\))+/,
/[^\[\](){}<>"'\s,.:]/,
].map(r => r.source).join(''), 'g')
let a = [], end = 0, match
while ((match = re.exec(text)) !== null) {
if (end < match.index)
a.push(m('span', attrs, text.substring(end, match.index)))
a.push(m('a[target=_blank]', {href: match[0], ...attrs}, match[0]))
a.push(m('a', {href: match[0], ...attrs}, match[0]))
end = re.lastIndex
}
if (end < text.length)
@ -444,38 +425,25 @@ let Content = {
}
let Buffer = {
controller: new AbortController(),
onbeforeremove: vnode => {
Buffer.controller.abort()
},
onupdate: vnode => {
if (bufferAutoscroll)
oncreate: vnode => {
if (vnode.dom !== undefined && bufferAutoscroll)
vnode.dom.scrollTop = vnode.dom.scrollHeight
},
oncreate: vnode => {
Buffer.onupdate(vnode)
window.addEventListener('resize', event => Buffer.onupdate(vnode),
{signal: Buffer.controller.signal})
onupdate: vnode => {
Buffer.oncreate(vnode)
},
view: vnode => {
let lines = []
let b = buffers.get(bufferCurrent)
if (b === undefined)
return m('.buffer')
return
let lastDateMark = undefined
let markBefore = b.lines.length
- b.newMessages - b.newUnimportantMessages
b.lines.forEach((line, i) => {
if (i == markBefore)
lines.push(m('.unread'))
if (line.isUnimportant && b.hideUnimportant)
return
let date = new Date(line.when)
let dateMark = date.toLocaleDateString()
if (dateMark !== lastDateMark) {
@ -483,10 +451,14 @@ let Buffer = {
lastDateMark = dateMark
}
if (i == markBefore)
lines.push(m('.unread'))
let attrs = {}
if (line.leaked)
attrs.class = 'leaked'
// TODO: Make use of isUnimportant.
lines.push(m('.time', {...attrs}, date.toLocaleTimeString()))
lines.push(m(Content, {...attrs}, line))
})
@ -504,21 +476,8 @@ let Log = {
vnode.dom.scrollTop = vnode.dom.scrollHeight
},
linkify: text => {
let re = new RegExp(linkRE, 'g'), a = [], end = 0, match
while ((match = re.exec(text)) !== null) {
if (end < match.index)
a.push(text.substring(end, match.index))
a.push(m('a[target=_blank]', {href: match[0]}, match[0]))
end = re.lastIndex
}
if (end < text.length)
a.push(text.substring(end))
return a
},
view: vnode => {
return m(".log", {}, Log.linkify(bufferLog))
return m(".log", {}, bufferLog)
},
}
@ -583,13 +542,11 @@ let Input = {
let handled = false
switch (event.keyCode) {
case 9:
if (!event.ctrlKey && !event.metaKey && !event.altKey &&
!event.shiftKey)
if (!event.shiftKey)
handled = Input.complete(textarea)
break
case 13:
if (!event.ctrlKey && !event.metaKey && !event.altKey &&
!event.shiftKey)
if (!event.shiftKey)
handled = Input.submit(textarea)
break
}
@ -613,7 +570,6 @@ let Main = {
return m('.xP', {}, [
m('.title', {}, [`xP (${state})`, m(Toolbar)]),
m('.middle', {}, [m(BufferList), m(BufferContainer)]),
// TODO: Indicate hideUnimportant.
m('.status', {}, bufferCurrent),
m(Input),
])
@ -621,28 +577,3 @@ let Main = {
}
window.addEventListener('load', () => m.mount(document.body, Main))
document.addEventListener('keydown', event => {
if (rpc.ws == undefined || event.ctrlKey || event.metaKey)
return
if (event.altKey && event.key == 'Tab') {
if (bufferLast !== undefined)
bufferActivate(bufferLast)
} else if (event.altKey && event.key == 'a') {
for (const [name, b] of buffers)
if (name !== bufferCurrent && b.newMessages) {
bufferActivate(name)
break
}
} else if (event.altKey && event.key == '!') {
for (const [name, b] of buffers)
if (name !== bufferCurrent && b.highlighted) {
bufferActivate(name)
break
}
} else
return
event.preventDefault()
})