255 lines
6.2 KiB
Go
255 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"lel/internal/clip"
|
|
"lel/internal/config"
|
|
"lel/internal/daemon"
|
|
"lel/internal/inject"
|
|
"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)
|
|
|
|
backend, err := inject.NewBackend(cfg.InjectionBackend, inject.Deps{
|
|
Clipboard: inject.ClipboardWriterFunc(clip.WriteClipboard),
|
|
Paster: inject.NewXdotoolPaster(nil),
|
|
Typer: inject.NewXdotoolTyper(nil),
|
|
})
|
|
if err != nil {
|
|
logger.Fatalf("backend error: %v", err)
|
|
}
|
|
|
|
d := daemon.New(cfg, x, logger, backend)
|
|
|
|
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
|
|
}
|
|
|
|
backend, err := inject.NewBackend(newCfg.InjectionBackend, inject.Deps{
|
|
Clipboard: inject.ClipboardWriterFunc(clip.WriteClipboard),
|
|
Paster: inject.NewXdotoolPaster(nil),
|
|
Typer: inject.NewXdotoolTyper(nil),
|
|
})
|
|
if err != nil {
|
|
_, _ = fmt.Fprintf(conn, "reload error: %v\n", err)
|
|
return
|
|
}
|
|
|
|
*mods = newMods
|
|
*keycode = newKeycode
|
|
*cfg = newCfg
|
|
d.UpdateConfig(newCfg)
|
|
d.UpdateBackend(backend)
|
|
|
|
_, _ = fmt.Fprintf(conn, "reloaded\n")
|
|
default:
|
|
_, _ = fmt.Fprintf(conn, "unknown command\n")
|
|
}
|
|
}
|