package bspc // import "hhvn.uk/hbspbar/bspc" import ( "io" "fmt" "time" "sync" "bufio" "errors" "strings" "os/exec" "encoding/json" "hhvn.uk/hbspbar/common" ) var ErrMonNotFound = errors.New("monitor not found") type State struct { Monitors []*Monitor FocusedMonitorID int } func (s *State) GetMon(id int) (*Monitor, error) { for _, m := range s.Monitors { if m.ID == id { return m, nil } } return nil, ErrMonNotFound } type Monitor struct { Focused bool ID int Name string Rectangle *Rect Desktops []*Desktop FocusedDesktopID int } func (m *Monitor) TopPadding(px uint) { Cmd("config", "-m", fmt.Sprintf("%d", m.ID), "top_padding", fmt.Sprintf("%d", px)) } type Rect struct { X uint Y uint Width uint Height uint } type Node struct { ID int FirstChild *Node SecondChild *Node } type Desktop struct { Focused bool Name string Layout string ID int Root *Node } var reloadEvents = []string{ "monitor_rename", "monitor_swap", "monitor_focus", "monitor_remove", "monitor_geometry", // only event we get for monitors on restart "desktop_add", "desktop_rename", "desktop_remove", "desktop_swap", "desktop_transfer", "desktop_focus", "desktop_activate", "desktop_layout", "node_add", "node_remove", "node_swap", "node_transfer" } var cmdlist []*exec.Cmd var cmdlock sync.Mutex func cmdRegister(c *exec.Cmd) int { cmdlock.Lock() defer cmdlock.Unlock() for i, v := range cmdlist { if v == nil { cmdlist[i] = c return i } } cmdlist = append(cmdlist, c) i := len(cmdlist) - 1 return i } func cmdDelete(i int) { cmdlock.Lock() defer cmdlock.Unlock() cmdlist[i] = nil } func getState() (*State, error) { cmd := exec.Command("bspc", "wm", "-d") out, err := cmd.StdoutPipe() if err != nil { return nil, common.Perror("cmd.StdoutPipe", err) } if err = cmd.Start(); err != nil { return nil, common.Perror("cmd.Start", err) } data, err := io.ReadAll(out) if err != nil { return nil, common.Perror("io.ReadAll", err) } if err = cmd.Wait(); err != nil { return nil, common.Perror("cmd.Wait", err) } var state State err = json.Unmarshal(data, &state) if err != nil { return nil, common.Perror("json.Unmarshal", err) } for _, m := range state.Monitors { m.Focused = m.ID == state.FocusedMonitorID for _, d := range m.Desktops { d.Focused = d.ID == m.FocusedDesktopID } } return &state, nil } type event struct { Name string Tokens []string } var InitErr error var NewState = make(chan *State, 1) // Buffered to prevent blocking on first send var StErr = make(chan error) var Event = make(chan *event) var EvErr = make(chan error) func init() { cmd := exec.Command("bspc", "subscribe", "all") cmdRegister(cmd) pipe, err := cmd.StdoutPipe() if err != nil { InitErr = common.Perror("cmd.StdoutPipe", err) return } if err = cmd.Start(); err != nil { InitErr = common.Perror("cmd.Start", err) return } istate, err := getState() if err != nil { InitErr = common.Perror("getState", err) return } NewState <- istate scanner := bufio.NewScanner(pipe) go func() { for { event, err := get(scanner) if event != nil { for i := 0; i < len(reloadEvents); i++ { if event.Name == reloadEvents[i] { state, serr := getState() if serr != nil { StErr <- serr return } NewState <- state } } Event <- event } if err != nil { EvErr <- err return } }}() } func Cmd(args ...string) { cmd := exec.Command("bspc", args...) cmdi := cmdRegister(cmd) pipe, _ := cmd.StderrPipe() scan := bufio.NewScanner(pipe) startt := time.Now() cmd.Start() for scan.Scan() { common.Error("[Started %v] `bspc %v`: %v\n", startt.Format("15:04:05.00"), args, scan.Text()) } if err := scan.Err(); err != nil { common.Perror("scan.Err", err) } cmd.Wait() cmdDelete(cmdi) } func Cleanup() { for _, c := range cmdlist { if c != nil { c.Process.Kill() } } } func getLine(s *bufio.Scanner) (string, error) { if s.Scan() { return s.Text(), nil } if err := s.Err(); err != nil { return "", common.Perror("scanner.Err", err) } return "", nil } func get(s *bufio.Scanner) (*event, error) { var line string var err error for { line, err = getLine(s) if len(line) > 0 && line[0] != 'W' { break } } if line == "" { return nil, nil } if err != nil { return nil, err } name, tokens, _ := strings.Cut(line, " ") return &event{name, strings.Split(tokens, " ")}, nil }