From 363feb8ce6ce49d8b3eb2144b598c7485e143b88 Mon Sep 17 00:00:00 2001 From: Rajin Khan Date: Sat, 19 Sep 2026 00:30:44 +0600 Subject: [PATCH 1/3] feat: offer a monochrome recording overlay --- src-tauri/src/actions.rs | 5 ++++- src-tauri/src/settings.rs | 7 ++++--- src-tauri/src/shortcut/mod.rs | 1 + src/bindings.ts | 6 +++--- src/components/settings/ShowOverlay.tsx | 4 ++++ src/i18n/locales/en/translation.json | 3 ++- src/overlay/RecordingOverlay.css | 4 ++++ src/overlay/RecordingOverlay.tsx | 4 +++- 8 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/actions.rs b/src-tauri/src/actions.rs index 8e752e50b2..17d87cb954 100644 --- a/src-tauri/src/actions.rs +++ b/src-tauri/src/actions.rs @@ -524,7 +524,9 @@ impl ShortcutAction for TranscribeAction { let overlay_started = Instant::now(); match settings.overlay_style { OverlayStyle::Live if model_supports_streaming => utils::show_streaming_overlay(app), - OverlayStyle::Live | OverlayStyle::Minimal => show_recording_overlay(app), + OverlayStyle::Live | OverlayStyle::Minimal | OverlayStyle::Monochrome => { + show_recording_overlay(app) + } OverlayStyle::None => {} // show_overlay_state no-ops on None anyway } // Everything above runs before capture can begin, so each span here is @@ -1034,6 +1036,7 @@ mod tests { assert!(should_use_streaming_overlay(OverlayStyle::Live, true)); assert!(!should_use_streaming_overlay(OverlayStyle::Live, false)); assert!(!should_use_streaming_overlay(OverlayStyle::Minimal, true)); + assert!(!should_use_streaming_overlay(OverlayStyle::Monochrome, true)); assert!(!should_use_streaming_overlay(OverlayStyle::None, true)); } } diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 44363d4905..f7721a86a6 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -119,8 +119,8 @@ pub enum OverlayPosition { Bottom, } -/// Which recording overlay to display. `Minimal` and `Live` share one base -/// (the pill); `Live` grows into the panel that shows live transcription text. +/// Which recording overlay to display. `Minimal`, `Monochrome`, and `Live` +/// share one pill; `Live` grows into the live transcription panel. /// `None` hides the overlay entirely. Decoupled from whether the model runs in /// streaming mode (that is driven purely by model capability). #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)] @@ -128,6 +128,7 @@ pub enum OverlayPosition { pub enum OverlayStyle { None, Minimal, + Monochrome, Live, } @@ -509,7 +510,7 @@ pub struct AppSettings { /// Experimental detector implementation. Silero remains the stable default. #[serde(default)] pub vad_backend: VadBackend, - /// Which recording overlay to show: None / Minimal / Live. Streaming mode is + /// Which recording overlay to show: None / Minimal / Monochrome / Live. Streaming mode is /// not gated on this — that follows model capability. Migrated from the old /// `overlay_position` (position `none` → style `None`). #[serde(default = "default_overlay_style")] diff --git a/src-tauri/src/shortcut/mod.rs b/src-tauri/src/shortcut/mod.rs index 8a603f4b74..94bcd4efb4 100644 --- a/src-tauri/src/shortcut/mod.rs +++ b/src-tauri/src/shortcut/mod.rs @@ -677,6 +677,7 @@ pub fn change_overlay_style_setting(app: AppHandle, style: String) -> Result<(), let parsed = match style.as_str() { "none" => OverlayStyle::None, "minimal" => OverlayStyle::Minimal, + "monochrome" => OverlayStyle::Monochrome, "live" => OverlayStyle::Live, other => { warn!("Invalid overlay style '{}', defaulting to minimal", other); diff --git a/src/bindings.ts b/src/bindings.ts index c6d3953515..68b4cd9bfa 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -1067,12 +1067,12 @@ export type ModelUnloadTimeout = "never" | "immediately" | "min_2" | "min_5" | " export type OrtAcceleratorSetting = "auto" | "cpu" | "cuda" | "directml" | "rocm" export type OverlayPosition = "top" | "bottom" /** - * Which recording overlay to display. `Minimal` and `Live` share one base - * (the pill); `Live` grows into the panel that shows live transcription text. + * Which recording overlay to display. `Minimal`, `Monochrome`, and `Live` + * share one pill; `Live` grows into the live transcription panel. * `None` hides the overlay entirely. Decoupled from whether the model runs in * streaming mode (that is driven purely by model capability). */ -export type OverlayStyle = "none" | "minimal" | "live" +export type OverlayStyle = "none" | "minimal" | "monochrome" | "live" export type PaginatedHistory = { entries: HistoryEntry[]; has_more: boolean } export type PasteMethod = "ctrl_v" | "direct" | "none" | "shift_insert" | "ctrl_shift_v" | "external_script" export type PermissionAccess = "allowed" | "denied" | "unknown" diff --git a/src/components/settings/ShowOverlay.tsx b/src/components/settings/ShowOverlay.tsx index d5f2448813..2b08091ae4 100644 --- a/src/components/settings/ShowOverlay.tsx +++ b/src/components/settings/ShowOverlay.tsx @@ -24,6 +24,10 @@ export const ShowOverlay: React.FC = React.memo( value: "minimal", label: t("settings.advanced.overlay.style.options.minimal"), }, + { + value: "monochrome", + label: t("settings.advanced.overlay.style.options.monochrome"), + }, { value: "live", label: t("settings.advanced.overlay.style.options.live"), diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index a2d24f009e..7592f7353d 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -302,10 +302,11 @@ "overlay": { "style": { "title": "Overlay", - "description": "Choose the recording overlay: None hides it, Minimal shows a compact pill, Live shows transcription in real time as you speak (streaming-capable models only — look for the Streaming badge in the model picker). On Linux 'None' is recommended.", + "description": "Choose the recording overlay: None hides it, Minimal shows a compact pill, Monochrome shows the same pill in gray and white, and Live shows transcription in real time as you speak (streaming-capable models only — look for the Streaming badge in the model picker). On Linux 'None' is recommended.", "options": { "none": "None", "minimal": "Minimal", + "monochrome": "Monochrome", "live": "Live" } }, diff --git a/src/overlay/RecordingOverlay.css b/src/overlay/RecordingOverlay.css index eb40068773..9f48c9f1a9 100644 --- a/src/overlay/RecordingOverlay.css +++ b/src/overlay/RecordingOverlay.css @@ -111,6 +111,10 @@ align-items: flex-end; /* bottom placement: flush to the bottom edge */ font-family: var(--s-font); } +.ov-stage.monochrome { + --s-accent: color-mix(in srgb, var(--color-text) 72%, var(--color-background)); + --s-accent-soft: color-mix(in srgb, var(--color-text) 18%, transparent); +} .ov-stage.top { align-items: flex-start; /* top placement: flush to the top edge */ } diff --git a/src/overlay/RecordingOverlay.tsx b/src/overlay/RecordingOverlay.tsx index dbab41e1a5..de033d7abd 100644 --- a/src/overlay/RecordingOverlay.tsx +++ b/src/overlay/RecordingOverlay.tsx @@ -40,6 +40,7 @@ const RecordingOverlay: React.FC = () => { // Overlay placement (top vs bottom of the screen). The Live panel grows downward // from a top overlay (oldest line under the pill) and upward from a bottom one. const [position, setPosition] = useState<"top" | "bottom">("bottom"); + const [monochrome, setMonochrome] = useState(false); // True once live text overflows the cap. A top overlay fades its top edge only // while overflowing, so the resting first line stays crisp flush under the pill. const [overflowing, setOverflowing] = useState(false); @@ -75,6 +76,7 @@ const RecordingOverlay: React.FC = () => { setPosition( settings.data.overlay_position === "top" ? "top" : "bottom", ); + setMonochrome(settings.data.overlay_style === "monochrome"); } } catch { // Keep the previous/default placement if settings can't be read. @@ -291,7 +293,7 @@ const RecordingOverlay: React.FC = () => { return (
Date: Sat, 19 Sep 2026 00:56:00 +0600 Subject: [PATCH 2/3] fix: update monochrome overlay when style changes --- src-tauri/src/shortcut/mod.rs | 1 + src/overlay/RecordingOverlay.tsx | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src-tauri/src/shortcut/mod.rs b/src-tauri/src/shortcut/mod.rs index 94bcd4efb4..052363ecc8 100644 --- a/src-tauri/src/shortcut/mod.rs +++ b/src-tauri/src/shortcut/mod.rs @@ -686,6 +686,7 @@ pub fn change_overlay_style_setting(app: AppHandle, style: String) -> Result<(), }; settings.overlay_style = parsed; settings::write_settings(&app, settings); + let _ = app.emit_to("recording_overlay", "overlay-style-changed", parsed); // Keep the cached overlay-enabled flag in sync so emit_levels stops (or // resumes) emitting on the next audio callback. diff --git a/src/overlay/RecordingOverlay.tsx b/src/overlay/RecordingOverlay.tsx index de033d7abd..1d66b475a1 100644 --- a/src/overlay/RecordingOverlay.tsx +++ b/src/overlay/RecordingOverlay.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import "./RecordingOverlay.css"; import { commands, events } from "@/bindings"; import type { + OverlayStyle, StreamPhase, StreamPhaseEvent, StreamTextEvent, @@ -96,6 +97,11 @@ const RecordingOverlay: React.FC = () => { setCaptureReady(false); }); + const unlistenStyle = await listen( + "overlay-style-changed", + (event) => setMonochrome(event.payload === "monochrome"), + ); + const unlistenReady = await listen("recording-ready", () => { setElapsed(0); setCaptureReady(true); @@ -126,6 +132,7 @@ const RecordingOverlay: React.FC = () => { return () => { unlistenShow(); unlistenHide(); + unlistenStyle(); unlistenReady(); unlistenLevel(); unlistenStream(); From fac512d8e0d9b59230c3b347769651fa138afe97 Mon Sep 17 00:00:00 2001 From: Rajin Khan Date: Sat, 19 Sep 2026 00:59:59 +0600 Subject: [PATCH 3/3] feat: separate monochrome overlay color from style --- src-tauri/src/actions.rs | 3 +-- src-tauri/src/lib.rs | 1 + src-tauri/src/settings.rs | 8 +++++--- src-tauri/src/shortcut/mod.rs | 11 ++++++++++- src/bindings.ts | 14 +++++++++++--- src/components/settings/ShowOverlay.tsx | 17 +++++++++++++---- src/i18n/locales/en/translation.json | 7 +++++-- src/overlay/RecordingOverlay.tsx | 13 ++++++------- src/stores/settingsStore.ts | 2 ++ 9 files changed, 54 insertions(+), 22 deletions(-) diff --git a/src-tauri/src/actions.rs b/src-tauri/src/actions.rs index 17d87cb954..34b44bda34 100644 --- a/src-tauri/src/actions.rs +++ b/src-tauri/src/actions.rs @@ -524,7 +524,7 @@ impl ShortcutAction for TranscribeAction { let overlay_started = Instant::now(); match settings.overlay_style { OverlayStyle::Live if model_supports_streaming => utils::show_streaming_overlay(app), - OverlayStyle::Live | OverlayStyle::Minimal | OverlayStyle::Monochrome => { + OverlayStyle::Live | OverlayStyle::Minimal => { show_recording_overlay(app) } OverlayStyle::None => {} // show_overlay_state no-ops on None anyway @@ -1036,7 +1036,6 @@ mod tests { assert!(should_use_streaming_overlay(OverlayStyle::Live, true)); assert!(!should_use_streaming_overlay(OverlayStyle::Live, false)); assert!(!should_use_streaming_overlay(OverlayStyle::Minimal, true)); - assert!(!should_use_streaming_overlay(OverlayStyle::Monochrome, true)); assert!(!should_use_streaming_overlay(OverlayStyle::None, true)); } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index facc9786a0..e2c466a00e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -662,6 +662,7 @@ pub fn run(cli_args: CliArgs) { shortcut::change_selected_language_setting, shortcut::change_overlay_position_setting, shortcut::change_overlay_style_setting, + shortcut::change_monochrome_overlay_setting, shortcut::change_debug_mode_setting, shortcut::change_word_correction_threshold_setting, shortcut::change_extra_recording_buffer_setting, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index f7721a86a6..69c741348d 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -119,7 +119,7 @@ pub enum OverlayPosition { Bottom, } -/// Which recording overlay to display. `Minimal`, `Monochrome`, and `Live` +/// Which recording overlay to display. `Minimal` and `Live` /// share one pill; `Live` grows into the live transcription panel. /// `None` hides the overlay entirely. Decoupled from whether the model runs in /// streaming mode (that is driven purely by model capability). @@ -128,7 +128,6 @@ pub enum OverlayPosition { pub enum OverlayStyle { None, Minimal, - Monochrome, Live, } @@ -510,11 +509,13 @@ pub struct AppSettings { /// Experimental detector implementation. Silero remains the stable default. #[serde(default)] pub vad_backend: VadBackend, - /// Which recording overlay to show: None / Minimal / Monochrome / Live. Streaming mode is + /// Which recording overlay to show: None / Minimal / Live. Streaming mode is /// not gated on this — that follows model capability. Migrated from the old /// `overlay_position` (position `none` → style `None`). #[serde(default = "default_overlay_style")] pub overlay_style: OverlayStyle, + #[serde(default)] + pub monochrome_overlay: bool, } fn default_model() -> String { @@ -971,6 +972,7 @@ pub fn get_default_settings() -> AppSettings { vad_enabled: default_vad_enabled(), vad_backend: VadBackend::default(), overlay_style: default_overlay_style(), + monochrome_overlay: false, } } diff --git a/src-tauri/src/shortcut/mod.rs b/src-tauri/src/shortcut/mod.rs index 052363ecc8..00108e0303 100644 --- a/src-tauri/src/shortcut/mod.rs +++ b/src-tauri/src/shortcut/mod.rs @@ -677,7 +677,6 @@ pub fn change_overlay_style_setting(app: AppHandle, style: String) -> Result<(), let parsed = match style.as_str() { "none" => OverlayStyle::None, "minimal" => OverlayStyle::Minimal, - "monochrome" => OverlayStyle::Monochrome, "live" => OverlayStyle::Live, other => { warn!("Invalid overlay style '{}', defaulting to minimal", other); @@ -698,6 +697,16 @@ pub fn change_overlay_style_setting(app: AppHandle, style: String) -> Result<(), Ok(()) } +#[tauri::command] +#[specta::specta] +pub fn change_monochrome_overlay_setting(app: AppHandle, enabled: bool) -> Result<(), String> { + let mut settings = settings::get_settings(&app); + settings.monochrome_overlay = enabled; + settings::write_settings(&app, settings); + let _ = app.emit_to("recording_overlay", "overlay-monochrome-changed", enabled); + Ok(()) +} + #[tauri::command] #[specta::specta] pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), String> { diff --git a/src/bindings.ts b/src/bindings.ts index 68b4cd9bfa..5a359072ec 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -117,6 +117,14 @@ async changeOverlayStyleSetting(style: string) : Promise> { else return { status: "error", error: e as any }; } }, +async changeMonochromeOverlaySetting(enabled: boolean) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("change_monochrome_overlay_setting", { enabled }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async changeDebugModeSetting(enabled: boolean) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("change_debug_mode_setting", { enabled }) }; @@ -1004,7 +1012,7 @@ vad_backend?: VadBackend; * not gated on this — that follows model capability. Migrated from the old * `overlay_position` (position `none` → style `None`). */ -overlay_style?: OverlayStyle } +overlay_style?: OverlayStyle; monochrome_overlay?: boolean } export type AudioDevice = { index: string; name: string; is_default: boolean } export type AutoSubmitKey = "enter" | "ctrl_enter" | "cmd_enter" export type AvailableAccelerators = { transcribe: string[]; ort: string[]; gpu_devices: GpuDeviceOption[] } @@ -1067,12 +1075,12 @@ export type ModelUnloadTimeout = "never" | "immediately" | "min_2" | "min_5" | " export type OrtAcceleratorSetting = "auto" | "cpu" | "cuda" | "directml" | "rocm" export type OverlayPosition = "top" | "bottom" /** - * Which recording overlay to display. `Minimal`, `Monochrome`, and `Live` + * Which recording overlay to display. `Minimal` and `Live` * share one pill; `Live` grows into the live transcription panel. * `None` hides the overlay entirely. Decoupled from whether the model runs in * streaming mode (that is driven purely by model capability). */ -export type OverlayStyle = "none" | "minimal" | "monochrome" | "live" +export type OverlayStyle = "none" | "minimal" | "live" export type PaginatedHistory = { entries: HistoryEntry[]; has_more: boolean } export type PasteMethod = "ctrl_v" | "direct" | "none" | "shift_insert" | "ctrl_shift_v" | "external_script" export type PermissionAccess = "allowed" | "denied" | "unknown" diff --git a/src/components/settings/ShowOverlay.tsx b/src/components/settings/ShowOverlay.tsx index 2b08091ae4..564dbef735 100644 --- a/src/components/settings/ShowOverlay.tsx +++ b/src/components/settings/ShowOverlay.tsx @@ -2,6 +2,7 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; +import { ToggleSwitch } from "../ui/ToggleSwitch"; import { useSettings } from "../../hooks/useSettings"; import type { OverlayPosition, OverlayStyle } from "@/bindings"; @@ -24,10 +25,6 @@ export const ShowOverlay: React.FC = React.memo( value: "minimal", label: t("settings.advanced.overlay.style.options.minimal"), }, - { - value: "monochrome", - label: t("settings.advanced.overlay.style.options.monochrome"), - }, { value: "live", label: t("settings.advanced.overlay.style.options.live"), @@ -70,6 +67,18 @@ export const ShowOverlay: React.FC = React.memo( /> + {selectedStyle !== "none" && ( + updateSetting("monochrome_overlay", enabled)} + isUpdating={isUpdating("monochrome_overlay")} + label={t("settings.advanced.overlay.monochrome.label")} + description={t("settings.advanced.overlay.monochrome.description")} + descriptionMode={descriptionMode} + grouped={grouped} + /> + )} + {selectedStyle !== "none" && ( { setPosition( settings.data.overlay_position === "top" ? "top" : "bottom", ); - setMonochrome(settings.data.overlay_style === "monochrome"); + setMonochrome(settings.data.monochrome_overlay ?? false); } } catch { // Keep the previous/default placement if settings can't be read. @@ -97,9 +96,9 @@ const RecordingOverlay: React.FC = () => { setCaptureReady(false); }); - const unlistenStyle = await listen( - "overlay-style-changed", - (event) => setMonochrome(event.payload === "monochrome"), + const unlistenMonochrome = await listen( + "overlay-monochrome-changed", + (event) => setMonochrome(event.payload), ); const unlistenReady = await listen("recording-ready", () => { @@ -132,7 +131,7 @@ const RecordingOverlay: React.FC = () => { return () => { unlistenShow(); unlistenHide(); - unlistenStyle(); + unlistenMonochrome(); unlistenReady(); unlistenLevel(); unlistenStream(); @@ -249,7 +248,7 @@ const RecordingOverlay: React.FC = () => { const collapsed = working && !hasText; return ( -
+
commands.changeLazyStreamCloseSetting(value as boolean), overlay_style: (value) => commands.changeOverlayStyleSetting(value as string), + monochrome_overlay: (value) => + commands.changeMonochromeOverlaySetting(value as boolean), vad_enabled: (value) => commands.changeVadEnabledSetting(value as boolean), vad_backend: async (value) => { const result = await commands.changeVadBackendSetting(value as VadBackend);