From ce49f66c899a9ba43443995dc69a8749d4d5dcc1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Jun 2026 15:14:30 -0700 Subject: [PATCH] fix: Add cross-platform handling for tray, updater, and icon generation - Add platform notes for tray startup, shortcut saving, and Linux-only settings - Handle stat error in processRecording (missing file) - Select correct binary name based on OS in updater - Increase tray icon canvas size from 64 to 128 pixels - Update frontend dist asset hash --- app.go | 20 ++++++- .../{index.bbf41643.js => index.91f83df4.js} | 4 +- frontend/dist/index.html | 2 +- internal/hotkey/hotkey.go | 2 + internal/linux/media.go | 7 +++ internal/linux/overlay.go | 7 +++ internal/linux/process.go | 7 +++ internal/linux/startup.go | 8 +++ internal/platform/platform.go | 8 +++ internal/platform/platform_linux.go | 7 +++ internal/platform/platform_windows.go | 7 +++ internal/services/whisper/whisper.go | 31 ++++++++--- internal/ui/tray/tray.go | 53 ++++++++++--------- internal/ui/tray/tray_linux.go | 6 +++ internal/ui/tray/tray_start_linux.go | 7 +++ internal/ui/tray/tray_start_nonlinux.go | 7 +++ internal/windows/media.go | 8 +++ internal/windows/overlay.go | 8 +++ internal/windows/process.go | 8 +++ internal/windows/startup.go | 8 +++ internal/xhotkey/hotkey_linux_portal.go | 7 +++ linux_press_daemon.go | 7 +++ linux_press_daemon_stub.go | 6 +++ main.go | 2 + main_instance_stub.go | 7 +++ main_instance_unix.go | 8 +++ text_insert_linux.go | 12 ++++- text_insert_nonlinux.go | 9 +++- text_insert_status_nonlinux.go | 6 +++ 29 files changed, 240 insertions(+), 39 deletions(-) rename frontend/dist/assets/{index.bbf41643.js => index.91f83df4.js} (82%) diff --git a/app.go b/app.go index fe74d3b..b1026bf 100644 --- a/app.go +++ b/app.go @@ -126,6 +126,9 @@ func (a *App) startup(ctx context.Context) { }) // Start system tray in a goroutine + // PLATFORM NOTE: On Linux, Wails' GTK main loop needs tray.Start() to be + // called synchronously (uses systray.Register). On Windows (and macOS), + // tray.Start() blocks for the Win32 message pump, so it must run in a goroutine. if runtime.GOOS == "linux" { tray.Start(a) } else { @@ -295,7 +298,15 @@ func (a *App) processRecording(recordingPath string) { // IDIOT-PROOFING: Ignore extremely short recordings (less than ~100ms or ~3KB) // that are likely accidental clicks or hardware glitches. stat, statErr := os.Stat(recordingPath) - if statErr == nil && stat.Size() < 4000 { + if statErr != nil { + logger.Error("Failed to stat recording file: %v", statErr) + if a.overlay != nil { + a.overlay.Hide() + } + tray.UpdateStatus("Ready") + return + } + if stat.Size() < 4000 { logger.Info("Discarding tiny recording (%d bytes)", stat.Size()) os.Remove(recordingPath) if a.overlay != nil { @@ -406,6 +417,9 @@ func (a *App) GetSettings() map[string]interface{} { conf["history"] = a.config.History conf["startup"] = platform.IsInStartup() conf["app_version"] = AppVersion + // PLATFORM NOTE: Linux-only settings — press daemon command and ydotool status. + // These are not included in the Windows build. See linux_press_daemon.go + // and text_insert_linux.go for the implementations. if runtime.GOOS == "linux" { if exePath, err := os.Executable(); err == nil { conf["linux_press_command"] = exePath + " --press" @@ -421,6 +435,10 @@ func (a *App) SaveSettings(settings map[string]interface{}) string { if val, ok := settings["api_key"].(string); ok { a.config.APIKey = val } + // PLATFORM NOTE: Shortcut saving is disabled on Linux because Linux uses + // the `--press` daemon approach (GNOME custom shortcuts) instead of the + // built-in hotkey listener. On Windows, we allow the user to configure + // the shortcut through the settings UI. if runtime.GOOS != "linux" { if val, ok := settings["shortcut"].(string); ok { _, _, modOnly, ok := hotkey.ParseShortcut(val) diff --git a/frontend/dist/assets/index.bbf41643.js b/frontend/dist/assets/index.91f83df4.js similarity index 82% rename from frontend/dist/assets/index.bbf41643.js rename to frontend/dist/assets/index.91f83df4.js index 4d80ce8..6be2ab0 100644 --- a/frontend/dist/assets/index.bbf41643.js +++ b/frontend/dist/assets/index.91f83df4.js @@ -1,4 +1,4 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const s of i)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 o(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerpolicy&&(s.referrerPolicy=i.referrerpolicy),i.crossorigin==="use-credentials"?s.credentials="include":i.crossorigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(i){if(i.ep)return;i.ep=!0;const s=o(i);fetch(i.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"),o=a=>a.metaKey||a.key==="Meta"||a.key==="OS"||a.code==="MetaLeft"||a.code==="MetaRight";e.textContent="Recording...",e.disabled=!0,t.value="";let n="";const i=a=>{a.preventDefault(),a.stopPropagation();const c=[];a.ctrlKey&&c.push("ctrl"),a.altKey&&c.push("alt"),a.shiftKey&&c.push("shift"),o(a)&&c.push("win");let l=a.key.toLowerCase();l==="control"&&(l="ctrl"),l==="alt"&&(l="alt"),l==="shift"&&(l="shift"),(l==="meta"||l==="os")&&(l="win"),(a.code==="MetaLeft"||a.code==="MetaRight")&&(l="win"),l===" "&&(l="space");const f=new Set(c);f.add(l);const g=Array.from(f).filter(w=>w!==l);g.push(l),n=g.join("+"),t.value=n,["ctrl","alt","shift","win"].includes(l)||r()},s=a=>{a.preventDefault(),a.stopPropagation(),n&&(n.split("+").every(l=>["ctrl","alt","shift","win"].includes(l))&&n.includes("+")?setTimeout(r,100):!a.ctrlKey&&!a.altKey&&!a.shiftKey&&!o(a)&&r())},r=()=>{if(!n){e.textContent="Record",e.disabled=!1,y();return}window.go.main.App.SaveSettings({shortcut:n}).then(a=>{a&&a.indexOf("Invalid shortcut")!==-1&&alert(a),e.textContent="Record",e.disabled=!1,y()})},y=()=>{window.removeEventListener("keydown",i,!0),window.removeEventListener("keyup",s,!0)};window.addEventListener("keydown",i,!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(o=>{const n=document.createElement("div");n.className="history-item";const i=document.createElement("span");i.className="history-time",i.textContent=new Date(o.timestamp).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});const s=document.createElement("div");s.textContent=o.text,n.appendChild(i),n.appendChild(s),t.appendChild(n)})}function h(e){const t=document.getElementById("linuxYdotoolStatus");if(!t||!e)return;const o=!!e.ready;t.style.display="block",t.style.background=o?"var(--green-dim)":"var(--red-dim)",t.style.color=o?"var(--green)":"var(--red)",t.style.border=o?"1px solid rgba(61, 186, 110, 0.25)":"1px solid rgba(224, 82, 82, 0.25)",t.innerHTML="";const n=document.createElement("div");n.style.fontWeight="600",n.textContent=o?"Automatic paste ready":"Automatic paste needs ydotool setup",t.appendChild(n);const i=document.createElement("div");if(i.style.marginTop="4px",i.textContent=e.message||"",t.appendChild(i),!o&&Array.isArray(e.setup_commands)&&e.setup_commands.length){const s=document.createElement("pre");s.style.whiteSpace="pre-wrap",s.style.margin="8px 0 0",s.style.color="inherit",s.textContent=e.setup_commands.join(` -`),t.appendChild(s);const r=document.createElement("div");r.style.marginTop="8px",r.style.fontWeight="600",r.textContent="Restart your computer after setup, then open WIS Free V3 again.",t.appendChild(r)}}async function v(){try{const e=await window.go.main.App.GetSettings();m(e.history||[])}catch(e){console.error("Failed to refresh history:",e)}}async function E(){try{const e=await window.go.main.App.GetSettings();document.getElementById("apiKey").value=e.api_key||"";const t=document.getElementById("shortcutInput");if(t&&(t.value=e.shortcut||"alt+z"),e.linux_press_mode){const n=document.getElementById("linuxPressSection"),i=document.getElementById("linuxPressCommand");n&&(n.style.display="block"),i&&(i.value=e.linux_press_command||""),h(e.linux_ydotool_status)}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 o=e.app_version&&String(e.app_version).trim()||"dev";document.getElementById("appVersion").textContent=o==="dev"?"dev":"v"+o,m(e.history||[])}catch(e){console.error("Failed to load settings:",e)}}async function I(){try{const e=await window.go.main.App.GetMicrophones(),t=document.getElementById("micDevice");e.forEach(o=>{if(o.index!==-1){const n=document.createElement("option");n.value=o.index,n.textContent=o.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}),d()};function d(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}),d()};window.saveAiModel=async function(){await window.go.main.App.SaveSettings({ai_model:document.getElementById("aiModel").value}),d()};window.saveLanguage=async function(){await window.go.main.App.SaveSettings({language:document.getElementById("language").value}),d()};window.saveMicDevice=async function(){const e=document.getElementById("micDevice").value;await window.go.main.App.SaveSettings({microphone_device:e==="default"?null:parseInt(e)}),d()};window.savePrompt=async function(){await window.go.main.App.SaveSettings({ai_prompt:document.getElementById("aiPrompt").value.trim()}),d("Prompt saved")};window.copyLinuxPressCommand=async function(){const e=document.getElementById("linuxPressCommand");if(!e)return;const t=e.value||"";try{navigator.clipboard&&navigator.clipboard.writeText?await navigator.clipboard.writeText(t):(e.focus(),e.select(),document.execCommand("copy")),d("Command copied")}catch(o){console.error("Failed to copy Linux command:",o)}};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 x(){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 o=document.getElementById("aiModel");for(let n of o.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"),o=t.querySelector('option[value^="local-"]');if(o&&o.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 o=await window.go.main.App.InstallWhisper(e);alert(o+` +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const l of i)if(l.type==="childList")for(const r of l.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&n(r)}).observe(document,{childList:!0,subtree:!0});function o(i){const l={};return i.integrity&&(l.integrity=i.integrity),i.referrerpolicy&&(l.referrerPolicy=i.referrerpolicy),i.crossorigin==="use-credentials"?l.credentials="include":i.crossorigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function n(i){if(i.ep)return;i.ep=!0;const l=o(i);fetch(i.href,l)}})();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"),o=a=>a.metaKey||a.key==="Meta"||a.key==="OS"||a.code==="MetaLeft"||a.code==="MetaRight";e.textContent="Recording...",e.disabled=!0,t.value="";let n="";const i=a=>{a.preventDefault(),a.stopPropagation();const c=[];a.ctrlKey&&c.push("ctrl"),a.altKey&&c.push("alt"),a.shiftKey&&c.push("shift"),o(a)&&c.push("win");let s=a.key.toLowerCase();s==="control"&&(s="ctrl"),s==="alt"&&(s="alt"),s==="shift"&&(s="shift"),(s==="meta"||s==="os")&&(s="win"),(a.code==="MetaLeft"||a.code==="MetaRight")&&(s="win"),s===" "&&(s="space");const f=new Set(c);f.add(s);const g=Array.from(f).filter(w=>w!==s);g.push(s),n=g.join("+"),t.value=n,["ctrl","alt","shift","win"].includes(s)||r()},l=a=>{a.preventDefault(),a.stopPropagation(),n&&(n.split("+").every(s=>["ctrl","alt","shift","win"].includes(s))&&n.includes("+")?setTimeout(r,100):!a.ctrlKey&&!a.altKey&&!a.shiftKey&&!o(a)&&r())},r=()=>{if(!n){e.textContent="Record",e.disabled=!1,y();return}window.go.main.App.SaveSettings({shortcut:n}).then(a=>{a&&a.indexOf("Invalid shortcut")!==-1&&alert(a),e.textContent="Record",e.disabled=!1,y()})},y=()=>{window.removeEventListener("keydown",i,!0),window.removeEventListener("keyup",l,!0)};window.addEventListener("keydown",i,!0),window.addEventListener("keyup",l,!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(o=>{const n=document.createElement("div");n.className="history-item";const i=document.createElement("span");i.className="history-time",i.textContent=new Date(o.timestamp).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});const l=document.createElement("div");l.textContent=o.text,n.appendChild(i),n.appendChild(l),t.appendChild(n)})}function h(e){const t=document.getElementById("linuxYdotoolStatus");if(!t||!e)return;const o=!!e.ready;t.style.display="block",t.style.background=o?"var(--green-dim)":"var(--red-dim)",t.style.color=o?"var(--green)":"var(--red)",t.style.border=o?"1px solid rgba(61, 186, 110, 0.25)":"1px solid rgba(224, 82, 82, 0.25)",t.innerHTML="";const n=document.createElement("div");n.style.fontWeight="600",n.textContent=o?"Direct typing ready":"Direct typing needs ydotool setup",t.appendChild(n);const i=document.createElement("div");if(i.style.marginTop="4px",i.textContent=e.message||"",t.appendChild(i),!o&&Array.isArray(e.setup_commands)&&e.setup_commands.length){const l=document.createElement("pre");l.style.whiteSpace="pre-wrap",l.style.margin="8px 0 0",l.style.color="inherit",l.textContent=e.setup_commands.join(` +`),t.appendChild(l);const r=document.createElement("div");r.style.marginTop="8px",r.style.fontWeight="600",r.textContent="Restart your computer after setup, then open WIS Free V3 again.",t.appendChild(r)}}async function v(){try{const e=await window.go.main.App.GetSettings();m(e.history||[])}catch(e){console.error("Failed to refresh history:",e)}}async function E(){try{const e=await window.go.main.App.GetSettings();document.getElementById("apiKey").value=e.api_key||"";const t=document.getElementById("shortcutInput");if(t&&(t.value=e.shortcut||"alt+z"),e.linux_press_mode){const n=document.getElementById("linuxPressSection"),i=document.getElementById("linuxPressCommand");n&&(n.style.display="block"),i&&(i.value=e.linux_press_command||""),h(e.linux_ydotool_status)}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 o=e.app_version&&String(e.app_version).trim()||"dev";document.getElementById("appVersion").textContent=o==="dev"?"dev":"v"+o,m(e.history||[])}catch(e){console.error("Failed to load settings:",e)}}async function I(){try{const e=await window.go.main.App.GetMicrophones(),t=document.getElementById("micDevice");e.forEach(o=>{if(o.index!==-1){const n=document.createElement("option");n.value=o.index,n.textContent=o.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}),d()};function d(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}),d()};window.saveAiModel=async function(){await window.go.main.App.SaveSettings({ai_model:document.getElementById("aiModel").value}),d()};window.saveLanguage=async function(){await window.go.main.App.SaveSettings({language:document.getElementById("language").value}),d()};window.saveMicDevice=async function(){const e=document.getElementById("micDevice").value;await window.go.main.App.SaveSettings({microphone_device:e==="default"?null:parseInt(e)}),d()};window.savePrompt=async function(){await window.go.main.App.SaveSettings({ai_prompt:document.getElementById("aiPrompt").value.trim()}),d("Prompt saved")};window.copyLinuxPressCommand=async function(){const e=document.getElementById("linuxPressCommand");if(!e)return;const t=e.value||"";try{navigator.clipboard&&navigator.clipboard.writeText?await navigator.clipboard.writeText(t):(e.focus(),e.select(),document.execCommand("copy")),d("Command copied")}catch(o){console.error("Failed to copy Linux command:",o)}};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 x(){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 o=document.getElementById("aiModel");for(let n of o.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"),o=t.querySelector('option[value^="local-"]');if(o&&o.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 o=await window.go.main.App.InstallWhisper(e);alert(o+` A terminal window will open showing the installation progress. This may take several minutes depending on your internet speed.`),setTimeout(p,5e3)}catch(o){alert("Installation failed: "+o)}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 o=document.getElementById("startupLabel");o&&(o.innerText="Run on Startup")}}catch(e){console.error("Failed to get environment:",e)}if(await E(),await I(),await x(),await p(),window.runtime&&typeof window.runtime.EventsOn=="function"&&(window.runtime.EventsOn("history:updated",function(){v()}),window.runtime.EventsOn("startup:changed",function(e){document.getElementById("startupToggle").checked=e})),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 5fa34e9..152239f 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -6,7 +6,7 @@ Wisp Settings - + diff --git a/internal/hotkey/hotkey.go b/internal/hotkey/hotkey.go index 8ea0393..44302e0 100644 --- a/internal/hotkey/hotkey.go +++ b/internal/hotkey/hotkey.go @@ -83,6 +83,8 @@ func (l *Listener) Start() { } if modOnly { + // WINDOWS-ONLY FEATURE: Modifier-only shortcuts (e.g. ctrl+win without a key) + // are only supported on Windows via modifier polling. On Linux, this is rejected. if runtime.GOOS != "windows" { logger.Error("Modifier-only shortcuts like %q are only supported on Windows", l.shortcut) l.mu.Unlock() diff --git a/internal/linux/media.go b/internal/linux/media.go index 282028b..023579e 100644 --- a/internal/linux/media.go +++ b/internal/linux/media.go @@ -1,4 +1,11 @@ //go:build linux + +// ============================================================ +// LINUX-ONLY FILE — Linux media playback management. +// Uses playerctl to pause/resume media during recording. +// The Windows equivalent is in internal/windows/ +// ============================================================ + package linux import ( diff --git a/internal/linux/overlay.go b/internal/linux/overlay.go index a2b373e..bbe56de 100644 --- a/internal/linux/overlay.go +++ b/internal/linux/overlay.go @@ -1,5 +1,12 @@ //go:build linux +// ============================================================ +// LINUX-ONLY FILE — No-op overlay implementation for Linux. +// On Linux, the overlay is intentionally disabled (no desktop +// notifications). The Windows equivalent with full Win32 overlay +// is in internal/windows/overlay.go +// ============================================================ + package linux // linuxOverlay intentionally does not show system notifications. Tray status diff --git a/internal/linux/process.go b/internal/linux/process.go index 65b70af..a392b8c 100644 --- a/internal/linux/process.go +++ b/internal/linux/process.go @@ -1,4 +1,11 @@ //go:build linux + +// ============================================================ +// LINUX-ONLY FILE — Linux process management utilities. +// Uses syscall.Kill(pid, 0) to check if a process is running. +// The Windows equivalent is in internal/windows/process.go +// ============================================================ + package linux import "syscall" diff --git a/internal/linux/startup.go b/internal/linux/startup.go index 56e94db..20b9ee5 100644 --- a/internal/linux/startup.go +++ b/internal/linux/startup.go @@ -1,4 +1,12 @@ //go:build linux + +// ============================================================ +// LINUX-ONLY FILE — Linux startup (autostart) management via +// XDG Desktop Entry files (.desktop). Creates/removes autostart +// entries and manages the application's .desktop file. +// The Windows equivalent is in internal/windows/ +// ============================================================ + package linux import ( diff --git a/internal/platform/platform.go b/internal/platform/platform.go index 9d4979f..870bd37 100644 --- a/internal/platform/platform.go +++ b/internal/platform/platform.go @@ -1,3 +1,11 @@ +// ============================================================ +// CROSS-PLATFORM FILE — This defines the Overlay interface +// that is implemented differently on each platform: +// - Windows: internal/windows/overlay.go (Win32 overlay) +// - Linux: internal/linux/overlay.go (no-op) +// This file itself is compiled on ALL platforms. +// ============================================================ + package platform // Overlay defines the cross-platform interface for screen overlays diff --git a/internal/platform/platform_linux.go b/internal/platform/platform_linux.go index 1ca1003..47acb09 100644 --- a/internal/platform/platform_linux.go +++ b/internal/platform/platform_linux.go @@ -1,4 +1,11 @@ //go:build linux + +// ============================================================ +// LINUX-ONLY FILE — Delegates all platform operations to the +// internal/linux package (overlay, media, startup, process). +// The Windows equivalent is platform_windows.go +// ============================================================ + package platform import "wis-free-v3/internal/linux" diff --git a/internal/platform/platform_windows.go b/internal/platform/platform_windows.go index 9ddbb81..cd52f30 100644 --- a/internal/platform/platform_windows.go +++ b/internal/platform/platform_windows.go @@ -1,4 +1,11 @@ //go:build windows + +// ============================================================ +// WINDOWS-ONLY FILE — Delegates all platform operations to the +// internal/windows package (overlay, media, startup, process). +// The Linux equivalent is platform_linux.go +// ============================================================ + package platform import "wis-free-v3/internal/windows" diff --git a/internal/services/whisper/whisper.go b/internal/services/whisper/whisper.go index ebf16e6..b434ce6 100644 --- a/internal/services/whisper/whisper.go +++ b/internal/services/whisper/whisper.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "time" @@ -115,7 +116,13 @@ func (m *Manager) Install(model string) error { return fmt.Errorf("failed to create install directory: %w", err) } - // Create install script + if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { + // On Linux/macOS, the automatic installer is not available. + // Users should install whisper.cpp from their package manager or build from source. + return fmt.Errorf("automatic whisper installation is not available on %s; install whisper.cpp from your package manager (e.g. 'sudo apt install whisper-cpp' or 'brew install whisper-cpp') or build from source at https://github.com/ggerganov/whisper.cpp, then place the binary and model in %s", runtime.GOOS, m.installDir) + } + + // Create install script (Windows batch file) scriptPath := filepath.Join(m.installDir, "install.bat") script := m.generateInstallScript(model) if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil { @@ -363,11 +370,18 @@ func (m *Manager) getBinaryPath() string { // whisper.cpp extracts to a Release subdirectory releaseDir := filepath.Join(m.installDir, "Release") - // Try different possible binary names - // whisper-cli.exe is the new standard (main.exe is deprecated) + // Try different possible binary names (platform-aware) + // On Windows: whisper-cli.exe / main.exe + // On Linux/macOS: whisper-cli / main possibleNames := []string{ - "whisper-cli.exe", - "main.exe", + "whisper-cli", + "main", + } + if runtime.GOOS == "windows" { + possibleNames = []string{ + "whisper-cli.exe", + "main.exe", + } } // First check in Release subdirectory @@ -386,8 +400,11 @@ func (m *Manager) getBinaryPath() string { } } - // Default to Release/main.exe - return filepath.Join(releaseDir, "main.exe") + // Default path + if runtime.GOOS == "windows" { + return filepath.Join(releaseDir, "main.exe") + } + return filepath.Join(releaseDir, "main") } // CheckOnline checks if internet is available diff --git a/internal/ui/tray/tray.go b/internal/ui/tray/tray.go index efc0e80..a809b6d 100644 --- a/internal/ui/tray/tray.go +++ b/internal/ui/tray/tray.go @@ -279,52 +279,55 @@ func initDynamicIcons() { } func createMicPNG(c color.Color) []byte { - img := image.NewRGBA(image.Rect(0, 0, 64, 64)) + const S = 128 // canvas size + img := image.NewRGBA(image.Rect(0, 0, S, S)) // All pixels are transparent by default (new RGBA starts with 0 alpha). + cx, cy := S/2, S/2 // center (64, 50) + // Let's draw the microphone parts - for y := 0; y < 64; y++ { - for x := 0; x < 64; x++ { + for y := 0; y < S; y++ { + for x := 0; x < S; x++ { drawPixel := false // 1. Capsule Body (Rounded Rectangle) - // Capsule center is X=32, Y=25. Width=14 (radius 7), height of straight part = 10 (Y from 20 to 30) - if x >= 25 && x <= 39 && y >= 20 && y <= 30 { + // Capsule center is X=64, Y=50. Width=28 (radius 14), height of straight part = 20 (Y from 40 to 60) + if x >= 50 && x <= 78 && y >= 40 && y <= 60 { drawPixel = true - } else if y < 20 { - // Top cap: center (32, 20), radius 7 - dx := float64(x - 32) - dy := float64(y - 20) - if dx*dx+dy*dy <= 49 { // 7^2 + } else if y < 40 { + // Top cap: center (64, 40), radius 14 + dx := float64(x - cx) + dy := float64(y - 40) + if dx*dx+dy*dy <= 196 { // 14^2 drawPixel = true } - } else if y > 30 && y <= 37 { - // Bottom cap: center (32, 30), radius 7 - dx := float64(x - 32) - dy := float64(y - 30) - if dx*dx+dy*dy <= 49 { + } else if y > 60 && y <= 74 { + // Bottom cap: center (64, 60), radius 14 + dx := float64(x - cx) + dy := float64(y - 60) + if dx*dx+dy*dy <= 196 { drawPixel = true } } // 2. U-stand - // Center of U-stand circle is (32, 25). - // Outer radius = 15, inner radius = 12 (thickness 3) - // Only draw for Y >= 25 and Y <= 40 - dx := float64(x - 32) - dy := float64(y - 25) + // Center of U-stand circle is (64, 50). + // Outer radius = 30, inner radius = 24 (thickness 6) + // Only draw for Y >= 50 and Y <= 80 + dx := float64(x - cx) + dy := float64(y - cy) distSq := dx*dx + dy*dy - if y >= 25 && y <= 40 && distSq >= 144 && distSq <= 225 { // 12^2 to 15^2 + if y >= 50 && y <= 80 && distSq >= 576 && distSq <= 900 { // 24^2 to 30^2 drawPixel = true } - // 3. Stem (Vertical line from Y=40 to 50, X=31 to 33) - if x >= 31 && x <= 33 && y >= 40 && y <= 50 { + // 3. Stem (Vertical line from Y=80 to 100, X=62 to 66) + if x >= 62 && x <= 66 && y >= 80 && y <= 100 { drawPixel = true } - // 4. Base (Horizontal line at Y=50 to 52, X=20 to 44) - if x >= 20 && x <= 44 && y >= 50 && y <= 52 { + // 4. Base (Horizontal line at Y=100 to 104, X=40 to 88) + if x >= 40 && x <= 88 && y >= 100 && y <= 104 { drawPixel = true } diff --git a/internal/ui/tray/tray_linux.go b/internal/ui/tray/tray_linux.go index 8c9bce5..ec7adb1 100644 --- a/internal/ui/tray/tray_linux.go +++ b/internal/ui/tray/tray_linux.go @@ -1,4 +1,10 @@ //go:build linux + +// ============================================================ +// LINUX-ONLY FILE — Sets the GTK program name for the system tray +// on Linux. This is required for proper desktop integration. +// ============================================================ + package tray /* diff --git a/internal/ui/tray/tray_start_linux.go b/internal/ui/tray/tray_start_linux.go index 8f5a6d5..1869c71 100644 --- a/internal/ui/tray/tray_start_linux.go +++ b/internal/ui/tray/tray_start_linux.go @@ -1,5 +1,12 @@ //go:build linux +// ============================================================ +// LINUX-ONLY FILE — Tray startup for Linux. On Linux, Wails' +// GTK main loop is already running, so we use systray.Register +// instead of systray.Run (which would start a second gtk_main). +// The Windows equivalent is tray_start_nonlinux.go +// ============================================================ + package tray import "github.com/getlantern/systray" diff --git a/internal/ui/tray/tray_start_nonlinux.go b/internal/ui/tray/tray_start_nonlinux.go index 6c0828f..a220956 100644 --- a/internal/ui/tray/tray_start_nonlinux.go +++ b/internal/ui/tray/tray_start_nonlinux.go @@ -1,5 +1,12 @@ //go:build !linux +// ============================================================ +// WINDOWS-ONLY FILE — Tray startup for Windows (and macOS). +// Uses systray.Run which blocks and runs the Win32 message pump. +// The Linux equivalent is tray_start_linux.go which uses +// systray.Register to integrate with the existing GTK loop. +// ============================================================ + package tray import ( diff --git a/internal/windows/media.go b/internal/windows/media.go index 2bf541e..0cfef21 100644 --- a/internal/windows/media.go +++ b/internal/windows/media.go @@ -1,4 +1,12 @@ //go:build windows + +// ============================================================ +// WINDOWS-ONLY FILE — Windows media playback management. +// Uses Win32 keybd_event to send VK_MEDIA_PLAY_PAUSE and a +// PowerShell script to check media playback state via SMTC. +// The Linux equivalent is internal/linux/media.go (playerctl) +// ============================================================ + package windows import ( diff --git a/internal/windows/overlay.go b/internal/windows/overlay.go index ca2ae55..65ebe29 100644 --- a/internal/windows/overlay.go +++ b/internal/windows/overlay.go @@ -1,4 +1,12 @@ //go:build windows + +// ============================================================ +// WINDOWS-ONLY FILE — Native Windows overlay window using +// Win32 API directly (CreateWindowEx, GDI drawing, etc.). +// This shows a pill-shaped overlay on screen during recording. +// The Linux equivalent is internal/linux/overlay.go (no-op). +// ============================================================ + package windows import ( diff --git a/internal/windows/process.go b/internal/windows/process.go index 6cfe8d4..d58b2c8 100644 --- a/internal/windows/process.go +++ b/internal/windows/process.go @@ -1,4 +1,12 @@ //go:build windows + +// ============================================================ +// WINDOWS-ONLY FILE — Windows process management utilities. +// Uses Win32 API (OpenProcess, GetExitCodeProcess) to check +// if a process is running. The Linux equivalent for process +// checking is in internal/linux/process.go +// ============================================================ + package windows import ( diff --git a/internal/windows/startup.go b/internal/windows/startup.go index 01a38d3..f089d41 100644 --- a/internal/windows/startup.go +++ b/internal/windows/startup.go @@ -1,4 +1,12 @@ //go:build windows + +// ============================================================ +// WINDOWS-ONLY FILE — Windows startup (autostart) management +// via the Windows Registry (HKCU\Software\Microsoft\Windows\ +// CurrentVersion\Run). The Linux equivalent for autostart +// management is internal/linux/startup.go (XDG .desktop files) +// ============================================================ + package windows import ( diff --git a/internal/xhotkey/hotkey_linux_portal.go b/internal/xhotkey/hotkey_linux_portal.go index dcca8d4..1e8fc12 100644 --- a/internal/xhotkey/hotkey_linux_portal.go +++ b/internal/xhotkey/hotkey_linux_portal.go @@ -1,5 +1,12 @@ //go:build linux +// ============================================================ +// LINUX-ONLY FILE — Portal-based global hotkey implementation +// for Linux using the org.freedesktop.portal.GlobalShortcuts +// D-Bus API (Wayland/desktop-agnostic). +// The Windows equivalent is in hotkey_windows.go +// ============================================================ + package hotkey import ( diff --git a/linux_press_daemon.go b/linux_press_daemon.go index 9465ec1..d139daa 100644 --- a/linux_press_daemon.go +++ b/linux_press_daemon.go @@ -1,5 +1,12 @@ //go:build linux +// ============================================================ +// LINUX-ONLY FILE — Local HTTP daemon that receives hotkey +// press/release pings from the GNOME custom shortcut helper. +// This is the Linux equivalent of the Windows hotkey listener. +// Any changes here will NOT affect the Windows build. +// ============================================================ + package main import ( diff --git a/linux_press_daemon_stub.go b/linux_press_daemon_stub.go index 7d99f82..f73435d 100644 --- a/linux_press_daemon_stub.go +++ b/linux_press_daemon_stub.go @@ -1,5 +1,11 @@ //go:build !linux +// ============================================================ +// WINDOWS-ONLY FILE — Stub for the Linux press daemon. +// On Windows, the press daemon is not used. +// The real implementation is in linux_press_daemon.go +// ============================================================ + package main func (a *App) startLinuxPressDaemon() {} diff --git a/main.go b/main.go index 4d14d2f..50853f6 100644 --- a/main.go +++ b/main.go @@ -87,6 +87,8 @@ func main() { OnBeforeClose: app.beforeClose, StartHidden: true, Bind: []interface{}{app}, + // PLATFORM NOTE: Linux-specific Wails options (sets the program name + // for desktop integration). These options are only applied on Linux. Linux: &linux.Options{ ProgramName: "wis-free-v3", }, diff --git a/main_instance_stub.go b/main_instance_stub.go index 3bf8331..6a0f094 100644 --- a/main_instance_stub.go +++ b/main_instance_stub.go @@ -1,5 +1,12 @@ //go:build windows +// ============================================================ +// WINDOWS-ONLY FILE — Stub for the Unix domain socket IPC. +// On Windows, single-instance enforcement uses a lock file +// (see main.go), and helper invocations don't use Unix sockets. +// The real Unix implementation is in main_instance_unix.go +// ============================================================ + package main // secondInstanceWake is unused on Windows (second-instance UX not wired here). diff --git a/main_instance_unix.go b/main_instance_unix.go index a49ab56..65ca712 100644 --- a/main_instance_unix.go +++ b/main_instance_unix.go @@ -1,5 +1,13 @@ //go:build unix && !windows +// ============================================================ +// UNIX-ONLY FILE — This file compiles on Linux (and macOS) but +// NOT on Windows. It provides Unix domain socket support for +// single-instance enforcement and IPC communication with +// helper processes (e.g. GNOME custom shortcuts). +// The Windows equivalent stub is main_instance_stub.go +// ============================================================ + package main import ( diff --git a/text_insert_linux.go b/text_insert_linux.go index a60db02..3d340ec 100644 --- a/text_insert_linux.go +++ b/text_insert_linux.go @@ -1,5 +1,11 @@ //go:build linux +// ============================================================ +// LINUX-ONLY FILE — This file compiles ONLY on Linux. +// Any changes here will NOT affect the Windows build. +// For the Windows equivalent, see text_insert_nonlinux.go +// ============================================================ + package main import ( @@ -36,11 +42,11 @@ func (a *App) insertTranscription(text string) { func (a *App) releaseLinuxInputFocus() { if a.ctx == nil { - time.Sleep(100 * time.Millisecond) + time.Sleep(30 * time.Millisecond) return } wailsruntime.WindowHide(a.ctx) - time.Sleep(150 * time.Millisecond) + time.Sleep(50 * time.Millisecond) } func typeLinuxTextWithYdotool(text string) error { @@ -114,6 +120,8 @@ func linuxYdotoolStatus() map[string]interface{} { status["message"] = "ydotool is ready for direct typing." return status } + +func getYdotoolSocketPath() (string, error) { if socketPath := strings.TrimSpace(os.Getenv("YDOTOOL_SOCKET")); socketPath != "" { if _, err := os.Stat(socketPath); err == nil { return socketPath, nil diff --git a/text_insert_nonlinux.go b/text_insert_nonlinux.go index fd8db1d..8a1e2fc 100644 --- a/text_insert_nonlinux.go +++ b/text_insert_nonlinux.go @@ -1,5 +1,12 @@ //go:build !linux +// ============================================================ +// WINDOWS-ONLY FILE — This file compiles on Windows (and macOS) +// but NOT on Linux. It uses robotgo for clipboard/paste which +// is Windows-specific in this app. The Linux equivalent is +// text_insert_linux.go which uses ydotool instead. +// ============================================================ + package main import ( @@ -37,6 +44,6 @@ func (a *App) insertTranscription(text string) { // pasteText simulates Ctrl+V to paste from clipboard func (a *App) pasteText() { - time.Sleep(200 * time.Millisecond) + time.Sleep(50 * time.Millisecond) robotgo.KeyTap("v", "ctrl") } diff --git a/text_insert_status_nonlinux.go b/text_insert_status_nonlinux.go index 8c23e2f..bfea564 100644 --- a/text_insert_status_nonlinux.go +++ b/text_insert_status_nonlinux.go @@ -1,5 +1,11 @@ //go:build !linux +// ============================================================ +// WINDOWS-ONLY FILE — Stub for the Linux ydotool status check. +// On Windows, ydotool is not used, so this returns "not ready". +// The real implementation is in text_insert_linux.go +// ============================================================ + package main func linuxYdotoolStatus() map[string]interface{} {