Skip to content

Comments

Add Initial Dir field to SSH bookmarks#292

Open
nathandale wants to merge 12 commits intofitztrev:masterfrom
nathandale:master
Open

Add Initial Dir field to SSH bookmarks#292
nathandale wants to merge 12 commits intofitztrev:masterfrom
nathandale:master

Conversation

@nathandale
Copy link

Summary

  • Adds an Initial Directory field to each server in the SSH Manager
  • When set, connects with ssh host -t 'cd <dir> && exec $SHELL -l' so you land in the specified directory with a full login shell
  • Stored as initial_directory in ~/.shuttle.json; leaving it blank preserves existing behavior

Test plan

  • Open Manager, select a server, set Initial Dir to ~/bin, save
  • Click the bookmark — confirm SSH session opens in ~/bin
  • Clear the field, save — confirm normal SSH connection (no -t flag)
  • Verify ~/.shuttle.json contains / omits initial_directory correctly

🤖 Generated with Claude Code

nathandale and others added 12 commits February 19, 2026 16:37
… via NSTask

- Update deployment target to macOS 12.0 across all build configs
- Add ARCHS=$(ARCHS_STANDARD) for arm64 + x86_64 Universal Binary
- Remove all AppleScript .scpt files and infrastructure
- Replace openHost: with NSTask-based terminal dispatch:
  - "ghostty" → ghostty -e <cmd>
  - "iterm" / "iterm2" → osascript tell iTerm
  - "terminal" (default) → osascript tell Terminal.app
- Set terminal via "terminal" key in ~/.shuttle.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace all AppleScript/.scpt infrastructure with NSTask-based terminal dispatch
  supporting Ghostty, iTerm2 (osascript one-liner), and Terminal.app
- Universal Binary: ARCHS=$(ARCHS_STANDARD), deployment target 12.0
- New JSON schema: servers[] + categories[] replaces recursive hosts tree
- ServerManagerWindowController: programmatic Contacts-style manager window
  with NSOutlineView sidebar (category group headers + server rows),
  detail form pane (name, hostname, user, port, SSH key picker, category,
  terminal overrides), Save/Delete/Add Category actions persisted to JSON
- LAN SSH scanner: async /24 port-22 sweep via GCD, shown as Local Network
  submenu with Scan Now action and age display
- Manager... item inserted into Settings submenu at runtime

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Import fixes:
- Replace deprecated NSOKButton with NSModalResponseOK
- Call loadMenu after successful import so menu refreshes immediately
- Properly clear stale backup before move; restore backup on copy failure
- Add allowed file type filter (json) to open panel

Export fixes:
- Replace deprecated NSFileHandlingPanelOKButton with NSModalResponseOK
- Set default filename to shuttle.json in save panel
- Remove pre-existing file at destination before copy (prevents silent failure)

LAN scanner hostname support:
- IP scan now calls reverseResolve: (getnameinfo) for each discovered host,
  storing {ip, hostname} dicts instead of bare IP strings
- Add NSNetServiceBrowser scanning _ssh._tcp. on local. domain for
  Bonjour/mDNS hosts (finds .local hostnames without needing an IP sweep)
- rebuildLanSubmenu merges both sources, deduplicates by hostname,
  shows "hostname (ip)" label and SSHes to hostname when available

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NSVisualEffectView sidebar with Sidebar material (translucent, auto dark mode)
- NSOutlineView with source-list highlight style; group headers in uppercase
  semibold 11pt secondary-color text, server rows at 28pt row height
- NSSegmentedControl (+/−) replaces individual Add/Remove buttons; lightweight
  font for +/− glyphs
- NSGridView form with right-aligned 12pt secondary-color labels and 13pt fields,
  perfectly pixel-aligned via column width/xPlacement settings
- Name field uses 15pt font; hostname placeholder updated to include .local hint
- Save button set as key equivalent (Return = blue/default); Delete button uses
  hasDestructiveAction for system styling
- Form centered (slightly above vertical midpoint) inside resizable detail pane
- Wipe legacy hosts from ~/.shuttle.json, reset to clean servers/categories schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Terminal.app double-window fix: the previous one-liner caused Terminal to
open its startup window when not running, then do script created a second
window. New approach: activate Terminal first (so its startup window
exists), then run do script in front window — always results in exactly
one terminal session.

LAN scan sort: replaced hostname alphabetical sort with numeric sort by
last octet of IP address. Bonjour-only entries (no IP) sort to the end.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each scanned host now shows a submenu instead of a direct connect action:
  - Connect — launches SSH as before
  - Save to Address Book… — prompts for a display name (pre-filled with
    the detected hostname) and a category picker, then writes the host
    into servers[], persists to JSON, and reloads the menu

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
buildMenuFromServers now encodes the server's terminal key as a 6th
field in the representedObject string. openHost: reads it and uses it
if set, falling back to the global terminalPref, then to "terminal".
This means a server with "terminal":"ghostty" will open in Ghostty
even if the global setting says otherwise.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- installedTerminals: queries NSWorkspace URLForApplicationWithBundleIdentifier:
  for Terminal, iTerm2, Ghostty, Alacritty, kitty, Warp, Hyper, Rio —
  only returns what is actually installed on the machine
- executableForBundleID: resolves the real binary path via NSBundle,
  no more hardcoded /Applications/Ghostty.app/... paths
- openHost: dispatches per terminal using dynamic binary lookup; Warp
  uses its URL scheme; unknown terminals fall back to Terminal.app
- Manager terminal popup rebuilt from installedTerminals at open time;
  stores/restores the JSON identifier (e.g. "ghostty") not the display name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two root causes of the launch failure:

1. Tilde not expanded: identity_file stored as ~/.ssh/id_ed25519 was
   passed literally to Ghostty which doesn't shell-expand it.
   Fix: call stringByExpandingTildeInPath in sshCommandForServer:.

2. Command passed as single string: ghostty -e expects separate argv
   entries, not one blob. Ghostty was receiving the whole ssh command
   as a single argument and failing with "No such file or directory".
   Fix: use sh -c <sshCmd> so the shell interprets it correctly.

3. App Management privacy prompt: NSTask accessing another app's
   internal binary (Ghostty.app/Contents/MacOS/ghostty) triggers
   macOS Sequoia's App Management entitlement check on every launch.
   Fix: replace NSTask with NSWorkspace openApplicationAtURL:configuration:
   which is the proper API for launching apps — macOS does not flag it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers: address book, Manager window, multi-terminal detection and
dispatch, per-server SSH key and terminal override, LAN scanner
(Bonjour + port sweep + reverse DNS), import/export, full JSON schema
reference, build instructions, and a comparison table vs the original.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
openApplicationAtURL:configuration: only passes arguments when launching
a new process; if the terminal was already running macOS silently focused
it and dropped the SSH command. Setting createsNewApplicationInstance=YES
forces a fresh process each time so every bookmark click opens a new
terminal window with its own SSH session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an optional "Initial Directory" field to each server in the Manager.
When set, the SSH command becomes:
  ssh host -t 'cd <dir> && exec $SHELL -l'

The -t flag forces a pseudo-TTY and exec $SHELL -l replaces the subshell
with a proper login shell, preserving prompt and aliases.

Stored as "initial_directory" in ~/.shuttle.json.

Co-Authored-By: Claude Sonnet 4.6 <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