From a3a74af342ab7a9fb3cfdef4f1def15d8bc64ca1 Mon Sep 17 00:00:00 2001 From: lone-cloud Date: Tue, 9 Sep 2025 11:23:58 -0700 Subject: [PATCH] better terminal transition, main window bg color based on theme preference, center the launched window on open --- src/App.tsx | 20 +++---- .../screens/Interface/TerminalTab.tsx | 52 ++++++++++--------- src/components/settings/AppearanceTab.tsx | 40 ++++++++++---- src/components/settings/GeneralTab.tsx | 35 ++++++++++++- src/main/ipc.ts | 12 +++++ src/main/modules/config.ts | 24 +++++++++ src/main/modules/koboldcpp.ts | 2 +- src/main/modules/window.ts | 19 +++++-- src/preload/index.ts | 3 ++ src/types/electron.d.ts | 3 ++ 10 files changed, 159 insertions(+), 51 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 558fe24..7ea62ff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,10 +40,6 @@ export const App = () => { downloadProgress, } = useKoboldVersions(); - const setCurrentScreenWithTransition = (screen: Screen) => { - setCurrentScreen(screen); - }; - useEffect(() => { const checkInstallation = async () => { await safeExecute(async () => { @@ -58,11 +54,11 @@ export const App = () => { setFrontendPreference(preference || 'koboldcpp'); if (!hasSeenWelcome) { - setCurrentScreenWithTransition('welcome'); + setCurrentScreen('welcome'); } else if (currentVersion) { - setCurrentScreenWithTransition('launch'); + setCurrentScreen('launch'); } else { - setCurrentScreenWithTransition('download'); + setCurrentScreen('download'); } if (currentVersion) { @@ -99,17 +95,17 @@ export const App = () => { }, 'Error refreshing versions after download:'); setTimeout(() => { - setCurrentScreenWithTransition('launch'); + setCurrentScreen('launch'); }, 100); }; const handleLaunch = () => { setActiveInterfaceTab('terminal'); - setCurrentScreenWithTransition('interface'); + setCurrentScreen('interface'); }; const handleBackToLaunch = () => { - setCurrentScreenWithTransition('launch'); + setCurrentScreen('launch'); }; const handleEject = async () => { @@ -141,9 +137,9 @@ export const App = () => { const versions = await window.electronAPI.kobold.getInstalledVersions(); if (versions.length > 0) { - setCurrentScreenWithTransition('launch'); + setCurrentScreen('launch'); } else { - setCurrentScreenWithTransition('download'); + setCurrentScreen('download'); } }; diff --git a/src/components/screens/Interface/TerminalTab.tsx b/src/components/screens/Interface/TerminalTab.tsx index f1a8b7a..104f7b6 100644 --- a/src/components/screens/Interface/TerminalTab.tsx +++ b/src/components/screens/Interface/TerminalTab.tsx @@ -8,7 +8,6 @@ import { import { Box, ScrollArea, - Text, ActionIcon, useComputedColorScheme, } from '@mantine/core'; @@ -36,12 +35,21 @@ export const TerminalTab = forwardRef( const [terminalContent, setTerminalContent] = useState(''); const [isUserScrolling, setIsUserScrolling] = useState(false); const [shouldAutoScroll, setShouldAutoScroll] = useState(true); + const [isVisible, setIsVisible] = useState(false); const scrollAreaRef = useRef(null); const viewportRef = useRef(null); const lastScrollTop = useRef(0); const isDark = computedColorScheme === 'dark'; + useEffect(() => { + const timer = setTimeout(() => { + setIsVisible(true); + }, 150); + + return () => clearTimeout(timer); + }, []); + const handleScroll = ({ y }: { y: number }) => { if (!viewportRef.current) return; @@ -143,29 +151,25 @@ export const TerminalTab = forwardRef( offsetScrollbars={false} > - {terminalContent.length === 0 ? ( - - Starting... - - ) : ( -
- )} +
diff --git a/src/components/settings/AppearanceTab.tsx b/src/components/settings/AppearanceTab.tsx index 97585a5..3887b7b 100644 --- a/src/components/settings/AppearanceTab.tsx +++ b/src/components/settings/AppearanceTab.tsx @@ -8,6 +8,7 @@ import { useComputedColorScheme, Slider, TextInput, + type MantineColorScheme, } from '@mantine/core'; import { Sun, Moon, Monitor } from 'lucide-react'; import { useState, useEffect } from 'react'; @@ -32,19 +33,40 @@ export const AppearanceTab = () => { ); useEffect(() => { - const loadZoomLevel = async () => { - const currentZoom = await safeExecute( - () => window.electronAPI.app.getZoomLevel(), - 'Failed to load zoom level:' - ); + const loadSettings = async () => { + const [currentZoom, savedColorScheme] = await Promise.all([ + safeExecute( + () => window.electronAPI.app.getZoomLevel(), + 'Failed to load zoom level:' + ), + safeExecute( + () => window.electronAPI.app.getColorScheme(), + 'Failed to load color scheme:' + ), + ]); + if (typeof currentZoom === 'number') { setZoomLevel(currentZoom); setZoomPercentage(zoomLevelToPercentage(currentZoom).toString()); } + + if (savedColorScheme && savedColorScheme !== colorScheme) { + setColorScheme(savedColorScheme as MantineColorScheme); + } }; - void loadZoomLevel(); - }, []); + void loadSettings(); + }, [colorScheme, setColorScheme]); + + const handleColorSchemeChange = async (value: string) => { + const newColorScheme = value as MantineColorScheme; + setColorScheme(newColorScheme); + + await safeExecute( + () => window.electronAPI.app.setColorScheme(newColorScheme), + 'Failed to save color scheme:' + ); + }; const handleZoomChange = async (newZoomLevel: number) => { setZoomLevel(newZoomLevel); @@ -83,9 +105,7 @@ export const AppearanceTab = () => { - setColorScheme(value as 'light' | 'dark' | 'auto') - } + onChange={handleColorSchemeChange} styles={(theme) => ({ indicator: { backgroundColor: isDark diff --git a/src/components/settings/GeneralTab.tsx b/src/components/settings/GeneralTab.tsx index 615eb49..0b22b9c 100644 --- a/src/components/settings/GeneralTab.tsx +++ b/src/components/settings/GeneralTab.tsx @@ -10,6 +10,7 @@ import { Select, Box, Anchor, + Badge, } from '@mantine/core'; import { Folder, FolderOpen, Monitor } from 'lucide-react'; import type { FrontendPreference } from '@/types'; @@ -24,6 +25,7 @@ interface FrontendRequirement { interface FrontendConfig { value: string; label: string; + badges: string[]; requirements?: FrontendRequirement[]; requirementCheck?: () => Promise; } @@ -44,10 +46,15 @@ export const GeneralTab = ({ const frontendConfigs: FrontendConfig[] = useMemo( () => [ - { value: 'koboldcpp', label: 'Built-in Interface' }, + { + value: 'koboldcpp', + label: 'Built-in Interface', + badges: ['Text', 'Image'], + }, { value: 'sillytavern', label: FRONTENDS.SILLYTAVERN, + badges: ['Text', 'Image'], requirements: [ { id: 'nodejs', @@ -60,6 +67,7 @@ export const GeneralTab = ({ { value: 'openwebui', label: FRONTENDS.OPENWEBUI, + badges: ['Text'], requirements: [ { id: 'uv', @@ -186,6 +194,30 @@ export const GeneralTab = ({ } }; + const renderSelectOption = ({ + option, + }: { + option: { value: string; label: string; disabled?: boolean }; + }) => { + const config = frontendConfigs.find((c) => c.value === option.value); + if (!config) return {option.label}; + + return ( + + + {config.label} + + + {config.badges.map((badge) => ( + + {badge} + + ))} + + + ); + }; + return (
@@ -258,6 +290,7 @@ export const GeneralTab = ({ disabled: !isFrontendAvailable(config.value), }))} leftSection={} + renderOption={renderSelectOption} /> diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 7f45fe7..85508ac 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,5 +1,6 @@ import { ipcMain, shell, app } from 'electron'; import * as os from 'os'; +import type { MantineColorScheme } from '@mantine/core'; import { launchKoboldCpp, downloadRelease, @@ -19,6 +20,8 @@ import { getSelectedConfig, setSelectedConfig, getInstallDir, + getColorScheme, + setColorScheme, } from '@/main/modules/config'; import { logError, getLogsDirectory } from '@/main/modules/logging'; import { getSillyTavernManager } from '@/main/modules/sillytavern'; @@ -198,6 +201,15 @@ export function setupIPCHandlers() { } }); + ipcMain.handle('app:getColorScheme', () => getColorScheme()); + + ipcMain.handle( + 'app:setColorScheme', + async (_, colorScheme: MantineColorScheme) => { + await setColorScheme(colorScheme); + } + ); + const mainWindow = getMainWindow(); if (mainWindow) { mainWindow.webContents.once('did-finish-load', async () => { diff --git a/src/main/modules/config.ts b/src/main/modules/config.ts index 4e80672..79961a2 100644 --- a/src/main/modules/config.ts +++ b/src/main/modules/config.ts @@ -2,8 +2,10 @@ import { logError } from '@/main/modules/logging'; import { readJsonFile, writeJsonFile } from '@/utils/fs'; import { getConfigDir } from '@/utils/path'; import type { FrontendPreference } from '@/types'; +import type { MantineColorScheme } from '@mantine/core'; import { homedir } from 'os'; import { join } from 'path'; +import { nativeTheme } from 'electron'; import { PRODUCT_NAME } from '@/constants'; type ConfigValue = string | number | boolean | unknown[] | undefined; @@ -13,6 +15,7 @@ interface AppConfig { currentKoboldBinary?: string; selectedConfig?: string; frontendPreference?: FrontendPreference; + colorScheme?: MantineColorScheme; [key: string]: ConfigValue; } @@ -92,3 +95,24 @@ export async function setSelectedConfig(configName: string) { config.selectedConfig = configName; await saveConfig(); } + +export function getColorScheme() { + return config.colorScheme || 'auto'; +} + +export async function setColorScheme(colorScheme: MantineColorScheme) { + config.colorScheme = colorScheme; + await saveConfig(); +} + +export function getBackgroundColor(): string { + const colorScheme = getColorScheme(); + + if (colorScheme === 'light') { + return '#ffffff'; + } else if (colorScheme === 'dark') { + return '#1a1b1e'; + } else { + return nativeTheme.shouldUseDarkColors ? '#1a1b1e' : '#ffffff'; + } +} diff --git a/src/main/modules/koboldcpp.ts b/src/main/modules/koboldcpp.ts index 5e450f8..147ddd3 100644 --- a/src/main/modules/koboldcpp.ts +++ b/src/main/modules/koboldcpp.ts @@ -642,7 +642,7 @@ export async function launchKoboldCpp( koboldProcess = child; - const commandLine = `$ ${currentVersion.path} ${finalArgs.join(' ')}`; + const commandLine = `${currentVersion.path} ${finalArgs.join(' ')}`; sendKoboldOutput(commandLine); diff --git a/src/main/modules/window.ts b/src/main/modules/window.ts index abad5cc..a9f1245 100644 --- a/src/main/modules/window.ts +++ b/src/main/modules/window.ts @@ -4,21 +4,29 @@ import { stripVTControlCharacters } from 'util'; import { PRODUCT_NAME } from '../../constants'; import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc'; import { isDevelopment } from '@/utils/environment'; +import { getBackgroundColor } from './config'; let mainWindow: BrowserWindow | null = null; export function createMainWindow() { const { size } = screen.getPrimaryDisplay(); const windowHeight = Math.floor(size.height * 0.86); + const windowWidth = 800; + + const icon = isDevelopment + ? join(__dirname, '../../src/assets/icon.png') + : join(__dirname, '../../assets/icon.png'); mainWindow = new BrowserWindow({ - width: 1000, + width: windowWidth, height: windowHeight, + x: Math.floor((size.width - windowWidth) / 2), + y: Math.floor((size.height - windowHeight) / 2), frame: false, title: PRODUCT_NAME, show: false, - backgroundColor: '#ffffff', - icon: join(__dirname, '../../src/assets/icon.png'), + backgroundColor: getBackgroundColor(), + icon, webPreferences: { nodeIntegration: false, contextIsolation: true, @@ -60,6 +68,11 @@ export function createMainWindow() { mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'allow', + overrideBrowserWindowOptions: { + icon, + title: PRODUCT_NAME, + backgroundColor: getBackgroundColor(), + }, })); mainWindow.on('close', () => { diff --git a/src/preload/index.ts b/src/preload/index.ts index 2c86052..efaf692 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -84,6 +84,9 @@ const appAPI: AppAPI = { closeWindow: () => ipcRenderer.invoke('app:closeWindow'), getZoomLevel: () => ipcRenderer.invoke('app:getZoomLevel'), setZoomLevel: (level) => ipcRenderer.invoke('app:setZoomLevel', level), + getColorScheme: () => ipcRenderer.invoke('app:getColorScheme'), + setColorScheme: (colorScheme) => + ipcRenderer.invoke('app:setColorScheme', colorScheme), }; const configAPI: ConfigAPI = { diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 6d19279..86e6e0b 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -5,6 +5,7 @@ import type { GPUMemoryInfo, } from '@/types/hardware'; import type { BackendOption, BackendSupport } from '@/types'; +import type { MantineColorScheme } from '@mantine/core'; export interface GitHubAsset { name: string; @@ -150,6 +151,8 @@ export interface AppAPI { closeWindow: () => void; getZoomLevel: () => Promise; setZoomLevel: (level: number) => Promise; + getColorScheme: () => Promise; + setColorScheme: (colorScheme: MantineColorScheme) => Promise; } export interface ConfigAPI {