Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,54 @@ that can then be committed and pushed to fix the builds.

See the [Contributor's Guide](po/README_CONTRIBUTORS.md) for more details.

## macOS Help Book

The macOS app bundles an Apple Help Book (`Ghostty.help`) that backs the
Help menu and is generated at build time. The content comes from the same
sources as the man pages and website docs: the config option and keybind
action doc comments (via `help_strings`) and the markdown in
`src/build/mdgen/`.

The generator is `src/extra/help_book.zig`. All markup lives as plain
HTML/CSS/JS files in `src/extra/help_book/`: pages with dynamic content are
`{{placeholder}}` templates filled in by `template.zig`, and markdown is
rendered to HTML by `markdown.zig`. The build (`src/build/GhosttyResources.zig`)
runs the generator, indexes the pages with `hiutil`, and installs the bundle
to `zig-out/help/Ghostty.help`, which the Xcode project copies into the app
bundle.

The book's `CFBundleVersion` is set to the app version (the `ghostty
+version` string), which is how Help Viewer's cache daemon (`helpd`) knows
to pick up new content after an update.

When iterating on the help book locally, `helpd` will happily keep serving
stale state: the content cache is only refreshed when the book version
changes, and the Core Spotlight search donations are only updated
incrementally, so search results for removed pages stick around (and open
as "content not available"). After building, run the reset script to purge
Help Viewer state (via `hiutil -P`, which also quits the Help Viewer app
"Tips" and `helpd`) so everything is rebuilt from the fresh content:

```shell-session
zig build -Demit-macos-app=false
macos/build.nu help-book-reset
```

Afterwards rebuild and relaunch the app; the book is re-registered with
`helpd` at app launch. For content-only iteration you can skip Help Viewer
entirely and open the generated pages in a browser:

```shell-session
open zig-out/help/Ghostty.help/Contents/Resources/en.lproj/index.html
```

The generator has tests covering the bundle structure, the markdown
renderer, and the templates:

```shell-session
zig build test -Dtest-filter="help book" -Dtest-filter=markdown -Dtest-filter=template
```

## Checking for Memory Leaks

While Zig does an amazing job of finding and preventing memory leaks,
Expand Down
17 changes: 17 additions & 0 deletions macos/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,20 @@
application.
(5) When done, quit via:
`osascript -e 'tell application "<absolute path to build/Debug/Ghostty.app>" to quit'`

## Help Book

- The Apple Help Book (`Ghostty.help`) behind the Help menu is generated
by `zig build` into `zig-out/help/` and copied into the app bundle by
the Xcode build. Its sources are `src/extra/help_book.zig` and the
HTML/CSS/JS assets in `src/extra/help_book/`; see "macOS Help Book" in
[HACKING.md](../HACKING.md).
- After changing help book sources, rebuild (`zig build
-Demit-macos-app=false`) and run `macos/build.nu help-book-reset` to
purge Help Viewer state (helpd's content cache and its Core Spotlight
search donations; stale donations surface as "content not available"
search results), then rebuild the app and relaunch it to re-register
the book (which might take roughly 10s to finish).
- Stale sibling copies of Ghostty.app (`macos/build/`, Xcode DerivedData)
can hijack help book resolution since all copies register the same book
id; make sure the copy you launch is freshly built.
4 changes: 4 additions & 0 deletions macos/Ghostty-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
</array>
</dict>
</array>
<key>CFBundleHelpBookFolder</key>
<string>Ghostty.help</string>
<key>CFBundleHelpBookName</key>
<string>com.mitchellh.ghostty.help</string>
<key>GhosttyBuild</key>
<string></string>
<key>GhosttyCommit</key>
Expand Down
4 changes: 4 additions & 0 deletions macos/Ghostty.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
29C15B1D2CDC3B2900520DD4 /* bat in Resources */ = {isa = PBXBuildFile; fileRef = 29C15B1C2CDC3B2000520DD4 /* bat */; };
55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; };
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
5F0A11AB2E1B4A5F00C4D9E1 /* Ghostty.help in Resources */ = {isa = PBXBuildFile; fileRef = 5F0A11AA2E1B4A5F00C4D9E1 /* Ghostty.help */; };
819324582F24E78800A9ED8F /* DockTilePlugin.plugin in Copy DockTilePlugin */ = {isa = PBXBuildFile; fileRef = 8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
819324642F24FF2100A9ED8F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
8F3A9B4C2FA6B88000A18D13 /* Ghostty.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 8F3A9B4B2FA6B88000A18D13 /* Ghostty.sdef */; };
Expand Down Expand Up @@ -73,6 +74,7 @@
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = "<group>"; };
55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = "<group>"; };
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
5F0A11AA2E1B4A5F00C4D9E1 /* Ghostty.help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Ghostty.help; path = "../zig-out/help/Ghostty.help"; sourceTree = "<group>"; };
810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GhosttyUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DockTilePlugin.plugin; sourceTree = BUILT_PRODUCTS_DIR; };
8F3A9B4B2FA6B88000A18D13 /* Ghostty.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.sdef; path = Ghostty.sdef; sourceTree = "<group>"; };
Expand Down Expand Up @@ -325,6 +327,7 @@
29C15B1C2CDC3B2000520DD4 /* bat */,
A586167B2B7703CC009BDB1D /* fish */,
55154BDF2B33911F001622DC /* ghostty */,
5F0A11AA2E1B4A5F00C4D9E1 /* Ghostty.help */,
A546F1132D7B68D7003B11A0 /* locale */,
A5985CE52C33060F00C57AD3 /* man */,
9351BE8E2D22937F003B3499 /* nvim */,
Expand Down Expand Up @@ -577,6 +580,7 @@
A586167C2B7703CC009BDB1D /* fish in Resources */,
8F3A9B4C2FA6B88000A18D13 /* Ghostty.sdef in Resources */,
55154BE02B33911F001622DC /* ghostty in Resources */,
5F0A11AB2E1B4A5F00C4D9E1 /* Ghostty.help in Resources */,
A546F1142D7B68D7003B11A0 /* locale in Resources */,
A5985CE62C33060F00C57AD3 /* man in Resources */,
9351BE8E3D22937F003B3499 /* nvim in Resources */,
Expand Down
6 changes: 6 additions & 0 deletions macos/Sources/App/macOS/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ class AppDelegate: NSObject,
// This registers the Ghostty => Services menu to exist.
NSApp.servicesMenu = menuServices

// Register our Help Book. `helpd` only auto-registers apps installed in /Applications.
// Explicit registration makes Help work when running from the build directory or Xcode as well.
if !NSHelpManager.shared.registerBooks(in: Bundle.main) {
Ghostty.logger.warning("help book registration failed; Help falls back to online docs")
}

// Setup a local event monitor for app-level keyboard shortcuts. See
// localEventHandler for more info why.
_ = NSEvent.addLocalMonitorForEvents(
Expand Down
24 changes: 24 additions & 0 deletions macos/build.nu
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@
# Build the macOS Ghostty app using xcodebuild with a clean environment
# to avoid Nix shell interference (NIX_LDFLAGS, NIX_CFLAGS_COMPILE, etc.).

# Reset macOS Help Viewer state for Ghostty's help book (dev helper).
#
# helpd keeps two kinds of state: a per-book content cache (refreshed
# when the book's CFBundleVersion changes) and Core Spotlight search
# donations, which are only updated incrementally — search results for
# pages that no longer exist stick around and open as "content not
# available". `hiutil -P` quits Help Viewer (Tips) and helpd and purges
# the persisted state including the donations; the explicit removal
# below covers Ghostty's content cache in case parts of the purge fail.
# Relaunch the app afterwards so it re-registers the book.
def "main help-book-reset" [] {
try { ^/usr/bin/hiutil -P }

let cache_globs = [
$"($env.HOME)/Library/Group Containers/group.com.apple.helpviewer.content/Library/Caches/com.mitchellh.ghostty*.help"
$"($env.HOME)/Library/Caches/com.apple.helpd/Generated/com.mitchellh.ghostty*"
]
for pattern in $cache_globs {
glob $pattern | each {|path| rm -r -f $path } | ignore
}

print "Help Viewer state purged. Relaunch the app to re-register the book. The registration might take roughly 10s to finish."
}

def main [
--scheme: string = "Ghostty" # Xcode scheme (Ghostty, Ghostty-iOS, DockTilePlugin)
--configuration: string = "Debug" # Build configuration (Debug, Release, ReleaseLocal)
Expand Down
43 changes: 43 additions & 0 deletions src/build/GhosttyResources.zig
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty

deps.help_strings.addImport(build_data_exe);

// The help book embeds the same app version.
{
const options = b.addOptions();
try cfg.addOptions(options);
build_data_exe.root_module.addOptions("build_options", options);
}

// Terminfo
terminfo: {
const os_tag = cfg.target.result.os.tag;
Expand Down Expand Up @@ -125,6 +132,42 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty
try steps.append(b.allocator, &install_step.step);
}

// Apple Help Book: generated bundle plus the hiutil search indices.
// Installed outside share/ghostty so the app bundle doesn't pick it up
// twice (the Xcode project references both directories). The host
// check is because hiutil is a macOS host tool.
if (cfg.target.result.os.tag == .macos and
b.graph.host.result.os.tag == .macos)
{
const run = b.addRunArtifact(build_data_exe);
run.addArg("+help-book");
const book_dir = run.addOutputDirectoryArg("Ghostty.help");
const lproj = book_dir.path(b, "Contents/Resources/en.lproj");
Comment thread
bo2themax marked this conversation as resolved.

const lsm = RunStep.create(b, "hiutil lsm index");
lsm.addArgs(&.{ "/usr/bin/hiutil", "-I", "lsm", "-Caf" });
const lsm_out = lsm.addOutputFileArg("Ghostty.helpindex");
lsm.addDirectoryArg(lproj);

const cs = RunStep.create(b, "hiutil corespotlight index");
cs.addArgs(&.{ "/usr/bin/hiutil", "-I", "corespotlight", "-Caf" });
const cs_out = cs.addOutputFileArg("Ghostty.cshelpindex");
cs.addDirectoryArg(lproj);

// Assemble the final bundle (pages + indices) into one tree.
const wf = b.addWriteFiles();
_ = wf.addCopyDirectory(book_dir, "Ghostty.help", .{});
_ = wf.addCopyFile(lsm_out, "Ghostty.help/Contents/Resources/en.lproj/Ghostty.helpindex");
_ = wf.addCopyFile(cs_out, "Ghostty.help/Contents/Resources/en.lproj/Ghostty.cshelpindex");

const install_step = b.addInstallDirectory(.{
.source_dir = wf.getDirectory(),
.install_dir = .prefix,
.install_subdir = "help",
});
try steps.append(b.allocator, &install_step.step);
}

// Themes
if (cfg.emit_themes) {
if (b.lazyDependency("iterm2_themes", .{})) |upstream| {
Expand Down
2 changes: 1 addition & 1 deletion src/cli/help.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub const Options = struct {
};

/// The `help` command shows general help about Ghostty. Recognized as either
/// `-h, `--help`, or like other actions `+help`.
/// `-h`, `--help`, or like other actions `+help`.
///
/// You can also specify `--help` or `-h` along with any action such as
/// `+list-themes` to see help for a specific action.
Expand Down
16 changes: 8 additions & 8 deletions src/cli/ssh.zig
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,17 @@ pub const Options = struct {
///
/// Examples:
///
/// # Basic invocation using defaults:
/// ghostty +ssh user@example.com
/// # Basic invocation using defaults:
/// ghostty +ssh user@example.com
///
/// # Forward Ghostty env vars but skip the terminfo install:
/// ghostty +ssh --terminfo=false user@example.com
/// # Forward Ghostty env vars but skip the terminfo install:
/// ghostty +ssh --terminfo=false user@example.com
///
/// # `ssh` flags (short-form `-p`, etc.) pass through unchanged:
/// ghostty +ssh -p 2222 -i ~/.ssh/id_ed25519 user@example.com
/// # `ssh` flags (short-form `-p`, etc.) pass through unchanged:
/// ghostty +ssh -p 2222 -i ~/.ssh/id_ed25519 user@example.com
///
/// # Use `--` explicitly if your ssh args might collide with our flags:
/// ghostty +ssh -- --some-rare-ssh-arg user@example.com
/// # Use `--` explicitly if your ssh args might collide with our flags:
/// ghostty +ssh -- --some-rare-ssh-arg user@example.com
///
/// Pass `--verbose` to see what `+ssh` is doing. For cache inspection
/// and management, see `ghostty +ssh-cache`.
Expand Down
15 changes: 8 additions & 7 deletions src/cli/ssh_cache.zig
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ pub const Options = struct {
/// `--prune=6h`, `--prune=1y`.
///
/// Examples:
/// ghostty +ssh-cache # List all cached destinations
/// ghostty +ssh-cache user@example.com # Show that destination
/// ghostty +ssh-cache example.com # Show all users on that host
/// ghostty +ssh-cache --add=user@example.com # Manually add a destination
/// ghostty +ssh-cache --remove=user@example.com # Remove a destination
/// ghostty +ssh-cache --prune=30d # Remove entries older than 30 days
/// ghostty +ssh-cache --clear # Clear entire cache
///
/// ghostty +ssh-cache # List all cached destinations
/// ghostty +ssh-cache user@example.com # Show that destination
/// ghostty +ssh-cache example.com # Show all users on that host
/// ghostty +ssh-cache --add=user@example.com # Manually add a destination
/// ghostty +ssh-cache --remove=user@example.com # Remove a destination
/// ghostty +ssh-cache --prune=30d # Remove entries older than 30 days
/// ghostty +ssh-cache --clear # Clear entire cache
pub fn run(alloc_gpa: Allocator) !u8 {
var arena = std.heap.ArenaAllocator.init(alloc_gpa);
defer arena.deinit();
Expand Down
Loading
Loading