diff --git a/README.md b/README.md index 47daeaa..82cf9ee 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ A simple shortcut menu for macOS 1. Download [Shuttle](http://fitztrev.github.io/shuttle/) 2. Copy to Applications +This fork also supports Ghostty.app. Set `"terminal": "Ghostty.app"` in your Shuttle JSON settings to use it. + ## Help See the [Wiki](https://github.com/fitztrev/shuttle/wiki) pages. diff --git a/Shuttle.xcodeproj/project.pbxproj b/Shuttle.xcodeproj/project.pbxproj index d174fef..b9711d6 100644 --- a/Shuttle.xcodeproj/project.pbxproj +++ b/Shuttle.xcodeproj/project.pbxproj @@ -18,6 +18,9 @@ A124BA191D45572B00218F2F /* iTerm2-stable-current-window.scpt in Resources */ = {isa = PBXBuildFile; fileRef = A124BA161D45572B00218F2F /* iTerm2-stable-current-window.scpt */; }; A124BA1A1D45572B00218F2F /* iTerm2-stable-new-tab-default.scpt in Resources */ = {isa = PBXBuildFile; fileRef = A124BA171D45572B00218F2F /* iTerm2-stable-new-tab-default.scpt */; }; A124BA1B1D45572B00218F2F /* iTerm2-stable-new-window.scpt in Resources */ = {isa = PBXBuildFile; fileRef = A124BA181D45572B00218F2F /* iTerm2-stable-new-window.scpt */; }; + A124BA1C1D45572B00218F2F /* ghostty-current-window.scpt in Resources */ = {isa = PBXBuildFile; fileRef = A124BA1F1D45572B00218F2F /* ghostty-current-window.scpt */; }; + A124BA1D1D45572B00218F2F /* ghostty-new-tab-default.scpt in Resources */ = {isa = PBXBuildFile; fileRef = A124BA201D45572B00218F2F /* ghostty-new-tab-default.scpt */; }; + A124BA1E1D45572B00218F2F /* ghostty-new-window.scpt in Resources */ = {isa = PBXBuildFile; fileRef = A124BA211D45572B00218F2F /* ghostty-new-window.scpt */; }; A12D9BF31BCF2C73004F52A6 /* iTerm2-nightly-current-window.scpt in Resources */ = {isa = PBXBuildFile; fileRef = A12D9BEA1BCF2C73004F52A6 /* iTerm2-nightly-current-window.scpt */; }; A12D9BF41BCF2C73004F52A6 /* iTerm2-nightly-new-tab-default.scpt in Resources */ = {isa = PBXBuildFile; fileRef = A12D9BEB1BCF2C73004F52A6 /* iTerm2-nightly-new-tab-default.scpt */; }; A12D9BF51BCF2C73004F52A6 /* iTerm2-nightly-new-window.scpt in Resources */ = {isa = PBXBuildFile; fileRef = A12D9BEC1BCF2C73004F52A6 /* iTerm2-nightly-new-window.scpt */; }; @@ -48,6 +51,9 @@ A124BA161D45572B00218F2F /* iTerm2-stable-current-window.scpt */ = {isa = PBXFileReference; lastKnownFileType = file; path = "iTerm2-stable-current-window.scpt"; sourceTree = ""; }; A124BA171D45572B00218F2F /* iTerm2-stable-new-tab-default.scpt */ = {isa = PBXFileReference; lastKnownFileType = file; path = "iTerm2-stable-new-tab-default.scpt"; sourceTree = ""; }; A124BA181D45572B00218F2F /* iTerm2-stable-new-window.scpt */ = {isa = PBXFileReference; lastKnownFileType = file; path = "iTerm2-stable-new-window.scpt"; sourceTree = ""; }; + A124BA1F1D45572B00218F2F /* ghostty-current-window.scpt */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ghostty-current-window.scpt"; sourceTree = ""; }; + A124BA201D45572B00218F2F /* ghostty-new-tab-default.scpt */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ghostty-new-tab-default.scpt"; sourceTree = ""; }; + A124BA211D45572B00218F2F /* ghostty-new-window.scpt */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ghostty-new-window.scpt"; sourceTree = ""; }; A12D9BEA1BCF2C73004F52A6 /* iTerm2-nightly-current-window.scpt */ = {isa = PBXFileReference; lastKnownFileType = file; path = "iTerm2-nightly-current-window.scpt"; sourceTree = ""; }; A12D9BEB1BCF2C73004F52A6 /* iTerm2-nightly-new-tab-default.scpt */ = {isa = PBXFileReference; lastKnownFileType = file; path = "iTerm2-nightly-new-tab-default.scpt"; sourceTree = ""; }; A12D9BEC1BCF2C73004F52A6 /* iTerm2-nightly-new-window.scpt */ = {isa = PBXFileReference; lastKnownFileType = file; path = "iTerm2-nightly-new-window.scpt"; sourceTree = ""; }; @@ -120,6 +126,9 @@ A124BA161D45572B00218F2F /* iTerm2-stable-current-window.scpt */, A124BA171D45572B00218F2F /* iTerm2-stable-new-tab-default.scpt */, A124BA181D45572B00218F2F /* iTerm2-stable-new-window.scpt */, + A124BA1F1D45572B00218F2F /* ghostty-current-window.scpt */, + A124BA201D45572B00218F2F /* ghostty-new-tab-default.scpt */, + A124BA211D45572B00218F2F /* ghostty-new-window.scpt */, A12D9BEA1BCF2C73004F52A6 /* iTerm2-nightly-current-window.scpt */, A12D9BEB1BCF2C73004F52A6 /* iTerm2-nightly-new-tab-default.scpt */, A12D9BEC1BCF2C73004F52A6 /* iTerm2-nightly-new-window.scpt */, @@ -256,6 +265,7 @@ buildActionMask = 2147483647; files = ( A12D9BF61BCF2C73004F52A6 /* terminal-current-window.scpt in Resources */, + A124BA1C1D45572B00218F2F /* ghostty-current-window.scpt in Resources */, A12D9BF31BCF2C73004F52A6 /* iTerm2-nightly-current-window.scpt in Resources */, A1B7B9DD1DB53ED200809327 /* Localizable.strings in Resources */, A12D9BF51BCF2C73004F52A6 /* iTerm2-nightly-new-window.scpt in Resources */, @@ -267,12 +277,14 @@ 0ADB3B0F178EF8DB004E9BB9 /* StatusIconAlt@2x.png in Resources */, 0ADB3B0E178EF8DB004E9BB9 /* StatusIcon@2x.png in Resources */, A124BA191D45572B00218F2F /* iTerm2-stable-current-window.scpt in Resources */, + A124BA1D1D45572B00218F2F /* ghostty-new-tab-default.scpt in Resources */, A12D9BF41BCF2C73004F52A6 /* iTerm2-nightly-new-tab-default.scpt in Resources */, C149EC0E15D5214600B1F558 /* Credits.rtf in Resources */, A12D9BF71BCF2C73004F52A6 /* terminal-new-tab-default.scpt in Resources */, C149EC1415D5214600B1F558 /* MainMenu.xib in Resources */, A124BA1B1D45572B00218F2F /* iTerm2-stable-new-window.scpt in Resources */, A124BA1A1D45572B00218F2F /* iTerm2-stable-new-tab-default.scpt in Resources */, + A124BA1E1D45572B00218F2F /* ghostty-new-window.scpt in Resources */, C159DC2815D5DE8000F5DE24 /* shuttle.icns in Resources */, A12D9BF81BCF2C73004F52A6 /* terminal-new-window.scpt in Resources */, 7E74A7C61789CE2F0079E0D2 /* shuttle.default.json in Resources */, diff --git a/Shuttle/AppDelegate.m b/Shuttle/AppDelegate.m index 2f77935..0733601 100644 --- a/Shuttle/AppDelegate.m +++ b/Shuttle/AppDelegate.m @@ -546,6 +546,11 @@ - (void) openHost:(NSMenuItem *) sender { NSString *terminalNewWindow = [[NSBundle mainBundle] pathForResource:@"terminal-new-window" ofType:@"scpt"]; NSString *terminalCurrentWindow = [[NSBundle mainBundle] pathForResource:@"terminal-current-window" ofType:@"scpt"]; NSString *terminalNewTabDefault = [[NSBundle mainBundle] pathForResource:@"terminal-new-tab-default" ofType:@"scpt"]; + + //Set Paths to Ghostty AppleScripts + NSString *ghosttyNewWindow = [[NSBundle mainBundle] pathForResource:@"ghostty-new-window" ofType:@"scpt"]; + NSString *ghosttyCurrentWindow = [[NSBundle mainBundle] pathForResource:@"ghostty-current-window" ofType:@"scpt"]; + NSString *ghosttyNewTabDefault = [[NSBundle mainBundle] pathForResource:@"ghostty-new-tab-default" ofType:@"scpt"]; //Set Path to virtual with screen AppleScripts NSString *terminalVirtualWithScreen = [[NSBundle mainBundle] pathForResource:@"virtual-with-screen" ofType:@"scpt"]; @@ -564,11 +569,26 @@ - (void) openHost:(NSMenuItem *) sender { passParameters = @[escapedObject, terminalTitle]; } // Check if Url - if (url) + if (url && [url scheme]) { [[NSWorkspace sharedWorkspace] openURL:url]; } + //If the JSON file is set to use Ghostty + else if ( [terminalPref rangeOfString: @"ghostty"].location !=NSNotFound ) { + if ( [terminalWindow isEqualToString:@"new"] ) { + [self runScript:ghosttyNewWindow handler:handlerName parameters:passParameters]; + } + if ( [terminalWindow isEqualToString:@"current"] ) { + [self runScript:ghosttyCurrentWindow handler:handlerName parameters:passParameters]; + } + if ( [terminalWindow isEqualToString:@"tab"] ) { + [self runScript:ghosttyNewTabDefault handler:handlerName parameters:passParameters]; + } + if ( [terminalWindow isEqualToString:@"virtual"] ) { + [self runScript:terminalVirtualWithScreen handler:handlerName parameters:passParameters]; + } + } //If the JSON file is set to use iTerm else if ( [terminalPref rangeOfString: @"iterm"].location !=NSNotFound ) { diff --git a/Shuttle/Shuttle.entitlements b/Shuttle/Shuttle.entitlements index 4a11143..7e9a5cc 100644 --- a/Shuttle/Shuttle.entitlements +++ b/Shuttle/Shuttle.entitlements @@ -6,6 +6,7 @@ com.apple.terminal com.googlecode.iterm2 + com.mitchellh.ghostty diff --git a/Shuttle/apple-scpt/ghostty-current-window.scpt b/Shuttle/apple-scpt/ghostty-current-window.scpt new file mode 100644 index 0000000..03a9b26 Binary files /dev/null and b/Shuttle/apple-scpt/ghostty-current-window.scpt differ diff --git a/Shuttle/apple-scpt/ghostty-new-tab-default.scpt b/Shuttle/apple-scpt/ghostty-new-tab-default.scpt new file mode 100644 index 0000000..9487175 Binary files /dev/null and b/Shuttle/apple-scpt/ghostty-new-tab-default.scpt differ diff --git a/Shuttle/apple-scpt/ghostty-new-window.scpt b/Shuttle/apple-scpt/ghostty-new-window.scpt new file mode 100644 index 0000000..3433f0f Binary files /dev/null and b/Shuttle/apple-scpt/ghostty-new-window.scpt differ diff --git a/Shuttle/shuttle.default.json b/Shuttle/shuttle.default.json index 51d1e30..8a2d16d 100644 --- a/Shuttle/shuttle.default.json +++ b/Shuttle/shuttle.default.json @@ -1,6 +1,6 @@ { "_comments": [ - "Valid terminals include: 'Terminal.app' or 'iTerm'", + "Valid terminals include: 'Terminal.app', 'iTerm', or 'Ghostty.app'", "In the editor value change 'default' to 'nano', 'vi', or another terminal based editor.", "Hosts will also be read from your ~/.ssh/config or /etc/ssh_config file, if available", "For more information on how to configure, please see http://fitztrev.github.io/shuttle/" @@ -57,4 +57,4 @@ ] } - \ No newline at end of file + diff --git a/apple-scripts/ghostty/ghostty-current-window.applescript b/apple-scripts/ghostty/ghostty-current-window.applescript new file mode 100644 index 0000000..92b50f3 --- /dev/null +++ b/apple-scripts/ghostty/ghostty-current-window.applescript @@ -0,0 +1,28 @@ +--for testing uncomment the "on run" block +--on run +-- set argsCmd to "ps aux | grep [s]sh" +-- set argsTheme to "Homebrew" +-- set argsTitle to "Custom title" +-- scriptRun(argsCmd, argsTheme, argsTitle) +--end run + +on scriptRun(argsCmd, argsTheme, argsTitle) + set withCmd to (argsCmd) + CommandRun(withCmd) +end scriptRun + +on CommandRun(withCmd) + tell application "/Applications/Ghostty.app" + if it is not running or (count windows) is 0 then + set surfaceConfig to new surface configuration + set targetWindow to new window with configuration surfaceConfig + set targetTerminal to focused terminal of selected tab of targetWindow + else + set targetWindow to front window + set targetTerminal to focused terminal of selected tab of targetWindow + end if + input text withCmd to targetTerminal + send key "enter" to targetTerminal + focus targetTerminal + end tell +end CommandRun diff --git a/apple-scripts/ghostty/ghostty-new-tab-default.applescript b/apple-scripts/ghostty/ghostty-new-tab-default.applescript new file mode 100644 index 0000000..68d9b4e --- /dev/null +++ b/apple-scripts/ghostty/ghostty-new-tab-default.applescript @@ -0,0 +1,29 @@ +--for testing uncomment the "on run" block +--on run +-- set argsCmd to "ps aux | grep [s]sh" +-- set argsTheme to "Homebrew" +-- set argsTitle to "Custom title" +-- scriptRun(argsCmd, argsTheme, argsTitle) +--end run + +on scriptRun(argsCmd, argsTheme, argsTitle) + set withCmd to (argsCmd) + CommandRun(withCmd) +end scriptRun + +on CommandRun(withCmd) + tell application "/Applications/Ghostty.app" + set surfaceConfig to new surface configuration + if it is not running or (count windows) is 0 then + set targetWindow to new window with configuration surfaceConfig + set targetTerminal to focused terminal of selected tab of targetWindow + else + set targetWindow to front window + set targetTab to new tab in targetWindow with configuration surfaceConfig + set targetTerminal to focused terminal of targetTab + end if + input text withCmd to targetTerminal + send key "enter" to targetTerminal + focus targetTerminal + end tell +end CommandRun diff --git a/apple-scripts/ghostty/ghostty-new-window.applescript b/apple-scripts/ghostty/ghostty-new-window.applescript new file mode 100644 index 0000000..38cd9e7 --- /dev/null +++ b/apple-scripts/ghostty/ghostty-new-window.applescript @@ -0,0 +1,23 @@ +--for testing uncomment the "on run" block +--on run +-- set argsCmd to "ps aux | grep [s]sh" +-- set argsTheme to "Homebrew" +-- set argsTitle to "Custom title" +-- scriptRun(argsCmd, argsTheme, argsTitle) +--end run + +on scriptRun(argsCmd, argsTheme, argsTitle) + set withCmd to (argsCmd) + CommandRun(withCmd) +end scriptRun + +on CommandRun(withCmd) + tell application "/Applications/Ghostty.app" + set surfaceConfig to new surface configuration + set newWindow to new window with configuration surfaceConfig + set newTerminal to focused terminal of selected tab of newWindow + input text withCmd to newTerminal + send key "enter" to newTerminal + focus newTerminal + end tell +end CommandRun diff --git a/tests/test_ghostty_support.py b/tests/test_ghostty_support.py new file mode 100644 index 0000000..3be3aa8 --- /dev/null +++ b/tests/test_ghostty_support.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def read(path): + return (ROOT / path).read_text() + + +def test_default_config_mentions_ghostty(): + config = read("Shuttle/shuttle.default.json") + assert "Ghostty.app" in config + + +def test_app_delegate_routes_ghostty_to_scripts(): + app_delegate = read("Shuttle/AppDelegate.m") + assert 'rangeOfString: @"ghostty"' in app_delegate + assert 'pathForResource:@"ghostty-new-window"' in app_delegate + assert 'pathForResource:@"ghostty-current-window"' in app_delegate + assert 'pathForResource:@"ghostty-new-tab-default"' in app_delegate + + +def test_shell_commands_are_not_treated_as_urls(): + app_delegate = read("Shuttle/AppDelegate.m") + assert "if (url && [url scheme])" in app_delegate + + +def test_ghostty_scripts_are_bundled_resources(): + project = read("Shuttle.xcodeproj/project.pbxproj") + for script in [ + "ghostty-new-window.scpt", + "ghostty-current-window.scpt", + "ghostty-new-tab-default.scpt", + ]: + assert script in project + + +def test_ghostty_has_apple_event_entitlement(): + entitlements = read("Shuttle/Shuttle.entitlements") + assert "com.mitchellh.ghostty" in entitlements + + +def test_ghostty_source_scripts_use_applescript_api(): + for source in [ + "apple-scripts/ghostty/ghostty-new-window.applescript", + "apple-scripts/ghostty/ghostty-current-window.applescript", + "apple-scripts/ghostty/ghostty-new-tab-default.applescript", + ]: + script = read(source) + assert 'tell application "/Applications/Ghostty.app"' in script + assert "input text" in script + assert 'send key "enter"' in script + + +if __name__ == "__main__": + for name, value in sorted(globals().items()): + if name.startswith("test_") and callable(value): + value()