Rename to cmux and add About panel

This commit is contained in:
Lawrence Chen 2026-01-26 03:05:03 -08:00
parent f6034a5979
commit 8320d5805a
18 changed files with 164 additions and 125 deletions

View file

@ -1,4 +1,4 @@
# GhosttyTabs # cmux
A macOS terminal app with vertical tabs, using libghostty (GhosttyKit.xcframework) for terminal emulation. 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) ### Build and launch (Release)
```bash ```bash
cd /Users/lawrencechen/fun/cmux-terminal/GhosttyTabs cd /Users/lawrencechen/fun/cmux-terminal/GhosttyTabs
pkill -9 GhosttyTabs 2>/dev/null pkill -9 cmux 2>/dev/null
xcodebuild -scheme GhosttyTabs -configuration Release build xcodebuild -scheme cmux -configuration Release build
open ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-cbjivvtpirygxbbgqlpdpiiyjnwh/Build/Products/Release/GhosttyTabs.app open ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-cbjivvtpirygxbbgqlpdpiiyjnwh/Build/Products/Release/cmux.app
``` ```
### Rebuild libghostty (optimized) ### Rebuild libghostty (optimized)
@ -26,18 +26,18 @@ cp -R /tmp/ghostty/macos/GhosttyKit.xcframework /Users/lawrencechen/fun/cmux-ter
### Project structure ### Project structure
- `Sources/` - Swift source files - `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 - `ContentView.swift` - Main UI with vertical tabs sidebar
- `TabManager.swift` - Tab state management - `TabManager.swift` - Tab state management
- `GhosttyTerminalView.swift` - libghostty terminal integration - `GhosttyTerminalView.swift` - libghostty terminal integration
- `GhosttyConfig.swift` - Ghostty config parser - `GhosttyConfig.swift` - Ghostty config parser
- `TerminalController.swift` - Unix socket server for programmatic control - `TerminalController.swift` - Unix socket server for programmatic control
- `tests/` - Test files and utilities - `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 - `test_ctrl_socket.py` - Main automated test suite
- `GhosttyKit.xcframework/` - libghostty static library (gitignored, rebuild from /tmp/ghostty) - `GhosttyKit.xcframework/` - libghostty static library (gitignored, rebuild from /tmp/ghostty)
- `ghostty.h` - Ghostty C API header - `ghostty.h` - Ghostty C API header
- `GhosttyTabs-Bridging-Header.h` - Swift bridging header - `cmux-Bridging-Header.h` - Swift bridging header
### Keyboard Shortcuts ### Keyboard Shortcuts
- `Cmd+T` / `Cmd+N` / `Ctrl+Shift+`` - New tab - `Cmd+T` / `Cmd+N` / `Ctrl+Shift+`` - New tab
@ -54,7 +54,7 @@ Reads user's Ghostty config from:
### Unix Socket Control API ### 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 #### Socket Commands
@ -84,12 +84,12 @@ Use `\n` for Enter (carriage return), `\t` for tab, `\r` for raw CR.
### Python Client Library ### Python Client Library
Located at `tests/ghosttytabs.py`: Located at `tests/cmux.py`:
```python ```python
from ghosttytabs import GhosttyTabs from cmux import cmux
with GhosttyTabs() as client: with cmux() as client:
# Send text with Enter # Send text with Enter
client.send("echo hello\n") client.send("echo hello\n")
@ -108,16 +108,16 @@ with GhosttyTabs() as client:
```bash ```bash
# Build and launch the app first # Build and launch the app first
pkill -9 GhosttyTabs 2>/dev/null pkill -9 cmux 2>/dev/null
xcodebuild -scheme GhosttyTabs -configuration Release build xcodebuild -scheme cmux -configuration Release build
open ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-cbjivvtpirygxbbgqlpdpiiyjnwh/Build/Products/Release/GhosttyTabs.app open ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-cbjivvtpirygxbbgqlpdpiiyjnwh/Build/Products/Release/cmux.app
sleep 3 sleep 3
# Run the main test suite (tests Ctrl+C, Ctrl+D) # Run the main test suite (tests Ctrl+C, Ctrl+D)
python3 tests/test_ctrl_socket.py python3 tests/test_ctrl_socket.py
# Interactive CLI for manual testing # Interactive CLI for manual testing
python3 tests/ghosttytabs.py python3 tests/cmux.py
``` ```
### Writing New Tests ### Writing New Tests
@ -148,7 +148,7 @@ python3 tests/ghosttytabs.py
### Test Files ### 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_ctrl_socket.py` - Automated Ctrl+C/D test suite (main tests)
- `tests/test_signals_auto.py` - PTY-based signal tests (standalone) - `tests/test_signals_auto.py` - PTY-based signal tests (standalone)
- `tests/test_ctrl_interactive.py` - Interactive manual tests - `tests/test_ctrl_interactive.py` - Interactive manual tests

