diff --git a/.github/assets/open-in-terax-context-menu.png b/.github/assets/open-in-terax-context-menu.png new file mode 100644 index 00000000..c856b7cb Binary files /dev/null and b/.github/assets/open-in-terax-context-menu.png differ diff --git a/src-tauri/installer-hooks.nsh b/src-tauri/installer-hooks.nsh new file mode 100644 index 00000000..e3ddb82b --- /dev/null +++ b/src-tauri/installer-hooks.nsh @@ -0,0 +1,26 @@ +; "Open in Terax" shell verbs for folders, folder backgrounds, and drives. +; HKCU matches installer currentUser scope. %V = clicked path. +; NoWorkingDirectory keeps Explorer from overriding %V (System32 on Drive). + +!macro NSIS_HOOK_POSTINSTALL + WriteRegStr HKCU "Software\Classes\Directory\shell\OpenInTerax" "" "Open in Terax" + WriteRegStr HKCU "Software\Classes\Directory\shell\OpenInTerax" "Icon" '"$INSTDIR\terax.exe",0' + WriteRegStr HKCU "Software\Classes\Directory\shell\OpenInTerax" "NoWorkingDirectory" "" + WriteRegStr HKCU "Software\Classes\Directory\shell\OpenInTerax\command" "" '"$INSTDIR\terax.exe" "%V"' + + WriteRegStr HKCU "Software\Classes\Directory\Background\shell\OpenInTerax" "" "Open in Terax" + WriteRegStr HKCU "Software\Classes\Directory\Background\shell\OpenInTerax" "Icon" '"$INSTDIR\terax.exe",0' + WriteRegStr HKCU "Software\Classes\Directory\Background\shell\OpenInTerax" "NoWorkingDirectory" "" + WriteRegStr HKCU "Software\Classes\Directory\Background\shell\OpenInTerax\command" "" '"$INSTDIR\terax.exe" "%V"' + + WriteRegStr HKCU "Software\Classes\Drive\shell\OpenInTerax" "" "Open in Terax" + WriteRegStr HKCU "Software\Classes\Drive\shell\OpenInTerax" "Icon" '"$INSTDIR\terax.exe",0' + WriteRegStr HKCU "Software\Classes\Drive\shell\OpenInTerax" "NoWorkingDirectory" "" + WriteRegStr HKCU "Software\Classes\Drive\shell\OpenInTerax\command" "" '"$INSTDIR\terax.exe" "%V"' +!macroend + +!macro NSIS_HOOK_POSTUNINSTALL + DeleteRegKey HKCU "Software\Classes\Directory\shell\OpenInTerax" + DeleteRegKey HKCU "Software\Classes\Directory\Background\shell\OpenInTerax" + DeleteRegKey HKCU "Software\Classes\Drive\shell\OpenInTerax" +!macroend diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7c520439..576f3a21 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,9 +1,34 @@ mod modules; use modules::{fs, git, net, pty, secrets, shell, workspace}; -use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; +use std::sync::Mutex; +use tauri::{Emitter, Manager, State, WebviewUrl, WebviewWindowBuilder}; use tauri_plugin_window_state::StateFlags; +/// Drained on first read so HMR / re-mounts can't replay the launch dir. +#[derive(Default)] +struct LaunchDir(Mutex>); + +#[tauri::command] +fn get_launch_dir(state: State<'_, LaunchDir>) -> Option { + state.0.lock().expect("LaunchDir mutex poisoned").take() +} + +fn parse_launch_dir() -> Option { + for arg in std::env::args().skip(1) { + if arg.starts_with('-') { + continue; + } + let Ok(canon) = std::fs::canonicalize(&arg) else { continue }; + if !canon.is_dir() { + continue; + } + let s = canon.to_string_lossy(); + return Some(s.strip_prefix(r"\\?\").unwrap_or(&s).to_string()); + } + None +} + #[tauri::command] async fn open_settings_window(app: tauri::AppHandle, tab: Option) -> Result<(), String> { let url_path = match tab.as_deref() { @@ -89,6 +114,7 @@ pub fn run() { workspace::bootstrap_registry(®istry); registry }) + .manage(LaunchDir(Mutex::new(parse_launch_dir()))) .invoke_handler(tauri::generate_handler![ pty::pty_open, pty::pty_write, @@ -138,6 +164,7 @@ pub fn run() { workspace::wsl_home, workspace::workspace_authorize, workspace::workspace_current_dir, + get_launch_dir, open_settings_window, secrets::secrets_get, secrets::secrets_set, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index dde801d1..ec36b774 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -60,7 +60,8 @@ "nsis": { "installMode": "currentUser", "languages": ["English"], - "displayLanguageSelector": false + "displayLanguageSelector": false, + "installerHooks": "./installer-hooks.nsh" } }, "category": "DeveloperTool", diff --git a/src/app/App.tsx b/src/app/App.tsx index c4d69745..9a9e32ca 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -38,6 +38,7 @@ import { type EditorPaneHandle, } from "@/modules/editor"; import { GitHistoryStack } from "@/modules/git-history"; +import { getLaunchDir } from "@/lib/launchDir"; import { useZoom } from "@/lib/useZoom"; import { FileExplorer, type FileExplorerHandle } from "@/modules/explorer"; import { @@ -161,7 +162,7 @@ export default function App() { closeActivePane, closePaneByLeaf, resetWorkspace, - } = useTabs(); + } = useTabs(getLaunchDir() ? { cwd: getLaunchDir() } : undefined); // Mirror `tabs` into a ref so callbacks scheduled with `setTimeout` // (e.g. cdInNewTab) read the latest pane state instead of a stale closure. diff --git a/src/lib/launchDir.ts b/src/lib/launchDir.ts new file mode 100644 index 00000000..4dbe6d1b --- /dev/null +++ b/src/lib/launchDir.ts @@ -0,0 +1,12 @@ +import { invoke } from "@tauri-apps/api/core"; + +let cached: string | undefined; + +export async function initLaunchDir(): Promise { + const dir = await invoke("get_launch_dir").catch(() => null); + cached = dir ? dir.replace(/\\/g, "/") : undefined; +} + +export function getLaunchDir(): string | undefined { + return cached; +} diff --git a/src/main.tsx b/src/main.tsx index 5f20487c..d499c645 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,12 +8,16 @@ import "./styles/globals.css"; import { getCurrentWindow } from "@tauri-apps/api/window"; import ReactDOM from "react-dom/client"; import App from "./app/App"; +import { initLaunchDir } from "./lib/launchDir"; import { USE_CUSTOM_WINDOW_CONTROLS } from "./lib/platform"; if (USE_CUSTOM_WINDOW_CONTROLS) { document.documentElement.dataset.chrome = "borderless"; } +// Seed before first paint so default tab mounts at target cwd (no flicker). +await initLaunchDir(); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( , );