wraith/src/components/terminal/TerminalView.vue
Vantz Stockwell 2dfe4f9d7a
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Has been cancelled
fix: focus terminal on mount so keyboard input works immediately
The watch on isActive never fired on initial mount because the value
starts as true (Vue watch only triggers on changes). Added explicit
focus + fit in onMounted with a short delay for DOM readiness.
Also added @click handler on container as fallback focus mechanism.

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

197 lines
6.1 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"
@click="handleFocus"
@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);
});
// Focus the terminal after mount so keyboard input works immediately
setTimeout(() => {
fit();
terminal.focus();
}, 50);
});
// Re-fit and focus terminal when switching back to this tab
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>