pax_global_header 0000666 0000000 0000000 00000000064 14740075321 0014515 g ustar 00root root 0000000 0000000 52 comment=8ecd84c65ff832702f0fc8543a691406bc653c8a
nwg-piotr-nwg-dock-8ecd84c/ 0000775 0000000 0000000 00000000000 14740075321 0015656 5 ustar 00root root 0000000 0000000 nwg-piotr-nwg-dock-8ecd84c/.github/ 0000775 0000000 0000000 00000000000 14740075321 0017216 5 ustar 00root root 0000000 0000000 nwg-piotr-nwg-dock-8ecd84c/.github/FUNDING.yml 0000664 0000000 0000000 00000000041 14740075321 0021026 0 ustar 00root root 0000000 0000000 github: nwg-piotr
liberapay: nwg
nwg-piotr-nwg-dock-8ecd84c/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14740075321 0021401 5 ustar 00root root 0000000 0000000 nwg-piotr-nwg-dock-8ecd84c/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000001220 14740075321 0024066 0 ustar 00root root 0000000 0000000 ---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- Linux distribution
- go version
- gtk-layer-shell version
**Additional context**
Add any other context about the problem here.
nwg-piotr-nwg-dock-8ecd84c/.gitignore 0000664 0000000 0000000 00000000465 14740075321 0017653 0 ustar 00root root 0000000 0000000 # Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Binaries built with make
bin
# Dependency directories (remove the comment below to include it)
# vendor/
/.idea
nwg-piotr-nwg-dock-8ecd84c/LICENSE 0000664 0000000 0000000 00000002055 14740075321 0016665 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2021 Piotr Miller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
nwg-piotr-nwg-dock-8ecd84c/Makefile 0000664 0000000 0000000 00000001313 14740075321 0017314 0 ustar 00root root 0000000 0000000 PREFIX ?= /usr
DESTDIR ?=
get:
go get github.com/gotk3/gotk3
go get github.com/gotk3/gotk3/gdk
go get github.com/gotk3/gotk3/glib
go get github.com/dlasky/gotk3-layershell/layershell
go get github.com/joshuarubin/go-sway
go get github.com/allan-simon/go-singleinstance
go get "github.com/sirupsen/logrus"
build:
go build -v -o bin/nwg-dock .
install:
-pkill -f nwg-dock
sleep 1
mkdir -p $(DESTDIR)$(PREFIX)/share/nwg-dock
mkdir -p $(DESTDIR)$(PREFIX)/bin
cp -r images $(DESTDIR)$(PREFIX)/share/nwg-dock
cp config/* $(DESTDIR)$(PREFIX)/share/nwg-dock
cp bin/nwg-dock $(DESTDIR)$(PREFIX)/bin
uninstall:
rm -r $(DESTDIR)$(PREFIX)/share/nwg-dock
rm $(DESTDIR)$(PREFIX)/bin/nwg-dock
run:
go run .
nwg-piotr-nwg-dock-8ecd84c/README.md 0000664 0000000 0000000 00000014306 14740075321 0017141 0 ustar 00root root 0000000 0000000 # nwg-dock
This application is a part of the [nwg-shell](https://nwg-piotr.github.io/nwg-shell) project.
**Contributing:** please read the [general contributing rules for the nwg-shell project](https://nwg-piotr.github.io/nwg-shell/contribution).
For **Hyprland support** please use [nwg-dock-hyprland](https://github.com/nwg-piotr/nwg-dock-hyprland).
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).
[](https://repology.org/project/nwg-dock/versions)
## Installation
### Requirements
- `go`>=1.20 (just to build)
- `gtk3`
- `gtk-layer-shell`
- [nwg-drawer](https://github.com/nwg-piotr/nwg-drawer) or
[nwg-launchers](https://github.com/nwg-piotr/nwg-launchers): optionally. You may use another launcher (see help),
or none at all. The launcher button won't show up, if so.
### Steps
1. Clone the repository, cd into it.
2. Install golang libraries with `make get`. First time it may take ages, be patient.
3. `make build`
4. `sudo make install`
## Running
Either start the dock permanently in the sway config file,
```text
exec nwg-dock [arguments]
```
or assign the command to some key binding. Running the command again kills the existing program instance, so that
you could use the same key to open and close the dock.
## Running the dock residently
If you run the program with the `-d` or `-r` argument (preferably in autostart), it will be running residently.
```text
exec_always nwg-dock -d
```
or
```text
exec_always nwg-dock -r
```
### `-d` for autohiDe
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.
### `-r` for just Resident
No hotspot will be created. To show/hide the dock, bind the `exec nwg-dock` command to some key or button.
How about the `Menu` key, which is usually useless?
Re-execution of the same command hides the dock. If a resident instance found, the `nwg-dock` command w/o
arguments sends `SIGUSR1` to it. Actually `pkill -USR1 nwg-dock` could be used instead. This also works in autohiDe
mode.
Re-execution of the command with the `-d` or `-r` argument won't kill the running instance. If the dock is
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`.
*NOTE: you need to kill the running instance before reloading sway, if you've just changed the arguments you
auto-start the dock with.*
```txt
$ nwg-dock -h
Usage of nwg-dock:
-a string
Alignment in full width/height: "start", "center" or "end" (default "center")
-c string
Command assigned to the launcher button
-d auto-hiDe: show dock when hotspot hovered, close when left or a button clicked
-debug
turn on debug messages
-f take Full screen width/height
-g string
quote-delimited, space-separated app_id list to iGnore in the dock
-hd int
Hotspot Delay [ms]; the smaller, the faster mouse pointer needs to enter hotspot for the dock to appear; set 0 to disable (default 20)
-i int
Icon size (default 48)
-l string
Layer "overlay", "top" or "bottom" (default "overlay")
-lp string
Launcher button position, 'start' or 'end' (default "end")
-mb int
Margin Bottom
-ml int
Margin Left
-mr int
Margin Right
-mt int
Margin Top
-nolauncher
don't show the launcher button
-nows
don't show the workspace switcher
-o string
name of Output to display the dock on
-p string
Position: "bottom", "top" or "left" (default "bottom")
-r Leave the program resident, but w/o hotspot
-s string
Styling: css file name (default "style.css")
-v display Version information
-w int
number of Workspaces you use (default 8)
-x set eXclusive zone: move other windows aside; overrides the "-l" argument
```

## Styling
Edit `~/.config/nwg-dock/style.css` to your taste.
## Troubleshooting
### An application icon is not displayed
The only thing the dock knows about the app is it's app_id.
```text
$ swaymsg -t get_tree
(...)
#13: con "piotr@msi:~" (xdg_shell, pid: 8512, app_id: "foot")
```
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).
If some app has no icon in the dock:
1. check the app class name (`swaymsg -t get_tree`);
2. find the app's .desktop file;
3. copy it to ~/.local/share/applications/` and rename to .desktop.
If the .desktop file contains proper icon definition (`Icon=`), it should work now.
## Credits
This program uses some great libraries:
- [gotk3](https://github.com/gotk3/gotk3) Copyright (c) 2013-2014 Conformal Systems LLC,
Copyright (c) 2015-2018 gotk3 contributors
- [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!
- [go-sway](https://github.com/joshuarubin/go-sway) Copyright (c) 2019 Joshua Rubin
- [go-singleinstance](github.com/allan-simon/go-singleinstance) Copyright (c) 2015 Allan Simon
nwg-piotr-nwg-dock-8ecd84c/config/ 0000775 0000000 0000000 00000000000 14740075321 0017123 5 ustar 00root root 0000000 0000000 nwg-piotr-nwg-dock-8ecd84c/config/hotspot.css 0000664 0000000 0000000 00000000101 14740075321 0021325 0 ustar 00root root 0000000 0000000 window {
background-color: rgba (0, 0, 0, 0);
border: none
}
nwg-piotr-nwg-dock-8ecd84c/config/style.css 0000664 0000000 0000000 00000001211 14740075321 0020770 0 ustar 00root root 0000000 0000000 window {
/*background-color: rgba (0, 0, 0, 0.8);*/
background: #36364f;
/*opacity: 0.9;*/
border-radius: 15px;
border-style: none;
border-width: 1px;
border-color: rgba (156, 142, 122, 0.7)
}
#box {
/* Define attributes of the box surrounding icons here */
padding: 10px
}
button, image {
background: none;
border-style: none;
box-shadow: none;
color: #999
}
button {
/*padding-left: 4px;
padding-right: 4px;*/
padding: 4px;
margin-left: 4px;
margin-right: 4px;
color: #eee;
font-size: 12px
}
button:hover {
background-color: rgba (255, 255, 255, 0.15);
border-radius: 2px;
}
button:focus {
box-shadow: 0 0 2px;
}
nwg-piotr-nwg-dock-8ecd84c/go.mod 0000664 0000000 0000000 00000001017 14740075321 0016763 0 ustar 00root root 0000000 0000000 module github.com/nwg-piotr/nwg-dock
go 1.23
require (
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
github.com/dlasky/gotk3-layershell v0.0.0-20240515133811-5c5115f0d774
github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56
github.com/joshuarubin/go-sway v1.2.0
github.com/sirupsen/logrus v1.9.3
)
require (
github.com/joshuarubin/lifecycle v1.1.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
)
nwg-piotr-nwg-dock-8ecd84c/go.sum 0000664 0000000 0000000 00000007717 14740075321 0017025 0 ustar 00root root 0000000 0000000 github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlasky/gotk3-layershell v0.0.0-20240515133811-5c5115f0d774 h1:o87OVL4olQBlVwN3+NSVQpS6gj9FWUYtxOfHXWZigUE=
github.com/dlasky/gotk3-layershell v0.0.0-20240515133811-5c5115f0d774/go.mod h1:JHLx2Wz4mAPVwn4PFhC69ydwyHP4A3wQvlg7HKVVc1U=
github.com/gotk3/gotk3 v0.6.1/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56 h1:eR+xxC8qqKuPMTucZqaklBxLIT7/4L7dzhlwKMrDbj8=
github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/joshuarubin/go-sway v1.2.0 h1:t3eqW504//uj9PDwFf0+IVfkD+WoOGaDX5gYIe0BHyM=
github.com/joshuarubin/go-sway v1.2.0/go.mod h1:qcDd6f25vJ0++wICwA1BainIcRC67p2Mb4lsrZ0k3/k=
github.com/joshuarubin/lifecycle v1.0.0/go.mod h1:sRy++ATvR9Ee21tkRdFkQeywAWvDsue66V70K0Dnl54=
github.com/joshuarubin/lifecycle v1.1.4 h1:9ZjvYSsWax9DC3Jpz6vGf/0KnU8FNMjh0/vJ3SpSBRQ=
github.com/joshuarubin/lifecycle v1.1.4/go.mod h1:QqHrqwMPMA9dbJY3XgIyVLhzHMSGOFrcCAQ59bke1mo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nwg-piotr-nwg-dock-8ecd84c/images/ 0000775 0000000 0000000 00000000000 14740075321 0017123 5 ustar 00root root 0000000 0000000 nwg-piotr-nwg-dock-8ecd84c/images/1.svg 0000664 0000000 0000000 00000017046 14740075321 0020014 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/10.svg 0000664 0000000 0000000 00000020272 14740075321 0020067 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/11.svg 0000664 0000000 0000000 00000017473 14740075321 0020101 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/12.svg 0000664 0000000 0000000 00000020370 14740075321 0020070 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/13.svg 0000664 0000000 0000000 00000020752 14740075321 0020075 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/14.svg 0000664 0000000 0000000 00000017530 14740075321 0020076 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/15.svg 0000664 0000000 0000000 00000020351 14740075321 0020072 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/16.svg 0000664 0000000 0000000 00000020715 14740075321 0020077 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/17.svg 0000664 0000000 0000000 00000017423 14740075321 0020102 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/18.svg 0000664 0000000 0000000 00000021311 14740075321 0020072 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/19.svg 0000664 0000000 0000000 00000020715 14740075321 0020102 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/2.svg 0000664 0000000 0000000 00000017750 14740075321 0020017 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/20.svg 0000664 0000000 0000000 00000021173 14740075321 0020071 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/3.svg 0000664 0000000 0000000 00000020305 14740075321 0020006 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/4.svg 0000664 0000000 0000000 00000017105 14740075321 0020013 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/5.svg 0000664 0000000 0000000 00000017727 14740075321 0020026 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/6.svg 0000664 0000000 0000000 00000020271 14740075321 0020013 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/7.svg 0000664 0000000 0000000 00000016777 14740075321 0020034 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/8.svg 0000664 0000000 0000000 00000020671 14740075321 0020021 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/9.svg 0000664 0000000 0000000 00000020275 14740075321 0020022 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/grid.svg 0000664 0000000 0000000 00000010111 14740075321 0020563 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/icon-missing.svg 0000664 0000000 0000000 00000015424 14740075321 0022251 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/task-empty.svg 0000664 0000000 0000000 00000003074 14740075321 0021746 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/task-multiple.svg 0000664 0000000 0000000 00000003710 14740075321 0022440 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/images/task-single.svg 0000664 0000000 0000000 00000003376 14740075321 0022076 0 ustar 00root root 0000000 0000000
nwg-piotr-nwg-dock-8ecd84c/main.go 0000664 0000000 0000000 00000045563 14740075321 0017146 0 ustar 00root root 0000000 0000000 package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
log "github.com/sirupsen/logrus"
"github.com/allan-simon/go-singleinstance"
"github.com/dlasky/gotk3-layershell/layershell"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
)
const version = "0.4.3"
type WindowState int
const (
WindowShow WindowState = iota
WindowHide
)
var (
appDirs []string
dataHome string
configDirectory string
pinnedFile string
pinned []string
oldTasks []task
mainBox *gtk.Box
src glib.SourceHandle
refreshMainBoxChannel chan struct{} = make(chan struct{}, 1)
outerOrientation, innerOrientation gtk.Orientation
widgetAnchor, menuAnchor gdk.Gravity
imgSizeScaled int
currentWsNum, targetWsNum int64
win *gtk.Window
windowStateChannel chan WindowState = make(chan WindowState, 1)
detectorEnteredAt int64
appIdsToIgnore []string
)
// Flags
var cssFileName = flag.String("s", "style.css", "Styling: css file name")
var targetOutput = flag.String("o", "", "name of Output to display the dock on")
var displayVersion = flag.Bool("v", false, "display Version information")
var autohide = flag.Bool("d", false, "auto-hiDe: show dock when hotspot hovered, close when left or a button clicked")
var full = flag.Bool("f", false, "take Full screen width/height")
var ignoreAppIds = flag.String("g", "", "quote-delimited, space-separated app_id list to iGnore in the dock")
var numWS = flag.Int64("w", 8, "number of Workspaces you use")
var position = flag.String("p", "bottom", "Position: \"bottom\", \"top\" or \"left\"")
var exclusive = flag.Bool("x", false, "set eXclusive zone: move other windows aside; overrides the \"-l\" argument")
var imgSize = flag.Int("i", 48, "Icon size")
var layer = flag.String("l", "overlay", "Layer \"overlay\", \"top\" or \"bottom\"")
var launcherCmd = flag.String("c", "", "Command assigned to the launcher button")
var launcherPos = flag.String("lp", "end", "Launcher button position, 'start' or 'end'")
var alignment = flag.String("a", "center", "Alignment in full width/height: \"start\", \"center\" or \"end\"")
var marginTop = flag.Int("mt", 0, "Margin Top")
var marginLeft = flag.Int("ml", 0, "Margin Left")
var marginRight = flag.Int("mr", 0, "Margin Right")
var marginBottom = flag.Int("mb", 0, "Margin Bottom")
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")
var noWs = flag.Bool("nows", false, "don't show the workspace switcher")
var noLauncher = flag.Bool("nolauncher", false, "don't show the launcher button")
var resident = flag.Bool("r", false, "Leave the program resident, but w/o hotspot")
var debug = flag.Bool("debug", false, "turn on debug messages")
func buildMainBox(tasks []task, vbox *gtk.Box) {
mainBox.Destroy()
mainBox, _ = gtk.BoxNew(innerOrientation, 0)
if *alignment == "start" {
vbox.PackStart(mainBox, false, true, 0)
} else if *alignment == "end" {
vbox.PackEnd(mainBox, false, true, 0)
} else {
vbox.PackStart(mainBox, true, false, 0)
}
var err error
pinned, err = loadTextFile(pinnedFile)
if err != nil {
pinned = nil
}
var allItems []string
for _, cntPin := range pinned {
if !isIn(allItems, cntPin) {
allItems = append(allItems, cntPin)
}
}
for _, cntTask := range tasks {
if !isIn(allItems, cntTask.ID) && !strings.Contains(*launcherCmd, cntTask.ID) {
allItems = append(allItems, cntTask.ID)
}
}
divider := 1
if len(allItems) > 0 {
divider = len(allItems)
}
// scale icons down when their number increases
if *imgSize*6/(divider) < *imgSize {
overflow := (len(allItems) - 6) / 3
imgSizeScaled = *imgSize * 6 / (6 + overflow)
} else {
imgSizeScaled = *imgSize
}
if *launcherPos == "start" {
button := launcherButton()
if button != nil {
mainBox.PackStart(button, false, false, 0)
}
}
var alreadyAdded []string
for _, pin := range pinned {
if !inTasks(tasks, pin) {
if !isIn(appIdsToIgnore, pin) {
button := pinnedButton(pin)
mainBox.PackStart(button, false, false, 0)
} else {
log.Debugf("Ignoring pin '%s'", pin)
}
} else {
instances := taskInstances(pin, tasks)
task := instances[0]
if !isIn(appIdsToIgnore, task.ID) {
if len(instances) == 1 {
button := taskButton(task, instances)
mainBox.PackStart(button, false, false, 0)
} else if !isIn(alreadyAdded, task.ID) {
button := taskButton(task, instances)
mainBox.PackStart(button, false, false, 0)
alreadyAdded = append(alreadyAdded, task.ID)
taskMenu(task.ID, instances)
} else {
continue
}
} else {
log.Debugf("Ignoring instance '%s'", task.ID)
}
}
}
alreadyAdded = nil
for _, task := range tasks {
if !inPinned(task.ID) {
instances := taskInstances(task.ID, tasks)
if !isIn(appIdsToIgnore, task.ID) {
if len(instances) == 1 {
button := taskButton(task, instances)
mainBox.PackStart(button, false, false, 0)
} else if !isIn(alreadyAdded, task.ID) {
button := taskButton(task, instances)
mainBox.PackStart(button, false, false, 0)
alreadyAdded = append(alreadyAdded, task.ID)
taskMenu(task.ID, instances)
} else {
continue
}
}
}
}
if !*noWs {
wsButton, e := gtk.ButtonNew()
if e != nil {
return
}
wsPixbuf, e := gdk.PixbufNewFromFileAtSize(filepath.Join(dataHome, fmt.Sprintf("nwg-dock/images/%v.svg", currentWsNum)),
imgSizeScaled, imgSizeScaled)
if e == nil {
wsImage, _ := gtk.ImageNewFromPixbuf(wsPixbuf)
wsButton.SetImage(wsImage)
wsButton.SetAlwaysShowImage(true)
wsButton.AddEvents(int(gdk.SCROLL_MASK))
wsUpdateChannel := getWorkspaceChangesChannel(context.Background())
go func() {
for {
activeWorkspace := <-wsUpdateChannel
targetWsNum = activeWorkspace
glib.TimeoutAdd(0, func() bool {
wsPixbuf, e = gdk.PixbufNewFromFileAtSize(filepath.Join(dataHome, fmt.Sprintf("nwg-dock/images/%v.svg", activeWorkspace)),
imgSizeScaled, imgSizeScaled)
if e == nil {
wsImage, _ = gtk.ImageNewFromPixbuf(wsPixbuf)
wsButton.SetImage(wsImage)
} else {
log.Warnf("Unable set set workspace image: %v", activeWorkspace)
}
return false
})
}
}()
wsButton.Connect("clicked", func() {
focusWorkspace(targetWsNum)
})
wsButton.Connect("enter-notify-event", cancelClose)
wsButton.Connect("scroll-event", func(btn *gtk.Button, e *gdk.Event) bool {
event := gdk.EventScrollNewFromEvent(e)
if event.Direction() == gdk.SCROLL_UP {
if targetWsNum < *numWS && targetWsNum < 20 {
targetWsNum++
} else {
targetWsNum = 1
}
wsUpdateChannel <- targetWsNum
return true
} else if event.Direction() == gdk.SCROLL_DOWN {
if targetWsNum > 1 {
targetWsNum--
} else {
targetWsNum = *numWS
}
wsUpdateChannel <- targetWsNum
return true
}
return false
})
}
if wsButton != nil {
mainBox.PackStart(wsButton, false, false, 0)
}
}
if *launcherPos == "end" {
button := launcherButton()
if button != nil {
mainBox.PackStart(button, false, false, 0)
}
}
mainBox.ShowAll()
}
func setupHotSpot(monitor gdk.Monitor, dockWindow *gtk.Window) gtk.Window {
w, h := dockWindow.GetSize()
win, _ := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
layershell.InitForWindow(win)
layershell.SetMonitor(win, &monitor)
var box *gtk.Box
if *position == "bottom" || *position == "top" {
box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
} else {
box, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
}
win.Add(box)
detectorBox, _ := gtk.EventBoxNew()
_ = detectorBox.SetProperty("name", "detector-box")
if *position == "bottom" {
box.PackStart(detectorBox, false, false, 0)
} else {
box.PackEnd(detectorBox, false, false, 0)
}
detectorBox.Connect("enter-notify-event", func() {
detectorEnteredAt = time.Now().UnixNano() / 1000000
})
hotspotBox, _ := gtk.EventBoxNew()
_ = hotspotBox.SetProperty("name", "hotspot-box")
if *position == "bottom" {
box.PackStart(hotspotBox, false, false, 0)
} else {
box.PackEnd(hotspotBox, false, false, 0)
}
hotspotBox.Connect("enter-notify-event", func() {
hotspotEnteredAt := time.Now().UnixNano() / 1000000
delay := hotspotEnteredAt - detectorEnteredAt
layershell.SetMonitor(dockWindow, &monitor)
if delay <= *hotspotDelay || *hotspotDelay == 0 {
log.Debugf("Delay %v < %v ms, let's show the window!", delay, *hotspotDelay)
dockWindow.Hide()
dockWindow.Show()
} else {
log.Debugf("Delay %v > %v ms, don't show the window :/", delay, *hotspotDelay)
}
})
if *position == "bottom" || *position == "top" {
detectorBox.SetSizeRequest(w, h/3)
hotspotBox.SetSizeRequest(w, 2)
if *position == "bottom" {
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_BOTTOM, true)
} else {
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_TOP, true)
}
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_LEFT, *full)
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_RIGHT, *full)
}
if *position == "left" {
detectorBox.SetSizeRequest(w/3, h)
hotspotBox.SetSizeRequest(2, h)
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_LEFT, true)
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_TOP, *full)
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_BOTTOM, *full)
}
layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_TOP)
layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_TOP, *marginTop)
layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_LEFT, *marginLeft)
layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_RIGHT, *marginRight)
layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_BOTTOM, *marginBottom)
layershell.SetExclusiveZone(win, -1)
return *win
}
func main() {
flag.Parse()
if *debug {
log.SetLevel(log.DebugLevel)
}
if *autohide && *resident {
log.Warn("autohiDe and Resident arguments are mutually exclusive, ignoring -d!")
*autohide = false
}
if *displayVersion {
fmt.Printf("nwg-dock version %s\n", version)
os.Exit(0)
}
if *autohide {
log.Info("Starting in autohiDe mode")
}
if *resident {
log.Info("Starting in resident mode")
}
if *ignoreAppIds != "" {
log.Infof("Ignoring app_ids: '%s'", *ignoreAppIds)
appIdsToIgnore = strings.Split(*ignoreAppIds, " ")
}
// Gentle SIGTERM handler thanks to reiki4040 https://gist.github.com/reiki4040/be3705f307d3cd136e85
// v0.2: we also need to support SIGUSR from now on.
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGUSR1)
go func() {
for {
s := <-signalChan
switch s {
case syscall.SIGTERM:
log.Info("SIGTERM received, bye bye!")
gtk.MainQuit()
case syscall.SIGUSR1:
if *resident || *autohide {
// As win.Show() called from inside a goroutine randomly crashes GTK,
// let's just set e helper variable here. We'll be checking it with glib.TimeoutAdd.
if !win.IsVisible() {
log.Debug("SIGUSR1 received, showing the window")
windowStateChannel <- WindowShow
} else {
log.Debug("SIGUSR1 received, hiding the window")
windowStateChannel <- WindowHide
}
} else {
log.Info("SIGUSR1 received, and I'm not resident, bye bye!")
gtk.MainQuit()
}
default:
log.Warn("Unknown signal")
}
}
}()
// Unless we are in autohide/resident mode, we probably want the same key/mouse binding to turn the dock off.
// Since v0.2 we can't just send SIGKILL if running instance found. We'll send SIGUSR1 instead.
// If it's running with `-r` or `-d` flag, it'll show the window. If not - it will die.
// Use md5-hashed $USER name to create unique lock files for multiple users
lockFilePath := fmt.Sprintf("%s/nwg-dock-%s.lock", tempDir(), md5Hash(os.Getenv("USER")))
lockFile, err := singleinstance.CreateLockFile(lockFilePath)
if err != nil {
pid, err := readTextFile(lockFilePath)
if err == nil {
i, err := strconv.Atoi(pid)
if err == nil {
if *autohide || *resident {
log.Info("Running instance found, terminating...")
} else {
_ = syscall.Kill(i, syscall.SIGUSR1)
log.Info("Sending SIGUSR1 to running instance and bye, bye!")
}
}
}
os.Exit(0)
}
defer lockFile.Close()
if !*noLauncher && *launcherCmd == "" {
if isCommand("nwg-drawer") {
*launcherCmd = "nwg-drawer"
} else if isCommand("nwggrid") {
*launcherCmd = "nwggrid -p"
}
if *launcherCmd != "" {
log.Infof("Using auto-detected launcher command: '%s'", *launcherCmd)
} else {
log.Info("Neither 'nwg-drawer' nor 'nwggrid' command found, and no other launcher specified; hiding the launcher button.")
}
}
dataHome = getDataHome()
configDirectory = configDir()
// if it doesn't exist:
createDir(configDirectory)
if !pathExists(fmt.Sprintf("%s/style.css", configDirectory)) {
err := copyFile(filepath.Join(dataHome, "nwg-dock/style.css"), fmt.Sprintf("%s/style.css", configDirectory))
if err != nil {
log.Warnf("Error copying file: %s", err)
}
}
cacheDirectory := cacheDir()
if cacheDirectory == "" {
log.Panic("Couldn't determine cache directory location")
}
pinnedFile = filepath.Join(cacheDirectory, "nwg-dock-pinned")
cssFile := filepath.Join(configDirectory, *cssFileName)
appDirs = getAppDirs()
gtk.Init(nil)
cssProvider, _ := gtk.CssProviderNew()
err = cssProvider.LoadFromPath(cssFile)
if err != nil {
log.Warnf("%s file not found, using GTK styling\n", cssFile)
} else {
log.Printf("Using style: %s\n", cssFile)
screen, _ := gdk.ScreenGetDefault()
gtk.AddProviderForScreen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
}
win, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
if err != nil {
log.Fatal("Unable to create window:", err)
}
layershell.InitForWindow(win)
var output2mon map[string]*gdk.Monitor
if *targetOutput != "" {
// We want to assign layershell to a monitor, but we only know the output name!
output2mon, err = mapOutputs()
if err == nil {
layershell.SetMonitor(win, output2mon[*targetOutput])
} else {
log.Warn(fmt.Sprintf("%s", err))
}
}
if *exclusive {
layershell.AutoExclusiveZoneEnable(win)
*layer = "top"
}
if *position == "bottom" || *position == "top" {
if *position == "bottom" {
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_BOTTOM, true)
widgetAnchor = gdk.GDK_GRAVITY_NORTH
menuAnchor = gdk.GDK_GRAVITY_SOUTH
} else {
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_TOP, true)
widgetAnchor = gdk.GDK_GRAVITY_SOUTH
menuAnchor = gdk.GDK_GRAVITY_NORTH
}
outerOrientation = gtk.ORIENTATION_VERTICAL
innerOrientation = gtk.ORIENTATION_HORIZONTAL
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_LEFT, *full)
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_RIGHT, *full)
}
if *position == "left" {
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_LEFT, true)
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_TOP, *full)
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_BOTTOM, *full)
outerOrientation = gtk.ORIENTATION_HORIZONTAL
innerOrientation = gtk.ORIENTATION_VERTICAL
widgetAnchor = gdk.GDK_GRAVITY_EAST
menuAnchor = gdk.GDK_GRAVITY_WEST
}
if *layer == "top" {
layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_TOP)
} else if *layer == "bottom" {
layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_BOTTOM)
} else {
layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_OVERLAY)
layershell.SetExclusiveZone(win, -1)
}
layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_TOP, *marginTop)
layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_LEFT, *marginLeft)
layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_RIGHT, *marginRight)
layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_BOTTOM, *marginBottom)
win.Connect("destroy", func() {
gtk.MainQuit()
})
// Close the window on leave, but not immediately, to avoid accidental closes
win.Connect("leave-notify-event", func() {
if *autohide {
src = glib.TimeoutAdd(uint(1000), func() bool {
win.Hide()
src = 0
return false
})
}
})
win.Connect("enter-notify-event", func() {
cancelClose()
})
outerBox, _ := gtk.BoxNew(outerOrientation, 0)
_ = outerBox.SetProperty("name", "box")
win.Add(outerBox)
alignmentBox, _ := gtk.BoxNew(innerOrientation, 0)
outerBox.PackStart(alignmentBox, true, true, 0)
mainBox, _ = gtk.BoxNew(innerOrientation, 0)
// We'll pack mainBox later, in buildMainBox
tasks, err := listTasks()
if err != nil {
log.Fatal("Couldn't list tasks:", err)
}
oldTasks = tasks
var oldWsNum int64
buildMainBox(tasks, alignmentBox)
refreshMainBox := func(currentTasks []task, forceRefresh bool) {
if forceRefresh || (len(currentTasks) != len(oldTasks) || currentWsNum != oldWsNum) {
glib.TimeoutAdd(0, func() bool {
log.Debug("refreshing...")
buildMainBox(currentTasks, alignmentBox)
oldTasks = currentTasks
oldWsNum = currentWsNum
targetWsNum = currentWsNum
return false
})
}
}
go func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
taskChannel, err := getTaskChangesChannel(ctx)
if err != nil {
log.Fatal("Unable to process sway tasks:", err)
}
for {
select {
// Refresh if any pin/unpin action happened
case <-refreshMainBoxChannel:
currentTasks, err := listTasks()
if err != nil {
log.Fatal("Unable to retrieve task list from sway!")
}
refreshMainBox(currentTasks, true)
// Refresh if the state of the workspace changes
case currentTasks := <-taskChannel:
refreshMainBox(currentTasks, false)
}
}
}()
win.ShowAll()
if *autohide {
glib.TimeoutAdd(uint(500), win.Hide)
mRefProvider, _ := gtk.CssProviderNew()
if err := mRefProvider.LoadFromPath(filepath.Join(dataHome, "nwg-dock/hotspot.css")); err != nil {
log.Errorf("%s", err)
}
if *targetOutput == "" {
// hot spots on all displays
monitors, _ := listMonitors()
for _, monitor := range monitors {
win := setupHotSpot(monitor, win)
ctx, _ := win.GetStyleContext()
ctx.AddProvider(mRefProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
win.ShowAll()
}
} else {
// hot spot on the selected display only
monitor := output2mon[*targetOutput]
win := setupHotSpot(*monitor, win)
ctx, _ := win.GetStyleContext()
ctx.AddProvider(mRefProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
win.ShowAll()
}
}
go func() {
for {
windowState := <-windowStateChannel
glib.TimeoutAdd(0, func() bool {
if windowState == WindowShow && win != nil && !win.IsVisible() {
win.ShowAll()
}
if windowState == WindowHide && win != nil && win.IsVisible() {
win.Hide()
}
return false
})
}
}()
gtk.Main()
}
nwg-piotr-nwg-dock-8ecd84c/tools.go 0000664 0000000 0000000 00000066434 14740075321 0017362 0 ustar 00root root 0000000 0000000 package main
import (
"context"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"github.com/joshuarubin/go-sway"
)
var descendants []sway.Node
type task struct {
conID int64
ID string // will be created out of app_id or window class
Name string
PID uint32
WsNum int64
}
func taskInstances(ID string, tasks []task) []task {
var found []task
for _, t := range tasks {
if strings.Contains(strings.ToUpper(t.ID), strings.ToUpper(ID)) {
found = append(found, t)
}
}
return found
}
type TaskChange struct {
Change sway.WindowEventChange
Task *task
}
type swayEventHandler struct {
taskUpdateChannel chan TaskChange
workspaceUpdateChannel chan int64
}
func (t swayEventHandler) Workspace(ctx context.Context, event sway.WorkspaceEvent) {
if event.Change == "focus" {
// TODO: sway.WorkspaceEvent.Current should contain a Workspace, but contains Node,
// this may be an error of the used library ...
t.workspaceUpdateChannel <- 0
}
}
func (t swayEventHandler) Mode(ctx context.Context, event sway.ModeEvent) {}
func (t swayEventHandler) BarConfigUpdate(ctx context.Context, event sway.BarConfigUpdateEvent) {}
func (t swayEventHandler) Binding(ctx context.Context, event sway.BindingEvent) {}
func (t swayEventHandler) Shutdown(ctx context.Context, event sway.ShutdownEvent) {}
func (t swayEventHandler) Tick(ctx context.Context, event sway.TickEvent) {}
func (t swayEventHandler) BarStateUpdate(ctx context.Context, event sway.BarStateUpdateEvent) {}
func (t swayEventHandler) BarStatusUpdate(ctx context.Context, event sway.BarStateUpdateEvent) {}
func (t swayEventHandler) Input(ctx context.Context, event sway.InputEvent) {}
func (t swayEventHandler) Window(ctx context.Context, window sway.WindowEvent) {
if window.Change == "new" || window.Change == "close" {
t.taskUpdateChannel <- TaskChange{
Change: window.Change,
// TODO: gather enough details form sway.WindowEvent to create the task
// structure and pass it on for smarter modifying the task array
Task: nil,
}
}
}
// TODO: The channel should *not* return a []task, but rather a TaskChange event which should
// be used to modify the list in the frontend ...
func getTaskChangesChannel(ctx context.Context) (chan []task, error) {
taskArrayChannel := make(chan []task, 1)
eventHandler := swayEventHandler{
taskUpdateChannel: make(chan TaskChange, 1),
}
go func() {
// Blocks execution until we cancel the context
if err := sway.Subscribe(ctx, eventHandler, sway.EventTypeWindow); err != nil {
log.Fatal("Unable to subscribe to sway event:", err)
}
}()
// Pretty hacky, but is simply used to convert a TaskChange to a task struct
go func() {
for {
<-eventHandler.taskUpdateChannel
tasks, err := listTasks()
if err != nil {
log.Errorf("Unable to process tasks from sway: %s", err.Error())
return
}
taskArrayChannel <- tasks
}
}()
return taskArrayChannel, nil
}
func getWorkspaceChangesChannel(ctx context.Context) chan int64 {
workspaceUpdateChannel := make(chan int64, 1)
eventHandler := swayEventHandler{
workspaceUpdateChannel: make(chan int64, 1),
}
go func() {
// Blocks execution until we cancel the context
if err := sway.Subscribe(ctx, eventHandler, sway.EventTypeWorkspace); err != nil {
log.Fatal("Unable to subscribe to sway event:", err)
}
}()
go func() {
ipc, _ := sway.New(ctx)
for {
<-eventHandler.workspaceUpdateChannel
workspaces, _ := ipc.GetWorkspaces(ctx)
for _, workspace := range workspaces {
if workspace.Focused {
workspaceUpdateChannel <- workspace.Num
break
}
}
}
}()
return workspaceUpdateChannel
}
// list sway tree, return tasks sorted by workspace numbers
func listTasks() ([]task, error) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
client, err := sway.New(ctx)
if err != nil {
return nil, err
}
tree, err := client.GetTree(ctx)
if err != nil {
return nil, err
}
workspaces, _ := client.GetWorkspaces(ctx)
if err != nil {
return nil, err
}
// In order not to add a separate function, let's set the global currentWsNum variable we need here
for _, ws := range workspaces {
if ws.Focused {
currentWsNum = ws.Num
break
}
}
// all nodes in the tree
nodes := tree.Nodes
// find outputs in all nodes
var outputs []*sway.Node
for _, n := range nodes {
if n.Type == "output" && !strings.HasPrefix(n.Name, "__") {
outputs = append(outputs, n)
}
}
// find workspaces in outputs
var workspaceNodes []*sway.Node
for _, o := range outputs {
nodes = o.Nodes
for _, n := range nodes {
if n.Type == "workspace" {
workspaceNodes = append(workspaceNodes, n)
}
}
}
var tasks []task
// find cons in workspaces recursively
for _, w := range workspaceNodes {
wsNum := workspaceNum(workspaces, w.Name)
descendants = nil
for _, con := range w.Nodes {
findDescendants(*con)
}
// create tasks from cons which represent tasks
for _, con := range descendants {
t, err := createTask(con, wsNum)
if err == nil {
tasks = append(tasks, *t)
} else {
log.Warn(err)
}
}
fNodes := w.FloatingNodes
for _, con := range fNodes {
t, err := createTask(*con, wsNum)
if err == nil {
tasks = append(tasks, *t)
} else {
log.Warn(err)
}
}
}
sort.Slice(tasks, func(i int, j int) bool {
return tasks[i].WsNum < tasks[j].WsNum
})
return tasks, nil
}
func findDescendants(con sway.Node) {
if len(con.Nodes) > 0 {
for _, node := range con.Nodes {
findDescendants(*node)
}
} else {
descendants = append(descendants, con)
}
}
func createTask(con sway.Node, wsNum int64) (*task, error) {
t := &task{}
t.conID = con.ID
if con.AppID != nil {
t.ID = *con.AppID
} else if con.WindowProperties != nil {
wp := *con.WindowProperties
t.ID = wp.Class
} else {
return nil, errors.New("damaged Node data received, task skipped")
}
t.Name = con.Name
if con.PID != nil {
t.PID = *con.PID
} else {
return nil, errors.New("damaged Node data received, task skipped")
}
t.WsNum = wsNum
return t, nil
}
func workspaceNum(workspaces []sway.Workspace, name string) int64 {
for _, ws := range workspaces {
if ws.Name == name {
return ws.Num
}
}
return 0
}
func pinnedButton(ID string) *gtk.Box {
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
button, _ := gtk.ButtonNew()
box.PackStart(button, false, false, 0)
image, err := createImage(ID, imgSizeScaled)
if err != nil {
pixbuf, err := gdk.PixbufNewFromFileAtSize(filepath.Join(dataHome, "nwg-dock/images/icon-missing.svg"),
imgSizeScaled, imgSizeScaled)
if err == nil {
image, _ = gtk.ImageNewFromPixbuf(pixbuf)
} else {
image, _ = gtk.ImageNew()
}
}
if image != nil {
button.SetImage(image)
button.SetImagePosition(gtk.POS_TOP)
button.SetAlwaysShowImage(true)
}
button.SetTooltipText(getName(ID))
pixbuf, _ := gdk.PixbufNewFromFileAtSize(filepath.Join(dataHome, "nwg-dock/images/task-empty.svg"),
imgSizeScaled, imgSizeScaled/8)
img, _ := gtk.ImageNewFromPixbuf(pixbuf)
box.PackStart(img, false, false, 0)
button.Connect("clicked", func() {
launch(ID)
})
button.Connect("button-release-event", func(btn *gtk.Button, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
if btnEvent.Button() == 1 {
launch(ID)
return true
} else if btnEvent.Button() == 3 {
contextMenu := pinnedMenuContext(ID)
contextMenu.PopupAtWidget(button, widgetAnchor, menuAnchor, nil)
return true
}
return false
})
button.Connect("enter-notify-event", cancelClose)
return box
}
func pinnedMenuContext(taskID string) gtk.Menu {
menu, _ := gtk.MenuNew()
menuItem, _ := gtk.MenuItemNewWithLabel("Unpin")
menuItem.Connect("activate", func() {
unpinTask(taskID)
})
menu.Append(menuItem)
menu.ShowAll()
return *menu
}
func launcherButton() *gtk.Button {
if !*noLauncher && *launcherCmd != "" {
button, _ := gtk.ButtonNew()
pixbuf, err := gdk.PixbufNewFromFileAtSize(filepath.Join(dataHome, "nwg-dock/images/grid.svg"), imgSizeScaled, imgSizeScaled)
if err == nil {
image, _ := gtk.ImageNewFromPixbuf(pixbuf)
button.SetImage(image)
button.SetAlwaysShowImage(true)
button.Connect("clicked", func() {
elements := strings.Split(*launcherCmd, " ")
cmd := exec.Command(elements[0], elements[1:]...)
go func() {
err := cmd.Run()
if err != nil {
log.Warnf("Unable to start program: %s", err.Error())
}
}()
if *autohide {
win.Hide()
}
})
button.Connect("enter-notify-event", cancelClose)
} else {
log.Errorf("Unable to show grid button: %s", err.Error())
}
return button
}
return nil
}
/*
Window on-leave-notify event hides the dock with glib Timeout 1000 ms.
We might have left the window by accident, so let's clear the timeout if window re-entered.
Furthermore - hovering a button triggers window on-leave-notify event, and the timeout
needs to be cleared as well.
*/
func cancelClose() {
if src > 0 {
glib.SourceRemove(src)
src = 0
}
}
func taskButton(t task, instances []task) *gtk.Box {
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
button, _ := gtk.ButtonNew()
box.PackStart(button, false, false, 0)
image, err := createImage(t.ID, imgSizeScaled)
if err != nil {
pixbuf, err := gdk.PixbufNewFromFileAtSize(filepath.Join(dataHome, "nwg-dock/images/icon-missing.svg"),
imgSizeScaled, imgSizeScaled)
if err == nil {
image, _ = gtk.ImageNewFromPixbuf(pixbuf)
} else {
image, _ = gtk.ImageNew()
}
}
if image != nil {
button.SetImage(image)
button.SetImagePosition(gtk.POS_TOP)
button.SetAlwaysShowImage(true)
}
button.SetTooltipText(getName(t.ID))
var img *gtk.Image
if len(instances) < 2 {
pixbuf, _ := gdk.PixbufNewFromFileAtSize(filepath.Join(dataHome, "nwg-dock/images/task-single.svg"),
imgSizeScaled, imgSizeScaled/8)
img, _ = gtk.ImageNewFromPixbuf(pixbuf)
} else {
pixbuf, _ := gdk.PixbufNewFromFileAtSize(filepath.Join(dataHome, "nwg-dock/images/task-multiple.svg"),
imgSizeScaled, imgSizeScaled/8)
img, _ = gtk.ImageNewFromPixbuf(pixbuf)
}
box.PackStart(img, false, false, 0)
button.Connect("enter-notify-event", cancelClose)
if len(instances) == 1 {
button.Connect("event", func(btn *gtk.Button, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
/* EVENT_BUTTON_PRESS would be more obvious, but it causes the misbehaviour:
if con is located on an external display, after pressing the button, the conID value
"freezes", and stays the same for all taskButtons, until the right mouse click.
A gotk3 bug or WTF? */
if btnEvent.Type() == gdk.EVENT_BUTTON_RELEASE || btnEvent.Type() == gdk.EVENT_TOUCH_END {
if btnEvent.Button() == 1 || btnEvent.Type() == gdk.EVENT_TOUCH_END {
focusCon(t.conID)
return true
} else if btnEvent.Button() == 3 {
contextMenu := taskMenuContext(t.ID, instances)
contextMenu.PopupAtWidget(button, widgetAnchor, menuAnchor, nil)
return true
}
}
return false
})
} else {
button.Connect("button-release-event", func(btn *gtk.Button, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
if btnEvent.Button() == 1 {
menu := taskMenu(t.ID, instances)
menu.PopupAtWidget(button, widgetAnchor, menuAnchor, nil)
return true
} else if btnEvent.Button() == 3 {
contextMenu := taskMenuContext(t.ID, instances)
contextMenu.PopupAtWidget(button, widgetAnchor, menuAnchor, nil)
return true
}
return false
})
}
return box
}
func taskMenu(taskID string, instances []task) gtk.Menu {
menu, _ := gtk.MenuNew()
iconName, _ := getIcon(taskID)
for _, instance := range instances {
menuItem, _ := gtk.MenuItemNew()
hbox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6)
image, _ := gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_MENU)
hbox.PackStart(image, false, false, 0)
title := instance.Name
if len(title) > 20 {
title = title[:20]
}
label, _ := gtk.LabelNew(fmt.Sprintf("%s (%v)", title, instance.WsNum))
hbox.PackStart(label, false, false, 0)
menuItem.Add(hbox)
menu.Append(menuItem)
conID := instance.conID
menuItem.Connect("activate", func() {
focusCon(conID)
})
}
menu.ShowAll()
return *menu
}
func taskMenuContext(taskID string, instances []task) gtk.Menu {
menu, _ := gtk.MenuNew()
iconName, err := getIcon(taskID)
if err != nil {
log.Warnf("%s %s", err, taskID)
}
for _, instance := range instances {
menuItem, _ := gtk.MenuItemNew()
hbox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6)
image, _ := gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_MENU)
hbox.PackStart(image, false, false, 0)
title := instance.Name
if len(title) > 20 {
title = title[:20]
}
label, _ := gtk.LabelNew(fmt.Sprintf("%s (%v)", title, instance.WsNum))
hbox.PackStart(label, false, false, 0)
menuItem.Add(hbox)
menu.Append(menuItem)
submenu, _ := gtk.MenuNew()
subitem, _ := gtk.MenuItemNewWithLabel("Close")
submenu.Append(subitem)
conID := instance.conID
subitem.Connect("activate", func() {
killCon(conID)
})
for i := 1; i < int(*numWS)+1; i++ {
subitem, _ := gtk.MenuItemNewWithLabel(fmt.Sprintf("To WS %v", i))
target := i
subitem.Connect("activate", func() {
con2WS(conID, target)
})
submenu.Append(subitem)
}
menuItem.SetSubmenu(submenu)
}
separator, _ := gtk.SeparatorMenuItemNew()
menu.Append(separator)
item, _ := gtk.MenuItemNewWithLabel("New window")
item.Connect("activate", func() {
launch(taskID)
})
menu.Append(item)
pinItem, _ := gtk.MenuItemNew()
if !inPinned(taskID) {
pinItem.SetLabel("Pin")
pinItem.Connect("activate", func() {
log.Infof("pin %s", taskID)
pinTask(taskID)
})
} else {
pinItem.SetLabel("Unpin")
pinItem.Connect("activate", func() {
log.Infof("unpin %s", taskID)
unpinTask(taskID)
})
}
menu.Append(pinItem)
menu.ShowAll()
return *menu
}
func inPinned(taskID string) bool {
for _, id := range pinned {
if strings.TrimSpace(taskID) == strings.TrimSpace(id) {
return true
}
}
return false
}
func inTasks(tasks []task, pinID string) bool {
for _, task := range tasks {
if strings.TrimSpace(task.ID) == strings.TrimSpace(pinID) {
return true
}
}
return false
}
func createImage(appID string, size int) (*gtk.Image, error) {
name, err := getIcon(appID)
if err != nil {
name = appID
}
pixbuf, err := createPixbuf(name, size)
if err != nil {
return nil, err
}
image, _ := gtk.ImageNewFromPixbuf(pixbuf)
return image, nil
}
func createPixbuf(icon string, size int) (*gdk.Pixbuf, error) {
if strings.HasPrefix(icon, "/") {
pixbuf, err := gdk.PixbufNewFromFileAtSize(icon, size, size)
if err != nil {
log.Errorf("%s", err)
return nil, err
}
return pixbuf, nil
}
iconTheme, err := gtk.IconThemeGetDefault()
if err != nil {
log.Fatal("Couldn't get default theme: ", err)
}
pixbuf, err := iconTheme.LoadIcon(icon, size, gtk.ICON_LOOKUP_FORCE_SIZE)
if err != nil {
ico, err := getIcon(icon)
if err != nil {
return nil, err
}
if strings.HasPrefix(ico, "/") {
pixbuf, err := gdk.PixbufNewFromFileAtSize(ico, size, size)
if err != nil {
return nil, err
}
return pixbuf, nil
}
pixbuf, err := iconTheme.LoadIcon(ico, size, gtk.ICON_LOOKUP_FORCE_SIZE)
if err != nil {
return nil, err
}
return pixbuf, nil
}
return pixbuf, nil
}
func cacheDir() string {
if os.Getenv("XDG_CACHE_HOME") != "" {
return os.Getenv("XDG_CONFIG_HOME")
}
if os.Getenv("HOME") != "" && pathExists(filepath.Join(os.Getenv("HOME"), ".cache")) {
p := filepath.Join(os.Getenv("HOME"), ".cache")
return p
}
return ""
}
func tempDir() string {
if os.Getenv("TMPDIR") != "" {
return os.Getenv("TMPDIR")
} else if os.Getenv("TEMP") != "" {
return os.Getenv("TEMP")
} else if os.Getenv("TMP") != "" {
return os.Getenv("TMP")
}
return "/tmp"
}
func readTextFile(path string) (string, error) {
bytes, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(bytes), nil
}
func configDir() string {
if os.Getenv("XDG_CONFIG_HOME") != "" {
return (fmt.Sprintf("%s/nwg-dock", os.Getenv("XDG_CONFIG_HOME")))
}
return fmt.Sprintf("%s/.config/nwg-dock", os.Getenv("HOME"))
}
func createDir(dir string) {
if _, err := os.Stat(dir); os.IsNotExist(err) {
err := os.MkdirAll(dir, os.ModePerm)
if err == nil {
log.Infof("Creating dir: %s", dir)
}
}
}
func copyFile(src, dst string) error {
log.Infof("Copying file: %s", dst)
var err error
var srcfd *os.File
var dstfd *os.File
var srcinfo os.FileInfo
if srcfd, err = os.Open(src); err != nil {
return err
}
defer srcfd.Close()
if dstfd, err = os.Create(dst); err != nil {
return err
}
defer dstfd.Close()
if _, err = io.Copy(dstfd, srcfd); err != nil {
return err
}
if srcinfo, err = os.Stat(src); err != nil {
return err
}
return os.Chmod(dst, srcinfo.Mode())
}
func getDataHome() string {
if os.Getenv("XDG_DATA_HOME") != "" {
return os.Getenv("XDG_DATA_HOME")
}
return "/usr/share/"
}
func getAppDirs() []string {
var dirs []string
xdgDataDirs := ""
home := os.Getenv("HOME")
xdgDataHome := os.Getenv("XDG_DATA_HOME")
if os.Getenv("XDG_DATA_DIRS") != "" {
xdgDataDirs = os.Getenv("XDG_DATA_DIRS")
} else {
xdgDataDirs = "/usr/local/share/:/usr/share/"
}
if xdgDataHome != "" {
dirs = append(dirs, filepath.Join(xdgDataHome, "applications"))
} else if home != "" {
dirs = append(dirs, filepath.Join(home, ".local/share/applications"))
}
for _, d := range strings.Split(xdgDataDirs, ":") {
dirs = append(dirs, filepath.Join(d, "applications"))
}
flatpakDirs := []string{filepath.Join(home, ".local/share/flatpak/exports/share/applications"),
"/var/lib/flatpak/exports/share/applications"}
for _, d := range flatpakDirs {
if !isIn(dirs, d) {
dirs = append(dirs, d)
}
}
return dirs
}
func isIn(slice []string, val string) bool {
for _, item := range slice {
if item == val {
return true
}
}
return false
}
func getIcon(appName string) (string, error) {
appName = strings.Split(appName, " ")[0]
if strings.HasPrefix(strings.ToUpper(appName), "GIMP") {
return "gimp", nil
}
p := ""
for _, d := range appDirs {
path := filepath.Join(d, fmt.Sprintf("%s.desktop", appName))
if pathExists(path) {
p = path
break
} else if pathExists(strings.ToLower(path)) {
p = strings.ToLower(path)
break
}
}
/* Some apps' app_id varies from their .desktop file name, e.g. 'gimp-2.9.9' or 'pamac-manager'.
Let's try to find a matching .desktop file name */
if !strings.HasPrefix(appName, "/") && p == "" { // skip icon paths given instead of names
p = searchDesktopDirs(appName)
}
if p != "" {
lines, err := loadTextFile(p)
if err != nil {
return "", err
}
for _, line := range lines {
if strings.HasPrefix(strings.ToUpper(line), "ICON") {
return strings.Split(line, "=")[1], nil
}
}
}
return "", errors.New(fmt.Sprintf("couldn't find the icon for %s", appName))
}
func searchDesktopDirs(badAppID string) string {
b4Separator := strings.Split(badAppID, "-")[0]
for _, d := range appDirs {
items, _ := os.ReadDir(d)
for _, item := range items {
if strings.Contains(item.Name(), b4Separator) {
//Let's check items starting from 'org.' first
if strings.Count(item.Name(), ".") > 1 && strings.HasSuffix(item.Name(),
fmt.Sprintf("%s.desktop", badAppID)) {
return filepath.Join(d, item.Name())
}
}
}
}
// exceptions like "class": "VirtualBox Manager" & virtualbox.desktop
b4Separator = strings.Split(badAppID, " ")[0]
for _, d := range appDirs {
items, _ := os.ReadDir(d)
for _, item := range items {
if strings.Contains(strings.ToUpper(item.Name()), strings.ToUpper(b4Separator)) {
return filepath.Join(d, item.Name())
}
}
}
return ""
}
func getExec(appName string) (string, error) {
cmd := appName
if strings.HasPrefix(strings.ToUpper(appName), "GIMP") {
cmd = "gimp"
}
for _, d := range appDirs {
files, _ := os.ReadDir(d)
path := ""
for _, f := range files {
if strings.HasSuffix(f.Name(), ".desktop") {
if f.Name() == fmt.Sprintf("%s.desktop", appName) ||
f.Name() == fmt.Sprintf("%s.desktop", strings.ToLower(appName)) {
path = filepath.Join(d, f.Name())
break
}
}
}
// as above in getIcon - for tasks w/ improper app_id
if path == "" {
path = searchDesktopDirs(appName)
}
if path != "" {
lines, err := loadTextFile(path)
if err != nil {
return "", err
}
for _, line := range lines {
if strings.HasPrefix(strings.ToUpper(line), "EXEC") {
l := line[5:]
cutAt := strings.Index(l, "%")
if cutAt != -1 {
l = l[:cutAt-1]
}
cmd = l
break
}
}
return cmd, nil
}
}
return cmd, nil
}
func getName(appName string) string {
name := appName
for _, d := range appDirs {
files, _ := os.ReadDir(d)
path := ""
for _, f := range files {
if strings.HasSuffix(f.Name(), ".desktop") {
if f.Name() == fmt.Sprintf("%s.desktop", appName) ||
f.Name() == fmt.Sprintf("%s.desktop", strings.ToLower(appName)) {
path = filepath.Join(d, f.Name())
break
}
}
}
if path != "" {
lines, err := loadTextFile(path)
if err != nil {
return name
}
for _, line := range lines {
if strings.HasPrefix(strings.ToUpper(line), "NAME") {
name = line[5:]
break
}
}
}
}
return name
}
func pathExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
func loadTextFile(path string) ([]string, error) {
bytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}
lines := strings.Split(string(bytes), "\n")
var output []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
output = append(output, line)
}
}
return output, nil
}
func pinTask(itemID string) {
for _, item := range pinned {
if item == itemID {
println(item, "already pinned")
return
}
}
pinned = append(pinned, itemID)
savePinned()
refreshMainBoxChannel <- struct{}{}
}
func unpinTask(itemID string) {
pinned = remove(pinned, itemID)
savePinned()
refreshMainBoxChannel <- struct{}{}
}
func remove(s []string, r string) []string {
for i, v := range s {
if v == r {
return append(s[:i], s[i+1:]...)
}
}
return s
}
func savePinned() {
f, err := os.OpenFile(pinnedFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
log.Fatal(err)
}
defer f.Close()
for _, line := range pinned {
if line != "" {
_, err := f.WriteString(line + "\n")
if err != nil {
log.Errorf("Error saving pinned", err)
}
}
}
}
func launch(ID string) {
command, err := getExec(ID)
if err != nil {
log.Errorf("%s", err)
}
// remove quotation marks if any
if strings.Contains(command, "\"") {
command = strings.ReplaceAll(command, "\"", "")
}
elements := strings.Split(command, " ")
// find prepended env variables, if any
envVarsNum := strings.Count(command, "=")
var envVars []string
cmdIdx := -1
if envVarsNum > 0 {
for idx, item := range elements {
if strings.Contains(item, "=") {
envVars = append(envVars, item)
} else if !strings.HasPrefix(item, "-") && cmdIdx == -1 {
cmdIdx = idx
}
}
}
if cmdIdx == -1 {
cmdIdx = 0
}
var args []string
for _, arg := range elements[1+cmdIdx:] {
if !strings.Contains(arg, "=") {
args = append(args, arg)
}
}
cmd := exec.Command(elements[cmdIdx], elements[1+cmdIdx:]...)
// set env variables
if len(envVars) > 0 {
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, envVars...)
}
msg := fmt.Sprintf("env vars: %s; command: '%s'; args: %s\n", envVars, elements[cmdIdx], args)
log.Info(msg)
if err := cmd.Start(); err != nil {
log.Error("Unable to launch command!", err.Error())
}
if *autohide {
win.Hide()
}
}
func focusCon(conID int64) {
cmd := fmt.Sprintf("[con_id=%v] focus", conID)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
client, err := sway.New(ctx)
if err != nil {
log.Panic(err)
}
if _, err = client.RunCommand(ctx, cmd); err != nil {
log.Errorf("Unable to focus to con %v: %s", conID, err.Error())
}
if *autohide {
src = glib.TimeoutAdd(uint(1000), func() bool {
win.Hide()
return false
})
}
}
func focusWorkspace(num int64) {
cmd := fmt.Sprintf("workspace number %v", num)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
client, err := sway.New(ctx)
if err != nil {
log.Panic(err)
}
if _, err = client.RunCommand(ctx, cmd); err != nil {
log.Errorf("Unable to focus to workspace %v: %s", num, err.Error())
}
if *autohide {
src = glib.TimeoutAdd(uint(1000), func() bool {
win.Hide()
return false
})
}
}
func killCon(conID int64) {
cmd := fmt.Sprintf("[con_id=%v] kill", conID)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
client, err := sway.New(ctx)
if err != nil {
log.Panic(err)
}
if _, err = client.RunCommand(ctx, cmd); err != nil {
log.Errorf("Unable to kill con %v: %s", conID, err.Error())
}
if *autohide {
src = glib.TimeoutAdd(uint(1000), func() bool {
win.Hide()
return false
})
}
}
func con2WS(conID int64, wsNum int) {
cmd := fmt.Sprintf("[con_id=%v] move to workspace number %v", conID, wsNum)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
client, err := sway.New(ctx)
if err != nil {
log.Panic(err)
}
if _, err = client.RunCommand(ctx, cmd); err != nil {
log.Errorf("Unable to move to workspace %v: %s", wsNum, err.Error())
}
refreshMainBoxChannel <- struct{}{}
if *autohide {
src = glib.TimeoutAdd(uint(1000), func() bool {
win.Hide()
return false
})
}
}
// Returns map output name -> gdk.Monitor
func mapOutputs() (map[string]*gdk.Monitor, error) {
result := make(map[string]*gdk.Monitor)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
client, err := sway.New(ctx)
if err != nil {
return nil, err
}
outputs, err := client.GetOutputs(ctx)
if err != nil {
return nil, err
}
display, err := gdk.DisplayGetDefault()
if err != nil {
return nil, err
}
num := display.GetNMonitors()
for i := 0; i < num; i++ {
monitor, _ := display.GetMonitor(i)
geometry := monitor.GetGeometry()
// assign output to monitor on the basis of the same x, y coordinates
for _, output := range outputs {
if int(output.Rect.X) == geometry.GetX() && int(output.Rect.Y) == geometry.GetY() {
result[output.Name] = monitor
}
}
}
return result, nil
}
func listMonitors() ([]gdk.Monitor, error) {
var monitors []gdk.Monitor
display, err := gdk.DisplayGetDefault()
if err != nil {
return nil, err
}
num := display.GetNMonitors()
for i := 0; i < num; i++ {
monitor, _ := display.GetMonitor(i)
monitors = append(monitors, *monitor)
}
return monitors, nil
}
// Returns output of a CLI command with optional arguments
func getCommandOutput(command string) string {
out, err := exec.Command("sh", "-c", command).Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
func isCommand(command string) bool {
cmd := strings.Fields(command)[0]
return getCommandOutput(fmt.Sprintf("command -v %s ", cmd)) != ""
}
func md5Hash(text string) string {
hash := md5.Sum([]byte(text))
return hex.EncodeToString(hash[:])
}