Files
NetMesh/components/sftp/hooks/useSftpPanePath.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

165 lines
5.3 KiB
TypeScript

import React, { useCallback, useMemo, useRef, useState } from "react";
import type { SftpFileEntry } from "../../../types";
import type { SftpPane } from "../../../application/state/sftp/types";
import {
isWindowsPath,
normalizeSftpNavigationPath,
} from "../../../application/state/sftp/utils";
import { filterHiddenFiles, isNavigableDirectory } from "../utils";
interface UseSftpPanePathParams {
connection: SftpPane["connection"] | null;
files: SftpFileEntry[];
showHiddenFiles: boolean;
onNavigateTo: (path: string) => void;
}
interface UseSftpPanePathResult {
isEditingPath: boolean;
editingPathValue: string;
showPathSuggestions: boolean;
pathSuggestionIndex: number;
pathInputRef: React.RefObject<HTMLInputElement>;
pathDropdownRef: React.RefObject<HTMLDivElement>;
pathSuggestions: { path: string; type: "folder" | "history" }[];
setEditingPathValue: (value: string) => void;
setShowPathSuggestions: (value: boolean) => void;
setPathSuggestionIndex: (value: number) => void;
handlePathBlur: () => void;
handlePathKeyDown: (e: React.KeyboardEvent) => void;
handlePathDoubleClick: () => void;
handlePathSubmit: (pathOverride?: string) => void;
}
export const useSftpPanePath = ({
connection,
files,
showHiddenFiles,
onNavigateTo,
}: UseSftpPanePathParams): UseSftpPanePathResult => {
const [isEditingPath, setIsEditingPath] = useState(false);
const [editingPathValue, setEditingPathValue] = useState("");
const [showPathSuggestions, setShowPathSuggestions] = useState(false);
const [pathSuggestionIndex, setPathSuggestionIndex] = useState(-1);
const pathInputRef = useRef<HTMLInputElement>(null);
const pathDropdownRef = useRef<HTMLDivElement>(null);
const pathSuggestions = useMemo(() => {
if (!isEditingPath || !connection) return [];
const currentValue = editingPathValue.trim().toLowerCase();
const suggestions: { path: string; type: "folder" | "history" }[] = [];
const folders = filterHiddenFiles(files, showHiddenFiles).filter(
(f) => isNavigableDirectory(f) && f.name !== "..",
);
folders.forEach((f) => {
const fullPath =
connection.currentPath === "/"
? `/${f.name}`
: `${connection.currentPath}/${f.name}`;
if (
!currentValue ||
fullPath.toLowerCase().includes(currentValue) ||
f.name.toLowerCase().includes(currentValue)
) {
suggestions.push({ path: fullPath, type: "folder" });
}
});
const quickPaths = ["/home", "/var", "/etc", "/tmp", "/usr", "/opt", "/root"];
quickPaths.forEach((qp) => {
if (!currentValue || qp.toLowerCase().includes(currentValue)) {
if (!suggestions.some((s) => s.path === qp)) {
suggestions.push({ path: qp, type: "history" });
}
}
});
return suggestions.slice(0, 8);
}, [connection, editingPathValue, files, isEditingPath, showHiddenFiles]);
const handlePathDoubleClick = () => {
if (!connection) return;
setEditingPathValue(connection.currentPath);
setIsEditingPath(true);
setShowPathSuggestions(true);
setPathSuggestionIndex(-1);
setTimeout(() => pathInputRef.current?.select(), 0);
};
const handlePathSubmit = useCallback((pathOverride?: string) => {
// Only treat //host/share as UNC when this pane is already on a Windows-style path.
const acceptForwardSlashUnc = !!(
connection && isWindowsPath(connection.currentPath)
);
const newPath = normalizeSftpNavigationPath(
pathOverride ?? editingPathValue,
{ acceptForwardSlashUnc },
);
setIsEditingPath(false);
setShowPathSuggestions(false);
setPathSuggestionIndex(-1);
if (connection && newPath !== connection.currentPath) {
onNavigateTo(newPath);
}
}, [connection, editingPathValue, onNavigateTo]);
const handlePathKeyDown = (e: React.KeyboardEvent) => {
if (showPathSuggestions && pathSuggestions.length > 0) {
if (e.key === "ArrowDown") {
e.preventDefault();
setPathSuggestionIndex((prev) =>
prev < pathSuggestions.length - 1 ? prev + 1 : 0,
);
return;
} else if (e.key === "ArrowUp") {
e.preventDefault();
setPathSuggestionIndex((prev) =>
prev > 0 ? prev - 1 : pathSuggestions.length - 1,
);
return;
} else if (e.key === "Tab" && pathSuggestionIndex >= 0) {
e.preventDefault();
setEditingPathValue(pathSuggestions[pathSuggestionIndex].path);
return;
}
}
if (e.key === "Enter") {
if (pathSuggestionIndex >= 0 && pathSuggestions[pathSuggestionIndex]) {
handlePathSubmit(pathSuggestions[pathSuggestionIndex].path);
} else {
handlePathSubmit();
}
} else if (e.key === "Escape") {
setIsEditingPath(false);
setShowPathSuggestions(false);
setPathSuggestionIndex(-1);
}
};
const handlePathBlur = useCallback(() => {
setTimeout(() => {
if (!pathDropdownRef.current?.contains(document.activeElement)) {
handlePathSubmit();
}
}, 150);
}, [handlePathSubmit]);
return {
isEditingPath,
editingPathValue,
showPathSuggestions,
pathSuggestionIndex,
pathInputRef,
pathDropdownRef,
pathSuggestions,
setEditingPathValue,
setShowPathSuggestions,
setPathSuggestionIndex,
handlePathBlur,
handlePathKeyDown,
handlePathDoubleClick,
handlePathSubmit,
};
};