All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m53s
Popup windows (tools/editor/help): - CSP broadened for macOS: added tauri: and https://tauri.localhost to default-src, https://ipc.localhost and tauri: to connect-src. WKWebView uses different IPC scheme than Windows WebView2. Theme application: - terminal.refresh() after theme change forces xterm.js canvas repaint of existing text — was only changing background, not text colors Selection highlighting: - Removed CSS .xterm-selection div rule entirely — xterm.js v6 canvas renderer doesn't use DOM selection divs, the CSS was a no-op that conflicted with theme-driven selectionBackground Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
237 lines
7.9 KiB
Vue
237 lines
7.9 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"
|
|
/>
|
|
|
|
<!-- Remote monitoring bar -->
|
|
<MonitorBar :session-id="props.sessionId" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, nextTick, onMounted, onBeforeUnmount, watch } from "vue";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { useTerminal } from "@/composables/useTerminal";
|
|
import { useSessionStore } from "@/stores/session.store";
|
|
import MonitorBar from "@/components/terminal/MonitorBar.vue";
|
|
import type { IDisposable } from "@xterm/xterm";
|
|
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);
|
|
let resizeDisposable: IDisposable | null = null;
|
|
|
|
// --- 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
|
|
resizeDisposable = 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.
|
|
// Must wait for the container to have real dimensions after becoming visible.
|
|
watch(
|
|
() => props.isActive,
|
|
(active) => {
|
|
if (active) {
|
|
// Double rAF ensures the container has been laid out by the browser
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
fit();
|
|
terminal.focus();
|
|
// Also notify the backend of the correct size
|
|
const session = sessionStore.sessions.find(s => s.id === props.sessionId);
|
|
const resizeCmd = session?.protocol === "local" ? "pty_resize" : "ssh_resize";
|
|
invoke(resizeCmd, {
|
|
sessionId: props.sessionId,
|
|
cols: terminal.cols,
|
|
rows: terminal.rows,
|
|
}).catch(() => {});
|
|
});
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
/** 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,
|
|
cursorAccent: theme.background,
|
|
selectionBackground: theme.selectionBackground ?? "rgba(88, 166, 255, 0.4)",
|
|
selectionForeground: theme.selectionForeground ?? "#ffffff",
|
|
selectionInactiveBackground: theme.selectionBackground ?? "rgba(88, 166, 255, 0.2)",
|
|
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,
|
|
};
|
|
|
|
// Sync the container background so areas outside the canvas match the theme
|
|
if (containerRef.value) {
|
|
containerRef.value.style.backgroundColor = theme.background;
|
|
}
|
|
|
|
// Force xterm.js to repaint all visible rows with the new theme colors
|
|
terminal.refresh(0, terminal.rows - 1);
|
|
}
|
|
|
|
// Watch for theme changes in the session store and apply to this terminal.
|
|
// Uses deep comparison because the theme is an object — a shallow watch may miss
|
|
// updates if Pinia returns the same reactive proxy wrapper after reassignment.
|
|
watch(() => sessionStore.activeTheme, (newTheme) => {
|
|
if (newTheme) applyTheme();
|
|
}, { deep: true });
|
|
|
|
onBeforeUnmount(() => {
|
|
if (resizeDisposable) {
|
|
resizeDisposable.dispose();
|
|
resizeDisposable = null;
|
|
}
|
|
});
|
|
|
|
function handleFocus(): void {
|
|
terminal.focus();
|
|
}
|
|
</script>
|