Merge pull request #61 from cjpais/debug-mode-toggle

add a very simple debug mode toggle
This commit is contained in:
CJ Pais 2025-08-01 18:23:19 -07:00 committed by GitHub
commit cbd55b728a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 336 additions and 4 deletions

View file

@ -2,9 +2,19 @@ pub mod audio;
pub mod models;
use crate::utils::cancel_current_operation;
use tauri::AppHandle;
use tauri::{AppHandle, Manager};
#[tauri::command]
pub fn cancel_operation(app: AppHandle) {
cancel_current_operation(&app);
}
#[tauri::command]
pub fn get_app_dir_path(app: AppHandle) -> Result<String, String> {
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|e| format!("Failed to get app data directory: {}", e))?;
Ok(app_data_dir.to_string_lossy().to_string())
}

View file

@ -158,8 +158,10 @@ pub fn run() {
shortcut::change_translate_to_english_setting,
shortcut::change_selected_language_setting,
shortcut::change_show_overlay_setting,
shortcut::change_debug_mode_setting,
trigger_update_check,
commands::cancel_operation,
commands::get_app_dir_path,
commands::models::get_available_models,
commands::models::get_model_info,
commands::models::download_model,

View file

@ -32,6 +32,8 @@ pub struct AppSettings {
pub selected_language: String,
#[serde(default = "default_show_overlay")]
pub show_overlay: bool,
#[serde(default = "default_debug_mode")]
pub debug_mode: bool,
}
fn default_model() -> String {
@ -61,6 +63,11 @@ fn default_show_overlay() -> bool {
true
}
fn default_debug_mode() -> bool {
// Default to false - debug mode should be opt-in
false
}
pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
pub fn get_default_settings() -> AppSettings {
@ -85,6 +92,7 @@ pub fn get_default_settings() -> AppSettings {
current_binding: default_shortcut.to_string(),
},
);
// bindings.insert(
// "test".to_string(),
// ShortcutBinding {
@ -107,6 +115,7 @@ pub fn get_default_settings() -> AppSettings {
translate_to_english: false,
selected_language: "auto".to_string(),
show_overlay: true,
debug_mode: false,
}
}

View file

@ -135,6 +135,14 @@ pub fn change_show_overlay_setting(app: AppHandle, enabled: bool) -> Result<(),
Ok(())
}
#[tauri::command]
pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
let mut settings = settings::get_settings(&app);
settings.debug_mode = enabled;
settings::write_settings(&app, settings);
Ok(())
}
fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> {
// Parse shortcut and return error if it fails
let shortcut = match binding.current_binding.parse::<Shortcut>() {

View file

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import { MicrophoneSelector } from "./MicrophoneSelector";
import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone";
import { PushToTalk } from "./PushToTalk";
@ -9,8 +9,37 @@ import { HandyShortcut } from "./HandyShortcut";
import { TranslateToEnglish } from "./TranslateToEnglish";
import { LanguageSelector } from "./LanguageSelector";
import { SettingsGroup } from "../ui/SettingsGroup";
import { DebugSettings } from "./debug";
import { useSettings } from "../../hooks/useSettings";
export const Settings: React.FC = () => {
const { settings, updateSetting } = useSettings();
// Handle keyboard shortcuts for debug mode toggle
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Check for Ctrl+Shift+D (Windows/Linux) or Cmd+Shift+D (macOS)
const isDebugShortcut =
event.shiftKey &&
event.key.toLowerCase() === "d" &&
(event.ctrlKey || event.metaKey);
if (isDebugShortcut) {
event.preventDefault();
const currentDebugMode = settings?.debug_mode ?? false;
updateSetting("debug_mode", !currentDebugMode);
}
};
// Add event listener when component mounts
document.addEventListener("keydown", handleKeyDown);
// Cleanup event listener when component unmounts
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [settings?.debug_mode, updateSetting]);
return (
<div className="max-w-3xl w-full mx-auto space-y-6">
<SettingsGroup>
@ -27,6 +56,12 @@ export const Settings: React.FC = () => {
<TranslateToEnglish descriptionMode="tooltip" grouped={true} />
<AlwaysOnMicrophone descriptionMode="tooltip" grouped={true} />
</SettingsGroup>
{settings?.debug_mode && (
<SettingsGroup title="Debug">
<DebugSettings descriptionMode="tooltip" grouped={true} />
</SettingsGroup>
)}
</div>
);
};

View file

@ -0,0 +1,69 @@
import React, { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import { TextDisplay, SettingsGroup } from "../../ui";
interface DebugPathsProps {
descriptionMode?: "tooltip" | "inline";
grouped?: boolean;
}
export const DebugPaths: React.FC<DebugPathsProps> = ({
descriptionMode = "inline",
grouped = false,
}) => {
const [appDirPath, setAppDirPath] = useState<string>("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadPaths = async () => {
try {
const result = await invoke<string>("get_app_dir_path");
setAppDirPath(result);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load paths");
} finally {
setLoading(false);
}
};
loadPaths();
}, []);
const handleCopy = (value: string) => {
// Could add a toast notification here if desired
console.log("Copied to clipboard:", value);
};
if (loading) {
return (
<div className="space-y-4">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-1/3 mb-2"></div>
<div className="h-8 bg-gray-100 rounded"></div>
</div>
</div>
);
}
if (error) {
return (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600 text-sm">Error loading paths: {error}</p>
</div>
);
}
return (
<TextDisplay
label="App Data Directory"
description="Main directory where application data, settings, and models are stored"
value={appDirPath}
descriptionMode={descriptionMode}
grouped={true}
copyable={true}
monospace={true}
onCopy={handleCopy}
/>
);
};

