feat(ui): wire quick connect input and global Ctrl+K shortcut

Quick connect bar in toolbar parses user@host:port format with
auto-protocol detection (port 3389 -> RDP, default SSH:22).
Global keydown listener triggers command palette on Ctrl/Cmd+K.
MainLayout now integrates ThemePicker, ImportDialog, and
CommandPalette overlays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-17 07:27:40 -04:00
parent f5de568b32
commit ae8127f33d

View File

@ -8,8 +8,29 @@
<span class="text-sm font-bold tracking-widest text-[var(--wraith-accent-blue)]">
WRAITH
</span>
<div class="flex items-center gap-3 text-xs text-[var(--wraith-text-secondary)]">
<!-- Quick Connect -->
<div class="flex-1 max-w-xs mx-4" style="--wails-draggable: no-drag">
<input
v-model="quickConnectInput"
type="text"
placeholder="Quick connect: user@host:port"
class="w-full px-2.5 py-1 text-xs rounded 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)] transition-colors"
@keydown.enter="handleQuickConnect"
/>
</div>
<div class="flex items-center gap-3 text-xs text-[var(--wraith-text-secondary)]" style="--wails-draggable: no-drag">
<span>{{ sessionStore.sessionCount }} session{{ sessionStore.sessionCount !== 1 ? "s" : "" }}</span>
<button
class="hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
title="Command palette (Ctrl+K)"
@click="commandPalette?.toggle()"
>
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
<path d="M11.5 7a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0zm-.82 4.74a6 6 0 1 1 1.06-1.06l3.04 3.04a.75.75 0 1 1-1.06 1.06l-3.04-3.04z" />
</svg>
</button>
<button
class="hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
title="Lock vault"
@ -89,12 +110,21 @@
</div>
<!-- Status bar -->
<StatusBar />
<StatusBar ref="statusBar" @open-theme-picker="themePicker?.open()" />
<!-- Command Palette (Ctrl+K) -->
<CommandPalette ref="commandPalette" @open-import="importDialog?.open()" />
<!-- Theme Picker -->
<ThemePicker ref="themePicker" @select="handleThemeSelect" />
<!-- Import Dialog -->
<ImportDialog ref="importDialog" />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ref, onMounted, onUnmounted } from "vue";
import { useAppStore } from "@/stores/app.store";
import { useConnectionStore } from "@/stores/connection.store";
import { useSessionStore } from "@/stores/session.store";
@ -106,6 +136,10 @@ import TabBar from "@/components/session/TabBar.vue";
import SessionContainer from "@/components/session/SessionContainer.vue";
import StatusBar from "@/components/common/StatusBar.vue";
import EditorWindow from "@/components/editor/EditorWindow.vue";
import CommandPalette from "@/components/common/CommandPalette.vue";
import ThemePicker from "@/components/common/ThemePicker.vue";
import ImportDialog from "@/components/common/ImportDialog.vue";
import type { ThemeDefinition } from "@/components/common/ThemePicker.vue";
import type { SidebarTab } from "@/components/sidebar/SidebarToggle.vue";
import type { FileEntry } from "@/composables/useSftp";
@ -115,15 +149,21 @@ const sessionStore = useSessionStore();
const sidebarWidth = ref(240);
const sidebarTab = ref<SidebarTab>("connections");
const quickConnectInput = ref("");
/** Currently open file in the editor panel (null = no file open). */
const editorFile = ref<{ content: string; path: string; sessionId: string } | null>(null);
/** Handle file open from SFTP sidebar — loads mock content for now. */
const commandPalette = ref<InstanceType<typeof CommandPalette> | null>(null);
const themePicker = ref<InstanceType<typeof ThemePicker> | null>(null);
const importDialog = ref<InstanceType<typeof ImportDialog> | null>(null);
const statusBar = ref<InstanceType<typeof StatusBar> | null>(null);
/** Handle file open from SFTP sidebar -- loads mock content for now. */
function handleOpenFile(entry: FileEntry): void {
if (!sessionStore.activeSession) return;
// TODO: Replace with Wails binding call SFTPService.ReadFile(sessionId, entry.path)
// TODO: Replace with Wails binding call -- SFTPService.ReadFile(sessionId, entry.path)
// Mock file content for development
const mockContent = `# ${entry.name}\n\n` +
`# File: ${entry.path}\n` +
@ -138,4 +178,87 @@ function handleOpenFile(entry: FileEntry): void {
sessionId: sessionStore.activeSession.id,
};
}
/** Handle theme selection from the ThemePicker. */
function handleThemeSelect(theme: ThemeDefinition): void {
statusBar.value?.setThemeName(theme.name);
}
/**
* Quick Connect: parse user@host:port and open a session.
* Default protocol: SSH, default port: 22.
* If port is 3389, use RDP.
*/
function handleQuickConnect(): void {
const raw = quickConnectInput.value.trim();
if (!raw) return;
let username = "";
let hostname = "";
let port = 22;
let protocol: "ssh" | "rdp" = "ssh";
let hostPart = raw;
// Extract username if present (user@...)
const atIdx = raw.indexOf("@");
if (atIdx > 0) {
username = raw.substring(0, atIdx);
hostPart = raw.substring(atIdx + 1);
}
// Extract port if present (...:port)
const colonIdx = hostPart.lastIndexOf(":");
if (colonIdx > 0) {
const portStr = hostPart.substring(colonIdx + 1);
const parsedPort = parseInt(portStr, 10);
if (!isNaN(parsedPort) && parsedPort > 0 && parsedPort <= 65535) {
port = parsedPort;
hostPart = hostPart.substring(0, colonIdx);
}
}
hostname = hostPart;
if (!hostname) return;
// Auto-detect RDP by port
if (port === 3389) {
protocol = "rdp";
}
// Create a temporary connection and session
// TODO: Replace with Wails binding create ephemeral session via SSHService.Connect / RDPService.Connect
const tempId = Math.max(...connectionStore.connections.map((c) => c.id), 0) + 1;
const name = username ? `${username}@${hostname}` : hostname;
connectionStore.connections.push({
id: tempId,
name,
hostname,
port,
protocol,
groupId: 1,
tags: [],
});
sessionStore.connect(tempId);
quickConnectInput.value = "";
}
/** Global keyboard shortcut handler. */
function handleKeydown(event: KeyboardEvent): void {
// Ctrl+K or Cmd+K open command palette
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
event.preventDefault();
commandPalette.value?.toggle();
}
}
onMounted(() => {
document.addEventListener("keydown", handleKeydown);
});
onUnmounted(() => {
document.removeEventListener("keydown", handleKeydown);
});
</script>