Skip to content

v1.0 ghostty action coverage: batches 1-4 (58/65 wired)#58

Open
i999rri wants to merge 39 commits into
feat/split-phase1-pane-treefrom
feat/v1-actions-batch1
Open

v1.0 ghostty action coverage: batches 1-4 (58/65 wired)#58
i999rri wants to merge 39 commits into
feat/split-phase1-pane-treefrom
feat/v1-actions-batch1

Conversation

@i999rri
Copy link
Copy Markdown
Owner

@i999rri i999rri commented May 20, 2026

Summary / 概要

Closes a large slice of #57 (v1.0 ghostty action parity). Across four batches this PR takes action coverage from 14/65 to 58/65 wired — 25 new full implementations, 18 explicit acks, 1 disabled-pending-fix. The remaining 7 are either deferred to their own PRs (TOGGLE_WINDOW_DECORATIONS, TOGGLE_BACKGROUND_OPACITY), waiting on multi-window (#55: GOTO_WINDOW, MOVE_TAB, PRESENT_TERMINAL), or not applicable on Windows.

日本語

#57 (v1.0 ghostty action 対応) の大半を一気に片付ける PR。4 つのバッチで action カバレッジを 14/65 → 58/65 まで上げる (新規実装 25、明示 ack 18、無効化保留 1)。残り 7 は別 PR (TOGGLE_WINDOW_DECORATIONS / TOGGLE_BACKGROUND_OPACITY)、multi-window #55 待ち (GOTO_WINDOW / MOVE_TAB / PRESENT_TERMINAL)、Windows 非対応 (SHOW_GTK_INSPECTOR / SHOW_ON_SCREEN_KEYBOARD)。

What's in / 入っているもの

batch 1 — close / max / clipboard / config (8)

