An application to display the albumn cover of the track played in cmus.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: first commit with basic functionality

Marcos Gabarda 3ed67fc8

+361
+32
.gitignore
··· 1 + # Created by https://gitignore.org 2 + # Go.gitignore 3 + 4 + # If you prefer the allow list template instead of the deny list, see community template: 5 + # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 6 + # 7 + # Binaries for programs and plugins 8 + cover-viewer 9 + *.exe 10 + *.exe~ 11 + *.dll 12 + *.so 13 + *.dylib 14 + 15 + # Test binary, built with `go test -c` 16 + *.test 17 + 18 + # Code coverage profiles and other test artifacts 19 + *.out 20 + coverage.* 21 + *.coverprofile 22 + profile.cov 23 + 24 + # Dependency directories (remove the comment below to include it) 25 + # vendor/ 26 + 27 + # Go workspace file 28 + go.work 29 + go.work.sum 30 + 31 + # env file 32 + .env
+13
.pre-commit-config.yaml
··· 1 + repos: 2 + - repo: https://github.com/pre-commit/pre-commit-hooks 3 + rev: v5.0.0 4 + hooks: 5 + - id: end-of-file-fixer 6 + - id: trailing-whitespace 7 + - repo: local 8 + hooks: 9 + - id: gofmt 10 + name: gofmt 11 + entry: gofmt -w . 12 + language: system 13 + pass_filenames: false
+38
README.md
··· 1 + # cmus cover viewer 2 + 3 + Small program in Go to display the cover art of the current track playing in 4 + [cmus](https://cmus.github.io/). 5 + 6 + This is how it'd look in [kitty](https://sw.kovidgoyal.net/kitty/) terminal and using 7 + also [cava](https://github.com/karlstav/cava): 8 + 9 + ![screenshot](./screenshot.png) 10 + 11 + ## How to use 12 + 13 + This program has two modes: **visualizer** to show the cover, and notify to send the 14 + change in the cover. 15 + 16 + ### Run visualizer 17 + 18 + ``` 19 + cover-viewer --mode visualizer 20 + ``` 21 + 22 + ### Notify from cmus 23 + 24 + This is based on the [status display](https://github.com/cmus/cmus/wiki/status-display-programs#usage--installation) from cmus. 25 + 26 + Assuming you use multiple programs, with a shell script, e.g. 27 + ~/.cmus/status_display_program.sh, you can add there this visualizer: 28 + 29 + ```bash 30 + #!/bin/sh 31 + 32 + # other programs 33 + program1 "$@" & 34 + program2 "$@" & 35 + 36 + # cover viewer 37 + echo "$@" | cover-viewer --mode notify 38 + ```
+27
go.mod
··· 1 + module cover-viewer 2 + 3 + go 1.24.2 4 + 5 + require ( 6 + github.com/blacktop/go-termimg v0.1.24 // indirect 7 + github.com/charmbracelet/colorprofile v0.3.3 // indirect 8 + github.com/charmbracelet/x/ansi v0.11.0 // indirect 9 + github.com/charmbracelet/x/cellbuf v0.0.14 // indirect 10 + github.com/charmbracelet/x/mosaic v0.0.0-20251118172736-77d017256798 // indirect 11 + github.com/charmbracelet/x/term v0.2.2 // indirect 12 + github.com/clipperhouse/displaywidth v0.4.1 // indirect 13 + github.com/clipperhouse/stringish v0.1.1 // indirect 14 + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect 15 + github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 // indirect 16 + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect 17 + github.com/makeworld-the-better-one/dither/v2 v2.4.0 // indirect 18 + github.com/mattn/go-runewidth v0.0.19 // indirect 19 + github.com/mattn/go-sixel v0.0.5 // indirect 20 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect 21 + github.com/rivo/uniseg v0.4.7 // indirect 22 + github.com/soniakeys/quant v1.0.0 // indirect 23 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 24 + golang.org/x/image v0.32.0 // indirect 25 + golang.org/x/sys v0.38.0 // indirect 26 + golang.org/x/term v0.37.0 // indirect 27 + )
+48
go.sum
··· 1 + github.com/blacktop/go-termimg v0.1.24 h1:gAACg+AD3NQ7dmYOh5AjInNgs/yHBdXryEgGcDpA1GU= 2 + github.com/blacktop/go-termimg v0.1.24/go.mod h1:2vuo4jOVaEmWYtWRmyG935Uc/wtQ8MoxaceFGi0DXRc= 3 + github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI= 4 + github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4= 5 + github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA= 6 + github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE= 7 + github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= 8 + github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= 9 + github.com/charmbracelet/x/mosaic v0.0.0-20251118172736-77d017256798 h1:uey91YESnaP5/lHmUjidqlH8mxtwwbDUh7kFCGwYHzg= 10 + github.com/charmbracelet/x/mosaic v0.0.0-20251118172736-77d017256798/go.mod h1:DW9EJPyH1uKfkr7IEAT5rZ6NSZTA/tOVnEqlt8Ku3rU= 11 + github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= 12 + github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= 13 + github.com/clipperhouse/displaywidth v0.4.1 h1:uVw9V8UDfnggg3K2U84VWY1YLQ/x2aKSCtkRyYozfoU= 14 + github.com/clipperhouse/displaywidth v0.4.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= 15 + github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= 16 + github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= 17 + github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= 18 + github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= 19 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 + github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg= 21 + github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E= 22 + github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= 23 + github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 24 + github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE= 25 + github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc= 26 + github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= 27 + github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 28 + github.com/mattn/go-sixel v0.0.5 h1:55w2FR5ncuhKhXrM5ly1eiqMQfZsnAHIpYNGZX03Cv8= 29 + github.com/mattn/go-sixel v0.0.5/go.mod h1:h2Sss+DiUEHy0pUqcIB6PFXo5Cy8sTQEFr3a9/5ZLNw= 30 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 31 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 32 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 + github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 34 + github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 35 + github.com/soniakeys/quant v1.0.0 h1:N1um9ktjbkZVcywBVAAYpZYSHxEfJGzshHCxx/DaI0Y= 36 + github.com/soniakeys/quant v1.0.0/go.mod h1:HI1k023QuVbD4H8i9YdfZP2munIHU4QpjsImz6Y6zds= 37 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 38 + github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 39 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 40 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 41 + golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= 42 + golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= 43 + golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 44 + golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 45 + golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= 46 + golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 47 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 48 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+203
main.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "flag" 6 + "fmt" 7 + "io" 8 + "log" 9 + "net" 10 + "os" 11 + "strings" 12 + 13 + termimg "github.com/blacktop/go-termimg" 14 + "github.com/dhowden/tag" 15 + ) 16 + 17 + var cmusKeys = map[string]bool{ 18 + "status": true, 19 + "file": true, 20 + "artist": true, 21 + "album": true, 22 + "albumartist": true, 23 + "title": true, 24 + "tracknumber": true, 25 + "duration": true, 26 + } 27 + 28 + var lastPath string 29 + 30 + func main() { 31 + log.SetOutput(os.Stderr) 32 + log.SetFlags(log.LstdFlags | log.Lshortfile) 33 + 34 + mode := flag.String("mode", "", "visualizer or notify") 35 + socketPath := flag.String("socket", "/tmp/music-viewer.sock", "unix socket path") 36 + flag.Parse() 37 + 38 + switch *mode { 39 + case "visualizer": 40 + if err := runVisualizer(*socketPath); err != nil { 41 + fmt.Fprintln(os.Stderr, "visualize error:", err) 42 + os.Exit(1) 43 + } 44 + case "notify": 45 + if err := runNotify(*socketPath); err != nil { 46 + fmt.Fprintln(os.Stderr, "notify error:", err) 47 + os.Exit(1) 48 + } 49 + default: 50 + fmt.Fprintln(os.Stderr, "usage: --mode visualizer|notify") 51 + os.Exit(1) 52 + } 53 + } 54 + 55 + // runNotify writes in the socket the content of the Stdin, meant to be the status 56 + // string from cmus. 57 + func runNotify(socketPath string) error { 58 + conn, err := net.Dial("unix", socketPath) 59 + if err != nil { 60 + log.Println(err) 61 + return err 62 + 63 + } 64 + defer conn.Close() 65 + 66 + _, err = io.Copy(conn, os.Stdin) 67 + return err 68 + } 69 + 70 + // runVisualizer listen to the socket and handles the message with the status of 71 + // cmus 72 + func runVisualizer(socketPath string) error { 73 + cleanScreen() 74 + _ = os.Remove(socketPath) 75 + 76 + ln, err := net.Listen("unix", socketPath) 77 + if err != nil { 78 + log.Println(err) 79 + return err 80 + } 81 + defer ln.Close() 82 + 83 + for { 84 + conn, err := ln.Accept() 85 + if err != nil { 86 + continue 87 + } 88 + handleConn(conn) 89 + } 90 + } 91 + 92 + // handleConn handles the socket connection 93 + func handleConn(conn net.Conn) { 94 + defer conn.Close() 95 + 96 + // read the message 97 + data, err := io.ReadAll(conn) 98 + if err != nil { 99 + log.Println(err) 100 + return 101 + } 102 + 103 + path, ok := extractFilePath(string(data)) 104 + if !ok { 105 + return 106 + } 107 + 108 + if path == lastPath { 109 + return 110 + } 111 + 112 + showCover(path) 113 + 114 + lastPath = path 115 + } 116 + 117 + func parseCmusLine(input string) map[string]string { 118 + parts := strings.Fields(input) 119 + result := make(map[string]string) 120 + 121 + var currentKey string 122 + var buffer []string 123 + 124 + flush := func() { 125 + if currentKey != "" && len(buffer) > 0 { 126 + result[currentKey] = strings.Join(buffer, " ") 127 + } 128 + buffer = nil 129 + } 130 + 131 + for _, part := range parts { 132 + if cmusKeys[part] { 133 + flush() 134 + currentKey = part 135 + continue 136 + } 137 + buffer = append(buffer, part) 138 + } 139 + 140 + flush() 141 + return result 142 + } 143 + 144 + // extractFilePath extract the file path from the status string 145 + func extractFilePath(input string) (string, bool) { 146 + data := parseCmusLine(input) 147 + path, ok := data["file"] 148 + return path, ok 149 + } 150 + 151 + // cleanScreen clean the screen 152 + func cleanScreen() { 153 + fmt.Print("\033[2J") 154 + fmt.Print("\033[H") 155 + fmt.Print("\n") 156 + } 157 + 158 + // showCover read the picture tag from the file and shows it in kitty terminal 159 + func showCover(path string) { 160 + f, err := os.Open(path) 161 + if err != nil { 162 + log.Println(err) 163 + return 164 + } 165 + defer f.Close() 166 + 167 + meta, err := tag.ReadFrom(f) 168 + if err != nil { 169 + log.Println(err) 170 + return 171 + } 172 + 173 + pic := meta.Picture() 174 + if pic == nil { 175 + log.Println(err) 176 + return 177 + } 178 + 179 + renderCover(pic.Data) 180 + } 181 + 182 + // renderCover renders the image in the terminal 183 + func renderCover(data []byte) { 184 + // clean screen 185 + fmt.Print("\033[2J\033[H") 186 + 187 + // get terminal size 188 + features := termimg.QueryTerminalFeatures() 189 + 190 + imgData := bytes.NewReader(data) 191 + img, err := termimg.From(imgData) 192 + if err != nil { 193 + log.Println(err) 194 + return 195 + } 196 + 197 + img. 198 + Width(features.WindowCols). 199 + Height(features.WindowRows). 200 + Scale(termimg.ScaleFit). 201 + Print() 202 + 203 + }
screenshot.png

This is a binary file and will not be displayed.