Update ghostty to v1.3.0 (#1142)
* Update ghostty to v1.3.0 * Add bell handling and AppleScript support * Add zsh shell integration handoff test * Fix Ghostty zsh integration handoff in cmux * Add terminal keypress notification dismissal test * Dismiss terminal notifications on keypress * Address PR review feedback * Tighten notification dismissal regression test * Pin GhosttyKit checksum for latest ghostty
This commit is contained in:
parent
55b619b538
commit
dea60ea71c
14 changed files with 1542 additions and 9 deletions
|
|
@ -23,6 +23,7 @@
|
||||||
A5001501 /* UITestRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001511 /* UITestRecorder.swift */; };
|
A5001501 /* UITestRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001511 /* UITestRecorder.swift */; };
|
||||||
A5001226 /* SocketControlSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001225 /* SocketControlSettings.swift */; };
|
A5001226 /* SocketControlSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001225 /* SocketControlSettings.swift */; };
|
||||||
A5001601 /* SentryHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001600 /* SentryHelper.swift */; };
|
A5001601 /* SentryHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001600 /* SentryHelper.swift */; };
|
||||||
|
A5001621 /* AppleScriptSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001620 /* AppleScriptSupport.swift */; };
|
||||||
A5001400 /* Panel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001410 /* Panel.swift */; };
|
A5001400 /* Panel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001410 /* Panel.swift */; };
|
||||||
A5001401 /* TerminalPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001411 /* TerminalPanel.swift */; };
|
A5001401 /* TerminalPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001411 /* TerminalPanel.swift */; };
|
||||||
A5001402 /* BrowserPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001412 /* BrowserPanel.swift */; };
|
A5001402 /* BrowserPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001412 /* BrowserPanel.swift */; };
|
||||||
|
|
@ -94,6 +95,7 @@
|
||||||
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008382 /* CommandPaletteSearchEngineTests.swift */; };
|
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008382 /* CommandPaletteSearchEngineTests.swift */; };
|
||||||
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000001 /* Localizable.xcstrings */; };
|
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000001 /* Localizable.xcstrings */; };
|
||||||
DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000002 /* InfoPlist.xcstrings */; };
|
DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000002 /* InfoPlist.xcstrings */; };
|
||||||
|
A5001623 /* cmux.sdef in Resources */ = {isa = PBXBuildFile; fileRef = A5001622 /* cmux.sdef */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
|
@ -165,6 +167,7 @@
|
||||||
A5001018 /* cmux-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "cmux-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>"; };
|
||||||
A5001600 /* SentryHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryHelper.swift; sourceTree = "<group>"; };
|
A5001600 /* SentryHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryHelper.swift; sourceTree = "<group>"; };
|
||||||
|
A5001620 /* AppleScriptSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleScriptSupport.swift; sourceTree = "<group>"; };
|
||||||
A5001510 /* CmuxWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/CmuxWebView.swift; sourceTree = "<group>"; };
|
A5001510 /* CmuxWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/CmuxWebView.swift; sourceTree = "<group>"; };
|
||||||
A5001511 /* UITestRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestRecorder.swift; sourceTree = "<group>"; };
|
A5001511 /* UITestRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestRecorder.swift; sourceTree = "<group>"; };
|
||||||
A5001520 /* PostHogAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalytics.swift; sourceTree = "<group>"; };
|
A5001520 /* PostHogAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalytics.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -235,6 +238,7 @@
|
||||||
A5008382 /* CommandPaletteSearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSearchEngineTests.swift; sourceTree = "<group>"; };
|
A5008382 /* CommandPaletteSearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSearchEngineTests.swift; sourceTree = "<group>"; };
|
||||||
DA7A10CA710E000000000001 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
DA7A10CA710E000000000001 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||||
DA7A10CA710E000000000002 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
DA7A10CA710E000000000002 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
||||||
|
A5001622 /* cmux.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.sdef; path = cmux.sdef; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
|
@ -284,6 +288,7 @@
|
||||||
A5002000 /* THIRD_PARTY_LICENSES.md in Resources */,
|
A5002000 /* THIRD_PARTY_LICENSES.md in Resources */,
|
||||||
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */,
|
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */,
|
||||||
DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */,
|
DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */,
|
||||||
|
A5001623 /* cmux.sdef in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -364,6 +369,7 @@
|
||||||
A5001541 /* PortScanner.swift */,
|
A5001541 /* PortScanner.swift */,
|
||||||
A5001225 /* SocketControlSettings.swift */,
|
A5001225 /* SocketControlSettings.swift */,
|
||||||
A5001600 /* SentryHelper.swift */,
|
A5001600 /* SentryHelper.swift */,
|
||||||
|
A5001620 /* AppleScriptSupport.swift */,
|
||||||
A5001090 /* AppDelegate.swift */,
|
A5001090 /* AppDelegate.swift */,
|
||||||
A5001091 /* NotificationsPage.swift */,
|
A5001091 /* NotificationsPage.swift */,
|
||||||
A5001092 /* TerminalNotificationStore.swift */,
|
A5001092 /* TerminalNotificationStore.swift */,
|
||||||
|
|
@ -415,6 +421,7 @@
|
||||||
C1ADE00001A1B2C3D4E5F719 /* claude */,
|
C1ADE00001A1B2C3D4E5F719 /* claude */,
|
||||||
DA7A10CA710E000000000001 /* Localizable.xcstrings */,
|
DA7A10CA710E000000000001 /* Localizable.xcstrings */,
|
||||||
DA7A10CA710E000000000002 /* InfoPlist.xcstrings */,
|
DA7A10CA710E000000000002 /* InfoPlist.xcstrings */,
|
||||||
|
A5001622 /* cmux.sdef */,
|
||||||
);
|
);
|
||||||
path = Resources;
|
path = Resources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -631,6 +638,7 @@
|
||||||
A5001540 /* PortScanner.swift in Sources */,
|
A5001540 /* PortScanner.swift in Sources */,
|
||||||
A5001226 /* SocketControlSettings.swift in Sources */,
|
A5001226 /* SocketControlSettings.swift in Sources */,
|
||||||
A5001601 /* SentryHelper.swift in Sources */,
|
A5001601 /* SentryHelper.swift in Sources */,
|
||||||
|
A5001621 /* AppleScriptSupport.swift in Sources */,
|
||||||
A5001093 /* AppDelegate.swift in Sources */,
|
A5001093 /* AppDelegate.swift in Sources */,
|
||||||
A5001094 /* NotificationsPage.swift in Sources */,
|
A5001094 /* NotificationsPage.swift in Sources */,
|
||||||
A5001095 /* TerminalNotificationStore.swift in Sources */,
|
A5001095 /* TerminalNotificationStore.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@
|
||||||
</array>
|
</array>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
|
<key>NSAppleScriptEnabled</key>
|
||||||
|
<true/>
|
||||||
|
<key>OSAScriptingDefinition</key>
|
||||||
|
<string>cmux.sdef</string>
|
||||||
<key>NSServices</key>
|
<key>NSServices</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,193 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"applescript.error.disabled": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "AppleScript is disabled by the macos-applescript configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "macos-applescript の設定で AppleScript は無効になっています。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"applescript.error.failedToCreateSplit": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Failed to create split."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "分割の作成に失敗しました。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"applescript.error.failedToCreateWindow": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Failed to create window."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "ウインドウの作成に失敗しました。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"applescript.error.failedToCreateWorkspace": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Failed to create workspace."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "ワークスペースの作成に失敗しました。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"applescript.error.missingAction": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Missing action string."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "アクション文字列がありません。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"applescript.error.missingInputText": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Missing input text."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "入力するテキストがありません。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"applescript.error.missingSplitDirection": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Missing or unknown split direction."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "分割方向がないか、不明です。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"applescript.error.missingTerminalTarget": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Missing terminal target."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "対象のターミナルがありません。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"applescript.error.terminalUnavailable": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Terminal is no longer available."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "ターミナルはもう利用できません。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"applescript.error.windowUnavailable": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Window is no longer available."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "ウインドウはもう利用できません。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"applescript.error.workspaceUnavailable": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Workspace is no longer available."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "ワークスペースはもう利用できません。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"about.build": {
|
"about.build": {
|
||||||
"extractionState": "manual",
|
"extractionState": "manual",
|
||||||
"localizations": {
|
"localizations": {
|
||||||
|
|
|
||||||
192
Resources/cmux.sdef
Normal file
192
Resources/cmux.sdef
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
|
||||||
|
|
||||||
|
<dictionary title="cmux Scripting Dictionary">
|
||||||
|
<suite name="cmux Suite" code="Cmux" description="cmux scripting support.">
|
||||||
|
<class name="application" code="capp" description="The cmux application.">
|
||||||
|
<cocoa class="NSApplication"/>
|
||||||
|
<property name="name" code="pnam" type="text" access="r" description="The name of the application."/>
|
||||||
|
<property name="frontmost" code="pisf" type="boolean" access="r" description="Is this the active application?">
|
||||||
|
<cocoa key="isActive"/>
|
||||||
|
</property>
|
||||||
|
<property name="front window" code="CMFW" type="window" access="r" description="The frontmost cmux window.">
|
||||||
|
<cocoa key="frontWindow"/>
|
||||||
|
</property>
|
||||||
|
<property name="version" code="vers" type="text" access="r" description="The version number of the application."/>
|
||||||
|
<responds-to command="perform action">
|
||||||
|
<cocoa method="handlePerformActionScriptCommand:"/>
|
||||||
|
</responds-to>
|
||||||
|
<responds-to command="new window">
|
||||||
|
<cocoa method="handleNewWindowScriptCommand:"/>
|
||||||
|
</responds-to>
|
||||||
|
<responds-to command="new tab">
|
||||||
|
<cocoa method="handleNewTabScriptCommand:"/>
|
||||||
|
</responds-to>
|
||||||
|
<responds-to command="quit">
|
||||||
|
<cocoa method="handleQuitScriptCommand:"/>
|
||||||
|
</responds-to>
|
||||||
|
|
||||||
|
<element type="window" access="r">
|
||||||
|
<cocoa key="scriptWindows"/>
|
||||||
|
</element>
|
||||||
|
|
||||||
|
<element type="terminal" access="r">
|
||||||
|
<cocoa key="terminals"/>
|
||||||
|
</element>
|
||||||
|
</class>
|
||||||
|
|
||||||
|
<class name="window" code="CMwn" plural="windows" description="A cmux window containing one or more workspaces.">
|
||||||
|
<cocoa class="CmuxScriptWindow"/>
|
||||||
|
<property name="id" code="ID " type="text" access="r" description="Stable ID for this window."/>
|
||||||
|
<property name="name" code="pnam" type="text" access="r" description="The title of the window.">
|
||||||
|
<cocoa key="title"/>
|
||||||
|
</property>
|
||||||
|
<property name="selected tab" code="CMsT" type="tab" access="r" description="The selected workspace in this window.">
|
||||||
|
<cocoa key="selectedTab"/>
|
||||||
|
</property>
|
||||||
|
<responds-to command="activate window">
|
||||||
|
<cocoa method="handleActivateWindowCommand:"/>
|
||||||
|
</responds-to>
|
||||||
|
<responds-to command="close window">
|
||||||
|
<cocoa method="handleCloseWindowCommand:"/>
|
||||||
|
</responds-to>
|
||||||
|
<element type="tab" access="r">
|
||||||
|
<cocoa key="tabs"/>
|
||||||
|
</element>
|
||||||
|
<element type="terminal" access="r">
|
||||||
|
<cocoa key="terminals"/>
|
||||||
|
</element>
|
||||||
|
</class>
|
||||||
|
|
||||||
|
<class name="tab" code="CMtb" plural="tabs" description="A cmux workspace.">
|
||||||
|
<cocoa class="CmuxScriptTab"/>
|
||||||
|
<property name="id" code="ID " type="text" access="r" description="Stable ID for this workspace."/>
|
||||||
|
<property name="name" code="pnam" type="text" access="r" description="The title of the workspace.">
|
||||||
|
<cocoa key="title"/>
|
||||||
|
</property>
|
||||||
|
<property name="index" code="pidx" type="integer" access="r" description="1-based index of this workspace in its window."/>
|
||||||
|
<property name="selected" code="CMsl" type="boolean" access="r" description="Whether this workspace is selected in its window."/>
|
||||||
|
<property name="focused terminal" code="CMfT" type="terminal" access="r" description="The currently focused terminal panel in this workspace.">
|
||||||
|
<cocoa key="focusedTerminal"/>
|
||||||
|
</property>
|
||||||
|
<responds-to command="select tab">
|
||||||
|
<cocoa method="handleSelectTabCommand:"/>
|
||||||
|
</responds-to>
|
||||||
|
<responds-to command="close tab">
|
||||||
|
<cocoa method="handleCloseTabCommand:"/>
|
||||||
|
</responds-to>
|
||||||
|
<element type="terminal" access="r">
|
||||||
|
<cocoa key="terminals"/>
|
||||||
|
</element>
|
||||||
|
</class>
|
||||||
|
|
||||||
|
<class name="terminal" code="CMtr" plural="terminals" description="An individual terminal panel.">
|
||||||
|
<cocoa class="CmuxScriptTerminal"/>
|
||||||
|
<property name="id" code="ID " type="text" access="r" description="Stable ID for this terminal panel."/>
|
||||||
|
<property name="name" code="pnam" type="text" access="r" description="Current terminal title.">
|
||||||
|
<cocoa key="title"/>
|
||||||
|
</property>
|
||||||
|
<property name="working directory" code="CMwd" type="text" access="r" description="Current working directory for the terminal process.">
|
||||||
|
<cocoa key="workingDirectory"/>
|
||||||
|
</property>
|
||||||
|
<responds-to command="split">
|
||||||
|
<cocoa method="handleSplitCommand:"/>
|
||||||
|
</responds-to>
|
||||||
|
<responds-to command="focus">
|
||||||
|
<cocoa method="handleFocusCommand:"/>
|
||||||
|
</responds-to>
|
||||||
|
<responds-to command="close">
|
||||||
|
<cocoa method="handleCloseCommand:"/>
|
||||||
|
</responds-to>
|
||||||
|
</class>
|
||||||
|
|
||||||
|
<enumeration name="split direction" code="CMSD" description="Direction for a new split.">
|
||||||
|
<enumerator name="right" code="GSrt" description="Split to the right."/>
|
||||||
|
<enumerator name="left" code="GSlf" description="Split to the left."/>
|
||||||
|
<enumerator name="down" code="GSdn" description="Split downward."/>
|
||||||
|
<enumerator name="up" code="GSup" description="Split upward."/>
|
||||||
|
</enumeration>
|
||||||
|
|
||||||
|
<command name="perform action" code="CmuxPfAc" description="Perform a Ghostty action string on a terminal.">
|
||||||
|
<direct-parameter type="text" description="The Ghostty action string."/>
|
||||||
|
<parameter name="on" code="CMoT" type="terminal" description="Target terminal.">
|
||||||
|
<cocoa key="on"/>
|
||||||
|
</parameter>
|
||||||
|
<result type="boolean" description="True when the action was performed."/>
|
||||||
|
</command>
|
||||||
|
|
||||||
|
<command name="new window" code="CmuxNWin" description="Create a new cmux window.">
|
||||||
|
<result type="window" description="The newly created window."/>
|
||||||
|
</command>
|
||||||
|
|
||||||
|
<command name="new tab" code="CmuxNTab" description="Create a new workspace.">
|
||||||
|
<parameter name="in" code="CMtW" type="window" optional="yes" description="Target window for the new workspace.">
|
||||||
|
<cocoa key="window"/>
|
||||||
|
</parameter>
|
||||||
|
<result type="tab" description="The newly created workspace."/>
|
||||||
|
</command>
|
||||||
|
|
||||||
|
<command name="split" code="CmuxSplt" description="Split a terminal in the given direction.">
|
||||||
|
<direct-parameter type="specifier" description="The terminal to split."/>
|
||||||
|
<parameter name="direction" code="CMpd" type="split direction" description="The direction to split.">
|
||||||
|
<cocoa key="direction"/>
|
||||||
|
</parameter>
|
||||||
|
<result type="terminal" description="The newly created terminal."/>
|
||||||
|
</command>
|
||||||
|
|
||||||
|
<command name="focus" code="CmuxFcus" description="Focus a terminal, bringing its window to the front.">
|
||||||
|
<direct-parameter type="specifier" description="The terminal to focus."/>
|
||||||
|
</command>
|
||||||
|
|
||||||
|
<command name="close" code="CmuxClos" description="Close a terminal.">
|
||||||
|
<direct-parameter type="specifier" description="The terminal to close."/>
|
||||||
|
</command>
|
||||||
|
|
||||||
|
<command name="activate window" code="CmuxAcWn" description="Activate a cmux window, bringing it to the front.">
|
||||||
|
<direct-parameter type="specifier" description="The window to activate."/>
|
||||||
|
</command>
|
||||||
|
|
||||||
|
<command name="select tab" code="CmuxSlTb" description="Select a workspace in its window.">
|
||||||
|
<direct-parameter type="specifier" description="The workspace to select."/>
|
||||||
|
</command>
|
||||||
|
|
||||||
|
<command name="close tab" code="CmuxClTb" description="Close a workspace.">
|
||||||
|
<direct-parameter type="specifier" description="The workspace to close."/>
|
||||||
|
</command>
|
||||||
|
|
||||||
|
<command name="close window" code="CmuxClWn" description="Close a window.">
|
||||||
|
<direct-parameter type="specifier" description="The window to close."/>
|
||||||
|
</command>
|
||||||
|
|
||||||
|
<command name="input text" code="CmuxInTx" description="Input text to a terminal as if it was pasted.">
|
||||||
|
<cocoa class="CmuxScriptInputTextCommand"/>
|
||||||
|
<direct-parameter type="text" description="The text to input."/>
|
||||||
|
<parameter name="to" code="CMiT" type="terminal" description="The terminal to input text to.">
|
||||||
|
<cocoa key="terminal"/>
|
||||||
|
</parameter>
|
||||||
|
</command>
|
||||||
|
</suite>
|
||||||
|
|
||||||
|
<suite name="Standard Suite" code="????" description="Common classes and commands for all applications.">
|
||||||
|
<command name="count" code="corecnte" description="Return the number of elements of a particular class within an object.">
|
||||||
|
<cocoa class="NSCountCommand"/>
|
||||||
|
<access-group identifier="*"/>
|
||||||
|
<direct-parameter type="specifier" requires-access="r" description="The objects to be counted."/>
|
||||||
|
<parameter name="each" code="kocl" type="type" optional="yes" description="The class of objects to be counted." hidden="yes">
|
||||||
|
<cocoa key="ObjectClass"/>
|
||||||
|
</parameter>
|
||||||
|
<result type="integer" description="The count."/>
|
||||||
|
</command>
|
||||||
|
|
||||||
|
<command name="exists" code="coredoex" description="Verify that an object exists.">
|
||||||
|
<cocoa class="NSExistsCommand"/>
|
||||||
|
<access-group identifier="*"/>
|
||||||
|
<direct-parameter type="any" requires-access="r" description="The object(s) to check."/>
|
||||||
|
<result type="boolean" description="Did the object(s) exist?"/>
|
||||||
|
</command>
|
||||||
|
|
||||||
|
<command name="quit" code="aevtquit" description="Quit the application.">
|
||||||
|
<cocoa class="NSQuitCommand"/>
|
||||||
|
</command>
|
||||||
|
</suite>
|
||||||
|
</dictionary>
|
||||||
|
|
@ -13,9 +13,7 @@
|
||||||
# - CMUX_ZSH_ZDOTDIR (set by cmux when it overwrote a user-provided ZDOTDIR)
|
# - CMUX_ZSH_ZDOTDIR (set by cmux when it overwrote a user-provided ZDOTDIR)
|
||||||
# - unset (zsh treats unset ZDOTDIR as $HOME)
|
# - unset (zsh treats unset ZDOTDIR as $HOME)
|
||||||
|
|
||||||
builtin typeset _cmux_had_ghostty_zdotdir=0
|
|
||||||
if [[ -n "${GHOSTTY_ZSH_ZDOTDIR+X}" ]]; then
|
if [[ -n "${GHOSTTY_ZSH_ZDOTDIR+X}" ]]; then
|
||||||
_cmux_had_ghostty_zdotdir=1
|
|
||||||
builtin export ZDOTDIR="$GHOSTTY_ZSH_ZDOTDIR"
|
builtin export ZDOTDIR="$GHOSTTY_ZSH_ZDOTDIR"
|
||||||
builtin unset GHOSTTY_ZSH_ZDOTDIR
|
builtin unset GHOSTTY_ZSH_ZDOTDIR
|
||||||
elif [[ -n "${CMUX_ZSH_ZDOTDIR+X}" ]]; then
|
elif [[ -n "${CMUX_ZSH_ZDOTDIR+X}" ]]; then
|
||||||
|
|
@ -33,9 +31,10 @@ fi
|
||||||
if [[ -o interactive ]]; then
|
if [[ -o interactive ]]; then
|
||||||
# We overwrote GhosttyKit's injected ZDOTDIR, so manually load Ghostty's
|
# We overwrote GhosttyKit's injected ZDOTDIR, so manually load Ghostty's
|
||||||
# zsh integration if available.
|
# zsh integration if available.
|
||||||
# Guard on GHOSTTY_ZSH_ZDOTDIR being set by Ghostty. When users configure
|
#
|
||||||
# shell-integration=none, Ghostty does not set this and we must skip.
|
# We can't rely on GHOSTTY_ZSH_ZDOTDIR here because Ghostty's own zsh
|
||||||
if [[ "$_cmux_had_ghostty_zdotdir" == "1" && -n "${GHOSTTY_RESOURCES_DIR:-}" ]]; then
|
# bootstrap unsets it before chaining into this cmux wrapper.
|
||||||
|
if [[ "${CMUX_LOAD_GHOSTTY_ZSH_INTEGRATION:-0}" == "1" && -n "${GHOSTTY_RESOURCES_DIR:-}" ]]; then
|
||||||
builtin typeset _cmux_ghostty="$GHOSTTY_RESOURCES_DIR/shell-integration/zsh/ghostty-integration"
|
builtin typeset _cmux_ghostty="$GHOSTTY_RESOURCES_DIR/shell-integration/zsh/ghostty-integration"
|
||||||
[[ -r "$_cmux_ghostty" ]] && builtin source -- "$_cmux_ghostty"
|
[[ -r "$_cmux_ghostty" ]] && builtin source -- "$_cmux_ghostty"
|
||||||
fi
|
fi
|
||||||
|
|
@ -47,5 +46,5 @@ fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
builtin unset _cmux_file _cmux_ghostty _cmux_integ _cmux_had_ghostty_zdotdir
|
builtin unset _cmux_file _cmux_ghostty _cmux_integ
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1490,6 +1490,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ScriptableMainWindowState {
|
||||||
|
let windowId: UUID
|
||||||
|
let tabManager: TabManager
|
||||||
|
let window: NSWindow?
|
||||||
|
}
|
||||||
|
|
||||||
struct SessionDisplayGeometry {
|
struct SessionDisplayGeometry {
|
||||||
let displayID: UInt32?
|
let displayID: UInt32?
|
||||||
let frame: CGRect
|
let frame: CGRect
|
||||||
|
|
@ -3414,6 +3420,86 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
||||||
windowForMainWindowId(windowId)
|
windowForMainWindowId(windowId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scriptableMainWindows() -> [ScriptableMainWindowState] {
|
||||||
|
var results: [ScriptableMainWindowState] = []
|
||||||
|
var seen: Set<UUID> = []
|
||||||
|
|
||||||
|
for window in NSApp.orderedWindows {
|
||||||
|
guard let context = contextForMainTerminalWindow(window, reindex: false) else { continue }
|
||||||
|
guard seen.insert(context.windowId).inserted else { continue }
|
||||||
|
results.append(
|
||||||
|
ScriptableMainWindowState(
|
||||||
|
windowId: context.windowId,
|
||||||
|
tabManager: context.tabManager,
|
||||||
|
window: context.window ?? windowForMainWindowId(context.windowId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining = mainWindowContexts.values
|
||||||
|
.sorted { $0.windowId.uuidString < $1.windowId.uuidString }
|
||||||
|
.filter { seen.insert($0.windowId).inserted }
|
||||||
|
|
||||||
|
for context in remaining {
|
||||||
|
results.append(
|
||||||
|
ScriptableMainWindowState(
|
||||||
|
windowId: context.windowId,
|
||||||
|
tabManager: context.tabManager,
|
||||||
|
window: context.window ?? windowForMainWindowId(context.windowId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func scriptableMainWindow(windowId: UUID) -> ScriptableMainWindowState? {
|
||||||
|
guard let context = mainWindowContexts.values.first(where: { $0.windowId == windowId }) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ScriptableMainWindowState(
|
||||||
|
windowId: context.windowId,
|
||||||
|
tabManager: context.tabManager,
|
||||||
|
window: context.window ?? windowForMainWindowId(context.windowId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scriptableMainWindowForTab(_ tabId: UUID) -> ScriptableMainWindowState? {
|
||||||
|
guard let context = contextContainingTabId(tabId) else { return nil }
|
||||||
|
return ScriptableMainWindowState(
|
||||||
|
windowId: context.windowId,
|
||||||
|
tabManager: context.tabManager,
|
||||||
|
window: context.window ?? windowForMainWindowId(context.windowId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func focusScriptableMainWindow(windowId: UUID, bringToFront shouldBringToFront: Bool) -> Bool {
|
||||||
|
guard let state = scriptableMainWindow(windowId: windowId),
|
||||||
|
let window = state.window else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
setActiveMainWindow(window)
|
||||||
|
if shouldBringToFront {
|
||||||
|
bringToFront(window)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func addWorkspace(windowId: UUID, workingDirectory: String? = nil, bringToFront shouldBringToFront: Bool = false) -> UUID? {
|
||||||
|
guard let state = scriptableMainWindow(windowId: windowId) else { return nil }
|
||||||
|
if shouldBringToFront, let window = state.window {
|
||||||
|
setActiveMainWindow(window)
|
||||||
|
bringToFront(window)
|
||||||
|
}
|
||||||
|
let workspace = state.tabManager.addWorkspace(
|
||||||
|
workingDirectory: workingDirectory,
|
||||||
|
select: shouldBringToFront
|
||||||
|
)
|
||||||
|
return workspace.id
|
||||||
|
}
|
||||||
|
|
||||||
private func markCommandPaletteOpenRequested(for window: NSWindow?) {
|
private func markCommandPaletteOpenRequested(for window: NSWindow?) {
|
||||||
guard let window,
|
guard let window,
|
||||||
let windowId = mainWindowId(for: window) else { return }
|
let windowId = mainWindowId(for: window) else { return }
|
||||||
|
|
|
||||||
705
Sources/AppleScriptSupport.swift
Normal file
705
Sources/AppleScriptSupport.swift
Normal file
|
|
@ -0,0 +1,705 @@
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
private enum AppleScriptStrings {
|
||||||
|
static let disabled = String(
|
||||||
|
localized: "applescript.error.disabled",
|
||||||
|
defaultValue: "AppleScript is disabled by the macos-applescript configuration."
|
||||||
|
)
|
||||||
|
static let missingAction = String(
|
||||||
|
localized: "applescript.error.missingAction",
|
||||||
|
defaultValue: "Missing action string."
|
||||||
|
)
|
||||||
|
static let missingInputText = String(
|
||||||
|
localized: "applescript.error.missingInputText",
|
||||||
|
defaultValue: "Missing input text."
|
||||||
|
)
|
||||||
|
static let missingTerminalTarget = String(
|
||||||
|
localized: "applescript.error.missingTerminalTarget",
|
||||||
|
defaultValue: "Missing terminal target."
|
||||||
|
)
|
||||||
|
static let missingSplitDirection = String(
|
||||||
|
localized: "applescript.error.missingSplitDirection",
|
||||||
|
defaultValue: "Missing or unknown split direction."
|
||||||
|
)
|
||||||
|
static let windowUnavailable = String(
|
||||||
|
localized: "applescript.error.windowUnavailable",
|
||||||
|
defaultValue: "Window is no longer available."
|
||||||
|
)
|
||||||
|
static let workspaceUnavailable = String(
|
||||||
|
localized: "applescript.error.workspaceUnavailable",
|
||||||
|
defaultValue: "Workspace is no longer available."
|
||||||
|
)
|
||||||
|
static let terminalUnavailable = String(
|
||||||
|
localized: "applescript.error.terminalUnavailable",
|
||||||
|
defaultValue: "Terminal is no longer available."
|
||||||
|
)
|
||||||
|
static let failedToCreateWindow = String(
|
||||||
|
localized: "applescript.error.failedToCreateWindow",
|
||||||
|
defaultValue: "Failed to create window."
|
||||||
|
)
|
||||||
|
static let failedToCreateWorkspace = String(
|
||||||
|
localized: "applescript.error.failedToCreateWorkspace",
|
||||||
|
defaultValue: "Failed to create workspace."
|
||||||
|
)
|
||||||
|
static let failedToCreateSplit = String(
|
||||||
|
localized: "applescript.error.failedToCreateSplit",
|
||||||
|
defaultValue: "Failed to create split."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
var fourCharCode: UInt32 {
|
||||||
|
utf8.reduce(0) { ($0 << 8) + UInt32($1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Workspace {
|
||||||
|
func scriptingTerminalPanels() -> [TerminalPanel] {
|
||||||
|
var results: [TerminalPanel] = []
|
||||||
|
var seen: Set<UUID> = []
|
||||||
|
|
||||||
|
for panelId in sidebarOrderedPanelIds() {
|
||||||
|
guard seen.insert(panelId).inserted,
|
||||||
|
let terminal = terminalPanel(for: panelId) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
results.append(terminal)
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining = panels.values
|
||||||
|
.compactMap { $0 as? TerminalPanel }
|
||||||
|
.sorted { $0.id.uuidString < $1.id.uuidString }
|
||||||
|
|
||||||
|
for terminal in remaining where seen.insert(terminal.id).inserted {
|
||||||
|
results.append(terminal)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
extension NSApplication {
|
||||||
|
var isAppleScriptEnabled: Bool {
|
||||||
|
GhosttyApp.shared.appleScriptAutomationEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func validateScript(command: NSScriptCommand) -> Bool {
|
||||||
|
guard isAppleScriptEnabled else {
|
||||||
|
command.scriptErrorNumber = errAEEventNotPermitted
|
||||||
|
command.scriptErrorString = AppleScriptStrings.disabled
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(scriptWindows)
|
||||||
|
var scriptWindows: [ScriptWindow] {
|
||||||
|
guard isAppleScriptEnabled,
|
||||||
|
let appDelegate = AppDelegate.shared else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return appDelegate.scriptableMainWindows().map { ScriptWindow(windowId: $0.windowId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(frontWindow)
|
||||||
|
var frontWindow: ScriptWindow? {
|
||||||
|
scriptWindows.first
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(valueInScriptWindowsWithUniqueID:)
|
||||||
|
func valueInScriptWindows(uniqueID: String) -> ScriptWindow? {
|
||||||
|
guard isAppleScriptEnabled,
|
||||||
|
let windowId = UUID(uuidString: uniqueID),
|
||||||
|
let appDelegate = AppDelegate.shared,
|
||||||
|
appDelegate.scriptableMainWindow(windowId: windowId) != nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ScriptWindow(windowId: windowId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(terminals)
|
||||||
|
var terminals: [ScriptTerminal] {
|
||||||
|
guard isAppleScriptEnabled,
|
||||||
|
let appDelegate = AppDelegate.shared else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return appDelegate.scriptableMainWindows()
|
||||||
|
.flatMap { state in
|
||||||
|
state.tabManager.tabs.flatMap { workspace in
|
||||||
|
workspace.scriptingTerminalPanels().map {
|
||||||
|
ScriptTerminal(workspaceId: workspace.id, terminalId: $0.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(valueInTerminalsWithUniqueID:)
|
||||||
|
func valueInTerminals(uniqueID: String) -> ScriptTerminal? {
|
||||||
|
guard isAppleScriptEnabled,
|
||||||
|
let terminalId = UUID(uuidString: uniqueID),
|
||||||
|
let appDelegate = AppDelegate.shared else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for state in appDelegate.scriptableMainWindows() {
|
||||||
|
for workspace in state.tabManager.tabs where workspace.terminalPanel(for: terminalId) != nil {
|
||||||
|
return ScriptTerminal(workspaceId: workspace.id, terminalId: terminalId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(handlePerformActionScriptCommand:)
|
||||||
|
func handlePerformActionScriptCommand(_ command: NSScriptCommand) -> NSNumber? {
|
||||||
|
guard validateScript(command: command) else { return nil }
|
||||||
|
|
||||||
|
guard let action = command.directParameter as? String else {
|
||||||
|
command.scriptErrorNumber = errAEParamMissed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.missingAction
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let terminal = command.evaluatedArguments?["on"] as? ScriptTerminal else {
|
||||||
|
command.scriptErrorNumber = errAEParamMissed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.missingTerminalTarget
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSNumber(value: terminal.perform(action: action))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(handleNewWindowScriptCommand:)
|
||||||
|
func handleNewWindowScriptCommand(_ command: NSScriptCommand) -> ScriptWindow? {
|
||||||
|
guard validateScript(command: command) else { return nil }
|
||||||
|
|
||||||
|
guard let appDelegate = AppDelegate.shared else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.failedToCreateWindow
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let windowId = appDelegate.createMainWindow()
|
||||||
|
return ScriptWindow(windowId: windowId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(handleNewTabScriptCommand:)
|
||||||
|
func handleNewTabScriptCommand(_ command: NSScriptCommand) -> ScriptTab? {
|
||||||
|
guard validateScript(command: command) else { return nil }
|
||||||
|
|
||||||
|
guard let appDelegate = AppDelegate.shared else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.failedToCreateWorkspace
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let targetWindow = command.evaluatedArguments?["window"] as? ScriptWindow {
|
||||||
|
guard let workspaceId = appDelegate.addWorkspace(windowId: targetWindow.windowId, bringToFront: false) else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.failedToCreateWorkspace
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ScriptTab(windowId: targetWindow.windowId, tabId: workspaceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let frontWindow = scriptWindows.first,
|
||||||
|
let workspaceId = appDelegate.addWorkspace(windowId: frontWindow.windowId, bringToFront: false) {
|
||||||
|
return ScriptTab(windowId: frontWindow.windowId, tabId: workspaceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
let windowId = appDelegate.createMainWindow()
|
||||||
|
return ScriptWindow(windowId: windowId).selectedTab
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(handleQuitScriptCommand:)
|
||||||
|
func handleQuitScriptCommand(_ command: NSScriptCommand) {
|
||||||
|
guard validateScript(command: command) else { return }
|
||||||
|
terminate(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
@objc(CmuxScriptWindow)
|
||||||
|
final class ScriptWindow: NSObject {
|
||||||
|
let windowId: UUID
|
||||||
|
|
||||||
|
init(windowId: UUID) {
|
||||||
|
self.windowId = windowId
|
||||||
|
}
|
||||||
|
|
||||||
|
private var state: AppDelegate.ScriptableMainWindowState? {
|
||||||
|
AppDelegate.shared?.scriptableMainWindow(windowId: windowId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(id)
|
||||||
|
var idValue: String {
|
||||||
|
guard NSApp.isAppleScriptEnabled else { return "" }
|
||||||
|
return windowId.uuidString
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(title)
|
||||||
|
var title: String {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let state else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
let windowTitle = state.window?.title.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
if !windowTitle.isEmpty {
|
||||||
|
return windowTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.tabManager.selectedWorkspace?.title ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(tabs)
|
||||||
|
var tabs: [ScriptTab] {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let state else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return state.tabManager.tabs.map { ScriptTab(windowId: windowId, tabId: $0.id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(selectedTab)
|
||||||
|
var selectedTab: ScriptTab? {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let selectedId = state?.tabManager.selectedTabId else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ScriptTab(windowId: windowId, tabId: selectedId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(terminals)
|
||||||
|
var terminals: [ScriptTerminal] {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let state else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return state.tabManager.tabs.flatMap { workspace in
|
||||||
|
workspace.scriptingTerminalPanels().map {
|
||||||
|
ScriptTerminal(workspaceId: workspace.id, terminalId: $0.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(valueInTabsWithUniqueID:)
|
||||||
|
func valueInTabs(uniqueID: String) -> ScriptTab? {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let tabId = UUID(uuidString: uniqueID),
|
||||||
|
let state,
|
||||||
|
state.tabManager.tabs.contains(where: { $0.id == tabId }) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ScriptTab(windowId: windowId, tabId: tabId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(valueInTerminalsWithUniqueID:)
|
||||||
|
func valueInTerminals(uniqueID: String) -> ScriptTerminal? {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let terminalId = UUID(uuidString: uniqueID),
|
||||||
|
let state else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for workspace in state.tabManager.tabs where workspace.terminalPanel(for: terminalId) != nil {
|
||||||
|
return ScriptTerminal(workspaceId: workspace.id, terminalId: terminalId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(handleActivateWindowCommand:)
|
||||||
|
func handleActivateWindow(_ command: NSScriptCommand) -> Any? {
|
||||||
|
guard NSApp.validateScript(command: command) else { return nil }
|
||||||
|
|
||||||
|
guard AppDelegate.shared?.focusScriptableMainWindow(windowId: windowId, bringToFront: true) == true else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.windowUnavailable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(handleCloseWindowCommand:)
|
||||||
|
func handleCloseWindow(_ command: NSScriptCommand) -> Any? {
|
||||||
|
guard NSApp.validateScript(command: command) else { return nil }
|
||||||
|
|
||||||
|
guard let window = state?.window else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.windowUnavailable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
window.performClose(nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override var objectSpecifier: NSScriptObjectSpecifier? {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let appClassDescription = NSApplication.shared.classDescription as? NSScriptClassDescription else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSUniqueIDSpecifier(
|
||||||
|
containerClassDescription: appClassDescription,
|
||||||
|
containerSpecifier: nil,
|
||||||
|
key: "scriptWindows",
|
||||||
|
uniqueID: windowId.uuidString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
@objc(CmuxScriptTab)
|
||||||
|
final class ScriptTab: NSObject {
|
||||||
|
let windowId: UUID
|
||||||
|
let tabId: UUID
|
||||||
|
|
||||||
|
init(windowId: UUID, tabId: UUID) {
|
||||||
|
self.windowId = windowId
|
||||||
|
self.tabId = tabId
|
||||||
|
}
|
||||||
|
|
||||||
|
private var state: AppDelegate.ScriptableMainWindowState? {
|
||||||
|
AppDelegate.shared?.scriptableMainWindow(windowId: windowId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var workspace: Workspace? {
|
||||||
|
state?.tabManager.tabs.first(where: { $0.id == tabId })
|
||||||
|
}
|
||||||
|
|
||||||
|
private var window: ScriptWindow {
|
||||||
|
ScriptWindow(windowId: windowId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(id)
|
||||||
|
var idValue: String {
|
||||||
|
guard NSApp.isAppleScriptEnabled else { return "" }
|
||||||
|
return tabId.uuidString
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(title)
|
||||||
|
var title: String {
|
||||||
|
guard NSApp.isAppleScriptEnabled else { return "" }
|
||||||
|
return workspace?.title ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(index)
|
||||||
|
var index: Int {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let state,
|
||||||
|
let idx = state.tabManager.tabs.firstIndex(where: { $0.id == tabId }) else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return idx + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(selected)
|
||||||
|
var selected: Bool {
|
||||||
|
guard NSApp.isAppleScriptEnabled else { return false }
|
||||||
|
return state?.tabManager.selectedTabId == tabId
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(focusedTerminal)
|
||||||
|
var focusedTerminal: ScriptTerminal? {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let terminalId = workspace?.focusedTerminalPanel?.id else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ScriptTerminal(workspaceId: tabId, terminalId: terminalId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(terminals)
|
||||||
|
var terminals: [ScriptTerminal] {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let workspace else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return workspace.scriptingTerminalPanels().map {
|
||||||
|
ScriptTerminal(workspaceId: tabId, terminalId: $0.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(valueInTerminalsWithUniqueID:)
|
||||||
|
func valueInTerminals(uniqueID: String) -> ScriptTerminal? {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let workspace,
|
||||||
|
let terminalId = UUID(uuidString: uniqueID),
|
||||||
|
workspace.terminalPanel(for: terminalId) != nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ScriptTerminal(workspaceId: tabId, terminalId: terminalId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(handleSelectTabCommand:)
|
||||||
|
func handleSelectTab(_ command: NSScriptCommand) -> Any? {
|
||||||
|
guard NSApp.validateScript(command: command) else { return nil }
|
||||||
|
|
||||||
|
guard let state,
|
||||||
|
let workspace else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.workspaceUnavailable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tabManager.selectWorkspace(workspace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(handleCloseTabCommand:)
|
||||||
|
func handleCloseTab(_ command: NSScriptCommand) -> Any? {
|
||||||
|
guard NSApp.validateScript(command: command) else { return nil }
|
||||||
|
|
||||||
|
guard let state,
|
||||||
|
let workspace else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.workspaceUnavailable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.tabManager.tabs.count > 1 {
|
||||||
|
state.tabManager.closeWorkspace(workspace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let window = state.window else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.windowUnavailable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
window.performClose(nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override var objectSpecifier: NSScriptObjectSpecifier? {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let windowClassDescription = window.classDescription as? NSScriptClassDescription,
|
||||||
|
let windowSpecifier = window.objectSpecifier else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSUniqueIDSpecifier(
|
||||||
|
containerClassDescription: windowClassDescription,
|
||||||
|
containerSpecifier: windowSpecifier,
|
||||||
|
key: "tabs",
|
||||||
|
uniqueID: tabId.uuidString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
@objc(CmuxScriptTerminal)
|
||||||
|
final class ScriptTerminal: NSObject {
|
||||||
|
let workspaceId: UUID
|
||||||
|
let terminalId: UUID
|
||||||
|
|
||||||
|
init(workspaceId: UUID, terminalId: UUID) {
|
||||||
|
self.workspaceId = workspaceId
|
||||||
|
self.terminalId = terminalId
|
||||||
|
}
|
||||||
|
|
||||||
|
private var state: AppDelegate.ScriptableMainWindowState? {
|
||||||
|
AppDelegate.shared?.scriptableMainWindowForTab(workspaceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var workspace: Workspace? {
|
||||||
|
state?.tabManager.tabs.first(where: { $0.id == workspaceId })
|
||||||
|
}
|
||||||
|
|
||||||
|
private var terminal: TerminalPanel? {
|
||||||
|
workspace?.terminalPanel(for: terminalId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(id)
|
||||||
|
var stableID: String {
|
||||||
|
guard NSApp.isAppleScriptEnabled else { return "" }
|
||||||
|
return terminalId.uuidString
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(title)
|
||||||
|
var title: String {
|
||||||
|
guard NSApp.isAppleScriptEnabled else { return "" }
|
||||||
|
return terminal?.displayTitle ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(workingDirectory)
|
||||||
|
var workingDirectory: String {
|
||||||
|
guard NSApp.isAppleScriptEnabled else { return "" }
|
||||||
|
return terminal?.directory ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func input(text: String) -> Bool {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let terminal else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
terminal.sendText(text)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func perform(action: String) -> Bool {
|
||||||
|
guard NSApp.isAppleScriptEnabled else { return false }
|
||||||
|
return terminal?.performBindingAction(action) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(handleSplitCommand:)
|
||||||
|
func handleSplit(_ command: NSScriptCommand) -> Any? {
|
||||||
|
guard NSApp.validateScript(command: command) else { return nil }
|
||||||
|
|
||||||
|
guard let directionCode = command.evaluatedArguments?["direction"] as? UInt32,
|
||||||
|
let direction = ScriptSplitDirection(code: directionCode)?.splitDirection else {
|
||||||
|
command.scriptErrorNumber = errAEParamMissed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.missingSplitDirection
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let state,
|
||||||
|
let workspace,
|
||||||
|
terminal != nil else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.terminalUnavailable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let newPanelId = state.tabManager.newSplit(tabId: workspaceId, surfaceId: terminalId, direction: direction),
|
||||||
|
workspace.terminalPanel(for: newPanelId) != nil else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.failedToCreateSplit
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ScriptTerminal(workspaceId: workspaceId, terminalId: newPanelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(handleFocusCommand:)
|
||||||
|
func handleFocus(_ command: NSScriptCommand) -> Any? {
|
||||||
|
guard NSApp.validateScript(command: command) else { return nil }
|
||||||
|
|
||||||
|
guard let state,
|
||||||
|
let workspace,
|
||||||
|
terminal != nil else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.terminalUnavailable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let app = AppDelegate.shared {
|
||||||
|
_ = app.focusScriptableMainWindow(windowId: state.windowId, bringToFront: true)
|
||||||
|
}
|
||||||
|
state.tabManager.selectWorkspace(workspace)
|
||||||
|
workspace.focusPanel(terminalId)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(handleCloseCommand:)
|
||||||
|
func handleClose(_ command: NSScriptCommand) -> Any? {
|
||||||
|
guard NSApp.validateScript(command: command) else { return nil }
|
||||||
|
|
||||||
|
guard let state,
|
||||||
|
let workspace,
|
||||||
|
terminal != nil else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.terminalUnavailable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if workspace.panels.count == 1 {
|
||||||
|
if state.tabManager.tabs.count > 1 {
|
||||||
|
state.tabManager.closeWorkspace(workspace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let window = state.window else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.windowUnavailable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
window.performClose(nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard workspace.closePanel(terminalId, force: true) else {
|
||||||
|
command.scriptErrorNumber = errAEEventFailed
|
||||||
|
command.scriptErrorString = AppleScriptStrings.terminalUnavailable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
AppDelegate.shared?.notificationStore?.clearNotifications(forTabId: workspaceId, surfaceId: terminalId)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override var objectSpecifier: NSScriptObjectSpecifier? {
|
||||||
|
guard NSApp.isAppleScriptEnabled,
|
||||||
|
let appClassDescription = NSApplication.shared.classDescription as? NSScriptClassDescription else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSUniqueIDSpecifier(
|
||||||
|
containerClassDescription: appClassDescription,
|
||||||
|
containerSpecifier: nil,
|
||||||
|
key: "terminals",
|
||||||
|
uniqueID: terminalId.uuidString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
@objc(CmuxScriptInputTextCommand)
|
||||||
|
final class ScriptInputTextCommand: NSScriptCommand {
|
||||||
|
override func performDefaultImplementation() -> Any? {
|
||||||
|
guard NSApp.validateScript(command: self) else { return nil }
|
||||||
|
|
||||||
|
guard let text = directParameter as? String else {
|
||||||
|
scriptErrorNumber = errAEParamMissed
|
||||||
|
scriptErrorString = AppleScriptStrings.missingInputText
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else {
|
||||||
|
scriptErrorNumber = errAEParamMissed
|
||||||
|
scriptErrorString = AppleScriptStrings.missingTerminalTarget
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard terminal.input(text: text) else {
|
||||||
|
scriptErrorNumber = errAEEventFailed
|
||||||
|
scriptErrorString = AppleScriptStrings.terminalUnavailable
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ScriptSplitDirection {
|
||||||
|
case right
|
||||||
|
case left
|
||||||
|
case down
|
||||||
|
case up
|
||||||
|
|
||||||
|
init?(code: UInt32) {
|
||||||
|
switch code {
|
||||||
|
case "GSrt".fourCharCode: self = .right
|
||||||
|
case "GSlf".fourCharCode: self = .left
|
||||||
|
case "GSdn".fourCharCode: self = .down
|
||||||
|
case "GSup".fourCharCode: self = .up
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var splitDirection: SplitDirection {
|
||||||
|
switch self {
|
||||||
|
case .right: return .right
|
||||||
|
case .left: return .left
|
||||||
|
case .down: return .down
|
||||||
|
case .up: return .up
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -708,6 +708,7 @@ class GhosttyApp {
|
||||||
private let backgroundLogLock = NSLock()
|
private let backgroundLogLock = NSLock()
|
||||||
private var backgroundLogSequence: UInt64 = 0
|
private var backgroundLogSequence: UInt64 = 0
|
||||||
private var appObservers: [NSObjectProtocol] = []
|
private var appObservers: [NSObjectProtocol] = []
|
||||||
|
private var bellAudioSound: NSSound?
|
||||||
private var backgroundEventCounter: UInt64 = 0
|
private var backgroundEventCounter: UInt64 = 0
|
||||||
private var defaultBackgroundUpdateScope: GhosttyDefaultBackgroundUpdateScope = .unscoped
|
private var defaultBackgroundUpdateScope: GhosttyDefaultBackgroundUpdateScope = .unscoped
|
||||||
private var defaultBackgroundScopeSource: String = "initialize"
|
private var defaultBackgroundScopeSource: String = "initialize"
|
||||||
|
|
@ -1524,6 +1525,75 @@ class GhosttyApp {
|
||||||
return found && enabled
|
return found && enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appleScriptAutomationEnabled() -> Bool {
|
||||||
|
guard let config else { return false }
|
||||||
|
var enabled = false
|
||||||
|
let key = "macos-applescript"
|
||||||
|
_ = ghostty_config_get(config, &enabled, key, UInt(key.lengthOfBytes(using: .utf8)))
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func shellIntegrationMode() -> String {
|
||||||
|
guard let config else { return "detect" }
|
||||||
|
var value: UnsafePointer<Int8>?
|
||||||
|
let key = "shell-integration"
|
||||||
|
guard ghostty_config_get(config, &value, key, UInt(key.lengthOfBytes(using: .utf8))),
|
||||||
|
let value else {
|
||||||
|
return "detect"
|
||||||
|
}
|
||||||
|
return String(cString: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bellFeatures() -> CUnsignedInt {
|
||||||
|
guard let config else { return 0 }
|
||||||
|
var features: CUnsignedInt = 0
|
||||||
|
let key = "bell-features"
|
||||||
|
_ = ghostty_config_get(config, &features, key, UInt(key.lengthOfBytes(using: .utf8)))
|
||||||
|
return features
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bellAudioPath() -> String? {
|
||||||
|
guard let config else { return nil }
|
||||||
|
var value = ghostty_config_path_s()
|
||||||
|
let key = "bell-audio-path"
|
||||||
|
guard ghostty_config_get(config, &value, key, UInt(key.lengthOfBytes(using: .utf8))),
|
||||||
|
let rawPath = value.path else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let path = String(cString: rawPath)
|
||||||
|
return path.isEmpty ? nil : path
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bellAudioVolume() -> Float {
|
||||||
|
guard let config else { return 0.5 }
|
||||||
|
var value: Double = 0.5
|
||||||
|
let key = "bell-audio-volume"
|
||||||
|
_ = ghostty_config_get(config, &value, key, UInt(key.lengthOfBytes(using: .utf8)))
|
||||||
|
return Float(min(1.0, max(0.0, value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func ringBell() {
|
||||||
|
let features = bellFeatures()
|
||||||
|
|
||||||
|
if (features & (1 << 0)) != 0 {
|
||||||
|
NSSound.beep()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (features & (1 << 1)) != 0,
|
||||||
|
let path = bellAudioPath(),
|
||||||
|
let sound = NSSound(contentsOfFile: path, byReference: false) {
|
||||||
|
sound.volume = bellAudioVolume()
|
||||||
|
bellAudioSound = sound
|
||||||
|
if !sound.play() {
|
||||||
|
bellAudioSound = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (features & (1 << 2)) != 0 {
|
||||||
|
NSApp.requestUserAttention(.informationalRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func applyDefaultBackground(
|
private func applyDefaultBackground(
|
||||||
color: NSColor,
|
color: NSColor,
|
||||||
opacity: Double,
|
opacity: Double,
|
||||||
|
|
@ -1690,6 +1760,13 @@ class GhosttyApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if action.tag == GHOSTTY_ACTION_RING_BELL {
|
||||||
|
performOnMain {
|
||||||
|
self.ringBell()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if action.tag == GHOSTTY_ACTION_RELOAD_CONFIG {
|
if action.tag == GHOSTTY_ACTION_RELOAD_CONFIG {
|
||||||
let soft = action.action.reload_config.soft
|
let soft = action.action.reload_config.soft
|
||||||
logThemeAction("reload request target=app soft=\(soft)")
|
logThemeAction("reload request target=app soft=\(soft)")
|
||||||
|
|
@ -1797,6 +1874,11 @@ class GhosttyApp {
|
||||||
guard let tabManager = AppDelegate.shared?.tabManager else { return false }
|
guard let tabManager = AppDelegate.shared?.tabManager else { return false }
|
||||||
return tabManager.newSplit(tabId: tabId, surfaceId: surfaceId, direction: direction) != nil
|
return tabManager.newSplit(tabId: tabId, surfaceId: surfaceId, direction: direction) != nil
|
||||||
}
|
}
|
||||||
|
case GHOSTTY_ACTION_RING_BELL:
|
||||||
|
performOnMain {
|
||||||
|
self.ringBell()
|
||||||
|
}
|
||||||
|
return true
|
||||||
case GHOSTTY_ACTION_GOTO_SPLIT:
|
case GHOSTTY_ACTION_GOTO_SPLIT:
|
||||||
guard let tabId = surfaceView.tabId,
|
guard let tabId = surfaceView.tabId,
|
||||||
let surfaceId = surfaceView.terminalSurface?.id,
|
let surfaceId = surfaceView.terminalSurface?.id,
|
||||||
|
|
@ -2739,6 +2821,9 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
||||||
?? "/bin/zsh"
|
?? "/bin/zsh"
|
||||||
let shellName = URL(fileURLWithPath: shell).lastPathComponent
|
let shellName = URL(fileURLWithPath: shell).lastPathComponent
|
||||||
if shellName == "zsh" {
|
if shellName == "zsh" {
|
||||||
|
if GhosttyApp.shared.shellIntegrationMode() != "none" {
|
||||||
|
env["CMUX_LOAD_GHOSTTY_ZSH_INTEGRATION"] = "1"
|
||||||
|
}
|
||||||
let candidateZdotdir = (env["ZDOTDIR"]?.isEmpty == false ? env["ZDOTDIR"] : nil)
|
let candidateZdotdir = (env["ZDOTDIR"]?.isEmpty == false ? env["ZDOTDIR"] : nil)
|
||||||
?? getenv("ZDOTDIR").map { String(cString: $0) }
|
?? getenv("ZDOTDIR").map { String(cString: $0) }
|
||||||
?? (ProcessInfo.processInfo.environment["ZDOTDIR"]?.isEmpty == false ? ProcessInfo.processInfo.environment["ZDOTDIR"] : nil)
|
?? (ProcessInfo.processInfo.environment["ZDOTDIR"]?.isEmpty == false ? ProcessInfo.processInfo.environment["ZDOTDIR"] : nil)
|
||||||
|
|
@ -4332,6 +4417,12 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
||||||
super.keyDown(with: event)
|
super.keyDown(with: event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if let terminalSurface {
|
||||||
|
AppDelegate.shared?.tabManager?.dismissNotificationOnDirectInteraction(
|
||||||
|
tabId: terminalSurface.tabId,
|
||||||
|
surfaceId: terminalSurface.id
|
||||||
|
)
|
||||||
|
}
|
||||||
if event.keyCode != 53 {
|
if event.keyCode != 53 {
|
||||||
endFindEscapeSuppression()
|
endFindEscapeSuppression()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -831,6 +831,54 @@ final class AppDelegateWindowContextRoutingTests: XCTestCase {
|
||||||
XCTAssertTrue(resolved === manager, "Expected registered window object identity to win even if identifier string changed")
|
XCTAssertTrue(resolved === manager, "Expected registered window object identity to win even if identifier string changed")
|
||||||
XCTAssertTrue(app.tabManager === manager)
|
XCTAssertTrue(app.tabManager === manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAddWorkspaceWithoutBringToFrontPreservesActiveWindowAndSelection() {
|
||||||
|
_ = NSApplication.shared
|
||||||
|
let app = AppDelegate()
|
||||||
|
|
||||||
|
let windowAId = UUID()
|
||||||
|
let windowBId = UUID()
|
||||||
|
let windowA = makeMainWindow(id: windowAId)
|
||||||
|
let windowB = makeMainWindow(id: windowBId)
|
||||||
|
defer {
|
||||||
|
windowA.orderOut(nil)
|
||||||
|
windowB.orderOut(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
let managerA = TabManager()
|
||||||
|
let managerB = TabManager()
|
||||||
|
app.registerMainWindow(
|
||||||
|
windowA,
|
||||||
|
windowId: windowAId,
|
||||||
|
tabManager: managerA,
|
||||||
|
sidebarState: SidebarState(),
|
||||||
|
sidebarSelectionState: SidebarSelectionState()
|
||||||
|
)
|
||||||
|
app.registerMainWindow(
|
||||||
|
windowB,
|
||||||
|
windowId: windowBId,
|
||||||
|
tabManager: managerB,
|
||||||
|
sidebarState: SidebarState(),
|
||||||
|
sidebarSelectionState: SidebarSelectionState()
|
||||||
|
)
|
||||||
|
|
||||||
|
windowA.makeKeyAndOrderFront(nil)
|
||||||
|
_ = app.synchronizeActiveMainWindowContext(preferredWindow: windowA)
|
||||||
|
XCTAssertTrue(app.tabManager === managerA)
|
||||||
|
|
||||||
|
let originalSelectedA = managerA.selectedTabId
|
||||||
|
let originalSelectedB = managerB.selectedTabId
|
||||||
|
let originalTabCountB = managerB.tabs.count
|
||||||
|
|
||||||
|
let createdWorkspaceId = app.addWorkspace(windowId: windowBId, bringToFront: false)
|
||||||
|
|
||||||
|
XCTAssertNotNil(createdWorkspaceId)
|
||||||
|
XCTAssertTrue(app.tabManager === managerA, "Expected non-focus workspace creation to preserve active window routing")
|
||||||
|
XCTAssertEqual(managerA.selectedTabId, originalSelectedA)
|
||||||
|
XCTAssertEqual(managerB.selectedTabId, originalSelectedB, "Expected background workspace creation to preserve selected tab")
|
||||||
|
XCTAssertEqual(managerB.tabs.count, originalTabCountB + 1)
|
||||||
|
XCTAssertTrue(managerB.tabs.contains(where: { $0.id == createdWorkspaceId }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|
@ -7476,6 +7524,24 @@ final class TerminalNotificationDirectInteractionTests: XCTestCase {
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func makeKeyEvent(characters: String, keyCode: UInt16, window: NSWindow) -> NSEvent {
|
||||||
|
guard let event = NSEvent.keyEvent(
|
||||||
|
with: .keyDown,
|
||||||
|
location: .zero,
|
||||||
|
modifierFlags: [],
|
||||||
|
timestamp: ProcessInfo.processInfo.systemUptime,
|
||||||
|
windowNumber: window.windowNumber,
|
||||||
|
context: nil,
|
||||||
|
characters: characters,
|
||||||
|
charactersIgnoringModifiers: characters,
|
||||||
|
isARepeat: false,
|
||||||
|
keyCode: keyCode
|
||||||
|
) else {
|
||||||
|
fatalError("Failed to create key event")
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
private func surfaceView(in hostedView: GhosttySurfaceScrollView) -> NSView? {
|
private func surfaceView(in hostedView: GhosttySurfaceScrollView) -> NSView? {
|
||||||
hostedView.subviews
|
hostedView.subviews
|
||||||
.compactMap { $0 as? NSScrollView }
|
.compactMap { $0 as? NSScrollView }
|
||||||
|
|
@ -7556,6 +7622,76 @@ final class TerminalNotificationDirectInteractionTests: XCTestCase {
|
||||||
XCTAssertFalse(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: terminalPanel.id))
|
XCTAssertFalse(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: terminalPanel.id))
|
||||||
XCTAssertEqual(GhosttySurfaceScrollView.flashCount(for: terminalPanel.id), 1)
|
XCTAssertEqual(GhosttySurfaceScrollView.flashCount(for: terminalPanel.id), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testTerminalKeyDownDismissesUnreadWhenSurfaceIsAlreadyFirstResponder() {
|
||||||
|
let appDelegate = AppDelegate.shared ?? AppDelegate()
|
||||||
|
let manager = TabManager()
|
||||||
|
let store = TerminalNotificationStore.shared
|
||||||
|
let window = makeWindow()
|
||||||
|
|
||||||
|
let originalTabManager = appDelegate.tabManager
|
||||||
|
let originalNotificationStore = appDelegate.notificationStore
|
||||||
|
let originalAppFocusOverride = AppFocusState.overrideIsFocused
|
||||||
|
|
||||||
|
store.replaceNotificationsForTesting([])
|
||||||
|
store.configureNotificationDeliveryHandlerForTesting { _, _ in }
|
||||||
|
appDelegate.tabManager = manager
|
||||||
|
appDelegate.notificationStore = store
|
||||||
|
|
||||||
|
defer {
|
||||||
|
store.replaceNotificationsForTesting([])
|
||||||
|
store.resetNotificationDeliveryHandlerForTesting()
|
||||||
|
appDelegate.tabManager = originalTabManager
|
||||||
|
appDelegate.notificationStore = originalNotificationStore
|
||||||
|
AppFocusState.overrideIsFocused = originalAppFocusOverride
|
||||||
|
window.orderOut(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let workspace = manager.selectedWorkspace,
|
||||||
|
let terminalPanel = workspace.focusedTerminalPanel else {
|
||||||
|
XCTFail("Expected an initial focused terminal panel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let contentView = window.contentView else {
|
||||||
|
XCTFail("Expected content view")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let hostedView = terminalPanel.hostedView
|
||||||
|
hostedView.frame = contentView.bounds
|
||||||
|
hostedView.autoresizingMask = [.width, .height]
|
||||||
|
contentView.addSubview(hostedView)
|
||||||
|
contentView.layoutSubtreeIfNeeded()
|
||||||
|
hostedView.layoutSubtreeIfNeeded()
|
||||||
|
|
||||||
|
guard let surfaceView = surfaceView(in: hostedView) as? GhosttyNSView else {
|
||||||
|
XCTFail("Expected terminal surface view")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
GhosttySurfaceScrollView.resetFlashCounts()
|
||||||
|
AppFocusState.overrideIsFocused = true
|
||||||
|
XCTAssertTrue(window.makeFirstResponder(surfaceView))
|
||||||
|
|
||||||
|
store.addNotification(
|
||||||
|
tabId: workspace.id,
|
||||||
|
surfaceId: terminalPanel.id,
|
||||||
|
title: "Unread",
|
||||||
|
subtitle: "",
|
||||||
|
body: ""
|
||||||
|
)
|
||||||
|
XCTAssertTrue(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: terminalPanel.id))
|
||||||
|
|
||||||
|
let event = makeKeyEvent(characters: "", keyCode: 122, window: window)
|
||||||
|
surfaceView.keyDown(with: event)
|
||||||
|
let drained = expectation(description: "flash drained")
|
||||||
|
DispatchQueue.main.async { drained.fulfill() }
|
||||||
|
wait(for: [drained], timeout: 1.0)
|
||||||
|
|
||||||
|
XCTAssertFalse(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: terminalPanel.id))
|
||||||
|
XCTAssertEqual(GhosttySurfaceScrollView.flashCount(for: terminalPanel.id), 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1459,3 +1459,83 @@ final class GhosttyMouseFocusTests: XCTestCase {
|
||||||
XCTAssertFalse(GhosttyApp.userConfigContainsCJKCodepointMap(configPaths: [fileA.path]))
|
XCTAssertFalse(GhosttyApp.userConfigContainsCJKCodepointMap(configPaths: [fileA.path]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class ZshShellIntegrationHandoffTests: XCTestCase {
|
||||||
|
func testGhosttyPromptHooksLoadWhenCmuxRequestsZshIntegration() throws {
|
||||||
|
let output = try runInteractiveZsh(cmuxLoadGhosttyIntegration: true)
|
||||||
|
|
||||||
|
XCTAssertTrue(output.contains("PRECMD=1"), output)
|
||||||
|
XCTAssertTrue(output.contains("PREEXEC=1"), output)
|
||||||
|
XCTAssertTrue(output.contains("PRECMDS=_ghostty_precmd"), output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGhosttyPromptHooksDoNotLoadWithoutCmuxHandoffFlag() throws {
|
||||||
|
let output = try runInteractiveZsh(cmuxLoadGhosttyIntegration: false)
|
||||||
|
|
||||||
|
XCTAssertTrue(output.contains("PRECMD=0"), output)
|
||||||
|
XCTAssertTrue(output.contains("PREEXEC=0"), output)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func runInteractiveZsh(cmuxLoadGhosttyIntegration: Bool) throws -> String {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let root = fileManager.temporaryDirectory
|
||||||
|
.appendingPathComponent("cmux-zsh-shell-integration-\(UUID().uuidString)")
|
||||||
|
try fileManager.createDirectory(at: root, withIntermediateDirectories: true)
|
||||||
|
defer { try? fileManager.removeItem(at: root) }
|
||||||
|
|
||||||
|
let userZdotdir = root.appendingPathComponent("zdotdir")
|
||||||
|
try fileManager.createDirectory(at: userZdotdir, withIntermediateDirectories: true)
|
||||||
|
try "\n".write(to: userZdotdir.appendingPathComponent(".zshenv"), atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
|
let repoRoot = URL(fileURLWithPath: #filePath)
|
||||||
|
.deletingLastPathComponent()
|
||||||
|
.deletingLastPathComponent()
|
||||||
|
let cmuxZdotdir = repoRoot.appendingPathComponent("Resources/shell-integration")
|
||||||
|
let ghosttyResources = repoRoot.appendingPathComponent("ghostty/src")
|
||||||
|
|
||||||
|
let process = Process()
|
||||||
|
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
||||||
|
process.arguments = [
|
||||||
|
"-i",
|
||||||
|
"-c",
|
||||||
|
"(( $+functions[_ghostty_deferred_init] )) && _ghostty_deferred_init >/dev/null 2>&1; " +
|
||||||
|
"print -r -- \"PRECMD=${+functions[_ghostty_precmd]} " +
|
||||||
|
"PREEXEC=${+functions[_ghostty_preexec]} PRECMDS=${(j:,:)precmd_functions}\""
|
||||||
|
]
|
||||||
|
process.environment = [
|
||||||
|
"HOME": root.path,
|
||||||
|
"TERM": "xterm-256color",
|
||||||
|
"SHELL": "/bin/zsh",
|
||||||
|
"USER": NSUserName(),
|
||||||
|
"ZDOTDIR": cmuxZdotdir.path,
|
||||||
|
"CMUX_ZSH_ZDOTDIR": userZdotdir.path,
|
||||||
|
"CMUX_SHELL_INTEGRATION": "0",
|
||||||
|
"GHOSTTY_RESOURCES_DIR": ghosttyResources.path,
|
||||||
|
]
|
||||||
|
if cmuxLoadGhosttyIntegration {
|
||||||
|
process.environment?["CMUX_LOAD_GHOSTTY_ZSH_INTEGRATION"] = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = Pipe()
|
||||||
|
let stderr = Pipe()
|
||||||
|
process.standardOutput = stdout
|
||||||
|
process.standardError = stderr
|
||||||
|
|
||||||
|
try process.run()
|
||||||
|
let deadline = Date().addingTimeInterval(5)
|
||||||
|
while process.isRunning && Date() < deadline {
|
||||||
|
_ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01))
|
||||||
|
}
|
||||||
|
if process.isRunning {
|
||||||
|
process.terminate()
|
||||||
|
process.waitUntilExit()
|
||||||
|
XCTFail("Timed out waiting for zsh to exit")
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = String(data: stdout.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
||||||
|
let error = String(data: stderr.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
||||||
|
|
||||||
|
XCTAssertEqual(process.terminationStatus, 0, error)
|
||||||
|
return output.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,11 @@ When we change the fork, update this document and the parent submodule SHA.
|
||||||
|
|
||||||
## Current fork changes
|
## Current fork changes
|
||||||
|
|
||||||
|
Fork rebased onto upstream `v1.3.0` plus newer `main` commits as of March 9, 2026.
|
||||||
|
|
||||||
### 1) OSC 99 (kitty) notification parser
|
### 1) OSC 99 (kitty) notification parser
|
||||||
|
|
||||||
- Commit: `4713b7e23` (Add OSC 99 notification parser)
|
- Commit: `a2252e7a9` (Add OSC 99 notification parser)
|
||||||
- Files:
|
- Files:
|
||||||
- `src/terminal/osc.zig`
|
- `src/terminal/osc.zig`
|
||||||
- `src/terminal/osc/parsers.zig`
|
- `src/terminal/osc/parsers.zig`
|
||||||
|
|
@ -24,13 +26,49 @@ When we change the fork, update this document and the parent submodule SHA.
|
||||||
|
|
||||||
### 2) macOS display link restart on display changes
|
### 2) macOS display link restart on display changes
|
||||||
|
|
||||||
- Commit: `7c2562cbe` (macos: restart display link after display ID change)
|
- Commit: `c07e6c5a5` (macos: restart display link after display ID change)
|
||||||
- Files:
|
- Files:
|
||||||
- `src/renderer/generic.zig`
|
- `src/renderer/generic.zig`
|
||||||
- Summary:
|
- Summary:
|
||||||
- Restarts the CVDisplayLink when `setMacOSDisplayID` updates the current CGDisplay.
|
- Restarts the CVDisplayLink when `setMacOSDisplayID` updates the current CGDisplay.
|
||||||
- Prevents a rare state where vsync is "running" but no callbacks arrive, which can look like a frozen surface until focus/occlusion changes.
|
- Prevents a rare state where vsync is "running" but no callbacks arrive, which can look like a frozen surface until focus/occlusion changes.
|
||||||
|
|
||||||
|
### 3) Keyboard copy mode selection C API
|
||||||
|
|
||||||
|
- Commit: `a50579bd5` (Add C API for keyboard copy mode selection)
|
||||||
|
- Files:
|
||||||
|
- `src/Surface.zig`
|
||||||
|
- `src/apprt/embedded.zig`
|
||||||
|
- Summary:
|
||||||
|
- Restores `ghostty_surface_select_cursor_cell` and `ghostty_surface_clear_selection`.
|
||||||
|
- Keeps cmux keyboard copy mode working against the refreshed Ghostty base.
|
||||||
|
|
||||||
|
### 4) macOS resize stale-frame mitigation
|
||||||
|
|
||||||
|
Sections 3 and 4 are grouped by feature, not by commit order. The fork branch HEAD is the
|
||||||
|
section 3 copy-mode commit, even though the section 4 resize commits were applied earlier.
|
||||||
|
|
||||||
|
- Commits:
|
||||||
|
- `769bbf7a9` (macos: reduce transient blank/scaled frames during resize)
|
||||||
|
- `9efcdfdf8` (macos: keep top-left gravity for stale-frame replay)
|
||||||
|
- Files:
|
||||||
|
- `pkg/macos/animation.zig`
|
||||||
|
- `src/Surface.zig`
|
||||||
|
- `src/apprt/embedded.zig`
|
||||||
|
- `src/renderer/Metal.zig`
|
||||||
|
- `src/renderer/generic.zig`
|
||||||
|
- `src/renderer/metal/IOSurfaceLayer.zig`
|
||||||
|
- Summary:
|
||||||
|
- Replays the last rendered frame during resize and keeps its geometry anchored correctly.
|
||||||
|
- Reduces transient blank or scaled frames while a macOS window is being resized.
|
||||||
|
|
||||||
|
## Upstreamed fork changes
|
||||||
|
|
||||||
|
### cursor-click-to-move respects OSC 133 click-to-move
|
||||||
|
|
||||||
|
- Was local in the fork as `10a585754`.
|
||||||
|
- Landed upstream as `bb646926f`, so it is no longer carried as a fork-only patch.
|
||||||
|
|
||||||
## Merge conflict notes
|
## Merge conflict notes
|
||||||
|
|
||||||
These files change frequently upstream; be careful when rebasing the fork:
|
These files change frequently upstream; be careful when rebasing the fork:
|
||||||
|
|
|
||||||
2
ghostty
2
ghostty
|
|
@ -1 +1 @@
|
||||||
Subproject commit 7dd589824d4c9bda8265355718800cccaf7189a0
|
Subproject commit a50579bd5ddec81c6244b9b349d4bf781f667cec
|
||||||
|
|
@ -463,6 +463,12 @@ typedef struct {
|
||||||
|
|
||||||
// Config types
|
// Config types
|
||||||
|
|
||||||
|
// config.Path
|
||||||
|
typedef struct {
|
||||||
|
const char* path;
|
||||||
|
bool optional;
|
||||||
|
} ghostty_config_path_s;
|
||||||
|
|
||||||
// config.Color
|
// config.Color
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t r;
|
uint8_t r;
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@
|
||||||
# Update this file in a reviewed PR whenever the ghostty submodule SHA changes.
|
# Update this file in a reviewed PR whenever the ghostty submodule SHA changes.
|
||||||
# Format: <ghostty_sha> <sha256>
|
# Format: <ghostty_sha> <sha256>
|
||||||
7dd589824d4c9bda8265355718800cccaf7189a0 3915af4256850a0a7bee671c3ba0a47cbfee5dbfc6d71caf952acefdf2ee4207
|
7dd589824d4c9bda8265355718800cccaf7189a0 3915af4256850a0a7bee671c3ba0a47cbfee5dbfc6d71caf952acefdf2ee4207
|
||||||
|
a50579bd5ddec81c6244b9b349d4bf781f667cec f7e9c0597468a263d6b75eaf815ccecd90c7933f3cf4ae58929569ff23b2666d
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue