wraith/frontend/src/components/common/StatusBar.vue
Vantz Stockwell a6db3ddfa4 feat: fix 6 frontend issues (F-1, F-5, F-6, F-7, F-10, F-11)
F-1 (Theme Application): Theme selection now applies to all active xterm.js
terminals at runtime via session store reactive propagation. TerminalView
watches sessionStore.activeTheme and calls terminal.options.theme = {...}.

F-5 (Tab Badges): isRootUser() now checks session.username, connection
options JSON, and connection tags — no longer hardcoded to false.

F-6 (Keyboard Shortcuts): Added Ctrl+W (close tab), Ctrl+Tab / Ctrl+Shift+Tab
(next/prev tab), Ctrl+1–9 (tab by index), Ctrl+B (toggle sidebar). Input
field guard prevents shortcuts from firing while typing.

F-7 (Status Bar Dimensions): StatusBar now reads live cols×rows from
sessionStore.activeDimensions. TerminalView hooks onResize to call
sessionStore.setTerminalDimensions(). Falls back to "120×40" until first resize.

F-10 (Multiple Sessions): Removed the "already connected" early-return guard.
Multiple SSH/RDP sessions to the same host are now allowed. Disambiguated tab
names auto-generated: "Asgard", "Asgard (2)", "Asgard (3)", etc.

F-11 (First-Run MobaConf): onMounted checks connectionStore.connections.length
after loadAll(). If empty, shows a dialog offering to import from MobaXTerm.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 13:41:28 -04:00

141 lines
4.8 KiB
Vue

<template>
<div class="h-6 flex items-center justify-between px-4 bg-[var(--wraith-bg-secondary)] border-t border-[var(--wraith-border)] text-[10px] text-[var(--wraith-text-muted)] shrink-0">
<!-- Left: connection info -->
<div class="flex items-center gap-3">
<template v-if="sessionStore.activeSession">
<span class="flex items-center gap-1">
<span
class="w-1.5 h-1.5 rounded-full"
:class="sessionStore.activeSession.protocol === 'ssh' ? 'bg-[#3fb950]' : 'bg-[#1f6feb]'"
/>
{{ sessionStore.activeSession.protocol.toUpperCase() }}
</span>
<span class="text-[var(--wraith-text-secondary)]">&middot;</span>
<span>{{ connectionInfo }}</span>
</template>
<template v-else>
<span>Ready</span>
</template>
</div>
<!-- Right: terminal info + update notification -->
<div class="flex items-center gap-3">
<!-- Update notification pill -->
<button
v-if="updateAvailable && updateInfo"
class="flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-medium transition-colors cursor-pointer"
:class="updateState === 'downloading'
? 'bg-[#1f6feb]/30 text-[#58a6ff]'
: 'bg-[#1f6feb]/20 text-[#58a6ff] hover:bg-[#1f6feb]/30'"
:disabled="updateState === 'downloading'"
@click="handleUpdate"
>
<template v-if="updateState === 'idle'">
<svg class="w-3 h-3" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2Zm.75 3.5v3.69l1.72-1.72a.75.75 0 1 1 1.06 1.06l-3 3a.75.75 0 0 1-1.06 0l-3-3a.75.75 0 1 1 1.06-1.06l1.72 1.72V5.5a.75.75 0 0 1 1.5 0Z" />
</svg>
v{{ updateInfo.latestVersion }} available &mdash; Update
</template>
<template v-else-if="updateState === 'downloading'">
<svg class="w-3 h-3 animate-spin" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0ZM1.5 8a6.5 6.5 0 1 1 13 0 6.5 6.5 0 0 1-13 0Z" opacity=".25" />
<path d="M8 0a8 8 0 0 1 8 8h-1.5A6.5 6.5 0 0 0 8 1.5V0Z" />
</svg>
Downloading...
</template>
</button>
<button
class="hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
title="Change terminal theme"
@click="emit('open-theme-picker')"
>
Theme: {{ activeThemeName }}
</button>
<span>UTF-8</span>
<span v-if="sessionStore.activeDimensions">
{{ sessionStore.activeDimensions.cols }}&times;{{ sessionStore.activeDimensions.rows }}
</span>
<span v-else>120&times;40</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from "vue";
import { Call } from "@wailsio/runtime";
import { useSessionStore } from "@/stores/session.store";
import { useConnectionStore } from "@/stores/connection.store";
const sessionStore = useSessionStore();
const connectionStore = useConnectionStore();
const activeThemeName = ref("Dracula");
const emit = defineEmits<{
(e: "open-theme-picker"): void;
}>();
interface UpdateInfoData {
available: boolean;
currentVersion: string;
latestVersion: string;
downloadUrl: string;
sha256: string;
}
const updateAvailable = ref(false);
const updateInfo = ref<UpdateInfoData | null>(null);
const updateState = ref<"idle" | "downloading">("idle");
const connectionInfo = computed(() => {
const session = sessionStore.activeSession;
if (!session) return "";
const conn = connectionStore.connections.find((c) => c.id === session.connectionId);
if (!conn) return session.name;
return `root@${conn.hostname}:${conn.port}`;
});
/** Check for updates on mount. */
onMounted(async () => {
try {
const info = await Call.ByName(
"github.com/vstockwell/wraith/internal/updater.UpdateService.CheckForUpdate"
) as UpdateInfoData | null;
if (info && info.available) {
updateAvailable.value = true;
updateInfo.value = info;
}
} catch {
// Silent fail — update check is non-critical.
}
});
/** Download and apply an update. */
async function handleUpdate(): Promise<void> {
if (!updateInfo.value || updateState.value === "downloading") return;
updateState.value = "downloading";
try {
const path = await Call.ByName(
"github.com/vstockwell/wraith/internal/updater.UpdateService.DownloadUpdate",
updateInfo.value
) as string;
await Call.ByName(
"github.com/vstockwell/wraith/internal/updater.UpdateService.ApplyUpdate",
path
);
} catch (e) {
console.error("Update failed:", e);
updateState.value = "idle";
}
}
function setThemeName(name: string): void {
activeThemeName.value = name;
}
defineExpose({ setThemeName, activeThemeName, updateAvailable, updateInfo });
</script>