diff --git a/Makefile b/Makefile index 6335ead..2cbe9a0 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \ panic.o uaccess.o qr.o time.o ksym.o klog.o sbuf.o) KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \ - bio.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o mouse.o \ + bio.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o keymap.o mouse.o \ pci.o pciuser.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o \ dbg.o netuser.o) @@ -108,7 +108,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS)) # Third-party software does NOT go here — see ports/. # ----------------------------------------------------------------------- ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o) -CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd +CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS)) # ----------------------------------------------------------------------- diff --git a/cmd/loadkeys/main.c b/cmd/loadkeys/main.c new file mode 100644 index 0000000..082e2dc --- /dev/null +++ b/cmd/loadkeys/main.c @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// loadkeys -- choose the keyboard layout. +// +// loadkeys print the active layout +// loadkeys -l list the layouts the kernel has +// loadkeys pl switch to Polish (programmers) +// loadkeys -c apply KEYMAP from /etc/vconsole.conf +// +// Named after the Linux tool because that is what anyone would type, but it is +// not the same program and takes no keymap files. Linux's loadkeys compiles a +// text keymap and uploads it a key at a time through KDSKBENT, which means the +// data lives in userspace and a machine whose /usr is not mounted has no +// layout at all. The layouts here are in the kernel and this only names one. +// +// The setting is machine-wide, not per-terminal: there is one keyboard, and +// switching to tty3 does not move anybody's fingers. + +#include "types.h" +#include "user.h" +#include "fcntl.h" +#include "posix.h" + +// No printf, memset or strcpy in lib/; this needs a little of each. +static void +puts_(const char *s) +{ + int n = 0; + while (s[n]) n++; + write(1, (void *)s, n); +} + +static void +zero(void *p, int n) +{ + char *c = p; + while (n-- > 0) *c++ = 0; +} + +static void +copy_bounded(char *dst, const char *src, int cap) +{ + int i = 0; + while (i < cap - 1 && src[i]) { dst[i] = src[i]; i++; } + dst[i] = 0; +} + +// /dev/console rather than fd 0: the caller may have redirected its input, +// and the layout being set belongs to the machine's keyboard, not to whichever +// terminal this process happens to be reading from. +static int +console(void) +{ + int fd = open("/dev/console", O_RDWR); + if (fd < 0) { + puts_("loadkeys: no console to ask\n"); + exit(); + } + return fd; +} + +#define VCONSOLE "/etc/vconsole.conf" + +// Pull KEYMAP= out of /etc/vconsole.conf. +// +// Deliberately not a config parser. One key, whole file in one buffer, first +// match wins -- and a missing or unreadable file is not an error, because the +// kernel's default is already a working keyboard and refusing to boot over a +// preference would be absurd. +static int +keymap_from_file(char *out, int cap) +{ + char buf[512]; + int fd, n, i, j; + + fd = open(VCONSOLE, O_RDONLY); + if (fd < 0) + return -1; + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n <= 0) + return -1; + buf[n] = 0; + + for (i = 0; i < n; i++) { + if (buf[i] == '#') { // skip to end of comment + while (i < n && buf[i] != '\n') i++; + continue; + } + if (buf[i] != 'K') + continue; + const char *k = "KEYMAP="; + for (j = 0; k[j] && buf[i + j] == k[j]; j++) + ; + if (k[j]) + continue; + i += j; + for (j = 0; j < cap - 1 && buf[i + j] && + buf[i + j] != '\n' && buf[i + j] != ' '; j++) + out[j] = buf[i + j]; + out[j] = 0; + return j > 0 ? 0 : -1; + } + return -1; +} + +int +main(int argc, char *argv[]) +{ + struct kb_layout k; + char want[16]; + int fd, i; + + if (argc > 2 || (argc == 2 && argv[1][0] == '-' && + argv[1][1] != 'l' && argv[1][1] != 'c')) { + puts_("usage: loadkeys [-l | -c | ]\n" + " -l list the available layouts\n" + " -c apply KEYMAP from " VCONSOLE "\n" + " switch to it; with no argument, print the active one\n"); + return 1; + } + + fd = console(); + + if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'c') { + if (keymap_from_file(want, sizeof(want)) < 0) { + close(fd); + return 0; // no preference recorded; not a failure + } + zero(&k, sizeof(k)); + copy_bounded(k.name, want, sizeof(k.name)); + if (ioctl(fd, KB_SETLAYOUT, &k) < 0) { + puts_("loadkeys: " VCONSOLE " asks for '"); + puts_(want); + puts_("', which this kernel does not have\n"); + close(fd); + return 1; + } + close(fd); + return 0; + } + + if (argc == 2 && argv[1][0] == '-') { // -l + for (i = 0; ; i++) { + zero(&k, sizeof(k)); + k.index = i; + if (ioctl(fd, KB_LISTLAYOUT, &k) < 0) + break; // past the end + puts_(k.name); + puts_("\t"); + puts_(k.desc); + puts_("\n"); + } + close(fd); + return 0; + } + + if (argc == 2) { // set + zero(&k, sizeof(k)); + copy_bounded(k.name, argv[1], sizeof(k.name)); + if (ioctl(fd, KB_SETLAYOUT, &k) < 0) { + puts_("loadkeys: no layout called '"); + puts_(argv[1]); + puts_("' -- try loadkeys -l\n"); + close(fd); + return 1; + } + close(fd); + return 0; + } + + zero(&k, sizeof(k)); // report + if (ioctl(fd, KB_GETLAYOUT, &k) < 0) { + puts_("loadkeys: the kernel has no layout to report\n"); + close(fd); + return 1; + } + puts_(k.name); + puts_("\t"); + puts_(k.desc); + puts_("\n"); + close(fd); + return 0; +} diff --git a/docs/architecture/terminals.md b/docs/architecture/terminals.md index 1500a54..9edfb09 100644 --- a/docs/architecture/terminals.md +++ b/docs/architecture/terminals.md @@ -109,17 +109,74 @@ Linux's numbers, because they do Linux's job. | `VT_GETSTATE` | active terminal and a bitmask of those in use | | `VT_OPENQRY` | the first terminal nobody has open | | `VT_GETMODE`/`VT_SETMODE` | accepted and ignored — the kernel drives switching and there is no process mode to set | +| `VT_GETCHORD`/`VT_SETCHORD` | SchOS's own: which key combination switches. See below | +| `KB_GETLAYOUT`/`KB_SETLAYOUT`/`KB_LISTLAYOUT` | SchOS's own: the keyboard layout, by name | ## The keyboard -`kbd.c` recognises Ctrl+Alt+Shift+F1..F6 and returns `KEY_VT_SWITCH + n`, which is above -0xFF on purpose: every value that fits in a byte is already either a character -or a UTF-8 continuation byte, and a chord that collided with one would be typed -into the terminal instead of switching away from it. +`kbd.c` turns scancodes into characters and chords. `keymap.c` holds the +layouts. Between them is one rule, and it is the rule the whole keycode space +is built around: + +> **A key produces either a Unicode codepoint or a value above `0x10FFFF`. +> Nothing in between means anything.** + +The chord codes always obeyed it. The navigation keys did not: `KEY_UP` was +`0xE2`, and `0xE0..0xE9` are exactly the UTF-8 lead bytes for three-byte +characters. Typing any of U+0800..U+FFFF — most punctuation, every arrow, every +maths symbol, the whole of CJK — produced a value the console read as an arrow +key and turned into `ESC [ A`. `∞`, `→`, `≤` and `'` vanished from the input +line while `ą` and `ę` came through, because those are two-byte sequences whose +lead byte is `0xC4` and nothing claimed `0xC4`. They are `KBD_*` now and sit +above every codepoint there is. What the chord *means* is decided in `console.c`, where the terminals are. The keyboard driver names it and stops. +### Layouts + +Three, in the kernel: `us`, `pl` (Polish programmers), `de` (QWERTZ). `loadkeys` +lists and switches them; `/etc/vconsole.conf` records the choice and the +`os.schos.keymap` job applies it at boot. + +**In the kernel, not in files.** Linux's `loadkeys` compiles a text keymap and +uploads it a key at a time through `KDSKBENT`, which puts the data in userspace +— and a machine whose `/usr` has not mounted then has no layout at all. Here +the kernel always has a working keyboard and userspace only names which one. + +A layout is four tables of codepoints — plain, Shift, AltGr, Shift+AltGr — +indexed by scancode. Two things are deliberately *not* tables: + +- **Ctrl is derived from the plain codepoint**, not looked up. The table it + replaced was indexed by scancode, which quietly assumed US: on QWERTZ the Z + and Y keys swap, so Ctrl+Z would have suspended a job only on some layouts. +- **Caps Lock is a case toggle on the result**, so `ą` from the AltGr level + becomes `Ą` — which no second table could have said, because Caps Lock + combines with AltGr rather than replacing it. + +Codepoints and not bytes is the entire point: `ó` fits in Latin-1 and `ą` does +not, and half a Polish alphabet is not a Polish layout. `kbdgetc()` encodes to +UTF-8 and hands the bytes to `consoleintr()` one call at a time, because that +function's contract is bytes — the serial line delivers UTF-8 already and +nothing could tell an encoded codepoint from a byte of the same value. + +**No dead keys.** On a real German layout the acute and circumflex keys wait +for the next key and combine; here they type the accent alone. It needs +per-keyboard pending state that this interface has no room for, so layouts that +depend on it — French AZERTY, Spanish — are not shipped rather than shipped +wrong. + +### Locale + +`/etc/locale.conf`, read by `/etc/profile` and `/etc/zsh/zshenv`, default +`LANG=C.UTF-8`. + +**Only the charset half of it is real.** musl decodes and encodes UTF-8 +whatever the locale says and ships no locale data, so collation stays codepoint +order and messages stay English whichever language is named. What `LANG` does +is tell the programs that ask that the terminal is UTF-8 — an unset `LANG` +reads as "ASCII only", and that is what makes a program fall back to `?`. + ### The chord is a setting, and that is the point **There is no combination that is free for everyone on every host**, and a @@ -273,13 +330,15 @@ Two bugs, both in the same half-second of using a shell, and both worth recording because the symptom pointed away from the cause. **Arrow keys corrupted the next two keystrokes.** They arrive from the keyboard -as single keycodes -- `KEY_UP` is `0xE2` -- and were pushed into the input -buffer exactly like that. No terminal has ever worked that way, and `0xE2` is -the worst byte to have picked: it is a UTF-8 lead byte for a three-byte -character, so a reader decoding UTF-8 swallowed the *next two keys* waiting for -continuations that never came. `consoleintr()` translates them to the DEC/xterm -sequences now -- `ESC[A`, `ESC[D`, `ESC[5~` and the rest -- which is what every -terminfo entry and every line editor already expects. +as single keycodes and were pushed into the input buffer exactly like that. No +terminal has ever worked that way. `consoleintr()` translates them to the +DEC/xterm sequences now -- `ESC[A`, `ESC[D`, `ESC[5~` and the rest -- which is +what every terminfo entry and every line editor already expects. + +The *value* those keycodes had was a second bug wearing the same coat, and it +outlived the fix: `KEY_UP` was `0xE2`, a UTF-8 lead byte, so a character typed +from the other direction was eaten just as thoroughly. See the keycode rule +above. **Then the end of the line vanished as you typed into it.** With the arrows working, editing mid-line left the buffer perfectly correct and the screen @@ -331,7 +390,9 @@ waiting for a different editor. advance. Combining marks land on top of their own cell rather than the preceding one, and CJK draws at half the width it wants. Both need a width table and a cell that can span two columns. -- **Typed input above U+07FF is unreliable.** Two-byte UTF-8 arrives intact; - three-byte sequences typed at the shell come through mangled. The console's - own decoder is not the problem — the same characters `cat`ed from a file - render correctly — so this belongs with keyboard and locale handling. +- **No dead keys**, so French and Spanish layouts are absent rather than + broken. See above. +- **No per-terminal layout.** One keyboard, one layout. Switching to tty3 does + not move anybody's fingers. +- **Only three layouts.** Adding one is a table in `keymap.c` and a line in the + catalogue; the mechanism is not the limit, the typing is. diff --git a/etc/locale.conf b/etc/locale.conf new file mode 100644 index 0000000..895068a --- /dev/null +++ b/etc/locale.conf @@ -0,0 +1,17 @@ +# /etc/locale.conf — the system locale. +# +# Same name and same format as systemd's, because it is the file anyone would +# look for and there is nothing to gain by inventing a third convention. +# +# **Only the charset half of this is real.** SchOS's libc is musl, which +# decodes and encodes UTF-8 whatever the locale says and ships no locale data +# at all -- so collation is still codepoint order, and messages are still +# English, whichever language is named below. What LANG does here is tell the +# programs that ask that the terminal is UTF-8, which is what stops them +# falling back to ASCII-only output. +# +# C.UTF-8 says exactly that and claims nothing else. A regional value +# (pl_PL.UTF-8, de_DE.UTF-8) is accepted and behaves identically today; set it +# if you want the intent recorded. + +LANG=C.UTF-8 diff --git a/etc/profile b/etc/profile index 02c0ad5..dc6e8e6 100644 --- a/etc/profile +++ b/etc/profile @@ -4,6 +4,15 @@ export PATH=/bin:/sbin:/usr/bin export HOME=/ export TERM=vt100 +# The locale, from /etc/locale.conf if it says anything. Only the charset half +# of it means much here -- see that file -- but a program that has to guess +# whether the terminal is UTF-8 guesses ASCII, so saying so matters. +if [ -f /etc/locale.conf ]; then + . /etc/locale.conf +fi +: ${LANG:=C.UTF-8} +export LANG + if [ -f /etc/kshrc ]; then . /etc/kshrc fi diff --git a/etc/schinit/keymap.job b/etc/schinit/keymap.job new file mode 100644 index 0000000..b178ed3 --- /dev/null +++ b/etc/schinit/keymap.job @@ -0,0 +1,19 @@ +# Apply the console keyboard layout named in /etc/vconsole.conf. +# +# RunAtLoad with no KeepAlive: it reads one line, sets one thing and exits. A +# job that has finished is not a job that failed. +# +# Ordering does not matter. The kernel starts on the built-in default, so +# there is no window in which the keyboard does not work -- this only changes +# which keys produce which characters, and it lands long before anyone can +# type. If the file is missing or names a layout the kernel does not have, +# loadkeys says so and the default stands. + +Label = os.schos.keymap +Program = /bin/loadkeys +Args = loadkeys -c +RunAtLoad = yes +KeepAlive = no + +StandardOut = /dev/tty1 +StandardError = /dev/tty1 diff --git a/etc/vconsole.conf b/etc/vconsole.conf new file mode 100644 index 0000000..070212e --- /dev/null +++ b/etc/vconsole.conf @@ -0,0 +1,14 @@ +# /etc/vconsole.conf — the console keyboard layout. +# +# Applied at boot by the os.schos.keymap job, and by `loadkeys -c` at any time +# after. `loadkeys -l` lists what the kernel has; `loadkeys ` switches +# for this session without touching this file. +# +# us English (US), QWERTY +# pl Polish (programmers) -- US QWERTY with the diacritics on AltGr +# de German, QWERTZ +# +# The layouts are in the kernel rather than in files here, so a machine with +# nothing mounted still has a working keyboard. This file only names one. + +KEYMAP=us diff --git a/etc/zsh/zshenv b/etc/zsh/zshenv index 1d30f5e..0baebcd 100644 --- a/etc/zsh/zshenv +++ b/etc/zsh/zshenv @@ -7,7 +7,17 @@ : ${TERM:=linux} : ${HOME:=/root} : ${PATH:=/bin:/usr/bin} -export TERM HOME PATH + +# UTF-8, said out loud. zsh's line editor handles multibyte input either way -- +# musl decodes UTF-8 whatever the locale claims -- but everything that branches +# on $LANG to decide whether it may print a non-ASCII character needs this set, +# and an unset LANG reads as "ASCII only". +if [ -f /etc/locale.conf ]; then + . /etc/locale.conf +fi +: ${LANG:=C.UTF-8} + +export TERM HOME PATH LANG # Where zsh's own functions live. Set here rather than compiled in so a second # version can be installed beside this one without either moving. diff --git a/include/kbd.h b/include/kbd.h index 915afab..b352262 100644 --- a/include/kbd.h +++ b/include/kbd.h @@ -1,4 +1,12 @@ // PC keyboard interface constants +// +// The scancode tables themselves are no longer here. They moved to +// kernel/dev/keymap.c when there became more than one of them -- a header +// full of `static` arrays gives every file that includes it its own private +// copy, which was harmless while exactly one file did. + +#ifndef KBD_H +#define KBD_H #define KBSTATP 0x64 // kbd controller status port(I) #define KBS_DIB 0x01 // kbd data in buffer @@ -24,25 +32,36 @@ // Nothing the kernel binds may involve it. #define RALT (1<<7) -// Special keycodes -#define KEY_HOME 0xE0 -#define KEY_END 0xE1 -#define KEY_UP 0xE2 -#define KEY_DN 0xE3 -#define KEY_LF 0xE4 -#define KEY_RT 0xE5 -#define KEY_PGUP 0xE6 -#define KEY_PGDN 0xE7 -#define KEY_INS 0xE8 -#define KEY_DEL 0xE9 +// Special keycodes. +// +// **Above every Unicode codepoint, deliberately**, and they did not use to be: +// KBD_UP was 0xE2 and its neighbours filled 0xE0..0xE9. Those are exactly the +// UTF-8 lead bytes for three-byte characters, so typing any of U+0800..U+FFFF +// -- which is most punctuation, every arrow, every maths symbol, and the whole +// of CJK -- produced a keycode the console read as an arrow key and turned +// into `ESC [ A`. `∞`, `→`, `≤` and `'` all vanished from the input line while +// `ą` and `ę` came through, because those are two-byte sequences whose lead +// byte is 0xC4 and nothing claimed 0xC4. +// +// The chord codes below already knew this rule and said so; the navigation +// keys predate them and were never brought in line. The rule, stated once: a +// value that a key produces is either a Unicode codepoint or it is above +// 0x10FFFF, and nothing in between means anything. +#define KBD_SPECIAL 0x110000 + +#define KBD_HOME (KBD_SPECIAL + 0x00) +#define KBD_END (KBD_SPECIAL + 0x01) +#define KBD_UP (KBD_SPECIAL + 0x02) +#define KBD_DN (KBD_SPECIAL + 0x03) +#define KBD_LF (KBD_SPECIAL + 0x04) +#define KBD_RT (KBD_SPECIAL + 0x05) +#define KBD_PGUP (KBD_SPECIAL + 0x06) +#define KBD_PGDN (KBD_SPECIAL + 0x07) +#define KBD_INS (KBD_SPECIAL + 0x08) +#define KBD_DEL (KBD_SPECIAL + 0x09) // Switch virtual terminal. // -// Above 0xFF deliberately. kbdgetc returns an int and consoleintr takes one, -// and every value that fits in a byte is already either a character or a -// UTF-8 continuation byte. A chord that collided with one would be typed into -// the terminal instead of switching away from it. -// // *Which* chord is a runtime setting, not a constant here, and that is the // interesting part. Three separate parties have a claim on every candidate: // @@ -76,13 +95,13 @@ // and no application asks for it. Right Alt being a separate modifier bit // here is what makes the distinction expressible at all -- it used to share // ALT, which is harmless for typing and useless for this. -#define KEY_VT_SWITCH 0x120 +#define KBD_VT_SWITCH (KBD_SPECIAL + 0x20) // Previous / next terminal, on the same modifiers as the chord above. The // minus and equals keys, which sit together and read as - and + -- a relative // move for when you have two terminals and do not care which number they are. -#define KEY_VT_PREV 0x130 -#define KEY_VT_NEXT 0x131 +#define KBD_VT_PREV (KBD_SPECIAL + 0x30) +#define KBD_VT_NEXT (KBD_SPECIAL + 0x31) #define SCAN_MINUS 0x0C // - / _ #define SCAN_EQUALS 0x0D // = / + @@ -93,82 +112,4 @@ // C('A') == Control-A #define C(x) (x - '@') -static uchar shiftcode[256] = -{ - [0x1D] CTL, - [0x2A] SHIFT, - [0x36] SHIFT, - [0x38] ALT, - [0x9D] CTL, - [0xB8] RALT // right Alt / AltGr -- see RALT above -}; - -static uchar togglecode[256] = -{ - [0x3A] CAPSLOCK, - [0x45] NUMLOCK, - [0x46] SCROLLLOCK -}; - -static uchar normalmap[256] = -{ - NO, 0x1B, '1', '2', '3', '4', '5', '6', // 0x00 - '7', '8', '9', '0', '-', '=', '\b', '\t', - 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', // 0x10 - 'o', 'p', '[', ']', '\n', NO, 'a', 's', - 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', // 0x20 - '\'', '`', NO, '\\', 'z', 'x', 'c', 'v', - 'b', 'n', 'm', ',', '.', '/', NO, '*', // 0x30 - NO, ' ', NO, NO, NO, NO, NO, NO, - NO, NO, NO, NO, NO, NO, NO, '7', // 0x40 - '8', '9', '-', '4', '5', '6', '+', '1', - '2', '3', '0', '.', NO, NO, NO, NO, // 0x50 - [0x9C] '\n', // KP_Enter - [0xB5] '/', // KP_Div - [0xC8] KEY_UP, [0xD0] KEY_DN, - [0xC9] KEY_PGUP, [0xD1] KEY_PGDN, - [0xCB] KEY_LF, [0xCD] KEY_RT, - [0x97] KEY_HOME, [0xCF] KEY_END, - [0xD2] KEY_INS, [0xD3] KEY_DEL -}; - -static uchar shiftmap[256] = -{ - NO, 033, '!', '@', '#', '$', '%', '^', // 0x00 - '&', '*', '(', ')', '_', '+', '\b', '\t', - 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', // 0x10 - 'O', 'P', '{', '}', '\n', NO, 'A', 'S', - 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', // 0x20 - '"', '~', NO, '|', 'Z', 'X', 'C', 'V', - 'B', 'N', 'M', '<', '>', '?', NO, '*', // 0x30 - NO, ' ', NO, NO, NO, NO, NO, NO, - NO, NO, NO, NO, NO, NO, NO, '7', // 0x40 - '8', '9', '-', '4', '5', '6', '+', '1', - '2', '3', '0', '.', NO, NO, NO, NO, // 0x50 - [0x9C] '\n', // KP_Enter - [0xB5] '/', // KP_Div - [0xC8] KEY_UP, [0xD0] KEY_DN, - [0xC9] KEY_PGUP, [0xD1] KEY_PGDN, - [0xCB] KEY_LF, [0xCD] KEY_RT, - [0x97] KEY_HOME, [0xCF] KEY_END, - [0xD2] KEY_INS, [0xD3] KEY_DEL -}; - -static uchar ctlmap[256] = -{ - NO, NO, NO, NO, NO, NO, NO, NO, - NO, NO, NO, NO, NO, NO, NO, NO, - C('Q'), C('W'), C('E'), C('R'), C('T'), C('Y'), C('U'), C('I'), - C('O'), C('P'), NO, NO, '\r', NO, C('A'), C('S'), - C('D'), C('F'), C('G'), C('H'), C('J'), C('K'), C('L'), NO, - NO, NO, NO, C('\\'), C('Z'), C('X'), C('C'), C('V'), - C('B'), C('N'), C('M'), NO, NO, C('/'), NO, NO, - [0x9C] '\r', // KP_Enter - [0xB5] C('/'), // KP_Div - [0xC8] KEY_UP, [0xD0] KEY_DN, - [0xC9] KEY_PGUP, [0xD1] KEY_PGDN, - [0xCB] KEY_LF, [0xCD] KEY_RT, - [0x97] KEY_HOME, [0xCF] KEY_END, - [0xD2] KEY_INS, [0xD3] KEY_DEL -}; - +#endif /* KBD_H */ diff --git a/include/keymap.h b/include/keymap.h new file mode 100644 index 0000000..c500eb1 --- /dev/null +++ b/include/keymap.h @@ -0,0 +1,50 @@ +// Keyboard layouts. +// +// A layout answers one question: given a scancode and the modifiers held, +// which Unicode codepoint was typed? Everything else about the keyboard -- +// which port to read, when a key is a release, what the terminal-switch chord +// is -- belongs to kernel/dev/kbd.c and is the same whatever the layout. +// +// Codepoints, not bytes. A layout that can only produce bytes can produce +// `ó` (U+00F3, which fits in Latin-1) but not `ą` (U+0105, which does not), +// and half a Polish alphabet is not a Polish layout. kbd.c encodes the +// codepoint to UTF-8 on the way to the console. +// +// **No dead keys.** On a German layout the acute and circumflex keys are dead +// -- pressed, they wait for the next key and combine -- and here they simply +// produce the accent character on its own. That is wrong, it is visibly +// wrong, and it needs somewhere to hold "a dead key is pending", which is +// per-keyboard state this interface has no room for. Layouts that need it are +// not shipped rather than shipped broken; see keymap_layouts[] for which. + +#ifndef KEYMAP_H +#define KEYMAP_H + +#include "types.h" + +// Which of a layout's four tables a set of modifiers selects. Plain, Shift, +// AltGr, and Shift+AltGr -- Ctrl is not one of them, because a control +// character is derived from the plain codepoint rather than looked up, which +// is what makes Ctrl+Z still be Ctrl+Z on a layout that moved the Z key. +#define KM_PLAIN 0 +#define KM_SHIFT 1 +#define KM_ALTGR 2 +#define KM_SHIFTALTGR 3 +#define KM_LEVELS 4 + +struct keylayout { + const char *name; // "us", "pl", "de" -- what you type + const char *desc; // what it is, for `loadkeys -l` + const uint32 *level[KM_LEVELS]; // 256 entries each; NULL falls back +}; + +// The active layout, and the catalogue. +const struct keylayout *keymap_active(void); +const struct keylayout *keymap_by_index(int i); // NULL past the end +int keymap_set(const char *name); // 0, or -1 if unknown + +// Scancode plus modifier bits (SHIFT/CTL/ALT/RALT/CAPSLOCK from kbd.h) to a +// Unicode codepoint, or one of the KBD_* specials, or 0 for "no character". +uint32 keymap_lookup(uint scancode, uint mods); + +#endif /* KEYMAP_H */ diff --git a/include/posix.h b/include/posix.h index 600f60f..fe4a4b8 100644 --- a/include/posix.h +++ b/include/posix.h @@ -163,6 +163,22 @@ struct rlimit { #define VT_GETCHORD 0x5620 #define VT_SETCHORD 0x5621 +// KB_GETLAYOUT / KB_SETLAYOUT / KB_LISTLAYOUT -- which keyboard layout the +// console uses. SchOS's own, and deliberately not Linux's KDGKBENT: that +// interface hands over one key of one table at a time and expects userspace to +// own the whole map, which means a `loadkeys` that ships its own data files and +// a boot that is broken until one is loaded. The layouts live in the kernel +// here, and userspace names the one it wants. +#define KB_GETLAYOUT 0x5622 // fill in the active layout +#define KB_SETLAYOUT 0x5623 // switch to the layout named in `name` +#define KB_LISTLAYOUT 0x5624 // describe layout `index`; -EINVAL past end + +struct kb_layout { + uint16 index; // in for KB_LISTLAYOUT, out for KB_GETLAYOUT + char name[16]; // "us", "pl", "de" + char desc[96]; // human-readable, for `loadkeys -l` +}; + // Modifier bits, matched exactly. These mirror kernel/dev/kbd.h's values so // that userspace and the keyboard driver cannot drift apart. #define VTC_SHIFT 0x01 // kbd.h SHIFT (1<<0) diff --git a/kernel/core/console.c b/kernel/core/console.c index c2174c7..13c2949 100644 --- a/kernel/core/console.c +++ b/kernel/core/console.c @@ -4,7 +4,7 @@ #include "klog.h" #include "dbg.h" #include "vt.h" -#include "kbd.h" /* KEY_VT_SWITCH -- the terminal-switch chords */ +#include "kbd.h" /* KBD_VT_SWITCH -- the terminal-switch chords */ #include "gfx.h" #include "theme.h" #include "irq.h" @@ -434,14 +434,14 @@ erase_one(struct vt *v) // --------------------------------------------------------------------------- // Ctrl+Alt+Shift+F1..F6 switch terminals. kbd.c reports them as -// KEY_VT_SWITCH+n so that the +// KBD_VT_SWITCH+n so that the // decision of what a chord *means* is made here, where the terminals are, // rather than in a driver that should not know they exist. int console_vt_hotkey(int key) { - if (key != KEY_VT_PREV && key != KEY_VT_NEXT && - (key < KEY_VT_SWITCH || key >= KEY_VT_SWITCH + NVT)) + if (key != KBD_VT_PREV && key != KBD_VT_NEXT && + (key < KBD_VT_SWITCH || key >= KBD_VT_SWITCH + NVT)) return 0; // A program that called VT_SETMODE(VT_PROCESS) owns switching on this // terminal. Dropping the chord entirely would be worse than not binding it: @@ -449,9 +449,9 @@ console_vt_hotkey(int key) // falls through to the line discipline as an ordinary key. if (!vt_kernel_switches()) return 0; - if (key == KEY_VT_PREV) vt_cycle(-1); - else if (key == KEY_VT_NEXT) vt_cycle(+1); - else vt_switch(key - KEY_VT_SWITCH); + if (key == KBD_VT_PREV) vt_cycle(-1); + else if (key == KBD_VT_NEXT) vt_cycle(+1); + else vt_switch(key - KBD_VT_SWITCH); return 1; } @@ -467,8 +467,8 @@ consoleintr(int (*getc)(void)) // Terminal switching happens before the line discipline sees anything: // the chord is not input for the terminal you are leaving. - if (c == KEY_VT_PREV || c == KEY_VT_NEXT || - (c >= KEY_VT_SWITCH && c < KEY_VT_SWITCH + NVT)) { + if (c == KBD_VT_PREV || c == KBD_VT_NEXT || + (c >= KBD_VT_SWITCH && c < KBD_VT_SWITCH + NVT)) { release(&input_lock); int taken = console_vt_hotkey(c); acquire(&input_lock); @@ -481,8 +481,10 @@ consoleintr(int (*getc)(void)) // terminal a terminal: the keyboard follows the eyes. struct vt *v = vt_current(); - // Input mapping, before anything looks at the character. - if (v->tio.c_iflag & ISTRIP) c &= 0x7f; + // Input mapping, before anything looks at the character. ISTRIP is about + // seven-bit lines and applies to characters only -- masking a keycode + // would turn an arrow key into whatever letter its low seven bits spell. + if ((v->tio.c_iflag & ISTRIP) && c < KBD_SPECIAL) c &= 0x7f; if (c == '\r') { if (v->tio.c_iflag & IGNCR) continue; if (v->tio.c_iflag & ICRNL) c = '\n'; @@ -567,12 +569,13 @@ consoleintr(int (*getc)(void)) // Arrow keys and the rest of the navigation block, as escape sequences. // - // These arrive as single keycodes -- KEY_UP is 0xE2 -- and used to be - // pushed into the input buffer exactly like that. A terminal has never - // worked that way, and 0xE2 in particular is the worst possible choice of - // byte: it is a UTF-8 lead byte for a three-byte character, so a reader - // decoding UTF-8 swallowed the next two keys waiting for continuations - // that never came. Every arrow key corrupted the two keystrokes after it. + // These arrive as single keycodes and used to be pushed into the input + // buffer exactly like that. A terminal has never worked that way, and the + // values chosen made it worse: KBD_UP was 0xE2, a UTF-8 lead byte for a + // three-byte character, so a reader decoding UTF-8 swallowed the next two + // keys waiting for continuations that never came -- every arrow key + // corrupted the two keystrokes after it. The keycodes now sit above + // U+10FFFF where nothing can collide with them; see include/kbd.h. // // What a terminal sends is `ESC [ A`, and what makes zsh's line editor // work is receiving that. The sequences below are the DEC/xterm ones that @@ -580,16 +583,16 @@ consoleintr(int (*getc)(void)) { const char *esc = 0; switch (c) { - case KEY_UP: esc = "\033[A"; break; - case KEY_DN: esc = "\033[B"; break; - case KEY_RT: esc = "\033[C"; break; - case KEY_LF: esc = "\033[D"; break; - case KEY_HOME: esc = "\033[H"; break; - case KEY_END: esc = "\033[F"; break; - case KEY_PGUP: esc = "\033[5~"; break; - case KEY_PGDN: esc = "\033[6~"; break; - case KEY_INS: esc = "\033[2~"; break; - case KEY_DEL: esc = "\033[3~"; break; + case KBD_UP: esc = "\033[A"; break; + case KBD_DN: esc = "\033[B"; break; + case KBD_RT: esc = "\033[C"; break; + case KBD_LF: esc = "\033[D"; break; + case KBD_HOME: esc = "\033[H"; break; + case KBD_END: esc = "\033[F"; break; + case KBD_PGUP: esc = "\033[5~"; break; + case KBD_PGDN: esc = "\033[6~"; break; + case KBD_INS: esc = "\033[2~"; break; + case KBD_DEL: esc = "\033[3~"; break; } if (esc) { // Not echoed. In raw mode -- which is where a line editor lives, and @@ -610,12 +613,19 @@ consoleintr(int (*getc)(void)) } } + // Any keycode still here is a special nobody translated. Dropping it is + // the only safe thing: storing it would truncate to a byte and type a + // character the user did not press. + if (c >= KBD_SPECIAL) + continue; + if (v->in.e - v->in.r >= VT_INPUT_BUF) continue; // buffer full: drop it - // Everything else goes in, including bytes above 127 -- those are UTF-8 - // continuation bytes, and the old `c >= 32 && c <= 126` test silently - // refused every non-ASCII character anyone tried to type. + // Everything else goes in, including bytes above 127 -- those are UTF-8, + // arriving a byte at a time from the serial line or encoded by kbdgetc, + // and the old `c >= 32 && c <= 126` test silently refused every non-ASCII + // character anyone tried to type. v->in.buf[v->in.e++ % VT_INPUT_BUF] = c; echo(v, c); diff --git a/kernel/dev/kbd.c b/kernel/dev/kbd.c index a91b5d7..b64d38b 100644 --- a/kernel/dev/kbd.c +++ b/kernel/dev/kbd.c @@ -5,6 +5,7 @@ #include "posix.h" #include "vt.h" /* NVT -- how many terminals the chords reach */ #include "input.h" +#include "keymap.h" // The chord, settable at run time through VT_SETCHORD. // @@ -21,14 +22,72 @@ uint vt_chord_mods = RALT; uint vt_chord_base = SCAN_F1; +// Which scancodes are modifiers, and which are locks. Hardware facts rather +// than layout ones -- every layout agrees that 0x2A is a Shift key -- so they +// stay here while the character tables live in keymap.c. +static const uchar shiftcode[256] = { + [0x1D] = CTL, + [0x2A] = SHIFT, + [0x36] = SHIFT, + [0x38] = ALT, + [0x9D] = CTL, + [0xB8] = RALT // right Alt / AltGr -- see RALT in kbd.h +}; + +static const uchar togglecode[256] = { + [0x3A] = CAPSLOCK, + [0x45] = NUMLOCK, + [0x46] = SCROLLLOCK +}; + +// A key can produce more than one byte. +// +// consoleintr's contract with getc() is bytes: the serial line hands it raw +// UTF-8 already, and it would have no way to tell an encoded codepoint apart +// from a byte that happened to have the same value. So the keyboard encodes +// here and hands the bytes over one call at a time -- kbdintr loops until +// getc() returns negative, so a queue of at most four drains on its own +// before the next scancode is read. +static uchar pend[4]; +static int pend_n, pend_i; + +static int +queue_utf8(uint32 cp) +{ + pend_i = 0; + if (cp < 0x80) { + pend_n = 0; + return (int)cp; // the common case: no queue at all + } + if (cp < 0x800) { + pend[0] = 0xC0 | (cp >> 6); + pend[1] = 0x80 | (cp & 0x3F); + pend_n = 2; + } else if (cp < 0x10000) { + pend[0] = 0xE0 | (cp >> 12); + pend[1] = 0x80 | ((cp >> 6) & 0x3F); + pend[2] = 0x80 | (cp & 0x3F); + pend_n = 3; + } else { + pend[0] = 0xF0 | (cp >> 18); + pend[1] = 0x80 | ((cp >> 12) & 0x3F); + pend[2] = 0x80 | ((cp >> 6) & 0x3F); + pend[3] = 0x80 | (cp & 0x3F); + pend_n = 4; + } + return pend[pend_i++]; +} + int kbdgetc(void) { static uint shift; - static uchar *charcode[4] = { - normalmap, shiftmap, ctlmap, ctlmap - }; - uint st, data, c; + uint st, data; + uint32 cp; + + if (pend_i < pend_n) + return pend[pend_i++]; // finish the last key first + pend_i = pend_n = 0; st = inb(KBSTATP); if((st & KBS_DIB) == 0) @@ -81,29 +140,25 @@ kbdgetc(void) // terminal". What it means is decided in console.c, which is where the // terminals are; this only has to name it. See console_vt_hotkey(). // - // The modifier test is an equality, not a mask. A mask fires on any superset, - // so a chord aimed at the host would switch terminals behind your back. - // // The modifier test is an equality, not a mask. `shift & ALT` would fire on // Ctrl+Alt+1 and on AltGr+1 as well, so a chord aimed at the host -- or a - // Polish keystroke -- would switch terminals behind your back. Exactly left - // Alt, and nothing else held. + // Polish keystroke -- would switch terminals behind your back. Exactly the + // modifiers named, and nothing else held. if (vt_chord_mods != 0 && (shift & (CTL | SHIFT | ALT | RALT)) == vt_chord_mods) { if (data >= vt_chord_base && data < vt_chord_base + NVT) - return KEY_VT_SWITCH + (data - vt_chord_base); - if (data == SCAN_MINUS) return KEY_VT_PREV; - if (data == SCAN_EQUALS) return KEY_VT_NEXT; + return KBD_VT_SWITCH + (data - vt_chord_base); + if (data == SCAN_MINUS) return KBD_VT_PREV; + if (data == SCAN_EQUALS) return KBD_VT_NEXT; } - c = charcode[shift & (CTL | SHIFT)][data]; - if(shift & CAPSLOCK){ - if('a' <= c && c <= 'z') - c += 'A' - 'a'; - else if('A' <= c && c <= 'Z') - c += 'a' - 'A'; - } - return c; + cp = keymap_lookup(data, shift); + if (cp == 0) + return 0; + if (cp >= KBD_SPECIAL) + return (int)cp; // arrows and the rest, not characters + + return queue_utf8(cp); } void diff --git a/kernel/dev/keymap.c b/kernel/dev/keymap.c new file mode 100644 index 0000000..94e7f0b --- /dev/null +++ b/kernel/dev/keymap.c @@ -0,0 +1,297 @@ +// Keyboard layouts. See include/keymap.h for the interface and its limits. +// +// Every table is indexed by set-1 scancode, with the E0-prefixed keys folded +// into 0x80..0xFF by kbd.c before the lookup -- the same numbering the single +// hardcoded table used, so a layout is a diff against a shape that was already +// proven to work rather than a new mapping to get wrong. +// +// The tables are `uint32` and not `uchar`. That is the whole reason this file +// exists: `ą` is U+0105 and does not fit in a byte, and a Polish layout that +// cannot type `ą` is not one. + +#include "types.h" +#include "defs.h" +#include "kbd.h" +#include "keymap.h" + +// Shared by every layout that does not move the navigation block, which is +// all of them -- the arrow keys are not a national matter. Merged into each +// level table below rather than consulted separately, because a layout is +// allowed to disagree and none currently does. +#define NAV_KEYS \ + [0xC8] = KBD_UP, [0xD0] = KBD_DN, \ + [0xC9] = KBD_PGUP, [0xD1] = KBD_PGDN, \ + [0xCB] = KBD_LF, [0xCD] = KBD_RT, \ + [0x97] = KBD_HOME, [0xCF] = KBD_END, \ + [0xD2] = KBD_INS, [0xD3] = KBD_DEL, \ + [0x9C] = '\n', /* KP_Enter */ \ + [0xB5] = '/' /* KP_Div */ + +// --- US --------------------------------------------------------------------- + +static const uint32 us_plain[256] = { + NO, 0x1B, '1', '2', '3', '4', '5', '6', // 0x00 + '7', '8', '9', '0', '-', '=', '\b', '\t', + 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', // 0x10 + 'o', 'p', '[', ']', '\n', NO, 'a', 's', + 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', // 0x20 + '\'', '`', NO, '\\', 'z', 'x', 'c', 'v', + 'b', 'n', 'm', ',', '.', '/', NO, '*', // 0x30 + NO, ' ', NO, NO, NO, NO, NO, NO, + NO, NO, NO, NO, NO, NO, NO, '7', // 0x40 + '8', '9', '-', '4', '5', '6', '+', '1', + '2', '3', '0', '.', NO, NO, NO, NO, // 0x50 + NAV_KEYS +}; + +static const uint32 us_shift[256] = { + NO, 0x1B, '!', '@', '#', '$', '%', '^', // 0x00 + '&', '*', '(', ')', '_', '+', '\b', '\t', + 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', // 0x10 + 'O', 'P', '{', '}', '\n', NO, 'A', 'S', + 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', // 0x20 + '"', '~', NO, '|', 'Z', 'X', 'C', 'V', + 'B', 'N', 'M', '<', '>', '?', NO, '*', // 0x30 + NO, ' ', NO, NO, NO, NO, NO, NO, + NO, NO, NO, NO, NO, NO, NO, '7', // 0x40 + '8', '9', '-', '4', '5', '6', '+', '1', + '2', '3', '0', '.', NO, NO, NO, NO, // 0x50 + NAV_KEYS +}; + +// --- Polish (programmers) --------------------------------------------------- +// +// US QWERTY with nine letters on AltGr. This is the layout essentially every +// Polish developer uses, and the reason is that the alternative -- "Polish +// (214)", a genuine national arrangement -- moves Y, Z and most of the +// punctuation, which makes every shell and every editor shortcut land +// somewhere else. The diacritics go on AltGr and nothing else moves. +// +// AltGr+x is ź and AltGr+z is ż, which looks arbitrary and is not: the letter +// x is unused in Polish, so its key is free, and ź and ż both need one. + +static const uint32 pl_altgr[256] = { + [0x1E] = 0x0105, // a -> ą + [0x2E] = 0x0107, // c -> ć + [0x12] = 0x0119, // e -> ę + [0x26] = 0x0142, // l -> ł + [0x31] = 0x0144, // n -> ń + [0x18] = 0x00F3, // o -> ó + [0x1F] = 0x015B, // s -> ś + [0x2D] = 0x017A, // x -> ź + [0x2C] = 0x017C, // z -> ż +}; + +static const uint32 pl_shiftaltgr[256] = { + [0x1E] = 0x0104, // A -> Ą + [0x2E] = 0x0106, // C -> Ć + [0x12] = 0x0118, // E -> Ę + [0x26] = 0x0141, // L -> Ł + [0x31] = 0x0143, // N -> Ń + [0x18] = 0x00D3, // O -> Ó + [0x1F] = 0x015A, // S -> Ś + [0x2D] = 0x0179, // X -> Ź + [0x2C] = 0x017B, // Z -> Ż +}; + +// --- German (QWERTZ) -------------------------------------------------------- +// +// Y and Z swap, the umlauts and ß take the punctuation keys on the home and +// top rows, and the key between left Shift and Z -- scancode 0x56, which a US +// keyboard does not have at all -- carries < > |. +// +// The acute (0x0D) and circumflex (0x29) keys are dead on a real German +// layout. Here they type the accent on its own. See include/keymap.h. + +static const uint32 de_plain[256] = { + NO, 0x1B, '1', '2', '3', '4', '5', '6', // 0x00 + '7', '8', '9', '0', 0x00DF, 0x00B4, '\b', '\t', // ß ´ + 'q', 'w', 'e', 'r', 't', 'z', 'u', 'i', // 0x10 + 'o', 'p', 0x00FC, '+', '\n', NO, 'a', 's', // ü + 'd', 'f', 'g', 'h', 'j', 'k', 'l', 0x00F6, // ö + 0x00E4, '^', NO, '#', 'y', 'x', 'c', 'v', // ä + 'b', 'n', 'm', ',', '.', '-', NO, '*', // 0x30 + NO, ' ', NO, NO, NO, NO, NO, NO, + NO, NO, NO, NO, NO, NO, NO, '7', // 0x40 + '8', '9', '-', '4', '5', '6', '+', '1', + '2', '3', '0', ',', NO, NO, '<', NO, // 0x50, 0x56 = <>| + NAV_KEYS +}; + +static const uint32 de_shift[256] = { + NO, 0x1B, '!', '"', 0x00A7, '$', '%', '&', // 0x00 § + '/', '(', ')', '=', '?', '`', '\b', '\t', + 'Q', 'W', 'E', 'R', 'T', 'Z', 'U', 'I', // 0x10 + 'O', 'P', 0x00DC, '*', '\n', NO, 'A', 'S', // Ü + 'D', 'F', 'G', 'H', 'J', 'K', 'L', 0x00D6, // Ö + 0x00C4, 0x00B0, NO, '\'', 'Y', 'X', 'C', 'V', // Ä ° + 'B', 'N', 'M', ';', ':', '_', NO, '*', // 0x30 + NO, ' ', NO, NO, NO, NO, NO, NO, + NO, NO, NO, NO, NO, NO, NO, '7', // 0x40 + '8', '9', '-', '4', '5', '6', '+', '1', + '2', '3', '0', ',', NO, NO, '>', NO, // 0x50 + NAV_KEYS +}; + +static const uint32 de_altgr[256] = { + [0x03] = 0x00B2, // 2 -> ² + [0x04] = 0x00B3, // 3 -> ³ + [0x08] = '{', // 7 + [0x09] = '[', // 8 + [0x0A] = ']', // 9 + [0x0B] = '}', // 0 + [0x0C] = '\\', // ß + [0x10] = '@', // q + [0x12] = 0x20AC, // e -> € + [0x1B] = '~', // + + [0x32] = 0x00B5, // m -> µ + [0x56] = '|', +}; + +// --- the catalogue ---------------------------------------------------------- +// +// Order matters only in that the first entry is the default. US, because a +// machine whose keyboard layout has never been set should behave the way the +// scancodes are printed on the majority of keyboards, and because every +// installation instruction anyone follows is written in ASCII. + +static const struct keylayout layouts[] = { + { "us", "English (US), QWERTY", + { us_plain, us_shift, 0, 0 } }, + { "pl", "Polish (programmers) -- US QWERTY, diacritics on AltGr", + { us_plain, us_shift, pl_altgr, pl_shiftaltgr } }, + { "de", "German, QWERTZ -- no dead keys, so the accent keys type the accent", + { de_plain, de_shift, de_altgr, 0 } }, +}; + +#define NLAYOUTS ((int)(sizeof(layouts) / sizeof(layouts[0]))) + +static const struct keylayout *active = &layouts[0]; + +const struct keylayout * +keymap_active(void) +{ + return active; +} + +const struct keylayout * +keymap_by_index(int i) +{ + if (i < 0 || i >= NLAYOUTS) + return 0; + return &layouts[i]; +} + +int +keymap_set(const char *name) +{ + for (int i = 0; i < NLAYOUTS; i++) { + const char *a = layouts[i].name, *b = name; + while (*a && *a == *b) { a++; b++; } + if (*a == 0 && *b == 0) { + active = &layouts[i]; + return 0; + } + } + return -1; +} + +// A control character from the plain codepoint rather than from a table of +// its own. +// +// The table this replaces was indexed by scancode, which quietly assumed a US +// keyboard: on QWERTZ, Ctrl+Z and Ctrl+Y would have swapped with the letters, +// so suspending a job would have depended on the layout. Deriving from what +// the key actually types is both shorter and correct everywhere. +static uint32 +control_of(uint32 cp) +{ + if (cp >= 'a' && cp <= 'z') return cp - 'a' + 1; + if (cp >= 'A' && cp <= 'Z') return cp - 'A' + 1; + switch (cp) { + case '@': case ' ': case '2': return 0; // NUL + case '[': return 0x1B; // ESC + case '\\': return 0x1C; + case ']': return 0x1D; + case '^': case '6': return 0x1E; + case '_': case '-': case '/': return 0x1F; + case '?': return 0x7F; + case '\n': case '\r': return '\r'; + } + return 0; // no control character on this key +} + +// Upper <-> lower for the letters these layouts can produce. +// +// Caps Lock needs this and Shift does not: Shift has its own table, so the +// layout has already said what the shifted key gives. Caps Lock has no table +// -- it applies to whatever came out, including `ą` from the AltGr level -- +// so the case pair has to be derived from the codepoint. +// +// Latin Extended-A is laid out in alternating pairs, but not all the same way +// round: U+0100..U+0137 and U+014A..U+0177 have the capital on the even +// codepoint, while U+0139..U+0148 and U+0179..U+017E have it on the odd one. +// L-stroke is U+0141/U+0142 and Z-dot is U+017B/U+017C, so Polish lands in +// both halves and getting the parity wrong would shift one of them to a +// neighbouring letter rather than to its own capital. +static uint32 +case_toggle(uint32 cp) +{ + if (cp >= 'a' && cp <= 'z') return cp - 'a' + 'A'; + if (cp >= 'A' && cp <= 'Z') return cp - 'A' + 'a'; + + // Latin-1: the accented letters run C0..DE and E0..FE, with multiplication + // and division signs punched out of the middle of each. Eszett (DF) has no + // single-codepoint capital and stays as it is. + if (cp >= 0xC0 && cp <= 0xDE && cp != 0xD7) return cp + 0x20; + if (cp >= 0xE0 && cp <= 0xFE && cp != 0xF7) return cp - 0x20; + + if ((cp >= 0x0100 && cp <= 0x0137) || (cp >= 0x014A && cp <= 0x0177)) + return (cp & 1) ? cp - 1 : cp + 1; // even is the capital + if ((cp >= 0x0139 && cp <= 0x0148) || (cp >= 0x0179 && cp <= 0x017E)) + return (cp & 1) ? cp + 1 : cp - 1; // odd is the capital + + return cp; +} + +uint32 +keymap_lookup(uint scancode, uint mods) +{ + const struct keylayout *l = active; + uint32 cp; + + if (scancode > 0xFF) + return 0; + + // AltGr first. It is the only modifier that selects a whole different table, + // and on a layout that has no AltGr level the lookup falls back rather than + // producing nothing -- otherwise holding AltGr on a US keyboard would make + // every key dead. + int level = KM_PLAIN; + if (mods & RALT) level = (mods & SHIFT) ? KM_SHIFTALTGR : KM_ALTGR; + else if (mods & SHIFT) level = KM_SHIFT; + + cp = l->level[level] ? l->level[level][scancode] : 0; + if (cp == 0 && level >= KM_ALTGR) { + level = (mods & SHIFT) ? KM_SHIFT : KM_PLAIN; + cp = l->level[level] ? l->level[level][scancode] : 0; + } + if (cp == 0) + return 0; + + // Caps Lock is a letter-case toggle, not a Shift: it applies to whatever + // the table produced, including a letter that only exists on AltGr. `ą` + // with Caps Lock on is `Ą`, and only the layout knows that -- so the case + // pair has to come from the codepoint, not from a second table lookup. + if ((mods & CAPSLOCK) && cp < KBD_SPECIAL) + cp = case_toggle(cp); + + // Ctrl is last and overrides: Ctrl+Shift+C is still Ctrl+C. + if (mods & CTL) { + if (cp >= KBD_SPECIAL) + return cp; // Ctrl+arrow stays an arrow + return control_of(cp); + } + + return cp; +} diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index bea244b..55ad862 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -18,6 +18,7 @@ #include "gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ reports them */ #include "vt.h" /* the terminals TIOC*/VT_* act on */ #include "pciuser.h" +#include "keymap.h" /* KB_*LAYOUT — the keyboard layout catalogue */ #include "netuser.h" // ----------------------------------------------------------------------- @@ -459,6 +460,35 @@ addr_t sys_pipe2(void) // Filesystem ops // ----------------------------------------------------------------------- +// Resolve a symlink opened as the final path component. +// +// divesymlink() has been in kernel/fs/fs.c since the fork and nothing ever +// called it, so a symlink behaved as a small file containing its own target: +// `cat link` printed the target's *name* rather than its contents, and every +// program that follows a symlink to find something followed it nowhere. +// +// Only the last component. A symlink in the middle of a path -- /a/link/b -- +// is still not resolved; that belongs in namex(), which would have to re-enter +// path lookup while holding a directory lock, and getting that wrong is a +// deadlock rather than a wrong answer. Final-component resolution is what +// open(), exec() and stat() need, which is nearly everything that matters. +// +// Takes a locked inode and returns a locked inode, or 0 with everything +// released -- the same contract divesymlink() already had. +static struct inode * +follow_final(struct inode *ip, int omode) +{ + if (ip->type != T_SYMLINK) + return ip; + if (omode & O_NOFOLLOW) { + // POSIX: opening a symlink itself is an error unless O_PATH, which + // this kernel does not have. Saying so beats handing back the link. + iunlockput(ip); + return 0; + } + return divesymlink(ip); +} + addr_t sys_open(void) { char *path; @@ -486,6 +516,7 @@ addr_t sys_open(void) } else { if (!(ip = namei(path))) { end_op(); return -ENOENT; } ilock(ip); + if (!(ip = follow_final(ip, omode))) { end_op(); return -ELOOP; } // Directories may only be opened for reading, but callers (e.g. // opendir()) legitimately OR in flags like O_DIRECTORY/O_CLOEXEC/ // O_NONBLOCK alongside O_RDONLY — only the access-mode bits matter. @@ -530,6 +561,7 @@ addr_t sys_openat(void) } else { if (!(ip = at_namei(dirfd, path))) { end_op(); return -ENOENT; } ilock(ip); + if (!(ip = follow_final(ip, omode))) { end_op(); return -ELOOP; } // Directories may only be opened for reading, but callers (e.g. // opendir()) legitimately OR in flags like O_DIRECTORY/O_CLOEXEC/ // O_NONBLOCK alongside O_RDONLY — only the access-mode bits matter. @@ -572,16 +604,21 @@ addr_t sys_fstat(void) return 0; } -addr_t sys_stat(void) +// stat/lstat differ in exactly one thing: whether a final symlink is followed. +// They used to be the same function, which was invisible while nothing +// followed symlinks anywhere and becomes wrong the moment something does. +static addr_t +do_stat(int dirfd, char *path, char *st, int follow) { - char *path; - char *st; struct inode *ip; - int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat); - if (argstr(0, &path) < 0 || argptr(1, &st, size) < 0) return -1; + begin_op(); - if (!(ip = namei(path))) { end_op(); return -ENOENT; } + if (!(ip = at_namei(dirfd, path))) { end_op(); return -ENOENT; } ilock(ip); + if (follow && ip->type == T_SYMLINK && !(ip = divesymlink(ip))) { + end_op(); + return -ELOOP; + } if (myproc()->syscall_map) stati_linux(ip, (struct linux_stat*)st); else stati(ip, (struct stat*)st); iunlockput(ip); @@ -589,7 +626,23 @@ addr_t sys_stat(void) return 0; } -addr_t sys_lstat(void) { return sys_stat(); } +addr_t sys_stat(void) +{ + char *path, *st; + int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat); + if (argstr(0, &path) < 0) return -EFAULT; + if (argptr(1, &st, size) < 0) return -EFAULT; + return do_stat(AT_FDCWD, path, st, 1); +} + +addr_t sys_lstat(void) +{ + char *path, *st; + int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat); + if (argstr(0, &path) < 0) return -EFAULT; + if (argptr(1, &st, size) < 0) return -EFAULT; + return do_stat(AT_FDCWD, path, st, 0); +} addr_t sys_access(void) { @@ -865,31 +918,71 @@ bad_rename: ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -err; } +// symlink(2). +// +// The errno matters more here than anywhere else in this file, because the one +// widespread use of symlink() is as an atomic lock: create a link, and if it +// already exists someone else holds the lock. zsh's history locking is exactly +// that, and it distinguishes EEXIST ("contended, wait and retry") from +// anything else ("this cannot work, give up"). Returning a bare -1 -- which is +// -EPERM -- put every zsh on the machine in the second bucket, and printed +// +// zsh: locking failed for /root/.zsh_history: operation not permitted +// +// on every boot, on a filesystem that supports symlinks perfectly well. +static addr_t +do_symlink(int newdirfd, char *target, char *linkpath) +{ + struct inode *ip; + int len; + + if (*target == 0) + return -ENOENT; // an empty target resolves to nothing + + begin_op(); + // Distinguishing the two ways createat() returns 0 -- the name is taken, + // or the path to it does not exist -- has to be done before the create, + // because createat() does not say which. + if ((ip = at_namei(newdirfd, linkpath)) != 0) { + iput(ip); + end_op(); + return -EEXIST; + } + if (!(ip = createat(newdirfd, linkpath, T_SYMLINK, 0, 0))) { + end_op(); + return -ENOENT; + } + len = strlen(target); + if (writei(ip, 0, (uint64)target, 0, len) != len) { + iunlockput(ip); + end_op(); + return -EIO; + } + iunlockput(ip); + end_op(); + return 0; +} + addr_t sys_symlink(void) { char *target, *linkpath; - if (argstr(0, &target) < 0 || argstr(1, &linkpath) < 0) return -1; - begin_op(); - struct inode *ip = create(linkpath, T_SYMLINK, 0, 0); - if (!ip) { end_op(); return -1; } - int len = strlen(target); - if (writei(ip, 0, (uint64)target, 0, len) != len) { iunlockput(ip); end_op(); return -1; } - iunlockput(ip); end_op(); - return 0; + if (argstr(0, &target) < 0 || argstr(1, &linkpath) < 0) return -EFAULT; + return do_symlink(AT_FDCWD, target, linkpath); } addr_t sys_readlink(void) { char *path, *buf; int bufsiz; - if (argstr(0, &path) < 0 || argint(2, &bufsiz) < 0 || bufsiz <= 0) return -1; - if (argptr(1, &buf, bufsiz) < 0) return -1; + if (argstr(0, &path) < 0) return -EFAULT; + if (argint(2, &bufsiz) < 0 || bufsiz <= 0) return -EINVAL; + if (argptr(1, &buf, bufsiz) < 0) return -EFAULT; struct inode *ip; if (!(ip = namei(path))) return -ENOENT; ilock(ip); if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; } int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz; - if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -1; } + if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -EIO; } iunlockput(ip); return n; } @@ -1255,6 +1348,32 @@ addr_t sys_ioctl(void) return 0; } + // Which keyboard layout the console uses. One layout for the machine, not + // one per terminal: there is one keyboard, and a user who switches to + // tty3 has not changed which keys their fingers are on. + if (request == KB_SETLAYOUT) { + struct kb_layout *k = (struct kb_layout*)user_ptr(argp, sizeof(*k)); + if (!k) return -EFAULT; + k->name[sizeof(k->name) - 1] = 0; + if (keymap_set(k->name) < 0) + return -EINVAL; + return 0; + } + if (request == KB_GETLAYOUT || request == KB_LISTLAYOUT) { + struct kb_layout *k = (struct kb_layout*)user_ptr(argp, sizeof(*k)); + if (!k) return -EFAULT; + const struct keylayout *l; + if (request == KB_GETLAYOUT) { + l = keymap_active(); + } else { + l = keymap_by_index(k->index); + if (!l) return -EINVAL; // the documented end of the list + } + safestrcpy(k->name, l->name, sizeof(k->name)); + safestrcpy(k->desc, l->desc, sizeof(k->desc)); + return 0; + } + if (request == VT_GETMODE) { struct vt_mode *m = (struct vt_mode*)user_ptr(argp, sizeof(*m)); if (!m) return -EFAULT; @@ -1596,18 +1715,10 @@ addr_t sys_symlinkat(void) { char *target, *linkpath; int newdirfd; - if (argstr(0, &target) < 0) return -1; + if (argstr(0, &target) < 0) return -EFAULT; argint(1, &newdirfd); - if (argstr(2, &linkpath) < 0) return -1; - begin_op(); - struct inode *ip = createat(newdirfd, linkpath, T_SYMLINK, 0, 0); - if (!ip) { end_op(); return -1; } - int len = strlen(target); - if (writei(ip, 0, (uint64)target, 0, len) != len) { - iunlockput(ip); end_op(); return -1; - } - iunlockput(ip); end_op(); - return 0; + if (argstr(2, &linkpath) < 0) return -EFAULT; + return do_symlink(newdirfd, target, linkpath); } addr_t sys_readlinkat(void) @@ -1616,15 +1727,15 @@ addr_t sys_readlinkat(void) char *path, *buf; int bufsiz; argint(0, &dirfd); - if (argstr(1, &path) < 0) return -1; - if (argint(3, &bufsiz) < 0 || bufsiz <= 0) return -1; - if (argptr(2, &buf, bufsiz) < 0) return -1; + if (argstr(1, &path) < 0) return -EFAULT; + if (argint(3, &bufsiz) < 0 || bufsiz <= 0) return -EINVAL; + if (argptr(2, &buf, bufsiz) < 0) return -EFAULT; struct inode *ip; if (!(ip = at_namei(dirfd, path))) return -ENOENT; ilock(ip); if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; } int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz; - if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -1; } + if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -EIO; } iunlockput(ip); return n; } @@ -1670,15 +1781,7 @@ addr_t sys_fstatat(void) return 0; } - begin_op(); - struct inode *ip = at_namei(dirfd, path); - if (!ip) { end_op(); return -ENOENT; } - ilock(ip); - if (myproc()->syscall_map) stati_linux(ip, (struct linux_stat*)st); - else stati(ip, (struct stat*)st); - iunlockput(ip); - end_op(); - return 0; + return do_stat(dirfd, path, st, !(flags & AT_SYMLINK_NOFOLLOW)); } // -----------------------------------------------------------------------