View file

@ -7,7 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; A5001002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001012 /* ContentView.swift */; };
A5001003 /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001013 /* TabManager.swift */; }; A5001003 /* TabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001013 /* TabManager.swift */; };
A5001004 /* GhosttyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001014 /* GhosttyConfig.swift */; }; A5001004 /* GhosttyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001014 /* GhosttyConfig.swift */; };
@ -49,16 +49,16 @@
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXFileReference 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; }; 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 = "<group>"; }; A5001011 /* cmuxApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = cmuxApp.swift; sourceTree = "<group>"; };
A5001012 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; A5001012 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
A5001013 /* TabManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = "<group>"; }; A5001013 /* TabManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManager.swift; sourceTree = "<group>"; };
A5001014 /* GhosttyConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyConfig.swift; sourceTree = "<group>"; }; A5001014 /* GhosttyConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyConfig.swift; sourceTree = "<group>"; };
A5001015 /* GhosttyTerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyTerminalView.swift; sourceTree = "<group>"; }; A5001015 /* GhosttyTerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyTerminalView.swift; sourceTree = "<group>"; };
A5001016 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; }; A5001016 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
A5001017 /* ghostty.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ghostty.h; sourceTree = "<group>"; }; A5001017 /* ghostty.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ghostty.h; sourceTree = "<group>"; };
A5001018 /* GhosttyTabs-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GhosttyTabs-Bridging-Header.h"; sourceTree = "<group>"; }; A5001018 /* cmux-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "cmux-Bridging-Header.h"; sourceTree = "<group>"; };
A5001019 /* TerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalController.swift; sourceTree = "<group>"; }; A5001019 /* TerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalController.swift; sourceTree = "<group>"; };
A5001090 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; A5001090 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
A5001091 /* NotificationsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPage.swift; sourceTree = "<group>"; }; A5001091 /* NotificationsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPage.swift; sourceTree = "<group>"; };
@ -117,7 +117,7 @@
A5001101 /* Assets.xcassets */, A5001101 /* Assets.xcassets */,
A5001016 /* GhosttyKit.xcframework */, A5001016 /* GhosttyKit.xcframework */,
A5001017 /* ghostty.h */, A5001017 /* ghostty.h */,
A5001018 /* GhosttyTabs-Bridging-Header.h */, A5001018 /* cmux-Bridging-Header.h */,
3196C9C2D01F054C1D3385DD /* GhosttyTabsUITests */, 3196C9C2D01F054C1D3385DD /* GhosttyTabsUITests */,
A5001042 /* Products */, A5001042 /* Products */,
); );
@ -126,7 +126,7 @@
A5001041 /* Sources */ = { A5001041 /* Sources */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A5001011 /* GhosttyTabsApp.swift */, A5001011 /* cmuxApp.swift */,
A5001012 /* ContentView.swift */, A5001012 /* ContentView.swift */,
A5001013 /* TabManager.swift */, A5001013 /* TabManager.swift */,
A5001014 /* GhosttyConfig.swift */, A5001014 /* GhosttyConfig.swift */,
@ -153,7 +153,7 @@
A5001042 /* Products */ = { A5001042 /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A5001000 /* GhosttyTabs.app */, A5001000 /* cmux.app */,
7E7E6EF344A568AC7FEE3715 /* GhosttyTabsUITests.xctest */, 7E7E6EF344A568AC7FEE3715 /* GhosttyTabsUITests.xctest */,
); );
name = Products; name = Products;
@ -185,7 +185,7 @@
); );
name = GhosttyTabs; name = GhosttyTabs;
productName = GhosttyTabs; productName = GhosttyTabs;
productReference = A5001000 /* GhosttyTabs.app */; productReference = A5001000 /* cmux.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
CB450DF0F0B3839599082C4D /* GhosttyTabsUITests */ = { CB450DF0F0B3839599082C4D /* GhosttyTabsUITests */ = {
@ -240,7 +240,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A5001001 /* GhosttyTabsApp.swift in Sources */, A5001001 /* cmuxApp.swift in Sources */,
A5001002 /* ContentView.swift in Sources */, A5001002 /* ContentView.swift in Sources */,
A5001003 /* TabManager.swift in Sources */, A5001003 /* TabManager.swift in Sources */,
A5001004 /* GhosttyConfig.swift in Sources */, A5001004 /* GhosttyConfig.swift in Sources */,
@ -346,6 +346,8 @@
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO; ENABLE_HARDENED_RUNTIME = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = cmux;
INFOPLIST_KEY_CFBundleName = cmux;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMainStoryboardFile = ""; INFOPLIST_KEY_NSMainStoryboardFile = "";
@ -368,10 +370,10 @@
"-framework", "-framework",
Carbon, Carbon,
); );
PRODUCT_BUNDLE_IDENTIFIER = com.ghosttytabs.app; PRODUCT_BUNDLE_IDENTIFIER = com.cmux.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = cmux;
SWIFT_EMIT_LOC_STRINGS = YES; 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; SWIFT_VERSION = 5.0;
}; };
name = Debug; name = Debug;
@ -387,6 +389,8 @@
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO; ENABLE_HARDENED_RUNTIME = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = cmux;
INFOPLIST_KEY_CFBundleName = cmux;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMainStoryboardFile = ""; INFOPLIST_KEY_NSMainStoryboardFile = "";
@ -410,10 +414,10 @@
Carbon, Carbon,
); );
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.ghosttytabs.app; PRODUCT_BUNDLE_IDENTIFIER = com.cmux.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = cmux;
SWIFT_EMIT_LOC_STRINGS = YES; 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; SWIFT_VERSION = 5.0;
}; };
name = Release; name = Release;
@ -427,7 +431,7 @@
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.ghosttytabs.appuitests; PRODUCT_BUNDLE_IDENTIFIER = com.cmux.appuitests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -444,7 +448,7 @@
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.ghosttytabs.appuitests; PRODUCT_BUNDLE_IDENTIFIER = com.cmux.appuitests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = GhosttyTabs; TEST_TARGET_NAME = GhosttyTabs;

