Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions extensions/ghostty/.eslintrc.json

This file was deleted.

6 changes: 6 additions & 0 deletions extensions/ghostty/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Ghostty Changelog

## [Fix] - 2026-03-25

- Fix "Open with Ghostty" to open the selected Finder item (file or folder), not just the current window directory
- Support files (opens parent directory), folders, multiple selections, and Path Finder
- Remove `run-applescript` dependency; use `runAppleScript` from `@raycast/utils`

## [Feature] - 2026-03-24

- Use new Ghostty AppleScript API
Expand Down
6 changes: 6 additions & 0 deletions extensions/ghostty/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { defineConfig } = require("eslint/config");
const raycastConfig = require("@raycast/eslint-config");

module.exports = defineConfig([
...raycastConfig,
]);
2,137 changes: 818 additions & 1,319 deletions extensions/ghostty/package-lock.json

Large diffs are not rendered by default.

122 changes: 61 additions & 61 deletions extensions/ghostty/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,17 @@
"friendlyusername",
"alexs",
"mathisfoxius",
"joaogf.dev"
"joaogf.dev",
"pernielsentikaer"
],
"platforms": [
"macOS"
],
"categories": [
"Productivity",
"Developer Tools"
],
"license": "MIT",
"preferences": [
{
"name": "workspaceParentDirectory",
"type": "directory",
"required": false,
"title": "Workspaces Parent Directory",
"description": "Directory to scan for git repos (used by Open Workspace from Git Repos)"
},
{
"name": "workspaceScanDepth",
"type": "dropdown",
"required": true,
"title": "Workspace Scan Depth",
"description": "Max depth to scan for git repositories",
"default": "3",
"data": [
{
"title": "2",
"value": "2"
},
{
"title": "3",
"value": "3"
},
{
"title": "4",
"value": "4"
},
{
"title": "5",
"value": "5"
},
{
"title": "6",
"value": "6"
}
]
},
{
"name": "autoFocusGhostty",
"type": "checkbox",
"required": false,
"title": "Auto Focus on Tab Selection",
"label": "Move cursor to selected tab",
"description": "Move cursor to the selected tab after selecting it",
"default": false
}
],
"commands": [
{
"name": "new-ghostty-window",
Expand Down Expand Up @@ -169,19 +125,66 @@
]
}
],
"preferences": [
{
"name": "workspaceParentDirectory",
"type": "directory",
"required": false,
"title": "Workspaces Parent Directory",
"description": "Directory to scan for git repos (used by Open Workspace from Git Repos)"
},
{
"name": "workspaceScanDepth",
"type": "dropdown",
"required": true,
"title": "Workspace Scan Depth",
"description": "Max depth to scan for git repositories",
"default": "3",
"data": [
{
"title": "2",
"value": "2"
},
{
"title": "3",
"value": "3"
},
{
"title": "4",
"value": "4"
},
{
"title": "5",
"value": "5"
},
{
"title": "6",
"value": "6"
}
]
},
{
"name": "autoFocusGhostty",
"type": "checkbox",
"required": false,
"title": "Auto Focus on Tab Selection",
"label": "Move cursor to selected tab",
"description": "Move cursor to the selected tab after selecting it",
"default": false
}
],
"dependencies": {
"@raycast/api": "^1.89.0",
"@raycast/utils": "^1.17.0",
"run-applescript": "^7.0.0",
"@raycast/utils": "^2.2.3",
"yaml": "^2.7.0"
},
"devDependencies": {
"@raycast/eslint-config": "^1.0.11",
"@types/node": "20.8.10",
"@types/react": "18.3.3",
"eslint": "^8.57.0",
"prettier": "^3.3.3",
"typescript": "^5.7.3"
"@raycast/eslint-config": "^2.1.1",
"@types/node": "22.13.10",
"@types/react": "19.0.10",
"eslint": "^9.22.0",
"prettier": "^3.5.3",
"typescript": "^5.8.2"
},
"scripts": {
"build": "ray build",
Expand All @@ -190,8 +193,5 @@
"lint": "ray lint",
"prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1",
"publish": "npx @raycast/api@latest publish"
},
"platforms": [
"macOS"
]
}
}
147 changes: 141 additions & 6 deletions extensions/ghostty/src/open-with-ghostty.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,146 @@
import { getPreferenceValues } from "@raycast/api";
import fs from "node:fs/promises";
import path from "node:path";
import {
closeMainWindow,
getPreferenceValues,
getSelectedFinderItems,
getFrontmostApplication,
popToRoot,
showToast,
Toast,
} from "@raycast/api";

