├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── fontfinder.go ├── go.mod ├── go.sum ├── gobar.go ├── gobar_test.go ├── parser.go └── parser_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: KenjiTakahashi 2 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | go: [1.17, 1.18, 1.19] 12 | 13 | steps: 14 | - name: setup 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: ${{ matrix.go }} 18 | - uses: actions/checkout@v3 19 | - name: dependencies 20 | run: go get -v -t -d ./... 21 | - name: build 22 | run: go build -v 23 | - name: test 24 | run: go test -v 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gobar 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | gobar 2 | 3 | Copyright (C) 2014-2015,2022 Karol 'Kenji Takahashi' Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 21 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/distatus/gobar/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/distatus/gobar/actions/workflows/tests.yml) 2 | 3 | **gobar** is a minimalistic X status bar written in pure Go. 4 | 5 | Supports xinerama, EWMH, font antialiasing and possibly some other fancy looking names and shortcuts. 6 | 7 | ## screenshot 8 | 9 | Two **gobar** instances, both fed by [osop](https://github.com/distatus/osop). 10 | 11 | For detailed configuration see [my](https://github.com/KenjiTakahashi/dotfiles/blob/master/dotxprofile) [dotfiles](https://github.com/KenjiTakahashi/dotfiles/tree/master/dotconfig/osop). 12 | 13 | ![screenshot](http://img.kenji.sx/gobar_dual.png) 14 | 15 | ## installation 16 | 17 | First, you have to [get Go](http://golang.org/doc/install). Note that version >= 1.1 is required. 18 | 19 | Then, just 20 | 21 | ```bash 22 | $ go get github.com/distatus/gobar 23 | ``` 24 | 25 | should get you going. 26 | 27 | ## usage 28 | 29 | Command line options reference follows: 30 | 31 | **-h --help** displays help message and exits. 32 | 33 | **--bottom** places bar on bottom of the screen *(defaults to false)*. 34 | 35 | **--geometries** takes comma separated list of monitor geometries *(defaults to `0x16+0+0`)*. 36 | 37 | Each geometry is in form of `x++`. If ``/`` is `0`, screen width/height is used. 38 | 39 | If geometry is empty, bar is not drawn on a respective monitor. 40 | 41 | If there are less geometries than monitors, last geometry is used for subsequent monitors. 42 | 43 | **--fonts** takes comma separated list of fonts. 44 | 45 | Each font element is in form of `[:]`. 46 | 47 | If omitted, or if incorrect path is specified, defaults to whatever it can find in the system. 48 | If nothing suitable is found, falls back to `Liberation Mono` that is always bundled with the Go font library. 49 | 50 | If `` part is omitted or incorrect, defaults to `12`. 51 | 52 | **--fg** takes main foreground color. Should be in form `0xAARRGGBB` *(defaults to `0xFFFFFFFF`)*. 53 | 54 | **--bg** takes main background color. Should be in form `0xAARRGGBB` *(defaults to `0xFF000000`)*. 55 | 56 | Other than that, an input string should be piped into the **gobar** executable. 57 | 58 | A really simple example could be displaying current date and time. 59 | ```bash 60 | $ while :; do date; sleep 1; done | gobar 61 | ``` 62 | 63 | Special tokens can also be used in the input string to allow nice formatting. 64 | 65 | #### Input string formatting syntax 66 | 67 | Each token should be preceded with `{` and will be active until `}`. Note that `{text}` is also treated as valid token and will output `text`. Escaping with `\` will print bracket(s) literally. 68 | 69 | **F<num>** sets active font, **<num>** should be index of one of the elements from fonts list specified in **--fonts=**. 70 | 71 | **S<num>,<num>...** specifies monitors to draw on. Multiple, comma separated, numbers can be specified. If not specified, draws to all available monitors. Negative number can be specified to set on which monitors to *not* draw. 72 | 73 | **CF0xAARRGGBB** sets active foreground color. 74 | 75 | **CB0xAARRGGBB** sets active background color. 76 | 77 | **AR** aligns next text piece to the right. 78 | -------------------------------------------------------------------------------- /fontfinder.go: -------------------------------------------------------------------------------- 1 | // gobar 2 | // 3 | // Copyright (C) 2022 Karol 'Kenji Takahashi' Woźniak 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included 13 | // in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 21 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | package main 24 | 25 | import ( 26 | "io" 27 | "log" 28 | "os" 29 | "strconv" 30 | "strings" 31 | 32 | "github.com/adrg/sysfont" 33 | "github.com/flopp/go-findfont" 34 | "github.com/jezek/xgbutil/xgraphics" 35 | "golang.org/x/image/font" 36 | "golang.org/x/image/font/inconsolata" 37 | "golang.org/x/image/font/opentype" 38 | ) 39 | 40 | func findFont(def string) font.Face { 41 | i := strings.LastIndexByte(def, ':') 42 | name, size := parseSize(def, i) 43 | 44 | fontPath, err := findfont.Find(name) 45 | if err != nil { 46 | log.Printf("Could not find font `%s`, trying alternate method: %s", def, err) 47 | return findFontFallback(def, size) 48 | } 49 | fontFile, err := os.Open(fontPath) 50 | if err != nil { 51 | log.Printf("Could not open font `%s`, trying to find another one: %s", fontPath, err) 52 | return findFontFallback(def, size) 53 | } 54 | face, err := parseFontFace(fontFile, size) 55 | if err != nil { 56 | log.Printf("Could not parse font `%s`, trying to find another one: %s", fontPath, err) 57 | return findFontFallback(def, size) 58 | } 59 | return face 60 | } 61 | 62 | var fallbackFinder *sysfont.Finder = nil 63 | 64 | func findFontFallback(def string, size float64) font.Face { 65 | if fallbackFinder == nil { 66 | fallbackFinder = sysfont.NewFinder(nil) 67 | } 68 | 69 | fontDef := fallbackFinder.Match(def) 70 | if fontDef == nil { 71 | log.Printf("Could not find font `%s`, using `inconsolata regular 8x16`", def) 72 | return inconsolata.Regular8x16 73 | } 74 | fontFile, err := os.Open(fontDef.Filename) 75 | if err != nil { 76 | log.Printf("Could not open font `%s`, using `inconsolata regular 8x16`: %s", fontDef.Filename, err) 77 | return inconsolata.Regular8x16 78 | } 79 | face, err := parseFontFace(fontFile, size) 80 | if err != nil { 81 | log.Printf("Could not parse font `%s`, using `inconsolata regular 8x16`: %s", fontDef.Filename, err) 82 | return inconsolata.Regular8x16 83 | } 84 | log.Printf("Found fallback font `%s`", fontDef.Filename) 85 | return face 86 | } 87 | 88 | func parseFontFace(file io.Reader, size float64) (font.Face, error) { 89 | otf, err := xgraphics.ParseFont(file) 90 | if err != nil { 91 | return nil, err 92 | } 93 | // XXX Can we somehow figure out DPI? 94 | face, err := opentype.NewFace(otf, &opentype.FaceOptions{Size: size, DPI: 72}) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return face, nil 99 | } 100 | 101 | func parseSize(def string, i int) (string, float64) { 102 | if i == -1 { 103 | log.Printf("Font size not specified for `%s`, using `12`", def) 104 | return def, 12 105 | } 106 | name, sizeStr := def[:i], def[i+1:] 107 | size, err := strconv.ParseFloat(sizeStr, 32) 108 | if err != nil { 109 | log.Printf("Invalid font size `%s` for `%s`, using `12`: `%s`", sizeStr, name, err) 110 | size = 12 111 | } 112 | return name, size 113 | } 114 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/distatus/gobar 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/adrg/sysfont v0.1.2 7 | github.com/flopp/go-findfont v0.1.0 8 | github.com/jezek/xgb v1.1.0 9 | github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 10 | golang.org/x/image v0.2.0 11 | ) 12 | 13 | require ( 14 | github.com/adrg/strutil v0.2.2 // indirect 15 | github.com/adrg/xdg v0.3.0 // indirect 16 | golang.org/x/text v0.5.0 // indirect 17 | ) 18 | 19 | replace github.com/jezek/xgbutil => github.com/distatus/xgbutil v0.0.0-20221230133850-77969a621d99 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/adrg/strutil v0.2.2 h1:XSd9+o2xaOon2oRum0JymNT+f0nfLiAiDzGOcjcIsMI= 2 | github.com/adrg/strutil v0.2.2/go.mod h1:EF2fjOFlGTepljfI+FzgTG13oXthR7ZAil9/aginnNQ= 3 | github.com/adrg/sysfont v0.1.2 h1:MSU3KREM4RhsQ+7QgH7wPEPTgAgBIz0Hw6Nd4u7QgjE= 4 | github.com/adrg/sysfont v0.1.2/go.mod h1:6d3l7/BSjX9VaeXWJt9fcrftFaD/t7l11xgSywCPZGk= 5 | github.com/adrg/xdg v0.3.0 h1:BO+k4wFj0IoTolBF1Apn8oZrX3LQrEbBA8+/9vyW9J4= 6 | github.com/adrg/xdg v0.3.0/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/distatus/xgbutil v0.0.0-20221230133850-77969a621d99 h1:7hpHTZ/YlX6qWg3DC+NlKjTZAyeRNPFPF7Wlap3KncU= 10 | github.com/distatus/xgbutil v0.0.0-20221230133850-77969a621d99/go.mod h1:cgcACHaGA5+hmySbhwirPOdwGTC349/ZGEBqcOkN9Q4= 11 | github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU= 12 | github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw= 13 | github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= 14 | github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 19 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 20 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 21 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 22 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 23 | golang.org/x/image v0.2.0 h1:/DcQ0w3VHKCC5p0/P2B0JpAZ9Z++V2KOo2fyU89CXBQ= 24 | golang.org/x/image v0.2.0/go.mod h1:la7oBXb9w3YFjBqaAwtynVioc1ZvOnNteUNrifGNmAI= 25 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 26 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 27 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 28 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 29 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 31 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 32 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 37 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 38 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 39 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 40 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 41 | golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= 42 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 43 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 44 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 45 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 46 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 49 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | -------------------------------------------------------------------------------- /gobar.go: -------------------------------------------------------------------------------- 1 | // gobar 2 | // 3 | // Copyright (C) 2014-2015,2022 Karol 'Kenji Takahashi' Woźniak 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included 13 | // in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 21 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | package main 24 | 25 | import ( 26 | "bufio" 27 | "flag" 28 | "fmt" 29 | "image" 30 | "log" 31 | "os" 32 | "strings" 33 | 34 | "github.com/jezek/xgb/xproto" 35 | "github.com/jezek/xgbutil" 36 | "github.com/jezek/xgbutil/ewmh" 37 | "github.com/jezek/xgbutil/xevent" 38 | "github.com/jezek/xgbutil/xgraphics" 39 | "github.com/jezek/xgbutil/xinerama" 40 | "github.com/jezek/xgbutil/xwindow" 41 | "golang.org/x/image/font" 42 | "golang.org/x/image/math/fixed" 43 | ) 44 | 45 | // fatal is a helper function to call when something terribly wrong 46 | // had happened. Logs given error and terminates application. 47 | func fatal(err error) { 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | } 52 | 53 | func contains(slice []uint, item uint) bool { 54 | for _, s := range slice { 55 | if s == item { 56 | return true 57 | } 58 | } 59 | return false 60 | } 61 | 62 | // headsEqual Checks whether Rects contained in xinerama.Heads are all equal. 63 | func headsEqual(h1, h2 xinerama.Heads) bool { 64 | if len(h1) != len(h2) { 65 | return false 66 | } 67 | for i, h := range h1 { 68 | x1, y1, w1, h1 := h.Pieces() 69 | x2, y2, w2, h2 := h2[i].Pieces() 70 | if x1 != x2 || y1 != y2 || w1 != w2 || h1 != h2 { 71 | return false 72 | } 73 | } 74 | return true 75 | } 76 | 77 | // Position defines bar placement on the screen. 78 | type Position uint8 79 | 80 | const ( 81 | BOTTOM Position = iota 82 | TOP 83 | ) 84 | 85 | // Geometry stores bars geometry on the screen (or actually monitor). 86 | type Geometry struct { 87 | Width uint16 88 | Height uint16 89 | X uint16 90 | Y uint16 91 | } 92 | 93 | func (g *Geometry) String() string { 94 | return fmt.Sprintf("%dx%d+%d+%d", g.Width, g.Height, g.X, g.Y) 95 | } 96 | 97 | // Bar stores and manages all X related stuff and configuration. 98 | type Bar struct { 99 | X *xgbutil.XUtil 100 | Windows []*xwindow.Window 101 | Geometries []*Geometry 102 | Foreground *xgraphics.BGRA 103 | Background *xgraphics.BGRA 104 | Colors []*xgraphics.BGRA 105 | Fonts fonts 106 | 107 | heads xinerama.Heads 108 | } 109 | 110 | // NewBar creates X windows for every monitor. 111 | // Also sets proper EWMH information for docked windows and 112 | // deals with dynamic geometry changes. 113 | func NewBar( 114 | X *xgbutil.XUtil, geometries []*Geometry, position Position, 115 | fg uint64, bg uint64, fonts fonts, 116 | ) *Bar { 117 | heads, err := xinerama.PhysicalHeads(X) 118 | fatal(err) 119 | 120 | bar := &Bar{ 121 | X: X, 122 | Windows: []*xwindow.Window{}, 123 | Geometries: []*Geometry{}, 124 | Foreground: NewBGRA(fg), 125 | Background: NewBGRA(bg), 126 | Fonts: fonts, 127 | heads: heads, 128 | } 129 | 130 | bar.create(geometries, position) 131 | 132 | xproto.ChangeWindowAttributesChecked( 133 | X.Conn(), X.RootWin(), xproto.CwEventMask, 134 | []uint32{xproto.EventMaskStructureNotify}, 135 | ) 136 | xevent.ConfigureNotifyFun(func(_ *xgbutil.XUtil, _ xevent.ConfigureNotifyEvent) { 137 | heads, err = xinerama.PhysicalHeads(X) 138 | if err != nil { 139 | log.Printf("Error `%s` getting updated heads, staying with the old ones\n", err) 140 | return 141 | } 142 | if !headsEqual(heads, bar.heads) { 143 | bar.destroy() 144 | bar.heads = heads 145 | bar.create(geometries, position) 146 | } 147 | }).Connect(X, X.RootWin()) 148 | 149 | return bar 150 | } 151 | 152 | // destroy Destroys all existing windows and resets geometries. 153 | func (b *Bar) destroy() { 154 | for i, window := range b.Windows { 155 | window.Destroy() 156 | b.Windows[i] = nil 157 | } 158 | b.Windows = []*xwindow.Window{} 159 | b.Geometries = []*Geometry{} 160 | } 161 | 162 | func (b *Bar) create(geometries []*Geometry, position Position) { 163 | maxHeight := xwindow.RootGeometry(b.X).Height() 164 | 165 | if len(geometries) == 0 { 166 | geometries = append(geometries, &Geometry{Height: 16}) 167 | } 168 | for i, head := range b.heads { 169 | var geometry *Geometry 170 | if i >= len(geometries) { 171 | if geometries[len(geometries)-1] == nil { 172 | break 173 | } 174 | geometry = geometries[len(geometries)-1] 175 | } else { 176 | if geometries[i] == nil { 177 | continue 178 | } 179 | geometry = geometries[i] 180 | } 181 | win, err := xwindow.Generate(b.X) 182 | if err != nil { 183 | log.Printf("Could not generate window for geometry `%s`", geometry) 184 | continue 185 | } 186 | 187 | width := int(geometry.Width) 188 | if width == 0 { 189 | width = head.Width() 190 | } 191 | height := int(geometry.Height) 192 | if height == 0 { 193 | height = head.Height() 194 | } 195 | y := int(geometry.Y) 196 | 197 | strutP := ewmh.WmStrutPartial{} 198 | strut := ewmh.WmStrut{} 199 | if position == BOTTOM { 200 | y = head.Height() - height - y 201 | bottom := uint(maxHeight - y) 202 | 203 | strutP.BottomStartX = uint(geometry.X) 204 | strutP.BottomEndX = uint(geometry.X + uint16(width)) 205 | strutP.Bottom = bottom 206 | strut.Bottom = bottom 207 | } else { 208 | strutP.TopStartX = uint(geometry.X) 209 | strutP.TopEndX = uint(geometry.X + uint16(width)) 210 | strutP.Top = uint(height) 211 | strut.Top = uint(height) 212 | } 213 | 214 | win.Create( 215 | b.X.RootWin(), 216 | int(geometry.X)+head.X(), 217 | y+head.Y(), 218 | width, height, 0, 219 | ) 220 | 221 | ewmh.WmWindowTypeSet(b.X, win.Id, []string{"_NET_WM_WINDOW_TYPE_DOCK"}) 222 | ewmh.WmStateSet(b.X, win.Id, []string{"_NET_WM_STATE_STICKY"}) 223 | ewmh.WmDesktopSet(b.X, win.Id, 0xFFFFFFFF) 224 | ewmh.WmStrutPartialSet(b.X, win.Id, &strutP) 225 | ewmh.WmStrutSet(b.X, win.Id, &strut) 226 | 227 | b.Windows = append(b.Windows, win) 228 | b.Geometries = append(b.Geometries, &Geometry{ 229 | X: geometry.X, 230 | Y: uint16(y), 231 | Width: uint16(width), 232 | Height: uint16(height), 233 | }) 234 | } 235 | } 236 | 237 | // Draw draws TextPieces into X monitors. 238 | func (b *Bar) Draw(text []*TextPiece) { 239 | imgs := make([]*xgraphics.Image, len(b.Windows)) 240 | for i, geometry := range b.Geometries { 241 | imgs[i] = xgraphics.New(b.X, image.Rect( 242 | 0, 0, int(geometry.Width), int(geometry.Height), 243 | )) 244 | imgs[i].For(func(x, y int) xgraphics.BGRA { return *b.Background }) 245 | } 246 | 247 | xsl := make([]fixed.Int26_6, len(b.Windows)) 248 | xsr := make([]fixed.Int26_6, len(b.Windows)) 249 | for i := range xsr { 250 | xsr[i] = fixed.I(int(b.Geometries[i].Width)) 251 | } 252 | for _, piece := range text { 253 | if piece.Background == nil { 254 | piece.Background = b.Background 255 | } 256 | if piece.Foreground == nil { 257 | piece.Foreground = b.Foreground 258 | } 259 | 260 | if piece.Font > uint(len(b.Fonts))-1 { 261 | log.Printf("Invalid font index `%d`, using `0`", piece.Font) 262 | piece.Font = 0 263 | } 264 | pFont := b.Fonts[piece.Font] 265 | width := font.MeasureString(pFont, piece.Text) 266 | 267 | screens := []uint{} 268 | if piece.Screens == nil { 269 | for i := range imgs { 270 | if !contains(piece.NotScreens, uint(i)) { 271 | screens = append(screens, uint(i)) 272 | } 273 | } 274 | } else { 275 | for _, screen := range piece.Screens { 276 | if int(screen) < len(xsl) && !contains(piece.NotScreens, screen) { 277 | screens = append(screens, screen) 278 | } 279 | } 280 | } 281 | 282 | for _, screen := range screens { 283 | xs := xsl[screen] 284 | if piece.Align == RIGHT { 285 | xs = xsr[screen] - width 286 | } 287 | 288 | // XXX Avoid the roundings? 289 | // Would waterfall inside xgraphics and create problems with adhering 290 | // to the image.Image interface. 291 | subimg := imgs[screen].SubImage(image.Rect( 292 | xs.Round(), 0, (xs + width).Round(), int(b.Geometries[screen].Height), 293 | )) 294 | if subimg == nil { 295 | log.Printf( 296 | "Cannot create Subimage for coords `%dx%dx%dx%d`\n", 297 | xs, 0, xs+width, int(b.Geometries[screen].Height), 298 | ) 299 | continue 300 | } 301 | subximg := subimg.(*xgraphics.Image) 302 | 303 | subximg.For(func(x, y int) xgraphics.BGRA { return *piece.Background }) 304 | 305 | xsNew := subximg.Text(fixed.Point26_6{X: xs, Y: 0}, piece.Foreground, pFont, piece.Text).X 306 | 307 | if piece.Align == LEFT { 308 | xsl[screen] = xsNew 309 | } else if piece.Align == RIGHT { 310 | xsr[screen] -= width 311 | } 312 | 313 | subximg.XPaint(b.Windows[screen].Id) 314 | subximg.Destroy() 315 | } 316 | } 317 | 318 | for i, img := range imgs { 319 | img.XSurfaceSet(b.Windows[i].Id) 320 | img.XDraw() 321 | img.XPaint(b.Windows[i].Id) 322 | img.Destroy() 323 | 324 | b.Windows[i].Map() 325 | } 326 | } 327 | 328 | type fonts []font.Face 329 | 330 | func (f *fonts) String() string { 331 | str := make([]string, len(*f)) 332 | for i, f := range *f { 333 | str[i] = fmt.Sprintf("%v", f) 334 | } 335 | return fmt.Sprintf("%q", strings.Join(str, ",")) 336 | } 337 | 338 | func (f *fonts) Set(value string) error { 339 | names := strings.Split(value, ",") 340 | for _, name := range names { 341 | font := findFont(name) 342 | *f = append(*f, font) 343 | } 344 | return nil 345 | } 346 | 347 | type Geometries []*Geometry 348 | 349 | func (g *Geometries) String() string { 350 | str := make([]string, len(*g)) 351 | for i, g := range *g { 352 | str[i] = g.String() 353 | } 354 | j := strings.Join(str, ",") 355 | if j == "" { 356 | j = "0x16+0+0" 357 | } 358 | return fmt.Sprintf("%q", j) 359 | } 360 | 361 | func (g *Geometries) Set(value string) error { 362 | if len(*g) > 0 { 363 | return fmt.Errorf("geometries flag already set") 364 | } 365 | if value == "" { 366 | return nil 367 | } 368 | for _, geometry := range strings.Split(value, ",") { 369 | if geometry == "" { 370 | *g = append(*g, nil) 371 | } else { 372 | geom := &Geometry{} 373 | _, err := fmt.Sscanf( 374 | geometry, "%dx%d+%d+%d", 375 | &geom.Width, &geom.Height, &geom.X, &geom.Y, 376 | ) 377 | if err != nil { 378 | geom = &Geometry{Height: 16} 379 | log.Printf("Bad geometry `%s`, using default", geometry) 380 | } 381 | *g = append(*g, geom) 382 | } 383 | } 384 | return nil 385 | } 386 | 387 | // main gets command line arguments, creates X connection and initializes Bar. 388 | // This is also where X event loop and Stdin reading lies. 389 | func main() { 390 | bottom := flag.Bool("bottom", false, "Place bar at the bottom of the screen") 391 | fgColor := flag.Uint64("fg", 0xFFFFFFFF, "Foreground color (0xAARRGGBB)") 392 | flag.Lookup("fg").DefValue = "0xFFFFFFFF" 393 | bgColor := flag.Uint64("bg", 0xFF000000, "Background color (0xAARRGGBB)") 394 | flag.Lookup("bg").DefValue = "0xFF000000" 395 | var fonts fonts 396 | flag.Var(&fonts, "fonts", "Comma separated list of fonts in form of path[:size]") 397 | var geometries Geometries 398 | flag.Var(&geometries, "geometries", "Comma separated list of monitor geometries (x++), for and , 0 means 100%") 399 | flag.Parse() 400 | 401 | if len(fonts) < 1 { 402 | fonts = append(fonts, findFontFallback("", 12)) 403 | } 404 | 405 | position := TOP 406 | if *bottom { 407 | position = BOTTOM 408 | } 409 | 410 | X, err := xgbutil.NewConn() 411 | fatal(err) 412 | 413 | bar := NewBar(X, geometries, position, *fgColor, *bgColor, fonts) 414 | parser := NewTextParser() 415 | 416 | stdin := make(chan []*TextPiece) 417 | go func() { 418 | defer close(stdin) 419 | reader := bufio.NewReader(os.Stdin) 420 | 421 | for { 422 | str, err := reader.ReadString('\n') 423 | if err != nil { 424 | log.Printf("Error reading stdin. Got `%s`", err) 425 | } else { 426 | stdin <- parser.Scan(strings.NewReader(str)) 427 | } 428 | } 429 | }() 430 | 431 | pingBefore, pingAfter, pingQuit := xevent.MainPing(X) 432 | for { 433 | select { 434 | case <-pingBefore: 435 | <-pingAfter 436 | case text := <-stdin: 437 | bar.Draw(text) 438 | case <-pingQuit: 439 | return 440 | } 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /gobar_test.go: -------------------------------------------------------------------------------- 1 | // gobar 2 | // Copyright (C) 2014-2015 Karol 'Kenji Takahashi' Woźniak 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining 5 | // a copy of this software and associated documentation files (the "Software"), 6 | // to deal in the Software without restriction, including without limitation 7 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | // and/or sell copies of the Software, and to permit persons to whom the 9 | // Software is furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 20 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "bytes" 26 | "fmt" 27 | "log" 28 | "os" 29 | "testing" 30 | ) 31 | 32 | func TestGeometriesSet(t *testing.T) { 33 | tests := []struct { 34 | input string 35 | logs string 36 | output Geometries 37 | }{ 38 | {"", "", Geometries{}}, 39 | {"0x16+0+0", "", Geometries{ 40 | {0, 16, 0, 0}, 41 | }}, 42 | {",0x16+0+0", "", Geometries{ 43 | nil, 44 | {0, 16, 0, 0}, 45 | }}, 46 | {"0x16+0+0,", "", Geometries{ 47 | {0, 16, 0, 0}, 48 | nil, 49 | }}, 50 | {",0x16+0+0,", "", Geometries{ 51 | nil, 52 | {0, 16, 0, 0}, 53 | nil, 54 | }}, 55 | {"22x01+20+15", "", Geometries{ 56 | {22, 1, 20, 15}, 57 | }}, 58 | {",0x16+0+0,22x01+20+15,", "", Geometries{ 59 | nil, 60 | {0, 16, 0, 0}, 61 | {22, 1, 20, 15}, 62 | nil, 63 | }}, 64 | {",0x16+0+0,,22x01+20+15,", "", Geometries{ 65 | nil, 66 | {0, 16, 0, 0}, 67 | nil, 68 | {22, 1, 20, 15}, 69 | nil, 70 | }}, 71 | {"wrongo", "Bad geometry `wrongo`, using default\n", Geometries{ 72 | {0, 16, 0, 0}, 73 | }}, 74 | } 75 | 76 | var stderr bytes.Buffer 77 | log.SetOutput(&stderr) 78 | 79 | for i, test := range tests { 80 | geometries := Geometries{} 81 | 82 | geometries.Set(test.input) 83 | logs, _ := stderr.ReadString('\n') 84 | 85 | assertEqual(t, test.input, test.output, geometries, "GeometriesSet", i) 86 | if logs != "" { 87 | assertEqual(t, test.input, test.logs, logs[20:], "GeometriesSet", i) 88 | } 89 | } 90 | 91 | geometries := Geometries{{0, 16, 0, 0}} 92 | err := geometries.Set("") 93 | assertEqualError(t, fmt.Errorf("geometries flag already set"), err, "GeometriesSet", -1) 94 | 95 | log.SetOutput(os.Stderr) 96 | } 97 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | // gobar 2 | // 3 | // Copyright (C) 2014,2022 Karol 'Kenji Takahashi' Woźniak 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included 13 | // in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 21 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | package main 24 | 25 | import ( 26 | "bufio" 27 | "io" 28 | "log" 29 | "regexp" 30 | "strconv" 31 | 32 | "github.com/jezek/xgbutil/xgraphics" 33 | ) 34 | 35 | // Align defines text piece alignment on the screen. 36 | type Align uint8 37 | 38 | const ( 39 | LEFT Align = iota 40 | RIGHT 41 | ) 42 | 43 | // EndScan is an artifical Error. 44 | // Raised when parser should stop scanning. 45 | type EndScan struct{} 46 | 47 | func (e EndScan) Error() string { return "EndScan" } 48 | 49 | // NewBGRA returns a new color definition in X compatible format. 50 | // Input should be a hexagonal representation with alpha, i.e 0xAARRGGBB. 51 | func NewBGRA(color uint64) *xgraphics.BGRA { 52 | a := uint8(color >> 24) 53 | r := uint8((color & 0x00ff0000) >> 16) 54 | g := uint8((color & 0x0000ff00) >> 8) 55 | b := uint8(color & 0x000000ff) 56 | return &xgraphics.BGRA{B: b, G: g, R: r, A: a} 57 | } 58 | 59 | // TextPiece stores formatting information for a text 60 | // within single pair of brackets. 61 | type TextPiece struct { 62 | Text string 63 | Font uint 64 | Align Align 65 | Foreground *xgraphics.BGRA 66 | Background *xgraphics.BGRA 67 | Screens []uint 68 | NotScreens []uint 69 | 70 | Origin *TextPiece 71 | } 72 | 73 | // TextParser is used to create a set of TextPieces from a textual definition. 74 | type TextParser struct { 75 | rgbPattern *regexp.Regexp 76 | } 77 | 78 | // NewTextParser creates TextParser instance with 79 | // correct necessary regexp definitions. 80 | func NewTextParser() *TextParser { 81 | return &TextParser{regexp.MustCompile(`^0[xX][0-9a-fA-F]{8}$`)} 82 | } 83 | 84 | // Tokenize turns textual definition into a series of valid tokens. 85 | // If no valid token is found at given place, char at 0 position is returned. 86 | func (tp *TextParser) Tokenize( 87 | data []byte, EOF bool, 88 | ) (advance int, token []byte, err error) { 89 | if EOF { 90 | return 91 | } 92 | switch { 93 | case data[0] == '\n': 94 | err = EndScan{} 95 | case len(data) < 2: 96 | advance, token, err = 1, data[:1], nil 97 | case string(data[:2]) == "{F": 98 | advance, token, err = 2, data[:2], nil 99 | case string(data[:2]) == "{S": 100 | advance, token, err = 2, data[:2], nil 101 | case len(data) < 3: 102 | advance, token, err = 1, data[:1], nil 103 | case string(data[:3]) == "{CF": 104 | advance, token, err = 3, data[:3], nil 105 | case string(data[:3]) == "{CB": 106 | advance, token, err = 3, data[:3], nil 107 | case string(data[:3]) == "{AR": 108 | advance, token, err = 3, data[:3], nil 109 | case len(data) >= 10 && tp.rgbPattern.Match(data[:10]): 110 | advance, token, err = 10, data[:10], nil 111 | case ('0' <= data[0] && data[0] <= '9') || data[0] == '-': 112 | i := 0 113 | if data[0] == '-' { 114 | i = 1 115 | } 116 | for _, n := range data[i:] { 117 | if !('0' <= n && n <= '9') { 118 | break 119 | } 120 | i++ 121 | } 122 | advance, token, err = i, data[:i], nil 123 | default: // Also contains '}' and ',' 124 | // TODO: Parsing whole text piece here, instead of returning 125 | // char-by-char, should perform better 126 | advance, token, err = 1, data[:1], nil 127 | } 128 | return 129 | } 130 | 131 | // Scan scans textual definition and returns array of TextPieces. 132 | // Possible empty pieces are omitted in the returned array. 133 | func (tp *TextParser) Scan(r io.Reader) []*TextPiece { 134 | var text []*TextPiece 135 | 136 | scanner := bufio.NewScanner(r) 137 | 138 | scanner.Split(tp.Tokenize) 139 | 140 | currentText := &TextPiece{} 141 | text = append(text, currentText) 142 | 143 | currentIndex := func() int { 144 | for i, t := range text { 145 | if t == currentText { 146 | return i 147 | } 148 | } 149 | return 0 150 | } 151 | 152 | moveCurrent := func(end bool) *TextPiece { 153 | newCurrent := &TextPiece{} 154 | if end { 155 | *newCurrent = *currentText.Origin 156 | } else { 157 | *newCurrent = *currentText 158 | newCurrent.Origin = currentText 159 | } 160 | newCurrent.Text = "" 161 | if currentText.Align == RIGHT { 162 | i := currentIndex() 163 | text = append(text, &TextPiece{}) 164 | copy(text[i+1:], text[i:]) 165 | text[i] = newCurrent 166 | } else { 167 | text = append(text, newCurrent) 168 | } 169 | currentText = newCurrent 170 | return newCurrent 171 | } 172 | 173 | logPieceError := func(err error, pieces ...string) { 174 | log.Printf("Problem parsing `%q`: %s", pieces, err) 175 | for _, piece := range pieces { 176 | currentText.Text += piece 177 | } 178 | } 179 | 180 | screening := false 181 | escaping := false 182 | bracketing := 0 183 | for scanner.Scan() { 184 | stext := scanner.Text() 185 | switch { 186 | case stext == "\\": 187 | escaping = true 188 | continue 189 | case !escaping && stext == "{F": 190 | scanner.Scan() 191 | text := scanner.Text() 192 | font, err := strconv.Atoi(text) 193 | if err != nil { 194 | logPieceError(err, stext, text) 195 | } 196 | newCurrent := moveCurrent(false) 197 | newCurrent.Font = uint(font) 198 | case !escaping && stext == "{S": 199 | scanner.Scan() 200 | text := scanner.Text() 201 | screen, err := strconv.Atoi(text) 202 | if err != nil { 203 | logPieceError(err, stext, text) 204 | } 205 | newCurrent := moveCurrent(false) 206 | if text[0] == '-' { 207 | newCurrent.NotScreens = append(newCurrent.NotScreens, uint(-screen)) 208 | } else { 209 | newCurrent.Screens = append(newCurrent.Screens, uint(screen)) 210 | } 211 | screening = true 212 | case !escaping && stext == "{CF": 213 | scanner.Scan() 214 | text := scanner.Text() 215 | fg, err := strconv.ParseUint(text, 0, 32) 216 | if err != nil { 217 | logPieceError(err, stext, text) 218 | } 219 | newCurrent := moveCurrent(false) 220 | newCurrent.Foreground = NewBGRA(fg) 221 | case !escaping && stext == "{CB": 222 | scanner.Scan() 223 | text := scanner.Text() 224 | bg, err := strconv.ParseUint(text, 0, 32) 225 | if err != nil { 226 | logPieceError(err, stext, text) 227 | } 228 | newCurrent := moveCurrent(false) 229 | newCurrent.Background = NewBGRA(bg) 230 | case !escaping && stext == "{AR": 231 | newCurrent := moveCurrent(false) 232 | newCurrent.Align = RIGHT 233 | case !escaping && stext == "{": 234 | bracketing++ 235 | case !escaping && stext == "}": 236 | if bracketing > 0 { 237 | bracketing-- 238 | continue 239 | } 240 | screening = false 241 | if currentText.Origin != nil { 242 | moveCurrent(true) 243 | continue 244 | } 245 | fallthrough 246 | default: 247 | if screening && stext == "," { 248 | scanner.Scan() 249 | text := scanner.Text() 250 | screen, err := strconv.Atoi(text) 251 | if err != nil { 252 | logPieceError(err, stext, text) 253 | } 254 | currentText.Screens = append(currentText.Screens, uint(screen)) 255 | } else { 256 | currentText.Text += stext 257 | } 258 | escaping = false 259 | } 260 | } 261 | 262 | //Remove possible empty pieces. 263 | var text2 []*TextPiece 264 | for _, piece := range text { 265 | if piece.Text != "" { 266 | text2 = append(text2, piece) 267 | } 268 | } 269 | 270 | return text2 271 | } 272 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | // gobar 2 | // 3 | // Copyright (C) 2014-2015,2022 Karol 'Kenji Takahashi' Woźniak 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the "Software"), 7 | // to deal in the Software without restriction, including without limitation 8 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | // and/or sell copies of the Software, and to permit persons to whom the 10 | // Software is furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included 13 | // in all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 21 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | package main 24 | 25 | import ( 26 | "errors" 27 | "reflect" 28 | "strings" 29 | "testing" 30 | 31 | "github.com/jezek/xgbutil/xgraphics" 32 | ) 33 | 34 | var TokenizeTests = []struct { 35 | input string 36 | advanceExpected int 37 | tokenExpected string 38 | }{ 39 | {"t", 1, "t"}, 40 | {"te", 1, "t"}, 41 | {"tes", 1, "t"}, 42 | {"test", 1, "t"}, 43 | {"{Ftest", 2, "{F"}, 44 | {"{Stest", 2, "{S"}, 45 | {"{CFtest", 3, "{CF"}, 46 | {"{CBtest", 3, "{CB"}, 47 | {"{ARtest", 3, "{AR"}, 48 | {"0xff1eF09atest", 10, "0xff1eF09a"}, 49 | {"0xff1eF0test", 1, "0"}, 50 | {"0312495test", 7, "0312495"}, 51 | {"5942130", 7, "5942130"}, 52 | } 53 | 54 | func assertEqual(t *testing.T, input, expected, actual interface{}, name string, i int) { 55 | if !reflect.DeepEqual(expected, actual) { 56 | t.Errorf("%s:%d(%v) == %v != %v\n", name, i, input, actual, expected) 57 | } 58 | } 59 | 60 | func assertEqualError(t *testing.T, expected, actual error, name string, i int) { 61 | if actual == nil && expected == nil { 62 | return 63 | } 64 | if (actual == nil && expected != nil) || expected.Error() != actual.Error() { 65 | t.Errorf("%s:%d expected error `%s`, got `%s`\n", name, i, expected, actual) 66 | } 67 | } 68 | 69 | func TestTokenize(t *testing.T) { 70 | parser := NewTextParser() 71 | 72 | for _, tt := range TokenizeTests { 73 | // Do manual copy to ensure that cap(input) == len(tt.input) 74 | input := make([]byte, len(tt.input)) 75 | for i, s := range tt.input { 76 | input[i] = byte(s) 77 | } 78 | 79 | advanceActual, tokenActual, err := parser.Tokenize(input, false) 80 | 81 | assertEqualError(t, nil, err, "Tokenize", 0) 82 | assertEqual(t, tt.input, tt.advanceExpected, advanceActual, "Tokenize", 0) 83 | assertEqual(t, tt.input, []byte(tt.tokenExpected), tokenActual, "Tokenize", 0) 84 | } 85 | } 86 | 87 | func TestTokenize_newline(t *testing.T) { 88 | parser := NewTextParser() 89 | 90 | input := "\ntest" 91 | advance, token, err := parser.Tokenize([]byte(input), false) 92 | 93 | assertEqual(t, input, 0, advance, "Tokenize_newline", 0) 94 | assertEqual(t, input, []byte(nil), token, "Tokenize_newline", 0) 95 | assertEqualError(t, errors.New("EndScan"), err, "Tokenize_newline", 0) 96 | } 97 | 98 | var ScanTests = []struct { 99 | input string 100 | expected []*TextPiece 101 | }{ 102 | {"test", []*TextPiece{ 103 | {Text: "test"}, 104 | }}, 105 | {"{F1test}", []*TextPiece{ 106 | {Text: "test", Font: 1}, 107 | }}, 108 | {"{CF0xFF00AA33test}", []*TextPiece{ 109 | {Text: "test", Foreground: &xgraphics.BGRA{B: 0x33, G: 0xAA, R: 0x00, A: 0xFF}}, 110 | }}, 111 | {"{CB0x33AA00FFtest}", []*TextPiece{ 112 | {Text: "test", Background: &xgraphics.BGRA{B: 0xFF, G: 0x00, R: 0xAA, A: 0x33}}, 113 | }}, 114 | {"{ARtest}", []*TextPiece{ 115 | {Text: "test", Align: RIGHT}, 116 | }}, 117 | {"{ARtest1{F1test2}}", []*TextPiece{ 118 | {Text: "test2", Font: 1, Align: RIGHT}, {Text: "test1", Align: RIGHT}, 119 | }}, 120 | {"{AR{F1test1}test2}", []*TextPiece{ 121 | {Text: "test2", Align: RIGHT}, {Text: "test1", Font: 1, Align: RIGHT}, 122 | }}, 123 | {"{S1test}", []*TextPiece{ 124 | {Text: "test", Screens: []uint{1}}, 125 | }}, 126 | {"{S1,2test}", []*TextPiece{ 127 | {Text: "test", Screens: []uint{1, 2}}, 128 | }}, 129 | {"{F1test1}test2", []*TextPiece{ 130 | {Text: "test1", Font: 1}, {Text: "test2"}, 131 | }}, 132 | {"test1{F1test2}", []*TextPiece{ 133 | {Text: "test1"}, {Text: "test2", Font: 1}, 134 | }}, 135 | {"test1{F1test2}test3", []*TextPiece{ 136 | {Text: "test1"}, {Text: "test2", Font: 1}, {Text: "test3"}, 137 | }}, 138 | {"{F1test1}{F2test2}", []*TextPiece{ 139 | {Text: "test1", Font: 1}, {Text: "test2", Font: 2}, 140 | }}, 141 | {"{F1test1}test2{F2test3}", []*TextPiece{ 142 | {Text: "test1", Font: 1}, {Text: "test2"}, {Text: "test3", Font: 2}, 143 | }}, 144 | {"{F1{F2test1}}", []*TextPiece{ 145 | {Text: "test1", Font: 2}, 146 | }}, 147 | {"{F1test1{F2test2}}", []*TextPiece{ 148 | {Text: "test1", Font: 1}, {Text: "test2", Font: 2}, 149 | }}, 150 | {"{F1{F2test1}test2}", []*TextPiece{ 151 | {Text: "test1", Font: 2}, {Text: "test2", Font: 1}, 152 | }}, 153 | {"{F1test1{F2test2}test3}", []*TextPiece{ 154 | {Text: "test1", Font: 1}, {Text: "test2", Font: 2}, {Text: "test3", Font: 1}, 155 | }}, 156 | {"{S1test1}{F1{S1test2}test3}", []*TextPiece{ 157 | {Text: "test1", Screens: []uint{1}}, 158 | {Text: "test2", Font: 1, Screens: []uint{1}}, 159 | {Text: "test3", Font: 1}, 160 | }}, 161 | {"}", []*TextPiece{ 162 | {Text: "}"}, 163 | }}, 164 | {"\\{F", []*TextPiece{ 165 | {Text: "{F"}, 166 | }}, 167 | {"\\{S", []*TextPiece{ 168 | {Text: "{S"}, 169 | }}, 170 | {"\\{CF", []*TextPiece{ 171 | {Text: "{CF"}, 172 | }}, 173 | {"\\{CB", []*TextPiece{ 174 | {Text: "{CB"}, 175 | }}, 176 | {"\\}", []*TextPiece{ 177 | {Text: "}"}, 178 | }}, 179 | {"{test1}", []*TextPiece{ 180 | {Text: "test1"}, 181 | }}, 182 | {"\\{test1}", []*TextPiece{ 183 | {Text: "{test1}"}, 184 | }}, 185 | {"\\{test1}{test2}", []*TextPiece{ 186 | {Text: "{test1}test2"}, 187 | }}, 188 | {"{F1test1}{test2}{ARtest3}", []*TextPiece{ 189 | {Text: "test1", Font: 1}, {Text: "test2"}, {Text: "test3", Align: RIGHT}, 190 | }}, 191 | {"{F1test1}test2", []*TextPiece{ 192 | {Text: "test1", Font: 1}, {Text: "test2"}, 193 | }}, 194 | {"{F1{S2test1}}test2", []*TextPiece{ 195 | {Text: "test1", Font: 1, Screens: []uint{2}}, {Text: "test2"}, 196 | }}, 197 | {"{F1{S2test1}test2}test3", []*TextPiece{ 198 | {Text: "test1", Font: 1, Screens: []uint{2}}, {Text: "test2", Font: 1}, {Text: "test3"}, 199 | }}, 200 | {"{S-0test1}", []*TextPiece{ 201 | {Text: "test1", NotScreens: []uint{0}}, 202 | }}, 203 | {"{S-1test1}", []*TextPiece{ 204 | {Text: "test1", NotScreens: []uint{1}}, 205 | }}, 206 | } 207 | 208 | func TestScan(t *testing.T) { 209 | parser := NewTextParser() 210 | 211 | for i, tt := range ScanTests { 212 | actual := parser.Scan(strings.NewReader(tt.input)) 213 | // We don't care about Origin 214 | for _, t := range actual { 215 | t.Origin = nil 216 | } 217 | 218 | assertEqual(t, tt.input, tt.expected, actual, "Scan", i) 219 | } 220 | } 221 | 222 | func BenchmarkScan(b *testing.B) { 223 | parser := NewTextParser() 224 | 225 | for i := 0; i < b.N; i++ { 226 | parser.Scan(strings.NewReader("{F1{S2test1}test2}test3")) 227 | } 228 | } 229 | --------------------------------------------------------------------------------