support multiple instances with different layouts
All checks were successful
continuous-integration/drone/push Build is passing

if specified layout for nth instance is different than 1st, the player data is
    shared instead of the waybar output.
This commit is contained in:
Harvey Tindall 2021-05-17 15:56:23 +01:00
parent ea4f47a1b1
commit 49b3212c48
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2

174
main.go
View File

@ -7,6 +7,7 @@ import (
"net" "net"
"os" "os"
"os/signal" "os/signal"
"strconv"
"strings" "strings"
"time" "time"
@ -20,7 +21,8 @@ import (
const ( const (
SOCK = "/tmp/waybar-mpris.sock" SOCK = "/tmp/waybar-mpris.sock"
LOGFILE = "/tmp/waybar-mpris.log" LOGFILE = "/tmp/waybar-mpris.log"
OUTFILE = "/tmp/waybar-mpris.out" OUTFILE = "/tmp/waybar-mpris.out" // Used for sharing waybar output when args are the same.
DATAFILE = "/tmp/waybar-mpris.data.out" // Used for sharing "\n"-separated player data between instances when args are different.
POLL = 1 POLL = 1
) )
@ -37,7 +39,9 @@ var (
INTERPOLATE = false INTERPOLATE = false
REPLACE = false REPLACE = false
isSharing = false isSharing = false
isDataSharing = false
WRITER io.Writer = os.Stdout WRITER io.Writer = os.Stdout
SHAREWRITER, DATAWRITER io.Writer
) )
const ( const (
@ -49,8 +53,10 @@ const (
cList = "ls" cList = "ls"
cShare = "sh" cShare = "sh"
cPreShare = "ps" cPreShare = "ps"
cDataShare = "ds"
rSuccess = "sc" rSuccess = "sc"
rInvalidCommand = "iv" rInvalidCommand = "iv"
rFailed = "fa"
) )
func stringToCmd(str string) string { func stringToCmd(str string) string {
@ -69,14 +75,81 @@ func stringToCmd(str string) string {
return cList return cList
case "share": case "share":
return cShare return cShare
case "data-share":
return cDataShare
case "pre-share": case "pre-share":
return cPreShare return cPreShare
} }
return "" return ""
} }
// length-µS\nposition-µS\nplaying (0 or 1)\nartist\nalbum\ntitle\nplayer\n
func fromData(p *player, cmd string) {
p.Duplicate = true
values := make([]string, 7)
prev := 0
current := 0
for i := range cmd {
if current == len(values) {
break
}
if cmd[i] == '\n' {
values[current] = cmd[prev:i]
prev = i + 1
current++
}
}
l, err := strconv.ParseInt(values[0], 10, 64)
if err != nil {
l = -1
}
p.Length = int(l) / 1000000
pos, err := strconv.ParseInt(values[1], 10, 64)
if err != nil {
pos = -1
}
p.Position = pos
if values[2] == "1" {
p.Playing = true
} else {
p.Playing = false
}
p.Artist = values[3]
p.Album = values[4]
p.Title = values[5]
p.Name = values[6]
}
func toData(p *player) (cmd string) {
cmd += strconv.FormatInt(int64(p.Length*1000000), 10) + "\n"
cmd += strconv.FormatInt(p.Position, 10) + "\n"
if p.Playing {
cmd += "1"
} else {
cmd += "0"
}
cmd += "\n"
cmd += p.Artist + "\n"
cmd += p.Album + "\n"
cmd += p.Title + "\n"
cmd += p.Name + "\n"
return
}
type player struct {
*mpris2.Player
Duplicate bool
}
func secondsToString(seconds int) string {
minutes := int(seconds / 60)
seconds -= int(minutes * 60)
return fmt.Sprintf("%02d:%02d", minutes, seconds)
}
// JSON returns json for waybar to consume. // JSON returns json for waybar to consume.
func playerJSON(p *mpris2.Player) string { func playerJSON(p *player) string {
symbol := PLAY symbol := PLAY
out := "{\"class\": \"" out := "{\"class\": \""
if p.Playing { if p.Playing {
@ -87,10 +160,15 @@ func playerJSON(p *mpris2.Player) string {
} }
var pos string var pos string
if SHOW_POS { if SHOW_POS {
if !p.Duplicate {
pos = p.StringPosition() pos = p.StringPosition()
if pos != "" { if pos != "" {
pos = "(" + pos + ")" pos = "(" + pos + ")"
} }
} else {
pos = "(" + secondsToString(int(p.Position/1000000)) + "/" + secondsToString(p.Length) + ")"
}
} }
var items []string var items []string
order := strings.Split(ORDER, ":") order := strings.Split(ORDER, ":")
@ -159,7 +237,7 @@ type players struct {
func (pl *players) JSON() string { func (pl *players) JSON() string {
if len(pl.mpris2.List) != 0 { if len(pl.mpris2.List) != 0 {
return playerJSON(pl.mpris2.List[pl.mpris2.Current]) return playerJSON(&player{pl.mpris2.List[pl.mpris2.Current], false})
} }
return "{}" return "{}"
} }
@ -217,18 +295,18 @@ func duplicateOutput() error {
for _, arg := range os.Args { for _, arg := range os.Args {
argString += arg + "|" argString += arg + "|"
} }
if string(buf[0:nr]) == argString {
conn.Close() conn.Close()
conn, err = net.Dial("unix", SOCK) conn, err = net.Dial("unix", SOCK)
if err != nil { if err != nil {
return err return err
} }
if string(buf[0:nr]) == argString {
// Tell other instance to share output in OUTFILE // Tell other instance to share output in OUTFILE
_, err := conn.Write([]byte(cShare)) _, err := conn.Write([]byte(cShare))
if err != nil { if err != nil {
log.Fatalf("Couldn't send command: %v", err) log.Fatalf("Couldn't send command: %v", err)
} }
buf := make([]byte, 2) buf = make([]byte, 2)
nr, err := conn.Read(buf) nr, err := conn.Read(buf)
if err != nil { if err != nil {
log.Fatalf("Couldn't read response: %v", err) log.Fatalf("Couldn't read response: %v", err)
@ -282,6 +360,55 @@ func duplicateOutput() error {
} }
} }
} }
} else {
_, err := conn.Write([]byte(cDataShare))
if err != nil {
log.Fatalf("Couldn't send command: %v", err)
}
buf = make([]byte, 2)
nr, err := conn.Read(buf)
if err != nil {
log.Fatalf("Couldn't read response: %v", err)
}
if resp := string(buf[0:nr]); resp == rSuccess {
f, err := os.Open(DATAFILE)
if err != nil {
log.Fatalf("Failed to open \"%s\": %v", DATAFILE, err)
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatalf("Failed to start watcher: %v", err)
}
defer watcher.Close()
err = watcher.Add(DATAFILE)
if err != nil {
log.Fatalf("Failed to watch file: %v", err)
}
p := &player{
&mpris2.Player{},
true,
}
for {
select {
case event, ok := <-watcher.Events:
if !ok {
log.Printf("Watcher failed: %v", err)
return err
}
if event.Op&fsnotify.Write == fsnotify.Write {
l, err := io.ReadAll(f)
if err != nil {
log.Printf("Failed to read file: %v", err)
return err
}
str := string(l)
fromData(p, str)
fmt.Fprintln(WRITER, playerJSON(p))
f.Seek(0, 0)
}
}
}
}
} }
return nil return nil
} }
@ -345,6 +472,25 @@ func listenForCommands(players *players) {
players.Toggle() players.Toggle()
case cList: case cList:
con.Write([]byte(players.mpris2.String())) con.Write([]byte(players.mpris2.String()))
case cDataShare:
if !isDataSharing {
f, err := os.OpenFile(DATAFILE, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
defer f.Close()
if err != nil {
fmt.Fprintf(con, "Failed: %v", err)
}
DATAWRITER = dataWrite{
emptyEveryWrite{file: f},
players,
}
if isSharing {
WRITER = io.MultiWriter(SHAREWRITER, DATAWRITER, os.Stdout)
} else {
WRITER = io.MultiWriter(DATAWRITER, os.Stdout)
}
isDataSharing = true
}
fmt.Fprint(con, rSuccess)
case cShare: case cShare:
if !isSharing { if !isSharing {
f, err := os.OpenFile(OUTFILE, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) f, err := os.OpenFile(OUTFILE, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
@ -352,8 +498,12 @@ func listenForCommands(players *players) {
if err != nil { if err != nil {
fmt.Fprintf(con, "Failed: %v", err) fmt.Fprintf(con, "Failed: %v", err)
} }
var out io.Writer = emptyEveryWrite{file: f} SHAREWRITER = emptyEveryWrite{file: f}
WRITER = io.MultiWriter(os.Stdout, out) if isDataSharing {
WRITER = io.MultiWriter(SHAREWRITER, DATAWRITER, os.Stdout)
} else {
WRITER = io.MultiWriter(SHAREWRITER, os.Stdout)
}
isSharing = true isSharing = true
} }
fmt.Fprint(con, rSuccess) fmt.Fprint(con, rSuccess)
@ -373,6 +523,16 @@ func listenForCommands(players *players) {
} }
} }
type dataWrite struct {
emptyEveryWrite
Players *players
}
func (w dataWrite) Write(p []byte) (n int, err error) {
line := toData(&player{w.Players.mpris2.List[w.Players.mpris2.Current], true})
return w.emptyEveryWrite.Write([]byte(line))
}
type emptyEveryWrite struct { type emptyEveryWrite struct {
file *os.File file *os.File
} }