generated from nhcarrigan/template
2db858080d
- Add font_size config field (10-24px, default 14px) - Add keyboard shortcuts: Ctrl++/- to adjust, Ctrl+0 to reset - Add font size slider in Settings > Appearance - Apply font size to Terminal and InputBar via CSS variable - Persist font size preference between sessions Closes #19
245 lines
7.4 KiB
Svelte
245 lines
7.4 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from "svelte";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { get } from "svelte/store";
|
|
import { initializeTauriListeners, cleanupTauriListeners } from "$lib/tauri";
|
|
import { configStore, applyTheme, applyFontSize } from "$lib/stores/config";
|
|
import { initNotificationSync, cleanupNotificationSync } from "$lib/stores/notifications";
|
|
import { conversationsStore } from "$lib/stores/conversations";
|
|
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
|
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
import "$lib/notifications/testNotifications";
|
|
import Terminal from "$lib/components/Terminal.svelte";
|
|
import InputBar from "$lib/components/InputBar.svelte";
|
|
import StatusBar from "$lib/components/StatusBar.svelte";
|
|
import AnimeGirl from "$lib/components/AnimeGirl.svelte";
|
|
import PermissionModal from "$lib/components/PermissionModal.svelte";
|
|
import UserQuestionModal from "$lib/components/UserQuestionModal.svelte";
|
|
import ConfigSidebar from "$lib/components/ConfigSidebar.svelte";
|
|
import AchievementNotification from "$lib/components/AchievementNotification.svelte";
|
|
import AchievementsPanel from "$lib/components/AchievementsPanel.svelte";
|
|
import UpdateNotification from "$lib/components/UpdateNotification.svelte";
|
|
|
|
let initialized = false;
|
|
let updateNotification: UpdateNotification;
|
|
let achievementPanelOpen = $state(false);
|
|
|
|
// Resizable panel state
|
|
let panelWidth = $state(320); // Default width in pixels
|
|
let isResizing = $state(false);
|
|
const MIN_PANEL_WIDTH = 200;
|
|
const MAX_PANEL_WIDTH = 600;
|
|
|
|
function startResize(event: MouseEvent) {
|
|
isResizing = true;
|
|
event.preventDefault();
|
|
document.addEventListener("mousemove", handleResize);
|
|
document.addEventListener("mouseup", stopResize);
|
|
}
|
|
|
|
function handleResize(event: MouseEvent) {
|
|
if (!isResizing) return;
|
|
const newWidth = event.clientX;
|
|
panelWidth = Math.max(MIN_PANEL_WIDTH, Math.min(MAX_PANEL_WIDTH, newWidth));
|
|
}
|
|
|
|
function stopResize() {
|
|
if (isResizing) {
|
|
isResizing = false;
|
|
document.removeEventListener("mousemove", handleResize);
|
|
document.removeEventListener("mouseup", stopResize);
|
|
// Save the panel width to config
|
|
configStore.updateConfig({ character_panel_width: panelWidth });
|
|
}
|
|
}
|
|
|
|
// Global keyboard shortcuts
|
|
function handleGlobalKeydown(event: KeyboardEvent) {
|
|
// Don't trigger shortcuts when typing in inputs (except for specific ones)
|
|
const target = event.target as HTMLElement;
|
|
const isInputFocused = target.tagName === "INPUT" || target.tagName === "TEXTAREA";
|
|
|
|
// Escape closes panels (always works)
|
|
if (event.key === "Escape") {
|
|
// Check if any panels are open and close them
|
|
if (achievementPanelOpen) {
|
|
achievementPanelOpen = false;
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
// ConfigSidebar handles its own escape via store
|
|
if (get(configStore.isSidebarOpen)) {
|
|
configStore.closeSidebar();
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Skip other shortcuts if user is typing in an input
|
|
if (isInputFocused) return;
|
|
|
|
// Ctrl+L - Clear terminal
|
|
if (event.ctrlKey && event.key === "l") {
|
|
event.preventDefault();
|
|
claudeStore.clearTerminal();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+, - Open settings
|
|
if (event.ctrlKey && event.key === ",") {
|
|
event.preventDefault();
|
|
configStore.openSidebar();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+C - Interrupt (only when processing)
|
|
if (event.ctrlKey && event.key === "c") {
|
|
if (get(isClaudeProcessing)) {
|
|
event.preventDefault();
|
|
handleInterrupt();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Ctrl++ or Ctrl+= - Increase font size
|
|
if (event.ctrlKey && (event.key === "+" || event.key === "=")) {
|
|
event.preventDefault();
|
|
configStore.increaseFontSize();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+- - Decrease font size
|
|
if (event.ctrlKey && event.key === "-") {
|
|
event.preventDefault();
|
|
configStore.decreaseFontSize();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+0 - Reset font size
|
|
if (event.ctrlKey && event.key === "0") {
|
|
event.preventDefault();
|
|
configStore.resetFontSize();
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function handleInterrupt() {
|
|
try {
|
|
const conversationId = get(claudeStore.activeConversationId);
|
|
if (!conversationId) return;
|
|
|
|
await invoke("interrupt_claude", { conversationId });
|
|
claudeStore.addLine("system", "Process interrupted");
|
|
} catch (error) {
|
|
console.error("Failed to interrupt:", error);
|
|
}
|
|
}
|
|
|
|
onMount(async () => {
|
|
if (!initialized) {
|
|
initialized = true;
|
|
|
|
// Initialize conversations store first to ensure activeConversationId is set
|
|
conversationsStore.initialize();
|
|
|
|
await initializeTauriListeners();
|
|
await configStore.loadConfig();
|
|
|
|
// Apply saved settings on startup
|
|
const config = configStore.getConfig();
|
|
applyTheme(config.theme);
|
|
applyFontSize(config.font_size);
|
|
|
|
// Apply always-on-top setting
|
|
if (config.always_on_top) {
|
|
const window = getCurrentWindow();
|
|
await window.setAlwaysOnTop(true);
|
|
}
|
|
|
|
// Load saved panel width
|
|
if (config.character_panel_width) {
|
|
panelWidth = config.character_panel_width;
|
|
}
|
|
|
|
// Initialize notification settings sync
|
|
initNotificationSync();
|
|
|
|
// Add global keyboard shortcut listener
|
|
window.addEventListener("keydown", handleGlobalKeydown);
|
|
|
|
// Check for updates on startup
|
|
if (config.update_checks_enabled) {
|
|
updateNotification?.checkForUpdates();
|
|
}
|
|
}
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (initialized) {
|
|
cleanupTauriListeners();
|
|
cleanupNotificationSync();
|
|
window.removeEventListener("keydown", handleGlobalKeydown);
|
|
initialized = false;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div class="app-container h-screen w-screen flex flex-col bg-[var(--bg-primary)] overflow-hidden">
|
|
<StatusBar onToggleAchievements={() => (achievementPanelOpen = !achievementPanelOpen)} />
|
|
|
|
<main class="flex-1 flex overflow-hidden">
|
|
<!-- Left panel: Character display -->
|
|
<div
|
|
class="character-panel flex flex-col items-center justify-center bg-[var(--bg-secondary)]/50"
|
|
style="width: {panelWidth}px; min-width: {MIN_PANEL_WIDTH}px; max-width: {MAX_PANEL_WIDTH}px;"
|
|
>
|
|
<AnimeGirl />
|
|
</div>
|
|
|
|
<!-- Resize handle -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="resize-handle w-1 cursor-col-resize bg-[var(--border-color)] hover:bg-[var(--accent-primary)] transition-colors flex-shrink-0"
|
|
class:bg-[var(--accent-primary)]={isResizing}
|
|
onmousedown={startResize}
|
|
></div>
|
|
|
|
<!-- Right panel: Terminal and input -->
|
|
<div class="terminal-panel flex-1 flex flex-col min-w-0">
|
|
<Terminal />
|
|
<InputBar />
|
|
</div>
|
|
</main>
|
|
|
|
<PermissionModal />
|
|
<UserQuestionModal />
|
|
<ConfigSidebar />
|
|
<AchievementNotification />
|
|
<AchievementsPanel
|
|
bind:isOpen={achievementPanelOpen}
|
|
onClose={() => (achievementPanelOpen = false)}
|
|
/>
|
|
<UpdateNotification bind:this={updateNotification} />
|
|
</div>
|
|
|
|
<style>
|
|
.app-container {
|
|
font-family:
|
|
"Inter",
|
|
-apple-system,
|
|
BlinkMacSystemFont,
|
|
"Segoe UI",
|
|
Roboto,
|
|
sans-serif;
|
|
}
|
|
|
|
.character-panel {
|
|
background: linear-gradient(180deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
|
|
}
|
|
|
|
.resize-handle:hover,
|
|
.resize-handle:active {
|
|
width: 4px;
|
|
}
|
|
</style>
|