Compare commits

..

7 Commits

Author SHA1 Message Date
2b13f891c9
xP: clean up 2022-09-07 14:45:44 +02:00
d55402234c
xP: add a temporary lock for autoscroll 2022-09-07 13:53:28 +02:00
e3149b9abf
xP: support showing buffer logs 2022-09-07 13:53:28 +02:00
976e7bfbb4
xP: separate input buffers 2022-09-07 13:04:30 +02:00
5fd76ba6f9
xC: add a trivial /relay command
For there is otherwise no way of getting that information.
2022-09-07 13:01:34 +02:00
41878a587f
xC: use liberty logging less
These messages cannot be relayed to frontends (they could be,
but it's useful to keep them distinct rather than redirected).
2022-09-07 13:01:30 +02:00
80089a4d65
xC: describe general.editor parse errors 2022-09-07 13:01:29 +02:00
3 changed files with 231 additions and 115 deletions

50
xC.c
View File

@ -2868,6 +2868,7 @@ relay_try_fetch_client (struct app_context *ctx, int listen_fd)
if (accept_error_is_transient (errno)) if (accept_error_is_transient (errno))
print_warning ("%s: %s", "accept", strerror (errno)); print_warning ("%s: %s", "accept", strerror (errno));
else else
// TODO: Rather dispose of the listening socket.
print_fatal ("%s: %s", "accept", strerror (errno)); print_fatal ("%s: %s", "accept", strerror (errno));
return true; return true;
} }
@ -12053,6 +12054,23 @@ handle_command_plugin (struct handler_args *a)
return true; return true;
} }
static bool
handle_command_relay (struct handler_args *a)
{
if (*a->arguments)
return false;
int len = 0;
LIST_FOR_EACH (struct client, c, a->ctx->clients)
len++;
if (a->ctx->relay_fd == -1)
log_global_status (a->ctx, "The relay is not enabled");
else
log_global_status (a->ctx, "The relay has #d clients", len);
return true;
}
static bool static bool
show_aliases_list (struct app_context *ctx) show_aliases_list (struct app_context *ctx)
{ {
@ -12789,6 +12807,9 @@ g_command_handlers[] =
{ "plugin", "Manage plugins", { "plugin", "Manage plugins",
"list | load <name> | unload <name>", "list | load <name> | unload <name>",
handle_command_plugin, 0 }, handle_command_plugin, 0 },
{ "relay", "Show relay information",
NULL,
handle_command_relay, 0 },
{ "alias", "List or set aliases", { "alias", "List or set aliases",
"[<name> <definition>]", "[<name> <definition>]",
@ -13887,22 +13908,27 @@ build_editor_command (struct app_context *ctx, const char *filename)
{ {
case 'F': case 'F':
str_append (&argument, filename); str_append (&argument, filename);
break; continue;
case 'L': case 'L':
str_append_printf (&argument, "%zu", line_one_based); str_append_printf (&argument, "%zu", line_one_based);
break; continue;
case 'C': case 'C':
str_append_printf (&argument, "%zu", column + 1); str_append_printf (&argument, "%zu", column + 1);
break; continue;
case 'B': case 'B':
str_append_printf (&argument, "%d", cursor + 1); str_append_printf (&argument, "%d", cursor + 1);
break; continue;
case '%': case '%':
case ' ': case ' ':
str_append_c (&argument, *editor); str_append_c (&argument, *editor);
break; continue;
default: }
print_warning ("unknown substitution variable");
const char *p = editor;
if (soft_assert (utf8_decode (&p, strlen (p)) > 0))
{
log_global_error (ctx, "Unknown substitution variable: %#&s",
xstrndup (editor, p - editor));
} }
} }
if (argument.len) if (argument.len)
@ -14755,7 +14781,8 @@ try_reap_child (struct app_context *ctx)
if (WIFSTOPPED (status)) if (WIFSTOPPED (status))
{ {
// We could also send SIGCONT but what's the point // We could also send SIGCONT but what's the point
print_debug ("a child has been stopped, killing its process group"); log_global_debug (ctx,
"A child has been stopped, killing its process group");
kill (-zombie, SIGKILL); kill (-zombie, SIGKILL);
return true; return true;
} }
@ -15253,7 +15280,7 @@ client_process_message (struct client *c,
if (!relay_command_message_deserialize (m, r) if (!relay_command_message_deserialize (m, r)
|| msg_unpacker_get_available (r)) || msg_unpacker_get_available (r))
{ {
print_error ("deserialization failed, killing client"); log_global_error (c->ctx, "Deserialization failed, killing client");
return false; return false;
} }
@ -15272,7 +15299,8 @@ client_process_message (struct client *c,
if (m->data.hello.version != RELAY_VERSION) if (m->data.hello.version != RELAY_VERSION)
{ {
// TODO: This should send back an error message and shut down. // TODO: This should send back an error message and shut down.
print_error ("protocol version mismatch, killing client"); log_global_error (c->ctx,
"Protocol version mismatch, killing client");
return false; return false;
} }
c->initialized = true; c->initialized = true;
@ -15300,7 +15328,7 @@ client_process_message (struct client *c,
client_process_buffer_log (c, m->command_seq, buffer); client_process_buffer_log (c, m->command_seq, buffer);
break; break;
default: default:
print_warning ("unhandled client command"); log_global_debug (c->ctx, "Unhandled client command");
relay_prepare_error (c->ctx, m->command_seq, "Unknown command"); relay_prepare_error (c->ctx, m->command_seq, "Unknown command");
relay_send (c); relay_send (c);
} }

View File

@ -16,6 +16,10 @@ body {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
padding: .05rem .3rem; padding: .05rem .3rem;
} }
.title {
display: flex;
justify-content: space-between;
}
.middle { .middle {
flex: auto; flex: auto;
@ -52,6 +56,12 @@ body {
grid-template-columns: max-content auto; grid-template-columns: max-content auto;
overflow-y: auto; overflow-y: auto;
} }
.log {
padding: .1rem .3rem;
font-family: monospace;
white-space: pre-wrap;
overflow-y: auto;
}
.date { .date {
padding: .3rem; padding: .3rem;

View File

@ -118,6 +118,11 @@ class RelayRpc extends EventTarget {
this.promised[seq] = {resolve, reject} this.promised[seq] = {resolve, reject}
}) })
} }
base64decode(str) {
return decodeURIComponent(atob(str).split('').map(c =>
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''))
}
} }
// ---- Event processing ------------------------------------------------------- // ---- Event processing -------------------------------------------------------
@ -126,10 +131,16 @@ let rpc = new RelayRpc(proxy)
let buffers = new Map() let buffers = new Map()
let bufferCurrent = undefined let bufferCurrent = undefined
let bufferLog = undefined
let bufferAutoscroll = true
let connecting = true let connecting = true
rpc.connect().then(result => { rpc.connect().then(result => {
buffers.clear() buffers.clear()
bufferCurrent = undefined bufferCurrent = undefined
bufferLog = undefined
bufferAutoscroll = true
rpc.send({command: 'Hello', version: 1}) rpc.send({command: 'Hello', version: 1})
connecting = false connecting = false
m.redraw() m.redraw()
@ -163,13 +174,24 @@ rpc.addEventListener('BufferRemove', event => {
}) })
rpc.addEventListener('BufferActivate', event => { rpc.addEventListener('BufferActivate', event => {
let e = event.detail let e = event.detail, b = buffers.get(e.bufferName)
let old = buffers.get(bufferCurrent)
bufferCurrent = e.bufferName bufferCurrent = e.bufferName
setTimeout(() => { bufferLog = undefined
let el = document.getElementById('input') bufferAutoscroll = true
if (el !== null)
el.focus() let textarea = document.getElementById('input')
}) if (textarea === null)
return
textarea.focus()
if (old !== undefined)
old.input = textarea.value
if (b !== undefined)
textarea.value = b.input || ''
else
textarea.value = ''
}) })
rpc.addEventListener('BufferLine', event => { rpc.addEventListener('BufferLine', event => {
@ -204,67 +226,97 @@ for (let i = 0; i < 24; i++) {
palette[232 + i] = `#${g}${g}${g}` palette[232 + i] = `#${g}${g}${g}`
} }
function applyColor(fg, bg, inverse) {
if (inverse)
[fg, bg] = [bg >= 0 ? bg : 15, fg >= 0 ? fg : 0]
let style = {}
if (fg >= 0)
style.color = palette[fg]
if (bg >= 0)
style.backgroundColor = palette[bg]
if (style)
return style
}
// ---- UI --------------------------------------------------------------------- // ---- UI ---------------------------------------------------------------------
let BufferList = { let Toolbar = {
toggleAutoscroll: () => {
bufferAutoscroll = !bufferAutoscroll
},
toggleLog: () => {
if (bufferLog) {
bufferLog = undefined
return
}
rpc.send({
command: 'BufferLog',
bufferName: bufferCurrent,
}).then(resp => {
bufferLog = rpc.base64decode(resp.log)
m.redraw()
})
},
view: vnode => { view: vnode => {
let items = [] return m('.toolbar', {}, [
buffers.forEach((b, name) => { m('button', {onclick: Toolbar.toggleAutoscroll},
let attrs = { bufferAutoscroll ? 'Pause autoscroll' : 'Unpause autoscroll'),
onclick: event => { m('button', {onclick: Toolbar.toggleLog},
rpc.send({command: 'BufferActivate', bufferName: name}) bufferLog === undefined ? 'Show log' : 'Hide log'),
}, ])
} },
}
let BufferList = {
activate: name => {
rpc.send({command: 'BufferActivate', bufferName: name})
},
view: vnode => {
let items = Array.from(buffers, ([name, b]) => {
let attrs = {onclick: event => BufferList.activate(name)}
if (name == bufferCurrent) if (name == bufferCurrent)
attrs.class = 'active' attrs.class = 'active'
items.push(m('.item', attrs, name)) return m('.item', attrs, name)
}) })
return m('.list', {}, items) return m('.list', {}, items)
}, },
} }
function linkify(text, attrs, a) {
let re = new RegExp([
/https?:\/\//,
/([^\[\](){}<>"'\s]|\([^\[\](){}<>"'\s]*\))+/,
/[^\[\](){}<>"'\s,.:]/,
].map(r => r.source).join(''), 'g')
let 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', {href: match[0], ...attrs}, match[0]))
end = re.lastIndex
}
if (end < text.length)
a.push(m('span', attrs, text.substring(end)))
}
let Content = { let Content = {
applyColor: (fg, bg, inverse) => {
if (inverse)
[fg, bg] = [bg >= 0 ? bg : 15, fg >= 0 ? fg : 0]
let style = {}
if (fg >= 0)
style.color = palette[fg]
if (bg >= 0)
style.backgroundColor = palette[bg]
if (style)
return style
},
linkify: (text, attrs) => {
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', {href: match[0], ...attrs}, match[0]))
end = re.lastIndex
}
if (end < text.length)
a.push(m('span', attrs, text.substring(end)))
return a
},
view: vnode => { view: vnode => {
let line = vnode.children[0] let line = vnode.children[0]
let content = [] let mark = undefined
switch (line.rendition) { switch (line.rendition) {
case 'Indent': content.push(m('span.mark', {}, '')); break case 'Indent': mark = m('span.mark', {}, ''); break
case 'Status': content.push(m('span.mark', {}, '')); break case 'Status': mark = m('span.mark', {}, ''); break
case 'Error': content.push(m('span.mark.error', {}, '⚠')); break case 'Error': mark = m('span.mark.error', {}, '⚠'); break
case 'Join': content.push(m('span.mark.join', {}, '→')); break case 'Join': mark = m('span.mark.join', {}, '→'); break
case 'Part': content.push(m('span.mark.part', {}, '←')); break case 'Part': mark = m('span.mark.part', {}, '←'); break
case 'Action': content.push(m('span.mark.action', {}, '✶')); break case 'Action': mark = m('span.mark.action', {}, '✶'); break
} }
let classes = new Set() let classes = new Set()
@ -275,14 +327,13 @@ let Content = {
classes.add(c) classes.add(c)
} }
let fg = -1, bg = -1, inverse = false let fg = -1, bg = -1, inverse = false
line.items.forEach(item => { return m('.content', {}, [mark, line.items.flatMap(item => {
switch (item.kind) { switch (item.kind) {
case 'Text': case 'Text':
linkify(item.text, { return Content.linkify(item.text, {
class: Array.from(classes.keys()).join(' '), class: Array.from(classes.keys()).join(' '),
style: applyColor(fg, bg, inverse), style: Content.applyColor(fg, bg, inverse),
}, content) })
break
case 'Reset': case 'Reset':
classes.clear() classes.clear()
fg = bg = -1 fg = bg = -1
@ -297,25 +348,30 @@ let Content = {
case 'FlipInverse': case 'FlipInverse':
inverse = !inverse inverse = !inverse
break break
case 'FlipBold': flip('b'); break case 'FlipBold':
case 'FlipItalic': flip('i'); break flip('b')
case 'FlipUnderline': flip('u'); break break
case 'FlipCrossedOut': flip('s'); break case 'FlipItalic':
case 'FlipMonospace': flip('m'); break flip('i')
break
case 'FlipUnderline':
flip('u')
break
case 'FlipCrossedOut':
flip('s')
break
case 'FlipMonospace':
flip('m')
break
} }
}) })])
return m('.content', {}, content)
}, },
} }
let Buffer = { let Buffer = {
oncreate: vnode => { oncreate: vnode => {
if (vnode.dom === undefined) if (vnode.dom !== undefined && bufferAutoscroll)
return vnode.dom.scrollTop = vnode.dom.scrollHeight
let el = vnode.dom.children[1]
if (el !== null)
el.scrollTop = el.scrollHeight
}, },
onupdate: vnode => { onupdate: vnode => {
@ -340,62 +396,84 @@ let Buffer = {
lines.push(m('.time', {}, date.toLocaleTimeString())) lines.push(m('.time', {}, date.toLocaleTimeString()))
lines.push(m(Content, {}, line)) lines.push(m(Content, {}, line))
}) })
return m('.buffer', {}, lines)
},
}
let Log = {
oncreate: vnode => {
if (vnode.dom !== undefined)
vnode.dom.scrollTop = vnode.dom.scrollHeight
},
view: vnode => {
return m(".log", {}, bufferLog)
},
}
let BufferContainer = {
view: vnode => {
return m('.buffer-container', {}, [ return m('.buffer-container', {}, [
m('.filler'), m('.filler'),
m('.buffer', {}, lines), bufferLog !== undefined ? m(Log) : m(Buffer),
]) ])
}, },
} }
function onKeyDown(event) { let Input = {
// TODO: And perhaps on other actions, too. complete: textarea => {
rpc.send({command: 'Active'})
// TODO: Cancel any current autocomplete.
let textarea = event.currentTarget
switch (event.keyCode) {
case 9:
if (textarea.selectionStart !== textarea.selectionEnd) if (textarea.selectionStart !== textarea.selectionEnd)
return return false
rpc.send({ rpc.send({
command: 'BufferComplete', command: 'BufferComplete',
bufferName: bufferCurrent, bufferName: bufferCurrent,
text: textarea.value, text: textarea.value,
position: textarea.selectionEnd, position: textarea.selectionEnd,
}).then(response => { }).then(resp => {
// TODO: Somehow display remaining options, or cycle through. // TODO: Somehow display remaining options, or cycle through.
if (response.completions.length) if (resp.completions.length)
textarea.setRangeText(response.completions[0], textarea.setRangeText(resp.completions[0],
response.start, textarea.selectionEnd, 'end') resp.start, textarea.selectionEnd, 'end')
if (response.completions.length === 1) if (resp.completions.length === 1)
textarea.setRangeText(' ', textarea.setRangeText(' ',
textarea.selectionStart, textarea.selectionEnd, 'end') textarea.selectionStart, textarea.selectionEnd, 'end')
}) })
break; return true
case 13: },
submit: textarea => {
rpc.send({ rpc.send({
command: 'BufferInput', command: 'BufferInput',
bufferName: bufferCurrent, bufferName: bufferCurrent,
text: textarea.value, text: textarea.value,
}) })
textarea.value = '' textarea.value = ''
break; return true
default: },
return
}
event.preventDefault() onKeyDown: event => {
} // TODO: And perhaps on other actions, too.
rpc.send({command: 'Active'})
// TODO: Cancel any current autocomplete.
let textarea = event.currentTarget
let handled = false
switch (event.keyCode) {
case 9:
handled = Input.complete(textarea)
break
case 13:
handled = Input.submit(textarea)
break
}
if (handled)
event.preventDefault()
},
// TODO: This should be remembered across buffer switches,
// and we'll probably have to intercept /all/ key presses.
let Input = {
view: vnode => { view: vnode => {
return m('textarea#input', { return m('textarea#input', {rows: 1, onkeydown: Input.onKeyDown})
rows: 1,
onkeydown: onKeyDown,
})
}, },
} }
@ -408,8 +486,8 @@ let Main = {
state = "Disconnected" state = "Disconnected"
return m('.xP', {}, [ return m('.xP', {}, [
m('.title', {}, `xP (${state})`), m('.title', {}, [`xP (${state})`, m(Toolbar)]),
m('.middle', {}, [m(BufferList), m(Buffer)]), m('.middle', {}, [m(BufferList), m(BufferContainer)]),
m('.status', {}, bufferCurrent), m('.status', {}, bufferCurrent),
m(Input), m(Input),
]) ])