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.
@ -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

View file

@ -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 = "<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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
@ -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;

View file

@ -3,7 +3,7 @@
<BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES">
<BuildActionEntries>
<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>
</BuildActionEntries>
</BuildAction>
@ -14,17 +14,17 @@
</TestableReference>
</Testables>
<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>
</TestAction>
<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">
<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>
</LaunchAction>
<ProfileAction buildConfiguration="Release" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES">
<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>
</ProfileAction>
<AnalyzeAction buildConfiguration="Release"/>

View file

@ -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"
)

View file

@ -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

View file

@ -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)

View file

@ -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")
}
}

View file

@ -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] = [:]

View file

@ -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] = []

View file

@ -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)
}
}

View file

@ -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

View file

@ -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)

View file

@ -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 ""

View file

@ -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:

View file

@ -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()):

View file

@ -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

View file

@ -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