From 8320d5805a06f9722d13f0dbbecbdc137c3f8a03 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Mon, 26 Jan 2026 03:05:03 -0800 Subject: [PATCH] Rename to cmux and add About panel --- CLAUDE.md | 32 +++++----- GhosttyTabs.xcodeproj/project.pbxproj | 38 ++++++----- .../{GhosttyTabs.xcscheme => cmux.xcscheme} | 8 +-- Package.swift | 6 +- README.md | 6 +- Sources/AppDelegate.swift | 7 ++ Sources/GhosttyTerminalView.swift | 12 +++- Sources/TerminalController.swift | 2 +- Sources/TerminalNotificationStore.swift | 4 +- .../{GhosttyTabsApp.swift => cmuxApp.swift} | 24 ++++++- ...ridging-Header.h => cmux-Bridging-Header.h | 0 scripts/rebuild.sh | 8 +-- tests/{ghosttytabs.py => cmux.py} | 64 +++++++++---------- tests/test_app_keystrokes.sh | 20 +++--- tests/test_ctrl_enter_keybind.py | 22 +++---- tests/test_ctrl_interactive.py | 8 +-- tests/test_ctrl_signals.sh | 2 +- tests/test_ctrl_socket.py | 26 ++++---- 18 files changed, 164 insertions(+), 125 deletions(-) rename GhosttyTabs.xcodeproj/xcshareddata/xcschemes/{GhosttyTabs.xcscheme => cmux.xcscheme} (79%) rename Sources/{GhosttyTabsApp.swift => cmuxApp.swift} (78%) rename GhosttyTabs-Bridging-Header.h => cmux-Bridging-Header.h (100%) rename tests/{ghosttytabs.py => cmux.py} (84%) diff --git a/CLAUDE.md b/CLAUDE.md index 1f4bdb46..7b6b24b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# GhosttyTabs +# cmux A macOS terminal app with vertical tabs, using libghostty (GhosttyKit.xcframework) for terminal emulation. @@ -12,9 +12,9 @@ A macOS terminal app with vertical tabs, using libghostty (GhosttyKit.xcframewor ### Build and launch (Release) ```bash cd /Users/lawrencechen/fun/cmux-terminal/GhosttyTabs -pkill -9 GhosttyTabs 2>/dev/null -xcodebuild -scheme GhosttyTabs -configuration Release build -open ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-cbjivvtpirygxbbgqlpdpiiyjnwh/Build/Products/Release/GhosttyTabs.app +pkill -9 cmux 2>/dev/null +xcodebuild -scheme cmux -configuration Release build +open ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-cbjivvtpirygxbbgqlpdpiiyjnwh/Build/Products/Release/cmux.app ``` ### Rebuild libghostty (optimized) @@ -26,18 +26,18 @@ cp -R /tmp/ghostty/macos/GhosttyKit.xcframework /Users/lawrencechen/fun/cmux-ter ### Project structure - `Sources/` - Swift source files - - `GhosttyTabsApp.swift` - App entry point with keyboard shortcuts + - `cmuxApp.swift` - App entry point with keyboard shortcuts - `ContentView.swift` - Main UI with vertical tabs sidebar - `TabManager.swift` - Tab state management - `GhosttyTerminalView.swift` - libghostty terminal integration - `GhosttyConfig.swift` - Ghostty config parser - `TerminalController.swift` - Unix socket server for programmatic control - `tests/` - Test files and utilities - - `ghosttytabs.py` - Python client library for socket API + - `cmux.py` - Python client library for socket API - `test_ctrl_socket.py` - Main automated test suite - `GhosttyKit.xcframework/` - libghostty static library (gitignored, rebuild from /tmp/ghostty) - `ghostty.h` - Ghostty C API header -- `GhosttyTabs-Bridging-Header.h` - Swift bridging header +- `cmux-Bridging-Header.h` - Swift bridging header ### Keyboard Shortcuts - `Cmd+T` / `Cmd+N` / `Ctrl+Shift+`` - New tab @@ -54,7 +54,7 @@ Reads user's Ghostty config from: ### Unix Socket Control API -GhosttyTabs exposes a Unix socket at `/tmp/ghosttytabs.sock` for programmatic control and automated testing. The socket is created when the app launches. +cmux exposes a Unix socket at `/tmp/cmux.sock` for programmatic control and automated testing. The socket is created when the app launches. #### Socket Commands @@ -84,12 +84,12 @@ Use `\n` for Enter (carriage return), `\t` for tab, `\r` for raw CR. ### Python Client Library -Located at `tests/ghosttytabs.py`: +Located at `tests/cmux.py`: ```python -from ghosttytabs import GhosttyTabs +from cmux import cmux -with GhosttyTabs() as client: +with cmux() as client: # Send text with Enter client.send("echo hello\n") @@ -108,16 +108,16 @@ with GhosttyTabs() as client: ```bash # Build and launch the app first -pkill -9 GhosttyTabs 2>/dev/null -xcodebuild -scheme GhosttyTabs -configuration Release build -open ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-cbjivvtpirygxbbgqlpdpiiyjnwh/Build/Products/Release/GhosttyTabs.app +pkill -9 cmux 2>/dev/null +xcodebuild -scheme cmux -configuration Release build +open ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-cbjivvtpirygxbbgqlpdpiiyjnwh/Build/Products/Release/cmux.app sleep 3 # Run the main test suite (tests Ctrl+C, Ctrl+D) python3 tests/test_ctrl_socket.py # Interactive CLI for manual testing -python3 tests/ghosttytabs.py +python3 tests/cmux.py ``` ### Writing New Tests @@ -148,7 +148,7 @@ python3 tests/ghosttytabs.py ### Test Files -- `tests/ghosttytabs.py` - Python client library for socket API +- `tests/cmux.py` - Python client library for socket API - `tests/test_ctrl_socket.py` - Automated Ctrl+C/D test suite (main tests) - `tests/test_signals_auto.py` - PTY-based signal tests (standalone) - `tests/test_ctrl_interactive.py` - Interactive manual tests diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 6990c194..4eeba9a1 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - A5001001 /* GhosttyTabsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001011 /* GhosttyTabsApp.swift */; }; + A5001001 /* cmuxApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001011 /* cmuxApp.swift */; }; A5001002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001012 /* ContentView.swift */; }; A5001003 /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001013 /* TabManager.swift */; }; A5001004 /* GhosttyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001014 /* GhosttyConfig.swift */; }; @@ -49,16 +49,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - A5001000 /* GhosttyTabs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GhosttyTabs.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A5001000 /* cmux.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = cmux.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7E7E6EF344A568AC7FEE3715 /* GhosttyTabsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GhosttyTabsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A5001011 /* GhosttyTabsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyTabsApp.swift; sourceTree = ""; }; + A5001011 /* cmuxApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = cmuxApp.swift; sourceTree = ""; }; A5001012 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; A5001013 /* TabManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = ""; }; A5001014 /* GhosttyConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyConfig.swift; sourceTree = ""; }; A5001015 /* GhosttyTerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyTerminalView.swift; sourceTree = ""; }; A5001016 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; A5001017 /* ghostty.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ghostty.h; sourceTree = ""; }; - A5001018 /* GhosttyTabs-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GhosttyTabs-Bridging-Header.h"; sourceTree = ""; }; + A5001018 /* cmux-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "cmux-Bridging-Header.h"; sourceTree = ""; }; A5001019 /* TerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalController.swift; sourceTree = ""; }; A5001090 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A5001091 /* NotificationsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPage.swift; sourceTree = ""; }; @@ -117,7 +117,7 @@ A5001101 /* Assets.xcassets */, A5001016 /* GhosttyKit.xcframework */, A5001017 /* ghostty.h */, - A5001018 /* GhosttyTabs-Bridging-Header.h */, + A5001018 /* cmux-Bridging-Header.h */, 3196C9C2D01F054C1D3385DD /* GhosttyTabsUITests */, A5001042 /* Products */, ); @@ -126,7 +126,7 @@ A5001041 /* Sources */ = { isa = PBXGroup; children = ( - A5001011 /* GhosttyTabsApp.swift */, + A5001011 /* cmuxApp.swift */, A5001012 /* ContentView.swift */, A5001013 /* TabManager.swift */, A5001014 /* GhosttyConfig.swift */, @@ -153,7 +153,7 @@ A5001042 /* Products */ = { isa = PBXGroup; children = ( - A5001000 /* GhosttyTabs.app */, + A5001000 /* cmux.app */, 7E7E6EF344A568AC7FEE3715 /* GhosttyTabsUITests.xctest */, ); name = Products; @@ -185,7 +185,7 @@ ); name = GhosttyTabs; productName = GhosttyTabs; - productReference = A5001000 /* GhosttyTabs.app */; + productReference = A5001000 /* cmux.app */; productType = "com.apple.product-type.application"; }; CB450DF0F0B3839599082C4D /* GhosttyTabsUITests */ = { @@ -240,7 +240,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - A5001001 /* GhosttyTabsApp.swift in Sources */, + A5001001 /* cmuxApp.swift in Sources */, A5001002 /* ContentView.swift in Sources */, A5001003 /* TabManager.swift in Sources */, A5001004 /* GhosttyConfig.swift in Sources */, @@ -346,6 +346,8 @@ DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = cmux; + INFOPLIST_KEY_CFBundleName = cmux; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = ""; @@ -368,10 +370,10 @@ "-framework", Carbon, ); - PRODUCT_BUNDLE_IDENTIFIER = com.ghosttytabs.app; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = com.cmux.app; + PRODUCT_NAME = cmux; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "GhosttyTabs-Bridging-Header.h"; + SWIFT_OBJC_BRIDGING_HEADER = "cmux-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Debug; @@ -387,6 +389,8 @@ DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = cmux; + INFOPLIST_KEY_CFBundleName = cmux; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSMainStoryboardFile = ""; @@ -410,10 +414,10 @@ Carbon, ); ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.ghosttytabs.app; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = com.cmux.app; + PRODUCT_NAME = cmux; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "GhosttyTabs-Bridging-Header.h"; + SWIFT_OBJC_BRIDGING_HEADER = "cmux-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Release; @@ -427,7 +431,7 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.ghosttytabs.appuitests; + PRODUCT_BUNDLE_IDENTIFIER = com.cmux.appuitests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_VERSION = 5.0; @@ -444,7 +448,7 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.ghosttytabs.appuitests; + PRODUCT_BUNDLE_IDENTIFIER = com.cmux.appuitests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_TARGET_NAME = GhosttyTabs; diff --git a/GhosttyTabs.xcodeproj/xcshareddata/xcschemes/GhosttyTabs.xcscheme b/GhosttyTabs.xcodeproj/xcshareddata/xcschemes/cmux.xcscheme similarity index 79% rename from GhosttyTabs.xcodeproj/xcshareddata/xcschemes/GhosttyTabs.xcscheme rename to GhosttyTabs.xcodeproj/xcshareddata/xcschemes/cmux.xcscheme index 99a6ecb3..f326bfd8 100644 --- a/GhosttyTabs.xcodeproj/xcshareddata/xcschemes/GhosttyTabs.xcscheme +++ b/GhosttyTabs.xcodeproj/xcshareddata/xcschemes/cmux.xcscheme @@ -3,7 +3,7 @@ - + @@ -14,17 +14,17 @@ - + - + - + diff --git a/Package.swift b/Package.swift index a02b7770..ccbc6ab2 100644 --- a/Package.swift +++ b/Package.swift @@ -2,19 +2,19 @@ import PackageDescription let package = Package( - name: "GhosttyTabs", + name: "cmux", platforms: [ .macOS(.v13) ], products: [ - .executable(name: "GhosttyTabs", targets: ["GhosttyTabs"]) + .executable(name: "cmux", targets: ["cmux"]) ], dependencies: [ .package(url: "https://github.com/migueldeicaza/SwiftTerm.git", from: "1.2.0") ], targets: [ .executableTarget( - name: "GhosttyTabs", + name: "cmux", dependencies: ["SwiftTerm"], path: "Sources" ) diff --git a/README.md b/README.md index f1e49300..55230ba2 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -# GhosttyTabs +# cmux Vertical tabs for Ghostty on macOS, built on libghostty. -[![Download macOS](https://img.shields.io/badge/Download-macOS-1b5fdd?style=for-the-badge&logo=apple)](releases/latest/download/GhosttyTabs-macos.zip) +[![Download macOS](https://img.shields.io/badge/Download-macOS-1b5fdd?style=for-the-badge&logo=apple)](releases/latest/download/cmux-macos.zip) ## Releases Tag a version like `v0.1.0` and push it to trigger the GitHub Actions release workflow. The workflow builds `GhosttyKit.xcframework`, builds the Release app, signs, notarizes, -staples, and uploads `GhosttyTabs-macos.zip` to the release. +staples, and uploads `cmux-macos.zip` to the release. ### Required GitHub secrets diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index a326d847..304a7a4c 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -17,6 +17,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent func applicationDidFinishLaunching(_ notification: Notification) { registerLaunchServicesBundle() enforceSingleInstance() + ensureApplicationIcon() observeDuplicateLaunches() configureUserNotifications() } @@ -46,6 +47,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent center.delegate = self } + private func ensureApplicationIcon() { + if let icon = NSImage(named: NSImage.applicationIconName) { + NSApplication.shared.applicationIconImage = icon + } + } + private func registerLaunchServicesBundle() { let bundleURL = Bundle.main.bundleURL.standardizedFileURL let registerStatus = LSRegisterURL(bundleURL as CFURL, true) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 1b7c2a73..2dd79c6c 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -70,12 +70,18 @@ class GhosttyApp { private(set) var defaultBackgroundColor: NSColor = .windowBackgroundColor private(set) var defaultBackgroundOpacity: Double = 1.0 let backgroundLogEnabled = { + if ProcessInfo.processInfo.environment["CMUX_DEBUG_BG"] == "1" { + return true + } if ProcessInfo.processInfo.environment["GHOSTTYTABS_DEBUG_BG"] == "1" { return true } + if UserDefaults.standard.bool(forKey: "cmuxDebugBG") { + return true + } return UserDefaults.standard.bool(forKey: "GhosttyTabsDebugBG") }() - private let backgroundLogURL = URL(fileURLWithPath: "/tmp/ghosttytabs-bg.log") + private let backgroundLogURL = URL(fileURLWithPath: "/tmp/cmux-bg.log") private var appObservers: [NSObjectProtocol] = [] private var displayLink: CVDisplayLink? private var displayLinkUsers = 0 @@ -555,7 +561,7 @@ class GhosttyApp { } func logBackground(_ message: String) { - let line = "GhosttyTabs bg: \(message)\n" + let line = "cmux bg: \(message)\n" if let data = line.data(using: .utf8) { if FileManager.default.fileExists(atPath: backgroundLogURL.path) == false { FileManager.default.createFile(atPath: backgroundLogURL.path, contents: nil) @@ -1696,7 +1702,7 @@ final class GhosttySurfaceScrollView: NSView { CAMediaTimingFunction(name: .easeOut), CAMediaTimingFunction(name: .easeIn) ] - self.flashLayer.add(animation, forKey: "ghosttytabs.flash") + self.flashLayer.add(animation, forKey: "cmux.flash") } } diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index bcf1a4d8..b061d479 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -5,7 +5,7 @@ import Foundation class TerminalController { static let shared = TerminalController() - private let socketPath = "/tmp/ghosttytabs.sock" + private let socketPath = "/tmp/cmux.sock" private var serverSocket: Int32 = -1 private var isRunning = false private var clientHandlers: [Int32: Thread] = [:] diff --git a/Sources/TerminalNotificationStore.swift b/Sources/TerminalNotificationStore.swift index edb40f61..70f5e71e 100644 --- a/Sources/TerminalNotificationStore.swift +++ b/Sources/TerminalNotificationStore.swift @@ -15,8 +15,8 @@ struct TerminalNotification: Identifiable, Hashable { final class TerminalNotificationStore: ObservableObject { static let shared = TerminalNotificationStore() - static let categoryIdentifier = "com.cmux.ghosttytabs.userNotification" - static let actionShowIdentifier = "com.cmux.ghosttytabs.userNotification.show" + static let categoryIdentifier = "com.cmux.app.userNotification" + static let actionShowIdentifier = "com.cmux.app.userNotification.show" @Published private(set) var notifications: [TerminalNotification] = [] diff --git a/Sources/GhosttyTabsApp.swift b/Sources/cmuxApp.swift similarity index 78% rename from Sources/GhosttyTabsApp.swift rename to Sources/cmuxApp.swift index 15b56353..a17b2a7a 100644 --- a/Sources/GhosttyTabsApp.swift +++ b/Sources/cmuxApp.swift @@ -1,7 +1,8 @@ +import AppKit import SwiftUI @main -struct GhosttyTabsApp: App { +struct cmuxApp: App { @StateObject private var tabManager = TabManager() @StateObject private var notificationStore = TerminalNotificationStore.shared @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate @@ -24,6 +25,12 @@ struct GhosttyTabsApp: App { } .windowStyle(.hiddenTitleBar) .commands { + CommandGroup(replacing: .appInfo) { + Button("About cmux") { + showAboutPanel() + } + } + // New tab commands CommandGroup(replacing: .newItem) { Button("New Tab") { @@ -93,4 +100,19 @@ struct GhosttyTabsApp: App { } } } + + private func showAboutPanel() { + let bundle = Bundle.main + let appName = bundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String + ?? bundle.object(forInfoDictionaryKey: "CFBundleName") as? String + ?? "cmux" + let version = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0" + let build = bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1" + NSApp.orderFrontStandardAboutPanel(options: [ + .applicationName: appName, + .version: version, + .applicationVersion: build + ]) + NSApp.activate(ignoringOtherApps: true) + } } diff --git a/GhosttyTabs-Bridging-Header.h b/cmux-Bridging-Header.h similarity index 100% rename from GhosttyTabs-Bridging-Header.h rename to cmux-Bridging-Header.h diff --git a/scripts/rebuild.sh b/scripts/rebuild.sh index b07fc262..3c256b5b 100755 --- a/scripts/rebuild.sh +++ b/scripts/rebuild.sh @@ -1,18 +1,18 @@ #!/bin/bash -# Rebuild and restart GhosttyTabs app +# Rebuild and restart cmux app set -e cd "$(dirname "$0")/.." # Kill existing app if running -pkill -9 -f "GhosttyTabs" 2>/dev/null || true +pkill -9 -f "cmux" 2>/dev/null || true # Build swift build # Copy to app bundle -cp .build/debug/GhosttyTabs .build/debug/GhosttyTabs.app/Contents/MacOS/ +cp .build/debug/cmux .build/debug/cmux.app/Contents/MacOS/ # Open the app -open .build/debug/GhosttyTabs.app +open .build/debug/cmux.app diff --git a/tests/ghosttytabs.py b/tests/cmux.py similarity index 84% rename from tests/ghosttytabs.py rename to tests/cmux.py index 8cbae208..708a5911 100755 --- a/tests/ghosttytabs.py +++ b/tests/cmux.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 """ -GhosttyTabs Python Client +cmux Python Client -A client library for programmatically controlling GhosttyTabs via Unix socket. +A client library for programmatically controlling cmux via Unix socket. Usage: - from ghosttytabs import GhosttyTabs + from cmux import cmux - client = GhosttyTabs() + client = cmux() client.connect() # Send text to terminal @@ -33,29 +33,29 @@ import os from typing import Optional, List, Tuple -class GhosttyTabsError(Exception): - """Exception raised for GhosttyTabs errors""" +class cmuxError(Exception): + """Exception raised for cmux errors""" pass -class GhosttyTabs: - """Client for controlling GhosttyTabs via Unix socket""" +class cmux: + """Client for controlling cmux via Unix socket""" - DEFAULT_SOCKET_PATH = "/tmp/ghosttytabs.sock" + DEFAULT_SOCKET_PATH = "/tmp/cmux.sock" def __init__(self, socket_path: str = None): self.socket_path = socket_path or self.DEFAULT_SOCKET_PATH self._socket: Optional[socket.socket] = None def connect(self) -> None: - """Connect to the GhosttyTabs socket""" + """Connect to the cmux socket""" if self._socket is not None: return if not os.path.exists(self.socket_path): - raise GhosttyTabsError( + raise cmuxError( f"Socket not found at {self.socket_path}. " - "Is GhosttyTabs running?" + "Is cmux running?" ) self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) @@ -64,7 +64,7 @@ class GhosttyTabs: self._socket.settimeout(5.0) except socket.error as e: self._socket = None - raise GhosttyTabsError(f"Failed to connect: {e}") + raise cmuxError(f"Failed to connect: {e}") def close(self) -> None: """Close the connection""" @@ -83,16 +83,16 @@ class GhosttyTabs: def _send_command(self, command: str) -> str: """Send a command and receive response""" if self._socket is None: - raise GhosttyTabsError("Not connected") + raise cmuxError("Not connected") try: self._socket.sendall((command + "\n").encode()) response = self._socket.recv(8192).decode().strip() return response except socket.timeout: - raise GhosttyTabsError("Command timed out") + raise cmuxError("Command timed out") except socket.error as e: - raise GhosttyTabsError(f"Socket error: {e}") + raise cmuxError(f"Socket error: {e}") def ping(self) -> bool: """Check if the server is responding""" @@ -126,25 +126,25 @@ class GhosttyTabs: response = self._send_command("new_tab") if response.startswith("OK "): return response[3:] - raise GhosttyTabsError(response) + raise cmuxError(response) def new_split(self, direction: str) -> None: """Create a split in the given direction (left/right/up/down).""" response = self._send_command(f"new_split {direction}") if not response.startswith("OK"): - raise GhosttyTabsError(response) + raise cmuxError(response) def close_tab(self, tab_id: str) -> None: """Close a tab by ID""" response = self._send_command(f"close_tab {tab_id}") if not response.startswith("OK"): - raise GhosttyTabsError(response) + raise cmuxError(response) def select_tab(self, tab: str | int) -> None: """Select a tab by ID or index""" response = self._send_command(f"select_tab {tab}") if not response.startswith("OK"): - raise GhosttyTabsError(response) + raise cmuxError(response) def list_surfaces(self, tab: str | int | None = None) -> List[Tuple[int, str, bool]]: """ @@ -172,13 +172,13 @@ class GhosttyTabs: """Focus a surface by ID or index in the current tab.""" response = self._send_command(f"focus_surface {surface}") if not response.startswith("OK"): - raise GhosttyTabsError(response) + raise cmuxError(response) def current_tab(self) -> str: """Get the current tab's ID""" response = self._send_command("current_tab") if response.startswith("ERROR"): - raise GhosttyTabsError(response) + raise cmuxError(response) return response def send(self, text: str) -> None: @@ -195,14 +195,14 @@ class GhosttyTabs: escaped = text.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") response = self._send_command(f"send {escaped}") if not response.startswith("OK"): - raise GhosttyTabsError(response) + raise cmuxError(response) def send_surface(self, surface: str | int, text: str) -> None: """Send text to a specific surface by ID or index in the current tab.""" escaped = text.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") response = self._send_command(f"send_surface {surface} {escaped}") if not response.startswith("OK"): - raise GhosttyTabsError(response) + raise cmuxError(response) def send_key(self, key: str) -> None: """ @@ -215,13 +215,13 @@ class GhosttyTabs: """ response = self._send_command(f"send_key {key}") if not response.startswith("OK"): - raise GhosttyTabsError(response) + raise cmuxError(response) def send_key_surface(self, surface: str | int, key: str) -> None: """Send a special key to a specific surface by ID or index in the current tab.""" response = self._send_command(f"send_key_surface {surface} {key}") if not response.startswith("OK"): - raise GhosttyTabsError(response) + raise cmuxError(response) def send_line(self, text: str) -> None: """Send text followed by Enter""" @@ -241,23 +241,23 @@ class GhosttyTabs: def main(): - """CLI interface for ghosttytabs""" + """CLI interface for cmux""" import sys import argparse - parser = argparse.ArgumentParser(description="GhosttyTabs CLI") + parser = argparse.ArgumentParser(description="cmux CLI") parser.add_argument("command", nargs="?", help="Command to send") parser.add_argument("args", nargs="*", help="Command arguments") - parser.add_argument("-s", "--socket", default=GhosttyTabs.DEFAULT_SOCKET_PATH, + parser.add_argument("-s", "--socket", default=cmux.DEFAULT_SOCKET_PATH, help="Socket path") args = parser.parse_args() try: - with GhosttyTabs(args.socket) as client: + with cmux(args.socket) as client: if not args.command: # Interactive mode - print("GhosttyTabs CLI (type 'help' for commands, 'quit' to exit)") + print("cmux CLI (type 'help' for commands, 'quit' to exit)") while True: try: line = input("> ").strip() @@ -278,7 +278,7 @@ def main(): command += " " + " ".join(args.args) response = client._send_command(command) print(response) - except GhosttyTabsError as e: + except cmuxError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) diff --git a/tests/test_app_keystrokes.sh b/tests/test_app_keystrokes.sh index 7f22ed35..2bc06b8e 100755 --- a/tests/test_app_keystrokes.sh +++ b/tests/test_app_keystrokes.sh @@ -1,24 +1,24 @@ #!/bin/bash -# Test script that sends keystrokes to GhosttyTabs via AppleScript +# Test script that sends keystrokes to cmux via AppleScript # This tests the actual keyboard input path through the app set -e -echo "=== GhosttyTabs Keystroke Test ===" +echo "=== cmux Keystroke Test ===" echo "" -# Check if GhosttyTabs is running -if ! pgrep -x "GhosttyTabs" > /dev/null; then - echo "Error: GhosttyTabs is not running" - echo "Please start GhosttyTabs first" +# Check if cmux is running +if ! pgrep -x "cmux" > /dev/null; then + echo "Error: cmux is not running" + echo "Please start cmux first" exit 1 fi -echo "GhosttyTabs is running" +echo "cmux is running" echo "" -# Activate GhosttyTabs -osascript -e 'tell application "GhosttyTabs" to activate' +# Activate cmux +osascript -e 'tell application "cmux" to activate' sleep 0.5 echo "Test 1: Testing Ctrl+C (SIGINT)" @@ -56,7 +56,7 @@ echo " If cat exited, Ctrl+D is working!" echo "" echo "=== Manual Verification Required ===" -echo "Please check the GhosttyTabs window to verify:" +echo "Please check the cmux window to verify:" echo " 1. The 'sleep 30' command was interrupted by Ctrl+C" echo " 2. The 'cat' command exited after Ctrl+D" echo "" diff --git a/tests/test_ctrl_enter_keybind.py b/tests/test_ctrl_enter_keybind.py index c9528d47..e84b8046 100644 --- a/tests/test_ctrl_enter_keybind.py +++ b/tests/test_ctrl_enter_keybind.py @@ -3,7 +3,7 @@ Automated test for ctrl+enter keybind using real keystrokes. Requires: - - GhosttyTabs running + - cmux running - Accessibility permissions for System Events (osascript) - keybind = ctrl+enter=text:\\r (or \\n/\\x0d) configured in Ghostty config """ @@ -14,10 +14,10 @@ import time import subprocess from pathlib import Path -# Add the directory containing ghosttytabs.py to the path +# Add the directory containing cmux.py to the path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from ghosttytabs import GhosttyTabs, GhosttyTabsError +from cmux import cmux, cmuxError def run_osascript(script: str) -> None: @@ -54,7 +54,7 @@ def find_config_with_keybind() -> Path | None: return None -def test_ctrl_enter_keybind(client: GhosttyTabs) -> tuple[bool, str]: +def test_ctrl_enter_keybind(client: cmux) -> tuple[bool, str]: marker = Path("/tmp") / f"ghostty_ctrl_enter_{os.getpid()}" marker.unlink(missing_ok=True) @@ -64,7 +64,7 @@ def test_ctrl_enter_keybind(client: GhosttyTabs) -> tuple[bool, str]: time.sleep(0.3) # Make sure the app is focused for keystrokes - run_osascript('tell application "GhosttyTabs" to activate') + run_osascript('tell application "cmux" to activate') time.sleep(0.2) # Clear any running command @@ -94,14 +94,14 @@ def test_ctrl_enter_keybind(client: GhosttyTabs) -> tuple[bool, str]: def run_tests() -> int: print("=" * 60) - print("GhosttyTabs Ctrl+Enter Keybind Test") + print("cmux Ctrl+Enter Keybind Test") print("=" * 60) print() - socket_path = GhosttyTabs.DEFAULT_SOCKET_PATH + socket_path = cmux.DEFAULT_SOCKET_PATH if not os.path.exists(socket_path): print(f"Error: Socket not found at {socket_path}") - print("Please make sure GhosttyTabs is running.") + print("Please make sure cmux is running.") return 1 config_path = find_config_with_keybind() @@ -109,19 +109,19 @@ def run_tests() -> int: print("Error: Required keybind not found in Ghostty config.") print("Add a line like:") print(" keybind = ctrl+enter=text:\\r") - print("Then restart GhosttyTabs and re-run this test.") + print("Then restart cmux and re-run this test.") return 1 print(f"Using keybind from: {config_path}") print() try: - with GhosttyTabs() as client: + with cmux() as client: ok, message = test_ctrl_enter_keybind(client) status = "✅" if ok else "❌" print(f"{status} {message}") return 0 if ok else 1 - except GhosttyTabsError as e: + except cmuxError as e: print(f"Error: {e}") return 1 except subprocess.CalledProcessError as e: diff --git a/tests/test_ctrl_interactive.py b/tests/test_ctrl_interactive.py index 63fa329b..85ecf87b 100755 --- a/tests/test_ctrl_interactive.py +++ b/tests/test_ctrl_interactive.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """ -Interactive test for Ctrl+C and Ctrl+D in GhosttyTabs terminal. +Interactive test for Ctrl+C and Ctrl+D in cmux terminal. This script tests that control signals are properly handled. -Run this script inside the GhosttyTabs terminal. +Run this script inside the cmux terminal. Tests: 1. Ctrl+C (SIGINT) - Should interrupt a running process @@ -72,10 +72,10 @@ def test_ctrl_d(): def main(): print("=" * 50) - print("GhosttyTabs Control Signal Test") + print("cmux Control Signal Test") print("=" * 50) print("\nThis script tests if Ctrl+C and Ctrl+D work correctly.") - print("Run this inside the GhosttyTabs terminal to verify the fix.\n") + print("Run this inside the cmux terminal to verify the fix.\n") # Check if running in a terminal if not os.isatty(sys.stdin.fileno()): diff --git a/tests/test_ctrl_signals.sh b/tests/test_ctrl_signals.sh index 8bd1ba5b..f331d82a 100755 --- a/tests/test_ctrl_signals.sh +++ b/tests/test_ctrl_signals.sh @@ -1,6 +1,6 @@ #!/bin/bash # Test script to verify Ctrl+C and Ctrl+D work correctly in the terminal -# Run this script inside the GhosttyTabs terminal to test signal handling +# Run this script inside the cmux terminal to test signal handling set -e diff --git a/tests/test_ctrl_socket.py b/tests/test_ctrl_socket.py index ce6b5fa3..35aac73f 100755 --- a/tests/test_ctrl_socket.py +++ b/tests/test_ctrl_socket.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 """ -Automated tests for Ctrl+C and Ctrl+D using the GhosttyTabs socket interface. +Automated tests for Ctrl+C and Ctrl+D using the cmux socket interface. Usage: python3 test_ctrl_socket.py Requirements: - - GhosttyTabs must be running with the socket controller enabled + - cmux must be running with the socket controller enabled """ import os @@ -15,10 +15,10 @@ import time import tempfile from pathlib import Path -# Add the directory containing ghosttytabs.py to the path +# Add the directory containing cmux.py to the path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from ghosttytabs import GhosttyTabs, GhosttyTabsError +from cmux import cmux, cmuxError class TestResult: @@ -36,7 +36,7 @@ class TestResult: self.message = msg -def test_connection(client: GhosttyTabs) -> TestResult: +def test_connection(client: cmux) -> TestResult: """Test that we can connect and ping the server""" result = TestResult("Connection") try: @@ -49,7 +49,7 @@ def test_connection(client: GhosttyTabs) -> TestResult: return result -def test_ctrl_c(client: GhosttyTabs) -> TestResult: +def test_ctrl_c(client: cmux) -> TestResult: """ Test Ctrl+C by: 1. Starting sleep command @@ -88,7 +88,7 @@ def test_ctrl_c(client: GhosttyTabs) -> TestResult: return result -def test_ctrl_d(client: GhosttyTabs) -> TestResult: +def test_ctrl_d(client: cmux) -> TestResult: """ Test Ctrl+D by: 1. Running cat command @@ -127,7 +127,7 @@ def test_ctrl_d(client: GhosttyTabs) -> TestResult: return result -def test_ctrl_c_python(client: GhosttyTabs) -> TestResult: +def test_ctrl_c_python(client: cmux) -> TestResult: """ Test Ctrl+C with Python process """ @@ -166,20 +166,20 @@ def test_ctrl_c_python(client: GhosttyTabs) -> TestResult: def run_tests(): """Run all tests""" print("=" * 60) - print("GhosttyTabs Ctrl+C/D Automated Tests") + print("cmux Ctrl+C/D Automated Tests") print("=" * 60) print() - socket_path = GhosttyTabs.DEFAULT_SOCKET_PATH + socket_path = cmux.DEFAULT_SOCKET_PATH if not os.path.exists(socket_path): print(f"Error: Socket not found at {socket_path}") - print("Please make sure GhosttyTabs is running.") + print("Please make sure cmux is running.") return 1 results = [] try: - with GhosttyTabs() as client: + with cmux() as client: # Test connection print("Testing connection...") results.append(test_connection(client)) @@ -215,7 +215,7 @@ def run_tests(): print(f" {status} {results[-1].message}") print() - except GhosttyTabsError as e: + except cmuxError as e: print(f"Error: {e}") return 1