import { runGhosttyCommand } from "./utils/command";
import { openGhosttyTabAtFinderLocation, openGhosttyWindowAtFinderLocation } from "./utils/scripts";
import { runAppleScript } from "./utils/applescript";

export default async function Command() {
/**
* Get selected items from Path Finder via AppleScript.
*/
async function getSelectedPathFinderItems(): Promise<string[]> {
const result = await runAppleScript(`
tell application "Path Finder"
set thePaths to {}
repeat with pfItem in (get selection)
set the end of thePaths to POSIX path of pfItem
end repeat
set AppleScript's text item delimiters to linefeed
return thePaths as text
end tell
`);
return result
.split("\n")
.map((p) => p.trim())
.filter(Boolean);
Comment thread
lederniermagicien marked this conversation as resolved.
}

/**
* Resolve each selected path to a directory.
* Directories pass through; files resolve to their parent.
* Deduplicates results.
*/
async function resolveDirectories(items: { path: string }[]): Promise<string[]> {
const results = await Promise.all(
items.map(async (item) => {
const info = await fs.stat(item.path);
return info.isDirectory() ? item.path : path.dirname(item.path);
}),
);
return [...new Set(results)];
}

/**
* Fallback: if nothing is selected, open the current Finder window's directory.
* Returns false if Finder isn't frontmost or has no open window.
*/
async function fallbackToFinderWindow(): Promise<boolean> {
const app = await getFrontmostApplication();
if (app.name !== "Finder") return false;

const currentDirectory = await runAppleScript(
`tell application "Finder" to get POSIX path of (target of front window as alias)`,
);
if (!currentDirectory) return false;

await openGhosttyAt(currentDirectory);
return true;
}

/**
* Open Ghostty at the given directory using the native AppleScript API.
* Opens as a new window or tab based on the user's preference.
* Sets the tab/window title to the directory name.
*/
async function openGhosttyAt(directory: string): Promise<void> {
const { openWithGhosttyMode } = getPreferenceValues<Preferences.OpenWithGhostty>();

const script = openWithGhosttyMode === "tab" ? openGhosttyTabAtFinderLocation : openGhosttyWindowAtFinderLocation;
await runGhosttyCommand(script);
// AppleScript escapes double-quotes by doubling them: " → ""
const dirLiteral = `"${directory.replace(/"/g, '""')}"`;
const directoryName = path.basename(directory);
const nameLiteral = `"${directoryName.replace(/"/g, '""')}"`;

const openCommand =
openWithGhosttyMode === "tab"
? `if (count of windows) is 0 then
set win to new window with configuration cfg
else
set win to front window
set newTab to new tab in win with configuration cfg
select tab newTab
end if`
: `set win to new window with configuration cfg`;

await runAppleScript(`
tell application "Ghostty"
activate
set cfg to new surface configuration
set initial working directory of cfg to ${dirLiteral}
${openCommand}
set term to focused terminal of selected tab of win
try
perform action ("set_tab_title:" & ${nameLiteral}) on term
perform action ("set_window_title:" & ${nameLiteral}) on term
end try
input text "clear" to term
send key "enter" to term
focus term
activate window win
end tell
`);
}

export default async function Command() {
try {
let selectedItems: { path: string }[] = [];
const app = await getFrontmostApplication();

if (app.name === "Finder") {
selectedItems = await getSelectedFinderItems();
} else if (app.name === "Path Finder") {
const paths = await getSelectedPathFinderItems();
selectedItems = paths.map((p) => ({ path: p }));
}

if (selectedItems.length > 0) {
const directories = await resolveDirectories(selectedItems);
for (const dir of directories) {
await openGhosttyAt(dir);
}
} else {
const ranFallback = await fallbackToFinderWindow();
if (!ranFallback) {
await showToast({
style: Toast.Style.Failure,
title: "No Finder item selected",
message: "Select a file or folder in Finder or Path Finder to open in Ghostty.",
});
}
}
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Cannot open in Ghostty",
message: String(error),
});
}
await closeMainWindow();
await popToRoot();
}
2 changes: 1 addition & 1 deletion extensions/ghostty/src/utils/applescript.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";

import { runAppleScript as executeAppleScript } from "run-applescript";
import { runAppleScript as executeAppleScript } from "@raycast/utils";

const execFileAsync = promisify(execFile);

Expand Down