feat: SFTP editor opens as popup window instead of inline overlay
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 6s
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 6s
Right-click → Edit now opens a separate Tauri window with the file content in a monospace editor. Ctrl+S saves back to remote via SFTP. Tab inserts 4 spaces. Modified indicator in toolbar. Removed the inline EditorWindow overlay that covered the terminal. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
016906fc9d
commit
3e548ed10e
@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Default capabilities for the main Wraith window","local":true,"windows":["main","tool-*"],"permissions":["core:default","core:event:default","core:window:default","shell:allow-open"]}}
|
{"default":{"identifier":"default","description":"Default capabilities for the main Wraith window","local":true,"windows":["main","tool-*"],"permissions":["core:default","core:event:default","core:window:default","core:window:allow-create","core:webview:default","core:webview:allow-create-webview-window","shell:allow-open"]}}
|
||||||
107
src/components/tools/FileEditor.vue
Normal file
107
src/components/tools/FileEditor.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full bg-[#0d1117]">
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="flex items-center gap-2 px-3 py-2 bg-[#161b22] border-b border-[#30363d] shrink-0">
|
||||||
|
<span class="text-xs text-[#8b949e] font-mono truncate flex-1">{{ filePath }}</span>
|
||||||
|
<span v-if="modified" class="text-[10px] text-[#e3b341]">modified</span>
|
||||||
|
<button
|
||||||
|
class="px-3 py-1 text-xs font-bold rounded bg-[#238636] text-white cursor-pointer disabled:opacity-40"
|
||||||
|
:disabled="saving || !modified"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
|
{{ saving ? "Saving..." : "Save" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor area -->
|
||||||
|
<div ref="editorContainer" class="flex-1 min-h-0" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
sessionId: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const filePath = ref("");
|
||||||
|
const content = ref("");
|
||||||
|
const modified = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const editorContainer = ref<HTMLElement | null>(null);
|
||||||
|
let editorContent = "";
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Parse path from URL
|
||||||
|
const params = new URLSearchParams(window.location.hash.split("?")[1] || "");
|
||||||
|
filePath.value = decodeURIComponent(params.get("path") || "");
|
||||||
|
|
||||||
|
if (!filePath.value || !props.sessionId) return;
|
||||||
|
|
||||||
|
// Load file content
|
||||||
|
try {
|
||||||
|
content.value = await invoke<string>("sftp_read_file", {
|
||||||
|
sessionId: props.sessionId,
|
||||||
|
path: filePath.value,
|
||||||
|
});
|
||||||
|
editorContent = content.value;
|
||||||
|
} catch (err) {
|
||||||
|
content.value = `Error loading file: ${err}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a simple textarea editor (CodeMirror can be added later)
|
||||||
|
if (editorContainer.value) {
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = content.value;
|
||||||
|
textarea.spellcheck = false;
|
||||||
|
textarea.style.cssText = `
|
||||||
|
width: 100%; height: 100%; resize: none; border: none; outline: none;
|
||||||
|
background: #0d1117; color: #e0e0e0; padding: 12px; font-size: 13px;
|
||||||
|
font-family: 'Cascadia Mono', 'Cascadia Code', Consolas, 'JetBrains Mono', monospace;
|
||||||
|
line-height: 1.5; tab-size: 4;
|
||||||
|
`;
|
||||||
|
textarea.addEventListener("input", () => {
|
||||||
|
editorContent = textarea.value;
|
||||||
|
modified.value = editorContent !== content.value;
|
||||||
|
});
|
||||||
|
textarea.addEventListener("keydown", (e) => {
|
||||||
|
// Ctrl+S to save
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||||
|
e.preventDefault();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
// Tab inserts spaces
|
||||||
|
if (e.key === "Tab") {
|
||||||
|
e.preventDefault();
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
textarea.value = textarea.value.substring(0, start) + " " + textarea.value.substring(end);
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = start + 4;
|
||||||
|
editorContent = textarea.value;
|
||||||
|
modified.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
editorContainer.value.appendChild(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function save(): Promise<void> {
|
||||||
|
if (!modified.value || saving.value) return;
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await invoke("sftp_write_file", {
|
||||||
|
sessionId: props.sessionId,
|
||||||
|
path: filePath.value,
|
||||||
|
content: editorContent,
|
||||||
|
});
|
||||||
|
content.value = editorContent;
|
||||||
|
modified.value = false;
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Save failed: ${err}`);
|
||||||
|
}
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -9,6 +9,7 @@
|
|||||||
<WhoisTool v-else-if="tool === 'whois'" :session-id="sessionId" />
|
<WhoisTool v-else-if="tool === 'whois'" :session-id="sessionId" />
|
||||||
<BandwidthTest v-else-if="tool === 'bandwidth'" :session-id="sessionId" />
|
<BandwidthTest v-else-if="tool === 'bandwidth'" :session-id="sessionId" />
|
||||||
<SubnetCalc v-else-if="tool === 'subnet-calc'" />
|
<SubnetCalc v-else-if="tool === 'subnet-calc'" />
|
||||||
|
<FileEditor v-else-if="tool === 'editor'" :session-id="sessionId" />
|
||||||
<SshKeyGen v-else-if="tool === 'ssh-keygen'" />
|
<SshKeyGen v-else-if="tool === 'ssh-keygen'" />
|
||||||
<PasswordGen v-else-if="tool === 'password-gen'" />
|
<PasswordGen v-else-if="tool === 'password-gen'" />
|
||||||
<div v-else class="flex-1 flex items-center justify-center text-sm text-[#484f58]">
|
<div v-else class="flex-1 flex items-center justify-center text-sm text-[#484f58]">
|
||||||
@ -27,6 +28,7 @@ import DnsLookup from "./DnsLookup.vue";
|
|||||||
import WhoisTool from "./WhoisTool.vue";
|
import WhoisTool from "./WhoisTool.vue";
|
||||||
import BandwidthTest from "./BandwidthTest.vue";
|
import BandwidthTest from "./BandwidthTest.vue";
|
||||||
import SubnetCalc from "./SubnetCalc.vue";
|
import SubnetCalc from "./SubnetCalc.vue";
|
||||||
|
import FileEditor from "./FileEditor.vue";
|
||||||
import SshKeyGen from "./SshKeyGen.vue";
|
import SshKeyGen from "./SshKeyGen.vue";
|
||||||
import PasswordGen from "./PasswordGen.vue";
|
import PasswordGen from "./PasswordGen.vue";
|
||||||
|
|
||||||
|
|||||||
@ -235,15 +235,6 @@
|
|||||||
<!-- Tab bar -->
|
<!-- Tab bar -->
|
||||||
<TabBar />
|
<TabBar />
|
||||||
|
|
||||||
<!-- Inline file editor -->
|
|
||||||
<EditorWindow
|
|
||||||
v-if="editorFile"
|
|
||||||
:content="editorFile.content"
|
|
||||||
:file-path="editorFile.path"
|
|
||||||
:session-id="editorFile.sessionId"
|
|
||||||
@close="editorFile = null"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Session area -->
|
<!-- Session area -->
|
||||||
<SessionContainer ref="sessionContainer" />
|
<SessionContainer ref="sessionContainer" />
|
||||||
</div>
|
</div>
|
||||||
@ -384,9 +375,20 @@ function handleThemeSelect(theme: ThemeDefinition): void {
|
|||||||
async function handleOpenFile(entry: FileEntry): Promise<void> {
|
async function handleOpenFile(entry: FileEntry): Promise<void> {
|
||||||
if (!activeSessionId.value) return;
|
if (!activeSessionId.value) return;
|
||||||
try {
|
try {
|
||||||
const content = await invoke<string>("sftp_read_file", { sessionId: activeSessionId.value, path: entry.path });
|
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
||||||
editorFile.value = { path: entry.path, content, sessionId: activeSessionId.value };
|
const fileName = entry.path.split("/").pop() || entry.path;
|
||||||
} catch (err) { console.error("Failed to open SFTP file:", err); }
|
const label = `editor-${Date.now()}`;
|
||||||
|
const sessionId = activeSessionId.value;
|
||||||
|
|
||||||
|
new WebviewWindow(label, {
|
||||||
|
title: `${fileName} — Wraith Editor`,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
resizable: true,
|
||||||
|
center: true,
|
||||||
|
url: `index.html#/tool/editor?sessionId=${sessionId}&path=${encodeURIComponent(entry.path)}`,
|
||||||
|
});
|
||||||
|
} catch (err) { console.error("Failed to open editor:", err); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleQuickConnect(): Promise<void> {
|
async function handleQuickConnect(): Promise<void> {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user