All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m2s
Checks Gitea releases API for latest version on startup. If newer version available, shows confirm dialog to open download page. Also adds "Check for Updates" button in Settings → About with version comparison, release notes display, and download button. Backend: check_for_updates command with semver comparison (6 tests). 96 total tests, zero warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
516 lines
22 KiB
Vue
516 lines
22 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="visible"
|
|
class="fixed inset-0 z-50 flex items-center justify-center"
|
|
@click.self="close"
|
|
@keydown.esc="close"
|
|
>
|
|
<!-- Backdrop -->
|
|
<div class="absolute inset-0 bg-black/50" @click="close" />
|
|
|
|
<!-- Modal -->
|
|
<div class="relative w-full max-w-lg bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between px-4 py-3 border-b border-[#30363d]">
|
|
<h3 class="text-sm font-semibold text-[var(--wraith-text-primary)]">Settings</h3>
|
|
<button
|
|
class="text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@click="close"
|
|
>
|
|
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.749.749 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.749.749 0 1 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="flex min-h-[400px] max-h-[70vh]">
|
|
<!-- Section tabs (left sidebar) -->
|
|
<div class="w-36 border-r border-[#30363d] py-2 shrink-0">
|
|
<button
|
|
v-for="section in sections"
|
|
:key="section.id"
|
|
class="w-full flex items-center gap-2 px-4 py-2 text-xs text-left transition-colors cursor-pointer"
|
|
:class="activeSection === section.id
|
|
? 'bg-[#1f6feb]/20 text-[var(--wraith-text-primary)] border-r-2 border-[#1f6feb]'
|
|
: 'text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)]'
|
|
"
|
|
@click="activeSection = section.id"
|
|
>
|
|
<span v-html="section.icon" />
|
|
{{ section.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Section content (right panel) -->
|
|
<div class="flex-1 overflow-y-auto p-4 space-y-4">
|
|
|
|
<!-- General -->
|
|
<template v-if="activeSection === 'general'">
|
|
<h4 class="text-xs font-semibold text-[var(--wraith-text-muted)] uppercase tracking-wider mb-3">General</h4>
|
|
|
|
<!-- Default protocol -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Default Protocol</label>
|
|
<select
|
|
v-model="settings.defaultProtocol"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors cursor-pointer"
|
|
>
|
|
<option value="ssh">SSH</option>
|
|
<option value="rdp">RDP</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Sidebar width -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">
|
|
Sidebar Width: {{ settings.sidebarWidth }}px
|
|
</label>
|
|
<input
|
|
v-model.number="settings.sidebarWidth"
|
|
type="range"
|
|
min="180"
|
|
max="400"
|
|
step="10"
|
|
class="w-full accent-[var(--wraith-accent-blue)]"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Terminal -->
|
|
<template v-if="activeSection === 'terminal'">
|
|
<h4 class="text-xs font-semibold text-[var(--wraith-text-muted)] uppercase tracking-wider mb-3">Terminal</h4>
|
|
|
|
<!-- Default theme -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Default Theme</label>
|
|
<select
|
|
v-model="settings.terminalTheme"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors cursor-pointer"
|
|
>
|
|
<option v-for="theme in themeNames" :key="theme" :value="theme">{{ theme }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Font size -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Font Size</label>
|
|
<input
|
|
v-model.number="settings.fontSize"
|
|
type="number"
|
|
min="8"
|
|
max="32"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Scrollback buffer -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Scrollback Buffer (lines)</label>
|
|
<input
|
|
v-model.number="settings.scrollbackBuffer"
|
|
type="number"
|
|
min="100"
|
|
max="100000"
|
|
step="100"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Vault -->
|
|
<template v-if="activeSection === 'vault'">
|
|
<h4 class="text-xs font-semibold text-[var(--wraith-text-muted)] uppercase tracking-wider mb-3">Vault</h4>
|
|
|
|
<!-- Change master password -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-2">Master Password</label>
|
|
<button
|
|
class="px-3 py-2 text-xs text-[var(--wraith-text-primary)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
|
|
@click="changeMasterPassword"
|
|
>
|
|
Change Master Password
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Export vault -->
|
|
<div class="pt-2">
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-2">Backup</label>
|
|
<div class="flex gap-2">
|
|
<button
|
|
class="px-3 py-2 text-xs text-[var(--wraith-text-primary)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
|
|
@click="exportVault"
|
|
>
|
|
Export Vault
|
|
</button>
|
|
<button
|
|
class="px-3 py-2 text-xs text-[var(--wraith-text-primary)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
|
|
@click="importVault"
|
|
>
|
|
Import Vault
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- AI Copilot -->
|
|
<template v-if="activeSection === 'copilot'">
|
|
<h4 class="text-xs font-semibold text-[var(--wraith-text-muted)] uppercase tracking-wider mb-3">Launch Presets</h4>
|
|
<p class="text-[10px] text-[var(--wraith-text-muted)] mb-3">
|
|
Configure quick-launch buttons for the AI copilot panel. Each preset spawns a shell and runs the command.
|
|
</p>
|
|
|
|
<div class="space-y-2">
|
|
<div
|
|
v-for="(preset, idx) in copilotPresets"
|
|
:key="idx"
|
|
class="flex items-center gap-2"
|
|
>
|
|
<input
|
|
v-model="preset.name"
|
|
type="text"
|
|
placeholder="Name"
|
|
class="w-24 px-2 py-1 text-xs rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)]"
|
|
/>
|
|
<input
|
|
v-model="preset.command"
|
|
type="text"
|
|
placeholder="Command (e.g. claude --dangerously-skip-permissions)"
|
|
class="flex-1 px-2 py-1 text-xs rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] font-mono"
|
|
/>
|
|
<button
|
|
class="text-[var(--wraith-text-muted)] hover:text-[var(--wraith-accent-red)] transition-colors cursor-pointer text-sm"
|
|
@click="copilotPresets.splice(idx, 1)"
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2 mt-3">
|
|
<button
|
|
class="px-3 py-1.5 text-xs rounded border border-[#30363d] text-[var(--wraith-text-secondary)] hover:bg-[#30363d] transition-colors cursor-pointer"
|
|
@click="copilotPresets.push({ name: '', shell: '', command: '' })"
|
|
>
|
|
+ Add Preset
|
|
</button>
|
|
<button
|
|
class="px-3 py-1.5 text-xs rounded bg-[var(--wraith-accent-blue)] text-black font-bold transition-colors cursor-pointer"
|
|
@click="saveCopilotPresets"
|
|
>
|
|
Save
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- About -->
|
|
<template v-if="activeSection === 'about'">
|
|
<h4 class="text-xs font-semibold text-[var(--wraith-text-muted)] uppercase tracking-wider mb-3">About</h4>
|
|
|
|
<div class="space-y-3">
|
|
<div class="bg-[#0d1117] rounded-lg p-4 text-center">
|
|
<div class="text-lg font-bold tracking-widest text-[var(--wraith-accent-blue)] mb-1">WRAITH</div>
|
|
<div class="text-xs text-[var(--wraith-text-secondary)]">Connection Manager</div>
|
|
</div>
|
|
|
|
<div class="space-y-2 text-xs">
|
|
<div class="flex justify-between py-1.5 border-b border-[#30363d]">
|
|
<span class="text-[var(--wraith-text-secondary)]">Version</span>
|
|
<span class="text-[var(--wraith-text-primary)]">{{ currentVersion }}</span>
|
|
</div>
|
|
<div class="flex justify-between py-1.5 border-b border-[#30363d]">
|
|
<span class="text-[var(--wraith-text-secondary)]">License</span>
|
|
<span class="text-[var(--wraith-text-primary)]">Proprietary</span>
|
|
</div>
|
|
<div class="flex justify-between py-1.5 border-b border-[#30363d]">
|
|
<span class="text-[var(--wraith-text-secondary)]">Runtime</span>
|
|
<span class="text-[var(--wraith-text-primary)]">Tauri v2</span>
|
|
</div>
|
|
<div class="flex justify-between py-1.5">
|
|
<span class="text-[var(--wraith-text-secondary)]">Frontend</span>
|
|
<span class="text-[var(--wraith-text-primary)]">Vue 3 + TypeScript</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Update check -->
|
|
<div class="pt-2">
|
|
<button
|
|
class="w-full px-3 py-2 text-xs font-bold rounded bg-[var(--wraith-accent-blue)] text-black cursor-pointer disabled:opacity-40"
|
|
:disabled="updateChecking"
|
|
@click="checkUpdates"
|
|
>
|
|
{{ updateChecking ? "Checking..." : "Check for Updates" }}
|
|
</button>
|
|
<div v-if="updateInfo" class="mt-2 p-3 rounded bg-[#0d1117] border border-[#30363d]">
|
|
<template v-if="updateInfo.updateAvailable">
|
|
<p class="text-xs text-[#3fb950] mb-1">Update available: v{{ updateInfo.latestVersion }}</p>
|
|
<p v-if="updateInfo.releaseNotes" class="text-[10px] text-[var(--wraith-text-muted)] mb-2 max-h-20 overflow-auto">{{ updateInfo.releaseNotes }}</p>
|
|
<button
|
|
class="w-full px-3 py-1.5 text-xs font-bold rounded bg-[#238636] text-white cursor-pointer"
|
|
@click="downloadUpdate"
|
|
>
|
|
Download v{{ updateInfo.latestVersion }}
|
|
</button>
|
|
</template>
|
|
<p v-else class="text-xs text-[var(--wraith-text-muted)]">You're on the latest version.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2 pt-2">
|
|
<a
|
|
href="#"
|
|
class="flex-1 px-3 py-2 text-xs text-center text-[var(--wraith-accent-blue)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
|
|
@click.prevent="openLink('docs')"
|
|
>
|
|
Documentation
|
|
</a>
|
|
<a
|
|
href="#"
|
|
class="flex-1 px-3 py-2 text-xs text-center text-[var(--wraith-accent-blue)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
|
|
@click.prevent="openLink('repo')"
|
|
>
|
|
Source Code
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="flex items-center justify-end px-4 py-3 border-t border-[#30363d]">
|
|
<button
|
|
class="px-3 py-1.5 text-xs text-white bg-[#1f6feb] hover:bg-[#388bfd] rounded transition-colors cursor-pointer"
|
|
@click="close"
|
|
>
|
|
Done
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, watch, onMounted } from "vue";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { getVersion } from "@tauri-apps/api/app";
|
|
import { open as shellOpen } from "@tauri-apps/plugin-shell";
|
|
|
|
type Section = "general" | "terminal" | "vault" | "copilot" | "about";
|
|
|
|
interface CopilotPreset { name: string; shell: string; command: string; }
|
|
|
|
const visible = ref(false);
|
|
const activeSection = ref<Section>("general");
|
|
const copilotPresets = ref<CopilotPreset[]>([]);
|
|
|
|
interface UpdateCheckInfo {
|
|
currentVersion: string;
|
|
latestVersion: string;
|
|
updateAvailable: boolean;
|
|
downloadUrl: string;
|
|
releaseNotes: string;
|
|
}
|
|
const updateChecking = ref(false);
|
|
const updateInfo = ref<UpdateCheckInfo | null>(null);
|
|
|
|
async function checkUpdates(): Promise<void> {
|
|
updateChecking.value = true;
|
|
updateInfo.value = null;
|
|
try {
|
|
updateInfo.value = await invoke<UpdateCheckInfo>("check_for_updates");
|
|
} catch (err) {
|
|
alert(`Update check failed: ${err}`);
|
|
}
|
|
updateChecking.value = false;
|
|
}
|
|
|
|
async function downloadUpdate(): Promise<void> {
|
|
if (!updateInfo.value?.downloadUrl) return;
|
|
try {
|
|
await shellOpen(updateInfo.value.downloadUrl);
|
|
} catch {
|
|
window.open(updateInfo.value.downloadUrl, "_blank");
|
|
}
|
|
}
|
|
const currentVersion = ref("loading...");
|
|
|
|
const sections = [
|
|
{
|
|
id: "general" as const,
|
|
label: "General",
|
|
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><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 0-2.29 1.939 1.939 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>`,
|
|
},
|
|
{
|
|
id: "terminal" as const,
|
|
label: "Terminal",
|
|
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25ZM7.25 8a.749.749 0 0 1-.22.53l-2.25 2.25a.749.749 0 1 1-1.06-1.06L5.44 8 3.72 6.28a.749.749 0 1 1 1.06-1.06l2.25 2.25c.141.14.22.331.22.53Zm1.5 1.5h3a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5Z"/></svg>`,
|
|
},
|
|
{
|
|
id: "vault" as const,
|
|
label: "Vault",
|
|
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M4 4v2h-.25A1.75 1.75 0 0 0 2 7.75v5.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0 0 14 13.25v-5.5A1.75 1.75 0 0 0 12.25 6H12V4a4 4 0 1 0-8 0Zm6.5 2V4a2.5 2.5 0 0 0-5 0v2ZM8 9.5a1.5 1.5 0 0 1 .5 2.915V13.5a.5.5 0 0 1-1 0v-1.085A1.5 1.5 0 0 1 8 9.5Z"/></svg>`,
|
|
},
|
|
{
|
|
id: "copilot" as const,
|
|
label: "AI Copilot",
|
|
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M5.5 8.5 9 5l-2-.5L4 7.5l1.5 1ZM1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25Z"/></svg>`,
|
|
},
|
|
{
|
|
id: "about" as const,
|
|
label: "About",
|
|
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>`,
|
|
},
|
|
];
|
|
|
|
// Theme names loaded from backend (populated in loadThemeNames)
|
|
const themeNames = ref<string[]>([
|
|
"Dracula",
|
|
"Nord",
|
|
"Monokai",
|
|
"One Dark",
|
|
"Solarized Dark",
|
|
"Gruvbox Dark",
|
|
"MobaXTerm Classic",
|
|
]);
|
|
|
|
const settings = ref({
|
|
defaultProtocol: "ssh" as "ssh" | "rdp",
|
|
sidebarWidth: 240,
|
|
terminalTheme: "Dracula",
|
|
fontSize: 14,
|
|
scrollbackBuffer: 5000,
|
|
});
|
|
|
|
/** Load saved settings from Rust backend on mount. */
|
|
onMounted(async () => {
|
|
// Populate version from Tauri app config
|
|
try { currentVersion.value = await getVersion(); } catch { currentVersion.value = "unknown"; }
|
|
|
|
try {
|
|
const [protocol, sidebarW, theme, fontSize, scrollback] = await Promise.all([
|
|
invoke<string | null>("get_setting", { key: "default_protocol" }),
|
|
invoke<string | null>("get_setting", { key: "sidebar_width" }),
|
|
invoke<string | null>("get_setting", { key: "terminal_theme" }),
|
|
invoke<string | null>("get_setting", { key: "font_size" }),
|
|
invoke<string | null>("get_setting", { key: "scrollback_buffer" }),
|
|
]);
|
|
if (protocol) settings.value.defaultProtocol = protocol as "ssh" | "rdp";
|
|
if (sidebarW) settings.value.sidebarWidth = Number(sidebarW);
|
|
if (theme) settings.value.terminalTheme = theme;
|
|
if (fontSize) settings.value.fontSize = Number(fontSize);
|
|
if (scrollback) settings.value.scrollbackBuffer = Number(scrollback);
|
|
} catch (err) {
|
|
console.error("SettingsModal: failed to load settings:", err);
|
|
}
|
|
|
|
// Load theme names from backend for the terminal theme dropdown
|
|
try {
|
|
const themes = await invoke<Array<{ name: string }>>("list_themes");
|
|
if (themes && themes.length > 0) {
|
|
themeNames.value = themes.map((t) => t.name);
|
|
}
|
|
} catch {
|
|
// Keep the hardcoded fallback list
|
|
}
|
|
});
|
|
|
|
/** Persist settings changes to Rust backend as they change. */
|
|
watch(
|
|
() => settings.value.defaultProtocol,
|
|
(val) => invoke("set_setting", { key: "default_protocol", value: val }).catch(console.error),
|
|
);
|
|
watch(
|
|
() => settings.value.sidebarWidth,
|
|
(val) => invoke("set_setting", { key: "sidebar_width", value: String(val) }).catch(console.error),
|
|
);
|
|
watch(
|
|
() => settings.value.terminalTheme,
|
|
(val) => invoke("set_setting", { key: "terminal_theme", value: val }).catch(console.error),
|
|
);
|
|
watch(
|
|
() => settings.value.fontSize,
|
|
(val) => invoke("set_setting", { key: "font_size", value: String(val) }).catch(console.error),
|
|
);
|
|
watch(
|
|
() => settings.value.scrollbackBuffer,
|
|
(val) => invoke("set_setting", { key: "scrollback_buffer", value: String(val) }).catch(console.error),
|
|
);
|
|
|
|
function open(): void {
|
|
visible.value = true;
|
|
activeSection.value = "general";
|
|
loadCopilotPresets();
|
|
}
|
|
|
|
async function loadCopilotPresets(): Promise<void> {
|
|
try {
|
|
const raw = await invoke<string | null>("get_setting", { key: "copilot_presets" });
|
|
if (raw) {
|
|
copilotPresets.value = JSON.parse(raw);
|
|
} else {
|
|
copilotPresets.value = [
|
|
{ name: "Claude Code", shell: "", command: "claude" },
|
|
{ name: "Gemini CLI", shell: "", command: "gemini" },
|
|
{ name: "Codex CLI", shell: "", command: "codex" },
|
|
];
|
|
}
|
|
} catch {
|
|
copilotPresets.value = [];
|
|
}
|
|
}
|
|
|
|
async function saveCopilotPresets(): Promise<void> {
|
|
try {
|
|
const json = JSON.stringify(copilotPresets.value.filter(p => p.name && p.command));
|
|
await invoke("set_setting", { key: "copilot_presets", value: json });
|
|
} catch (err) {
|
|
console.error("Failed to save copilot presets:", err);
|
|
}
|
|
}
|
|
|
|
function close(): void {
|
|
visible.value = false;
|
|
}
|
|
|
|
async function changeMasterPassword(): Promise<void> {
|
|
const oldPw = prompt("Current master password:");
|
|
if (!oldPw) return;
|
|
const newPw = prompt("New master password:");
|
|
if (!newPw) return;
|
|
const confirmPw = prompt("Confirm new master password:");
|
|
if (newPw !== confirmPw) {
|
|
alert("Passwords do not match.");
|
|
return;
|
|
}
|
|
try {
|
|
await invoke("unlock", { password: oldPw });
|
|
await invoke("create_vault", { password: newPw });
|
|
alert("Master password changed successfully.");
|
|
} catch (err) {
|
|
alert(`Failed to change password: ${err}`);
|
|
}
|
|
}
|
|
|
|
function exportVault(): void {
|
|
alert("Export vault is not yet available. Your data is stored in %APPDATA%\\Wraith\\wraith.db");
|
|
}
|
|
|
|
function importVault(): void {
|
|
alert("Import vault is not yet available. Copy wraith.db to %APPDATA%\\Wraith\\ to restore.");
|
|
}
|
|
|
|
function openLink(target: string): void {
|
|
const urls: Record<string, string> = {
|
|
docs: "https://github.com/wraith/docs",
|
|
repo: "https://github.com/wraith",
|
|
};
|
|
const url = urls[target] ?? target;
|
|
shellOpen(url).catch(console.error);
|
|
}
|
|
|
|
defineExpose({ open, close, visible });
|
|
</script>
|