All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m0s
/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>
144 lines
3.6 KiB
TypeScript
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,
|
|
};
|
|
}
|