diff options
Diffstat (limited to 'surfaces')
| -rw-r--r-- | surfaces/hangul.c | 148 | ||||
| -rw-r--r-- | surfaces/input.c | 1 | ||||
| -rw-r--r-- | surfaces/main.c | 65 | ||||
| -rw-r--r-- | surfaces/vega.h | 19 |
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 |
