// 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. // bit 1: rasterize text and its colour band with the tagline face's measured // height. Logical cell geometry stays body-sized; this is a visual role. 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 #define PARDES_CELL_TAGLINE 0x02u // The look-hover affordance: the word under the pointer is a real Look or // Exec operand. Hosts that draw the affordance as more than the quiet tint // (macOS composes a liquid-glass overlay) read this instead of guessing // from the background colour, which a real selection shares. #define PARDES_CELL_HOVER 0x04u // One coherent snapshot for the full-window shader pass (CRT); // time/frame advance only on pardes_animation_tick's display clock. typedef struct { uint32_t flags; float time_seconds; uint32_t frame; } pardes_scene_s; #define PARDES_SCENE_CRT 0x01u // One backend-neutral pane transition. Boxes are logical cell rectangles; // the native shader converts them to backing pixels using the same measured // cell as the renderer. The list is already in paint order: moving first, // opening next, frozen closing tombstones last, and slot order within a phase. typedef struct { float x; float y; float w; float h; } pardes_panel_box_s; typedef struct { uint32_t serial; uint8_t pane; uint8_t phase; uint8_t effect; uint8_t motion; uint16_t frame; uint16_t frame_count; pardes_panel_box_s from; pardes_panel_box_s to; float launch; } pardes_panel_track_s; #define PARDES_PANEL_OPENING 0u #define PARDES_PANEL_MOVING 1u #define PARDES_PANEL_CLOSING 2u #define PARDES_PANEL_OFF 0u #define PARDES_PANEL_SLIDE 1u #define PARDES_PANEL_ZOOM 2u #define PARDES_PANEL_DISSOLVE 3u #define PARDES_PANEL_ASCII 4u #define PARDES_PANEL_VERTICAL 5u // Character effects composed in the core: the host receives finished glyphs // and only clips the panel batch for them. #define PARDES_PANEL_EDGES 6u #define PARDES_PANEL_FALL 7u #define PARDES_PANEL_WAVE 8u #define PARDES_PANEL_CURTAIN 9u #define PARDES_PANEL_SCRAMBLE 10u #define PARDES_PANEL_TYPEWRITER 11u // ---------------------------------------------------------------- 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_BACK = 8, PARDES_MOUSE_FORWARD = 9, } 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. Three 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); // Ask the host for the system clipboard. The host answers by calling // pardes_paste, which may be synchronous inside this call. Main thread, // inside pardes_tick. void (*read_clipboard)(void *userdata); } 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); // Advance one frame-count animation step. Call only from the scheduled ~60 Hz // callback, never from ordinary input or pty pumps: those drain work but do // not represent elapsed display time. Returns whether anything advanced. bool pardes_animation_tick(void); // The core asked to exit (the Exit builtin, or the last pane closing). bool pardes_should_quit(void); // A theme/pane transition, scene effect, or rotation coast wants ~60 Hz ticks. bool pardes_animating(void); // What to paint where the grid does not: the window background behind the // titlebar and behind a live resize the view has not caught up with. The // theme's own background, so it changes the instant the theme does — chrome // (taglines, the move box) fades instead, which is why this is not it. // // PARDES_COLOR_DEFAULT means the theme declares NO background of its own. A // terminal wears whatever it was already wearing; a window has nothing to // wear, so the host should go transparent and show its own backdrop. uint32_t pardes_theme_bg(void); // The percentage the host must paint window BACKGROUNDS at: the ground behind // the grid, cell and band backgrounds, and the chrome rules over them. Glyph // ink and the block cursor are never scaled by it — see shaders/ui.frag.glsl, // which states the same rule for the SDL shell. // // Read it once per pump beside pardes_theme_bg(): reading also acknowledges // whatever `WindowOpacity` asked for. 100 is opaque and is the default. uint8_t pardes_window_opacity(void); // Backdrop material strength, 0..100; zero disables blur. Not a pixel radius. uint8_t pardes_window_blur(void); // Pane taglines use this percentage of the current body font size. It is the // embedded default before init and the live core setting afterwards; the host // polls it and rebuilds only tagline glyph/metric state. uint8_t pardes_gui_tagline_font_percent(void); // Where a reduced-height tagline band sits inside its body-sized grid row, in // PHYSICAL PIXELS down from the row's top, and the rule joining the topbar band // to the first pane-tag band. The core owns this geometry so that this shell and // the SDL one cannot disagree about it; centring every band is what this shell // used to do, and it left a strip of window background between the two. // // Points in, pixels out: multiply by the backing scale before calling and divide // the answer back, the same snapping the cell metrics already use. uint32_t pardes_tagline_band_offset(uint16_t row, float canvas_h, uint32_t cell_h, uint32_t tagline_h); uint32_t pardes_topbar_pane_border_px(uint32_t cell_h, uint32_t tagline_h); // The column a compact tagline band anchors at: the pane's left edge, so a tag // row advances on the tagline face's own narrower pitch instead of centring a // smaller glyph inside every body-width cell. CELLS, not pixels — the host // knows both widths — and fractional, because an animating panel's origin is. // // glyph_x = origin * body_cell_w + (col - origin) * tagline_cell_w float pardes_tagline_origin_col(uint16_t col, uint16_t row); // The inverse, for the pointer: which grid column `x` falls in on `row`, given // that a tag row's glyphs step at the narrower pitch. Compacting the text // without compacting this makes a click drift one word further right for every // word along the row. `x` and both widths must share a unit; only the ratio is // read, so a host measuring in points passes points. uint16_t pardes_grid_col_at(float x, uint16_t row, float body_w, float tagline_w); // The tag band's own background, the base a compact tag row is painted on: the // pane-wide band goes down in THIS colour on the body grid, then each cell's // own background on the narrower grid its glyph uses. One pass would put a // highlighted word's box on body pitch and its letters on tagline pitch. // PARDES_COLOR_DEFAULT before there is a session to ask. uint32_t pardes_tagline_bg(void); // The fallback font PREFERENCE ORDER, shared with the SDL shell. Only the order // travels: resolving a name is the host's business, because SDL matches font // FILE STEMS while walking the font directories and CoreText matches PostScript // and family names, which for one face are routinely different strings. // // `pardes_fallback_font_name` returns a borrowed, NOT NUL-terminated pointer // and writes its byte length through `len`; NULL past the end. uint32_t pardes_fallback_font_count(void); const char *pardes_fallback_font_name(uint32_t index, uint32_t *len); // Colour of that rule: a compiled override, else the theme's scrollbar track. // PARDES_COLOR_DEFAULT before there is a session to ask — do not draw it then. uint32_t pardes_topbar_pane_border_rgb(void); // Rows the global topbar occupies, and therefore the row index of the FIRST // pane tagline — the one the rule above joins the topbar band to. Mirrors // pardes.TOPBAR_H, and src/macos.zig asserts the two agree. #define PARDES_TOPBAR_H 1u // The active pane's tag row background, for hosts that afford the active // tagline more than a tint — macOS raises it like an old-school bevel. uint32_t pardes_tag_active_bg(void); // Persistent full-window effect switches and their display-clock time. A zero // flags word means draw the CoreText frame directly; any nonzero combination // is one postprocess pass. Snapshot by value, with no borrowed storage. pardes_scene_s pardes_scene(void); // The shared Metal/Core Image pass is unavailable for this session. Clears // scene effects and future panel transitions so Config reflects what the host // can actually present and the display clock cannot retry forever. void pardes_postprocessor_unavailable(void); // A transient postprocess failure will be presented as the canonical grid. // Drop current panel tracks so a later scene retry cannot resume them midway. void pardes_panel_animation_failed(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); // The pointer left the view. A clamped motion on its final cell is not the // same event: it would keep hover previews and resize hints alive off-window. void pardes_mouse_pixel(pardes_mouse_button_e button, pardes_mouse_kind_e kind, uint16_t col, uint16_t row, uint32_t mods, float x, float y, float body_w, float body_h, float tagline_w, float tagline_h); void pardes_row_metrics(uint16_t body_w, uint16_t body_h, uint16_t tagline_w, uint16_t tagline_h); uint32_t pardes_tag_text_inset(void); uint32_t pardes_grip_columns(void); uint32_t pardes_tag_layer_limit(void); // Tag layer fields 11 and 12 carry tag-bottom placement and chrome border RGB. uint32_t pardes_tag_layer_value(uint32_t index, uint32_t field); const pardes_cell_s *pardes_tag_layer_cells(uint32_t index); uint32_t pardes_body_layer_limit(void); uint32_t pardes_body_layer_value(uint32_t index, uint32_t field); const pardes_cell_s *pardes_body_layer_cells(uint32_t index); void pardes_pointer_leave(void); // Host file watchers call this after their debounce interval. `pane` is the // opaque watch slot (including pardes' private ThemeFile slot), and `generation` // makes events queued before a replacement inert. The callback only schedules // the ordinary main-thread pump; text, PDF, and theme IO happen in pardes_tick. void pardes_watch_changed(uint8_t pane, uint32_t generation); // 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); // The gesture boundaries around pardes_scroll, and the whole of scroll // momentum. `begin` is fingers down: it catches a coast the way a hand catches // a spinning dial, and drops whatever the last gesture left banked. `end` is // fingers up: a swipe still moving when it ended becomes a fling, in // proportion to how hard it was thrown, on the same curve pardes_rotate_end // uses — momentum ramps up from zero rather than switching on at a threshold. // // A coast makes pardes_animating true and is spent by pardes_animation_tick, // and it stops at the end of a document instead of spinning against the edge. // // A host whose windowing system already flings for it needs no suppression: // the system's momentum arrives as ordinary pardes_scroll calls, every one of // which cancels the coast before spending its own travel, so that momentum // simply takes the gesture over. A host that calls neither — a discrete wheel // has no phases — scrolls with no momentum at all. void pardes_scroll_begin(void); void pardes_scroll_end(void); // 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 and to // stop a fling still coasting. void pardes_rotate(float degrees); // The fingers lifted. How fast they were moving decides everything: a slow // twist stops exactly where it was put, a flick keeps turning in proportion to // how hard it was thrown, and the two are the same curve — momentum ramps up // from zero rather than switching on at a threshold. // // A coast makes pardes_animating true and is spent by // pardes_animation_tick, so a host that already re-pumps for theme transitions // needs no new machinery; one that never calls this simply has a dial with no // momentum. void pardes_rotate_end(void); // 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); // Frozen old grid and its byte-per-cell semantic change mask. Both are either // present for exactly cols*rows cells or NULL together, and are borrowed until // the next pardes_frame call just like the canonical cells. const pardes_cell_s *pardes_frame_previous_cells(void); const uint8_t *pardes_frame_changed_cells(void); uint16_t pardes_frame_cols(void); uint16_t pardes_frame_rows(void); // One rasterized pixel attachment: a PDF page, or an image pane's pixels. // // Geometry is in PHYSICAL PIXELS, the space pardes_resize's cell_w/cell_h put // the core in. `cell_x`/`cell_y` are the pane body's origin in CELLS and the // only thing to multiply out; `dst_*` is relative to that origin and `src_*` // is the crop of the raster to take. Both are already clipped to the viewport, // so a continuous-scroll page needs no overflow clip of its own — but the body // (`cell_w` x `cell_h` cells) is still the rectangle nothing may paint past. // // `serial`, `page` and `revision` together are the cache key: a host holds its // decoded texture while all three hold still, and panning, fit and scrolling // deliberately do not move them. typedef struct { uint32_t serial; uint32_t page; uint32_t revision; uint16_t cell_x; uint16_t cell_y; uint16_t cell_w; uint16_t cell_h; uint32_t dst_x; uint32_t dst_y; uint32_t dst_w; uint32_t dst_h; uint32_t src_x; uint32_t src_y; uint32_t src_w; uint32_t src_h; // subpixel vertical displacement a proportional wheel kept float offset_y; uint32_t iw; uint32_t ih; // iw * ih * 4 bytes, RGBA8, borrowed until the next pardes_frame const uint8_t *rgba; // PDF paper RGB; PARDES_COLOR_DEFAULT means an ordinary image. uint32_t paper_bg; } pardes_image_s; // This frame's attachments, in paint order. Ask after pardes_frame; both are // valid until the next one, exactly like the cell buffer. uint32_t pardes_frame_images(void); const pardes_image_s *pardes_frame_image_list(void); // Shader transition records for this frame, borrowed until pardes_frame is // called again. Empty means draw the canonical grid without a panel pass. uint32_t pardes_frame_panel_tracks(void); const pardes_panel_track_s *pardes_frame_panel_track_list(void); // Call only after the host has put this frame on screen. `true` commits the // borrowed panel-track list above; `false` records a direct canonical fallback. // Returns true only when remapping a stationary pointer newly started a clock // the host was not already running. bool pardes_frame_presented(bool animated_panels); // The file behind the FOCUSED pane, or NULL when there is none: a terminal, an // output buffer, or nothing focused. PDFs and images count — they are real // paths, and a titlebar proxy icon is about the file, not about who may edit // it. Borrowed until the next call, like pardes_font_take. const char *pardes_active_path(void); // ...and whether that pane holds edits which are not on disk. Always false for // anything with no buffer to save, PDFs and images included. bool pardes_active_dirty(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); // Mouse pointer affordance: 0 = default text pointer, 1 = link hand, // 2 = a validated Look/Exec target under the pointer (the acme arrow). uint32_t pardes_pointer_shape(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 must be rejected explicitly while // keeping the face that works; otherwise Config would leave the request pending // forever or claim a fallback was accepted. // Optional size in native points; zero preserves the current size. const char *pardes_font_take(uint16_t *size_hundredths); // Boot, zoom and display-scale changes observe the face already on screen; // this never consumes a pending request. `point_hundredths` is the effective // body size (14pt => 1400). bool pardes_font_observe(const char *effective_name, size_t len, uint16_t point_hundredths); // A taken Font request is not effective until the host says what CoreText // actually accepted. Calling this without a request from pardes_font_take is // inert, so an unrelated host report cannot consume the request. bool pardes_font_ack(const char *effective_name, size_t len, uint16_t point_hundredths); void pardes_font_reject(void); #ifdef __cplusplus } #endif #endif // PARDES_H