All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 4m5s
VUE-1: store workspace save interval ID, clear in onUnmounted VUE-2: extract beforeunload handler to named function, remove in onUnmounted VUE-3: move useTerminal() to <script setup> top level in DetachedSession VUE-4: call useTerminal() before nextTick await in CopilotPanel launch() VUE-5: remove duplicate ResizeObserver from LocalTerminalView (useTerminal already creates one) VUE-6: store terminal.onResize() IDisposable, dispose in onBeforeUnmount VUE-7: extract connectSsh(), connectRdp(), resolveCredentials() from 220-line connect() VUE-8: check session protocol before ssh_resize vs pty_resize in TerminalView Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
557 lines
27 KiB
Vue
557 lines
27 KiB
Vue
<template>
|
|
<div class="h-screen w-screen flex flex-col overflow-hidden">
|
|
<!-- Toolbar -->
|
|
<div
|
|
class="h-10 flex items-center justify-between px-4 bg-[var(--wraith-bg-secondary)] border-b border-[var(--wraith-border)] shrink-0"
|
|
data-tauri-drag-region
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-sm font-bold tracking-widest text-[var(--wraith-accent-blue)]">
|
|
WRAITH
|
|
</span>
|
|
|
|
<!-- File menu -->
|
|
<div class="relative">
|
|
<button
|
|
class="text-xs text-[var(--wraith-text-secondary)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer px-2 py-1 rounded hover:bg-[var(--wraith-bg-tertiary)]"
|
|
@click="showFileMenu = !showFileMenu"
|
|
@blur="closeFileMenuDeferred"
|
|
>
|
|
File
|
|
</button>
|
|
<div
|
|
v-if="showFileMenu"
|
|
class="absolute top-full left-0 mt-0.5 w-56 bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden z-50 py-1"
|
|
>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleFileMenuAction('new-connection')"
|
|
>
|
|
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"/></svg>
|
|
<span class="flex-1">New Connection</span>
|
|
<kbd class="text-[10px] text-[var(--wraith-text-muted)]">Ctrl+N</kbd>
|
|
</button>
|
|
<div class="border-t border-[#30363d] my-1" />
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleFileMenuAction('settings')"
|
|
>
|
|
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0a8.2 8.2 0 0 1 .701.031C8.955.017 9.209 0 9.466 0a1.934 1.934 0 0 1 1.466.665c.33.367.51.831.54 1.316a7.96 7.96 0 0 1 .82.4c.463-.207.97-.29 1.476-.19.504.1.963.37 1.3.77.339.404.516.91.5 1.423a1.94 1.94 0 0 1-.405 1.168 8.02 8.02 0 0 1 .356.9 1.939 1.939 0 0 1 1.48.803 1.941 1.941 0 0 1 0 2.29 1.939 1.939 0 0 1-1.48.803c-.095.316-.215.622-.357.9a1.94 1.94 0 0 1-.094 2.59 1.94 1.94 0 0 1-2.776.22 7.96 7.96 0 0 1-.82.4 1.94 1.94 0 0 1-2.006 1.98A8.2 8.2 0 0 1 8 16a8.2 8.2 0 0 1-.701-.031 1.938 1.938 0 0 1-2.005-1.98 7.96 7.96 0 0 1-.82-.4 1.94 1.94 0 0 1-2.776-.22 1.94 1.94 0 0 1-.094-2.59 8.02 8.02 0 0 1-.357-.9A1.939 1.939 0 0 1 0 8.945a1.941 1.941 0 0 1 1.247-.803c.095-.316.215-.622.357-.9a1.94 1.94 0 0 1 .094-2.59 1.94 1.94 0 0 1 2.776-.22c.258-.157.532-.293.82-.4A1.934 1.934 0 0 1 6.834.665 1.934 1.934 0 0 1 8.3.03 8.2 8.2 0 0 1 8 0ZM8 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"/></svg>
|
|
<span class="flex-1">Settings</span>
|
|
</button>
|
|
<div class="border-t border-[#30363d] my-1" />
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleFileMenuAction('exit')"
|
|
>
|
|
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="currentColor"><path d="M2 2.75C2 1.784 2.784 1 3.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5a.75.75 0 0 1-1.5 0v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h3.5a.75.75 0 0 1 0 1.5h-3.5A1.75 1.75 0 0 1 2 13.25Zm10.44 4.5-1.97-1.97a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l1.97-1.97H6.75a.75.75 0 0 1 0-1.5Z"/></svg>
|
|
<span class="flex-1">Exit</span>
|
|
<kbd class="text-[10px] text-[var(--wraith-text-muted)]">Alt+F4</kbd>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tools menu -->
|
|
<div class="relative">
|
|
<button
|
|
class="text-xs text-[var(--wraith-text-secondary)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer px-2 py-1 rounded hover:bg-[var(--wraith-bg-tertiary)]"
|
|
@click="showToolsMenu = !showToolsMenu"
|
|
@blur="closeToolsMenuDeferred"
|
|
>
|
|
Tools
|
|
</button>
|
|
<div
|
|
v-if="showToolsMenu"
|
|
class="absolute top-full left-0 mt-0.5 w-56 bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden z-50 py-1"
|
|
>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleToolAction('network-scanner')"
|
|
>
|
|
<span class="flex-1">Network Scanner</span>
|
|
</button>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleToolAction('port-scanner')"
|
|
>
|
|
<span class="flex-1">Port Scanner</span>
|
|
</button>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleToolAction('ping')"
|
|
>
|
|
<span class="flex-1">Ping</span>
|
|
</button>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleToolAction('traceroute')"
|
|
>
|
|
<span class="flex-1">Traceroute</span>
|
|
</button>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleToolAction('dns-lookup')"
|
|
>
|
|
<span class="flex-1">DNS Lookup</span>
|
|
</button>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleToolAction('whois')"
|
|
>
|
|
<span class="flex-1">Whois</span>
|
|
</button>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleToolAction('bandwidth')"
|
|
>
|
|
<span class="flex-1">Bandwidth Test</span>
|
|
</button>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleToolAction('subnet-calc')"
|
|
>
|
|
<span class="flex-1">Subnet Calculator</span>
|
|
</button>
|
|
<div class="border-t border-[#30363d] my-1" />
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleToolAction('docker')"
|
|
>
|
|
<span class="flex-1">Docker Manager</span>
|
|
</button>
|
|
<div class="border-t border-[#30363d] my-1" />
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleToolAction('wake-on-lan')"
|
|
>
|
|
<span class="flex-1">Wake on LAN</span>
|
|
</button>
|
|
<div class="border-t border-[#30363d] my-1" />
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleToolAction('ssh-keygen')"
|
|
>
|
|
<span class="flex-1">SSH Key Generator</span>
|
|
</button>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleToolAction('password-gen')"
|
|
>
|
|
<span class="flex-1">Password Generator</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Help menu -->
|
|
<div class="relative">
|
|
<button
|
|
class="text-xs text-[var(--wraith-text-secondary)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer px-2 py-1 rounded hover:bg-[var(--wraith-bg-tertiary)]"
|
|
@click="showHelpMenu = !showHelpMenu"
|
|
@blur="closeHelpMenuDeferred"
|
|
>
|
|
Help
|
|
</button>
|
|
<div
|
|
v-if="showHelpMenu"
|
|
class="absolute top-full left-0 mt-0.5 w-56 bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden z-50 py-1"
|
|
>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleHelpAction('guide')"
|
|
>
|
|
<span class="flex-1">Getting Started</span>
|
|
</button>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleHelpAction('shortcuts')"
|
|
>
|
|
<span class="flex-1">Keyboard Shortcuts</span>
|
|
</button>
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleHelpAction('mcp')"
|
|
>
|
|
<span class="flex-1">MCP Integration</span>
|
|
</button>
|
|
<div class="border-t border-[#30363d] my-1" />
|
|
<button
|
|
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@mousedown.prevent="handleHelpAction('about')"
|
|
>
|
|
<span class="flex-1">About Wraith</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Connect -->
|
|
<div class="flex-1 max-w-xs mx-4">
|
|
<input
|
|
v-model="quickConnectInput"
|
|
type="text"
|
|
placeholder="Quick connect: user@host:port"
|
|
class="w-full px-2.5 py-1 text-xs rounded bg-[var(--wraith-bg-tertiary)] border border-[var(--wraith-border)] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
@keydown.enter="handleQuickConnect"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 text-xs text-[var(--wraith-text-secondary)]">
|
|
<span>{{ sessionStore.sessionCount }} session{{ sessionStore.sessionCount !== 1 ? "s" : "" }}</span>
|
|
|
|
<button
|
|
class="hover:text-[var(--wraith-accent-blue)] transition-colors cursor-pointer"
|
|
:class="{ 'text-[var(--wraith-accent-blue)]': copilotVisible }"
|
|
title="AI Copilot (Ctrl+Shift+G)"
|
|
@click="copilotVisible = !copilotVisible"
|
|
>
|
|
AI
|
|
</button>
|
|
|
|
<button
|
|
class="hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
title="Command palette (Ctrl+K)"
|
|
@click="commandPalette?.toggle()"
|
|
>
|
|
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M11.5 7a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0zm-.82 4.74a6 6 0 1 1 1.06-1.06l3.04 3.04a.75.75 0 1 1-1.06 1.06l-3.04-3.04z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<button
|
|
class="hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
title="Lock vault"
|
|
@click="appStore.lock()"
|
|
>
|
|
🔒
|
|
</button>
|
|
<button
|
|
class="hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
title="Settings"
|
|
@click="settingsModal?.open()"
|
|
>
|
|
⚙
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main content area -->
|
|
<div class="flex flex-1 min-h-0">
|
|
<!-- Sidebar -->
|
|
<div
|
|
v-if="sidebarVisible"
|
|
class="flex flex-col bg-[var(--wraith-bg-secondary)] border-r border-[var(--wraith-border)] shrink-0"
|
|
:style="{ width: sidebarWidth + 'px' }"
|
|
>
|
|
<SidebarToggle v-model="sidebarTab" />
|
|
|
|
<!-- Search (connections mode only) -->
|
|
<div v-if="sidebarTab === 'connections'" class="px-3 py-2">
|
|
<input
|
|
v-model="connectionStore.searchQuery"
|
|
type="text"
|
|
placeholder="Search connections..."
|
|
class="w-full px-2.5 py-1.5 text-xs rounded bg-[var(--wraith-bg-tertiary)] border border-[var(--wraith-border)] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Sidebar content -->
|
|
<div class="flex-1 overflow-y-auto">
|
|
<!-- Connection tree -->
|
|
<ConnectionTree v-if="sidebarTab === 'connections'" />
|
|
|
|
<!-- SFTP browser -->
|
|
<template v-else-if="sidebarTab === 'sftp'">
|
|
<template v-if="activeSessionId">
|
|
<FileTree
|
|
:session-id="activeSessionId"
|
|
class="flex-1 min-h-0"
|
|
@open-file="handleOpenFile"
|
|
/>
|
|
<TransferProgress />
|
|
</template>
|
|
<div v-else class="flex items-center justify-center py-8 px-3">
|
|
<p class="text-[var(--wraith-text-muted)] text-xs text-center">
|
|
No active session
|
|
</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content area -->
|
|
<div class="flex-1 flex flex-col min-w-0">
|
|
<!-- Tab bar -->
|
|
<TabBar />
|
|
|
|
<!-- Session area -->
|
|
<SessionContainer ref="sessionContainer" />
|
|
</div>
|
|
|
|
<!-- AI Copilot Panel -->
|
|
<CopilotPanel v-if="copilotVisible" />
|
|
</div>
|
|
|
|
<!-- Status bar -->
|
|
<StatusBar ref="statusBar" @open-theme-picker="themePicker?.open()" />
|
|
|
|
<CommandPalette
|
|
ref="commandPalette"
|
|
@open-settings="settingsModal?.open()"
|
|
@open-new-connection="connectionEditDialog?.openNew()"
|
|
/>
|
|
|
|
<ThemePicker ref="themePicker" @select="handleThemeSelect" />
|
|
<SettingsModal ref="settingsModal" />
|
|
<ConnectionEditDialog ref="connectionEditDialog" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
import { useAppStore } from "@/stores/app.store";
|
|
import { useConnectionStore } from "@/stores/connection.store";
|
|
import { useSessionStore } from "@/stores/session.store";
|
|
import SidebarToggle from "@/components/sidebar/SidebarToggle.vue";
|
|
import ConnectionTree from "@/components/sidebar/ConnectionTree.vue";
|
|
import TabBar from "@/components/session/TabBar.vue";
|
|
import SessionContainer from "@/components/session/SessionContainer.vue";
|
|
import StatusBar from "@/components/common/StatusBar.vue";
|
|
import CommandPalette from "@/components/common/CommandPalette.vue";
|
|
import ThemePicker from "@/components/common/ThemePicker.vue";
|
|
import SettingsModal from "@/components/common/SettingsModal.vue";
|
|
import ConnectionEditDialog from "@/components/connections/ConnectionEditDialog.vue";
|
|
import FileTree from "@/components/sftp/FileTree.vue";
|
|
import TransferProgress from "@/components/sftp/TransferProgress.vue";
|
|
import CopilotPanel from "@/components/ai/CopilotPanel.vue";
|
|
import type { FileEntry } from "@/composables/useSftp";
|
|
|
|
import type { ThemeDefinition } from "@/components/common/ThemePicker.vue";
|
|
import type { SidebarTab } from "@/components/sidebar/SidebarToggle.vue";
|
|
|
|
const appStore = useAppStore();
|
|
const connectionStore = useConnectionStore();
|
|
const sessionStore = useSessionStore();
|
|
|
|
const activeSessionId = computed(() => sessionStore.activeSessionId);
|
|
const sidebarWidth = ref(240);
|
|
const sidebarVisible = ref(true);
|
|
const sidebarTab = ref<SidebarTab>("connections");
|
|
const copilotVisible = ref(false);
|
|
const quickConnectInput = ref("");
|
|
|
|
const commandPalette = ref<InstanceType<typeof CommandPalette> | null>(null);
|
|
const themePicker = ref<InstanceType<typeof ThemePicker> | null>(null);
|
|
const settingsModal = ref<InstanceType<typeof SettingsModal> | null>(null);
|
|
const connectionEditDialog = ref<InstanceType<typeof ConnectionEditDialog> | null>(null);
|
|
const statusBar = ref<InstanceType<typeof StatusBar> | null>(null);
|
|
const sessionContainer = ref<InstanceType<typeof SessionContainer> | null>(null);
|
|
|
|
const showFileMenu = ref(false);
|
|
const showToolsMenu = ref(false);
|
|
const showHelpMenu = ref(false);
|
|
|
|
function closeFileMenuDeferred(): void {
|
|
setTimeout(() => { showFileMenu.value = false; }, 150);
|
|
}
|
|
|
|
function closeToolsMenuDeferred(): void {
|
|
setTimeout(() => { showToolsMenu.value = false; }, 150);
|
|
}
|
|
|
|
function closeHelpMenuDeferred(): void {
|
|
setTimeout(() => { showHelpMenu.value = false; }, 150);
|
|
}
|
|
|
|
async function handleHelpAction(page: string): Promise<void> {
|
|
showHelpMenu.value = false;
|
|
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
|
const label = `help-${page}-${Date.now()}`;
|
|
new WebviewWindow(label, {
|
|
title: `Wraith — Help`,
|
|
width: 750,
|
|
height: 600,
|
|
resizable: true,
|
|
center: true,
|
|
url: `index.html#/tool/help?page=${page}`,
|
|
});
|
|
}
|
|
|
|
async function handleToolAction(tool: string): Promise<void> {
|
|
showToolsMenu.value = false;
|
|
|
|
// Tools that don't need a session
|
|
const localTools = ["ssh-keygen", "password-gen", "subnet-calc"];
|
|
|
|
if (!localTools.includes(tool) && !activeSessionId.value) {
|
|
alert("Connect to a server first — network tools run through SSH sessions.");
|
|
return;
|
|
}
|
|
|
|
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
|
|
|
const toolConfig: Record<string, { title: string; width: number; height: number }> = {
|
|
"network-scanner": { title: "Network Scanner", width: 800, height: 600 },
|
|
"port-scanner": { title: "Port Scanner", width: 700, height: 500 },
|
|
"ping": { title: "Ping", width: 600, height: 400 },
|
|
"traceroute": { title: "Traceroute", width: 600, height: 500 },
|
|
"dns-lookup": { title: "DNS Lookup", width: 600, height: 400 },
|
|
"whois": { title: "Whois", width: 700, height: 500 },
|
|
"bandwidth": { title: "Bandwidth Test", width: 700, height: 450 },
|
|
"subnet-calc": { title: "Subnet Calculator", width: 650, height: 350 },
|
|
"docker": { title: "Docker Manager", width: 900, height: 600 },
|
|
"wake-on-lan": { title: "Wake on LAN", width: 500, height: 300 },
|
|
"ssh-keygen": { title: "SSH Key Generator", width: 700, height: 500 },
|
|
"password-gen": { title: "Password Generator", width: 500, height: 400 },
|
|
};
|
|
|
|
const config = toolConfig[tool];
|
|
if (!config) return;
|
|
|
|
const sessionId = activeSessionId.value || "";
|
|
|
|
// Open tool in a new Tauri window
|
|
const label = `tool-${tool}-${Date.now()}`;
|
|
new WebviewWindow(label, {
|
|
title: `Wraith — ${config.title}`,
|
|
width: config.width,
|
|
height: config.height,
|
|
resizable: true,
|
|
center: true,
|
|
url: `index.html#/tool/${tool}?sessionId=${sessionId}`,
|
|
});
|
|
}
|
|
|
|
async function handleFileMenuAction(action: string): Promise<void> {
|
|
showFileMenu.value = false;
|
|
switch (action) {
|
|
case "new-connection": connectionEditDialog.value?.openNew(); break;
|
|
case "settings": settingsModal.value?.open(); break;
|
|
case "exit": try { await getCurrentWindow().close(); } catch { window.close(); } break;
|
|
}
|
|
}
|
|
|
|
function handleThemeSelect(theme: ThemeDefinition): void {
|
|
statusBar.value?.setThemeName(theme.name);
|
|
sessionStore.setTheme(theme);
|
|
}
|
|
|
|
async function handleOpenFile(entry: FileEntry): Promise<void> {
|
|
if (!activeSessionId.value) return;
|
|
try {
|
|
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
|
const fileName = entry.path.split("/").pop() || entry.path;
|
|
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> {
|
|
const raw = quickConnectInput.value.trim();
|
|
if (!raw) return;
|
|
let username = "", hostname = "", port = 22, protocol: "ssh" | "rdp" = "ssh", hostPart = raw;
|
|
const atIdx = raw.indexOf("@");
|
|
if (atIdx > 0) { username = raw.substring(0, atIdx); hostPart = raw.substring(atIdx + 1); }
|
|
const colonIdx = hostPart.lastIndexOf(":");
|
|
if (colonIdx > 0) {
|
|
const portStr = hostPart.substring(colonIdx + 1);
|
|
const parsedPort = parseInt(portStr, 10);
|
|
if (!isNaN(parsedPort) && parsedPort > 0 && parsedPort <= 65535) { port = parsedPort; hostPart = hostPart.substring(0, colonIdx); }
|
|
}
|
|
hostname = hostPart; if (!hostname) return;
|
|
if (port === 3389) protocol = "rdp";
|
|
const name = username ? `${username}@${hostname}` : hostname;
|
|
try {
|
|
const conn = await invoke<{ id: number }>("create_connection", { name, hostname, port, protocol, groupId: null, credentialId: null, color: "", tags: username ? [username] : [], notes: "", options: username ? JSON.stringify({ username }) : "{}" });
|
|
connectionStore.connections.push({ id: conn.id, name, hostname, port, protocol, groupId: null, tags: username ? [username] : [], options: username ? JSON.stringify({ username }) : "{}" });
|
|
await sessionStore.connect(conn.id);
|
|
quickConnectInput.value = "";
|
|
} catch (err) { console.error("Quick connect failed:", err); }
|
|
}
|
|
|
|
function handleKeydown(event: KeyboardEvent): void {
|
|
const target = event.target as HTMLElement;
|
|
const isInputFocused = target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
|
|
const ctrl = event.ctrlKey || event.metaKey;
|
|
if (ctrl && event.key === "k") { event.preventDefault(); commandPalette.value?.toggle(); return; }
|
|
if (isInputFocused) return;
|
|
if (ctrl && event.key === "w") { event.preventDefault(); const active = sessionStore.activeSession; if (active) sessionStore.closeSession(active.id); return; }
|
|
if (ctrl && event.key === "Tab" && !event.shiftKey) { event.preventDefault(); const sessions = sessionStore.sessions; if (sessions.length < 2) return; const idx = sessions.findIndex((s) => s.id === sessionStore.activeSessionId); const next = sessions[(idx + 1) % sessions.length]; sessionStore.activateSession(next.id); return; }
|
|
if (ctrl && event.key === "Tab" && event.shiftKey) { event.preventDefault(); const sessions = sessionStore.sessions; if (sessions.length < 2) return; const idx = sessions.findIndex((s) => s.id === sessionStore.activeSessionId); const prev = sessions[(idx - 1 + sessions.length) % sessions.length]; sessionStore.activateSession(prev.id); return; }
|
|
if (ctrl && event.key >= "1" && event.key <= "9") { const tabIndex = parseInt(event.key, 10) - 1; const sessions = sessionStore.sessions; if (tabIndex < sessions.length) { event.preventDefault(); sessionStore.activateSession(sessions[tabIndex].id); } return; }
|
|
if (ctrl && event.key === "b") { event.preventDefault(); sidebarVisible.value = !sidebarVisible.value; return; }
|
|
if (ctrl && event.shiftKey && event.key.toLowerCase() === "g") { event.preventDefault(); copilotVisible.value = !copilotVisible.value; return; }
|
|
if (ctrl && event.key === "f") { const active = sessionStore.activeSession; if (active?.protocol === "ssh") { event.preventDefault(); sessionContainer.value?.openActiveSearch(); } return; }
|
|
}
|
|
|
|
let workspaceSaveInterval: ReturnType<typeof setInterval> | null = null;
|
|
|
|
function handleBeforeUnload(e: BeforeUnloadEvent): void {
|
|
if (sessionStore.sessions.length > 0) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
document.addEventListener("keydown", handleKeydown);
|
|
|
|
// Confirm before closing if sessions are active (synchronous — won't hang)
|
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
|
|
await connectionStore.loadAll();
|
|
|
|
// Restore workspace — reconnect saved tabs (non-blocking, non-fatal)
|
|
setTimeout(async () => {
|
|
try {
|
|
const workspace = await invoke<{ tabs: { connectionId: number; protocol: string; position: number }[] } | null>("load_workspace");
|
|
if (workspace?.tabs?.length) {
|
|
for (const tab of workspace.tabs.sort((a, b) => a.position - b.position)) {
|
|
try { await sessionStore.connect(tab.connectionId); } catch {}
|
|
}
|
|
}
|
|
} catch {}
|
|
}, 500);
|
|
|
|
// Auto-save workspace every 30 seconds instead of on close
|
|
// (onCloseRequested was hanging the window close on Windows)
|
|
workspaceSaveInterval = setInterval(() => {
|
|
const tabs = sessionStore.sessions
|
|
.filter(s => s.protocol === "ssh" || s.protocol === "rdp")
|
|
.map((s, i) => ({ connectionId: s.connectionId, protocol: s.protocol, position: i }));
|
|
if (tabs.length > 0) {
|
|
invoke("save_workspace", { tabs }).catch(() => {});
|
|
}
|
|
}, 30000);
|
|
|
|
// Check for updates on startup via Tauri updater plugin (non-blocking)
|
|
invoke<{ currentVersion: string; latestVersion: string; updateAvailable: boolean; downloadUrl: string }>("check_for_updates")
|
|
.then((info) => {
|
|
if (info.updateAvailable) {
|
|
if (confirm(`Wraith v${info.latestVersion} is available (you have v${info.currentVersion}). Open download page?`)) {
|
|
import("@tauri-apps/plugin-shell").then(({ open }) => open(info.downloadUrl)).catch(() => window.open(info.downloadUrl, "_blank"));
|
|
}
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener("keydown", handleKeydown);
|
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
if (workspaceSaveInterval !== null) {
|
|
clearInterval(workspaceSaveInterval);
|
|
workspaceSaveInterval = null;
|
|
}
|
|
});
|
|
</script>
|