Our existing logic already ensured that setupFeatures() was always
called, but that was happening from two code paths: explicitly when
shell integration is .none and implicitly via setup().
We can simplify this by always calling setupFeatures() once, outside of
the (automatic) shell integration path.
There's one small behavioral change: we previously didn't set up shell
features in the automatic shell integration path if we didn't have a
resources directory (as a side effect). Resources are required for shell
integrations, but we don't need them to export GHOSTTY_SHELL_FEATURES,
which could potentially still be useful on its on.
This adds some new special case handling for key sequences when an
unbound keyboard input is received. If the current keybinding set scope
(i.e. active tables) has a `catch_all` binding that would `ignore`
input, then the entire key sequence is dropped.
Normally, when an unbound key sequence is received, Ghostty encodes it
and sends it to the running program.
This special behavior is useful for things like Vim mode which have
`g>g` to scroll to top, and a `catch_all=ignore` to drop all other
input. If the user presses `g>h` (unbound), you don't want `gh` to show
up in your terminal input, because the `catch_all=ignore` indicates that
the user wants that mode to drop all unbound input.
This adds some new special case handling for key sequences when an
unbound keyboard input is received. If the current keybinding set scope
(i.e. active tables) has a `catch_all` binding that would `ignore`
input, then the entire key sequence is dropped.
Normally, when an unbound key sequence is received, Ghostty encodes it
and sends it to the running program.
This special behavior is useful for things like Vim mode which have `g>g`
to scroll to top, and a `catch_all=ignore` to drop all other input. If
the user presses `g>h` (unbound), you don't want `gh` to show up in your
terminal input, because the `catch_all=ignore` indicates that the user
wants that mode to drop all unbound input.
Fixes#2127
This adds a UI similar to macOS to show the current state of key
sequences and/or key tables.
https://github.com/user-attachments/assets/4399d2af-a88c-4b70-922b-7727dc4d2053
**AI disclosure:** AI was used for various things, but I did write most
of the code myself, especially around the memory management of
properties since agents can't get that quite right yet. 😄
Our existing logic already ensured that setupFeatures() was always
called, but that was happening from two code paths: explicitly when
shell integration is .none and implicitly via setup().
We can simplify this by always calling setupFeatures() once, outside of
the (automatic) shell integration path.
There's one small behavioral change: we previously didn't set up shell
features in the automatic shell integration path if we didn't have a
resources directory (as a side effect). Resources are required for shell
integrations, but we don't need them to export GHOSTTY_SHELL_FEATURES,
which could potentially still be useful on its on.
Add a unit test to prevent regressions in our failure state.
For example, we always want to set GHOSTTY_SHELL_FEATURES, even if
automatic shell integration fails, because it's also used for manual
shell integration (e.g. #5048).
Add a unit test to prevent regressions in our failure state.
For example, we always want to set GHOSTTY_SHELL_FEATURES, even if
automatic shell integration fails, because it's also used for manual
shell integration (e.g. #5048).
> **Note**: This is a re-submission of #9952, which was closed in favor
of #9975. However, as noted in my [comment on
#9975](https://github.com/ghostty-org/ghostty/pull/9975#issuecomment-3677916608),
the issue still persists.
## Summary
- Fix `window-position-x/y` not being applied when `window-width/height`
is also configured
## Problem
When both `window-position-x/y` and `window-width/height` are
configured, the window position was not being applied correctly. The
window would appear near the center of the screen instead of the
specified position.
This worked correctly in v1.2.3 but regressed afterwards.
## Root Cause
This is a regression introduced in c75bade89 (#9747).
The commit refactored the default size logic from a computed `NSRect?`
property to a `DefaultSize` enum with `.frame` and
`.contentIntrinsicSize` cases.
**Before (working):**
```swift
private var defaultSize: NSRect? {
// ... calculate frame ...
return adjustForWindowPosition(frame: frame, on: screen) // ← position was applied
}
```
**After (broken):**
```swift
enum DefaultSize {
case frame(NSRect)
case contentIntrinsicSize
func apply(to window: NSWindow) {
case .contentIntrinsicSize:
window.setContentSize(size)
window.constrainToScreen()
// ← adjustForWindowPosition call was lost
}
}
```
When `window-width/height` is configured, the `.contentIntrinsicSize`
case is used. This case only called `setContentSize` and
`constrainToScreen`, but did not apply the window position adjustment.
## Why This Fix is Correct
`DefaultSize.apply()` is intentionally **not** responsible for
position—it only handles **size**:
1. `apply()` is also called from `returnToDefaultSize(_:)` menu action
2. When user triggers "Return to Default Size", only the **size** should
reset—**not the position**
3. If we added position logic inside `apply()`, the window would
unexpectedly jump back to its initial position
Therefore, position adjustment belongs **outside** of `apply()`,
specifically during initial window setup in `windowDidLoad`.
## Fix
Call `adjustForWindowPosition` after applying the content intrinsic size
to ensure the window position is correctly set during initial window
creation.
Closes#9995
**Problem**: `ghostty +list-keybinds` doesn't display bindings from key
tables.
**Solution**: Iterate over `keybinds.tables` and collect bindings from
each table, prefixing them with `table_name/` in the output. Bindings
are sorted with default bindings first, then table bindings grouped
alphabetically by table name.
https://github.com/user-attachments/assets/de73b66a-fc23-4913-a083-7a4aa992c5ec
> _My theme is "Monokai Pro Octagon" which makes `blue` → `orange`_
<details>
<summary>Default theme output</summary>
<img width="942" height="824" alt="Screenshot 2025-12-23 at 08 34 10"
src="https://github.com/user-attachments/assets/21b3a746-930c-4795-b538-f92455cf5fa5"
/>
</details>
I chose `8` (for `table_style`) since table names are secondary (thus,
dim gray color) and color is distinct without overpowering. Obviously,
open to whatever is preferred.
**Testing**: Manual verification with configs containing multiple key
tables, chained bindings within tables, and mixed default/table
bindings.
---
> **AI Disclosure**: Claude Code for research and review. All code typed
by me.
- Display keybindings grouped by their source table, with table name as prefix
- Sort default bindings before table bindings, maintaining visual hierarchy
- Support keybindings defined in key tables alongside default bindings
- Enable users to discover all available keybindings across the entire config
Fixes#10020
This improves parsing key tables so that the following edge cases are
now handled correctly, which were regressions from prior tip behavior:
- `/=action`
- `ctrl+/=action`
- `table//=action` (valid to bind `/` in a table)
- `table/a>//=action` (valid to bind a table with a sequence)
Fixes#10020
This improves parsing key tables so that the following edge cases
are now handled correctly, which were regressions from prior tip
behavior:
- `/=action`
- `ctrl+/=action`
- `table//=action` (valid to bind `/` in a table)
- `table/a>//=action` (valid to bind a table with a sequence)
Fixes#9961
This implements chained keybinds as described in #9961.
```
keybind = ctrl+shift+f=toggle_fullscreen
keybind = chain=toggle_window_decorations
```
These work with tables and sequences. For tables, the chain is unique
per table, so the following works:
```
keybind = foo/ctrl+shift+f=toggle_fullscreen
keybind = foo/chain=toggle_window_decorations
```
For sequences, it applies to the most recent sequence:
```
keybind = ctrl+b>f=toggle_fullscreen
keybind = chain=toggle_window_decorations
```
## TODO
Some limitations to resolve in future PRs (make an issue) or commits:
- [x] GTK: Global shortcuts cannot be chained: #10019
- [x] Inspector doesn't show chained keybinds
- [x] `+list-keybinds` doesn't show chains
**AI disclosure:** AI helped write tests, but everything else was
organic. AI did surprisingly bad at trying to implement this feature, so
I threw all of its work away! 😄
When window-width/height is configured, the window size is set via
setContentSize in windowDidLoad. However, window-position-x/y was not
being applied after this resize, causing the window to appear at an
incorrect position.
This was a regression introduced in c75bade89 which refactored the
default size logic from a computed NSRect property to a DefaultSize
enum. The original code called adjustForWindowPosition after calculating
the frame, but this was lost during the refactoring.
Fixes the issue by calling adjustForWindowPosition after applying
contentIntrinsicSize to ensure window position is correctly set.