aboutsummaryrefslogtreecommitdiffstats
path: root/surfaces
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-27 20:03:30 +0100
committereunhhu <[email protected]>2026-09-27 20:03:30 +0100
commit7b93a66dbffaf2b20b0a1925786f56db54066c9b (patch)
tree000f214ae6743dcc3d6e861fc2a27b49d875ee2d /surfaces
parentc20c8fa13c77d08d093aede126a1ec7646da0905 (diff)
downloadvega-7b93a66dbffaf2b20b0a1925786f56db54066c9b.tar.gz
vega-7b93a66dbffaf2b20b0a1925786f56db54066c9b.zip
Type Hangul on the keyboard (두벌식)
Surfaces that read keys (the display, through evdev) had only a US layout's letters. After the 한/영 key (Lang1, or AltRight standing for it), letter keys now give jamo that compose syllables in the entry line (surfaces/hangul.c): an initial, a vowel and an optional final, joined vowels and finals, a final that moves on to a vowel after it (도깨비불), Shift's doubled consonants, a lone jamo as its compatibility letter. The syllable being composed shows after the entry line; Backspace takes it back a jamo at a time before deleting what was typed; Enter, Tab, the toggle or any key but a letter finishes it. The script language gains hangul, letters HEX and back to drive it. The check types ten cases through it (안녕하세요, 값을, 안자, 와원, 까원, 갑 after Backspace, 한글, ㅏ, 안ㅈ, 안녕 2) and follows the composing line jamo by jamo; the messenger check has Alice type 안녕 on her display with recorded keys, drawn on Bob's.
Diffstat (limited to 'surfaces')
-rw-r--r--surfaces/hangul.c148
-rw-r--r--surfaces/input.c1
-rw-r--r--surfaces/main.c65
-rw-r--r--surfaces/vega.h19
4 files changed, 225 insertions, 8 deletions
diff --git a/surfaces/hangul.c b/surfaces/hangul.c
new file mode 100644
index 0000000..7a6ad42
--- /dev/null
+++ b/surfaces/hangul.c
@@ -0,0 +1,148 @@
+#include "vega.h"
+#include <string.h>
+
+/* Hangul typed on a US keyboard as the standard two-set layout (두벌식): a
+ * letter key gives a jamo, and jamo compose a syllable as they come. A
+ * syllable is an initial consonant (19), a vowel (21) and an optional final
+ * (27), which Unicode composes as U+AC00 + (initial * 21 + vowel) * 28 +
+ * final. Two vowels or two finals may join into one (ㅗ+ㅏ ㅘ, ㄹ+ㄱ ㄺ); a
+ * vowel after a final takes the final, or a joined final's second half, as
+ * its own initial (도깨비불). A jamo alone shows as its compatibility letter. */
+
+/* Each letter key's jamo: an initial's index + 1, or a vowel's index + 1 as
+ * a negative number; lower case first, then with Shift (the doubled
+ * consonants ㄲ ㄸ ㅃ ㅆ ㅉ and the vowels ㅒ ㅖ; other letters as lower case). */
+static const signed char plain[26] = {
+ /* a ㅁ */ 7, /* b ㅠ */ -18, /* c ㅊ */ 15, /* d ㅇ */ 12, /* e ㄷ */ 4, /* f ㄹ */ 6, /* g ㅎ */ 19,
+ /* h ㅗ */ -9, /* i ㅑ */ -3, /* j ㅓ */ -5, /* k ㅏ */ -1, /* l ㅣ */ -21, /* m ㅡ */ -19, /* n ㅜ */ -14,
+ /* o ㅐ */ -2, /* p ㅔ */ -6, /* q ㅂ */ 8, /* r ㄱ */ 1, /* s ㄴ */ 3, /* t ㅅ */ 10, /* u ㅕ */ -7,
+ /* v ㅍ */ 18, /* w ㅈ */ 13, /* x ㅌ */ 17, /* y ㅛ */ -13, /* z ㅋ */ 16,
+};
+static signed char shifted(char upper) {
+ switch (upper) {
+ case 'Q': return 9; /* ㅃ */
+ case 'W': return 14; /* ㅉ */
+ case 'E': return 5; /* ㄸ */
+ case 'R': return 2; /* ㄲ */
+ case 'T': return 11; /* ㅆ */
+ case 'O': return -4; /* ㅒ */
+ case 'P': return -8; /* ㅖ */
+ default: return plain[upper - 'A'];
+ }
+}
+
+/* An initial as a final (0: it cannot end a syllable: ㄸ ㅃ ㅉ), and a
+ * single final as an initial. */
+static const unsigned char final_of[19] = {1, 2, 4, 7, 0, 8, 16, 17, 0, 19, 20, 21, 22, 0, 23, 24, 25, 26, 27};
+static signed char initial_of(unsigned final) {
+ for (int i = 0; i < 19; ++i)
+ if (final_of[i] == final && final) return (signed char)i;
+ return -1;
+}
+
+/* Joined finals: the first final, the initial added, the joined final. */
+static const unsigned char joined_finals[][3] = {
+ {1, 9, 3}, {4, 12, 5}, {4, 18, 6}, {8, 0, 9}, {8, 6, 10}, {8, 7, 11},
+ {8, 9, 12}, {8, 16, 13}, {8, 17, 14}, {8, 18, 15}, {17, 9, 18},
+};
+/* Joined vowels: the first vowel, the vowel added, the joined vowel. */
+static const unsigned char joined_vowels[][3] = {{8, 0, 9}, {8, 1, 10}, {8, 20, 11}, {13, 4, 14}, {13, 5, 15},
+ {13, 20, 16}, {18, 20, 19}};
+
+/* The compatibility letters (U+3131..) of the initials; the vowels' are
+ * U+314F + index. */
+static const uint16_t initial_letters[19] = {0x3131, 0x3132, 0x3134, 0x3137, 0x3138, 0x3139, 0x3141,
+ 0x3142, 0x3143, 0x3145, 0x3146, 0x3147, 0x3148, 0x3149,
+ 0x314A, 0x314B, 0x314C, 0x314D, 0x314E};
+
+void vega_hangul_reset(VegaHangul *h) { *h = (VegaHangul){-1, -1, 0, 0, {{0}}}; }
+
+/* The syllable (or lone jamo) as UTF-8 into out: its bytes (0 when none). */
+static size_t spell(signed char initial, signed char vowel, unsigned char final, char out[4]) {
+ uint32_t c = initial >= 0 && vowel >= 0 ? 0xAC00u + ((unsigned)initial * 21 + (unsigned)vowel) * 28 + final
+ : initial >= 0 ? initial_letters[initial]
+ : vowel >= 0 ? 0x314Fu + (unsigned)vowel
+ : 0;
+ if (!c) return 0;
+ out[0] = (char)(0xE0 | c >> 12);
+ out[1] = (char)(0x80 | (c >> 6 & 0x3F));
+ out[2] = (char)(0x80 | (c & 0x3F));
+ return 3;
+}
+
+size_t vega_hangul_composing(const VegaHangul *h, char out[4]) { return spell(h->initial, h->vowel, h->final, out); }
+
+size_t vega_hangul_finish(VegaHangul *h, char out[4]) {
+ size_t length = vega_hangul_composing(h, out);
+ vega_hangul_reset(h);
+ return length;
+}
+
+/* What it composed so far is kept for Backspace, then it changes. */
+static void step(VegaHangul *h, signed char initial, signed char vowel, unsigned char final) {
+ if (h->steps < sizeof(h->before) / sizeof(*h->before))
+ h->before[h->steps++] = (struct VegaJamo){h->initial, h->vowel, h->final};
+ h->initial = initial, h->vowel = vowel, h->final = final;
+}
+
+/* A new syllable from here: what was composed is finished into done. */
+static size_t begin(VegaHangul *h, signed char initial, signed char vowel, char done[4]) {
+ size_t length = vega_hangul_finish(h, done);
+ step(h, initial, vowel, 0);
+ return length;
+}
+
+bool vega_hangul_type(VegaHangul *h, char key, char done[4], size_t *done_length) {
+ *done_length = 0;
+ signed char jamo = key >= 'a' && key <= 'z' ? plain[key - 'a'] : key >= 'A' && key <= 'Z' ? shifted(key) : 0;
+ if (!jamo) return false;
+ if (jamo > 0) { /* a consonant */
+ signed char consonant = (signed char)(jamo - 1);
+ if (h->vowel < 0 || h->initial < 0) { /* nothing, a lone initial or a lone vowel: this one starts anew */
+ if (h->initial < 0 && h->vowel < 0) step(h, consonant, -1, 0);
+ else *done_length = begin(h, consonant, -1, done);
+ } else if (!h->final && final_of[consonant]) step(h, h->initial, h->vowel, final_of[consonant]);
+ else {
+ unsigned char joined = 0;
+ for (size_t i = 0; i < sizeof(joined_finals) / sizeof(*joined_finals) && !joined; ++i)
+ if (joined_finals[i][0] == h->final && joined_finals[i][1] == (unsigned char)consonant) joined = joined_finals[i][2];
+ if (joined) step(h, h->initial, h->vowel, joined);
+ else *done_length = begin(h, consonant, -1, done);
+ }
+ return true;
+ }
+ signed char vowel = (signed char)(-jamo - 1);
+ if (h->final) { /* 도깨비불: the final (or its second half) moves on to start the next syllable */
+ signed char moved = -1;
+ unsigned char kept = 0;
+ for (size_t i = 0; i < sizeof(joined_finals) / sizeof(*joined_finals); ++i)
+ if (joined_finals[i][2] == h->final) kept = joined_finals[i][0], moved = (signed char)joined_finals[i][1];
+ if (moved < 0) moved = initial_of(h->final);
+ char out[4];
+ *done_length = spell(h->initial, h->vowel, kept, out);
+ memcpy(done, out, *done_length);
+ vega_hangul_reset(h);
+ step(h, moved, -1, 0); /* so Backspace leaves the initial first */
+ step(h, moved, vowel, 0);
+ return true;
+ }
+ if (h->vowel >= 0) {
+ unsigned char joined = 0;
+ for (size_t i = 0; i < sizeof(joined_vowels) / sizeof(*joined_vowels) && !joined; ++i)
+ if (joined_vowels[i][0] == (unsigned char)h->vowel && joined_vowels[i][1] == (unsigned char)vowel)
+ joined = joined_vowels[i][2];
+ if (joined) step(h, h->initial, (signed char)joined, 0);
+ else *done_length = begin(h, -1, vowel, done);
+ return true;
+ }
+ step(h, h->initial, vowel, 0); /* after an initial, or alone */
+ return true;
+}
+
+bool vega_hangul_back(VegaHangul *h) {
+ if (h->initial < 0 && h->vowel < 0) return false;
+ if (!h->steps) { vega_hangul_reset(h); return true; }
+ struct VegaJamo before = h->before[--h->steps];
+ h->initial = before.initial, h->vowel = before.vowel, h->final = before.final;
+ return true;
+}
diff --git a/surfaces/input.c b/surfaces/input.c
index 9403a75..e13693d 100644
--- a/surfaces/input.c
+++ b/surfaces/input.c
@@ -52,6 +52,7 @@ static const struct { unsigned short code; const char *name; char plain, shifted
{KEY_HOME, "Home", 0, 0}, {KEY_UP, "ArrowUp", 0, 0}, {KEY_PAGEUP, "PageUp", 0, 0}, {KEY_LEFT, "ArrowLeft", 0, 0},
{KEY_RIGHT, "ArrowRight", 0, 0}, {KEY_END, "End", 0, 0}, {KEY_DOWN, "ArrowDown", 0, 0},
{KEY_PAGEDOWN, "PageDown", 0, 0}, {KEY_INSERT, "Insert", 0, 0}, {KEY_DELETE, "Delete", 0, 0},
+ {KEY_HANGEUL, "Lang1", 0, 0}, {KEY_RIGHTALT, "AltRight", 0, 0}, /* 한/영, or the right Alt that stands for it */
};
static bool has(int fd, unsigned type, unsigned code) {
diff --git a/surfaces/main.c b/surfaces/main.c
index 010a1bb..b0c32b5 100644
--- a/surfaces/main.c
+++ b/surfaces/main.c
@@ -37,9 +37,12 @@
* written again replaces the running one and states keep their values by
* name: aup watch app.aup -o app.pmod beside it makes a saved edit show at once.
* Script commands, one per line: key KEYNAME | text HEX|- | type HEX | enter |
- * tab | frame [N] | point VIEW ROW | click LINE COLUMN | time MS | tick TIMER | swap MODULE | show. key presses and
+ * tab | hangul | letters HEX | back | frame [N] | point VIEW ROW | click LINE COLUMN | time MS | tick TIMER |
+ * swap MODULE | show. key presses and
* releases the input bound to that key; text commits text to the screen's text
- * input; type, enter and tab act on the TUI's input line; point focuses a row
+ * input; type, enter and tab act on the TUI's input line, and so do hangul (the
+ * 한/영 key), letters (keys typed, each byte one, through Hangul when it is on)
+ * and back (Backspace); point focuses a row
* (0 first) of a list or table; click is a mouse click on the last shown TUI
* frame (1 first, as terminals report it); time sets the time the next
* events see (live surfaces use the clock) and tick ticks a timer (live
@@ -61,6 +64,10 @@ static VegaGrid *drawn; /* the TUI's last frame, which a click lands on */
static char entry[VEGA_TEXT];
static size_t entry_length;
static bool typing;
+/* Hangul typed on the keyboard (Lang1, the 한/영 key, or AltRight switches):
+ * the syllable being composed shows after the entry line until it is done. */
+static bool korean;
+static VegaHangul hangul = {-1, -1, 0, 0, {{0, 0, 0}}};
/* The module file last read, and a digest of its bytes. A loaded module
* keeps nothing of them, so one buffer, grown to the largest, serves every load. */
@@ -357,15 +364,38 @@ static bool raw_mode(void) {
/* Whether the app takes Tab itself; otherwise Tab switches typing and keys. */
static bool tab_switches(void) { return screen.prompt >= 0 && key_input("Tab") < 0; }
+static void append(const char *bytes, size_t length) {
+ if (entry_length + length < sizeof(entry)) { memcpy(entry + entry_length, bytes, length); entry_length += length; }
+}
+
+/* The syllable being composed, done: into the entry line. */
+static void finish_hangul(void) {
+ char done[4];
+ append(done, vega_hangul_finish(&hangul, done));
+}
+
+/* The entry line as shown: what was typed, then the syllable being composed. */
+static size_t shown_entry(char out[VEGA_TEXT + 4]) {
+ memcpy(out, entry, entry_length);
+ return entry_length + vega_hangul_composing(&hangul, out + entry_length);
+}
+
/* One key from the terminal, as typed text or as a Pure key name. */
static void handle_key(const char *name) {
int input;
- if (!strcmp(name, "Tab") && tab_switches()) { typing = !typing; return; }
+ if (!strcmp(name, "Lang1") || !strcmp(name, "AltRight")) { /* 한/영 */
+ finish_hangul();
+ korean = !korean;
+ return;
+ }
+ if (!strcmp(name, "Tab") && tab_switches()) { finish_hangul(); typing = !typing; return; }
if (typing && !strcmp(name, "Enter")) {
+ finish_hangul();
if (entry_length && commit(entry, entry_length)) entry_length = 0;
return;
}
if (typing && !strcmp(name, "Backspace")) {
+ if (vega_hangul_back(&hangul)) return; /* a jamo of the syllable being composed */
while (entry_length && ((unsigned char)entry[entry_length - 1] & 0xC0) == 0x80) --entry_length;
if (entry_length) --entry_length;
return;
@@ -375,7 +405,14 @@ static void handle_key(const char *name) {
static void handle_text(const char *bytes, size_t length) {
if (typing) {
- if (entry_length + length < sizeof(entry)) { memcpy(entry + entry_length, bytes, length); entry_length += length; }
+ char done[4];
+ size_t finished;
+ if (korean && length == 1 && vega_hangul_type(&hangul, bytes[0], done, &finished)) {
+ append(done, finished);
+ return;
+ }
+ finish_hangul(); /* anything but a letter ends the syllable */
+ append(bytes, length);
return;
}
if (length != 1) return;
@@ -457,7 +494,8 @@ static int interactive(void) {
/* Tick what is due, draw, then wait for input, a frame or the next tick. */
bool ticked = false;
long due = vega_timers_run(&timers, &app, vega_now(), &ticked);
- vega_draw(&grid, &app, &screen, entry, entry_length, typing);
+ char line[VEGA_TEXT + 4];
+ vega_draw(&grid, &app, &screen, line, shown_entry(line), typing);
paint_terminal(&grid);
struct pollfd wait = {STDIN_FILENO, POLLIN, 0};
int timeout = screen.ticks ? 16 : watched ? 200 : -1;
@@ -597,7 +635,8 @@ static int live(VegaInput *input, unsigned columns, unsigned rows, bool recorded
bool ticked = false;
long due = node ? -1 : vega_timers_run(&timers, &app, vega_now(), &ticked);
PureSnapshot *snapshot = node ? node->read(node->host, &app) : NULL;
- if (!node || snapshot) vega_draw(&grid, &app, &screen, entry, entry_length, typing);
+ char line[VEGA_TEXT + 4];
+ if (!node || snapshot) vega_draw(&grid, &app, &screen, line, shown_entry(line), typing);
if (snapshot) node->release(node->host, snapshot);
if (!display) { /* a node's pixels: each frame into a file */
if (!write_frame(&grid, path)) { fprintf(stderr, "vega: cannot write %s\n", path); status = 1; break; }
@@ -688,6 +727,14 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin
} else if (!strcmp(command, "tab")) {
handle_key("Tab");
acted = false;
+ } else if (!strcmp(command, "hangul") || !strcmp(command, "back")) {
+ handle_key(!strcmp(command, "hangul") ? "Lang1" : "Backspace");
+ acted = false;
+ } else if (!strcmp(command, "letters")) {
+ char keys[VEGA_TEXT];
+ size_t count = unhex(argument, keys, sizeof(keys));
+ for (size_t i = 0; i < count; ++i) handle_text(keys + i, 1);
+ acted = false;
} else if (!strcmp(command, "point")) {
char name[PURE_MODULE_NAME];
long row = -1;
@@ -728,14 +775,16 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin
else if (surface == SURFACE_SAY) vega_say(stdout, &app, &screen, true);
else if ((shown = true) && painting) {
Out out = {0};
- vega_draw(&grid, &app, &screen, entry, entry_length, typing);
+ char prompt[VEGA_TEXT + 4];
+ vega_draw(&grid, &app, &screen, prompt, shown_entry(prompt), typing);
paint(&grid, &out);
printf("paint %zu\n", out.used);
fwrite(out.bytes, 1, out.used, stdout);
putchar('\n');
free(out.bytes);
} else {
- vega_draw(&grid, &app, &screen, entry, entry_length, typing);
+ char prompt[VEGA_TEXT + 4];
+ vega_draw(&grid, &app, &screen, prompt, shown_entry(prompt), typing);
dump(&grid);
printf("cells %lu\n", vega_evaluations() - counted);
char path[512];
diff --git a/surfaces/vega.h b/surfaces/vega.h
index fd8780b..cd6b1ee 100644
--- a/surfaces/vega.h
+++ b/surfaces/vega.h
@@ -153,6 +153,25 @@ bool vega_display_show(VegaDisplay *);
const uint32_t *vega_display_captured(const VegaDisplay *, size_t *stride);
void vega_display_close(VegaDisplay *);
+/* Hangul typed on a US keyboard as the two-set layout (두벌식, hangul.c):
+ * letter keys give jamo that compose syllables as they come. type takes a
+ * letter (A-Z with Shift) and gives what it finished (UTF-8, at most one
+ * syllable) into done; false for a key that is not a letter. composing is
+ * the syllable being composed (0 bytes: none); back takes back its last jamo
+ * (false: none was being composed); finish ends it into out. */
+struct VegaJamo { signed char initial, vowel; unsigned char final; };
+typedef struct {
+ signed char initial, vowel; /* -1: none */
+ unsigned char final; /* 0: none */
+ unsigned char steps; /* what it was before each jamo, for Backspace */
+ struct VegaJamo before[8];
+} VegaHangul;
+void vega_hangul_reset(VegaHangul *);
+bool vega_hangul_type(VegaHangul *, char key, char done[4], size_t *done_length);
+size_t vega_hangul_composing(const VegaHangul *, char out[4]);
+bool vega_hangul_back(VegaHangul *);
+size_t vega_hangul_finish(VegaHangul *, char out[4]);
+
/* Input from evdev: keyboards (key names and the text they type on a US
* layout), mice and touch screens (presses at a pixel of a width x height
* screen). With count paths, recorded events are read from those files