├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── config ├── hotspot.css └── style.css ├── go.mod ├── go.sum ├── images ├── 1.svg ├── 10.svg ├── 11.svg ├── 12.svg ├── 13.svg ├── 14.svg ├── 15.svg ├── 16.svg ├── 17.svg ├── 18.svg ├── 19.svg ├── 2.svg ├── 20.svg ├── 3.svg ├── 4.svg ├── 5.svg ├── 6.svg ├── 7.svg ├── 8.svg ├── 9.svg ├── grid.svg ├── icon-missing.svg ├── task-empty.svg ├── task-multiple.svg └── task-single.svg ├── main.go └── tools.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: nwg-piotr 2 | liberapay: nwg 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - Linux distribution 28 | - go version 29 | - gtk-layer-shell version 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Binaries built with make 15 | bin 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | /.idea 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Piotr Miller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr 2 | DESTDIR ?= 3 | 4 | get: 5 | go get github.com/gotk3/gotk3 6 | go get github.com/gotk3/gotk3/gdk 7 | go get github.com/gotk3/gotk3/glib 8 | go get github.com/dlasky/gotk3-layershell/layershell 9 | go get github.com/joshuarubin/go-sway 10 | go get github.com/allan-simon/go-singleinstance 11 | go get "github.com/sirupsen/logrus" 12 | 13 | build: 14 | go build -v -o bin/nwg-dock . 15 | 16 | install: 17 | -pkill -f nwg-dock 18 | sleep 1 19 | mkdir -p $(DESTDIR)$(PREFIX)/share/nwg-dock 20 | mkdir -p $(DESTDIR)$(PREFIX)/bin 21 | cp -r images $(DESTDIR)$(PREFIX)/share/nwg-dock 22 | cp config/* $(DESTDIR)$(PREFIX)/share/nwg-dock 23 | cp bin/nwg-dock $(DESTDIR)$(PREFIX)/bin 24 | 25 | uninstall: 26 | rm -r $(DESTDIR)$(PREFIX)/share/nwg-dock 27 | rm $(DESTDIR)$(PREFIX)/bin/nwg-dock 28 | 29 | run: 30 | go run . 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nwg-dock 2 | 3 | This application is a part of the [nwg-shell](https://nwg-piotr.github.io/nwg-shell) project. 4 | 5 | **Contributing:** please read the [general contributing rules for the nwg-shell project](https://nwg-piotr.github.io/nwg-shell/contribution). 6 | 7 | For **Hyprland support** please use [nwg-dock-hyprland](https://github.com/nwg-piotr/nwg-dock-hyprland). 8 | 9 | Fully configurable (w/ command line arguments and css) dock, written in Go, aimed exclusively at [sway](https://github.com/swaywm/sway) Wayland compositor. It features pinned buttons, task buttons, the workspace switcher and the launcher button. The latter by default starts [nwg-drawer](https://github.com/nwg-piotr/nwg-drawer) or `nwggrid` (application grid) - if found. In the picture(s) below the dock has been shown together with [nwg-panel](https://github.com/nwg-piotr/nwg-panel). 10 | 11 | Screenshot 12 | 13 | [![Packaging status](https://repology.org/badge/vertical-allrepos/nwg-dock.svg)](https://repology.org/project/nwg-dock/versions) 14 | 15 | ## Installation 16 | 17 | ### Requirements 18 | 19 | - `go`>=1.20 (just to build) 20 | - `gtk3` 21 | - `gtk-layer-shell` 22 | - [nwg-drawer](https://github.com/nwg-piotr/nwg-drawer) or 23 | [nwg-launchers](https://github.com/nwg-piotr/nwg-launchers): optionally. You may use another launcher (see help), 24 | or none at all. The launcher button won't show up, if so. 25 | 26 | ### Steps 27 | 28 | 1. Clone the repository, cd into it. 29 | 2. Install golang libraries with `make get`. First time it may take ages, be patient. 30 | 3. `make build` 31 | 4. `sudo make install` 32 | 33 | ## Running 34 | 35 | Either start the dock permanently in the sway config file, 36 | 37 | ```text 38 | exec nwg-dock [arguments] 39 | ``` 40 | 41 | or assign the command to some key binding. Running the command again kills the existing program instance, so that 42 | you could use the same key to open and close the dock. 43 | 44 | ## Running the dock residently 45 | 46 | If you run the program with the `-d` or `-r` argument (preferably in autostart), it will be running residently. 47 | 48 | ```text 49 | exec_always nwg-dock -d 50 | ``` 51 | 52 | or 53 | 54 | ```text 55 | exec_always nwg-dock -r 56 | ``` 57 | 58 | ### `-d` for autohiDe 59 | 60 | Move the mouse pointer to expected dock location for the dock to show up. It will be hidden a second after you leave the window. Invisible hot spots will be created on all your outputs, unless you specify one with the `-o` argument. 61 | 62 | ### `-r` for just Resident 63 | 64 | No hotspot will be created. To show/hide the dock, bind the `exec nwg-dock` command to some key or button. 65 | How about the `Menu` key, which is usually useless? 66 | 67 | Re-execution of the same command hides the dock. If a resident instance found, the `nwg-dock` command w/o 68 | arguments sends `SIGUSR1` to it. Actually `pkill -USR1 nwg-dock` could be used instead. This also works in autohiDe 69 | mode. 70 | 71 | Re-execution of the command with the `-d` or `-r` argument won't kill the running instance. If the dock is 72 | running residently, another instance will just exit with 0 code. In case you'd like to terminate it anyway, you need to `pkill -f nwg-dock`. 73 | 74 | *NOTE: you need to kill the running instance before reloading sway, if you've just changed the arguments you 75 | auto-start the dock with.* 76 | 77 | ```txt 78 | $ nwg-dock -h 79 | Usage of nwg-dock: 80 | -a string 81 | Alignment in full width/height: "start", "center" or "end" (default "center") 82 | -c string 83 | Command assigned to the launcher button 84 | -d auto-hiDe: show dock when hotspot hovered, close when left or a button clicked 85 | -debug 86 | turn on debug messages 87 | -f take Full screen width/height 88 | -g string 89 | quote-delimited, space-separated app_id list to iGnore in the dock 90 | -hd int 91 | Hotspot Delay [ms]; the smaller, the faster mouse pointer needs to enter hotspot for the dock to appear; set 0 to disable (default 20) 92 | -i int 93 | Icon size (default 48) 94 | -l string 95 | Layer "overlay", "top" or "bottom" (default "overlay") 96 | -lp string 97 | Launcher button position, 'start' or 'end' (default "end") 98 | -mb int 99 | Margin Bottom 100 | -ml int 101 | Margin Left 102 | -mr int 103 | Margin Right 104 | -mt int 105 | Margin Top 106 | -nolauncher 107 | don't show the launcher button 108 | -nows 109 | don't show the workspace switcher 110 | -o string 111 | name of Output to display the dock on 112 | -p string 113 | Position: "bottom", "top" or "left" (default "bottom") 114 | -r Leave the program resident, but w/o hotspot 115 | -s string 116 | Styling: css file name (default "style.css") 117 | -v display Version information 118 | -w int 119 | number of Workspaces you use (default 8) 120 | -x set eXclusive zone: move other windows aside; overrides the "-l" argument 121 | ``` 122 | 123 | Screenshot
124 | 125 | ## Styling 126 | 127 | Edit `~/.config/nwg-dock/style.css` to your taste. 128 | 129 | ## Troubleshooting 130 | 131 | ### An application icon is not displayed 132 | 133 | The only thing the dock knows about the app is it's app_id. 134 | 135 | ```text 136 | $ swaymsg -t get_tree 137 | (...) 138 | #13: con "piotr@msi:~" (xdg_shell, pid: 8512, app_id: "foot") 139 | ``` 140 | 141 | Now it'll look for an icon named 'foot'. If that fails, it'll look for a .desktop file named 'foot.desktop', which should contain the icon name or path. If this fails as well, no icon will be displayed. I've added workarounds for some most common exceptions, but it's impossible to predict every single application misbehaviour. This is either programmers fault (improper class name), or bad packaging (.desktop file name different from the application class name). 142 | 143 | If some app has no icon in the dock: 144 | 145 | 1. check the app class name (`swaymsg -t get_tree`); 146 | 2. find the app's .desktop file; 147 | 3. copy it to ~/.local/share/applications/` and rename to .desktop. 148 | 149 | If the .desktop file contains proper icon definition (`Icon=`), it should work now. 150 | 151 | ## Credits 152 | 153 | This program uses some great libraries: 154 | 155 | - [gotk3](https://github.com/gotk3/gotk3) Copyright (c) 2013-2014 Conformal Systems LLC, 156 | Copyright (c) 2015-2018 gotk3 contributors 157 | - [gotk3-layershell](https://github.com/dlasky/gotk3-layershell) by [@dlasky](https://github.com/dlasky/gotk3-layershell/commits?author=dlasky) - many thanks for writing this software, and for patience with my requests! 158 | - [go-sway](https://github.com/joshuarubin/go-sway) Copyright (c) 2019 Joshua Rubin 159 | - [go-singleinstance](github.com/allan-simon/go-singleinstance) Copyright (c) 2015 Allan Simon 160 | -------------------------------------------------------------------------------- /config/hotspot.css: -------------------------------------------------------------------------------- 1 | window { 2 | background-color: rgba (0, 0, 0, 0); 3 | border: none 4 | } 5 | -------------------------------------------------------------------------------- /config/style.css: -------------------------------------------------------------------------------- 1 | window { 2 | /*background-color: rgba (0, 0, 0, 0.8);*/ 3 | background: #36364f; 4 | /*opacity: 0.9;*/ 5 | border-radius: 15px; 6 | border-style: none; 7 | border-width: 1px; 8 | border-color: rgba (156, 142, 122, 0.7) 9 | } 10 | 11 | #box { 12 | /* Define attributes of the box surrounding icons here */ 13 | padding: 10px 14 | } 15 | 16 | button, image { 17 | background: none; 18 | border-style: none; 19 | box-shadow: none; 20 | color: #999 21 | } 22 | 23 | button { 24 | /*padding-left: 4px; 25 | padding-right: 4px;*/ 26 | padding: 4px; 27 | margin-left: 4px; 28 | margin-right: 4px; 29 | color: #eee; 30 | font-size: 12px 31 | } 32 | 33 | button:hover { 34 | background-color: rgba (255, 255, 255, 0.15); 35 | border-radius: 2px; 36 | } 37 | 38 | button:focus { 39 | box-shadow: 0 0 2px; 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nwg-piotr/nwg-dock 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 7 | github.com/dlasky/gotk3-layershell v0.0.0-20240515133811-5c5115f0d774 8 | github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56 9 | github.com/joshuarubin/go-sway v1.2.0 10 | github.com/sirupsen/logrus v1.9.3 11 | ) 12 | 13 | require ( 14 | github.com/joshuarubin/lifecycle v1.1.4 // indirect 15 | go.uber.org/multierr v1.11.0 // indirect 16 | golang.org/x/sync v0.10.0 // indirect 17 | golang.org/x/sys v0.29.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA= 2 | github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/dlasky/gotk3-layershell v0.0.0-20240515133811-5c5115f0d774 h1:o87OVL4olQBlVwN3+NSVQpS6gj9FWUYtxOfHXWZigUE= 7 | github.com/dlasky/gotk3-layershell v0.0.0-20240515133811-5c5115f0d774/go.mod h1:JHLx2Wz4mAPVwn4PFhC69ydwyHP4A3wQvlg7HKVVc1U= 8 | github.com/gotk3/gotk3 v0.6.1/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= 9 | github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56 h1:eR+xxC8qqKuPMTucZqaklBxLIT7/4L7dzhlwKMrDbj8= 10 | github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= 11 | github.com/joshuarubin/go-sway v1.2.0 h1:t3eqW504//uj9PDwFf0+IVfkD+WoOGaDX5gYIe0BHyM= 12 | github.com/joshuarubin/go-sway v1.2.0/go.mod h1:qcDd6f25vJ0++wICwA1BainIcRC67p2Mb4lsrZ0k3/k= 13 | github.com/joshuarubin/lifecycle v1.0.0/go.mod h1:sRy++ATvR9Ee21tkRdFkQeywAWvDsue66V70K0Dnl54= 14 | github.com/joshuarubin/lifecycle v1.1.4 h1:9ZjvYSsWax9DC3Jpz6vGf/0KnU8FNMjh0/vJ3SpSBRQ= 15 | github.com/joshuarubin/lifecycle v1.1.4/go.mod h1:QqHrqwMPMA9dbJY3XgIyVLhzHMSGOFrcCAQ59bke1mo= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 19 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 22 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 23 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 25 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 26 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 27 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 28 | golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 29 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 30 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 31 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 32 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 33 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 34 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 35 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= 37 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 39 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 40 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 41 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 42 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 43 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 45 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 46 | -------------------------------------------------------------------------------- /images/1.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /images/10.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /images/11.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /images/12.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /images/13.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /images/14.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /images/15.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /images/16.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /images/17.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /images/18.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /images/19.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /images/2.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /images/20.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /images/3.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 71 | 75 | 76 | 80 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /images/4.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /images/5.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /images/6.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /images/7.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /images/8.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /images/9.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 60 | 66 | 70 | 74 | 78 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /images/grid.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 30 | 50 | 54 | 58 | 59 | -------------------------------------------------------------------------------- /images/icon-missing.svg: -------------------------------------------------------------------------------- 1 | 2 | 22 | 24 | 32 | 36 | 40 | 44 | 48 | 52 | 53 | 55 | 59 | 63 | 64 | 67 | 71 | 75 | 76 | 85 | 88 | 92 | 96 | 97 | 106 | 116 | 121 | 122 | 147 | 152 | 159 | 166 | 173 | 180 | 181 | 183 | 184 | 186 | image/svg+xml 187 | 189 | 190 | 191 | 192 | 193 | 198 | 201 | 204 | 209 | 213 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /images/task-empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | -------------------------------------------------------------------------------- /images/task-multiple.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 61 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /images/task-single.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | "syscall" 13 | "time" 14 | 15 | log "github.com/sirupsen/logrus" 16 | 17 | "github.com/allan-simon/go-singleinstance" 18 | "github.com/dlasky/gotk3-layershell/layershell" 19 | "github.com/gotk3/gotk3/gdk" 20 | "github.com/gotk3/gotk3/glib" 21 | "github.com/gotk3/gotk3/gtk" 22 | ) 23 | 24 | const version = "0.4.3" 25 | 26 | type WindowState int 27 | 28 | const ( 29 | WindowShow WindowState = iota 30 | WindowHide 31 | ) 32 | 33 | var ( 34 | appDirs []string 35 | dataHome string 36 | configDirectory string 37 | pinnedFile string 38 | pinned []string 39 | oldTasks []task 40 | mainBox *gtk.Box 41 | src glib.SourceHandle 42 | refreshMainBoxChannel chan struct{} = make(chan struct{}, 1) 43 | outerOrientation, innerOrientation gtk.Orientation 44 | widgetAnchor, menuAnchor gdk.Gravity 45 | imgSizeScaled int 46 | currentWsNum, targetWsNum int64 47 | win *gtk.Window 48 | windowStateChannel chan WindowState = make(chan WindowState, 1) 49 | detectorEnteredAt int64 50 | appIdsToIgnore []string 51 | ) 52 | 53 | // Flags 54 | var cssFileName = flag.String("s", "style.css", "Styling: css file name") 55 | var targetOutput = flag.String("o", "", "name of Output to display the dock on") 56 | var displayVersion = flag.Bool("v", false, "display Version information") 57 | var autohide = flag.Bool("d", false, "auto-hiDe: show dock when hotspot hovered, close when left or a button clicked") 58 | var full = flag.Bool("f", false, "take Full screen width/height") 59 | var ignoreAppIds = flag.String("g", "", "quote-delimited, space-separated app_id list to iGnore in the dock") 60 | var numWS = flag.Int64("w", 8, "number of Workspaces you use") 61 | var position = flag.String("p", "bottom", "Position: \"bottom\", \"top\" or \"left\"") 62 | var exclusive = flag.Bool("x", false, "set eXclusive zone: move other windows aside; overrides the \"-l\" argument") 63 | var imgSize = flag.Int("i", 48, "Icon size") 64 | var layer = flag.String("l", "overlay", "Layer \"overlay\", \"top\" or \"bottom\"") 65 | var launcherCmd = flag.String("c", "", "Command assigned to the launcher button") 66 | var launcherPos = flag.String("lp", "end", "Launcher button position, 'start' or 'end'") 67 | var alignment = flag.String("a", "center", "Alignment in full width/height: \"start\", \"center\" or \"end\"") 68 | var marginTop = flag.Int("mt", 0, "Margin Top") 69 | var marginLeft = flag.Int("ml", 0, "Margin Left") 70 | var marginRight = flag.Int("mr", 0, "Margin Right") 71 | var marginBottom = flag.Int("mb", 0, "Margin Bottom") 72 | var hotspotDelay = flag.Int64("hd", 20, "Hotspot Delay [ms]; the smaller, the faster mouse pointer needs to enter hotspot for the dock to appear; set 0 to disable") 73 | var noWs = flag.Bool("nows", false, "don't show the workspace switcher") 74 | var noLauncher = flag.Bool("nolauncher", false, "don't show the launcher button") 75 | var resident = flag.Bool("r", false, "Leave the program resident, but w/o hotspot") 76 | var debug = flag.Bool("debug", false, "turn on debug messages") 77 | 78 | func buildMainBox(tasks []task, vbox *gtk.Box) { 79 | mainBox.Destroy() 80 | mainBox, _ = gtk.BoxNew(innerOrientation, 0) 81 | 82 | if *alignment == "start" { 83 | vbox.PackStart(mainBox, false, true, 0) 84 | } else if *alignment == "end" { 85 | vbox.PackEnd(mainBox, false, true, 0) 86 | } else { 87 | vbox.PackStart(mainBox, true, false, 0) 88 | } 89 | 90 | var err error 91 | pinned, err = loadTextFile(pinnedFile) 92 | if err != nil { 93 | pinned = nil 94 | } 95 | 96 | var allItems []string 97 | for _, cntPin := range pinned { 98 | if !isIn(allItems, cntPin) { 99 | allItems = append(allItems, cntPin) 100 | } 101 | } 102 | for _, cntTask := range tasks { 103 | if !isIn(allItems, cntTask.ID) && !strings.Contains(*launcherCmd, cntTask.ID) { 104 | allItems = append(allItems, cntTask.ID) 105 | } 106 | } 107 | 108 | divider := 1 109 | if len(allItems) > 0 { 110 | divider = len(allItems) 111 | } 112 | 113 | // scale icons down when their number increases 114 | if *imgSize*6/(divider) < *imgSize { 115 | overflow := (len(allItems) - 6) / 3 116 | imgSizeScaled = *imgSize * 6 / (6 + overflow) 117 | } else { 118 | imgSizeScaled = *imgSize 119 | } 120 | 121 | if *launcherPos == "start" { 122 | button := launcherButton() 123 | if button != nil { 124 | mainBox.PackStart(button, false, false, 0) 125 | } 126 | } 127 | 128 | var alreadyAdded []string 129 | for _, pin := range pinned { 130 | if !inTasks(tasks, pin) { 131 | if !isIn(appIdsToIgnore, pin) { 132 | button := pinnedButton(pin) 133 | mainBox.PackStart(button, false, false, 0) 134 | } else { 135 | log.Debugf("Ignoring pin '%s'", pin) 136 | } 137 | } else { 138 | instances := taskInstances(pin, tasks) 139 | task := instances[0] 140 | if !isIn(appIdsToIgnore, task.ID) { 141 | if len(instances) == 1 { 142 | button := taskButton(task, instances) 143 | mainBox.PackStart(button, false, false, 0) 144 | } else if !isIn(alreadyAdded, task.ID) { 145 | button := taskButton(task, instances) 146 | mainBox.PackStart(button, false, false, 0) 147 | alreadyAdded = append(alreadyAdded, task.ID) 148 | taskMenu(task.ID, instances) 149 | } else { 150 | continue 151 | } 152 | } else { 153 | log.Debugf("Ignoring instance '%s'", task.ID) 154 | } 155 | } 156 | } 157 | 158 | alreadyAdded = nil 159 | for _, task := range tasks { 160 | if !inPinned(task.ID) { 161 | instances := taskInstances(task.ID, tasks) 162 | if !isIn(appIdsToIgnore, task.ID) { 163 | if len(instances) == 1 { 164 | button := taskButton(task, instances) 165 | mainBox.PackStart(button, false, false, 0) 166 | } else if !isIn(alreadyAdded, task.ID) { 167 | button := taskButton(task, instances) 168 | mainBox.PackStart(button, false, false, 0) 169 | alreadyAdded = append(alreadyAdded, task.ID) 170 | taskMenu(task.ID, instances) 171 | } else { 172 | continue 173 | } 174 | } 175 | } 176 | } 177 | 178 | if !*noWs { 179 | wsButton, e := gtk.ButtonNew() 180 | if e != nil { 181 | return 182 | } 183 | wsPixbuf, e := gdk.PixbufNewFromFileAtSize(filepath.Join(dataHome, fmt.Sprintf("nwg-dock/images/%v.svg", currentWsNum)), 184 | imgSizeScaled, imgSizeScaled) 185 | if e == nil { 186 | wsImage, _ := gtk.ImageNewFromPixbuf(wsPixbuf) 187 | wsButton.SetImage(wsImage) 188 | wsButton.SetAlwaysShowImage(true) 189 | wsButton.AddEvents(int(gdk.SCROLL_MASK)) 190 | 191 | wsUpdateChannel := getWorkspaceChangesChannel(context.Background()) 192 | go func() { 193 | for { 194 | activeWorkspace := <-wsUpdateChannel 195 | targetWsNum = activeWorkspace 196 | 197 | glib.TimeoutAdd(0, func() bool { 198 | wsPixbuf, e = gdk.PixbufNewFromFileAtSize(filepath.Join(dataHome, fmt.Sprintf("nwg-dock/images/%v.svg", activeWorkspace)), 199 | imgSizeScaled, imgSizeScaled) 200 | 201 | if e == nil { 202 | wsImage, _ = gtk.ImageNewFromPixbuf(wsPixbuf) 203 | wsButton.SetImage(wsImage) 204 | } else { 205 | log.Warnf("Unable set set workspace image: %v", activeWorkspace) 206 | } 207 | 208 | return false 209 | }) 210 | } 211 | }() 212 | 213 | wsButton.Connect("clicked", func() { 214 | focusWorkspace(targetWsNum) 215 | }) 216 | 217 | wsButton.Connect("enter-notify-event", cancelClose) 218 | 219 | wsButton.Connect("scroll-event", func(btn *gtk.Button, e *gdk.Event) bool { 220 | event := gdk.EventScrollNewFromEvent(e) 221 | if event.Direction() == gdk.SCROLL_UP { 222 | if targetWsNum < *numWS && targetWsNum < 20 { 223 | targetWsNum++ 224 | } else { 225 | targetWsNum = 1 226 | } 227 | 228 | wsUpdateChannel <- targetWsNum 229 | return true 230 | } else if event.Direction() == gdk.SCROLL_DOWN { 231 | if targetWsNum > 1 { 232 | targetWsNum-- 233 | } else { 234 | targetWsNum = *numWS 235 | } 236 | 237 | wsUpdateChannel <- targetWsNum 238 | return true 239 | } 240 | return false 241 | }) 242 | } 243 | if wsButton != nil { 244 | mainBox.PackStart(wsButton, false, false, 0) 245 | } 246 | } 247 | 248 | if *launcherPos == "end" { 249 | button := launcherButton() 250 | if button != nil { 251 | mainBox.PackStart(button, false, false, 0) 252 | } 253 | } 254 | 255 | mainBox.ShowAll() 256 | } 257 | 258 | func setupHotSpot(monitor gdk.Monitor, dockWindow *gtk.Window) gtk.Window { 259 | w, h := dockWindow.GetSize() 260 | win, _ := gtk.WindowNew(gtk.WINDOW_TOPLEVEL) 261 | 262 | layershell.InitForWindow(win) 263 | layershell.SetMonitor(win, &monitor) 264 | 265 | var box *gtk.Box 266 | if *position == "bottom" || *position == "top" { 267 | box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) 268 | } else { 269 | box, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) 270 | } 271 | win.Add(box) 272 | 273 | detectorBox, _ := gtk.EventBoxNew() 274 | _ = detectorBox.SetProperty("name", "detector-box") 275 | 276 | if *position == "bottom" { 277 | box.PackStart(detectorBox, false, false, 0) 278 | } else { 279 | box.PackEnd(detectorBox, false, false, 0) 280 | } 281 | 282 | detectorBox.Connect("enter-notify-event", func() { 283 | detectorEnteredAt = time.Now().UnixNano() / 1000000 284 | }) 285 | 286 | hotspotBox, _ := gtk.EventBoxNew() 287 | _ = hotspotBox.SetProperty("name", "hotspot-box") 288 | 289 | if *position == "bottom" { 290 | box.PackStart(hotspotBox, false, false, 0) 291 | } else { 292 | box.PackEnd(hotspotBox, false, false, 0) 293 | } 294 | 295 | hotspotBox.Connect("enter-notify-event", func() { 296 | hotspotEnteredAt := time.Now().UnixNano() / 1000000 297 | delay := hotspotEnteredAt - detectorEnteredAt 298 | layershell.SetMonitor(dockWindow, &monitor) 299 | if delay <= *hotspotDelay || *hotspotDelay == 0 { 300 | log.Debugf("Delay %v < %v ms, let's show the window!", delay, *hotspotDelay) 301 | dockWindow.Hide() 302 | dockWindow.Show() 303 | } else { 304 | log.Debugf("Delay %v > %v ms, don't show the window :/", delay, *hotspotDelay) 305 | } 306 | }) 307 | 308 | if *position == "bottom" || *position == "top" { 309 | detectorBox.SetSizeRequest(w, h/3) 310 | hotspotBox.SetSizeRequest(w, 2) 311 | if *position == "bottom" { 312 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_BOTTOM, true) 313 | } else { 314 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_TOP, true) 315 | } 316 | 317 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_LEFT, *full) 318 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_RIGHT, *full) 319 | } 320 | 321 | if *position == "left" { 322 | detectorBox.SetSizeRequest(w/3, h) 323 | hotspotBox.SetSizeRequest(2, h) 324 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_LEFT, true) 325 | 326 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_TOP, *full) 327 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_BOTTOM, *full) 328 | } 329 | 330 | layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_TOP) 331 | 332 | layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_TOP, *marginTop) 333 | layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_LEFT, *marginLeft) 334 | layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_RIGHT, *marginRight) 335 | layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_BOTTOM, *marginBottom) 336 | 337 | layershell.SetExclusiveZone(win, -1) 338 | 339 | return *win 340 | } 341 | 342 | func main() { 343 | flag.Parse() 344 | if *debug { 345 | log.SetLevel(log.DebugLevel) 346 | } 347 | 348 | if *autohide && *resident { 349 | log.Warn("autohiDe and Resident arguments are mutually exclusive, ignoring -d!") 350 | *autohide = false 351 | } 352 | 353 | if *displayVersion { 354 | fmt.Printf("nwg-dock version %s\n", version) 355 | os.Exit(0) 356 | } 357 | if *autohide { 358 | log.Info("Starting in autohiDe mode") 359 | } 360 | if *resident { 361 | log.Info("Starting in resident mode") 362 | } 363 | if *ignoreAppIds != "" { 364 | log.Infof("Ignoring app_ids: '%s'", *ignoreAppIds) 365 | appIdsToIgnore = strings.Split(*ignoreAppIds, " ") 366 | } 367 | 368 | // Gentle SIGTERM handler thanks to reiki4040 https://gist.github.com/reiki4040/be3705f307d3cd136e85 369 | // v0.2: we also need to support SIGUSR from now on. 370 | signalChan := make(chan os.Signal, 1) 371 | signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGUSR1) 372 | 373 | go func() { 374 | for { 375 | s := <-signalChan 376 | switch s { 377 | case syscall.SIGTERM: 378 | log.Info("SIGTERM received, bye bye!") 379 | gtk.MainQuit() 380 | case syscall.SIGUSR1: 381 | if *resident || *autohide { 382 | // As win.Show() called from inside a goroutine randomly crashes GTK, 383 | // let's just set e helper variable here. We'll be checking it with glib.TimeoutAdd. 384 | if !win.IsVisible() { 385 | log.Debug("SIGUSR1 received, showing the window") 386 | windowStateChannel <- WindowShow 387 | } else { 388 | log.Debug("SIGUSR1 received, hiding the window") 389 | windowStateChannel <- WindowHide 390 | } 391 | } else { 392 | log.Info("SIGUSR1 received, and I'm not resident, bye bye!") 393 | gtk.MainQuit() 394 | } 395 | default: 396 | log.Warn("Unknown signal") 397 | } 398 | } 399 | }() 400 | 401 | // Unless we are in autohide/resident mode, we probably want the same key/mouse binding to turn the dock off. 402 | // Since v0.2 we can't just send SIGKILL if running instance found. We'll send SIGUSR1 instead. 403 | // If it's running with `-r` or `-d` flag, it'll show the window. If not - it will die. 404 | 405 | // Use md5-hashed $USER name to create unique lock files for multiple users 406 | lockFilePath := fmt.Sprintf("%s/nwg-dock-%s.lock", tempDir(), md5Hash(os.Getenv("USER"))) 407 | lockFile, err := singleinstance.CreateLockFile(lockFilePath) 408 | if err != nil { 409 | pid, err := readTextFile(lockFilePath) 410 | if err == nil { 411 | i, err := strconv.Atoi(pid) 412 | if err == nil { 413 | if *autohide || *resident { 414 | log.Info("Running instance found, terminating...") 415 | } else { 416 | _ = syscall.Kill(i, syscall.SIGUSR1) 417 | log.Info("Sending SIGUSR1 to running instance and bye, bye!") 418 | } 419 | } 420 | } 421 | os.Exit(0) 422 | } 423 | defer lockFile.Close() 424 | 425 | if !*noLauncher && *launcherCmd == "" { 426 | if isCommand("nwg-drawer") { 427 | *launcherCmd = "nwg-drawer" 428 | } else if isCommand("nwggrid") { 429 | *launcherCmd = "nwggrid -p" 430 | } 431 | 432 | if *launcherCmd != "" { 433 | log.Infof("Using auto-detected launcher command: '%s'", *launcherCmd) 434 | } else { 435 | log.Info("Neither 'nwg-drawer' nor 'nwggrid' command found, and no other launcher specified; hiding the launcher button.") 436 | } 437 | } 438 | 439 | dataHome = getDataHome() 440 | configDirectory = configDir() 441 | // if it doesn't exist: 442 | createDir(configDirectory) 443 | 444 | if !pathExists(fmt.Sprintf("%s/style.css", configDirectory)) { 445 | err := copyFile(filepath.Join(dataHome, "nwg-dock/style.css"), fmt.Sprintf("%s/style.css", configDirectory)) 446 | if err != nil { 447 | log.Warnf("Error copying file: %s", err) 448 | } 449 | } 450 | 451 | cacheDirectory := cacheDir() 452 | if cacheDirectory == "" { 453 | log.Panic("Couldn't determine cache directory location") 454 | } 455 | pinnedFile = filepath.Join(cacheDirectory, "nwg-dock-pinned") 456 | cssFile := filepath.Join(configDirectory, *cssFileName) 457 | 458 | appDirs = getAppDirs() 459 | 460 | gtk.Init(nil) 461 | 462 | cssProvider, _ := gtk.CssProviderNew() 463 | 464 | err = cssProvider.LoadFromPath(cssFile) 465 | if err != nil { 466 | log.Warnf("%s file not found, using GTK styling\n", cssFile) 467 | } else { 468 | log.Printf("Using style: %s\n", cssFile) 469 | screen, _ := gdk.ScreenGetDefault() 470 | gtk.AddProviderForScreen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 471 | } 472 | 473 | win, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL) 474 | if err != nil { 475 | log.Fatal("Unable to create window:", err) 476 | } 477 | 478 | layershell.InitForWindow(win) 479 | 480 | var output2mon map[string]*gdk.Monitor 481 | if *targetOutput != "" { 482 | // We want to assign layershell to a monitor, but we only know the output name! 483 | output2mon, err = mapOutputs() 484 | if err == nil { 485 | layershell.SetMonitor(win, output2mon[*targetOutput]) 486 | } else { 487 | log.Warn(fmt.Sprintf("%s", err)) 488 | } 489 | } 490 | 491 | if *exclusive { 492 | layershell.AutoExclusiveZoneEnable(win) 493 | *layer = "top" 494 | } 495 | 496 | if *position == "bottom" || *position == "top" { 497 | if *position == "bottom" { 498 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_BOTTOM, true) 499 | 500 | widgetAnchor = gdk.GDK_GRAVITY_NORTH 501 | menuAnchor = gdk.GDK_GRAVITY_SOUTH 502 | } else { 503 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_TOP, true) 504 | 505 | widgetAnchor = gdk.GDK_GRAVITY_SOUTH 506 | menuAnchor = gdk.GDK_GRAVITY_NORTH 507 | } 508 | 509 | outerOrientation = gtk.ORIENTATION_VERTICAL 510 | innerOrientation = gtk.ORIENTATION_HORIZONTAL 511 | 512 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_LEFT, *full) 513 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_RIGHT, *full) 514 | } 515 | 516 | if *position == "left" { 517 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_LEFT, true) 518 | 519 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_TOP, *full) 520 | layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_BOTTOM, *full) 521 | 522 | outerOrientation = gtk.ORIENTATION_HORIZONTAL 523 | innerOrientation = gtk.ORIENTATION_VERTICAL 524 | 525 | widgetAnchor = gdk.GDK_GRAVITY_EAST 526 | menuAnchor = gdk.GDK_GRAVITY_WEST 527 | } 528 | 529 | if *layer == "top" { 530 | layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_TOP) 531 | } else if *layer == "bottom" { 532 | layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_BOTTOM) 533 | } else { 534 | layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_OVERLAY) 535 | layershell.SetExclusiveZone(win, -1) 536 | } 537 | 538 | layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_TOP, *marginTop) 539 | layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_LEFT, *marginLeft) 540 | layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_RIGHT, *marginRight) 541 | layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_BOTTOM, *marginBottom) 542 | 543 | win.Connect("destroy", func() { 544 | gtk.MainQuit() 545 | }) 546 | 547 | // Close the window on leave, but not immediately, to avoid accidental closes 548 | win.Connect("leave-notify-event", func() { 549 | if *autohide { 550 | src = glib.TimeoutAdd(uint(1000), func() bool { 551 | win.Hide() 552 | src = 0 553 | return false 554 | }) 555 | } 556 | }) 557 | 558 | win.Connect("enter-notify-event", func() { 559 | cancelClose() 560 | }) 561 | 562 | outerBox, _ := gtk.BoxNew(outerOrientation, 0) 563 | _ = outerBox.SetProperty("name", "box") 564 | win.Add(outerBox) 565 | 566 | alignmentBox, _ := gtk.BoxNew(innerOrientation, 0) 567 | outerBox.PackStart(alignmentBox, true, true, 0) 568 | 569 | mainBox, _ = gtk.BoxNew(innerOrientation, 0) 570 | // We'll pack mainBox later, in buildMainBox 571 | 572 | tasks, err := listTasks() 573 | if err != nil { 574 | log.Fatal("Couldn't list tasks:", err) 575 | } 576 | oldTasks = tasks 577 | var oldWsNum int64 578 | 579 | buildMainBox(tasks, alignmentBox) 580 | 581 | refreshMainBox := func(currentTasks []task, forceRefresh bool) { 582 | if forceRefresh || (len(currentTasks) != len(oldTasks) || currentWsNum != oldWsNum) { 583 | glib.TimeoutAdd(0, func() bool { 584 | log.Debug("refreshing...") 585 | buildMainBox(currentTasks, alignmentBox) 586 | oldTasks = currentTasks 587 | oldWsNum = currentWsNum 588 | targetWsNum = currentWsNum 589 | return false 590 | }) 591 | } 592 | } 593 | 594 | go func() { 595 | ctx, cancel := context.WithCancel(context.Background()) 596 | defer cancel() 597 | 598 | taskChannel, err := getTaskChangesChannel(ctx) 599 | if err != nil { 600 | log.Fatal("Unable to process sway tasks:", err) 601 | } 602 | 603 | for { 604 | select { 605 | 606 | // Refresh if any pin/unpin action happened 607 | case <-refreshMainBoxChannel: 608 | currentTasks, err := listTasks() 609 | if err != nil { 610 | log.Fatal("Unable to retrieve task list from sway!") 611 | } 612 | 613 | refreshMainBox(currentTasks, true) 614 | 615 | // Refresh if the state of the workspace changes 616 | case currentTasks := <-taskChannel: 617 | refreshMainBox(currentTasks, false) 618 | 619 | } 620 | } 621 | }() 622 | 623 | win.ShowAll() 624 | 625 | if *autohide { 626 | glib.TimeoutAdd(uint(500), win.Hide) 627 | 628 | mRefProvider, _ := gtk.CssProviderNew() 629 | if err := mRefProvider.LoadFromPath(filepath.Join(dataHome, "nwg-dock/hotspot.css")); err != nil { 630 | log.Errorf("%s", err) 631 | } 632 | 633 | if *targetOutput == "" { 634 | // hot spots on all displays 635 | monitors, _ := listMonitors() 636 | for _, monitor := range monitors { 637 | win := setupHotSpot(monitor, win) 638 | 639 | ctx, _ := win.GetStyleContext() 640 | ctx.AddProvider(mRefProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 641 | 642 | win.ShowAll() 643 | } 644 | } else { 645 | // hot spot on the selected display only 646 | monitor := output2mon[*targetOutput] 647 | win := setupHotSpot(*monitor, win) 648 | 649 | ctx, _ := win.GetStyleContext() 650 | ctx.AddProvider(mRefProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 651 | 652 | win.ShowAll() 653 | } 654 | } 655 | 656 | go func() { 657 | for { 658 | windowState := <-windowStateChannel 659 | 660 | glib.TimeoutAdd(0, func() bool { 661 | if windowState == WindowShow && win != nil && !win.IsVisible() { 662 | win.ShowAll() 663 | } 664 | if windowState == WindowHide && win != nil && win.IsVisible() { 665 | win.Hide() 666 | } 667 | 668 | return false 669 | }) 670 | } 671 | }() 672 | 673 | gtk.Main() 674 | } 675 | --------------------------------------------------------------------------------