package hyprman import ( "bufio" "encoding/json" "fmt" "io" "log" "net" "os" "os/exec" "path/filepath" "strings" "time" yaml "github.com/goccy/go-yaml" ) func DefaultConfigPath() string { configPath, exists := os.LookupEnv("XDG_CONFIG_HOME") if !exists { home, exists := os.LookupEnv("HOME") if !exists { log.Fatalln("failed to set default icons.json path is $HOME or $XDG_CONFIG_HOME set?") } configPath = filepath.Join(home, ".config") } return filepath.Join(configPath, "hyprman", "config.yml") } func hyprSocketBase() string { runtimeDir, exists := os.LookupEnv("XDG_RUNTIME_DIR") if !exists { log.Fatalln("XDG_RUNTIME_DIR not set") } instanceSig, exists := os.LookupEnv("HYPRLAND_INSTANCE_SIGNATURE") if !exists { log.Fatalln("HYPRLAND_INSTANCE_SIGNATURE not set") } return filepath.Join(runtimeDir, "hypr", instanceSig) } func hyprSocket() string { return filepath.Join(hyprSocketBase(), ".socket.sock") } func hyprSocket2() string { return filepath.Join(hyprSocketBase(), ".socket2.sock") } type Workspace struct { Name string Id int } type Client struct { Class string Workspace Workspace } type Icons map[string]string type Hyprman struct { Config Config } type Config struct { Classes Icons NoClientIcon string `yaml:"no-client"` DefaultIcon string `yaml:"default-icon"` } func (hm *Hyprman) LoadConfig(path string) { var config Config data, err := os.ReadFile(path) if err != nil { log.Fatal(fmt.Errorf("failed to read config at %s:\n%w", path, err)) } if err := yaml.Unmarshal([]byte(data), &config); err != nil { log.Fatal(fmt.Errorf("failed to read config at %s:\n%w", path, err)) } hm.Config = config } func hyprctl(cmd string) []byte { socketPath := hyprSocket() conn, err := net.Dial("unix", socketPath) if err != nil { log.Fatalln("Error connecting to hyprland IPC:", err) } defer conn.Close() _, err = conn.Write([]byte(cmd)) if err != nil { log.Fatal(err) } buf := make([]byte, 1024*8) // Bigger? n, err := conn.Read(buf) if err != nil { log.Fatal(err) } return buf[0:n] } type ActiveWorkspace struct { Name string Id int } type Monitor struct { ActiveWorkspace ActiveWorkspace Id int } // generic or interface? func getMonitors() []Monitor { data := hyprctl("[-j]/monitors") var monitors []Monitor err := json.Unmarshal(data, &monitors) if err != nil { log.Fatalln(err) } return monitors } func getClients() []Client { data := hyprctl("[-j]/clients") var clients []Client err := json.Unmarshal(data, &clients) if err != nil { log.Fatalln(err) } return clients } type EwwWorkspace struct { Icon string `json:"icon"` Class string `json:"class"` Id int `json:"id"` } func (hm *Hyprman) pickIcon(class string) string { for k, v := range hm.Config.Classes { if strings.Contains(k, class) { return v } } return hm.Config.DefaultIcon } func openWorkspaces(monitors []Monitor) []int { open := make([]int, len(monitors)) for i, m := range monitors { open[i] = m.ActiveWorkspace.Id } return open } func (hm *Hyprman) generateEwwClasses() { monitors := getMonitors() clients := getClients() var ewwClasses [][]EwwWorkspace monitor := make([]EwwWorkspace, 9) for i := range 9 { monitor[i] = EwwWorkspace{ hm.Config.NoClientIcon, fmt.Sprintf("ws-button-%d", i+1), i + 1, } } for _, client := range clients { id := client.Workspace.Id - 1 currentIcon := monitor[id].Icon if currentIcon == hm.Config.NoClientIcon { monitor[id].Icon = hm.pickIcon(client.Class) } else { monitor[id].Icon = fmt.Sprintf("%s%s", currentIcon, hm.pickIcon(client.Class)) } } for _, id := range openWorkspaces(monitors) { // activeId := m.ActiveWorkspace.Id monitor[id-1].Class = fmt.Sprintf("%s %s", monitor[id-1].Class, "ws-button-open") } for i, m := range monitors { activeId := m.ActiveWorkspace.Id - 1 ewwClasses = append(ewwClasses, make([]EwwWorkspace, 9)) copy(ewwClasses[i], monitor) ewwClasses[i][activeId].Class = fmt.Sprintf("%s %s-%d", monitor[activeId].Class, "ws-button-active", activeId+1) } bytes, err := json.Marshal(ewwClasses) if err != nil { panic(err) } fmt.Println(string(bytes)) } func ewwBar1(cmd string) { time.Sleep(200 * time.Second) if err := exec.Command("eww", cmd, "bar1").Run(); err != nil { log.Fatal(err) } } func notify(message string) { cmd := exec.Command("notify-send", "hyprman", message) cmd.Run() } func (hm *Hyprman) handleHyprEvent(line string) { s := strings.Split(line, ">>") event, _ := s[0], s[1] switch event { case "monitorremoved": notify("Monitor removed closing bar1") go ewwBar1("close") hm.generateEwwClasses() case "monitoradded": notify("Monitor added opening bar1") go ewwBar1("open") hm.generateEwwClasses() case "workspace", "focusedmon", "activewindow", "createworkspace", "destroyworkspace", "moveworkspace", "renameworkspace", "openwindow", "closewindow", "movewindow", "movewindowv2", "changefloatingmode", "windowtitle", "togglegroup", "moveintogroup", "moveoutofgroup", "configreloaded": hm.generateEwwClasses() } } func (hm *Hyprman) LaunchEww() { for i := range getMonitors() { if err := exec.Command("eww", "open", fmt.Sprintf("bar%d", i)).Run(); err != nil { notify(fmt.Sprintf("Error lanching eww:\n%v", err)) } } } func (hm *Hyprman) Watch() { socketPath := hyprSocket2() conn, err := net.Dial("unix", socketPath) if err != nil { log.Fatalln("Error connecting to hyprland IPC:", err) } defer conn.Close() reader := bufio.NewReader(conn) hm.generateEwwClasses() for { line, err := reader.ReadString('\n') if err != nil { if err == io.EOF { // log.Printf("reached EOF for %s\n", socketPath) break } // log.Println("error reading line") continue } hm.handleHyprEvent(line) } }