View file

@ -3,7 +3,7 @@
<BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES"> <BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES"> <BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="GhosttyTabs.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/> <BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="cmux.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
</BuildActionEntry> </BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
</BuildAction> </BuildAction>
@ -14,17 +14,17 @@
</TestableReference> </TestableReference>
</Testables> </Testables>
<MacroExpansion> <MacroExpansion>
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="GhosttyTabs.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/> <BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="cmux.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
</MacroExpansion> </MacroExpansion>
</TestAction> </TestAction>
<LaunchAction buildConfiguration="Release" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="NO" debugDocumentVersioning="YES" allowLocationSimulation="YES"> <LaunchAction buildConfiguration="Release" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="NO" debugDocumentVersioning="YES" allowLocationSimulation="YES">
<BuildableProductRunnable runnableDebuggingMode="0"> <BuildableProductRunnable runnableDebuggingMode="0">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="GhosttyTabs.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/> <BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="cmux.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
</BuildableProductRunnable> </BuildableProductRunnable>
</LaunchAction> </LaunchAction>
<ProfileAction buildConfiguration="Release" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES"> <ProfileAction buildConfiguration="Release" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES">
<BuildableProductRunnable runnableDebuggingMode="0"> <BuildableProductRunnable runnableDebuggingMode="0">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="GhosttyTabs.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/> <BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="A5001050" BuildableName="cmux.app" BlueprintName="GhosttyTabs" ReferencedContainer="container:GhosttyTabs.xcodeproj"/>
</BuildableProductRunnable> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>
<AnalyzeAction buildConfiguration="Release"/> <AnalyzeAction buildConfiguration="Release"/>

View file

