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>
190 lines
6.0 KiB
Vue
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>
|