Skip to content

Commit 14258c4

Browse files
committed
v0.0.2: spotlight input bar, system theme sync, persistent spotlight position
1 parent a2f6de4 commit 14258c4

13 files changed

Lines changed: 566 additions & 30 deletions

File tree

.github/workflows/release.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,16 @@ jobs:
133133
APPLE_ID: ${{ secrets.APPLE_ID }}
134134
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
135135
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
136+
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
136137
run: npx electron-builder --mac --${{ matrix.arch }} -c.mac.identity=auto -c.mac.notarize=true --publish never
137138

138139
- name: Package for macOS (unsigned fallback)
139140
if: matrix.os == 'macos-latest' && (steps.apple_cert.outcome != 'success' || steps.mac_build_signed.outcome == 'failure')
141+
env:
142+
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
140143
run: |
141144
rm -rf dist/
142-
npx electron-builder --mac --${{ matrix.arch }} --publish never
145+
npx electron-builder --mac --${{ matrix.arch }} -c.mac.notarize=false --publish never
143146
144147
- name: Package for Linux
145148
if: matrix.os == 'ubuntu-latest'

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
## [0.0.2] - 2026-04-06
11+
12+
### Added
13+
14+
- **Spotlight Input Bar** — Lightweight quick-chat bar (⇧⌘I) for submitting queries without opening the full app
15+
- **Spotlight Shortcut** — Dedicated configurable shortcut for the spotlight, independent from the global app shortcut
16+
- **Draggable Spotlight** — Spotlight bar can be dragged to any position on screen
17+
- **Persistent Spotlight Position** — Spotlight position is saved to config and restored across app restarts
18+
- **Spotlight Settings** — Shortcut recorder in Settings → General for the spotlight shortcut
19+
20+
21+
### Fixed
22+
23+
- **System Theme Sync** — App now listens for OS dark/light mode changes in real-time when set to "Auto" (previously only checked once at startup)
24+
825
## [0.0.1] - 2026-03-20
926

1027
### Added

electron.vite.config.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,21 @@ export default defineConfig({
1414
rollupOptions: {
1515
input: {
1616
index: resolve(__dirname, 'src/preload/index.ts'),
17-
'content-preload': resolve(__dirname, 'src/preload/content-preload.ts')
17+
'content-preload': resolve(__dirname, 'src/preload/content-preload.ts'),
18+
'spotlight-preload': resolve(__dirname, 'src/preload/spotlight-preload.ts')
1819
}
1920
}
2021
}
2122
},
2223
renderer: {
24+
build: {
25+
rollupOptions: {
26+
input: {
27+
index: resolve(__dirname, 'src/renderer/index.html'),
28+
spotlight: resolve(__dirname, 'src/renderer/spotlight.html')
29+
}
30+
}
31+
},
2332
plugins: [tailwindcss(), svelte()]
2433
}
2534
})

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "open-webui",
3-
"version": "0.0.1",
3+
"version": "0.0.2",
44
"license": "AGPL-3.0",
55
"description": "Open WebUI Desktop",
66
"main": "./out/main/index.js",

src/main/index.ts

