From 89af7f984174c24767dad8715597c26c9fa06f5f Mon Sep 17 00:00:00 2001 From: jahruz67 Date: Tue, 19 May 2026 20:17:10 -0700 Subject: [PATCH 1/2] Add Linux press-trigger daemon hotkey flow --- app.go | 70 ++++++++++++---------- frontend/dist/assets/index.fbf781c1.js | 2 +- frontend/dist/index.html | 13 ++-- linux_press_daemon.go | 83 ++++++++++++++++++++++++++ linux_press_daemon_stub.go | 5 ++ 5 files changed, 135 insertions(+), 38 deletions(-) create mode 100644 linux_press_daemon.go create mode 100644 linux_press_daemon_stub.go diff --git a/app.go b/app.go index 1f9adb7..6ee1378 100644 --- a/app.go +++ b/app.go @@ -6,8 +6,8 @@ import ( "os" "path/filepath" "runtime" - "sync/atomic" "strings" + "sync/atomic" "time" "wis-free-v3/internal/audio/recorder" @@ -90,6 +90,7 @@ func (a *App) startup(ctx context.Context) { // Initialize components a.startupHeadless() + a.startLinuxPressDaemon() // When the user launches the app again while it is already running (tray-only), // the second process signals us here so the settings window becomes visible. @@ -338,7 +339,6 @@ func (a *App) processRecording(recordingPath string) { transcribeDuration := time.Since(startTranscribe) logger.Info("Transcription completed in %v", transcribeDuration) - if err != nil { logger.Error("Transcription failed: %v", err) tray.UpdateStatus("Ready") @@ -380,14 +380,14 @@ func (a *App) processRecording(recordingPath string) { } else { // Save old clipboard oldClip, clipErr := wailsruntime.ClipboardGetText(a.ctx) - + // Copy to clipboard wailsruntime.ClipboardSetText(a.ctx, refinedText) // Paste a.pasteText() - // Restore old clipboard after a short delay, but ONLY if the user + // Restore old clipboard after a short delay, but ONLY if the user // hasn't manually copied something else or another burst hasn't finished. if clipErr == nil && oldClip != "" { go func() { @@ -437,6 +437,12 @@ func (a *App) GetSettings() map[string]interface{} { conf["history"] = a.config.History conf["startup"] = platform.IsInStartup() conf["app_version"] = AppVersion + if runtime.GOOS == "linux" { + if exePath, err := os.Executable(); err == nil { + conf["linux_press_command"] = exePath + " --press" + } + conf["linux_press_mode"] = true + } return conf } @@ -445,35 +451,37 @@ func (a *App) SaveSettings(settings map[string]interface{}) string { if val, ok := settings["api_key"].(string); ok { a.config.APIKey = val } - if val, ok := settings["shortcut"].(string); ok { - _, _, modOnly, ok := hotkey.ParseShortcut(val) - if !ok { - logger.Error("Invalid shortcut: %s (rejected)", val) - return "Invalid shortcut - use modifiers plus a key (e.g. ctrl+k), or modifier-only on Windows (e.g. ctrl+win)" - } - if modOnly && runtime.GOOS != "windows" { - return "Modifier-only shortcuts (like ctrl+win) are only supported on Windows" - } + if runtime.GOOS != "linux" { + if val, ok := settings["shortcut"].(string); ok { + _, _, modOnly, ok := hotkey.ParseShortcut(val) + if !ok { + logger.Error("Invalid shortcut: %s (rejected)", val) + return "Invalid shortcut - use modifiers plus a key (e.g. ctrl+k), or modifier-only on Windows (e.g. ctrl+win)" + } + if modOnly && runtime.GOOS != "windows" { + return "Modifier-only shortcuts (like ctrl+win) are only supported on Windows" + } - a.config.Shortcut = val - // Update existing listener with new shortcut (hot-swap) - if a.hotkeyListener != nil { - a.hotkeyListener.UpdateShortcut(val) - } else { - // Should not happen if app started correctly, but just in case - a.hotkeyListener = hotkey.NewListener(val, a.StartRecording, a.StopRecording) - if runtime.GOOS != "windows" { - a.hotkeyListener.SetRegistrationErrorCallback(func(err error) { - logger.Error("Linux hotkey registration failed: %v", err) - go func() { - time.Sleep(2 * time.Second) - if a.overlay != nil { - a.overlay.Show("Shortcut registration failed. Please add a custom system shortcut calling 'wis-free-v3 --action=toggle' as a fallback.") - } - }() - }) + a.config.Shortcut = val + // Update existing listener with new shortcut (hot-swap) + if a.hotkeyListener != nil { + a.hotkeyListener.UpdateShortcut(val) + } else { + // Should not happen if app started correctly, but just in case + a.hotkeyListener = hotkey.NewListener(val, a.StartRecording, a.StopRecording) + if runtime.GOOS != "windows" { + a.hotkeyListener.SetRegistrationErrorCallback(func(err error) { + logger.Error("Linux hotkey registration failed: %v", err) + go func() { + time.Sleep(2 * time.Second) + if a.overlay != nil { + a.overlay.Show("Shortcut registration failed. Please add a custom system shortcut calling 'wis-free-v3 --action=toggle' as a fallback.") + } + }() + }) + } + a.hotkeyListener.Start() } - a.hotkeyListener.Start() } } if val, ok := settings["whisper_model"].(string); ok { diff --git a/frontend/dist/assets/index.fbf781c1.js b/frontend/dist/assets/index.fbf781c1.js index 3455c60..d37b90e 100644 --- a/frontend/dist/assets/index.fbf781c1.js +++ b/frontend/dist/assets/index.fbf781c1.js @@ -1,3 +1,3 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))n(a);new MutationObserver(a=>{for(const s of a)if(s.type==="childList")for(const r of s.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&n(r)}).observe(document,{childList:!0,subtree:!0});function i(a){const s={};return a.integrity&&(s.integrity=a.integrity),a.referrerpolicy&&(s.referrerPolicy=a.referrerpolicy),a.crossorigin==="use-credentials"?s.credentials="include":a.crossorigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(a){if(a.ep)return;a.ep=!0;const s=i(a);fetch(a.href,s)}})();let u=!1;window.toggleApiKey=function(){const e=document.getElementById("apiKey"),t=document.getElementById("eyeBtn");u=!u,e.type=u?"text":"password",t.textContent=u?"\u{1F648}":"\u{1F441}\uFE0F"};window.recordShortcut=function(){const e=document.getElementById("recordBtn"),t=document.getElementById("shortcutInput"),i=o=>o.metaKey||o.key==="Meta"||o.key==="OS"||o.code==="MetaLeft"||o.code==="MetaRight";e.textContent="Recording...",e.disabled=!0,t.value="";let n="";const a=o=>{o.preventDefault(),o.stopPropagation();const d=[];o.ctrlKey&&d.push("ctrl"),o.altKey&&d.push("alt"),o.shiftKey&&d.push("shift"),i(o)&&d.push("win");let l=o.key.toLowerCase();l==="control"&&(l="ctrl"),l==="alt"&&(l="alt"),l==="shift"&&(l="shift"),(l==="meta"||l==="os")&&(l="win"),(o.code==="MetaLeft"||o.code==="MetaRight")&&(l="win"),l===" "&&(l="space");const y=new Set(d);y.add(l);const w=Array.from(y).filter(g=>g!==l);w.push(l),n=w.join("+"),t.value=n,["ctrl","alt","shift","win"].includes(l)||r()},s=o=>{o.preventDefault(),o.stopPropagation(),n&&(n.split("+").every(l=>["ctrl","alt","shift","win"].includes(l))&&n.includes("+")?setTimeout(r,100):!o.ctrlKey&&!o.altKey&&!o.shiftKey&&!i(o)&&r())},r=()=>{if(!n){e.textContent="Record",e.disabled=!1,f();return}window.go.main.App.SaveSettings({shortcut:n}).then(o=>{o&&o.indexOf("Invalid shortcut")!==-1&&alert(o),e.textContent="Record",e.disabled=!1,f()})},f=()=>{window.removeEventListener("keydown",a,!0),window.removeEventListener("keyup",s,!0)};window.addEventListener("keydown",a,!0),window.addEventListener("keyup",s,!0)};function m(e){const t=document.getElementById("historyList");if(!e||e.length===0){t.innerHTML='
No history yet.
';return}t.innerHTML="",[...e].reverse().forEach(i=>{const n=document.createElement("div");n.className="history-item";const a=document.createElement("span");a.className="history-time",a.textContent=new Date(i.timestamp).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});const s=document.createElement("div");s.textContent=i.text,n.appendChild(a),n.appendChild(s),t.appendChild(n)})}async function h(){try{const e=await window.go.main.App.GetSettings();m(e.history||[])}catch(e){console.error("Failed to refresh history:",e)}}async function v(){try{const e=await window.go.main.App.GetSettings();document.getElementById("apiKey").value=e.api_key||"",document.getElementById("shortcutInput").value=e.shortcut||"alt+z",document.getElementById("whisperModel").value=e.whisper_model||"whisper-large-v3-turbo",document.getElementById("aiModel").value=e.ai_model||"llama-3.3-70b-versatile",document.getElementById("aiPrompt").value=e.ai_prompt||"",document.getElementById("language").value=e.language||"en",document.getElementById("startupToggle").checked=e.startup||!1;const t=e.app_version&&String(e.app_version).trim()||"dev";document.getElementById("appVersion").textContent=t==="dev"?"dev":"v"+t,m(e.history||[])}catch(e){console.error("Failed to load settings:",e)}}async function E(){try{const e=await window.go.main.App.GetMicrophones(),t=document.getElementById("micDevice");e.forEach(i=>{if(i.index!==-1){const n=document.createElement("option");n.value=i.index,n.textContent=i.name,t.appendChild(n)}})}catch(e){console.error("Failed to load mics:",e)}}window.saveApiKey=async function(){const e=document.getElementById("apiKey").value.trim();await window.go.main.App.SaveSettings({api_key:e}),c()};function c(e="Saved"){const t=document.getElementById("saveStatus");t.textContent=e,t.classList.add("show"),setTimeout(()=>t.classList.remove("show"),2e3)}window.saveWhisperModel=async function(){await window.go.main.App.SaveSettings({whisper_model:document.getElementById("whisperModel").value}),c()};window.saveAiModel=async function(){await window.go.main.App.SaveSettings({ai_model:document.getElementById("aiModel").value}),c()};window.saveLanguage=async function(){await window.go.main.App.SaveSettings({language:document.getElementById("language").value}),c()};window.saveMicDevice=async function(){const e=document.getElementById("micDevice").value;await window.go.main.App.SaveSettings({microphone_device:e==="default"?null:parseInt(e)}),c()};window.savePrompt=async function(){await window.go.main.App.SaveSettings({ai_prompt:document.getElementById("aiPrompt").value.trim()}),c("Prompt saved")};window.toggleStartup=async function(){await window.go.main.App.ToggleStartup(document.getElementById("startupToggle").checked)};window.clearHistory=async function(){confirm("Clear all transcription history?")&&(await window.go.main.App.ClearHistory(),m([]))};async function I(){try{const e=await window.go.main.App.CheckOnline(),t=document.getElementById("connectionStatus");if(e)t.innerHTML="\u25CF Online",t.style.background="var(--green-dim)",t.style.color="var(--green)",t.style.border="1px solid rgba(61, 186, 110, 0.2)";else{t.innerHTML="\u25CF Offline \u2014 install local Whisper for transcription",t.style.background="var(--red-dim)",t.style.color="var(--red)",t.style.border="1px solid rgba(224, 82, 82, 0.2)";const i=document.getElementById("aiModel");for(let n of i.options)n.value!=="None"&&!n.value.startsWith("local-")&&(n.disabled=!0,n.text.includes("(offline)")||(n.text+=" (offline)"))}}catch(e){console.error("Connection check failed:",e)}}async function p(){try{const e=await window.go.main.App.GetWhisperInfo(),t=document.getElementById("whisperModel"),i=t.querySelector('option[value^="local-"]');if(i&&i.remove(),e.installed){document.getElementById("whisperNotInstalled").style.display="none",document.getElementById("whisperInstalled").style.display="block",document.getElementById("installedModel").textContent=e.model+" model";const n=document.createElement("option");n.value="local-"+e.model,n.textContent="\u2B21 Local \u2014 "+e.model+" (offline)",n.style.fontWeight="bold",t.insertBefore(n,t.firstChild)}else document.getElementById("whisperNotInstalled").style.display="block",document.getElementById("whisperInstalled").style.display="none"}catch(e){console.error("Whisper status check failed:",e)}}window.installWhisper=async function(){const e=document.getElementById("whisperModelSelect").value,t=document.getElementById("installBtn");t.disabled=!0,t.textContent="Starting installation...";try{const i=await window.go.main.App.InstallWhisper(e);alert(i+` +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))n(a);new MutationObserver(a=>{for(const s of a)if(s.type==="childList")for(const r of s.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&n(r)}).observe(document,{childList:!0,subtree:!0});function i(a){const s={};return a.integrity&&(s.integrity=a.integrity),a.referrerpolicy&&(s.referrerPolicy=a.referrerpolicy),a.crossorigin==="use-credentials"?s.credentials="include":a.crossorigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(a){if(a.ep)return;a.ep=!0;const s=i(a);fetch(a.href,s)}})();let u=!1;window.toggleApiKey=function(){const e=document.getElementById("apiKey"),t=document.getElementById("eyeBtn");u=!u,e.type=u?"text":"password",t.textContent=u?"\u{1F648}":"\u{1F441}\uFE0F"};window.recordShortcut=function(){const e=document.getElementById("recordBtn"),t=document.getElementById("shortcutInput"),i=o=>o.metaKey||o.key==="Meta"||o.key==="OS"||o.code==="MetaLeft"||o.code==="MetaRight";e.textContent="Recording...",e.disabled=!0,t.value="";let n="";const a=o=>{o.preventDefault(),o.stopPropagation();const d=[];o.ctrlKey&&d.push("ctrl"),o.altKey&&d.push("alt"),o.shiftKey&&d.push("shift"),i(o)&&d.push("win");let l=o.key.toLowerCase();l==="control"&&(l="ctrl"),l==="alt"&&(l="alt"),l==="shift"&&(l="shift"),(l==="meta"||l==="os")&&(l="win"),(o.code==="MetaLeft"||o.code==="MetaRight")&&(l="win"),l===" "&&(l="space");const y=new Set(d);y.add(l);const w=Array.from(y).filter(g=>g!==l);w.push(l),n=w.join("+"),t.value=n,["ctrl","alt","shift","win"].includes(l)||r()},s=o=>{o.preventDefault(),o.stopPropagation(),n&&(n.split("+").every(l=>["ctrl","alt","shift","win"].includes(l))&&n.includes("+")?setTimeout(r,100):!o.ctrlKey&&!o.altKey&&!o.shiftKey&&!i(o)&&r())},r=()=>{if(!n){e.textContent="Record",e.disabled=!1,f();return}window.go.main.App.SaveSettings({shortcut:n}).then(o=>{o&&o.indexOf("Invalid shortcut")!==-1&&alert(o),e.textContent="Record",e.disabled=!1,f()})},f=()=>{window.removeEventListener("keydown",a,!0),window.removeEventListener("keyup",s,!0)};window.addEventListener("keydown",a,!0),window.addEventListener("keyup",s,!0)};function m(e){const t=document.getElementById("historyList");if(!e||e.length===0){t.innerHTML='
No history yet.
';return}t.innerHTML="",[...e].reverse().forEach(i=>{const n=document.createElement("div");n.className="history-item";const a=document.createElement("span");a.className="history-time",a.textContent=new Date(i.timestamp).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});const s=document.createElement("div");s.textContent=i.text,n.appendChild(a),n.appendChild(s),t.appendChild(n)})}async function h(){try{const e=await window.go.main.App.GetSettings();m(e.history||[])}catch(e){console.error("Failed to refresh history:",e)}}async function v(){try{const e=await window.go.main.App.GetSettings();document.getElementById("apiKey").value=e.api_key||"",e.linux_press_mode&&document.getElementById("linuxPressSection")&&(document.getElementById("linuxPressSection").style.display="block",document.getElementById("linuxPressCommand").value=e.linux_press_command||""),document.getElementById("whisperModel").value=e.whisper_model||"whisper-large-v3-turbo",document.getElementById("aiModel").value=e.ai_model||"llama-3.3-70b-versatile",document.getElementById("aiPrompt").value=e.ai_prompt||"",document.getElementById("language").value=e.language||"en",document.getElementById("startupToggle").checked=e.startup||!1;const t=e.app_version&&String(e.app_version).trim()||"dev";document.getElementById("appVersion").textContent=t==="dev"?"dev":"v"+t,m(e.history||[])}catch(e){console.error("Failed to load settings:",e)}}async function E(){try{const e=await window.go.main.App.GetMicrophones(),t=document.getElementById("micDevice");e.forEach(i=>{if(i.index!==-1){const n=document.createElement("option");n.value=i.index,n.textContent=i.name,t.appendChild(n)}})}catch(e){console.error("Failed to load mics:",e)}}window.saveApiKey=async function(){const e=document.getElementById("apiKey").value.trim();await window.go.main.App.SaveSettings({api_key:e}),c()};function c(e="Saved"){const t=document.getElementById("saveStatus");t.textContent=e,t.classList.add("show"),setTimeout(()=>t.classList.remove("show"),2e3)}window.saveWhisperModel=async function(){await window.go.main.App.SaveSettings({whisper_model:document.getElementById("whisperModel").value}),c()};window.saveAiModel=async function(){await window.go.main.App.SaveSettings({ai_model:document.getElementById("aiModel").value}),c()};window.saveLanguage=async function(){await window.go.main.App.SaveSettings({language:document.getElementById("language").value}),c()};window.saveMicDevice=async function(){const e=document.getElementById("micDevice").value;await window.go.main.App.SaveSettings({microphone_device:e==="default"?null:parseInt(e)}),c()};window.savePrompt=async function(){await window.go.main.App.SaveSettings({ai_prompt:document.getElementById("aiPrompt").value.trim()}),c("Prompt saved")};window.toggleStartup=async function(){await window.go.main.App.ToggleStartup(document.getElementById("startupToggle").checked)};window.clearHistory=async function(){confirm("Clear all transcription history?")&&(await window.go.main.App.ClearHistory(),m([]))};async function I(){try{const e=await window.go.main.App.CheckOnline(),t=document.getElementById("connectionStatus");if(e)t.innerHTML="\u25CF Online",t.style.background="var(--green-dim)",t.style.color="var(--green)",t.style.border="1px solid rgba(61, 186, 110, 0.2)";else{t.innerHTML="\u25CF Offline \u2014 install local Whisper for transcription",t.style.background="var(--red-dim)",t.style.color="var(--red)",t.style.border="1px solid rgba(224, 82, 82, 0.2)";const i=document.getElementById("aiModel");for(let n of i.options)n.value!=="None"&&!n.value.startsWith("local-")&&(n.disabled=!0,n.text.includes("(offline)")||(n.text+=" (offline)"))}}catch(e){console.error("Connection check failed:",e)}}async function p(){try{const e=await window.go.main.App.GetWhisperInfo(),t=document.getElementById("whisperModel"),i=t.querySelector('option[value^="local-"]');if(i&&i.remove(),e.installed){document.getElementById("whisperNotInstalled").style.display="none",document.getElementById("whisperInstalled").style.display="block",document.getElementById("installedModel").textContent=e.model+" model";const n=document.createElement("option");n.value="local-"+e.model,n.textContent="\u2B21 Local \u2014 "+e.model+" (offline)",n.style.fontWeight="bold",t.insertBefore(n,t.firstChild)}else document.getElementById("whisperNotInstalled").style.display="block",document.getElementById("whisperInstalled").style.display="none"}catch(e){console.error("Whisper status check failed:",e)}}window.copyLinuxPressCommand=function(){const e=document.getElementById("linuxPressCommand");if(!e)return;const t=e.value||"";navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(t).then(()=>c("Command copied")):((e.focus(),e.select(),document.execCommand("copy")),c("Command copied"))};window.installWhisper=async function(){const e=document.getElementById("whisperModelSelect").value,t=document.getElementById("installBtn");t.disabled=!0,t.textContent="Starting installation...";try{const i=await window.go.main.App.InstallWhisper(e);alert(i+` A terminal window will open showing the installation progress. This may take several minutes depending on your internet speed.`),setTimeout(p,5e3)}catch(i){alert("Installation failed: "+i)}t.disabled=!1,t.textContent="Install Offline Whisper"};window.uninstallWhisper=async function(){if(!!confirm("Uninstall offline Whisper? This will delete the downloaded model."))try{const e=await window.go.main.App.UninstallWhisper();alert(e),p()}catch(e){alert("Uninstall failed: "+e)}};async function S(){try{if(navigator.userAgent.toLowerCase().includes("linux")){const t=document.getElementById("offlineWhisperSection");t&&(t.style.display="none");const i=document.getElementById("startupLabel");i&&(i.innerText="Run on Startup")}}catch(e){console.error("Failed to get environment:",e)}if(await v(),await E(),await I(),await p(),window.runtime&&typeof window.runtime.EventsOn=="function"&&window.runtime.EventsOn("history:updated",function(){h()}),navigator.userAgent.toLowerCase().includes("linux")){const e=document.getElementById("whisperModel");e&&Array.from(e.options).forEach(t=>{t.value.startsWith("local-")&&t.remove()})}}S(); diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 7bcc52c..312acb7 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -43,18 +43,19 @@
Input
- -
- + + diff --git a/linux_press_daemon.go b/linux_press_daemon.go new file mode 100644 index 0000000..79dd6b1 --- /dev/null +++ b/linux_press_daemon.go @@ -0,0 +1,83 @@ +//go:build linux + +package main + +import ( + "net/http" + "os" + "os/exec" + "sync" + "time" +) + +const linuxPressAddr = "127.0.0.1:9876" + +var ( + linuxPressMu sync.Mutex + linuxPressTimer *time.Timer + linuxPressRecording bool +) + +func init() { + if hasPressFlag(os.Args[1:]) { + if err := sendLinuxPressPing(); err != nil { + _ = exec.Command("notify-send", "Voice App", "Please open the main application first").Run() + os.Exit(1) + } + os.Exit(0) + } +} + +func hasPressFlag(args []string) bool { + for _, arg := range args { + if arg == "--press" { + return true + } + } + return false +} + +func sendLinuxPressPing() error { + client := &http.Client{Timeout: 300 * time.Millisecond} + resp, err := client.Get("http://" + linuxPressAddr + "/press") + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return os.ErrNotExist + } + return nil +} + +func (a *App) startLinuxPressDaemon() { + mux := http.NewServeMux() + mux.HandleFunc("/press", func(w http.ResponseWriter, r *http.Request) { + linuxPressMu.Lock() + defer linuxPressMu.Unlock() + + if !linuxPressRecording { + linuxPressRecording = true + go a.StartRecording() + } + + if linuxPressTimer != nil { + linuxPressTimer.Stop() + } + + linuxPressTimer = time.AfterFunc(300*time.Millisecond, func() { + linuxPressMu.Lock() + defer linuxPressMu.Unlock() + if linuxPressRecording { + linuxPressRecording = false + go a.StopRecording() + } + }) + + w.WriteHeader(http.StatusNoContent) + }) + + go func() { + _ = http.ListenAndServe(linuxPressAddr, mux) + }() +} diff --git a/linux_press_daemon_stub.go b/linux_press_daemon_stub.go new file mode 100644 index 0000000..7d99f82 --- /dev/null +++ b/linux_press_daemon_stub.go @@ -0,0 +1,5 @@ +//go:build !linux + +package main + +func (a *App) startLinuxPressDaemon() {} From 7fe25ae125407b6ae01b133e8fcf0e273ae99ba5 Mon Sep 17 00:00:00 2001 From: jahruz67 Date: Tue, 19 May 2026 20:45:19 -0700 Subject: [PATCH 2/2] Fix Linux hotkey UI changes in source frontend files --- app.go | 70 ++++++++++++---------- frontend/dist/assets/index.fbf781c1.js | 2 +- frontend/dist/index.html | 13 ++-- frontend/index.html | 13 ++-- frontend/src/main.js | 41 ++++++++----- linux_press_daemon.go | 83 ++++++++++++++++++++++++++ linux_press_daemon_stub.go | 5 ++ 7 files changed, 167 insertions(+), 60 deletions(-) create mode 100644 linux_press_daemon.go create mode 100644 linux_press_daemon_stub.go diff --git a/app.go b/app.go index 1f9adb7..6ee1378 100644 --- a/app.go +++ b/app.go @@ -6,8 +6,8 @@ import ( "os" "path/filepath" "runtime" - "sync/atomic" "strings" + "sync/atomic" "time" "wis-free-v3/internal/audio/recorder" @@ -90,6 +90,7 @@ func (a *App) startup(ctx context.Context) { // Initialize components a.startupHeadless() + a.startLinuxPressDaemon() // When the user launches the app again while it is already running (tray-only), // the second process signals us here so the settings window becomes visible. @@ -338,7 +339,6 @@ func (a *App) processRecording(recordingPath string) { transcribeDuration := time.Since(startTranscribe) logger.Info("Transcription completed in %v", transcribeDuration) - if err != nil { logger.Error("Transcription failed: %v", err) tray.UpdateStatus("Ready") @@ -380,14 +380,14 @@ func (a *App) processRecording(recordingPath string) { } else { // Save old clipboard oldClip, clipErr := wailsruntime.ClipboardGetText(a.ctx) - + // Copy to clipboard wailsruntime.ClipboardSetText(a.ctx, refinedText) // Paste a.pasteText() - // Restore old clipboard after a short delay, but ONLY if the user + // Restore old clipboard after a short delay, but ONLY if the user // hasn't manually copied something else or another burst hasn't finished. if clipErr == nil && oldClip != "" { go func() { @@ -437,6 +437,12 @@ func (a *App) GetSettings() map[string]interface{} { conf["history"] = a.config.History conf["startup"] = platform.IsInStartup() conf["app_version"] = AppVersion + if runtime.GOOS == "linux" { + if exePath, err := os.Executable(); err == nil { + conf["linux_press_command"] = exePath + " --press" + } + conf["linux_press_mode"] = true + } return conf } @@ -445,35 +451,37 @@ func (a *App) SaveSettings(settings map[string]interface{}) string { if val, ok := settings["api_key"].(string); ok { a.config.APIKey = val } - if val, ok := settings["shortcut"].(string); ok { - _, _, modOnly, ok := hotkey.ParseShortcut(val) - if !ok { - logger.Error("Invalid shortcut: %s (rejected)", val) - return "Invalid shortcut - use modifiers plus a key (e.g. ctrl+k), or modifier-only on Windows (e.g. ctrl+win)" - } - if modOnly && runtime.GOOS != "windows" { - return "Modifier-only shortcuts (like ctrl+win) are only supported on Windows" - } + if runtime.GOOS != "linux" { + if val, ok := settings["shortcut"].(string); ok { + _, _, modOnly, ok := hotkey.ParseShortcut(val) + if !ok { + logger.Error("Invalid shortcut: %s (rejected)", val) + return "Invalid shortcut - use modifiers plus a key (e.g. ctrl+k), or modifier-only on Windows (e.g. ctrl+win)" + } + if modOnly && runtime.GOOS != "windows" { + return "Modifier-only shortcuts (like ctrl+win) are only supported on Windows" + } - a.config.Shortcut = val - // Update existing listener with new shortcut (hot-swap) - if a.hotkeyListener != nil { - a.hotkeyListener.UpdateShortcut(val) - } else { - // Should not happen if app started correctly, but just in case - a.hotkeyListener = hotkey.NewListener(val, a.StartRecording, a.StopRecording) - if runtime.GOOS != "windows" { - a.hotkeyListener.SetRegistrationErrorCallback(func(err error) { - logger.Error("Linux hotkey registration failed: %v", err) - go func() { - time.Sleep(2 * time.Second) - if a.overlay != nil { - a.overlay.Show("Shortcut registration failed. Please add a custom system shortcut calling 'wis-free-v3 --action=toggle' as a fallback.") - } - }() - }) + a.config.Shortcut = val + // Update existing listener with new shortcut (hot-swap) + if a.hotkeyListener != nil { + a.hotkeyListener.UpdateShortcut(val) + } else { + // Should not happen if app started correctly, but just in case + a.hotkeyListener = hotkey.NewListener(val, a.StartRecording, a.StopRecording) + if runtime.GOOS != "windows" { + a.hotkeyListener.SetRegistrationErrorCallback(func(err error) { + logger.Error("Linux hotkey registration failed: %v", err) + go func() { + time.Sleep(2 * time.Second) + if a.overlay != nil { + a.overlay.Show("Shortcut registration failed. Please add a custom system shortcut calling 'wis-free-v3 --action=toggle' as a fallback.") + } + }() + }) + } + a.hotkeyListener.Start() } - a.hotkeyListener.Start() } } if val, ok := settings["whisper_model"].(string); ok { diff --git a/frontend/dist/assets/index.fbf781c1.js b/frontend/dist/assets/index.fbf781c1.js index 3455c60..d37b90e 100644 --- a/frontend/dist/assets/index.fbf781c1.js +++ b/frontend/dist/assets/index.fbf781c1.js @@ -1,3 +1,3 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))n(a);new MutationObserver(a=>{for(const s of a)if(s.type==="childList")for(const r of s.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&n(r)}).observe(document,{childList:!0,subtree:!0});function i(a){const s={};return a.integrity&&(s.integrity=a.integrity),a.referrerpolicy&&(s.referrerPolicy=a.referrerpolicy),a.crossorigin==="use-credentials"?s.credentials="include":a.crossorigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(a){if(a.ep)return;a.ep=!0;const s=i(a);fetch(a.href,s)}})();let u=!1;window.toggleApiKey=function(){const e=document.getElementById("apiKey"),t=document.getElementById("eyeBtn");u=!u,e.type=u?"text":"password",t.textContent=u?"\u{1F648}":"\u{1F441}\uFE0F"};window.recordShortcut=function(){const e=document.getElementById("recordBtn"),t=document.getElementById("shortcutInput"),i=o=>o.metaKey||o.key==="Meta"||o.key==="OS"||o.code==="MetaLeft"||o.code==="MetaRight";e.textContent="Recording...",e.disabled=!0,t.value="";let n="";const a=o=>{o.preventDefault(),o.stopPropagation();const d=[];o.ctrlKey&&d.push("ctrl"),o.altKey&&d.push("alt"),o.shiftKey&&d.push("shift"),i(o)&&d.push("win");let l=o.key.toLowerCase();l==="control"&&(l="ctrl"),l==="alt"&&(l="alt"),l==="shift"&&(l="shift"),(l==="meta"||l==="os")&&(l="win"),(o.code==="MetaLeft"||o.code==="MetaRight")&&(l="win"),l===" "&&(l="space");const y=new Set(d);y.add(l);const w=Array.from(y).filter(g=>g!==l);w.push(l),n=w.join("+"),t.value=n,["ctrl","alt","shift","win"].includes(l)||r()},s=o=>{o.preventDefault(),o.stopPropagation(),n&&(n.split("+").every(l=>["ctrl","alt","shift","win"].includes(l))&&n.includes("+")?setTimeout(r,100):!o.ctrlKey&&!o.altKey&&!o.shiftKey&&!i(o)&&r())},r=()=>{if(!n){e.textContent="Record",e.disabled=!1,f();return}window.go.main.App.SaveSettings({shortcut:n}).then(o=>{o&&o.indexOf("Invalid shortcut")!==-1&&alert(o),e.textContent="Record",e.disabled=!1,f()})},f=()=>{window.removeEventListener("keydown",a,!0),window.removeEventListener("keyup",s,!0)};window.addEventListener("keydown",a,!0),window.addEventListener("keyup",s,!0)};function m(e){const t=document.getElementById("historyList");if(!e||e.length===0){t.innerHTML='
No history yet.
';return}t.innerHTML="",[...e].reverse().forEach(i=>{const n=document.createElement("div");n.className="history-item";const a=document.createElement("span");a.className="history-time",a.textContent=new Date(i.timestamp).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});const s=document.createElement("div");s.textContent=i.text,n.appendChild(a),n.appendChild(s),t.appendChild(n)})}async function h(){try{const e=await window.go.main.App.GetSettings();m(e.history||[])}catch(e){console.error("Failed to refresh history:",e)}}async function v(){try{const e=await window.go.main.App.GetSettings();document.getElementById("apiKey").value=e.api_key||"",document.getElementById("shortcutInput").value=e.shortcut||"alt+z",document.getElementById("whisperModel").value=e.whisper_model||"whisper-large-v3-turbo",document.getElementById("aiModel").value=e.ai_model||"llama-3.3-70b-versatile",document.getElementById("aiPrompt").value=e.ai_prompt||"",document.getElementById("language").value=e.language||"en",document.getElementById("startupToggle").checked=e.startup||!1;const t=e.app_version&&String(e.app_version).trim()||"dev";document.getElementById("appVersion").textContent=t==="dev"?"dev":"v"+t,m(e.history||[])}catch(e){console.error("Failed to load settings:",e)}}async function E(){try{const e=await window.go.main.App.GetMicrophones(),t=document.getElementById("micDevice");e.forEach(i=>{if(i.index!==-1){const n=document.createElement("option");n.value=i.index,n.textContent=i.name,t.appendChild(n)}})}catch(e){console.error("Failed to load mics:",e)}}window.saveApiKey=async function(){const e=document.getElementById("apiKey").value.trim();await window.go.main.App.SaveSettings({api_key:e}),c()};function c(e="Saved"){const t=document.getElementById("saveStatus");t.textContent=e,t.classList.add("show"),setTimeout(()=>t.classList.remove("show"),2e3)}window.saveWhisperModel=async function(){await window.go.main.App.SaveSettings({whisper_model:document.getElementById("whisperModel").value}),c()};window.saveAiModel=async function(){await window.go.main.App.SaveSettings({ai_model:document.getElementById("aiModel").value}),c()};window.saveLanguage=async function(){await window.go.main.App.SaveSettings({language:document.getElementById("language").value}),c()};window.saveMicDevice=async function(){const e=document.getElementById("micDevice").value;await window.go.main.App.SaveSettings({microphone_device:e==="default"?null:parseInt(e)}),c()};window.savePrompt=async function(){await window.go.main.App.SaveSettings({ai_prompt:document.getElementById("aiPrompt").value.trim()}),c("Prompt saved")};window.toggleStartup=async function(){await window.go.main.App.ToggleStartup(document.getElementById("startupToggle").checked)};window.clearHistory=async function(){confirm("Clear all transcription history?")&&(await window.go.main.App.ClearHistory(),m([]))};async function I(){try{const e=await window.go.main.App.CheckOnline(),t=document.getElementById("connectionStatus");if(e)t.innerHTML="\u25CF Online",t.style.background="var(--green-dim)",t.style.color="var(--green)",t.style.border="1px solid rgba(61, 186, 110, 0.2)";else{t.innerHTML="\u25CF Offline \u2014 install local Whisper for transcription",t.style.background="var(--red-dim)",t.style.color="var(--red)",t.style.border="1px solid rgba(224, 82, 82, 0.2)";const i=document.getElementById("aiModel");for(let n of i.options)n.value!=="None"&&!n.value.startsWith("local-")&&(n.disabled=!0,n.text.includes("(offline)")||(n.text+=" (offline)"))}}catch(e){console.error("Connection check failed:",e)}}async function p(){try{const e=await window.go.main.App.GetWhisperInfo(),t=document.getElementById("whisperModel"),i=t.querySelector('option[value^="local-"]');if(i&&i.remove(),e.installed){document.getElementById("whisperNotInstalled").style.display="none",document.getElementById("whisperInstalled").style.display="block",document.getElementById("installedModel").textContent=e.model+" model";const n=document.createElement("option");n.value="local-"+e.model,n.textContent="\u2B21 Local \u2014 "+e.model+" (offline)",n.style.fontWeight="bold",t.insertBefore(n,t.firstChild)}else document.getElementById("whisperNotInstalled").style.display="block",document.getElementById("whisperInstalled").style.display="none"}catch(e){console.error("Whisper status check failed:",e)}}window.installWhisper=async function(){const e=document.getElementById("whisperModelSelect").value,t=document.getElementById("installBtn");t.disabled=!0,t.textContent="Starting installation...";try{const i=await window.go.main.App.InstallWhisper(e);alert(i+` +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))n(a);new MutationObserver(a=>{for(const s of a)if(s.type==="childList")for(const r of s.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&n(r)}).observe(document,{childList:!0,subtree:!0});function i(a){const s={};return a.integrity&&(s.integrity=a.integrity),a.referrerpolicy&&(s.referrerPolicy=a.referrerpolicy),a.crossorigin==="use-credentials"?s.credentials="include":a.crossorigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(a){if(a.ep)return;a.ep=!0;const s=i(a);fetch(a.href,s)}})();let u=!1;window.toggleApiKey=function(){const e=document.getElementById("apiKey"),t=document.getElementById("eyeBtn");u=!u,e.type=u?"text":"password",t.textContent=u?"\u{1F648}":"\u{1F441}\uFE0F"};window.recordShortcut=function(){const e=document.getElementById("recordBtn"),t=document.getElementById("shortcutInput"),i=o=>o.metaKey||o.key==="Meta"||o.key==="OS"||o.code==="MetaLeft"||o.code==="MetaRight";e.textContent="Recording...",e.disabled=!0,t.value="";let n="";const a=o=>{o.preventDefault(),o.stopPropagation();const d=[];o.ctrlKey&&d.push("ctrl"),o.altKey&&d.push("alt"),o.shiftKey&&d.push("shift"),i(o)&&d.push("win");let l=o.key.toLowerCase();l==="control"&&(l="ctrl"),l==="alt"&&(l="alt"),l==="shift"&&(l="shift"),(l==="meta"||l==="os")&&(l="win"),(o.code==="MetaLeft"||o.code==="MetaRight")&&(l="win"),l===" "&&(l="space");const y=new Set(d);y.add(l);const w=Array.from(y).filter(g=>g!==l);w.push(l),n=w.join("+"),t.value=n,["ctrl","alt","shift","win"].includes(l)||r()},s=o=>{o.preventDefault(),o.stopPropagation(),n&&(n.split("+").every(l=>["ctrl","alt","shift","win"].includes(l))&&n.includes("+")?setTimeout(r,100):!o.ctrlKey&&!o.altKey&&!o.shiftKey&&!i(o)&&r())},r=()=>{if(!n){e.textContent="Record",e.disabled=!1,f();return}window.go.main.App.SaveSettings({shortcut:n}).then(o=>{o&&o.indexOf("Invalid shortcut")!==-1&&alert(o),e.textContent="Record",e.disabled=!1,f()})},f=()=>{window.removeEventListener("keydown",a,!0),window.removeEventListener("keyup",s,!0)};window.addEventListener("keydown",a,!0),window.addEventListener("keyup",s,!0)};function m(e){const t=document.getElementById("historyList");if(!e||e.length===0){t.innerHTML='
No history yet.
';return}t.innerHTML="",[...e].reverse().forEach(i=>{const n=document.createElement("div");n.className="history-item";const a=document.createElement("span");a.className="history-time",a.textContent=new Date(i.timestamp).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});const s=document.createElement("div");s.textContent=i.text,n.appendChild(a),n.appendChild(s),t.appendChild(n)})}async function h(){try{const e=await window.go.main.App.GetSettings();m(e.history||[])}catch(e){console.error("Failed to refresh history:",e)}}async function v(){try{const e=await window.go.main.App.GetSettings();document.getElementById("apiKey").value=e.api_key||"",e.linux_press_mode&&document.getElementById("linuxPressSection")&&(document.getElementById("linuxPressSection").style.display="block",document.getElementById("linuxPressCommand").value=e.linux_press_command||""),document.getElementById("whisperModel").value=e.whisper_model||"whisper-large-v3-turbo",document.getElementById("aiModel").value=e.ai_model||"llama-3.3-70b-versatile",document.getElementById("aiPrompt").value=e.ai_prompt||"",document.getElementById("language").value=e.language||"en",document.getElementById("startupToggle").checked=e.startup||!1;const t=e.app_version&&String(e.app_version).trim()||"dev";document.getElementById("appVersion").textContent=t==="dev"?"dev":"v"+t,m(e.history||[])}catch(e){console.error("Failed to load settings:",e)}}async function E(){try{const e=await window.go.main.App.GetMicrophones(),t=document.getElementById("micDevice");e.forEach(i=>{if(i.index!==-1){const n=document.createElement("option");n.value=i.index,n.textContent=i.name,t.appendChild(n)}})}catch(e){console.error("Failed to load mics:",e)}}window.saveApiKey=async function(){const e=document.getElementById("apiKey").value.trim();await window.go.main.App.SaveSettings({api_key:e}),c()};function c(e="Saved"){const t=document.getElementById("saveStatus");t.textContent=e,t.classList.add("show"),setTimeout(()=>t.classList.remove("show"),2e3)}window.saveWhisperModel=async function(){await window.go.main.App.SaveSettings({whisper_model:document.getElementById("whisperModel").value}),c()};window.saveAiModel=async function(){await window.go.main.App.SaveSettings({ai_model:document.getElementById("aiModel").value}),c()};window.saveLanguage=async function(){await window.go.main.App.SaveSettings({language:document.getElementById("language").value}),c()};window.saveMicDevice=async function(){const e=document.getElementById("micDevice").value;await window.go.main.App.SaveSettings({microphone_device:e==="default"?null:parseInt(e)}),c()};window.savePrompt=async function(){await window.go.main.App.SaveSettings({ai_prompt:document.getElementById("aiPrompt").value.trim()}),c("Prompt saved")};window.toggleStartup=async function(){await window.go.main.App.ToggleStartup(document.getElementById("startupToggle").checked)};window.clearHistory=async function(){confirm("Clear all transcription history?")&&(await window.go.main.App.ClearHistory(),m([]))};async function I(){try{const e=await window.go.main.App.CheckOnline(),t=document.getElementById("connectionStatus");if(e)t.innerHTML="\u25CF Online",t.style.background="var(--green-dim)",t.style.color="var(--green)",t.style.border="1px solid rgba(61, 186, 110, 0.2)";else{t.innerHTML="\u25CF Offline \u2014 install local Whisper for transcription",t.style.background="var(--red-dim)",t.style.color="var(--red)",t.style.border="1px solid rgba(224, 82, 82, 0.2)";const i=document.getElementById("aiModel");for(let n of i.options)n.value!=="None"&&!n.value.startsWith("local-")&&(n.disabled=!0,n.text.includes("(offline)")||(n.text+=" (offline)"))}}catch(e){console.error("Connection check failed:",e)}}async function p(){try{const e=await window.go.main.App.GetWhisperInfo(),t=document.getElementById("whisperModel"),i=t.querySelector('option[value^="local-"]');if(i&&i.remove(),e.installed){document.getElementById("whisperNotInstalled").style.display="none",document.getElementById("whisperInstalled").style.display="block",document.getElementById("installedModel").textContent=e.model+" model";const n=document.createElement("option");n.value="local-"+e.model,n.textContent="\u2B21 Local \u2014 "+e.model+" (offline)",n.style.fontWeight="bold",t.insertBefore(n,t.firstChild)}else document.getElementById("whisperNotInstalled").style.display="block",document.getElementById("whisperInstalled").style.display="none"}catch(e){console.error("Whisper status check failed:",e)}}window.copyLinuxPressCommand=function(){const e=document.getElementById("linuxPressCommand");if(!e)return;const t=e.value||"";navigator.clipboard&&navigator.clipboard.writeText?navigator.clipboard.writeText(t).then(()=>c("Command copied")):((e.focus(),e.select(),document.execCommand("copy")),c("Command copied"))};window.installWhisper=async function(){const e=document.getElementById("whisperModelSelect").value,t=document.getElementById("installBtn");t.disabled=!0,t.textContent="Starting installation...";try{const i=await window.go.main.App.InstallWhisper(e);alert(i+` A terminal window will open showing the installation progress. This may take several minutes depending on your internet speed.`),setTimeout(p,5e3)}catch(i){alert("Installation failed: "+i)}t.disabled=!1,t.textContent="Install Offline Whisper"};window.uninstallWhisper=async function(){if(!!confirm("Uninstall offline Whisper? This will delete the downloaded model."))try{const e=await window.go.main.App.UninstallWhisper();alert(e),p()}catch(e){alert("Uninstall failed: "+e)}};async function S(){try{if(navigator.userAgent.toLowerCase().includes("linux")){const t=document.getElementById("offlineWhisperSection");t&&(t.style.display="none");const i=document.getElementById("startupLabel");i&&(i.innerText="Run on Startup")}}catch(e){console.error("Failed to get environment:",e)}if(await v(),await E(),await I(),await p(),window.runtime&&typeof window.runtime.EventsOn=="function"&&window.runtime.EventsOn("history:updated",function(){h()}),navigator.userAgent.toLowerCase().includes("linux")){const e=document.getElementById("whisperModel");e&&Array.from(e.options).forEach(t=>{t.value.startsWith("local-")&&t.remove()})}}S(); diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 7bcc52c..312acb7 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -43,18 +43,19 @@
Input
- -
- + + diff --git a/frontend/index.html b/frontend/index.html index 72321d1..9b779b8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -41,18 +41,19 @@
Input
- -
- + + diff --git a/frontend/src/main.js b/frontend/src/main.js index 2f439fe..11cecc1 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -24,12 +24,18 @@ async function loadSettings() { // Populate fields document.getElementById('apiKey').value = settings.api_key || ''; - document.getElementById('shortcutInput').value = settings.shortcut || 'alt+z'; document.getElementById('whisperModel').value = settings.whisper_model || 'whisper-large-v3-turbo'; document.getElementById('aiModel').value = settings.ai_model || 'llama-3.3-70b-versatile'; document.getElementById('aiPrompt').value = settings.ai_prompt || ''; document.getElementById('startupToggle').checked = settings.startup || false; + if (settings.linux_press_mode) { + const section = document.getElementById('linuxPressSection'); + const cmdInput = document.getElementById('linuxPressCommand'); + if (section) section.style.display = 'block'; + if (cmdInput) cmdInput.value = settings.linux_press_command || ''; + } + // Load history renderHistory(settings.history || []); @@ -123,25 +129,28 @@ async function saveApiKey() { showToast('API Key saved'); } -// Save Shortcut -async function saveShortcut() { - const shortcut = document.getElementById('shortcutInput').value.trim().toLowerCase(); - if (!shortcut) { - alert('Please enter a shortcut'); - return; +async function copyLinuxPressCommand() { + const cmdInput = document.getElementById('linuxPressCommand'); + if (!cmdInput) return; + + const command = cmdInput.value || ''; + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(command); + } else { + cmdInput.focus(); + cmdInput.select(); + document.execCommand('copy'); + } + showToast('Command copied'); + } catch (err) { + console.error('Failed to copy Linux command:', err); } - - const parts = shortcut.split('+'); - if (parts.length < 2) { - alert('Shortcut must have a modifier + key (e.g., ctrl+x)'); - return; - } - - await saveSetting('shortcut', shortcut); - showToast('Shortcut saved: ' + shortcut); } +window.copyLinuxPressCommand = copyLinuxPressCommand; + // Save Whisper Model async function saveWhisperModel() { const model = document.getElementById('whisperModel').value; diff --git a/linux_press_daemon.go b/linux_press_daemon.go new file mode 100644 index 0000000..79dd6b1 --- /dev/null +++ b/linux_press_daemon.go @@ -0,0 +1,83 @@ +//go:build linux + +package main + +import ( + "net/http" + "os" + "os/exec" + "sync" + "time" +) + +const linuxPressAddr = "127.0.0.1:9876" + +var ( + linuxPressMu sync.Mutex + linuxPressTimer *time.Timer + linuxPressRecording bool +) + +func init() { + if hasPressFlag(os.Args[1:]) { + if err := sendLinuxPressPing(); err != nil { + _ = exec.Command("notify-send", "Voice App", "Please open the main application first").Run() + os.Exit(1) + } + os.Exit(0) + } +} + +func hasPressFlag(args []string) bool { + for _, arg := range args { + if arg == "--press" { + return true + } + } + return false +} + +func sendLinuxPressPing() error { + client := &http.Client{Timeout: 300 * time.Millisecond} + resp, err := client.Get("http://" + linuxPressAddr + "/press") + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return os.ErrNotExist + } + return nil +} + +func (a *App) startLinuxPressDaemon() { + mux := http.NewServeMux() + mux.HandleFunc("/press", func(w http.ResponseWriter, r *http.Request) { + linuxPressMu.Lock() + defer linuxPressMu.Unlock() + + if !linuxPressRecording { + linuxPressRecording = true + go a.StartRecording() + } + + if linuxPressTimer != nil { + linuxPressTimer.Stop() + } + + linuxPressTimer = time.AfterFunc(300*time.Millisecond, func() { + linuxPressMu.Lock() + defer linuxPressMu.Unlock() + if linuxPressRecording { + linuxPressRecording = false + go a.StopRecording() + } + }) + + w.WriteHeader(http.StatusNoContent) + }) + + go func() { + _ = http.ListenAndServe(linuxPressAddr, mux) + }() +} diff --git a/linux_press_daemon_stub.go b/linux_press_daemon_stub.go new file mode 100644 index 0000000..7d99f82 --- /dev/null +++ b/linux_press_daemon_stub.go @@ -0,0 +1,5 @@ +//go:build !linux + +package main + +func (a *App) startLinuxPressDaemon() {}