setup project with daemon, socket, cmd, air, notifying, state

This commit is contained in:
LeonardoTrapani
2025-08-07 00:01:49 +02:00
parent d3fc5ee3a4
commit f7936610c9
11 changed files with 457 additions and 25 deletions
+57
View File
@@ -0,0 +1,57 @@
package bus
import (
"bufio"
"net"
"os"
"path/filepath"
)
const SockName = "control.sock"
const ProtoVer = "0.1"
// ~/.cache/hyprvoice/control.sock
func SockPath() (string, error) {
dir, err := os.UserCacheDir()
if err != nil {
return "", err
}
hd := filepath.Join(dir, "hyprvoice")
return filepath.Join(hd, SockName), nil
}
func Listen() (net.Listener, error) {
sp, err := SockPath()
if err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(sp), 0o700); err != nil {
return nil, err
}
_ = os.Remove(sp) // stale socket from last run
return net.Listen("unix", sp)
}
func Dial() (net.Conn, error) {
sp, err := SockPath()
if err != nil {
return nil, err
}
return net.Dial("unix", sp)
}
func SendCommand(cmd byte) (string, error) {
c, err := Dial()
if err != nil {
return "", err
}
defer c.Close()
_, err = c.Write([]byte{cmd, '\n'})
if err != nil {
return "", err
}
resp, err := bufio.NewReader(c).ReadString('\n')
return resp, err
}
+9
View File
@@ -0,0 +1,9 @@
package bus
import "testing"
func TestSockPath(t *testing.T) {
if _, err := SockPath(); err != nil {
t.Fatalf("SockPath: %v", err)
}
}
+80
View File
@@ -0,0 +1,80 @@
package hotkeydaemon
import (
"bufio"
"fmt"
"net"
"sync"
"time"
"github.com/leonardotrapani/hyprvoice/internal/bus"
"github.com/leonardotrapani/hyprvoice/internal/notify"
)
type Daemon struct {
mu sync.Mutex
recording bool
notifier notify.Notifier
}
func New(n notify.Notifier) *Daemon {
if n == nil {
n = notify.Desktop{}
}
return &Daemon{
notifier: n,
}
}
func (d *Daemon) Rec() bool {
d.mu.Lock()
defer d.mu.Unlock()
return d.recording
}
func (d *Daemon) Run() error {
ln, err := bus.Listen()
if err != nil {
return err
}
for {
c, err := ln.Accept()
if err != nil {
continue
}
go d.handle(c)
}
}
func (d *Daemon) handle(c net.Conn) {
defer c.Close()
line, _ := bufio.NewReader(c).ReadString('\n')
if len(line) == 0 {
fmt.Fprint(c, "ERR empty\n")
return
}
cmd := line[0]
d.mu.Lock()
defer d.mu.Unlock()
switch cmd {
case 't': // toggle
d.recording = !d.recording
d.notifier.RecordingChanged(d.recording)
fmt.Fprintf(c, "STATUS recording=%t\n", d.recording)
case 's': // status
fmt.Fprintf(c, "STATUS recording=%t\n", d.recording)
case 'v': // protocol version
fmt.Fprintf(c, "STATUS proto=%s\n", bus.ProtoVer)
case 'q': // quit daemon
fmt.Fprint(c, "OK quitting\n")
go func() {
time.Sleep(100 * time.Millisecond) // give time for client to read
panic("daemon exit requested")
}()
default:
fmt.Fprintf(c, "ERR unknown=%q\n", cmd)
}
}
+29
View File
@@ -0,0 +1,29 @@
package hotkeydaemon
import (
"testing"
"time"
"github.com/leonardotrapani/hyprvoice/internal/bus"
"github.com/leonardotrapani/hyprvoice/internal/notify"
)
func TestToggle(t *testing.T) {
d := New(notify.Nop{})
go d.Run()
time.Sleep(50 * time.Millisecond) // give listener time to start
defer bus.SendCommand('q')
if out, _ := bus.SendCommand('t'); out != "STATUS recording=true\n" {
t.Fatalf("unexpected: %s", out)
}
if !d.Rec() {
t.Fatalf("state should be true after first toggle")
}
if out, _ := bus.SendCommand('t'); out != "STATUS recording=false\n" {
t.Fatalf("unexpected: %s", out)
}
if d.Rec() {
t.Fatalf("state should be false after second toggle")
}
}
+33
View File
@@ -0,0 +1,33 @@
package notify
import (
"fmt"
"os/exec"
)
type Notifier interface {
RecordingChanged(on bool)
Error(msg string)
}
type Desktop struct{}
func (Desktop) RecordingChanged(on bool) {
state := "Stopped"
if on {
state = "Started"
}
exec.Command("notify-send", "-a", "Hyprvoice",
fmt.Sprintf("Hyprvoice: %s Recording", state)).Run()
}
func (Desktop) Error(msg string) {
exec.Command("notify-send", "-a", "Hyprvoice", "-u", "critical", msg).Run()
}
// Nop is a Notifier that does absolutely nothing.
// Useful in unit tests or headless builds.
type Nop struct{}
func (Nop) RecordingChanged(on bool) {}
func (Nop) Error(msg string) {}