// 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 #include #include #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; // What the core just did, for a shell that can answer with something the hand // feels. Taken with pardes_take_haptic once per pump; the two acme verbs are // distinguished because they deserve distinct taps. typedef enum { PARDES_HAPTIC_NONE = 0, PARDES_HAPTIC_EXEC = 1, PARDES_HAPTIC_LOOK = 2, } pardes_haptic_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. // // The return value is whether this tick did any IO — bytes arrived from a pty, // or an effect was performed. It is NOT a repaint signal, and a host that uses // it as one shows a stale screen: moving the cursor, extending a selection, // changing mode and scrolling all mutate the grid while queueing nothing and // performing nothing, so they tick false. Mark the view dirty after any call // into the core and use this only to decide whether there was work. 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 CELLS, sign following the grid // (positive scrolls down and right). The core has no fractional scroll — it // moves a row or a column at a time — so libpardes accumulates here and emits // whole 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, and // so the quantizer is unit-tested on a machine with no trackpad. A discrete // wheel notch should go through pardes_mouse instead. void pardes_scroll(float delta_rows, float delta_cols, uint16_t col, uint16_t row); // A two-finger trackpad rotation, in degrees since the last call, positive // counterclockwise (AppKit's sign, unchanged). The core has no rotation: this // is spent as the search-step keys, clockwise `n` and counterclockwise `N`, a // notch at a time with the remainder kept — the same accumulate-and-spend // shape as pardes_scroll, and in Zig for the same reason. Feed it the raw // per-event delta; pass 0 at gesture start to drop a stale remainder. void pardes_rotate(float degrees); // Run one builtin command line, exactly as executing the same text in a tag // would. This is the core's own `command` event, which is how a nested pardes // talks to its host; here it is what a menu item is made of, and what opens // the file named on argv or dropped on the Dock icon (`Look `). void pardes_command(const char *text, size_t len); // `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); // The Look or Exec the core performed since this was last asked, and clears // it. Call it once per pump, after pardes_tick — the input functions run the // dispatch synchronously, so a gesture's pulse is already waiting by the time // its tick returns. PARDES_HAPTIC_NONE means nothing to feel. pardes_haptic_e pardes_take_haptic(void); // The font file the `Font` builtin asked for since this was last called, and // clears it; NULL when nothing was asked. Ask once per pump, beside the haptic // above. The string is a NUL-terminated absolute path owned by libpardes and // valid until the next call. // // A path rather than a family name: the core found the file by walking the // font directories itself, so both shells agree on which faces exist and // neither has to ask its platform to resolve a name it might resolve // differently. A `.ttc` collection names its first face, which is the cut the // file is named after. // // The host loads it, re-measures its cell, and reports the new grid through // pardes_resize. A file the host cannot load is one to ignore: keep wearing // the face that works, because a terminal that cannot draw has no way back // out of it. const char *pardes_font_take(void); #ifdef __cplusplus } #endif #endif // PARDES_H