119 Commits

Author SHA1 Message Date
Piotr Miller
c4629e0c28 Merge pull request #80 from nwg-piotr/lockfile
Move lock file to XDG_DATA_HOME/nwg-drawer/
2022-12-11 23:21:32 +01:00
piotr
ab5e2ea6ae move lock file to dataDir #79 2022-12-11 23:05:57 +01:00
piotr
63117e2605 bump to 0.3.6 2022-12-11 21:44:01 +01:00
Piotr Miller
d22609f403 Merge pull request #77 from nwg-piotr/debu68
fix #68
2022-12-01 00:12:46 +01:00
piotr
cb6a7f44fe go-sway -> 1.2.0 2022-12-01 00:03:17 +01:00
piotr
85cc2d78ee fix userDirsFile path #68 2022-11-28 23:39:36 +01:00
piotr
d3a0fd04a4 log if userDirsFile found #68 2022-11-28 23:26:19 +01:00
piotr
52e63667c9 print userDirsMap -> log.Debugf #68 2022-11-28 23:03:12 +01:00
piotr
f399f589fb add a line to debug #68 2022-11-18 01:40:50 +01:00
Piotr Miller
2058f124fd Merge pull request #75 from nwg-piotr/fix74
sanitize no longer valid pinned items #74
2022-11-12 15:40:14 +01:00
piotr
ebdbe71ac6 hide and delete invalid pinned items #74 2022-11-12 03:41:24 +01:00
Piotr Miller
3fe6d71234 Merge pull request #73 from nwg-piotr/fix-72
Fix inconsistent behaviour of normal resident instance
2022-10-30 23:54:41 +01:00
piotr
0e5b1cf659 bump to 0.3.3 2022-10-30 22:58:19 +01:00
piotr
6494557979 focus 1st pinned if any, or focus 1st appBox entry; fixes #72 2022-10-30 22:53:06 +01:00
Piotr Miller
f68678f687 Merge pull request #71 from nwg-piotr/fix-45-revisited
Fix 45 revisited
2022-10-26 02:13:02 +02:00
piotr
e31df6e721 bump to 0.3.2 2022-10-25 04:06:30 +02:00
piotr
9509a65625 attempt to fix #45 (revisited) 2022-10-25 04:02:05 +02:00
Piotr Miller
bc84a05378 Merge pull request #70 from nwg-piotr/check69
Fix #69
2022-09-25 20:24:41 +02:00
piotr
a249c37612 del redundant type conversion 2022-09-21 00:56:09 +02:00
piotr
c7fa6e836a fix log line formatting 2022-09-21 00:54:42 +02:00
piotr
87a62e7bb6 handle error closing file 2022-09-21 00:53:48 +02:00
piotr
11241cef0b replace deprecated types 2022-09-21 00:48:28 +02:00
piotr
87d2a74db0 fix setting data dir #69 2022-09-21 00:32:29 +02:00
piotr
41f7d58edf debug #69 2022-09-20 23:01:35 +02:00
piotr
583ddfde0b update .gitignore 2022-06-23 00:16:56 +02:00
Piotr Miller
383869906a Merge pull request #65 from SPFabGerman/OnlyShowIn
Added support for Hidden, OnlyShowIn and NotShowIn
2022-06-11 00:19:45 +02:00
SPFabGerman
54553ffae1 Added support for Hidden, OnlyShowIn and NotShowIn 2022-06-09 21:32:38 +02:00
Piotr Miller
d8e7e0f6c2 Merge pull request #63 from nwg-piotr/fix62
Fixes accidental crash on trimming long descriptions
2022-05-16 22:27:54 +02:00
piotr
7230d82be3 fix trimming names as well #62 2022-05-16 01:16:02 +02:00
piotr
02b3930ef9 trim string, not runes #62 2022-05-16 01:01:25 +02:00
piotr
f3b3635bd8 trim comments > 120 chars #61 2022-05-08 01:43:16 +02:00
nwg-piotr
bb84a2fc46 remove accidental binary 2022-04-13 11:36:19 +02:00
Piotr Miller
93587a47db Merge pull request #60 from nwg-piotr/dark
prefer dark theme
2022-04-13 11:22:15 +02:00
nwg-piotr
afe99b42f0 prefer dark theme 2022-04-13 11:14:50 +02:00
Piotr Miller
e052a86b12 Merge pull request #58 from jovanlanik/main
Support for changing GTK theme
2022-03-18 02:06:06 +01:00
Jovan Lanik
45cb286296 Add support for changing GTK theme 2022-03-13 21:58:42 +01:00
piotr
d050598086 bump to 0.2.8 2022-02-15 01:19:30 +01:00
Piotr Miller
42ddd815f4 Merge pull request #52 from snehrbass/main
Add flags to set window margins
2022-02-15 01:02:48 +01:00
Ste_ph_en
8b1dfb4421 Add flags to set margins 2022-02-14 18:57:57 -05:00
Piotr Miller
d572c1125d Merge pull request #54 from Raffy23/51-fix-zombie-processes
Collect signals of child processes
2022-02-15 00:42:56 +01:00
Piotr Miller
6b055bb48a Merge pull request #53 from Raffy23/50-fix-high-cpu-load
Fix high CPU load
2022-02-15 00:37:54 +01:00
Raphael Ludwig
165b925da2 Fix regression that prevented pinned items to update while window is open 2022-02-14 20:59:48 +01:00
Raphael Ludwig
058f8684aa Use a channel and a goroutine instead of busy polling a boolean flag to reduce cpu consumption 2022-02-13 23:21:21 +01:00
Raphael Ludwig
f4b51b0706 Collect signals of child processes
Add logging if starting of a process was not successful
2022-02-13 23:06:21 +01:00
Piotr Miller
b781f58097 Update README.md 2022-02-04 11:45:17 +01:00
Piotr Miller
68137935a9 Update README.md 2022-02-02 14:47:28 +01:00
piotr
98efb36614 bump version 2022-01-15 23:58:23 +01:00
Piotr Miller
59533536b1 Merge pull request #48 from nightly-brew/detach-from-process-group
Use Setsid when spawning programs
2022-01-15 03:58:52 +01:00
nightly-brew
07821f39b7 Use Setsid when starting the selected program to make sure it's not killed by signals sent to the drawer's process group. 2022-01-15 02:36:07 +01:00
piotr
4ea160e524 update README 2022-01-13 23:58:10 +01:00
Piotr Miller
8fb4209a97 Merge pull request #47 from nwg-piotr/not-wayland
remove bin
2022-01-13 23:46:57 +01:00
piotr
c85c364ba5 ignore bin 2022-01-13 23:42:42 +01:00
piotr
8239254485 remove bin #40 2022-01-13 23:42:31 +01:00
Piotr Miller
7681055a23 Merge pull request #46 from nwg-piotr/fix45
check if fileSearchResultWrapper exists #45
2022-01-12 11:17:56 +01:00
piotr
15c9029935 check if fileSearchResultWrapper exists #45 2022-01-12 11:06:05 +01:00
Piotr Miller
f67f1a9950 Merge pull request #43 from nwg-piotr/fix-pinned
create empty pinned file if not found on startup (and some other minor issues)
2022-01-11 14:41:39 +01:00
piotr
2c63e256ba update readme 2022-01-11 14:36:56 +01:00
piotr
92095d5b97 hide fileSearchResultWrapper initially 2022-01-11 12:31:47 +01:00
piotr
a6977d9444 create pinned file if not found 2022-01-11 11:45:05 +01:00
Piotr Miller
378aa33cf3 Merge pull request #42 from nwg-piotr/issue36
Close on RMB only #36
2022-01-10 23:55:16 +01:00
piotr
9d6d572f72 close on LMB only #36 2022-01-10 22:04:58 +01:00
piotr
8fdb174643 hot fix to #41 2022-01-09 23:20:48 +01:00
piotr
0810250b3a default temt -> foot; consume Esc key event 2022-01-09 14:30:59 +01:00
piotr
6e76f49729 fix #38 @nightly-brew 2022-01-09 12:56:51 +01:00
Piotr Miller
94c94b8b1e Merge pull request #35 from nwg-piotr/term
support for the foot terminal
2021-11-29 00:47:33 +01:00
piotr
3d6f3e6e3c support foot terminal 2021-11-21 21:45:10 +01:00
piotr
2e305e6e52 bump to 0.2.1 2021-10-01 00:40:27 +02:00
Piotr Miller
0b28592f0f Merge pull request #32 from nwg-piotr/fix31
Hide resident window on SIGUSR1 if visible
2021-10-01 00:03:39 +02:00
piotr
8daf645e3b close resident on SIGUSR1 if visible #31 2021-09-30 12:33:39 +02:00
piotr
1c7a481108 fresh binary 2021-09-29 22:57:57 +02:00
Piotr Miller
2043ed3af5 Merge pull request #29 from aajonusonline/main
Add support for absolute paths
2021-09-29 22:54:47 +02:00
Piotr Miller
53a1c895e9 Merge branch 'main' into main 2021-09-29 22:53:38 +02:00
Piotr Miller
63bc6eb3b2 Merge pull request #30 from nwg-piotr/v020
v0.2.0 allows to run a resident instance of nwg-drawer
2021-09-29 22:49:58 +02:00
piotr
6f10951238 wait for binary to be released while installing 2021-09-29 22:41:44 +02:00
piotr
9e2bbf155c update README 2021-09-29 22:02:29 +02:00
piotr
26cd4fb0cf other logging levels 2021-09-28 03:15:36 +02:00
piotr
8c0d158e3d skip loading if file doesn't exist 2021-09-28 03:04:39 +02:00
piotr
875c8078d6 remove 1 error message 2021-09-28 02:51:03 +02:00
piotr
1f91ade8e6 multiple fixes 2021-09-28 02:40:23 +02:00
piotr
a9fe4a7371 update README 2021-09-27 02:27:04 +02:00
piotr
941764ee9f update README 2021-09-27 02:20:08 +02:00
piotr
00fd88275e update README 2021-09-27 01:57:32 +02:00
piotr
419c991b95 fix config dir detection 2021-09-27 01:38:00 +02:00
piotr
fa3a8d9bb7 excluded dirs 2021-09-25 04:39:05 +02:00
piotr
fb00364e51 migrate config files 2021-09-25 03:44:38 +02:00
piotr
62b7b4f90e comments 2021-09-25 01:41:39 +02:00
piotr
97c0e0c972 kill running instance if any 2021-09-24 04:08:22 +02:00
piotr
2ad8a8c241 kill running instance if any 2021-09-24 04:01:04 +02:00
piotr
920d95e6e1 descriptions 2021-09-24 03:36:51 +02:00
piotr
c19814256f watch .desktop & pinned file changes 2021-09-24 03:01:55 +02:00
wtl
e341759b9e Add support for absolute paths
In reference to https://github.com/nwg-piotr/nwg-bar/issues/1
2021-09-23 16:05:17 +03:00
piotr
e6088e7345 remove stale code 2021-09-23 00:45:35 +02:00
piotr
bd1aa18506 fix build and run 2021-09-23 00:38:56 +02:00
piotr
5fcc6239b6 resolve conflicts 2021-09-23 00:25:35 +02:00
piotr
c974593f77 restore binary 2021-09-22 23:45:14 +02:00
piotr
aa9d4ea97a temporarily remove binary 2021-09-22 23:35:23 +02:00
Piotr Miller
80f8d2eb03 Merge pull request #27 from aajonusonline/main
use `unknown` icon when one is not available
2021-09-22 23:09:23 +02:00
wtl
0a6fcf6e95 remove getImageFromIcon 2021-09-22 18:37:33 +03:00
wtl
54e880df47 use unknown icon when one is not available 2021-09-22 14:51:16 +03:00
Piotr Miller
a1bb05c816 Merge pull request #26 from nwg-piotr/logging
fixed formatting
2021-09-19 00:54:09 +02:00
Piotr Miller
7b76c271d9 Merge pull request #25 from nwg-piotr/logging
Logging + 1 bug fixed
2021-09-17 02:52:44 +02:00
piotr
1df4c1a812 fixed formatting 2021-09-17 02:49:48 +02:00
piotr
3768e21b61 bump to 0.1.12 2021-09-17 02:38:09 +02:00
piotr
c1a807615f avoid closing window on item pinned 2021-09-17 02:35:43 +02:00
piotr
b6dd4099fd logging with logrus 2021-09-17 02:33:39 +02:00
Piotr Miller
6ca8e6a175 Merge pull request #24 from nwg-piotr/fix_nodisplay
Fix nodisplay, fix running in terminal
2021-09-07 02:51:26 +02:00
piotr
f12be46583 fix #23 2021-09-07 02:47:17 +02:00
piotr
9d50258042 don't skip hidden entries: fixes #22 added in #19 2021-09-07 02:07:29 +02:00
piotr
1be8e432fa print errors in human-readable format 2021-09-05 01:57:24 +02:00
piotr
aa227b51b8 update README 2021-09-05 01:35:44 +02:00
piotr
56d6cd264d update README 2021-09-05 01:34:57 +02:00
piotr
bea2fdf90c verrsion bump 2021-09-05 01:26:36 +02:00
Piotr Miller
6a0f9d4a93 Merge pull request #20 from james-lawrence/use-terminal-env
use terminal environment variable if present
2021-09-05 01:12:30 +02:00
James Lawrence
206a602e77 use terminal environment variable if present 2021-09-04 15:29:58 -04:00
Piotr Miller
c655f99cd9 Merge pull request #19 from james-lawrence/bugfix/whitespace-handling
Bugfix/whitespace handling
2021-09-01 02:18:26 +02:00
James Lawrence
3a59228eaa remove old parse entry version 2021-08-31 06:33:04 -04:00
James Lawrence
57826c064d improve parsing of desktop entries.
- speed up parsing by not creating nearly as many strings.
- fix basic whitespace handling. fixes #4

