diff --git a/apps/desktop/src/components/ProjectSettingsMenuAction.svelte b/apps/desktop/src/components/ProjectSettingsMenuAction.svelte index ddd8e2a19c..6a72b493d3 100644 --- a/apps/desktop/src/components/ProjectSettingsMenuAction.svelte +++ b/apps/desktop/src/components/ProjectSettingsMenuAction.svelte @@ -6,7 +6,7 @@ import { historyPath } from '$lib/routes/routes.svelte'; import { SETTINGS } from '$lib/settings/userSettings'; import { SHORTCUT_SERVICE } from '$lib/shortcuts/shortcutService'; - import { getEditorUri, openExternalUrl, showFileInFolder } from '$lib/utils/url'; + import { getEditorUri, openExternalUrl, showFileInFolder, openInTerminal } from '$lib/utils/url'; import { inject } from '@gitbutler/shared/context'; import { mergeUnlisten } from '@gitbutler/ui/utils/mergeUnlisten'; @@ -38,6 +38,13 @@ }) ); }), + shortcutService.on('open-in-terminal', async () => { + const project = await projectsService.fetchProject(projectId); + if (!project) { + throw new Error(`Project not found: ${projectId}`); + } + await openInTerminal($userSettings.defaultTerminal.appName, vscodePath(project.path)); + }), shortcutService.on('show-in-finder', async () => { const project = await projectsService.fetchProject(projectId); if (!project) { diff --git a/apps/desktop/src/components/profileSettings/GeneralSettings.svelte b/apps/desktop/src/components/profileSettings/GeneralSettings.svelte index 988f5cdb61..34e46b5b24 100644 --- a/apps/desktop/src/components/profileSettings/GeneralSettings.svelte +++ b/apps/desktop/src/components/profileSettings/GeneralSettings.svelte @@ -7,7 +7,11 @@ import { SETTINGS_SERVICE } from '$lib/config/appSettingsV2'; import { showError } from '$lib/notifications/toasts'; import { PROJECTS_SERVICE } from '$lib/project/projectsService'; - import { SETTINGS, type CodeEditorSettings } from '$lib/settings/userSettings'; + import { + SETTINGS, + type CodeEditorSettings, + type TerminalSettings + } from '$lib/settings/userSettings'; import { UPDATER_SERVICE } from '$lib/updater/updater'; import { USER_SERVICE } from '$lib/user/userService'; @@ -58,6 +62,19 @@ label: option.displayName, value: option.schemeIdentifer })); + const terminalOptions: TerminalSettings[] = [ + { appName: 'Terminal.app', displayName: 'Terminal' }, + { appName: 'Ghostty.app', displayName: 'Ghostty' }, + { appName: 'Warp.app', displayName: 'Warp' }, + { appName: 'iTerm.app', displayName: 'iTerm 2' }, + { appName: 'Alacritty.app', displayName: 'Alacritty' }, + { appName: 'WezTerm.app', displayName: 'WezTerm' }, + { appName: 'Hyper.app', displayName: 'Hyper' } + ]; + const terminalOptionsForSelect = terminalOptions.map((option) => ({ + label: option.displayName, + value: option.appName + })); $effect(() => { if ($user && !loaded) { @@ -174,32 +191,58 @@ {/if} - - {#snippet title()} - Default code editor - {/snippet} - {#snippet actions()} - - {/snippet} - +
+ + {#snippet title()} + Default code editor + {/snippet} + {#snippet actions()} + + {/snippet} + + + + {#snippet title()} + Default terminal + {/snippet} + {#snippet actions()} + + {/snippet} + +
{#snippet title()} diff --git a/apps/desktop/src/lib/settings/userSettings.ts b/apps/desktop/src/lib/settings/userSettings.ts index 3083f0b244..31f17e1672 100644 --- a/apps/desktop/src/lib/settings/userSettings.ts +++ b/apps/desktop/src/lib/settings/userSettings.ts @@ -9,6 +9,10 @@ export type CodeEditorSettings = { schemeIdentifer: string; displayName: string; }; +export type TerminalSettings = { + appName: string; + displayName: string; +}; export interface Settings { aiSummariesEnabled?: boolean; @@ -30,6 +34,7 @@ export interface Settings { inlineUnifiedDiffs: boolean; diffContrast: 'light' | 'medium' | 'strong'; defaultCodeEditor: CodeEditorSettings; + defaultTerminal: TerminalSettings; } const defaults: Settings = { @@ -50,7 +55,8 @@ const defaults: Settings = { diffLigatures: false, inlineUnifiedDiffs: false, diffContrast: 'light', - defaultCodeEditor: { schemeIdentifer: 'vscode', displayName: 'VSCode' } + defaultCodeEditor: { schemeIdentifer: 'vscode', displayName: 'VSCode' }, + defaultTerminal: { appName: 'Terminal.app', displayName: 'Terminal' } }; export function loadUserSettings(): Writable { diff --git a/apps/desktop/src/lib/utils/url.ts b/apps/desktop/src/lib/utils/url.ts index 20236c46f4..93ff3dedb5 100644 --- a/apps/desktop/src/lib/utils/url.ts +++ b/apps/desktop/src/lib/utils/url.ts @@ -51,3 +51,7 @@ export function getEditorUri(params: EditorUriParams): string { return `${params.schemeId}://file${pathString}${positionSuffix}${searchSuffix}`; } + +export async function openInTerminal(appName: string, path: string) { + await invoke('open_in_terminal', { appName, path }); +} diff --git a/crates/but-api/src/commands/open.rs b/crates/but-api/src/commands/open.rs index fdcafebdf4..b60f9246be 100644 --- a/crates/but-api/src/commands/open.rs +++ b/crates/but-api/src/commands/open.rs @@ -150,3 +150,24 @@ pub fn show_in_finder(_app: &App, params: ShowInFinderParams) -> Result<(), Erro Ok(()) } + +#[derive(Deserialize)] +pub struct OpenInTerminalParams { + pub app_name: String, + pub path: String, +} + +pub fn open_in_terminal(_app: &App, params: OpenInTerminalParams) -> Result<(), Error> { + #[cfg(target_os = "macos")] + { + use std::process::Command; + Command::new("open") + .arg("-a") + .arg(¶ms.app_name) + .arg(¶ms.path) + .status() + .with_context(|| format!("Failed to show '{}' in Finder", params.path))?; + } + + Ok(()) +} diff --git a/crates/gitbutler-tauri/src/main.rs b/crates/gitbutler-tauri/src/main.rs index eddb05745d..c7db0eceb3 100644 --- a/crates/gitbutler-tauri/src/main.rs +++ b/crates/gitbutler-tauri/src/main.rs @@ -271,6 +271,7 @@ fn main() { modes::edit_changes_from_initial, open::open_url, open::show_in_finder, + open::open_in_terminal, forge::pr_templates, forge::pr_template, settings::get_app_settings, diff --git a/crates/gitbutler-tauri/src/menu.rs b/crates/gitbutler-tauri/src/menu.rs index 87b59b90ff..9aa908ee7c 100644 --- a/crates/gitbutler-tauri/src/menu.rs +++ b/crates/gitbutler-tauri/src/menu.rs @@ -158,7 +158,8 @@ pub fn build( .build(handle)?, ) .separator() - .text("project/open-in-vscode", "Open in Editor"); + .text("project/open-in-vscode", "Open in Editor") + .text("project/open-in-terminal", "Open in Terminal"); #[cfg(target_os = "macos")] { @@ -322,6 +323,11 @@ pub fn handle_event( return; } + if event.id() == "project/open-in-terminal" { + emit(webview, SHORTCUT_EVENT, "open-in-terminal"); + return; + } + if event.id() == "project/show-in-finder" { emit(webview, SHORTCUT_EVENT, "show-in-finder"); return; diff --git a/crates/gitbutler-tauri/src/open.rs b/crates/gitbutler-tauri/src/open.rs index 6f4f20b36f..691dcabeaa 100644 --- a/crates/gitbutler-tauri/src/open.rs +++ b/crates/gitbutler-tauri/src/open.rs @@ -15,3 +15,9 @@ pub fn open_url(app: State<'_, App>, url: String) -> Result<(), Error> { pub fn show_in_finder(app: State<'_, App>, path: String) -> Result<(), Error> { open::show_in_finder(&app, open::ShowInFinderParams { path }) } + +#[tauri::command(async)] +#[instrument(skip(app), err(Debug))] +pub fn open_in_terminal(app: State<'_, App>, app_name: String, path: String) -> Result<(), Error> { + open::open_in_terminal(&app, open::OpenInTerminalParams { app_name, path }) +}