Lines changed: 181 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ if (process.platform === 'linux') {
9494

9595
let mainWindow: BrowserWindow | null = null
9696
let contentWindow: BrowserWindow | null = null
97+
let spotlightWindow: BrowserWindow | null = null
9798
let tray: Tray | null = null
9899
let isQuiting = false
99100

@@ -103,34 +104,151 @@ let SERVER_STATUS: string | null = null
103104
let SERVER_REACHABLE = false
104105
let SERVER_PID: number | null = null
105106

106-
// ─── Global Shortcut ────────────────────────────────────
107+
// ─── Global Shortcuts ───────────────────────────────────
107108

108-
const registerGlobalShortcut = (accelerator?: string): void => {
109+
const registerShortcuts = (globalAccel?: string, spotlightAccel?: string): void => {
109110
globalShortcut.unregisterAll()
110-
if (!accelerator) return
111-
try {
112-
globalShortcut.register(accelerator, () => {
113-
if (contentWindow && !contentWindow.isDestroyed()) {
114-
contentWindow.show()
115-
contentWindow.focus()
111+
112+
// Global shortcut – bring main window to foreground
113+
if (globalAccel) {
114+
try {
115+
globalShortcut.register(globalAccel, () => {
116+
if (mainWindow) {
117+
mainWindow.show()
118+
mainWindow.focus()
119+
} else {
120+
createMainWindow()
121+
}
122+
})
123+
} catch (error) {
124+
log.warn('Failed to register global shortcut:', globalAccel, error)
125+
}
126+
}
127+
128+
// Spotlight shortcut – toggle the spotlight input bar
129+
if (spotlightAccel) {
130+
try {
131+
globalShortcut.register(spotlightAccel, () => {
132+
toggleSpotlight()
133+
})
134+
} catch (error) {
135+
log.warn('Failed to register spotlight shortcut:', spotlightAccel, error)
136+
}
137+
}
138+
}
139+
140+
// ─── Spotlight Window ───────────────────────────────────
141+
142+
// Remember where the user dragged the spotlight
143+
let spotlightPosition: { x: number; y: number } | null = null
144+
145+
// Load persisted spotlight position from config (call after CONFIG is loaded)
146+
function loadSpotlightPosition(): void {
147+
if (CONFIG?.spotlightPosition) {
148+
spotlightPosition = { ...CONFIG.spotlightPosition }
149+
}
150+
}
151+
152+
function getDefaultSpotlightPosition(): { x: number; y: number } {
153+
const { screen } = require('electron')
154+
const cursorPoint = screen.getCursorScreenPoint()
155+
const activeDisplay = screen.getDisplayNearestPoint(cursorPoint)
156+
const { width: screenW } = activeDisplay.workAreaSize
157+
const { x: screenX, y: screenY } = activeDisplay.workArea
158+
const winW = 748
159+
return {
160+
x: Math.round(screenX + (screenW - winW) / 2),
161+
y: Math.round(screenY + 160)
162+
}
163+
}
164+
165+
function createSpotlightWindow(): BrowserWindow {
166+
const pos = spotlightPosition || getDefaultSpotlightPosition()
167+
168+
const winW = 748
169+
const winH = 86
170+
171+
spotlightWindow = new BrowserWindow({
172+
width: winW,
173+
height: winH,
174+
x: pos.x,
175+
y: pos.y,
176+
frame: false,
177+
transparent: true,
178+
alwaysOnTop: true,
179+
skipTaskbar: true,
180+
resizable: false,
181+
hasShadow: false,
182+
show: false,
183+
icon: path.join(__dirname, 'assets/icon.png'),
184+
webPreferences: {
185+
preload: join(__dirname, '../preload/spotlight-preload.js'),
186+
sandbox: false,
187+
webviewTag: false
188+
}
189+
})
190+
191+
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
192+
spotlightWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/spotlight.html`)
193+
} else {
194+
spotlightWindow.loadFile(join(__dirname, '../renderer/spotlight.html'))
195+
}
196+
197+
// Save position when user drags to a new spot
198+
spotlightWindow.on('moved', () => {
199+
if (spotlightWindow && !spotlightWindow.isDestroyed()) {
200+
const [x, y] = spotlightWindow.getPosition()
201+
spotlightPosition = { x, y }
202+
// Persist to config for cross-session recall
203+
setConfig({ spotlightPosition: { x, y } }).catch((err) =>
204+
log.warn('Failed to persist spotlight position:', err)
205+
)
206+
}
207+
})
208+
209+
spotlightWindow.on('blur', () => {
210+
spotlightWindow?.hide()
211+
})
212+
213+
spotlightWindow.on('closed', () => {
214+
spotlightWindow = null
215+
})
216+
217+
return spotlightWindow
218+
}
219+
220+
function toggleSpotlight(): void {
221+
if (spotlightWindow && !spotlightWindow.isDestroyed()) {
222+
if (spotlightWindow.isVisible()) {
223+
spotlightWindow.hide()
224+
} else {
225+
// Restore to saved position, or default if none saved
226+
if (spotlightPosition) {
227+
spotlightWindow.setPosition(spotlightPosition.x, spotlightPosition.y)
116228
} else {
117-
mainWindow?.show()
118-
mainWindow?.focus()
229+
const pos = getDefaultSpotlightPosition()
230+
spotlightWindow.setPosition(pos.x, pos.y)
119231
}
232+
spotlightWindow.show()
233+
spotlightWindow.focus()
234+
}
235+
} else {
236+
const win = createSpotlightWindow()
237+
win.once('ready-to-show', () => {
238+
win.show()
239+
win.focus()
120240
})
121-
} catch (error) {
122-
log.warn('Failed to register global shortcut:', accelerator, error)
123241
}
124242
}
125243

126244
// ─── Windows ────────────────────────────────────────────
127245

128246
function createMainWindow(show = true): void {
129247
mainWindow = new BrowserWindow({
130-
width: 1100,
131-
height: 700,
132-
minWidth: 900,
133-
minHeight: 560,
248+
width: 1280,
249+
height: 800,
250+
minWidth: 1280,
251+
minHeight: 800,
134252
icon: path.join(__dirname, 'assets/icon.png'),
135253
show: false,
136254
titleBarStyle: process.platform === 'win32' ? 'default' : 'hidden',
@@ -192,10 +310,10 @@ function createContentWindow(url: string, connectionId: string): BrowserWindow {
192310
}
193311

194312
contentWindow = new BrowserWindow({
195-
width: 1200,
313+
width: 1280,
196314
height: 800,
197-
minWidth: 900,
198-
minHeight: 560,
315+
minWidth: 1280,
316+
minHeight: 800,
199317
icon: path.join(__dirname, 'assets/icon.png'),
200318
show: false,
201319
titleBarStyle: process.platform === 'win32' ? 'default' : 'hidden',
@@ -603,6 +721,7 @@ if (!gotTheLock) {
603721

604722
app.whenReady().then(async () => {
605723
CONFIG = await getConfig()
724+
loadSpotlightPosition()
606725
log.info('Config:', CONFIG)
607726

608727
app.name = 'Open WebUI'
@@ -649,7 +768,7 @@ if (!gotTheLock) {
649768
await setConfig(config)
650769
CONFIG = await getConfig()
651770
updateTray()
652-
registerGlobalShortcut(CONFIG.globalShortcut)
771+
registerShortcuts(CONFIG.globalShortcut, CONFIG.spotlightShortcut)
653772
})
654773

655774
// Python/uv
@@ -803,6 +922,43 @@ if (!gotTheLock) {
803922
// Misc
804923
ipcMain.handle('app:reset', () => resetAppHandler())
805924

925+
// Spotlight
926+
ipcMain.handle('spotlight:submit', async (_event, query: string) => {
927+
const config = await getConfig()
928+
if (!config.defaultConnectionId || config.connections.length === 0) {
929+
// No default connection — just show main window
930+
mainWindow?.show()
931+
mainWindow?.focus()
932+
return
933+
}
934+
const conn = config.connections.find((c) => c.id === config.defaultConnectionId)
935+
if (!conn) {
936+
mainWindow?.show()
937+
mainWindow?.focus()
938+
return
939+
}
940+
941+
let url = conn.url
942+
if (conn.type === 'local' && SERVER_URL) {
943+
url = SERVER_URL
944+
}
945+
if (url.startsWith('http://0.0.0.0')) {
946+
url = url.replace('http://0.0.0.0', 'http://localhost')
947+
}
948+
949+
// Navigate to the connection URL with query
950+
const targetUrl = `${url}/?q=${encodeURIComponent(query)}`
951+
sendToRenderer('connection:open', { url: targetUrl, connectionId: conn.id })
952+
953+
// Show main window and hide spotlight
954+
mainWindow?.show()
955+
mainWindow?.focus()
956+
spotlightWindow?.hide()
957+
})
958+
ipcMain.handle('spotlight:close', () => {
959+
spotlightWindow?.hide()
960+
})
961+
806962
// Open Terminal
807963
ipcMain.handle('open-terminal:start', async () => {
808964
try {
@@ -1058,7 +1214,7 @@ if (!gotTheLock) {
10581214

10591215

10601216
// Global shortcut
1061-
registerGlobalShortcut(CONFIG.globalShortcut)
1217+
registerShortcuts(CONFIG.globalShortcut, CONFIG.spotlightShortcut)
10621218

10631219
// Enable screen capture
10641220
session.defaultSession.setDisplayMediaRequestHandler(
@@ -1145,6 +1301,10 @@ if (!gotTheLock) {
11451301
globalShortcut.unregisterAll()
11461302
mainWindow = null
11471303
contentWindow = null
1304+
if (spotlightWindow && !spotlightWindow.isDestroyed()) {
1305+
spotlightWindow.destroy()
1306+
}
1307+
spotlightWindow = null
11481308
tray?.destroy()
11491309
tray = null
11501310
})

src/main/utils/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,7 @@ export interface AppConfig {
780780
connections: Connection[]
781781
runInBackground: boolean
782782
globalShortcut: string
783+
spotlightShortcut: string
783784
dataDir: string
784785
localServer: {
785786
port: number
@@ -800,6 +801,7 @@ export interface AppConfig {
800801
}
801802
envVars: Record<string, string>
802803
showSidebar: boolean
804+
spotlightPosition: { x: number; y: number } | null
803805
}
804806

805807
const DEFAULT_CONFIG: AppConfig = {
@@ -808,6 +810,7 @@ const DEFAULT_CONFIG: AppConfig = {
808810
connections: [],
809811
runInBackground: true,
810812
globalShortcut: 'Alt+CommandOrControl+O',
813+
spotlightShortcut: 'Shift+CommandOrControl+I',
811814
dataDir: '',
812815
localServer: {
813816
port: 8080,
@@ -825,7 +828,8 @@ const DEFAULT_CONFIG: AppConfig = {
825828
extraArgs: []
826829
},
827830
envVars: {},
828-
showSidebar: true
831+
showSidebar: true,
832+
spotlightPosition: null
829833
}
830834

831835
export const getConfig = async (): Promise<AppConfig> => {

src/preload/spotlight-preload.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ipcRenderer, contextBridge } from 'electron'
2+
3+
const api = {
4+
submitQuery: (query: string): void => {
5+
ipcRenderer.invoke('spotlight:submit', query)
6+
},
7+
closeSpotlight: (): void => {
8+
ipcRenderer.invoke('spotlight:close')
9+
}
10+
}
11+
12+
if (process.contextIsolated) {
13+
try {
14+
contextBridge.exposeInMainWorld('spotlightAPI', api)
15+
} catch (error) {
16+
console.error(error)
17+
}
18+
} else {
19+
// @ts-ignore
20+
window.spotlightAPI = api
21+
}

0 commit comments

Comments
 (0)