View file

@ -0,0 +1,30 @@
import React from "react";
import { DebugPaths } from "./DebugPaths";
interface DebugSettingsProps {
descriptionMode?: "tooltip" | "inline";
grouped?: boolean;
}
export const DebugSettings: React.FC<DebugSettingsProps> = ({
descriptionMode = "inline",
grouped = false,
}) => {
return (
<div className="space-y-6">
{descriptionMode === "inline" && !grouped && (
<div className="mb-6">
<div className="flex items-center space-x-2 mb-2">
<h2 className="text-xl font-semibold text-gray-900">Debug</h2>
</div>
<p className="text-sm text-gray-600">
Debug information and developer tools. These settings are only
visible when debug mode is enabled.
</p>
</div>
)}
<DebugPaths descriptionMode={descriptionMode} grouped={true} />
</div>
);
};

View file

@ -0,0 +1,2 @@
export { DebugSettings } from "./DebugSettings";
export { DebugPaths } from "./DebugPaths";

View file

@ -6,6 +6,7 @@ interface SettingContainerProps {
children: React.ReactNode;
descriptionMode?: "inline" | "tooltip";
grouped?: boolean;
layout?: "horizontal" | "stacked";
}
export const SettingContainer: React.FC<SettingContainerProps> = ({
@ -14,6 +15,7 @@ export const SettingContainer: React.FC<SettingContainerProps> = ({
children,
descriptionMode = "tooltip",
grouped = false,
layout = "horizontal",
}) => {
const [showTooltip, setShowTooltip] = useState(false);
const tooltipRef = useRef<HTMLDivElement>(null);
@ -41,12 +43,78 @@ export const SettingContainer: React.FC<SettingContainerProps> = ({
};
const containerClasses = grouped
? "px-4 p-2"
: "px-4 p-2 rounded-lg border border-mid-gray/20";
if (layout === "stacked") {
if (descriptionMode === "tooltip") {
return (
<div className={containerClasses}>
<div className="flex items-center gap-2 mb-2">
<h3 className="text-sm font-medium">{title}</h3>
<div
ref={tooltipRef}
className="relative"
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
onClick={toggleTooltip}
>
<svg
className="w-4 h-4 text-mid-gray cursor-help hover:text-logo-primary transition-colors duration-200 select-none"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-label="More information"
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggleTooltip();
}
}}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{showTooltip && (
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-background border border-mid-gray/80 rounded-lg shadow-lg z-50 max-w-xs min-w-[200px] whitespace-normal animate-in fade-in-0 zoom-in-95 duration-200">
<p className="text-sm text-center leading-relaxed">
{description}
</p>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-[6px] border-r-[6px] border-t-[6px] border-l-transparent border-r-transparent border-t-mid-gray/80"></div>
</div>
)}
</div>
</div>
<div className="w-full">{children}</div>
</div>
);
}
return (
<div className={containerClasses}>
<div className="mb-2">
<h3 className="text-sm font-medium">{title}</h3>
<p className="text-sm">{description}</p>
</div>
<div className="w-full">{children}</div>
</div>
);
}
// Horizontal layout (default)
const horizontalContainerClasses = grouped
? "flex items-center justify-between px-4 p-2"
: "flex items-center justify-between px-4 p-2 rounded-lg border border-mid-gray/20";
if (descriptionMode === "tooltip") {
return (
<div className={containerClasses}>
<div className={horizontalContainerClasses}>
<div className="max-w-2/3">
<div className="flex items-center gap-2">
<h3 className="text-sm font-medium ">{title}</h3>
@ -96,7 +164,7 @@ export const SettingContainer: React.FC<SettingContainerProps> = ({
}
return (
<div className={containerClasses}>
<div className={horizontalContainerClasses}>
<div className="max-w-2/3">
<h3 className="text-sm font-medium ">{title}</h3>
<p className="text-sm">{description}</p>

View file

@ -0,0 +1,93 @@
import React, { useState } from "react";
import { SettingContainer } from "./SettingContainer";
interface TextDisplayProps {
label: string;
description: string;
value: string;
descriptionMode?: "inline" | "tooltip";
grouped?: boolean;
placeholder?: string;
copyable?: boolean;
monospace?: boolean;
onCopy?: (value: string) => void;
}
export const TextDisplay: React.FC<TextDisplayProps> = ({
label,
description,
value,
descriptionMode = "tooltip",
grouped = false,
placeholder = "Not available",
copyable = false,
monospace = false,
onCopy,
}) => {
const [showCopied, setShowCopied] = useState(false);
const handleCopy = async () => {
if (!value || !copyable) return;
try {
await navigator.clipboard.writeText(value);
setShowCopied(true);
setTimeout(() => setShowCopied(false), 1500);
if (onCopy) {
onCopy(value);
}
} catch (err) {
console.error("Failed to copy to clipboard:", err);
}
};
const displayValue = value || placeholder;
const textClasses = monospace ? "font-mono break-all" : "break-words";
return (
<SettingContainer
title={label}
description={description}
descriptionMode={descriptionMode}
grouped={grouped}
layout="stacked"
>
<div className="flex items-center space-x-2">
<div className="flex-1 min-w-0">
<div
className={`px-2 min-h-8 flex items-center bg-mid-gray/10 border border-mid-gray/80 rounded text-xs ${textClasses} ${!value ? "opacity-60" : ""}`}
>
{displayValue}
</div>
</div>
{copyable && value && (
<button
onClick={handleCopy}
className="flex items-center justify-center px-2 py-1 w-12 min-h-8 text-xs font-semibold bg-mid-gray/10 hover:bg-logo-primary/10 border border-mid-gray/80 hover:border-logo-primary hover:text-logo-primary rounded transition-all duration-150 flex-shrink-0 cursor-pointer"
title="Copy to clipboard"
>
{showCopied ? (
<div className="flex items-center space-x-1">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
) : (
"Copy"
)}
</button>
)}
</div>
</SettingContainer>
);
};

View file

@ -2,3 +2,4 @@ export { Dropdown } from "./Dropdown";
export { ToggleSwitch } from "./ToggleSwitch";
export { SettingContainer } from "./SettingContainer";
export { SettingsGroup } from "./SettingsGroup";
export { TextDisplay } from "./TextDisplay";

View file

@ -202,6 +202,9 @@ export const useSettings = (): UseSettingsReturn => {
case "show_overlay":
await invoke("change_show_overlay_setting", { enabled: value });
break;
case "debug_mode":
await invoke("change_debug_mode_setting", { enabled: value });
break;
case "bindings":
// Handle bindings separately - they use their own invoke methods
break;
@ -247,6 +250,7 @@ export const useSettings = (): UseSettingsReturn => {
translate_to_english: false,
selected_language: "auto",
show_overlay: true,
debug_mode: false,
};
const defaultValue = defaults[key];

View file

@ -30,6 +30,7 @@ export const SettingsSchema = z.object({
translate_to_english: z.boolean(),
selected_language: z.string(),
show_overlay: z.boolean(),
debug_mode: z.boolean(),
});
export const BindingResponseSchema = z.object({