Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ pub fn run(cli_args: CliArgs) {
shortcut::change_autostart_setting,
shortcut::change_translate_to_english_setting,
shortcut::change_selected_language_setting,
shortcut::change_overlay_enabled_setting,
shortcut::change_overlay_position_setting,
shortcut::change_debug_mode_setting,
shortcut::change_word_correction_threshold_setting,
Expand Down
30 changes: 22 additions & 8 deletions src-tauri/src/overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,10 +349,12 @@ struct OverlayPayload<'a> {
fn show_overlay_state(app_handle: &AppHandle, state: &str) {
// Check if overlay should be shown based on position setting
let settings = settings::get_settings(app_handle);
if matches!(
settings.overlay_position,
OverlayPosition::None | OverlayPosition::Notch
) {
if !settings.overlay_enabled
|| matches!(
settings.overlay_position,
OverlayPosition::None | OverlayPosition::Notch
)
{
return;
}

Expand Down Expand Up @@ -427,7 +429,10 @@ fn show_overlay_state(app_handle: &AppHandle, state: &str) {
/// Shows the recording overlay window with fade-in animation.
pub fn show_recording_overlay(app_handle: &AppHandle) {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
if settings::get_settings(app_handle).overlay_position == OverlayPosition::Notch {
if {
let settings = settings::get_settings(app_handle);
settings.overlay_enabled && settings.overlay_position == OverlayPosition::Notch
} {
crate::notch::update_state(crate::notch::NotchState::Recording);
}
show_overlay_state(app_handle, "recording");
Expand All @@ -436,7 +441,10 @@ pub fn show_recording_overlay(app_handle: &AppHandle) {
/// Shows the transcribing overlay window.
pub fn show_transcribing_overlay(app_handle: &AppHandle) {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
if settings::get_settings(app_handle).overlay_position == OverlayPosition::Notch {
if {
let settings = settings::get_settings(app_handle);
settings.overlay_enabled && settings.overlay_position == OverlayPosition::Notch
} {
crate::notch::update_state(crate::notch::NotchState::Transcribing);
}
show_overlay_state(app_handle, "transcribing");
Expand Down Expand Up @@ -507,7 +515,10 @@ pub fn emit_levels(app_handle: &AppHandle, levels: &Vec<f32>) {

// Forward peak level to the native notch indicator (only in notch mode)
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
if settings::get_settings(app_handle).overlay_position == OverlayPosition::Notch {
if {
let settings = settings::get_settings(app_handle);
settings.overlay_enabled && settings.overlay_position == OverlayPosition::Notch
} {
let peak = levels.iter().copied().fold(0.0f32, f32::max);
crate::notch::update_audio_level(peak);
}
Expand Down Expand Up @@ -568,7 +579,10 @@ pub fn emit_streaming_text(app_handle: &AppHandle, text: &str) {

// Forward streaming text to the native notch indicator (only in notch mode)
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
if settings::get_settings(app_handle).overlay_position == OverlayPosition::Notch {
if {
let settings = settings::get_settings(app_handle);
settings.overlay_enabled && settings.overlay_position == OverlayPosition::Notch
} {
crate::notch::update_streaming_text(text);
}
}
30 changes: 28 additions & 2 deletions src-tauri/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@ pub struct AppSettings {
pub translate_to_english: bool,
#[serde(default = "default_selected_language")]
pub selected_language: String,
#[serde(default = "default_overlay_enabled")]
pub overlay_enabled: bool,
#[serde(default = "default_overlay_position")]
pub overlay_position: OverlayPosition,
#[serde(default = "default_debug_mode")]
Expand Down Expand Up @@ -465,6 +467,17 @@ fn default_selected_language() -> String {
"auto".to_string()
}

fn default_overlay_enabled() -> bool {
true
}

fn default_overlay_enabled_for_new_install() -> bool {
#[cfg(target_os = "linux")]
return false;
#[cfg(not(target_os = "linux"))]
return true;
}

fn default_overlay_position() -> OverlayPosition {
#[cfg(target_os = "linux")]
return OverlayPosition::None;
Expand Down Expand Up @@ -1118,7 +1131,11 @@ fn recover_settings_from_value(settings_value: JsonValue) -> AppSettings {
recover_field!(selected_output_device);
recover_field!(translate_to_english);
recover_field!(selected_language);
recover_field!(overlay_enabled);
recover_field!(overlay_position);
if !object.contains_key("overlay_enabled") {
settings.overlay_enabled = settings.overlay_position != OverlayPosition::None;
}
recover_field!(debug_mode);
recover_field!(log_level);
recover_field!(custom_words);
Expand Down Expand Up @@ -1258,10 +1275,18 @@ fn read_or_create_app_settings(app: &AppHandle, log_existing: bool) -> AppSettin
match serde_json::from_value::<AppSettings>(settings_value.clone()) {
Ok(mut settings) => {
let settings_updated = settings_value.as_object().is_some_and(|object| {
apply_custom_post_process_base_url(
let mut updated = apply_custom_post_process_base_url(
&mut settings,
configured_custom_post_process_base_url(object),
)
);

if !object.contains_key("overlay_enabled") {
settings.overlay_enabled =
settings.overlay_position != OverlayPosition::None;
updated = true;
}

updated
});
if log_existing {
debug!(
Expand Down Expand Up @@ -1322,6 +1347,7 @@ pub fn get_default_settings() -> AppSettings {
selected_output_device: None,
translate_to_english: false,
selected_language: "auto".to_string(),
overlay_enabled: default_overlay_enabled_for_new_install(),
overlay_position: default_overlay_position(),
debug_mode: false,
log_level: default_log_level(),
Expand Down
23 changes: 23 additions & 0 deletions src-tauri/src/shortcut/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,28 @@ pub fn change_selected_language_setting(app: AppHandle, language: String) -> Res
Ok(())
}

#[tauri::command]
#[specta::specta]
pub fn change_overlay_enabled_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
let mut settings = settings::get_settings(&app);
settings.overlay_enabled = enabled;

if enabled && settings.overlay_position == OverlayPosition::None {
settings.overlay_position = OverlayPosition::Bottom;
}

settings::write_settings(&app, settings.clone());

#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
if !enabled || settings.overlay_position != OverlayPosition::Notch {
crate::notch::update_state(crate::notch::NotchState::Hidden);
}

crate::utils::update_overlay_position(&app);

Ok(())
}

#[tauri::command]
#[specta::specta]
pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Result<(), String> {
Expand All @@ -628,6 +650,7 @@ pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Resu
}
};
settings.overlay_position = parsed;
settings.overlay_enabled = parsed != OverlayPosition::None;
settings::write_settings(&app, settings);

// Dismiss the notch indicator when switching away from notch mode
Expand Down
10 changes: 9 additions & 1 deletion src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ async changeSelectedLanguageSetting(language: string) : Promise<Result<null, str
else return { status: "error", error: e as any };
}
},
async changeOverlayEnabledSetting(enabled: boolean) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("change_overlay_enabled_setting", { enabled }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async changeOverlayPositionSetting(position: string) : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("change_overlay_position_setting", { position }) };
Expand Down Expand Up @@ -926,7 +934,7 @@ async isLaptop() : Promise<Result<boolean, string>> {
/** user-defined types **/

export type ActivationMode = "toggle" | "hold" | "hold_or_toggle"
export type AppSettings = { bindings?: Partial<{ [key in string]: ShortcutBinding }>; activation_mode?: ActivationMode; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; microphone_priority?: string[]; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: number; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; auto_submit?: boolean; auto_submit_key?: AutoSubmitKey; stt_provider_id?: string; stt_providers?: SttProvider[]; stt_api_keys?: Partial<{ [key in string]: string }>; stt_cloud_models?: Partial<{ [key in string]: string }>; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string; keyboard_implementation?: KeyboardImplementation; show_tray_icon?: boolean; paste_delay_ms?: number; typing_tool?: TypingTool; external_script_path: string | null; app_theme?: AppTheme; stt_verified_providers?: string[]; post_process_verified_providers?: string[]; post_process_input_prices?: Partial<{ [key in string]: number }>; post_process_output_prices?: Partial<{ [key in string]: number }>; stt_cloud_options?: Partial<{ [key in string]: string }>; stt_realtime_enabled?: Partial<{ [key in string]: boolean }>; stats_date_range?: StatsDateRange; dictionary_terms?: string[]; dictionary_context?: string }
export type AppSettings = { bindings?: Partial<{ [key in string]: ShortcutBinding }>; activation_mode?: ActivationMode; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; microphone_priority?: string[]; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_enabled?: boolean; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: number; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; auto_submit?: boolean; auto_submit_key?: AutoSubmitKey; stt_provider_id?: string; stt_providers?: SttProvider[]; stt_api_keys?: Partial<{ [key in string]: string }>; stt_cloud_models?: Partial<{ [key in string]: string }>; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string; keyboard_implementation?: KeyboardImplementation; show_tray_icon?: boolean; paste_delay_ms?: number; typing_tool?: TypingTool; external_script_path: string | null; app_theme?: AppTheme; stt_verified_providers?: string[]; post_process_verified_providers?: string[]; post_process_input_prices?: Partial<{ [key in string]: number }>; post_process_output_prices?: Partial<{ [key in string]: number }>; stt_cloud_options?: Partial<{ [key in string]: string }>; stt_realtime_enabled?: Partial<{ [key in string]: boolean }>; stats_date_range?: StatsDateRange; dictionary_terms?: string[]; dictionary_context?: string }
export type AppTheme = "dark" | "light" | "system"
export type AudioDevice = { index: string; name: string; is_default: boolean }
export type AutoSubmitKey = "enter" | "ctrl_enter" | "cmd_enter"
Expand Down
73 changes: 57 additions & 16 deletions src/components/settings/ShowOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { platform } from "@tauri-apps/plugin-os";
import { Dropdown } from "../ui/Dropdown";
import { SettingContainer } from "../ui/SettingContainer";
import { ToggleSwitch } from "../ui/ToggleSwitch";
import { useSettings } from "../../hooks/useSettings";
import type { OverlayPosition } from "@/bindings";
import { cn } from "@/lib/utils";

interface ShowOverlayProps {
descriptionMode?: "inline" | "tooltip";
Expand All @@ -18,7 +19,6 @@ export const ShowOverlay: React.FC<ShowOverlayProps> = React.memo(

const overlayOptions = useMemo(() => {
const opts = [
{ value: "none", label: t("settings.advanced.overlay.options.none") },
{
value: "bottom",
label: t("settings.advanced.overlay.options.bottom"),
Expand All @@ -36,23 +36,64 @@ export const ShowOverlay: React.FC<ShowOverlayProps> = React.memo(

const selectedPosition = (getSetting("overlay_position") ||
"bottom") as OverlayPosition;
const overlayEnabled = getSetting("overlay_enabled");
const isEnabled = overlayEnabled ?? selectedPosition !== "none";
const effectivePosition =
selectedPosition === "none" ? "bottom" : selectedPosition;

const isOverlayUpdating =
isUpdating("overlay_enabled") || isUpdating("overlay_position");

return (
<SettingContainer
title={t("settings.advanced.overlay.title")}
description={t("settings.advanced.overlay.description")}
descriptionMode={descriptionMode}
grouped={grouped}
>
<Dropdown
options={overlayOptions}
selectedValue={selectedPosition}
onSelect={(value) =>
updateSetting("overlay_position", value as OverlayPosition)
}
disabled={isUpdating("overlay_position")}
<>
<ToggleSwitch
label={t("settings.advanced.overlay.title")}
description={t("settings.advanced.overlay.description")}
checked={isEnabled}
onChange={async (checked) => {
if (checked && selectedPosition === "none") {
await updateSetting("overlay_position", "bottom");
}

await updateSetting("overlay_enabled", checked);
}}
disabled={isOverlayUpdating}
grouped={grouped}
descriptionMode={descriptionMode}
/>
</SettingContainer>
{isEnabled && (
<SettingContainer
title={t("settings.advanced.overlay.position_label")}
description=""
grouped={grouped}
descriptionMode="tooltip"
>
<div className="flex bg-glass-bg border border-glass-border rounded-lg p-0.5 gap-0.5">
{overlayOptions.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() =>
updateSetting(
"overlay_position",
opt.value as OverlayPosition,
)
}
disabled={isOverlayUpdating}
className={cn(
"px-3 py-1.5 text-xs rounded-md transition-all duration-200 cursor-pointer select-none",
effectivePosition === opt.value
? "bg-accent text-accent-foreground shadow-sm"
: "text-muted-foreground hover:bg-glass-highlight hover:text-foreground",
)}
>
{opt.label}
</button>
))}
</div>
</SettingContainer>
)}
</>
);
},
);
5 changes: 3 additions & 2 deletions src/i18n/locales/ar/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,9 @@
"description": ".عرض أيقونة Handless في شريط النظام"
},
"overlay": {
"title": "موقع التراكب",
"description": "عرض تراكب الملاحظات المرئية أثناء التسجيل والتفريغ. على نظام Linux يوصى بـ 'بلا'.",
"title": "تغذية راجعة للتراكب",
"description": "يظهر تراكبًا مرئيًا أثناء التسجيل والتفريغ للحصول على تعليقات فورية حول الحالة.",
"position_label": "الموقع",
"options": {
"none": "بلا",
"bottom": "أسفل",
Expand Down
5 changes: 3 additions & 2 deletions src/i18n/locales/cs/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,9 @@
"description": "Zobrazit ikonu Handless v systémové liště."
},
"overlay": {
"title": "Pozice překryvu",
"description": "Zobrazovat vizuální překryv během nahrávání a přepisu. Na Linuxu je doporučeno 'Žádné'.",
"title": "Zpětná vazba překryvu",
"description": "Během nahrávání a přepisu zobrazuje vizuální překryv pro okamžitou zpětnou vazbu o stavu.",
"position_label": "Pozice",
"options": {
"none": "Žádné",
"bottom": "Dole",
Expand Down
5 changes: 3 additions & 2 deletions src/i18n/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,9 @@
"description": "Das Handless-Symbol in der Taskleiste anzeigen."
},
"overlay": {
"title": "Overlay-Position",
"description": "Visuelles Feedback-Overlay während Aufnahme und Transkription anzeigen. Unter Linux wird 'Keine' empfohlen.",
"title": "Overlay-Feedback",
"description": "Zeigt während der Aufnahme und Transkription ein visuelles Overlay für sofortiges Status-Feedback an.",
"position_label": "Position",
"options": {
"none": "Keine",
"bottom": "Unten",
Expand Down
5 changes: 3 additions & 2 deletions src/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,9 @@
"description": "Display the Handless icon in the system tray."
},
"overlay": {
"title": "Overlay Position",
"description": "Display visual feedback overlay during recording and transcription. On Linux 'None' is recommended.",
"title": "Overlay Feedback",
"description": "Display a visual overlay during recording and transcription for immediate status feedback.",
"position_label": "Position",
"options": {
"none": "None",
"bottom": "Bottom",
Expand Down
5 changes: 3 additions & 2 deletions src/i18n/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,9 @@
"description": "Mostrar el icono de Handless en la bandeja del sistema."
},
"overlay": {
"title": "Posición de Superposición",
"description": "Mostrar superposición de retroalimentación visual durante la grabación y transcripción. En Linux se recomienda 'Ninguna'.",
"title": "Retroalimentación de Superposición",
"description": "Muestra una superposición visual durante la grabación y la transcripción para obtener comentarios de estado inmediatos.",
"position_label": "Posición",
"options": {
"none": "Ninguna",
"bottom": "Abajo",
Expand Down
5 changes: 3 additions & 2 deletions src/i18n/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,9 @@
"description": "Afficher l'icône de Handless dans la barre système."
},
"overlay": {
"title": "Position de la fenêtre d'enregistrement",
"description": "Afficher un retour visuel pendant l'enregistrement et la transcription. Sur Linux, 'Aucune' est recommandé.",
"title": "Retour visuel de superposition",
"description": "Affiche une superposition visuelle pendant l’enregistrement et la transcription pour un retour d’état immédiat.",
"position_label": "Position",
"options": {
"none": "Aucune",
"bottom": "Bas",
Expand Down
Loading