goos: linux
goarch: amd64
pkg: github.com/nwg-piotr/nwg-drawer
cpu: AMD Ryzen 7 1800X Eight-Core Processor
BenchmarkDesktopEntryParserOld-16    	   10000	    146094 ns/op
BenchmarkDesktopEntryParser-16       	   26097	     43303 ns/op
PASS
ok  	github.com/nwg-piotr/nwg-drawer	3.090s
2021-08-31 06:32:40 -04:00
Piotr Miller
c5e39f179f Merge pull request #18 from nwg-piotr/searchres
focus first search result #17
2021-08-30 03:17:49 +02:00
piotr
ce48848589 focus first search result #17 2021-08-29 03:37:00 +02:00
12 changed files with 850 additions and 275 deletions

6
.gitignore vendored
View File

@@ -11,5 +11,11 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Binaries built with make
bin
nwg-drawer
/.idea
# Dependency directories (remove the comment below to include it)
# vendor/

View File

@@ -5,11 +5,15 @@ get:
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"
go get github.com/fsnotify/fsnotify
build:
go build -o bin/nwg-drawer *.go
go build -o bin/nwg-drawer .
install:
-pkill -f nwg-drawer
sleep 1
mkdir -p /usr/share/nwg-drawer
cp -r desktop-directories /usr/share/nwg-drawer
cp drawer.css /usr/share/nwg-drawer
@@ -20,4 +24,4 @@ uninstall:
rm /usr/bin/nwg-drawer
run:
go run *.go
go run .

View File

