All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m51s
Popup windows (tools/editor/help):
- CSP script-src 'self' blocked Tauri's inline IPC bridge scripts in
child WebviewWindows. Added 'unsafe-inline' to script-src. Still
restrictive (was null before SEC-4).
Theme application:
- Watcher on sessionStore.activeTheme needed { deep: true } — Pinia
reactive proxy identity doesn't change on object replacement
- LocalTerminalView.vue had ZERO theme support — added full applyTheme()
with watcher and mount-time application
- Container background now syncs with theme (was stuck on CSS variable)
Cursor blink:
- terminal.focus() after mount in useTerminal.ts — terminal opened
without focus, xterm.js rendered static outline instead of blinking block
Selection highlighting:
- applyTheme() was overwriting theme without selectionBackground/
selectionForeground/selectionInactiveBackground — selection invisible
after any theme change
- Removed !important from terminal.css that overrode canvas selection
- Bumped default selection opacity 0.3 → 0.4
Status bar:
- h-6 text-[10px] → h-8 text-xs (24px/10px → 32px/12px)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
179 lines
5.6 KiB
Vue
179 lines
5.6 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" />
|
|
|
|
<!-- Picker panel -->
|
|
<div class="relative w-full max-w-md 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)]">Terminal Theme</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>
|
|
|
|
<!-- Loading state -->
|
|
<div v-if="loading" class="py-8 text-center text-sm text-[var(--wraith-text-muted)]">
|
|
Loading themes...
|
|
</div>
|
|
|
|
<!-- Theme list -->
|
|
<div v-else class="max-h-96 overflow-y-auto py-2">
|
|
<button
|
|
v-for="theme in themes"
|
|
:key="theme.name"
|
|
class="w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors cursor-pointer"
|
|
:class="activeTheme === theme.name ? 'bg-[#1f6feb]/20' : 'hover:bg-[#30363d]'"
|
|
@click="selectTheme(theme)"
|
|
>
|
|
<!-- Active indicator -->
|
|
<span class="w-4 flex items-center justify-center shrink-0">
|
|
<svg
|
|
v-if="activeTheme === theme.name"
|
|
class="w-4 h-4 text-[#3fb950]"
|
|
viewBox="0 0 16 16"
|
|
fill="currentColor"
|
|
>
|
|
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />
|
|
</svg>
|
|
</span>
|
|
|
|
<!-- Theme info -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="text-sm text-[var(--wraith-text-primary)]">{{ theme.name }}</div>
|
|
<!-- Color swatch: 16 ANSI colors as small blocks -->
|
|
<div class="flex gap-0.5 mt-1.5">
|
|
<span
|
|
v-for="(color, i) in themeColors(theme)"
|
|
:key="i"
|
|
class="w-4 h-3 rounded-sm"
|
|
:style="{ backgroundColor: color }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview: fg on bg -->
|
|
<div
|
|
class="px-2 py-1 rounded text-[10px] font-mono shrink-0"
|
|
:style="{ backgroundColor: theme.background, color: theme.foreground }"
|
|
>
|
|
~/wraith $
|
|
</div>
|
|
</button>
|
|
|
|
<!-- Empty state (no themes loaded) -->
|
|
<div
|
|
v-if="themes.length === 0"
|
|
class="px-4 py-8 text-center text-sm text-[var(--wraith-text-muted)]"
|
|
>
|
|
No themes available
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from "vue";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
|
|
export interface ThemeDefinition {
|
|
id?: number;
|
|
name: string;
|
|
foreground: string;
|
|
background: string;
|
|
cursor: string;
|
|
black: string;
|
|
red: string;
|
|
green: string;
|
|
yellow: string;
|
|
blue: string;
|
|
magenta: string;
|
|
cyan: string;
|
|
white: string;
|
|
brightBlack: string;
|
|
brightRed: string;
|
|
brightGreen: string;
|
|
brightYellow: string;
|
|
brightBlue: string;
|
|
brightMagenta: string;
|
|
brightCyan: string;
|
|
brightWhite: string;
|
|
selectionBackground?: string;
|
|
selectionForeground?: string;
|
|
isBuiltin?: boolean;
|
|
}
|
|
|
|
const visible = ref(false);
|
|
const activeTheme = ref("Dracula");
|
|
const themes = ref<ThemeDefinition[]>([]);
|
|
const loading = ref(false);
|
|
|
|
/** Load themes from the Rust backend. */
|
|
async function loadThemes(): Promise<void> {
|
|
loading.value = true;
|
|
try {
|
|
const result = await invoke<ThemeDefinition[]>("list_themes");
|
|
themes.value = result || [];
|
|
} catch (err) {
|
|
console.error("ThemePicker: failed to load themes from backend:", err);
|
|
themes.value = [];
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
/** Load saved active theme name from settings on mount. */
|
|
onMounted(async () => {
|
|
await loadThemes();
|
|
try {
|
|
const saved = await invoke<string | null>("get_setting", { key: "active_theme" });
|
|
if (saved) activeTheme.value = saved;
|
|
} catch {
|
|
// No saved theme — keep default
|
|
}
|
|
});
|
|
|
|
function themeColors(theme: ThemeDefinition): string[] {
|
|
return [
|
|
theme.black, theme.red, theme.green, theme.yellow,
|
|
theme.blue, theme.magenta, theme.cyan, theme.white,
|
|
theme.brightBlack, theme.brightRed, theme.brightGreen, theme.brightYellow,
|
|
theme.brightBlue, theme.brightMagenta, theme.brightCyan, theme.brightWhite,
|
|
];
|
|
}
|
|
|
|
const emit = defineEmits<{
|
|
(e: "select", theme: ThemeDefinition): void;
|
|
}>();
|
|
|
|
function selectTheme(theme: ThemeDefinition): void {
|
|
activeTheme.value = theme.name;
|
|
emit("select", theme);
|
|
invoke("set_setting", { key: "active_theme", value: theme.name }).catch(console.error);
|
|
}
|
|
|
|
function open(): void {
|
|
visible.value = true;
|
|
}
|
|
|
|
function close(): void {
|
|
visible.value = false;
|
|
}
|
|
|
|
defineExpose({ open, close, visible, activeTheme });
|
|
</script>
|