nandi/cosmicnimpublic Fork 0
46a59a5
Commits
Clone
git clone https://git.rickub.com/nandi/cosmicnim.git
git clone ssh://git@rickub.com/nandi/cosmicnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Map the keyboard to the keypad, and check the ABI version

on_key reports each press as UTF-8: the typed character for ordinary keys,
the key's name otherwise. The calculator accepts both spellings of every
operator — the ASCII one a keyboard emits and the typographic one printed
on the key — plus Enter, Escape and Backspace, which needed a real
operation since a computed result is not a typed entry and cannot be
rubbed out.

Also adds cosmic_abi_version, because this session hit the reason for it
twice. Changing a callback signature leaves every symbol resolvable, so a
stale library reads its arguments from whatever is in the registers: the
keypad came out with garbage widths and looked like a layout bug for two
rounds. The binding now refuses to run on a mismatch and says to fetch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-20T11:48:47-07:00 Browse files
46a59a5 parent: 2d2278d
modified README.md +15 -1
@@ -29,11 +29,25 @@ it calls `on_press` with that id, the host mutates its own state, and the next
2929 `on_view` reflects it.
3030
3131 ```c
32-typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *b);
32+typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *b, float w, float h);
3333 typedef void (*cosmic_on_press)(void *ctx, int32_t id);
34+typedef void (*cosmic_on_key)(void *ctx, const char *key);
35+uint32_t cosmic_abi_version(void);
3436 int32_t cosmic_run(const CosmicConfig *config); /* blocks until closed */
3537 ```
3638
39+`on_view` is handed the space actually available, which is not the size the
40+config asked for once a tiling compositor has had its say. `on_key` reports
41+the typed character for ordinary keys and the key's name otherwise.
42+
43+## ABI version
44+
45+Check `cosmic_abi_version` before `cosmic_run`; the Nim binding does it for
46+you and refuses to run on a mismatch. A changed callback signature leaves
47+every symbol resolvable, so without the check a stale library reads its
48+arguments from whatever is in the registers — which looks like a layout bug,
49+not a version problem.
50+
3751 Because the tree is rebuilt every frame, it can depend on state: a button
3852 disappears, or goes inert, simply by not being described that way this time
3953 round. See `cosmic_ffi.h` for the full list of containers, leaves and
@@ -29,11 +29,25 @@ it calls `on_press` with that id, the host mutates its own state, and the next
29 `on_view` reflects it.29 `on_view` reflects it.
30 30
31 ```c31 ```c
32-typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *b);32+typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *b, float w, float h);
33 typedef void (*cosmic_on_press)(void *ctx, int32_t id);33 typedef void (*cosmic_on_press)(void *ctx, int32_t id);
34+typedef void (*cosmic_on_key)(void *ctx, const char *key);
35+uint32_t cosmic_abi_version(void);
34 int32_t cosmic_run(const CosmicConfig *config); /* blocks until closed */36 int32_t cosmic_run(const CosmicConfig *config); /* blocks until closed */
35 ```37 ```
36 38
39+`on_view` is handed the space actually available, which is not the size the
40+config asked for once a tiling compositor has had its say. `on_key` reports
41+the typed character for ordinary keys and the key's name otherwise.
42+
43+## ABI version
44+
45+Check `cosmic_abi_version` before `cosmic_run`; the Nim binding does it for
46+you and refuses to run on a mismatch. A changed callback signature leaves
47+every symbol resolvable, so without the check a stale library reads its
48+arguments from whatever is in the registers — which looks like a layout bug,
49+not a version problem.
50+
37 Because the tree is rebuilt every frame, it can depend on state: a button51 Because the tree is rebuilt every frame, it can depend on state: a button
38 disappears, or goes inert, simply by not being described that way this time52 disappears, or goes inert, simply by not being described that way this time
39 round. See `cosmic_ffi.h` for the full list of containers, leaves and53 round. See `cosmic_ffi.h` for the full list of containers, leaves and
modified cosmic_ffi/Cargo.lock +1 -1
@@ -964,7 +964,7 @@ dependencies = [
964964
965965 [[package]]
966966 name = "cosmic_ffi"
967-version = "0.5.0"
967+version = "0.6.0"
968968 dependencies = [
969969 "libcosmic",
970970 ]
@@ -964,7 +964,7 @@ dependencies = [
964 964
965 [[package]]965 [[package]]
966 name = "cosmic_ffi"966 name = "cosmic_ffi"
967-version = "0.5.0"967+version = "0.6.0"
968 dependencies = [968 dependencies = [
969 "libcosmic",969 "libcosmic",
970 ]970 ]
modified cosmic_ffi/Cargo.toml +1 -1
@@ -1,6 +1,6 @@
11 [package]
22 name = "cosmic_ffi"
3-version = "0.5.0"
3+version = "0.6.0"
44 edition = "2021"
55
66 [lib]
@@ -1,6 +1,6 @@
1 [package]1 [package]
2 name = "cosmic_ffi"2 name = "cosmic_ffi"
3-version = "0.5.0"3+version = "0.6.0"
4 edition = "2021"4 edition = "2021"
5 5
6 [lib]6 [lib]
modified cosmic_ffi/cosmic_ffi.h +13 -0
@@ -20,15 +20,28 @@ typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *builder, float width,
2020 float height);
2121 typedef void (*cosmic_on_press)(void *ctx, int32_t id);
2222
23+/* Each key press, as NUL-terminated UTF-8: the typed character for ordinary
24+ keys ("7", "+"), otherwise the key's name ("Enter", "Backspace",
25+ "Escape", "ArrowLeft"). Valid for the duration of the call only. */
26+typedef void (*cosmic_on_key)(void *ctx, const char *key);
27+
2328 typedef struct {
2429 const char *title;
2530 cosmic_on_view on_view;
2631 cosmic_on_press on_press;
32+ cosmic_on_key on_key; /* may be NULL */
2733 void *ctx;
2834 uint32_t width; /* 0 for a default */
2935 uint32_t height; /* 0 for a default */
3036 } CosmicConfig;
3137
38+/* Bumped whenever anything here changes meaning: a new export, or a changed
39+ callback signature. A signature change leaves every symbol resolvable, so
40+ without checking this a mismatched library reads its arguments from
41+ whatever is in the registers. Check it before cosmic_run. */
42+#define COSMIC_ABI_VERSION 6
43+uint32_t cosmic_abi_version(void);
44+
3245 /* Opens the window and blocks until it closes. 0 on success.
3346 Must be called from the main thread. */
3447 int32_t cosmic_run(const CosmicConfig *config);
@@ -20,15 +20,28 @@ typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *builder, float width,
20 float height);20 float height);
21 typedef void (*cosmic_on_press)(void *ctx, int32_t id);21 typedef void (*cosmic_on_press)(void *ctx, int32_t id);
22 22
23+/* Each key press, as NUL-terminated UTF-8: the typed character for ordinary
24+ keys ("7", "+"), otherwise the key's name ("Enter", "Backspace",
25+ "Escape", "ArrowLeft"). Valid for the duration of the call only. */
26+typedef void (*cosmic_on_key)(void *ctx, const char *key);
27+
23 typedef struct {28 typedef struct {
24 const char *title;29 const char *title;
25 cosmic_on_view on_view;30 cosmic_on_view on_view;
26 cosmic_on_press on_press;31 cosmic_on_press on_press;
32+ cosmic_on_key on_key; /* may be NULL */
27 void *ctx;33 void *ctx;
28 uint32_t width; /* 0 for a default */34 uint32_t width; /* 0 for a default */
29 uint32_t height; /* 0 for a default */35 uint32_t height; /* 0 for a default */
30 } CosmicConfig;36 } CosmicConfig;
31 37
38+/* Bumped whenever anything here changes meaning: a new export, or a changed
39+ callback signature. A signature change leaves every symbol resolvable, so
40+ without checking this a mismatched library reads its arguments from
41+ whatever is in the registers. Check it before cosmic_run. */
42+#define COSMIC_ABI_VERSION 6
43+uint32_t cosmic_abi_version(void);
44+
32 /* Opens the window and blocks until it closes. 0 on success.45 /* Opens the window and blocks until it closes. 0 on success.
33 Must be called from the main thread. */46 Must be called from the main thread. */
34 int32_t cosmic_run(const CosmicConfig *config);47 int32_t cosmic_run(const CosmicConfig *config);
modified cosmic_ffi/src/lib.rs +46 -1
@@ -25,11 +25,17 @@ pub type OnView =
2525 /// Called when the button carrying `id` is pressed.
2626 pub type OnPress = Option<unsafe extern "C" fn(ctx: *mut c_void, id: i32)>;
2727
28+/// Called on each key press with a NUL-terminated UTF-8 name: the typed
29+/// character for ordinary keys ("7", "+"), otherwise the key's name
30+/// ("Enter", "Backspace", "Escape", "ArrowLeft"). Valid for the call only.
31+pub type OnKey = Option<unsafe extern "C" fn(ctx: *mut c_void, key: *const c_char)>;
32+
2833 #[repr(C)]
2934 pub struct CosmicConfig {
3035 pub title: *const c_char,
3136 pub on_view: OnView,
3237 pub on_press: OnPress,
38+ pub on_key: OnKey,
3339 pub ctx: *mut c_void,
3440 /// Initial window size. 0 means "pick a default".
3541 pub width: u32,
@@ -38,9 +44,10 @@ pub struct CosmicConfig {
3844
3945 type Element = cosmic::Element<'static, Message>;
4046
41-#[derive(Debug, Clone, Copy)]
47+#[derive(Debug, Clone)]
4248 enum Message {
4349 Pressed(i32),
50+ Key(String),
4451 }
4552
4653 // ---------------------------------------------------------------- builder
@@ -378,6 +385,7 @@ pub extern "C" fn cosmic_space_unit(step: i32) -> f32 {
378385 struct Host {
379386 on_view: OnView,
380387 on_press: OnPress,
388+ on_key: OnKey,
381389 ctx: *mut c_void,
382390 }
383391
@@ -399,6 +407,14 @@ impl Host {
399407 unsafe { cb(self.ctx, id) };
400408 }
401409 }
410+
411+ fn key(&self, name: &str) {
412+ let Some(cb) = self.on_key else { return };
413+ let Ok(c) = std::ffi::CString::new(name) else {
414+ return;
415+ };
416+ unsafe { cb(self.ctx, c.as_ptr()) };
417+ }
402418 }
403419
404420 struct Flags {
@@ -437,10 +453,25 @@ impl cosmic::Application for App {
437453 fn update(&mut self, message: Message) -> cosmic::app::Task<Message> {
438454 match message {
439455 Message::Pressed(id) => self.flags.host.press(id),
456+ Message::Key(name) => self.flags.host.key(&name),
440457 }
441458 cosmic::app::Task::none()
442459 }
443460
461+ fn subscription(&self) -> cosmic::iced::Subscription<Message> {
462+ use cosmic::iced::keyboard;
463+ keyboard::on_key_press(|key, _modifiers| {
464+ let name = match key {
465+ keyboard::Key::Character(c) => c.to_string(),
466+ // Debug gives the variant name, which is exactly the spelling
467+ // a host wants to match on: "Enter", "Backspace", "ArrowUp".
468+ keyboard::Key::Named(named) => format!("{named:?}"),
469+ _ => String::new(),
470+ };
471+ (!name.is_empty()).then_some(Message::Key(name))
472+ })
473+ }
474+
444475 fn view(&self) -> cosmic::Element<Message> {
445476 // `responsive` hands the closure the space actually on offer, which is
446477 // the only way the host can know it.
@@ -456,6 +487,19 @@ unsafe fn str_or(ptr: *const c_char, fallback: &str) -> String {
456487 CStr::from_ptr(ptr).to_str().unwrap_or(fallback).to_string()
457488 }
458489
490+/// Bumped whenever anything in this header changes meaning — a new export,
491+/// or a changed callback signature. A signature change keeps every symbol
492+/// resolvable, so without this a mismatched library reads its arguments from
493+/// whatever happens to be in the registers.
494+pub const ABI_VERSION: u32 = 6;
495+
496+/// The ABI this library implements. A host that does not recognise the number
497+/// should refuse to run rather than guess.
498+#[no_mangle]
499+pub extern "C" fn cosmic_abi_version() -> u32 {
500+ ABI_VERSION
501+}
502+
459503 /// Run the window. Blocks until it closes. Returns 0 on success.
460504 ///
461505 /// # Safety
@@ -473,6 +517,7 @@ pub unsafe extern "C" fn cosmic_run(config: *const CosmicConfig) -> i32 {
473517 host: Host {
474518 on_view: c.on_view,
475519 on_press: c.on_press,
520+ on_key: c.on_key,
476521 ctx: c.ctx,
477522 },
478523 };
@@ -25,11 +25,17 @@ pub type OnView =
25 /// Called when the button carrying `id` is pressed.25 /// Called when the button carrying `id` is pressed.
26 pub type OnPress = Option<unsafe extern "C" fn(ctx: *mut c_void, id: i32)>;26 pub type OnPress = Option<unsafe extern "C" fn(ctx: *mut c_void, id: i32)>;
27 27
28+/// Called on each key press with a NUL-terminated UTF-8 name: the typed
29+/// character for ordinary keys ("7", "+"), otherwise the key's name
30+/// ("Enter", "Backspace", "Escape", "ArrowLeft"). Valid for the call only.
31+pub type OnKey = Option<unsafe extern "C" fn(ctx: *mut c_void, key: *const c_char)>;
32+
28 #[repr(C)]33 #[repr(C)]
29 pub struct CosmicConfig {34 pub struct CosmicConfig {
30 pub title: *const c_char,35 pub title: *const c_char,
31 pub on_view: OnView,36 pub on_view: OnView,
32 pub on_press: OnPress,37 pub on_press: OnPress,
38+ pub on_key: OnKey,
33 pub ctx: *mut c_void,39 pub ctx: *mut c_void,
34 /// Initial window size. 0 means "pick a default".40 /// Initial window size. 0 means "pick a default".
35 pub width: u32,41 pub width: u32,
@@ -38,9 +44,10 @@ pub struct CosmicConfig {
38 44
39 type Element = cosmic::Element<'static, Message>;45 type Element = cosmic::Element<'static, Message>;
40 46
41-#[derive(Debug, Clone, Copy)]47+#[derive(Debug, Clone)]
42 enum Message {48 enum Message {
43 Pressed(i32),49 Pressed(i32),
50+ Key(String),
44 }51 }
45 52
46 // ---------------------------------------------------------------- builder53 // ---------------------------------------------------------------- builder
@@ -378,6 +385,7 @@ pub extern "C" fn cosmic_space_unit(step: i32) -> f32 {
378 struct Host {385 struct Host {
379 on_view: OnView,386 on_view: OnView,
380 on_press: OnPress,387 on_press: OnPress,
388+ on_key: OnKey,
381 ctx: *mut c_void,389 ctx: *mut c_void,
382 }390 }
383 391
@@ -399,6 +407,14 @@ impl Host {
399 unsafe { cb(self.ctx, id) };407 unsafe { cb(self.ctx, id) };
400 }408 }
401 }409 }
410+
411+ fn key(&self, name: &str) {
412+ let Some(cb) = self.on_key else { return };
413+ let Ok(c) = std::ffi::CString::new(name) else {
414+ return;
415+ };
416+ unsafe { cb(self.ctx, c.as_ptr()) };
417+ }
402 }418 }
403 419
404 struct Flags {420 struct Flags {
@@ -437,10 +453,25 @@ impl cosmic::Application for App {
437 fn update(&mut self, message: Message) -> cosmic::app::Task<Message> {453 fn update(&mut self, message: Message) -> cosmic::app::Task<Message> {
438 match message {454 match message {
439 Message::Pressed(id) => self.flags.host.press(id),455 Message::Pressed(id) => self.flags.host.press(id),
456+ Message::Key(name) => self.flags.host.key(&name),
440 }457 }
441 cosmic::app::Task::none()458 cosmic::app::Task::none()
442 }459 }
443 460
461+ fn subscription(&self) -> cosmic::iced::Subscription<Message> {
462+ use cosmic::iced::keyboard;
463+ keyboard::on_key_press(|key, _modifiers| {
464+ let name = match key {
465+ keyboard::Key::Character(c) => c.to_string(),
466+ // Debug gives the variant name, which is exactly the spelling
467+ // a host wants to match on: "Enter", "Backspace", "ArrowUp".
468+ keyboard::Key::Named(named) => format!("{named:?}"),
469+ _ => String::new(),
470+ };
471+ (!name.is_empty()).then_some(Message::Key(name))
472+ })
473+ }
474+
444 fn view(&self) -> cosmic::Element<Message> {475 fn view(&self) -> cosmic::Element<Message> {
445 // `responsive` hands the closure the space actually on offer, which is476 // `responsive` hands the closure the space actually on offer, which is
446 // the only way the host can know it.477 // the only way the host can know it.
@@ -456,6 +487,19 @@ unsafe fn str_or(ptr: *const c_char, fallback: &str) -> String {
456 CStr::from_ptr(ptr).to_str().unwrap_or(fallback).to_string()487 CStr::from_ptr(ptr).to_str().unwrap_or(fallback).to_string()
457 }488 }
458 489
490+/// Bumped whenever anything in this header changes meaning — a new export,
491+/// or a changed callback signature. A signature change keeps every symbol
492+/// resolvable, so without this a mismatched library reads its arguments from
493+/// whatever happens to be in the registers.
494+pub const ABI_VERSION: u32 = 6;
495+
496+/// The ABI this library implements. A host that does not recognise the number
497+/// should refuse to run rather than guess.
498+#[no_mangle]
499+pub extern "C" fn cosmic_abi_version() -> u32 {
500+ ABI_VERSION
501+}
502+
459 /// Run the window. Blocks until it closes. Returns 0 on success.503 /// Run the window. Blocks until it closes. Returns 0 on success.
460 ///504 ///
461 /// # Safety505 /// # Safety
@@ -473,6 +517,7 @@ pub unsafe extern "C" fn cosmic_run(config: *const CosmicConfig) -> i32 {
473 host: Host {517 host: Host {
474 on_view: c.on_view,518 on_view: c.on_view,
475 on_press: c.on_press,519 on_press: c.on_press,
520+ on_key: c.on_key,
476 ctx: c.ctx,521 ctx: c.ctx,
477 },522 },
478 };523 };
modified cosmicnim.nimble +1 -1
@@ -1,4 +1,4 @@
1-version = "0.5.0"
1+version = "0.6.0"
22 author = "nandi"
33 description = "libcosmic behind a C ABI, driven from Nim"
44 license = "MIT"
@@ -1,4 +1,4 @@
1-version = "0.5.0"1+version = "0.6.0"
2 author = "nandi"2 author = "nandi"
3 description = "libcosmic behind a C ABI, driven from Nim"3 description = "libcosmic behind a C ABI, driven from Nim"
4 license = "MIT"4 license = "MIT"
modified examples/calculator.nim +39 -2
@@ -4,6 +4,7 @@
44 ## in the source are the rows on screen. State that has no button — a pending
55 ## operator, an error — simply changes what gets described next frame.
66
7+
78 import std/[math, strformat, strutils]
89 import cosmicnim
910
@@ -39,6 +40,7 @@ const
3940 IdClear* = 16'i32
4041 IdSign* = 17'i32
4142 IdPercent* = 18'i32
43+ IdBackspace* = 19'i32
4244
4345 Keypad: array[5, array[4, Key]] = [
4446 [Key(label: "C", id: IdClear, style: ButtonDestructive),
@@ -146,12 +148,41 @@ proc onPress*(ctx: pointer; id: int32) {.cdecl.} =
146148 else: c.entry = "-" & c.entry
147149 else:
148150 c.acc = -c.acc
151+ of IdBackspace:
152+ # Only the typed entry can be rubbed out; a computed result cannot.
153+ if c.entry.len > 0:
154+ c.entry.setLen(c.entry.len - 1)
155+ if c.entry == "-": c.entry = ""
149156 of IdPercent:
150157 if c.entry.len > 0: c.entry = format(parseFloat(c.entry) / 100.0)
151158 else: c.acc = c.acc / 100.0
152159 else:
153160 discard
154161
162+proc keyToId*(key: string): int32 =
163+ ## The id a key press stands for, or -1 for a key the calculator ignores.
164+ ## Both spellings of each operator are accepted: the ASCII one a keyboard
165+ ## actually produces, and the typographic one printed on the key.
166+ if key.len == 1 and key[0] in '0'..'9':
167+ return int32(ord(key[0]) - ord('0'))
168+ case key
169+ of ".", ",": IdDot
170+ of "+": IdAdd
171+ of "-", "": IdSub
172+ of "*", "x", "×": IdMul
173+ of "/", "÷": IdDiv
174+ of "=", "Enter", "NumpadEnter": IdEquals
175+ of "c", "C", "Escape", "Delete": IdClear
176+ of "Backspace": IdBackspace
177+ of "%": IdPercent
178+ of "n", "N": IdSign
179+ else: -1
180+
181+proc onKey(ctx: pointer; key: cstring) {.cdecl.} =
182+ let id = keyToId($key)
183+ if id >= 0:
184+ onPress(ctx, id)
185+
155186 proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} =
156187 let c = cast[ptr Calc](ctx)
157188
@@ -162,9 +193,14 @@ proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} =
162193 gap = space(SpaceXs)
163194 pad = space(SpaceM)
164195 display = 96.0
165- keyW = max(MinKeyW, (float(width) - 2 * pad - float(Cols - 1) * gap) / float(Cols))
196+ # A dimension that is not a sane finite number means the library could not
197+ # say, so fall back to the size the config asked for rather than dividing
198+ # by infinity and collapsing every key to its floor.
199+ availW = if float(width) > 0 and float(width) < 1.0e5: float(width) else: 420.0
200+ availH = if float(height) > 0 and float(height) < 1.0e5: float(height) else: 640.0
201+ keyW = max(MinKeyW, (availW - 2 * pad - float(Cols - 1) * gap) / float(Cols))
166202 keyH = max(MinKeyH,
167- (float(height) - display - 2 * pad - float(Rows - 1) * gap) / float(Rows))
203+ (availH - display - 2 * pad - float(Rows - 1) * gap) / float(Rows))
168204
169205 b.container:
170206 b.fill()
@@ -196,6 +232,7 @@ proc main() =
196232 title: "Nim ❤ COSMIC",
197233 onView: onView,
198234 onPress: onPress,
235+ onKey: onKey,
199236 ctx: addr calc,
200237 width: 420,
201238 height: 640,
@@ -4,6 +4,7 @@
4 ## in the source are the rows on screen. State that has no button — a pending4 ## in the source are the rows on screen. State that has no button — a pending
5 ## operator, an error — simply changes what gets described next frame.5 ## operator, an error — simply changes what gets described next frame.
6 6
7+
7 import std/[math, strformat, strutils]8 import std/[math, strformat, strutils]
8 import cosmicnim9 import cosmicnim
9 10
@@ -39,6 +40,7 @@ const
39 IdClear* = 16'i3240 IdClear* = 16'i32
40 IdSign* = 17'i3241 IdSign* = 17'i32
41 IdPercent* = 18'i3242 IdPercent* = 18'i32
43+ IdBackspace* = 19'i32
42 44
43 Keypad: array[5, array[4, Key]] = [45 Keypad: array[5, array[4, Key]] = [
44 [Key(label: "C", id: IdClear, style: ButtonDestructive),46 [Key(label: "C", id: IdClear, style: ButtonDestructive),
@@ -146,12 +148,41 @@ proc onPress*(ctx: pointer; id: int32) {.cdecl.} =
146 else: c.entry = "-" & c.entry148 else: c.entry = "-" & c.entry
147 else:149 else:
148 c.acc = -c.acc150 c.acc = -c.acc
151+ of IdBackspace:
152+ # Only the typed entry can be rubbed out; a computed result cannot.
153+ if c.entry.len > 0:
154+ c.entry.setLen(c.entry.len - 1)
155+ if c.entry == "-": c.entry = ""
149 of IdPercent:156 of IdPercent:
150 if c.entry.len > 0: c.entry = format(parseFloat(c.entry) / 100.0)157 if c.entry.len > 0: c.entry = format(parseFloat(c.entry) / 100.0)
151 else: c.acc = c.acc / 100.0158 else: c.acc = c.acc / 100.0
152 else:159 else:
153 discard160 discard
154 161
162+proc keyToId*(key: string): int32 =
163+ ## The id a key press stands for, or -1 for a key the calculator ignores.
164+ ## Both spellings of each operator are accepted: the ASCII one a keyboard
165+ ## actually produces, and the typographic one printed on the key.
166+ if key.len == 1 and key[0] in '0'..'9':
167+ return int32(ord(key[0]) - ord('0'))
168+ case key
169+ of ".", ",": IdDot
170+ of "+": IdAdd
171+ of "-", "": IdSub
172+ of "*", "x", "×": IdMul
173+ of "/", "÷": IdDiv
174+ of "=", "Enter", "NumpadEnter": IdEquals
175+ of "c", "C", "Escape", "Delete": IdClear
176+ of "Backspace": IdBackspace
177+ of "%": IdPercent
178+ of "n", "N": IdSign
179+ else: -1
180+
181+proc onKey(ctx: pointer; key: cstring) {.cdecl.} =
182+ let id = keyToId($key)
183+ if id >= 0:
184+ onPress(ctx, id)
185+
155 proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} =186 proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} =
156 let c = cast[ptr Calc](ctx)187 let c = cast[ptr Calc](ctx)
157 188
@@ -162,9 +193,14 @@ proc onView(ctx: pointer; b: Builder; width, height: cfloat) {.cdecl.} =
162 gap = space(SpaceXs)193 gap = space(SpaceXs)
163 pad = space(SpaceM)194 pad = space(SpaceM)
164 display = 96.0195 display = 96.0
165- keyW = max(MinKeyW, (float(width) - 2 * pad - float(Cols - 1) * gap) / float(Cols))196+ # A dimension that is not a sane finite number means the library could not
197+ # say, so fall back to the size the config asked for rather than dividing
198+ # by infinity and collapsing every key to its floor.
199+ availW = if float(width) > 0 and float(width) < 1.0e5: float(width) else: 420.0
200+ availH = if float(height) > 0 and float(height) < 1.0e5: float(height) else: 640.0
201+ keyW = max(MinKeyW, (availW - 2 * pad - float(Cols - 1) * gap) / float(Cols))
166 keyH = max(MinKeyH,202 keyH = max(MinKeyH,
167- (float(height) - display - 2 * pad - float(Rows - 1) * gap) / float(Rows))203+ (availH - display - 2 * pad - float(Rows - 1) * gap) / float(Rows))
168 204
169 b.container:205 b.container:
170 b.fill()206 b.fill()
@@ -196,6 +232,7 @@ proc main() =
196 title: "Nim ❤ COSMIC",232 title: "Nim ❤ COSMIC",
197 onView: onView,233 onView: onView,
198 onPress: onPress,234 onPress: onPress,
235+ onKey: onKey,
199 ctx: addr calc,236 ctx: addr calc,
200 width: 420,237 width: 420,
201 height: 640,238 height: 640,
modified nim/cosmicnim.nim +25 -1
@@ -36,10 +36,16 @@ type
3636 ## CosmicConfig asked for, which a tiling compositor will override.
3737 OnPress* = proc (ctx: pointer; id: int32) {.cdecl.}
3838
39+ OnKey* = proc (ctx: pointer; key: cstring) {.cdecl.}
40+ ## Each key press: the typed character for ordinary keys ("7", "+"),
41+ ## otherwise the key's name ("Enter", "Backspace", "Escape",
42+ ## "ArrowLeft"). The string is only valid during the call.
43+
3944 CosmicConfig* = object
4045 title*: cstring
4146 onView*: OnView
4247 onPress*: OnPress
48+ onKey*: OnKey ## may be nil
4349 ctx*: pointer
4450 width*: uint32 ## 0 for a default
4551 height*: uint32 ## 0 for a default
@@ -54,9 +60,27 @@ type
5460 SpaceStep* = enum
5561 SpaceNone, SpaceXxxs, SpaceXxs, SpaceXs, SpaceS, SpaceM, SpaceL, SpaceXl
5662
57-proc cosmicRun*(config: ptr CosmicConfig): int32
63+const AbiVersion* = 6'u32
64+ ## The ABI these bindings are written against. Bumped whenever a signature
65+ ## changes, which is invisible to the dynamic linker: every symbol still
66+ ## resolves, and the arguments are simply read from the wrong registers.
67+
68+proc cosmicAbiVersion*(): uint32
69+ {.cdecl, importc: "cosmic_abi_version", dynlib: libCosmicFfi.}
70+
71+proc rawCosmicRun(config: ptr CosmicConfig): int32
5872 {.cdecl, importc: "cosmic_run", dynlib: libCosmicFfi.}
5973
74+proc cosmicRun*(config: ptr CosmicConfig): int32 =
75+ ## Refuses to run against a library that speaks a different ABI, rather
76+ ## than letting the callbacks read whatever is in the registers.
77+ let found = cosmicAbiVersion()
78+ if found != AbiVersion:
79+ raise newException(LibraryError,
80+ "libcosmic_ffi speaks ABI " & $found & ", these bindings expect " &
81+ $AbiVersion & " -- run `just fetch` to get the matching release")
82+ rawCosmicRun(config)
83+
6084 # --- raw builder calls; prefer the wrappers below ---
6185
6286 proc beginColumn(b: Builder) {.cdecl, importc: "cosmic_column", dynlib: libCosmicFfi.}
@@ -36,10 +36,16 @@ type
36 ## CosmicConfig asked for, which a tiling compositor will override.36 ## CosmicConfig asked for, which a tiling compositor will override.
37 OnPress* = proc (ctx: pointer; id: int32) {.cdecl.}37 OnPress* = proc (ctx: pointer; id: int32) {.cdecl.}
38 38
39+ OnKey* = proc (ctx: pointer; key: cstring) {.cdecl.}
40+ ## Each key press: the typed character for ordinary keys ("7", "+"),
41+ ## otherwise the key's name ("Enter", "Backspace", "Escape",
42+ ## "ArrowLeft"). The string is only valid during the call.
43+
39 CosmicConfig* = object44 CosmicConfig* = object
40 title*: cstring45 title*: cstring
41 onView*: OnView46 onView*: OnView
42 onPress*: OnPress47 onPress*: OnPress
48+ onKey*: OnKey ## may be nil
43 ctx*: pointer49 ctx*: pointer
44 width*: uint32 ## 0 for a default50 width*: uint32 ## 0 for a default
45 height*: uint32 ## 0 for a default51 height*: uint32 ## 0 for a default
@@ -54,9 +60,27 @@ type
54 SpaceStep* = enum60 SpaceStep* = enum
55 SpaceNone, SpaceXxxs, SpaceXxs, SpaceXs, SpaceS, SpaceM, SpaceL, SpaceXl61 SpaceNone, SpaceXxxs, SpaceXxs, SpaceXs, SpaceS, SpaceM, SpaceL, SpaceXl
56 62
57-proc cosmicRun*(config: ptr CosmicConfig): int3263+const AbiVersion* = 6'u32
64+ ## The ABI these bindings are written against. Bumped whenever a signature
65+ ## changes, which is invisible to the dynamic linker: every symbol still
66+ ## resolves, and the arguments are simply read from the wrong registers.
67+
68+proc cosmicAbiVersion*(): uint32
69+ {.cdecl, importc: "cosmic_abi_version", dynlib: libCosmicFfi.}
70+
71+proc rawCosmicRun(config: ptr CosmicConfig): int32
58 {.cdecl, importc: "cosmic_run", dynlib: libCosmicFfi.}72 {.cdecl, importc: "cosmic_run", dynlib: libCosmicFfi.}
59 73
74+proc cosmicRun*(config: ptr CosmicConfig): int32 =
75+ ## Refuses to run against a library that speaks a different ABI, rather
76+ ## than letting the callbacks read whatever is in the registers.
77+ let found = cosmicAbiVersion()
78+ if found != AbiVersion:
79+ raise newException(LibraryError,
80+ "libcosmic_ffi speaks ABI " & $found & ", these bindings expect " &
81+ $AbiVersion & " -- run `just fetch` to get the matching release")
82+ rawCosmicRun(config)
83+
60 # --- raw builder calls; prefer the wrappers below ---84 # --- raw builder calls; prefer the wrappers below ---
61 85
62 proc beginColumn(b: Builder) {.cdecl, importc: "cosmic_column", dynlib: libCosmicFfi.}86 proc beginColumn(b: Builder) {.cdecl, importc: "cosmic_column", dynlib: libCosmicFfi.}
modified tests/tcalculator.nim +45 -0
@@ -69,7 +69,52 @@ suite "calculator":
6969 var c = fresh()
7070 check c.press(6, IdDiv, 3, IdEquals) == "2"
7171
72+ test "backspace rubs out typed digits only":
73+ var c = fresh()
74+ check c.press(1, 2, 3, IdBackspace) == "12"
75+ check c.press(IdBackspace, IdBackspace) == "0" # empty entry shows acc
76+ c = fresh()
77+ check c.press(2, IdAdd, 3, IdEquals) == "5"
78+ check c.press(IdBackspace) == "5" # a result is not an entry
79+
7280 test "a new number replaces the result, not appends to it":
7381 var c = fresh()
7482 check c.press(2, IdAdd, 3, IdEquals) == "5"
7583 check c.press(7) == "7"
84+
85+suite "keyboard":
86+ proc typed(c: var Calc; keys: varargs[string]): string =
87+ for k in keys:
88+ let id = keyToId(k)
89+ if id >= 0: onPress(addr c, id)
90+ c.display
91+
92+ test "digits and operators come from the characters you type":
93+ var c = fresh()
94+ check c.typed("1", "2", "+", "3", "Enter") == "15"
95+
96+ test "both spellings of an operator work":
97+ var c = fresh()
98+ check c.typed("8", "/", "2", "=") == "4"
99+ c = fresh()
100+ check c.typed("8", "\u00f7", "2", "=") == "4" # the divide sign on the key
101+ c = fresh()
102+ check c.typed("3", "*", "4", "=") == "12"
103+ c = fresh()
104+ check c.typed("3", "\u00d7", "4", "=") == "12"
105+
106+ test "escape and c both clear":
107+ var c = fresh()
108+ check c.typed("9", "Escape") == "0"
109+ check c.typed("9", "c") == "0"
110+
111+ test "backspace is wired to the key":
112+ var c = fresh()
113+ check c.typed("1", "2", "3", "Backspace") == "12"
114+
115+ test "unmapped keys are ignored, not mistaken for a digit":
116+ var c = fresh()
117+ check keyToId("F1") == -1
118+ check keyToId("ArrowUp") == -1
119+ check keyToId("q") == -1
120+ check c.typed("5", "F1", "ArrowUp", "q") == "5"
@@ -69,7 +69,52 @@ suite "calculator":
69 var c = fresh()69 var c = fresh()
70 check c.press(6, IdDiv, 3, IdEquals) == "2"70 check c.press(6, IdDiv, 3, IdEquals) == "2"
71 71
72+ test "backspace rubs out typed digits only":
73+ var c = fresh()
74+ check c.press(1, 2, 3, IdBackspace) == "12"
75+ check c.press(IdBackspace, IdBackspace) == "0" # empty entry shows acc
76+ c = fresh()
77+ check c.press(2, IdAdd, 3, IdEquals) == "5"
78+ check c.press(IdBackspace) == "5" # a result is not an entry
79+
72 test "a new number replaces the result, not appends to it":80 test "a new number replaces the result, not appends to it":
73 var c = fresh()81 var c = fresh()
74 check c.press(2, IdAdd, 3, IdEquals) == "5"82 check c.press(2, IdAdd, 3, IdEquals) == "5"
75 check c.press(7) == "7"83 check c.press(7) == "7"
84+
85+suite "keyboard":
86+ proc typed(c: var Calc; keys: varargs[string]): string =
87+ for k in keys:
88+ let id = keyToId(k)
89+ if id >= 0: onPress(addr c, id)
90+ c.display
91+
92+ test "digits and operators come from the characters you type":
93+ var c = fresh()
94+ check c.typed("1", "2", "+", "3", "Enter") == "15"
95+
96+ test "both spellings of an operator work":
97+ var c = fresh()
98+ check c.typed("8", "/", "2", "=") == "4"
99+ c = fresh()
100+ check c.typed("8", "\u00f7", "2", "=") == "4" # the divide sign on the key
101+ c = fresh()
102+ check c.typed("3", "*", "4", "=") == "12"
103+ c = fresh()
104+ check c.typed("3", "\u00d7", "4", "=") == "12"
105+
106+ test "escape and c both clear":
107+ var c = fresh()
108+ check c.typed("9", "Escape") == "0"
109+ check c.typed("9", "c") == "0"
110+
111+ test "backspace is wired to the key":
112+ var c = fresh()
113+ check c.typed("1", "2", "3", "Backspace") == "12"
114+
115+ test "unmapped keys are ignored, not mistaken for a digit":
116+ var c = fresh()
117+ check keyToId("F1") == -1
118+ check keyToId("ArrowUp") == -1
119+ check keyToId("q") == -1
120+ check c.typed("5", "F1", "ArrowUp", "q") == "5"