Handle mouse shape and config change Ghostty actions (#13)#17
Draft
mvbmir wants to merge 90 commits into
Draft
Conversation
ci: add AUR publish workflow
Replaces the light background app icon with a dark variant (#1E1E2E background) while preserving and slightly brightening the blue gradient chevron. All sizes regenerated from 512px source.
Dark mode app icon
Co-authored-by: Codex <noreply@openai.com>
Replace pane reparenting with atomic split tree rebuild
Co-authored-by: Codex <noreply@openai.com>
Add a size check to package.sh that catches unoptimized libghostty builds before they get shipped. A Debug build is ~89MB vs ~30MB for ReleaseFast, and causes ~7x slower terminal IO throughput.
Instead of trusting whatever zig-out/ contains, package.sh now runs the zig build itself with -Doptimize=ReleaseFast. This prevents accidentally shipping a Debug build, which causes ~7x slower IO.
Keep the coalesced idle wakeup path from main, retain explicit GLArea renders, and redraw on remap so hidden terminals refresh when shown again. Co-authored-by: Codex <noreply@openai.com>
Fail fast when Zig or the Ghostty submodule is missing, and document the new package.sh build flow in the README.\n\nCo-authored-by: Codex <noreply@openai.com>
Fix: reject Debug builds of libghostty in packaging
* Fix 10+ second freeze on pane split/close Replaces surgical widget reparenting with Ghostty-style atomic tree rebuild. The old code detached and reattached GLArea widgets on the same tick, which breaks GTK4's GL context lifecycle. New architecture: - SplitNode data model is the source of truth for split layout - SplitTreeContainer manages async rebuild: tear down old widget tree, then rebuild from the data model on the next idle tick - The one-tick separation between unrealize and realize prevents the GLArea breakage that caused the freeze - Scrollback is fully preserved across split/close operations Removes ~215 lines of surgical reparenting code from window.rs, replaced by data model mutations that delegate to the container. * Wire control socket to GTK UI Connects the existing limux-control Unix socket server to the GTK window, enabling programmatic workspace management from external tools (limux-cli, scripts, editor integrations). New control_bridge module starts a socket listener in a background thread and dispatches commands to the GTK main loop via polling. Supported commands: workspace.create, workspace.list, workspace.rename, workspace.activate, workspace.close, surface.send_text. Also adds TerminalHandle::send_text() for injecting text into terminal surfaces, and pane::first_terminal_handle() for resolving the active terminal in a workspace. * Update app icon to dark mode variant Replaces the light background app icon with a dark variant (#1E1E2E background) while preserving and slightly brightening the blue gradient chevron. All sizes regenerated from 512px source. * Fix wrapped workspace root pane resolution Co-authored-by: Codex <noreply@openai.com> * host: coalesce ghostty wakeup idles Co-authored-by: Codex <noreply@openai.com> * Reject Debug builds of libghostty in packaging script Add a size check to package.sh that catches unoptimized libghostty builds before they get shipped. A Debug build is ~89MB vs ~30MB for ReleaseFast, and causes ~7x slower terminal IO throughput. * Build libghostty with ReleaseFast automatically in package.sh Instead of trusting whatever zig-out/ contains, package.sh now runs the zig build itself with -Doptimize=ReleaseFast. This prevents accidentally shipping a Debug build, which causes ~7x slower IO. * host: reconcile render throttling with main Keep the coalesced idle wakeup path from main, retain explicit GLArea renders, and redraw on remap so hidden terminals refresh when shown again. Co-authored-by: Codex <noreply@openai.com> * Clarify packaging prerequisites Fail fast when Zig or the Ghostty submodule is missing, and document the new package.sh build flow in the README.\n\nCo-authored-by: Codex <noreply@openai.com> * host: wire control socket into GTK UI Align the GTK host bridge with the current control protocol, remove the CLI fallback that would double-run workspace commands, and dispatch control commands onto the GLib main context directly. Co-authored-by: Codex <noreply@openai.com> * host: drop unused control helper Remove the stale pane helper pulled in from the original PR branch so the host bridge stays warning-free under stricter checks. Co-authored-by: Codex <noreply@openai.com> --------- Co-authored-by: am.will <42459108+am-will@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Ctrl+Tab and Ctrl+Shift+Tab are the standard tab cycling shortcuts across browsers and most terminal emulators. The current defaults (Ctrl+Shift+Right/Left) are less discoverable and conflict with text selection shortcuts in many editors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Save the current font size to ~/.config/limux/settings.json whenever the user changes it via Ctrl+/-, and restore it on new terminal creation. Closes am-will#43
The connect_map handler in apply_split_ratio_after_layout fired on every workspace switch, overwriting user-adjusted split positions with the stale ratio captured at build time. Now it only fires once for the initial layout. Closes #2
Track font size on the Rust side by reading the default from the ghostty config file and applying deltas on increase/decrease. Removes the ghostty_surface_font_size C API export and reverts the submodule pointer so builds work without a custom ghostty fork.
Both position_notify handlers (data model + session persistence) now skip updates when the Paned widget is unmapped. GTK fires position events with stale values during unmap/remap cycles, corrupting the stored ratio when switching workspaces.
Zoom in/out/reset now applies to every terminal surface across all workspaces and tabs, not just the focused one.
The previous is_mapped() guard was insufficient. GTK fires position_notify events with stale positions during the remap transition while is_mapped() is already true. Two changes fix this: 1. Add an applying flag that suppresses position_notify during programmatic set_position calls (initial layout and workspace re-map). 2. Re-apply the current data model ratio on every map event instead of only once, so workspace switches always restore the correct split positions.
- Change default increase font size keybinding from <Ctrl>plus to <Ctrl>equal so it matches Ctrl+= without requiring Shift - Broadcast zoom changes to all terminals across all workspaces/tabs - Fix split ratio corruption with applying flag to suppress position_notify during programmatic set_position calls
The is_mapped() check in position_notify handlers prevented GTK from recording the initial position during split creation (new Paneds aren't mapped yet when first laid out). The applying flag alone is sufficient to prevent ratio corruption during workspace switches.
The idle callback for initial layout now captures the ratio as a plain f64 instead of reading from the shared cell. This prevents early position_notify events (which fire with position=0 before layout) from corrupting the ratio before the idle callback runs. The connect_map handler still reads from the cell so workspace switches restore drag-adjusted ratios correctly.
Add handlers for GHOSTTY_ACTION_MOUSE_SHAPE (tag 35) and GHOSTTY_ACTION_CONFIG_CHANGE (tag 47). Mouse shape converts the Ghostty cursor enum to CSS cursor names and applies them to the GLArea widget. Config change forwards the provided config to the appropriate update function without freeing it (owned by Ghostty core). Closes #13
Top bar (always visible): - Sidebar toggle icon, new-workspace "+" icon, workspace indicator pills (active/hover/unread states), minimize/maximize/close window buttons - Custom buttons instead of gtk::WindowControls so hover shape, padding, and border-radius match the pane bar - Empty header space drags the window via gtk::WindowHandle Pane header: - Drag filler between the last tab and the action icons (a WindowHandle inside tab_overlay) so empty space moves the window without conflicting with the tabs' DragSource - Middle-click on a tab closes it - Ctrl+W now closes the active tab instead of the whole pane; the pane (and workspace) still collapses when the last tab closes, via the existing on_empty callback path Sidebar: - Drops the "WORKSPACES" title and the big "+" button at the bottom - Each row has a close X on the top-right and the favorite star moved underneath; both reveal on hover/selected - Cleaner row styling, single-layer selection highlight (the default ListBox row background is suppressed so only the inner row box paints) New-workspace UX: - "+" clones the active workspace's folder instead of opening the folder picker; the picker only shows on first run / when no workspace exists Fix: GHOSTTY_ACTION_CONFIG_CHANGE handler is now a no-op. Calling ghostty_*_update_config from inside the action callback re-fires the same action and recursed until the stack overflowed, preventing the app from starting.
Follow-up to the cmux redesign (#5), adding persistent user settings and fixing a pile of spacing/split-ratio bugs. Settings: - General → "Top bar" (switch). When off, removes the top bar entirely and relocates its buttons: dock toggle, settings cog, +, and window controls move into a new WindowHandle-wrapped sidebar header (with an hexpand spacer separating the left and right groups). When the sidebar is collapsed, the dock toggle parks on the active workspace's leading pane (before the first tab) via a new `leading_box` slot on every pane. - General → "Window controls side" (Left/Right). Moves minimize/maximize/ close between the two ends of whichever header is active (top bar or sidebar header). Applies live. - Both persist under `interface.*` in settings.json. Workspace row: - Close X in the top-right (replaces the old star), star moved below next to the folder path. Both 20×20, halign End, hover-to-reveal. - Double-click a row triggers inline rename. Pane & tabs: - Settings cog removed from per-pane action row; now lives in the top bar / sidebar header, freeing tab space in every pane. - Middle-click a tab closes it (same path as the close X). - Ctrl+W closes the active tab; the pane only collapses when the last tab closes (via the existing on_empty callback). - Pane header now draggable: a WindowHandle filler fills the space between the last tab and the action icons, so empty header space moves the window without conflicting with tab DnD. New workspace UX: - Clicking "+" clones the active workspace's folder instead of opening the folder picker. The picker only appears on first run / when no workspace exists. Split ratio fix (the big one): - gtk::Paned's `position` is absolute pixels, so any width change (sidebar toggle, window resize) made position-notify fire with a wrong ratio = position / new_width, corrupting the saved ratio and drifting splits from 50/50 over time. - Fixes: * shrink_start_child / shrink_end_child on every Paned so the ratio wins over larger child minimums (e.g. wide tab strips). * position-notify tracks `last_size`; if width changed since the last notify it's an auto-adjust (skip), otherwise it's a real user drag (update ratio). * Per-frame tick callback observes the paned's actual width and re-applies `position = ratio * new_width` on size changes. GtkWidget's `width` / `height` properties don't reliably emit notify across GTK 4.x versions, so polling is intentional; the check is O(1). * Startup uses a one-shot tick callback to apply the ratio once the paned first has a non-zero allocation. Spacing / visual polish: - Custom minimize/maximize/close buttons replace gtk::WindowControls (Adwaita's 24px circular defaults couldn't be overridden cleanly). Same .limux-top-bar-btn class as the other top bar icons → consistent square hover with matching padding. - Pane action icons and tab close X aligned to the same CSS pattern (border-radius: 6px, margin 0 1px, valign Center). - First workspace row gets a 4px top margin so the above-first gap visually matches the between-workspace gap. - sidebar_drag_area hidden in both top-bar and no-top-bar modes since the respective headers already handle window drag. - Border-bottom on the sidebar header removed. Structural refactors: - Extracted handle_config_change() to dedupe the appearance + top-bar + save/revert logic that ran in two places. - Inlined apply_window_controls_side's single delegating wrapper. - Dropped the unused on_config_changed field from PaneCallbacks (now that the settings cog lives in the top bar, no pane opens the dialog). - toggle_top_bar (keyboard shortcut) now calls apply_top_bar_mode too, so buttons don't get stranded when the user toggles off via Ctrl+Shift+M. apply_top_bar_mode respects both `show_top_bar` (persistent) and `top_bar_visible` (transient). cargo fmt / cargo clippy -D warnings / cargo test all clean.
The keyboard-shortcut side of middle-click tab close is being pulled into its own PR that properly routes close-tab through the configurable shortcut system (adds CloseTab shortcut, remaps CloseFocusedPane to Ctrl+Alt+W, updates the close-pane button tooltip to reflect whichever binding is active). Keeps on this branch: - Middle-click a tab still closes it (uses remove_tab directly) - All other redesign + settings work Reverts from this branch: - Ctrl+W → close_active_tab_in_pane. Restored to the original close-pane behavior. - pub fn close_active_tab_in_pane — helper not needed here; will be re-added in the follow-up PR.
Some users may prefer to navigate workspaces solely via the sidebar. Add a dedicated "Workspace indicators on the top bar" switch in General settings that hides the pills without hiding the top bar. - New field: InterfaceConfig.show_workspace_indicators (default true) - Persisted under interface.show_workspace_indicators in settings.json - apply_top_bar_mode hides each pill (not the container box) so indicator_box keeps its hexpand spacer role between the left group and the window controls on the right - Called from workspace creation paths (add_workspace_from_state, create_workspace_for_tab) so newly-added pills honor the setting
mvbmir
pushed a commit
that referenced
this pull request
Apr 17, 2026
Credit: implementation approach adapted from #17 by @threehymns.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GHOSTTY_ACTION_MOUSE_SHAPE(tag 35): converts Ghostty's cursor shape enum to CSS cursor names and applies them to the GLArea widget viaset_cursor_from_name. Cursor now changes correctly for links in TUI apps, resize handles, etc.GHOSTTY_ACTION_CONFIG_CHANGE(tag 47): forwards the config object provided by Ghostty core to the appropriateghostty_app_update_config/ghostty_surface_update_configfunction, enabling live config updates without a full reload.ghostty_action_config_change_sstruct, and both union fields.Note:
GHOSTTY_ACTION_RELOAD_CONFIG(46) andGHOSTTY_ACTION_SHOW_CHILD_EXITED(54) were already handled.GHOSTTY_ACTION_COLOR_CHANGE(45) is tracked separately in #9.Closes #13