mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
kind of working finally
This commit is contained in:
@@ -20,7 +20,6 @@ import (
|
||||
|
||||
"github.com/go-vgo/robotgo"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
// App struct
|
||||
@@ -105,8 +104,7 @@ func (a *App) StartRecording() {
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
a.recordingPath = filepath.Join(tempDir, fmt.Sprintf("wis_recording_%s.wav", timestamp))
|
||||
|
||||
// Start recording (now faster due to device reuse)
|
||||
go func() {
|
||||
// Start recording
|
||||
err := a.audioRecorder.Start(a.recordingPath)
|
||||
if err != nil {
|
||||
logger.Error("Failed to start recording: %v", err)
|
||||
@@ -122,7 +120,6 @@ func (a *App) StartRecording() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 3. Handle secondary tasks in background
|
||||
go func() {
|
||||
@@ -241,7 +238,7 @@ func (a *App) processRecording() {
|
||||
config.Save(a.config, "")
|
||||
|
||||
// Copy to clipboard
|
||||
clipboard.Write(clipboard.FmtText, []byte(refinedText))
|
||||
runtime.ClipboardSetText(a.ctx, refinedText)
|
||||
|
||||
// Paste
|
||||
a.pasteText()
|
||||
@@ -260,13 +257,11 @@ func (a *App) processRecording() {
|
||||
|
||||
// pasteText simulates Ctrl+V to paste from clipboard
|
||||
func (a *App) pasteText() {
|
||||
// Give a small delay for clipboard to update
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// Give a substantial delay for Linux GTK clipboard sync
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Simulate Ctrl+V
|
||||
robotgo.KeyToggle("control", "down")
|
||||
robotgo.KeyTap("v")
|
||||
robotgo.KeyToggle("control", "up")
|
||||
// Simulate Ctrl+V using modern robotgo API
|
||||
robotgo.KeyTap("v", "ctrl")
|
||||
}
|
||||
|
||||
// GetSettings returns the current configuration
|
||||
@@ -291,8 +286,8 @@ func (a *App) SaveSettings(settings map[string]interface{}) string {
|
||||
}
|
||||
if val, ok := settings["shortcut"].(string); ok {
|
||||
// Validate shortcut before applying
|
||||
trigger, _ := hotkey.ParseShortcut(val)
|
||||
if len(trigger) == 0 {
|
||||
_, _, ok := hotkey.ParseShortcut(val)
|
||||
if !ok {
|
||||
logger.Error("Invalid shortcut: %s (rejected)", val)
|
||||
return "Invalid shortcut - must have at least one modifier and a regular key"
|
||||
}
|
||||
@@ -403,12 +398,6 @@ func (a *App) startupHeadless() {
|
||||
fmt.Printf("Failed to initialize logger: %v\n", err)
|
||||
}
|
||||
|
||||
// Initialize clipboard
|
||||
err = clipboard.Init()
|
||||
if err != nil {
|
||||
logger.Error("Failed to initialize clipboard: %v", err)
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
configPath, err := config.GetConfigPath()
|
||||
if err != nil {
|
||||
@@ -422,8 +411,10 @@ func (a *App) startupHeadless() {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize heavy components in background for instant startup
|
||||
go func() {
|
||||
// Initialize heavy components synchronously instead of in background.
|
||||
// This prevents a known ALSA/GTK initialization race condition bug on Linux
|
||||
// where Miniaudio and GTK try to probe audio devices concurrently resulting in SIGABRT.
|
||||
|
||||
// Initialize transcriber
|
||||
a.transcriber = transcriber.NewClient(
|
||||
a.config.APIKey,
|
||||
@@ -457,8 +448,7 @@ func (a *App) startupHeadless() {
|
||||
a.hotkeyListener = hotkey.NewListener(a.config.Shortcut, a.StartRecording, a.StopRecording)
|
||||
a.hotkeyListener.Start()
|
||||
|
||||
logger.Info("Background components initialized successfully!")
|
||||
}()
|
||||
logger.Info("Components initialized successfully!")
|
||||
|
||||
logger.Info("Basic app components loaded, continuing startup...")
|
||||
}
|
||||
|
||||
Regular → Executable
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
:root{--bg-color: #0b0f1a;--card-bg: rgba(30, 41, 59, .7);--input-bg: rgba(51, 65, 85, .5);--text-color: #f8fafc;--text-muted: #94a3b8;--primary-color: #3b82f6;--primary-hover: #2563eb;--accent-color: #6366f1;--border-color: rgba(75, 85, 99, .4);--success-color: #10b981;--glass-border: rgba(255, 255, 255, .1)}body{background:radial-gradient(circle at top right,#1e293b,#0b0f1a);color:var(--text-color);font-family:Inter,system-ui,-apple-system,sans-serif;margin:0;padding:30px;min-height:100vh;letter-spacing:-.01em}.container{max-width:720px;margin:0 auto}h1{font-size:28px;font-weight:700;background:linear-gradient(135deg,#60a5fa,#a78bfa);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:30px}.section{background:var(--card-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);padding:24px;border-radius:16px;margin-bottom:24px;border:1px solid var(--glass-border);box-shadow:0 10px 15px -3px #0003;transition:transform .2s ease}.section:hover{border-color:#fff3}label{display:block;color:var(--text-muted);font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px}input[type=text],input[type=password],select,textarea{width:100%;background:var(--input-bg);border:1px solid var(--border-color);color:#fff;padding:12px 14px;border-radius:10px;font-size:14px;transition:all .2s ease;box-sizing:border-box}textarea{resize:vertical;min-height:80px}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--primary-color);background:rgba(51,65,85,.8);box-shadow:0 0 0 3px #3b82f633}.form-control{max-width:480px;width:100%}.input-wrapper{position:relative;flex:1;min-width:0}.input-wrapper input{width:100%;padding-right:44px;box-sizing:border-box;display:block}.eye-btn{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:transparent!important;border:none!important;box-shadow:none!important;cursor:pointer;padding:4px!important;font-size:16px;opacity:.5;transition:opacity .2s;line-height:1;display:flex;align-items:center;justify-content:center}.eye-btn:hover{opacity:1;transform:translateY(-50%) scale(1.1)}.save-status{position:fixed;bottom:30px;right:30px;background:var(--success-color);color:#fff;padding:12px 24px;border-radius:12px;font-weight:600;box-shadow:0 10px 15px -3px #0003;transform:translateY(100px);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);z-index:1000}.save-status.show{transform:translateY(0);opacity:1}button{background-color:var(--primary-color);color:#fff;border:none;padding:10px 20px;border-radius:10px;cursor:pointer;font-weight:600;font-size:14px;transition:all .2s cubic-bezier(.4,0,.2,1);box-shadow:0 4px 6px -1px #0003;white-space:nowrap;flex-shrink:0}button:hover{transform:translateY(-1px);box-shadow:0 10px 15px -3px #0000004d;background-color:var(--primary-hover)}button:active{transform:translateY(0)}button[style*="background: transparent"]{background:transparent!important;box-shadow:none!important;text-decoration:underline;opacity:.7}button[style*="background: transparent"]:hover{opacity:1}.flex-row{display:flex;gap:12px;align-items:center;flex-wrap:nowrap}.flex-between{display:flex;justify-content:space-between;align-items:center}.history-list{max-height:250px;overflow-y:auto;margin-top:10px;padding-right:5px}.history-item{background:rgba(255,255,255,.03);padding:14px;border-radius:12px;margin-bottom:10px;border:1px solid rgba(255,255,255,.05);transition:all .2s ease}.history-item:hover{background:rgba(255,255,255,.06);transform:translate(2px)}.history-time{color:var(--primary-color);font-size:11px;font-weight:700;margin-bottom:6px;display:block}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:10px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
:root{--bg-color: #0b0f1a;--card-bg: rgba(30, 41, 59, .7);--input-bg: rgba(51, 65, 85, .5);--text-color: #f8fafc;--text-muted: #94a3b8;--primary-color: #3b82f6;--primary-hover: #2563eb;--accent-color: #6366f1;--border-color: rgba(75, 85, 99, .4);--success-color: #10b981;--glass-border: rgba(255, 255, 255, .1);color-scheme:dark}body{background:radial-gradient(circle at top right,#1e293b,#0b0f1a);color:var(--text-color);font-family:Inter,system-ui,-apple-system,sans-serif;margin:0;padding:30px;min-height:100vh;letter-spacing:-.01em}.container{max-width:720px;margin:0 auto}h1{font-size:28px;font-weight:700;background:linear-gradient(135deg,#60a5fa,#a78bfa);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:30px}.section{background:var(--card-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);padding:24px;border-radius:16px;margin-bottom:24px;border:1px solid var(--glass-border);box-shadow:0 10px 15px -3px #0003;transition:transform .2s ease}.section:hover{border-color:#fff3}label{display:block;color:var(--text-muted);font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px}input[type=checkbox]{accent-color:var(--primary-color);width:18px;height:18px;cursor:pointer}input[type=text],input[type=password],select,textarea{width:100%;background:var(--input-bg);border:1px solid var(--border-color);color:#fff;padding:12px 14px;border-radius:10px;font-size:14px;transition:all .2s ease;box-sizing:border-box}select{appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right 14px center;background-size:16px;padding-right:40px}select option{background:var(--bg-color);color:#fff}textarea{resize:vertical;min-height:80px}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--primary-color);background:rgba(51,65,85,.8);box-shadow:0 0 0 3px #3b82f633}.form-control{max-width:480px;width:100%}.input-wrapper{position:relative;flex:1;min-width:0}.input-wrapper input{width:100%;padding-right:44px;box-sizing:border-box;display:block}.eye-btn{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:transparent!important;border:none!important;box-shadow:none!important;cursor:pointer;padding:4px!important;font-size:16px;opacity:.5;transition:opacity .2s;line-height:1;display:flex;align-items:center;justify-content:center}.eye-btn:hover{opacity:1;transform:translateY(-50%) scale(1.1)}.save-status{position:fixed;bottom:30px;right:30px;background:var(--success-color);color:#fff;padding:12px 24px;border-radius:12px;font-weight:600;box-shadow:0 10px 15px -3px #0003;transform:translateY(100px);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);z-index:1000}.save-status.show{transform:translateY(0);opacity:1}button{background-color:var(--primary-color);color:#fff;border:none;padding:10px 20px;border-radius:10px;cursor:pointer;font-weight:600;font-size:14px;transition:all .2s cubic-bezier(.4,0,.2,1);box-shadow:0 4px 6px -1px #0003;white-space:nowrap;flex-shrink:0}button:hover{transform:translateY(-1px);box-shadow:0 10px 15px -3px #0000004d;background-color:var(--primary-hover)}button:active{transform:translateY(0)}button[style*="background: transparent"]{background:transparent!important;box-shadow:none!important;text-decoration:underline;opacity:.7}button[style*="background: transparent"]:hover{opacity:1}.flex-row{display:flex;gap:12px;align-items:center;flex-wrap:nowrap}.flex-between{display:flex;justify-content:space-between;align-items:center}.history-list{max-height:250px;overflow-y:auto;margin-top:10px;padding-right:5px}.history-item{background:rgba(255,255,255,.03);padding:14px;border-radius:12px;margin-bottom:10px;border:1px solid rgba(255,255,255,.05);transition:all .2s ease}.history-item:hover{background:rgba(255,255,255,.06);transform:translate(2px)}.history-time{color:var(--primary-color);font-size:11px;font-weight:700;margin-bottom:6px;display:block}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:10px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}
|
||||
Vendored
+4
-4
@@ -6,8 +6,8 @@
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<title>wis-free-v3 Settings</title>
|
||||
|
||||
<script type="module" crossorigin src="/assets/index.33b5a8c3.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.9197445a.css">
|
||||
<script type="module" crossorigin src="/assets/index.c6252eaf.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.d8635aeb.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -110,7 +110,7 @@
|
||||
|
||||
<!-- Startup -->
|
||||
<div class="section flex-between">
|
||||
<span style="font-weight: 500;">Run on Windows Startup</span>
|
||||
<span id="startupLabel" style="font-weight: 500;">Run on System Startup</span>
|
||||
<label class="flex-row" style="margin: 0; cursor: pointer;">
|
||||
<input type="checkbox" id="startupToggle" onchange="toggleStartup()" style="width: auto;">
|
||||
<span>Enable</span>
|
||||
@@ -130,7 +130,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Offline Mode -->
|
||||
<div class="section"
|
||||
<div id="offlineWhisperSection" class="section"
|
||||
style="margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border-color);">
|
||||
<label>Offline Whisper</label>
|
||||
|
||||
|
||||
+40
-6
@@ -108,7 +108,7 @@
|
||||
|
||||
<!-- Startup -->
|
||||
<div class="section flex-between">
|
||||
<span style="font-weight: 500;">Run on Windows Startup</span>
|
||||
<span id="startupLabel" style="font-weight: 500;">Run on System Startup</span>
|
||||
<label class="flex-row" style="margin: 0; cursor: pointer;">
|
||||
<input type="checkbox" id="startupToggle" onchange="toggleStartup()" style="width: auto;">
|
||||
<span>Enable</span>
|
||||
@@ -128,7 +128,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Offline Mode -->
|
||||
<div class="section"
|
||||
<div id="offlineWhisperSection" class="section"
|
||||
style="margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border-color);">
|
||||
<label>Offline Whisper</label>
|
||||
|
||||
@@ -489,10 +489,44 @@
|
||||
};
|
||||
|
||||
// Init
|
||||
loadSettings();
|
||||
loadMics();
|
||||
checkConnection();
|
||||
checkWhisperStatus();
|
||||
async function initialize() {
|
||||
// Check platform definitively using userAgent to guarantee it fires immediately without Wails timing issues
|
||||
try {
|
||||
const isLinux = navigator.userAgent.toLowerCase().includes('linux');
|
||||
if (isLinux) {
|
||||
// Hide Offline Whisper section
|
||||
const offlineSection = document.getElementById('offlineWhisperSection');
|
||||
if (offlineSection) {
|
||||
offlineSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Clean up 'Windows' reference in startup text specifically for Linux OS
|
||||
const startupLabel = document.getElementById('startupLabel');
|
||||
if (startupLabel) {
|
||||
startupLabel.innerText = 'Run on Linux OS Startup';
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to get environment:', err);
|
||||
}
|
||||
|
||||
await loadSettings();
|
||||
await loadMics();
|
||||
await checkConnection();
|
||||
await checkWhisperStatus();
|
||||
|
||||
// If Linux, ensure Local Whisper option is removed from Whisper Select as a secondary guard
|
||||
if (navigator.userAgent.toLowerCase().includes('linux')) {
|
||||
const whisperSelect = document.getElementById('whisperModel');
|
||||
if (whisperSelect) {
|
||||
Array.from(whisperSelect.options).forEach(opt => {
|
||||
if (opt.value.startsWith('local-')) opt.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initialize();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../esbuild/bin/esbuild" "$@"
|
||||
else
|
||||
exec node "$basedir/../esbuild/bin/esbuild" "$@"
|
||||
fi
|
||||
+1
@@ -0,0 +1 @@
|
||||
../esbuild/bin/esbuild
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esbuild\bin\esbuild" %*
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../esbuild/bin/esbuild" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../esbuild/bin/esbuild" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@"
|
||||
else
|
||||
exec node "$basedir/../nanoid/bin/nanoid.cjs" "$@"
|
||||
fi
|
||||
+1
@@ -0,0 +1 @@
|
||||
../nanoid/bin/nanoid.cjs
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nanoid\bin\nanoid.cjs" %*
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../resolve/bin/resolve" "$@"
|
||||
else
|
||||
exec node "$basedir/../resolve/bin/resolve" "$@"
|
||||
fi
|
||||
+1
@@ -0,0 +1 @@
|
||||
../resolve/bin/resolve
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\resolve\bin\resolve" %*
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../resolve/bin/resolve" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../resolve/bin/resolve" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../resolve/bin/resolve" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../resolve/bin/resolve" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../rollup/dist/bin/rollup" "$@"
|
||||
else
|
||||
exec node "$basedir/../rollup/dist/bin/rollup" "$@"
|
||||
fi
|
||||
+1
@@ -0,0 +1 @@
|
||||
../rollup/dist/bin/rollup
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rollup\dist\bin\rollup" %*
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../rollup/dist/bin/rollup" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../rollup/dist/bin/rollup" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../rollup/dist/bin/rollup" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../rollup/dist/bin/rollup" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../vite/bin/vite.js" "$@"
|
||||
fi
|
||||
+1
@@ -0,0 +1 @@
|
||||
../vite/bin/vite.js
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\vite\bin\vite.js" %*
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../vite/bin/vite.js" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../vite/bin/vite.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+4
-4
@@ -42,10 +42,10 @@
|
||||
"esbuild-windows-arm64": "0.15.18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-64": {
|
||||
"node_modules/esbuild-linux-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
|
||||
"integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
|
||||
"integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -53,7 +53,7 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# esbuild
|
||||
|
||||
This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
|
||||
Generated
Vendored
Regular → Executable
BIN
Binary file not shown.
Generated
Vendored
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "esbuild-windows-64",
|
||||
"name": "esbuild-linux-64",
|
||||
"version": "0.15.18",
|
||||
"description": "The Windows 64-bit binary for esbuild, a JavaScript bundler.",
|
||||
"description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.",
|
||||
"repository": "https://github.com/evanw/esbuild",
|
||||
"license": "MIT",
|
||||
"preferUnplugged": true,
|
||||
@@ -9,7 +9,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"os": [
|
||||
"win32"
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
# esbuild
|
||||
|
||||
This is the Windows 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Unfortunately even though npm shims "bin" commands on Windows with auto-
|
||||
// generated forwarding scripts, it doesn't strip the ".exe" from the file name
|
||||
// first. So it's possible to publish executables via npm on all platforms
|
||||
// except Windows. I consider this a npm bug.
|
||||
//
|
||||
// My workaround is to add this script as another layer of indirection. It'll
|
||||
// be slower because node has to boot up just to shell out to the actual exe,
|
||||
// but Windows is somewhat of a second-class platform to npm so it's the best
|
||||
// I can do I think.
|
||||
const esbuild_exe = require.resolve('esbuild-windows-64/esbuild.exe');
|
||||
const child_process = require('child_process');
|
||||
child_process.spawnSync(esbuild_exe, process.argv.slice(2), { stdio: 'inherit' });
|
||||
BIN
Binary file not shown.
@@ -1 +1 @@
|
||||
be0c7dc3573b2470ab6a8ec4c48845b6
|
||||
5fbf12469d224a93954efecb5886e8a6
|
||||
@@ -10,6 +10,7 @@
|
||||
--border-color: rgba(75, 85, 99, 0.4);
|
||||
--success-color: #10b981;
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -63,6 +64,13 @@ label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
accent-color: var(--primary-color);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
select,
|
||||
@@ -78,6 +86,21 @@ textarea {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 14px center;
|
||||
background-size: 16px;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
select option {
|
||||
background: var(--bg-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
|
||||
+26
-3
@@ -1,14 +1,37 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {config} from '../models';
|
||||
|
||||
export function CheckOnline():Promise<boolean>;
|
||||
|
||||
export function ClearHistory():Promise<void>;
|
||||
|
||||
export function GetMicrophones(): Promise<Array<{ [key: string]: any }>>;
|
||||
export function GetAvailableWhisperModels():Promise<Array<Record<string, string>>>;
|
||||
|
||||
export function GetSettings(): Promise<{ [key: string]: any }>;
|
||||
export function GetConfig():Promise<config.Config>;
|
||||
|
||||
export function GetMicrophones():Promise<Array<Record<string, any>>>;
|
||||
|
||||
export function GetSettings():Promise<Record<string, any>>;
|
||||
|
||||
export function GetWhisperInfo():Promise<Record<string, any>>;
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
|
||||
export function SaveSettings(arg1: { [key: string]: any }): Promise<string>;
|
||||
export function InstallWhisper(arg1:string):Promise<string>;
|
||||
|
||||
export function IsWhisperInstalled():Promise<boolean>;
|
||||
|
||||
export function Quit():Promise<void>;
|
||||
|
||||
export function SaveSettings(arg1:Record<string, any>):Promise<string>;
|
||||
|
||||
export function ShowSettings():Promise<void>;
|
||||
|
||||
export function StartRecording():Promise<void>;
|
||||
|
||||
export function StopRecording():Promise<void>;
|
||||
|
||||
export function ToggleStartup(arg1:boolean):Promise<string>;
|
||||
|
||||
export function UninstallWhisper():Promise<string>;
|
||||
|
||||
Regular → Executable
+45
-24
@@ -1,10 +1,23 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function CheckOnline() {
|
||||
return window['go']['main']['App']['CheckOnline']();
|
||||
}
|
||||
|
||||
export function ClearHistory() {
|
||||
return window['go']['main']['App']['ClearHistory']();
|
||||
}
|
||||
|
||||
export function GetAvailableWhisperModels() {
|
||||
return window['go']['main']['App']['GetAvailableWhisperModels']();
|
||||
}
|
||||
|
||||
export function GetConfig() {
|
||||
return window['go']['main']['App']['GetConfig']();
|
||||
}
|
||||
|
||||
export function GetMicrophones() {
|
||||
return window['go']['main']['App']['GetMicrophones']();
|
||||
}
|
||||
@@ -13,38 +26,46 @@ export function GetSettings() {
|
||||
return window['go']['main']['App']['GetSettings']();
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function SaveSettings(arg1) {
|
||||
return window['go']['main']['App']['SaveSettings'](arg1);
|
||||
}
|
||||
|
||||
export function ToggleStartup(arg1) {
|
||||
return window['go']['main']['App']['ToggleStartup'](arg1);
|
||||
}
|
||||
|
||||
export function CheckOnline() {
|
||||
return window['go']['main']['App']['CheckOnline']();
|
||||
}
|
||||
|
||||
export function IsWhisperInstalled() {
|
||||
return window['go']['main']['App']['IsWhisperInstalled']();
|
||||
}
|
||||
|
||||
export function GetWhisperInfo() {
|
||||
return window['go']['main']['App']['GetWhisperInfo']();
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function InstallWhisper(arg1) {
|
||||
return window['go']['main']['App']['InstallWhisper'](arg1);
|
||||
}
|
||||
|
||||
export function IsWhisperInstalled() {
|
||||
return window['go']['main']['App']['IsWhisperInstalled']();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
return window['go']['main']['App']['Quit']();
|
||||
}
|
||||
|
||||
export function SaveSettings(arg1) {
|
||||
return window['go']['main']['App']['SaveSettings'](arg1);
|
||||
}
|
||||
|
||||
export function ShowSettings() {
|
||||
return window['go']['main']['App']['ShowSettings']();
|
||||
}
|
||||
|
||||
export function StartRecording() {
|
||||
return window['go']['main']['App']['StartRecording']();
|
||||
}
|
||||
|
||||
export function StopRecording() {
|
||||
return window['go']['main']['App']['StopRecording']();
|
||||
}
|
||||
|
||||
export function ToggleStartup(arg1) {
|
||||
return window['go']['main']['App']['ToggleStartup'](arg1);
|
||||
}
|
||||
|
||||
export function UninstallWhisper() {
|
||||
return window['go']['main']['App']['UninstallWhisper']();
|
||||
}
|
||||
|
||||
export function GetAvailableWhisperModels() {
|
||||
return window['go']['main']['App']['GetAvailableWhisperModels']();
|
||||
}
|
||||
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
export namespace config {
|
||||
|
||||
export class HistoryItem {
|
||||
text: string;
|
||||
timestamp: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new HistoryItem(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.text = source["text"];
|
||||
this.timestamp = source["timestamp"];
|
||||
}
|
||||
}
|
||||
export class Config {
|
||||
api_key: string;
|
||||
shortcut: string;
|
||||
whisper_model: string;
|
||||
ai_model: string;
|
||||
ai_prompt: string;
|
||||
language: string;
|
||||
microphone_device?: number;
|
||||
history: HistoryItem[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Config(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.api_key = source["api_key"];
|
||||
this.shortcut = source["shortcut"];
|
||||
this.whisper_model = source["whisper_model"];
|
||||
this.ai_model = source["ai_model"];
|
||||
this.ai_prompt = source["ai_prompt"];
|
||||
this.language = source["language"];
|
||||
this.microphone_device = source["microphone_device"];
|
||||
this.history = this.convertValues(source["history"], HistoryItem);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+81
@@ -247,3 +247,84 @@ export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
|
||||
// Notification types
|
||||
export interface NotificationOptions {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string; // macOS and Linux only
|
||||
body?: string;
|
||||
categoryId?: string;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
id?: string;
|
||||
title?: string;
|
||||
destructive?: boolean; // macOS-specific
|
||||
}
|
||||
|
||||
export interface NotificationCategory {
|
||||
id?: string;
|
||||
actions?: NotificationAction[];
|
||||
hasReplyField?: boolean;
|
||||
replyPlaceholder?: string;
|
||||
replyButtonTitle?: string;
|
||||
}
|
||||
|
||||
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||
// Initializes the notification service for the application.
|
||||
// This must be called before sending any notifications.
|
||||
export function InitializeNotifications(): Promise<void>;
|
||||
|
||||
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||
// Cleans up notification resources and releases any held connections.
|
||||
export function CleanupNotifications(): Promise<void>;
|
||||
|
||||
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||
// Checks if notifications are available on the current platform.
|
||||
export function IsNotificationAvailable(): Promise<boolean>;
|
||||
|
||||
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||
// Requests notification authorization from the user (macOS only).
|
||||
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||
// Checks the current notification authorization status (macOS only).
|
||||
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||
// Sends a basic notification with the given options.
|
||||
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||
// Sends a notification with action buttons. Requires a registered category.
|
||||
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||
|
||||
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||
// Removes a previously registered notification category.
|
||||
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||
|
||||
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||
// Removes all pending notifications from the notification center.
|
||||
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||
|
||||
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||
// Removes a specific pending notification by its identifier.
|
||||
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||
// Removes all delivered notifications from the notification center.
|
||||
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||
|
||||
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||
// Removes a specific delivered notification by its identifier.
|
||||
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||
// Removes a notification by its identifier (cross-platform convenience function).
|
||||
export function RemoveNotification(identifier: string): Promise<void>;
|
||||
@@ -240,3 +240,59 @@ export function CanResolveFilePaths() {
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
|
||||
export function InitializeNotifications() {
|
||||
return window.runtime.InitializeNotifications();
|
||||
}
|
||||
|
||||
export function CleanupNotifications() {
|
||||
return window.runtime.CleanupNotifications();
|
||||
}
|
||||
|
||||
export function IsNotificationAvailable() {
|
||||
return window.runtime.IsNotificationAvailable();
|
||||
}
|
||||
|
||||
export function RequestNotificationAuthorization() {
|
||||
return window.runtime.RequestNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function CheckNotificationAuthorization() {
|
||||
return window.runtime.CheckNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function SendNotification(options) {
|
||||
return window.runtime.SendNotification(options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options) {
|
||||
return window.runtime.SendNotificationWithActions(options);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category) {
|
||||
return window.runtime.RegisterNotificationCategory(category);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryId) {
|
||||
return window.runtime.RemoveNotificationCategory(categoryId);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications() {
|
||||
return window.runtime.RemoveAllPendingNotifications();
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier) {
|
||||
return window.runtime.RemovePendingNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications() {
|
||||
return window.runtime.RemoveAllDeliveredNotifications();
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier) {
|
||||
return window.runtime.RemoveDeliveredNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier) {
|
||||
return window.runtime.RemoveNotification(identifier);
|
||||
}
|
||||
@@ -6,9 +6,8 @@ require (
|
||||
github.com/gen2brain/malgo v0.11.24
|
||||
github.com/getlantern/systray v1.2.2
|
||||
github.com/go-vgo/robotgo v0.110.8
|
||||
github.com/robotn/gohook v0.42.2
|
||||
github.com/wailsapp/wails/v2 v2.11.0
|
||||
golang.design/x/clipboard v0.7.1
|
||||
golang.design/x/mainthread v0.3.0
|
||||
golang.org/x/sys v0.33.0
|
||||
)
|
||||
|
||||
@@ -66,9 +65,7 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 // indirect
|
||||
golang.org/x/image v0.28.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
)
|
||||
|
||||
@@ -91,8 +91,6 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:Om
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robotn/gohook v0.42.2 h1:AI9OVh5o59c76jp9Xcc4NpIvze2YeKX1Rn8JvflAUXY=
|
||||
github.com/robotn/gohook v0.42.2/go.mod h1:PYgH0f1EaxhCvNSqIVTfo+SIUh1MrM2Uhe2w7SvFJDE=
|
||||
github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
|
||||
github.com/robotn/xgb v0.10.0 h1:O3kFbIwtwZ3pgLbp1h5slCQ4OpY8BdwugJLrUe6GPIM=
|
||||
github.com/robotn/xgb v0.10.0/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
|
||||
@@ -139,24 +137,21 @@ github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSB
|
||||
github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c=
|
||||
golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg=
|
||||
golang.design/x/mainthread v0.3.0 h1:UwFus0lcPodNpMOGoQMe87jSFwbSsEY//CA7yVmu4j8=
|
||||
golang.design/x/mainthread v0.3.0/go.mod h1:vYX7cF2b3pTJMGM/hc13NmN6kblKnf4/IyvHeu259L0=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 h1:Wdx0vgH5Wgsw+lF//LJKmWOJBLWX6nprsMqnf99rYDE=
|
||||
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
|
||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f h1:/n+PL2HlfqeSiDCuhdBbRNlGS/g2fM4OHufalHaTVG8=
|
||||
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mTVAhA48LpfW/8fNR0ifStlH2axyfg=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
||||
+79
-98
@@ -1,13 +1,11 @@
|
||||
// Package hotkey provides global keyboard shortcut detection and handling.
|
||||
// It uses the gohook library to capture system-wide key events.
|
||||
package hotkey
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"time"
|
||||
"wis-free-v3/internal/logger"
|
||||
|
||||
hook "github.com/robotn/gohook"
|
||||
"wis-free-v3/internal/xhotkey"
|
||||
)
|
||||
|
||||
// Listener handles global hotkey events and triggers callbacks when the
|
||||
@@ -16,145 +14,128 @@ type Listener struct {
|
||||
startCallback func()
|
||||
stopCallback func()
|
||||
isListening bool
|
||||
stopChan chan struct{}
|
||||
triggerKeys []uint16
|
||||
modifiers [][]uint16
|
||||
shortcut string
|
||||
hk *hotkey.Hotkey
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewListener creates a new hotkey listener with the specified shortcut and callbacks.
|
||||
// The shortcut should be in format like "ctrl+k" or "alt+shift+space".
|
||||
// onStart is called when the shortcut is pressed, onStop when it's released.
|
||||
func NewListener(shortcut string, onStart, onStop func()) *Listener {
|
||||
trigger, mods := ParseShortcut(shortcut)
|
||||
|
||||
logger.Info("Hotkey listener created: shortcut=%s, trigger=%d, modifiers=%v",
|
||||
shortcut, trigger, mods)
|
||||
logger.Info("Hotkey listener created: shortcut=%s", shortcut)
|
||||
|
||||
return &Listener{
|
||||
startCallback: onStart,
|
||||
stopCallback: onStop,
|
||||
stopChan: make(chan struct{}),
|
||||
triggerKeys: trigger,
|
||||
modifiers: mods,
|
||||
shortcut: shortcut,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateShortcut changes the shortcut without stopping the listener.
|
||||
// This allows hot-swapping the shortcut while the application is running.
|
||||
func (l *Listener) UpdateShortcut(shortcut string) {
|
||||
trigger, mods := ParseShortcut(shortcut)
|
||||
|
||||
l.mu.Lock()
|
||||
l.triggerKeys = trigger
|
||||
l.modifiers = mods
|
||||
l.shortcut = shortcut
|
||||
wasListening := l.isListening
|
||||
|
||||
if l.hk != nil {
|
||||
l.hk.Unregister()
|
||||
l.hk = nil
|
||||
l.isListening = false
|
||||
}
|
||||
l.mu.Unlock()
|
||||
|
||||
logger.Info("Hotkey updated: shortcut=%s, trigger=%d", shortcut, trigger)
|
||||
logger.Info("Hotkey updated: shortcut=%s", shortcut)
|
||||
|
||||
if wasListening {
|
||||
l.Start()
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins listening for the configured shortcut in a background goroutine.
|
||||
// It's safe to call Start multiple times; subsequent calls are ignored.
|
||||
func (l *Listener) Start() {
|
||||
if l.isListening {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if l.hk != nil {
|
||||
return
|
||||
}
|
||||
l.isListening = true
|
||||
|
||||
go l.eventLoop()
|
||||
key, mods, ok := ParseShortcut(l.shortcut)
|
||||
if !ok {
|
||||
logger.Error("Failed to parse shortcut: %s", l.shortcut)
|
||||
return
|
||||
}
|
||||
|
||||
l.hk = hotkey.New(mods, key)
|
||||
if err := l.hk.Register(); err != nil {
|
||||
logger.Error("Failed to register hotkey %s: %v", l.shortcut, err)
|
||||
l.hk = nil
|
||||
return
|
||||
}
|
||||
|
||||
l.isListening = true
|
||||
logger.Info("Hotkey listener started, waiting for %s", l.shortcut)
|
||||
|
||||
go l.eventLoop(l.hk)
|
||||
}
|
||||
|
||||
// Stop terminates the hotkey listener.
|
||||
// It's safe to call Stop multiple times.
|
||||
func (l *Listener) Stop() {
|
||||
if !l.isListening {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if l.hk == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := l.hk.Unregister()
|
||||
if err != nil {
|
||||
logger.Error("Failed to unregister hotkey: %v", err)
|
||||
}
|
||||
|
||||
l.hk = nil
|
||||
l.isListening = false
|
||||
close(l.stopChan)
|
||||
logger.Info("Hotkey listener stopped")
|
||||
}
|
||||
|
||||
// eventLoop runs the main keyboard event processing loop.
|
||||
func (l *Listener) eventLoop() {
|
||||
logger.Info("Hotkey listener started")
|
||||
|
||||
evChan := hook.Start()
|
||||
defer hook.End()
|
||||
|
||||
func (l *Listener) eventLoop(hk *hotkey.Hotkey) {
|
||||
var isRecording bool
|
||||
pressedKeys := make(map[uint16]bool)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-l.stopChan:
|
||||
return
|
||||
|
||||
case ev := <-evChan:
|
||||
l.handleKeyEvent(ev, pressedKeys, &isRecording)
|
||||
case _, ok := <-hk.Keydown():
|
||||
if !ok {
|
||||
return // Hotkey was unregistered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleKeyEvent processes a single keyboard event.
|
||||
func (l *Listener) handleKeyEvent(ev hook.Event, pressedKeys map[uint16]bool, isRecording *bool) {
|
||||
// Update pressed keys state
|
||||
switch ev.Kind {
|
||||
case hook.KeyDown, hook.KeyHold:
|
||||
pressedKeys[ev.Rawcode] = true
|
||||
case hook.KeyUp:
|
||||
delete(pressedKeys, ev.Rawcode)
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
// Read current configuration
|
||||
l.mu.RLock()
|
||||
triggerVariants := l.triggerKeys
|
||||
modGroups := l.modifiers
|
||||
l.mu.RUnlock()
|
||||
|
||||
if len(triggerVariants) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Check trigger key first (fast path)
|
||||
triggerPressed := false
|
||||
for _, t := range triggerVariants {
|
||||
if pressedKeys[t] {
|
||||
triggerPressed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 2. State transition handling
|
||||
active := false
|
||||
if triggerPressed {
|
||||
active = true
|
||||
for _, group := range modGroups {
|
||||
groupPressed := false
|
||||
for _, m := range group {
|
||||
if pressedKeys[m] {
|
||||
groupPressed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !groupPressed {
|
||||
active = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if active && !*isRecording {
|
||||
if !isRecording {
|
||||
logger.Info("Shortcut activated: starting recording")
|
||||
go l.startCallback()
|
||||
*isRecording = true
|
||||
} else if !active && *isRecording {
|
||||
isRecording = true
|
||||
}
|
||||
|
||||
case _, ok := <-hk.Keyup():
|
||||
if !ok {
|
||||
return // Hotkey was unregistered
|
||||
}
|
||||
if isRecording {
|
||||
// X11 AutoRepeat Debounce logic
|
||||
// Wait a tiny fraction of a second. If we get a Keydown during this window,
|
||||
// it's an auto-repeat from holding the key, so we ignore both the Keyup and Keydown.
|
||||
select {
|
||||
case _, downOk := <-hk.Keydown():
|
||||
if !downOk {
|
||||
return
|
||||
}
|
||||
// AutoRepeat detected, skip this release!
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
// Key was genuinely physically released
|
||||
logger.Info("Shortcut released: stopping recording")
|
||||
go l.stopCallback()
|
||||
*isRecording = false
|
||||
isRecording = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+78
-92
@@ -1,112 +1,98 @@
|
||||
// Package hotkey provides global keyboard shortcut detection and handling.
|
||||
package hotkey
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
// Windows Virtual Key Codes
|
||||
// See: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
const (
|
||||
VK_LCTRL = 162 // Left Control
|
||||
VK_RCTRL = 163 // Right Control
|
||||
VK_LSHIFT = 160 // Left Shift
|
||||
VK_RSHIFT = 161 // Right Shift
|
||||
VK_LALT = 164 // Left Alt (Menu)
|
||||
VK_RALT = 165 // Right Alt (Menu)
|
||||
VK_LWIN = 91 // Left Windows key
|
||||
VK_RWIN = 92 // Right Windows key
|
||||
"wis-free-v3/internal/xhotkey"
|
||||
)
|
||||
|
||||
// keyMap maps human-readable key names to Windows Virtual Key Codes.
|
||||
// Each key may have multiple valid codes (e.g., left and right variants).
|
||||
var keyMap = map[string][]uint16{
|
||||
// Modifier keys
|
||||
"ctrl": {VK_LCTRL, VK_RCTRL, 17},
|
||||
"control": {VK_LCTRL, VK_RCTRL, 17},
|
||||
"shift": {VK_LSHIFT, VK_RSHIFT, 16},
|
||||
"alt": {VK_LALT, VK_RALT, 18},
|
||||
"win": {VK_LWIN, VK_RWIN, 91, 92},
|
||||
"windows": {VK_LWIN, VK_RWIN, 91, 92},
|
||||
"meta": {VK_LWIN, VK_RWIN, 91, 92},
|
||||
"super": {VK_LWIN, VK_RWIN, 91, 92},
|
||||
|
||||
// Special keys
|
||||
"backspace": {8},
|
||||
"tab": {9},
|
||||
"enter": {13},
|
||||
"escape": {27},
|
||||
"esc": {27},
|
||||
"space": {32},
|
||||
"left": {37},
|
||||
"up": {38},
|
||||
"right": {39},
|
||||
"down": {40},
|
||||
"delete": {46},
|
||||
"del": {46},
|
||||
|
||||
// Number keys (top row)
|
||||
"0": {48}, "1": {49}, "2": {50}, "3": {51}, "4": {52},
|
||||
"5": {53}, "6": {54}, "7": {55}, "8": {56}, "9": {57},
|
||||
|
||||
// Letter keys
|
||||
"a": {65}, "b": {66}, "c": {67}, "d": {68}, "e": {69},
|
||||
"f": {70}, "g": {71}, "h": {72}, "i": {73}, "j": {74},
|
||||
"k": {75}, "l": {76}, "m": {77}, "n": {78}, "o": {79},
|
||||
"p": {80}, "q": {81}, "r": {82}, "s": {83}, "t": {84},
|
||||
"u": {85}, "v": {86}, "w": {87}, "x": {88}, "y": {89},
|
||||
"z": {90},
|
||||
|
||||
// Function keys
|
||||
"f1": {112}, "f2": {113}, "f3": {114}, "f4": {115},
|
||||
"f5": {116}, "f6": {117}, "f7": {118}, "f8": {119},
|
||||
"f9": {120}, "f10": {121}, "f11": {122}, "f12": {123},
|
||||
// KeyMap maps human-readable key names to golang.design/x/hotkey Key instances.
|
||||
var KeyMap = map[string]hotkey.Key{
|
||||
"space": hotkey.KeySpace,
|
||||
"0": hotkey.Key0,
|
||||
"1": hotkey.Key1,
|
||||
"2": hotkey.Key2,
|
||||
"3": hotkey.Key3,
|
||||
"4": hotkey.Key4,
|
||||
"5": hotkey.Key5,
|
||||
"6": hotkey.Key6,
|
||||
"7": hotkey.Key7,
|
||||
"8": hotkey.Key8,
|
||||
"9": hotkey.Key9,
|
||||
"a": hotkey.KeyA,
|
||||
"b": hotkey.KeyB,
|
||||
"c": hotkey.KeyC,
|
||||
"d": hotkey.KeyD,
|
||||
"e": hotkey.KeyE,
|
||||
"f": hotkey.KeyF,
|
||||
"g": hotkey.KeyG,
|
||||
"h": hotkey.KeyH,
|
||||
"i": hotkey.KeyI,
|
||||
"j": hotkey.KeyJ,
|
||||
"k": hotkey.KeyK,
|
||||
"l": hotkey.KeyL,
|
||||
"m": hotkey.KeyM,
|
||||
"n": hotkey.KeyN,
|
||||
"o": hotkey.KeyO,
|
||||
"p": hotkey.KeyP,
|
||||
"q": hotkey.KeyQ,
|
||||
"r": hotkey.KeyR,
|
||||
"s": hotkey.KeyS,
|
||||
"t": hotkey.KeyT,
|
||||
"u": hotkey.KeyU,
|
||||
"v": hotkey.KeyV,
|
||||
"w": hotkey.KeyW,
|
||||
"x": hotkey.KeyX,
|
||||
"y": hotkey.KeyY,
|
||||
"z": hotkey.KeyZ,
|
||||
"return": hotkey.KeyReturn,
|
||||
"enter": hotkey.KeyReturn,
|
||||
"escape": hotkey.KeyEscape,
|
||||
"esc": hotkey.KeyEscape,
|
||||
"delete": hotkey.KeyDelete,
|
||||
"del": hotkey.KeyDelete,
|
||||
"tab": hotkey.KeyTab,
|
||||
"left": hotkey.KeyLeft,
|
||||
"right": hotkey.KeyRight,
|
||||
"up": hotkey.KeyUp,
|
||||
"down": hotkey.KeyDown,
|
||||
"f1": hotkey.KeyF1,
|
||||
"f2": hotkey.KeyF2,
|
||||
"f3": hotkey.KeyF3,
|
||||
"f4": hotkey.KeyF4,
|
||||
"f5": hotkey.KeyF5,
|
||||
"f6": hotkey.KeyF6,
|
||||
"f7": hotkey.KeyF7,
|
||||
"f8": hotkey.KeyF8,
|
||||
"f9": hotkey.KeyF9,
|
||||
"f10": hotkey.KeyF10,
|
||||
"f11": hotkey.KeyF11,
|
||||
"f12": hotkey.KeyF12,
|
||||
}
|
||||
|
||||
// modifierKeys identifies which key names are modifiers.
|
||||
var modifierKeys = map[string]bool{
|
||||
"ctrl": true, "control": true,
|
||||
"shift": true,
|
||||
"alt": true,
|
||||
"win": true, "windows": true, "meta": true, "super": true,
|
||||
}
|
||||
|
||||
// ParseShortcut parses a shortcut string into a trigger key and modifier keys.
|
||||
// The shortcut format is "modifier+modifier+key" (e.g., "ctrl+shift+k").
|
||||
//
|
||||
// Returns:
|
||||
// - trigger: the primary key code that activates the shortcut
|
||||
// - modifiers: slice of modifier key code variants that must be held
|
||||
func ParseShortcut(shortcut string) (trigger []uint16, modifiers [][]uint16) {
|
||||
// ParseShortcut parses a shortcut string and returns the hotkey.Key and a list of modifiers.
|
||||
// Returns false if parsing fails.
|
||||
func ParseShortcut(shortcut string) (hotkey.Key, []hotkey.Modifier, bool) {
|
||||
parts := strings.Split(strings.ToLower(strings.TrimSpace(shortcut)), "+")
|
||||
if len(parts) == 0 {
|
||||
return nil, nil
|
||||
return 0, nil, false
|
||||
}
|
||||
|
||||
// Last part is always the trigger key
|
||||
// The last part is the trigger key
|
||||
triggerName := strings.TrimSpace(parts[len(parts)-1])
|
||||
if codes, ok := keyMap[triggerName]; ok && len(codes) > 0 {
|
||||
trigger = codes
|
||||
k, exists := KeyMap[triggerName]
|
||||
if !exists {
|
||||
return 0, nil, false
|
||||
}
|
||||
|
||||
// Preceding parts are modifiers
|
||||
// The preceding parts are modifiers
|
||||
var mods []hotkey.Modifier
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
modName := strings.TrimSpace(parts[i])
|
||||
if codes, ok := keyMap[modName]; ok {
|
||||
modifiers = append(modifiers, codes)
|
||||
if m, ok := ModMap[modName]; ok {
|
||||
mods = append(mods, m)
|
||||
}
|
||||
}
|
||||
|
||||
return trigger, modifiers
|
||||
return k, mods, true
|
||||
}
|
||||
|
||||
// IsModifier returns true if the given key name is a modifier key.
|
||||
func IsModifier(keyName string) bool {
|
||||
return modifierKeys[strings.ToLower(keyName)]
|
||||
}
|
||||
|
||||
// GetKeyCode returns the virtual key code(s) for a given key name.
|
||||
// Returns nil if the key name is not recognized.
|
||||
func GetKeyCode(keyName string) []uint16 {
|
||||
return keyMap[strings.ToLower(keyName)]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build darwin
|
||||
|
||||
package hotkey
|
||||
|
||||
import "wis-free-v3/internal/xhotkey"
|
||||
|
||||
var ModMap = map[string]hotkey.Modifier{
|
||||
"ctrl": hotkey.ModCtrl,
|
||||
"control": hotkey.ModCtrl,
|
||||
"shift": hotkey.ModShift,
|
||||
"alt": hotkey.ModOption,
|
||||
"win": hotkey.ModCmd,
|
||||
"windows": hotkey.ModCmd,
|
||||
"meta": hotkey.ModCmd,
|
||||
"super": hotkey.ModCmd,
|
||||
"cmd": hotkey.ModCmd,
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build linux
|
||||
|
||||
package hotkey
|
||||
|
||||
import "wis-free-v3/internal/xhotkey"
|
||||
|
||||
var ModMap = map[string]hotkey.Modifier{
|
||||
"ctrl": hotkey.ModCtrl,
|
||||
"control": hotkey.ModCtrl,
|
||||
"shift": hotkey.ModShift,
|
||||
"alt": hotkey.Mod1,
|
||||
"win": hotkey.Mod4,
|
||||
"windows": hotkey.Mod4,
|
||||
"meta": hotkey.Mod4,
|
||||
"super": hotkey.Mod4,
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build windows
|
||||
|
||||
package hotkey
|
||||
|
||||
import "wis-free-v3/internal/xhotkey"
|
||||
|
||||
var ModMap = map[string]hotkey.Modifier{
|
||||
"ctrl": hotkey.ModCtrl,
|
||||
"control": hotkey.ModCtrl,
|
||||
"shift": hotkey.ModShift,
|
||||
"alt": hotkey.ModAlt,
|
||||
"win": hotkey.ModWin,
|
||||
"windows": hotkey.ModWin,
|
||||
"meta": hotkey.ModWin,
|
||||
"super": hotkey.ModWin,
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
@@ -5,6 +5,7 @@ package tray
|
||||
import (
|
||||
_ "embed"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"wis-free-v3/internal/config"
|
||||
"wis-free-v3/internal/logger"
|
||||
@@ -16,6 +17,12 @@ import (
|
||||
//go:embed icon.ico
|
||||
var iconData []byte
|
||||
|
||||
//go:embed icon_recording.png
|
||||
var iconRecordingData []byte
|
||||
|
||||
//go:embed icon_transcribing.png
|
||||
var iconTranscribingData []byte
|
||||
|
||||
// App defines the interface required by the tray package to interact with the main application.
|
||||
type App interface {
|
||||
Quit()
|
||||
@@ -120,6 +127,14 @@ func UpdateStatus(status string) {
|
||||
if statusMenuItem != nil {
|
||||
statusMenuItem.SetTitle("Status: " + status)
|
||||
systray.SetTooltip("wis-free-v3 - " + status)
|
||||
|
||||
if strings.Contains(status, "Recording") {
|
||||
systray.SetIcon(iconRecordingData)
|
||||
} else if strings.Contains(status, "Transcribing") {
|
||||
systray.SetIcon(iconTranscribingData)
|
||||
} else {
|
||||
systray.SetIcon(iconData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [changkun] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
# Copyright 2021 The golang.design Initiative Authors.
|
||||
# All rights reserved. Use of this source code is governed
|
||||
# by a MIT license that can be found in the LICENSE file.
|
||||
#
|
||||
# Written by Changkun Ou <changkun.de>
|
||||
|
||||
name: hotkey
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
platform_test:
|
||||
env:
|
||||
DISPLAY: ':0.0'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go: ['1.17.x', '1.18.x', '1.19.x']
|
||||
steps:
|
||||
- name: Install and run dependencies (xvfb libx11-dev)
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y xvfb libx11-dev x11-utils libegl1-mesa-dev libgles2-mesa-dev
|
||||
Xvfb :0 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
# Wait for Xvfb
|
||||
MAX_ATTEMPTS=120 # About 60 seconds
|
||||
COUNT=0
|
||||
echo -n "Waiting for Xvfb to be ready..."
|
||||
while ! xdpyinfo -display "${DISPLAY}" >/dev/null 2>&1; do
|
||||
echo -n "."
|
||||
sleep 0.50s
|
||||
COUNT=$(( COUNT + 1 ))
|
||||
if [ "${COUNT}" -ge "${MAX_ATTEMPTS}" ]; then
|
||||
echo " Gave up waiting for X server on ${DISPLAY}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "Done - Xvfb is ready!"
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
stable: 'false'
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Run Tests with CGO_ENABLED=1
|
||||
if: ${{ runner.os == 'Linux' || runner.os == 'macOS'}}
|
||||
run: |
|
||||
CGO_ENABLED=1 go test -v -covermode=atomic .
|
||||
|
||||
- name: Run Tests with CGO_ENABLED=0
|
||||
if: ${{ runner.os == 'Linux' || runner.os == 'macOS'}}
|
||||
run: |
|
||||
CGO_ENABLED=0 go test -v -covermode=atomic .
|
||||
|
||||
- name: Run Tests on Windows
|
||||
if: ${{ runner.os == 'Windows'}}
|
||||
run: |
|
||||
go test -v -covermode=atomic .
|
||||
@@ -0,0 +1,15 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Changkun Ou <changkun.de>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,90 @@
|
||||
# hotkey [](https://pkg.go.dev/golang.design/x/hotkey)  
|
||||
|
||||
cross platform hotkey package in Go
|
||||
|
||||
```go
|
||||
import "golang.design/x/hotkey"
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Cross platform supports: macOS, Linux (X11), and Windows
|
||||
- Global hotkey registration without focus on a window
|
||||
|
||||
## API Usage
|
||||
|
||||
Package hotkey provides the basic facility to register a system-level
|
||||
global hotkey shortcut so that an application can be notified if a user
|
||||
triggers the desired hotkey. A hotkey must be a combination of modifiers
|
||||
and a single key.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"golang.design/x/hotkey"
|
||||
"golang.design/x/hotkey/mainthread"
|
||||
)
|
||||
|
||||
func main() { mainthread.Init(fn) } // Not necessary when use in Fyne, Ebiten or Gio.
|
||||
func fn() {
|
||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
|
||||
err := hk.Register()
|
||||
if err != nil {
|
||||
log.Fatalf("hotkey: failed to register hotkey: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("hotkey: %v is registered\n", hk)
|
||||
<-hk.Keydown()
|
||||
log.Printf("hotkey: %v is down\n", hk)
|
||||
<-hk.Keyup()
|
||||
log.Printf("hotkey: %v is up\n", hk)
|
||||
hk.Unregister()
|
||||
log.Printf("hotkey: %v is unregistered\n", hk)
|
||||
}
|
||||
```
|
||||
|
||||
Note platform specific details:
|
||||
|
||||
- On macOS, due to the OS restriction (other platforms does not have this
|
||||
restriction), hotkey events must be handled on the "main thread".
|
||||
Therefore, in order to use this package properly, one must start an OS
|
||||
main event loop on the main thread, For self-contained applications,
|
||||
using [golang.design/x/hotkey/mainthread](https://pkg.go.dev/golang.design/x/hotkey/mainthread)
|
||||
is possible. It is uncessary or applications based on other GUI frameworks,
|
||||
such as fyne, ebiten, or Gio. See the "[./examples](./examples)" folder
|
||||
for more examples.
|
||||
- On Linux (X11), when AutoRepeat is enabled in the X server, the Keyup
|
||||
is triggered automatically and continuously as Keydown continues.
|
||||
- On Linux (X11), some keys may be mapped to multiple Mod keys. To
|
||||
correctly register the key combination, one must use the correct
|
||||
underlying keycode combination. For example, a regular Ctrl+Alt+S
|
||||
might be registered as: Ctrl+Mod2+Mod4+S.
|
||||
- If this package did not include a desired key, one can always provide
|
||||
the keycode to the API. For example, if a key code is 0x15, then the
|
||||
corresponding key is `hotkey.Key(0x15)`.
|
||||
|
||||
## Examples
|
||||
|
||||
| Description | Folder |
|
||||
|:------------|:------:|
|
||||
| A minimum example | [minimum](./examples/minimum/main.go) |
|
||||
| Register multiple hotkeys | [multiple](./examples/multiple/main.go) |
|
||||
| A example to use in GLFW | [glfw](./examples/glfw/main.go) |
|
||||
| A example to use in Fyne | [fyne](./examples/fyne/main.go) |
|
||||
| A example to use in Ebiten | [ebiten](./examples/ebiten/main.go) |
|
||||
| A example to use in Gio | [gio](./examples/gio/main.go) |
|
||||
|
||||
## Who is using this package?
|
||||
|
||||
The main purpose of building this package is to support the
|
||||
[midgard](https://changkun.de/s/midgard) project.
|
||||
|
||||
To know more projects, check our [wiki](https://github.com/golang-design/hotkey/wiki) page.
|
||||
|
||||
## License
|
||||
|
||||
MIT | © 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de).
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
// Package hotkey provides the basic facility to register a system-level
|
||||
// global hotkey shortcut so that an application can be notified if a user
|
||||
// triggers the desired hotkey. A hotkey must be a combination of modifiers
|
||||
// and a single key.
|
||||
//
|
||||
// Note platform specific details:
|
||||
//
|
||||
// - On macOS, due to the OS restriction (other platforms does not have
|
||||
// this restriction), hotkey events must be handled on the "main thread".
|
||||
// Therefore, in order to use this package properly, one must start an
|
||||
// OS main event loop on the main thread, For self-contained applications,
|
||||
// using [mainthread] package.
|
||||
// is possible. It is uncessary or applications based on other GUI frameworks,
|
||||
// such as fyne, ebiten, or Gio. See the "[examples]" for more examples.
|
||||
//
|
||||
// - On Linux (X11), when AutoRepeat is enabled in the X server, the
|
||||
// Keyup is triggered automatically and continuously as Keydown continues.
|
||||
//
|
||||
// - On Linux (X11), some keys may be mapped to multiple Mod keys. To
|
||||
// correctly register the key combination, one must use the correct
|
||||
// underlying keycode combination. For example, a regular Ctrl+Alt+S
|
||||
// might be registered as: Ctrl+Mod2+Mod4+S.
|
||||
//
|
||||
// - If this package did not include a desired key, one can always provide
|
||||
// the keycode to the API. For example, if a key code is 0x15, then the
|
||||
// corresponding key is `hotkey.Key(0x15)`.
|
||||
//
|
||||
// THe following is a minimum example:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "log"
|
||||
//
|
||||
// "wis-free-v3/internal/xhotkey"
|
||||
// "wis-free-v3/internal/xhotkey/mainthread"
|
||||
// )
|
||||
//
|
||||
// func main() { mainthread.Init(fn) } // Not necessary when use in Fyne, Ebiten or Gio.
|
||||
// func fn() {
|
||||
// hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
|
||||
// err := hk.Register()
|
||||
// if err != nil {
|
||||
// log.Fatalf("hotkey: failed to register hotkey: %v", err)
|
||||
// }
|
||||
//
|
||||
// log.Printf("hotkey: %v is registered\n", hk)
|
||||
// <-hk.Keydown()
|
||||
// log.Printf("hotkey: %v is down\n", hk)
|
||||
// <-hk.Keyup()
|
||||
// log.Printf("hotkey: %v is up\n", hk)
|
||||
// hk.Unregister()
|
||||
// log.Printf("hotkey: %v is unregistered\n", hk)
|
||||
// }
|
||||
//
|
||||
// [mainthread]: https://pkg.go.dev/golang.design/x/hotkey/mainthread
|
||||
// [examples]: https://github.com/golang-design/hotkey/tree/main/examples
|
||||
package hotkey
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Event represents a hotkey event
|
||||
type Event struct{}
|
||||
|
||||
// Hotkey is a combination of modifiers and key to trigger an event
|
||||
type Hotkey struct {
|
||||
platformHotkey
|
||||
|
||||
mods []Modifier
|
||||
key Key
|
||||
|
||||
keydownIn chan<- Event
|
||||
keydownOut <-chan Event
|
||||
keyupIn chan<- Event
|
||||
keyupOut <-chan Event
|
||||
}
|
||||
|
||||
// New creates a new hotkey for the given modifiers and keycode.
|
||||
func New(mods []Modifier, key Key) *Hotkey {
|
||||
keydownIn, keydownOut := newEventChan()
|
||||
keyupIn, keyupOut := newEventChan()
|
||||
hk := &Hotkey{
|
||||
mods: mods,
|
||||
key: key,
|
||||
keydownIn: keydownIn,
|
||||
keydownOut: keydownOut,
|
||||
keyupIn: keyupIn,
|
||||
keyupOut: keyupOut,
|
||||
}
|
||||
|
||||
// Make sure the hotkey is unregistered when the created
|
||||
// hotkey is garbage collected.
|
||||
runtime.SetFinalizer(hk, func(x interface{}) {
|
||||
hk := x.(*Hotkey)
|
||||
hk.unregister()
|
||||
close(hk.keydownIn)
|
||||
close(hk.keyupIn)
|
||||
})
|
||||
return hk
|
||||
}
|
||||
|
||||
// Register registers a combination of hotkeys. If the hotkey has
|
||||
// registered. This function will invalidates the old registration
|
||||
// and overwrites its callback.
|
||||
func (hk *Hotkey) Register() error { return hk.register() }
|
||||
|
||||
// Keydown returns a channel that receives a signal when the hotkey is triggered.
|
||||
func (hk *Hotkey) Keydown() <-chan Event { return hk.keydownOut }
|
||||
|
||||
// Keyup returns a channel that receives a signal when the hotkey is released.
|
||||
func (hk *Hotkey) Keyup() <-chan Event { return hk.keyupOut }
|
||||
|
||||
// Unregister unregisters the hotkey.
|
||||
func (hk *Hotkey) Unregister() error {
|
||||
err := hk.unregister()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset a new event channel.
|
||||
close(hk.keydownIn)
|
||||
close(hk.keyupIn)
|
||||
hk.keydownIn, hk.keydownOut = newEventChan()
|
||||
hk.keyupIn, hk.keyupOut = newEventChan()
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns a string representation of the hotkey.
|
||||
func (hk *Hotkey) String() string {
|
||||
s := fmt.Sprintf("%v", hk.key)
|
||||
for _, mod := range hk.mods {
|
||||
s += fmt.Sprintf("+%v", mod)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// newEventChan returns a sender and a receiver of a buffered channel
|
||||
// with infinite capacity.
|
||||
func newEventChan() (chan<- Event, <-chan Event) {
|
||||
in, out := make(chan Event), make(chan Event)
|
||||
|
||||
go func() {
|
||||
var q []Event
|
||||
|
||||
for {
|
||||
e, ok := <-in
|
||||
if !ok {
|
||||
close(out)
|
||||
return
|
||||
}
|
||||
q = append(q, e)
|
||||
for len(q) > 0 {
|
||||
select {
|
||||
case out <- q[0]:
|
||||
q[0] = Event{}
|
||||
q = q[1:]
|
||||
case e, ok := <-in:
|
||||
if ok {
|
||||
q = append(q, e)
|
||||
break
|
||||
}
|
||||
for _, e := range q {
|
||||
out <- e
|
||||
}
|
||||
close(out)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return in, out
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package hotkey
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Cocoa -framework Carbon
|
||||
#include <stdint.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Carbon/Carbon.h>
|
||||
|
||||
extern void keydownCallback(uintptr_t handle);
|
||||
extern void keyupCallback(uintptr_t handle);
|
||||
int registerHotKey(int mod, int key, uintptr_t handle, EventHotKeyRef* ref);
|
||||
int unregisterHotKey(EventHotKeyRef ref);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"runtime/cgo"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Hotkey is a combination of modifiers and key to trigger an event
|
||||
type platformHotkey struct {
|
||||
mu sync.Mutex
|
||||
registered bool
|
||||
hkref C.EventHotKeyRef
|
||||
}
|
||||
|
||||
func (hk *Hotkey) register() error {
|
||||
hk.mu.Lock()
|
||||
defer hk.mu.Unlock()
|
||||
if hk.registered {
|
||||
return errors.New("hotkey already registered")
|
||||
}
|
||||
|
||||
// Note: we use handle number as hotkey id in the C side.
|
||||
// A cgo handle could ran out of space, but since in hotkey purpose
|
||||
// we won't have that much number of hotkeys. So this should be fine.
|
||||
|
||||
h := cgo.NewHandle(hk)
|
||||
var mod Modifier
|
||||
for _, m := range hk.mods {
|
||||
mod += m
|
||||
}
|
||||
|
||||
ret := C.registerHotKey(C.int(mod), C.int(hk.key), C.uintptr_t(h), &hk.hkref)
|
||||
if ret == C.int(-1) {
|
||||
return errors.New("failed to register the hotkey")
|
||||
}
|
||||
|
||||
hk.registered = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hk *Hotkey) unregister() error {
|
||||
hk.mu.Lock()
|
||||
defer hk.mu.Unlock()
|
||||
if !hk.registered {
|
||||
return errors.New("hotkey is not registered")
|
||||
}
|
||||
|
||||
ret := C.unregisterHotKey(hk.hkref)
|
||||
if ret == C.int(-1) {
|
||||
return errors.New("failed to unregister the current hotkey")
|
||||
}
|
||||
hk.registered = false
|
||||
return nil
|
||||
}
|
||||
|
||||
//export keydownCallback
|
||||
func keydownCallback(h uintptr) {
|
||||
hk := cgo.Handle(h).Value().(*Hotkey)
|
||||
hk.keydownIn <- Event{}
|
||||
}
|
||||
|
||||
//export keyupCallback
|
||||
func keyupCallback(h uintptr) {
|
||||
hk := cgo.Handle(h).Value().(*Hotkey)
|
||||
hk.keyupIn <- Event{}
|
||||
}
|
||||
|
||||
// Modifier represents a modifier.
|
||||
// See: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
|
||||
type Modifier uint32
|
||||
|
||||
// All kinds of Modifiers
|
||||
const (
|
||||
ModCtrl Modifier = 0x1000
|
||||
ModShift Modifier = 0x200
|
||||
ModOption Modifier = 0x800
|
||||
ModCmd Modifier = 0x100
|
||||
)
|
||||
|
||||
// Key represents a key.
|
||||
// See: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
|
||||
type Key uint8
|
||||
|
||||
// All kinds of keys
|
||||
const (
|
||||
KeySpace Key = 49
|
||||
Key1 Key = 18
|
||||
Key2 Key = 19
|
||||
Key3 Key = 20
|
||||
Key4 Key = 21
|
||||
Key5 Key = 23
|
||||
Key6 Key = 22
|
||||
Key7 Key = 26
|
||||
Key8 Key = 28
|
||||
Key9 Key = 25
|
||||
Key0 Key = 29
|
||||
KeyA Key = 0
|
||||
KeyB Key = 11
|
||||
KeyC Key = 8
|
||||
KeyD Key = 2
|
||||
KeyE Key = 14
|
||||
KeyF Key = 3
|
||||
KeyG Key = 5
|
||||
KeyH Key = 4
|
||||
KeyI Key = 34
|
||||
KeyJ Key = 38
|
||||
KeyK Key = 40
|
||||
KeyL Key = 37
|
||||
KeyM Key = 46
|
||||
KeyN Key = 45
|
||||
KeyO Key = 31
|
||||
KeyP Key = 35
|
||||
KeyQ Key = 12
|
||||
KeyR Key = 15
|
||||
KeyS Key = 1
|
||||
KeyT Key = 17
|
||||
KeyU Key = 32
|
||||
KeyV Key = 9
|
||||
KeyW Key = 13
|
||||
KeyX Key = 7
|
||||
KeyY Key = 16
|
||||
KeyZ Key = 6
|
||||
|
||||
KeyReturn Key = 0x24
|
||||
KeyEscape Key = 0x35
|
||||
KeyDelete Key = 0x33
|
||||
KeyTab Key = 0x30
|
||||
|
||||
KeyLeft Key = 0x7B
|
||||
KeyRight Key = 0x7C
|
||||
KeyUp Key = 0x7E
|
||||
KeyDown Key = 0x7D
|
||||
|
||||
KeyF1 Key = 0x7A
|
||||
KeyF2 Key = 0x78
|
||||
KeyF3 Key = 0x63
|
||||
KeyF4 Key = 0x76
|
||||
KeyF5 Key = 0x60
|
||||
KeyF6 Key = 0x61
|
||||
KeyF7 Key = 0x62
|
||||
KeyF8 Key = 0x64
|
||||
KeyF9 Key = 0x65
|
||||
KeyF10 Key = 0x6D
|
||||
KeyF11 Key = 0x67
|
||||
KeyF12 Key = 0x6F
|
||||
KeyF13 Key = 0x69
|
||||
KeyF14 Key = 0x6B
|
||||
KeyF15 Key = 0x71
|
||||
KeyF16 Key = 0x6A
|
||||
KeyF17 Key = 0x40
|
||||
KeyF18 Key = 0x4F
|
||||
KeyF19 Key = 0x50
|
||||
KeyF20 Key = 0x5A
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build darwin
|
||||
|
||||
#include <stdint.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Carbon/Carbon.h>
|
||||
extern void keydownCallback(uintptr_t handle);
|
||||
extern void keyupCallback(uintptr_t handle);
|
||||
|
||||
static OSStatus
|
||||
keydownHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) {
|
||||
EventHotKeyID k;
|
||||
GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(k), NULL, &k);
|
||||
keydownCallback((uintptr_t)k.id); // use id as handle
|
||||
return noErr;
|
||||
}
|
||||
|
||||
static OSStatus
|
||||
keyupHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) {
|
||||
EventHotKeyID k;
|
||||
GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(k), NULL, &k);
|
||||
keyupCallback((uintptr_t)k.id); // use id as handle
|
||||
return noErr;
|
||||
}
|
||||
|
||||
// registerHotkeyWithCallback registers a global system hotkey for callbacks.
|
||||
int registerHotKey(int mod, int key, uintptr_t handle, EventHotKeyRef* ref) {
|
||||
__block OSStatus s;
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
EventTypeSpec keydownEvent;
|
||||
keydownEvent.eventClass = kEventClassKeyboard;
|
||||
keydownEvent.eventKind = kEventHotKeyPressed;
|
||||
EventTypeSpec keyupEvent;
|
||||
keyupEvent.eventClass = kEventClassKeyboard;
|
||||
keyupEvent.eventKind = kEventHotKeyReleased;
|
||||
InstallApplicationEventHandler(
|
||||
&keydownHandler, 1, &keydownEvent, NULL, NULL
|
||||
);
|
||||
InstallApplicationEventHandler(
|
||||
&keyupHandler, 1, &keyupEvent, NULL, NULL
|
||||
);
|
||||
|
||||
EventHotKeyID hkid = {.id = handle};
|
||||
s = RegisterEventHotKey(
|
||||
key, mod, hkid, GetApplicationEventTarget(), 0, ref
|
||||
);
|
||||
});
|
||||
if (s != noErr) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int unregisterHotKey(EventHotKeyRef ref) {
|
||||
OSStatus s = UnregisterEventHotKey(ref);
|
||||
if (s != noErr) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build darwin && cgo
|
||||
|
||||
package hotkey_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"wis-free-v3/internal/xhotkey"
|
||||
)
|
||||
|
||||
// TestHotkey should always run success.
|
||||
// This is a test to run and for manually testing the registration of multiple
|
||||
// hotkeys. Registered hotkeys:
|
||||
// Ctrl+Shift+S
|
||||
// Ctrl+Option+S
|
||||
func TestHotkey(t *testing.T) {
|
||||
tt := time.Second * 5
|
||||
done := make(chan struct{}, 2)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tt)
|
||||
go func() {
|
||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
|
||||
if err := hk.Register(); err != nil {
|
||||
t.Errorf("failed to register hotkey: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
done <- struct{}{}
|
||||
return
|
||||
case <-hk.Keydown():
|
||||
fmt.Println("triggered ctrl+shift+s")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModOption}, hotkey.KeyS)
|
||||
if err := hk.Register(); err != nil {
|
||||
t.Errorf("failed to register hotkey: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
done <- struct{}{}
|
||||
return
|
||||
case <-hk.Keydown():
|
||||
fmt.Println("triggered ctrl+option+s")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-done
|
||||
<-done
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build linux
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/XKBlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern void hotkeyDown(uintptr_t hkhandle);
|
||||
extern void hotkeyUp(uintptr_t hkhandle);
|
||||
extern int checkCancel(uintptr_t hkhandle);
|
||||
|
||||
int displayTest() {
|
||||
Display* d = NULL;
|
||||
for (int i = 0; i < 42; i++) {
|
||||
d = XOpenDisplay(0);
|
||||
if (d == NULL) continue;
|
||||
break;
|
||||
}
|
||||
if (d == NULL) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// waitHotkey blocks until the hotkey is triggered.
|
||||
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key) {
|
||||
Display* d = NULL;
|
||||
for (int i = 0; i < 42; i++) {
|
||||
d = XOpenDisplay(0);
|
||||
if (d == NULL) continue;
|
||||
break;
|
||||
}
|
||||
if (d == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Optional: Ask X server to only send one release at the physical end of auto-repeat.
|
||||
Bool supported;
|
||||
XkbSetDetectableAutoRepeat(d, True, &supported);
|
||||
|
||||
int keycode = XKeysymToKeycode(d, key);
|
||||
XGrabKey(d, keycode, mod, DefaultRootWindow(d), False, GrabModeAsync, GrabModeAsync);
|
||||
XSelectInput(d, DefaultRootWindow(d), KeyPressMask | KeyReleaseMask);
|
||||
XEvent ev;
|
||||
|
||||
while(1) {
|
||||
if (checkCancel(hkhandle) == 1) {
|
||||
break;
|
||||
}
|
||||
if (XPending(d) > 0) {
|
||||
XNextEvent(d, &ev);
|
||||
switch(ev.type) {
|
||||
case KeyPress:
|
||||
hotkeyDown(hkhandle);
|
||||
continue;
|
||||
case KeyRelease:
|
||||
hotkeyUp(hkhandle);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
usleep(10000); // Poll every 10ms for snappy responsiveness without high CPU
|
||||
}
|
||||
}
|
||||
|
||||
XUngrabKey(d, keycode, mod, DefaultRootWindow(d));
|
||||
XCloseDisplay(d);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build linux
|
||||
|
||||
package hotkey
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lX11
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
int displayTest();
|
||||
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const errmsg = `Failed to initialize the X11 display, and the clipboard package
|
||||
will not work properly. Install the following dependency may help:
|
||||
|
||||
apt install -y libx11-dev
|
||||
If the clipboard package is in an environment without a frame buffer,
|
||||
such as a cloud server, it may also be necessary to install xvfb:
|
||||
apt install -y xvfb
|
||||
and initialize a virtual frame buffer:
|
||||
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
export DISPLAY=:99.0
|
||||
Then this package should be ready to use.
|
||||
`
|
||||
|
||||
func init() {
|
||||
if C.displayTest() != 0 {
|
||||
panic(errmsg)
|
||||
}
|
||||
}
|
||||
|
||||
type platformHotkey struct {
|
||||
mu sync.Mutex
|
||||
registered bool
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
canceled chan struct{}
|
||||
}
|
||||
|
||||
// Nothing needs to do for register
|
||||
func (hk *Hotkey) register() error {
|
||||
hk.mu.Lock()
|
||||
if hk.registered {
|
||||
hk.mu.Unlock()
|
||||
return errors.New("hotkey already registered.")
|
||||
}
|
||||
hk.registered = true
|
||||
hk.ctx, hk.cancel = context.WithCancel(context.Background())
|
||||
hk.canceled = make(chan struct{})
|
||||
hk.mu.Unlock()
|
||||
|
||||
go hk.handle()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Nothing needs to do for unregister
|
||||
func (hk *Hotkey) unregister() error {
|
||||
hk.mu.Lock()
|
||||
defer hk.mu.Unlock()
|
||||
if !hk.registered {
|
||||
return errors.New("hotkey is not registered.")
|
||||
}
|
||||
hk.cancel()
|
||||
hk.registered = false
|
||||
<-hk.canceled
|
||||
return nil
|
||||
}
|
||||
|
||||
// handle registers an application global hotkey to the system,
|
||||
// and returns a channel that will signal if the hotkey is triggered.
|
||||
func (hk *Hotkey) handle() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
// KNOWN ISSUE: if a hotkey is grabbed by others, C side will crash the program
|
||||
|
||||
var mod Modifier
|
||||
for _, m := range hk.mods {
|
||||
mod = mod | m
|
||||
}
|
||||
h := cgo.NewHandle(hk)
|
||||
defer h.Delete()
|
||||
|
||||
for {
|
||||
// Just call it once! The C code handles its own loop until checkCancel returns 1.
|
||||
_ = C.waitHotkey(C.uintptr_t(h), C.uint(mod), C.int(hk.key))
|
||||
close(hk.canceled)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//export checkCancel
|
||||
func checkCancel(h uintptr) C.int {
|
||||
hk := cgo.Handle(h).Value().(*Hotkey)
|
||||
select {
|
||||
case <-hk.ctx.Done():
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
//export hotkeyDown
|
||||
func hotkeyDown(h uintptr) {
|
||||
hk := cgo.Handle(h).Value().(*Hotkey)
|
||||
hk.keydownIn <- Event{}
|
||||
}
|
||||
|
||||
//export hotkeyUp
|
||||
func hotkeyUp(h uintptr) {
|
||||
hk := cgo.Handle(h).Value().(*Hotkey)
|
||||
hk.keyupIn <- Event{}
|
||||
}
|
||||
|
||||
// Modifier represents a modifier.
|
||||
type Modifier uint32
|
||||
|
||||
// All kinds of Modifiers
|
||||
// See /usr/include/X11/X.h
|
||||
const (
|
||||
ModCtrl Modifier = (1 << 2)
|
||||
ModShift Modifier = (1 << 0)
|
||||
Mod1 Modifier = (1 << 3)
|
||||
Mod2 Modifier = (1 << 4)
|
||||
Mod3 Modifier = (1 << 5)
|
||||
Mod4 Modifier = (1 << 6)
|
||||
Mod5 Modifier = (1 << 7)
|
||||
)
|
||||
|
||||
// Key represents a key.
|
||||
// See /usr/include/X11/keysymdef.h
|
||||
type Key uint16
|
||||
|
||||
// All kinds of keys
|
||||
const (
|
||||
KeySpace Key = 0x0020
|
||||
Key1 Key = 0x0030
|
||||
Key2 Key = 0x0031
|
||||
Key3 Key = 0x0032
|
||||
Key4 Key = 0x0033
|
||||
Key5 Key = 0x0034
|
||||
Key6 Key = 0x0035
|
||||
Key7 Key = 0x0036
|
||||
Key8 Key = 0x0037
|
||||
Key9 Key = 0x0038
|
||||
Key0 Key = 0x0039
|
||||
KeyA Key = 0x0061
|
||||
KeyB Key = 0x0062
|
||||
KeyC Key = 0x0063
|
||||
KeyD Key = 0x0064
|
||||
KeyE Key = 0x0065
|
||||
KeyF Key = 0x0066
|
||||
KeyG Key = 0x0067
|
||||
KeyH Key = 0x0068
|
||||
KeyI Key = 0x0069
|
||||
KeyJ Key = 0x006a
|
||||
KeyK Key = 0x006b
|
||||
KeyL Key = 0x006c
|
||||
KeyM Key = 0x006d
|
||||
KeyN Key = 0x006e
|
||||
KeyO Key = 0x006f
|
||||
KeyP Key = 0x0070
|
||||
KeyQ Key = 0x0071
|
||||
KeyR Key = 0x0072
|
||||
KeyS Key = 0x0073
|
||||
KeyT Key = 0x0074
|
||||
KeyU Key = 0x0075
|
||||
KeyV Key = 0x0076
|
||||
KeyW Key = 0x0077
|
||||
KeyX Key = 0x0078
|
||||
KeyY Key = 0x0079
|
||||
KeyZ Key = 0x007a
|
||||
|
||||
KeyReturn Key = 0xff0d
|
||||
KeyEscape Key = 0xff1b
|
||||
KeyDelete Key = 0xffff
|
||||
KeyTab Key = 0xff1b
|
||||
|
||||
KeyLeft Key = 0xff51
|
||||
KeyRight Key = 0xff53
|
||||
KeyUp Key = 0xff52
|
||||
KeyDown Key = 0xff54
|
||||
|
||||
KeyF1 Key = 0xffbe
|
||||
KeyF2 Key = 0xffbf
|
||||
KeyF3 Key = 0xffc0
|
||||
KeyF4 Key = 0xffc1
|
||||
KeyF5 Key = 0xffc2
|
||||
KeyF6 Key = 0xffc3
|
||||
KeyF7 Key = 0xffc4
|
||||
KeyF8 Key = 0xffc5
|
||||
KeyF9 Key = 0xffc6
|
||||
KeyF10 Key = 0xffc7
|
||||
KeyF11 Key = 0xffc8
|
||||
KeyF12 Key = 0xffc9
|
||||
KeyF13 Key = 0xffca
|
||||
KeyF14 Key = 0xffcb
|
||||
KeyF15 Key = 0xffcc
|
||||
KeyF16 Key = 0xffcd
|
||||
KeyF17 Key = 0xffce
|
||||
KeyF18 Key = 0xffcf
|
||||
KeyF19 Key = 0xffd0
|
||||
KeyF20 Key = 0xffd1
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build linux && cgo
|
||||
|
||||
package hotkey_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"wis-free-v3/internal/xhotkey"
|
||||
)
|
||||
|
||||
// TestHotkey should always run success.
|
||||
// This is a test to run and for manually testing, registered combination:
|
||||
// Ctrl+Alt+A (Ctrl+Mod2+Mod4+A on Linux)
|
||||
func TestHotkey(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
hk := hotkey.New([]hotkey.Modifier{
|
||||
hotkey.ModCtrl, hotkey.Mod2, hotkey.Mod4}, hotkey.KeyA)
|
||||
if err := hk.Register(); err != nil {
|
||||
t.Errorf("failed to register hotkey: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-hk.Keydown():
|
||||
fmt.Println("triggered")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build !windows && !cgo
|
||||
|
||||
package hotkey
|
||||
|
||||
type platformHotkey struct{}
|
||||
|
||||
// Modifier represents a modifier
|
||||
type Modifier uint32
|
||||
|
||||
// Key represents a key.
|
||||
type Key uint8
|
||||
|
||||
func (hk *Hotkey) register() error {
|
||||
panic("hotkey: cannot use when CGO_ENABLED=0")
|
||||
}
|
||||
|
||||
// unregister deregisteres a system hotkey.
|
||||
func (hk *Hotkey) unregister() error {
|
||||
panic("hotkey: cannot use when CGO_ENABLED=0")
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build (linux || darwin) && !cgo
|
||||
|
||||
package hotkey_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"wis-free-v3/internal/xhotkey"
|
||||
)
|
||||
|
||||
// TestHotkey should always run success.
|
||||
// This is a test to run and for manually testing, registered combination:
|
||||
// Ctrl+Alt+A (Ctrl+Mod2+Mod4+A on Linux)
|
||||
func TestHotkey(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
return
|
||||
}
|
||||
t.Fatalf("expect to fail when CGO_ENABLED=0")
|
||||
}()
|
||||
|
||||
hk := hotkey.New([]hotkey.Modifier{}, hotkey.Key(0))
|
||||
err := hk.Register()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hk.Unregister()
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
package hotkey_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"wis-free-v3/internal/xhotkey/mainthread"
|
||||
)
|
||||
|
||||
// The test cannot be run twice since the mainthread loop may not be terminated:
|
||||
// go test -v -count=1
|
||||
func TestMain(m *testing.M) {
|
||||
mainthread.Init(func() { os.Exit(m.Run()) })
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build windows
|
||||
|
||||
package hotkey
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"wis-free-v3/internal/xhotkey/internal/win"
|
||||
)
|
||||
|
||||
type platformHotkey struct {
|
||||
mu sync.Mutex
|
||||
hotkeyId uint64
|
||||
registered bool
|
||||
funcs chan func()
|
||||
canceled chan struct{}
|
||||
}
|
||||
|
||||
var hotkeyId uint64 // atomic
|
||||
|
||||
// register registers a system hotkey. It returns an error if
|
||||
// the registration is failed. This could be that the hotkey is
|
||||
// conflict with other hotkeys.
|
||||
func (hk *Hotkey) register() error {
|
||||
hk.mu.Lock()
|
||||
if hk.registered {
|
||||
hk.mu.Unlock()
|
||||
return errors.New("hotkey already registered")
|
||||
}
|
||||
|
||||
mod := uint8(0)
|
||||
for _, m := range hk.mods {
|
||||
mod = mod | uint8(m)
|
||||
}
|
||||
|
||||
hk.hotkeyId = atomic.AddUint64(&hotkeyId, 1)
|
||||
hk.funcs = make(chan func())
|
||||
hk.canceled = make(chan struct{})
|
||||
go hk.handle()
|
||||
|
||||
var (
|
||||
ok bool
|
||||
err error
|
||||
done = make(chan struct{})
|
||||
)
|
||||
hk.funcs <- func() {
|
||||
ok, err = win.RegisterHotKey(0, uintptr(hk.hotkeyId), uintptr(mod), uintptr(hk.key))
|
||||
done <- struct{}{}
|
||||
}
|
||||
<-done
|
||||
if !ok {
|
||||
close(hk.canceled)
|
||||
hk.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
hk.registered = true
|
||||
hk.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// unregister deregisteres a system hotkey.
|
||||
func (hk *Hotkey) unregister() error {
|
||||
hk.mu.Lock()
|
||||
defer hk.mu.Unlock()
|
||||
if !hk.registered {
|
||||
return errors.New("hotkey is not registered")
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
hk.funcs <- func() {
|
||||
win.UnregisterHotKey(0, uintptr(hk.hotkeyId))
|
||||
done <- struct{}{}
|
||||
close(hk.canceled)
|
||||
}
|
||||
<-done
|
||||
|
||||
<-hk.canceled
|
||||
hk.registered = false
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
// wmHotkey represents hotkey message
|
||||
wmHotkey uint32 = 0x0312
|
||||
wmQuit uint32 = 0x0012
|
||||
)
|
||||
|
||||
// handle handles the hotkey event loop.
|
||||
func (hk *Hotkey) handle() {
|
||||
// We could optimize this. So far each hotkey is served in an
|
||||
// individual thread. If we have too many hotkeys, then a program
|
||||
// have to create too many threads to serve them.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
tk := time.NewTicker(time.Second / 100)
|
||||
for range tk.C {
|
||||
msg := win.MSG{}
|
||||
if !win.PeekMessage(&msg, 0, 0, 0) {
|
||||
select {
|
||||
case f := <-hk.funcs:
|
||||
f()
|
||||
case <-hk.canceled:
|
||||
return
|
||||
default:
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !win.GetMessage(&msg, 0, 0, 0) {
|
||||
return
|
||||
}
|
||||
|
||||
switch msg.Message {
|
||||
case wmHotkey:
|
||||
hk.keydownIn <- Event{}
|
||||
|
||||
tk := time.NewTicker(time.Second / 100)
|
||||
for range tk.C {
|
||||
if win.GetAsyncKeyState(int(hk.key)) == 0 {
|
||||
hk.keyupIn <- Event{}
|
||||
break
|
||||
}
|
||||
}
|
||||
case wmQuit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modifier represents a modifier.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey
|
||||
type Modifier uint8
|
||||
|
||||
// All kinds of Modifiers
|
||||
const (
|
||||
ModAlt Modifier = 0x1
|
||||
ModCtrl Modifier = 0x2
|
||||
ModShift Modifier = 0x4
|
||||
ModWin Modifier = 0x8
|
||||
)
|
||||
|
||||
// Key represents a key.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
type Key uint16
|
||||
|
||||
// All kinds of Keys
|
||||
const (
|
||||
KeySpace Key = 0x20
|
||||
Key0 Key = 0x30
|
||||
Key1 Key = 0x31
|
||||
Key2 Key = 0x32
|
||||
Key3 Key = 0x33
|
||||
Key4 Key = 0x34
|
||||
Key5 Key = 0x35
|
||||
Key6 Key = 0x36
|
||||
Key7 Key = 0x37
|
||||
Key8 Key = 0x38
|
||||
Key9 Key = 0x39
|
||||
KeyA Key = 0x41
|
||||
KeyB Key = 0x42
|
||||
KeyC Key = 0x43
|
||||
KeyD Key = 0x44
|
||||
KeyE Key = 0x45
|
||||
KeyF Key = 0x46
|
||||
KeyG Key = 0x47
|
||||
KeyH Key = 0x48
|
||||
KeyI Key = 0x49
|
||||
KeyJ Key = 0x4A
|
||||
KeyK Key = 0x4B
|
||||
KeyL Key = 0x4C
|
||||
KeyM Key = 0x4D
|
||||
KeyN Key = 0x4E
|
||||
KeyO Key = 0x4F
|
||||
KeyP Key = 0x50
|
||||
KeyQ Key = 0x51
|
||||
KeyR Key = 0x52
|
||||
KeyS Key = 0x53
|
||||
KeyT Key = 0x54
|
||||
KeyU Key = 0x55
|
||||
KeyV Key = 0x56
|
||||
KeyW Key = 0x57
|
||||
KeyX Key = 0x58
|
||||
KeyY Key = 0x59
|
||||
KeyZ Key = 0x5A
|
||||
|
||||
KeyReturn Key = 0x0D
|
||||
KeyEscape Key = 0x1B
|
||||
KeyDelete Key = 0x2E
|
||||
KeyTab Key = 0x09
|
||||
|
||||
KeyLeft Key = 0x25
|
||||
KeyRight Key = 0x27
|
||||
KeyUp Key = 0x26
|
||||
KeyDown Key = 0x28
|
||||
|
||||
KeyF1 Key = 0x70
|
||||
KeyF2 Key = 0x71
|
||||
KeyF3 Key = 0x72
|
||||
KeyF4 Key = 0x73
|
||||
KeyF5 Key = 0x74
|
||||
KeyF6 Key = 0x75
|
||||
KeyF7 Key = 0x76
|
||||
KeyF8 Key = 0x77
|
||||
KeyF9 Key = 0x78
|
||||
KeyF10 Key = 0x79
|
||||
KeyF11 Key = 0x7A
|
||||
KeyF12 Key = 0x7B
|
||||
KeyF13 Key = 0x7C
|
||||
KeyF14 Key = 0x7D
|
||||
KeyF15 Key = 0x7E
|
||||
KeyF16 Key = 0x7F
|
||||
KeyF17 Key = 0x80
|
||||
KeyF18 Key = 0x81
|
||||
KeyF19 Key = 0x82
|
||||
KeyF20 Key = 0x83
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build windows
|
||||
|
||||
package hotkey_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"wis-free-v3/internal/xhotkey"
|
||||
)
|
||||
|
||||
// TestHotkey should always run success.
|
||||
// This is a test to run and for manually testing, registered combination:
|
||||
// Ctrl+Shift+S
|
||||
func TestHotkey(t *testing.T) {
|
||||
tt := time.Second * 5
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tt)
|
||||
defer cancel()
|
||||
|
||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
|
||||
if err := hk.Register(); err != nil {
|
||||
t.Errorf("failed to register hotkey: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-hk.Keydown():
|
||||
fmt.Println("triggered")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright 2021 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package win
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32")
|
||||
registerHotkey = user32.NewProc("RegisterHotKey")
|
||||
unregisterHotkey = user32.NewProc("UnregisterHotKey")
|
||||
getMessage = user32.NewProc("GetMessageW")
|
||||
peekMessage = user32.NewProc("PeekMessageA")
|
||||
sendMessage = user32.NewProc("SendMessageW")
|
||||
getAsyncKeyState = user32.NewProc("GetAsyncKeyState")
|
||||
quitMessage = user32.NewProc("PostQuitMessage")
|
||||
)
|
||||
|
||||
// RegisterHotKey defines a system-wide hot key.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey
|
||||
func RegisterHotKey(hwnd, id uintptr, mod uintptr, k uintptr) (bool, error) {
|
||||
ret, _, err := registerHotkey.Call(
|
||||
hwnd, id, mod, k,
|
||||
)
|
||||
return ret != 0, err
|
||||
}
|
||||
|
||||
// UnregisterHotKey frees a hot key previously registered by the calling
|
||||
// thread.
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterhotkey
|
||||
func UnregisterHotKey(hwnd, id uintptr) (bool, error) {
|
||||
ret, _, err := unregisterHotkey.Call(hwnd, id)
|
||||
return ret != 0, err
|
||||
}
|
||||
|
||||
// MSG contains message information from a thread's message queue.
|
||||
//
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msg
|
||||
type MSG struct {
|
||||
HWnd uintptr
|
||||
Message uint32
|
||||
WParam uintptr
|
||||
LParam uintptr
|
||||
Time uint32
|
||||
Pt struct { //POINT
|
||||
x, y int32
|
||||
}
|
||||
}
|
||||
|
||||
// SendMessage sends the specified message to a window or windows.
|
||||
// The SendMessage function calls the window procedure for the specified
|
||||
// window and does not return until the window procedure has processed
|
||||
// the message.
|
||||
// The return value specifies the result of the message processing;
|
||||
// it depends on the message sent.
|
||||
//
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessage
|
||||
func SendMessage(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
|
||||
ret, _, _ := sendMessage.Call(
|
||||
hwnd,
|
||||
uintptr(msg),
|
||||
wParam,
|
||||
lParam,
|
||||
)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// GetMessage retrieves a message from the calling thread's message
|
||||
// queue. The function dispatches incoming sent messages until a posted
|
||||
// message is available for retrieval.
|
||||
//
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage
|
||||
func GetMessage(msg *MSG, hWnd uintptr, msgFilterMin, msgFilterMax uint32) bool {
|
||||
ret, _, _ := getMessage.Call(
|
||||
uintptr(unsafe.Pointer(msg)),
|
||||
hWnd,
|
||||
uintptr(msgFilterMin),
|
||||
uintptr(msgFilterMax),
|
||||
)
|
||||
|
||||
return ret != 0
|
||||
}
|
||||
|
||||
// PeekMessage dispatches incoming sent messages, checks the thread message
|
||||
// queue for a posted message, and retrieves the message (if any exist).
|
||||
//
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-peekmessagea
|
||||
func PeekMessage(msg *MSG, hWnd uintptr, msgFilterMin, msgFilterMax uint32) bool {
|
||||
ret, _, _ := peekMessage.Call(
|
||||
uintptr(unsafe.Pointer(msg)),
|
||||
hWnd,
|
||||
uintptr(msgFilterMin),
|
||||
uintptr(msgFilterMax),
|
||||
0, // PM_NOREMOVE
|
||||
)
|
||||
|
||||
return ret != 0
|
||||
}
|
||||
|
||||
// PostQuitMessage indicates to the system that a thread has made
|
||||
// a request to terminate (quit). It is typically used in response
|
||||
// to a WM_DESTROY message.
|
||||
//
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage
|
||||
func PostQuitMessage(exitCode int) {
|
||||
quitMessage.Call(uintptr(exitCode))
|
||||
}
|
||||
|
||||
func GetAsyncKeyState(keycode int) uintptr {
|
||||
ret, _, _ := getAsyncKeyState.Call(uintptr(keycode))
|
||||
return ret
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2022 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
// Package mainthread wrapps the golang.design/x/mainthread, and
|
||||
// provides a different implementation for macOS so that it can
|
||||
// handle main thread events for the NSApplication.
|
||||
package mainthread
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2022 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build windows || linux || (darwin && !cgo)
|
||||
|
||||
package mainthread
|
||||
|
||||
import "golang.design/x/mainthread"
|
||||
|
||||
// Call calls f on the main thread and blocks until f finishes.
|
||||
func Call(f func()) { mainthread.Call(f) }
|
||||
|
||||
// Init initializes the functionality of running arbitrary subsequent functions be called on the main system thread.
|
||||
//
|
||||
// Init must be called in the main.main function.
|
||||
func Init(main func()) { mainthread.Init(main) }
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2022 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package mainthread
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Cocoa
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Dispatch/Dispatch.h>
|
||||
|
||||
extern void os_main(void);
|
||||
extern void wakeupMainThread(void);
|
||||
static bool isMainThread() {
|
||||
return [NSThread isMainThread];
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
// Call calls f on the main thread and blocks until f finishes.
|
||||
func Call(f func()) {
|
||||
if C.isMainThread() {
|
||||
f()
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
mainFuncs <- f
|
||||
C.wakeupMainThread()
|
||||
}()
|
||||
}
|
||||
|
||||
// Init initializes the functionality of running arbitrary subsequent functions be called on the main system thread.
|
||||
//
|
||||
// Init must be called in the main.main function.
|
||||
func Init(f func()) {
|
||||
go func() {
|
||||
f()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
C.os_main()
|
||||
}
|
||||
|
||||
var mainFuncs = make(chan func(), 1)
|
||||
|
||||
//export dispatchMainFuncs
|
||||
func dispatchMainFuncs() {
|
||||
for {
|
||||
select {
|
||||
case f := <-mainFuncs:
|
||||
f()
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2022 The golang.design Initiative Authors.
|
||||
// All rights reserved. Use of this source code is governed
|
||||
// by a MIT license that can be found in the LICENSE file.
|
||||
//
|
||||
// Written by Changkun Ou <changkun.de>
|
||||
|
||||
//go:build darwin
|
||||
|
||||
#include <stdint.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
extern void dispatchMainFuncs();
|
||||
|
||||
void wakeupMainThread(void) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
dispatchMainFuncs();
|
||||
});
|
||||
}
|
||||
|
||||
// The following three lines of code must run on the main thread.
|
||||
// It must handle it using golang.design/x/mainthread.
|
||||
//
|
||||
// inspired from here: https://github.com/cehoffman/dotfiles/blob/4be8e893517e970d40746a9bdc67fe5832dd1c33/os/mac/iTerm2HotKey.m
|
||||
void os_main(void) {
|
||||
[NSApplication sharedApplication];
|
||||
[NSApp disableRelaunchOnLogin];
|
||||
[NSApp run];
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# golang.design/x/mainthread v0.3.0
|
||||
## explicit; go 1.16
|
||||
golang.design/x/mainthread
|
||||
Regular → Executable
+8
-2
@@ -50,8 +50,8 @@ if command_exists pkg-config; then
|
||||
echo "[WARNING] Missing Wails dependencies (GTK3 / WebKit2GTK)."
|
||||
MISSING_DEPS=1
|
||||
fi
|
||||
if ! pkg-config --exists x11 xtst xcb; then
|
||||
echo "[WARNING] Missing gohook dependencies (X11 / Xtst / Xcb)."
|
||||
if ! pkg-config --exists x11 xtst xcb xkbcommon-x11; then
|
||||
echo "[WARNING] Missing gohook dependencies (X11 / Xtst / Xcb / Xkbcommon)."
|
||||
MISSING_DEPS=1
|
||||
fi
|
||||
if ! pkg-config --exists alsa; then
|
||||
@@ -92,6 +92,12 @@ fi
|
||||
|
||||
# 3. Build the application
|
||||
echo "[2/3] Building with Wails..."
|
||||
|
||||
# Pre-emptively fix npm bin permissions if they got messed up (common issue on some systems)
|
||||
if [ -d "frontend/node_modules/.bin" ]; then
|
||||
chmod +x frontend/node_modules/.bin/* 2>/dev/null || true
|
||||
fi
|
||||
|
||||
wails build -platform linux/amd64 -clean
|
||||
|
||||
if [ ! -f "$EXECUTABLE" ]; then
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Read original appicon
|
||||
f, err := os.Open("build/appicon.png")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
img, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
|
||||
// Generic tint function
|
||||
tint := func(name string, tintColor color.RGBA) {
|
||||
out := image.NewRGBA(bounds)
|
||||
// Draw original
|
||||
draw.Draw(out, bounds, img, image.Point{}, draw.Src)
|
||||
|
||||
// Draw tint over it using Atop or standard blending
|
||||
// We'll just manually blend the pixels to preserve alpha
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
r1, g1, b1, a1 := out.At(x, y).RGBA()
|
||||
if a1 == 0 {
|
||||
continue
|
||||
}
|
||||
// Convert back to 8-bit
|
||||
r, g, b, a := uint8(r1>>8), uint8(g1>>8), uint8(b1>>8), uint8(a1>>8)
|
||||
|
||||
// Overlay tintColor with 50% opacity
|
||||
// dst = (src * alpha + dst * (1-alpha))
|
||||
alpha := 0.5
|
||||
nr := uint8(float64(tintColor.R)*alpha + float64(r)*(1-alpha))
|
||||
ng := uint8(float64(tintColor.G)*alpha + float64(g)*(1-alpha))
|
||||
nb := uint8(float64(tintColor.B)*alpha + float64(b)*(1-alpha))
|
||||
|
||||
out.Set(x, y, color.RGBA{nr, ng, nb, a})
|
||||
}
|
||||
}
|
||||
|
||||
outF, err := os.Create("internal/ui/tray/" + name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer outF.Close()
|
||||
|
||||
png.Encode(outF, out)
|
||||
}
|
||||
|
||||
// Recording = Red
|
||||
tint("icon_recording.png", color.RGBA{255, 0, 0, 255})
|
||||
// Transcribing = Blue
|
||||
tint("icon_transcribing.png", color.RGBA{0, 150, 255, 255})
|
||||
}
|
||||
Reference in New Issue
Block a user