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; entries: Ref; isLoading: Ref; followTerminal: Ref; navigateTo: (path: string) => Promise; goUp: () => Promise; refresh: () => Promise; } // Persist the last browsed path per session so switching tabs restores position const sessionPaths: Record = {}; /** * 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): UseSftpReturn { const currentPath = ref("/"); const entries = ref([]); const isLoading = ref(false); const followTerminal = ref(true); let unlistenCwd: UnlistenFn | null = null; let currentSessionId = ""; async function listDirectory(sessionId: string, path: string): Promise { try { const result = await invoke("sftp_list", { sessionId, path }); return result ?? []; } catch (err) { console.error("SFTP list error:", err); return []; } } async function navigateTo(path: string): Promise { 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 { const parts = currentPath.value.split("/").filter(Boolean); if (parts.length <= 1) { await navigateTo("/"); return; } parts.pop(); await navigateTo("/" + parts.join("/")); } async function refresh(): Promise { await navigateTo(currentPath.value); } async function switchToSession(sessionId: string): Promise { 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(`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, }; }