wraith/src/composables/useSftp.ts
Vantz Stockwell e9b504c733
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m0s
fix: SFTP browser — default to / instead of /home, strip quotes from CWD
/home doesn't exist on macOS (home dirs are /Users/). Changed default
SFTP path to / so it always loads. OSC 7 parser now strips stray
quotes from shell printf output that produced paths like /"/path".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 01:29:38 -04:00

144 lines
3.6 KiB
TypeScript

import { ref, watch, onBeforeUnmount, type Ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
export interface FileEntry {
name: string;
path: string;
size: number;
isDir: boolean;
permissions: string;
modTime: string;
}
export interface UseSftpReturn {
currentPath: Ref<string>;
entries: Ref<FileEntry[]>;
isLoading: Ref<boolean>;
followTerminal: Ref<boolean>;
navigateTo: (path: string) => Promise<void>;
goUp: () => Promise<void>;
refresh: () => Promise<void>;
}
// Persist the last browsed path per session so switching tabs restores position
const sessionPaths: Record<string, string> = {};
/**
* Composable that manages SFTP file browsing state.
* Accepts a reactive session ID ref so it reinitializes on tab switch
* without destroying the component.
*/
export function useSftp(sessionIdRef: Ref<string>): UseSftpReturn {
const currentPath = ref("/");
const entries = ref<FileEntry[]>([]);
const isLoading = ref(false);
const followTerminal = ref(true);
let unlistenCwd: UnlistenFn | null = null;
let currentSessionId = "";
async function listDirectory(sessionId: string, path: string): Promise<FileEntry[]> {
try {
const result = await invoke<FileEntry[]>("sftp_list", { sessionId, path });
return result ?? [];
} catch (err) {
console.error("SFTP list error:", err);
return [];
}
}
async function navigateTo(path: string): Promise<void> {
if (!currentSessionId) return;
isLoading.value = true;
try {
currentPath.value = path;
sessionPaths[currentSessionId] = path;
entries.value = await listDirectory(currentSessionId, path);
} finally {
isLoading.value = false;
}
}
async function goUp(): Promise<void> {
const parts = currentPath.value.split("/").filter(Boolean);
if (parts.length <= 1) {
await navigateTo("/");
return;
}
parts.pop();
await navigateTo("/" + parts.join("/"));
}
async function refresh(): Promise<void> {
await navigateTo(currentPath.value);
}
async function switchToSession(sessionId: string): Promise<void> {
if (!sessionId) {
entries.value = [];
return;
}
// Save current path for the old session
if (currentSessionId) {
sessionPaths[currentSessionId] = currentPath.value;
}
// Unlisten old CWD events
if (unlistenCwd) {
unlistenCwd();
unlistenCwd = null;
}
currentSessionId = sessionId;
// Restore saved path or default to root
const savedPath = sessionPaths[sessionId] || "/";
currentPath.value = savedPath;
// Load the directory
isLoading.value = true;
try {
entries.value = await listDirectory(sessionId, savedPath);
} finally {
isLoading.value = false;
}
// Listen for CWD changes on the new session
try {
unlistenCwd = await listen<string>(`ssh:cwd:${sessionId}`, (event) => {
if (!followTerminal.value) return;
const newPath = event.payload;
if (newPath && newPath !== currentPath.value) {
navigateTo(newPath);
}
});
} catch {
// Event listener setup failed — non-fatal
}
}
// React to session ID changes
watch(sessionIdRef, (newId) => {
switchToSession(newId);
}, { immediate: true });
onBeforeUnmount(() => {
if (currentSessionId) {
sessionPaths[currentSessionId] = currentPath.value;
}
if (unlistenCwd) unlistenCwd();
});
return {
currentPath,
entries,
isLoading,
followTerminal,
navigateTo,
goUp,
refresh,
};
}