wraith/src/components/terminal/TerminalView.vue
Vantz Stockwell 0cd4cc0f64 feat: Phase 5 complete — themes, editor, shortcuts, workspace, settings
Theme service: 7 built-in themes seeded from Rust, ThemePicker
loads from backend. Workspace service: save/load snapshots, crash
recovery detection. SettingsModal: full port with Tauri invoke.
CommandPalette, HostKeyDialog, ConnectionEditDialog all ported.

CodeMirror editor: inline above terminal, reads/writes via SFTP.
Full keyboard shortcuts: Ctrl+K/W/Tab/Shift+Tab/1-9/B/F.
Terminal search bar via Ctrl+F (SearchAddon).
Tab badges: protocol dots, ROOT warning, environment pills.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:36:06 -04:00

190 lines
6.0 KiB
Vue

<template>
<div class="terminal-wrapper flex flex-col h-full relative">
<!-- Inline terminal search bar shown when Ctrl+F is pressed -->
<div
v-if="searchVisible"
class="absolute top-2 right-2 z-20 flex items-center gap-1 bg-[#161b22] border border-[#30363d] rounded-lg shadow-lg px-2 py-1"
>
<input
ref="searchInputRef"
v-model="searchQuery"
type="text"
placeholder="Find in terminal…"
class="w-48 px-1.5 py-0.5 text-xs 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)] rounded transition-colors"
@keydown.enter="findNext"
@keydown.shift.enter.prevent="findPrevious"
@keydown.escape="closeSearch"
@input="onSearchInput"
/>
<button
class="p-1 text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
title="Previous match (Shift+Enter)"
@click="findPrevious"
>
<svg class="w-3 h-3" viewBox="0 0 16 16" fill="currentColor">
<path d="M4.427 9.573 8 6l3.573 3.573a.75.75 0 0 0 1.06-1.06L8.53 4.409a.75.75 0 0 0-1.06 0L3.367 8.513a.75.75 0 0 0 1.06 1.06z" />
</svg>
</button>
<button
class="p-1 text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
title="Next match (Enter)"
@click="findNext"
>
<svg class="w-3 h-3" viewBox="0 0 16 16" fill="currentColor">
<path d="M4.427 6.427 8 10l3.573-3.573a.75.75 0 0 1 1.06 1.06L8.53 11.591a.75.75 0 0 1-1.06 0L3.367 7.487a.75.75 0 0 1 1.06-1.06z" />
</svg>
</button>
<button
class="p-1 text-[var(--wraith-text-muted)] hover:text-[var(--wraith-accent-red)] transition-colors cursor-pointer"
title="Close (Esc)"
@click="closeSearch"
>
<svg class="w-3 h-3" 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 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z" />
</svg>
</button>
</div>
<!-- Terminal container -->
<div
ref="containerRef"
class="terminal-container flex-1"
@focus="handleFocus"
/>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted, watch } from "vue";
import { useTerminal } from "@/composables/useTerminal";
import { useSessionStore } from "@/stores/session.store";
import "@/assets/css/terminal.css";
const props = defineProps<{
sessionId: string;
isActive: boolean;
}>();
const sessionStore = useSessionStore();
const containerRef = ref<HTMLElement | null>(null);
const { terminal, searchAddon, mount, fit } = useTerminal(props.sessionId);
// --- Search state ---
const searchVisible = ref(false);
const searchQuery = ref("");
const searchInputRef = ref<HTMLInputElement | null>(null);
/** Open the inline search bar and focus it. */
function openSearch(): void {
searchVisible.value = true;
nextTick(() => {
searchInputRef.value?.focus();
searchInputRef.value?.select();
});
}
/** Close the search bar and refocus the terminal. */
function closeSearch(): void {
searchVisible.value = false;
searchQuery.value = "";
terminal.focus();
}
/** Decoration options for search highlights — xterm SearchAddon spec. */
const searchDecorations = {
matchBackground: "#f0883e40",
matchBorder: "#f0883e",
matchOverviewRuler: "#f0883e",
activeMatchBackground: "#f0883e80",
activeMatchBorder: "#f0883e",
activeMatchColorOverviewRuler: "#f0883e",
} as const;
/** Find the next match. */
function findNext(): void {
if (!searchQuery.value) return;
searchAddon.findNext(searchQuery.value, { caseSensitive: false, decorations: searchDecorations });
}
/** Find the previous match. */
function findPrevious(): void {
if (!searchQuery.value) return;
searchAddon.findPrevious(searchQuery.value, { caseSensitive: false, decorations: searchDecorations });
}
/** Re-search as the user types. */
function onSearchInput(): void {
if (searchQuery.value) {
findNext();
}
}
// Expose openSearch so SessionContainer / MainLayout can trigger Ctrl+F
defineExpose({ openSearch });
onMounted(() => {
if (containerRef.value) {
mount(containerRef.value);
}
// Apply the current theme immediately if one is already active
if (sessionStore.activeTheme) {
applyTheme();
}
// Track terminal dimensions in the session store
terminal.onResize(({ cols, rows }) => {
sessionStore.setTerminalDimensions(props.sessionId, cols, rows);
});
});
// Re-fit and focus terminal when this tab becomes active
watch(
() => props.isActive,
(active) => {
if (active) {
setTimeout(() => {
fit();
terminal.focus();
}, 0);
}
},
);
/** Apply the session store's active theme to this terminal instance. */
function applyTheme(): void {
const theme = sessionStore.activeTheme;
if (!theme) return;
terminal.options.theme = {
background: theme.background,
foreground: theme.foreground,
cursor: theme.cursor,
black: theme.black,
red: theme.red,
green: theme.green,
yellow: theme.yellow,
blue: theme.blue,
magenta: theme.magenta,
cyan: theme.cyan,
white: theme.white,
brightBlack: theme.brightBlack,
brightRed: theme.brightRed,
brightGreen: theme.brightGreen,
brightYellow: theme.brightYellow,
brightBlue: theme.brightBlue,
brightMagenta: theme.brightMagenta,
brightCyan: theme.brightCyan,
brightWhite: theme.brightWhite,
};
}
// Watch for theme changes in the session store and apply to this terminal
watch(() => sessionStore.activeTheme, (newTheme) => {
if (newTheme) applyTheme();
});
function handleFocus(): void {
terminal.focus();
}
</script>