All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m53s
The Tauri native updater needs update.json hosted at a static URL. Gitea packages don't support a 'latest' alias, so the endpoint returned 'package does not exist'. Reverted Settings and startup check to use check_for_updates command which queries the Gitea releases API directly and works reliably. The native auto-updater will work once we have proper static hosting for update.json (or a redirect endpoint). For now, the manual check + download page approach is functional. 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>
|