Rename to cmux and add About panel
This commit is contained in:
parent
f6034a5979
commit
8320d5805a
18 changed files with 164 additions and 125 deletions
32
CLAUDE.md
32
CLAUDE.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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"/>
|
||||||
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
# GhosttyTabs
|
# cmux
|
||||||
|
|
||||||
Vertical tabs for Ghostty on macOS, built on libghostty.
|
Vertical tabs for Ghostty on macOS, built on libghostty.
|
||||||
|
|
||||||
[](releases/latest/download/GhosttyTabs-macos.zip)
|
[](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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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] = [:]
|
||||||
|
|
|
||||||
|
|
@ -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] = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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()):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue