Compare commits
5 Commits
d7b0b447b7
...
8cd94b30f6
Author | SHA1 | Date | |
---|---|---|---|
8cd94b30f6 | |||
2d30b6d115 | |||
cf14cb8122 | |||
31e9c6d2d5 | |||
d2af6cf64c |
@ -382,7 +382,8 @@ function codegen_union(name, cg, gotype, tagfield, tagvar) {
|
||||
print "}"
|
||||
print ""
|
||||
|
||||
print "func (u *" gotype ") MarshalJSON() ([]byte, error) {"
|
||||
# This cannot be a pointer method, it wouldn't work recursively.
|
||||
print "func (u " gotype ") MarshalJSON() ([]byte, error) {"
|
||||
print "\treturn json.Marshal(u.Interface)"
|
||||
print "}"
|
||||
print ""
|
||||
|
4
xC-proto
4
xC-proto
@ -71,7 +71,7 @@ struct EventMessage {
|
||||
PART,
|
||||
ACTION,
|
||||
} rendition;
|
||||
// Unix timestamp in seconds.
|
||||
// Unix timestamp in milliseconds.
|
||||
u64 when;
|
||||
// Broken-up text, with in-band formatting.
|
||||
union ItemData switch (enum Item {
|
||||
@ -104,6 +104,8 @@ struct EventMessage {
|
||||
} items<>;
|
||||
case BUFFER_CLEAR:
|
||||
string buffer_name;
|
||||
|
||||
// Restriction: command_seq is strictly increasing, across both of these.
|
||||
case ERROR:
|
||||
u32 command_seq;
|
||||
string error;
|
||||
|
181
xC.c
181
xC.c
@ -1032,7 +1032,7 @@ input_el__restore (struct input_el *self)
|
||||
static void
|
||||
input_el__start_over (struct input_el *self)
|
||||
{
|
||||
wchar_t x[] = { L'g' & 31, 0 };
|
||||
wchar_t x[] = { L'c' & 31, 0 };
|
||||
el_wpush (self->editline, x);
|
||||
|
||||
int dummy_count = 0;
|
||||
@ -3105,7 +3105,7 @@ relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer,
|
||||
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;
|
||||
e->when = line->when * 1000;
|
||||
|
||||
size_t len = 0;
|
||||
for (size_t i = 0; line->items[i].type; i++)
|
||||
@ -13271,12 +13271,6 @@ process_input (struct app_context *ctx, char *user_input)
|
||||
// The amount of crap that goes into this is truly insane.
|
||||
// It's mostly because of Editline's total ignorance of this task.
|
||||
|
||||
static void
|
||||
completion_init (struct completion *self)
|
||||
{
|
||||
memset (self, 0, sizeof *self);
|
||||
}
|
||||
|
||||
static void
|
||||
completion_free (struct completion *self)
|
||||
{
|
||||
@ -13295,13 +13289,13 @@ completion_add_word (struct completion *self, size_t start, size_t end)
|
||||
self->words[self->words_len++] = (struct completion_word) { start, end };
|
||||
}
|
||||
|
||||
static void
|
||||
completion_parse (struct completion *self, const char *line, size_t len)
|
||||
static struct completion
|
||||
completion_make (const char *line, size_t len)
|
||||
{
|
||||
self->line = xstrndup (line, len);
|
||||
struct completion self = { .line = xstrndup (line, len) };
|
||||
|
||||
// The first and the last word may be empty
|
||||
const char *s = self->line;
|
||||
const char *s = self.line;
|
||||
while (true)
|
||||
{
|
||||
const char *start = s;
|
||||
@ -13309,10 +13303,11 @@ completion_parse (struct completion *self, const char *line, size_t len)
|
||||
const char *end = start + word_len;
|
||||
s = end + strspn (end, WORD_BREAKING_CHARS);
|
||||
|
||||
completion_add_word (self, start - self->line, end - self->line);
|
||||
completion_add_word (&self, start - self.line, end - self.line);
|
||||
if (s == end)
|
||||
break;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
static void
|
||||
@ -13486,14 +13481,13 @@ complete_set (struct app_context *ctx, struct completion *data,
|
||||
}
|
||||
|
||||
static void
|
||||
complete_topic (struct app_context *ctx, struct completion *data,
|
||||
complete_topic (struct buffer *buffer, struct completion *data,
|
||||
const char *word, struct strv *output)
|
||||
{
|
||||
(void) data;
|
||||
|
||||
// TODO: make it work in other server-related buffers, too, i.e. when we're
|
||||
// completing the third word and the second word is a known channel name
|
||||
struct buffer *buffer = ctx->current_buffer;
|
||||
if (buffer->type != BUFFER_CHANNEL)
|
||||
return;
|
||||
|
||||
@ -13509,10 +13503,9 @@ complete_topic (struct app_context *ctx, struct completion *data,
|
||||
}
|
||||
|
||||
static void
|
||||
complete_nicknames (struct app_context *ctx, struct completion *data,
|
||||
complete_nicknames (struct buffer *buffer, struct completion *data,
|
||||
const char *word, struct strv *output)
|
||||
{
|
||||
struct buffer *buffer = ctx->current_buffer;
|
||||
if (buffer->type == BUFFER_SERVER)
|
||||
{
|
||||
struct user *self_user = buffer->server->irc_user;
|
||||
@ -13534,9 +13527,9 @@ complete_nicknames (struct app_context *ctx, struct completion *data,
|
||||
}
|
||||
}
|
||||
|
||||
static char **
|
||||
complete_word (struct app_context *ctx, struct completion *data,
|
||||
const char *word)
|
||||
static struct strv
|
||||
complete_word (struct app_context *ctx, struct buffer *buffer,
|
||||
struct completion *data, const char *word)
|
||||
{
|
||||
char *initial = completion_word (data, 0);
|
||||
|
||||
@ -13555,11 +13548,11 @@ complete_word (struct app_context *ctx, struct completion *data,
|
||||
}
|
||||
else if (data->location == 1 && !strcmp (initial, "/topic"))
|
||||
{
|
||||
complete_topic (ctx, data, word, &words);
|
||||
complete_nicknames (ctx, data, word, &words);
|
||||
complete_topic (buffer, data, word, &words);
|
||||
complete_nicknames (buffer, data, word, &words);
|
||||
}
|
||||
else
|
||||
complete_nicknames (ctx, data, word, &words);
|
||||
complete_nicknames (buffer, data, word, &words);
|
||||
|
||||
cstr_set (&initial, NULL);
|
||||
LIST_FOR_EACH (struct hook, iter, ctx->completion_hooks)
|
||||
@ -13568,17 +13561,12 @@ complete_word (struct app_context *ctx, struct completion *data,
|
||||
hook->complete (hook, data, word, &words);
|
||||
}
|
||||
|
||||
if (words.len == 1)
|
||||
{
|
||||
// Nothing matched
|
||||
strv_free (&words);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (words.len == 2)
|
||||
if (words.len <= 2)
|
||||
{
|
||||
// When nothing matches, this copies the sentinel value
|
||||
words.vector[0] = words.vector[1];
|
||||
words.vector[1] = NULL;
|
||||
words.len--;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -13589,7 +13577,7 @@ complete_word (struct app_context *ctx, struct completion *data,
|
||||
else
|
||||
words.vector[0] = xstrndup (words.vector[1], prefix);
|
||||
}
|
||||
return words.vector;
|
||||
return words;
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
@ -13655,26 +13643,26 @@ locale_to_utf8 (struct app_context *ctx, const char *locale,
|
||||
return str_steal (&utf8);
|
||||
}
|
||||
|
||||
static void
|
||||
utf8_vector_to_locale (struct app_context *ctx, char **vector)
|
||||
{
|
||||
for (; *vector; vector++)
|
||||
{
|
||||
char *converted = iconv_xstrdup
|
||||
(ctx->term_from_utf8, *vector, -1, NULL);
|
||||
if (!soft_assert (converted))
|
||||
converted = xstrdup ("");
|
||||
|
||||
cstr_set (vector, converted);
|
||||
}
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
static struct strv
|
||||
make_completions (struct app_context *ctx, struct buffer *buffer,
|
||||
const char *line_utf8, size_t start, size_t end)
|
||||
{
|
||||
struct completion comp = completion_make (line_utf8, strlen (line_utf8));
|
||||
completion_locate (&comp, start);
|
||||
char *word = xstrndup (line_utf8 + start, end - start);
|
||||
struct strv completions = complete_word (ctx, buffer, &comp, word);
|
||||
free (word);
|
||||
completion_free (&comp);
|
||||
return completions;
|
||||
}
|
||||
|
||||
/// Takes a line in locale-specific encoding and position of a word to complete,
|
||||
/// returns a vector of matches in locale-specific encoding.
|
||||
static char **
|
||||
make_completions (struct app_context *ctx, char *line, int start, int end)
|
||||
make_input_completions
|
||||
(struct app_context *ctx, const char *line, int start, int end)
|
||||
{
|
||||
int *fixes[] = { &start, &end };
|
||||
char *line_utf8 = locale_to_utf8 (ctx, line, fixes, N_ELEMENTS (fixes));
|
||||
@ -13683,20 +13671,23 @@ make_completions (struct app_context *ctx, char *line, int start, int end)
|
||||
|
||||
hard_assert (start >= 0 && end >= 0 && start <= end);
|
||||
|
||||
struct completion c;
|
||||
completion_init (&c);
|
||||
completion_parse (&c, line, strlen (line));
|
||||
completion_locate (&c, start);
|
||||
char *word = xstrndup (line + start, end - start);
|
||||
char **completions = complete_word (ctx, &c, word);
|
||||
free (word);
|
||||
completion_free (&c);
|
||||
|
||||
if (completions)
|
||||
utf8_vector_to_locale (ctx, completions);
|
||||
|
||||
struct strv completions =
|
||||
make_completions (ctx, ctx->current_buffer, line_utf8, start, end);
|
||||
free (line_utf8);
|
||||
return completions;
|
||||
if (!completions.len)
|
||||
{
|
||||
strv_free (&completions);
|
||||
return NULL;
|
||||
}
|
||||
for (size_t i = 0; i < completions.len; i++)
|
||||
{
|
||||
char *converted = iconv_xstrdup
|
||||
(ctx->term_from_utf8, completions.vector[i], -1, NULL);
|
||||
if (!soft_assert (converted))
|
||||
converted = xstrdup ("?");
|
||||
cstr_set (&completions.vector[i], converted);
|
||||
}
|
||||
return completions.vector;
|
||||
}
|
||||
|
||||
// --- Common code for user actions --------------------------------------------
|
||||
@ -14381,7 +14372,7 @@ app_readline_completion (const char *text, int start, int end)
|
||||
// Don't iterate over filenames and stuff
|
||||
rl_attempted_completion_over = true;
|
||||
|
||||
return make_completions (g_ctx, rl_line_buffer, start, end);
|
||||
return make_input_completions (g_ctx, rl_line_buffer, start, end);
|
||||
}
|
||||
|
||||
static int
|
||||
@ -14425,8 +14416,6 @@ static unsigned char
|
||||
on_editline_complete (EditLine *editline, int key)
|
||||
{
|
||||
(void) key;
|
||||
(void) editline;
|
||||
|
||||
struct app_context *ctx = g_ctx;
|
||||
|
||||
// First prepare what Readline would have normally done for us...
|
||||
@ -14440,7 +14429,7 @@ on_editline_complete (EditLine *editline, int key)
|
||||
while (el_start && !strchr (WORD_BREAKING_CHARS, copy[el_start - 1]))
|
||||
el_start--;
|
||||
|
||||
char **completions = make_completions (ctx, copy, el_start, el_end);
|
||||
char **completions = make_input_completions (ctx, copy, el_start, el_end);
|
||||
|
||||
// XXX: possibly incorrect wrt. shift state encodings
|
||||
copy[el_end] = '\0';
|
||||
@ -14538,12 +14527,6 @@ app_editline_init (struct input_el *self)
|
||||
// Just what are you doing?
|
||||
CALL_ (input, bind_control, 'u', "vi-kill-line-prev");
|
||||
|
||||
// See input_el__redisplay(), functionally important
|
||||
CALL_ (input, bind_control, 'q', "ed-redisplay");
|
||||
// This is what buffered el_wgets() does, functionally important;
|
||||
// perhaps it could be bound somewhere more appropriate
|
||||
CALL_ (input, bind_control, 'g', "ed-start-over");
|
||||
|
||||
// We need to hide the prompt and input first
|
||||
CALL_ (input, bind, "\r", "send-line");
|
||||
CALL_ (input, bind, "\n", "send-line");
|
||||
@ -14552,6 +14535,11 @@ app_editline_init (struct input_el *self)
|
||||
|
||||
// Source the user's defaults file
|
||||
el_source (self->editline, NULL);
|
||||
|
||||
// See input_el__redisplay(), functionally important
|
||||
CALL_ (input, bind_control, 'q', "ed-redisplay");
|
||||
// This is what buffered el_wgets() does, functionally important
|
||||
CALL_ (input, bind_control, 'c', "ed-start-over");
|
||||
}
|
||||
|
||||
#endif // HAVE_EDITLINE
|
||||
@ -15178,12 +15166,56 @@ client_message_buffer_name (const struct relay_command_message *m)
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
client_process_buffer_complete (struct client *c, uint32_t seq,
|
||||
struct buffer *buffer, struct relay_command_data_buffer_complete *req)
|
||||
{
|
||||
struct str *line = &req->text;
|
||||
uint32_t end = req->position;
|
||||
if (line->len < end || line->len != strlen (line->str))
|
||||
{
|
||||
relay_prepare_error (c->ctx, seq, "Invalid arguments");
|
||||
goto out;
|
||||
}
|
||||
|
||||
uint32_t start = end;
|
||||
while (start && !strchr (WORD_BREAKING_CHARS, line->str[start - 1]))
|
||||
start--;
|
||||
|
||||
struct strv completions =
|
||||
make_completions (c->ctx, buffer, line->str, start, end);
|
||||
if (completions.len > UINT32_MAX)
|
||||
{
|
||||
relay_prepare_error (c->ctx, seq, "Internal error");
|
||||
goto out_internal;
|
||||
}
|
||||
|
||||
struct relay_event_data_response *e =
|
||||
&relay_prepare (c->ctx)->data.response;
|
||||
e->event = RELAY_EVENT_RESPONSE;
|
||||
e->command_seq = seq;
|
||||
e->data.command = RELAY_COMMAND_BUFFER_COMPLETE;
|
||||
|
||||
struct relay_response_data_buffer_complete *resp =
|
||||
&e->data.buffer_complete;
|
||||
resp->start = start;
|
||||
resp->completions_len = completions.len;
|
||||
resp->completions = xcalloc (completions.len, sizeof *resp->completions);
|
||||
for (size_t i = 0; i < completions.len; i++)
|
||||
resp->completions[i] = str_from_cstr (completions.vector[i]);
|
||||
|
||||
out_internal:
|
||||
strv_free (&completions);
|
||||
out:
|
||||
relay_send (c);
|
||||
}
|
||||
|
||||
static void
|
||||
client_process_buffer_log
|
||||
(struct client *c, uint32_t seq, struct buffer *buffer)
|
||||
{
|
||||
struct relay_event_message *m = relay_prepare (c->ctx);
|
||||
struct relay_event_data_response *e = &m->data.response;
|
||||
struct relay_event_data_response *e =
|
||||
&relay_prepare (c->ctx)->data.response;
|
||||
e->event = RELAY_EVENT_RESPONSE;
|
||||
e->command_seq = seq;
|
||||
e->data.command = RELAY_COMMAND_BUFFER_LOG;
|
||||
@ -15254,9 +15286,8 @@ client_process_message (struct client *c,
|
||||
reset_autoaway (c->ctx);
|
||||
break;
|
||||
case RELAY_COMMAND_BUFFER_COMPLETE:
|
||||
// TODO: Run the completion machinery.
|
||||
relay_prepare_error (c->ctx, m->command_seq, "Not implemented");
|
||||
relay_send (c);
|
||||
client_process_buffer_complete (c, m->command_seq, buffer,
|
||||
&m->data.buffer_complete);
|
||||
break;
|
||||
case RELAY_COMMAND_BUFFER_INPUT:
|
||||
(void) process_input_utf8 (c->ctx,
|
||||
|
@ -86,19 +86,19 @@ body {
|
||||
padding: .1rem .3rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.content span.b {
|
||||
.content .b {
|
||||
font-weight: bold;
|
||||
}
|
||||
.content span.i {
|
||||
.content .i {
|
||||
font-style: italic;
|
||||
}
|
||||
.content span.u {
|
||||
.content .u {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.content span.s {
|
||||
.content .s {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.content span.m {
|
||||
.content .m {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
|
318
xP/public/xP.js
318
xP/public/xP.js
@ -1,5 +1,175 @@
|
||||
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
'use strict'
|
||||
|
||||
// ---- RPC --------------------------------------------------------------------
|
||||
|
||||
class RelayRpc extends EventTarget {
|
||||
constructor(url) {
|
||||
super()
|
||||
this.url = url
|
||||
this.commandSeq = 0
|
||||
}
|
||||
|
||||
connect() {
|
||||
// We can't close the connection immediately, as that queues a task.
|
||||
if (this.ws !== undefined)
|
||||
throw "Already connecting or connected"
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let ws = this.ws = new WebSocket(this.url)
|
||||
ws.onopen = event => {
|
||||
this._initialize()
|
||||
resolve()
|
||||
}
|
||||
// It's going to be code 1006 with no further info.
|
||||
ws.onclose = event => {
|
||||
reject()
|
||||
this.ws = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_initialize() {
|
||||
this.ws.onopen = undefined
|
||||
this.ws.onmessage = event => {
|
||||
this._process(event.data)
|
||||
}
|
||||
this.ws.onerror = event => {
|
||||
this.dispatchEvent(new CustomEvent('error'))
|
||||
}
|
||||
this.ws.onclose = event => {
|
||||
let message = "Connection closed: " +
|
||||
event.code + " (" + event.reason + ")"
|
||||
for (const seq in this.promised)
|
||||
this.promised[seq].reject(message)
|
||||
|
||||
this.ws = undefined
|
||||
this.dispatchEvent(new CustomEvent('close', {
|
||||
detail: {message, code: event.code, reason: event.reason},
|
||||
}))
|
||||
|
||||
// Now connect() can be called again.
|
||||
}
|
||||
|
||||
this.promised = {}
|
||||
}
|
||||
|
||||
_process(data) {
|
||||
console.log(data)
|
||||
|
||||
if (typeof data !== 'string')
|
||||
throw "Binary messages not supported"
|
||||
|
||||
let message = JSON.parse(data)
|
||||
if (typeof message !== 'object')
|
||||
throw "Invalid message"
|
||||
let e = message.data
|
||||
if (typeof e !== 'object')
|
||||
throw "Invalid message"
|
||||
|
||||
switch (e.event) {
|
||||
case 'Error':
|
||||
if (this.promised[e.commandSeq] !== undefined)
|
||||
this.promised[e.commandSeq].reject(e.error)
|
||||
else
|
||||
console.error("Unawaited error")
|
||||
break
|
||||
case 'Response':
|
||||
if (this.promised[e.commandSeq] !== undefined)
|
||||
this.promised[e.commandSeq].resolve(e.data)
|
||||
else
|
||||
console.error("Unawaited response")
|
||||
break
|
||||
default:
|
||||
if (typeof e.event !== 'string')
|
||||
throw "Invalid event tag"
|
||||
|
||||
this.dispatchEvent(new CustomEvent(e.event, {detail: e}))
|
||||
|
||||
// Minor abstraction layering violation.
|
||||
m.redraw()
|
||||
return
|
||||
}
|
||||
|
||||
delete this.promised[e.commandSeq]
|
||||
for (const seq in this.promised) {
|
||||
// We don't particularly care about wraparound issues.
|
||||
if (seq >= e.commandSeq)
|
||||
continue
|
||||
|
||||
this.promised[seq].reject("No response")
|
||||
delete this.promised[seq]
|
||||
}
|
||||
}
|
||||
|
||||
send(params) {
|
||||
if (this.ws === undefined)
|
||||
throw "Not connected"
|
||||
if (typeof params !== 'object')
|
||||
throw "Method parameters must be an object"
|
||||
|
||||
let seq = ++this.commandSeq
|
||||
if (seq >= 1 << 32)
|
||||
seq = this.commandSeq = 0
|
||||
|
||||
this.ws.send(JSON.stringify({commandSeq: seq, data: params}))
|
||||
return new Promise((resolve, reject) => {
|
||||
this.promised[seq] = {resolve, reject}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Event processing -------------------------------------------------------
|
||||
|
||||
// TODO: Probably reset state on disconnect, and indicate to user.
|
||||
let rpc = new RelayRpc(proxy)
|
||||
rpc.connect()
|
||||
.then(result => {
|
||||
rpc.send({command: 'Hello', version: 1})
|
||||
})
|
||||
|
||||
let buffers = new Map()
|
||||
let bufferCurrent = undefined
|
||||
|
||||
rpc.addEventListener('BufferUpdate', event => {
|
||||
let e = event.detail, b = buffers.get(e.bufferName)
|
||||
if (b === undefined) {
|
||||
b = {lines: []}
|
||||
buffers.set(e.bufferName, b)
|
||||
}
|
||||
// TODO: Update any buffer properties.
|
||||
})
|
||||
|
||||
rpc.addEventListener('BufferRename', event => {
|
||||
let e = event.detail
|
||||
buffers.set(e.new, buffers.get(e.bufferName))
|
||||
buffers.delete(e.bufferName)
|
||||
})
|
||||
|
||||
rpc.addEventListener('BufferRemove', event => {
|
||||
let e = event.detail
|
||||
buffers.delete(e.bufferName)
|
||||
})
|
||||
|
||||
rpc.addEventListener('BufferActivate', event => {
|
||||
let e = event.detail
|
||||
bufferCurrent = e.bufferName
|
||||
// TODO: Somehow scroll to the end of it immediately.
|
||||
// TODO: Focus the textarea.
|
||||
})
|
||||
|
||||
rpc.addEventListener('BufferLine', event => {
|
||||
let e = event.detail, b = buffers.get(e.bufferName)
|
||||
if (b !== undefined)
|
||||
b.lines.push({when: e.when, rendition: e.rendition, items: e.items})
|
||||
})
|
||||
|
||||
rpc.addEventListener('BufferClear', event => {
|
||||
let e = event.detail, b = buffers.get(e.bufferName)
|
||||
if (b !== undefined)
|
||||
b.lines.length = 0
|
||||
})
|
||||
|
||||
// --- Colours -----------------------------------------------------------------
|
||||
|
||||
@ -33,69 +203,6 @@ function applyColor(fg, bg, inverse) {
|
||||
return style
|
||||
}
|
||||
|
||||
// ---- Event processing -------------------------------------------------------
|
||||
|
||||
// TODO: Probably reset state on disconnect, and indicate to user.
|
||||
let socket = new WebSocket(proxy)
|
||||
|
||||
let commandSeq = 0
|
||||
function send(command) {
|
||||
socket.send(JSON.stringify({commandSeq: ++commandSeq, data: command}))
|
||||
}
|
||||
|
||||
socket.onopen = function(event) {
|
||||
send({command: 'Hello', version: 1})
|
||||
}
|
||||
|
||||
let buffers = new Map()
|
||||
let bufferCurrent = undefined
|
||||
|
||||
socket.onmessage = function(event) {
|
||||
console.log(event.data)
|
||||
|
||||
let e = JSON.parse(event.data).data
|
||||
switch (e.event) {
|
||||
case 'BufferUpdate':
|
||||
{
|
||||
let b = buffers.get(e.bufferName)
|
||||
if (b === undefined) {
|
||||
b = {lines: []}
|
||||
buffers.set(e.bufferName, b)
|
||||
}
|
||||
// TODO: Update any buffer properties.
|
||||
break
|
||||
}
|
||||
case 'BufferRename':
|
||||
buffers.set(e.new, buffers.get(e.bufferName))
|
||||
buffers.delete(e.bufferName)
|
||||
break
|
||||
case 'BufferRemove':
|
||||
buffers.delete(e.bufferName)
|
||||
break
|
||||
case 'BufferActivate':
|
||||
bufferCurrent = e.bufferName
|
||||
// TODO: Somehow scroll to the end of it immediately.
|
||||
// TODO: Focus the textarea.
|
||||
break
|
||||
case 'BufferLine':
|
||||
{
|
||||
let b = buffers.get(e.bufferName)
|
||||
if (b !== undefined)
|
||||
b.lines.push({when: e.when, rendition: e.rendition, items: e.items})
|
||||
break
|
||||
}
|
||||
case 'BufferClear':
|
||||
{
|
||||
let b = buffers.get(e.bufferName)
|
||||
if (b !== undefined)
|
||||
b.lines.length = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
m.redraw()
|
||||
}
|
||||
|
||||
// ---- UI ---------------------------------------------------------------------
|
||||
|
||||
let BufferList = {
|
||||
@ -103,8 +210,8 @@ let BufferList = {
|
||||
let items = []
|
||||
buffers.forEach((b, name) => {
|
||||
let attrs = {
|
||||
onclick: e => {
|
||||
send({command: 'BufferActivate', bufferName: name})
|
||||
onclick: event => {
|
||||
rpc.send({command: 'BufferActivate', bufferName: name})
|
||||
},
|
||||
}
|
||||
if (name == bufferCurrent)
|
||||
@ -115,6 +222,24 @@ let BufferList = {
|
||||
},
|
||||
}
|
||||
|
||||
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 = {
|
||||
view: vnode => {
|
||||
let line = vnode.children[0]
|
||||
@ -139,11 +264,10 @@ let Content = {
|
||||
line.items.forEach(item => {
|
||||
switch (item.kind) {
|
||||
case 'Text':
|
||||
// TODO: Detect and transform links.
|
||||
content.push(m('span', {
|
||||
linkify(item.text, {
|
||||
class: Array.from(classes.keys()).join(' '),
|
||||
style: applyColor(fg, bg, inverse),
|
||||
}, item.text))
|
||||
}, content)
|
||||
break
|
||||
case 'Reset':
|
||||
classes.clear()
|
||||
@ -179,7 +303,7 @@ let Buffer = {
|
||||
|
||||
let lastDateMark = undefined
|
||||
b.lines.forEach(line => {
|
||||
let date = new Date(line.when * 1000)
|
||||
let date = new Date(line.when)
|
||||
let dateMark = date.toLocaleDateString()
|
||||
if (dateMark !== lastDateMark) {
|
||||
lines.push(m('.date', {}, dateMark))
|
||||
@ -196,26 +320,54 @@ let Buffer = {
|
||||
},
|
||||
}
|
||||
|
||||
function onKeyDown(event) {
|
||||
// TODO: And perhaps on other actions, too.
|
||||
rpc.send({command: 'Active'})
|
||||
|
||||
// TODO: Cancel any current autocomplete.
|
||||
|
||||
let textarea = event.currentTarget
|
||||
switch (event.keyCode) {
|
||||
case 9:
|
||||
if (textarea.selectionStart !== textarea.selectionEnd)
|
||||
return
|
||||
rpc.send({
|
||||
command: 'BufferComplete',
|
||||
bufferName: bufferCurrent,
|
||||
text: textarea.value,
|
||||
position: textarea.selectionEnd,
|
||||
}).then(response => {
|
||||
// TODO: Somehow display remaining options, or cycle through.
|
||||
if (response.completions.length)
|
||||
textarea.setRangeText(response.completions[0],
|
||||
response.start, textarea.selectionEnd, 'end')
|
||||
if (response.completions.length === 1)
|
||||
textarea.setRangeText(' ',
|
||||
textarea.selectionStart, textarea.selectionEnd, 'end')
|
||||
})
|
||||
break;
|
||||
case 13:
|
||||
rpc.send({
|
||||
command: 'BufferInput',
|
||||
bufferName: bufferCurrent,
|
||||
text: textarea.value,
|
||||
})
|
||||
textarea.value = ''
|
||||
break;
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// TODO: This should be remembered across buffer switches,
|
||||
// and we'll probably have to intercept /all/ key presses.
|
||||
let Input = {
|
||||
view: vnode => {
|
||||
return m('textarea', {
|
||||
rows: 1,
|
||||
onkeydown: e => {
|
||||
// TODO: And perhaps on other actions, too.
|
||||
send({command: 'Active'})
|
||||
if (e.keyCode !== 13)
|
||||
return
|
||||
|
||||
send({
|
||||
command: 'BufferInput',
|
||||
bufferName: bufferCurrent,
|
||||
text: e.currentTarget.value,
|
||||
})
|
||||
e.preventDefault()
|
||||
e.currentTarget.value = ''
|
||||
},
|
||||
onkeydown: onKeyDown,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user