@@ -15,12 +15,19 @@ You may pin applications by right-clicking them. Pinned items will appear above
a pinned item to unpin it. The pinned items cache is shared with [nwg-menu](https://github.com/nwg-piotr/nwg-menu)
and `nwggrid`.
![screenshot-01.png](https://scrot.cloud/images/2021/05/30/screenshot-01.png)
![screenshot.png](https://raw.githubusercontent.com/nwg-piotr/nwg-shell-resources/master/images/nwg-shell/nwg-drawer.png)
[more screenshots](https://scrot.cloud/album/nwg-drawer.Bogd) | [see on YouTube](https://youtu.be/iIgxJQhCQf0)
[see on YouTube](https://youtu.be/iIgxJQhCQf0)
[![Packaging status](https://repology.org/badge/vertical-allrepos/nwg-drawer.svg)](https://repology.org/project/nwg-drawer/versions)
To close the window w/o running a program, you may use `Esc` key, or right-click the window next to the icons.
## v0.2.x note
1. Placing config files in the nwg-panel config directory was a mistake, sorry. The 0.2.0 version migrates them to `~/.config/nwg-drawer`.
2. From now on you may run the program residently, which should speed it up (but also occupy some resources!). See "Running" below.
## Installation
### Dependencies
@@ -33,7 +40,7 @@ and `nwggrid`.
Optional (recommended):
- thunar
- alacritty
- foot
You may use another file manager and terminal emulator (see command line arguments), but mentioned above have been
confirmed to work well with the program. Also see **Files** below.
@@ -45,9 +52,6 @@ confirmed to work well with the program. Also see **Files** below.
3. `make build`
4. `sudo make install`
Building the gotk3 library takes quite a lot of time. If your machine is x86_64, you may skip steps 2-3, and
install the provided binary by executing step 4.
## Command line arguments
```text
@@ -55,12 +59,15 @@ $ nwg-drawer -h
Usage of nwg-drawer:
-c uint
number of Columns (default 6)
-d Turn on Debug messages
-fm string
File Manager (default "thunar")
-fscol uint
File Search result COLumns (default 2)
-fslen int
File Search name length Limit (default 80)
File Search name LENgth Limit (default 80)
-g string
GTK theme name, eg. "Adwaita-dark"
-is int
Icon Size (default 64)
-lang string
@@ -73,6 +80,7 @@ Usage of nwg-drawer:
name of the Output to display the drawer on (sway only)
-ovl
use OVerLay layer
-r Leave the program resident in memory
-s string
Styling: css file name (default "drawer.css")
-spacing uint
@@ -82,20 +90,56 @@ Usage of nwg-drawer:
-v display Version information
```
*NOTE: the `$TERM` environment variable overrides the `-term` argument if defined.*
## Running
Since v0.2.x you may use the drawer in two ways:
1. Simply run the `nwg-drawer` command, by adding a key binding to your sway config file, e.g.:
```text
bindsym Mod1+F1 exec nwg-drawer
```
2. Run a resident instance on startup, and use the `nwg-drawer` command to show the window, e.g.:
```text
exec_always nwg-drawer -r
bindsym Mod1+F1 exec nwg-drawer
```
The second line does nothing but `pkill -USR1 nwg-drawer`, so you may just use this command instead. Actually
this should be a little bit faster.
Running a resident instance should speed up use of the drawer significantly. Pay attention to the fact, that you
need to `pkill -f nwg-drawer` and reload sway to apply any new arguments!
## Logging
In case you encounter an issue, you may need debug messages. If you use the resident instance, you'll see nothing
in the terminal. Please edit your sway config file:
```text
exec nwg-drawer -r -d 2> ~/drawer.log
```
exit sway, launch it again and include the `drawer.log` content in the GitHub issue. Do not use `exec_always` here: it'll destroy the log file content on sway reload.
## Styling
Edit `~/.config/nwg-panel/drawer.css` to your taste.
Edit `~/.config/nwg-drawer/drawer.css` to your taste.
## Files
When the search phrase is at least 3 characters long, your XDG user directories are being searched.
![screenshot-03.png](https://scrot.cloud/images/2021/05/30/screenshot-03.png)
![screenshot-02.png](https://raw.githubusercontent.com/nwg-piotr/nwg-shell/main/images/nwg-drawer/search.png)
Use the **left mouse button** to open a file with the `xdg-open` command. As configuring file associations for it is
PITA, you may override them, by creating the `~/.config/nwg-panel/preferred-apps.json` file with your own definitions.
### Sample file content
### Sample `preferred-apps.json` file content
```json
{
@@ -108,8 +152,8 @@ PITA, you may override them, by creating the `~/.config/nwg-panel/preferred-apps
}
```
Use the **right mouse button** to open the file with your file manager (see `-fm` argument). The result depends on the
file manager you use.
Use the **right mouse button** to open the file with your file manager (see `-fm` argument). The result depends
on the file manager you use.
- thunar will open the file location
- pcmanfm will open the file with its associated program
@@ -117,6 +161,16 @@ file manager you use.
I've noy yet tried other file managers.
### File search exclusions
You may want to exclude some paths inside your XDG user directories from searching. If so, define exclusions in the
`~/.config/nwg-panel/excluded-dirs` file, e.g. like this:
```text
# exclude all paths containing 'node_modules'
node_modules
```
## Credits
This program uses some great libraries:
@@ -126,3 +180,5 @@ 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
- [logrus](https://github.com/sirupsen/logrus) Copyright (c) 2014 Simon Eskildsen
- [fsnotify](https://github.com/fsnotify/fsnotify) Copyright (c) 2012-2019 fsnotify Authors

Binary file not shown.

18
go.mod
View File

@@ -1,10 +1,20 @@
module github.com/nwg-piotr/nwg-drawer
go 1.16
go 1.19
require (
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
github.com/dlasky/gotk3-layershell v0.0.0-20210331230524-5cca0b819261
github.com/gotk3/gotk3 v0.6.0
github.com/joshuarubin/go-sway v0.0.4
github.com/dlasky/gotk3-layershell v0.0.0-20210827021656-e6ecab2731f7
github.com/fsnotify/fsnotify v1.5.1
github.com/gotk3/gotk3 v0.6.1
github.com/joshuarubin/go-sway v1.2.0
github.com/sirupsen/logrus v1.9.0
)
require (
github.com/joshuarubin/lifecycle v1.0.0 // indirect
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect
golang.org/x/sync v0.0.0-20190412183630-56d357773e84 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
)

29
go.sum
View File

@@ -1,24 +1,35 @@
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 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlasky/gotk3-layershell v0.0.0-20210331230524-5cca0b819261 h1:eoXn91ckLWKMXmQKX34UHEF2XMyQpRnnP80fDiu+kys=
github.com/dlasky/gotk3-layershell v0.0.0-20210331230524-5cca0b819261/go.mod h1:d56Gslp3IaiT8lqxD/lO1Msz1wYgD8D/HTKHgSdg9tU=
github.com/gotk3/gotk3 v0.5.3-0.20210223154815-289cfb6dbf32/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/gotk3/gotk3 v0.6.0 h1:Aqlq4/6VabNwtCyA9M9zFNad5yHAqCi5heWnZ9y+3dA=
github.com/gotk3/gotk3 v0.6.0/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/joshuarubin/go-sway v0.0.4 h1:dpmIwQ/LytG+oMrjmaVKdk1aPdW2feXK/+wAcLKIx4A=
github.com/joshuarubin/go-sway v0.0.4/go.mod h1:qcDd6f25vJ0++wICwA1BainIcRC67p2Mb4lsrZ0k3/k=
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-20210827021656-e6ecab2731f7 h1:LDo0kwt+oW9a4lWlj2OakIgMW1ySXXVRGFt8GHUdYYA=
github.com/dlasky/gotk3-layershell v0.0.0-20210827021656-e6ecab2731f7/go.mod h1:JHLx2Wz4mAPVwn4PFhC69ydwyHP4A3wQvlg7HKVVc1U=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/gotk3/gotk3 v0.6.1 h1:GJ400a0ecEEWrzjBvzBzH+pB/esEMIGdB9zPSmBdoeo=
github.com/gotk3/gotk3 v0.6.1/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 h1:N/lPEC8f+dBZ1Tn99vShqp36LwB+LI7XNAiNadZeLUQ=
github.com/joshuarubin/lifecycle v1.0.0/go.mod h1:sRy++ATvR9Ee21tkRdFkQeywAWvDsue66V70K0Dnl54=
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.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
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 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84 h1:IqXQ59gzdXv58Jmm2xn0tSOR9i6HqroaOFRQ3wR/dJQ=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

320
main.go
View File

@@ -3,15 +3,17 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"path"
"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"
@@ -19,16 +21,16 @@ import (
"github.com/gotk3/gotk3/gtk"
)
const version = "0.1.8"
const version = "0.3.6"
var (
appDirs []string
configDirectory string
pinnedFile string
pinned []string
src glib.SourceHandle
id2entry map[string]desktopEntry
preferredApps map[string]interface{}
exclusions []string
)
var categoryNames = [...]string{
@@ -59,6 +61,7 @@ type desktopEntry struct {
CommentLoc string
Icon string
Exec string
Category string
Terminal bool
NoDisplay bool
}
@@ -80,6 +83,7 @@ var desktopEntries []desktopEntry
// UI elements
var (
win *gtk.Window
resultWindow *gtk.ScrolledWindow
fileSearchResults []string
searchEntry *gtk.SearchEntry
@@ -96,73 +100,156 @@ var (
statusLabel *gtk.Label
status string
ignore string
desktopTrigger bool
pinnedItemsChanged chan interface{} = make(chan interface{}, 1)
)
func defaultStringIfBlank(s, fallback string) string {
s = strings.TrimSpace(s)
// os.Getenv("TERM") returns "linux" instead of empty string, if program has been started
// from a key binding defined in the config file. See #23.
if s == "" || s == "linux" {
return fallback
}
return s
}
// Flags
var cssFileName = flag.String("s", "drawer.css", "Styling: css file name")
var targetOutput = flag.String("o", "", "name of the Output to display the drawer on (sway only)")
var displayVersion = flag.Bool("v", false, "display Version information")
var overlay = flag.Bool("ovl", false, "use OVerLay layer")
var gtkTheme = flag.String("g", "", "GTK theme name")
var iconSize = flag.Int("is", 64, "Icon Size")
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 fsColumns = flag.Uint("fscol", 2, "File Search result COLumns")
var columnsNumber = flag.Uint("c", 6, "number of Columns")
var itemSpacing = flag.Uint("spacing", 20, "icon spacing")
var lang = flag.String("lang", "", "force lang, e.g. \"en\", \"pl\"")
var fileManager = flag.String("fm", "thunar", "File Manager")
var term = flag.String("term", "alacritty", "Terminal emulator")
var nameLimit = flag.Int("fslen", 80, "File Search name length Limit")
var term = flag.String("term", defaultStringIfBlank(os.Getenv("TERM"), "foot"), "Terminal emulator")
var nameLimit = flag.Int("fslen", 80, "File Search name LENgth Limit")
var noCats = flag.Bool("nocats", false, "Disable filtering by category")
var noFS = flag.Bool("nofs", false, "Disable file search")
var resident = flag.Bool("r", false, "Leave the program resident in memory")
var debug = flag.Bool("d", false, "Turn on Debug messages")
func main() {
timeStart := time.Now()
flag.Parse()
if *debug {
log.SetLevel(log.DebugLevel)
}
if *displayVersion {
fmt.Printf("nwg-drawer version %s\n", version)
os.Exit(0)
}
// Gentle SIGTERM handler thanks to reiki4040 https://gist.github.com/reiki4040/be3705f307d3cd136e85
// v0.2: we also need to support SIGUSR from now on
showWindowChannel := make(chan interface{}, 1)
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGTERM)
signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGUSR1)
go func() {
for {
s := <-signalChan
if s == syscall.SIGTERM {
println("SIGTERM received, bye bye!")
switch s {
case syscall.SIGTERM:
log.Info("SIGTERM received, bye bye")
gtk.MainQuit()
case syscall.SIGUSR1:
if *resident {
// 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")
showWindowChannel <- struct{}{}
} else {
log.Debug("SIGUSR1 received, hiding the window")
restoreStateAndHide()
}
} else {
log.Info("SIGUSR1 received, and I'm not resident, bye bye")
gtk.MainQuit()
}
default:
log.Info("Unknown signal")
}
}
}()
// We want the same key/mouse binding to turn the dock off: kill the running instance and exit.
lockFilePath := fmt.Sprintf("%s/nwg-drawer.lock", tempDir())
// If running instance found, we want it to show the window. The new instance will send SIGUSR1 and die
// (equivalent of `pkill -USR1 nwg-drawer`).
// Otherwise the command may behave in two ways:
// 1. kill the running non-residennt instance and exit;
// 2. die if a resident instance found.
lockFilePath := path.Join(dataDir(), "nwg-drawer.lock")
lockFile, err := singleinstance.CreateLockFile(lockFilePath)
if err != nil {
pid, err := readTextFile(lockFilePath)
if err == nil {
i, err := strconv.Atoi(pid)
if err == nil {
println("Running instance found, sending SIGTERM and exiting...")
syscall.Kill(i, syscall.SIGTERM)
if *resident {
log.Warnf("Resident instance already running (PID %v)", i)
} else {
log.Infof("Showing resident instance (PID %v)", i)
err := syscall.Kill(i, syscall.SIGUSR1)
if err != nil {
return
}
}
}
}
os.Exit(0)
}
defer lockFile.Close()
log.Infof("term: %s", *term)
// LANGUAGE
if *lang == "" && os.Getenv("LANG") != "" {
*lang = strings.Split(os.Getenv("LANG"), ".")[0]
}
println(fmt.Sprintf("lang: %s", *lang))
log.Info(fmt.Sprintf("lang: %s", *lang))
// ENVIRONMENT
configDirectory = configDir()
// Placing the drawer config files in the nwg-panel config directory was a mistake.
// Let's move them to their own location.
oldConfigDirectory, err := oldConfigDir()
if err == nil {
for _, p := range []string{"drawer.css", "preferred-apps.json"} {
if pathExists(path.Join(oldConfigDirectory, p)) {
log.Infof("File %s found in stale location, moving to %s", p, configDirectory)
if !pathExists(path.Join(configDirectory, p)) {
err = os.Rename(path.Join(oldConfigDirectory, p), path.Join(configDirectory, p))
if err == nil {
log.Info("Success")
} else {
log.Warn(err)
}
} else {
log.Warnf("Failed moving %s to %s: path already exists!", path.Join(oldConfigDirectory, p), path.Join(configDirectory, p))
}
}
}
}
// Copy default style sheet if not found
if !pathExists(filepath.Join(configDirectory, "drawer.css")) {
copyFile(filepath.Join(getDataHome(), "nwg-drawer/drawer.css"), filepath.Join(configDirectory, "drawer.css"))
err := copyFile("/usr/share/nwg-drawer/drawer.css", filepath.Join(configDirectory, "drawer.css"))
if err != nil {
log.Errorf("Failed copying 'drawer.css' file: %s", err)
}
}
cacheDirectory := cacheDir()
@@ -175,47 +262,83 @@ func main() {
pinned, err = loadTextFile(pinnedFile)
if err != nil {
pinned = nil
savePinned()
}
println(fmt.Sprintf("Found %v pinned items", len(pinned)))
log.Info(fmt.Sprintf("Found %v pinned items", len(pinned)))
cssFile := filepath.Join(configDirectory, *cssFileName)
if !strings.HasPrefix(*cssFileName, "/") {
*cssFileName = filepath.Join(configDirectory, *cssFileName)
}
appDirs = getAppDirs()
setUpCategories()
desktopFiles := listDesktopFiles()
println(fmt.Sprintf("Found %v desktop files", len(desktopFiles)))
log.Info(fmt.Sprintf("Found %v desktop files", len(desktopFiles)))
status = parseDesktopFiles(desktopFiles)
// For opening files we use xdg-open. As its configuration is PITA, we may override some associations
// in the ~/.config/nwg-panel/preferred-apps.json file.
paFile := filepath.Join(configDirectory, "preferred-apps.json")
preferredApps, err = loadPreferredApps(paFile)
if err != nil {
println(fmt.Sprintf("Custom associations file %s not found or invalid", paFile))
paFile := path.Join(configDirectory, "preferred-apps.json")
if pathExists(paFile) {
preferredApps, err = loadPreferredApps(paFile)
if err != nil {
log.Infof("Custom associations file %s not found or invalid", paFile)
} else {
log.Infof("Found %v associations in %s", len(preferredApps), paFile)
}
} else {
println(fmt.Sprintf("Found %v associations in %s", len(preferredApps), paFile))
fmt.Println(preferredApps)
log.Infof("%s file not found", paFile)
}
// Load user-defined paths excluded from file search
exFile := path.Join(configDirectory, "excluded-dirs")
if pathExists(exFile) {
exclusions, err = loadTextFile(exFile)
if err != nil {
log.Infof("Search exclusions file %s not found %s", exFile, err)
} else {
log.Infof("Found %v search exclusions in %s", len(exclusions), exFile)
}
} else {
log.Infof("%s file not found", exFile)
}
// USER INTERFACE
gtk.Init(nil)
settings, _ := gtk.SettingsGetDefault()
if *gtkTheme != "" {
err = settings.SetProperty("gtk-theme-name", *gtkTheme)
if err != nil {
log.Error("Unable to set theme:", err)
} else {
log.Infof("User demanded theme: %s", *gtkTheme)
}
} else {
err := settings.SetProperty("gtk-application-prefer-dark-theme", true)
if err != nil {
log.Error("Error setting 'gtk-application-prefer-dark-theme' property")
return
}
log.Info("Preferring dark theme variants")
}
cssProvider, _ := gtk.CssProviderNew()
err = cssProvider.LoadFromPath(cssFile)
err = cssProvider.LoadFromPath(*cssFileName)
if err != nil {
println(fmt.Sprintf("ERROR: %s css file not found or erroneous. Using GTK styling.", cssFile))
println(fmt.Sprintf(">>> %s", err))
log.Errorf("ERROR: %s css file not found or erroneous. Using GTK styling.", *cssFileName)
log.Errorf("%s", err)
} else {
println(fmt.Sprintf("Using style from %s", cssFile))
log.Info(fmt.Sprintf("Using style from %s", *cssFileName))
screen, _ := gdk.ScreenGetDefault()
gtk.AddProviderForScreen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
}
win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
win, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
if err != nil {
log.Fatal("Unable to create window:", err)
}
@@ -232,7 +355,7 @@ func main() {
layershell.SetMonitor(win, monitor)
} else {
println(err)
log.Errorf("%s", err)
}
}
@@ -248,6 +371,11 @@ func main() {
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.SetKeyboardMode(win, layershell.LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE)
}
@@ -255,49 +383,47 @@ func main() {
gtk.MainQuit()
})
win.Connect("key-press-event", func(window *gtk.Window, event *gdk.Event) bool {
win.Connect("key-release-event", func(window *gtk.Window, event *gdk.Event) bool {
key := &gdk.EventKey{Event: event}
switch key.KeyVal() {
case gdk.KEY_Escape:
if key.KeyVal() == gdk.KEY_Escape {
s, _ := searchEntry.GetText()
if s != "" {
searchEntry.GrabFocus()
searchEntry.SetText("")
} else {
gtk.MainQuit()
if !*resident {
gtk.MainQuit()
} else {
restoreStateAndHide()
}
}
return false
return true
}
return false
})
win.Connect("key-press-event", func(window *gtk.Window, event *gdk.Event) bool {
key := &gdk.EventKey{Event: event}
switch key.KeyVal() {
case gdk.KEY_downarrow, gdk.KEY_Up, gdk.KEY_Down, gdk.KEY_Left, gdk.KEY_Right, gdk.KEY_Tab,
gdk.KEY_Return, gdk.KEY_Page_Up, gdk.KEY_Page_Down, gdk.KEY_Home, gdk.KEY_End:
return false
default:
if !searchEntry.IsFocus() {
searchEntry.GrabFocus()
searchEntry.GrabFocusWithoutSelecting()
}
return false
}
})
// Close the window on leave, but not immediately, to avoid accidental closes
win.Connect("leave-notify-event", func() {
src = glib.TimeoutAdd(uint(500), func() bool {
gtk.MainQuit()
return false
})
})
win.Connect("enter-notify-event", func() {
cancelClose()
})
/*
In case someone REALLY needed to use X11 - for some stupid Zoom meeting or something, this allows
the drawer to behave properly on Openbox, and possibly somewhere else. For sure not on i3.
This feature is not really supported and will stay undocumented.
*/
if !wayland() {
println("Not Wayland, oh really?")
log.Info("Not Wayland, oh really?")
win.SetDecorated(false)
win.Maximize()
}
@@ -330,13 +456,15 @@ func main() {
resultWindow, _ = gtk.ScrolledWindowNew(nil, nil)
resultWindow.SetEvents(int(gdk.ALL_EVENTS_MASK))
resultWindow.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
resultWindow.Connect("enter-notify-event", func() {
cancelClose()
})
resultWindow.Connect("button-release-event", func(sw *gtk.ScrolledWindow, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
if btnEvent.Button() == 1 || btnEvent.Button() == 3 {
gtk.MainQuit()
if btnEvent.Button() == 3 {
if !*resident {
gtk.MainQuit()
} else {
restoreStateAndHide()
}
return true
}
return false
@@ -362,6 +490,7 @@ func main() {
}
userDirsMap = mapXdgUserDirs()
log.Debugf("User dirs map: %s", userDirsMap)
placeholder, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
resultsWrapper.PackStart(placeholder, true, true, 0)
@@ -381,6 +510,7 @@ func main() {
statusLineWrapper.PackStart(statusLabel, true, false, 0)
win.ShowAll()
if !*noFS {
fileSearchResultWrapper.SetSizeRequest(appFlowBox.GetAllocatedWidth(), 1)
fileSearchResultWrapper.Hide()
@@ -388,8 +518,90 @@ func main() {
if !*noCats {
categoriesWrapper.SetSizeRequest(1, categoriesWrapper.GetAllocatedHeight()*2)
}
if *resident {
win.Hide()
}
t := time.Now()
println(fmt.Sprintf("UI created in %v ms. Thank you for your patience.", t.Sub(timeStart).Milliseconds()))
log.Info(fmt.Sprintf("UI created in %v ms. Thank you for your patience.", t.Sub(timeStart).Milliseconds()))
// Check if showing the window has been requested (SIGUSR1)
go func() {
for {
select {
case <-showWindowChannel:
log.Debug("Showing window")
glib.TimeoutAdd(0, func() bool {
if win != nil && !win.IsVisible() {
// Refresh files before displaying the root window
// some .desktop file changed
if desktopTrigger {
log.Debug(".desktop file changed")
desktopFiles = listDesktopFiles()
status = parseDesktopFiles(desktopFiles)
appFlowBox = setUpAppsFlowBox(nil, "")
desktopTrigger = false
}
// Show window and focus the search box
win.ShowAll()
if fileSearchResultWrapper != nil {
fileSearchResultWrapper.Hide()
}
// focus 1st element
var button gtk.IWidget
if pinnedFlowBox.GetChildren().Length() > 0 {
button, err = pinnedFlowBox.GetChildAtIndex(0).GetChild()
} else {
button, err = appFlowBox.GetChildAtIndex(0).GetChild()
}
if err == nil {
button.ToWidget().GrabFocus()
}
}
return false
})
case <-pinnedItemsChanged:
glib.TimeoutAdd(0, func() bool {
log.Debug("pinned file changed")
pinned, _ = loadTextFile(pinnedFile)
pinnedFlowBox = setUpPinnedFlowBox()
return false
})
}
}
}()
go watchFiles()
gtk.Main()
}
func restoreStateAndHide() {
timeStart1 := time.Now()
if win != nil {
win.Hide()
}
// clear search
searchEntry.SetText("")
// clear category filter (in gotk3 it means: rebuild, as we have no filtering here)
appFlowBox = setUpAppsFlowBox(nil, "")
for _, btn := range catButtons {
btn.SetImagePosition(gtk.POS_LEFT)
btn.SetSizeRequest(0, 0)
}
// scroll to the top
if resultWindow != nil {
resultWindow.GetVAdjustment().SetValue(0)
}
t := time.Now()
log.Debugf(fmt.Sprintf("UI hidden and restored in the backgroud in %v ms", t.Sub(timeStart1).Milliseconds()))
}

327
tools.go
View File

@@ -7,18 +7,19 @@ import (
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"syscall"
"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"
)
@@ -27,19 +28,6 @@ func wayland() bool {
return os.Getenv("WAYLAND_DISPLAY") != "" || os.Getenv("XDG_SESSION_TYPE") == "wayland"
}
/*
Window leave-notify-event event quits the program with glib Timeout 500 ms.
We might have left the window by accident, so let's clear the timeout if window re-entered.
Furthermore - hovering a widget triggers window leave-notify-event event, and the timeout
needs to be cleared as well.
*/
func cancelClose() {
if src > 0 {
glib.SourceRemove(src)
src = 0
}
}
func createPixbuf(icon string, size int) (*gdk.Pixbuf, error) {
iconTheme, err := gtk.IconThemeGetDefault()
if err != nil {
@@ -49,7 +37,7 @@ func createPixbuf(icon string, size int) (*gdk.Pixbuf, error) {
if strings.Contains(icon, "/") {
pixbuf, err := gdk.PixbufNewFromFileAtSize(icon, size, size)
if err != nil {
println(err)
log.Errorf("%s", err)
return nil, err
}
return pixbuf, nil
@@ -90,9 +78,10 @@ func mapXdgUserDirs() map[string]string {
result["pictures"] = filepath.Join(home, "Pictures")
result["videos"] = filepath.Join(home, "Videos")
userDirsFile := filepath.Join(home, ".config/user-dirs.dirs")
userDirsFile := filepath.Join(filepath.Join(os.Getenv("XDG_CONFIG_HOME"), "user-dirs.dirs"))
if pathExists(userDirsFile) {
println(fmt.Sprintf("Using XDG user dirs from %s", userDirsFile))
log.Debugf("userDirsFile found: %s", userDirsFile)
log.Info(fmt.Sprintf("Using XDG user dirs from %s", userDirsFile))
lines, _ := loadTextFile(userDirsFile)
for _, l := range lines {
if strings.HasPrefix(l, "XDG_DOCUMENTS_DIR") {
@@ -116,7 +105,7 @@ func mapXdgUserDirs() map[string]string {
}
}
} else {
println(fmt.Sprintf("%s file not found, using defaults", userDirsFile))
log.Warnf("userDirsFile %s not found, using defaults", userDirsFile)
}
return result
@@ -142,19 +131,8 @@ func cacheDir() string {
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 := ioutil.ReadFile(path)
bytes, err := os.ReadFile(path)
if err != nil {
return "", err
}
@@ -162,14 +140,43 @@ func readTextFile(path string) (string, error) {
return string(bytes), nil
}
func configDir() string {
func oldConfigDir() (string, error) {
if os.Getenv("XDG_CONFIG_HOME") != "" {
dir := fmt.Sprintf("%s/nwg-panel", os.Getenv("XDG_CONFIG_HOME"))
createDir(dir)
return (fmt.Sprintf("%s/nwg-panel", os.Getenv("XDG_CONFIG_HOME")))
dir := path.Join(os.Getenv("XDG_CONFIG_HOME"), "nwg-panel")
return dir, nil
} else if os.Getenv("HOME") != "" {
dir := path.Join(os.Getenv("HOME"), ".config/nwg-panel")
return dir, nil
}
dir := fmt.Sprintf("%s/.config/nwg-panel", os.Getenv("HOME"))
return "", errors.New("old config dir not found")
}
func configDir() string {
var dir string
if os.Getenv("XDG_CONFIG_HOME") != "" {
dir = path.Join(os.Getenv("XDG_CONFIG_HOME"), "nwg-drawer")
} else if os.Getenv("HOME") != "" {
dir = path.Join(os.Getenv("HOME"), ".config/nwg-drawer")
}
log.Infof("Config dir: %s", dir)
createDir(dir)
return dir
}
func dataDir() string {
var dir string
if os.Getenv("XDG_DATA_HOME") != "" {
dir = path.Join(os.Getenv("XDG_DATA_HOME"), "nwg-drawer")
} else if os.Getenv("HOME") != "" {
dir = path.Join(os.Getenv("HOME"), ".local/share/nwg-drawer")
}
log.Infof("Data dir: %s", dir)
createDir(dir)
return dir
}
@@ -177,13 +184,13 @@ func createDir(dir string) {
if _, err := os.Stat(dir); os.IsNotExist(err) {
err := os.MkdirAll(dir, os.ModePerm)
if err == nil {
fmt.Println("Creating dir:", dir)
log.Infof("Creating dir: %s", dir)
}
}
}
func copyFile(src, dst string) error {
fmt.Println("Copying file:", dst)
log.Infof("Copying file: %s", dst)
var err error
var srcfd *os.File
@@ -193,12 +200,22 @@ func copyFile(src, dst string) error {
if srcfd, err = os.Open(src); err != nil {
return err
}
defer srcfd.Close()
defer func(srcfd *os.File) {
err := srcfd.Close()
if err != nil {
log.Errorf("Error closing file: %v", srcfd)
}
}(srcfd)
if dstfd, err = os.Create(dst); err != nil {
return err
}
defer dstfd.Close()
defer func(dstfd *os.File) {
err := dstfd.Close()
if err != nil {
log.Errorf("Error closing file: %v", dstfd)
}
}(dstfd)
if _, err = io.Copy(dstfd, srcfd); err != nil {
return err
@@ -209,13 +226,6 @@ func copyFile(src, dst string) error {
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 := ""
@@ -239,11 +249,17 @@ func getAppDirs() []string {
"/var/lib/flatpak/exports/share/applications"}
for _, d := range flatpakDirs {
if !isIn(dirs, d) {
if pathExists(d) && !isIn(dirs, d) {
dirs = append(dirs, d)
}
}
return dirs
var confirmedDirs []string
for _, d := range dirs {
if pathExists(d) {
confirmedDirs = append(confirmedDirs, d)
}
}
return confirmedDirs
}
func loadPreferredApps(path string) (map[string]interface{}, error) {
@@ -251,12 +267,20 @@ func loadPreferredApps(path string) (map[string]interface{}, error) {
if err != nil {
return nil, err
}
defer jsonFile.Close()
defer func(jsonFile *os.File) {
err := jsonFile.Close()
if err != nil {
log.Errorf("Error closing file: %v", jsonFile)
}
}(jsonFile)
byteValue, _ := ioutil.ReadAll(jsonFile)
byteValue, _ := io.ReadAll(jsonFile)
var result map[string]interface{}
json.Unmarshal([]byte(byteValue), &result)
err = json.Unmarshal(byteValue, &result)
if err != nil {
return nil, err
}
if len(result) == 0 {
return nil, errors.New("json invalid or empty")
@@ -265,8 +289,8 @@ func loadPreferredApps(path string) (map[string]interface{}, error) {
return result, nil
}
func listFiles(dir string) ([]fs.FileInfo, error) {
files, err := ioutil.ReadDir(dir)
func listFiles(dir string) ([]fs.DirEntry, error) {
files, err := os.ReadDir(dir)
if err == nil {
return files, nil
}
@@ -290,12 +314,12 @@ func listDesktopFiles() []string {
}
func setUpCategories() {
path := filepath.Join(getDataHome(), "nwg-drawer/desktop-directories")
var other category
for _, cName := range categoryNames {
fileName := fmt.Sprintf("%s.directory", cName)
lines, err := loadTextFile(filepath.Join(path, fileName))
fp := filepath.Join("/usr/share/nwg-drawer/desktop-directories", fileName)
lines, err := loadTextFile(fp)
if err == nil {
var cat category
cat.Name = cName
@@ -340,6 +364,8 @@ func setUpCategories() {
} else {
other = cat
}
} else {
log.Errorf("Couldn't open %s", fp)
}
}
sort.Slice(categories, func(i, j int) bool {
@@ -349,110 +375,37 @@ func setUpCategories() {
}
func parseDesktopFiles(desktopFiles []string) string {
desktopEntries = nil
id2entry = make(map[string]desktopEntry)
var added []string
skipped := 0
hidden := 0
for _, file := range desktopFiles {
lines, err := loadTextFile(file)
if err == nil {
parts := strings.Split(file, "/")
desktopID := parts[len(parts)-1]
name := ""
nameLoc := ""
comment := ""
commentLoc := ""
icon := ""
exec := ""
terminal := false
noDisplay := false
categories := ""
for _, l := range lines {
if strings.HasPrefix(l, "[") && l != "[Desktop Entry]" {
break
}
if strings.HasPrefix(l, "Name=") {
name = strings.Split(l, "=")[1]
continue
}
if strings.HasPrefix(l, fmt.Sprintf("Name[%s]=", strings.Split(*lang, "_")[0])) {
nameLoc = strings.Split(l, "=")[1]
continue
}
if strings.HasPrefix(l, "Comment=") {
comment = strings.Split(l, "=")[1]
continue
}
if strings.HasPrefix(l, fmt.Sprintf("Comment[%s]=", strings.Split(*lang, "_")[0])) {
commentLoc = strings.Split(l, "=")[1]
continue
}
if strings.HasPrefix(l, "Icon=") {
icon = strings.Split(l, "=")[1]
continue
}
if strings.HasPrefix(l, "Exec=") {
exec = strings.Split(l, "Exec=")[1]
disallowed := [2]string{"\"", "'"}
for _, char := range disallowed {
exec = strings.Replace(exec, char, "", -1)
}
continue
}
if strings.HasPrefix(l, "Categories=") {
categories = strings.Split(l, "Categories=")[1]
continue
}
if l == "Terminal=true" {
terminal = true
continue
}
if l == "NoDisplay=true" {
noDisplay = true
hidden++
continue
}
}
// if name[ln] not found, let's try to find name[ln_LN]
if nameLoc == "" {
nameLoc = name
}
if commentLoc == "" {
commentLoc = comment
}
if !isIn(added, desktopID) {
added = append(added, desktopID)
var entry desktopEntry
entry.DesktopID = desktopID
entry.Name = name
entry.NameLoc = nameLoc
entry.Comment = comment
entry.CommentLoc = commentLoc
entry.Icon = icon
entry.Exec = exec
entry.Terminal = terminal
entry.NoDisplay = noDisplay
desktopEntries = append(desktopEntries, entry)
id2entry[entry.DesktopID] = entry
assignToLists(entry.DesktopID, categories)
} else {
skipped++
}
id := filepath.Base(file)
if _, ok := id2entry[id]; ok {
skipped++
continue
}
entry, err := parseDesktopEntryFile(id, file)
if err != nil {
continue
}
if entry.NoDisplay {
hidden++
// We still need hidden entries, so `continue` is disallowed here
// Fixes bug introduced in #19
}
id2entry[entry.DesktopID] = entry
desktopEntries = append(desktopEntries, entry)
assignToLists(entry.DesktopID, entry.Category)
}
sort.Slice(desktopEntries, func(i, j int) bool {
return desktopEntries[i].NameLoc < desktopEntries[j].NameLoc
})
summary := fmt.Sprintf("%v entries (+%v hidden)", len(desktopEntries)-hidden, hidden)
println(fmt.Sprintf("Skipped %v duplicates; %v .desktop entries hidden by \"NoDisplay=true\"", skipped, hidden))
log.Infof("Skipped %v duplicates; %v .desktop entries hidden by \"NoDisplay=true\"", skipped, hidden)
return summary
}
@@ -526,7 +479,7 @@ func pathExists(name string) bool {
}
func loadTextFile(path string) ([]string, error) {
bytes, err := ioutil.ReadFile(path)
bytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}
@@ -534,7 +487,7 @@ func loadTextFile(path string) ([]string, error) {
var output []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
if line != "" && !strings.HasPrefix(line, "#") {
output = append(output, line)
}
@@ -545,20 +498,20 @@ func loadTextFile(path string) ([]string, error) {
func pinItem(itemID string) {
for _, item := range pinned {
if item == itemID {
println(item, "already pinned")
log.Warnf("%s already pinned", itemID)
return
}
}
pinned = append(pinned, itemID)
savePinned()
println(itemID, "pinned")
log.Infof("%s pinned", itemID)
}
func unpinItem(itemID string) {
if isIn(pinned, itemID) {
pinned = remove(pinned, itemID)
savePinned()
println(itemID, "unpinned")
log.Infof("%s unpinned", itemID)
}
}
@@ -577,14 +530,20 @@ func savePinned() {
log.Fatal(err)
}
defer f.Close()
defer func(f *os.File) {
err := f.Close()
if err != nil {
log.Errorf("Error closing file: %v", f)
}
}(f)
for _, line := range pinned {
if line != "" {
//skip invalid lines
if line != "" && id2entry[line].DesktopID != "" {
_, err := f.WriteString(line + "\n")
if err != nil {
println("Error saving pinned", err)
log.Error("Error saving pinned", err)
}
}
}
@@ -623,7 +582,13 @@ func launch(command string, terminal bool) {
cmd := exec.Command(elements[cmdIdx], elements[1+cmdIdx:]...)
if terminal {
args := []string{"-e", elements[cmdIdx]}
var args []string
if *term != "foot" {
args = []string{"-e", elements[cmdIdx]}
} else {
args = []string{elements[cmdIdx]}
}
cmd = exec.Command(*term, args...)
}
@@ -634,14 +599,27 @@ func launch(command string, terminal bool) {
}
msg := fmt.Sprintf("env vars: %s; command: '%s'; args: %s\n", envVars, elements[cmdIdx], elements[1+cmdIdx:])
println(msg)
log.Info(msg)
go cmd.Run()
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
glib.TimeoutAdd(uint(150), func() bool {
if cmd.Start() != nil {
log.Warn("Unable to launch terminal emulator!")
} else {
// Collect the exit code of the child process to prevent zombies
// if the drawer runs in resident mode
go func() {
_ = cmd.Wait()
}()
}
if *resident {
restoreStateAndHide()
} else {
gtk.MainQuit()
return false
})
}
}
func open(filePath string, xdgOpen bool) {
@@ -659,10 +637,23 @@ func open(filePath string, xdgOpen bool) {
} else {
cmd = exec.Command(*fileManager, filePath)
}
fmt.Printf("Executing: %s", cmd)
cmd.Start()
log.Infof("Executing: %s", cmd)
gtk.MainQuit()
if cmd.Start() != nil {
log.Warn("Unable to execute command!")
} else {
// Collect the exit code of the child process to prevent zombies
// if the drawer runs in resident mode
go func() {
_ = cmd.Wait()
}()
}
if *resident {
restoreStateAndHide()
} else {
gtk.MainQuit()
}
}
// Returns map output name -> gdk.Monitor

View File

@@ -6,6 +6,8 @@ import (
"path/filepath"
"strings"
log "github.com/sirupsen/logrus"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk"
)
@@ -30,6 +32,10 @@ func setUpPinnedFlowBox() *gtk.FlowBox {
if len(pinned) > 0 {
for _, desktopID := range pinned {
entry := id2entry[desktopID]
if entry.DesktopID == "" {
log.Debugf("Pinned item doesn't seem to exist: %s", desktopID)
continue
}
btn, _ := gtk.ButtonNew()
@@ -65,7 +71,6 @@ func setUpPinnedFlowBox() *gtk.FlowBox {
return true
} else if btnEvent.Button() == 3 {
unpinItem(entry.DesktopID)
pinnedFlowBox = setUpPinnedFlowBox()
return true
}
return false
@@ -76,6 +81,9 @@ func setUpPinnedFlowBox() *gtk.FlowBox {
btn.Connect("enter-notify-event", func() {
statusLabel.SetText(entry.CommentLoc)
})
btn.Connect("focus-in-event", func() {
statusLabel.SetText(entry.CommentLoc)
})
flowBox.Add(btn)
}
pinnedFlowBoxWrapper.PackStart(flowBox, true, false, 0)
@@ -85,10 +93,6 @@ func setUpPinnedFlowBox() *gtk.FlowBox {
item.(*gtk.Widget).SetCanFocus(false)
})
}
flowBox.Connect("enter-notify-event", func() {
cancelClose()
})
flowBox.ShowAll()
return flowBox
@@ -108,9 +112,7 @@ func setUpCategoriesButtonBox() *gtk.EventBox {
}
eventBox, _ := gtk.EventBoxNew()
eventBox.Connect("enter-notify-event", func() {
cancelClose()
})
hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
eventBox.Add(hBox)
button, _ := gtk.ButtonNewWithLabel("All")
@@ -131,7 +133,6 @@ func setUpCategoriesButtonBox() *gtk.EventBox {
button.SetProperty("name", "category-button")
catButtons = append(catButtons, button)
button.SetLabel(cat.DisplayName)
// fix #8
button.SetAlwaysShowImage(true)
hBox.PackStart(button, false, false, 0)
name := cat.Name
@@ -233,20 +234,26 @@ func flowBoxButton(entry desktopEntry) *gtk.Button {
button, _ := gtk.ButtonNew()
button.SetAlwaysShowImage(true)
var pixbuf *gdk.Pixbuf
var img *gtk.Image
var err error
if entry.Icon != "" {
pixbuf, _ := createPixbuf(entry.Icon, *iconSize)
img, _ = gtk.ImageNewFromPixbuf(pixbuf)
pixbuf, err = createPixbuf(entry.Icon, *iconSize)
} else {
img, _ = gtk.ImageNewFromIconName("image-missing", gtk.ICON_SIZE_INVALID)
log.Warnf("Undefined icon for %s", entry.Name)
pixbuf, err = createPixbuf("image-missing", *iconSize)
}
if err != nil {
pixbuf, _ = createPixbuf("unknown", *iconSize)
}
img, _ = gtk.ImageNewFromPixbuf(pixbuf)
button.SetImage(img)
button.SetImagePosition(gtk.POS_TOP)
name := entry.NameLoc
if len(name) > 20 {
r := []rune(name)
name = string(r[:17])
r := []rune(name[:17])
name = string(r)
name = fmt.Sprintf("%s…", name)
}
button.SetLabel(name)
@@ -255,6 +262,11 @@ func flowBoxButton(entry desktopEntry) *gtk.Button {
exec := entry.Exec
terminal := entry.Terminal
desc := entry.CommentLoc
if len(desc) > 120 {
r := []rune(desc[:117])
desc = string(r)
desc = fmt.Sprintf("%s…", desc)
}
button.Connect("button-release-event", func(btn *gtk.Button, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
if btnEvent.Button() == 1 {
@@ -262,7 +274,7 @@ func flowBoxButton(entry desktopEntry) *gtk.Button {
return true
} else if btnEvent.Button() == 3 {
pinItem(ID)
pinnedFlowBox = setUpPinnedFlowBox()
return true
}
return false
})
@@ -272,6 +284,9 @@ func flowBoxButton(entry desktopEntry) *gtk.Button {
button.Connect("enter-notify-event", func() {
statusLabel.SetText(desc)
})
button.Connect("focus-in-event", func() {
statusLabel.SetText(desc)
})
return button
}
@@ -281,9 +296,6 @@ func setUpFileSearchResultContainer() *gtk.FlowBox {
}
flowBox, _ := gtk.FlowBoxNew()
flowBox.SetProperty("orientation", gtk.ORIENTATION_VERTICAL)
flowBox.Connect("enter-notify-event", func() {
cancelClose()
})
fileSearchResultWrapper.PackStart(flowBox, false, false, 10)
return flowBox
@@ -295,7 +307,19 @@ func walk(path string, d fs.DirEntry, e error) error {
}
// don't search leading part of the path, as e.g. '/home/user/Pictures'
toSearch := strings.Split(path, ignore)[1]
if strings.Contains(strings.ToLower(toSearch), strings.ToLower(phrase)) {
// Remaing part of the path (w/o file name) must be checked against being present in excluded dirs
doSearch := true
parts := strings.Split(toSearch, "/")
remainingPart := ""
if len(parts) > 1 {
remainingPart = strings.Join(parts[:len(parts)-1], "/")
}
if remainingPart != "" && isExcluded(remainingPart) {
doSearch = false
}
if doSearch && strings.Contains(strings.ToLower(toSearch), strings.ToLower(phrase)) {
// mark directories
if d.IsDir() {
fileSearchResults = append(fileSearchResults, fmt.Sprintf("#is_dir#%s", path))
@@ -303,15 +327,16 @@ func walk(path string, d fs.DirEntry, e error) error {
fileSearchResults = append(fileSearchResults, path)
}
}
return nil
}
func setUpSearchEntry() *gtk.SearchEntry {
searchEntry, _ := gtk.SearchEntryNew()
searchEntry.SetPlaceholderText("Type to search")
searchEntry.Connect("enter-notify-event", func() {
/*searchEntry.Connect("enter-notify-event", func() {
cancelClose()
})
})*/
searchEntry.Connect("search-changed", func() {
for _, btn := range catButtons {
btn.SetImagePosition(gtk.POS_LEFT)
@@ -351,6 +376,28 @@ func setUpSearchEntry() *gtk.SearchEntry {
fileSearchResultWrapper.Hide()
}
}
// focus 1st search result #17
var w *gtk.Widget
if appFlowBox != nil {
b := appFlowBox.GetChildAtIndex(0)
if b != nil {
button, err := b.GetChild()
if err == nil {
button.ToWidget().GrabFocus()
w = button.ToWidget()
}
}
}
if w == nil && fileSearchResultFlowBox != nil {
f := fileSearchResultFlowBox.GetChildAtIndex(0)
if f != nil {
button, err := f.GetChild()
if err == nil {
button.ToWidget().SetCanFocus(true)
button.ToWidget().GrabFocus()
}
}
}
} else {
// clear search results
appFlowBox = setUpAppsFlowBox(nil, "")
@@ -364,13 +411,19 @@ func setUpSearchEntry() *gtk.SearchEntry {
}
}
})
searchEntry.Connect("focus-in-event", func() {
searchEntry.SetText("")
})
return searchEntry
}
func isExcluded(dir string) bool {
for _, exclusion := range exclusions {
if strings.Contains(dir, exclusion) {
return true
}
}
return false
}
func searchUserDir(dir string) {
fileSearchResults = nil
ignore = userDirsMap[dir]
@@ -381,10 +434,14 @@ func searchUserDir(dir string) {
fileSearchResultFlowBox.Add(btn)
for _, path := range fileSearchResults {
log.Debugf("Path: %s", path)
partOfPathToShow := strings.Split(path, userDirsMap[dir])[1]
if partOfPathToShow != "" {
btn := setUpUserFileSearchResultButton(partOfPathToShow, path)
fileSearchResultFlowBox.Add(btn)
if !(strings.HasPrefix(path, "#is_dir#") && isExcluded(path)) {
btn := setUpUserFileSearchResultButton(partOfPathToShow, path)
fileSearchResultFlowBox.Add(btn)
}
}
}
fileSearchResultFlowBox.Hide()
@@ -409,6 +466,7 @@ func setUpUserDirButton(iconName, displayName, entryName string, userDirsMap map
}
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
button, _ := gtk.ButtonNew()
button.SetAlwaysShowImage(true)
img, _ := gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_MENU)
button.SetImage(img)
@@ -441,6 +499,7 @@ func setUpUserFileSearchResultButton(fileName, filePath string) *gtk.Box {
if strings.HasPrefix(filePath, "#is_dir#") {
filePath = filePath[8:]
img, _ := gtk.ImageNewFromIconName("folder", gtk.ICON_SIZE_MENU)
button.SetAlwaysShowImage(true)
button.SetImage(img)
}
@@ -466,6 +525,10 @@ func setUpUserFileSearchResultButton(fileName, filePath string) *gtk.Box {
return false
})
button.Connect("activate", func() {
open(filePath, true)
})
box.PackStart(button, false, true, 0)
return box
}

64
watcher.go Normal file
View File

@@ -0,0 +1,64 @@
package main
import (
"os"
"path/filepath"
"strings"
log "github.com/sirupsen/logrus"
"github.com/fsnotify/fsnotify"
)
// Thanks to Steve Domino https://medium.com/@skdomino/watch-this-file-watching-in-go-5b5a247cf71f
var watcher *fsnotify.Watcher
func watchFiles() {
// creates a new file watcher
watcher, _ = fsnotify.NewWatcher()
defer watcher.Close()
if err := watcher.Add(pinnedFile); err != nil {
log.Errorf("ERROR", err)
}
for _, fp := range appDirs {
if err := filepath.Walk(fp, watchDir); err != nil {
log.Errorf("ERROR", err)
}
}
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
if strings.HasSuffix(event.Name, ".desktop") &&
(event.Op.String() == "CREATE" ||
event.Op.String() == "REMOVE" ||
event.Op.String() == "RENAME") {
desktopTrigger = true
} else if event.Name == pinnedFile {
// TODO: This can be used to propagate information about the changed file to the
// GUI to avoid recreating everything
pinnedItemsChanged <- struct{}{}
}
case err := <-watcher.Errors:
log.Errorf("ERROR", err)
}
}
}()
<-done
}
func watchDir(path string, fi os.FileInfo, err error) error {
if fi.Mode().IsDir() {
return watcher.Add(path)
}
return nil
}

105
xdgdesktop_parser.go Normal file
View File

@@ -0,0 +1,105 @@
package main
import (
"bufio"
"fmt"
"io"
"os"
"strconv"
"strings"
)
func parseDesktopEntryFile(id string, path string) (e desktopEntry, err error) {
o, err := os.Open(path)
if err != nil {
return e, err
}
defer o.Close()
return parseDesktopEntry(id, o)
}
func parseDesktopEntry(id string, in io.Reader) (entry desktopEntry, err error) {
cleanexec := strings.NewReplacer("\"", "", "'", "")
entry.DesktopID = id
localizedName := fmt.Sprintf("Name[%s]", strings.Split(*lang, "_")[0])
localizedComment := fmt.Sprintf("Comment[%s]", strings.Split(*lang, "_")[0])
scanner := bufio.NewScanner(in)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
l := scanner.Text()
if strings.HasPrefix(l, "[") && l != "[Desktop Entry]" {
break
}
name, value := parseKeypair(l)
if value == "" {
continue
}
switch name {
case "Name":
entry.Name = value
case localizedName:
entry.NameLoc = value
case "Comment":
entry.Comment = value
case localizedComment:
entry.CommentLoc = value
case "Icon":
entry.Icon = value
case "Categories":
entry.Category = value
case "Terminal":
entry.Terminal, _ = strconv.ParseBool(value)
case "NoDisplay":
if !entry.NoDisplay {
entry.NoDisplay, _ = strconv.ParseBool(value)
}
case "Hidden":
if !entry.NoDisplay {
entry.NoDisplay, _ = strconv.ParseBool(value)
}
case "OnlyShowIn":
if !entry.NoDisplay {
entry.NoDisplay = true
currentDesktop := os.Getenv("XDG_CURRENT_DESKTOP")
if currentDesktop != "" {
for _, ele := range strings.Split(value, ";") {
if ele == currentDesktop && ele != "" {
entry.NoDisplay = false
}
}
}
}
case "NotShowIn":
currentDesktop := os.Getenv("XDG_CURRENT_DESKTOP")
if !entry.NoDisplay && currentDesktop != "" {
for _, ele := range strings.Split(value, ";") {
if ele == currentDesktop && ele != "" {
entry.NoDisplay = true
}
}
}
case "Exec":
entry.Exec = cleanexec.Replace(value)
}
}
// if name[ln] not found, let's try to find name[ln_LN]
if entry.NameLoc == "" {
entry.NameLoc = entry.Name
}
if entry.CommentLoc == "" {
entry.CommentLoc = entry.Comment
}
return entry, err
}
func parseKeypair(s string) (string, string) {
if idx := strings.IndexRune(s, '='); idx > 0 {
return strings.TrimSpace(s[:idx]), strings.TrimSpace(s[idx+1:])
}
return s, ""
}

53
xdgdesktop_parser_test.go Normal file
View File

@@ -0,0 +1,53 @@
package main
import (
"strings"
"testing"
)
var result desktopEntry
func BenchmarkDesktopEntryParser(b *testing.B) {
var entry desktopEntry
for n := 0; n < b.N; n++ {
entry, _ = parseDesktopEntryFile("id", "./desktop-directories/game.directory")
}
result = entry
}
func TestWhitespaceHandling(t *testing.T) {
const whitespace = `[Desktop Entry]
Categories = Debugger; Development; Git; IDE; Programming; TextEditor;
Comment = Editor for building and debugging modern web and cloud applications
Exec = bash -c "code-insiders ~/Workspaces/Linux/Flutter.code-workspace"
GenericName = Text Editor
Icon = vscode-flutter
Keywords = editor; IDE; plaintext; text; write;
MimeType = application/x-shellscript; inode/directory; text/english; text/plain; text/x-c; text/x-c++; text/x-c++hdr; text/x-c++src; text/x-chdr; text/x-csrc; text/x-java; text/x-makefile; text/x-moc; text/x-pascal; text/x-tcl; text/x-tex;
Name = VSCode Insiders with Flutter
Name[pt] = VSCode Insiders com Flutter
StartupNotify = true
StartupWMClass = code - insiders
Terminal = false
NoDisplay = false
Type = Application
Version = 1.0`
*lang = "pt"
entry, err := parseDesktopEntry("id", strings.NewReader(whitespace))
if err != nil {
t.Fatal(err)
}
if entry.Name != "VSCode Insiders with Flutter" {
t.Error("failed to parse desktop entry name")
}
if entry.NameLoc != "VSCode Insiders com Flutter" {
t.Error("failed to parse localized name")
}
if entry.NoDisplay {
t.Error("failed to parse desktop entry no display")
}
}