Add X11 daemon with tray status
This commit is contained in:
parent
3506770d09
commit
a7f50fed75
19 changed files with 1202 additions and 4 deletions
233
cmd/leld/main.go
Normal file
233
cmd/leld/main.go
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"lel/internal/config"
|
||||
"lel/internal/daemon"
|
||||
"lel/internal/ui"
|
||||
"lel/internal/x11"
|
||||
|
||||
"github.com/BurntSushi/xgb/xproto"
|
||||
"github.com/getlantern/systray"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var configPath string
|
||||
var dryRun bool
|
||||
var noTray bool
|
||||
flag.StringVar(&configPath, "config", "", "path to config.toml")
|
||||
flag.BoolVar(&dryRun, "dry-run", false, "register hotkey and log events without recording")
|
||||
flag.BoolVar(&noTray, "no-tray", false, "disable system tray icon")
|
||||
flag.Parse()
|
||||
|
||||
logger := log.New(os.Stderr, "leld: ", log.LstdFlags)
|
||||
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
logger.Fatalf("config error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Streaming {
|
||||
logger.Printf("streaming mode is not supported; falling back to non-streaming")
|
||||
}
|
||||
|
||||
runtimeDir := ensureRuntimeDir(logger)
|
||||
lockFile := filepath.Join(runtimeDir, "lel.lock")
|
||||
lock, err := lockSingleInstance(lockFile)
|
||||
if err != nil {
|
||||
logger.Fatalf("another instance is running (lock %s): %v", lockFile, err)
|
||||
}
|
||||
defer lock.Close()
|
||||
|
||||
x, err := x11.New()
|
||||
if err != nil {
|
||||
logger.Fatalf("x11 connection failed: %v", err)
|
||||
}
|
||||
defer x.Close()
|
||||
|
||||
mods, keycode, err := x.ParseHotkey(cfg.Hotkey)
|
||||
if err != nil {
|
||||
logger.Fatalf("hotkey parse failed: %v", err)
|
||||
}
|
||||
if err := x.GrabHotkey(mods, keycode); err != nil {
|
||||
logger.Fatalf("grab hotkey failed: %v", err)
|
||||
}
|
||||
defer x.UngrabHotkey(mods, keycode)
|
||||
|
||||
d := daemon.New(cfg, x, logger)
|
||||
|
||||
sockPath := filepath.Join(runtimeDir, "ctl.sock")
|
||||
if err := os.RemoveAll(sockPath); err != nil {
|
||||
logger.Fatalf("remove socket failed: %v", err)
|
||||
}
|
||||
ln, err := net.Listen("unix", sockPath)
|
||||
if err != nil {
|
||||
logger.Fatalf("listen socket failed: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
reloadPath := configPath
|
||||
if reloadPath == "" {
|
||||
reloadPath = config.DefaultPath()
|
||||
}
|
||||
go serveControl(logger, ln, d, &cfg, x, &mods, &keycode, reloadPath)
|
||||
|
||||
logger.Printf("ready (hotkey: %s)", cfg.Hotkey)
|
||||
|
||||
if noTray {
|
||||
runX11Loop(logger, x, d, mods, keycode, dryRun)
|
||||
return
|
||||
}
|
||||
|
||||
onReady := func() {
|
||||
systray.SetTitle("lel")
|
||||
systray.SetTooltip("lel: idle")
|
||||
systray.SetIcon(ui.IconIdle())
|
||||
status := systray.AddMenuItem("Idle", "")
|
||||
status.Disable()
|
||||
systray.AddSeparator()
|
||||
quit := systray.AddMenuItem("Quit", "Quit lel")
|
||||
|
||||
go func() {
|
||||
for st := range d.StateChanges() {
|
||||
switch st {
|
||||
case daemon.StateRecording:
|
||||
systray.SetIcon(ui.IconRecording())
|
||||
systray.SetTooltip("lel: recording")
|
||||
status.SetTitle("Recording")
|
||||
case daemon.StateTranscribing:
|
||||
systray.SetIcon(ui.IconTranscribing())
|
||||
systray.SetTooltip("lel: transcribing")
|
||||
status.SetTitle("Transcribing")
|
||||
default:
|
||||
systray.SetIcon(ui.IconIdle())
|
||||
systray.SetTooltip("lel: idle")
|
||||
status.SetTitle("Idle")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for range quit.ClickedCh {
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
|
||||
go runX11Loop(logger, x, d, mods, keycode, dryRun)
|
||||
}
|
||||
|
||||
systray.Run(onReady, func() {})
|
||||
}
|
||||
|
||||
func matchMods(state uint16, want uint16) bool {
|
||||
masked := state & ^uint16(xproto.ModMaskLock|xproto.ModMask2)
|
||||
return masked == want
|
||||
}
|
||||
|
||||
func runX11Loop(logger *log.Logger, x *x11.Conn, d *daemon.Daemon, mods uint16, keycode xproto.Keycode, dryRun bool) {
|
||||
for {
|
||||
ev, err := x.X.WaitForEvent()
|
||||
if err != nil {
|
||||
logger.Printf("x11 event error: %v", err)
|
||||
continue
|
||||
}
|
||||
switch e := ev.(type) {
|
||||
case xproto.KeyPressEvent:
|
||||
if e.Detail == keycode && matchMods(e.State, mods) {
|
||||
if dryRun {
|
||||
logger.Printf("hotkey pressed (dry-run)")
|
||||
continue
|
||||
}
|
||||
d.Toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ensureRuntimeDir(logger *log.Logger) string {
|
||||
dir := os.Getenv("XDG_RUNTIME_DIR")
|
||||
if dir == "" {
|
||||
dir = "/tmp"
|
||||
}
|
||||
dir = filepath.Join(dir, "lel")
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
logger.Fatalf("runtime dir error: %v", err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func lockSingleInstance(path string) (*os.File, error) {
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func serveControl(logger *log.Logger, ln net.Listener, d *daemon.Daemon, cfg *config.Config, x *x11.Conn, mods *uint16, keycode *xproto.Keycode, configPath string) {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
logger.Printf("control accept error: %v", err)
|
||||
continue
|
||||
}
|
||||
go handleConn(logger, conn, d, cfg, x, mods, keycode, configPath)
|
||||
}
|
||||
}
|
||||
|
||||
func handleConn(logger *log.Logger, conn net.Conn, d *daemon.Daemon, cfg *config.Config, x *x11.Conn, mods *uint16, keycode *xproto.Keycode, configPath string) {
|
||||
defer conn.Close()
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
line, _ := reader.ReadString('\n')
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
switch line {
|
||||
case "status":
|
||||
_, _ = fmt.Fprintf(conn, "state=%s\n", d.State())
|
||||
case "stop":
|
||||
_, _ = fmt.Fprintf(conn, "stopping\n")
|
||||
logger.Printf("stop requested")
|
||||
os.Exit(0)
|
||||
case "reload":
|
||||
newCfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(conn, "reload error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
newMods, newKeycode, err := x.ParseHotkey(newCfg.Hotkey)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(conn, "reload error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
x.UngrabHotkey(*mods, *keycode)
|
||||
if err := x.GrabHotkey(newMods, newKeycode); err != nil {
|
||||
_, _ = fmt.Fprintf(conn, "reload error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
*mods = newMods
|
||||
*keycode = newKeycode
|
||||
*cfg = newCfg
|
||||
d.UpdateConfig(newCfg)
|
||||
|
||||
_, _ = fmt.Fprintf(conn, "reloaded\n")
|
||||
default:
|
||||
_, _ = fmt.Fprintf(conn, "unknown command\n")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue