Compare commits
2 commits
29b15f19c1
...
a24f7de7df
| Author | SHA1 | Date | |
|---|---|---|---|
| a24f7de7df | |||
| 234a0b74a1 |
8 changed files with 81 additions and 22 deletions
|
|
@ -15,7 +15,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.0.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"react": "^18.3.0",
|
"react": "^18.3.0",
|
||||||
"react-dom": "^18.3.0"
|
"react-dom": "^18.3.0"
|
||||||
|
|
|
||||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
|
|
@ -14,9 +14,15 @@ importers:
|
||||||
'@tauri-apps/plugin-clipboard-manager':
|
'@tauri-apps/plugin-clipboard-manager':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.3.2
|
version: 2.3.2
|
||||||
|
'@tauri-apps/plugin-opener':
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.5.4
|
||||||
'@xterm/addon-fit':
|
'@xterm/addon-fit':
|
||||||
specifier: ^0.10.0
|
specifier: ^0.10.0
|
||||||
version: 0.10.0(@xterm/xterm@5.5.0)
|
version: 0.10.0(@xterm/xterm@5.5.0)
|
||||||
|
'@xterm/addon-web-links':
|
||||||
|
specifier: ^0.12.0
|
||||||
|
version: 0.12.0
|
||||||
'@xterm/xterm':
|
'@xterm/xterm':
|
||||||
specifier: ^5.5.0
|
specifier: ^5.5.0
|
||||||
version: 5.5.0
|
version: 5.5.0
|
||||||
|
|
@ -511,6 +517,9 @@ packages:
|
||||||
'@tauri-apps/plugin-clipboard-manager@2.3.2':
|
'@tauri-apps/plugin-clipboard-manager@2.3.2':
|
||||||
resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==}
|
resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-opener@2.5.4':
|
||||||
|
resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==}
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||||
|
|
||||||
|
|
@ -580,6 +589,9 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@xterm/xterm': ^5.0.0
|
'@xterm/xterm': ^5.0.0
|
||||||
|
|
||||||
|
'@xterm/addon-web-links@0.12.0':
|
||||||
|
resolution: {integrity: sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==}
|
||||||
|
|
||||||
'@xterm/xterm@5.5.0':
|
'@xterm/xterm@5.5.0':
|
||||||
resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==}
|
resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==}
|
||||||
|
|
||||||
|
|
@ -1182,6 +1194,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.11.0
|
'@tauri-apps/api': 2.11.0
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-opener@2.5.4':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.11.0
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.3
|
'@babel/parser': 7.29.3
|
||||||
|
|
@ -1274,6 +1290,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@xterm/xterm': 5.5.0
|
'@xterm/xterm': 5.5.0
|
||||||
|
|
||||||
|
'@xterm/addon-web-links@0.12.0': {}
|
||||||
|
|
||||||
'@xterm/xterm@5.5.0': {}
|
'@xterm/xterm@5.5.0': {}
|
||||||
|
|
||||||
assertion-error@2.0.1: {}
|
assertion-error@2.0.1: {}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ tauri-build = { version = "2", features = [] }
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-clipboard-manager = "2"
|
tauri-plugin-clipboard-manager = "2"
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
"core:event:default",
|
"core:event:default",
|
||||||
"core:window:default",
|
"core:window:default",
|
||||||
"clipboard-manager:allow-read-text",
|
"clipboard-manager:allow-read-text",
|
||||||
"clipboard-manager:allow-write-text"
|
"clipboard-manager:allow-write-text",
|
||||||
|
"opener:allow-open-url"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ pub fn run() {
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
.manage(PtyManager::new())
|
.manage(PtyManager::new())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
commands::list_distros,
|
commands::list_distros,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ use portable_pty::{CommandBuilder, MasterPty, PtySize, native_pty_system};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
|
/// Sentinel "distro" name used to spawn Windows PowerShell instead of WSL.
|
||||||
|
/// Frontend appends this to the distro list it shows in the dropdown.
|
||||||
|
pub const POWERSHELL_DISTRO: &str = "PowerShell";
|
||||||
|
|
||||||
pub type PaneId = u64;
|
pub type PaneId = u64;
|
||||||
|
|
||||||
/// What we keep alive for each spawned PTY.
|
/// What we keep alive for each spawned PTY.
|
||||||
|
|
@ -62,26 +66,40 @@ impl PtyManager {
|
||||||
})
|
})
|
||||||
.context("openpty failed")?;
|
.context("openpty failed")?;
|
||||||
|
|
||||||
let mut cmd = CommandBuilder::new("wsl.exe");
|
let is_powershell = distro.as_deref() == Some(POWERSHELL_DISTRO);
|
||||||
if let Some(d) = distro.as_deref() {
|
|
||||||
cmd.arg("-d");
|
|
||||||
cmd.arg(d);
|
|
||||||
}
|
|
||||||
// Default new panes to the WSL user's home (~) rather than the
|
|
||||||
// Windows-side cwd we inherit from the launcher (typically
|
|
||||||
// C:\Users\<you>, which shows up as /mnt/c/Users/<you> inside WSL).
|
|
||||||
// wsl.exe resolves `~` against the distro's default shell.
|
|
||||||
let resolved_cwd = cwd.as_deref().unwrap_or("~");
|
|
||||||
cmd.arg("--cd");
|
|
||||||
cmd.arg(resolved_cwd);
|
|
||||||
// Force a login shell so .bashrc etc. run and PATH is populated.
|
|
||||||
// wsl.exe without an explicit command launches the default shell
|
|
||||||
// interactively, which is exactly what we want.
|
|
||||||
|
|
||||||
let child = pair
|
let cmd = if is_powershell {
|
||||||
.slave
|
// cwd from the leaf is ignored — leaves may carry Linux-style
|
||||||
.spawn_command(cmd)
|
// paths (e.g. `~`, `/mnt/d/...`) from a previously-assigned WSL
|
||||||
.context("failed to spawn wsl.exe; is WSL installed?")?;
|
// distro that PowerShell wouldn't understand. PowerShell starts
|
||||||
|
// in its own default cwd; user can `cd` if they want.
|
||||||
|
let mut c = CommandBuilder::new("powershell.exe");
|
||||||
|
c.arg("-NoLogo");
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
let mut c = CommandBuilder::new("wsl.exe");
|
||||||
|
if let Some(d) = distro.as_deref() {
|
||||||
|
c.arg("-d");
|
||||||
|
c.arg(d);
|
||||||
|
}
|
||||||
|
// Default new panes to the WSL user's home (~) rather than the
|
||||||
|
// Windows-side cwd we inherit from the launcher (typically
|
||||||
|
// C:\Users\<you>, which shows up as /mnt/c/Users/<you> inside WSL).
|
||||||
|
// wsl.exe resolves `~` against the distro's default shell.
|
||||||
|
let resolved_cwd = cwd.as_deref().unwrap_or("~");
|
||||||
|
c.arg("--cd");
|
||||||
|
c.arg(resolved_cwd);
|
||||||
|
// wsl.exe without an explicit command launches the default shell
|
||||||
|
// interactively, which is exactly what we want.
|
||||||
|
c
|
||||||
|
};
|
||||||
|
|
||||||
|
let spawn_err = if is_powershell {
|
||||||
|
"failed to spawn powershell.exe"
|
||||||
|
} else {
|
||||||
|
"failed to spawn wsl.exe; is WSL installed?"
|
||||||
|
};
|
||||||
|
let child = pair.slave.spawn_command(cmd).context(spawn_err)?;
|
||||||
|
|
||||||
// We need to keep the master alive (drop = close the PTY), but we
|
// We need to keep the master alive (drop = close the PTY), but we
|
||||||
// also need the reader and writer split from it.
|
// also need the reader and writer split from it.
|
||||||
|
|
|
||||||
10
src/App.tsx
10
src/App.tsx
|
|
@ -49,6 +49,9 @@ import "./lib/layout/Gutter.css";
|
||||||
|
|
||||||
const LEGACY_STORAGE_KEY = "tiletopia.tree.v1";
|
const LEGACY_STORAGE_KEY = "tiletopia.tree.v1";
|
||||||
const SAVE_DEBOUNCE_MS = 500;
|
const SAVE_DEBOUNCE_MS = 500;
|
||||||
|
/** Sentinel "distro" the backend recognises to spawn powershell.exe instead
|
||||||
|
* of wsl.exe. Must match `POWERSHELL_DISTRO` in `src-tauri/src/pty.rs`. */
|
||||||
|
const POWERSHELL_DISTRO = "PowerShell";
|
||||||
|
|
||||||
function isInteractiveDistro(name: string): boolean {
|
function isInteractiveDistro(name: string): boolean {
|
||||||
return !name.toLowerCase().startsWith("docker-desktop");
|
return !name.toLowerCase().startsWith("docker-desktop");
|
||||||
|
|
@ -100,11 +103,14 @@ export default function App() {
|
||||||
let resolvedDefault: string | undefined;
|
let resolvedDefault: string | undefined;
|
||||||
try {
|
try {
|
||||||
resolvedDistros = await listDistros();
|
resolvedDistros = await listDistros();
|
||||||
resolvedDefault =
|
|
||||||
resolvedDistros.find(isInteractiveDistro) ?? resolvedDistros[0];
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("list_distros failed:", e);
|
console.warn("list_distros failed:", e);
|
||||||
}
|
}
|
||||||
|
// Append PowerShell as a pseudo-distro so it appears in the titlebar
|
||||||
|
// default-picker and the per-pane dropdown.
|
||||||
|
resolvedDistros = [...resolvedDistros, POWERSHELL_DISTRO];
|
||||||
|
resolvedDefault =
|
||||||
|
resolvedDistros.find(isInteractiveDistro) ?? resolvedDistros[0];
|
||||||
|
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import { useRef, useEffect } from "react";
|
import { useRef, useEffect } from "react";
|
||||||
import { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
import { FitAddon } from "@xterm/addon-fit";
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
|
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||||
import {
|
import {
|
||||||
readText as clipboardReadText,
|
readText as clipboardReadText,
|
||||||
writeText as clipboardWriteText,
|
writeText as clipboardWriteText,
|
||||||
} from "@tauri-apps/plugin-clipboard-manager";
|
} from "@tauri-apps/plugin-clipboard-manager";
|
||||||
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
import {
|
import {
|
||||||
spawnPane,
|
spawnPane,
|
||||||
writeToPane,
|
writeToPane,
|
||||||
|
|
@ -124,6 +126,16 @@ export default function XtermPane({
|
||||||
const fit = new FitAddon();
|
const fit = new FitAddon();
|
||||||
fitRef.current = fit;
|
fitRef.current = fit;
|
||||||
term.loadAddon(fit);
|
term.loadAddon(fit);
|
||||||
|
// Underlines http(s) URLs in the terminal output and routes clicks
|
||||||
|
// through Tauri's opener plugin so they open in the user's default
|
||||||
|
// browser (WebView2 won't navigate on a plain window.open).
|
||||||
|
term.loadAddon(
|
||||||
|
new WebLinksAddon((_event, uri) => {
|
||||||
|
void openUrl(uri).catch((err) =>
|
||||||
|
console.warn("openUrl failed:", err),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
term.open(container);
|
term.open(container);
|
||||||
|
|
||||||
// Initial size — fit before asking the PTY for its dimensions.
|
// Initial size — fit before asking the PTY for its dimensions.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue