271 Commits

Author SHA1 Message Date
85320215be remove search bar 2025-08-06 03:41:42 +02:00
f35903bfb7 allow for CUSTOM_DATA_DIRS variable 2025-08-05 03:58:33 +02:00
c8f8e27a65 only serach in custom director.
hide&wait for the program to exit.
2024-12-20 04:17:36 +01:00
Piotr Miller
4110554466 Merge pull request #141 from msmafra/patch-1
Update main.go
2024-12-14 00:56:03 +01:00
Marcelo dos Santos Mafra
7d1242d6ce Update main.go
Very little misspell
2024-12-13 16:37:34 -03:00
Piotr Miller
953239bb5c Merge pull request #140 from nwg-piotr/diamond
Migrate to diamondburned gtk3 bindings
2024-12-01 01:16:41 +01:00
piotr
2136a6165c update comments 2024-12-01 01:13:10 +01:00
piotr
a752be7434 update README.md 2024-12-01 01:10:23 +01:00
piotr
517054f013 update get 2024-11-29 00:34:06 +01:00
piotr
8b0d4f943b sort desktopEntries case insensitive 2024-11-29 00:21:20 +01:00
piotr
5c839eb576 userDirButton -> connect "activate" signal 2024-11-28 03:13:03 +01:00
piotr
68f42b7569 disallow userDirButton parent selection 2024-11-28 00:46:10 +01:00
piotr
725ce44058 fix gtk-theme-name detection 2024-11-27 03:20:25 +01:00
piotr
2d755f609d migrate to diamondburned GTK bindings 2024-11-27 03:05:46 +01:00
Piotr Miller
423adcd096 Merge pull request #138 from Vescrity/main
Fix env XDG_DATA_DIRS makes catagory disappear
2024-11-21 00:20:36 +01:00
vescrity
1b61a42b10 Fix env XDG_DATA_DIRS makes catagory disappear 2024-11-19 19:44:09 +08:00
piotr
c0eb0965a8 update README.md 2024-10-26 00:58:25 +02:00
piotr
c940139fd9 add default styling for #math-result label #134 2024-10-26 00:43:34 +02:00
piotr
10b1b2a1e6 Merge remote-tracking branch 'origin/main' 2024-10-26 00:38:44 +02:00
piotr
b4b7d3486a add CSS ID to math result label #134 2024-10-26 00:38:20 +02:00
Piotr Miller
61a5595d84 Merge pull request #132 from msmafra/main
Update README.md
2024-10-15 11:27:46 +02:00
Marcelo dos Santos Mafra
cb44c7215f Update README.md
nwg-drawer seems to look in its own folder ~/.config/nwg-drawer for preferred-apps.json and excluded-dirs and not nwg-panel's folder
2024-10-11 17:59:30 -03:00
piotr
e7fafebf55 bump to 0.5.2 2024-10-09 02:12:19 +02:00
piotr
702cbb1c5c update golang.org/x/sys 2024-10-09 02:07:51 +02:00
piotr
7de2854041 set gtk-layer-shell namespace 2024-10-09 02:06:30 +02:00
Piotr Miller
be6b71673d Merge pull request #131 from nwg-piotr/calc
add a simple calculator in the search box
2024-10-03 01:20:01 +02:00
piotr
9c48c585b7 add rounded corners 2024-10-03 01:10:35 +02:00
piotr
3977c4fc7c go -> 1.23 2024-10-03 01:05:14 +02:00
piotr
46a526ced8 close result win on btn release 2024-10-03 01:04:53 +02:00
piotr
12da77fbfc update README.md 2024-10-03 00:48:23 +02:00
piotr
484c6ccb29 add comments 2024-10-03 00:33:54 +02:00
piotr
7aac5a4f52 add expression calculator 2024-09-30 22:09:56 +02:00
Piotr Miller
44e4c95900 Merge pull request #130 from nwg-piotr/backports
Merge pull request #129 from nwg-piotr/main
2024-09-22 14:05:20 +02:00
Piotr Miller
86a3398bc3 Merge pull request #129 from nwg-piotr/main
A bunch of fixes
2024-09-22 14:04:33 +02:00
piotr
c4a268332a bump to 0.5.0 2024-09-22 13:57:01 +02:00
piotr
e77d189f4a println -> log.Debug 2024-09-22 13:54:32 +02:00
piotr
e3cb0af2bd println -> log.Debug 2024-09-22 13:51:46 +02:00
piotr
1015f4c48f fix hyprclt & mapping output->monitor 2024-09-22 13:44:02 +02:00
piotr
86c92328cd update dependencies 2024-09-22 13:29:07 +02:00
piotr
4afa9f1aa3 add riverctl spawn 2024-09-22 13:26:18 +02:00
Piotr Miller
0bce8cf971 Merge pull request #127 from mattkae/main
Update gotk3 to the latest version to avoid build failures
2024-08-11 22:20:16 +02:00
Matthew Kosarek
e6a1130524 Update gotk3 to the latest version to avoid build failures 2024-08-11 15:59:14 -04:00
Piotr Miller
433c05ca1e Merge pull request #125 from nwg-piotr/icon-theme
update README.md
2024-06-29 20:20:34 +02:00
piotr
cd06a981cb update README.md 2024-06-29 20:19:12 +02:00
Piotr Miller
0f6f92433e Merge pull request #124 from nwg-piotr/icon-theme
Add the `-pbuseicontheme` flagIcon theme
2024-06-29 03:01:20 +02:00
piotr
3ffd09b5d7 add more desc 2024-06-29 02:40:01 +02:00
piotr
071841da46 bump to 0.4.9 2024-06-29 02:38:43 +02:00
piotr
c7a0b564c0 improve description 2024-06-29 02:34:41 +02:00
piotr
6862711010 add pbUseIconTheme flag #122 2024-06-29 02:32:27 +02:00
piotr
6d671062df change logging level 2024-06-17 02:16:52 +02:00
piotr
9aebff5d98 fix typos 2024-06-17 02:10:03 +02:00
piotr
051590ce4b revert gotk3 to v0.6.3 2024-06-17 02:03:39 +02:00
piotr
16be4ff00d bump to 0.4.8 2024-06-17 02:01:20 +02:00
piotr
9f0f708cae Merge remote-tracking branch 'origin/main' 2024-06-17 01:53:43 +02:00
piotr
4e04e7a4f4 update dependencies 2024-06-17 01:53:25 +02:00
Piotr Miller
85a0b87024 Merge pull request #121 from RRRRRm/open_close
Add -open and -close options
2024-06-17 01:52:21 +02:00
RRRRRm
6649dcefdb Correct the default terminal 2024-06-17 00:31:52 +01:00
Zijia Xiong
e501038c64 Merge branch 'open_close' of github.com:RRRRRm/nwg-drawer into open_close 2024-06-16 19:04:31 +01:00
Zijia Xiong
008f31278d Fix logic about receive signals 2024-06-16 19:01:26 +01:00
RRRRRm
69e3b06b39 Update README.md,
Add some information about new flags `-open` and `-close`
2024-06-16 18:35:01 +01:00
Zijia Xiong
99765402d7 Change usage of SIGINT to SIGRTMIN+3 2024-06-14 00:48:10 +01:00
Zijia Xiong
3fb70fc0cb Fix bug of -open and -close 2024-06-13 23:30:58 +01:00
Zijia Xiong
04a04c1417 Add -close and -open options. 2024-06-13 19:09:33 +01:00
Piotr Miller
a54a56c4b9 Update FUNDING.yml 2024-02-16 13:17:28 +01:00
piotr
0cd48d1631 unhardcode desktop-directories path 2024-02-08 03:12:17 +01:00
piotr
943ca39055 update README.md 2024-02-08 02:51:16 +01:00
piotr
42b279ee02 update README.md 2024-02-08 02:49:57 +01:00
Piotr Miller
42f15ec2cb Merge pull request #116 from nwg-piotr/data
un-hardcode data dir
2024-02-08 02:35:16 +01:00
piotr
936f793be4 update README.md 2024-02-08 02:28:21 +01:00
piotr
b59058c5c2 focus 1st power button on Tab key 2024-02-08 02:23:34 +01:00
piotr
3ea4a7824c bump to 0.4.7 2024-02-08 01:32:37 +01:00
piotr
701d4e62f2 unhardcode data dir again #115 2024-02-08 01:31:22 +01:00
piotr
d4996af84a unhardcode data dir #115 2024-02-08 01:20:03 +01:00
piotr
71ce3b5d75 Merge remote-tracking branch 'origin/main' 2024-02-06 03:40:14 +01:00
piotr
b056b4c436 bump to 0.4.6 2024-02-06 03:39:35 +01:00
Piotr Miller
42b55d41df Merge pull request #113 from nwg-piotr/touch
Don't launch if the window has been scrolled
2024-02-06 03:38:09 +01:00
piotr
ec0ce767d9 get proper adjustment 2024-02-06 03:35:29 +01:00
piotr
ec3caa02d2 add comments #110 2024-02-06 03:33:12 +01:00
piotr
6b39eba14c check if window scrolled #110 2024-02-06 03:21:42 +01:00
piotr
631b651eed bump to 0.4.5 2024-02-05 01:02:34 +01:00
piotr
df644f4196 gotk3 -> 0.6.3 2024-02-05 01:01:30 +01:00
piotr
be89ff4b97 update README.md 2024-02-03 16:44:55 +01:00
piotr
5cab646d08 update README.md 2024-02-03 16:40:57 +01:00
piotr
f93e6923d0 update README.md 2024-02-03 16:39:34 +01:00
Piotr Miller
2d114e58bc Update README.md 2024-02-03 15:53:10 +01:00
piotr
11f2f67184 update README.md 2024-02-03 04:26:49 +01:00
piotr
038fddc9da update README.md 2024-02-03 04:23:30 +01:00
piotr
945f73adbf Merge remote-tracking branch 'origin/main' 2024-02-03 04:15:05 +01:00
Piotr Miller
f8572b3db8 Update README.md 2024-02-03 04:14:35 +01:00
piotr
9012a45be4 update README.md 2024-02-03 04:12:06 +01:00
piotr
fe311ced2a bump to 0.4.4 2024-02-03 04:06:01 +01:00
Piotr Miller
bd12c281b1 Merge pull request #112 from nwg-piotr/power
add optional PowerBar
2024-02-02 02:06:56 +01:00
piotr
ff34cf8194 fix finding userDirsFile 2024-02-02 01:39:58 +01:00
piotr
8e02b59f83 clear status label on button exit 2024-02-01 18:24:14 +01:00
Piotr Miller
7b2ed76fac update lock icon 2024-02-01 09:48:06 +01:00
piotr
b0dc1404e7 formatting 2024-02-01 04:14:42 +01:00
piotr
6da6787272 add powerButton size argument 2024-02-01 04:09:31 +01:00
piotr
01c26ba092 add powerButton function 2024-02-01 03:52:56 +01:00
piotr
db265ccf6a update dependencies 2024-02-01 01:09:57 +01:00
piotr
ffc256ba87 fix Makefile; modify icons 2024-02-01 01:09:13 +01:00
Piotr Miller
b5f1713c3e add power icons 2024-01-31 10:50:44 +01:00
piotr
b1908537d1 cp README.md, LICENSE 2024-01-27 14:52:34 +01:00
piotr
664917c105 update README.md 2024-01-27 04:15:17 +01:00
piotr
bf66683a09 update README.md 2024-01-27 03:31:21 +01:00
piotr
ff8e2435b6 rename variable 2023-11-27 00:18:03 +01:00
Piotr Miller
223e16f77c Merge pull request #106 from nwg-piotr/otput-hypr
Make binding to an output work on Hyprland
2023-11-26 23:40:28 +01:00
piotr
55c095c053 fix -wm help 2023-11-26 23:34:48 +01:00
piotr
ee2008348f bump to 0.4.3 2023-11-26 23:17:00 +01:00
piotr
0570f90bec update help 2023-11-26 23:16:39 +01:00
piotr
a2917d68dd support output assignment on Hyprland 2023-11-26 23:14:18 +01:00
piotr
b05b0f484a improve help 2023-11-25 15:54:05 +01:00
piotr
b61e665c71 bump to 0.4.2 2023-11-22 01:23:59 +01:00
piotr
8408c669ef remove unused function 2023-11-22 01:13:48 +01:00
Piotr Miller
d66066c3b6 Merge pull request #105 from nwg-piotr/fix-wm
remove `XDG_CURRENT_DESKTOP` as default `-wm` value; allow capitalized `Hyprland`
2023-11-21 14:53:22 +01:00
piotr
dac55cdd0d remove setting XDG_CURRENT_DESKTOP as dafault -wm value; allow capitalized Hyprland value 2023-11-21 14:26:04 +01:00
Piotr Miller
03365d24e5 Merge pull request #104 from Schuldkroete/main
Make the description line at the bottom of the drawer themable
2023-11-21 14:20:22 +01:00
Schuldkröte
8e58654336 add gtk name property to statusLineWrapper and statusLabel 2023-11-21 11:47:01 +01:00
piotr
33e0032725 bump to 0.4.1 2023-11-18 01:41:53 +01:00
Piotr Miller
c05fff189d Merge pull request #103 from gouvinb/main
Change command execution logic and use `/usr/bin/env` command
2023-11-18 01:40:09 +01:00
gouvinb
57d130ca51 Restore the code to trim % and everything that follows 2023-11-09 15:49:50 +01:00
gouvinb
954a6ba9c9 change command execution logic and use env command 2023-11-09 04:24:41 +01:00
piotr
67b16ea6a6 bump to 0.4.0 2023-11-08 02:30:01 +01:00
piotr
3a02b16761 update versions 2023-11-08 02:25:43 +01:00
piotr
c67fef6671 fix line formatting 2023-11-08 02:24:41 +01:00
Piotr Miller
444617865e Merge pull request #102 from gouvinb/main
add support to hyprlandctl
2023-11-08 01:01:10 +01:00
gouvinb
e3de9cc67b make better log with -wm flag 2023-11-07 10:32:23 +01:00
gouvinb
1c11329581 Reworked command building.
Typical shell-supported command sets:

- `cmd`
- `cmd --arg`
- `cmd --arg=value`
- `ENV_VAR="value" cmd`
- `ENV_VAR="value" cmd --arg`
- `ENV_VAR="value" cmd --arg=value`

The listed commands above are now better managed.

Initially, the command types `cmd --arg=value` or `ENV_VAR="value" cmd --arg=value` were not correctly parsed. After finding a solution to this problem, I conducted further tests and discovered cases that were not currently handled. These cases are as follows:

- `cmd "arg with space"`
- `ENV_VAR="value with space" cmd`
2023-11-06 15:35:42 +01:00
gouvinb
87ee23afe2 print the correct executed command (prefix supported) 2023-11-06 12:40:10 +01:00
gouvinb
808fd032a0 fix prefix command with hypr or hyprland and update -wm description 2023-11-06 00:05:16 +01:00
gouvinb
1d3f023dc8 add support to hyprlandctl
BREAKING CHANGE: '-swaymsg' removed, use '-wm sway' instead
2023-11-05 23:36:18 +01:00
Piotr Miller
ccfe7776c6 Merge pull request #101 from gouvinb/main
Add the `swaymsg` flag to launch a program with `swaymsg exec` and prevent potential involuntary closing with Jetbrains IDEs
2023-11-05 14:03:26 +01:00
gouvinb
5a7dbefeb0 adds the "swaymsg" flag to launch a program with "swaymsg exec" and prevent potential involuntary closing with Jetbrains IDEs 2023-11-01 00:44:50 +01:00
piotr
fd26bb39e8 adjust help descriptions 2023-07-28 01:18:08 +02:00
piotr
bc74ebc927 update usage 2023-07-28 01:15:31 +02:00
piotr
d65d075bdb update usage 2023-07-28 01:13:57 +02:00
Piotr Miller
58ea8ea8db Merge pull request #95 from trinitronx/add-keyboard-mode-flag
Add keyboard mode flag to support both: "on-demand" & "exclusive" GTK layer-shell modes
2023-07-28 00:51:52 +02:00
Piotr Miller
94b8b8d7f0 Merge pull request #96 from nwg-piotr/theme
Force GTK_THEME for libadwaita apps
2023-07-28 00:49:47 +02:00
piotr
ebfaba1881 undo version bump 2023-07-28 00:47:27 +02:00
piotr
6656568c6a update -ft description 2023-07-27 03:44:29 +02:00
piotr
b3fa492b44 bump to 0.3.10 2023-07-27 03:27:56 +02:00
piotr
c0faecdb8b add -ft argument 2023-07-27 03:27:27 +02:00
James Cuzella
830edefa22 Refactor -k flag as Bool to toggle keyboard mode on-demand / exclusive 2023-07-24 20:48:35 -06:00
James Cuzella
aeeb4e4890 Fix warn messages for short -k flag 2023-07-23 22:49:52 -06:00
James Cuzella
6bbfbea1a8 Handle empty string passed to --keyboard/-k flag 2023-07-23 22:45:16 -06:00
James Cuzella
d88d9795d1 Document --keyboard/-k flag in README 2023-07-23 22:22:41 -06:00
James Cuzella
f85356bdc7 Drop keyboard mode: 'none' b/c it doesn't make sense with this app to not have keyboard input at all 2023-07-23 20:30:14 -06:00
James Cuzella
37c9c2d520 Add --keyboard/-k flag to set GTK layer shell keyboard mode (default: exclusive) 2023-07-23 20:26:09 -06:00
Piotr Miller
2903abd831 Merge pull request #94 from 6543-forks/smal_code_refactor
smal code refactors
2023-07-07 22:48:39 +02:00
Piotr Miller
9cc81fa38a Merge pull request #93 from 6543-forks/fix_lint_errors
log actual error message too
2023-07-07 22:45:03 +02:00
Piotr Miller
edf4c81f85 Merge pull request #92 from 6543-forks/reduce_syscalls
Save result of os.Getenv()
2023-07-07 22:43:50 +02:00
Piotr Miller
9582726cb9 Merge pull request #90 from 6543-forks/update_go_deps
update golang lib dependencies
2023-07-07 22:26:53 +02:00
6543
7a6ae82a75 log the unknown signal 2023-07-06 18:03:28 +02:00
6543
22ddb71603 declare unused var with _ and always name event the same 2023-07-06 17:56:40 +02:00
6543
ea7813761a gitignore vendor directory 2023-07-06 17:16:26 +02:00
6543
0cafb3c3ad save result of os.Getenv and use that if possible to reduce some syscalls 2023-07-06 17:15:49 +02:00
6543
89cffad81d log actual error message too 2023-07-06 17:14:55 +02:00
6543
1e854558cf update golang lib dependencies 2023-07-06 16:10:31 +02:00
Piotr Miller
3df7a30533 Update README.md 2023-03-11 02:10:14 +01:00
piotr
e383f7a470 bump to 0.3.9 (go 1.20) 2023-02-06 01:57:11 +01:00
Piotr Miller
820848d984 Merge pull request #85 from nwg-piotr/add84
Add `-i` flag to force an icon theme
2023-01-22 22:41:42 +01:00
piotr
fa01d5bb32 bump to 0.3.8 2023-01-22 22:35:00 +01:00
piotr
8be1fc1ea2 allow forcing icon theme #84 2023-01-22 22:30:09 +01:00
Piotr Miller
0e5d38907f Merge pull request #83 from nwg-piotr/fix82
fixed trimming strings on non-ASCII characters
2023-01-12 22:50:59 +01:00
piotr
7ba7abd817 bump to 0.3.7 2023-01-12 22:48:52 +01:00
piotr
cedcf8619f fix cutting utf-8 strings #82 2023-01-12 02:27:27 +01:00
Piotr Miller
8ff2d5c89c Create FUNDING.yml 2022-12-15 12:05:39 +01:00
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
19 changed files with 1693 additions and 459 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: nwg-piotr
liberapay: nwg

8
.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/
vendor/

View File

@@ -1,24 +1,31 @@
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/diamondburned/gotk4/pkg/gdk/v3
go get github.com/diamondburned/gotk4/pkg/glib/v2
go get github.com/diamondburned/gotk4/pkg/gtk/v3
go get github.com/diamondburned/gotk4-layer-shell/pkg/gtklayershell
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 -v -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 -r img /usr/share/nwg-drawer
cp drawer.css /usr/share/nwg-drawer
cp bin/nwg-drawer /usr/bin
install -Dm 644 -t "/usr/share/licenses/nwg-drawer" LICENSE
install -Dm 644 -t "/usr/share/doc/nwg-drawer" README.md
uninstall:
rm -r /usr/share/nwg-drawer
rm /usr/bin/nwg-drawer
run:
go build -o bin/nwg-drawer *.go
bin/nwg-drawer
go run .

170
README.md
View File

