From 387fd15bf231e1cf38eaa7ad1543aaf911a4325e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaroslav=20de=20la=20Pe=C3=B1a=20Smirnov?= Date: Sun, 20 Nov 2022 02:42:19 +0300 Subject: Make waybar-mpris more flexible customizable Instead of customizing by indicating just the order and using flags for other customization options (e.g. separator), read a C-like format string with tokens/verbs that are replaced with their respective information (e.g. `%a` for artist). Also made it possible to customize the tooltip. The principle is the same as with the module's text. --- README.md | 32 +++++---- main.go | 222 +++++++++++++++++++++++++++++++------------------------------- 2 files changed, 130 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index 87eca47..0a8712c 100644 --- a/README.md +++ b/README.md @@ -57,21 +57,29 @@ When running, the program will pipe out json in waybar's format. Add something l ``` -Usage of waybar-mpris: - --autofocus Auto switch to currently playing music players. - --interpolate Interpolate track position (helpful for players that don't update regularly, e.g mpDris2) - --order string Element order. (default "SYMBOL:ARTIST:ALBUM:TITLE:POSITION") - --pause string Pause symbol/text to use. (default "\uf8e3") - --play string Play symbol/text to use. (default "▶") - --position Show current position between brackets, e.g (04:50/05:00) - --replace Replace any running instances - --send string send command to already runnning waybar-mpris instance. (options: player-next/player-prev/next/prev/toggle) - --separator string Separator string to use between artist, album, and title. (default " - ") +Usage of ./waybar-mpris: + --autofocus Auto switch to currently playing music players. + --interpolate Interpolate track position (helpful for players that don't update regularly, e.g mpDris2) + --pause string Pause symbol/text to use. (default "\uf8e3") + --play string Play symbol/text to use. (default "▶") + --replace Replace existing waybar-mpris if found. When false, new instance will clone the original instances output. + --send string send command to already runnning waybar-mpris instance. (options: player-next/player-prev/next/prev/toggle/list) + --text-format string Format of the waybar module text. (default "%i %a - %t (%p/%d)") + --tooltip-format string Format of the waybar module tooltip. (default "%t by %a from %A\n(%P)") ``` -* Modify the order of components with `--order`. `SYMBOL` is the play/paused icon or text, `POSITION` is the track position (if enabled), other options are self explanatory. +* `--text-format` specifies in what format to display the module's text in + waybar; the accepted tokens are as follows: + - `%i` play/pause icon + - `%a` track artist + - `%A` album + - `%t` track title + - `%p` current position + - `%l` track length + - `%P` current media player +* `--tooltip-format` same as `text-format` but for the tooltip; same rules + apply. * `--play/--pause` specify the symbols or text to display when music is paused/playing respectively. -* `--separator` specifies a string to separate the artist, album and title text. * `--autofocus` makes waybar-mpris automatically focus on currently playing music players. * `--position` enables the display of the track position. * `--interpolate` increments the track position every second. This is useful for players (e.g mpDris2) that don't regularly update the position. diff --git a/main.go b/main.go index 206ea44..4593ca2 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "io" "log" @@ -28,20 +29,32 @@ const ( // Mostly default values for flag options. var ( - PLAY = "▶" - PAUSE = "" - SEP = " - " - ORDER = "SYMBOL:ARTIST:ALBUM:TITLE:POSITION" - AUTOFOCUS = false + playIcon = "▶" + pauseIcon = "" + textFormat = "%i %a - %t (%p/%d)" + tooltipFormat = "%t by %a from %A\n(%P)" + autofocus = false // Available commands that can be sent to running instances. - COMMANDS = []string{"player-next", "player-prev", "next", "prev", "toggle", "list"} - SHOW_POS = false - INTERPOLATE = false - REPLACE = false + commands = []string{"player-next", "player-prev", "next", "prev", "toggle", "list"} + showPos = false + interpolate = false + replace = false isSharing = false isDataSharing = false - WRITER io.Writer = os.Stdout - SHAREWRITER, DATAWRITER io.Writer + writer io.Writer = os.Stdout + shareWriter, dataWriter io.Writer +) + +// Format tokens `%` +const ( + tokIcon = 'i' + tokArtist = 'a' + tokAlbum = 'A' + tokTitle = 't' + tokPosition = 'p' + tokLength = 'd' + tokPlayer = 'P' + tokPercent = '%' ) const ( @@ -142,97 +155,78 @@ type player struct { Duplicate bool } +// This represents the output in JSON format that waybar is expecting +type waybarData struct { + Class string `json:"class"` + Text string `json:"text"` + Tooltip string `json:"tooltip"` +} + 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. -func playerJSON(p *player) string { - artist := strings.ReplaceAll(p.Artist, "\"", "\\\"") - album := strings.ReplaceAll(p.Album, "\"", "\\\"") - title := strings.ReplaceAll(p.Title, "\"", "\\\"") - name := strings.ReplaceAll(p.Name, "\"", "\\\"") - symbol := PLAY - out := "{\"class\": \"" - if p.Playing { - symbol = PAUSE - out += "playing" - } else { - out += "paused" - } - var pos string - if SHOW_POS { - if !p.Duplicate { - pos = p.StringPosition() - if pos != "" { - pos = "(" + pos + ")" - } - } else { - pos = "(" + secondsToString(int(p.Position/1000000)) + "/" + secondsToString(p.Length) + ")" - - } - } - var items []string - order := strings.Split(ORDER, ":") - for _, v := range order { - switch v { - case "SYMBOL": - items = append(items, symbol) - case "ARTIST": - if artist != "" { - items = append(items, artist) +func formatOutput(p *player, icon string, fstr string) string { + var buf []byte + for i := 0; i < len(fstr); i++ { + if fstr[i] == '%' { + i++ + if i >= len(fstr) { + break } - case "ALBUM": - if album != "" { - items = append(items, album) + switch fstr[i] { + case tokIcon: + buf = append(buf, []byte(icon)...) + case tokArtist: + buf = append(buf, []byte(p.Artist)...) + case tokAlbum: + buf = append(buf, []byte(p.Album)...) + case tokTitle: + buf = append(buf, []byte(p.Title)...) + case tokPosition: + position := secondsToString(int(p.Position / 1000000)) + buf = append(buf, []byte(position)...) + case tokLength: + length := secondsToString(p.Length) + buf = append(buf, []byte(length)...) + case tokPlayer: + buf = append(buf, []byte(p.Name)...) + default: + buf = append(buf, fstr[i]) } - case "TITLE": - if title != "" { - items = append(items, title) - } - case "POSITION": - if pos != "" && SHOW_POS { - items = append(items, pos) - } - case "PLAYER": - if name != "" { - items = append(items, name) - } - } - } - if len(items) == 0 { - return "{}" - } - text := "" - for i, v := range items { - right := "" - if (v == symbol || v == pos) && i != len(items)-1 { - right = " " - } else if i != len(items)-1 && items[i+1] != symbol && items[i+1] != pos { - right = SEP } else { - right = " " + buf = append(buf, fstr[i]) } - text += v + right } - out += "\",\"text\":\"" + text + "\"," - out += "\"tooltip\":\"" + strings.ReplaceAll(title, "&", "&") + "\\n" - if artist != "" { - out += "by " + strings.ReplaceAll(artist, "&", "&") + "\\n" + + out := strings.ReplaceAll(string(buf), `"`, `\"`) + out = strings.ReplaceAll(out, "&", "&") + + return out +} + +// Returns JSON for waybar to consume. +func playerJSON(p *player) string { + data := waybarData{} + + icon := playIcon + if p.Playing { + icon = pauseIcon + data.Class = "playing" + } else { + data.Class = "paused" } - if album != "" { - out += "from " + strings.ReplaceAll(album, "&", "&") + "\\n" + + data.Text = formatOutput(p, icon, textFormat) + data.Tooltip = formatOutput(p, icon, tooltipFormat) + + out, err := json.Marshal(data) + if err != nil { + return "{}" } - out += "(" + name + ")\"}" - return out - // return fmt.Sprintf("{\"class\":\"%s\",\"text\":\"%s\",\"tooltip\":\"%s\"}", data["class"], data["text"], data["tooltip"]) - // out, err := json.Marshal(data) - // if err != nil { - // return "{}" - // } - // return string(out) + return string(out) } type players struct { @@ -407,7 +401,7 @@ func duplicateOutput() error { } str := string(l) fromData(p, str) - fmt.Fprintln(WRITER, playerJSON(p)) + fmt.Fprintln(writer, playerJSON(p)) f.Seek(0, 0) } } @@ -483,14 +477,14 @@ func listenForCommands(players *players) { if err != nil { fmt.Fprintf(con, "Failed: %v", err) } - DATAWRITER = dataWrite{ + dataWriter = dataWrite{ emptyEveryWrite{file: f}, players, } if isSharing { - WRITER = io.MultiWriter(os.Stdout, SHAREWRITER, DATAWRITER) + writer = io.MultiWriter(os.Stdout, shareWriter, dataWriter) } else { - WRITER = io.MultiWriter(os.Stdout, DATAWRITER) + writer = io.MultiWriter(os.Stdout, dataWriter) } isDataSharing = true } @@ -502,11 +496,11 @@ func listenForCommands(players *players) { if err != nil { fmt.Fprintf(con, "Failed: %v", err) } - SHAREWRITER = emptyEveryWrite{file: f} + shareWriter = emptyEveryWrite{file: f} if isDataSharing { - WRITER = io.MultiWriter(SHAREWRITER, DATAWRITER, os.Stdout) + writer = io.MultiWriter(shareWriter, dataWriter, os.Stdout) } else { - WRITER = io.MultiWriter(SHAREWRITER, os.Stdout) + writer = io.MultiWriter(shareWriter, os.Stdout) } isSharing = true } @@ -565,16 +559,20 @@ func main() { } mw := io.MultiWriter(logfile, os.Stdout) log.SetOutput(mw) - flag.StringVar(&PLAY, "play", PLAY, "Play symbol/text to use.") - flag.StringVar(&PAUSE, "pause", PAUSE, "Pause symbol/text to use.") - flag.StringVar(&SEP, "separator", SEP, "Separator string to use between artist, album, and title.") - flag.StringVar(&ORDER, "order", ORDER, "Element order. An extra \"PLAYER\" element is also available.") - flag.BoolVar(&AUTOFOCUS, "autofocus", AUTOFOCUS, "Auto switch to currently playing music players.") - flag.BoolVar(&SHOW_POS, "position", SHOW_POS, "Show current position between brackets, e.g (04:50/05:00)") - flag.BoolVar(&INTERPOLATE, "interpolate", INTERPOLATE, "Interpolate track position (helpful for players that don't update regularly, e.g mpDris2)") - flag.BoolVar(&REPLACE, "replace", REPLACE, "replace existing waybar-mpris if found. When false, new instance will clone the original instances output.") + flag.StringVar(&playIcon, "play", playIcon, "Play symbol/text to use.") + flag.StringVar(&pauseIcon, "pause", pauseIcon, "Pause symbol/text to use.") + flag.StringVar(&textFormat, "text-format", textFormat, + "Format of the waybar module text.") + flag.StringVar(&tooltipFormat, "tooltip-format", tooltipFormat, + "Format of the waybar module tooltip.") + flag.BoolVar(&autofocus, "autofocus", autofocus, + "Auto switch to currently playing music players.") + flag.BoolVar(&interpolate, "interpolate", interpolate, + "Interpolate track position (helpful for players that don't update regularly, e.g mpDris2)") + flag.BoolVar(&replace, "replace", replace, + "Replace existing waybar-mpris if found. When false, new instance will clone the original instances output.") var command string - flag.StringVar(&command, "send", "", "send command to already runnning waybar-mpris instance. (options: "+strings.Join(COMMANDS, "/")+")") + flag.StringVar(&command, "send", "", "send command to already runnning waybar-mpris instance. (options: "+strings.Join(commands, "/")+")") flag.Parse() os.Stderr = logfile @@ -585,7 +583,7 @@ func main() { // fmt.Println("New array", players) // Start command listener if _, err := os.Stat(SOCK); err == nil { - if REPLACE { + if replace { fmt.Printf("Socket %s already exists, this could mean waybar-mpris is already running.\nStarting this instance will overwrite the file, possibly stopping other instances from accepting commands.\n", SOCK) var input string ignoreChoice := false @@ -614,34 +612,34 @@ func main() { log.Fatalln("Error connecting to DBus:", err) } players := &players{ - mpris2: mpris2.NewMpris2(conn, INTERPOLATE, POLL, AUTOFOCUS), + mpris2: mpris2.NewMpris2(conn, interpolate, POLL, autofocus), } players.mpris2.Reload() players.mpris2.Sort() lastLine := "" go listenForCommands(players) go players.mpris2.Listen() - if SHOW_POS { + if showPos { go func() { for { time.Sleep(POLL * time.Second) if len(players.mpris2.List) != 0 { if players.mpris2.List[players.mpris2.Current].Playing { - go fmt.Fprintln(WRITER, players.JSON()) + go fmt.Fprintln(writer, players.JSON()) } } } }() } - fmt.Fprintln(WRITER, players.JSON()) + fmt.Fprintln(writer, players.JSON()) for v := range players.mpris2.Messages { if v.Name == "refresh" { - if AUTOFOCUS { + if autofocus { players.mpris2.Sort() } if l := players.JSON(); l != lastLine { lastLine = l - fmt.Fprintln(WRITER, l) + fmt.Fprintln(writer, l) } } } -- cgit v1.2.3