From e234123c172f9c259b9338fd505d0745d5011546 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:46:26 -0800 Subject: [PATCH] Include local changes --- .gitignore | 4 +- CLAUDE.md | 8 ++++ CONTRIBUTING.md | 98 ++++++++++++++++++++++++++++++++++++++++ Sources/TabManager.swift | 85 ++++++++++++++++++++++++++++++++++ scripts/setup.sh | 30 ++++++++++++ 5 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100755 scripts/setup.sh diff --git a/.gitignore b/.gitignore index a7b6b482..65162d86 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,8 @@ xcuserdata/ # Swift Package Manager .swiftpm/ -# GhosttyKit binary (rebuild from /tmp/ghostty with zig build) -GhosttyKit.xcframework/ +# GhosttyKit binary (built from ghostty submodule via scripts/setup.sh) +GhosttyKit.xcframework GhosttyKit.xcframework.bak-*/ # Release artifacts diff --git a/CLAUDE.md b/CLAUDE.md index 9874e01a..52cee48a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,13 @@ # cmuxterm agent notes +## Initial setup + +Run the setup script to initialize submodules and build GhosttyKit: + +```bash +./scripts/setup.sh +``` + ## Local dev After making code changes, always run the reload script to launch the Debug app: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..57199aa8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,98 @@ +# Contributing to cmuxterm + +## Prerequisites + +- macOS 14+ +- Xcode 15+ +- [Zig](https://ziglang.org/) (install via `brew install zig`) + +## Getting Started + +1. Clone the repository with submodules: + ```bash + git clone --recursive https://github.com/manaflow-ai/cmuxterm.git + cd cmuxterm + ``` + +2. Run the setup script: + ```bash + ./scripts/setup.sh + ``` + + This will: + - Initialize git submodules (ghostty, homebrew-cmuxterm) + - Build the GhosttyKit.xcframework from source + - Create the necessary symlinks + +3. Build and run the debug app: + ```bash + ./scripts/reload.sh + ``` + +## Development Scripts + +| Script | Description | +|--------|-------------| +| `./scripts/setup.sh` | One-time setup (submodules + xcframework) | +| `./scripts/reload.sh` | Build and launch Debug app | +| `./scripts/reloadp.sh` | Build and launch Release app | +| `./scripts/reload2.sh` | Reload both Debug and Release | +| `./scripts/rebuild.sh` | Clean rebuild | + +## Rebuilding GhosttyKit + +If you make changes to the ghostty submodule, rebuild the xcframework: + +```bash +cd ghostty +zig build -Demit-xcframework=true -Doptimize=ReleaseFast +``` + +## Running Tests + +### Basic tests (run on VM) + +```bash +ssh cmux-vm 'cd /Users/cmux/GhosttyTabs && xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination "platform=macOS" build && pkill -x "cmuxterm DEV" || true && APP=$(find /Users/cmux/Library/Developer/Xcode/DerivedData -path "*/Build/Products/Debug/cmuxterm DEV.app" -print -quit) && open "$APP" && for i in {1..20}; do [ -S /tmp/cmuxterm.sock ] && break; sleep 0.5; done && python3 tests/test_update_timing.py && python3 tests/test_signals_auto.py && python3 tests/test_ctrl_socket.py && python3 tests/test_notifications.py' +``` + +### UI tests (run on VM) + +```bash +ssh cmux-vm 'cd /Users/cmux/GhosttyTabs && xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination "platform=macOS" -only-testing:GhosttyTabsUITests test' +``` + +## Ghostty Submodule + +The `ghostty` submodule points to [manaflow-ai/ghostty](https://github.com/manaflow-ai/ghostty), a fork of the upstream Ghostty project. + +### Making changes to ghostty + +```bash +cd ghostty +git checkout -b my-feature +# make changes +git add . +git commit -m "Description of changes" +git push manaflow my-feature +``` + +### Keeping the fork updated + +```bash +cd ghostty +git fetch origin +git checkout main +git merge origin/main +git push manaflow main +``` + +Then update the parent repo: + +```bash +cd .. +git add ghostty +git commit -m "Update ghostty submodule" +``` + +See `docs/ghostty-fork.md` for details on fork changes and conflict notes. diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 0c8f872e..3ad07fc5 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -274,6 +274,9 @@ class TabManager: ObservableObject { let previousSurfaceId = focusedSurfaceId(for: previousTabId) { lastFocusedSurfaceByTab[previousTabId] = previousSurfaceId } + if !isNavigatingHistory, let selectedTabId { + recordTabInHistory(selectedTabId) + } DispatchQueue.main.async { [weak self] in self?.focusSelectedTabSurface(previousTabId: previousTabId) self?.updateWindowTitleForSelectedTab() @@ -287,6 +290,12 @@ class TabManager: ObservableObject { private var suppressFocusFlash = false private var lastFocusedSurfaceByTab: [UUID: UUID] = [:] + // Recent tab history for back/forward navigation (like browser history) + private var tabHistory: [UUID] = [] + private var historyIndex: Int = -1 + private var isNavigatingHistory = false + private let maxHistorySize = 50 + init() { addTab() observers.append(NotificationCenter.default.addObserver( @@ -687,6 +696,82 @@ class TabManager: ObservableObject { selectedTabId = lastTab.id } + // MARK: - Recent Tab History Navigation + + private func recordTabInHistory(_ tabId: UUID) { + // If we're not at the end of history, truncate forward history + if historyIndex < tabHistory.count - 1 { + tabHistory = Array(tabHistory.prefix(historyIndex + 1)) + } + + // Don't add duplicate consecutive entries + if tabHistory.last == tabId { + return + } + + tabHistory.append(tabId) + + // Trim history if it exceeds max size + if tabHistory.count > maxHistorySize { + tabHistory.removeFirst(tabHistory.count - maxHistorySize) + } + + historyIndex = tabHistory.count - 1 + } + + func navigateBack() { + guard historyIndex > 0 else { return } + + // Find the previous valid tab in history (skip closed tabs) + var targetIndex = historyIndex - 1 + while targetIndex >= 0 { + let tabId = tabHistory[targetIndex] + if tabs.contains(where: { $0.id == tabId }) { + isNavigatingHistory = true + historyIndex = targetIndex + selectedTabId = tabId + isNavigatingHistory = false + return + } + // Remove closed tab from history + tabHistory.remove(at: targetIndex) + historyIndex -= 1 + targetIndex -= 1 + } + } + + func navigateForward() { + guard historyIndex < tabHistory.count - 1 else { return } + + // Find the next valid tab in history (skip closed tabs) + let targetIndex = historyIndex + 1 + while targetIndex < tabHistory.count { + let tabId = tabHistory[targetIndex] + if tabs.contains(where: { $0.id == tabId }) { + isNavigatingHistory = true + historyIndex = targetIndex + selectedTabId = tabId + isNavigatingHistory = false + return + } + // Remove closed tab from history + tabHistory.remove(at: targetIndex) + // Don't increment targetIndex since we removed the element + } + } + + var canNavigateBack: Bool { + historyIndex > 0 && tabHistory.prefix(historyIndex).contains { tabId in + tabs.contains { $0.id == tabId } + } + } + + var canNavigateForward: Bool { + historyIndex < tabHistory.count - 1 && tabHistory.suffix(from: historyIndex + 1).contains { tabId in + tabs.contains { $0.id == tabId } + } + } + func newSplit(tabId: UUID, surfaceId: UUID, direction: SplitTree.NewDirection) -> Bool { guard let tab = tabs.first(where: { $0.id == tabId }) else { return false } return tab.newSplit(from: surfaceId, direction: direction) != nil diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 00000000..e137a8bd --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +cd "$PROJECT_DIR" + +echo "==> Initializing submodules..." +git submodule update --init --recursive + +echo "==> Checking for zig..." +if ! command -v zig &> /dev/null; then + echo "Error: zig is not installed." + echo "Install via: brew install zig" + exit 1 +fi + +echo "==> Building GhosttyKit.xcframework (this may take a few minutes)..." +cd ghostty +zig build -Demit-xcframework=true -Doptimize=ReleaseFast +cd "$PROJECT_DIR" + +echo "==> Creating symlink for GhosttyKit.xcframework..." +ln -sf ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework + +echo "==> Setup complete!" +echo "" +echo "You can now build and run the app:" +echo " ./scripts/reload.sh"