From 4045c57fb7588b928aed74b776396c948adf64cf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?BLC=20Core=20Studio=20=E2=80=94=20Developer=20Tools?=
<207100624+BLCCoreStudio@users.noreply.github.com>
Date: Sun, 6 Sep 2026 04:43:48 +0300
Subject: [PATCH 01/41] fix(linux): guard KDE Wayland WebKit renderer startup
---
apps/desktop/src-tauri/src/main.rs | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs
index 52d291c..3e57df2 100644
--- a/apps/desktop/src-tauri/src/main.rs
+++ b/apps/desktop/src-tauri/src/main.rs
@@ -1,5 +1,35 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+#[cfg(target_os = "linux")]
+fn apply_linux_webkit_compatibility() {
+ use std::os::unix::process::CommandExt;
+ use std::process::Command;
+
+ let wayland = std::env::var_os("WAYLAND_DISPLAY").is_some();
+ let kde = std::env::var("XDG_CURRENT_DESKTOP")
+ .ok()
+ .is_some_and(|desktop| desktop.to_ascii_lowercase().contains("kde"));
+ let override_present = std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_some();
+
+ if !wayland || !kde || override_present {
+ return;
+ }
+
+ let Ok(executable) = std::env::current_exe() else {
+ return;
+ };
+
+ let error = Command::new(executable)
+ .args(std::env::args_os().skip(1))
+ .env("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
+ .exec();
+
+ eprintln!("failed to restart BLCVoice with the KDE/Wayland WebKit compatibility guard: {error}");
+}
+
fn main() {
+ #[cfg(target_os = "linux")]
+ apply_linux_webkit_compatibility();
+
blcvoice_desktop_lib::run();
}
From c6bf3705abf238da47603aaceb35a7ce2ffea0d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?BLC=20Core=20Studio=20=E2=80=94=20Developer=20Tools?=
<207100624+BLCCoreStudio@users.noreply.github.com>
Date: Sun, 6 Sep 2026 04:43:55 +0300
Subject: [PATCH 02/41] fix(build): avoid accidental host BLAS linkage
---
.cargo/config.toml | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .cargo/config.toml
diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..6f81ecd
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,5 @@
+# Keep the default local ASR build self-contained and deterministic.
+# Host-wide BLAS/MKL installations must not silently change BLCVoice's native link graph.
+# Developers can still override this environment variable explicitly for controlled experiments.
+[env]
+TRANSCRIBE_CMAKE_ARGS = { value = "-DTRANSCRIBE_USE_SYSTEM_BLAS=OFF", force = false }
From d2a273b704cd4cf3f7cd0272ed607eac6c8f5b40 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?BLC=20Core=20Studio=20=E2=80=94=20Developer=20Tools?=
<207100624+BLCCoreStudio@users.noreply.github.com>
Date: Sun, 6 Sep 2026 04:44:07 +0300
Subject: [PATCH 03/41] feat(overlay): add compact waveform recording surface
---
apps/desktop/ui/overlay.html | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/apps/desktop/ui/overlay.html b/apps/desktop/ui/overlay.html
index 830c008..3d5427b 100644
--- a/apps/desktop/ui/overlay.html
+++ b/apps/desktop/ui/overlay.html
@@ -7,12 +7,25 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
-
BLCVoice
+
+ BLCVoice
+ 00:00
+
Ready
+
Ctrl Shift Space
From 236bb7723573326e9b4c9a8c4c7f18fb3b818c16 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?BLC=20Core=20Studio=20=E2=80=94=20Developer=20Tools?=
<207100624+BLCCoreStudio@users.noreply.github.com>
Date: Sun, 6 Sep 2026 04:44:18 +0300
Subject: [PATCH 04/41] feat(overlay): style lightweight waveform states
---
apps/desktop/ui/overlay.css | 156 ++++++++++++++++++++++++++----------
1 file changed, 113 insertions(+), 43 deletions(-)
diff --git a/apps/desktop/ui/overlay.css b/apps/desktop/ui/overlay.css
index 64fc706..9084153 100644
--- a/apps/desktop/ui/overlay.css
+++ b/apps/desktop/ui/overlay.css
@@ -1,8 +1,8 @@
:root {
- color-scheme: light dark;
+ color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- background: #111318;
- color: #f7f8fa;
+ background: transparent;
+ color: #f7f8fb;
}
* { box-sizing: border-box; }
@@ -13,7 +13,7 @@ body {
height: 100%;
margin: 0;
overflow: hidden;
- background: #111318;
+ background: transparent;
}
body {
@@ -23,83 +23,153 @@ body {
}
.overlay-shell {
- width: 100%;
- height: 100%;
+ width: calc(100% - 8px);
+ height: calc(100% - 8px);
display: grid;
- grid-template-columns: auto 1fr auto;
+ grid-template-columns: 54px minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
- padding: 14px 16px;
- border: 1px solid rgba(255, 255, 255, 0.12);
- background: linear-gradient(180deg, #1c1f26 0%, #14171c 100%);
+ padding: 12px 14px;
+ border: 1px solid rgba(255, 255, 255, 0.11);
+ border-radius: 16px;
+ background: rgba(20, 22, 27, 0.94);
+ box-shadow: 0 16px 44px rgba(0, 0, 0, 0.34);
+ backdrop-filter: blur(18px);
+ -webkit-backdrop-filter: blur(18px);
+ transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
+}
+
+.overlay-shell.recording {
+ border-color: rgba(141, 126, 255, 0.38);
+ background: rgba(23, 22, 31, 0.96);
+}
+
+.overlay-shell.working {
+ border-color: rgba(126, 164, 255, 0.28);
+}
+
+.overlay-shell.success {
+ border-color: rgba(86, 218, 158, 0.3);
}
-.state-indicator {
- width: 14px;
- height: 14px;
+.overlay-shell.failed {
+ border-color: rgba(255, 112, 112, 0.36);
+}
+
+.waveform {
+ height: 34px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 3px;
+}
+
+.waveform span {
+ width: 3px;
+ height: 8px;
border-radius: 999px;
- background: #8d96a8;
- box-shadow: 0 0 0 5px rgba(141, 150, 168, 0.12);
+ background: #8f98a9;
+ opacity: 0.62;
+ transform-origin: center;
}
-.state-indicator.recording {
- background: #ff5263;
- box-shadow: 0 0 0 5px rgba(255, 82, 99, 0.16);
- animation: pulse 1.15s ease-in-out infinite;
+.recording .waveform span {
+ background: #9b8cff;
+ opacity: 1;
+ animation: voice-wave 920ms ease-in-out infinite;
}
-.state-indicator.working {
- background: #8aa7ff;
- box-shadow: 0 0 0 5px rgba(138, 167, 255, 0.14);
+.recording .waveform span:nth-child(2) { animation-delay: -720ms; }
+.recording .waveform span:nth-child(3) { animation-delay: -540ms; }
+.recording .waveform span:nth-child(4) { animation-delay: -360ms; }
+.recording .waveform span:nth-child(5) { animation-delay: -180ms; }
+.recording .waveform span:nth-child(6) { animation-delay: -620ms; }
+.recording .waveform span:nth-child(7) { animation-delay: -440ms; }
+
+.working .waveform span {
+ background: #88a8ff;
+ animation: working-wave 1s ease-in-out infinite;
}
-.state-indicator.success {
- background: #57d99a;
- box-shadow: 0 0 0 5px rgba(87, 217, 154, 0.14);
+.working .waveform span:nth-child(2n) { animation-delay: -500ms; }
+
+.success .waveform span {
+ height: 4px;
+ background: #58dca0;
+ opacity: 0.95;
}
-.state-indicator.failed {
- background: #ff8a65;
- box-shadow: 0 0 0 5px rgba(255, 138, 101, 0.15);
+.failed .waveform span {
+ height: 5px;
+ background: #ff7777;
+ opacity: 0.9;
}
.copy {
min-width: 0;
display: grid;
- gap: 3px;
+ gap: 4px;
+}
+
+.title-row {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
}
.copy strong {
- font-size: 0.88rem;
+ min-width: 0;
+ overflow: hidden;
+ font-size: 0.86rem;
line-height: 1.15;
letter-spacing: -0.01em;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.copy span {
+.copy > span {
overflow: hidden;
- color: #aeb6c6;
- font-size: 0.75rem;
+ color: #a9b0be;
+ font-size: 0.72rem;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
+.timer {
+ flex: none;
+ color: #a99dff;
+ font-size: 0.65rem;
+ font-variant-numeric: tabular-nums;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+}
+
.shortcut-hint {
padding: 5px 7px;
- border: 1px solid rgba(255, 255, 255, 0.1);
- border-radius: 7px;
- color: #8f98a8;
- font-size: 0.62rem;
- font-weight: 650;
- letter-spacing: 0.02em;
+ border: 1px solid rgba(255, 255, 255, 0.09);
+ border-radius: 8px;
+ color: #858e9f;
+ background: rgba(255, 255, 255, 0.035);
+ font-size: 0.59rem;
+ font-weight: 700;
+ letter-spacing: 0.015em;
white-space: nowrap;
}
-@keyframes pulse {
- 0%, 100% { transform: scale(1); opacity: 1; }
- 50% { transform: scale(0.78); opacity: 0.7; }
+@keyframes voice-wave {
+ 0%, 100% { height: 7px; transform: scaleY(0.8); }
+ 50% { height: 29px; transform: scaleY(1); }
+}
+
+@keyframes working-wave {
+ 0%, 100% { height: 6px; opacity: 0.45; }
+ 50% { height: 18px; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
- .state-indicator { animation: none !important; }
+ .waveform span { animation: none !important; }
+ .recording .waveform span:nth-child(odd) { height: 18px; }
+ .recording .waveform span:nth-child(even) { height: 10px; }
}
From eb08371e69f4348c18bf062f7c7c1ca62247ac19 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?BLC=20Core=20Studio=20=E2=80=94=20Developer=20Tools?=
<207100624+BLCCoreStudio@users.noreply.github.com>
Date: Sun, 6 Sep 2026 04:44:32 +0300
Subject: [PATCH 05/41] feat(overlay): position left and show recording
lifecycle
---
apps/desktop/ui/overlay.js | 106 ++++++++++++++++++++++++++++++++-----
1 file changed, 92 insertions(+), 14 deletions(-)
diff --git a/apps/desktop/ui/overlay.js b/apps/desktop/ui/overlay.js
index fa13dd4..5307451 100644
--- a/apps/desktop/ui/overlay.js
+++ b/apps/desktop/ui/overlay.js
@@ -1,27 +1,85 @@
"use strict";
const listen = window.__TAURI__?.event?.listen;
-const getCurrentWindow = window.__TAURI__?.window?.getCurrentWindow;
+const tauriWindow = window.__TAURI__?.window;
+const getCurrentWindow = tauriWindow?.getCurrentWindow;
const overlayWindow = getCurrentWindow ? getCurrentWindow() : null;
+const shell = document.getElementById("overlay-shell");
const title = document.getElementById("overlay-title");
const message = document.getElementById("overlay-message");
-const indicator = document.getElementById("state-indicator");
const shortcutHint = document.getElementById("shortcut-hint");
+const recordingTimer = document.getElementById("recording-timer");
let lifecycleGeneration = 0;
let unlisten = null;
+let timerId = null;
+let recordingStartedAt = null;
-function render(label, detail, kind, showShortcut = true) {
+function truncate(text, max = 78) {
+ if (typeof text !== "string") return "";
+ const normalized = text.replace(/\s+/g, " ").trim();
+ return normalized.length > max ? `${normalized.slice(0, max - 1)}…` : normalized;
+}
+
+function formatElapsed(ms) {
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
+}
+
+function stopTimer() {
+ if (timerId !== null) {
+ window.clearInterval(timerId);
+ timerId = null;
+ }
+ recordingStartedAt = null;
+ recordingTimer.hidden = true;
+}
+
+function startTimer() {
+ stopTimer();
+ recordingStartedAt = Date.now();
+ recordingTimer.textContent = "00:00";
+ recordingTimer.hidden = false;
+ timerId = window.setInterval(() => {
+ if (recordingStartedAt === null) return;
+ recordingTimer.textContent = formatElapsed(Date.now() - recordingStartedAt);
+ }, 250);
+}
+
+function render(label, detail, kind, options = {}) {
+ const { showShortcut = true, showTimer = false } = options;
title.textContent = label;
message.textContent = detail;
- indicator.className = `state-indicator ${kind}`;
+ shell.className = `overlay-shell ${kind}`;
shortcutHint.hidden = !showShortcut;
+ if (!showTimer) recordingTimer.hidden = true;
+}
+
+async function positionNearLeftEdge() {
+ if (!overlayWindow || !tauriWindow?.currentMonitor || !tauriWindow?.LogicalPosition) return;
+ try {
+ const monitor = await tauriWindow.currentMonitor();
+ if (!monitor) return;
+
+ const scale = monitor.scaleFactor || 1;
+ const logicalX = monitor.position.x / scale + 22;
+ const logicalHeight = monitor.size.height / scale;
+ const logicalY = monitor.position.y / scale + Math.max(22, (logicalHeight - 88) / 2);
+ await overlayWindow.setPosition(
+ new tauriWindow.LogicalPosition(Math.round(logicalX), Math.round(logicalY)),
+ );
+ } catch {
+ // Positioning is cosmetic. Never let it interfere with dictation.
+ }
}
async function showOverlay() {
if (!overlayWindow) return;
try {
+ await positionNearLeftEdge();
await overlayWindow.show();
} catch {
// The overlay is advisory UI; a visibility failure must never affect dictation.
@@ -45,32 +103,51 @@ function applyLifecycle(payload) {
switch (payload.state) {
case "starting":
- render("Preparing", "Loading the selected local model…", "working");
+ stopTimer();
+ render("Preparing", "Loading the selected local model…", "working", { showShortcut: false });
void showOverlay();
break;
case "recording":
- render("Listening", "Press the shortcut again to stop", "recording");
+ startTimer();
+ render("Listening", "Speak naturally · press the shortcut again to stop", "recording", {
+ showShortcut: true,
+ showTimer: true,
+ });
+ recordingTimer.hidden = false;
void showOverlay();
break;
case "finishing":
- render("Transcribing", "Processing locally and preparing text submission…", "working", false);
+ stopTimer();
+ render("Transcribing", "Processing your speech locally…", "working", { showShortcut: false });
void showOverlay();
break;
- case "completed":
- render("Submitted", "Complete transcript accepted by the insertion backend", "success", false);
+ case "completed": {
+ stopTimer();
+ const preview = truncate(payload.text);
+ render("Sent ✓", preview || "Transcript submitted to the focused app", "success", {
+ showShortcut: false,
+ });
void showOverlay();
- void hideOverlay(generation, 1400);
+ void hideOverlay(generation, 1700);
break;
+ }
case "noSpeech":
- render("No speech", "Nothing was inserted", "idle", false);
+ stopTimer();
+ render("No speech", "Nothing was inserted", "idle", { showShortcut: false });
void showOverlay();
void hideOverlay(generation, 1500);
break;
- case "failed":
- render("Dictation failed", payload.message || "Open BLCVoice for diagnostics", "failed", false);
+ case "failed": {
+ stopTimer();
+ const recovered = truncate(payload.recoverableText);
+ const detail = recovered
+ ? `Text recovered: ${recovered}`
+ : payload.message || "Open BLCVoice for diagnostics";
+ render("Not sent", detail, "failed", { showShortcut: false });
void showOverlay();
- void hideOverlay(generation, 3200);
+ void hideOverlay(generation, recovered ? 4200 : 3200);
break;
+ }
default:
break;
}
@@ -88,6 +165,7 @@ async function bootstrap() {
}
window.addEventListener("beforeunload", () => {
+ stopTimer();
if (unlisten) {
unlisten();
unlisten = null;
From efca5b78493a51431a6cb12fce53e6d76730bab6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?BLC=20Core=20Studio=20=E2=80=94=20Developer=20Tools?=
<207100624+BLCCoreStudio@users.noreply.github.com>
Date: Sun, 6 Sep 2026 04:45:09 +0300
Subject: [PATCH 06/41] feat(ui): reorganize desktop app around focused
navigation
---
apps/desktop/ui/index.html | 316 +++++++++++++++++++------------------
1 file changed, 163 insertions(+), 153 deletions(-)
diff --git a/apps/desktop/ui/index.html b/apps/desktop/ui/index.html
index 057662e..cfa6ac7 100644
--- a/apps/desktop/ui/index.html
+++ b/apps/desktop/ui/index.html
@@ -7,180 +7,190 @@
BLCVoice
+
-
-
-
-