@@ -1,31 +1,33 @@
# nwg-drawer
<img src="https://github.com/nwg-piotr/nwg-drawer/assets/20579136/1a7578e8-5332-4e4c-bdce-9b9bf875c0e7" width="90" style="margin-right:10px" align=left alt="logo">
<H1>nwg-drawer</H1><br>
This application is a part of the [nwg-shell](https://github.com/nwg-piotr/nwg-shell) project.
This application is a part of the [nwg-shell](https://nwg-piotr.github.io/nwg-shell) project.
Nwg-drawer is a golang replacement to the `nwggrid` command
(a part of [nwg-launchers](https://github.com/nwg-piotr/nwg-launchers)). It's being developed with
[sway](https://github.com/swaywm/sway) in mind, but should also work with other wlroots-based Wayland compositors.
X Window System is not officially supported, but you should be able to use the drawer on some floating
window managers (tested on Openbox).
**Nwg-drawer** is an application launcher. It's being developed with [sway](https://github.com/swaywm/sway) and
[Hyprland](https://github.com/hyprwm/Hyprland) in mind, but should also work with other wlroots-based Wayland
compositors.
The `nwg-drawer` command displays the application grid. The search entry allows to look for installed applications,
and for files in XDG user directories. The grid view may also be filtered by categories.
You may pin applications by right-clicking them. Pinned items will appear above the application grid. Right-click
a pinned item to unpin it. The pinned items cache is shared with [nwg-menu](https://github.com/nwg-piotr/nwg-menu)
and `nwggrid`.
a pinned item to unpin it. The pinned items cache is shared with [nwg-menu](https://github.com/nwg-piotr/nwg-menu).
![screenshot-01.png](https://scrot.cloud/images/2021/05/30/screenshot-01.png)
Below the grid there is the **power bar** - a row of buttons to lock the screen, exit the compositor, reboot, suspend
and power the machine off. For each button to appear, you need to provide a corresponding command. See "Command line
arguments" below. If the power bar is present, pressing **Tab** will move focus to its first button.
[more screenshots](https://scrot.cloud/album/nwg-drawer.Bogd) | [see on YouTube](https://youtu.be/iIgxJQhCQf0)
<img src="https://github.com/nwg-piotr/nwg-drawer/assets/20579136/8f4eacb4-5395-4350-889b-a9037aa34f08" width=640 alt="screenshot"><br>
[![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 the `Esc` key, or right-click the window next to the grid.
## Installation
[![Packaging status](https://repology.org/badge/vertical-allrepos/nwg-drawer.svg)](https://repology.org/project/nwg-drawer/versions)
### Dependencies
- go >=1.16 (just to build)
- go
- gtk3
- gtk-layer-shell
- xdg-utils
@@ -33,7 +35,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 +47,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,63 +54,141 @@ $ nwg-drawer -h
Usage of nwg-drawer:
-c uint
number of Columns (default 6)
-close
close drawer of existing instance
-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)
-ft
Force Theme for libadwaita apps, by adding 'GTK_THEME=<default-gtk-theme>' env var
-g string
GTK theme name
-i string
GTK icon theme name
-is int
Icon Size (default 64)
-k set GTK layer shell Keyboard interactivity to 'on-demand' mode
-lang string
force lang, e.g. "en", "pl"
-mb int
Margin Bottom
-ml int
Margin Left
-mr int
Margin Right
-mt int
Margin Top
-nocats
Disable filtering by category
-nofs
Disable file search
-o string
name of the Output to display the drawer on (sway only)
name of the Output to display the drawer on (sway & Hyprland only)
-open
open drawer of existing instance
-ovl
use OVerLay layer
-pbexit string
command for the Exit power bar icon
-pblock string
command for the Lock power bar icon
-pbpoweroff string
command for the Poweroff power bar icon
-pbreboot string
command for the Reboot power bar icon
-pbsize int
power bar icon size (only works w/ built-in icons) (default 64)
-pbsleep string
command for the sleep power bar icon
-pbuseicontheme
use icon theme instead of built-in icons in power bar
-r Leave the program resident in memory
-s string
Styling: css file name (default "drawer.css")
-spacing uint
icon spacing (default 20)
-term string
Terminal emulator (default "alacritty")
Terminal emulator (default "foot")
-v display Version information
-wm string
use swaymsg exec (with 'sway' argument) or hyprctl dispatch exec (with 'hyprland') to launch programs
```
*NOTE: the `$TERM` environment variable overrides the `-term` argument if defined.*
*NOTE: the `$TERM` environment variable overrides the `-term` argument.*
## Running
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 the compositor to apply any new arguments!
If you want to explicitly specify commands to open and close the resident instance, which can be helpful for touchpad gestures, please use the `-open` and `-close` parameters. Similarly, some signals can also be use: pkill -USR2 nwg-drawer to open and pkill -SIGRTMIN+3 nwg-drawer to close.
For a MacOS-style three-finger pinch:
```text
bindgesture pinch:4:inward exec pkill -SIGUSR2 nwg-drawer
bindgesture pinch:4:outward exec pkill -SIGRTMIN+3 nwg-drawer
```
## 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
## File search
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)
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.
PITA, you may override them, by creating the `~/.config/nwg-drawer/preferred-apps.json` file with your own definitions.
### Sample file content
### Sample `preferred-apps.json` file content
```json
{
"\\.pdf$": "atril",
"\\.svg$": "inkscape",
"\\.(jpg|png|tiff|gif)$": "feh",
"\\.(jpg|png|tiff|gif)$": "swayimg",
"\\.(mp3|ogg|flac|wav|wma)$": "audacious",
"\\.(avi|mp4|mkv|mov|wav)$": "mpv",
"\\.(doc|docx|xls|xlsx)$": "libreoffice"
}
```
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
@@ -119,12 +196,39 @@ 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-drawer/excluded-dirs` file, e.g. like this:
```text
# exclude all paths containing 'node_modules'
node_modules
```
### Calculations in the search box
If the search box is not empty, and you press Enter, the search box content will be evaluated as an arithmetic operation.
If the result is not an error, it will be displayed in a small window, and copied to the clipboard with wl-copy.
Press any key to close the window.
You may change the result label styling e.g. like this:
```css
/* math operation result label */
#math-label {
font-weight: bold;
font-size: 16px
}
```
## 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!
- [gotk4](https://github.com/diamondburned/gotk4) by [diamondburned](https://github.com/diamondburned) released under [GNU Affero General Public License v3.0](https://github.com/diamondburned/gotk4/blob/4/LICENSE.md)
- [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
- [expr](https://github.com/expr-lang/expr) Copyright (c) 2018 Anton Medvedev

Binary file not shown.

View File

@@ -32,3 +32,9 @@ button:hover {
border: 1px dotted gray;
border-radius: 15px
}
/* math operation result label */
#math-label {
font-weight: bold;
font-size: 16px
}

20
go.mod
View File

@@ -1,10 +1,22 @@
module github.com/nwg-piotr/nwg-drawer
go 1.16
go 1.23
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/diamondburned/gotk4-layer-shell/pkg v0.0.0-20240109211357-6efa9f6dc438
github.com/diamondburned/gotk4/pkg v0.3.1
github.com/expr-lang/expr v1.16.9
github.com/fsnotify/fsnotify v1.8.0
github.com/joshuarubin/go-sway v1.2.0
github.com/sirupsen/logrus v1.9.3
)
require (
github.com/KarpelesLab/weak v0.1.1 // indirect
github.com/joshuarubin/lifecycle v1.1.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/sys v0.27.0 // indirect
)

52
go.sum
View File

@@ -1,24 +1,50 @@
github.com/KarpelesLab/weak v0.1.1 h1:fNnlPo3aypS9tBzoEQluY13XyUfd/eWaSE/vMvo9s4g=
github.com/KarpelesLab/weak v0.1.1/go.mod h1:pzXsWs5f2bf+fpgHayTlBE1qJpO3MpJKo5sRaLu1XNw=
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/joshuarubin/lifecycle v1.0.0 h1:N/lPEC8f+dBZ1Tn99vShqp36LwB+LI7XNAiNadZeLUQ=
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/diamondburned/gotk4-layer-shell/pkg v0.0.0-20240109211357-6efa9f6dc438 h1:Ymnl4B+Fn4srLxXbRV2RY1iHT2SH3oAkOfxeEeMI3Fg=
github.com/diamondburned/gotk4-layer-shell/pkg v0.0.0-20240109211357-6efa9f6dc438/go.mod h1:AjrxxF6teeNWgaEg0zIUwoqFtXlVTHlEGZvrOn7RXaQ=
github.com/diamondburned/gotk4/pkg v0.3.1 h1:uhkXSUPUsCyz3yujdvl7DSN8jiLS2BgNTQE95hk6ygg=
github.com/diamondburned/gotk4/pkg v0.3.1/go.mod h1:DqeOW+MxSZFg9OO+esk4JgQk0TiUJJUBfMltKhG+ub4=
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
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 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
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 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=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.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=

64
img/exit.svg Normal file
View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg8"
version="1.1"
viewBox="0 0 48 48"
height="48"
width="48"
sodipodi:docname="exit1.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="9.4201949"
inkscape:cx="17.197096"
inkscape:cy="24.256398"
inkscape:window-width="2552"
inkscape:window-height="1372"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8"
showgrid="false"
showguides="true">
<sodipodi:guide
position="-17.76063,69.315161"
orientation="0,-1"
id="guide1"
inkscape:locked="false" />
</sodipodi:namedview>
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<path
id="path831"
style="opacity:1;vector-effect:none;fill:#f2f2f2;fill-opacity:1;stroke:none;stroke-width:3.75;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
d="M 24 4 C 12.954338 4 4 12.954338 4 24 C 4 35.045662 12.954338 44 24 44 C 31.79942 44 38.549265 39.531179 41.845703 33.017578 L 40.427734 33.017578 C 37.240509 38.816001 31.084255 42.75 24 42.75 C 13.644691 42.75 5.25 34.355309 5.25 24 C 5.25 13.644691 13.644691 5.25 24 5.25 C 31.084255 5.25 37.240509 9.1839991 40.427734 14.982422 L 41.845703 14.982422 C 38.549265 8.4688214 31.79942 4 24 4 z " />
<path
id="path5"
style="color:#000000;fill:#f2f2f2;fill-opacity:1;stroke-linejoin:bevel;-inkscape-stroke:none"
d="M 34.980469,18.517578 V 19.8125 l 6.347656,3.591797 -16.929687,-0.115235 -0.0078,1.125 18.927734,0.128907 v -1.304688 z" />
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

66
img/lock.svg Normal file
View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="48"
height="48"
viewBox="0 0 48 48"
version="1.1"
id="svg8"
sodipodi:docname="lock.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="16.208306"
inkscape:cx="18.447332"
inkscape:cy="19.43448"
inkscape:window-width="1912"
inkscape:window-height="1012"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8"
showgrid="false" />
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<path
style="vector-effect:none;fill:#f2f2f2;fill-opacity:1;stroke:none;stroke-width:3.75238;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
d="m 117.78998,28.062266 -1.08641,0.0064 2e-5,0.0024 -37.799575,0.0816 -0.007,-1.249983 37.379655,-0.0791 c -1.83062,-8.704176 -9.52473,-14.924431 -18.41926,-14.890941 -8.428083,0.0507 -15.786638,5.718662 -17.987023,13.854598 l -1.291486,0.0077 C 80.806771,16.95191 88.730946,10.732773 97.849919,10.670451 107.928,10.61958 116.47101,18.070836 117.78998,28.062266 Z m -0.43153,7.507607 C 115.13057,44.412889 107.20639,50.632007 98.087432,50.694325 88.009353,50.745197 79.466338,43.293941 78.147368,33.30251 l 1.086411,-0.0064 -1.4e-5,-0.0024 38.306055,-0.0816 0.007,1.249983 -37.886139,0.0791 c 1.830589,8.704029 9.524477,14.924242 18.418861,14.890942 8.428078,-0.0507 15.786638,-5.718664 17.987018,-13.854598 z"
id="path817"
sodipodi:nodetypes="cccccccccccccccccccccc" />
<path
id="path2"
style="color:#000000;opacity:1;fill:#f2f2f2;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;-inkscape-stroke:none"
d="M 4.28125 20.666016 C 4.0995845 21.748484 4 22.858366 4 23.992188 C 4 35.02783 12.958497 43.986328 23.994141 43.986328 C 35.029785 43.986328 43.988281 35.02783 43.988281 23.992188 C 43.988281 22.858366 43.888701 21.748484 43.707031 20.666016 L 43.542969 20.666016 L 42.566406 20.666016 L 5.421875 20.666016 L 4.5546875 20.666016 L 4.28125 20.666016 z M 5.2597656 21.791016 L 42.728516 21.791016 C 42.813924 22.51349 42.863281 23.246468 42.863281 23.992188 C 42.863281 34.419838 34.421791 42.861328 23.994141 42.861328 C 13.566489 42.861328 5.125 34.419838 5.125 23.992188 C 5.125 23.246468 5.1743575 22.51349 5.2597656 21.791016 z " />
<path
style="color:#000000;fill:#f2f2f2;fill-opacity:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;-inkscape-stroke:none"
d="m 23.880859,30.039062 c -1.679508,0 -3.048828,1.369321 -3.048828,3.048829 v 1.808593 c 0,1.679508 1.36932,3.048828 3.048828,3.048828 h 0.226563 c 1.679508,0 3.048828,-1.36932 3.048828,-3.048828 v -1.808593 c 0,-1.679508 -1.36932,-3.048829 -3.048828,-3.048829 z m 0,1.125 h 0.226563 c 1.075718,0 1.923828,0.848111 1.923828,1.923829 v 1.808593 c 0,1.075718 -0.84811,1.923828 -1.923828,1.923828 h -0.226563 c -1.075718,0 -1.923828,-0.84811 -1.923828,-1.923828 v -1.808593 c 0,-1.075718 0.84811,-1.923828 1.923828,-1.923829 z"
id="rect2" />
<path
id="path3"
style="color:#000000;fill:#f2f2f2;-inkscape-stroke:none;fill-opacity:1"
d="M 23.994141 4 C 16.432673 4 10.050669 9.0735967 8.0585938 15.998047 L 9.2265625 15.998047 C 11.17813 9.6962096 17.046647 5.125 23.994141 5.125 C 30.941634 5.125 36.810151 9.6962096 38.761719 15.998047 L 39.929688 15.998047 C 37.937612 9.0735967 31.555607 4 23.994141 4 z " />
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

52
img/poweroff.svg Normal file
View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg8"
version="1.1"
viewBox="0 0 48 48"
height="48"
width="48"
sodipodi:docname="poweroff.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="6.9411765"
inkscape:cx="16.927966"
inkscape:cy="16.927966"
inkscape:window-width="2552"
inkscape:window-height="1372"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<path
id="path822"
d="M 20.25,4 V 19 H 21.5 V 4 Z m 6.25,0 v 15 h 1.25 V 4 Z M 14,6.7319336 C 7.9628546,10.218607 4.0033064,16.702931 4,24 4,35.045695 12.954305,44 24,44 35.045695,44 44,35.045695 44,24 43.994912,16.704586 40.035832,10.222562 34,6.7368164 V 8.1870117 C 39.351053,11.561829 42.735765,17.485293 42.75,24 42.75,34.355339 34.355339,42.75 24,42.75 13.644661,42.75 5.25,34.355339 5.25,24 5.2650811,17.48603 8.6494321,11.563781 14,8.1894531 Z"
style="opacity:1;vector-effect:none;fill:#f2f2f2;fill-opacity:1;stroke:none;stroke-width:3.75;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

59
img/reboot.svg Normal file
View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="48"
height="48"
viewBox="0 0 48 48"
version="1.1"
id="svg8"
sodipodi:docname="reboot.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="6.9411765"
inkscape:cx="16.927966"
inkscape:cy="16.927966"
inkscape:window-width="2552"
inkscape:window-height="1372"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8"
showgrid="false" />
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="matrix(-1.2507463,0,0,1.2508411,44.011945,-356.92327)"
style="fill:#f2f2f2">
<path
style="opacity:1;vector-effect:none;fill:#f2f2f2;fill-opacity:1;stroke:none;stroke-width:3.00189;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
d="M 19.001953,1.1308594 V 2 H 19 v 11 h 1 V 2.3359375 A 15,15 45 0 1 32,17 15,15 45 0 1 21.001953,31.455078 v 1.033203 A 16.009488,16.010701 45 0 0 33.009766,17 16.009488,16.010701 45 0 0 19.001953,1.1308594 Z M 12.998047,1.5117188 A 16.009488,16.010701 45 0 0 0.99023438,17 16.009488,16.010701 45 0 0 14.998047,32.869141 V 32 H 15 V 21 H 14 V 31.664062 A 15,15 45 0 1 2,17 15,15 45 0 1 12.998047,2.5449219 Z"
transform="matrix(0.70668771,-0.70663419,0.70668771,0.70663419,-8.0273788,304.53335)"
id="path817" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

52
img/sleep.svg Normal file
View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg8"
version="1.1"
viewBox="0 0 48 48"
height="48"
width="48"
sodipodi:docname="sleep.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="6.9411765"
inkscape:cx="16.927966"
inkscape:cy="16.927966"
inkscape:window-width="2552"
inkscape:window-height="1372"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<path
style="opacity:1;vector-effect:none;fill:#f2f2f2;fill-opacity:1;stroke:none;stroke-width:3.75;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill"
d="M 24,4 C 12.954305,4 4,12.954305 4,24 4,35.045695 12.954305,44 24,44 35.045695,44 44,35.045695 44,24 44,12.954305 35.045695,4 24,4 Z m 0,1.25 C 34.355339,5.25 42.75,13.644661 42.75,24 42.75,34.355339 34.355339,42.75 24,42.75 13.644661,42.75 5.25,34.355339 5.25,24 5.25,13.644661 13.644661,5.25 24,5.25 Z M 19,16.5 v 15 h 1.25 v -15 z m 8.75,0 v 15 H 29 v -15 z"
id="path831" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

602
main.go
View File

@@ -3,32 +3,40 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"path"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/diamondburned/gotk4-layer-shell/pkg/gtklayershell"
"github.com/expr-lang/expr"
"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"
log "github.com/sirupsen/logrus"
"github.com/diamondburned/gotk4/pkg/gdk/v3"
"github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v3"
)
const version = "0.1.10"
const version = "0.6.0"
var (
appDirs []string
configDirectory string
dataDirectory string
pinnedFile string
pinned []string
src glib.SourceHandle
id2entry map[string]desktopEntry
preferredApps map[string]interface{}
exclusions []string
hyprlandMonitors []monitor
beenScrolled bool
firstPowerBtn *gtk.Button
)
var categoryNames = [...]string{
@@ -64,6 +72,30 @@ type desktopEntry struct {
NoDisplay bool
}
type monitor struct {
Id int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Make string `json:"make"`
Model string `json:"model"`
Serial string `json:"serial"`
Width int `json:"width"`
Height int `json:"height"`
RefreshRate float64 `json:"refreshRate"`
X int `json:"x"`
Y int `json:"y"`
ActiveWorkspace struct {
Id int `json:"id"`
Name string `json:"name"`
} `json:"activeWorkspace"`
Reserved []int `json:"reserved"`
Scale float64 `json:"scale"`
Transform int `json:"transform"`
Focused bool `json:"focused"`
DpmsStatus bool `json:"dpmsStatus"`
Vrr bool `json:"vrr"`
}
// slices below will hold DesktopID strings
var (
listUtility []string
@@ -81,9 +113,10 @@ var desktopEntries []desktopEntry
// UI elements
var (
win *gtk.Window
resultWindow *gtk.ScrolledWindow
fileSearchResults []string
searchEntry *gtk.SearchEntry
//searchEntry *gtk.SearchEntry
phrase string
fileSearchResultFlowBox *gtk.FlowBox
userDirsMap map[string]string
@@ -97,9 +130,11 @@ var (
statusLabel *gtk.Label
status string
ignore string
desktopTrigger bool
pinnedItemsChanged chan interface{} = make(chan interface{}, 1)
)
func defaultStringIfBlank(s, fallback string) string {
func defaultTermIfBlank(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.
@@ -109,71 +144,193 @@ func defaultStringIfBlank(s, fallback string) string {
return s
}
func validateWm() {
if !(*wm == "sway" || *wm == "hyprland" || *wm == "Hyprland") && *wm != "" {
*wm = ""
log.Warn("-wm argument supports only 'sway' or 'hyprland' string.")
}
}
// 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 targetOutput = flag.String("o", "", "name of the Output to display the drawer on (sway & Hyprland only)")
var displayVersion = flag.Bool("v", false, "display Version information")
var keyboard = flag.Bool("k", false, "set GTK layer shell Keyboard interactivity to 'on-demand' mode")
var overlay = flag.Bool("ovl", false, "use OVerLay layer")
var flagDrawerOpen = flag.Bool("open", false, "open drawer of existing instance")
var flagDrawerClose = flag.Bool("close", false, "close drawer of existing instance")
var gtkTheme = flag.String("g", "", "GTK theme name")
var gtkIconTheme = flag.String("i", "", "GTK icon 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 forceTheme = flag.Bool("ft", false, "Force Theme for libadwaita apps, by adding 'GTK_THEME=<default-gtk-theme>' env var")
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", defaultStringIfBlank(os.Getenv("TERM"), "alacritty"), "Terminal emulator")
var nameLimit = flag.Int("fslen", 80, "File Search name length Limit")
var term = flag.String("term", defaultTermIfBlank(os.Getenv("TERM"), "foot"), "Terminal emulator")
var wm = flag.String("wm", "", "use swaymsg exec (with 'sway' argument) or hyprctl dispatch exec (with 'hyprland') or riverctl spawn (with 'river') to launch programs")
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 pbExit = flag.String("pbexit", "", "command for the Exit power bar icon")
var pbLock = flag.String("pblock", "", "command for the Lock power bar icon")
var pbPoweroff = flag.String("pbpoweroff", "", "command for the Poweroff power bar icon")
var pbReboot = flag.String("pbreboot", "", "command for the Reboot power bar icon")
var pbSleep = flag.String("pbsleep", "", "command for the sleep power bar icon")
var pbSize = flag.Int("pbsize", 64, "power bar icon size (only works w/ built-in icons)")
var pbUseIconTheme = flag.Bool("pbuseicontheme", false, "use icon theme instead of built-in icons in power bar")
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)
}
validateWm()
// 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)
const (
SIG25 = syscall.Signal(0x25) // Which is SIGRTMIN+3 on Linux, it's not used by the system
)
signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGUSR1, syscall.SIGUSR2, SIG25)
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: // toggle drawer
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()
}
case syscall.SIGUSR2: // open drawer
if *resident {
log.Debug("SIGUSR2 received, showing the window")
showWindowChannel <- struct{}{}
} else {
log.Info("SIGUSR2 received, and I'm not resident but I'm still here, doing nothing")
}
case SIG25: // close drawer
if *resident {
log.Debug("SIG25 received, hiding the window")
if win.IsVisible() {
restoreStateAndHide()
}
} else {
log.Info("A signal received, and I'm not resident, bye bye")
gtk.MainQuit()
}
default:
log.Infof("Unknown signal: %s", s.String())
}
}
}()
// 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-resident instance and exit;
// 2. die if a resident instance found.
lockFilePath := path.Join(dataHome(), "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 {
var err error
if *flagDrawerClose {
log.Infof("Closing resident instance (PID %v)", i)
err = syscall.Kill(i, SIG25)
} else if *flagDrawerOpen {
log.Infof("Showing resident instance (PID %v)", i)
err = syscall.Kill(i, syscall.SIGUSR2)
} else {
log.Infof("Toggling 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()
dataDirectory = dataDir()
// 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(filepath.Join(dataDirectory, "drawer.css"), filepath.Join(configDirectory, "drawer.css"))
if err != nil {
log.Errorf("Failed copying 'drawer.css' file: %s", err)
}
}
cacheDirectory := cacheDir()
@@ -186,119 +343,186 @@ 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")
paFile := path.Join(configDirectory, "preferred-apps.json")
if pathExists(paFile) {
preferredApps, err = loadPreferredApps(paFile)
if err != nil {
println(fmt.Sprintf("Custom associations file %s not found or invalid", paFile))
log.Infof("Custom associations file %s not found or invalid", paFile)
} else {
println(fmt.Sprintf("Found %v associations in %s", len(preferredApps), paFile))
log.Infof("Found %v associations in %s", len(preferredApps), paFile)
}
} else {
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)
gtk.Init()
cssProvider, _ := gtk.CssProviderNew()
err = cssProvider.LoadFromPath(cssFile)
if err != nil {
println(fmt.Sprintf("ERROR: %s css file not found or erroneous. Using GTK styling.", cssFile))
println(fmt.Sprintf("%s", err))
settings := gtk.SettingsGetDefault()
if *gtkTheme != "" {
settings.SetObjectProperty("gtk-theme-name", *gtkTheme)
log.Infof("User demanded theme: %s", *gtkTheme)
} else {
println(fmt.Sprintf("Using style from %s", cssFile))
screen, _ := gdk.ScreenGetDefault()
gtk.AddProviderForScreen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
settings.SetObjectProperty("gtk-application-prefer-dark-theme", true)
log.Info("Preferring dark theme variants")
}
win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
if *gtkIconTheme != "" {
settings.SetObjectProperty("gtk-icon-theme-name", *gtkIconTheme)
log.Infof("User demanded icon theme: %s", *gtkIconTheme)
}
cssProvider := gtk.NewCSSProvider()
err = cssProvider.LoadFromPath(*cssFileName)
if err != nil {
log.Errorf("ERROR: %s css file not found or erroneous. Using GTK styling.", *cssFileName)
} else {
log.Info(fmt.Sprintf("Using style from %s", *cssFileName))
screen := gdk.ScreenGetDefault()
gtk.StyleContextAddProviderForScreen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
}
win = gtk.NewWindow(gtk.WindowToplevel)
if err != nil {
log.Fatal("Unable to create window:", err)
}
if wayland() {
layershell.InitForWindow(win)
gtklayershell.InitForWindow(win)
gtklayershell.SetNamespace(win, "nwg-drawer")
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()
log.Debugf("output2mon: %v", output2mon)
if err == nil {
monitor := output2mon[*targetOutput]
layershell.SetMonitor(win, monitor)
mon := output2mon[*targetOutput]
gtklayershell.SetMonitor(win, mon)
} else {
println(fmt.Sprintf("%s", err))
log.Errorf("%s", err)
}
}
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_BOTTOM, true)
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_TOP, true)
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_LEFT, true)
layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_RIGHT, true)
gtklayershell.SetAnchor(win, gtklayershell.LayerShellEdgeBottom, true)
gtklayershell.SetAnchor(win, gtklayershell.LayerShellEdgeTop, true)
gtklayershell.SetAnchor(win, gtklayershell.LayerShellEdgeLeft, true)
gtklayershell.SetAnchor(win, gtklayershell.LayerShellEdgeRight, true)
if *overlay {
layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_OVERLAY)
layershell.SetExclusiveZone(win, -1)
gtklayershell.SetLayer(win, gtklayershell.LayerShellLayerOverlay)
gtklayershell.SetExclusiveZone(win, -1)
} else {
layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_TOP)
gtklayershell.SetLayer(win, gtklayershell.LayerShellLayerTop)
}
gtklayershell.SetMargin(win, gtklayershell.LayerShellEdgeTop, *marginTop)
gtklayershell.SetMargin(win, gtklayershell.LayerShellEdgeLeft, *marginLeft)
gtklayershell.SetMargin(win, gtklayershell.LayerShellEdgeRight, *marginRight)
gtklayershell.SetMargin(win, gtklayershell.LayerShellEdgeBottom, *marginBottom)
if *keyboard {
log.Info("Setting GTK layer shell keyboard mode to: on-demand")
gtklayershell.SetKeyboardMode(win, gtklayershell.LayerShellKeyboardModeOnDemand)
} else {
log.Info("Setting GTK layer shell keyboard mode to default: exclusive")
gtklayershell.SetKeyboardMode(win, gtklayershell.LayerShellKeyboardModeExclusive)
}
layershell.SetKeyboardMode(win, layershell.LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE)
}
win.Connect("destroy", func() {
gtk.MainQuit()
})
win.Connect("key-press-event", func(window *gtk.Window, event *gdk.Event) bool {
key := &gdk.EventKey{Event: event}
switch key.KeyVal() {
case gdk.KEY_Escape:
s, _ := searchEntry.GetText()
win.Connect("key-release-event", func(_ *gtk.Window, event *gdk.Event) bool {
//key := &gdk.EventKey{Event: event}
key := event.AsKey()
if key.Keyval() == gdk.KEY_Escape {
//s := searchEntry.Text()
s := ""
if s != "" {
searchEntry.GrabFocus()
searchEntry.SetText("")
//searchEntry.GrabFocus()
//searchEntry.SetText("")
} else {
if !*resident {
gtk.MainQuit()
} else {
restoreStateAndHide()
}
return false
}
return true
} else if key.Keyval() == gdk.KEY_Tab {
if firstPowerBtn != nil {
firstPowerBtn.GrabFocus()
}
} else if key.Keyval() == gdk.KEY_Return {
//s := searchEntry.Text()
s := ""
if s != "" {
// Check if the search box content is an arithmetic expression. If so, display the result
// and copy to the clipboard with wl-copy.
result, e := expr.Eval(s, nil)
if e == nil {
log.Debugf("Setting up mathemathical operation result window. Operation: %s, result: %v", s, result)
setUpOperationResultWindow(s, fmt.Sprintf("%v", result))
}
}
return true
}
return true
})
win.Connect("key-press-event", func(_ *gtk.Window, event *gdk.Event) bool {
//key := &gdk.EventKey{Event: event}
key := event.AsKey()
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.GrabFocusWithoutSelecting()
//if !searchEntry.IsFocus() {
// 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()
})
/*
@@ -307,99 +531,263 @@ func main() {
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()
}
// Set up UI
outerVBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
outerVBox := gtk.NewBox(gtk.OrientationVertical, 0)
win.Add(outerVBox)
searchBoxWrapper, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
outerVBox.PackStart(searchBoxWrapper, false, false, 10)
//searchBoxWrapper := gtk.NewBox(gtk.OrientationHorizontal, 0)
//outerVBox.PackStart(searchBoxWrapper, false, false, 10)
searchEntry = setUpSearchEntry()
searchEntry.SetMaxWidthChars(30)
searchBoxWrapper.PackStart(searchEntry, true, false, 0)
//searchEntry = setUpSearchEntry()
//searchEntry.SetMaxWidthChars(30)
//searchBoxWrapper.PackStart(searchEntry, true, false, 0)
if !*noCats {
categoriesWrapper, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
categoriesWrapper = gtk.NewBox(gtk.OrientationHorizontal, 0)
categoriesButtonBox := setUpCategoriesButtonBox()
categoriesWrapper.PackStart(categoriesButtonBox, true, false, 0)
outerVBox.PackStart(categoriesWrapper, false, false, 0)
}
pinnedWrapper, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
pinnedWrapper := gtk.NewBox(gtk.OrientationHorizontal, 0)
outerVBox.PackStart(pinnedWrapper, false, false, 0)
pinnedFlowBoxWrapper, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
pinnedFlowBoxWrapper = gtk.NewBox(gtk.OrientationHorizontal, 0)
outerVBox.PackStart(pinnedFlowBoxWrapper, false, false, 0)
pinnedFlowBox = setUpPinnedFlowBox()
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 = gtk.NewScrolledWindow(nil, nil)
resultWindow.SetEvents(int(gdk.AllEventsMask))
resultWindow.SetPolicy(gtk.PolicyAutomatic, gtk.PolicyAutomatic)
// On touch screen we don't want the button-release-event to launch the app if the user just wanted to scroll the
// window. Let's forbid doing so if the content has been scrolled. We will reset the value on button-press-event.
// Resolves https://github.com/nwg-piotr/nwg-drawer/issues/110
vAdj := resultWindow.VAdjustment()
vAdj.Connect("value-changed", func() {
beenScrolled = true
})
resultWindow.Connect("button-release-event", func(sw *gtk.ScrolledWindow, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
if btnEvent.Button() == 1 || btnEvent.Button() == 3 {
hAdj := resultWindow.HAdjustment()
hAdj.Connect("value-changed", func() {
beenScrolled = true
})
resultWindow.Connect("button-release-event", func(_ *gtk.ScrolledWindow, event *gdk.Event) bool {
//btnEvent := gdk.EventButtonNewFromEvent(event)
btnEvent := event.AsButton()
if btnEvent.Button() == 3 {
if !*resident {
gtk.MainQuit()
} else {
restoreStateAndHide()
}
return true
}
return false
})
outerVBox.PackStart(resultWindow, true, true, 10)
resultsWrapper, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
resultsWrapper := gtk.NewBox(gtk.OrientationVertical, 0)
resultWindow.Add(resultsWrapper)
appSearchResultWrapper, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
appSearchResultWrapper = gtk.NewBox(gtk.OrientationVertical, 0)
resultsWrapper.PackStart(appSearchResultWrapper, false, false, 0)
appFlowBox = setUpAppsFlowBox(nil, "")
// Focus 1st pinned item if any, otherwise focus 1st found app icon
var button gtk.IWidget
if pinnedFlowBox.GetChildren().Length() > 0 {
button, err = pinnedFlowBox.GetChildAtIndex(0).GetChild()
var button gtk.Widget
if len(pinnedFlowBox.Children()) > 0 {
button = pinnedFlowBox.ChildAtIndex(0).Widget
} else {
button, err = appFlowBox.GetChildAtIndex(0).GetChild()
button = appFlowBox.ChildAtIndex(0).Widget
}
if err == nil {
button.ToWidget().GrabFocus()
button.GrabFocus()
}
userDirsMap = mapXdgUserDirs()
log.Debugf("User dirs map: %s", userDirsMap)
placeholder, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
placeholder := gtk.NewBox(gtk.OrientationVertical, 0)
resultsWrapper.PackStart(placeholder, true, true, 0)
placeholder.SetSizeRequest(20, 20)
if !*noFS {
wrapper, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
fileSearchResultWrapper, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
fileSearchResultWrapper.SetProperty("name", "files-box")
wrapper := gtk.NewBox(gtk.OrientationHorizontal, 0)
fileSearchResultWrapper = gtk.NewBox(gtk.OrientationHorizontal, 0)
fileSearchResultWrapper.SetObjectProperty("name", "files-box")
wrapper.PackStart(fileSearchResultWrapper, true, false, 0)
resultsWrapper.PackEnd(wrapper, false, false, 10)
}
statusLineWrapper, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
// Power Button Bar
if dataDirectory != "" {
if *pbExit != "" || *pbLock != "" || *pbPoweroff != "" || *pbReboot != "" || *pbSleep != "" {
powerBarWrapper := gtk.NewBox(gtk.OrientationHorizontal, 0)
outerVBox.PackStart(powerBarWrapper, false, false, 0)
powerButtonsWrapper := gtk.NewBox(gtk.OrientationHorizontal, 0)
powerBarWrapper.PackStart(powerButtonsWrapper, true, false, 12)
if *pbPoweroff != "" {
btn := gtk.NewButton()
if !*pbUseIconTheme {
btn = powerButton(filepath.Join(dataDirectory, "img/poweroff.svg"), *pbPoweroff)
} else {
btn = powerButton("system-shutdown-symbolic", *pbPoweroff)
}
powerButtonsWrapper.PackEnd(btn, true, false, 0)
firstPowerBtn = btn
}
if *pbSleep != "" {
btn := gtk.NewButton()
if !*pbUseIconTheme {
btn = powerButton(filepath.Join(dataDirectory, "img/sleep.svg"), *pbSleep)
} else {
btn = powerButton("face-yawn-symbolic", *pbSleep)
}
powerButtonsWrapper.PackEnd(btn, true, false, 0)
firstPowerBtn = btn
}
if *pbReboot != "" {
btn := gtk.NewButton()
if !*pbUseIconTheme {
btn = powerButton(filepath.Join(dataDirectory, "img/reboot.svg"), *pbReboot)
} else {
btn = powerButton("system-reboot-symbolic", *pbReboot)
}
powerButtonsWrapper.PackEnd(btn, true, false, 0)
firstPowerBtn = btn
}
if *pbExit != "" {
btn := gtk.NewButton()
if !*pbUseIconTheme {
btn = powerButton(filepath.Join(dataDirectory, "img/exit.svg"), *pbExit)
} else {
btn = powerButton("system-log-out-symbolic", *pbExit)
}
powerButtonsWrapper.PackEnd(btn, true, false, 0)
firstPowerBtn = btn
}
if *pbLock != "" {
btn := gtk.NewButton()
if !*pbUseIconTheme {
btn = powerButton(filepath.Join(dataDirectory, "img/lock.svg"), *pbLock)
} else {
btn = powerButton("system-lock-screen-symbolic", *pbLock)
}
powerButtonsWrapper.PackEnd(btn, true, false, 0)
firstPowerBtn = btn
}
}
} else {
log.Warn("Couldn't find data dir, power bar icons unavailable")
}
statusLineWrapper := gtk.NewBox(gtk.OrientationHorizontal, 0)
statusLineWrapper.SetObjectProperty("name", "status-line-wrapper")
outerVBox.PackStart(statusLineWrapper, false, false, 10)
statusLabel, _ = gtk.LabelNew(status)
statusLabel = gtk.NewLabel(status)
statusLabel.SetObjectProperty("name", "status-label")
statusLineWrapper.PackStart(statusLabel, true, false, 0)
win.ShowAll()
if !*noFS {
fileSearchResultWrapper.SetSizeRequest(appFlowBox.GetAllocatedWidth(), 1)
fileSearchResultWrapper.SetSizeRequest(appFlowBox.AllocatedWidth(), 1)
fileSearchResultWrapper.Hide()
}
if !*noCats {
categoriesWrapper.SetSizeRequest(1, categoriesWrapper.GetAllocatedHeight()*2)
categoriesWrapper.SetSizeRequest(1, categoriesWrapper.AllocatedHeight()*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.Widget
if len(pinnedFlowBox.Children()) > 0 {
button = pinnedFlowBox.ChildAtIndex(0).Widget
} else {
button = appFlowBox.ChildAtIndex(0).Widget
}
if err == nil {
button.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("")
// One day or another we'll add SetFilterFunction here; it was impossible on the gotk3 library
appFlowBox = setUpAppsFlowBox(nil, "")
for _, btn := range catButtons {
btn.SetImagePosition(gtk.PosLeft)
btn.SetSizeRequest(0, 0)
}
// scroll to the top
if resultWindow != nil {
resultWindow.VAdjustment().SetValue(0)
}
t := time.Now()
log.Debugf(fmt.Sprintf("UI hidden and restored in the background in %v ms", t.Sub(timeStart1).Milliseconds()))
}

11
run_forever.sh Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# Infinite loop to run the command
while true; do
echo "Starting nwg-drawer..."
bin/nwg-drawer -nocats -nofs -ovl -d
done &
# Return immediately after starting the loop
echo "The loop is running in the background."

456
tools.go
View File

@@ -7,49 +7,36 @@ import (
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"syscall"
"time"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
"github.com/joshuarubin/go-sway"
log "github.com/sirupsen/logrus"
"github.com/diamondburned/gotk4/pkg/gdk/v3"
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v3"
)
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 {
log.Fatal("Couldn't get default theme: ", err)
}
func createPixbuf(icon string, size int) (*gdkpixbuf.Pixbuf, error) {
iconTheme := gtk.IconThemeGetDefault()
if strings.Contains(icon, "/") {
pixbuf, err := gdk.PixbufNewFromFileAtSize(icon, size, size)
pixbuf, err := gdkpixbuf.NewPixbufFromFileAtSize(icon, size, size)
if err != nil {
println(fmt.Sprintf("%s", err))
log.Errorf("%s", err)
return nil, err
}
return pixbuf, nil
@@ -59,18 +46,18 @@ func createPixbuf(icon string, size int) (*gdk.Pixbuf, error) {
icon = strings.Split(icon, ".")[0]
}
pixbuf, err := iconTheme.LoadIcon(icon, size, gtk.ICON_LOOKUP_FORCE_SIZE)
pixbuf, err := iconTheme.LoadIcon(icon, size, gtk.IconLookupForceSize)
if err != nil {
if strings.HasPrefix(icon, "/") {
pixbuf, err := gdk.PixbufNewFromFileAtSize(icon, size, size)
if err != nil {
return nil, err
pixbuf, e := gdkpixbuf.NewPixbufFromFileAtSize(icon, size, size)
if e != nil {
return nil, e
}
return pixbuf, nil
}
pixbuf, err := iconTheme.LoadIcon(icon, size, gtk.ICON_LOOKUP_FORCE_SIZE)
pixbuf, err := iconTheme.LoadIcon(icon, size, gtk.IconLookupForceSize)
if err != nil {
return nil, err
}
@@ -90,9 +77,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(configHome(), "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 +104,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
@@ -132,29 +120,18 @@ func getUserDir(home, line string) string {
}
func cacheDir() string {
if os.Getenv("XDG_CACHE_HOME") != "" {
return os.Getenv("XDG_CONFIG_HOME")
if xdgCache := os.Getenv("XDG_CACHE_HOME"); xdgCache != "" {
return xdgCache
}
if os.Getenv("HOME") != "" && pathExists(filepath.Join(os.Getenv("HOME"), ".cache")) {
p := filepath.Join(os.Getenv("HOME"), ".cache")
if home := os.Getenv("HOME"); home != "" && pathExists(filepath.Join(home, ".cache")) {
p := filepath.Join(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 := ioutil.ReadFile(path)
bytes, err := os.ReadFile(path)
if err != nil {
return "", err
}
@@ -162,14 +139,50 @@ func readTextFile(path string) (string, error) {
return string(bytes), nil
}
func configDir() string {
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")))
func oldConfigDir() (string, error) {
if xdgConfig := os.Getenv("XDG_CONFIG_HOME"); xdgConfig != "" {
dir := path.Join(xdgConfig, "nwg-panel")
return dir, nil
} else if home := os.Getenv("HOME"); home != "" {
dir := path.Join(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 xdgConfig := os.Getenv("XDG_CONFIG_HOME"); xdgConfig != "" {
dir = path.Join(xdgConfig, "nwg-drawer")
} else if home := os.Getenv("HOME"); home != "" {
dir = path.Join(home, ".config/nwg-drawer")
}
log.Infof("Config dir: %s", dir)
createDir(dir)
return dir
}
func configHome() string {
if os.Getenv("XDG_CONFIG_HOME") != "" {
return os.Getenv("XDG_CONFIG_HOME")
}
return path.Join(os.Getenv("HOME"), ".config")
}
func dataHome() string {
var dir string
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
dir = path.Join(xdgData, "nwg-drawer")
} else if home := os.Getenv("HOME"); home != "" {
dir = path.Join(home, ".local/share/nwg-drawer")
}
log.Debugf("Data home: %s", dir)
createDir(dir)
return dir
}
@@ -177,13 +190,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: '%s' => '%s'", src, dst)
var err error
var srcfd *os.File
@@ -193,12 +206,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,41 +232,56 @@ 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")
func dataDir() string {
xdgDataDirs := os.Getenv("XDG_DATA_DIRS")
if xdgDataDirs == "" {
xdgDataDirs = "/usr/local/share/:/usr/share/"
}
return "/usr/share/"
for _, d := range strings.Split(xdgDataDirs, ":") {
p := filepath.Join(d, "nwg-drawer")
q := filepath.Join(p, "desktop-directories")
if pathExists(q) {
log.Infof("Data dir: %v", p)
return p
}
}
log.Warnf("Data dir not found")
return ""
}
func getAppDirs() []string {
var dirs []string
xdgDataDirs := ""
log.Info("App dirs::")
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"}
//xdgDataHome := os.Getenv("XDG_DATA_HOME")
//xdgDataDirs := os.Getenv("XDG_DATA_DIRS")
for _, d := range flatpakDirs {
if !isIn(dirs, d) {
dirs = append(dirs, d)
custom := os.Getenv("CUSTOM_DATA_DIRS")
if home != "" {
if custom == "" {
dirs = append(dirs, filepath.Join(home, "/my_applications"))
} else {
dirs = append(dirs, filepath.Join(home, custom))
}
}
log.Infof("App dirs: %v", dirs)
var confirmedDirs []string
for _, d := range dirs {
if pathExists(d) {
confirmedDirs = append(confirmedDirs, d)
}
}
return dirs
log.Infof("App dirs: %v", dirs)
return confirmedDirs
}
func loadPreferredApps(path string) (map[string]interface{}, error) {
@@ -251,12 +289,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 +311,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 +336,13 @@ func listDesktopFiles() []string {
}
func setUpCategories() {
path := filepath.Join(getDataHome(), "nwg-drawer/desktop-directories")
var other category
dDir := dataDir()
for _, cName := range categoryNames {
fileName := fmt.Sprintf("%s.directory", cName)
lines, err := loadTextFile(filepath.Join(path, fileName))
fp := filepath.Join(dDir, "desktop-directories", fileName)
lines, err := loadTextFile(fp)
if err == nil {
var cat category
cat.Name = cName
@@ -340,6 +387,8 @@ func setUpCategories() {
} else {
other = cat
}
} else {
log.Errorf("Couldn't open %s", fp)
}
}
sort.Slice(categories, func(i, j int) bool {
@@ -349,6 +398,7 @@ func setUpCategories() {
}
func parseDesktopFiles(desktopFiles []string) string {
desktopEntries = nil
id2entry = make(map[string]desktopEntry)
skipped := 0
hidden := 0
@@ -367,7 +417,7 @@ func parseDesktopFiles(desktopFiles []string) string {
if entry.NoDisplay {
hidden++
// We still need hidden entries, so `continue` is disallowed here
// Fixes introduced in #19
// Fixes bug introduced in #19
}
id2entry[entry.DesktopID] = entry
@@ -375,10 +425,10 @@ func parseDesktopFiles(desktopFiles []string) string {
assignToLists(entry.DesktopID, entry.Category)
}
sort.Slice(desktopEntries, func(i, j int) bool {
return desktopEntries[i].NameLoc < desktopEntries[j].NameLoc
return strings.ToLower(desktopEntries[i].NameLoc) < strings.ToLower(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
}
@@ -452,7 +502,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
}
@@ -460,7 +510,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)
}
@@ -471,20 +521,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)
}
}
@@ -503,20 +553,26 @@ 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)
}
}
}
}
func launch(command string, terminal bool) {
func launch(command string, terminal bool, terminate bool) {
// trim % and everything afterwards
if strings.Contains(command, "%") {
cutAt := strings.Index(command, "%")
@@ -525,49 +581,65 @@ func launch(command string, terminal bool) {
}
}
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
themeToPrepend := ""
//add "GTK_THEME=<default_gtk_theme>" environment variable
if *forceTheme {
settings := gtk.SettingsGetDefault()
th := settings.ObjectProperty("gtk-theme-name")
themeToPrepend = th.(string)
}
cmd := exec.Command(elements[cmdIdx], elements[1+cmdIdx:]...)
if themeToPrepend != "" {
command = fmt.Sprintf("GTK_THEME=%q %s", themeToPrepend, command)
}
var elements = []string{"/usr/bin/env", "-S", command}
cmd := exec.Command(elements[0], elements[1:]...)
if terminal {
args := []string{"-e", elements[cmdIdx]}
cmd = exec.Command(*term, args...)
var prefixCommand = *term
var args []string
if prefixCommand != "foot" {
args = []string{"-e", command}
} else {
args = elements
}
cmd = exec.Command(prefixCommand, args...)
} else if *wm == "sway" {
cmd = exec.Command("swaymsg", "exec", strings.Join(elements, " "))
} else if *wm == "hyprland" || *wm == "Hyprland" {
cmd = exec.Command("hyprctl", "dispatch", "exec", strings.Join(elements, " "))
} else if *wm == "river" {
cmd = exec.Command("riverctl", "spawn", strings.Join(elements, " "))
}
// set env variables
if len(envVars) > 0 {
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, envVars...)
msg := fmt.Sprintf("Executing command: %q; args: %q\n", cmd.Args[0], cmd.Args[1:])
log.Info(msg)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
msg := fmt.Sprintf("env vars: %s; command: '%s'; args: %s\n", envVars, elements[cmdIdx], elements[1+cmdIdx:])
println(msg)
go cmd.Run()
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() {
restoreStateAndHide()
_ = cmd.Wait()
gtk.MainQuit()
return false
})
}()
}
if terminate {
if *resident {
restoreStateAndHide()
} else {
}
}
}
func open(filePath string, xdgOpen bool) {
@@ -585,16 +657,46 @@ func open(filePath string, xdgOpen bool) {
} else {
cmd = exec.Command(*fileManager, filePath)
}
fmt.Printf("Executing: %s", cmd)
cmd.Start()
log.Infof("Executing: %s", cmd)
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
func mapOutputs() (map[string]*gdk.Monitor, error) {
result := make(map[string]*gdk.Monitor)
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
err := listHyprlandMonitors()
if err == nil {
display := gdk.DisplayGetDefault()
num := display.NMonitors()
for i := 0; i < num; i++ {
mon := display.Monitor(i)
output := hyprlandMonitors[i]
result[output.Name] = mon
}
} else {
return nil, err
}
} else if os.Getenv("SWAYSOCK") != "" {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
@@ -608,21 +710,79 @@ func mapOutputs() (map[string]*gdk.Monitor, error) {
return nil, err
}
display, err := gdk.DisplayGetDefault()
display := gdk.DisplayGetDefault()
if err != nil {
return nil, err
}
num := display.GetNMonitors()
num := display.NMonitors()
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
}
mon := display.Monitor(i)
output := outputs[i]
result[output.Name] = mon
}
} else {
return nil, errors.New("output assignment only supported on sway and Hyprland")
}
return result, nil
}
// KAdot / https://stackoverflow.com/a/38537764/4040598 - thanks!
func substring(s string, start int, end int) string {
startStrIdx := 0
i := 0
for j := range s {
if i == start {
startStrIdx = j
}
if i == end {
return s[startStrIdx:j]
}
i++
}
return s[startStrIdx:]
}
func hyprctl(cmd string) ([]byte, error) {
his := os.Getenv("HYPRLAND_INSTANCE_SIGNATURE")
xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR")
hyprDir := ""
if xdgRuntimeDir != "" {
hyprDir = fmt.Sprintf("%s/hypr", xdgRuntimeDir)
} else {
hyprDir = "/tmp/hypr"
}
socketFile := fmt.Sprintf("%s/%s/.socket.sock", hyprDir, his)
conn, err := net.Dial("unix", socketFile)
if err != nil {
return nil, err
}
message := []byte(cmd)
_, err = conn.Write(message)
if err != nil {
return nil, err
}
reply := make([]byte, 102400)
n, err := conn.Read(reply)
if err != nil {
return nil, err
}
defer conn.Close()
return reply[:n], nil
}
func listHyprlandMonitors() error {
reply, err := hyprctl("j/monitors")
if err != nil {
return err
} else {
err = json.Unmarshal([]byte(reply), &hyprlandMonitors)
}
return err
}

View File

@@ -6,15 +6,20 @@ import (
"path/filepath"
"strings"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk"
"github.com/diamondburned/gotk4-layer-shell/pkg/gtklayershell"
log "github.com/sirupsen/logrus"
"github.com/diamondburned/gotk4/pkg/gdk/v3"
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v3"
)
func setUpPinnedFlowBox() *gtk.FlowBox {
if pinnedFlowBox != nil {
pinnedFlowBox.Destroy()
}
flowBox, _ := gtk.FlowBoxNew()
flowBox := gtk.NewFlowBox()
if uint(len(pinned)) >= *columnsNumber {
flowBox.SetMaxChildrenPerLine(*columnsNumber)
} else if len(pinned) > 0 {
@@ -24,26 +29,30 @@ func setUpPinnedFlowBox() *gtk.FlowBox {
flowBox.SetColumnSpacing(*itemSpacing)
flowBox.SetRowSpacing(*itemSpacing)
flowBox.SetHomogeneous(true)
flowBox.SetProperty("name", "pinned-box")
flowBox.SetSelectionMode(gtk.SELECTION_NONE)
flowBox.SetObjectProperty("name", "pinned-box")
flowBox.SetSelectionMode(gtk.SelectionNone)
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()
btn := gtk.NewButton()
var img *gtk.Image
if entry.Icon != "" {
pixbuf, _ := createPixbuf(entry.Icon, *iconSize)
img, _ = gtk.ImageNewFromPixbuf(pixbuf)
img = gtk.NewImageFromPixbuf(pixbuf)
} else {
img, _ = gtk.ImageNewFromIconName("image-missing", gtk.ICON_SIZE_INVALID)
img = gtk.NewImageFromIconName("image-missing", int(gtk.IconSizeInvalid))
}
btn.SetImage(img)
btn.SetAlwaysShowImage(true)
btn.SetImagePosition(gtk.POS_TOP)
btn.SetImagePosition(gtk.PosTop)
name := ""
if entry.NameLoc != "" {
@@ -52,43 +61,36 @@ func setUpPinnedFlowBox() *gtk.FlowBox {
name = entry.Name
}
if len(name) > 20 {
r := []rune(name)
name = string(r[:17])
name = fmt.Sprintf("%s…", name)
r := substring(name, 0, 17)
name = fmt.Sprintf("%s…", r)
}
btn.SetLabel(name)
btn.Connect("button-release-event", func(row *gtk.Button, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
btn.Connect("button-release-event", func(row *gtk.Button, event *gdk.Event) bool {
btnEvent := event.AsButton()
if btnEvent.Button() == 1 {
launch(entry.Exec, entry.Terminal)
launch(entry.Exec, entry.Terminal, true)
return true
} else if btnEvent.Button() == 3 {
unpinItem(entry.DesktopID)
pinnedFlowBox = setUpPinnedFlowBox()
return true
}
return false
})
btn.Connect("activate", func() {
launch(entry.Exec, entry.Terminal)
launch(entry.Exec, entry.Terminal, true)
})
btn.Connect("enter-notify-event", func() {
statusLabel.SetText(entry.CommentLoc)
})
btn.Connect("focus-in-event", func() {
statusLabel.SetText(entry.CommentLoc)
})
flowBox.Add(btn)
btn.Parent().(*gtk.FlowBoxChild).SetCanFocus(false)
}
pinnedFlowBoxWrapper.PackStart(flowBox, true, false, 0)
//While moving focus with arrow keys we want buttons to get focus directly
flowBox.GetChildren().Foreach(func(item interface{}) {
item.(*gtk.Widget).SetCanFocus(false)
})
}
flowBox.Connect("enter-notify-event", func() {
cancelClose()
})
flowBox.ShowAll()
return flowBox
@@ -107,19 +109,17 @@ func setUpCategoriesButtonBox() *gtk.EventBox {
"other": listOther,
}
eventBox, _ := gtk.EventBoxNew()
eventBox.Connect("enter-notify-event", func() {
cancelClose()
})
hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
eventBox := gtk.NewEventBox()
hBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
eventBox.Add(hBox)
button, _ := gtk.ButtonNewWithLabel("All")
button.SetProperty("name", "category-button")
button := gtk.NewButtonWithLabel("All")
button.SetObjectProperty("name", "category-button")
button.Connect("clicked", func(item *gtk.Button) {
searchEntry.SetText("")
//searchEntry.SetText("")
appFlowBox = setUpAppsFlowBox(nil, "")
for _, btn := range catButtons {
btn.SetImagePosition(gtk.POS_LEFT)
btn.SetImagePosition(gtk.PosLeft)
btn.SetSizeRequest(0, 0)
}
})
@@ -127,24 +127,23 @@ func setUpCategoriesButtonBox() *gtk.EventBox {
for _, cat := range categories {
if isSupposedToShowUp(cat.Name) {
button, _ = gtk.ButtonNewFromIconName(cat.Icon, gtk.ICON_SIZE_MENU)
button.SetProperty("name", "category-button")
button = gtk.NewButtonFromIconName(cat.Icon, int(gtk.IconSizeMenu))
button.SetObjectProperty("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
b := *button
button.Connect("clicked", func(item *gtk.Button) {
searchEntry.SetText("")
// !!! since gotk3 FlowBox type does not implement set_filter_func, we need to rebuild appFlowBox
//searchEntry.SetText("")
// One day or another we'll add SetFilterFunction here; it was impossible on the gotk3 library
appFlowBox = setUpAppsFlowBox(lists[name], "")
for _, btn := range catButtons {
btn.SetImagePosition(gtk.POS_LEFT)
btn.SetImagePosition(gtk.PosLeft)
}
w := b.GetAllocatedWidth()
b.SetImagePosition(gtk.POS_TOP)
w := b.AllocatedWidth()
b.SetImagePosition(gtk.PosTop)
b.SetSizeRequest(w, 0)
if fileSearchResultWrapper != nil {
fileSearchResultWrapper.Hide()
@@ -186,13 +185,13 @@ func setUpAppsFlowBox(categoryList []string, searchPhrase string) *gtk.FlowBox {
if appFlowBox != nil {
appFlowBox.Destroy()
}
flowBox, _ := gtk.FlowBoxNew()
flowBox := gtk.NewFlowBox()
flowBox.SetMinChildrenPerLine(*columnsNumber)
flowBox.SetMaxChildrenPerLine(*columnsNumber)
flowBox.SetColumnSpacing(*itemSpacing)
flowBox.SetRowSpacing(*itemSpacing)
flowBox.SetHomogeneous(true)
flowBox.SetSelectionMode(gtk.SELECTION_NONE)
flowBox.SetSelectionMode(gtk.SelectionNone)
for _, entry := range desktopEntries {
if searchPhrase == "" {
@@ -205,6 +204,7 @@ func setUpAppsFlowBox(categoryList []string, searchPhrase string) *gtk.FlowBox {
} else {
button := flowBoxButton(entry)
flowBox.Add(button)
button.Parent().(*gtk.FlowBoxChild).SetCanFocus(false)
}
}
} else {
@@ -214,40 +214,43 @@ func setUpAppsFlowBox(categoryList []string, searchPhrase string) *gtk.FlowBox {
strings.Contains(strings.ToLower(entry.Exec), strings.ToLower(searchPhrase))) {
button := flowBoxButton(entry)
flowBox.Add(button)
button.Parent().(*gtk.FlowBoxChild).SetCanFocus(false)
}
}
}
hWrapper, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
hWrapper := gtk.NewBox(gtk.OrientationHorizontal, 0)
appSearchResultWrapper.PackStart(hWrapper, false, false, 0)
hWrapper.PackStart(flowBox, true, false, 0)
// While moving focus with arrow keys we want buttons to get focus directly
flowBox.GetChildren().Foreach(func(item interface{}) {
item.(*gtk.Widget).SetCanFocus(false)
})
resultWindow.ShowAll()
return flowBox
}
func flowBoxButton(entry desktopEntry) *gtk.Button {
button, _ := gtk.ButtonNew()
button := gtk.NewButton()
button.SetAlwaysShowImage(true)
var pixbuf *gdkpixbuf.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.NewImageFromPixbuf(pixbuf)
button.SetImage(img)
button.SetImagePosition(gtk.POS_TOP)
button.SetImagePosition(gtk.PosTop)
name := entry.NameLoc
if len(name) > 20 {
r := []rune(name)
name = string(r[:17])
name = fmt.Sprintf("%s…", name)
r := substring(name, 0, 17)
name = fmt.Sprintf("%s…", r)
}
button.SetLabel(name)
@@ -255,23 +258,85 @@ func flowBoxButton(entry desktopEntry) *gtk.Button {
exec := entry.Exec
terminal := entry.Terminal
desc := entry.CommentLoc
button.Connect("button-release-event", func(btn *gtk.Button, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
if len(desc) > 120 {
r := substring(desc, 0, 117)
desc = fmt.Sprintf("%s…", r)
}
button.Connect("button-press-event", func() {
// if not scrolled from now on, we will allow launching apps on button-release-event
beenScrolled = false
})
button.Connect("button-release-event", func(btn *gtk.Button, event *gdk.Event) bool {
btnEvent := event.AsButton()
if btnEvent.Button() == 1 {
launch(exec, terminal)
if !beenScrolled {
launch(exec, terminal, true)
return true
}
} else if btnEvent.Button() == 3 {
pinItem(ID)
pinnedFlowBox = setUpPinnedFlowBox()
return true
}
return false
})
button.Connect("activate", func() {
launch(exec, terminal)
launch(exec, terminal, true)
})
button.Connect("enter-notify-event", func() {
statusLabel.SetText(desc)
})
button.Connect("leave-notify-event", func() {
statusLabel.SetText("")
})
button.Connect("focus-in-event", func() {
statusLabel.SetText(desc)
})
return button
}
func powerButton(iconPathOrName, command string) *gtk.Button {
button := gtk.NewButton()
button.SetAlwaysShowImage(true)
var pixbuf *gdkpixbuf.Pixbuf
var img *gtk.Image
var err error
if !*pbUseIconTheme {
pixbuf, err = gdkpixbuf.NewPixbufFromFileAtSize(iconPathOrName, *pbSize, *pbSize)
if err != nil {
pixbuf, _ = createPixbuf("unknown", *pbSize)
log.Warnf("Couldn't find icon %s", iconPathOrName)
}
img = gtk.NewImageFromPixbuf(pixbuf)
} else {
img = gtk.NewImageFromIconName(iconPathOrName, int(gtk.IconSizeDialog))
}
button.SetImage(img)
button.SetImagePosition(gtk.PosTop)
button.Connect("button-release-event", func(btn *gtk.Button, event *gdk.Event) bool {
btnEvent := event.AsButton()
if btnEvent.Button() == 1 {
launch(command, false, true)
return true
}
return false
})
button.Connect("activate", func() {
launch(command, false, true)
})
button.Connect("enter-notify-event", func() {
statusLabel.SetText(command)
})
button.Connect("leave-notify-event", func() {
statusLabel.SetText("")
})
button.Connect("focus-in-event", func() {
statusLabel.SetText(command)
})
return button
}
@@ -279,11 +344,8 @@ func setUpFileSearchResultContainer() *gtk.FlowBox {
if fileSearchResultFlowBox != nil {
fileSearchResultFlowBox.Destroy()
}
flowBox, _ := gtk.FlowBoxNew()
flowBox.SetProperty("orientation", gtk.ORIENTATION_VERTICAL)
flowBox.Connect("enter-notify-event", func() {
cancelClose()
})
flowBox := gtk.NewFlowBox()
flowBox.SetObjectProperty("orientation", gtk.OrientationVertical)
fileSearchResultWrapper.PackStart(flowBox, false, false, 10)
return flowBox
@@ -295,7 +357,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)) {
// Remaining 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,22 +377,20 @@ 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() {
cancelClose()
})
searchEntry.Connect("search-changed", func() {
sEntry := gtk.NewSearchEntry()
sEntry.SetPlaceholderText("Type to search")
sEntry.Connect("search-changed", func() {
for _, btn := range catButtons {
btn.SetImagePosition(gtk.POS_LEFT)
btn.SetImagePosition(gtk.PosLeft)
btn.SetSizeRequest(0, 0)
}
phrase, _ = searchEntry.GetText()
phrase = sEntry.Text()
if len(phrase) > 0 {
// search apps
@@ -338,7 +410,7 @@ func setUpSearchEntry() *gtk.SearchEntry {
searchUserDir(key)
}
}
if fileSearchResultFlowBox.GetChildren().Length() == 0 {
if len(fileSearchResultFlowBox.Children()) == 0 {
fileSearchResultWrapper.Hide()
statusLabel.SetText("0 results")
}
@@ -352,26 +424,22 @@ func setUpSearchEntry() *gtk.SearchEntry {
}
}
// focus 1st search result #17
var w *gtk.Widget
var w *gtk.Button
if appFlowBox != nil {
b := appFlowBox.GetChildAtIndex(0)
b := appFlowBox.ChildAtIndex(0)
if b != nil {
button, err := b.GetChild()
if err == nil {
button.ToWidget().GrabFocus()
w = button.ToWidget()
}
button := b.Child().(*gtk.Button)
button.SetCanFocus(true)
button.GrabFocus()
w = button
}
}
if w == nil && fileSearchResultFlowBox != nil {
f := fileSearchResultFlowBox.GetChildAtIndex(0)
f := fileSearchResultFlowBox.ChildAtIndex(0)
if f != nil {
//f.SetCanFocus(false)
button, err := f.GetChild()
if err == nil {
button.ToWidget().SetCanFocus(true)
button.ToWidget().GrabFocus()
}
button := f.Child().(*gtk.Box)
button.SetCanFocus(true)
button.GrabFocus()
}
}
} else {
@@ -387,11 +455,17 @@ func setUpSearchEntry() *gtk.SearchEntry {
}
}
})
/*searchEntry.Connect("focus-in-event", func() {
searchEntry.SetText("")
})*/
return searchEntry
return sEntry
}
func isExcluded(dir string) bool {
for _, exclusion := range exclusions {
if strings.Contains(dir, exclusion) {
return true
}
}
return false
}
func searchUserDir(dir string) {
@@ -402,25 +476,28 @@ func searchUserDir(dir string) {
if len(fileSearchResults) > 0 {
btn := setUpUserDirButton(fmt.Sprintf("folder-%s", dir), "", dir, userDirsMap)
fileSearchResultFlowBox.Add(btn)
btn.Parent().(*gtk.FlowBoxChild).SetCanFocus(false)
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)) {
button := setUpUserFileSearchResultButton(partOfPathToShow, path)
fileSearchResultFlowBox.Add(button)
button.Parent().(*gtk.FlowBoxChild).SetCanFocus(false)
}
}
}
fileSearchResultFlowBox.Hide()
statusLabel.SetText(fmt.Sprintf("%v results | LMB: xdg-open | RMB: file manager",
fileSearchResultFlowBox.GetChildren().Length()))
num := uint(fileSearchResultFlowBox.GetChildren().Length() / *fsColumns)
len(fileSearchResultFlowBox.Children())))
num := uint(len(fileSearchResultFlowBox.Children())) / *fsColumns
fileSearchResultFlowBox.SetMinChildrenPerLine(num + 1)
fileSearchResultFlowBox.SetMaxChildrenPerLine(num + 1)
//While moving focus with arrow keys we want buttons to get focus directly
fileSearchResultFlowBox.GetChildren().Foreach(func(item interface{}) {
item.(*gtk.Widget).SetCanFocus(false)
})
fileSearchResultFlowBox.ShowAll()
}
}
@@ -430,10 +507,10 @@ func setUpUserDirButton(iconName, displayName, entryName string, userDirsMap map
parts := strings.Split(userDirsMap[entryName], "/")
displayName = parts[(len(parts) - 1)]
}
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
button, _ := gtk.ButtonNew()
box := gtk.NewBox(gtk.OrientationHorizontal, 0)
button := gtk.NewButton()
button.SetAlwaysShowImage(true)
img, _ := gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_MENU)
img := gtk.NewImageFromIconName(iconName, int(gtk.IconSizeMenu))
button.SetImage(img)
if len(displayName) > *nameLimit {
@@ -441,8 +518,8 @@ func setUpUserDirButton(iconName, displayName, entryName string, userDirsMap map
}
button.SetLabel(displayName)
button.Connect("button-release-event", func(btn *gtk.Button, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
button.Connect("button-release-event", func(btn *gtk.Button, event *gdk.Event) bool {
btnEvent := event.AsButton()
if btnEvent.Button() == 1 {
open(userDirsMap[entryName], true)
return true
@@ -453,18 +530,22 @@ func setUpUserDirButton(iconName, displayName, entryName string, userDirsMap map
return false
})
button.Connect("activate", func() {
open(userDirsMap[entryName], true)
})
box.PackStart(button, false, true, 0)
return box
}
func setUpUserFileSearchResultButton(fileName, filePath string) *gtk.Box {
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
button, _ := gtk.ButtonNew()
box := gtk.NewBox(gtk.OrientationHorizontal, 0)
button := gtk.NewButton()
// in the walk function we've marked directories with the '#is_dir#' prefix
if strings.HasPrefix(filePath, "#is_dir#") {
filePath = filePath[8:]
img, _ := gtk.ImageNewFromIconName("folder", gtk.ICON_SIZE_MENU)
img := gtk.NewImageFromIconName("folder", int(gtk.IconSizeMenu))
button.SetAlwaysShowImage(true)
button.SetImage(img)
}
@@ -479,8 +560,8 @@ func setUpUserFileSearchResultButton(fileName, filePath string) *gtk.Box {
button.SetTooltipText(tooltipText)
}
button.Connect("button-release-event", func(btn *gtk.Button, e *gdk.Event) bool {
btnEvent := gdk.EventButtonNewFromEvent(e)
button.Connect("button-release-event", func(btn *gtk.Button, event *gdk.Event) bool {
btnEvent := event.AsButton()
if btnEvent.Button() == 1 {
open(filePath, true)
return true
@@ -494,7 +575,54 @@ func setUpUserFileSearchResultButton(fileName, filePath string) *gtk.Box {
button.Connect("activate", func() {
open(filePath, true)
})
box.PackStart(button, false, true, 0)
return box
}
func setUpOperationResultWindow(operation string, result string) {
window := gtk.NewWindow(gtk.WindowToplevel)
window.SetModal(true)
if wayland() {
gtklayershell.InitForWindow(window)
gtklayershell.SetLayer(window, gtklayershell.LayerShellLayerOverlay)
gtklayershell.SetKeyboardMode(window, gtklayershell.LayerShellKeyboardModeExclusive)
}
// any key to close the window
window.Connect("key-release-event", func(_ *gtk.Window, event *gdk.Event) bool {
window.Destroy()
return true
})
// any button to close the window
window.Connect("button-release-event", func(_ *gtk.Window, event *gdk.Event) bool {
window.Destroy()
return true
})
outerVBox := gtk.NewBox(gtk.OrientationVertical, 6)
window.Add(outerVBox)
vBox := gtk.NewBox(gtk.OrientationHorizontal, 5)
outerVBox.PackStart(vBox, true, true, 6)
lbl := gtk.NewLabel(fmt.Sprintf("%s = %s", operation, result))
lbl.SetObjectProperty("name", "math-label")
vBox.PackStart(lbl, true, true, 12)
mRefProvider := gtk.NewCSSProvider()
css := "window { background-color: rgba (0, 0, 0, 255); color: #fff; border: solid 1px grey; border-radius: 5px}"
err := mRefProvider.LoadFromData(css)
if err != nil {
log.Warn(err)
}
ctx := window.StyleContext()
ctx.AddProvider(mRefProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
window.ShowAll()
if wayland() {
cmd := fmt.Sprintf("wl-copy %v", result)
launch(cmd, false, false)
}
}

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: %s", err)
}
for _, fp := range appDirs {
if err := filepath.Walk(fp, watchDir); err != nil {
log.Errorf("ERROR: %s", 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: %s", err)
}
}
}()
<-done
}
func watchDir(path string, fi os.FileInfo, err error) error {
if fi.Mode().IsDir() {
return watcher.Add(path)
}
return nil
}

View File

@@ -54,7 +54,34 @@ func parseDesktopEntry(id string, in io.Reader) (entry desktopEntry, err error)
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)
}