summaryrefslogtreecommitdiff
path: root/src/macos/pardes.h
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-08 10:44:56 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commitc3c8bbd8d8add99088c774c54bc1acf1e39ec895 (patch)
treea602212f59134909532093765a80c87f219c6c4b /src/macos/pardes.h
parent8aafc3fa24c7475a07259eb06cd5d217f510df98 (diff)
downloadpardes-c3c8bbd8d8add99088c774c54bc1acf1e39ec895.tar.gz
pardes-c3c8bbd8d8add99088c774c54bc1acf1e39ec895.zip
a native macOS backend: libpardes plus an AppKit shell
Adds -Dplatform=macos, a fourth backend beside tty, gui and web. Zig keeps the core, the ptys, every effect and the worker threads; Swift owns NSApplication, the window, input translation, and drawing the cell grid with CoreText. They meet at a hand-written C ABI in src/macos/pardes.h, built as a static library the app links. The ABI is src/web.zig's boundary with the wasm removed, because both hosts are the same animal: someone else owns the clock, feeds events in through flat functions, and reads one packed cell buffer out. The browser proved the shape. The one divergence is that the browser has no processes and forwards every effect to JavaScript, whereas forkpty is right here, so src/macos.zig performs them — spawn, write, resize_pty, save_file, new_file, write_dump, open_link, set_clipboard. lsp, pipe and watch are answered with nothing and marked; the core already tolerates that, since the browser answers none of them either. This deliberately inverts ghostty's split, which was studied first and is written up in docs/ghostty-macos-notes.md. Ghostty hands Zig a bare NSView*, installs its own CALayer and owns the frame clock; Swift never renders. Pardes does the opposite because its frame is already a cell grid and CoreText draws one natively — the alternative is a second hand-rolled glyph atlas, which is what most of gui.zig's 4,300 lines already are. It would also have been written blind: the Swift half cannot be compiled here. What makes the scaffold verifiable rather than dead code is that the Zig half is ordinary POSIX and builds and tests on Linux. Borrowing ghostty's best trick, build.zig translate-C's the header into the test build and src/macos.zig asserts every constant, struct layout, and exported function's arity and widths against it. That guard earned its place immediately: pardes_scroll grew a cell coordinate after the Swift view had been written against the older form. Skipped, and named as the upgrade path in docs/macos.md: the Xcode project, xcframework, lipo and codesigning ghostty needs. All four exist for distribution; a dev build is a swiftc invocation and a directory with a plist. The Swift app is a scaffold and says so — every uncertain API spelling carries an UNVERIFIED marker, and no part of it has been compiled. tty is unaffected: 75/75 snapshot scripts and both unit suites pass.
Diffstat (limited to 'src/macos/pardes.h')
-rw-r--r--src/macos/pardes.h204
1 files changed, 204 insertions, 0 deletions
diff --git a/src/macos/pardes.h b/src/macos/pardes.h
new file mode 100644
index 00000000..e0d6fa32
--- /dev/null
+++ b/src/macos/pardes.h
@@ -0,0 +1,204 @@
+// libpardes — the C ABI the macOS app links against.
+//
+// Hand-written, and kept honest by a test: build.zig translate-C's this file
+// into the unit-test build, and src/macos.zig asserts every constant and
+// struct layout below against the Zig side (ghostty's trick — see docs/macos.md).
+// Change nothing here without running `zig build unit-test -Dplatform=macos`.
+//
+// Division of labour, which is the whole design in two lines:
+// Zig owns the core, the ptys, every effect, and the worker threads.
+// Swift owns NSApplication, the window, input translation, and drawing.
+//
+// There is exactly one core per process, so there are no handles: the state is
+// a file-scoped singleton, same as src/web.zig. Every function below must be
+// called from the main thread. The one exception is the `wakeup` callback,
+// which fires from a pty reader thread.
+
+#ifndef PARDES_H
+#define PARDES_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// ---------------------------------------------------------------- frame
+
+// One rendered cell. Mirrors pardes.Cell (src/pardes.zig) flattened for the
+// wire; the identical encoding is spelled a second time for the browser in
+// src/web.zig as WebCell.
+typedef struct {
+ // UTF-8 grapheme, `len` bytes. The core caps graphemes at seven bytes; the
+ // eighth is always zero so a debugger prints something sensible.
+ uint8_t text[8];
+ // 0x01000000 terminal default
+ // 0x02000000 | index 256-color palette entry
+ // 0x00RRGGBB direct color
+ uint32_t fg;
+ uint32_t bg;
+ // bit 0 bold, 1 dim, 2 italic, 3 blink, 4 reverse, 5 invisible,
+ // 6 strikethrough; bits 8.. hold PARDES_UL_*.
+ uint16_t attrs;
+ uint8_t len;
+ // bit 0: the core never painted this cell — draw it as the default cell
+ // (background only), which is what makes a partial redraw cheap.
+ uint8_t flags;
+} pardes_cell_s;
+
+#define PARDES_COLOR_DEFAULT 0x01000000u
+#define PARDES_COLOR_INDEXED 0x02000000u
+#define PARDES_COLOR_TAG_MASK 0xff000000u
+#define PARDES_COLOR_RGB_MASK 0x00ffffffu
+
+#define PARDES_ATTR_BOLD 0x0001u
+#define PARDES_ATTR_DIM 0x0002u
+#define PARDES_ATTR_ITALIC 0x0004u
+#define PARDES_ATTR_BLINK 0x0008u
+#define PARDES_ATTR_REVERSE 0x0010u
+#define PARDES_ATTR_INVISIBLE 0x0020u
+#define PARDES_ATTR_STRIKETHROUGH 0x0040u
+#define PARDES_ATTR_UL_SHIFT 8
+// Literals rather than (1u << n): Swift's macro importer is dependable on a
+// plain integer and less so on an expression, and nothing here can compile the
+// Swift side to find out. The ABI guard asserts each value against the encoder.
+
+#define PARDES_UL_OFF 0
+#define PARDES_UL_SINGLE 1
+#define PARDES_UL_DOUBLE 2
+#define PARDES_UL_CURLY 3
+#define PARDES_UL_DOTTED 4
+#define PARDES_UL_DASHED 5
+
+#define PARDES_CELL_DEFAULT 0x01u
+
+// ---------------------------------------------------------------- input
+
+// Modifier bitmask shared by key and mouse events.
+#define PARDES_MOD_CTRL 0x0001u
+#define PARDES_MOD_ALT 0x0002u
+#define PARDES_MOD_SHIFT 0x0004u
+
+// Keys that carry no text. The four editing keys are their ASCII controls;
+// everything else lives in a private-use plane so it can never collide with a
+// real codepoint arriving as text.
+#define PARDES_KEY_ENTER 0x0Du
+#define PARDES_KEY_ESCAPE 0x1Bu
+#define PARDES_KEY_TAB 0x09u
+#define PARDES_KEY_BACKSPACE 0x7Fu
+#define PARDES_KEY_UP 0xF0001u
+#define PARDES_KEY_DOWN 0xF0002u
+#define PARDES_KEY_LEFT 0xF0003u
+#define PARDES_KEY_RIGHT 0xF0004u
+#define PARDES_KEY_HOME 0xF0005u
+#define PARDES_KEY_END 0xF0006u
+#define PARDES_KEY_PAGE_UP 0xF0007u
+#define PARDES_KEY_PAGE_DOWN 0xF0008u
+#define PARDES_KEY_DELETE 0xF0009u
+
+// acme's three buttons carry the whole vocabulary: 1 selects, 2 executes,
+// 3 looks. Wheel buttons are ordinary buttons, not a separate axis.
+typedef enum {
+ PARDES_MOUSE_LEFT = 0,
+ PARDES_MOUSE_MIDDLE = 1,
+ PARDES_MOUSE_RIGHT = 2,
+ PARDES_MOUSE_WHEEL_UP = 3,
+ PARDES_MOUSE_WHEEL_DOWN = 4,
+ PARDES_MOUSE_WHEEL_LEFT = 5,
+ PARDES_MOUSE_WHEEL_RIGHT = 6,
+ PARDES_MOUSE_NONE = 7,
+} pardes_mouse_button_e;
+
+typedef enum {
+ PARDES_MOUSE_PRESS = 0,
+ PARDES_MOUSE_RELEASE = 1,
+ PARDES_MOUSE_MOTION = 2,
+ PARDES_MOUSE_DRAG = 3,
+} pardes_mouse_kind_e;
+
+// ---------------------------------------------------------------- runtime
+
+// What the host lends the core. Two callbacks, because everything else the
+// core wants done it already does itself: it owns the ptys, and it opens URLs
+// through /usr/bin/open. Copied by value during pardes_init, so the struct
+// need only outlive that call.
+typedef struct {
+ // Passed back to every callback below. Conventionally the AppDelegate.
+ void *userdata;
+
+ // "Your state moved; please pump me." Called from a pty reader thread, so
+ // the host must hop to the main thread before calling pardes_tick — see
+ // AppDelegate.wakeup, which does exactly one DispatchQueue.main.async.
+ void (*wakeup)(void *userdata);
+
+ // The yank register changed. `text` is borrowed for the duration of the
+ // call only. Main thread, inside pardes_tick.
+ void (*set_clipboard)(void *userdata, const char *text, size_t len);
+} pardes_runtime_s;
+
+// ---------------------------------------------------------------- lifecycle
+
+// Returns 0 on success, or a nonzero opaque error code. Pass the real window
+// grid, not a placeholder: init delivers it as the first resize EVENT (the
+// core defers each shell's greeting until it has seen one), and the first
+// forkpty takes its winsize straight off the core — boot at 80x24 and the
+// shell draws its first prompt to a width the window never had.
+int pardes_init(const pardes_runtime_s *runtime, uint16_t cols, uint16_t rows);
+void pardes_deinit(void);
+
+// Drain pty output into the core and perform the effects it queued. Call after
+// every input function and on every wakeup. Returns true if anything changed
+// and the host should mark its view dirty.
+bool pardes_tick(void);
+
+// The core asked to exit (the Exit builtin, or the last pane closing).
+bool pardes_should_quit(void);
+
+// A theme transition is mid-flight and wants ~60 Hz ticks until it settles.
+bool pardes_animating(void);
+
+// ---------------------------------------------------------------- events in
+
+// `cp` is a codepoint or one of PARDES_KEY_*; `text`/`len` are the host's
+// already-composed characters for printable keys and empty for functional
+// ones. `text` is borrowed for the call.
+void pardes_key(uint32_t cp, const char *text, size_t len, uint32_t mods);
+void pardes_paste(const char *text, size_t len);
+void pardes_mouse(pardes_mouse_button_e button, pardes_mouse_kind_e kind,
+ uint16_t col, uint16_t row, uint32_t mods);
+
+// Trackpad/precision wheel distance in rows, sign following the grid (positive
+// scrolls down). The core has no fractional scroll — it moves a row at a time —
+// so libpardes accumulates here and emits whole-row wheel presses, keeping the
+// remainder. Both other shells do this same accumulation host-side (stepScroll
+// in gui.zig, the drain loop in web/app.mjs); it lives in Zig here so the Swift
+// side stays a translator. A discrete wheel notch should go through
+// pardes_mouse instead.
+void pardes_scroll(float delta_rows, uint16_t col, uint16_t row);
+
+// `cell_w`/`cell_h` are one cell in physical pixels, which only the native PDF
+// placement path reads. Pass the backing-store size, not points.
+void pardes_resize(uint16_t cols, uint16_t rows, uint16_t cell_w,
+ uint16_t cell_h);
+
+// ---------------------------------------------------------------- frame out
+
+// Render one frame. Returns the cell count (cols * rows), or 0 on failure.
+// The buffer returned by pardes_frame_cells is valid until the next call.
+uint32_t pardes_frame(void);
+const pardes_cell_s *pardes_frame_cells(void);
+uint16_t pardes_frame_cols(void);
+uint16_t pardes_frame_rows(void);
+
+// -1 when the cursor is hidden. `bar` asks for a thin insert-mode caret.
+int32_t pardes_cursor_x(void);
+int32_t pardes_cursor_y(void);
+bool pardes_cursor_bar(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // PARDES_H