feat(linux): size the quick terminal card as a percentage of the window - #22
Merged
melonamin merged 2 commits intoAug 11, 2026
Merged
Conversation
The quick terminal card is placed by fixed pixel margins (56px top, 44px on the other three sides), so its size does not track the window: on a large window it stays a thin inset panel, and on a small one the margins eat most of the content. macOS sizes the same card proportionally — WindowContentView uses 0.9 x 0.9 of the window content below the titlebar, centered. Add the host-free half of that parity. LinuxQuickCardPolicy gains cardSizePercent (90, the macOS constant) and cardAllocation(overlayWidth:overlayHeight:headerHeight:), returning the card rectangle in overlay coordinates: cardSizePercent% of the area BELOW the header, centered horizontally in the full overlay and vertically in that available area, so the header stays visible and clickable above the card. The name is qualified because the unqualified sizePercent already means the session overlay's own percentage elsewhere in the port, and the two are unrelated numbers. The function is total over its Int32 inputs. The multiply widens to 64 bits and comes back through Int32(clamping:), the idiom GhosttySurfaceGeometry.initialBackingSize already uses — a naive `width * cardSizePercent / 100` traps near Int32.max, while `width / 100 * cardSizePercent` would silently change the rounding. A headerHeight at or beyond overlayHeight (a window shorter than its own header) collapses the available height to 0 instead of going negative. There are deliberately no pixel floors: macOS has none, and a floor here could not raise the window's own minimum size anyway — it could only clip the card. Also rewrite the cardCSS shadow-budget comment, which bounded the shadow against the quick card's 44px margin that the next commit removes. The replacement bound is a threshold rather than a constant — a 5% band clears the shadow's 40px reach only above ~800px — and the floating overlay card's different model (overlaySizePercent with a 240x160 floor, computed once at open time) is spelled out so the two cards are not read as sharing math. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wire the allocation math into GTK. The deck overlay gains a GtkOverlay::get-child-position handler that hands quickFrame an explicit rectangle from LinuxQuickCardPolicy.cardAllocation on every layout pass, and setQuick drops the 56px top / 44px side margins that used to place it. Because the handler re-measures the overlay on each pass, the card now tracks a live window resize and hidden-toolbar mode instead of holding a fixed inset. The handler answers ONLY for quickFrame. Every other deckOverlay child returns 0 and keeps GTK's default, alignment-based placement: the per-session floating overlay frames, the zoom host, the dashboard host, the Ctrl-Tab switcher box and the GL-error label. The zoom case is load-bearing — zooming .quick hides quickFrame and adds a FILL/expand zoomHost, which must never be given the card rectangle. Three details are load-bearing and documented at the handler: - connect() uses flags 0, NOT G_CONNECT_AFTER. The signal is declared when="last" with a boolean-handled accumulator and the class default returns TRUE for every child, so an AFTER connection would never run. - The frame keeps GTK_ALIGN_FILL and zero margins, because gtk_widget_size_allocate re-applies align and margins INSIDE the returned rectangle — copying syncOverlay's GTK_ALIGN_CENTER would collapse the card back to its natural size and silently defeat the explicit allocation. - The header is measured only while it is shown. A hidden-but-previously- allocated GTK4 widget retains its last allocated height, which would otherwise inset the card by a strip that is not on screen in hidden-toolbar mode. Teardown rides the usual registry recovery: controllerForWidget resolves through gWindows, which windowWillClose has already left, so a late emission on a closing window falls through to default placement rather than touching a freed controller. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owner
|
Thank you — this is a focused GTK implementation, and the allocation behavior checks out against the current base. Local build, lint, Linux tests, core tests, and the exact-head CI suite all passed. Merging. |
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.
What
The Linux quick terminal is placed by fixed pixel margins — 56 px at the top and 44 px on the other three sides — so its size does not track the window. The bigger the window, the closer the card gets to filling it: on a 2560×1440 window it covers 96.5% of the width and reads as near-fullscreen, and the sidebar disappears underneath it.
macOS sizes the same card proportionally:
QuickTerminalPaneisgeo.size.width * 0.9 × geo.size.height * 0.9(agterm/Views/WindowContentView.swift:781), inside awindowOverlayLayerthat spans the full window width and is inset only bytitlebarHeight(:126). So the macOS rule is "90% of the window, centered, below the titlebar" — andsite/docs.html:681already documents the quick terminal as "overlaid at 90% of the window". Linux was the outlier; the docs were already right.Root cause
A fixed margin and a percentage disagree in opposite directions as the window changes size, so a single pair of constants cannot match the macOS rule at more than one window size. Measured on the two builds, same content, same conditions:
The "before" column is the tell: the card's proportion drifts with the window because the band is pinned at 44 px, while the "after" column is constant by construction.
Fix
Two commits: the host-free math, then the GTK wiring.
LinuxQuickCardPolicygainscardSizePercent(90 — the macOS constant) andcardAllocation(overlayWidth:overlayHeight:headerHeight:), returning the card rectangle in overlay coordinates: 90% of the area below the header, centered horizontally in the full overlay and vertically in that available area, so the header stays visible and clickable above the card.The name is qualified deliberately: the unqualified
sizePercentalready means the session overlay's own percentage elsewhere in the port (AppControllerSurfaces.swift:147), and the two are unrelated numbers with different models — the overlay card hasmax(240, …)/max(160, …)pixel floors and is sized once at open time, not per layout pass.The function is total over its
Int32inputs. The multiply widens to 64 bits and comes back throughInt32(clamping:), the idiomGhosttySurfaceGeometry.initialBackingSizealready uses — a naivewidth * cardSizePercent / 100traps nearInt32.max, whilewidth / 100 * cardSizePercentwould silently change the rounding. AheaderHeightat or beyondoverlayHeight(a window shorter than its own header) collapses the available height to 0 rather than going negative.There are deliberately no pixel floors. macOS has none, and a floor here could not raise the window's own minimum size anyway — it could only clip the card.
The wiring is a
GtkOverlay::get-child-positionhandler on the deck overlay, which handsquickFramean explicit rectangle on every layout pass, plus the removal of the four margin calls insetQuick. Because the handler re-measures the overlay each pass, the card now tracks a live window resize and a hidden-toolbar toggle instead of holding a fixed inset.Three details are load-bearing, and are pinned in the handler's doc comment:
connect()uses flags 0, notG_CONNECT_AFTER. The signal is declaredwhen="last"with a boolean-handled accumulator and the class default returns TRUE for every child, so an AFTER connection would never run.GTK_ALIGN_FILLand zero margins.gtk_widget_size_allocatere-applies align and margins inside the returned rectangle, so copyingsyncOverlay'sGTK_ALIGN_CENTERwould collapse the card back to its natural size and silently defeat the explicit allocation.The handler answers only for
quickFrame. Every otherdeckOverlaychild returns 0 and keeps GTK's default, alignment-based placement: the per-session floating overlay frames, the zoom host, the dashboard host, the Ctrl-Tab switcher box and the GL-error label. The zoom case is load-bearing — zooming.quickhidesquickFrameand adds a FILL/expandzoomHost, which must never be given the card rectangle.Teardown rides the usual registry recovery:
controllerForWidgetresolves throughgWindows, whichwindowWillClosehas already left, so a late emission on a closing window falls through to default placement rather than touching a freed controller.One second-order behavioural change worth stating. Dropping the margins removes the quick frame's contribution to
GtkOverlay::measure, so a window with a live quick terminal can now be dragged smaller than before — previously its own minimum plus 88/100 px of margin raised the window minimum. This is an improvement, but it is a real change.Keep-in-sync
Nothing owed, recorded so it is not relitigated:
AppActionsaction, noCommandcase, noagtermctlsubcommand, so the write→read-back rule ontreeis vacuous. A configurable size (quick.resize --percent, mirroringsession.overlay.resize/overlaySizePercent) is the obvious shape if it is ever wanted; it is deferred, not exempt, and out of scope for a parity fix.site// agent-skill impact. No command, flag, keybinding, mode or model change — andsite/docs.html:681already says 90%, so this change makes the code match the docs rather than the other way round.Change
feat(linux): add percent-based quick card allocation mathagterm-linux/Tests/AgtermLinuxTests/LinuxQuickCardPolicyTests.swiftagterm-linux/Sources/AgtermLinux/LinuxQuickCardPolicy.swiftfeat(linux): size the quick card via get-child-positionagterm-linux/Sources/AgtermLinux/AppControllerCallbacks.swiftagterm-linux/Sources/AgtermLinux/AppController.swiftAppController.swiftstays at 985 lines against the 1000-linefile_lengthlimit — a net 0, and no lint limit was touched. The signal connect is one line there calling aninstallQuickCardPlacement(on:)helper in the callbacks file, following the existinginstallEmptyWindowKeyControllerprecedent: the inlineunsafeBitCastfor this 4-argument signature is ~226 columns, past the 200-columnline_lengthlimit.The 13 removed lines in
LinuxQuickCardPolicy.swiftare thecardCSSshadow-budget comment, which bounded the shadow against the 44 px margin this change removes. Its replacement states a threshold instead of a constant — a 5% band clears the shadow's 8 px offset + 32 px blur only above ~800 px — and spells out the floating overlay card's different model so the two cards are not read as sharing math.Testing
Run locally:
swift test --package-path agterm-linux— 210 tests / 30 suites. One failure:IntegrationServiceTests"Flatpak process environments do not offer a host launcher", which reproduces on an unmodifiedlinux-portcheckout on this box (a realagtermctlis installed at/opt/agterm-linux/bin, sopackageCLI()resolves.installedbefore the flatpak guard is reached). The newLinux quick-terminal card allocationsuite passes — 10 tests, including a 20-case@Test(arguments:)sweep.swift test --package-path agtermCore— 2040 tests, 3 failures, all inCodexStatusHookTestsand all pre-existing here (Manjaro ships a/usr/bin/plutilvia libplist, so the hook takes the macOS branch). Confirmed with--filter CodexStatusHookTests; this change touches no file underagtermCore.swift buildfor bothAgtermLinuxandagtermctl-linux— clean.swiftlint lint --strict— 0 violations in 410 files.scripts/check-linux-core-boundary.sh,scripts/check-linux-cli-drift.sh,git diff --check linux-port...HEAD— all clean.scripts/test-linux-ui.sh— 13 of 14 scenarios pass, includinghidden-toolbar, which is the scenario nearest this change (it toggles the very header the handler measures). The 14th,context-menu, fails identically on a pristinelinux-portbuild — I built one and ran the same matrix against both to check rather than assume. Note this box also needs a privatedbus-daemon+at-spi2-registrydfor the suite to see the app at all; that is a host quirk, no repo file was changed for it.linux-portat86c29ff, under identical conditions. Every pixel value quoted is measured — the card rectangle is located by itsalpha(#ffffff, 0.18)over#1e2228border, which renders as a distinctive(71,74,79)— not eyeballed.Not covered by any automated gate — stated plainly:
The card's on-screen geometry has no assertion behind it.
LinuxQuickCardPolicyTestspins the math and the GTK handler is not unit-testable, but no AT-SPI scenario reads the card's extents, so deleting theinstallQuickCardPlacementline would silently revert this feature with every test still green. The hidden-toolbar centering and the live-resize tracking rest on manual verification on a real Wayland session, which is where the before/after above was confirmed by eye.A scenario closing that gap is specified — drive
agtermctl quick, wait on thequickVisibleread-back, then compare the quick frame'sAtspiWINDOW extents against the window's, the same shape as the existingverify_sidebar_row_height_follows_font_size— but it is not in this PR: it cannot be executed from the review worktree here, and shipping an unverified e2e scenario seemed worse than shipping none. Happy to add it in a follow-up if you would rather have it alongside.