| Bind the terminal backend from the side that renders into it 3cfee15 nandi 17d ago | 1 | (ns glimmer-tui.ffi |
| 2 | "Raw bindings for libjolttui's retained-tree ABI |
| 3 | (`crates/jolt-tui/include/jolttui.h`). |
| 4 | |
| 5 | Nothing here interprets a prop or an event; that is `glimmer-tui.core`'s job. |
| 6 | This namespace exists so the boundary is one readable list of symbols, and so |
| 7 | the marshalling rules that come with it are stated once. |
| 8 | |
| 9 | **Strings returned by this library are borrowed.** They come out of scratch |
| 10 | buffers the next call of the same family overwrites — props, tags and screen |
| 11 | lines share one, dumps have their own, and an event's name and its text have |
| 12 | one each, so an event can be read whole. Jolt copies a `:string` return into a |
| 13 | Scheme string as it crosses, so holding the value is safe. |
| 14 | |
| 15 | **Every call belongs to the thread that opened the session.** The library |
| 16 | keeps its state in thread-local storage, so a call from anywhere else is inert |
| 17 | rather than unsound — still a bug. `glimmer-tui.core` routes off-thread work |
| 18 | through glimmer's `schedule`." |
| 19 | (:require [jolt.ffi :as ffi])) |
| 20 | |
| 21 | ;; --- the session ------------------------------------------------------------- |
| 22 | ;; Two ways in, one code path out of them: `raw-open` takes the terminal, and |
| 23 | ;; `raw-headless` opens a session of a fixed size with no terminal at all — |
| 24 | ;; same layout, same painting, same focus ring, read back with `screen-line`. |
| 25 | (ffi/defcfn raw-open "tui_open" [:int] :int) |
| 26 | (ffi/defcfn raw-headless "tui_headless" [:int :int] :int) |
| 27 | (ffi/defcfn close! "tui_close" [] :void) |
| 28 | (ffi/defcfn raw-should-close "tui_should_close" [] :int) |
| 29 | (ffi/defcfn quit-loop! "tui_quit" [] :void) |
| 30 | |
| 31 | ;; --- the loop ---------------------------------------------------------------- |
| 32 | ;; `tick` waits up to timeout_ms for input and answers how much it handled; |
| 33 | ;; `frame!` lays the tree out, paints it and sends only the cells that changed. |
| 34 | (ffi/defcfn tick "tui_tick" [:int] :int) |
| 35 | (ffi/defcfn frame! "tui_frame" [] :void) |
| 36 | |
| 37 | ;; --- the screen, in cells ---------------------------------------------------- |
| 38 | (ffi/defcfn screen-width "tui_screen_width" [] :int) |
| 39 | (ffi/defcfn screen-height "tui_screen_height" [] :int) |
| 40 | (ffi/defcfn screen-line "tui_screen_line" [:int] :string) |
| 41 | |
| 42 | ;; --- input by hand ----------------------------------------------------------- |
| 43 | ;; The same entry points a real terminal's input arrives through, so a test |
| 44 | ;; drives the UI exactly as a person does. |
| 45 | (ffi/defcfn raw-feed-key "tui_feed_key" [:string] :int) |
| 46 | (ffi/defcfn raw-feed-click "tui_feed_click" [:int :int] :int) |
| 47 | (ffi/defcfn raw-feed-wheel "tui_feed_wheel" [:int :int :int] :int) |
| 48 | (ffi/defcfn focus "tui_focus" [] :int) |
| 49 | |
| 50 | ;; --- the tree ---------------------------------------------------------------- |
| 51 | (ffi/defcfn tree-root "tui_tree_root" [] :int) |
| 52 | (ffi/defcfn node-new "tui_node_new" [:string] :int) |
| 53 | (ffi/defcfn node-free! "tui_node_free" [:int] :void) |
| 54 | (ffi/defcfn raw-node-exists "tui_node_exists" [:int] :int) |
| 55 | |
| 56 | (ffi/defcfn node-set-str! "tui_node_set_str" [:int :string :string] :void) |
| 57 | (ffi/defcfn node-set-num! "tui_node_set_num" [:int :string :double] :void) |
| 58 | (ffi/defcfn raw-node-set-bool "tui_node_set_bool" [:int :string :int] :void) |
| 59 | (ffi/defcfn node-clear-props! "tui_node_clear_props" [:int] :void) |
| 60 | |
| 61 | (ffi/defcfn node-get-str "tui_node_get_str" [:int :string] :string) |
| 62 | (ffi/defcfn node-get-num "tui_node_get_num" [:int :string] :double) |
| 63 | (ffi/defcfn raw-node-get-bool "tui_node_get_bool" [:int :string] :int) |
| 64 | |
| 65 | (ffi/defcfn node-tag "tui_node_tag" [:int] :string) |
| 66 | (ffi/defcfn node-parent "tui_node_parent" [:int] :int) |
| 67 | (ffi/defcfn node-child-count "tui_node_child_count" [:int] :int) |
| 68 | (ffi/defcfn node-child-at "tui_node_child_at" [:int :int] :int) |
| 69 | (ffi/defcfn tree-dump "tui_tree_dump" [:int] :string) |
| 70 | |
| 71 | (ffi/defcfn raw-node-append "tui_node_append" [:int :int] :int) |
| 72 | (ffi/defcfn node-remove! "tui_node_remove" [:int :int] :void) |
| 73 | (ffi/defcfn raw-node-insert-after "tui_node_insert_after" [:int :int :int] :int) |
| 74 | (ffi/defcfn raw-node-replace "tui_node_replace" [:int :int :int] :int) |
| 75 | |
| 76 | ;; --- events ------------------------------------------------------------------ |
| 77 | (ffi/defcfn raw-poll-event "tui_tree_poll_event" [] :int) |
| 78 | (ffi/defcfn event-node "tui_tree_event_node" [] :int) |
| 79 | (ffi/defcfn event-name "tui_tree_event_name" [] :string) |
| 80 | (ffi/defcfn event-text "tui_tree_event_text" [] :string) |
| 81 | (ffi/defcfn event-num "tui_tree_event_num" [] :double) |
| 82 | |
| 83 | ;; --- the int/bool seam ------------------------------------------------------- |
| 84 | ;; C has no booleans; every predicate here crosses as 0 or 1. Converting at the |
| 85 | ;; binding rather than at each call site keeps the rest of the backend written |
| 86 | ;; in jolt's own truthiness. |
| 87 | |
| 88 | (defn open! |
| 89 | "Take the terminal: raw mode, the alternate screen, the cursor hidden unless a |
| 90 | focused entry asks for it, and mouse reporting when `mouse?`." |
| 91 | [mouse?] |
| 92 | (not (zero? (raw-open (if mouse? 1 0))))) |
| 93 | |
| 94 | (defn headless! |
| 95 | "Open a session of `width` by `height` cells with no terminal at all." |
| 96 | [width height] |
| 97 | (not (zero? (raw-headless width height)))) |
| 98 | |
| 99 | (defn should-close? [] (not (zero? (raw-should-close)))) |
| 100 | (defn node-exists? [node] (not (zero? (raw-node-exists node)))) |
| 101 | (defn node-set-bool! [node key value] (raw-node-set-bool node key (if value 1 0))) |
| 102 | (defn node-get-bool [node key] (not (zero? (raw-node-get-bool node key)))) |
| 103 | (defn node-append! [parent child] (not (zero? (raw-node-append parent child)))) |
| 104 | (defn node-insert-after! [parent child sibling] |
| 105 | (not (zero? (raw-node-insert-after parent child sibling)))) |
| 106 | (defn node-replace! [parent old new] (not (zero? (raw-node-replace parent old new)))) |
| 107 | (defn poll-event! [] (not (zero? (raw-poll-event)))) |
| 108 | |
| 109 | (defn feed-key! |
| 110 | "Type one key, named as the terminal names it. True when the backend acted on |
| 111 | it, false when it went out as a `key` event instead." |
| 112 | [name] |
| 113 | (not (zero? (raw-feed-key name)))) |
| 114 | |
| 115 | (defn feed-click! [x y] (not (zero? (raw-feed-click x y)))) |
| 116 | (defn feed-wheel! [x y by] (not (zero? (raw-feed-wheel x y by)))) |