claude/input-module-keyboard-events-379361 #6
68
build.zig
68
build.zig
|
|
@ -178,6 +178,13 @@ pub fn build(b: *std.Build) void {
|
|||
.root_source_file = b.path("system/services/vfs/protocol.zig"),
|
||||
});
|
||||
|
||||
// The input wire protocol: the input service's public interface, exposed as its own
|
||||
// module the same way vfs-protocol is. Shared by the input service, the runtime's
|
||||
// `input` helper (subscribe/publish), and every source and subscriber.
|
||||
const input_protocol_module = b.addModule("input-protocol", .{
|
||||
.root_source_file = b.path("system/services/input/protocol.zig"),
|
||||
});
|
||||
|
||||
// The danos-native user-space runtime: system_call wrappers, the C-convention
|
||||
// heap, IPC helpers, the process start shim, device access. This is the stable
|
||||
// application ABI; POSIX compatibility is a separate library on top (see below).
|
||||
|
|
@ -193,6 +200,7 @@ pub fn build(b: *std.Build) void {
|
|||
.{ .name = "abi", .module = abi_module },
|
||||
.{ .name = "device-abi", .module = device_abi_module },
|
||||
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
||||
.{ .name = "input-protocol", .module = input_protocol_module },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -203,6 +211,21 @@ pub fn build(b: *std.Build) void {
|
|||
.root_source_file = b.path("library/mmio/mmio.zig"),
|
||||
});
|
||||
|
||||
// Keyboard layouts compiled from the X11 xkeyboard-config database into native Zig
|
||||
// (keycode + modifiers -> keysym/character). The `layouts` tables are generated by
|
||||
// tools/make-xkeyboard-config.py; `xkeyboard-config` is the hand-written API over them.
|
||||
// No target set, so each inherits its importer's. See library/xkeyboard-config/.
|
||||
const xkb_layouts_module = b.addModule("layouts", .{
|
||||
.root_source_file = b.path("library/xkeyboard-config/generated/layouts.zig"),
|
||||
});
|
||||
const xkeyboard_config_module = b.addModule("xkeyboard-config", .{
|
||||
.root_source_file = b.path("library/xkeyboard-config/xkeyboard-config.zig"),
|
||||
.imports = &.{
|
||||
.{ .name = "layouts", .module = xkb_layouts_module },
|
||||
},
|
||||
});
|
||||
_ = xkeyboard_config_module;
|
||||
|
||||
// The POSIX / C compatibility layer, a separate library layered strictly over the
|
||||
// runtime (it calls the runtime's IPC/heap, never system calls directly). This is
|
||||
// the one place POSIX/C spellings are allowed verbatim — see docs/coding-standards.md
|
||||
|
|
@ -298,7 +321,15 @@ pub fn build(b: *std.Build) void {
|
|||
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "vfs-test", "system/services/vfs/vfs-test.zig");
|
||||
const hpet_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "hpet", "system/drivers/hpet/hpet.zig");
|
||||
const bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "bus", "system/drivers/bus/bus.zig");
|
||||
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "bus", "system/drivers/ps2-bus/ps2-bus.zig");
|
||||
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
||||
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
|
||||
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "device-manager", "system/services/device-manager/device-manager.zig");
|
||||
// The input service and its exercisers: the fan-out server, a hardware-free synthetic
|
||||
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
|
||||
const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "input", "system/services/input/input.zig");
|
||||
const input_source_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "input-source", "system/services/input-source/input-source.zig");
|
||||
const input_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "input-test", "system/services/input-test/input-test.zig");
|
||||
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "args-echo", "system/services/args-echo/args-echo.zig");
|
||||
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, "process-test", "system/services/process-test/process-test.zig");
|
||||
|
||||
|
|
@ -316,8 +347,20 @@ pub fn build(b: *std.Build) void {
|
|||
mk_run.addFileArg(hpet_exe.getEmittedBin());
|
||||
mk_run.addArg("bus");
|
||||
mk_run.addFileArg(bus_exe.getEmittedBin());
|
||||
mk_run.addArg("ps2-bus");
|
||||
mk_run.addFileArg(ps2_bus_exe.getEmittedBin());
|
||||
mk_run.addArg("ps2-keyboard");
|
||||
mk_run.addFileArg(ps2_keyboard_exe.getEmittedBin());
|
||||
mk_run.addArg("ps2-mouse");
|
||||
mk_run.addFileArg(ps2_mouse_exe.getEmittedBin());
|
||||
mk_run.addArg("device-manager");
|
||||
mk_run.addFileArg(device_manager_exe.getEmittedBin());
|
||||
mk_run.addArg("input");
|
||||
mk_run.addFileArg(input_exe.getEmittedBin());
|
||||
mk_run.addArg("input-source");
|
||||
mk_run.addFileArg(input_source_exe.getEmittedBin());
|
||||
mk_run.addArg("input-test");
|
||||
mk_run.addFileArg(input_test_exe.getEmittedBin());
|
||||
mk_run.addArg("args-echo");
|
||||
mk_run.addFileArg(args_echo_exe.getEmittedBin());
|
||||
mk_run.addArg("process-test");
|
||||
|
|
@ -328,8 +371,12 @@ pub fn build(b: *std.Build) void {
|
|||
for ([_]struct { *std.Build.Step.Compile, []const u8 }{
|
||||
.{ vfs_exe, "system/services" },
|
||||
.{ device_manager_exe, "system/services" },
|
||||
.{ input_exe, "system/services" },
|
||||
.{ hpet_exe, "system/drivers" },
|
||||
.{ bus_exe, "system/drivers" },
|
||||
.{ ps2_bus_exe, "system/drivers" },
|
||||
.{ ps2_keyboard_exe, "system/drivers" },
|
||||
.{ ps2_mouse_exe, "system/drivers" },
|
||||
}) |entry| {
|
||||
const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } });
|
||||
b.getInstallStep().dependOn(&step.step);
|
||||
|
|
@ -470,4 +517,25 @@ pub fn build(b: *std.Build) void {
|
|||
});
|
||||
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
|
||||
}
|
||||
|
||||
// The xkeyboard-config keymap tests need its generated `layouts` import wired, so they
|
||||
// don't fit the plain loop above. Its keycode->character assertions are the end-to-end
|
||||
// proof that the xkb-data -> generator -> Zig-lookup pipeline is correct.
|
||||
const xkb_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("library/xkeyboard-config/xkeyboard-config.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "layouts", .module = xkb_layouts_module },
|
||||
},
|
||||
}),
|
||||
});
|
||||
test_step.dependOn(&b.addRunArtifact(xkb_tests).step);
|
||||
|
||||
// Convenience: `zig build gen-xkeyboard-config` regenerates the layout tables from the
|
||||
// vendored data (offline). `fetch` (the network step) stays a manual script run.
|
||||
const gen_xkb = b.addSystemCommand(&.{ "python3", "tools/make-xkeyboard-config.py", "generate" });
|
||||
const gen_xkb_step = b.step("gen-xkeyboard-config", "Regenerate library/xkeyboard-config/generated from the vendored data");
|
||||
gen_xkb_step.dependOn(&gen_xkb.step);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,11 @@ rather than restate it. Roughly in the order things happen at runtime:
|
|||
microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the
|
||||
supervision link as the kill authority, and child-exit notifications over the
|
||||
same endpoints IRQs arrive on.
|
||||
16. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
||||
16. **[input.md](input.md) — the input module.** Broadcasting input events (keyboard,
|
||||
mouse, joystick): why a synchronous rendezvous can't fan out to many listeners, the
|
||||
asynchronous `ipc_send` primitive built to fix it, and the per-device subscribe/publish
|
||||
service layered on top.
|
||||
17. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
||||
how `while (true) hlt` parks the CPU safely once there's nothing left to do.
|
||||
|
||||
Start with the north star:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
# The input module: broadcasting input events
|
||||
|
||||
A keyboard driver has one keystroke and *many* programs that might want it — a shell, a
|
||||
window server, a logger. None of them owns the hardware, and the driver should not know
|
||||
who is listening. So between the drivers and the listeners sits the **input service**
|
||||
(`system/services/input/`): drivers **publish** events to it, programs **subscribe**, and
|
||||
it fans each event out to every interested subscriber. It is an ordinary ring-3 process
|
||||
reached over IPC, like the [VFS server](../system/services/vfs/vfs.zig) — no kernel knows
|
||||
what a key is.
|
||||
|
||||
## One service, several device classes
|
||||
|
||||
The service carries three device classes today — **keyboard**, **mouse**, and
|
||||
**joystick/gamepad** — and is built to take more
|
||||
([protocol.zig](../system/services/input/protocol.zig)). Each class has its own typed
|
||||
event:
|
||||
|
||||
- `KeyEvent` — `key_down`/`key_up` (physical make/break) and `key_press` (a character was
|
||||
produced, carrying the Unicode scalar); plus a layout-independent `keycode` and a
|
||||
`modifiers` bitmask.
|
||||
- `MouseEvent` — relative `motion` (`dx`/`dy`), `button_down`/`button_up`, and `scroll`.
|
||||
- `JoystickEvent` — `axis` moves (a signed value on a `control` index) and
|
||||
`button_down`/`button_up`.
|
||||
|
||||
All three travel in one **`InputEvent` envelope** tagged with a `DeviceKind`, so the
|
||||
fan-out is a single code path and a subscriber can take a mix of classes on one stream.
|
||||
Decode an envelope with `asKeyboard()` / `asMouse()` / `asJoystick()` (each returns null
|
||||
unless the tag matches). A subscriber names the classes it wants with a **`device_mask`**,
|
||||
and the service routes each event only to subscribers whose mask includes its class — so a
|
||||
mouse-only listener never wakes for keystrokes.
|
||||
|
||||
## Why this needed a new kernel primitive
|
||||
|
||||
The interesting part is delivery, and it runs straight into the shape of danos IPC.
|
||||
[ipc.md](ipc.md) describes a **synchronous rendezvous**: a server holds exactly one
|
||||
pending reply (`Task.ipc_client`) and *must* answer it on its next `replyWait`. Two
|
||||
consequences decide the whole design:
|
||||
|
||||
1. **You cannot block N subscribers waiting for "the next event".** A server can hold only
|
||||
one caller at a time, so the natural "subscriber calls `next_event()` and blocks" API
|
||||
is impossible for more than one subscriber. Delivery therefore has to be **push** — the
|
||||
service reaching out to subscribers — not pull.
|
||||
|
||||
2. **A synchronous push can hang the whole service.** If the service delivered with
|
||||
`ipc_call`, it would block until each subscriber replied. `ipc_call` has no timeout, and
|
||||
the kernel does **not** wake a caller parked on a *dead* peer's endpoint (it only fails a
|
||||
peer that was mid-reply — see [process.zig](../system/kernel/process.zig)
|
||||
`releaseTaskResourcesLocked`). One subscriber that exits mid-delivery would wedge input
|
||||
for everyone. That is the opposite of the resilience the microkernel is for.
|
||||
|
||||
The fix is the asynchronous send that [ipc.md](ipc.md) had already earmarked as future
|
||||
work ("asynchronous / buffered send … for notifications between servers"):
|
||||
|
||||
```
|
||||
ipc_send(handle, message_ptr, message_len) -> 0 / -errno
|
||||
```
|
||||
|
||||
`ipc_send` copies a small payload into the endpoint's **bounded queue** and wakes a
|
||||
receiver, then returns immediately — it never blocks and so can never hang on a dead or
|
||||
slow subscriber. The receiver picks it up through the same `replyWait` it already runs:
|
||||
the wake arrives as a **buffered message** — `notify_badge_bit | notify_message_bit` set in
|
||||
the badge (distinguishing it from a bare IRQ/child-exit notification), the sender's task id
|
||||
in the low bits, and the payload in the receive buffer, with no reply owed. The queue holds
|
||||
16 messages per endpoint; a full queue **drops the oldest**, because a buffered message is
|
||||
discrete data, not a coalescing "level" like an interrupt. See
|
||||
[ipc-synchronous.zig](../system/kernel/ipc-synchronous.zig) (`sendLocked`, `popPost`, and
|
||||
the `replyWait` receive loop).
|
||||
|
||||
This is the async counterpart of `ipc_call`, and the input service is its first consumer.
|
||||
|
||||
## How the pieces fit
|
||||
|
||||
```
|
||||
keyboard/mouse driver, input-source input service subscriber(s)
|
||||
----------------------------------- ------------- -------------
|
||||
connectSource(); loop: replyWait: subscribeKeyboard()/…All:
|
||||
publishKeyboardEvent(k) ─ ipc_call ─▶ publish → broadcast: createIpcEndpoint()
|
||||
publishMouseEvent(m) for each sub whose callCap(subscribe,
|
||||
publishJoystickEvent(j) mask matches event.device: send_cap = ep,
|
||||
ipc_send(sub_ep) ──────▶ device_mask)
|
||||
reply ok loop: next()
|
||||
subscribe → store {ep cap, └─ replyWait(ep)
|
||||
task id, device_mask} → InputEvent
|
||||
```
|
||||
|
||||
- A **subscriber** calls `input.subscribe(mask)` — or a typed helper: `subscribeKeyboard()`,
|
||||
`subscribeMouse()`, `subscribeJoystick()` (one class, `next()` returns the decoded event),
|
||||
or `subscribeAll()` (every class, `next()` returns a tagged `InputEvent`)
|
||||
([library/runtime/input.zig](../library/runtime/input.zig)). It creates its own endpoint
|
||||
and hands it to the service as a **capability** (M13 capability passing — the input
|
||||
service is that feature's first real user), along with its `device_mask`. Then it loops on
|
||||
`next()`, a `replyWait` on that endpoint returning each pushed event.
|
||||
- A **source** (a keyboard, mouse, or joystick driver) calls `input.connectSource()` and the
|
||||
method for its class: `publishKeyboardEvent`, `publishMouseEvent`, or
|
||||
`publishJoystickEvent`. Publishing is a short synchronous `ipc_call` the service answers at
|
||||
once; the service's own fan-out is asynchronous, so publishing never blocks on a slow
|
||||
subscriber.
|
||||
- The **service** ([input.zig](../system/services/input/input.zig)) keeps a small subscriber
|
||||
table (endpoint handle + owning task id + `device_mask`). On `publish` it `ipc_send`s the
|
||||
event to every subscriber whose mask includes the event's device class. On `subscribe` it
|
||||
stores the passed capability and mask and, as housekeeping, prunes any slot whose owning
|
||||
process has exited (checked against `process_enumerate`) — not for correctness (an async
|
||||
send to an orphaned endpoint is harmless) but to reclaim the slot.
|
||||
|
||||
Publisher and subscriber must be **separate processes**: a single thread that both
|
||||
published and serviced its own subscription would deadlock (its `publish` call blocks until
|
||||
the service delivers to its endpoint, which only the same thread could receive).
|
||||
|
||||
## Status and follow-ups
|
||||
|
||||
- **Synthetic sources, for now.** The `ps2-bus` driver owns PNP0303, which carries *both*
|
||||
the 0x60/0x64 ports and IRQ1, so reading real scancodes/packets has to live in the bus,
|
||||
not in [keyboard.zig](../system/drivers/ps2-bus/keyboard.zig) /
|
||||
[mouse.zig](../system/drivers/ps2-bus/mouse.zig). Until that lands, the keyboard driver
|
||||
publishes a synthetic key stream, the mouse driver a synthetic motion/click stream, and
|
||||
the hardware-free `input-source` rotates through all three classes (including a synthetic
|
||||
joystick, which has no driver yet) — all via the `input.synthetic*Event` helpers. The
|
||||
fan-out and per-device routing are real; only the bytes are placeholder. **Follow-up:** the
|
||||
bus binds IRQ1/IRQ12, reads port 0x60, and `ps2-library` decodes scan-set-1 →
|
||||
keycodes and mouse packets; the drivers publish decoded events.
|
||||
- **Keycode → character** is a keymap, and danos has one:
|
||||
[`library/xkeyboard-config`](../library/xkeyboard-config/README.md) compiles the X11
|
||||
xkeyboard-config layouts (us, gb, de, fr, …) to native Zig —
|
||||
`xkb.map(layout, keycode, mods)` → keysym + Unicode character. Wiring it in to fill a
|
||||
`key_press` event's `character` (in the keyboard driver, or a small keymap service) is the
|
||||
natural next step.
|
||||
- **Drop-oldest under overflow** is a defined loss; the 16-slot ring absorbs normal bursts.
|
||||
Real backpressure/flow-control is future work.
|
||||
- **`publish` is unauthenticated** — any process may publish, consistent with the current
|
||||
bring-up trust model (see [driver-model.md](driver-model.md)). A source capability is
|
||||
future work.
|
||||
|
||||
## Verifying it
|
||||
|
||||
The `input` case (`python3 test/qemu_test.py input`, in
|
||||
[tests.zig](../system/kernel/tests.zig) `inputTest`) boots the real kernel and spawns the
|
||||
service, the synthetic source (which cycles keyboard, mouse, and joystick events), and a
|
||||
subscriber that took all three classes. It passes only when the subscriber heartbeats
|
||||
`input-test: ok` — proof that an event travelled source → service → subscriber over IPC,
|
||||
exercising `ipc_send`, capability-passing subscription, and per-device routing. Each
|
||||
serial line names the class received, so the log shows all three arriving on one stream.
|
||||
|
||||
## See also
|
||||
|
||||
- [ipc.md](ipc.md) — the synchronous rendezvous and the notification path `ipc_send` extends.
|
||||
- [syscall.md](syscall.md) — the system-call surface, including `ipc_send`.
|
||||
- [driver-model.md](driver-model.md) — class drivers, capability passing (M13), the trust model.
|
||||
|
|
@ -95,6 +95,11 @@ This is what makes a user-space driver possible at all, and it's the subject of
|
|||
every capability is either well-known (the registry) or inherited — there's no way
|
||||
to delegate one.
|
||||
- **Asynchronous / buffered send** for the cases where a rendezvous is the wrong
|
||||
shape (logging, notifications between servers).
|
||||
shape (logging, notifications between servers). *Landed as `ipc_send`* — a
|
||||
non-blocking post to an endpoint's bounded payload queue, delivered through
|
||||
`reply_wait` as a buffered message (badge bit `notify_message_bit`). Built for, and
|
||||
first used by, the [input service](input.md)'s keyboard-event broadcast, where a
|
||||
synchronous push would let one dead subscriber hang the fan-out. A full queue drops
|
||||
the oldest (discrete messages, not a coalescing level like the notification ring).
|
||||
- **A bounded reply.** `MSG_MAX` is 256 bytes and the copy runs under the big kernel
|
||||
lock; a bulk transfer wants shared pages, not a copy.
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ Everything else---including`read()`,`write()`,`malloc()`, and`fork()`---will run
|
|||
- **What it does:**Used strictly by your background user-space servers (like your disk driver or filesystem). It sends a reply to the last client that called it, and immediately puts the server to sleep until the next request arrives.[[1](https://news.ycombinator.com/item?id=33078441)]
|
||||
3. **`Yield()`/`Thread_Ctrl()`**
|
||||
- **What it does:**Allows a thread to voluntarily give up its CPU time slice, or allows a root task to spawn/kill threads.
|
||||
4. **`ipc_send(endpoint, message_buffer)`(Asynchronous Send)**
|
||||
- **What it does:**Posts a small payload to an endpoint's bounded queue and returns *without* blocking — no rendezvous, no reply. The receiver picks it up through the same `IPC_ReplyWait`, as a buffered message. It is the async counterpart of `IPC_Call`, for one-to-many broadcasts where a synchronous rendezvous would let one dead or slow receiver hang the sender. The [input service](input.md) — keyboard-event fan-out — is its first user. A full queue drops the oldest message (a buffered message is discrete data, unlike a coalescing interrupt notification).
|
||||
|
||||
* * * * *
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
//! ownership of its hardware; the claim is the capability the kernel checks before
|
||||
//! mapping registers or routing an IRQ.
|
||||
|
||||
const std = @import("std");
|
||||
const abi = @import("abi");
|
||||
const device_abi = @import("device-abi");
|
||||
const sc = @import("system-call.zig");
|
||||
|
|
@ -108,3 +109,19 @@ pub fn ioRead(device_id: u64, resource_index: u64, offset: u64, width: u8) ?u32
|
|||
pub fn ioWrite(device_id: u64, resource_index: u64, offset: u64, width: u8, value: u32) bool {
|
||||
return !failed(sc.systemCall5(.io_write, device_id, resource_index, offset, width, value));
|
||||
}
|
||||
|
||||
/// Find DeviceDescription by hid
|
||||
///
|
||||
/// Utility function for driver development
|
||||
pub fn findDeviceDescriptorByHid(buffer: []DeviceDescriptor, hid_needle: []const u8) ?DeviceDescriptor {
|
||||
const total = enumerate(buffer);
|
||||
const n = @min(total, buffer.len);
|
||||
for (@as([]DeviceDescriptor, buffer[0..n])) |d| {
|
||||
const hid_haystack = d.hid[0..@intCast(d.hid_len)];
|
||||
if (std.mem.eql(u8, hid_haystack, hid_needle)) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
//! User-space input helpers: the client and publisher sides of the input service, so a
|
||||
//! program listening for input events — or a driver broadcasting them — doesn't hand-roll
|
||||
//! the IPC. Layered over `ipc` (endpoints, capability passing, `send`) and the shared
|
||||
//! `input-protocol` wire format, the way `device.zig` layers over the raw `device_*` calls.
|
||||
//! See system/services/input/input.zig.
|
||||
//!
|
||||
//! The service carries several device classes (keyboard, mouse, joystick/gamepad). A
|
||||
//! **source** publishes its class with the matching method:
|
||||
//! var source = input.connectSource() orelse return;
|
||||
//! _ = source.publishKeyboardEvent(.{ .kind = ..., .keycode = ..., ... });
|
||||
//! _ = source.publishMouseEvent(.{ ... });
|
||||
//! _ = source.publishJoystickEvent(.{ ... });
|
||||
//!
|
||||
//! A **subscriber** either takes one class with a typed helper —
|
||||
//! var keys = input.subscribeKeyboard() orelse return;
|
||||
//! while (true) { const key = keys.next() orelse continue; ... }
|
||||
//! — or takes several at once and inspects the tagged envelope:
|
||||
//! var listener = input.subscribeAll() orelse return;
|
||||
//! while (true) {
|
||||
//! const event = listener.next() orelse continue;
|
||||
//! if (event.asKeyboard()) |k| { ... } else if (event.asMouse()) |m| { ... }
|
||||
//! }
|
||||
|
||||
const std = @import("std");
|
||||
const abi = @import("abi");
|
||||
const ipc = @import("ipc.zig");
|
||||
const system = @import("system.zig");
|
||||
const protocol = @import("input-protocol");
|
||||
|
||||
pub const DeviceKind = protocol.DeviceKind;
|
||||
pub const InputEvent = protocol.InputEvent;
|
||||
pub const KeyEvent = protocol.KeyEvent;
|
||||
pub const MouseEvent = protocol.MouseEvent;
|
||||
pub const JoystickEvent = protocol.JoystickEvent;
|
||||
pub const EventKind = protocol.EventKind;
|
||||
pub const MouseEventKind = protocol.MouseEventKind;
|
||||
pub const JoystickEventKind = protocol.JoystickEventKind;
|
||||
pub const Keycode = protocol.Keycode;
|
||||
|
||||
/// Interest masks re-exported so a caller can `subscribe(input.device_keyboard | input.device_mouse)`.
|
||||
pub const device_keyboard = protocol.device_keyboard;
|
||||
pub const device_mouse = protocol.device_mouse;
|
||||
pub const device_joystick = protocol.device_joystick;
|
||||
pub const device_all = protocol.device_all;
|
||||
|
||||
/// Look up the input service, retrying while it is still coming up. Both a subscriber and
|
||||
/// a source race the service's registration at boot, so both wait for it here rather than
|
||||
/// failing. Returns the service endpoint handle, or null if it never appears.
|
||||
fn lookupService() ?ipc.Handle {
|
||||
var attempts: usize = 0;
|
||||
while (attempts < 100) : (attempts += 1) {
|
||||
if (ipc.lookup(.input)) |handle| return handle;
|
||||
system.sleep(50);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- subscribing ------------------------------------------------------------
|
||||
|
||||
/// A subscription to the input service: our own endpoint, which the service pushes events
|
||||
/// to. `next` returns each event as a tagged `InputEvent`; use `asKeyboard`/`asMouse`/
|
||||
/// `asJoystick` to decode. Created with `subscribe`/`subscribeAll`; for a single device
|
||||
/// class prefer the typed helpers (`subscribeKeyboard`, ...), which return decoded events.
|
||||
pub const Subscriber = struct {
|
||||
/// The endpoint the service delivers events to (created and owned by us; its handle
|
||||
/// was handed to the service as a capability at subscribe time).
|
||||
endpoint: ipc.Handle,
|
||||
receive: [protocol.event_size]u8 = undefined,
|
||||
|
||||
/// Block until the next event is pushed, and return it. Events arrive as asynchronous
|
||||
/// buffered messages (`ipc_send` from the service), so nothing is owed in reply — the
|
||||
/// empty reply this issues is a harmless no-op. Returns null for any non-event wake-up
|
||||
/// (there should be none), so callers can loop.
|
||||
pub fn next(self: *Subscriber) ?InputEvent {
|
||||
const got = ipc.replyWait(self.endpoint, &.{}, &self.receive, null);
|
||||
if (!got.isMessage() or got.len < protocol.event_size) return null;
|
||||
return std.mem.bytesToValue(InputEvent, self.receive[0..protocol.event_size]);
|
||||
}
|
||||
};
|
||||
|
||||
/// Subscribe to the input classes named in `device_mask` (an OR of `device_*`, or
|
||||
/// `device_all`). Creates an endpoint for the service to push to and hands it over as a
|
||||
/// capability. Returns a `Subscriber` to loop `next` on, or null on failure.
|
||||
pub fn subscribe(device_mask: u32) ?Subscriber {
|
||||
const service = lookupService() orelse return null;
|
||||
const endpoint = ipc.createIpcEndpoint() orelse return null;
|
||||
|
||||
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.subscribe), .device_mask = device_mask };
|
||||
var reply: [protocol.reply_size]u8 = undefined;
|
||||
const result = ipc.callCap(service, std.mem.asBytes(&request), &reply, endpoint) catch return null;
|
||||
if (result.len < protocol.reply_size) return null;
|
||||
if (std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status != 0) return null;
|
||||
return .{ .endpoint = endpoint };
|
||||
}
|
||||
|
||||
/// Subscribe to every input class (keyboard, mouse, joystick) on one stream.
|
||||
pub fn subscribeAll() ?Subscriber {
|
||||
return subscribe(device_all);
|
||||
}
|
||||
|
||||
/// A subscriber filtered to keyboard events, whose `next` returns a decoded `KeyEvent`.
|
||||
pub const KeyboardSubscriber = struct {
|
||||
inner: Subscriber,
|
||||
pub fn next(self: *KeyboardSubscriber) ?KeyEvent {
|
||||
return (self.inner.next() orelse return null).asKeyboard();
|
||||
}
|
||||
};
|
||||
|
||||
/// A subscriber filtered to mouse events, whose `next` returns a decoded `MouseEvent`.
|
||||
pub const MouseSubscriber = struct {
|
||||
inner: Subscriber,
|
||||
pub fn next(self: *MouseSubscriber) ?MouseEvent {
|
||||
return (self.inner.next() orelse return null).asMouse();
|
||||
}
|
||||
};
|
||||
|
||||
/// A subscriber filtered to joystick/gamepad events, whose `next` returns a decoded
|
||||
/// `JoystickEvent`.
|
||||
pub const JoystickSubscriber = struct {
|
||||
inner: Subscriber,
|
||||
pub fn next(self: *JoystickSubscriber) ?JoystickEvent {
|
||||
return (self.inner.next() orelse return null).asJoystick();
|
||||
}
|
||||
};
|
||||
|
||||
/// Subscribe to keyboard events only; `next` returns decoded `KeyEvent`s.
|
||||
pub fn subscribeKeyboard() ?KeyboardSubscriber {
|
||||
return .{ .inner = subscribe(device_keyboard) orelse return null };
|
||||
}
|
||||
|
||||
/// Subscribe to mouse events only; `next` returns decoded `MouseEvent`s.
|
||||
pub fn subscribeMouse() ?MouseSubscriber {
|
||||
return .{ .inner = subscribe(device_mouse) orelse return null };
|
||||
}
|
||||
|
||||
/// Subscribe to joystick/gamepad events only; `next` returns decoded `JoystickEvent`s.
|
||||
pub fn subscribeJoystick() ?JoystickSubscriber {
|
||||
return .{ .inner = subscribe(device_joystick) orelse return null };
|
||||
}
|
||||
|
||||
// --- publishing -------------------------------------------------------------
|
||||
|
||||
/// A connection to the input service for a source (a keyboard/mouse/joystick driver) that
|
||||
/// publishes events. Each `publish*Event` is a short synchronous call the service answers
|
||||
/// at once; its own fan-out to subscribers is asynchronous, so publishing never blocks on
|
||||
/// a slow subscriber.
|
||||
pub const Publisher = struct {
|
||||
service: ipc.Handle,
|
||||
|
||||
fn publish(self: Publisher, event: InputEvent) bool {
|
||||
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.publish), .event = event };
|
||||
var reply: [protocol.reply_size]u8 = undefined;
|
||||
const len = ipc.call(self.service, std.mem.asBytes(&request), &reply) catch return false;
|
||||
if (len < protocol.reply_size) return false;
|
||||
return std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status == 0;
|
||||
}
|
||||
|
||||
/// Broadcast a keyboard event to every subscriber that took keyboard events.
|
||||
pub fn publishKeyboardEvent(self: Publisher, event: KeyEvent) bool {
|
||||
return self.publish(InputEvent.fromKeyboard(event));
|
||||
}
|
||||
/// Broadcast a mouse event to every subscriber that took mouse events.
|
||||
pub fn publishMouseEvent(self: Publisher, event: MouseEvent) bool {
|
||||
return self.publish(InputEvent.fromMouse(event));
|
||||
}
|
||||
/// Broadcast a joystick/gamepad event to every subscriber that took joystick events.
|
||||
pub fn publishJoystickEvent(self: Publisher, event: JoystickEvent) bool {
|
||||
return self.publish(InputEvent.fromJoystick(event));
|
||||
}
|
||||
};
|
||||
|
||||
/// Connect to the input service as an event source, waiting for it to come up. Returns a
|
||||
/// `Publisher`, or null if the service never registered.
|
||||
pub fn connectSource() ?Publisher {
|
||||
return .{ .service = lookupService() orelse return null };
|
||||
}
|
||||
|
||||
// --- synthetic scaffolding --------------------------------------------------
|
||||
|
||||
/// Synthetic key events, shared by the demo source and the keyboard driver's placeholder
|
||||
/// stream while real scancode decoding is still a follow-up. `step` rolls through A..E,
|
||||
/// emitting for each key a `key_down`, then a `key_press` carrying the character, then a
|
||||
/// `key_up`. Scaffolding, not wire protocol — hence it lives with the helpers.
|
||||
pub fn syntheticKeyEvent(step: usize) KeyEvent {
|
||||
const Key = struct { code: Keycode, character: u32 };
|
||||
const keys = [_]Key{
|
||||
.{ .code = .a, .character = 'A' },
|
||||
.{ .code = .b, .character = 'B' },
|
||||
.{ .code = .c, .character = 'C' },
|
||||
.{ .code = .d, .character = 'D' },
|
||||
.{ .code = .e, .character = 'E' },
|
||||
};
|
||||
const key = keys[(step / 3) % keys.len];
|
||||
return switch (step % 3) {
|
||||
0 => .{ .kind = @intFromEnum(EventKind.key_down), .keycode = @intFromEnum(key.code), .character = 0, .modifiers = 0 },
|
||||
1 => .{ .kind = @intFromEnum(EventKind.key_press), .keycode = @intFromEnum(key.code), .character = key.character, .modifiers = 0 },
|
||||
else => .{ .kind = @intFromEnum(EventKind.key_up), .keycode = @intFromEnum(key.code), .character = 0, .modifiers = 0 },
|
||||
};
|
||||
}
|
||||
|
||||
/// Synthetic mouse events (placeholder until real PS/2 packet decoding). `step` alternates
|
||||
/// a small diagonal motion with a left-button click.
|
||||
pub fn syntheticMouseEvent(step: usize) MouseEvent {
|
||||
return switch (step % 3) {
|
||||
0 => .{ .kind = @intFromEnum(MouseEventKind.motion), .button = 0, .dx = 1, .dy = 1, .scroll_x = 0, .scroll_y = 0, .buttons = 0 },
|
||||
1 => .{ .kind = @intFromEnum(MouseEventKind.button_down), .button = protocol.mouse_button_left, .dx = 0, .dy = 0, .scroll_x = 0, .scroll_y = 0, .buttons = protocol.mouse_button_left },
|
||||
else => .{ .kind = @intFromEnum(MouseEventKind.button_up), .button = protocol.mouse_button_left, .dx = 0, .dy = 0, .scroll_x = 0, .scroll_y = 0, .buttons = 0 },
|
||||
};
|
||||
}
|
||||
|
||||
/// Synthetic joystick/gamepad events (placeholder until a real controller driver). `step`
|
||||
/// sweeps axis 0 and toggles button 0.
|
||||
pub fn syntheticJoystickEvent(step: usize) JoystickEvent {
|
||||
return switch (step % 3) {
|
||||
0 => .{ .kind = @intFromEnum(JoystickEventKind.axis), .control = 0, .value = 16384, .buttons = 0 },
|
||||
1 => .{ .kind = @intFromEnum(JoystickEventKind.button_down), .control = 0, .value = 0, .buttons = 1 },
|
||||
else => .{ .kind = @intFromEnum(JoystickEventKind.button_up), .control = 0, .value = 0, .buttons = 0 },
|
||||
};
|
||||
}
|
||||
|
|
@ -79,6 +79,16 @@ pub fn call(h: Handle, message: []const u8, reply: []u8) CallError!usize {
|
|||
return (try callCap(h, message, reply, null)).len;
|
||||
}
|
||||
|
||||
/// Post `message` to endpoint `h`'s asynchronous queue and return immediately — no
|
||||
/// rendezvous, no reply, no blocking. The receiver picks it up through `replyWait` as a
|
||||
/// buffered message (`Received.isMessage`). Unlike `call`, this **cannot hang on a dead
|
||||
/// or slow peer**, which is why a broadcaster (the input service) delivers events this
|
||||
/// way. The payload must fit an endpoint slot (64 bytes); a full queue drops the oldest
|
||||
/// message. Returns false on failure (bad handle, oversized payload, bad buffer).
|
||||
pub fn send(h: Handle, message: []const u8) bool {
|
||||
return !failed(sc.systemCall3(.ipc_send, h, @intFromPtr(message.ptr), message.len));
|
||||
}
|
||||
|
||||
/// Set in `Received.badge` when what arrived is an asynchronous notification — a
|
||||
/// bound device interrupt — rather than a client's message. The low bits carry the
|
||||
/// GSI. See `isNotification`.
|
||||
|
|
@ -89,6 +99,12 @@ pub const notify_badge_bit: u64 = abi.notify_badge_bit;
|
|||
/// rather than a device interrupt. The low bits carry the child's process id.
|
||||
pub const notify_exit_bit: u64 = abi.notify_exit_bit;
|
||||
|
||||
/// Set alongside `notify_badge_bit` when the wake-up is a **buffered message** — a payload
|
||||
/// posted with `send` (`ipc_send`) — rather than a bare device interrupt or child-exit
|
||||
/// notice. The payload is in the `replyWait` receive buffer (`Received.len` bytes); the
|
||||
/// low bits of the badge carry the sender's task id. See `Received.isMessage`.
|
||||
pub const notify_message_bit: u64 = abi.notify_message_bit;
|
||||
|
||||
/// The result of a `replyWait`: the request length, the sender's badge (a task id, or
|
||||
/// an IRQ notification if the high bit is set), and any capability the request carried.
|
||||
pub const Received = struct {
|
||||
|
|
@ -109,6 +125,19 @@ pub const Received = struct {
|
|||
return self.isNotification() and self.badge & notify_exit_bit != 0;
|
||||
}
|
||||
|
||||
/// True if this wake-up is a **buffered message** posted with `send` (`ipc_send`):
|
||||
/// there is a payload in the receive buffer (`self.len` bytes) and no reply is owed.
|
||||
/// The subscriber side of a broadcast branches on this.
|
||||
pub fn isMessage(self: Received) bool {
|
||||
return self.isNotification() and self.badge & notify_message_bit != 0;
|
||||
}
|
||||
|
||||
/// The task id of whoever posted a buffered message, meaningful only when
|
||||
/// `isMessage`. (The badge's low bits, with the three high marker bits masked off.)
|
||||
pub fn senderTaskId(self: Received) u32 {
|
||||
return @intCast(self.badge & ~(notify_badge_bit | notify_exit_bit | notify_message_bit));
|
||||
}
|
||||
|
||||
/// The interrupt source (a GSI), meaningful only when `isNotification` and
|
||||
/// not `isChildExit`.
|
||||
pub fn source(self: Received) u64 {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ pub const ipc = @import("ipc.zig");
|
|||
pub const start = @import("start.zig");
|
||||
/// The VFS wire protocol (shared with the VFS server).
|
||||
pub const vfs_protocol = @import("vfs-protocol");
|
||||
/// Keyboard-event listening (subscribe/next) and broadcasting (publish), over the input
|
||||
/// service. See library/runtime/input.zig and system/services/input/.
|
||||
pub const input = @import("input.zig");
|
||||
/// The input wire protocol (shared with the input service and its clients).
|
||||
pub const input_protocol = @import("input-protocol");
|
||||
/// POSIX-style file API: open/read/write/lseek/stat/close.
|
||||
/// C stdio: fopen/fread/fwrite/fseek/ftell/fclose over unistd.
|
||||
/// Device access for drivers: enumerate/claim/mmioMap.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
//! stubs, one per kernel call. Numbers come from `abi.SystemCall`, the single
|
||||
//! source of truth shared with the kernel dispatcher.
|
||||
|
||||
const std = @import("std");
|
||||
const abi = @import("abi");
|
||||
const sc = @import("system-call.zig");
|
||||
|
||||
|
|
@ -98,6 +99,16 @@ pub fn processes(out: []abi.ProcessDescriptor) usize {
|
|||
return sc.systemCall2(.process_enumerate, @intFromPtr(out.ptr), out.len);
|
||||
}
|
||||
|
||||
/// Whether a process spawned under `name` (its argv[0]) is currently alive.
|
||||
pub fn isProcessRunning(name: []const u8) bool {
|
||||
var table: [32]ProcessDescriptor = undefined;
|
||||
const total = processes(&table);
|
||||
for (table[0..@min(total, table.len)]) |descriptor| {
|
||||
if (std.mem.eql(u8, descriptor.name[0..descriptor.name_length], name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// End process `id`. Only its supervisor — the process that spawned it — may;
|
||||
/// anyone else gets false, as does a stale or unknown id (ids are never reused).
|
||||
/// Delivery is prompt but asynchronous, like a signal: a target caught running on
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
# xkeyboard-config — X11 keyboard layouts, compiled to Zig
|
||||
|
||||
This module turns a physical key (a **USB HID usage**, as the [input module](../../docs/input.md)
|
||||
delivers in `KeyEvent.keycode`) plus a modifier state into a **keysym** and, when the key
|
||||
produces one, a **character** (a Unicode scalar). It is what lets a `keycode` become a
|
||||
`character` — a keymap — without danos shipping an X11 runtime.
|
||||
|
||||
The layout data comes from the X11 [xkeyboard-config](https://gitlab.freedesktop.org/xkeyboard-config/xkeyboard-config)
|
||||
database, but it is **compiled to native Zig at build time** rather than parsed at runtime.
|
||||
`tools/make-xkeyboard-config.py` reads the vendored xkb data and emits pure-data tables into
|
||||
`generated/layouts.zig`; `xkeyboard-config.zig` is the hand-written API over them. This is
|
||||
the same build-time-codegen pattern as `tools/make-initial-ramdisk.py`.
|
||||
|
||||
## Using it
|
||||
|
||||
```zig
|
||||
const xkb = @import("xkeyboard-config");
|
||||
|
||||
const m = xkb.map(xkb.us, key_event.keycode, .{ .shift = shift_held, .caps_lock = caps });
|
||||
if (m.character) |ch| { /* a printable Unicode scalar */ }
|
||||
// m.keysym is always set (e.g. an X11 keysym for Return / F1 / a dead key).
|
||||
|
||||
const layout = xkb.byName("gb") orelse xkb.us; // choose a layout by name
|
||||
for (xkb.all) |l| { /* enumerate available layouts */ }
|
||||
```
|
||||
|
||||
`Modifiers` carries `shift`, `caps_lock`, `level3` (AltGr), and `control`. `map` selects the
|
||||
level from the key's XKB *type* (the generated data) and those modifiers (the policy, in
|
||||
`xkeyboard-config.zig`), so data and semantics stay separable.
|
||||
|
||||
Layouts: **us, gb, de, fr, es, dvorak**.
|
||||
|
||||
## Regenerating
|
||||
|
||||
```sh
|
||||
python3 tools/make-xkeyboard-config.py fetch # network: download + vendor the data subset
|
||||
python3 tools/make-xkeyboard-config.py generate # offline: emit generated/layouts.zig
|
||||
# or, from the build:
|
||||
zig build gen-xkeyboard-config
|
||||
```
|
||||
|
||||
- **`fetch`** downloads the pinned xkeyboard-config release (version + sha256 in the script),
|
||||
resolves the `include` graph for the configured layouts, and vendors *only* the symbols
|
||||
files actually reached (plus `keysymdef.h`, `COPYING`, and `PROVENANCE.md`) into `vendor/`.
|
||||
Run it when bumping the version or adding a layout.
|
||||
- **`generate`** is deterministic and offline — same vendored input produces byte-identical
|
||||
output. To add a layout, extend `TARGETS` (and `HID_TO_NAME` if a new physical key is
|
||||
involved), then re-run `fetch` (to vendor any new includes) and `generate`.
|
||||
|
||||
## Scope
|
||||
|
||||
A pragmatic subset, enough for real Latin-script typing:
|
||||
|
||||
- **Group 1 only** — no multi-layout group switching.
|
||||
- **No dead-key / compose composition** — a dead key returns its keysym with no `character`
|
||||
(composing `´` + `e` → `é` is a higher layer's job).
|
||||
- **Curated key types** — the common XKB types (one/two-level, alphabetic, four-level, …);
|
||||
unmapped keys and unknown types fall back to level-by-shift.
|
||||
- **6 layouts** — extend via `TARGETS` as above.
|
||||
|
||||
## Licensing
|
||||
|
||||
xkeyboard-config and `keysymdef.h` (xorgproto) are MIT/X11 licensed. The vendored data
|
||||
subset carries the upstream `vendor/COPYING`, and `vendor/PROVENANCE.md` records the exact
|
||||
version, source URL, and sha256. The generated tables are a derived work under the same terms.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,190 @@
|
|||
Copyright 1996 by Joseph Moss
|
||||
Copyright (C) 2002-2007 Free Software Foundation, Inc.
|
||||
Copyright (C) Dmitry Golubev <lastguru@mail.ru>, 2003-2004
|
||||
Copyright (C) 2004, Gregory Mokhin <mokhin@bog.msu.ru>
|
||||
Copyright (C) 2006 Erdal Ronahî
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software and its
|
||||
documentation for any purpose is hereby granted without fee, provided that
|
||||
the above copyright notice appear in all copies and that both that
|
||||
copyright notice and this permission notice appear in supporting
|
||||
documentation, and that the name of the copyright holder(s) not be used in
|
||||
advertising or publicity pertaining to distribution of the software without
|
||||
specific, written prior permission. The copyright holder(s) makes no
|
||||
representations about the suitability of this software for any purpose. It
|
||||
is provided "as is" without express or implied warranty.
|
||||
|
||||
THE COPYRIGHT HOLDER(S) DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
|
||||
INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
|
||||
EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR
|
||||
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
|
||||
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
Copyright (c) 1996 Digital Equipment Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL DIGITAL EQUIPMENT CORPORATION BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
|
||||
THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of the Digital Equipment
|
||||
Corporation shall not be used in advertising or otherwise to promote
|
||||
the sale, use or other dealings in this Software without prior written
|
||||
authorization from Digital Equipment Corporation.
|
||||
|
||||
|
||||
Copyright 1996, 1998 The Open Group
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software and its
|
||||
documentation for any purpose is hereby granted without fee, provided that
|
||||
the above copyright notice appear in all copies and that both that
|
||||
copyright notice and this permission notice appear in supporting
|
||||
documentation.
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of The Open Group shall
|
||||
not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization
|
||||
from The Open Group.
|
||||
|
||||
|
||||
Copyright 2004-2005 Sun Microsystems, Inc. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
Copyright (c) 1996 by Silicon Graphics Computer Systems, Inc.
|
||||
|
||||
Permission to use, copy, modify, and distribute this
|
||||
software and its documentation for any purpose and without
|
||||
fee is hereby granted, provided that the above copyright
|
||||
notice appear in all copies and that both that copyright
|
||||
notice and this permission notice appear in supporting
|
||||
documentation, and that the name of Silicon Graphics not be
|
||||
used in advertising or publicity pertaining to distribution
|
||||
of the software without specific prior written permission.
|
||||
Silicon Graphics makes no representation about the suitability
|
||||
of this software for any purpose. It is provided "as is"
|
||||
without any express or implied warranty.
|
||||
|
||||
SILICON GRAPHICS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
|
||||
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON
|
||||
GRAPHICS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
|
||||
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
|
||||
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH
|
||||
THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
Copyright (c) 1996 X Consortium
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of the X Consortium shall
|
||||
not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization
|
||||
from the X Consortium.
|
||||
|
||||
|
||||
Copyright (C) 2004, 2006 Ævar Arnfjörð Bjarmason <avarab@gmail.com>
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software and its
|
||||
documentation for any purpose is hereby granted without fee, provided that
|
||||
the above copyright notice appear in all copies and that both that
|
||||
copyright notice and this permission notice appear in supporting
|
||||
documentation.
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of a copyright holder shall
|
||||
not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization of
|
||||
the copyright holder.
|
||||
|
||||
|
||||
Copyright (C) 1999, 2000 by Anton Zinoviev <anton@lml.bas.bg>
|
||||
|
||||
This software may be used, modified, copied, distributed, and sold,
|
||||
in both source and binary form provided that the above copyright
|
||||
and these terms are retained. Under no circumstances is the author
|
||||
responsible for the proper functioning of this software, nor does
|
||||
the author assume any responsibility for damages incurred with its
|
||||
use.
|
||||
|
||||
Permission is granted to anyone to use, distribute and modify
|
||||
this file in any way, provided that the above copyright notice
|
||||
is left intact and the author of the modification summarizes
|
||||
the changes in this header.
|
||||
|
||||
This file is distributed without any expressed or implied warranty.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Vendored xkeyboard-config subset
|
||||
|
||||
- **Package**: xkeyboard-config 2.44
|
||||
- **Source**: https://gitlab.freedesktop.org/xkeyboard-config/xkeyboard-config/-/archive/xkeyboard-config-2.44/xkeyboard-config-2.44.tar.gz
|
||||
- **sha256**: `35e34edeaf4e8da8d0696ff6b241ee11ddb1b8c6730bac7252d4d0a88ea5f05b`
|
||||
- **keysymdef.h**: xorgproto, copied from `/opt/homebrew/include/X11/keysymdef.h`
|
||||
- **License**: MIT/X11 (see COPYING)
|
||||
|
||||
Only the symbols files reachable from the generated layouts (tools/make-xkeyboard-config.py `TARGETS`) are vendored; regenerate with
|
||||
`python3 tools/make-xkeyboard-config.py fetch` then `... generate`.
|
||||
|
||||
Vendored symbols files:
|
||||
|
||||
- `symbols/de`
|
||||
- `symbols/es`
|
||||
- `symbols/fr`
|
||||
- `symbols/gb`
|
||||
- `symbols/kpdl`
|
||||
- `symbols/latin`
|
||||
- `symbols/level3`
|
||||
- `symbols/us`
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,250 @@
|
|||
// Keyboard layouts for Spain.
|
||||
|
||||
// Modified for a real Spanish keyboard by Jon Tombs.
|
||||
default partial alphanumeric_keys
|
||||
xkb_symbols "basic" {
|
||||
|
||||
include "latin(type4)"
|
||||
|
||||
name[Group1]="Spanish";
|
||||
|
||||
key <TLDE> { [ masculine, ordfeminine, backslash, backslash ] };
|
||||
key <AE01> { [ 1, exclam, bar, exclamdown ] };
|
||||
key <AE03> { [ 3, periodcentered, numbersign, sterling ] };
|
||||
key <AE04> { [ 4, dollar, asciitilde, dollar ] };
|
||||
key <AE11> { [apostrophe, question, backslash, questiondown ] };
|
||||
key <AE12> { [exclamdown, questiondown, dead_cedilla, dead_ogonek] };
|
||||
|
||||
key <AD11> { [dead_grave, dead_circumflex, bracketleft, dead_abovering ] };
|
||||
key <AD12> { [ plus, asterisk, bracketright, dead_macron ] };
|
||||
|
||||
key <AC10> { [ ntilde, Ntilde, dead_tilde, dead_doubleacute ] };
|
||||
key <AC11> { [dead_acute, dead_diaeresis, braceleft, dead_caron ] };
|
||||
key <BKSL> { [ ccedilla, Ccedilla, braceright, dead_breve ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "winkeys" {
|
||||
|
||||
include "es(basic)"
|
||||
name[Group1]="Spanish (Windows)";
|
||||
include "eurosign(5)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "nodeadkeys" {
|
||||
|
||||
include "es(basic)"
|
||||
|
||||
name[Group1]="Spanish (no dead keys)";
|
||||
|
||||
key <AE12> { [exclamdown, questiondown, cedilla, ogonek ] };
|
||||
key <AD11> { [ grave, asciicircum, bracketleft, degree ] };
|
||||
key <AD12> { [ plus, asterisk, bracketright, macron ] };
|
||||
key <AC07> { [ j, J, ezh, EZH ] };
|
||||
key <AC10> { [ ntilde, Ntilde, asciitilde, doubleacute ] };
|
||||
key <AC11> { [ acute, diaeresis, braceleft, caron ] };
|
||||
key <BKSL> { [ ccedilla, Ccedilla, braceright, breve ] };
|
||||
key <AB10> { [ minus, underscore, ellipsis, abovedot ] };
|
||||
};
|
||||
|
||||
// Spanish Dvorak mapping (note R-H exchange)
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "dvorak" {
|
||||
|
||||
name[Group1]="Spanish (Dvorak)";
|
||||
|
||||
key <TLDE> {[ masculine, ordfeminine, backslash, degree ]};
|
||||
key <AE01> {[ 1, exclam, bar, onesuperior ]};
|
||||
key <AE02> {[ 2, quotedbl, at, twosuperior ]};
|
||||
key <AE03> {[ 3, periodcentered, numbersign, threesuperior ]};
|
||||
key <AE04> {[ 4, dollar, asciitilde, onequarter ]};
|
||||
key <AE05> {[ 5, percent, brokenbar, fiveeighths ]};
|
||||
key <AE06> {[ 6, ampersand, notsign, threequarters ]};
|
||||
key <AE07> {[ 7, slash, onehalf, seveneighths ]};
|
||||
key <AE08> {[ 8, parenleft, oneeighth, threeeighths ]};
|
||||
key <AE09> {[ 9, parenright, asciicircum ]};
|
||||
key <AE10> {[ 0, equal, grave, dead_doubleacute ]};
|
||||
key <AE11> {[ apostrophe, question, dead_macron, dead_ogonek ]};
|
||||
key <AE12> {[ exclamdown, questiondown, dead_breve, dead_abovedot ]};
|
||||
|
||||
key <AD01> {[ period, colon, less, guillemotleft ]};
|
||||
key <AD02> {[ comma, semicolon, greater, guillemotright ]};
|
||||
key <AD03> {[ ntilde, Ntilde, lstroke, Lstroke ]};
|
||||
key <AD04> {[ p, P, paragraph ]};
|
||||
key <AD05> {[ y, Y, yen ]};
|
||||
key <AD06> {[ f, F, tslash, Tslash ]};
|
||||
key <AD07> {[ g, G, dstroke, Dstroke ]};
|
||||
key <AD08> {[ c, C, cent, copyright ]};
|
||||
key <AD09> {[ h, H, hstroke, Hstroke ]};
|
||||
key <AD10> {[ l, L, sterling ]};
|
||||
key <AD11> {[ dead_grave, dead_circumflex, bracketleft, dead_caron ]};
|
||||
key <AD12> {[ plus, asterisk, bracketright, plusminus ]};
|
||||
|
||||
key <AC01> {[ a, A, ae, AE ]};
|
||||
key <AC02> {[ o, O, oslash, Oslash ]};
|
||||
key <AC03> {[ e, E, EuroSign ]};
|
||||
key <AC04> {[ u, U, aring, Aring ]};
|
||||
key <AC05> {[ i, I, oe, OE ]};
|
||||
key <AC06> {[ d, D, eth, ETH ]};
|
||||
key <AC07> {[ r, R, registered, trademark ]};
|
||||
key <AC08> {[ t, T, thorn, THORN ]};
|
||||
key <AC09> {[ n, N, eng, ENG ]};
|
||||
key <AC10> {[ s, S, ssharp, section ]};
|
||||
key <AC11> {[ dead_acute, dead_diaeresis, braceleft, dead_tilde ]};
|
||||
key <BKSL> {[ ccedilla, Ccedilla, braceright, dead_cedilla ]};
|
||||
|
||||
key <LSGT> {[ less, greater, guillemotleft, guillemotright ]};
|
||||
key <AB01> {[ minus, underscore, hyphen, macron ]};
|
||||
key <AB02> {[ q, Q, currency ]};
|
||||
key <AB03> {[ j, J ]};
|
||||
key <AB04> {[ k, K, kra ]};
|
||||
key <AB05> {[ x, X, multiply, division ]};
|
||||
key <AB06> {[ b, B ]};
|
||||
key <AB07> {[ m, M, mu ]};
|
||||
key <AB08> {[ w, W ]};
|
||||
key <AB09> {[ v, V ]};
|
||||
key <AB10> {[ z, Z ]};
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "cat" {
|
||||
|
||||
include "es(basic)"
|
||||
|
||||
name[Group1]="Catalan (Spain, with middle-dot L)";
|
||||
|
||||
key <AC09> { [ l, L, 0x1000140, 0x100013F ] };
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "ast" {
|
||||
|
||||
include "es(basic)"
|
||||
|
||||
name[Group1]="Asturian (Spain, with bottom-dot H and L)";
|
||||
|
||||
key <AC06> { [ h, H, 0x1001E25, 0x1001E24 ] };
|
||||
key <AC09> { [ l, L, 0x1001E37, 0x1001E36 ] };
|
||||
};
|
||||
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "olpc" {
|
||||
|
||||
// #HW-SPECIFIC
|
||||
|
||||
// http://wiki.laptop.org/go/OLPC_Spanish_Keyboard
|
||||
|
||||
include "us(basic)"
|
||||
name[Group1]="Spanish";
|
||||
|
||||
key <AE00> { [ masculine, ordfeminine ] };
|
||||
key <AE01> { [ 1, exclam, bar ] };
|
||||
key <AE02> { [ 2, quotedbl, at ] };
|
||||
key <AE03> { [ 3, dead_grave, numbersign, grave ] };
|
||||
key <AE05> { [ 5, percent, asciicircum, dead_circumflex ] };
|
||||
key <AE06> { [ 6, ampersand, notsign ] };
|
||||
key <AE07> { [ 7, slash, backslash ] };
|
||||
key <AE08> { [ 8, parenleft ] };
|
||||
key <AE09> { [ 9, parenright ] };
|
||||
key <AE10> { [ 0, equal ] };
|
||||
key <AE11> { [ apostrophe, question ] };
|
||||
key <AE12> { [ exclamdown, questiondown ] };
|
||||
|
||||
key <AD03> { [ e, E, EuroSign ] };
|
||||
key <AD11> { [ dead_acute, dead_diaeresis, acute, dead_abovering ] };
|
||||
key <AD12> { [ bracketleft, braceleft ] };
|
||||
|
||||
key <AC10> { [ ntilde, Ntilde ] };
|
||||
key <AC11> { [ plus, asterisk, dead_tilde ] };
|
||||
key <AC12> { [ bracketright, braceright, section ] };
|
||||
|
||||
key <AB08> { [ comma, semicolon ] };
|
||||
key <AB09> { [ period, colon ] };
|
||||
key <AB10> { [ minus, underscore ] };
|
||||
|
||||
key <I219> { [ less, greater, ISO_Next_Group ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "olpcm" {
|
||||
|
||||
// #HW-SPECIFIC
|
||||
|
||||
// Mechanical (non-membrane) OLPC Spanish keyboard layout.
|
||||
// See: http://wiki.laptop.org/go/OLPC_Spanish_Non-membrane_Keyboard
|
||||
|
||||
include "us(basic)"
|
||||
name[Group1]="Spanish";
|
||||
|
||||
key <AE00> { [ questiondown, exclamdown, backslash ] };
|
||||
key <AE01> { [ 1, exclam, bar ] };
|
||||
key <AE02> { [ 2, quotedbl, at ] };
|
||||
key <AE03> { [ 3, dead_grave, numbersign, grave ] };
|
||||
key <AE04> { [ 4, dollar, asciitilde, dead_tilde ] };
|
||||
key <AE05> { [ 5, percent, asciicircum, dead_circumflex ] };
|
||||
key <AE06> { [ 6, ampersand, notsign ] };
|
||||
key <AE07> { [ 7, slash, backslash ] }; // no '\' label on olpcm, leave for compatibility
|
||||
key <AE08> { [ 8, parenleft, masculine ] };
|
||||
key <AE09> { [ 9, parenright, ordfeminine ] };
|
||||
key <AE10> { [ 0, equal ] };
|
||||
key <AE11> { [ apostrophe, question ] };
|
||||
|
||||
key <AD03> { [ e, E, EuroSign ] };
|
||||
key <AD11> { [ dead_acute, dead_diaeresis, dead_abovering, acute ] };
|
||||
key <AD12> { [ plus, asterisk ] };
|
||||
|
||||
key <AC10> { [ ntilde, Ntilde ] };
|
||||
// no AC11 or AC12 on olpcm
|
||||
|
||||
key <AB08> { [ comma, semicolon ] };
|
||||
key <AB09> { [ period, colon ] };
|
||||
key <AB10> { [ minus, underscore ] };
|
||||
|
||||
key <AA02> { [ less, greater ] };
|
||||
key <AA06> { [ bracketleft, braceleft, ccedilla, Ccedilla ] };
|
||||
key <AA07> { [ bracketright, braceright ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "deadtilde" {
|
||||
|
||||
include "es(basic)"
|
||||
|
||||
name[Group1]="Spanish (dead tilde)";
|
||||
|
||||
key <AE04> { [ 4, dollar, dead_tilde, dollar ] };
|
||||
key <AC10> { [ ntilde, Ntilde, asciitilde, dead_doubleacute ] };
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "olpc2" {
|
||||
// #HW-SPECIFIC
|
||||
|
||||
// Modified variant of US International layout, specifically for Peru
|
||||
// Contact: Sayamindu Dasgupta <sayamindu@laptop.org>
|
||||
|
||||
include "us(olpc)"
|
||||
name[Group1]="Spanish";
|
||||
|
||||
key <AE03> { [ 3, numbersign, dead_grave, dead_grave] }; // combining grave
|
||||
key <I236> { [ XF86Start ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
// EXTRAS:
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "sun_type6" {
|
||||
include "sun_vndr/es(sun_type6)"
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,249 @@
|
|||
// Keyboard layouts for Great Britain.
|
||||
|
||||
default partial alphanumeric_keys
|
||||
xkb_symbols "basic" {
|
||||
|
||||
// The basic UK layout, also known as the IBM 166 layout,
|
||||
// but with the useless brokenbar pushed two levels up.
|
||||
|
||||
include "latin"
|
||||
|
||||
name[Group1]="English (UK)";
|
||||
|
||||
key <TLDE> { [ grave, notsign, bar, bar ] };
|
||||
key <AE02> { [ 2, quotedbl, twosuperior, oneeighth ] };
|
||||
key <AE03> { [ 3, sterling, threesuperior, sterling ] };
|
||||
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||
|
||||
key <AC11> { [apostrophe, at, dead_circumflex, dead_caron] };
|
||||
key <BKSL> { [numbersign, asciitilde, dead_grave, dead_breve ] };
|
||||
|
||||
key <LSGT> { [ backslash, bar, bar, brokenbar ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "intl" {
|
||||
|
||||
// A UK layout but with five accents made into dead keys:
|
||||
// grave, diaeresis, circumflex, acute, and tilde.
|
||||
// By Phil Jones <philjones1 at blueyonder.co.uk>.
|
||||
|
||||
include "latin"
|
||||
|
||||
name[Group1]="English (UK, intl., with dead keys)";
|
||||
|
||||
key <TLDE> { [ dead_grave, notsign, bar, bar ] };
|
||||
key <AE02> { [ 2, dead_diaeresis, twosuperior, onehalf ] };
|
||||
key <AE03> { [ 3, sterling, threesuperior, onethird ] };
|
||||
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||
key <AE06> { [ 6, dead_circumflex, threequarters, onesixth ] };
|
||||
|
||||
key <AC11> { [ dead_acute, at, apostrophe, bar ] };
|
||||
key <BKSL> { [ numbersign, dead_tilde, bar, bar ] };
|
||||
|
||||
key <LSGT> { [ backslash, bar, bar, bar ] };
|
||||
key <AB08> { [ comma, less, ccedilla, Ccedilla ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "extd" {
|
||||
// Clone of the Microsoft "United Kingdom Extended" layout, which
|
||||
// includes dead keys for: grave; diaeresis; circumflex; tilde; and
|
||||
// accute. It also enables direct access to accute characters using
|
||||
// the Multi_key (Alt Gr).
|
||||
//
|
||||
// Taken from...
|
||||
// "Windows Keyboard Layouts"
|
||||
// https://docs.microsoft.com/en-gb/globalization/windows-keyboard-layouts#U
|
||||
//
|
||||
// -- Jonathan Miles <jon@cybah.co.uk>
|
||||
|
||||
include "latin"
|
||||
|
||||
name[Group1]="English (UK, extended, Windows)";
|
||||
|
||||
key <TLDE> { [ dead_grave, notsign, brokenbar, NoSymbol ] };
|
||||
key <AE02> { [ 2, quotedbl, dead_diaeresis, onehalf ] };
|
||||
key <AE03> { [ 3, sterling, threesuperior, onethird ] };
|
||||
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||
key <AE06> { [ 6, asciicircum, dead_circumflex, NoSymbol ] };
|
||||
|
||||
key <AD02> { [ w, W, wacute, Wacute ] };
|
||||
key <AD03> { [ e, E, eacute, Eacute ] };
|
||||
key <AD06> { [ y, Y, yacute, Yacute ] };
|
||||
key <AD07> { [ u, U, uacute, Uacute ] };
|
||||
key <AD08> { [ i, I, iacute, Iacute ] };
|
||||
key <AD09> { [ o, O, oacute, Oacute ] };
|
||||
key <AD12> { [ bracketright, braceright, NoSymbol, bar ] };
|
||||
|
||||
key <AC01> { [ a, A, aacute, Aacute ] };
|
||||
key <AC11> { [ apostrophe, at, dead_acute, grave ] };
|
||||
key <BKSL> { [ numbersign, asciitilde, dead_tilde, backslash ] };
|
||||
|
||||
key <LSGT> { [ backslash, bar, NoSymbol, NoSymbol ] };
|
||||
key <AB03> { [ c, C, ccedilla, Ccedilla ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
// Describe the differences between the US Colemak layout
|
||||
// and a UK variant. By Andy Buckley (andy@insectnation.org)
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "colemak" {
|
||||
include "us(colemak)"
|
||||
|
||||
name[Group1]="English (UK, Colemak)";
|
||||
|
||||
key <TLDE> { [ grave, notsign, bar, asciitilde ] };
|
||||
key <AE02> { [ 2, quotedbl, twosuperior, oneeighth ] };
|
||||
key <AE03> { [ 3, sterling, threesuperior, sterling ] };
|
||||
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||
|
||||
key <AC11> { [apostrophe, at, dead_circumflex, dead_caron] };
|
||||
key <BKSL> { [numbersign, asciitilde, dead_grave, dead_breve ] };
|
||||
|
||||
key <LSGT> { [ backslash, bar, asciitilde, brokenbar ] };
|
||||
};
|
||||
|
||||
// Colemak-DH (ISO) layout, UK Variant, https://colemakmods.github.io/mod-dh/
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "colemak_dh" {
|
||||
include "us(colemak_dh)"
|
||||
|
||||
name[Group1]="English (UK, Colemak-DH)";
|
||||
|
||||
key <TLDE> { [ grave, notsign, bar, asciitilde ] };
|
||||
key <AE02> { [ 2, quotedbl, twosuperior, oneeighth ] };
|
||||
key <AE03> { [ 3, sterling, threesuperior, sterling ] };
|
||||
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||
|
||||
key <AC11> { [apostrophe, at, dead_circumflex, dead_caron] };
|
||||
key <BKSL> { [numbersign, asciitilde, dead_grave, dead_breve ] };
|
||||
|
||||
key <AB05> { [ backslash, bar, asciitilde, brokenbar ] };
|
||||
};
|
||||
|
||||
|
||||
// Dvorak (UK) keymap (by odaen) allowing the usage of
|
||||
// the £ and ? key and swapping the @ and " keys.
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "dvorak" {
|
||||
include "us(dvorak-alt-intl)"
|
||||
|
||||
name[Group1]="English (UK, Dvorak)";
|
||||
|
||||
key <TLDE> { [ grave, notsign, bar, bar ] };
|
||||
key <AE02> { [ 2, quotedbl, twosuperior, NoSymbol ] };
|
||||
key <AE03> { [ 3, sterling, threesuperior, NoSymbol ] };
|
||||
key <AD01> { [ apostrophe, at ] };
|
||||
key <BKSL> { [ numbersign, asciitilde ] };
|
||||
key <LSGT> { [ backslash, bar ] };
|
||||
};
|
||||
|
||||
// Dvorak letter positions, but punctuation all in the normal UK positions.
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "dvorakukp" {
|
||||
include "gb(dvorak)"
|
||||
|
||||
name[Group1]="English (UK, Dvorak, with UK punctuation)";
|
||||
|
||||
key <AE11> { [ minus, underscore ] };
|
||||
key <AE12> { [ equal, plus ] };
|
||||
key <AD11> { [ bracketleft, braceleft ] };
|
||||
key <AD12> { [ bracketright, braceright ] };
|
||||
key <AD01> { [ slash, question ] };
|
||||
key <AC11> { [apostrophe, at, dead_circumflex, dead_caron] };
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "mac" {
|
||||
|
||||
include "latin"
|
||||
|
||||
name[Group1]= "English (UK, Macintosh)";
|
||||
|
||||
key <TLDE> { [ section, plusminus ] };
|
||||
key <AE02> { [ 2, at, EuroSign ] };
|
||||
key <AE03> { [ 3, sterling, numbersign ] };
|
||||
key <LSGT> { [ grave, asciitilde ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
include "level3(enter_switch)"
|
||||
};
|
||||
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "mac_intl" {
|
||||
|
||||
include "latin"
|
||||
|
||||
name[Group1]="English (UK, Macintosh, intl.)";
|
||||
|
||||
key <TLDE> { [ section, plusminus, notsign, notsign ] }; //dead_grave
|
||||
key <AE02> { [ 2, at, EuroSign, onehalf ] };
|
||||
key <AE03> { [ 3, sterling, twosuperior, onethird ] };
|
||||
key <AE04> { [ 4, dollar, threesuperior, onequarter ] };
|
||||
key <AE06> { [ 6, dead_circumflex, NoSymbol, onesixth ] };
|
||||
key <AD09> { [ o, O, oe, OE ] };
|
||||
|
||||
key <AC11> { [ dead_acute, dead_diaeresis, dead_diaeresis, bar ] }; //dead_doubleacute
|
||||
key <BKSL> { [ backslash, bar, numbersign, bar ] };
|
||||
|
||||
key <LSGT> { [ dead_grave, dead_tilde, brokenbar, bar ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "pl" {
|
||||
|
||||
// Polish accented letters on upper levels of corresponding base letters.
|
||||
// Idea from Wawrzyniec Niewodniczański, adapted by Aleksander Kowalski.
|
||||
|
||||
include "gb(basic)"
|
||||
|
||||
name[Group1]="Polish (British keyboard)";
|
||||
|
||||
key <AD03> { [ e, E, eogonek, Eogonek ] };
|
||||
key <AD09> { [ o, O, oacute, Oacute ] };
|
||||
|
||||
key <AC01> { [ a, A, aogonek, Aogonek ] };
|
||||
key <AC02> { [ s, S, sacute, Sacute ] };
|
||||
|
||||
key <AB01> { [ z, Z, zabovedot, Zabovedot ] };
|
||||
key <AB02> { [ x, X, zacute, Zacute ] };
|
||||
key <AB03> { [ c, C, cacute, Cacute ] };
|
||||
key <AB06> { [ n, N, nacute, Nacute ] };
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "gla" {
|
||||
|
||||
// Grave-accented letters on the upper levels of the relevant vowels.
|
||||
|
||||
include "gb(basic)"
|
||||
|
||||
name[Group1]="Scottish Gaelic";
|
||||
|
||||
key <AD03> { [ e, E, egrave, Egrave ] };
|
||||
key <AD07> { [ u, U, ugrave, Ugrave ] };
|
||||
key <AD08> { [ i, I, igrave, Igrave ] };
|
||||
key <AD09> { [ o, O, ograve, Ograve ] };
|
||||
|
||||
key <AC01> { [ a, A, agrave, Agrave ] };
|
||||
};
|
||||
|
||||
// EXTRAS:
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "sun_type6" {
|
||||
include "sun_vndr/gb(sun_type6)"
|
||||
};
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
// The <KPDL> key is a mess.
|
||||
// It was probably originally meant to be a decimal separator.
|
||||
// Except since it was declared by USA people it didn't use the original
|
||||
// SI separator "," but a "." (since then the USA managed to f-up the SI
|
||||
// by making "." an accepted alternative, but standards still use "," as
|
||||
// default)
|
||||
// As a result users of SI-abiding countries expect either a "." or a ","
|
||||
// or a "decimal_separator" which may or may not be translated in one of the
|
||||
// above depending on applications.
|
||||
// It's not possible to define a default per-country since user expectations
|
||||
// depend on the conflicting choices of their most-used applications,
|
||||
// operating system, etc. Therefore it needs to be a configuration setting
|
||||
// Copyright © 2007 Nicolas Mailhot <nicolas.mailhot @ laposte.net>
|
||||
|
||||
|
||||
// Legacy <KPDL> #1
|
||||
// This assumes KP_Decimal will be translated in a dot
|
||||
partial keypad_keys
|
||||
xkb_symbols "dot" {
|
||||
|
||||
key.type[Group1]="KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, KP_Decimal ] }; // <delete> <separator>
|
||||
};
|
||||
|
||||
|
||||
// Legacy <KPDL> #2
|
||||
// This assumes KP_Separator will be translated in a comma
|
||||
partial keypad_keys
|
||||
xkb_symbols "comma" {
|
||||
|
||||
key.type[Group1]="KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, KP_Separator ] }; // <delete> <separator>
|
||||
};
|
||||
|
||||
|
||||
// Period <KPDL>, usual keyboard serigraphy in most countries
|
||||
partial keypad_keys
|
||||
xkb_symbols "dotoss" {
|
||||
|
||||
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, period, comma, 0x100202F ] }; // <delete> . , ⍽ (narrow no-break space)
|
||||
};
|
||||
|
||||
|
||||
// Period <KPDL>, usual keyboard serigraphy in most countries, latin-9 restriction
|
||||
partial keypad_keys
|
||||
xkb_symbols "dotoss_latin9" {
|
||||
|
||||
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, period, comma, nobreakspace ] }; // <delete> . , ⍽ (no-break space)
|
||||
};
|
||||
|
||||
|
||||
// Comma <KPDL>, what most non anglo-saxon people consider the real separator
|
||||
partial keypad_keys
|
||||
xkb_symbols "commaoss" {
|
||||
|
||||
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, comma, period, 0x100202F ] }; // <delete> , . ⍽ (narrow no-break space)
|
||||
};
|
||||
|
||||
|
||||
// Momayyez <KPDL>: Bahrain, Iran, Iraq, Kuwait, Oman, Qatar, Saudi Arabia, Syria, UAE
|
||||
partial keypad_keys
|
||||
xkb_symbols "momayyezoss" {
|
||||
|
||||
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, 0x100066B, comma, 0x100202F ] }; // <delete> ? , ⍽ (narrow no-break space)
|
||||
};
|
||||
|
||||
|
||||
// Abstracted <KPDL>, pray everything will work out (it usually does not)
|
||||
partial keypad_keys
|
||||
xkb_symbols "kposs" {
|
||||
|
||||
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, KP_Decimal, KP_Separator, 0x100202F ] }; // <delete> ? ? ⍽ (narrow no-break space)
|
||||
};
|
||||
|
||||
// Spreadsheets may be configured to use the dot as decimal
|
||||
// punctuation, comma as a thousands separator and then semi-colon as
|
||||
// the list separator. Of these, dot and semi-colon is most important
|
||||
// when entering data by the keyboard; the comma can then be inferred
|
||||
// and added to the presentation afterwards. Using semi-colon as a
|
||||
// general separator may in fact be preferred to avoid ambiguities
|
||||
// in data files. Most times a decimal separator is hard-coded, it
|
||||
// seems to be period, probably since this is the syntax used in
|
||||
// (most) programming languages.
|
||||
partial keypad_keys
|
||||
xkb_symbols "semi" {
|
||||
|
||||
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ NoSymbol, NoSymbol, semicolon ] };
|
||||
};
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
// Common Latin alphabet layout
|
||||
|
||||
default partial
|
||||
xkb_symbols "basic" {
|
||||
|
||||
key <AE01> { [ 1, exclam, onesuperior, exclamdown ] };
|
||||
key <AE02> { [ 2, at, twosuperior, oneeighth ] };
|
||||
key <AE03> { [ 3, numbersign, threesuperior, sterling ] };
|
||||
key <AE04> { [ 4, dollar, onequarter, dollar ] };
|
||||
key <AE05> { [ 5, percent, onehalf, threeeighths ] };
|
||||
key <AE06> { [ 6, asciicircum, threequarters, fiveeighths ] };
|
||||
key <AE07> { [ 7, ampersand, braceleft, seveneighths ] };
|
||||
key <AE08> { [ 8, asterisk, bracketleft, trademark ] };
|
||||
key <AE09> { [ 9, parenleft, bracketright, plusminus ] };
|
||||
key <AE10> { [ 0, parenright, braceright, degree ] };
|
||||
key <AE11> { [ minus, underscore, backslash, questiondown ] };
|
||||
key <AE12> { [ equal, plus, dead_cedilla, dead_ogonek ] };
|
||||
|
||||
key <AD01> { [ q, Q, at, Greek_OMEGA ] };
|
||||
key <AD02> { [ w, W, U017F, section ] };
|
||||
key <AD03> { [ e, E, e, E ] };
|
||||
key <AD04> { [ r, R, paragraph, registered ] };
|
||||
key <AD05> { [ t, T, tslash, Tslash ] };
|
||||
key <AD06> { [ y, Y, leftarrow, yen ] };
|
||||
key <AD07> { [ u, U, downarrow, uparrow ] };
|
||||
key <AD08> { [ i, I, rightarrow, idotless ] };
|
||||
key <AD09> { [ o, O, oslash, Oslash ] };
|
||||
key <AD10> { [ p, P, thorn, THORN ] };
|
||||
key <AD11> { [bracketleft, braceleft, dead_diaeresis, dead_abovering ] };
|
||||
key <AD12> { [bracketright, braceright, dead_tilde, dead_macron ] };
|
||||
|
||||
key <AC01> { [ a, A, ae, AE ] };
|
||||
key <AC02> { [ s, S, ssharp, U1E9E ] };
|
||||
key <AC03> { [ d, D, eth, ETH ] };
|
||||
key <AC04> { [ f, F, dstroke, ordfeminine ] };
|
||||
key <AC05> { [ g, G, eng, ENG ] };
|
||||
key <AC06> { [ h, H, hstroke, Hstroke ] };
|
||||
key <AC07> { [ j, J, dead_hook, dead_horn ] };
|
||||
key <AC08> { [ k, K, kra, ampersand ] };
|
||||
key <AC09> { [ l, L, lstroke, Lstroke ] };
|
||||
key <AC10> { [ semicolon, colon, dead_acute, dead_doubleacute ] };
|
||||
key <AC11> { [apostrophe, quotedbl, dead_circumflex, dead_caron ] };
|
||||
key <TLDE> { [ grave, asciitilde, notsign, notsign ] };
|
||||
|
||||
key <BKSL> { [ backslash, bar, dead_grave, dead_breve ] };
|
||||
key <AB01> { [ z, Z, guillemotleft, less ] };
|
||||
key <AB02> { [ x, X, guillemotright, greater ] };
|
||||
key <AB03> { [ c, C, cent, copyright ] };
|
||||
key <AB04> { [ v, V, doublelowquotemark, singlelowquotemark ] };
|
||||
key <AB05> { [ b, B, leftdoublequotemark, leftsinglequotemark ] };
|
||||
key <AB06> { [ n, N, rightdoublequotemark, rightsinglequotemark ] };
|
||||
key <AB07> { [ m, M, mu, masculine ] };
|
||||
key <AB08> { [ comma, less, U2022, multiply ] }; // bullet
|
||||
key <AB09> { [ period, greater, periodcentered, division ] };
|
||||
key <AB10> { [ slash, question, dead_belowdot, dead_abovedot ] };
|
||||
};
|
||||
|
||||
// Northern Europe ( Danish, Finnish, Norwegian, Swedish) common layout
|
||||
|
||||
partial
|
||||
xkb_symbols "type2" {
|
||||
|
||||
include "latin"
|
||||
|
||||
key <AE01> { [ 1, exclam, exclamdown, onesuperior ] };
|
||||
key <AE02> { [ 2, quotedbl, at, twosuperior ] };
|
||||
key <AE03> { [ 3, numbersign, sterling, threesuperior] };
|
||||
key <AE04> { [ 4, currency, dollar, onequarter ] };
|
||||
key <AE05> { [ 5, percent, onehalf, cent ] };
|
||||
key <AE06> { [ 6, ampersand, yen, fiveeighths ] };
|
||||
key <AE07> { [ 7, slash, braceleft, division ] };
|
||||
key <AE08> { [ 8, parenleft, bracketleft, guillemotleft] };
|
||||
key <AE09> { [ 9, parenright, bracketright, guillemotright] };
|
||||
key <AE10> { [ 0, equal, braceright, degree ] };
|
||||
|
||||
key <AD03> { [ e, E, EuroSign, cent ] };
|
||||
key <AD04> { [ r, R, registered, registered ] };
|
||||
key <AD05> { [ t, T, thorn, THORN ] };
|
||||
key <AD09> { [ o, O, oe, OE ] };
|
||||
key <AD11> { [ aring, Aring, dead_diaeresis, dead_abovering ] };
|
||||
key <AD12> { [dead_diaeresis, dead_circumflex, dead_tilde, dead_caron ] };
|
||||
|
||||
key <AC01> { [ a, A, ordfeminine, masculine ] };
|
||||
|
||||
key <AB03> { [ c, C, copyright, copyright ] };
|
||||
key <AB08> { [ comma, semicolon, dead_cedilla, dead_ogonek ] };
|
||||
key <AB09> { [ period, colon, periodcentered, dead_abovedot ] };
|
||||
key <AB10> { [ minus, underscore, dead_belowdot, dead_abovedot ] };
|
||||
};
|
||||
|
||||
// Slavic Latin ( Albanian, Croatian, Polish, Slovene, Yugoslav)
|
||||
// common layout
|
||||
|
||||
partial
|
||||
xkb_symbols "type3" {
|
||||
|
||||
include "latin"
|
||||
|
||||
key <AD01> { [ q, Q, backslash, Greek_OMEGA ] };
|
||||
key <AD02> { [ w, W, bar, section ] };
|
||||
key <AD06> { [ z, Z, leftarrow, yen ] };
|
||||
|
||||
key <AC04> { [ f, F, bracketleft, ordfeminine ] };
|
||||
key <AC05> { [ g, G, bracketright, ENG ] };
|
||||
key <AC08> { [ k, K, lstroke, ampersand ] };
|
||||
|
||||
key <AB01> { [ y, Y, guillemotleft, less ] };
|
||||
key <AB04> { [ v, V, at, grave ] };
|
||||
key <AB05> { [ b, B, braceleft, apostrophe ] };
|
||||
key <AB06> { [ n, N, braceright, acute ] };
|
||||
key <AB07> { [ m, M, section, masculine ] };
|
||||
key <AB08> { [ comma, semicolon, less, multiply ] };
|
||||
key <AB09> { [ period, colon, greater, division ] };
|
||||
};
|
||||
|
||||
// Another common Latin layout
|
||||
// (German, Estonian, Spanish, Icelandic, Italian, Latin American, Portuguese)
|
||||
|
||||
partial
|
||||
xkb_symbols "type4" {
|
||||
|
||||
include "latin"
|
||||
|
||||
key <AE02> { [ 2, quotedbl, at, oneeighth ] };
|
||||
key <AE06> { [ 6, ampersand, notsign, fiveeighths ] };
|
||||
key <AE07> { [ 7, slash, braceleft, seveneighths ] };
|
||||
key <AE08> { [ 8, parenleft, bracketleft, trademark ] };
|
||||
key <AE09> { [ 9, parenright, bracketright, plusminus ] };
|
||||
key <AE10> { [ 0, equal, braceright, degree ] };
|
||||
|
||||
key <AD03> { [ e, E, EuroSign, cent ] };
|
||||
|
||||
key <AB08> { [ comma, semicolon, U2022, multiply ] }; // bullet
|
||||
key <AB09> { [ period, colon, periodcentered, division ] };
|
||||
key <AB10> { [ minus, underscore, dead_belowdot, dead_abovedot ] };
|
||||
};
|
||||
|
||||
partial
|
||||
xkb_symbols "nodeadkeys" {
|
||||
|
||||
key <AE12> { [ equal, plus, cedilla, ogonek ] };
|
||||
key <AD11> { [bracketleft, braceleft, diaeresis, degree ] };
|
||||
key <AD12> { [bracketright, braceright, asciitilde, macron ] };
|
||||
key <AC07> { [ j, J, ezh, EZH ] };
|
||||
key <AC10> { [ semicolon, colon, acute, doubleacute ] };
|
||||
key <AC11> { [apostrophe, quotedbl, asciicircum, caron ] };
|
||||
key <BKSL> { [ backslash, bar, grave, breve ] };
|
||||
key <AB10> { [ slash, question, ellipsis, abovedot ] };
|
||||
};
|
||||
|
||||
partial
|
||||
xkb_symbols "type2_nodeadkeys" {
|
||||
|
||||
include "latin(nodeadkeys)"
|
||||
|
||||
key <AD11> { [ aring, Aring, diaeresis, degree ] };
|
||||
key <AD12> { [ diaeresis, asciicircum, asciitilde, caron ] };
|
||||
key <AB08> { [ comma, semicolon, cedilla, ogonek ] };
|
||||
key <AB09> { [ period, colon, periodcentered, abovedot ] };
|
||||
key <AB10> { [ minus, underscore, ellipsis, abovedot ] };
|
||||
};
|
||||
|
||||
partial
|
||||
xkb_symbols "type3_nodeadkeys" {
|
||||
|
||||
include "latin(nodeadkeys)"
|
||||
};
|
||||
|
||||
partial
|
||||
xkb_symbols "type4_nodeadkeys" {
|
||||
|
||||
include "latin(nodeadkeys)"
|
||||
|
||||
key <AB10> { [ minus, underscore, ellipsis, abovedot ] };
|
||||
};
|
||||
|
||||
// Added 2008.03.05 by Marcin Woliński
|
||||
// See http://marcinwolinski.pl/keyboard/ for a description.
|
||||
// Used by pl(intl)
|
||||
//
|
||||
// ┌─────┐
|
||||
// │ 2 4 │ 2 = Shift, 4 = Level3 + Shift
|
||||
// │ 1 3 │ 1 = Normal, 3 = Level3
|
||||
// └─────┘
|
||||
// ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━┓
|
||||
// │ ~ ~ │ ! ' │ @ " │ # ˝ │ $ ¸ │ % ˇ │ ^ ^ │ & ˘ │ * ̇ │ ( ̣ │ ) ° │ _ ¯ │ + ˛ ┃ ⌫ Back- ┃
|
||||
// │ ` ` │ 1 ¡ │ 2 © │ 3 • │ 4 § │ 5 € │ 6 ¢ │ 7 − │ 8 × │ 9 ÷ │ 0 ° │ - – │ = — ┃ space ┃
|
||||
// ┢━━━━━┷━┱───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┺━┳━━━━━━━┫
|
||||
// ┃ ┃ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │ { « │ } » ┃ Enter ┃
|
||||
// ┃Tab ↹ ┃ q │ w │ e │ r │ t │ y │ u │ i │ o │ p │ [ ‹ │ ] › ┃ ⏎ ┃
|
||||
// ┣━━━━━━━┻┱────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┺┓ ┃
|
||||
// ┃ ┃ A │ S │ D │ F │ G │ H │ J │ K │ L │ : “ │ " ” │ | ¶ ┃ ┃
|
||||
// ┃Caps ⇬ ┃ a │ s │ d │ f │ g │ h │ j │ k │ l │ ; ‘ │ ' ’ │ \ ┃ ┃
|
||||
// ┣━━━━━━━━┹────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┲┷━━━━━┻━━━━━━┫
|
||||
// ┃ │ Z │ X │ C │ V │ B │ N │ M │ < „ │ > · │ ? ¿ ┃ ┃
|
||||
// ┃Shift ⇧ │ z │ x │ c │ v │ b │ n │ m │ , ‚ │ . … │ / ⁄ ┃Shift ⇧ ┃
|
||||
// ┣━━━━━━━┳━━━━━┷━┳━━━┷━━━┱─┴─────┴─────┴─────┴─────┴─────┴───┲━┷━━━━━╈━━━━━┻━┳━━━━━━━┳━━━┛
|
||||
// ┃ ┃ ┃ ┃ ␣ ⍽ ┃ ┃ ┃ ┃
|
||||
// ┃Ctrl ┃Meta ┃Alt ┃ ␣ Space ⍽ ┃AltGr ⇮┃Menu ┃Ctrl ┃
|
||||
// ┗━━━━━━━┻━━━━━━━┻━━━━━━━┹───────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┛
|
||||
|
||||
partial
|
||||
xkb_symbols "intl" {
|
||||
|
||||
key <TLDE> { [ grave, asciitilde, dead_grave, dead_tilde ] };
|
||||
key <AE01> { [ 1, exclam, exclamdown, dead_acute ] };
|
||||
key <AE02> { [ 2, at, copyright, dead_diaeresis ] };
|
||||
key <AE03> { [ 3, numbersign, U2022, dead_doubleacute ] }; // U+2022 is bullet (the name bullet does not work)
|
||||
key <AE04> { [ 4, dollar, section, dead_cedilla ] };
|
||||
key <AE05> { [ 5, percent, EuroSign, dead_caron ] };
|
||||
key <AE06> { [ 6, asciicircum, cent, dead_circumflex ] };
|
||||
key <AE07> { [ 7, ampersand, U2212, dead_breve ] }; // U+2212 is MINUS SIGN
|
||||
key <AE08> { [ 8, asterisk, multiply, dead_abovedot ] };
|
||||
key <AE09> { [ 9, parenleft, division, dead_belowdot ] };
|
||||
key <AE10> { [ 0, parenright, degree, dead_abovering ] };
|
||||
key <AE11> { [ minus, underscore, endash, dead_macron ] };
|
||||
key <AE12> { [ equal, plus, emdash, dead_ogonek ] };
|
||||
|
||||
key <AD01> { [ q, Q ] };
|
||||
key <AD02> { [ w, W ] };
|
||||
key <AD03> { [ e, E ] };
|
||||
key <AD04> { [ r, R ] };
|
||||
key <AD05> { [ t, T ] };
|
||||
key <AD06> { [ y, Y ] };
|
||||
key <AD07> { [ u, U ] };
|
||||
key <AD08> { [ i, I ] };
|
||||
key <AD09> { [ o, O ] };
|
||||
key <AD10> { [ p, P ] };
|
||||
key <AD11> { [bracketleft, braceleft, U2039, guillemotleft ] };
|
||||
key <AD12> { [bracketright, braceright, U203A, guillemotright ] };
|
||||
|
||||
key <AC01> { [ a, A ] };
|
||||
key <AC02> { [ s, S ] };
|
||||
key <AC03> { [ d, D ] };
|
||||
key <AC04> { [ f, F ] };
|
||||
key <AC05> { [ g, G ] };
|
||||
key <AC06> { [ h, H ] };
|
||||
key <AC07> { [ j, J ] };
|
||||
key <AC08> { [ k, K ] };
|
||||
key <AC09> { [ l, L ] };
|
||||
key <AC10> { [ semicolon, colon, leftsinglequotemark, leftdoublequotemark ] };
|
||||
key <AC11> { [apostrophe, quotedbl, rightsinglequotemark, rightdoublequotemark ] };
|
||||
|
||||
key <BKSL> { [ backslash, bar, NoSymbol, paragraph ] };
|
||||
key <AB01> { [ z, Z ] };
|
||||
key <AB02> { [ x, X ] };
|
||||
key <AB03> { [ c, C ] };
|
||||
key <AB04> { [ v, V ] };
|
||||
key <AB05> { [ b, B ] };
|
||||
key <AB06> { [ n, N ] };
|
||||
key <AB07> { [ m, M ] };
|
||||
key <AB08> { [ comma, less, singlelowquotemark, doublelowquotemark ] };
|
||||
key <AB09> { [ period, greater, ellipsis, periodcentered ] };
|
||||
key <AB10> { [ slash, question, U2044, questiondown ] }; // U+2044 is FRACTION SLASH
|
||||
};
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
// These variants assign ISO_Level3_Shift to various keys
|
||||
// so that levels 3 and 4 can be reached.
|
||||
|
||||
// The default behaviour:
|
||||
// the right Alt key (AltGr) chooses the third symbol engraved on a key.
|
||||
default partial modifier_keys
|
||||
xkb_symbols "ralt_switch" {
|
||||
key <RALT> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The right Alt key never chooses the third level.
|
||||
// This option attempts to undo the effect of a layout's inclusion of
|
||||
// 'ralt_switch'. You may want to also select another level3 option
|
||||
// to map the level3 shift to some other key.
|
||||
partial modifier_keys
|
||||
xkb_symbols "ralt_alt" {
|
||||
key <RALT> {[ Alt_R, Meta_R ], type[group1]="TWO_LEVEL" };
|
||||
modifier_map Mod1 { <RALT> };
|
||||
};
|
||||
|
||||
// The right Alt key (while pressed) chooses the third shift level,
|
||||
// and Compose is mapped to its second level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "ralt_switch_multikey" {
|
||||
key <RALT> {[ ISO_Level3_Shift, Multi_key ], type[group1]="TWO_LEVEL" };
|
||||
};
|
||||
|
||||
// Either Alt key (while pressed) chooses the third shift level.
|
||||
// (To be used mostly to imitate Mac OS functionality.)
|
||||
partial modifier_keys
|
||||
xkb_symbols "alt_switch" {
|
||||
include "level3(lalt_switch)"
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
// The left Alt key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "lalt_switch" {
|
||||
key <LALT> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The right Ctrl key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "switch" {
|
||||
key <RCTL> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The Menu key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "menu_switch" {
|
||||
key <MENU> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// Either Win key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "win_switch" {
|
||||
include "level3(lwin_switch)"
|
||||
include "level3(rwin_switch)"
|
||||
};
|
||||
|
||||
// The left Win key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "lwin_switch" {
|
||||
key <LWIN> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The right Win key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "rwin_switch" {
|
||||
key <RWIN> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The Enter key on the kepypad (while pressed) chooses the third shift level.
|
||||
// (This is especially useful for Mac laptops which miss the right Alt key.)
|
||||
partial modifier_keys
|
||||
xkb_symbols "enter_switch" {
|
||||
key <KPEN> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The CapsLock key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "caps_switch" {
|
||||
key <CAPS> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The CapsLock key (while pressed) chooses the third shift level and
|
||||
// Ctrl + CapsLock has the original CapsLock function.
|
||||
// The 2023 DIN standard for German keyboards recommends it as an option:
|
||||
// - https://de.wikipedia.org/wiki/E1_(Tastaturbelegung)#Feststelltaste/Umschaltsperre
|
||||
// - https://en.wikipedia.org/wiki/Caps_Lock#Abolition
|
||||
partial modifier_keys
|
||||
xkb_symbols "caps_switch_capslock_with_ctrl" {
|
||||
virtual_modifiers LevelThree;
|
||||
|
||||
key <CAPS> {
|
||||
type[Group1] = "PC_CONTROL_LEVEL2",
|
||||
symbols[Group1] = [ ISO_Level3_Shift, Caps_Lock ],
|
||||
// Explicit actions are preferred over modMap None/Mod5 { Caps_Lock }
|
||||
// because they have no side effect
|
||||
actions[Group1] = [ SetMods(modifiers = LevelThree), LockMods(modifiers = Lock) ]
|
||||
};
|
||||
};
|
||||
|
||||
// The Backslash key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "bksl_switch" {
|
||||
key <BKSL> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The AC11 key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "ac11_switch" {
|
||||
key <AC11> {[ ISO_Level3_Shift ], type[Group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The Less/Greater key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "lsgt_switch" {
|
||||
key <LSGT> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The CapsLock key (while pressed) chooses the third shift level,
|
||||
// and latches when pressed together with another third-level chooser.
|
||||
partial modifier_keys
|
||||
xkb_symbols "caps_switch_latch" {
|
||||
key <CAPS> {[ ISO_Level3_Shift, ISO_Level3_Shift, ISO_Level3_Latch ],
|
||||
type[group1]="THREE_LEVEL" };
|
||||
};
|
||||
|
||||
// The Backslash key (while pressed) chooses the third shift level,
|
||||
// and latches when pressed together with another third-level chooser.
|
||||
partial modifier_keys
|
||||
xkb_symbols "bksl_switch_latch" {
|
||||
key <BKSL> {[ ISO_Level3_Shift, ISO_Level3_Shift, ISO_Level3_Latch ],
|
||||
type[group1]="THREE_LEVEL" };
|
||||
};
|
||||
|
||||
// The Less/Greater key (while pressed) chooses the third shift level,
|
||||
// and latches when pressed together with another third-level chooser.
|
||||
partial modifier_keys
|
||||
xkb_symbols "lsgt_switch_latch" {
|
||||
key <LSGT> {[ ISO_Level3_Shift, ISO_Level3_Shift, ISO_Level3_Latch ],
|
||||
type[group1]="THREE_LEVEL" };
|
||||
};
|
||||
|
||||
// Top-row digit key 4 chooses third shift level when pressed alone.
|
||||
partial modifier_keys
|
||||
xkb_symbols "4_switch_isolated" {
|
||||
override key <AE04> {[ ISO_Level3_Shift ]};
|
||||
};
|
||||
|
||||
// Top-row digit key 9 chooses third shift level when pressed alone.
|
||||
partial modifier_keys
|
||||
xkb_symbols "9_switch_isolated" {
|
||||
override key <AE09> {[ ISO_Level3_Shift ]};
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,153 @@
|
|||
//! xkeyboard-config — keyboard layouts, compiled from the X11 xkeyboard-config database
|
||||
//! into native Zig. It turns a physical key (a USB HID usage, as the input module delivers)
|
||||
//! plus a modifier state into a **keysym** and, when the key produces one, a **character**
|
||||
//! (a Unicode scalar). This is the piece that lets a `KeyEvent.keycode` become a
|
||||
//! `KeyEvent.character`, without shipping an X11 runtime.
|
||||
//!
|
||||
//! The layout tables in `generated/layouts.zig` are produced by
|
||||
//! `tools/make-xkeyboard-config.py` (see ./README.md to regenerate). Those tables are
|
||||
//! deliberately pure data — each key carries its up-to-four levels and an XKB *type*. The
|
||||
//! type -> level selection semantics (which modifier picks which level) live here, so the
|
||||
//! data and the policy are separable.
|
||||
//!
|
||||
//! Scope (documented in README.md): group 1 only, no dead-key/compose composition (a dead
|
||||
//! key returns its keysym with no character), and a curated set of key types. Layouts:
|
||||
//! us, gb, de, fr, es, dvorak.
|
||||
//!
|
||||
//! Upstream xkeyboard-config and keysymdef.h are MIT/X11 licensed; see vendor/COPYING and
|
||||
//! vendor/PROVENANCE.md.
|
||||
|
||||
const std = @import("std");
|
||||
const generated = @import("layouts");
|
||||
|
||||
pub const Level = generated.Level;
|
||||
pub const KeyType = generated.KeyType;
|
||||
pub const Key = generated.Key;
|
||||
pub const Layout = generated.Layout;
|
||||
|
||||
/// The generated layouts, by name — as pointers, so they share identity with `all` and
|
||||
/// `byName` (and match the `*const Layout` that `map` takes).
|
||||
pub const us: *const Layout = &generated.us;
|
||||
pub const gb: *const Layout = &generated.gb;
|
||||
pub const de: *const Layout = &generated.de;
|
||||
pub const fr: *const Layout = &generated.fr;
|
||||
pub const es: *const Layout = &generated.es;
|
||||
pub const dvorak: *const Layout = &generated.dvorak;
|
||||
|
||||
/// Every generated layout, for enumeration (e.g. a settings UI).
|
||||
pub const all = generated.all;
|
||||
|
||||
/// The modifier state that selects a key's level. `level3` is AltGr (ISO Level3 Shift);
|
||||
/// `control` is accepted for completeness but does not affect level selection here.
|
||||
pub const Modifiers = struct {
|
||||
shift: bool = false,
|
||||
caps_lock: bool = false,
|
||||
level3: bool = false,
|
||||
control: bool = false,
|
||||
};
|
||||
|
||||
/// The result of a lookup: the X11 `keysym`, and the `character` it produces (a Unicode
|
||||
/// scalar) when it is a printable key — null for keys that produce none (Return, F1, a
|
||||
/// bare dead key, an unmapped key).
|
||||
pub const Mapping = struct {
|
||||
keysym: u32,
|
||||
character: ?u21,
|
||||
};
|
||||
|
||||
/// Which level (0..3) a key of `kind` selects under `mods`. XKB's canonical semantics:
|
||||
/// Shift picks the odd level, AltGr (level3) adds 2, and Caps acts like Shift for the
|
||||
/// alphabetic types. See the XKB "key types" — this covers the ones the vendored layouts
|
||||
/// use; anything else falls back to shift-or-not.
|
||||
fn selectLevel(kind: KeyType, mods: Modifiers) usize {
|
||||
const shift_or_caps = mods.shift != mods.caps_lock; // XOR: Caps behaves like Shift
|
||||
const low: usize = if (mods.shift) 1 else 0;
|
||||
const high: usize = if (mods.level3) 2 else 0;
|
||||
return switch (kind) {
|
||||
.one_level => 0,
|
||||
.two_level, .keypad, .other => low,
|
||||
.alphabetic => if (shift_or_caps) 1 else 0,
|
||||
.four_level => low + high,
|
||||
.four_level_alphabetic => (if (shift_or_caps) @as(usize, 1) else 0) + high,
|
||||
// Caps affects only the base pair, not the AltGr pair.
|
||||
.four_level_semialphabetic => if (mods.level3) 2 + low else (if (shift_or_caps) @as(usize, 1) else 0),
|
||||
};
|
||||
}
|
||||
|
||||
/// Map a physical key (`hid_usage`, a USB HID keyboard-page usage) under `mods` on
|
||||
/// `layout` to its keysym and character. Falls back gracefully when the selected level is
|
||||
/// undefined for the key: it drops the AltGr component, then the shift component, so a key
|
||||
/// with only a base/shift pair still yields something sensible under AltGr.
|
||||
pub fn map(layout: *const Layout, hid_usage: u8, mods: Modifiers) Mapping {
|
||||
const key = &layout.keys[hid_usage];
|
||||
var level = selectLevel(key.kind, mods);
|
||||
// Fall back to a defined level: full -> without AltGr -> base.
|
||||
if (key.levels[level].keysym == 0 and key.levels[level].unicode == 0) {
|
||||
const candidates = [_]usize{ level & 1, 0 };
|
||||
for (candidates) |candidate| {
|
||||
if (key.levels[candidate].keysym != 0 or key.levels[candidate].unicode != 0) {
|
||||
level = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const chosen = key.levels[level];
|
||||
return .{
|
||||
.keysym = chosen.keysym,
|
||||
.character = if (chosen.unicode != 0) @intCast(chosen.unicode) else null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Look up a layout by its name (`"us"`, `"gb"`, ...), or null if unknown.
|
||||
pub fn byName(name: []const u8) ?*const Layout {
|
||||
for (all) |layout| {
|
||||
if (std.mem.eql(u8, layout.name, name)) return layout;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- tests (host-run via `zig build test`) ---------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
// USB HID usages used in the tests (keyboard page 0x07).
|
||||
const hid_a: u8 = 0x04;
|
||||
const hid_1: u8 = 0x1e;
|
||||
const hid_3: u8 = 0x20;
|
||||
|
||||
test "us: letters obey shift and caps" {
|
||||
try testing.expectEqual(@as(?u21, 'a'), map(us, hid_a, .{}).character);
|
||||
try testing.expectEqual(@as(?u21, 'A'), map(us, hid_a, .{ .shift = true }).character);
|
||||
try testing.expectEqual(@as(?u21, 'A'), map(us, hid_a, .{ .caps_lock = true }).character);
|
||||
// Shift + Caps cancels for an alphabetic key.
|
||||
try testing.expectEqual(@as(?u21, 'a'), map(us, hid_a, .{ .shift = true, .caps_lock = true }).character);
|
||||
}
|
||||
|
||||
test "us: digits and their shifted symbols" {
|
||||
try testing.expectEqual(@as(?u21, '1'), map(us, hid_1, .{}).character);
|
||||
try testing.expectEqual(@as(?u21, '!'), map(us, hid_1, .{ .shift = true }).character);
|
||||
try testing.expectEqual(@as(?u21, '3'), map(us, hid_3, .{}).character);
|
||||
try testing.expectEqual(@as(?u21, '#'), map(us, hid_3, .{ .shift = true }).character);
|
||||
// A digit is not alphabetic: Caps alone must not shift it.
|
||||
try testing.expectEqual(@as(?u21, '3'), map(us, hid_3, .{ .caps_lock = true }).character);
|
||||
}
|
||||
|
||||
test "layouts differ: GB pound vs US hash on shift+3" {
|
||||
try testing.expectEqual(@as(?u21, '#'), map(us, hid_3, .{ .shift = true }).character);
|
||||
try testing.expectEqual(@as(?u21, '£'), map(gb, hid_3, .{ .shift = true }).character);
|
||||
}
|
||||
|
||||
test "french azerty places q where us has a" {
|
||||
try testing.expectEqual(@as(?u21, 'q'), map(fr, hid_a, .{}).character);
|
||||
try testing.expectEqual(@as(?u21, 'Q'), map(fr, hid_a, .{ .shift = true }).character);
|
||||
}
|
||||
|
||||
test "byName resolves and rejects" {
|
||||
try testing.expect(byName("us") == us);
|
||||
try testing.expect(byName("gb") == gb);
|
||||
try testing.expect(byName("nonsense") == null);
|
||||
}
|
||||
|
||||
test "unmapped key yields no character" {
|
||||
// HID 0x00 is not a key; every level is empty.
|
||||
try testing.expectEqual(@as(?u21, null), map(us, 0x00, .{}).character);
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@ pub const SystemCall = enum(u64) {
|
|||
clock = 23, // clock() -> nanoseconds since boot: a monotonic time source (for timeouts/delays)
|
||||
process_enumerate = 24, // process_enumerate(buffer, maximum) -> total: snapshot the task table
|
||||
process_kill = 25, // process_kill(id) -> 0/-errno: end a process this process spawned
|
||||
ipc_send = 26, // ipc_send(handle, message_ptr, message_len) -> 0/-errno: post a payload to an endpoint's async queue without blocking
|
||||
_,
|
||||
};
|
||||
|
||||
|
|
@ -83,6 +84,15 @@ pub const notify_badge_bit: u64 = 1 << 63;
|
|||
/// notifications, which never set this bit). The microkernel's SIGCHLD.
|
||||
pub const notify_exit_bit: u64 = 1 << 62;
|
||||
|
||||
/// Set (alongside `notify_badge_bit`) in the badge of a **buffered message** — a payload
|
||||
/// posted to an endpoint's async queue by `ipc_send`, delivered through `ipc_reply_wait`
|
||||
/// like a notification (no reply owed) but carrying bytes in the receive buffer, not just
|
||||
/// a badge. This is what distinguishes a payload-bearing async message from a bare IRQ /
|
||||
/// child-exit notification (which sets neither this nor `notify_exit_bit`). The low bits
|
||||
/// carry the sender's task id. The async counterpart of the synchronous `ipc_call`, for
|
||||
/// broadcasts where a rendezvous is the wrong shape (the input service is the first user).
|
||||
pub const notify_message_bit: u64 = 1 << 61;
|
||||
|
||||
/// Capacity of `ProcessDescriptor.name` — matches the longest name `system_spawn`
|
||||
/// accepts, so a process's recorded name (its argv[0]) is never truncated.
|
||||
pub const maximum_process_name = 64;
|
||||
|
|
@ -113,6 +123,7 @@ pub const ProcessDescriptor = extern struct {
|
|||
/// during bring-up. The VFS server registers under `vfs`; clients look it up.
|
||||
pub const ServiceId = enum(u32) {
|
||||
vfs = 1,
|
||||
input = 2,
|
||||
_,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
//! Discovery backends (ACPI today, device-tree later) translate their native
|
||||
//! hardware description into this one shape, so the rest of the kernel walks a
|
||||
//! plain `Device` tree without knowing which firmware described the machine —
|
||||
//! the same discipline `root.zig`'s `MemoryKind` applies to memory and `architecture`
|
||||
//! the same discipline `ps2-library.zig`'s `MemoryKind` applies to memory and `architecture`
|
||||
//! applies to the CPU.
|
||||
//!
|
||||
//! This is deliberately minimal: enough to *describe* what was discovered (a
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
//! PS/2 Keyboard Driver
|
||||
//!
|
||||
//! Spawned by the ps2-bus driver once the controller is initialized and port 1
|
||||
//! has passed its interface test and device reset. The bus driver hands us our
|
||||
//! device HID as argv[1]; we use it to locate our own device descriptor.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const ps2 = @import("ps2-library.zig");
|
||||
const device = runtime.device;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const hid = runtime.argument(1);
|
||||
if (hid.len == 0) {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: no HID argument\n");
|
||||
return;
|
||||
}
|
||||
writeLine("system/drivers/ps2-bus/keyboard: starting for hid {s}\n", .{hid});
|
||||
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: out of memory\n");
|
||||
return;
|
||||
};
|
||||
if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
|
||||
writeLine("system/drivers/ps2-bus/keyboard: no device for hid {s}\n", .{hid});
|
||||
return;
|
||||
}
|
||||
|
||||
// The 8042 ports (0x60/0x64) and this keyboard's IRQ1 both live on the same
|
||||
// PNP0303 node, which the ps2-bus driver exclusively owns — so the keyboard is
|
||||
// served through the bus and does not claim the controller itself.
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: served by ps2-bus (controller owned by bus)\n");
|
||||
|
||||
// Broadcast keyboard events through the input service so programs can listen for them
|
||||
// (docs/input.md). Until the bus reads real IRQ1 scancodes and hands them here (a
|
||||
// follow-up), we publish the same synthetic stand-in stream the demo source uses — the
|
||||
// fan-out path is real, only the source of the bytes is placeholder.
|
||||
var source = runtime.input.connectSource() orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: input service unavailable\n");
|
||||
return;
|
||||
};
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: ok\n");
|
||||
|
||||
var step: usize = 0;
|
||||
while (true) : (step +%= 1) {
|
||||
_ = source.publishKeyboardEvent(runtime.input.syntheticKeyEvent(step));
|
||||
runtime.system.sleep(200);
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
//! PS/2 Mouse Driver
|
||||
//!
|
||||
//! Spawned by the ps2-bus driver once the controller is initialized and port 2
|
||||
//! has passed its interface test and device reset. The bus driver hands us our
|
||||
//! device HID as argv[1]; we use it to locate our own device descriptor.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const ps2 = @import("ps2-library.zig");
|
||||
const device = runtime.device;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const hid = runtime.argument(1);
|
||||
if (hid.len == 0) {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: no HID argument\n");
|
||||
return;
|
||||
}
|
||||
writeLine("system/drivers/ps2-bus/mouse: starting for hid {s}\n", .{hid});
|
||||
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: out of memory\n");
|
||||
return;
|
||||
};
|
||||
const mouse_device_descriptor = device.findDeviceDescriptorByHid(buffer, hid) orelse {
|
||||
writeLine("system/drivers/ps2-bus/mouse: no device for hid {s}\n", .{hid});
|
||||
return;
|
||||
};
|
||||
|
||||
// The mouse's own node (PNP0F13) carries IRQ12 and is not claimed by the bus,
|
||||
// so this driver takes exclusive ownership of it. Port IO still goes through
|
||||
// the bus, which owns the shared 8042 ports.
|
||||
if (!device.claim(mouse_device_descriptor.id)) {
|
||||
writeLine("system/drivers/ps2-bus/mouse: unable to claim device for hid {s}\n", .{hid});
|
||||
return;
|
||||
}
|
||||
writeLine("system/drivers/ps2-bus/mouse: claimed device for hid {s}\n", .{hid});
|
||||
|
||||
// Broadcast mouse events through the input service so programs can listen for them
|
||||
// (docs/input.md). As with the keyboard, decoding real PS/2 mouse packets is a
|
||||
// follow-up; for now we publish the synthetic stand-in stream. The fan-out path is
|
||||
// real, only the source of the movement is placeholder.
|
||||
var source = runtime.input.connectSource() orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: input service unavailable\n");
|
||||
return;
|
||||
};
|
||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: ok\n");
|
||||
|
||||
var step: usize = 0;
|
||||
while (true) : (step +%= 1) {
|
||||
_ = source.publishMouseEvent(runtime.input.syntheticMouseEvent(step));
|
||||
runtime.system.sleep(200);
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
//! The PS/2 Controller is located on the mainboard.
|
||||
//! In the early days the controller was a single chip (Intel 8042).
|
||||
//! As of today it is part of the Advanced Integrated Peripheral.
|
||||
//!
|
||||
//! It shows up in the device discovery as:
|
||||
//! KBD_ [acpi_device] hid=PNP0303 (PS/2 Keyboard)
|
||||
//! - io_port 0x60 len 0x1
|
||||
//! - io_port 0x64 len 0x1
|
||||
//! - irq 0x1 len 0x1
|
||||
//! MOU_ [acpi_device] hid=PNP0F13 (PS/2 Mouse)
|
||||
//! - irq 0xc len 0x1
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const ps2 = @import("ps2-library.zig");
|
||||
const device = runtime.device;
|
||||
|
||||
/// Format one whole log line and emit it in a single `debug_write`, so output
|
||||
/// from the child drivers (which run concurrently) can never interleave with it.
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// Ask the device on `port` what it is, then spawn the matching driver from the
|
||||
/// initial-ramdisk, handing it the device's HID as argv[1]. The driver is chosen
|
||||
/// from what the device reports, not from the port number.
|
||||
fn spawnIdentifiedDriver(controller: ps2.Controller, port: ps2.Port) void {
|
||||
const device_type = controller.identifyDevice(port) orelse {
|
||||
writeLine("system/drivers/ps2-bus: identify timed out on port {s}\n", .{@tagName(port)});
|
||||
return;
|
||||
};
|
||||
const driver_name = device_type.driverName() orelse {
|
||||
writeLine("system/drivers/ps2-bus: unrecognized device on port {s}\n", .{@tagName(port)});
|
||||
return;
|
||||
};
|
||||
const hid = device_type.hid() orelse "";
|
||||
if (runtime.system.spawnWithArguments(driver_name, &.{hid}) != null) {
|
||||
writeLine("system/drivers/ps2-bus: port {s} is a {s}, spawned {s}\n", .{ @tagName(port), hid, driver_name });
|
||||
} else {
|
||||
writeLine("system/drivers/ps2-bus: failed to spawn {s}\n", .{driver_name});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: out of memory\n");
|
||||
return;
|
||||
};
|
||||
|
||||
var has_two_channels = false;
|
||||
// The 8042's IO ports (0x60/0x64) are enumerated under the keyboard ACPI node
|
||||
// (PNP0303), so we init the controller from that descriptor — but which device
|
||||
// is on which port is decided later by identify, not by this HID.
|
||||
const maybe_controller_device_descriptor = device.findDeviceDescriptorByHid(buffer, "PNP0303");
|
||||
if (maybe_controller_device_descriptor) |controller_device_descriptor| {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: found PS/2 controller\n");
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: initializing controller\n");
|
||||
|
||||
if (!device.claim(controller_device_descriptor.id)) {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: unable to claim controller \n");
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = ps2.Controller.init(controller_device_descriptor) orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: controller is missing its IO ports\n");
|
||||
return;
|
||||
};
|
||||
|
||||
controller.disablePort(.One);
|
||||
controller.disablePort(.Two);
|
||||
controller.flushOutputBuffer();
|
||||
|
||||
const current = controller.readConfigurationByte() orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
|
||||
return;
|
||||
};
|
||||
|
||||
const update = current & ~(ps2.configuration_first_port_interrupt |
|
||||
ps2.configuration_second_port_interrupt |
|
||||
ps2.configuration_first_port_translation);
|
||||
|
||||
if (controller.writeConfigurationByte(update) == null) {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (controller.performSelfTest()) | reply | {
|
||||
if (reply != ps2.response_controller_test_passed) {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: perform controller self test failed\n");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: controller self test timed out\n");
|
||||
return;
|
||||
}
|
||||
|
||||
has_two_channels = controller.hasTwoChannels() orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: controller channels timed out\n");
|
||||
return;
|
||||
};
|
||||
|
||||
if (has_two_channels) {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: has two channels\n");
|
||||
// keep the bus quiet until we have tested the ports and are ready to use them
|
||||
controller.disablePort(.Two);
|
||||
} else {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: has one channel\n");
|
||||
}
|
||||
|
||||
// interface tests: always test port 1, test port 2 only if it exists
|
||||
const port_one_works = (controller.testPort(.One) orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: port 1 test timed out\n");
|
||||
return;
|
||||
}) == ps2.response_port_test_passed;
|
||||
|
||||
var port_two_works = false;
|
||||
if (has_two_channels) {
|
||||
port_two_works = (controller.testPort(.Two) orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: port 2 test timed out\n");
|
||||
return;
|
||||
}) == ps2.response_port_test_passed;
|
||||
}
|
||||
|
||||
if (!port_one_works and !port_two_works) {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: no usable ports\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// enable the working ports and, via a read-modify-write, their interrupts
|
||||
controller.enablePort(.One);
|
||||
if (port_two_works) controller.enablePort(.Two);
|
||||
|
||||
var configuration = controller.readConfigurationByte() orelse {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
|
||||
return;
|
||||
};
|
||||
if (port_one_works) configuration |= ps2.Port.One.interruptBit();
|
||||
if (port_two_works) configuration |= ps2.Port.Two.interruptBit();
|
||||
_ = controller.writeConfigurationByte(configuration);
|
||||
|
||||
// reset each working device; a failing device is logged but does not
|
||||
// abort bring-up of the other one
|
||||
if (port_one_works) {
|
||||
if (controller.resetDevice(.One)) |passed| {
|
||||
if (!passed) _ = runtime.system.write("system/drivers/ps2-bus: port 1 device reset failed\n");
|
||||
} else {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: port 1 device reset timed out\n");
|
||||
}
|
||||
}
|
||||
if (port_two_works) {
|
||||
if (controller.resetDevice(.Two)) |passed| {
|
||||
if (!passed) _ = runtime.system.write("system/drivers/ps2-bus: port 2 device reset failed\n");
|
||||
} else {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: port 2 device reset timed out\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Identify the device on each working port and hand it off to the driver
|
||||
// that matches what it reported — a port is not assumed to be a keyboard
|
||||
// or a mouse by its number.
|
||||
if (port_one_works) spawnIdentifiedDriver(controller, .One);
|
||||
if (port_two_works) spawnIdentifiedDriver(controller, .Two);
|
||||
} else {
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: no PS/2 controller found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
_ = runtime.system.write("system/drivers/ps2-bus: ok\n");
|
||||
while (true) runtime.system.sleep(1000);
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
|
|
@ -0,0 +1,437 @@
|
|||
//! shared definitions between the different PS/2 drivers
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const device = runtime.device;
|
||||
const system = runtime.system;
|
||||
|
||||
/// PS-2 io ports:
|
||||
/// The PS/2 Controller itself uses 2 IO ports (usually, IO ports 0x60 and 0x64). Like many IO
|
||||
/// ports, reads and writes may access different internal registers.
|
||||
///
|
||||
/// Historical note: The PC-XT PPI had used port 0x61 to reset the keyboard interrupt request
|
||||
/// signal (among other unrelated functions). Port 0x61 has no keyboard related functions on AT and
|
||||
/// PS/2 compatibles.
|
||||
///
|
||||
/// The Data Port (typically IO Port 0x60) is used for reading data that was received from a PS/2
|
||||
/// device or from the PS/2 controller itself and writing data to a PS/2 device or to the PS/2
|
||||
/// controller itself.
|
||||
// Access type: Read/Write
|
||||
pub const dataPort = 0x60;
|
||||
// Access type: Read
|
||||
pub const statusRegisterPort = 0x64;
|
||||
// Access type: Write
|
||||
pub const CommandRegisterPort = 0x64;
|
||||
|
||||
/// How long to poll the status register before giving up. PS/2 controller
|
||||
/// responses normally arrive within a few milliseconds.
|
||||
pub const default_wait_timeout_nanoseconds: u64 = 10_000_000; // 10 ms
|
||||
|
||||
/// A PS/2 device reset (0xFF) runs the device's self-test (BAT), whose reply can
|
||||
/// take far longer than an ordinary controller response.
|
||||
pub const device_reset_timeout_nanoseconds: u64 = 750_000_000; // 750 ms
|
||||
|
||||
/// PS/2 controller commands, written to the command register (port 0x64).
|
||||
pub const cmd_read_configuration_byte: u8 = 0x20; // read controller configuration byte (internal RAM byte 0)
|
||||
pub const cmd_write_configuration_byte: u8 = 0x60; // write controller configuration byte (internal RAM byte 0)
|
||||
pub const cmd_disable_second_port: u8 = 0xA7; // disable second PS/2 port (dual-channel controllers only)
|
||||
pub const cmd_enable_second_port: u8 = 0xA8; // enable second PS/2 port (dual-channel controllers only)
|
||||
pub const cmd_test_second_port: u8 = 0xA9; // test second PS/2 port
|
||||
pub const cmd_test_controller: u8 = 0xAA; // controller self-test
|
||||
pub const cmd_test_first_port: u8 = 0xAB; // test first PS/2 port
|
||||
pub const cmd_diagnostic_dump: u8 = 0xAC; // read all bytes of internal RAM
|
||||
pub const cmd_disable_first_port: u8 = 0xAD; // disable first PS/2 port
|
||||
pub const cmd_enable_first_port: u8 = 0xAE; // enable first PS/2 port
|
||||
pub const cmd_read_controller_input_port: u8 = 0xC0; // read controller input port
|
||||
pub const cmd_read_controller_output_port: u8 = 0xD0; // read controller output port
|
||||
pub const cmd_write_controller_output_port: u8 = 0xD1; // write next data byte to the controller output port
|
||||
pub const cmd_write_first_port_output: u8 = 0xD2; // write next data byte to the first port output buffer
|
||||
pub const cmd_write_second_port_output: u8 = 0xD3; // write next data byte to the second port output buffer
|
||||
pub const cmd_write_second_port_input: u8 = 0xD4; // write next data byte to the second port input buffer (to the mouse)
|
||||
pub const cmd_pulse_system_reset: u8 = 0xFE; // pulse output line 0 low: resets the CPU
|
||||
|
||||
/// PS/2 status register bits (read from the status port, 0x64). Bits 4 and 5
|
||||
/// are chipset-specific and intentionally omitted.
|
||||
pub const status_output_buffer_full: u8 = 1 << 0; // 1 = a byte is waiting to be read from the data port
|
||||
pub const status_input_buffer_full: u8 = 1 << 1; // 1 = the controller has not yet consumed the last write
|
||||
pub const status_system_flag: u8 = 1 << 2; // set once the controller passes POST
|
||||
pub const status_command_or_data: u8 = 1 << 3; // 1 = last write was a command, 0 = data
|
||||
pub const status_timeout_error: u8 = 1 << 6; // 1 = time-out error
|
||||
pub const status_parity_error: u8 = 1 << 7; // 1 = parity error
|
||||
|
||||
/// Controller configuration byte bits (internal RAM byte 0; read/written via 0x20/0x60).
|
||||
pub const configuration_first_port_interrupt: u8 = 1 << 0; // 1 = first port IRQ (IRQ1) enabled
|
||||
pub const configuration_second_port_interrupt: u8 = 1 << 1; // 1 = second port IRQ (IRQ12) enabled
|
||||
pub const configuration_system_flag: u8 = 1 << 2; // 1 = system passed POST
|
||||
pub const configuration_first_port_clock_disabled: u8 = 1 << 4; // 1 = first port clock disabled
|
||||
pub const configuration_second_port_clock_disabled: u8 = 1 << 5; // 1 = second port clock disabled
|
||||
pub const configuration_first_port_translation: u8 = 1 << 6; // 1 = first port scancode translation enabled
|
||||
|
||||
/// Controller output port bits (read/written via 0xD0/0xD1).
|
||||
pub const output_port_system_reset: u8 = 1 << 0; // WARNING: keep this 1; writing 0 can lock the machine
|
||||
pub const output_port_a20_gate: u8 = 1 << 1; // A20 gate
|
||||
pub const output_port_second_port_clock: u8 = 1 << 2; // dual-channel controllers only
|
||||
pub const output_port_second_port_data: u8 = 1 << 3; // dual-channel controllers only
|
||||
pub const output_port_first_port_output_full: u8 = 1 << 4; // output buffer full from first port (IRQ1)
|
||||
pub const output_port_second_port_output_full: u8 = 1 << 5; // output buffer full from second port (IRQ12)
|
||||
pub const output_port_first_port_clock: u8 = 1 << 6; // first port clock
|
||||
pub const output_port_first_port_data: u8 = 1 << 7; // first port data
|
||||
|
||||
/// Controller self-test (0xAA) result codes.
|
||||
pub const response_controller_test_passed: u8 = 0x55;
|
||||
pub const response_controller_test_failed: u8 = 0xFC;
|
||||
|
||||
/// Port test (0xAB / 0xA9) result codes.
|
||||
pub const response_port_test_passed: u8 = 0x00;
|
||||
pub const response_port_test_clock_stuck_low: u8 = 0x01;
|
||||
pub const response_port_test_clock_stuck_high: u8 = 0x02;
|
||||
pub const response_port_test_data_stuck_low: u8 = 0x03;
|
||||
pub const response_port_test_data_stuck_high: u8 = 0x04;
|
||||
|
||||
/// PS/2 device commands, written to the data port (0x60) to reach the attached device.
|
||||
pub const device_cmd_identify: u8 = 0xF2; // identify device
|
||||
pub const device_cmd_enable_scanning: u8 = 0xF4;
|
||||
pub const device_cmd_disable_scanning: u8 = 0xF5;
|
||||
pub const device_cmd_reset: u8 = 0xFF; // reset and run the device self-test (BAT)
|
||||
|
||||
/// PS/2 device response bytes, read from the data port (0x60).
|
||||
pub const device_response_self_test_passed: u8 = 0xAA; // BAT succeeded after a reset
|
||||
pub const device_response_echo: u8 = 0xEE;
|
||||
pub const device_response_acknowledge: u8 = 0xFA; // ACK
|
||||
pub const device_response_self_test_failed_1: u8 = 0xFC; // BAT failure
|
||||
pub const device_response_self_test_failed_2: u8 = 0xFD; // BAT failure
|
||||
pub const device_response_resend: u8 = 0xFE; // ask the host to resend the last byte
|
||||
|
||||
/// PS/2 device identify (0xF2) reply bytes. A keyboard returns a two-byte id
|
||||
/// beginning with 0xAB; a mouse returns a single-byte id (0x00/0x03/0x04); an
|
||||
/// ancient AT keyboard returns nothing at all.
|
||||
pub const identify_keyboard_mf2: u8 = 0xAB; // first byte of a MF2 keyboard id (a subtype byte follows)
|
||||
pub const identify_mouse_standard: u8 = 0x00;
|
||||
pub const identify_mouse_scroll: u8 = 0x03; // mouse with scroll wheel
|
||||
pub const identify_mouse_five_button: u8 = 0x04; // 5-button mouse
|
||||
|
||||
fn waitReadable(id: u64, cmd_index: u64, wait_timeout_nanoseconds: u64) bool {
|
||||
const deadline = system.clock() + wait_timeout_nanoseconds;
|
||||
while (system.clock() < deadline) {
|
||||
if (status(id, cmd_index) & status_output_buffer_full != 0) return true; // OBF set -> data ready
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn waitWritable(id: u64, cmd_index: u64, wait_timeout_nanoseconds: u64) bool {
|
||||
const deadline = system.clock() + wait_timeout_nanoseconds;
|
||||
while (system.clock() < deadline) {
|
||||
if (status(id, cmd_index) & status_input_buffer_full == 0) return true; // IBF clear -> ok to write
|
||||
}
|
||||
return false; // timed out
|
||||
}
|
||||
|
||||
pub fn status(id: u64, cmd_index: u64) u8 {
|
||||
return @intCast(device.ioRead(id, cmd_index, 0, 1) orelse 0);
|
||||
}
|
||||
|
||||
pub fn sendCommand(id: u64, cmd_index: u64, byte: u8, timeout_nanoseconds: u64) bool {
|
||||
// wait IBF clear
|
||||
if (!waitWritable(id, cmd_index, timeout_nanoseconds)) return false;
|
||||
return device.ioWrite(id, cmd_index, 0, 1, byte);
|
||||
}
|
||||
|
||||
pub fn readData(id: u64, status_index: u64, data_index: u64, timeout_nanoseconds: u64) ?u8 {
|
||||
// OBF lives in the status register (0x64); wait for it there, then read the data port (0x60)
|
||||
if (!waitReadable(id, status_index, timeout_nanoseconds)) return null;
|
||||
return @intCast(device.ioRead(id, data_index, 0, 1) orelse 0);
|
||||
}
|
||||
|
||||
pub fn writeData(id: u64, status_index: u64, data_index: u64, byte: u8, timeout_nanoseconds: u64) bool {
|
||||
// IBF lives in the status register (0x64); wait for it to clear there, then write the data port (0x60)
|
||||
if (!waitWritable(id, status_index, timeout_nanoseconds)) return false;
|
||||
return device.ioWrite(id, data_index, 0, 1, byte);
|
||||
}
|
||||
|
||||
pub const Port = enum(u2) {
|
||||
One,
|
||||
Two,
|
||||
|
||||
/// Command register byte that disables this port.
|
||||
fn disableCommand(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.One => cmd_disable_first_port,
|
||||
.Two => cmd_disable_second_port,
|
||||
};
|
||||
}
|
||||
|
||||
/// Command register byte that enables this port (and its clock).
|
||||
fn enableCommand(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.One => cmd_enable_first_port,
|
||||
.Two => cmd_enable_second_port,
|
||||
};
|
||||
}
|
||||
|
||||
/// Command register byte that runs this port's interface test.
|
||||
fn testCommand(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.One => cmd_test_first_port,
|
||||
.Two => cmd_test_second_port,
|
||||
};
|
||||
}
|
||||
|
||||
/// Configuration-byte bit that, when set, disables this port's clock.
|
||||
pub fn clockDisabledBit(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.One => configuration_first_port_clock_disabled,
|
||||
.Two => configuration_second_port_clock_disabled,
|
||||
};
|
||||
}
|
||||
|
||||
/// Configuration-byte bit that, when set, enables this port's interrupt.
|
||||
pub fn interruptBit(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.One => configuration_first_port_interrupt,
|
||||
.Two => configuration_second_port_interrupt,
|
||||
};
|
||||
}
|
||||
|
||||
/// Command register byte that writes the next data byte into this port's
|
||||
/// output buffer (makes a byte appear as if it came from the device).
|
||||
pub fn writeOutputBufferCommand(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.One => cmd_write_first_port_output,
|
||||
.Two => cmd_write_second_port_output,
|
||||
};
|
||||
}
|
||||
|
||||
/// Controller command that must prefix a byte destined for this port's
|
||||
/// device. Port 1 is the default target of the data port, so it needs no
|
||||
/// prefix (null); port 2 requires the "write second port input" command.
|
||||
pub fn deviceInputCommand(self: Port) ?u8 {
|
||||
return switch (self) {
|
||||
.One => null,
|
||||
.Two => cmd_write_second_port_input,
|
||||
};
|
||||
}
|
||||
|
||||
/// Controller output-port bit driving this port's clock line.
|
||||
pub fn outputPortClockBit(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.One => output_port_first_port_clock,
|
||||
.Two => output_port_second_port_clock,
|
||||
};
|
||||
}
|
||||
|
||||
/// Controller output-port bit driving this port's data line.
|
||||
pub fn outputPortDataBit(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.One => output_port_first_port_data,
|
||||
.Two => output_port_second_port_data,
|
||||
};
|
||||
}
|
||||
|
||||
/// Controller output-port bit set when this port's output buffer is full
|
||||
/// (wired to the port's IRQ line).
|
||||
pub fn outputPortBufferFullBit(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.One => output_port_first_port_output_full,
|
||||
.Two => output_port_second_port_output_full,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// The kind of device attached to a port, as reported by the device itself in
|
||||
/// response to the identify command — not assumed from the port number.
|
||||
pub const DeviceType = enum {
|
||||
keyboard,
|
||||
mouse,
|
||||
unknown,
|
||||
|
||||
/// Initial-ramdisk name of the driver that serves this device type, or null
|
||||
/// if we could not classify it.
|
||||
pub fn driverName(self: DeviceType) ?[]const u8 {
|
||||
return switch (self) {
|
||||
.keyboard => "ps2-keyboard",
|
||||
.mouse => "ps2-mouse",
|
||||
.unknown => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Canonical ACPI HID for this device type, handed to the spawned driver as
|
||||
/// its command-line argument, or null if we could not classify it.
|
||||
pub fn hid(self: DeviceType) ?[]const u8 {
|
||||
return switch (self) {
|
||||
.keyboard => "PNP0303",
|
||||
.mouse => "PNP0F13",
|
||||
.unknown => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// A single PS/2 (8042) controller. Construct one with `Controller.init` and
|
||||
/// drive the controller through its methods; there is only ever one 8042 per
|
||||
/// machine, but holding the resolved resource indices in an instance keeps the
|
||||
/// call sites free of global state.
|
||||
pub const Controller = struct {
|
||||
device_id: u64,
|
||||
/// Resource index of the command/status port (0x64).
|
||||
status_index: u64,
|
||||
/// Resource index of the data port (0x60).
|
||||
data_index: u64,
|
||||
|
||||
/// Resolve the controller's IO-port resource indices from its device
|
||||
/// descriptor. Returns null if either the data or command/status port is
|
||||
/// missing from the descriptor.
|
||||
pub fn init(device_descriptor: device.DeviceDescriptor) ?Controller {
|
||||
var data_index: ?u64 = null;
|
||||
var status_index: ?u64 = null;
|
||||
|
||||
for (device_descriptor.resources, 0..device_descriptor.resource_count) |resource, resource_index| {
|
||||
if (resource.kind != @intFromEnum(device.ResourceKind.io_port)) continue;
|
||||
if (resource.start == dataPort) {
|
||||
data_index = @intCast(resource_index);
|
||||
} else if (resource.start == statusRegisterPort) {
|
||||
status_index = @intCast(resource_index);
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.device_id = device_descriptor.id,
|
||||
.data_index = data_index orelse return null,
|
||||
.status_index = status_index orelse return null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn disablePort(self: Controller, port: Port) void {
|
||||
// port enable/disable are controller commands and go to the command register (0x64)
|
||||
_ = sendCommand(self.device_id, self.status_index, port.disableCommand(), default_wait_timeout_nanoseconds);
|
||||
}
|
||||
|
||||
pub fn enablePort(self: Controller, port: Port) void {
|
||||
// enabling a port also starts its clock
|
||||
_ = sendCommand(self.device_id, self.status_index, port.enableCommand(), default_wait_timeout_nanoseconds);
|
||||
}
|
||||
|
||||
/// Run a port's interface test. Returns the controller's reply — compare it
|
||||
/// to `response_port_test_passed` (0x00) — or null on timeout.
|
||||
pub fn testPort(self: Controller, port: Port) ?u8 {
|
||||
if (!sendCommand(self.device_id, self.status_index, port.testCommand(), default_wait_timeout_nanoseconds)) return null;
|
||||
return readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
|
||||
}
|
||||
|
||||
pub fn flushOutputBuffer(self: Controller) void {
|
||||
// flush any stale byte the controller buffered
|
||||
_ = device.ioRead(self.device_id, self.data_index, 0, 1);
|
||||
}
|
||||
|
||||
pub fn readConfigurationByte(self: Controller) ?u8 {
|
||||
// ask the controller to place its configuration byte in the output buffer, then read it
|
||||
if (!sendCommand(self.device_id, self.status_index, cmd_read_configuration_byte, default_wait_timeout_nanoseconds)) return null;
|
||||
return readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
|
||||
}
|
||||
|
||||
pub fn writeConfigurationByte(self: Controller, update_byte: u8) ?u8 {
|
||||
// command 0x60 makes the controller store the next data-port byte as its configuration byte
|
||||
if (!sendCommand(self.device_id, self.status_index, cmd_write_configuration_byte, default_wait_timeout_nanoseconds)) return null;
|
||||
if (!writeData(self.device_id, self.status_index, self.data_index, update_byte, default_wait_timeout_nanoseconds)) return null;
|
||||
return update_byte;
|
||||
}
|
||||
|
||||
/// Run the controller self-test. Returns the reply — compare it to
|
||||
/// `response_controller_test_passed` (0x55) — or null on timeout.
|
||||
pub fn performSelfTest(self: Controller) ?u8 {
|
||||
if (!sendCommand(self.device_id, self.status_index, cmd_test_controller, default_wait_timeout_nanoseconds)) return null;
|
||||
return readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
|
||||
}
|
||||
|
||||
/// Detect whether this is a dual-channel controller by temporarily enabling
|
||||
/// port 2 and checking whether its clock turned on. Note: this leaves port 2
|
||||
/// enabled; the caller should disable it again to keep the bus quiet until
|
||||
/// device bring-up.
|
||||
pub fn hasTwoChannels(self: Controller) ?bool {
|
||||
self.enablePort(.Two);
|
||||
const configuration = self.readConfigurationByte() orelse return null;
|
||||
return (configuration & Port.Two.clockDisabledBit()) == 0;
|
||||
}
|
||||
|
||||
/// Reset the device attached to `port` (device command 0xFF) and wait for
|
||||
/// its power-on self-test (BAT) result. Returns true if the device both
|
||||
/// acknowledged and passed, false if it reported a self-test failure, or
|
||||
/// null on timeout. The BAT reply can be slow, so the response reads use
|
||||
/// `device_reset_timeout_nanoseconds`.
|
||||
pub fn resetDevice(self: Controller, port: Port) ?bool {
|
||||
// A byte destined for port 2 must be prefixed with the "write to second
|
||||
// port input buffer" controller command (0xD4); port 1 is the default.
|
||||
if (port.deviceInputCommand()) |prefix| {
|
||||
if (!sendCommand(self.device_id, self.status_index, prefix, default_wait_timeout_nanoseconds)) return null;
|
||||
}
|
||||
if (!writeData(self.device_id, self.status_index, self.data_index, device_cmd_reset, default_wait_timeout_nanoseconds)) return null;
|
||||
|
||||
// A successful reset yields both an ACK (0xFA) and a self-test-passed
|
||||
// byte (0xAA). Their order is not guaranteed, so accept either ordering.
|
||||
var saw_acknowledge = false;
|
||||
var saw_self_test_passed = false;
|
||||
var reads: u8 = 0;
|
||||
while (reads < 2) : (reads += 1) {
|
||||
const reply = readData(self.device_id, self.status_index, self.data_index, device_reset_timeout_nanoseconds) orelse return null;
|
||||
switch (reply) {
|
||||
device_response_acknowledge => saw_acknowledge = true,
|
||||
device_response_self_test_passed => saw_self_test_passed = true,
|
||||
device_response_self_test_failed_1, device_response_self_test_failed_2 => return false,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
return saw_acknowledge and saw_self_test_passed;
|
||||
}
|
||||
|
||||
/// Send one command byte to the device on `port` (applying the port-2 prefix
|
||||
/// as needed) and consume its acknowledgement. Returns true on ACK (0xFA),
|
||||
/// false on any other reply, or null on timeout.
|
||||
pub fn sendToDevice(self: Controller, port: Port, byte: u8) ?bool {
|
||||
if (port.deviceInputCommand()) |prefix| {
|
||||
if (!sendCommand(self.device_id, self.status_index, prefix, default_wait_timeout_nanoseconds)) return null;
|
||||
}
|
||||
if (!writeData(self.device_id, self.status_index, self.data_index, byte, default_wait_timeout_nanoseconds)) return null;
|
||||
const reply = readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds) orelse return null;
|
||||
return reply == device_response_acknowledge;
|
||||
}
|
||||
|
||||
/// Discard any bytes sitting in the output buffer (for example the device-id
|
||||
/// byte a mouse emits after a reset) so they cannot be mistaken for the reply
|
||||
/// to a subsequent command.
|
||||
pub fn drainOutputBuffer(self: Controller) void {
|
||||
var guard: u8 = 0;
|
||||
while (guard < 16) : (guard += 1) {
|
||||
if (status(self.device_id, self.status_index) & status_output_buffer_full == 0) return;
|
||||
_ = device.ioRead(self.device_id, self.data_index, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask the device on `port` what it is (command 0xF2) and classify the reply.
|
||||
/// Scanning is disabled around the query so a streaming device cannot inject
|
||||
/// data bytes that look like the identifier. Returns the device type, or null
|
||||
/// if the identify command itself timed out.
|
||||
pub fn identifyDevice(self: Controller, port: Port) ?DeviceType {
|
||||
// Clear any leftover bytes (e.g. a post-reset mouse id) before we start.
|
||||
self.drainOutputBuffer();
|
||||
|
||||
// Stop the device reporting so its data can't be mistaken for the reply.
|
||||
if (self.sendToDevice(port, device_cmd_disable_scanning) == null) return null;
|
||||
|
||||
if (self.sendToDevice(port, device_cmd_identify) == null) return null;
|
||||
|
||||
// After the ACK, the device sends 0, 1, or 2 identifier bytes.
|
||||
const first = readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
|
||||
const device_type: DeviceType = if (first) |id| switch (id) {
|
||||
identify_keyboard_mf2 => blk: {
|
||||
// A MF2 keyboard sends a second subtype byte; consume and ignore it.
|
||||
_ = readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
|
||||
break :blk .keyboard;
|
||||
},
|
||||
identify_mouse_standard, identify_mouse_scroll, identify_mouse_five_button => .mouse,
|
||||
else => .unknown,
|
||||
} else
|
||||
// No identifier bytes at all is a legacy AT keyboard.
|
||||
.keyboard;
|
||||
|
||||
// Resume scanning so the device works once its driver takes over.
|
||||
_ = self.sendToDevice(port, device_cmd_enable_scanning);
|
||||
return device_type;
|
||||
}
|
||||
};
|
||||
|
|
@ -57,6 +57,29 @@ pub const EPERM: i64 = 9; // not permitted (process_kill by anyone but the super
|
|||
/// shared kernel↔user ABI (system/abi.zig), because ring 3 has to test the same bit.
|
||||
pub const notify_badge_bit: u64 = abi.notify_badge_bit;
|
||||
|
||||
/// Set (with `notify_badge_bit`) when a `replyWait` wake carries a buffered payload
|
||||
/// posted by `send` (`ipc_send`), rather than a bare IRQ/exit notification. Shared with
|
||||
/// ring 3 through the ABI so the receiver can tell "a message arrived" from "the hardware
|
||||
/// spoke".
|
||||
pub const notify_message_bit: u64 = abi.notify_message_bit;
|
||||
|
||||
/// Largest payload a single `send` (`ipc_send`) may post. Kept small — the payload rides
|
||||
/// inline in every `Endpoint`, and the async path is for events (a `KeyEvent` is 16
|
||||
/// bytes), not bulk transfer, which is what `call` and future shared pages are for.
|
||||
pub const POST_MAXIMUM: usize = 64;
|
||||
|
||||
/// Depth of an endpoint's async payload ring. Absorbs a burst while a receiver is briefly
|
||||
/// busy; a full ring drops the *oldest* message (see `send`).
|
||||
const post_capacity: usize = 16;
|
||||
|
||||
/// One buffered message: a length-prefixed payload plus the sender's task id (delivered
|
||||
/// in the low bits of the receiver's badge).
|
||||
const PostSlot = struct {
|
||||
length: u16 = 0,
|
||||
sender_id: u64 = 0,
|
||||
bytes: [POST_MAXIMUM]u8 = undefined,
|
||||
};
|
||||
|
||||
/// End of the user (low) canonical half — user buffers must lie below it.
|
||||
const user_half_end: u64 = 0x0000_8000_0000_0000;
|
||||
|
||||
|
|
@ -74,6 +97,12 @@ pub const Endpoint = struct {
|
|||
notify_buffer: [8]u64 = undefined,
|
||||
notify_head: u8 = 0,
|
||||
notify_tail: u8 = 0,
|
||||
// Pending buffered messages (payloads posted by `send`), a small FIFO ring. Unlike
|
||||
// notifications — which are a level and coalesce — these are discrete messages, so a
|
||||
// full ring drops the oldest rather than merging.
|
||||
post_buffer: [post_capacity]PostSlot = undefined,
|
||||
post_head: u16 = 0,
|
||||
post_tail: u16 = 0,
|
||||
};
|
||||
|
||||
pub fn createIpcEndpoint() ?*Endpoint {
|
||||
|
|
@ -265,12 +294,23 @@ pub fn replyWait(endpoint: *Endpoint, reply_ptr: u64, reply_len: u64, receive_pt
|
|||
scheduler.readyLocked(client); // its `call` now returns
|
||||
}
|
||||
|
||||
// (2) Receive the next request (or notification), blocking until one is ready.
|
||||
// (2) Receive the next request (or notification / buffered message), blocking until
|
||||
// one is ready. Bare notifications (IRQ/exit) come first — they're latency-sensitive
|
||||
// and carry no payload — then buffered messages, then synchronous client requests.
|
||||
while (true) {
|
||||
if (popNotify(endpoint)) |badge| {
|
||||
out_badge.* = badge | notify_badge_bit;
|
||||
return 0; // notification: no payload, no reply owed, no cap
|
||||
}
|
||||
if (popPost(endpoint)) |slot| {
|
||||
const n = @min(@as(usize, slot.length), receive_cap);
|
||||
// Copy from the kernel-resident ring slot (source aspace 0) into the receiver.
|
||||
if (!copyAcross(0, @intFromPtr(&slot.bytes), me.aspace, receive_ptr, n)) {
|
||||
continue; // bad receive buffer: drop this message, keep serving
|
||||
}
|
||||
out_badge.* = slot.sender_id | notify_badge_bit | notify_message_bit;
|
||||
return @intCast(n); // async message: payload delivered, no reply owed, no cap
|
||||
}
|
||||
if (dequeueSender(endpoint)) |caller| {
|
||||
const n = @min(caller.ipc_send_len, receive_cap);
|
||||
if (!copyAcross(caller.aspace, caller.ipc_send_ptr, me.aspace, receive_ptr, n)) {
|
||||
|
|
@ -306,6 +346,44 @@ fn popNotify(endpoint: *Endpoint) ?u64 {
|
|||
return badge;
|
||||
}
|
||||
|
||||
/// Take the oldest buffered message from the post ring, or null if empty. Returns a
|
||||
/// pointer into the endpoint's own storage — valid until the next `send`/`popPost` under
|
||||
/// the same lock region, which is all the copy-out in `replyWait` needs.
|
||||
fn popPost(endpoint: *Endpoint) ?*const PostSlot {
|
||||
if (endpoint.post_head == endpoint.post_tail) return null;
|
||||
const slot = &endpoint.post_buffer[endpoint.post_head % post_capacity];
|
||||
endpoint.post_head +%= 1;
|
||||
return slot;
|
||||
}
|
||||
|
||||
/// Client-free side of async IPC (`ipc_send`): copy `[source_va, len)` from address space
|
||||
/// `source_as` into `endpoint`'s post ring and wake a waiting receiver — **without
|
||||
/// blocking the sender** and with no reply owed. `sender_id` rides along, delivered in the
|
||||
/// low bits of the receiver's badge. Returns 0, or a negative errno (`-E2BIG` if the
|
||||
/// payload exceeds `POST_MAXIMUM`, `-EFAULT` if the source buffer is unmapped / out of the
|
||||
/// user half). A full ring drops the *oldest* message (advancing `post_head`), because a
|
||||
/// buffered message is discrete, not a level: keeping the newest keeps input responsive.
|
||||
/// Precondition: the big kernel lock is held.
|
||||
pub fn sendLocked(endpoint: *Endpoint, source_as: u64, source_va: u64, len: u64, sender_id: u64) i64 {
|
||||
if (len > POST_MAXIMUM) return -E2BIG;
|
||||
// Drop the oldest if the ring is full, so this newest message always lands.
|
||||
if (endpoint.post_tail -% endpoint.post_head >= post_capacity) endpoint.post_head +%= 1;
|
||||
const slot = &endpoint.post_buffer[endpoint.post_tail % post_capacity];
|
||||
if (!copyFromUser(source_as, source_va, slot.bytes[0..@intCast(len)])) return -EFAULT;
|
||||
slot.length = @intCast(len);
|
||||
slot.sender_id = sender_id;
|
||||
endpoint.post_tail +%= 1;
|
||||
scheduler.wakeLocked(&endpoint.receive_wait_queue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// `sendLocked` wrapped in its own critical section, for the `ipc_send` syscall path.
|
||||
pub fn send(endpoint: *Endpoint, source_as: u64, source_va: u64, len: u64, sender_id: u64) i64 {
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
return sendLocked(endpoint, source_as, source_va, len, sender_id);
|
||||
}
|
||||
|
||||
/// Post an asynchronous notification carrying `badge` to `endpoint` and wake a waiting
|
||||
/// receiver. Precondition: the big kernel lock is held.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ fn system_call(state: *architecture.CpuState) void {
|
|||
.ipc_lookup => systemIpcLookup(state),
|
||||
.ipc_call => systemIpcCall(state),
|
||||
.ipc_reply_wait => systemIpcReplyWait(state),
|
||||
.ipc_send => systemIpcSend(state),
|
||||
.device_enumerate => systemDeviceEnumerate(state),
|
||||
.device_claim => systemDeviceClaim(state),
|
||||
.mmio_map => systemMmioMap(state),
|
||||
|
|
@ -263,6 +264,18 @@ fn systemIpcReplyWait(state: *architecture.CpuState) void {
|
|||
architecture.setSystemCallResult3(state, received_cap);
|
||||
}
|
||||
|
||||
/// ipc_send(handle, message_ptr, message_len) -> 0/-errno: post a payload to an
|
||||
/// endpoint's async queue and wake a receiver, without blocking the caller. The async
|
||||
/// counterpart of ipc_call — for broadcasts (the input service) where a rendezvous would
|
||||
/// let one dead subscriber hang the sender. Delivered through ipc_reply_wait as a
|
||||
/// buffered message (badge carries notify_message_bit and the caller's task id).
|
||||
fn systemIpcSend(state: *architecture.CpuState) void {
|
||||
const me = scheduler.current();
|
||||
const endpoint = ipc.resolveHandle(me, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||
const r = ipc.send(endpoint, me.aspace, architecture.systemCallArg(state, 1), architecture.systemCallArg(state, 2), me.id);
|
||||
architecture.setSystemCallResult(state, @bitCast(r));
|
||||
}
|
||||
|
||||
/// device_enumerate(buffer, maximum) -> total: snapshot the device table into the caller's
|
||||
/// buffer (up to `maximum` entries), returning the total device count.
|
||||
fn systemDeviceEnumerate(state: *architecture.CpuState) void {
|
||||
|
|
@ -700,6 +713,11 @@ fn systemIrqAck(state: *architecture.CpuState) void {
|
|||
if (irq.ack(gsi)) architecture.setSystemCallResult(state, 0) else fail(state);
|
||||
}
|
||||
|
||||
/// Whether the debug_write stream sits at the start of a line — the last emitted
|
||||
/// byte was a newline (true at boot: nothing emitted yet). Guarded by the kernel
|
||||
/// lock in `systemDebugWrite`, like the stream it describes.
|
||||
var write_at_line_start: bool = true;
|
||||
|
||||
/// debug_write(ptr, len): copy bytes from user memory into the kernel log.
|
||||
/// A bring-up diagnostic — real output goes through the VFS/console later.
|
||||
///
|
||||
|
|
@ -709,17 +727,27 @@ fn systemIrqAck(state: *architecture.CpuState) void {
|
|||
/// Known gap (fine for trusted user code): a pointer into an *unmapped* hole in
|
||||
/// the user half passes the check and the read #PFs -> on_fault halts — a
|
||||
/// self-DoS, not an isolation break. Fault-recovering copy-in is a later item.
|
||||
///
|
||||
/// The emit runs under the kernel lock, so a message is atomic on the wire — two
|
||||
/// processes writing from different cores can interleave *messages*, never bytes.
|
||||
/// The "DANOS-INIT: " marker is emitted only at the start of a line (not per
|
||||
/// call), so a process may assemble a line from several writes without the marker
|
||||
/// (or, with the lock, another byte) landing in the middle. Cleanly-terminated
|
||||
/// lines from concurrent writers stay whole either way.
|
||||
fn systemDebugWrite(state: *architecture.CpuState) void {
|
||||
const ptr = architecture.systemCallArg(state, 0);
|
||||
const len = architecture.systemCallArg(state, 1);
|
||||
if (len <= write_buffer.len and ptr < user_half_end and ptr + len <= user_half_end) {
|
||||
const source: [*]const u8 = @ptrFromInt(ptr);
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
@memcpy(write_buffer[0..len], source[0..len]); // keep the latest message
|
||||
write_len = len;
|
||||
write_from_user = architecture.fromUser(state);
|
||||
write_count += 1;
|
||||
log.write("DANOS-INIT: ");
|
||||
if (write_at_line_start) log.write("DANOS-INIT: ");
|
||||
log.write(source[0..len]);
|
||||
if (len != 0) write_at_line_start = source[len - 1] == '\n';
|
||||
architecture.setSystemCallResult(state, len);
|
||||
} else {
|
||||
fail(state);
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||
initialRamdiskTest(boot_information);
|
||||
} else if (eql(case, "vfs")) {
|
||||
vfsTest(boot_information);
|
||||
} else if (eql(case, "input")) {
|
||||
inputTest(boot_information);
|
||||
} else if (eql(case, "hpet")) {
|
||||
hpetTest(boot_information);
|
||||
} else if (eql(case, "iopass")) {
|
||||
|
|
@ -1585,6 +1587,51 @@ fn vfsTest(boot_information: *const BootInformation) void {
|
|||
result();
|
||||
}
|
||||
|
||||
/// The full input path: spawn the input service, a synthetic source, and a subscriber from
|
||||
/// the initial_ramdisk. The source publishes keyboard, mouse, and joystick events in turn;
|
||||
/// the service routes them (with the asynchronous ipc_send) to the subscriber, which took
|
||||
/// all three classes and — only once it has received one — heartbeats "input-test: ok".
|
||||
/// Seeing that marker proves an event travelled source -> service -> subscriber over IPC,
|
||||
/// exercising the async buffered-send primitive, capability-passing subscription, and
|
||||
/// per-device routing. The source and service stay silent after startup so the subscriber's
|
||||
/// line is the one left in the shared evidence buffer.
|
||||
fn inputTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: input\n", .{});
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over an initial_ramdisk", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||
check("initial_ramdisk image is valid", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
|
||||
process.write_count = 0;
|
||||
process.write_from_user = false;
|
||||
_ = spawnNamed(rd, "input"); // the fan-out service
|
||||
_ = spawnNamed(rd, "input-source"); // a synthetic keyboard publishing events
|
||||
_ = spawnNamed(rd, "input-test"); // the subscriber whose "ok" line is the marker
|
||||
|
||||
// Wait for the subscriber's success heartbeat (it beats once per received event).
|
||||
const prefix = "input-test: ok";
|
||||
scheduler.setPriority(1);
|
||||
const deadline = architecture.millis() + 12000;
|
||||
while (architecture.millis() < deadline) {
|
||||
if (process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix) and process.write_count >= 2) break;
|
||||
scheduler.yield();
|
||||
}
|
||||
scheduler.setPriority(4);
|
||||
|
||||
const ok = process.write_len >= prefix.len and eql(process.write_buffer[0..prefix.len], prefix);
|
||||
check("a subscriber received a broadcast key event over IPC (source -> service -> subscriber)", ok);
|
||||
check("events kept flowing (service + async send stay up)", process.write_count >= 2);
|
||||
check("client syscalls came from user mode (CPL 3)", process.write_from_user);
|
||||
result();
|
||||
}
|
||||
|
||||
/// Process arguments, end to end: spawn args-echo bare (its argv[0] is the
|
||||
/// initial-ramdisk name). Instance 1 sees argc == 1 and respawns itself through
|
||||
/// `system_spawn` with the extra arguments "alpha beta-42" — the syscall argument
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//! hiding the trade-offs. Keeping them here makes them visible at a glance and gives
|
||||
//! one spot to change them. They're plain `comptime` constants (zero runtime cost);
|
||||
//! any one can later be promoted to a `-D` build option if a target needs to vary it
|
||||
//! (see build.zig's `-Dtest-case` for the pattern). This keeps root.zig to what it
|
||||
//! (see build.zig's `-Dtest-case` for the pattern). This keeps ps2-library.zig to what it
|
||||
//! actually is — the bootloader↔kernel handoff *contract* — with tunables living here.
|
||||
|
||||
/// Ceiling on logical CPUs the kernel tracks — the size of the per-CPU bookkeeping
|
||||
|
|
|
|||
|
|
@ -12,19 +12,34 @@
|
|||
//! HPET, decides `hpet` serves it, and brings that driver all the way up. (The
|
||||
//! kernel still auto-spawns the whole initial-ramdisk at boot; increment 3 removes
|
||||
//! that redundancy so the manager is the sole owner of driver spawning.)
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const device = runtime.device;
|
||||
const system = runtime.system;
|
||||
|
||||
/// The driver that serves each device class — the policy table. In a fuller system
|
||||
/// Format one whole log line and emit it in a single `debug_write`, so output
|
||||
/// from the drivers this manager starts (which run concurrently) can never land
|
||||
/// in the middle of it.
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// The driver that serves each device — the policy table. In a fuller system
|
||||
/// this comes from the drivers describing what they bind (or a manifest under
|
||||
/// /system/drivers); for now it is a small static map, which is enough to prove the
|
||||
/// manager reads the tree and decides. `null` = no driver for this class yet.
|
||||
fn driverFor(class: u64) ?[]const u8 {
|
||||
if (class == @intFromEnum(device.DeviceClass.timer)) return "hpet"; // the HPET
|
||||
fn driverFor(d: device.DeviceDescriptor) ?[]const u8 {
|
||||
// detect device via DeviceClass
|
||||
if (d.class == @intFromEnum(device.DeviceClass.timer)) return "hpet";
|
||||
// detect device via hid
|
||||
const hid = d.hid[0..@intCast(d.hid_len)];
|
||||
if (std.mem.eql(u8, hid, "PNP0303") or std.mem.eql(u8, hid, "PNP0F13")) return "ps2-bus";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
pub fn main() void {
|
||||
// Enumerate into a heap buffer (too big for the one-page user stack).
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
|
|
@ -36,16 +51,16 @@ pub fn main() void {
|
|||
|
||||
var matched: usize = 0;
|
||||
for (buffer[0..count]) |descriptor| {
|
||||
const driver_name = driverFor(descriptor.class) orelse continue;
|
||||
const driver_name = driverFor(descriptor) orelse continue;
|
||||
matched += 1;
|
||||
if (runtime.system.spawn(driver_name) != null) {
|
||||
_ = runtime.system.write("device-manager: spawned ");
|
||||
_ = runtime.system.write(driver_name);
|
||||
_ = runtime.system.write("\n");
|
||||
if (!system.isProcessRunning(driver_name)) {
|
||||
if (runtime.system.spawn(driver_name) != null) {
|
||||
writeLine("device-manager: spawned {s}\n", .{driver_name});
|
||||
} else {
|
||||
writeLine("device-manager: failed to spawn {s}\n", .{driver_name});
|
||||
}
|
||||
} else {
|
||||
_ = runtime.system.write("device-manager: failed to spawn ");
|
||||
_ = runtime.system.write(driver_name);
|
||||
_ = runtime.system.write("\n");
|
||||
writeLine("device-manager: already spawned {s}\n", .{driver_name});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const runtime = @import("runtime");
|
|||
/// microkernel keeps such choices in user space, not the kernel. Drivers are absent
|
||||
/// on purpose: the device manager owns those. (A future init reads this from a
|
||||
/// manifest under /system/services instead of a hardcoded list.)
|
||||
const boot_services = [_][]const u8{ "vfs", "device-manager" };
|
||||
const boot_services = [_][]const u8{ "vfs", "input", "device-manager" };
|
||||
|
||||
pub fn main() void {
|
||||
// Prove the heap end to end: allocate through the runtime allocator (which
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
//! system/services/input-source — a hardware-free synthetic input source, used to exercise
|
||||
//! the input service end to end without a real PS/2 controller (the `input` test case, and
|
||||
//! any bring-up where there is no hardware). It stands in for a driver: it connects to the
|
||||
//! input service and publishes a rolling stream that cycles through all device classes —
|
||||
//! keyboard, mouse, and joystick/gamepad — which the service routes to interested
|
||||
//! subscribers.
|
||||
//!
|
||||
//! It stays silent after startup (no per-event logging) so it can share the boot serial
|
||||
//! transcript with a subscriber whose output is the test's success marker. The real
|
||||
//! keyboard and mouse drivers publish their own synthetic streams today; swapping in
|
||||
//! decoded hardware is a follow-up (see docs/input.md).
|
||||
|
||||
const runtime = @import("runtime");
|
||||
const input = runtime.input;
|
||||
const system = runtime.system;
|
||||
|
||||
pub fn main() void {
|
||||
var source = input.connectSource() orelse {
|
||||
_ = system.write("input-source: input service unavailable\n");
|
||||
return;
|
||||
};
|
||||
_ = system.write("input-source: publishing synthetic input events\n");
|
||||
|
||||
var step: usize = 0;
|
||||
while (true) : (step +%= 1) {
|
||||
// Rotate across the device classes so every publish path (and the service's
|
||||
// per-device routing) is exercised.
|
||||
switch (step % 3) {
|
||||
0 => _ = source.publishKeyboardEvent(input.syntheticKeyEvent(step)),
|
||||
1 => _ = source.publishMouseEvent(input.syntheticMouseEvent(step)),
|
||||
else => _ = source.publishJoystickEvent(input.syntheticJoystickEvent(step)),
|
||||
}
|
||||
system.sleep(200);
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
//! system/services/input-test — the input service's client and test oracle, the input
|
||||
//! counterpart of vfs-test. It subscribes to *all* device classes and loops receiving the
|
||||
//! events a source broadcasts, logging each with its class. It emits the success marker
|
||||
//! `"input-test: ok"` only **after it has received at least one of each class** (keyboard,
|
||||
//! mouse, and joystick), then heartbeats it. So the in-kernel `input` test case seeing that
|
||||
//! marker proves not just that IPC delivery works but that the service *routed* all three
|
||||
//! device classes to one subscription — source -> service -> subscriber, per device.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const input = runtime.input;
|
||||
const system = runtime.system;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
var listener = input.subscribeAll() orelse {
|
||||
_ = system.write("input-test: could not subscribe\n");
|
||||
return;
|
||||
};
|
||||
_ = system.write("input-test: subscribed\n");
|
||||
|
||||
var seen_keyboard = false;
|
||||
var seen_mouse = false;
|
||||
var seen_joystick = false;
|
||||
while (true) {
|
||||
const event = listener.next() orelse continue;
|
||||
// Decode the class-specific payload from the tagged envelope and note the class.
|
||||
if (event.asKeyboard()) |key| {
|
||||
seen_keyboard = true;
|
||||
writeLine("input-test: got keyboard code={d} char={d}\n", .{ key.keycode, key.character });
|
||||
} else if (event.asMouse()) |mouse| {
|
||||
seen_mouse = true;
|
||||
writeLine("input-test: got mouse dx={d} dy={d} buttons={d}\n", .{ mouse.dx, mouse.dy, mouse.buttons });
|
||||
} else if (event.asJoystick()) |joystick| {
|
||||
seen_joystick = true;
|
||||
writeLine("input-test: got joystick control={d} value={d}\n", .{ joystick.control, joystick.value });
|
||||
} else {
|
||||
writeLine("input-test: got device={d}\n", .{event.device});
|
||||
}
|
||||
|
||||
// The success marker: only once every class has been routed here does this appear,
|
||||
// and then it heartbeats. Seeing "input-test: ok" proves per-device fan-out works.
|
||||
if (seen_keyboard and seen_mouse and seen_joystick) {
|
||||
_ = system.write("input-test: ok all classes received (keyboard, mouse, joystick)\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
//! system/services/input — the user-space input service. Shipped in the initial_ramdisk,
|
||||
//! spawned as a ring-3 process, and published under the well-known `input` service id. It
|
||||
//! is the fan-out point between **sources** (keyboard, mouse, and joystick/gamepad drivers)
|
||||
//! and **subscribers** (any program that wants input): a source `publish`es an
|
||||
//! `InputEvent`, and the service pushes it to every subscriber whose interest mask includes
|
||||
//! that event's device class (keyboard / mouse / joystick).
|
||||
//!
|
||||
//! The delivery discipline is the whole design (see docs/input.md). The kernel's IPC is a
|
||||
//! synchronous rendezvous: a server holds one pending reply, so it cannot park N
|
||||
//! subscribers blocked in a "wait for next event" call. Broadcasting therefore has to be
|
||||
//! *push* — the service delivering to subscribers. But a synchronous push (`ipc_call`)
|
||||
//! would let one dead or wedged subscriber hang the whole broadcast, since the kernel
|
||||
//! never wakes a sender parked on a dead peer's endpoint. So delivery uses the
|
||||
//! asynchronous `ipc.send`: it posts the event to each subscriber's endpoint queue and
|
||||
//! returns at once, and can never block on a subscriber. That primitive exists for exactly
|
||||
//! this ([ipc.md](../../../docs/ipc.md), "asynchronous / buffered send").
|
||||
//!
|
||||
//! A subscriber registers by handing the service its own endpoint as a capability (M13
|
||||
//! capability passing — this service is its first real consumer). The service keeps that
|
||||
//! handle and `ipc.send`s each event to it.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const protocol = runtime.input_protocol;
|
||||
const ipc = runtime.ipc;
|
||||
const system = runtime.system;
|
||||
|
||||
/// One registered subscriber: the endpoint we push events to (a capability it handed us at
|
||||
/// subscribe time) and the task id that owns it (the subscribe call's badge), so a slot
|
||||
/// left behind by a subscriber that exited can be reclaimed.
|
||||
const Subscriber = struct {
|
||||
used: bool = false,
|
||||
endpoint: ipc.Handle = 0,
|
||||
task_id: u32 = 0,
|
||||
/// Which device classes this subscriber wants (an OR of protocol.device_*). An event
|
||||
/// is delivered only if its device's bit is set here.
|
||||
device_mask: u32 = 0,
|
||||
};
|
||||
|
||||
var subscribers = [_]Subscriber{.{}} ** 8;
|
||||
|
||||
/// Drop any subscriber whose owning process is no longer alive, so its slot (and the
|
||||
/// endpoint reference it holds) can be reused. Cheap and only run on subscribe — the async
|
||||
/// `send` to a dead subscriber's orphaned endpoint is harmless (it just fills a queue no
|
||||
/// one drains), so this is housekeeping, not correctness.
|
||||
fn pruneDeadSubscribers() void {
|
||||
var table: [32]system.ProcessDescriptor = undefined;
|
||||
const total = system.processes(&table);
|
||||
const count = @min(total, table.len);
|
||||
for (&subscribers) |*sub| {
|
||||
if (!sub.used) continue;
|
||||
var alive = false;
|
||||
for (table[0..count]) |descriptor| {
|
||||
if (descriptor.id == sub.task_id) {
|
||||
alive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!alive) sub.* = .{};
|
||||
}
|
||||
}
|
||||
|
||||
/// Register `endpoint` (owned by task `task_id`) to receive the device classes in
|
||||
/// `device_mask`. Returns false if the subscriber table is full.
|
||||
fn addSubscriber(endpoint: ipc.Handle, task_id: u32, device_mask: u32) bool {
|
||||
for (&subscribers) |*sub| {
|
||||
if (!sub.used) {
|
||||
sub.* = .{ .used = true, .endpoint = endpoint, .task_id = task_id, .device_mask = device_mask };
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Push `event` to every subscriber whose interest mask includes its device class.
|
||||
/// `ipc.send` never blocks, so a slow or dead subscriber cannot stall delivery to others.
|
||||
fn broadcast(event: protocol.InputEvent) void {
|
||||
const bytes = std.mem.asBytes(&event);
|
||||
const bit = protocol.deviceBit(event.device);
|
||||
for (&subscribers) |*sub| {
|
||||
if (sub.used and sub.device_mask & bit != 0) _ = ipc.send(sub.endpoint, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle one request. `got` carries the sender badge (a task id) and, for subscribe, the
|
||||
/// subscriber's endpoint capability in `got.cap`. Writes a `Reply` into `out` and returns
|
||||
/// its length.
|
||||
fn handle(message: []const u8, got: ipc.Received, out: []u8) usize {
|
||||
const reply = struct {
|
||||
fn write(buffer: []u8, status: i32) usize {
|
||||
const header = protocol.Reply{ .status = status };
|
||||
@memcpy(buffer[0..protocol.reply_size], std.mem.asBytes(&header));
|
||||
return protocol.reply_size;
|
||||
}
|
||||
};
|
||||
|
||||
if (message.len < protocol.request_size) return reply.write(out, -1);
|
||||
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
||||
|
||||
switch (@as(protocol.Operation, @enumFromInt(request.operation))) {
|
||||
.subscribe => {
|
||||
const endpoint = got.cap orelse return reply.write(out, -1); // no endpoint passed
|
||||
// A zero mask means "everything" (a subscriber that named no class still wants input).
|
||||
const mask = if (request.device_mask == 0) protocol.device_all else request.device_mask;
|
||||
pruneDeadSubscribers();
|
||||
if (!addSubscriber(endpoint, @intCast(got.badge), mask)) return reply.write(out, -1); // table full
|
||||
return reply.write(out, 0);
|
||||
},
|
||||
.publish => {
|
||||
broadcast(request.event);
|
||||
return reply.write(out, 0);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||
_ = system.write("input: no endpoint\n");
|
||||
return;
|
||||
};
|
||||
if (!ipc.register(.input, endpoint)) {
|
||||
_ = system.write("input: register failed\n");
|
||||
return;
|
||||
}
|
||||
_ = system.write("input: ready\n");
|
||||
|
||||
var reply_buffer: [protocol.reply_size]u8 = undefined;
|
||||
var reply_len: usize = 0;
|
||||
var receive: [protocol.request_size]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
|
||||
// Only synchronous client requests (subscribe/publish) arrive here; nothing sends
|
||||
// this service asynchronous messages, so a notification wake would be spurious.
|
||||
if (got.isNotification()) {
|
||||
reply_len = 0;
|
||||
continue;
|
||||
}
|
||||
reply_len = handle(receive[0..got.len], got, &reply_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
//! The input wire protocol — the message format spoken between the user-space input
|
||||
//! service ([input.zig](input.zig)) and the two kinds of process that reach it: a
|
||||
//! **source** (a keyboard, mouse, or joystick/gamepad driver) that publishes events, and a
|
||||
//! **subscriber** (any program) that subscribes and is then pushed each event.
|
||||
//!
|
||||
//! The service handles several device classes over one endpoint. Each class has its own
|
||||
//! typed event (`KeyEvent`, `MouseEvent`, `JoystickEvent`); they all travel in a common
|
||||
//! `InputEvent` envelope tagged with a `DeviceKind`, so the fan-out path is one code path
|
||||
//! and a subscriber can take a mix of devices on a single stream. A subscriber declares
|
||||
//! which classes it wants with a `device_mask`, and the service routes accordingly.
|
||||
//!
|
||||
//! Two message shapes ride over the endpoint, tagged by `Operation`, like the
|
||||
//! [VFS protocol](../vfs/protocol.zig):
|
||||
//!
|
||||
//! - **subscribe / publish**: a synchronous `ipc_call` carrying a `Request`. `subscribe`
|
||||
//! hands the service the subscriber's own endpoint as a capability (`send_cap`) and a
|
||||
//! `device_mask`; `publish` carries an `InputEvent`. The reply is a `Reply`.
|
||||
//! - **delivery**: the service pushes each `InputEvent` to every interested subscriber with
|
||||
//! the asynchronous `ipc_send` — no reply owed, and a dead subscriber can never stall the
|
||||
//! broadcast. Received in the subscriber's buffer with `Received.isMessage()` set.
|
||||
//!
|
||||
//! This is a danos-native contract, shared by the input service, the `runtime.input`
|
||||
//! client helpers, and every source/subscriber. Everything fits one IPC message.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// The classes of input device the service fans out. Each names a typed event and a bit in
|
||||
/// the subscription mask.
|
||||
pub const DeviceKind = enum(u32) {
|
||||
keyboard = 0,
|
||||
mouse = 1,
|
||||
joystick = 2, // joysticks and gamepads/controllers
|
||||
};
|
||||
|
||||
/// Subscription-interest bits (`Request.device_mask`) — which device classes a subscriber
|
||||
/// wants. OR them together, or use `device_all`.
|
||||
pub const device_keyboard: u32 = 1 << 0;
|
||||
pub const device_mouse: u32 = 1 << 1;
|
||||
pub const device_joystick: u32 = 1 << 2;
|
||||
pub const device_all: u32 = device_keyboard | device_mouse | device_joystick;
|
||||
|
||||
/// The subscription bit for a `DeviceKind` value (as it appears in `InputEvent.device`).
|
||||
/// An unknown device maps to 0, so it matches no subscriber.
|
||||
pub fn deviceBit(device: u32) u32 {
|
||||
return switch (device) {
|
||||
@intFromEnum(DeviceKind.keyboard) => device_keyboard,
|
||||
@intFromEnum(DeviceKind.mouse) => device_mouse,
|
||||
@intFromEnum(DeviceKind.joystick) => device_joystick,
|
||||
else => 0,
|
||||
};
|
||||
}
|
||||
|
||||
// --- keyboard ---------------------------------------------------------------
|
||||
|
||||
/// What happened to a key. `key_down`/`key_up` are the physical make/break; `key_press`
|
||||
/// is the higher-level "a character was produced", carrying it in `KeyEvent.character`.
|
||||
pub const EventKind = enum(u32) {
|
||||
key_down = 0,
|
||||
key_up = 1,
|
||||
key_press = 2,
|
||||
};
|
||||
|
||||
/// One keyboard event. `keycode` names the physical key (layout-independent); `character`
|
||||
/// is the Unicode scalar for `key_press` (else 0); `modifiers` is an OR of `modifier_*`.
|
||||
pub const KeyEvent = extern struct {
|
||||
kind: u32, // an EventKind
|
||||
keycode: u32, // a Keycode
|
||||
character: u32, // Unicode scalar for key_press, else 0
|
||||
modifiers: u32, // OR of modifier_*
|
||||
};
|
||||
|
||||
pub const modifier_shift: u32 = 1 << 0;
|
||||
pub const modifier_control: u32 = 1 << 1;
|
||||
pub const modifier_alt: u32 = 1 << 2;
|
||||
|
||||
/// A minimal danos-native keycode namespace — enough for the synthetic source and to show
|
||||
/// the shape. A real set (USB HID usage-style) fills in with the scancode decoder.
|
||||
pub const Keycode = enum(u32) {
|
||||
unknown = 0,
|
||||
a = 4, // deliberately USB-HID-usage-aligned so a real decoder can extend this
|
||||
b = 5,
|
||||
c = 6,
|
||||
d = 7,
|
||||
e = 8,
|
||||
enter = 40,
|
||||
_,
|
||||
};
|
||||
|
||||
// --- mouse ------------------------------------------------------------------
|
||||
|
||||
/// What a mouse event reports. `motion` carries relative `dx`/`dy`; `button_down`/`up`
|
||||
/// name a button in `button`; `scroll` carries `scroll_x`/`scroll_y`.
|
||||
pub const MouseEventKind = enum(u32) {
|
||||
motion = 0,
|
||||
button_down = 1,
|
||||
button_up = 2,
|
||||
scroll = 3,
|
||||
};
|
||||
|
||||
pub const mouse_button_left: u32 = 1 << 0;
|
||||
pub const mouse_button_right: u32 = 1 << 1;
|
||||
pub const mouse_button_middle: u32 = 1 << 2;
|
||||
|
||||
/// One mouse event. Relative motion (`dx`/`dy`) and wheel (`scroll_*`) are signed;
|
||||
/// `buttons` is the current pressed-button bitmask (`mouse_button_*`).
|
||||
pub const MouseEvent = extern struct {
|
||||
kind: u32, // a MouseEventKind
|
||||
button: u32, // the mouse_button_* bit for button_down/up, else 0
|
||||
dx: i32, // relative X motion (.motion)
|
||||
dy: i32, // relative Y motion (.motion)
|
||||
scroll_x: i32, // horizontal wheel (.scroll)
|
||||
scroll_y: i32, // vertical wheel (.scroll)
|
||||
buttons: u32, // current pressed-button bitmask
|
||||
};
|
||||
|
||||
// --- joystick / gamepad -----------------------------------------------------
|
||||
|
||||
/// What a joystick/gamepad event reports. `axis` carries a signed `value` on axis
|
||||
/// `control`; `button_down`/`up` name a button index in `control`.
|
||||
pub const JoystickEventKind = enum(u32) {
|
||||
axis = 0,
|
||||
button_down = 1,
|
||||
button_up = 2,
|
||||
};
|
||||
|
||||
/// One joystick/gamepad event. `control` is the axis index (`.axis`) or button index
|
||||
/// (button events); `value` is the axis position (signed, e.g. -32768..32767) for `.axis`;
|
||||
/// `buttons` is the current pressed-button bitmask.
|
||||
pub const JoystickEvent = extern struct {
|
||||
kind: u32, // a JoystickEventKind
|
||||
control: u32, // axis index (.axis) or button index (button events)
|
||||
value: i32, // axis value for .axis, else 0
|
||||
buttons: u32, // current pressed-button bitmask
|
||||
};
|
||||
|
||||
// --- the common envelope ----------------------------------------------------
|
||||
|
||||
/// The largest per-device event, so `InputEvent` can hold any of them inline.
|
||||
pub const max_event_size: usize = @max(@sizeOf(KeyEvent), @max(@sizeOf(MouseEvent), @sizeOf(JoystickEvent)));
|
||||
|
||||
/// The tagged envelope broadcast to subscribers: a `DeviceKind` plus the raw bytes of the
|
||||
/// matching per-device event. Decode it with `asKeyboard`/`asMouse`/`asJoystick` (each
|
||||
/// returns null unless `device` matches), or build one with the `from*` constructors.
|
||||
pub const InputEvent = extern struct {
|
||||
device: u32, // a DeviceKind
|
||||
_padding: u32 = 0,
|
||||
data: [max_event_size]u8 = [_]u8{0} ** max_event_size,
|
||||
|
||||
pub fn asKeyboard(self: InputEvent) ?KeyEvent {
|
||||
if (self.device != @intFromEnum(DeviceKind.keyboard)) return null;
|
||||
return std.mem.bytesToValue(KeyEvent, self.data[0..@sizeOf(KeyEvent)]);
|
||||
}
|
||||
pub fn asMouse(self: InputEvent) ?MouseEvent {
|
||||
if (self.device != @intFromEnum(DeviceKind.mouse)) return null;
|
||||
return std.mem.bytesToValue(MouseEvent, self.data[0..@sizeOf(MouseEvent)]);
|
||||
}
|
||||
pub fn asJoystick(self: InputEvent) ?JoystickEvent {
|
||||
if (self.device != @intFromEnum(DeviceKind.joystick)) return null;
|
||||
return std.mem.bytesToValue(JoystickEvent, self.data[0..@sizeOf(JoystickEvent)]);
|
||||
}
|
||||
|
||||
pub fn fromKeyboard(event: KeyEvent) InputEvent {
|
||||
return pack(.keyboard, std.mem.asBytes(&event));
|
||||
}
|
||||
pub fn fromMouse(event: MouseEvent) InputEvent {
|
||||
return pack(.mouse, std.mem.asBytes(&event));
|
||||
}
|
||||
pub fn fromJoystick(event: JoystickEvent) InputEvent {
|
||||
return pack(.joystick, std.mem.asBytes(&event));
|
||||
}
|
||||
|
||||
fn pack(device: DeviceKind, bytes: []const u8) InputEvent {
|
||||
var self = InputEvent{ .device = @intFromEnum(device) };
|
||||
@memcpy(self.data[0..bytes.len], bytes);
|
||||
return self;
|
||||
}
|
||||
};
|
||||
|
||||
// --- request / reply --------------------------------------------------------
|
||||
|
||||
/// Which side of a request this is.
|
||||
pub const Operation = enum(u32) {
|
||||
subscribe = 0, // register the caller's endpoint (send_cap) for the classes in device_mask
|
||||
publish = 1, // a source submits `event` to broadcast to interested subscribers
|
||||
};
|
||||
|
||||
/// Request header. For `subscribe`, `device_mask` is the OR of `device_*` bits the caller
|
||||
/// wants (0 means all) and the caller's receive endpoint travels as the call's capability;
|
||||
/// `event` is ignored. For `publish`, `event` is the event to broadcast.
|
||||
pub const Request = extern struct {
|
||||
operation: u32, // an Operation
|
||||
device_mask: u32 = 0, // subscribe: interested device classes (0 => all)
|
||||
event: InputEvent = .{ .device = 0 },
|
||||
};
|
||||
|
||||
/// Reply header. `status` is 0 on success or a negative errno.
|
||||
pub const Reply = extern struct {
|
||||
status: i32,
|
||||
_padding: u32 = 0,
|
||||
};
|
||||
|
||||
pub const request_size: usize = @sizeOf(Request);
|
||||
pub const reply_size: usize = @sizeOf(Reply);
|
||||
pub const event_size: usize = @sizeOf(InputEvent);
|
||||
|
||||
comptime {
|
||||
// The delivery path posts a bare InputEvent through ipc_send, so it must fit an
|
||||
// endpoint's async payload slot (POST_MAXIMUM is 64).
|
||||
if (event_size > 64) @compileError("InputEvent must fit the ipc_send payload (POST_MAXIMUM)");
|
||||
}
|
||||
|
|
@ -250,6 +250,12 @@ CASES = [
|
|||
{"name": "vfs",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# The input service: a synthetic keyboard source publishes events, the service
|
||||
# broadcasts them over the async ipc_send primitive, and a subscriber (which joined by
|
||||
# passing its endpoint as a capability) receives them — source -> service -> subscriber.
|
||||
{"name": "input",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# IO passthrough + IRQ-as-IPC: a user-space HPET driver maps device MMIO into
|
||||
# its own address space, binds the device's interrupt to an IPC endpoint, and
|
||||
# is woken by the hardware five times while blocked (never polling).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,468 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Vendor xkeyboard-config and compile it to a native Zig keymap library.
|
||||
|
||||
danos does not ship an X11 runtime, but it wants X11's keyboard *data*: the layout
|
||||
tables (US, UK, German, ...) that turn a physical key + modifiers into a character.
|
||||
So we compile xkeyboard-config down to Zig at build time, the same way the initial
|
||||
ramdisk is packed by a host-side Python tool.
|
||||
|
||||
Two subcommands:
|
||||
|
||||
fetch Download the pinned xkeyboard-config release, verify its sha256, and copy
|
||||
the transitively-needed data (the symbols files for the configured layouts
|
||||
plus everything they `include`) into library/xkeyboard-config/vendor/,
|
||||
alongside a vendored keysymdef.h, the upstream COPYING, and a PROVENANCE.md.
|
||||
This is the one step that needs the network; run it when bumping the version.
|
||||
|
||||
generate Parse the vendored data and emit library/xkeyboard-config/generated/layouts.zig
|
||||
(deterministic, offline). Run whenever the layout list or emitter changes.
|
||||
|
||||
The pipeline per key: our events carry a USB HID usage; HID_TO_NAME maps it to an XKB
|
||||
key name (<AC01>); the layout's symbols give the keysyms per level; keysymdef.h resolves
|
||||
each keysym name to its value and Unicode scalar. Level *selection* (which modifier picks
|
||||
which level) is deliberately left to the Zig side (xkeyboard-config.zig) — we only emit the
|
||||
per-key type and its up-to-4 keysyms here.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
# --- pinned upstream -------------------------------------------------------
|
||||
|
||||
VERSION = "2.44"
|
||||
TARBALL_URL = (
|
||||
"https://gitlab.freedesktop.org/xkeyboard-config/xkeyboard-config/-/archive/"
|
||||
f"xkeyboard-config-{VERSION}/xkeyboard-config-{VERSION}.tar.gz"
|
||||
)
|
||||
TARBALL_SHA256 = "35e34edeaf4e8da8d0696ff6b241ee11ddb1b8c6730bac7252d4d0a88ea5f05b"
|
||||
|
||||
# keysymdef.h is xorgproto, not xkeyboard-config; fetch copies it from the build host.
|
||||
KEYSYMDEF_CANDIDATES = [
|
||||
"/opt/homebrew/include/X11/keysymdef.h",
|
||||
"/usr/include/X11/keysymdef.h",
|
||||
"/usr/X11/include/X11/keysymdef.h",
|
||||
"/usr/X11R6/include/X11/keysymdef.h",
|
||||
]
|
||||
|
||||
# The layouts we generate: (zig name, symbols file, section or None=default).
|
||||
TARGETS = [
|
||||
("us", "us", None),
|
||||
("gb", "gb", None),
|
||||
("de", "de", None),
|
||||
("fr", "fr", None),
|
||||
("es", "es", None),
|
||||
("dvorak", "us", "dvorak"),
|
||||
]
|
||||
|
||||
# USB HID usage (keyboard page 0x07) -> XKB key name. The physical keys we can turn into
|
||||
# characters; positions are the ANSI standard shared by HID usages and XKB names.
|
||||
HID_TO_NAME = {
|
||||
0x04: "AC01", 0x05: "AB05", 0x06: "AB03", 0x07: "AC03", 0x08: "AD03",
|
||||
0x09: "AC04", 0x0A: "AC05", 0x0B: "AC06", 0x0C: "AD08", 0x0D: "AC07",
|
||||
0x0E: "AC08", 0x0F: "AC09", 0x10: "AB07", 0x11: "AB06", 0x12: "AD09",
|
||||
0x13: "AD10", 0x14: "AD01", 0x15: "AD04", 0x16: "AC02", 0x17: "AD05",
|
||||
0x18: "AD07", 0x19: "AB04", 0x1A: "AD02", 0x1B: "AB02", 0x1C: "AD06",
|
||||
0x1D: "AB01",
|
||||
0x1E: "AE01", 0x1F: "AE02", 0x20: "AE03", 0x21: "AE04", 0x22: "AE05",
|
||||
0x23: "AE06", 0x24: "AE07", 0x25: "AE08", 0x26: "AE09", 0x27: "AE10",
|
||||
0x2C: "SPCE",
|
||||
0x2D: "AE11", 0x2E: "AE12", 0x2F: "AD11", 0x30: "AD12", 0x31: "BKSL",
|
||||
0x33: "AC10", 0x34: "AC11", 0x35: "TLDE",
|
||||
0x36: "AB08", 0x37: "AB09", 0x38: "AB10",
|
||||
0x64: "LSGT", # the extra key on ISO keyboards (102nd key)
|
||||
}
|
||||
|
||||
# XKB type name -> the KeyType enum tag emitted for the Zig side.
|
||||
TYPE_TO_TAG = {
|
||||
"ONE_LEVEL": "one_level",
|
||||
"TWO_LEVEL": "two_level",
|
||||
"ALPHABETIC": "alphabetic",
|
||||
"FOUR_LEVEL": "four_level",
|
||||
"FOUR_LEVEL_ALPHABETIC": "four_level_alphabetic",
|
||||
"FOUR_LEVEL_SEMIALPHABETIC": "four_level_semialphabetic",
|
||||
"FOUR_LEVEL_MIXED_KEYPAD": "keypad",
|
||||
"FOUR_LEVEL_KEYPAD": "keypad",
|
||||
"KEYPAD": "keypad",
|
||||
"PC_SYSRQ": "other",
|
||||
}
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LIB = os.path.join(REPO, "library", "xkeyboard-config")
|
||||
VENDOR = os.path.join(LIB, "vendor")
|
||||
|
||||
|
||||
# --- keysymdef.h: keysym name -> (value, unicode scalar or None) ------------
|
||||
|
||||
def parse_keysymdef(path):
|
||||
table = {}
|
||||
line_re = re.compile(r"#define\s+XK_(\w+)\s+0x([0-9a-fA-F]+)\s*(?:/\*\s*U\+([0-9A-Fa-f]+)|/\*<U\+([0-9A-Fa-f]+))?")
|
||||
with open(path, encoding="latin-1") as f:
|
||||
for line in f:
|
||||
m = line_re.match(line)
|
||||
if not m:
|
||||
continue
|
||||
name = m.group(1)
|
||||
value = int(m.group(2), 16)
|
||||
uni = m.group(3) or m.group(4)
|
||||
unicode_scalar = int(uni, 16) if uni else None
|
||||
# First definition wins (keysymdef lists the canonical one first).
|
||||
table.setdefault(name, (value, unicode_scalar))
|
||||
return table
|
||||
|
||||
|
||||
def resolve_keysym(name, keysymdef):
|
||||
"""A keysym token from a symbols file -> (keysym value, unicode scalar or 0)."""
|
||||
if name in ("NoSymbol", "VoidSymbol", ""):
|
||||
return (0, 0)
|
||||
# Unicode escape forms: U00E9 / u00E9.
|
||||
m = re.fullmatch(r"[Uu]([0-9A-Fa-f]{4,6})", name)
|
||||
if m:
|
||||
cp = int(m.group(1), 16)
|
||||
return (0x01000000 + cp, cp)
|
||||
# Raw hex keysym value: 0x1000161 (a Unicode keysym) or a plain keysym number.
|
||||
m = re.fullmatch(r"0x([0-9A-Fa-f]+)", name)
|
||||
if m:
|
||||
value = int(m.group(1), 16)
|
||||
if 0x01000100 <= value <= 0x0110FFFF:
|
||||
return (value, value - 0x01000000)
|
||||
if 0x20 <= value <= 0x7E or 0xA0 <= value <= 0xFF:
|
||||
return (value, value)
|
||||
return (value, 0)
|
||||
entry = keysymdef.get(name)
|
||||
if entry is None:
|
||||
return (0, 0) # unknown named keysym (dead_*, ISO_*, rare) -> no character
|
||||
value, unicode_scalar = entry
|
||||
return (value, unicode_scalar or 0)
|
||||
|
||||
|
||||
# --- symbols parsing --------------------------------------------------------
|
||||
|
||||
def strip_comments(text):
|
||||
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
|
||||
text = re.sub(r"//[^\n]*", "", text)
|
||||
return text
|
||||
|
||||
|
||||
class KeyDef:
|
||||
__slots__ = ("levels", "type_name")
|
||||
|
||||
def __init__(self, levels, type_name):
|
||||
self.levels = levels # list of keysym-name strings (group 1)
|
||||
self.type_name = type_name # explicit XKB type name, or None
|
||||
|
||||
|
||||
class Symbols:
|
||||
"""Loads and resolves xkb_symbols sections from a directory of symbols files."""
|
||||
|
||||
def __init__(self, symbols_dir):
|
||||
self.dir = symbols_dir
|
||||
self._files = {} # filename -> {section_name: body, "__default__": name}
|
||||
self.visited_files = set()
|
||||
|
||||
def _load(self, fname):
|
||||
if fname in self._files:
|
||||
return self._files[fname]
|
||||
path = os.path.join(self.dir, fname)
|
||||
self.visited_files.add(fname)
|
||||
with open(path, encoding="latin-1") as f:
|
||||
text = strip_comments(f.read())
|
||||
sections = {}
|
||||
default = None
|
||||
# Find each `[flags] xkb_symbols "NAME" {` and brace-match its body.
|
||||
for m in re.finditer(r'(\w[\w\s]*?)?\bxkb_symbols\s+"([^"]+)"\s*\{', text):
|
||||
flags = m.group(1) or ""
|
||||
name = m.group(2)
|
||||
body, _ = self._brace_match(text, m.end() - 1)
|
||||
sections[name] = body
|
||||
if "default" in flags.split() and default is None:
|
||||
default = name
|
||||
if default is None and sections:
|
||||
default = next(iter(sections))
|
||||
sections["__default__"] = default
|
||||
self._files[fname] = sections
|
||||
return sections
|
||||
|
||||
@staticmethod
|
||||
def _brace_match(text, open_index):
|
||||
depth = 0
|
||||
for i in range(open_index, len(text)):
|
||||
c = text[i]
|
||||
if c == "{":
|
||||
depth += 1
|
||||
elif c == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return text[open_index + 1:i], i
|
||||
raise ValueError("unbalanced braces")
|
||||
|
||||
def resolve(self, fname, section=None, stack=()):
|
||||
"""Merged {key name -> KeyDef} for (fname, section), following includes."""
|
||||
sections = self._load(fname)
|
||||
if section is None:
|
||||
section = sections["__default__"]
|
||||
key = (fname, section)
|
||||
if key in stack:
|
||||
return {}
|
||||
body = sections.get(section)
|
||||
if body is None:
|
||||
return {}
|
||||
keys = {}
|
||||
default_type = None
|
||||
for stmt in self._statements(body):
|
||||
kind = stmt[0]
|
||||
if kind == "include":
|
||||
augment, inc_file, inc_section = stmt[1], stmt[2], stmt[3]
|
||||
inc = self.resolve(inc_file, inc_section, stack + (key,))
|
||||
for n, kd in inc.items():
|
||||
if augment and n in keys:
|
||||
continue
|
||||
keys[n] = kd
|
||||
elif kind == "default_type":
|
||||
default_type = stmt[1]
|
||||
elif kind == "key":
|
||||
augment, name, kd = stmt[1], stmt[2], stmt[3]
|
||||
if augment and name in keys:
|
||||
continue
|
||||
keys[name] = kd
|
||||
if default_type is not None:
|
||||
for kd in keys.values():
|
||||
if kd.type_name is None:
|
||||
kd.type_name = default_type
|
||||
return keys
|
||||
|
||||
def _statements(self, body):
|
||||
"""Yield ('include', augment, file, section) / ('default_type', name) /
|
||||
('key', augment, name, KeyDef) in source order."""
|
||||
# Recognise the three statement heads and step through the body in order.
|
||||
head = re.compile(
|
||||
r'(?P<inc>(?:(?P<augi>augment|override|replace)\s+)?include\s+"(?P<incarg>[^"]+)")'
|
||||
r'|(?P<dtype>key\.type(?:\[[^\]]*\])?\s*=\s*"(?P<dtypearg>[^"]+)")'
|
||||
r'|(?P<key>(?:(?P<augk>augment|override|replace)\s+)?key\s+<(?P<kname>[^>]+)>\s*\{)'
|
||||
)
|
||||
i = 0
|
||||
while i < len(body):
|
||||
m = head.search(body, i)
|
||||
if not m:
|
||||
break
|
||||
if m.group("inc"):
|
||||
inc_file, inc_section = self._parse_include(m.group("incarg"))
|
||||
yield ("include", m.group("augi") == "augment", inc_file, inc_section)
|
||||
i = m.end()
|
||||
elif m.group("dtype"):
|
||||
yield ("default_type", m.group("dtypearg"))
|
||||
i = m.end()
|
||||
else: # a key definition; brace-match its body
|
||||
kbody, end = self._brace_match(body, m.end() - 1)
|
||||
kd = self._parse_key_body(kbody)
|
||||
if kd is not None:
|
||||
yield ("key", m.group("augk") == "augment", m.group("kname"), kd)
|
||||
i = end + 1
|
||||
|
||||
@staticmethod
|
||||
def _parse_include(arg):
|
||||
m = re.fullmatch(r"([^()]+)(?:\(([^)]+)\))?", arg.strip())
|
||||
return (m.group(1), m.group(2))
|
||||
|
||||
@staticmethod
|
||||
def _parse_key_body(kbody):
|
||||
type_name = None
|
||||
mt = re.search(r'type(?:\[[^\]]*\])?\s*=\s*"([^"]+)"', kbody)
|
||||
if mt:
|
||||
type_name = mt.group(1)
|
||||
groups = re.findall(r"\[([^\]]*)\]", kbody)
|
||||
if not groups:
|
||||
return None
|
||||
levels = [tok.strip() for tok in groups[0].split(",")]
|
||||
levels = [t for t in levels if t != ""]
|
||||
if not levels:
|
||||
return None
|
||||
return KeyDef(levels, type_name)
|
||||
|
||||
|
||||
# --- type inference + emission ---------------------------------------------
|
||||
|
||||
def is_case_pair(a, b):
|
||||
return len(a) == 1 and len(b) == 1 and a.isalpha() and a.islower() and b == a.upper()
|
||||
|
||||
|
||||
def key_type_tag(kd):
|
||||
if kd.type_name is not None:
|
||||
return TYPE_TO_TAG.get(kd.type_name, "other")
|
||||
n = len(kd.levels)
|
||||
if n <= 1:
|
||||
return "one_level"
|
||||
if n == 2:
|
||||
return "alphabetic" if is_case_pair(kd.levels[0], kd.levels[1]) else "two_level"
|
||||
return "four_level_alphabetic" if is_case_pair(kd.levels[0], kd.levels[1]) else "four_level"
|
||||
|
||||
|
||||
def build_layout(symbols, keysymdef, fname, section):
|
||||
keys = symbols.resolve(fname, section)
|
||||
table = [] # 256 entries: (tag, [(keysym, unicode) x4])
|
||||
for hid in range(256):
|
||||
name = HID_TO_NAME.get(hid)
|
||||
kd = keys.get(name) if name else None
|
||||
if kd is None:
|
||||
table.append(("one_level", [(0, 0)] * 4))
|
||||
continue
|
||||
levels = [resolve_keysym(kd.levels[i], keysymdef) if i < len(kd.levels) else (0, 0)
|
||||
for i in range(4)]
|
||||
table.append((key_type_tag(kd), levels))
|
||||
return table
|
||||
|
||||
|
||||
def emit_zig(layouts):
|
||||
out = io.StringIO()
|
||||
out.write("// GENERATED by tools/make-xkeyboard-config.py from xkeyboard-config "
|
||||
f"{VERSION}. Do not edit.\n")
|
||||
out.write("// Keyboard layout tables compiled from the X11 xkeyboard-config database\n")
|
||||
out.write("// (MIT/X11 licensed; see ../vendor/COPYING and ../vendor/PROVENANCE.md).\n\n")
|
||||
out.write("pub const Level = struct { keysym: u32 = 0, unicode: u21 = 0 };\n\n")
|
||||
out.write("pub const KeyType = enum {\n")
|
||||
out.write(" one_level,\n two_level,\n alphabetic,\n four_level,\n"
|
||||
" four_level_alphabetic,\n four_level_semialphabetic,\n keypad,\n other,\n};\n\n")
|
||||
out.write("pub const Key = struct { kind: KeyType = .one_level, levels: [4]Level = [_]Level{.{}} ** 4 };\n\n")
|
||||
out.write("pub const Layout = struct { name: []const u8, keys: [256]Key };\n\n")
|
||||
|
||||
names = []
|
||||
for zig_name, fname, section in TARGETS:
|
||||
table = layouts[zig_name]
|
||||
names.append(zig_name)
|
||||
out.write(f"pub const {zig_name}: Layout = .{{\n")
|
||||
out.write(f' .name = "{zig_name}",\n')
|
||||
out.write(" .keys = .{\n")
|
||||
for hid, (tag, levels) in enumerate(table):
|
||||
if tag == "one_level" and all(k == 0 and u == 0 for k, u in levels):
|
||||
out.write(" .{},\n")
|
||||
continue
|
||||
parts = ", ".join(f".{{ .keysym = {k}, .unicode = {u} }}" for k, u in levels)
|
||||
out.write(f" .{{ .kind = .{tag}, .levels = .{{ {parts} }} }},\n")
|
||||
out.write(" },\n};\n\n")
|
||||
|
||||
out.write("pub const all = [_]*const Layout{ " + ", ".join("&" + n for n in names) + " };\n")
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
# --- fetch ------------------------------------------------------------------
|
||||
|
||||
def collect_needed(symbols):
|
||||
for _, fname, section in TARGETS:
|
||||
symbols.resolve(fname, section)
|
||||
return set(symbols.visited_files)
|
||||
|
||||
|
||||
def find_keysymdef():
|
||||
env = os.environ.get("KEYSYMDEF")
|
||||
if env and os.path.isfile(env):
|
||||
return env
|
||||
for p in KEYSYMDEF_CANDIDATES:
|
||||
if os.path.isfile(p):
|
||||
return p
|
||||
sys.exit("error: keysymdef.h not found; install xorgproto or set KEYSYMDEF=/path/to/keysymdef.h")
|
||||
|
||||
|
||||
def cmd_fetch(_args):
|
||||
local = os.environ.get("XKB_TARBALL")
|
||||
if local:
|
||||
data = open(local, "rb").read()
|
||||
else:
|
||||
print(f"downloading {TARBALL_URL}")
|
||||
data = urllib.request.urlopen(TARBALL_URL).read()
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
if digest != TARBALL_SHA256:
|
||||
sys.exit(f"error: sha256 mismatch\n expected {TARBALL_SHA256}\n got {digest}")
|
||||
|
||||
tar = tarfile.open(fileobj=io.BytesIO(data), mode="r:gz")
|
||||
members = tar.getnames()
|
||||
root = members[0].split("/")[0]
|
||||
|
||||
# Extract symbols/ + COPYING to a temp view, resolve includes, keep only what's needed.
|
||||
tmp = os.path.join(VENDOR, ".upstream")
|
||||
for m in tar.getmembers():
|
||||
if m.name.startswith(f"{root}/symbols/") or m.name == f"{root}/COPYING":
|
||||
m.name = m.name[len(root) + 1:]
|
||||
tar.extract(m, tmp)
|
||||
tar.close()
|
||||
|
||||
needed = collect_needed(Symbols(os.path.join(tmp, "symbols")))
|
||||
|
||||
os.makedirs(os.path.join(VENDOR, "symbols"), exist_ok=True)
|
||||
for fname in sorted(needed):
|
||||
src = os.path.join(tmp, "symbols", fname)
|
||||
dst = os.path.join(VENDOR, "symbols", fname)
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
with open(src, "rb") as s, open(dst, "wb") as d:
|
||||
d.write(s.read())
|
||||
_copy(os.path.join(tmp, "COPYING"), os.path.join(VENDOR, "COPYING"))
|
||||
|
||||
keysymdef = find_keysymdef()
|
||||
_copy(keysymdef, os.path.join(VENDOR, "keysymdef.h"))
|
||||
|
||||
_rmtree(tmp)
|
||||
|
||||
with open(os.path.join(VENDOR, "PROVENANCE.md"), "w") as f:
|
||||
f.write("# Vendored xkeyboard-config subset\n\n")
|
||||
f.write(f"- **Package**: xkeyboard-config {VERSION}\n")
|
||||
f.write(f"- **Source**: {TARBALL_URL}\n")
|
||||
f.write(f"- **sha256**: `{TARBALL_SHA256}`\n")
|
||||
f.write(f"- **keysymdef.h**: xorgproto, copied from `{keysymdef}`\n")
|
||||
f.write("- **License**: MIT/X11 (see COPYING)\n\n")
|
||||
f.write("Only the symbols files reachable from the generated layouts "
|
||||
"(tools/make-xkeyboard-config.py `TARGETS`) are vendored; regenerate with\n"
|
||||
"`python3 tools/make-xkeyboard-config.py fetch` then `... generate`.\n\n")
|
||||
f.write("Vendored symbols files:\n\n")
|
||||
for fname in sorted(needed):
|
||||
f.write(f"- `symbols/{fname}`\n")
|
||||
print(f"vendored {len(needed)} symbols files + keysymdef.h + COPYING into {VENDOR}")
|
||||
|
||||
|
||||
def cmd_generate(args):
|
||||
symbols_dir = args.symbols or os.path.join(VENDOR, "symbols")
|
||||
keysymdef_path = args.keysymdef or os.path.join(VENDOR, "keysymdef.h")
|
||||
keysymdef = parse_keysymdef(keysymdef_path)
|
||||
symbols = Symbols(symbols_dir)
|
||||
layouts = {zig_name: build_layout(symbols, keysymdef, fname, section)
|
||||
for zig_name, fname, section in TARGETS}
|
||||
out_dir = os.path.join(LIB, "generated")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
out_path = os.path.join(out_dir, "layouts.zig")
|
||||
with open(out_path, "w") as f:
|
||||
f.write(emit_zig(layouts))
|
||||
print(f"wrote {out_path} ({len(TARGETS)} layouts)")
|
||||
|
||||
|
||||
def _copy(src, dst):
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
with open(src, "rb") as s, open(dst, "wb") as d:
|
||||
d.write(s.read())
|
||||
|
||||
|
||||
def _rmtree(path):
|
||||
for root, dirs, files in os.walk(path, topdown=False):
|
||||
for name in files:
|
||||
os.remove(os.path.join(root, name))
|
||||
for name in dirs:
|
||||
os.rmdir(os.path.join(root, name))
|
||||
if os.path.isdir(path):
|
||||
os.rmdir(path)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
sub.add_parser("fetch", help="download + vendor the needed xkeyboard-config data")
|
||||
g = sub.add_parser("generate", help="emit generated/layouts.zig from the vendored data")
|
||||
g.add_argument("--symbols", help="override the vendored symbols/ dir (for development)")
|
||||
g.add_argument("--keysymdef", help="override the vendored keysymdef.h (for development)")
|
||||
args = parser.parse_args()
|
||||
{"fetch": cmd_fetch, "generate": cmd_generate}[args.command](args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue