mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
14.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -31,6 +32,7 @@ type App struct {
|
||||
config *config.Config
|
||||
overlay platform.Overlay
|
||||
recordingPath string
|
||||
recording int32
|
||||
isQuitting bool
|
||||
wasMediaPlaying bool
|
||||
whisperManager *whisper.Manager
|
||||
@@ -82,8 +84,47 @@ func (a *App) startup(ctx context.Context) {
|
||||
}
|
||||
}()
|
||||
|
||||
// Accept explicit commands from helper invocations (Linux desktop shortcuts).
|
||||
go func() {
|
||||
for cmd := range secondInstanceCommand {
|
||||
switch cmd {
|
||||
case instanceCmdShow:
|
||||
wailsruntime.WindowShow(ctx)
|
||||
case instanceCmdStart:
|
||||
a.StartRecording()
|
||||
case instanceCmdStop:
|
||||
a.StopRecording()
|
||||
case instanceCmdToggle:
|
||||
a.ToggleRecording()
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Start system tray in a goroutine
|
||||
go tray.Start(a)
|
||||
if runtime.GOOS == "linux" {
|
||||
tray.Start(a)
|
||||
} else {
|
||||
go tray.Start(a)
|
||||
}
|
||||
|
||||
if initialAction != "" {
|
||||
action := initialAction
|
||||
initialAction = ""
|
||||
go func() {
|
||||
switch action {
|
||||
case "show":
|
||||
wailsruntime.WindowShow(ctx)
|
||||
case "start":
|
||||
a.StartRecording()
|
||||
case "stop":
|
||||
a.StopRecording()
|
||||
case "toggle":
|
||||
a.ToggleRecording()
|
||||
default:
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// beforeClose is called when the window is about to close
|
||||
@@ -103,8 +144,27 @@ func (a *App) Quit() {
|
||||
wailsruntime.Quit(a.ctx)
|
||||
}
|
||||
|
||||
var lastToggle time.Time
|
||||
|
||||
func (a *App) ToggleRecording() {
|
||||
// Debounce toggle calls to prevent rapid firing from double-binds or Wayland glitches
|
||||
if time.Since(lastToggle) < 500*time.Millisecond {
|
||||
return
|
||||
}
|
||||
lastToggle = time.Now()
|
||||
|
||||
if atomic.LoadInt32(&a.recording) == 1 {
|
||||
a.StopRecording()
|
||||
return
|
||||
}
|
||||
a.StartRecording()
|
||||
}
|
||||
|
||||
// StartRecording starts the audio recording
|
||||
func (a *App) StartRecording() {
|
||||
if !atomic.CompareAndSwapInt32(&a.recording, 0, 1) {
|
||||
return
|
||||
}
|
||||
logger.Info("StartRecording triggered")
|
||||
|
||||
// 1. Show overlay immediately (most visible feedback)
|
||||
@@ -120,6 +180,7 @@ func (a *App) StartRecording() {
|
||||
if a.overlay != nil {
|
||||
a.overlay.Hide()
|
||||
}
|
||||
atomic.StoreInt32(&a.recording, 0)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -141,6 +202,7 @@ func (a *App) StartRecording() {
|
||||
if a.overlay != nil {
|
||||
a.overlay.Hide()
|
||||
}
|
||||
atomic.StoreInt32(&a.recording, 0)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -160,6 +222,9 @@ func (a *App) StartRecording() {
|
||||
|
||||
// StopRecording stops the audio recording and triggers transcription
|
||||
func (a *App) StopRecording() {
|
||||
if !atomic.CompareAndSwapInt32(&a.recording, 1, 0) {
|
||||
return
|
||||
}
|
||||
logger.Info("StopRecording triggered")
|
||||
|
||||
// Resume media if it was playing before
|
||||
@@ -169,12 +234,15 @@ func (a *App) StopRecording() {
|
||||
}
|
||||
|
||||
if a.audioRecorder == nil {
|
||||
atomic.StoreInt32(&a.recording, 0)
|
||||
return
|
||||
}
|
||||
|
||||
err := a.audioRecorder.Stop()
|
||||
if err != nil {
|
||||
logger.Error("Failed to stop recording: %v", err)
|
||||
// Attempt to keep state consistent: if stop failed, we are likely still recording.
|
||||
atomic.StoreInt32(&a.recording, 1)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -329,7 +397,7 @@ func (a *App) SaveSettings(settings map[string]interface{}) string {
|
||||
a.hotkeyListener.UpdateShortcut(val)
|
||||
} else {
|
||||
// Should not happen if app started correctly, but just in case
|
||||
a.hotkeyListener = hotkey.NewListener(val, a.StartRecording, a.StopRecording)
|
||||
a.hotkeyListener = hotkey.NewListener(val, a.ToggleRecording, func() {})
|
||||
a.hotkeyListener.Start()
|
||||
}
|
||||
}
|
||||
@@ -480,8 +548,8 @@ func (a *App) startupHeadless() {
|
||||
logger.Error("Failed to ensure desktop file: %v", err)
|
||||
}
|
||||
|
||||
// Initialize Hotkey Listener
|
||||
a.hotkeyListener = hotkey.NewListener(a.config.Shortcut, a.StartRecording, a.StopRecording)
|
||||
// Initialize Hotkey Listener (toggle mode: keydown toggles, keyup ignored)
|
||||
a.hotkeyListener = hotkey.NewListener(a.config.Shortcut, a.ToggleRecording, func() {})
|
||||
a.hotkeyListener.Start()
|
||||
|
||||
logger.Info("Components initialized successfully!")
|
||||
|
||||
Regular → Executable
BIN
Binary file not shown.
-3
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
:root{--bg-color:#0b0f1a;--card-bg:#1e293bb3;--input-bg:#33415580;--text-color:#f8fafc;--text-muted:#94a3b8;--primary-color:#3b82f6;--primary-hover:#2563eb;--accent-color:#6366f1;--border-color:#4b556366;--success-color:#10b981;--glass-border:#ffffff1a;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}body{color:var(--text-color);letter-spacing:-.01em;background:radial-gradient(circle at 100% 0,#1e293b,#0b0f1a);min-height:100vh;margin:0;padding:30px;font-family:Inter,system-ui,-apple-system,sans-serif}.container{max-width:720px;margin:0 auto}h1{background:linear-gradient(135deg,#60a5fa,#a78bfa);-webkit-text-fill-color:transparent;-webkit-background-clip:text;background-clip:text;margin-bottom:30px;font-size:28px;font-weight:700}.section{background:var(--card-bg);-webkit-backdrop-filter:blur(12px);border:1px solid var(--glass-border);border-radius:16px;margin-bottom:24px;padding:24px;transition:transform .2s;box-shadow:0 10px 15px -3px #0003}.section:hover{border-color:#fff3}label{color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px;font-size:13px;font-weight:600;display:block}input[type=checkbox]{accent-color:var(--primary-color);cursor:pointer;width:18px;height:18px}input[type=text],input[type=password],select,textarea{background:var(--input-bg);border:1px solid var(--border-color);color:#fff;box-sizing:border-box;border-radius:10px;width:100%;padding:12px 14px;font-size:14px;transition:all .2s}select{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-position:right 14px center;background-repeat:no-repeat;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{border-color:var(--primary-color);background:#334155cc;outline:none;box-shadow:0 0 0 3px #3b82f633}.form-control{width:100%;max-width:480px}.input-wrapper{flex:1;min-width:0;position:relative}.input-wrapper input{box-sizing:border-box;width:100%;padding-right:44px;display:block}.eye-btn{cursor:pointer;opacity:.5;justify-content:center;align-items:center;font-size:16px;line-height:1;transition:opacity .2s;display:flex;position:absolute;top:50%;right:8px;transform:translateY(-50%);box-shadow:none!important;background:0 0!important;border:none!important;padding:4px!important}.eye-btn:hover{opacity:1;transform:translateY(-50%)scale(1.1)}.save-status{background:var(--success-color);color:#fff;opacity:0;z-index:1000;border-radius:12px;padding:12px 24px;font-weight:600;transition:all .3s cubic-bezier(.4,0,.2,1);position:fixed;bottom:30px;right:30px;transform:translateY(100px);box-shadow:0 10px 15px -3px #0003}.save-status.show{opacity:1;transform:translateY(0)}button{background-color:var(--primary-color);color:#fff;cursor:pointer;white-space:nowrap;border:none;border-radius:10px;flex-shrink:0;padding:10px 20px;font-size:14px;font-weight:600;transition:all .2s cubic-bezier(.4,0,.2,1);box-shadow:0 4px 6px -1px #0003}button:hover{background-color:var(--primary-hover);transform:translateY(-1px);box-shadow:0 10px 15px -3px #0000004d}button:active{transform:translateY(0)}button[style*="background: transparent"]{opacity:.7;text-decoration:underline;box-shadow:none!important;background:0 0!important}button[style*="background: transparent"]:hover{opacity:1}.flex-row{flex-wrap:nowrap;align-items:center;gap:12px;display:flex}.flex-between{justify-content:space-between;align-items:center;display:flex}.history-list{max-height:250px;margin-top:10px;padding-right:5px;overflow-y:auto}.history-item{background:#ffffff08;border:1px solid #ffffff0d;border-radius:12px;margin-bottom:10px;padding:14px;transition:all .2s}.history-item:hover{background:#ffffff0f;transform:translate(2px)}.history-time{color:var(--primary-color);margin-bottom:6px;font-size:11px;font-weight:700;display:block}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:10px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}
|
||||
+3
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
-2
@@ -5,8 +5,9 @@
|
||||
<meta charset="UTF-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-BNpC-7X0.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BsntdVbh.css">
|
||||
|
||||
<script type="module" crossorigin src="/assets/index.c31e3052.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.d8635aeb.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -182,6 +183,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Vendored
+4
@@ -32,6 +32,10 @@ export function StartRecording():Promise<void>;
|
||||
|
||||
export function StopRecording():Promise<void>;
|
||||
|
||||
export function ToggleRecording():Promise<void>;
|
||||
|
||||
export function ToggleStartup(arg1:boolean):Promise<string>;
|
||||
|
||||
export function UninstallWhisper():Promise<string>;
|
||||
|
||||
export function Version():Promise<string>;
|
||||
|
||||
@@ -62,6 +62,10 @@ export function StopRecording() {
|
||||
return window['go']['main']['App']['StopRecording']();
|
||||
}
|
||||
|
||||
export function ToggleRecording() {
|
||||
return window['go']['main']['App']['ToggleRecording']();
|
||||
}
|
||||
|
||||
export function ToggleStartup(arg1) {
|
||||
return window['go']['main']['App']['ToggleStartup'](arg1);
|
||||
}
|
||||
@@ -69,3 +73,7 @@ export function ToggleStartup(arg1) {
|
||||
export function UninstallWhisper() {
|
||||
return window['go']['main']['App']['UninstallWhisper']();
|
||||
}
|
||||
|
||||
export function Version() {
|
||||
return window['go']['main']['App']['Version']();
|
||||
}
|
||||
|
||||
+82
-1
@@ -246,4 +246,85 @@ export function OnFileDropOff() :void
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
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>;
|
||||
@@ -239,4 +239,60 @@ 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);
|
||||
}
|
||||
@@ -154,11 +154,11 @@ func (l *Listener) eventLoop(hk *xhk.Hotkey) {
|
||||
// If Wayland doesn't send Deactivated (Keyup), this acts as a Toggle fallback.
|
||||
select {
|
||||
case <-hk.Keyup():
|
||||
// Out-of-order X11 auto-repeat. Ignore both.
|
||||
// Out-of-order auto-repeat. Ignore both.
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
// Genuine second press. Toggle off.
|
||||
logger.Info("Shortcut activated again: stopping recording (Wayland toggle fallback)")
|
||||
go l.stopCallback()
|
||||
logger.Info("Shortcut activated again: toggling recording (Wayland toggle fallback)")
|
||||
go l.startCallback()
|
||||
isRecording = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ var ModMap = map[string]xhk.Modifier{
|
||||
"control": xhk.ModCtrl,
|
||||
"shift": xhk.ModShift,
|
||||
"alt": xhk.Mod1,
|
||||
"option": xhk.Mod1,
|
||||
"win": xhk.Mod4,
|
||||
"windows": xhk.Mod4,
|
||||
"meta": xhk.Mod4,
|
||||
|
||||
@@ -14,19 +14,24 @@ const appName = "wis-free-v3"
|
||||
const baseDesktopFileTemplate = `[Desktop Entry]
|
||||
Type=Application
|
||||
Name=WIS Free V3
|
||||
Comment=Voice Recording and Transcription
|
||||
%s
|
||||
%s
|
||||
Icon=%s
|
||||
StartupWMClass=%s
|
||||
Terminal=false
|
||||
Categories=Utility;
|
||||
Keywords=voice;recorder;transcription;
|
||||
`
|
||||
|
||||
const autostartDesktopFileTemplate = `[Desktop Entry]
|
||||
Type=Application
|
||||
Name=WIS Free V3
|
||||
Comment=Voice Recording and Transcription
|
||||
%s
|
||||
Icon=wis-free-v3
|
||||
StartupWMClass=wis-free-v3
|
||||
%s
|
||||
Icon=%s
|
||||
StartupWMClass=%s
|
||||
Terminal=false
|
||||
Categories=Utility;
|
||||
X-GNOME-Autostart-enabled=true
|
||||
@@ -60,7 +65,7 @@ func EnsureDesktopFile(iconBytes []byte) error {
|
||||
return fmt.Errorf("failed to write icon to pixmaps: %w", err)
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(baseDesktopFileTemplate, desktopExecField(exePath), appName, appName)
|
||||
content := fmt.Sprintf(baseDesktopFileTemplate, desktopExecField(exePath), desktopTryExecField(exePath), iconPath, appName)
|
||||
return os.WriteFile(desktopPath, []byte(content), 0644)
|
||||
}
|
||||
|
||||
@@ -113,6 +118,11 @@ func desktopExecField(exePath string) string {
|
||||
return `Exec="` + escaped + `"`
|
||||
}
|
||||
|
||||
// desktopTryExecField returns one line: TryExec=/path (desktop entries do not accept quoting for TryExec)
|
||||
func desktopTryExecField(exePath string) string {
|
||||
return "TryExec=" + exePath
|
||||
}
|
||||
|
||||
func AddToStartup() error {
|
||||
autostartPath, err := getAutostartPath()
|
||||
if err != nil {
|
||||
@@ -124,7 +134,17 @@ func AddToStartup() error {
|
||||
return fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(autostartDesktopFileTemplate, desktopExecField(exePath))
|
||||
// Prefer a concrete icon file path if we already created one in EnsureDesktopFile.
|
||||
// Fall back to the icon name (theme lookup) if it doesn't exist.
|
||||
iconValue := appName
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
iconPath := filepath.Join(home, ".local", "share", "pixmaps", appName+".png")
|
||||
if _, statErr := os.Stat(iconPath); statErr == nil {
|
||||
iconValue = iconPath
|
||||
}
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(autostartDesktopFileTemplate, desktopExecField(exePath), desktopTryExecField(exePath), iconValue, appName)
|
||||
if err := os.WriteFile(autostartPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write autostart file: %w", err)
|
||||
}
|
||||
|
||||
@@ -46,21 +46,6 @@ var statusMenuItem *systray.MenuItem
|
||||
var triggerCountItem *systray.MenuItem
|
||||
var triggerCount int
|
||||
|
||||
// Start initializes and runs the system tray.
|
||||
// This function blocks until the tray is terminated.
|
||||
func Start(app App) {
|
||||
// Pin this goroutine to one OS thread for the whole lifetime of systray.Run.
|
||||
// getlantern/systray creates the notify icon and runs the Win32 message pump on
|
||||
// whichever thread calls Run; if the goroutine migrates between threads, callbacks
|
||||
// and the tray context menu break until restart (often seen after sleep/resume).
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
systray.Run(
|
||||
func() { onReady(app) },
|
||||
onExit,
|
||||
)
|
||||
}
|
||||
|
||||
func appDisplayName(app App) string {
|
||||
v := app.Version()
|
||||
if v != "" && v != "dev" {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build linux
|
||||
|
||||
package tray
|
||||
|
||||
import "github.com/getlantern/systray"
|
||||
|
||||
// On Linux, Wails already runs the GTK main loop on the main thread.
|
||||
// Calling systray.Run() would start a second gtk_main() and may abort.
|
||||
// Instead, we only register the tray and let the existing GTK loop drive it.
|
||||
func Start(app App) {
|
||||
systray.Register(
|
||||
func() { onReady(app) },
|
||||
onExit,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build !linux
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/getlantern/systray"
|
||||
)
|
||||
|
||||
// Start initializes and runs the system tray.
|
||||
// This function blocks until the tray is terminated.
|
||||
func Start(app App) {
|
||||
// Pin this goroutine to one OS thread for the whole lifetime of systray.Run.
|
||||
// getlantern/systray creates the notify icon and runs the Win32 message pump on
|
||||
// whichever thread calls Run; if the goroutine migrates between threads, callbacks
|
||||
// and the tray context menu break until restart (often seen after sleep/resume).
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
systray.Run(
|
||||
func() { onReady(app) },
|
||||
onExit,
|
||||
)
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// 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);
|
||||
|
||||
// Effective modifier masks for XGrabKey when NumLock / CapsLock are on.
|
||||
// See: https://stackoverflow.com/questions/4037230/how-to-handle-global-hotkeys-with-x11-xlib
|
||||
static unsigned int mod_masks[4];
|
||||
|
||||
static void init_mod_masks(unsigned int mod) {
|
||||
mod_masks[0] = mod;
|
||||
mod_masks[1] = mod | Mod2Mask;
|
||||
mod_masks[2] = mod | LockMask;
|
||||
mod_masks[3] = mod | Mod2Mask | LockMask;
|
||||
}
|
||||
|
||||
static void grab_all(Display* d, int keycode, unsigned int mod, Window w) {
|
||||
init_mod_masks(mod);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
XGrabKey(d, keycode, mod_masks[i], w, False, GrabModeAsync, GrabModeAsync);
|
||||
}
|
||||
}
|
||||
|
||||
static void ungrab_all(Display* d, int keycode, unsigned int mod, Window w) {
|
||||
init_mod_masks(mod);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
XUngrabKey(d, keycode, mod_masks[i], w);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
XCloseDisplay(d);
|
||||
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);
|
||||
if (keycode == 0) {
|
||||
XCloseDisplay(d);
|
||||
return -1;
|
||||
}
|
||||
|
||||
Window root = DefaultRootWindow(d);
|
||||
grab_all(d, keycode, mod, root);
|
||||
XEvent ev;
|
||||
|
||||
while(1) {
|
||||
if (checkCancel(hkhandle) == 1) {
|
||||
break;
|
||||
}
|
||||
if (XPending(d) > 0) {
|
||||
XNextEvent(d, &ev);
|
||||
switch(ev.type) {
|
||||
case KeyPress:
|
||||
if (ev.xkey.keycode == (unsigned)keycode) {
|
||||
hotkeyDown(hkhandle);
|
||||
}
|
||||
continue;
|
||||
case KeyRelease:
|
||||
if (ev.xkey.keycode == (unsigned)keycode) {
|
||||
hotkeyUp(hkhandle);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
usleep(10000); // Poll every 10ms for snappy responsiveness without high CPU
|
||||
}
|
||||
}
|
||||
|
||||
ungrab_all(d, keycode, mod, root);
|
||||
XCloseDisplay(d);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build linux && !cgo
|
||||
//go:build linux
|
||||
|
||||
package hotkey
|
||||
|
||||
@@ -14,10 +14,6 @@ func (hk *Hotkey) unregister() error {
|
||||
hk.mu.Unlock()
|
||||
return errors.New("hotkey is not registered.")
|
||||
}
|
||||
if hk.backend != linuxHKPortal {
|
||||
hk.mu.Unlock()
|
||||
return errors.New("hotkey: invalid backend state")
|
||||
}
|
||||
hk.registered = false
|
||||
hk.mu.Unlock()
|
||||
return hk.cleanupPortal()
|
||||
@@ -1,42 +0,0 @@
|
||||
//go:build linux && cgo
|
||||
|
||||
package hotkey
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
)
|
||||
|
||||
func (hk *Hotkey) register() error {
|
||||
if usePortalBackend() {
|
||||
if err := hk.registerPortal(); err == nil {
|
||||
return nil
|
||||
} else {
|
||||
log.Printf("wis-free-v3: Wayland global-shortcuts portal unavailable (%v); trying X11", err)
|
||||
}
|
||||
}
|
||||
return hk.registerX11()
|
||||
}
|
||||
|
||||
func (hk *Hotkey) unregister() error {
|
||||
hk.mu.Lock()
|
||||
if !hk.registered {
|
||||
hk.mu.Unlock()
|
||||
return errors.New("hotkey is not registered.")
|
||||
}
|
||||
switch hk.backend {
|
||||
case linuxHKPortal:
|
||||
hk.registered = false
|
||||
hk.mu.Unlock()
|
||||
return hk.cleanupPortal()
|
||||
case linuxHKX11:
|
||||
hk.cancel()
|
||||
hk.registered = false
|
||||
hk.mu.Unlock()
|
||||
<-hk.canceled
|
||||
return nil
|
||||
default:
|
||||
hk.mu.Unlock()
|
||||
return errors.New("hotkey: invalid backend state")
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
package hotkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
@@ -11,7 +10,6 @@ import (
|
||||
|
||||
const (
|
||||
linuxHKNone = iota
|
||||
linuxHKX11
|
||||
linuxHKPortal
|
||||
)
|
||||
|
||||
@@ -20,11 +18,6 @@ type platformHotkey struct {
|
||||
registered bool
|
||||
backend int
|
||||
|
||||
// X11 (CGO)
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
canceled chan struct{}
|
||||
|
||||
// Wayland / XDG portal global shortcuts
|
||||
portalStop chan struct{}
|
||||
portalDone chan struct{}
|
||||
|
||||
@@ -23,20 +23,13 @@ const (
|
||||
ifaceSession = "org.freedesktop.portal.Session"
|
||||
wisfreeGlobalShortcutID = "com.wisfree.push-to-record"
|
||||
envForcePortal = "WISFREE_USE_PORTAL_HOTKEY"
|
||||
envForceX11 = "WISFREE_USE_X11_HOTKEY"
|
||||
)
|
||||
|
||||
func usePortalBackend() bool {
|
||||
if os.Getenv(envForceX11) == "1" {
|
||||
if os.Getenv(envForcePortal) == "0" {
|
||||
return false
|
||||
}
|
||||
if os.Getenv(envForcePortal) == "1" {
|
||||
return true
|
||||
}
|
||||
if os.Getenv("WAYLAND_DISPLAY") != "" {
|
||||
return true
|
||||
}
|
||||
return strings.EqualFold(os.Getenv("XDG_SESSION_TYPE"), "wayland")
|
||||
return true
|
||||
}
|
||||
|
||||
func randomPortalToken() string {
|
||||
@@ -162,7 +155,7 @@ func portalWaitRequest(conn *dbus.Conn, reqPath dbus.ObjectPath) (uint32, map[st
|
||||
if sig == nil || sig.Path != reqPath {
|
||||
continue
|
||||
}
|
||||
if sig.Name != "Response" {
|
||||
if !strings.HasSuffix(sig.Name, ".Response") {
|
||||
continue
|
||||
}
|
||||
if len(sig.Body) < 2 {
|
||||
@@ -208,6 +201,9 @@ func (hk *Hotkey) registerPortal() error {
|
||||
createOpts := map[string]dbus.Variant{
|
||||
"handle_token": dbus.MakeVariant(randomPortalToken()),
|
||||
"session_handle_token": dbus.MakeVariant(randomPortalToken()),
|
||||
// Required by xdg-desktop-portal on some desktops (e.g. Fedora/GNOME).
|
||||
// This should match the app's .desktop file id when possible.
|
||||
"app_id": dbus.MakeVariant("wis-free-v3"),
|
||||
}
|
||||
|
||||
var createReqPath dbus.ObjectPath
|
||||
@@ -270,7 +266,7 @@ func (hk *Hotkey) registerPortal() error {
|
||||
if code != 0 {
|
||||
_ = conn.Object(portalBusName, sessPath).Call(ifaceSession+".Close", 0).Store()
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("BindShortcuts rejected (code %d); install a desktop with GlobalShortcuts portal support (e.g. recent KDE Plasma) or set %s=1 to force X11 hotkeys under XWayland", code, envForceX11)
|
||||
return fmt.Errorf("BindShortcuts rejected (code %d); install a desktop with GlobalShortcuts portal support (e.g. recent KDE Plasma or GNOME)", code)
|
||||
}
|
||||
if sc, ok := results["shortcuts"]; ok {
|
||||
val := sc.Value()
|
||||
@@ -323,8 +319,8 @@ func (hk *Hotkey) portalSignalLoop() {
|
||||
ch := make(chan *dbus.Signal, 32)
|
||||
conn.Signal(ch)
|
||||
rule := fmt.Sprintf(
|
||||
"type='signal',interface='%s',path='%s'",
|
||||
ifaceGlobalShortcuts, string(portalObjectPath),
|
||||
"type='signal',interface='%s'",
|
||||
ifaceGlobalShortcuts,
|
||||
)
|
||||
if err := conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, rule).Store(); err != nil {
|
||||
log.Printf("wis-free-v3 hotkey: AddMatch GlobalShortcuts: %v", err)
|
||||
@@ -340,9 +336,6 @@ func (hk *Hotkey) portalSignalLoop() {
|
||||
if !ok || sig == nil {
|
||||
return
|
||||
}
|
||||
if sig.Path != portalObjectPath {
|
||||
continue
|
||||
}
|
||||
if len(sig.Body) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
//go:build linux && cgo
|
||||
|
||||
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"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
)
|
||||
|
||||
func (hk *Hotkey) registerX11() error {
|
||||
if C.displayTest() != 0 {
|
||||
return fmt.Errorf("X11 display not available (is DISPLAY set?)")
|
||||
}
|
||||
hk.mu.Lock()
|
||||
if hk.registered {
|
||||
hk.mu.Unlock()
|
||||
return errors.New("hotkey already registered.")
|
||||
}
|
||||
hk.backend = linuxHKX11
|
||||
hk.registered = true
|
||||
hk.ctx, hk.cancel = context.WithCancel(context.Background())
|
||||
hk.canceled = make(chan struct{})
|
||||
hk.mu.Unlock()
|
||||
|
||||
go hk.handleX11()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hk *Hotkey) handleX11() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
var mod Modifier
|
||||
for _, m := range hk.mods {
|
||||
mod = mod | m
|
||||
}
|
||||
h := cgo.NewHandle(hk)
|
||||
defer h.Delete()
|
||||
|
||||
for {
|
||||
_ = 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{}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"wis-free-v3/internal/logger"
|
||||
"wis-free-v3/internal/platform"
|
||||
@@ -40,18 +41,32 @@ var instanceLock *os.File
|
||||
// from scripts/VERSION. Default is used for plain `go build` / `wails build`.
|
||||
var AppVersion = "dev"
|
||||
|
||||
var initialAction string
|
||||
|
||||
func main() {
|
||||
// systray's package init calls runtime.LockOSThread() on the program's startup
|
||||
// thread. Undo that so the main goroutine is not permanently bound; the tray
|
||||
// goroutine locks itself in tray.Start instead (see internal/ui/tray/tray.go).
|
||||
runtime.UnlockOSThread()
|
||||
|
||||
// If we are being used as a helper command (e.g. GNOME custom shortcut),
|
||||
// signal the running instance and exit.
|
||||
for _, arg := range os.Args[1:] {
|
||||
if strings.HasPrefix(arg, "--action=") {
|
||||
action := strings.TrimPrefix(arg, "--action=")
|
||||
initialAction = action
|
||||
if tryNotifyRunningInstanceAction(action) {
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure only one instance of the application is running
|
||||
if !acquireInstanceLock() {
|
||||
// Another copy is already running (often left in the tray). Without this,
|
||||
// we would exit silently and the user sees "nothing happens" when clicking
|
||||
// the launcher again.
|
||||
tryNotifyRunningInstanceToShow()
|
||||
_ = tryNotifyRunningInstanceToShow()
|
||||
os.Exit(0)
|
||||
}
|
||||
runSecondInstanceListener()
|
||||
|
||||
+53
-4
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -15,6 +16,17 @@ const instanceSockName = "instance.sock"
|
||||
// secondInstanceWake is closed when a second process asks the running app to show its window.
|
||||
var secondInstanceWake = make(chan struct{}, 8)
|
||||
|
||||
// secondInstanceCommand receives commands from a spawned second process (e.g. desktop shortcut).
|
||||
// Commands are 1-byte values written to the unix socket.
|
||||
var secondInstanceCommand = make(chan byte, 16)
|
||||
|
||||
const (
|
||||
instanceCmdShow byte = 1
|
||||
instanceCmdStart byte = 2
|
||||
instanceCmdStop byte = 3
|
||||
instanceCmdToggle byte = 4
|
||||
)
|
||||
|
||||
func instanceSocketPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
@@ -23,17 +35,40 @@ func instanceSocketPath() (string, error) {
|
||||
return filepath.Join(home, configDir, instanceSockName), nil
|
||||
}
|
||||
|
||||
func tryNotifyRunningInstanceToShow() {
|
||||
func tryNotifyRunningInstance(cmd byte) bool {
|
||||
path, err := instanceSocketPath()
|
||||
if err != nil {
|
||||
return
|
||||
return false
|
||||
}
|
||||
c, err := net.DialTimeout("unix", path, 500*time.Millisecond)
|
||||
if err != nil {
|
||||
return
|
||||
return false
|
||||
}
|
||||
defer c.Close()
|
||||
_, _ = c.Write([]byte{1})
|
||||
if cmd == 0 {
|
||||
cmd = instanceCmdShow
|
||||
}
|
||||
_, _ = c.Write([]byte{cmd})
|
||||
return true
|
||||
}
|
||||
|
||||
func tryNotifyRunningInstanceToShow() bool {
|
||||
return tryNotifyRunningInstance(instanceCmdShow)
|
||||
}
|
||||
|
||||
func tryNotifyRunningInstanceAction(action string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(action)) {
|
||||
case "show":
|
||||
return tryNotifyRunningInstance(instanceCmdShow)
|
||||
case "start":
|
||||
return tryNotifyRunningInstance(instanceCmdStart)
|
||||
case "stop":
|
||||
return tryNotifyRunningInstance(instanceCmdStop)
|
||||
case "toggle":
|
||||
return tryNotifyRunningInstance(instanceCmdToggle)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func runSecondInstanceListener() {
|
||||
@@ -54,6 +89,20 @@ func runSecondInstanceListener() {
|
||||
}
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
buf := make([]byte, 1)
|
||||
n, _ := conn.Read(buf)
|
||||
if n == 1 {
|
||||
select {
|
||||
case secondInstanceCommand <- buf[0]:
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
// Backwards-compatible: any connection with no payload = show window.
|
||||
select {
|
||||
case secondInstanceCommand <- instanceCmdShow:
|
||||
default:
|
||||
}
|
||||
}
|
||||
_, _ = io.Copy(io.Discard, conn)
|
||||
select {
|
||||
case secondInstanceWake <- struct{}{}:
|
||||
|
||||
Reference in New Issue
Block a user