mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
refactor: implement cross-platform abstraction for process management, media control, and startup configuration (TESTING)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# wis-free-v3
|
||||
|
||||
A high-performance, native Windows voice dictation application built in Go using the Wails framework. **wis-free-v3** provides instant speech-to-text with AI-powered refinement, operating as a background service with global hotkey support.
|
||||
A high-performance, cross-platform (Windows & Linux) voice dictation application built in Go using the Wails framework. **wis-free-v3** provides instant speech-to-text with AI-powered refinement, operating as a background service with global hotkey support.
|
||||
|
||||
---
|
||||
|
||||
@@ -8,6 +8,7 @@ A high-performance, native Windows voice dictation application built in Go using
|
||||
|
||||
- **Blazing Fast**: Native implementation ensures zero lag during recording and transcription.
|
||||
- **Global Accessibility**: Trigger from anywhere via configurable global hotkeys.
|
||||
- **Cross-Platform**: Supports Windows natively and Linux (X11 strongly recommended; Wayland users may need to map desktop shortcut triggers).
|
||||
- **AI-Powered Refinement**: Integrates Groq (Whisper + Llama) for intelligent punctuation and grammar fixing.
|
||||
- **Offline Capability**: Supports local Whisper.cpp for sensitive or offline workflows.
|
||||
- **Micro-Automation**: Automatically pastes transcribed text directly into your active window.
|
||||
|
||||
@@ -8,15 +8,14 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"wis-free-v3/internal/audio/media"
|
||||
|
||||
"wis-free-v3/internal/audio/recorder"
|
||||
"wis-free-v3/internal/config"
|
||||
"wis-free-v3/internal/hotkey"
|
||||
"wis-free-v3/internal/logger"
|
||||
"wis-free-v3/internal/platform"
|
||||
"wis-free-v3/internal/services/transcriber"
|
||||
"wis-free-v3/internal/services/whisper"
|
||||
"wis-free-v3/internal/system/startup"
|
||||
"wis-free-v3/internal/ui/overlay"
|
||||
"wis-free-v3/internal/ui/tray"
|
||||
|
||||
"github.com/go-vgo/robotgo"
|
||||
@@ -31,7 +30,7 @@ type App struct {
|
||||
hotkeyListener *hotkey.Listener
|
||||
transcriber *transcriber.Client
|
||||
config *config.Config
|
||||
overlay *overlay.Overlay
|
||||
overlay platform.Overlay
|
||||
recordingPath string
|
||||
isQuitting bool
|
||||
wasMediaPlaying bool
|
||||
@@ -131,7 +130,7 @@ func (a *App) StartRecording() {
|
||||
tray.UpdateStatus("Recording...")
|
||||
|
||||
// Pause media if playing (this is slow due to PowerShell)
|
||||
a.wasMediaPlaying = media.PauseMedia()
|
||||
a.wasMediaPlaying = platform.PauseMedia()
|
||||
if a.wasMediaPlaying {
|
||||
logger.Info("Media paused for recording")
|
||||
}
|
||||
@@ -143,7 +142,7 @@ func (a *App) StopRecording() {
|
||||
logger.Info("StopRecording triggered")
|
||||
|
||||
// Resume media if it was playing before
|
||||
media.ResumeMedia(a.wasMediaPlaying)
|
||||
platform.ResumeMedia(a.wasMediaPlaying)
|
||||
if a.wasMediaPlaying {
|
||||
logger.Info("Media resumed after recording")
|
||||
}
|
||||
@@ -281,7 +280,7 @@ func (a *App) GetSettings() map[string]interface{} {
|
||||
conf["language"] = a.config.Language
|
||||
conf["microphone_device"] = a.config.MicrophoneDevice
|
||||
conf["history"] = a.config.History
|
||||
conf["startup"] = startup.IsInStartup()
|
||||
conf["startup"] = platform.IsInStartup()
|
||||
return conf
|
||||
}
|
||||
|
||||
@@ -377,9 +376,9 @@ func (a *App) GetMicrophones() []map[string]interface{} {
|
||||
func (a *App) ToggleStartup(enable bool) string {
|
||||
var err error
|
||||
if enable {
|
||||
err = startup.AddToStartup()
|
||||
err = platform.AddToStartup()
|
||||
} else {
|
||||
err = startup.RemoveFromStartup()
|
||||
err = platform.RemoveFromStartup()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -445,7 +444,7 @@ func (a *App) startupHeadless() {
|
||||
}
|
||||
|
||||
// Initialize Overlay
|
||||
a.overlay = overlay.NewOverlay()
|
||||
a.overlay = platform.NewOverlay()
|
||||
|
||||
// Connect volume feedback from recorder to overlay
|
||||
if a.audioRecorder != nil && a.overlay != nil {
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
module wis-free-v3
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/gen2brain/malgo v0.11.24
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//go:build linux
|
||||
package linux
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"wis-free-v3/internal/logger"
|
||||
)
|
||||
|
||||
// IsPlaying checks if media is currently playing using playerctl
|
||||
func IsPlaying() bool {
|
||||
cmd := exec.Command("playerctl", "status")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return string(out) == "Playing\n"
|
||||
}
|
||||
|
||||
// TogglePlayPause sends the play-pause command via playerctl
|
||||
func TogglePlayPause() {
|
||||
cmd := exec.Command("playerctl", "play-pause")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
logger.Error("Failed to toggle media on Linux: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// PauseMedia checks if playing, pauses if so, returns whether we paused
|
||||
func PauseMedia() bool {
|
||||
wasPlaying := IsPlaying()
|
||||
if wasPlaying {
|
||||
TogglePlayPause()
|
||||
}
|
||||
return wasPlaying
|
||||
}
|
||||
|
||||
// ResumeMedia resumes only if we paused it
|
||||
func ResumeMedia(wasPaused bool) {
|
||||
if wasPaused {
|
||||
TogglePlayPause()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build linux
|
||||
package linux
|
||||
|
||||
// linuxOverlay provides a simple dummy implementation for Linux.
|
||||
// It relies on tray icon status updates for visual feedback instead of raw drawing.
|
||||
type linuxOverlay struct{}
|
||||
|
||||
func NewOverlay() *linuxOverlay {
|
||||
return &linuxOverlay{}
|
||||
}
|
||||
|
||||
func (o *linuxOverlay) Show(message string) {}
|
||||
func (o *linuxOverlay) Hide() {}
|
||||
func (o *linuxOverlay) SetVolume(level float64) {}
|
||||
func (o *linuxOverlay) Close() {}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build linux
|
||||
package linux
|
||||
|
||||
import "syscall"
|
||||
|
||||
// IsProcessRunning checks if a process with the given PID exists on Linux.
|
||||
func IsProcessRunning(pid int) bool {
|
||||
// sending signal 0 checks if the process exists and we have permission
|
||||
err := syscall.Kill(pid, 0)
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//go:build linux
|
||||
package linux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
appName = "wis-free-v3"
|
||||
desktopFileContent = `[Desktop Entry]
|
||||
Type=Application
|
||||
Name=WIS Free V3
|
||||
Exec="%s"
|
||||
Terminal=false
|
||||
Categories=Utility;
|
||||
X-GNOME-Autostart-enabled=true
|
||||
`
|
||||
)
|
||||
|
||||
func getAutostartPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
configDir := os.Getenv("XDG_CONFIG_HOME")
|
||||
if configDir == "" {
|
||||
configDir = filepath.Join(home, ".config")
|
||||
}
|
||||
autostartDir := filepath.Join(configDir, "autostart")
|
||||
|
||||
if err := os.MkdirAll(autostartDir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(autostartDir, appName+".desktop"), nil
|
||||
}
|
||||
|
||||
func getExecutablePath() (string, error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Abs(exe)
|
||||
}
|
||||
|
||||
func AddToStartup() error {
|
||||
autostartPath, err := getAutostartPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine autostart path: %w", err)
|
||||
}
|
||||
|
||||
exePath, err := getExecutablePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(desktopFileContent, exePath)
|
||||
if err := os.WriteFile(autostartPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write autostart file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveFromStartup() error {
|
||||
autostartPath, err := getAutostartPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine autostart path: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Remove(autostartPath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to remove autostart file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsInStartup() bool {
|
||||
autostartPath, err := getAutostartPath()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, err = os.Stat(autostartPath)
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package platform
|
||||
|
||||
// Overlay defines the cross-platform interface for screen overlays
|
||||
type Overlay interface {
|
||||
Show(message string)
|
||||
Hide()
|
||||
SetVolume(level float64)
|
||||
Close()
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//go:build linux
|
||||
package platform
|
||||
|
||||
import "wis-free-v3/internal/linux"
|
||||
|
||||
func NewOverlay() Overlay {
|
||||
return linux.NewOverlay()
|
||||
}
|
||||
|
||||
func PauseMedia() bool {
|
||||
return linux.PauseMedia()
|
||||
}
|
||||
|
||||
func ResumeMedia(wasPaused bool) {
|
||||
linux.ResumeMedia(wasPaused)
|
||||
}
|
||||
|
||||
func AddToStartup() error {
|
||||
return linux.AddToStartup()
|
||||
}
|
||||
|
||||
func RemoveFromStartup() error {
|
||||
return linux.RemoveFromStartup()
|
||||
}
|
||||
|
||||
func IsInStartup() bool {
|
||||
return linux.IsInStartup()
|
||||
}
|
||||
|
||||
func IsProcessRunning(pid int) bool {
|
||||
return linux.IsProcessRunning(pid)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//go:build windows
|
||||
package platform
|
||||
|
||||
import "wis-free-v3/internal/windows"
|
||||
|
||||
func NewOverlay() Overlay {
|
||||
return windows.NewOverlay()
|
||||
}
|
||||
|
||||
func PauseMedia() bool {
|
||||
return windows.PauseMedia()
|
||||
}
|
||||
|
||||
func ResumeMedia(wasPaused bool) {
|
||||
windows.ResumeMedia(wasPaused)
|
||||
}
|
||||
|
||||
func AddToStartup() error {
|
||||
return windows.AddToStartup()
|
||||
}
|
||||
|
||||
func RemoveFromStartup() error {
|
||||
return windows.RemoveFromStartup()
|
||||
}
|
||||
|
||||
func IsInStartup() bool {
|
||||
return windows.IsInStartup()
|
||||
}
|
||||
|
||||
func IsProcessRunning(pid int) bool {
|
||||
return windows.IsProcessRunning(pid)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package whisper
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func hideWindowContext(cmd *exec.Cmd) {
|
||||
// Not needed on non-Windows platforms
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//go:build windows
|
||||
|
||||
package whisper
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func hideWindowContext(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"wis-free-v3/internal/logger"
|
||||
@@ -285,11 +284,8 @@ func (m *Manager) Transcribe(audioPath string) (string, error) {
|
||||
// Set working directory to the binary's location so it can find DLLs
|
||||
cmd.Dir = filepath.Dir(binaryPath)
|
||||
|
||||
// Hide the console window
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
|
||||
}
|
||||
// Hide the console window on Windows
|
||||
hideWindowContext(cmd)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
outputStr := string(output)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"wis-free-v3/internal/config"
|
||||
"wis-free-v3/internal/logger"
|
||||
"wis-free-v3/internal/system/startup"
|
||||
"wis-free-v3/internal/platform"
|
||||
|
||||
"github.com/getlantern/systray"
|
||||
)
|
||||
@@ -50,9 +50,9 @@ func onReady(app App) {
|
||||
|
||||
menuSettings := systray.AddMenuItem("Settings", "Open settings window")
|
||||
menuStartup := systray.AddMenuItemCheckbox(
|
||||
"Start with Windows",
|
||||
"Automatically start when Windows boots",
|
||||
startup.IsInStartup(),
|
||||
"Start with system",
|
||||
"Automatically start when computer boots",
|
||||
platform.IsInStartup(),
|
||||
)
|
||||
|
||||
systray.AddSeparator()
|
||||
@@ -82,18 +82,18 @@ func handleMenuEvents(app App, settings, startupItem, exit *systray.MenuItem) {
|
||||
// toggleStartup handles the startup toggle menu item.
|
||||
func toggleStartup(item *systray.MenuItem) {
|
||||
if item.Checked() {
|
||||
if err := startup.RemoveFromStartup(); err != nil {
|
||||
if err := platform.RemoveFromStartup(); err != nil {
|
||||
logger.Error("Failed to remove from startup: %v", err)
|
||||
} else {
|
||||
item.Uncheck()
|
||||
logger.Info("Removed from Windows startup")
|
||||
logger.Info("Removed from system startup")
|
||||
}
|
||||
} else {
|
||||
if err := startup.AddToStartup(); err != nil {
|
||||
if err := platform.AddToStartup(); err != nil {
|
||||
logger.Error("Failed to add to startup: %v", err)
|
||||
} else {
|
||||
item.Check()
|
||||
logger.Info("Added to Windows startup")
|
||||
logger.Info("Added to system startup")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
package media
|
||||
//go:build windows
|
||||
package windows
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
@@ -10,8 +11,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
keybd_event = user32.NewProc("keybd_event")
|
||||
user32Media = syscall.NewLazyDLL("user32.dll")
|
||||
keybd_event = user32Media.NewProc("keybd_event")
|
||||
)
|
||||
|
||||
//go:embed check-media.ps1
|
||||
@@ -1,4 +1,5 @@
|
||||
package overlay
|
||||
//go:build windows
|
||||
package windows
|
||||
|
||||
import (
|
||||
"math"
|
||||
@@ -255,8 +256,17 @@ func (o *Overlay) run() {
|
||||
procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
|
||||
procDispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
|
||||
} else {
|
||||
// Higher resolution sleep for better responsiveness while keeping CPU low
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
o.mu.RLock()
|
||||
isShowing := o.isShowing
|
||||
o.mu.RUnlock()
|
||||
|
||||
if isShowing {
|
||||
// Higher resolution sleep for smooth animation when visible
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
} else {
|
||||
// Greatly reduce wakeups to save CPU when hidden (app is idle 99% of the time)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build windows
|
||||
package windows
|
||||
|
||||
import "syscall"
|
||||
|
||||
// IsProcessRunning checks if a process with the given PID exists on Windows.
|
||||
func IsProcessRunning(pid int) bool {
|
||||
const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
||||
|
||||
handle, err := syscall.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
syscall.CloseHandle(handle)
|
||||
return true
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// Package startup manages Windows startup registry entries for the application.
|
||||
// It allows the application to automatically start when the user logs in.
|
||||
package startup
|
||||
//go:build windows
|
||||
package windows
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"wis-free-v3/internal/logger"
|
||||
"wis-free-v3/internal/platform"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
@@ -87,7 +87,7 @@ func acquireInstanceLock() bool {
|
||||
// Check for existing lock file
|
||||
if data, err := os.ReadFile(lockPath); err == nil {
|
||||
if pid, err := strconv.Atoi(string(data)); err == nil {
|
||||
if isProcessRunning(pid) {
|
||||
if platform.IsProcessRunning(pid) {
|
||||
logger.Info("Another instance is already running (PID: %d)", pid)
|
||||
return false
|
||||
}
|
||||
@@ -136,16 +136,5 @@ func getLockPath() (string, error) {
|
||||
return filepath.Join(homeDir, configDir, lockFile), nil
|
||||
}
|
||||
|
||||
// isProcessRunning checks if a process with the given PID exists on Windows.
|
||||
func isProcessRunning(pid int) bool {
|
||||
const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
||||
|
||||
handle, err := syscall.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
syscall.CloseHandle(handle)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit on error
|
||||
set -e
|
||||
|
||||
# Change to the root directory of the project
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
APP_NAME="wis-free-v3"
|
||||
BUILD_DIR="build/bin"
|
||||
EXECUTABLE="$BUILD_DIR/$APP_NAME"
|
||||
|
||||
echo "========================================"
|
||||
echo " wis-free-v3 - Linux Build Script"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# 1. Check for basic tools
|
||||
if ! command_exists go; then
|
||||
echo "[ERROR] Go is not installed. Please install Go 1.23+."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command_exists wails; then
|
||||
echo "[INFO] Wails CLI not found. Installing..."
|
||||
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
export PATH=$PATH:$(go env GOPATH)/bin
|
||||
fi
|
||||
|
||||
# 2. Check for Linux dependencies
|
||||
echo "[1/3] Checking system dependencies..."
|
||||
MISSING_DEPS=0
|
||||
DEPS=("gcc" "pkg-config")
|
||||
|
||||
for dep in "${DEPS[@]}"; do
|
||||
if ! command_exists $dep; then
|
||||
echo "[WARNING] Missing basic build tool: $dep"
|
||||
MISSING_DEPS=1
|
||||
fi
|
||||
done
|
||||
|
||||
# We can't easily check C headers, but we can try to find them with pkg-config
|
||||
if command_exists pkg-config; then
|
||||
if ! pkg-config --exists gtk+-3.0 webkit2gtk-4.0; 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)."
|
||||
MISSING_DEPS=1
|
||||
fi
|
||||
if ! pkg-config --exists alsa; then
|
||||
echo "[WARNING] Missing audio dependencies (ALSA)."
|
||||
MISSING_DEPS=1
|
||||
fi
|
||||
if ! pkg-config --exists ayatana-appindicator3-0.1; then
|
||||
echo "[WARNING] Missing systray dependencies (ayatana-appindicator3)."
|
||||
MISSING_DEPS=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $MISSING_DEPS -eq 1 ]; then
|
||||
echo ""
|
||||
echo "It looks like you are missing some required libraries."
|
||||
|
||||
DEBIAN_DEPS="build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.0-dev libx11-dev libx11-xcb-dev libxtst-dev libasound2-dev libayatana-appindicator3-dev libxkbcommon-x11-dev"
|
||||
ARCH_DEPS="base-devel pkgconf gtk3 webkit2gtk libx11 libxtst alsa-lib libayatana-appindicator libxkbcommon-x11"
|
||||
|
||||
echo "The full list of dependencies needed:"
|
||||
echo " [Ubuntu/Debian]: sudo apt update && sudo apt install -y $DEBIAN_DEPS"
|
||||
echo " [Arch Linux]: sudo pacman -S $ARCH_DEPS"
|
||||
echo ""
|
||||
|
||||
if command_exists apt-get; then
|
||||
read -p "Would you like to automatically install missing dependencies now? (Requires sudo) (y/N): " INSTALL_DEPS
|
||||
if [[ "$INSTALL_DEPS" == "y" || "$INSTALL_DEPS" == "Y" ]]; then
|
||||
echo "Installing dependencies..."
|
||||
sudo apt update && sudo apt install -y $DEBIAN_DEPS
|
||||
else
|
||||
echo "Skipping installation."
|
||||
read -p "Press Enter to attempt build anyway, or Ctrl+C to cancel..."
|
||||
fi
|
||||
else
|
||||
read -p "Press Enter to attempt build anyway, or Ctrl+C to cancel..."
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Build the application
|
||||
echo "[2/3] Building with Wails..."
|
||||
wails build -platform linux/amd64 -clean
|
||||
|
||||
if [ ! -f "$EXECUTABLE" ]; then
|
||||
echo "[ERROR] Build failed. Binary not found at $EXECUTABLE."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[3/3] Build successful!"
|
||||
echo " Output: $EXECUTABLE"
|
||||
echo ""
|
||||
|
||||
# 4. Optional Installation
|
||||
read -p "Would you like to install it globally to /usr/local/bin and add a desktop shortcut? (y/n): " INSTALL
|
||||
if [[ "$INSTALL" == "y" || "$INSTALL" == "Y" ]]; then
|
||||
echo "Installing..."
|
||||
|
||||
# Needs sudo
|
||||
sudo cp "$EXECUTABLE" "/usr/local/bin/$APP_NAME"
|
||||
sudo chmod +x "/usr/local/bin/$APP_NAME"
|
||||
|
||||
# Create Desktop shortcut
|
||||
DESKTOP_FILE="/usr/share/applications/$APP_NAME.desktop"
|
||||
|
||||
# Try to grab the icon from the Wails build directory if available
|
||||
ICON_PATH="/usr/share/pixmaps/$APP_NAME.png"
|
||||
if [ -f "build/appicon.png" ]; then
|
||||
sudo cp "build/appicon.png" "$ICON_PATH"
|
||||
fi
|
||||
|
||||
cat << EOF > /tmp/$APP_NAME.desktop
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=WIS Free V3
|
||||
Comment=Voice Dictation App
|
||||
Exec=$APP_NAME
|
||||
Icon=$APP_NAME
|
||||
Terminal=false
|
||||
Categories=Utility;Audio;
|
||||
EOF
|
||||
|
||||
sudo mv /tmp/$APP_NAME.desktop "$DESKTOP_FILE"
|
||||
sudo chmod 644 "$DESKTOP_FILE"
|
||||
|
||||
echo ""
|
||||
echo "Installation complete! You can now launch 'WIS Free V3' from your app launcher,"
|
||||
echo "or by typing '$APP_NAME' in your terminal."
|
||||
else
|
||||
echo "Skipping installation. You can run the app directly via: ./$EXECUTABLE"
|
||||
fi
|
||||
Reference in New Issue
Block a user