Action Win32 primitive Notes
RING_BELL MessageBeep(MB_OK) OS-defined "Default Beep" sound
CLOSE_WINDOW / CLOSE_ALL_WINDOWS / QUIT Window.Close() Same effect in single-window mode
TOGGLE_MAXIMIZE WM_SYSCOMMAND + SC_MAXIMIZE / SC_RESTORE Same path the caption button uses (avoids #26 NVIDIA AV)
COPY_TITLE_TO_CLIPBOARD Clipboard::write Reads back the TabViewItem.Header that SET_TITLE populates
OPEN_CONFIG ShellExecuteW "open" on %LOCALAPPDATA%\ghostty\config Falls through to "Open With" dialog on first run
TOGGLE_VISIBILITY ShowWindow(SW_MINIMIZE / SW_RESTORE) SW_MINIMIZE keeps the taskbar entry visible

batch 2 — reload / mouse / health / size (6)

Action Win32 primitive Notes
RELOAD_CONFIG ghostty_config_load_default_files + ghostty_app_update_config 4MB worker thread for the parser; UI-thread swap; default ctrl+shift+, keybind now works
CONFIG_CHANGE ghostty_config_clone + ReplaceConfig Notification, not request — sync the cached pointer so soft reload doesn't re-apply stale data
RENDERER_HEALTH fprintf(stderr, ...) Log on health flip; recovery UX is a separate design problem
MOUSE_VISIBILITY ShowCursor with state-tracked toggle ShowCursor is a counter — gate on a local bool to avoid the counter walking
QUIT_TIMER (no-op) Windows quits with the last window; the macOS-style countdown doesn't apply
RESET_WINDOW_SIZE SetWindowPos to recorded INITIAL_SIZE or 1280x720 DIP fallback DPI-scaled fallback when INITIAL_SIZE hasn't fired yet

batch 3 — size / scrollbar / render / lifecycle (7)

Action Win32 primitive Notes
INITIAL_SIZE Member cache (m_initialWidth / m_initialHeight) Honors window-width × cell-width-px from config; RESET_WINDOW_SIZE consumes it
CELL_SIZE Member cache (m_cellWidth / m_cellHeight) Pre-wired for future snap-to-cell logic; no current reader
SCROLLBAR (no-op ack) We don't render a scrollbar widget
RENDER UI-thread Tick() Same path as wakeup_cb; ghostty_app_tick is idempotent when nothing is dirty
SHOW_CHILD_EXITED fprintf(stderr, "[child_exited] exit_code=%u after_ms=%llu", ...) In-terminal overlay UI is a separate iteration
CHECK_FOR_UPDATES ShellExecuteW to GitHub releases No in-app Sparkle-equivalent updater yet
PROGRESS_REPORT ITaskbarList3::SetProgressState / SetProgressValue OSC 9;4 → taskbar progress; manager is cached on UI thread

batch 4 — high-priority Win32 (4)

Action Win32 primitive Notes
DESKTOP_NOTIFICATION Microsoft.Windows.AppNotifications.AppNotificationBuilder + Show Manager registered in App::OnLaunched; Package.appxmanifest carries the COM activator + toast activation extension. Toast click currently spawns a second instance — separate follow-up
TOGGLE_FULLSCREEN SetWindowLongPtrW + SetWindowPos + WINDOWPLACEMENT save/restore Borderless FS; custom title bar still draws on top — follow-up
MOUSE_OVER_LINK Win32 TOOLTIPS_CLASS popup in TRACK mode Lazy-init; positioned ~20px below-right of cursor
SIZE_LIMIT WM_GETMINMAXINFO via SetWindowSubclass Subclass installed lazily on first SIZE_LIMIT; MainWindow* carried via dwRefData

Acknowledged (no behavior change, just return true)

Routing them through action_cb so they don't fall through to the unhandled-default while the surface-side UI is built later.

  • Informational state (7): READONLY, SECURE_INPUT, KEY_SEQUENCE, KEY_TABLE, PROMPT_TITLE, PWD, COMMAND_FINISHED
  • Feature pending (11): UNDO, REDO, START_SEARCH, END_SEARCH, SEARCH_TOTAL, SEARCH_SELECTED, INSPECTOR, RENDER_INSPECTOR, TOGGLE_TAB_OVERVIEW, TOGGLE_QUICK_TERMINAL, TOGGLE_COMMAND_PALETTE

Disabled — #if 0-guarded (1)

  • FLOAT_WINDOWWS_EX_TOPMOST toggle written, verified Win32-side. No keybind we tried reached action_cb: ctrl+shift+f collides with default start_search; ctrl+shift+alt+f is swallowed by the WinUI Alt-menu accelerator; ctrl+shift+backslash produced no observable call either. Body preserved verbatim; re-enable once the dispatch path is understood.
日本語: 詳細

batch 1〜4 + ack バンドル + 無効化 FLOAT_WINDOW、計 22 コミット (Package.appxmanifest の toast activator 登録含む)。各 action のコメントは英語で残し、commit メッセージで「なぜ」を残す方針。

Test plan / 検証

Suggested keybinds (%LOCALAPPDATA%\ghostty\config):

keybind = ctrl+shift+m=toggle_maximize
keybind = ctrl+shift+c=copy_title_to_clipboard
keybind = global:ctrl+shift+h=toggle_visibility
keybind = f11=toggle_fullscreen
# ctrl+, and ctrl+shift+, are ghostty defaults (open_config / reload_config)
# — leave them alone instead of overriding.
  • RING_BELL: printf "\a" produces the OS bell sound
  • CLOSE_WINDOW / QUIT: window closes via keybind
  • TOGGLE_MAXIMIZE: keybind toggles maximize / restore
  • COPY_TITLE_TO_CLIPBOARD: keybind puts the tab title on the clipboard
  • OPEN_CONFIG: Ctrl+, opens config in the default editor (ghostty default)
  • RELOAD_CONFIG: edit background = 003366 → save → Ctrl+Shift+, → background changes without restart
  • CONFIG_CHANGE: covered by the reload test (no separate UI surface)
  • MOUSE_VISIBILITY — handler #if 0-guarded pending Windows keyboard input path drops fields: MOUSE_VISIBILITY + physical-key bindings don't fire #60 (action doesn't reach action_cb on Windows; suspected cause: ghostty_surface_key not populating event.utf8 for printable keystrokes)
  • RESET_WINDOW_SIZE: with keybind = ctrl+alt+r=reset_window_size (the unicode-trigger workaround for Windows keyboard input path drops fields: MOUSE_VISIBILITY + physical-key bindings don't fire #60), drag the window to a custom size → Ctrl+Alt+R snaps it back to startup dimensions. Physical-key triggers like ctrl+shift+digit_0 don't fire because of the same ghostty_surface_key field-population issue
  • TOGGLE_VISIBILITY: global: keybind hides → reveals
  • TOGGLE_FULLSCREEN: F11 → borderless FS → F11 again → original window
  • MOUSE_OVER_LINK — tooltip body #if 0-guarded pending MOUSE_OVER_LINK: Win32 tooltip crashes the process on URL click #61 (action reaches action_cb but the Win32 TOOLTIPS_CLASS tooltip crashed the process on URL click)
  • SIZE_LIMIT: drag the window to its minimum → it stops at ghostty's hardcoded 10-cell × 4-row floor (the min is not user-configurable; ghostty sends the limit once at startup as cell_width * 10 by cell_height * 4)
  • DESKTOP_NOTIFICATION: echo "\e]9;hello`a"` in pwsh → Windows toast appears. Known limitation: clicking the toast spawns a second Ghostty instance (toast activation routing not wired yet — follow-up)
  • PROGRESS_REPORT: echo "\e]9;4;1;50`a"→ taskbar button shows a progress bar;echo "`e]9;4;0`a"` clears it
  • SHOW_CHILD_EXITED: exit 42 in pwsh → VS Debug output shows [child_exited] exit_code=42 after_ms=... (mirrored through OutputDebugStringA so it survives the surface teardown)
  • CHECK_FOR_UPDATES: bind to keybind = ctrl+shift+u=check_for_updates → opens GitHub releases in browser
日本語

検証用 keybind (%LOCALAPPDATA%\ghostty\config):

keybind = ctrl+shift+m=toggle_maximize
keybind = ctrl+shift+c=copy_title_to_clipboard
keybind = global:ctrl+shift+h=toggle_visibility
keybind = f11=toggle_fullscreen
# ctrl+, と ctrl+shift+, は ghostty デフォルト (open_config / reload_config)。
# 上書きしないこと。

チェックリスト:

  • RING_BELL: printf "\a" で OS ベル音
  • CLOSE_WINDOW / QUIT: キーバインドでウインドウが閉じる
  • TOGGLE_MAXIMIZE: キーバインドで最大化 / 復元
  • COPY_TITLE_TO_CLIPBOARD: キーバインドでタイトルがクリップボードへ
  • OPEN_CONFIG: Ctrl+, で config がデフォルトエディタで開く
  • RELOAD_CONFIG: config を編集 → 保存 → Ctrl+Shift+, → 再起動なしで色が変わる
  • CONFIG_CHANGE: 上の reload テストでカバー
  • MOUSE_VISIBILITYWindows keyboard input path drops fields: MOUSE_VISIBILITY + physical-key bindings don't fire #60 待ちで #if 0 無効化 (Windows で action_cb まで届かない、ghostty_surface_keyevent.utf8 populate 漏れが疑われる)
  • RESET_WINDOW_SIZE: keybind = ctrl+alt+r=reset_window_size (Windows keyboard input path drops fields: MOUSE_VISIBILITY + physical-key bindings don't fire #60 の unicode-trigger workaround) で、サイズを変えてから Ctrl+Alt+R 押すと起動時サイズに戻る。物理キートリガー (ctrl+shift+digit_0 等) は同じ ghostty_surface_key のフィールド populate 問題で発火しない
  • TOGGLE_VISIBILITY: global: 付き keybind で隠す → 再表示
  • TOGGLE_FULLSCREEN: F11 で全画面 → 再度 F11 で元に戻る
  • MOUSE_OVER_LINKMOUSE_OVER_LINK: Win32 tooltip crashes the process on URL click #61 待ちで tooltip 本体を #if 0 無効化 (action は届くが Win32 TOOLTIPS_CLASS ツールチップが URL クリックでプロセスを落とす)
  • SIZE_LIMIT: ウインドウを限界まで小さくドラッグ → ghostty の hardcoded な 10 セル × 4 行で止まる (min は user 指定不可、起動時に cell_width * 10 × cell_height * 4 で送られてくる)
  • DESKTOP_NOTIFICATION: pwsh で echo "\e]9;hello`a"` → Windows のトースト。既知の制限: トーストをクリックすると Ghostty が二重起動 (activation routing 未対応、別 PR で)
  • PROGRESS_REPORT: echo "\e]9;4;1;50`a" でタスクバーに進捗バー、echo "`e]9;4;0`a"` で消去
  • SHOW_CHILD_EXITED: pwsh で exit 42 → VS のデバッグ出力に [child_exited] exit_code=42 after_ms=... (OutputDebugStringA 経路でサーフェスのテアダウンを生き残る)
  • CHECK_FOR_UPDATES: keybind = ctrl+shift+u=check_for_updates で releases ページが開く

Out of scope / スコープ外

  • TOGGLE_WINDOW_DECORATIONS — interacts with our custom XAML title bar; isolated PR for easier review
  • TOGGLE_BACKGROUND_OPACITY — DComp swap chain blend mode; renderer change deserves its own PR
  • Toast click activation routing — currently spawns a new Ghostty instance; needs WinMain cmdline parsing for ----AppNotificationActivated: plus single-instance handoff
  • INSPECTOR / RENDER_INSPECTOR — ImGui debug inspector port (Metal-on-macOS → DX-on-Windows)
  • START_SEARCH / END_SEARCH / SEARCH_TOTAL / SEARCH_SELECTED — terminal search bar UI
  • TOGGLE_TAB_OVERVIEW / TOGGLE_QUICK_TERMINAL / TOGGLE_COMMAND_PALETTE — new overlay UIs
  • UNDO / REDO — host-side undo stack (recently-closed tab restore, etc.)
  • GOTO_WINDOW / MOVE_TAB / PRESENT_TERMINAL — multi-window (Multi-window support #55) dependent
  • Refactor of action_cb itself into a dedicated ActionDispatcher class — tracked in Refactor action_cb: move dispatch into a dedicated ActionDispatcher #59, lands after this merges
日本語
  • TOGGLE_WINDOW_DECORATIONS: カスタムタイトルバー干渉、別 PR
  • TOGGLE_BACKGROUND_OPACITY: DComp blend mode、renderer 変更で別 PR
  • トーストクリック時の activation ルーティング: 現状は新規インスタンスが立ち上がる。WinMain----AppNotificationActivated: を見て single-instance に転送する処理が別途必要
  • INSPECTOR / RENDER_INSPECTOR: ImGui inspector の Metal → DX 移植
  • search 系: 検索バー UI
  • overlay 系 (TAB_OVERVIEW / QUICK_TERMINAL / COMMAND_PALETTE): 各 overlay UI
  • UNDO / REDO: host 側 undo stack (閉じたタブ復元等)
  • multi-window 系: Multi-window support #55 待ち
  • action_cb のリファクタ (ActionDispatcher クラスへ抽出): Refactor action_cb: move dispatch into a dedicated ActionDispatcher #59、本 PR マージ後に着手

🤖 Generated with Claude Code

i999rri added 11 commits May 21, 2026 07:31
Without this the terminal BEL character (\x07) is silent and the
user has no audible cue when an interactive program emits a bell —
backwards from the typical terminal experience and inconsistent
with Mac / GTK ghostty which do produce a sound.

MessageBeep(MB_OK) is the native Windows path: the OS plays the
sound bound to the user's "Default Beep" entry in Sound settings,
which is the conventional place for this and lets each user pick
the volume / sound they prefer without an app-side config knob.
The call is asynchronous and thread-safe so it runs straight from
action_cb on the renderer thread.

Honouring ghostty's `bell-features` (audio / attention / title /
unread) granularly is a follow-up — this PR ships the audio
component which is the one users actually notice.
Without these the ghostty side could request the host to terminate
the app (quit keybind, command palette quit, etc.) and nothing
happened — the user had to use the title-bar close button instead.

In a single-window build the three lifecycle actions collapse to
the same thing: close the lone window, which terminates the app.
Multi-window support (#55) will need to split them back out
(`CLOSE_WINDOW` = close active, `CLOSE_ALL_WINDOWS` = close every
window, `QUIT` = shut down the process with whatever multi-window
quit confirmation we adopt).

Dispatch to the UI thread because action_cb fires from the
renderer thread but Close() touches XAML state.
Without this the ghostty keybind that toggles maximize did
nothing — only the caption button click path could maximize /
restore the window, so keyboard-driven users had to fall back to
Win+arrow or alt-space.

Route the action through the same WM_SYSCOMMAND
SC_MAXIMIZE / SC_RESTORE the caption-button click sends. Going
via WM_SYSCOMMAND (instead of OverlappedPresenter.Maximize /
Restore) keeps us off the NVIDIA driver AV codepath documented
in issue #26.

Dispatched to the UI thread because action_cb is called from the
renderer thread and SendMessage / IsZoomed expect the message
loop owner.
Without this the ghostty open-config keybind (Cmd/Ctrl+, in
default bindings) silently does nothing on Windows — the user
has to open `%LOCALAPPDATA%\ghostty\config` manually in a file
manager / editor every time they want to tweak settings.

Pass the absolute config path to ShellExecuteW with the "open"
verb so Windows picks whatever editor the user has associated
with extension-less plain-text files. First-run users with no
association get the system "Open With" dialog, which is the
right OS-native discovery flow.
Without this the ghostty copy-title keybind silently did nothing
on Windows. Useful when the title contains the cwd / running
command and the user wants to paste it elsewhere.

Pull the title back out of the TabViewItem.Header that
SET_TITLE / SET_TAB_TITLE already populates and send it through
the same Clipboard::write helper the selection-copy path uses.
Empty titles are skipped so we don't blow away whatever the user
already had on the clipboard.
Without this the ghostty float-window keybind silently dropped
on Windows even though the underlying primitive (WS_EX_TOPMOST)
has been there since Win32 day one. Users who keep a small
ghostty pane on top of a video / chat window needed to alt-Tab
back and forth instead.

SetWindowPos with HWND_TOPMOST / HWND_NOTOPMOST + SWP_NOMOVE |
SWP_NOSIZE | SWP_NOACTIVATE flips the z-order flag in place
without resizing or repositioning the window. The TOGGLE variant
reads the current WS_EX_TOPMOST bit so the action follows the
window's actual state rather than a host-side cached one.
Without this the ghostty toggle-visibility keybind silently
dropped. The action is the building block for hide-on-blur /
hotkey-hide workflows: press the bind to hide the window, press
it again to bring it back.

ShowWindow(SW_HIDE) / ShowWindow(SW_SHOW) is the native Win32
idiom; SetForegroundWindow on the show branch raises the window
to the foreground because the user just asked for it back. Note
that the keybind needs the `global:` qualifier in the user's
config for the show side to work — without that the bind only
fires when the window has keyboard focus, so a hidden window
can't be brought back via the bind alone (taskbar / alt-tab
still work).
MSVC /W4 flags _wgetenv as deprecated (C4996) which the project
build treats as an error. Swapping the CRT helper for the
Win32 GetEnvironmentVariableW path is the documented modern fix:
the API writes into a caller buffer so there's no heap-allocation
cleanup, and it doesn't trip the security-deprecation warning.
Static analyzer (C6011) flagged the post-GlobalLock memcpy as a
possible null deref. GlobalLock can fail under low memory or if
the handle was invalidated between alloc and lock; the existing
code assumed success and called memcpy with the null `dest`.

Bail out cleanly when the lock fails: GlobalFree the allocation
(SetClipboardData would otherwise inherit an unwritten buffer
and the OS would own a corrupted handle) and CloseClipboard so
we don't leave the clipboard locked from other apps. Surfaced
now because the new COPY_TITLE_TO_CLIPBOARD path covers a code
path the analyzer hadn't previously walked.
User feedback: ShowWindow(SW_HIDE) on Ctrl+Shift+H makes the
window disappear from the taskbar too, leaving no discoverable way
to bring it back. ghostty's `global:` keybind qualifier isn't
wired to Win32 RegisterHotKey on this port yet, so SW_HIDE plus a
non-global bind on the same key means the user has to relaunch
the app to recover.

Switch to SW_MINIMIZE / SW_RESTORE on IsIconic — the window stays
in the taskbar, the bind continues to fire while focused, and
the taskbar click is the natural restore path Windows users
expect. Mac-style full hide can come back once global hotkeys land.
The verification round confirmed every batch-1 action (RING_BELL,
TOGGLE_MAXIMIZE, CLOSE_WINDOW/QUIT, COPY_TITLE_TO_CLIPBOARD,
OPEN_CONFIG, TOGGLE_VISIBILITY) arrives at action_cb and fires
its handler. FLOAT_WINDOW didn't appear in traces because the
Ctrl+Shift+F default bind to start_search took precedence in the
user's config — that's a config concern, not a host code issue.

Drops the per-action OutputDebugStringA + the <cstdio> include
it required.
@i999rri i999rri self-assigned this May 21, 2026
i999rri and others added 7 commits May 22, 2026 00:41
Without a host handler the default ctrl+shift+, keybind just bounces
off action_cb, leaving the user no way to pick up a config edit
short of a full app restart — annoying enough that we kept reaching
for the restart button while iterating on keybind tests.

Parse the fresh config on a 4MB-stack worker (the parser stack-
overflows the UI thread's default 1MB, same constraint as the init
path in GhosttyApp::Create), then hand the result to the UI thread
where ghostty_app_tick lives. The swap step replaces the owned
config and frees the old one after ghostty_app_update_config has
applied the new pointer; soft reloads skip the parse and just
re-apply the existing config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three keybind attempts (ctrl+shift+f, ctrl+shift+alt+f, and
ctrl+shift+backslash) all failed to reach action_cb: the first
collides with the default start_search, the second is intercepted
by the WinUI Alt-menu accelerator before ghostty sees it, and the
third produced no observable action_cb call either despite no
known conflict. Until the dispatch path is understood the handler
is dead code; #if 0 guards it so a future test build can re-enable
the body verbatim once the keybind side is sorted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without a host handler the action_cb default (return false) gives no
visible breadcrumb when the generic renderer flips to its degraded
path — texture allocation failures and shader faults are exactly the
kind of issue that takes longer to diagnose if we only learn about
them via downstream symptoms (missing glyphs, blank frames).

Surface the health flip as a single stderr line; that's enough to
make it findable in the debugger output without committing to a
user-facing recovery UX yet, which is its own design problem.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The action only matters for apprts that keep a process alive after
the last window closes (macOS-style "quit after countdown"). Windows
exits as soon as the last top-level HWND is gone — CLOSE_WINDOW
already triggers Close() — so START / STOP don't need wiring on
either side.

Make the ignore explicit so a future libghostty audit doesn't flag
the action as falling through to the unhandled-action default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without a host handler the cursor stays parked over the terminal
while the user types — distracting against a dark theme and out of
step with the Vim / Helix / Kitty convention ghostty itself follows.

Toggle ShowCursor on HIDDEN <-> VISIBLE transitions, but gate on a
local bool: ShowCursor is a counter, so blindly calling it on every
event would let the counter walk arbitrarily far from zero and break
the next state change. Dispatching to the UI thread lines the call
up with the same thread WM_MOUSEMOVE drives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ghostty fires this after it has internally applied a new config —
it's a notification, not a "please apply this" request. If the host
ignores it the GhosttyApp-stored m_config drifts out of sync with
what libghostty is actually running, which would silently break the
RELOAD_CONFIG soft path (it would re-apply the stale copy) and lie
to any future host-side config query (theme color, padding, etc.).

Clone the incoming pointer because libghostty owns it, then swap on
the UI thread the same way the reload_config flow does so the free
of the previous config can't race a tick that's still reading it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without a handler the action falls through to action_cb's default
and the user's "reset window size" keybind silently does nothing,
which is worse than picking a reasonable default — at least one
makes the keybind feel alive.

Snap the HWND back to 1280x720 DIPs, DPI-scaled. The truly correct
restore target is whatever ghostty sent us via INITIAL_SIZE at
startup, but that handler isn't wired yet (#57 still has it open);
1280x720 matches the WinUI fresh-window default and lands around
80x24 cells at our typical font sizes, which is close enough to
"reset" semantics to be useful in the meantime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
i999rri and others added 10 commits May 22, 2026 01:21
The previous RESET_WINDOW_SIZE handler reset to a hardcoded 1280x720
DIPs, which means anyone with a config-driven startup size (window
width/height in cells, custom fonts that yield non-default pixel
dimensions) saw their window snap to the wrong footprint on reset.

Record the dimensions ghostty hands us via INITIAL_SIZE (already
expressed in physical pixels post cell-to-pixel math) and feed them
straight to SetWindowPos when reset fires. Keep the 1280x720 fallback
for the case where ghostty never sends INITIAL_SIZE — that path
matches the old behavior and the WinUI fresh-window default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ghostty fires this whenever the active font / DPI / config combination
changes the per-cell pixel dimensions. Without caching it locally,
any host-side logic that wants whole-cell rounding (snap-to-cell
window resize, split ratio quantization, etc.) has to round-trip
through libghostty every time the value is needed.

Stash width and height on MainWindow so future features can read
them straight off the view object. No behavior change today —
nothing yet reads m_cellWidth / m_cellHeight, the wiring is the
prerequisite for the features that will.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The terminal surface fills the available area without a scrollbar
widget, so the total / offset / visible-len ghostty sends here has
nowhere to render. Returning true keeps the action from falling
through to action_cb's unhandled-default branch and avoids any
future "unhandled action" log on the libghostty side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ghostty asks for an explicit repaint when it wants a frame out
sooner than the next natural tick — e.g., right after a config swap
that changes colors and the user wants to see them immediately, not
on the next keystroke. Without a handler that request gets dropped
and the frame lags behind until something else wakes the loop.

Route through the same UI-thread Tick path wakeup_cb uses, since
ghostty_app_tick already handles the "render the dirty surfaces"
job and is idempotent when nothing is dirty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a shell process exits unexpectedly during development the
default (return false) drops the notification, and the only signal
is the surface vanishing — no way to tell whether the shell quit
cleanly or crashed without instrumenting the host with logging.

Log the exit code and runtime to stderr so the next debugger run
puts that breadcrumb in front of the developer. An in-terminal
overlay would be the proper UI for users running with
confirm-close-surface=true, but that's a separate design problem
worth its own iteration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without a host handler the action falls through and the menu /
keybind silently does nothing. We don't ship a Sparkle-style
in-app updater on Windows yet — and writing one is its own project
— so the lowest-friction substitute is to point the user at the
releases page and let them grab the next build manually.

Use ShellExecuteW so the user's default browser opens without the
visible cmd / rundll32 flash that std.process.Child fallback would
produce, matching the same path OPEN_URL takes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OSC 9;4 progress reports (pnpm install, make, large copies, etc.)
were being dropped — the shell sent percent-complete updates and
nothing in the host surfaced them, so long-running operations gave
no visual signal of progress outside the terminal itself.

Map ghostty's state machine onto ITaskbarList3 so the percentage
shows up on the taskbar button, which is the Windows-native
surface for background-task progress. Cache the ITaskbarList3
instance on the UI thread (where COM is STA-initialized) because
CoCreateInstance + HrInit per OSC sequence would be wasteful.
Skip SetProgressValue when the state would make it meaningless
(NOPROGRESS, INDETERMINATE) or when ghostty reports no percentage
so the bar doesn't snap to 0% on bare state transitions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GHOSTTY_ACTION_READONLY, SECURE_INPUT, KEY_SEQUENCE, KEY_TABLE,
PROMPT_TITLE, PWD, and COMMAND_FINISHED all carry state libghostty
wants the host to surface visually — read-only banner, secure-input
padlock, pending-chord indicator, modal-table label, prompt-driven
title source, PWD breadcrumb, post-command summary. None of those
surfaces exist on our side yet and each is its own design pass.

Bundle the seven into a single ack branch so they don't fall through
to the unhandled-default and so the day-one wiring is in place when
each gets its actual UI in a later PR. Keeping them in one branch
keeps the action_cb body honest about which actions are "explicitly
no-op for now" versus "deliberately implemented".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without a handler the OSC bell sequences and the shell-side "your
build finished" notifications get dropped — the terminal beep
(RING_BELL) is the only signal that something completed in the
background. That's fine for a foreground session but loses tasks
that finish while the user has switched windows.

Register the AppNotificationManager on app launch so the manager
is ready before any surface starts dispatching, then on each
DESKTOP_NOTIFICATION build a two-line payload (title + body) and
hand it to Show. The register call is in a try/catch because some
MSIX activation paths reportedly throw on first run; failure mode
is "toasts silently no-op" rather than a crash. The Show call also
catches hresult_error so a transient OS refusal can't kill the
renderer thread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The default keybind for fullscreen sat idle without a handler —
pressing it produced no visible change, leaving the only path to
fullscreen the manual maximize button, which still draws the
caption / taskbar chrome.

Implement the borderless-fullscreen pattern: stash WINDOWPLACEMENT
and GWL_STYLE on entry, swap WS_OVERLAPPEDWINDOW off and resize to
the monitor rect, then restore both on exit. Storing
WINDOWPLACEMENT (not a plain RECT) preserves a maximised origin so
toggling FS from maximised returns to maximised. The macOS-specific
NON_NATIVE enum variants collapse to the same Win32 behavior.

Known limitation: our custom title bar sits in the XAML content
tree so it still draws on top of the swap chain in FS mode. Hiding
it is a separate follow-up — the window itself does fill the
monitor correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
i999rri and others added 3 commits May 22, 2026 01:42
The hand-cursor change on link hover was already wired through
MOUSE_SHAPE, but the URL itself stayed hidden — the user had to
either trust the link blindly or Ctrl+click to find out where it
points. That's a usability gap the rest of the desktop solves with
a hover tooltip.

Wire up a Win32 TOOLTIPS_CLASS popup in TRACK mode (manual
positioning suits the DComp surface, which has no XAML hit-test
geometry to attach a tooltip to). Lazy-init the window and the
text-backing wstring as function-local statics so we don't churn
the tooltip handle on every hover, and so TTM_UPDATETIPTEXT keeps
referencing live memory. Empty url hides the tooltip; non-empty
url positions it ~20px below-right of the cursor and shows the
text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without a handler ghostty's min/max constraints (window-width /
window-height capped in config, or shell-driven ratios) silently
don't apply on Windows — the user can drag the window to any size,
including below the minimum that breaks the rendered layout.

WM_GETMINMAXINFO is the canonical hook for enforcing track-size
limits in Win32, so install a SetWindowSubclass-based proc lazily
on the first SIZE_LIMIT (no cost for apps that never set one) and
let it clamp ptMin/MaxTrackSize against the cached limits. The
subclass is a static MainWindow method so it has private-member
access; dwRefData carries the MainWindow pointer so multiple
windows (when we get there) wouldn't collide on a single global.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…undo)

UNDO, REDO, the four SEARCH actions (START / END / TOTAL / SELECTED),
INSPECTOR and its RENDER_INSPECTOR companion, plus the three overlay
toggles (TAB_OVERVIEW, QUICK_TERMINAL, COMMAND_PALETTE) all dispatch
correctly from libghostty but have no surface on our side yet — each
is a multi-day feature (search bar, ImGui inspector port, three
distinct overlay UIs, host-side undo stack) that deserves its own PR.

Routing the eleven through a single ack today keeps the unhandled-
default branch quiet so a future libghostty audit doesn't flag them
as missing; the proper implementation work is still tracked under
#57's feature backlog. Grouped separately from the existing acked-
informational bundle (READONLY / SECURE_INPUT / KEY_SEQUENCE / etc.)
because the intent is different: those are state notifications, these
are feature toggles we'll genuinely wire up later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@i999rri i999rri changed the title v1.0 actions batch 1: lifecycle, window-state, title clipboard v1.0 ghostty action coverage: batches 1-4 (58/65 wired) May 21, 2026
i999rri and others added 6 commits May 22, 2026 03:55
Without these manifest entries AppNotificationManager.Register at
startup throws "No COM servers are registered for this app" from
deep inside WindowsAppRuntime's PushNotificationUtility, which our
try/catch in App::OnLaunched did swallow — but it lit up the
debugger every launch and left DESKTOP_NOTIFICATION silently
broken (Show would fail the same way per call).

Declare a CLSID via com:ComServer and tie it to the toast
activation extension. We don't implement a custom IActivator
factory — the AppNotifications projection handles activation
through its own internal mechanism; what the manifest needs to see
is the class registration plus the toastNotificationActivation
hint pointing at the same CLSID. After this change Register
returns success and Show actually surfaces toasts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After registering the COM activator, AppNotificationManager.Register
moved past the "No COM servers" error but immediately failed deeper
at PushNotificationUtility.h(256) with HRESULT 0x80070490
(ERROR_NOT_FOUND). Comparing against Microsoft's official packaged
toast sample, the difference was the missing Arguments attribute on
com:ExeServer: Windows uses that string to know how to launch the
exe in "I was activated by a toast click" mode, and the registration
flow validates its presence before completing.

Add Arguments="----AppNotificationActivated:" so Register can
finish. We still don't intercept that command-line in WinMain — a
toast click currently spawns a new Ghostty instance instead of
forwarding to the existing one — but that activation-routing UX is
a separate problem; what this change buys is "Register succeeds and
Show actually surfaces toasts."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original ShowCursor-based implementation looked correct in
isolation but did nothing in practice: WinUI 3 owns the cursor
through ProtectedCursor / InputSystemCursor, and its WM_SETCURSOR
handler resets the cursor on every mouse move, immediately undoing
the counter we decremented. The user reported the cursor not
hiding while typing, and that's the reason.

Install a WndProc subclass lazily on the first MOUSE_VISIBILITY
and short-circuit WM_SETCURSOR with SetCursor(NULL) while
m_cursorHidden is true. That's the only intercept point that beats
XAML's cursor restoration. Flipping m_cursorHidden back to false
lets the next mouse move — which is exactly the event that fires
the VISIBLE side of the action — fall through to DefSubclassProc
and restore the normal cursor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both actions came back as "doesn't work" during verification but
the test paths can fail silently — RESET_WINDOW_SIZE when the new
size happens to match the current size (no visible change),
SHOW_CHILD_EXITED when the stderr buffer flushes after the surface
teardown clobbers our chance to see it in the Output window.

For RESET_WINDOW_SIZE, log the recorded INITIAL_SIZE values and the
SetWindowPos result (including GetLastError on failure) so the next
test confirms whether the handler runs and whether SetWindowPos
actually succeeded.

For SHOW_CHILD_EXITED, mirror the stderr print through
OutputDebugStringA so the line shows up in VS's Debug output even
when the renderer thread's stderr flush is interrupted by the
surface closing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both came back as "not working" during verification with no signal
from inside the handler so far. The default for the gating config
keys is the most likely culprit (mouse-hide-while-typing defaults
to false, so MOUSE_VISIBILITY never fires unless the user opts in;
link-url defaults to true so MOUSE_OVER_LINK should fire, but the
hover detection might be silent if the URL substring doesn't
match).

Print the fire / payload size to both stderr and OutputDebugStringA
so the next test confirms whether the action reaches us at all.
Once we know that, we can pick the right next step — config nudge
versus a real bug in the host-side handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verification turned up a regression: clicking a URL with the tooltip
code in place exits the process with code 3 immediately after
~16 [mouse_over_link] fires, even though the OPEN_URL path was
shipping cleanly before this PR. The action handler itself is
correct (the diagnostic line proves it), so the suspect is the
tooltip popup interacting with the DComp surface's click routing —
TTM_TRACKACTIVATE / SetWindowPos against a top-level TOPMOST popup
on top of an HWND that's already a composition island isn't a
combo I trust without more digging.

#if 0 the tooltip body for now and ack the action with return true.
URL click goes back to working, and the unrelated MOUSE_VISIBILITY
/ RESET_WINDOW_SIZE diagnostics aren't affected. The tooltip work
is preserved verbatim for a follow-up PR once the click path is
understood.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verification turned up no [mouse_visibility] log after multiple
restart-and-type tries with mouse-hide-while-typing=true in config.
ghostty's firing condition in Surface.zig:2686 is event.action ==
.press && event.utf8.len > 0 && !self.mouse.hidden, and we already
know typing does reach ghostty (the codepoint resolver runs), so
the most plausible explanation is our ghostty_surface_key call site
not populating event.utf8 for printable keystrokes. Even if that
gets fixed, the host-side implementation uses a WM_SETCURSOR
subclass that loses to WinUI's ProtectedCursor on every mouse move
anyway.

#if 0 the handler the same way FLOAT_WINDOW is guarded so the body
survives verbatim for re-enable once #60 is unblocked. The header
members and the CursorVisibilitySubclassProc declaration stay in
place; nothing depends on them and rewinding the disable will be
a single guard removal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Those two prints were added to confirm whether the action handler
was running and whether SetWindowPos succeeded. Verification's done
(see PR #58 checklist), and this handler is otherwise quiet: in
normal use the prints would just be noise in the debug output.

Keep the [child_exited] + [renderer_health] stderr lines — those
are genuine crash-diagnosis breadcrumbs that pay off the next time
something abnormal happens. RESET_WINDOW_SIZE's success path
doesn't have the same recovery value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant