feat: enhance Linux support with ydotool integration and improve logging

This commit is contained in:
Your Name
2026-06-09 00:25:05 -07:00
parent bc3d8cbce3
commit 05c8fbc474
7 changed files with 421 additions and 118 deletions
+28 -12
View File
@@ -9,7 +9,6 @@ import (
"sync"
"sync/atomic"
"math"
"unsafe"
"wis-free-v3/internal/logger"
@@ -202,14 +201,16 @@ func (r *AudioRecorder) Stop() error {
return nil
}
// Stop and release the capture device so Linux privacy indicators turn off
// between recordings. The next Start will initialize it again.
// Stop the capture device. We keep the device initialized between recordings
// to avoid the expensive re-initialization cycle. ALSA privacy indicators
// will still turn off because we've stopped the stream.
if r.device != nil {
if err := r.device.Stop(); err != nil {
logger.Error("Failed to stop audio device: %v", err)
}
r.device.Uninit()
r.device = nil
// Do NOT Uninit the device between recordings. Keeping it alive
// avoids expensive ALSA device re-probe and reduces CPU spikes.
// The device will be fully cleaned up in Cleanup().
}
// Finalize WAV file
@@ -262,13 +263,30 @@ func (r *AudioRecorder) onAudioData(_, inputSamples []byte, _ uint32) {
// Calculate volume if callback is set
if r.OnVolume != nil {
r.calculateVolume(inputSamples[:n])
}
}
}
// calculateVolume processes audio samples to compute the current volume level.
// Extracted to avoid allocations in the hot audio callback path.
func (r *AudioRecorder) calculateVolume(samples []byte) {
// S16LE: 2 bytes per sample
samples := len(inputSamples) / 2
sampleCount := len(samples) / 2
if sampleCount == 0 {
return
}
var maxAmplitude float64
for i := 0; i < samples; i++ {
// Read as int16
val := int16(binary.LittleEndian.Uint16(inputSamples[i*2 : i*2+2]))
absVal := math.Abs(float64(val))
// Use direct slice indexing to avoid per-sample allocations
for i := 0; i < sampleCount; i++ {
offset := i * 2
// Read as int16 via inlined LittleEndian to avoid function call overhead
val := int16(samples[offset]) | int16(samples[offset+1])<<8
absVal := float64(val)
if absVal < 0 {
absVal = -absVal
}
if absVal > maxAmplitude {
maxAmplitude = absVal
}
@@ -278,8 +296,6 @@ func (r *AudioRecorder) onAudioData(_, inputSamples []byte, _ uint32) {
level := maxAmplitude / 32767.0
r.OnVolume(level)
}
}
}
// writeWAVHeader writes a standard RIFF WAV header to the output file.
func (r *AudioRecorder) writeWAVHeader(dataSize uint32) error {
+16 -4
View File
@@ -21,6 +21,7 @@ type Listener struct {
hk *xhk.Hotkey
stopModPoll chan struct{}
mu sync.RWMutex
eventLoopDone chan struct{}
}
// NewListener creates a new hotkey listener with the specified shortcut and callbacks.
@@ -144,11 +145,22 @@ func (l *Listener) stopListeningLocked() {
}
l.hk = nil
}
// Wait for event loop to fully terminate before allowing re-registration
if l.eventLoopDone != nil {
<-l.eventLoopDone
l.eventLoopDone = nil
}
l.isListening = false
}
// eventLoop runs the main keyboard event processing loop.
func (l *Listener) eventLoop(hk *xhk.Hotkey) {
done := make(chan struct{})
l.mu.Lock()
l.eventLoopDone = done
l.mu.Unlock()
defer close(done)
var isRecording bool
for {
@@ -159,7 +171,7 @@ func (l *Listener) eventLoop(hk *xhk.Hotkey) {
}
if !isRecording {
logger.Info("Shortcut activated: starting recording")
go l.startCallback()
l.startCallback()
isRecording = true
} else {
// We received a second Keydown while already recording.
@@ -171,9 +183,9 @@ func (l *Listener) eventLoop(hk *xhk.Hotkey) {
// Genuine second press. Toggle off.
logger.Info("Shortcut activated again: toggling recording (Wayland toggle fallback)")
if l.stopCallback != nil {
go l.stopCallback()
l.stopCallback()
} else {
go l.startCallback()
l.startCallback()
}
isRecording = false
}
@@ -196,7 +208,7 @@ func (l *Listener) eventLoop(hk *xhk.Hotkey) {
case <-time.After(50 * time.Millisecond):
// Key was genuinely physically released
logger.Info("Shortcut released: stopping recording")
go l.stopCallback()
l.stopCallback()
isRecording = false
}
}
+8 -1
View File
@@ -76,6 +76,11 @@ func Error(format string, args ...interface{}) {
log("ERROR", format, args...)
}
// Debug logs a debug message with timestamp.
func Debug(format string, args ...interface{}) {
log("DEBUG", format, args...)
}
// log writes a formatted log message to the log file and console.
func log(level, format string, args ...interface{}) {
logMutex.Lock()
@@ -103,7 +108,9 @@ func log(level, format string, args ...interface{}) {
if logFile != nil {
logFile.WriteString(logLine)
logFile.Sync()
// Do NOT call Sync() on every write — that's a massive performance
// bottleneck (forces fsync to disk for every single log line).
// The kernel will flush buffered writes in its own time.
}
}
+34 -4
View File
@@ -338,6 +338,36 @@ func (hk *Hotkey) registerPortal() error {
return nil
}
// safeSendKeydown sends a keydown event to the hotkey channel, recovering from panic
// if the channel has been closed (e.g. during hotkey re-registration).
func (hk *Hotkey) safeSendKeydown() {
defer func() {
if r := recover(); r != nil {
logger.Debug("safeSendKeydown: recovered from panic: %v", r)
}
}()
select {
case hk.keydownIn <- Event{}:
default:
// Channel buffer is full or closed; skip.
}
}
// safeSendKeyup sends a keyup event to the hotkey channel, recovering from panic
// if the channel has been closed (e.g. during hotkey re-registration).
func (hk *Hotkey) safeSendKeyup() {
defer func() {
if r := recover(); r != nil {
logger.Debug("safeSendKeyup: recovered from panic: %v", r)
}
}()
select {
case hk.keyupIn <- Event{}:
default:
// Channel buffer is full or closed; skip.
}
}
func (hk *Hotkey) portalSignalLoop() {
defer close(hk.portalDone)
@@ -388,15 +418,15 @@ func (hk *Hotkey) portalSignalLoop() {
switch sig.Name {
case ifaceGlobalShortcuts + ".Activated":
go func() { hk.keydownIn <- Event{} }()
hk.safeSendKeydown()
case ifaceGlobalShortcuts + ".Deactivated":
go func() { hk.keyupIn <- Event{} }()
hk.safeSendKeyup()
default:
// Fallback for different bus routing names just in case
if strings.HasSuffix(sig.Name, ".Activated") {
go func() { hk.keydownIn <- Event{} }()
hk.safeSendKeydown()
} else if strings.HasSuffix(sig.Name, ".Deactivated") {
go func() { hk.keyupIn <- Event{} }()
hk.safeSendKeyup()
}
}
}
+76 -65
View File
@@ -11,21 +11,26 @@ import (
const linuxPressAddr = "127.0.0.1:9876"
var (
linuxPressMu sync.Mutex
linuxPressReleaseTimer *time.Timer
linuxPressDetectTimer *time.Timer
linuxPressRecording bool
linuxPressHoldMode bool
linuxPressDetectingHold bool
)
// linuxPressState holds the daemon's state, protected by a mutex.
// All field access must be done while holding the mutex.
type linuxPressState struct {
mu sync.Mutex
releaseTimer *time.Timer
detectTimer *time.Timer
recording bool
holdMode bool
detectingHold bool
}
var pressState linuxPressState
const (
// GNOME custom shortcuts commonly auto-repeat while the key is held.
// If we see a second ping quickly, treat the shortcut as push-to-talk.
// linuxPressHoldDetectWindow is the window in which a second ping indicates
// the shortcut is being held (push-to-talk mode).
linuxPressHoldDetectWindow = 1200 * time.Millisecond
// Once hold mode is confirmed, lack of fresh pings means the key was released.
// linuxPressReleaseGrace is how long after the last ping to wait before
// stopping recording in hold mode.
linuxPressReleaseGrace = 450 * time.Millisecond
)
@@ -63,41 +68,7 @@ func sendLinuxPressPing() error {
func (a *App) startLinuxPressDaemon() {
mux := http.NewServeMux()
mux.HandleFunc("/press", func(w http.ResponseWriter, r *http.Request) {
linuxPressMu.Lock()
defer linuxPressMu.Unlock()
if !linuxPressRecording {
linuxPressRecording = true
linuxPressHoldMode = false
linuxPressDetectingHold = true
go a.StartRecording()
if linuxPressDetectTimer != nil {
linuxPressDetectTimer.Stop()
}
linuxPressDetectTimer = time.AfterFunc(linuxPressHoldDetectWindow, func() {
linuxPressMu.Lock()
defer linuxPressMu.Unlock()
linuxPressDetectingHold = false
})
w.WriteHeader(http.StatusNoContent)
return
}
if linuxPressDetectingHold || linuxPressHoldMode {
linuxPressHoldMode = true
linuxPressDetectingHold = false
if linuxPressDetectTimer != nil {
linuxPressDetectTimer.Stop()
linuxPressDetectTimer = nil
}
resetLinuxPressReleaseTimer(a)
w.WriteHeader(http.StatusNoContent)
return
}
stopLinuxPressRecording(a)
a.handleLinuxPressPing()
w.WriteHeader(http.StatusNoContent)
})
@@ -106,33 +77,73 @@ func (a *App) startLinuxPressDaemon() {
}()
}
func resetLinuxPressReleaseTimer(a *App) {
if linuxPressReleaseTimer != nil {
linuxPressReleaseTimer.Stop()
func (a *App) handleLinuxPressPing() {
pressState.mu.Lock()
defer pressState.mu.Unlock()
if !pressState.recording {
// First ping: start recording with hold detection
pressState.recording = true
pressState.holdMode = false
pressState.detectingHold = true
go a.StartRecording()
if pressState.detectTimer != nil {
pressState.detectTimer.Stop()
}
pressState.detectTimer = time.AfterFunc(linuxPressHoldDetectWindow, func() {
pressState.mu.Lock()
defer pressState.mu.Unlock()
pressState.detectingHold = false
})
return
}
linuxPressReleaseTimer = time.AfterFunc(linuxPressReleaseGrace, func() {
linuxPressMu.Lock()
defer linuxPressMu.Unlock()
if linuxPressRecording && linuxPressHoldMode {
stopLinuxPressRecording(a)
// Already recording. If we're still in the hold-detect window or confirmed hold mode,
// this is a hold-mode keepalive ping (key auto-repeat).
if pressState.detectingHold || pressState.holdMode {
pressState.holdMode = true
pressState.detectingHold = false
if pressState.detectTimer != nil {
pressState.detectTimer.Stop()
pressState.detectTimer = nil
}
a.resetLinuxPressReleaseTimerLocked()
return
}
// Already recording but not in hold mode: this is a toggle-off ping (second press).
a.stopLinuxPressRecordingLocked()
}
func (a *App) resetLinuxPressReleaseTimerLocked() {
if pressState.releaseTimer != nil {
pressState.releaseTimer.Stop()
}
pressState.releaseTimer = time.AfterFunc(linuxPressReleaseGrace, func() {
pressState.mu.Lock()
defer pressState.mu.Unlock()
if pressState.recording && pressState.holdMode {
a.stopLinuxPressRecordingLocked()
}
})
}
func stopLinuxPressRecording(a *App) {
if linuxPressReleaseTimer != nil {
linuxPressReleaseTimer.Stop()
linuxPressReleaseTimer = nil
func (a *App) stopLinuxPressRecordingLocked() {
// Caller must hold pressState.mu
if pressState.releaseTimer != nil {
pressState.releaseTimer.Stop()
pressState.releaseTimer = nil
}
if linuxPressDetectTimer != nil {
linuxPressDetectTimer.Stop()
linuxPressDetectTimer = nil
if pressState.detectTimer != nil {
pressState.detectTimer.Stop()
pressState.detectTimer = nil
}
if linuxPressRecording {
linuxPressRecording = false
linuxPressHoldMode = false
linuxPressDetectingHold = false
if pressState.recording {
pressState.recording = false
pressState.holdMode = false
pressState.detectingHold = false
go a.StopRecording()
}
}
+229 -12
View File
@@ -33,7 +33,7 @@ fi
if [ -z "$APP_VERSION" ]; then
APP_VERSION="dev"
fi
echo "[0/3] App version: $APP_VERSION"
echo "[0/4] App version: $APP_VERSION"
echo ""
# Function to check if a command exists
@@ -54,7 +54,7 @@ if ! command_exists wails; then
fi
# 2. Check for Linux dependencies
echo "[1/3] Checking system dependencies..."
echo "[1/4] Checking system dependencies..."
MISSING_DEPS=0
# 2a. GNOME tray icon support check (before build — avoids launching app if this will fail)
@@ -79,7 +79,7 @@ if [ "$XDG_CURRENT_DESKTOP" = "GNOME" ] || [ "$XDG_CURRENT_DESKTOP" = "ubuntu:GN
if [ "$EXT_INSTALLED" = false ]; then
echo ""
echo "==============================================================="
echo " ERROR: GNOME AppIndicator extension is not installed"
echo " WARNING: GNOME AppIndicator extension is not installed"
echo "==============================================================="
echo ""
echo " GNOME does not show system tray icons without this extension."
@@ -104,7 +104,10 @@ if [ "$XDG_CURRENT_DESKTOP" = "GNOME" ] || [ "$XDG_CURRENT_DESKTOP" = "ubuntu:GN
echo " Settings > Extensions > AppIndicator and KStatusNotifierItem Support"
echo ""
echo "==============================================================="
exit 1
# Do NOT exit — just warn and continue. The user can still use the app
# with a custom shortcut even without the tray icon.
echo " Continuing (the app will work, but the tray icon may be hidden)."
echo ""
else
echo "[INFO] GNOME AppIndicator extension is installed."
echo " Make sure it's enabled in Settings > Extensions."
@@ -120,6 +123,39 @@ for dep in "${DEPS[@]}"; do
fi
done
# ydotool check for text injection
if ! command_exists ydotool; then
echo ""
echo "==============================================================="
echo " WARNING: ydotool is not installed"
echo "==============================================================="
echo ""
echo " ydotool is required for automatic text injection (paste) on"
echo " Wayland. Without it, transcribed text will only be copied to"
echo " your clipboard."
echo ""
echo " Install ydotool and configure it:"
echo ""
if command_exists dnf; then
echo " sudo dnf install ydotool"
elif command_exists apt-get; then
echo " sudo apt install ydotool"
elif command_exists pacman; then
echo " sudo pacman -S ydotool"
else
echo " Install ydotool from your distribution's package manager."
fi
echo ""
echo " Then set up udev rules for /dev/uinput access:"
echo " echo 'KERNEL==\"uinput\", SUBSYSTEM==\"misc\", TAG+=\"uaccess\", OPTIONS+=\"static_node=uinput\"' |"
echo " sudo tee /etc/udev/rules.d/80-uinput.rules"
echo " sudo udevadm control --reload-rules && sudo udevadm trigger"
echo ""
echo " Then enable the ydotool user service (see the --install-systemd flag below)."
echo "==============================================================="
echo ""
fi
# We can't easily check C headers, but we can try to find them with pkg-config
# (Fedora often ships webkit2gtk-4.1.pc; Debian/Ubuntu often use webkit2gtk-4.0.pc)
webkit2_ok() {
@@ -160,15 +196,15 @@ if [ $MISSING_DEPS -eq 1 ]; then
DEBIAN_DEPS="build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.0-dev libasound2-dev libayatana-appindicator3-dev"
# Runtime nicety (optional): playerctl pauses media while recording
DEBIAN_RUNTIME_OPT="playerctl"
DEBIAN_RUNTIME_OPT="playerctl ydotool"
# Fedora 40+: WebKit2GTK 4.0 packages are gone; use 4.1 + Wails -tags webkit2_41 (see wails build below).
# pkgconf-pkg-config provides `pkg-config` on Fedora.
FEDORA_DEPS="gcc gcc-c++ make pkgconf-pkg-config gtk3-devel webkit2gtk4.1-devel alsa-lib-devel libayatana-appindicator-gtk3-devel"
# Same as FEDORA_DEPS but classic libappindicator (some spins/repos lack Ayatana -devel)
FEDORA_DEPS_ALT="gcc gcc-c++ make pkgconf-pkg-config gtk3-devel webkit2gtk4.1-devel alsa-lib-devel libappindicator-gtk3-devel"
FEDORA_RUNTIME_OPT="playerctl xdg-desktop-portal"
FEDORA_RUNTIME_OPT="playerctl xdg-desktop-portal ydotool"
ARCH_DEPS="base-devel pkgconf gtk3 webkit2gtk alsa-lib libayatana-appindicator"
ARCH_RUNTIME_OPT="playerctl"
ARCH_RUNTIME_OPT="playerctl ydotool"
echo "The full list of dependencies needed:"
echo " [Ubuntu/Debian]: sudo apt update && sudo apt install -y $DEBIAN_DEPS"
@@ -207,7 +243,7 @@ if [ $MISSING_DEPS -eq 1 ]; then
fi
# 3. Build the application
echo "[2/3] Building with Wails..."
echo "[2/4] 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
@@ -253,14 +289,139 @@ fi
# doesn't block the terminal while we ask about installation.
pkill -f "$EXECUTABLE" 2>/dev/null || true
echo "[3/3] Build successful!"
echo "[3/4] 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
echo "[4/4] Installation options"
echo ""
echo " (i) Install systemwide: sudo ./$0 --install"
echo " (u) Install user-local: ./$0 --install-user"
echo " (s) Install systemd service (ydotool): ./$0 --install-systemd"
echo " (h) Show this help"
echo ""
echo " Without flags, the build-only mode finishes here."
echo " Binary is ready at: $EXECUTABLE"
echo ""
# Parse flags
INSTALL_MODE="none" # none, system, user
INSTALL_SYSTEMD=false
for arg in "$@"; do
case "$arg" in
--install)
INSTALL_MODE="system"
;;
--install-user)
INSTALL_MODE="user"
;;
--install-systemd)
INSTALL_SYSTEMD=true
;;
--help|-h)
echo "Usage: $0 [--install|--install-user|--install-systemd|--help]"
echo ""
echo " (no flags) Build only — outputs binary to $EXECUTABLE"
echo " --install Build + install system-wide (/usr/local/bin)"
echo " --install-user Build + install per-user (~/.local/bin)"
echo " --install-systemd Generate and enable systemd user units for ydotool"
echo " --help Show this message"
echo ""
exit 0
;;
esac
done
# If no install flags were passed, do interactive prompt (backward-compatible)
if [ "$INSTALL_MODE" = "none" ] && [ "$INSTALL_SYSTEMD" = false ]; then
read -p "Would you like to install it system-wide to /usr/local/bin? (y/n): " INSTALL
if [[ "$INSTALL" == "y" || "$INSTALL" == "Y" ]]; then
echo "Installing..."
INSTALL_MODE="system"
else
read -p "Install per-user to ~/.local/bin? (y/n): " INSTALL_USER
if [[ "$INSTALL_USER" == "y" || "$INSTALL_USER" == "Y" ]]; then
INSTALL_MODE="user"
fi
fi
read -p "Would you like to set up the ydotool systemd user service? (y/n): " SETUP_SYSTEMD
if [[ "$SETUP_SYSTEMD" == "y" || "$SETUP_SYSTEMD" == "Y" ]]; then
INSTALL_SYSTEMD=true
fi
fi
# --- Systemd ydotool service setup ---
if [ "$INSTALL_SYSTEMD" = true ]; then
echo ""
echo "========================================"
echo " Setting up ydotool systemd user service"
echo "========================================"
echo ""
SYSTEMD_USER_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/systemd/user"
mkdir -p "$SYSTEMD_USER_DIR"
# Create ydotool.service for the user session
cat > "$SYSTEMD_USER_DIR/ydotool.service" << 'YDSVCEOF'
[Unit]
Description=ydotool daemon — simulate keyboard input on Wayland
Documentation=man:ydotool(1)
[Service]
Type=simple
ExecStart=/usr/bin/ydotoold
Restart=on-failure
RestartSec=2
[Install]
WantedBy=default.target
YDSVCEOF
echo "[INFO] Created $SYSTEMD_USER_DIR/ydotool.service"
# Check/create udev rule for uinput
UDEV_RULE_FILE="/etc/udev/rules.d/80-uinput.rules"
if [ ! -f "$UDEV_RULE_FILE" ]; then
echo ""
echo " /dev/uinput permission rule not found."
echo " The following rule is needed for ydotool to inject keystrokes:"
echo ""
echo ' KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"'
echo ""
read -p " Create $UDEV_RULE_FILE now? (requires sudo) (y/N): " CREATE_UDEV
if [[ "$CREATE_UDEV" == "y" || "$CREATE_UDEV" == "Y" ]]; then
echo 'KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"' | \
sudo tee "$UDEV_RULE_FILE" > /dev/null
sudo udevadm control --reload-rules && sudo udevadm trigger
echo "[INFO] udev rule created and reloaded."
echo " You must log out and back in (or reboot) for the permissions to take effect."
else
echo " Skipping udev rule creation. ydotoold may not be able to inject keystrokes."
echo " You can create it manually later."
fi
else
echo "[INFO] udev rule already exists at $UDEV_RULE_FILE"
fi
# Reload systemd user daemon and enable the service
echo ""
echo " Reloading systemd user daemon..."
systemctl --user daemon-reload
echo " Enabling ydotool user service..."
systemctl --user enable ydotool.service
echo " Starting ydotool user service..."
systemctl --user start ydotool.service || echo " [WARN] Could not start (may need logout/login for udev perms)"
echo ""
echo " ydotool systemd service is now set up."
echo " You can check its status with: systemctl --user status ydotool"
echo ""
fi
# --- Binary installation ---
if [ "$INSTALL_MODE" = "system" ]; then
echo "Installing system-wide..."
# Needs sudo
sudo cp "$EXECUTABLE" "/usr/local/bin/$APP_NAME"
@@ -296,6 +457,62 @@ EOF
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 ""
elif [ "$INSTALL_MODE" = "user" ]; then
echo "Installing per-user..."
LOCAL_BIN_DIR="$HOME/.local/bin"
mkdir -p "$LOCAL_BIN_DIR"
cp "$EXECUTABLE" "$LOCAL_BIN_DIR/$APP_NAME"
chmod +x "$LOCAL_BIN_DIR/$APP_NAME"
# Add to PATH warning
case ":${PATH}:" in
*:"$LOCAL_BIN_DIR":*) ;;
*)
echo "[WARNING] $LOCAL_BIN_DIR is not in your PATH."
echo " Add it to your shell profile:"
echo ' export PATH="$HOME/.local/bin:$PATH"'
echo ""
;;
esac
# Create user-local desktop file
DESKTOP_DIR="$HOME/.local/share/applications"
mkdir -p "$DESKTOP_DIR"
ICONS_DIR="$HOME/.local/share/pixmaps"
mkdir -p "$ICONS_DIR"
if [ -f "build/appicon.png" ]; then
cp "build/appicon.png" "$ICONS_DIR/$APP_NAME.png"
fi
cat > "$DESKTOP_DIR/$APP_NAME.desktop" << EOF
[Desktop Entry]
Type=Application
Name=WIS Free V3
Comment=Voice Dictation App
Exec=$LOCAL_BIN_DIR/$APP_NAME
TryExec=$LOCAL_BIN_DIR/$APP_NAME
Icon=$APP_NAME
StartupWMClass=$APP_NAME
Terminal=false
Categories=Utility;Audio;
EOF
chmod 644 "$DESKTOP_DIR/$APP_NAME.desktop"
echo ""
echo "User-local installation complete!"
echo " Binary: $LOCAL_BIN_DIR/$APP_NAME"
echo " Desktop: $DESKTOP_DIR/$APP_NAME.desktop"
echo ""
elif [ "$INSTALL_MODE" = "none" ] && [ "$INSTALL_SYSTEMD" = false ]; then
echo "Skipping installation. You can run the app directly via: ./$EXECUTABLE"
fi
echo "========================================"
echo " Build finished successfully!"
echo "========================================"
+10
View File
@@ -80,9 +80,15 @@ if ! pkg-config --exists gtk+-3.0 || ! webkit2_ok || ! pkg-config --exists alsa
echo ""
echo "Ubuntu/Debian:"
echo " sudo apt update && sudo apt install -y build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.0-dev libasound2-dev libayatana-appindicator3-dev dpkg-dev rpm"
echo " sudo apt install -y ydotool (recommended for automatic text injection)"
echo ""
echo "Fedora:"
echo " sudo dnf install -y gcc gcc-c++ make pkgconf-pkg-config gtk3-devel webkit2gtk4.1-devel alsa-lib-devel libayatana-appindicator-gtk3-devel rpm-build"
echo " sudo dnf install -y ydotool (recommended for automatic text injection)"
echo ""
echo "Arch Linux:"
echo " sudo pacman -S base-devel pkgconf gtk3 webkit2gtk alsa-lib libayatana-appindicator"
echo " sudo pacman -S ydotool (recommended for automatic text injection)"
echo ""
exit 1
fi
@@ -185,8 +191,10 @@ Architecture: $ARCH_DEB
Maintainer: $MAINTAINER
Installed-Size: $INSTALLED_SIZE
Depends: libgtk-3-0, libwebkit2gtk-4.0-37 | libwebkit2gtk-4.1-0, libasound2, libayatana-appindicator3-1
Recommends: ydotool
Description: $COMMENT
WIS Free V3 is a desktop voice dictation app built with Go and Wails.
On Wayland, install ydotool for automatic text injection.
EOF
dpkg-deb --build "$DEB_ROOT" "$DIST_DIR/${APP_NAME}_${PKG_VERSION}-${PKG_RELEASE}_${ARCH_DEB}.deb"
@@ -206,9 +214,11 @@ Summary: $COMMENT
License: $LICENSE
Requires: gtk3
Requires: alsa-lib
Requires: libayatana-appindicator-gtk3
%description
WIS Free V3 is a desktop voice dictation app built with Go and Wails.
On Wayland, install ydotool for automatic text injection.
%install
rm -rf %{buildroot}