@ -2,19 +2,19 @@
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "GhosttyTabs", name: "cmux",
platforms: [ platforms: [
.macOS(.v13) .macOS(.v13)
], ],
products: [ products: [
.executable(name: "GhosttyTabs", targets: ["GhosttyTabs"]) .executable(name: "cmux", targets: ["cmux"])
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/migueldeicaza/SwiftTerm.git", from: "1.2.0") .package(url: "https://github.com/migueldeicaza/SwiftTerm.git", from: "1.2.0")
], ],
targets: [ targets: [
.executableTarget( .executableTarget(
name: "GhosttyTabs", name: "cmux",
dependencies: ["SwiftTerm"], dependencies: ["SwiftTerm"],
path: "Sources" path: "Sources"
) )

View file

@ -1,14 +1,14 @@
# GhosttyTabs # cmux
Vertical tabs for Ghostty on macOS, built on libghostty. 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 ## Releases
Tag a version like `v0.1.0` and push it to trigger the GitHub Actions release workflow. 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, 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 ### Required GitHub secrets

View file

@ -17,6 +17,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
registerLaunchServicesBundle() registerLaunchServicesBundle()
enforceSingleInstance() enforceSingleInstance()
ensureApplicationIcon()
observeDuplicateLaunches() observeDuplicateLaunches()
configureUserNotifications() configureUserNotifications()
} }
@ -46,6 +47,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
center.delegate = self center.delegate = self
} }
private func ensureApplicationIcon() {
if let icon = NSImage(named: NSImage.applicationIconName) {
NSApplication.shared.applicationIconImage = icon
}
}
private func registerLaunchServicesBundle() { private func registerLaunchServicesBundle() {
let bundleURL = Bundle.main.bundleURL.standardizedFileURL let bundleURL = Bundle.main.bundleURL.standardizedFileURL
let registerStatus = LSRegisterURL(bundleURL as CFURL, true) let registerStatus = LSRegisterURL(bundleURL as CFURL, true)

View file

@ -70,12 +70,18 @@ class GhosttyApp {
private(set) var defaultBackgroundColor: NSColor = .windowBackgroundColor private(set) var defaultBackgroundColor: NSColor = .windowBackgroundColor
private(set) var defaultBackgroundOpacity: Double = 1.0 private(set) var defaultBackgroundOpacity: Double = 1.0
let backgroundLogEnabled = { let backgroundLogEnabled = {
if ProcessInfo.processInfo.environment["CMUX_DEBUG_BG"] == "1" {
return true
}
if ProcessInfo.processInfo.environment["GHOSTTYTABS_DEBUG_BG"] == "1" { if ProcessInfo.processInfo.environment["GHOSTTYTABS_DEBUG_BG"] == "1" {
return true return true
} }
if UserDefaults.standard.bool(forKey: "cmuxDebugBG") {
return true
}
return UserDefaults.standard.bool(forKey: "GhosttyTabsDebugBG") 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 appObservers: [NSObjectProtocol] = []
private var displayLink: CVDisplayLink? private var displayLink: CVDisplayLink?
private var displayLinkUsers = 0 private var displayLinkUsers = 0
@ -555,7 +561,7 @@ class GhosttyApp {
} }
func logBackground(_ message: String) { func logBackground(_ message: String) {
let line = "GhosttyTabs bg: \(message)\n" let line = "cmux bg: \(message)\n"
if let data = line.data(using: .utf8) { if let data = line.data(using: .utf8) {
if FileManager.default.fileExists(atPath: backgroundLogURL.path) == false { if FileManager.default.fileExists(atPath: backgroundLogURL.path) == false {
FileManager.default.createFile(atPath: backgroundLogURL.path, contents: nil) FileManager.default.createFile(atPath: backgroundLogURL.path, contents: nil)
@ -1696,7 +1702,7 @@ final class GhosttySurfaceScrollView: NSView {
CAMediaTimingFunction(name: .easeOut), CAMediaTimingFunction(name: .easeOut),
CAMediaTimingFunction(name: .easeIn) CAMediaTimingFunction(name: .easeIn)
] ]
self.flashLayer.add(animation, forKey: "ghosttytabs.flash") self.flashLayer.add(animation, forKey: "cmux.flash")
} }
} }

View file

@ -5,7 +5,7 @@ import Foundation
class TerminalController { class TerminalController {
static let shared = TerminalController() static let shared = TerminalController()
private let socketPath = "/tmp/ghosttytabs.sock" private let socketPath = "/tmp/cmux.sock"
private var serverSocket: Int32 = -1 private var serverSocket: Int32 = -1
private var isRunning = false private var isRunning = false
private var clientHandlers: [Int32: Thread] = [:] private var clientHandlers: [Int32: Thread] = [:]

View file

@ -15,8 +15,8 @@ struct TerminalNotification: Identifiable, Hashable {
final class TerminalNotificationStore: ObservableObject { final class TerminalNotificationStore: ObservableObject {
static let shared = TerminalNotificationStore() static let shared = TerminalNotificationStore()
static let categoryIdentifier = "com.cmux.ghosttytabs.userNotification" static let categoryIdentifier = "com.cmux.app.userNotification"
static let actionShowIdentifier = "com.cmux.ghosttytabs.userNotification.show" static let actionShowIdentifier = "com.cmux.app.userNotification.show"
@Published private(set) var notifications: [TerminalNotification] = [] @Published private(set) var notifications: [TerminalNotification] = []

View file

@ -1,7 +1,8 @@
import AppKit
import SwiftUI import SwiftUI
@main @main
struct GhosttyTabsApp: App { struct cmuxApp: App {
@StateObject private var tabManager = TabManager() @StateObject private var tabManager = TabManager()
@StateObject private var notificationStore = TerminalNotificationStore.shared @StateObject private var notificationStore = TerminalNotificationStore.shared
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@ -24,6 +25,12 @@ struct GhosttyTabsApp: App {
} }
.windowStyle(.hiddenTitleBar) .windowStyle(.hiddenTitleBar)
.commands { .commands {
CommandGroup(replacing: .appInfo) {
Button("About cmux") {
showAboutPanel()
}
}
// New tab commands // New tab commands
CommandGroup(replacing: .newItem) { CommandGroup(replacing: .newItem) {
Button("New Tab") { 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)
}
} }

View file

@ -1,18 +1,18 @@
#!/bin/bash #!/bin/bash
# Rebuild and restart GhosttyTabs app # Rebuild and restart cmux app
set -e set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
# Kill existing app if running # Kill existing app if running
pkill -9 -f "GhosttyTabs" 2>/dev/null || true pkill -9 -f "cmux" 2>/dev/null || true
# Build # Build
swift build swift build
# Copy to app bundle # 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 the app
open .build/debug/GhosttyTabs.app open .build/debug/cmux.app

View file

@ -1,13 +1,13 @@
#!/usr/bin/env python3 #!/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: Usage:
from ghosttytabs import GhosttyTabs from cmux import cmux
client = GhosttyTabs() client = cmux()
client.connect() client.connect()
# Send text to terminal # Send text to terminal
@ -33,29 +33,29 @@ import os
from typing import Optional, List, Tuple from typing import Optional, List, Tuple
class GhosttyTabsError(Exception): class cmuxError(Exception):
"""Exception raised for GhosttyTabs errors""" """Exception raised for cmux errors"""
pass pass
class GhosttyTabs: class cmux:
"""Client for controlling GhosttyTabs via Unix socket""" """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): def __init__(self, socket_path: str = None):
self.socket_path = socket_path or self.DEFAULT_SOCKET_PATH self.socket_path = socket_path or self.DEFAULT_SOCKET_PATH
self._socket: Optional[socket.socket] = None self._socket: Optional[socket.socket] = None
def connect(self) -> None: def connect(self) -> None:
"""Connect to the GhosttyTabs socket""" """Connect to the cmux socket"""
if self._socket is not None: if self._socket is not None:
return return
if not os.path.exists(self.socket_path): if not os.path.exists(self.socket_path):
raise GhosttyTabsError( raise cmuxError(
f"Socket not found at {self.socket_path}. " f"Socket not found at {self.socket_path}. "
"Is GhosttyTabs running?" "Is cmux running?"
) )
self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
@ -64,7 +64,7 @@ class GhosttyTabs:
self._socket.settimeout(5.0) self._socket.settimeout(5.0)
except socket.error as e: except socket.error as e:
self._socket = None self._socket = None
raise GhosttyTabsError(f"Failed to connect: {e}") raise cmuxError(f"Failed to connect: {e}")
def close(self) -> None: def close(self) -> None:
"""Close the connection""" """Close the connection"""
@ -83,16 +83,16 @@ class GhosttyTabs:
def _send_command(self, command: str) -> str: def _send_command(self, command: str) -> str:
"""Send a command and receive response""" """Send a command and receive response"""
if self._socket is None: if self._socket is None:
raise GhosttyTabsError("Not connected") raise cmuxError("Not connected")
try: try:
self._socket.sendall((command + "\n").encode()) self._socket.sendall((command + "\n").encode())
response = self._socket.recv(8192).decode().strip() response = self._socket.recv(8192).decode().strip()
return response return response
except socket.timeout: except socket.timeout:
raise GhosttyTabsError("Command timed out") raise cmuxError("Command timed out")
except socket.error as e: except socket.error as e:
raise GhosttyTabsError(f"Socket error: {e}") raise cmuxError(f"Socket error: {e}")
def ping(self) -> bool: def ping(self) -> bool:
"""Check if the server is responding""" """Check if the server is responding"""
@ -126,25 +126,25 @@ class GhosttyTabs:
response = self._send_command("new_tab") response = self._send_command("new_tab")
if response.startswith("OK "): if response.startswith("OK "):
return response[3:] return response[3:]
raise GhosttyTabsError(response) raise cmuxError(response)
def new_split(self, direction: str) -> None: def new_split(self, direction: str) -> None:
"""Create a split in the given direction (left/right/up/down).""" """Create a split in the given direction (left/right/up/down)."""
response = self._send_command(f"new_split {direction}") response = self._send_command(f"new_split {direction}")
if not response.startswith("OK"): if not response.startswith("OK"):
raise GhosttyTabsError(response) raise cmuxError(response)
def close_tab(self, tab_id: str) -> None: def close_tab(self, tab_id: str) -> None:
"""Close a tab by ID""" """Close a tab by ID"""
response = self._send_command(f"close_tab {tab_id}") response = self._send_command(f"close_tab {tab_id}")
if not response.startswith("OK"): if not response.startswith("OK"):
raise GhosttyTabsError(response) raise cmuxError(response)
def select_tab(self, tab: str | int) -> None: def select_tab(self, tab: str | int) -> None:
"""Select a tab by ID or index""" """Select a tab by ID or index"""
response = self._send_command(f"select_tab {tab}") response = self._send_command(f"select_tab {tab}")
if not response.startswith("OK"): 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]]: 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.""" """Focus a surface by ID or index in the current tab."""
response = self._send_command(f"focus_surface {surface}") response = self._send_command(f"focus_surface {surface}")
if not response.startswith("OK"): if not response.startswith("OK"):
raise GhosttyTabsError(response) raise cmuxError(response)
def current_tab(self) -> str: def current_tab(self) -> str:
"""Get the current tab's ID""" """Get the current tab's ID"""
response = self._send_command("current_tab") response = self._send_command("current_tab")
if response.startswith("ERROR"): if response.startswith("ERROR"):
raise GhosttyTabsError(response) raise cmuxError(response)
return response return response
def send(self, text: str) -> None: def send(self, text: str) -> None:
@ -195,14 +195,14 @@ class GhosttyTabs:
escaped = text.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") escaped = text.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
response = self._send_command(f"send {escaped}") response = self._send_command(f"send {escaped}")
if not response.startswith("OK"): if not response.startswith("OK"):
raise GhosttyTabsError(response) raise cmuxError(response)
def send_surface(self, surface: str | int, text: str) -> None: def send_surface(self, surface: str | int, text: str) -> None:
"""Send text to a specific surface by ID or index in the current tab.""" """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") escaped = text.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
response = self._send_command(f"send_surface {surface} {escaped}") response = self._send_command(f"send_surface {surface} {escaped}")
if not response.startswith("OK"): if not response.startswith("OK"):
raise GhosttyTabsError(response) raise cmuxError(response)
def send_key(self, key: str) -> None: def send_key(self, key: str) -> None:
""" """
@ -215,13 +215,13 @@ class GhosttyTabs:
""" """
response = self._send_command(f"send_key {key}") response = self._send_command(f"send_key {key}")
if not response.startswith("OK"): if not response.startswith("OK"):
raise GhosttyTabsError(response) raise cmuxError(response)
def send_key_surface(self, surface: str | int, key: str) -> None: 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.""" """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}") response = self._send_command(f"send_key_surface {surface} {key}")
if not response.startswith("OK"): if not response.startswith("OK"):
raise GhosttyTabsError(response) raise cmuxError(response)
def send_line(self, text: str) -> None: def send_line(self, text: str) -> None:
"""Send text followed by Enter""" """Send text followed by Enter"""
@ -241,23 +241,23 @@ class GhosttyTabs:
def main(): def main():
"""CLI interface for ghosttytabs""" """CLI interface for cmux"""
import sys import sys
import argparse 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("command", nargs="?", help="Command to send")
parser.add_argument("args", nargs="*", help="Command arguments") 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") help="Socket path")
args = parser.parse_args() args = parser.parse_args()
try: try:
with GhosttyTabs(args.socket) as client: with cmux(args.socket) as client:
if not args.command: if not args.command:
# Interactive mode # Interactive mode
print("GhosttyTabs CLI (type 'help' for commands, 'quit' to exit)") print("cmux CLI (type 'help' for commands, 'quit' to exit)")
while True: while True:
try: try:
line = input("> ").strip() line = input("> ").strip()
@ -278,7 +278,7 @@ def main():
command += " " + " ".join(args.args) command += " " + " ".join(args.args)
response = client._send_command(command) response = client._send_command(command)
print(response) print(response)
except GhosttyTabsError as e: except cmuxError as e:
print(f"Error: {e}", file=sys.stderr) print(f"Error: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)

View file

@ -1,24 +1,24 @@
#!/bin/bash #!/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 # This tests the actual keyboard input path through the app
set -e set -e
echo "=== GhosttyTabs Keystroke Test ===" echo "=== cmux Keystroke Test ==="
echo "" echo ""
# Check if GhosttyTabs is running # Check if cmux is running
if ! pgrep -x "GhosttyTabs" > /dev/null; then if ! pgrep -x "cmux" > /dev/null; then
echo "Error: GhosttyTabs is not running" echo "Error: cmux is not running"
echo "Please start GhosttyTabs first" echo "Please start cmux first"
exit 1 exit 1
fi fi
echo "GhosttyTabs is running" echo "cmux is running"
echo "" echo ""
# Activate GhosttyTabs # Activate cmux
osascript -e 'tell application "GhosttyTabs" to activate' osascript -e 'tell application "cmux" to activate'
sleep 0.5 sleep 0.5
echo "Test 1: Testing Ctrl+C (SIGINT)" echo "Test 1: Testing Ctrl+C (SIGINT)"
@ -56,7 +56,7 @@ echo " If cat exited, Ctrl+D is working!"
echo "" echo ""
echo "=== Manual Verification Required ===" 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 " 1. The 'sleep 30' command was interrupted by Ctrl+C"
echo " 2. The 'cat' command exited after Ctrl+D" echo " 2. The 'cat' command exited after Ctrl+D"
echo "" echo ""

View file

@ -3,7 +3,7 @@
Automated test for ctrl+enter keybind using real keystrokes. Automated test for ctrl+enter keybind using real keystrokes.
Requires: Requires:
- GhosttyTabs running - cmux running
- Accessibility permissions for System Events (osascript) - Accessibility permissions for System Events (osascript)
- keybind = ctrl+enter=text:\\r (or \\n/\\x0d) configured in Ghostty config - keybind = ctrl+enter=text:\\r (or \\n/\\x0d) configured in Ghostty config
""" """
@ -14,10 +14,10 @@ import time
import subprocess import subprocess
from pathlib import Path 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__))) 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: def run_osascript(script: str) -> None:
@ -54,7 +54,7 @@ def find_config_with_keybind() -> Path | None:
return 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 = Path("/tmp") / f"ghostty_ctrl_enter_{os.getpid()}"
marker.unlink(missing_ok=True) marker.unlink(missing_ok=True)
@ -64,7 +64,7 @@ def test_ctrl_enter_keybind(client: GhosttyTabs) -> tuple[bool, str]:
time.sleep(0.3) time.sleep(0.3)
# Make sure the app is focused for keystrokes # 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) time.sleep(0.2)
# Clear any running command # Clear any running command
@ -94,14 +94,14 @@ def test_ctrl_enter_keybind(client: GhosttyTabs) -> tuple[bool, str]:
def run_tests() -> int: def run_tests() -> int:
print("=" * 60) print("=" * 60)
print("GhosttyTabs Ctrl+Enter Keybind Test") print("cmux Ctrl+Enter Keybind Test")
print("=" * 60) print("=" * 60)
print() print()
socket_path = GhosttyTabs.DEFAULT_SOCKET_PATH socket_path = cmux.DEFAULT_SOCKET_PATH
if not os.path.exists(socket_path): if not os.path.exists(socket_path):
print(f"Error: Socket not found at {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 return 1
config_path = find_config_with_keybind() config_path = find_config_with_keybind()
@ -109,19 +109,19 @@ def run_tests() -> int:
print("Error: Required keybind not found in Ghostty config.") print("Error: Required keybind not found in Ghostty config.")
print("Add a line like:") print("Add a line like:")
print(" keybind = ctrl+enter=text:\\r") 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 return 1
print(f"Using keybind from: {config_path}") print(f"Using keybind from: {config_path}")
print() print()
try: try:
with GhosttyTabs() as client: with cmux() as client:
ok, message = test_ctrl_enter_keybind(client) ok, message = test_ctrl_enter_keybind(client)
status = "" if ok else "" status = "" if ok else ""
print(f"{status} {message}") print(f"{status} {message}")
return 0 if ok else 1 return 0 if ok else 1
except GhosttyTabsError as e: except cmuxError as e:
print(f"Error: {e}") print(f"Error: {e}")
return 1 return 1
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:

View file

@ -1,9 +1,9 @@
#!/usr/bin/env python3 #!/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. This script tests that control signals are properly handled.
Run this script inside the GhosttyTabs terminal. Run this script inside the cmux terminal.
Tests: Tests:
1. Ctrl+C (SIGINT) - Should interrupt a running process 1. Ctrl+C (SIGINT) - Should interrupt a running process
@ -72,10 +72,10 @@ def test_ctrl_d():
def main(): def main():
print("=" * 50) print("=" * 50)
print("GhosttyTabs Control Signal Test") print("cmux Control Signal Test")
print("=" * 50) print("=" * 50)
print("\nThis script tests if Ctrl+C and Ctrl+D work correctly.") 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 # Check if running in a terminal
if not os.isatty(sys.stdin.fileno()): if not os.isatty(sys.stdin.fileno()):

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Test script to verify Ctrl+C and Ctrl+D work correctly in the terminal # 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 set -e

View file

@ -1,12 +1,12 @@
#!/usr/bin/env python3 #!/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: Usage:
python3 test_ctrl_socket.py python3 test_ctrl_socket.py
Requirements: Requirements:
- GhosttyTabs must be running with the socket controller enabled - cmux must be running with the socket controller enabled
""" """
import os import os
@ -15,10 +15,10 @@ import time
import tempfile import tempfile
from pathlib import Path 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__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ghosttytabs import GhosttyTabs, GhosttyTabsError from cmux import cmux, cmuxError
class TestResult: class TestResult:
@ -36,7 +36,7 @@ class TestResult:
self.message = msg self.message = msg
def test_connection(client: GhosttyTabs) -> TestResult: def test_connection(client: cmux) -> TestResult:
"""Test that we can connect and ping the server""" """Test that we can connect and ping the server"""
result = TestResult("Connection") result = TestResult("Connection")
try: try:
@ -49,7 +49,7 @@ def test_connection(client: GhosttyTabs) -> TestResult:
return result return result
def test_ctrl_c(client: GhosttyTabs) -> TestResult: def test_ctrl_c(client: cmux) -> TestResult:
""" """
Test Ctrl+C by: Test Ctrl+C by:
1. Starting sleep command 1. Starting sleep command
@ -88,7 +88,7 @@ def test_ctrl_c(client: GhosttyTabs) -> TestResult:
return result return result
def test_ctrl_d(client: GhosttyTabs) -> TestResult: def test_ctrl_d(client: cmux) -> TestResult:
""" """
Test Ctrl+D by: Test Ctrl+D by:
1. Running cat command 1. Running cat command
@ -127,7 +127,7 @@ def test_ctrl_d(client: GhosttyTabs) -> TestResult:
return result return result
def test_ctrl_c_python(client: GhosttyTabs) -> TestResult: def test_ctrl_c_python(client: cmux) -> TestResult:
""" """
Test Ctrl+C with Python process Test Ctrl+C with Python process
""" """
@ -166,20 +166,20 @@ def test_ctrl_c_python(client: GhosttyTabs) -> TestResult:
def run_tests(): def run_tests():
"""Run all tests""" """Run all tests"""
print("=" * 60) print("=" * 60)
print("GhosttyTabs Ctrl+C/D Automated Tests") print("cmux Ctrl+C/D Automated Tests")
print("=" * 60) print("=" * 60)
print() print()
socket_path = GhosttyTabs.DEFAULT_SOCKET_PATH socket_path = cmux.DEFAULT_SOCKET_PATH
if not os.path.exists(socket_path): if not os.path.exists(socket_path):
print(f"Error: Socket not found at {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 return 1
results = [] results = []
try: try:
with GhosttyTabs() as client: with cmux() as client:
# Test connection # Test connection
print("Testing connection...") print("Testing connection...")
results.append(test_connection(client)) results.append(test_connection(client))
@ -215,7 +215,7 @@ def run_tests():
print(f" {status} {results[-1].message}") print(f" {status} {results[-1].message}")
print() print()
except GhosttyTabsError as e: except cmuxError as e:
print(f"Error: {e}") print(f"Error: {e}")
return 1 return 1