diff --git a/Makefile b/Makefile index 9144a52..cfd5213 100644 --- a/Makefile +++ b/Makefile @@ -6,11 +6,14 @@ get: 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 . install: + -pkill -f nwg-drawer + sleep 1 mkdir -p /usr/share/nwg-drawer cp -r desktop-directories /usr/share/nwg-drawer cp drawer.css /usr/share/nwg-drawer @@ -21,5 +24,4 @@ uninstall: rm /usr/bin/nwg-drawer run: - go build -o bin/nwg-drawer . - bin/nwg-drawer + go run . diff --git a/README.md b/README.md index 51641ca..9bdb617 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,11 @@ and `nwggrid`. [![Packaging status](https://repology.org/badge/vertical-allrepos/nwg-drawer.svg)](https://repology.org/project/nwg-drawer/versions) +## v0.2.x note + +1. Placing config files in the nwg-panel config directory was a mistake, sorry. The 0.2.0 version migrates them to `~/.config/nwg-drawer`. +2. From now on you may run the program residently, which should speed it up. See "Running" below. + ## Installation ### Dependencies @@ -55,12 +60,13 @@ $ nwg-drawer -h Usage of nwg-drawer: -c uint number of Columns (default 6) + -d Turn on Debug messages -fm string File Manager (default "thunar") -fscol uint File Search result COLumns (default 2) -fslen int - File Search name length Limit (default 80) + File Search name LENgth Limit (default 80) -is int Icon Size (default 64) -lang string @@ -73,6 +79,7 @@ Usage of nwg-drawer: name of the Output to display the drawer on (sway only) -ovl use OVerLay layer + -r Leave the program resident in memory -s string Styling: css file name (default "drawer.css") -spacing uint @@ -84,9 +91,43 @@ Usage of nwg-drawer: *NOTE: the `$TERM` environment variable overrides the `-term` argument if defined.* +## Running + +Since v0.2.x you may use the drawer in two ways: + +1. Simply run the `nwg-drawer` command, by adding a key binding to your sway config file, e.g.: + +```text +bindsym Mod1+F1 exec nwg-drawer +``` + +2. Run a resident instance on startup, and use the `nwg-drawer` command to show the window, e.g.: + +```text +exec_always nwg-drawer -r +bindsym Mod1+F1 exec nwg-drawer +``` + +The second line does nothing but `pkill -USR1 nwg-drawer`, so you may just use this command instead. Actually +this should be a little bit faster. + +Running a resident instance should speed up use of the drawer significantly. Pay attention to the fact, that you +need to `pkill -f nwg-drawer` and reload sway to apply any new arguments! + +## Logging + +In case you encounter an issue, you may need debug messages. If you use the resident instance, you'll see nothing +in the terminal. Please edit your sway config file: + +```text +exec nwg-drawer -r -d 2> ~/drawer.log +``` + +exit sway, launch it again and include the `drawer.log` content in the GitHub issue. Do not use `exec_always` here: it'll destroy the log file content on sway reload. + ## Styling -Edit `~/.config/nwg-panel/drawer.css` to your taste. +Edit `~/.config/nwg-drawer/drawer.css` to your taste. ## Files @@ -97,7 +138,7 @@ When the search phrase is at least 3 characters long, your XDG user directories Use the **left mouse button** to open a file with the `xdg-open` command. As configuring file associations for it is PITA, you may override them, by creating the `~/.config/nwg-panel/preferred-apps.json` file with your own definitions. -### Sample file content +### Sample `preferred-apps.json` file content ```json { @@ -110,8 +151,8 @@ PITA, you may override them, by creating the `~/.config/nwg-panel/preferred-apps } ``` -Use the **right mouse button** to open the file with your file manager (see `-fm` argument). The result depends on the -file manager you use. +Use the **right mouse button** to open the file with your file manager (see `-fm` argument). The result depends +on the file manager you use. - thunar will open the file location - pcmanfm will open the file with its associated program @@ -119,6 +160,16 @@ file manager you use. I've noy yet tried other file managers. +### File search exclusions + +You may want to exclude some paths inside your XDG user directories from searching. If so, define exclusions in the +`~/.config/nwg-panel/excluded-dirs` file, e.g. like this: + +```text +# exclude all paths containing 'node_modules' +node_modules +``` + ## Credits This program uses some great libraries: @@ -128,3 +179,5 @@ Copyright (c) 2015-2018 gotk3 contributors - [gotk3-layershell](https://github.com/dlasky/gotk3-layershell) by [@dlasky](https://github.com/dlasky/gotk3-layershell/commits?author=dlasky) - many thanks for writing this software, and for patience with my requests! - [go-sway](https://github.com/joshuarubin/go-sway) Copyright (c) 2019 Joshua Rubin - [go-singleinstance](github.com/allan-simon/go-singleinstance) Copyright (c) 2015 Allan Simon +- [logrus](https://github.com/sirupsen/logrus) Copyright (c) 2014 Simon Eskildsen +- [fsnotify](https://github.com/fsnotify/fsnotify) Copyright (c) 2012-2019 fsnotify Authors \ No newline at end of file diff --git a/bin/nwg-drawer b/bin/nwg-drawer index 154f699..ea7d8d4 100755 Binary files a/bin/nwg-drawer and b/bin/nwg-drawer differ diff --git a/go.mod b/go.mod index 3f18a8c..4ef0ff9 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,9 @@ go 1.16 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/dlasky/gotk3-layershell v0.0.0-20210827021656-e6ecab2731f7 + github.com/fsnotify/fsnotify v1.5.1 + github.com/gotk3/gotk3 v0.6.1 github.com/joshuarubin/go-sway v0.0.4 github.com/sirupsen/logrus v1.8.1 ) diff --git a/go.sum b/go.sum index 43ae0c6..4e40637 100644 --- a/go.sum +++ b/go.sum @@ -3,11 +3,12 @@ github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.m github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlasky/gotk3-layershell v0.0.0-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/dlasky/gotk3-layershell v0.0.0-20210827021656-e6ecab2731f7 h1:LDo0kwt+oW9a4lWlj2OakIgMW1ySXXVRGFt8GHUdYYA= +github.com/dlasky/gotk3-layershell v0.0.0-20210827021656-e6ecab2731f7/go.mod h1:JHLx2Wz4mAPVwn4PFhC69ydwyHP4A3wQvlg7HKVVc1U= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/gotk3/gotk3 v0.6.1 h1:GJ400a0ecEEWrzjBvzBzH+pB/esEMIGdB9zPSmBdoeo= +github.com/gotk3/gotk3 v0.6.1/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/joshuarubin/go-sway 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= @@ -26,5 +27,6 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= golang.org/x/sync v0.0.0-20190412183630-56d357773e84 h1:IqXQ59gzdXv58Jmm2xn0tSOR9i6HqroaOFRQ3wR/dJQ= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go index f2c3990..a3418f8 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/signal" + "path" "path/filepath" "strconv" "strings" @@ -20,16 +21,16 @@ import ( "github.com/gotk3/gotk3/gtk" ) -const version = "0.1.12" +const version = "0.2.0" var ( appDirs []string configDirectory string pinnedFile string pinned []string - src glib.SourceHandle id2entry map[string]desktopEntry preferredApps map[string]interface{} + exclusions []string ) var categoryNames = [...]string{ @@ -82,6 +83,7 @@ var desktopEntries []desktopEntry // UI elements var ( + win *gtk.Window resultWindow *gtk.ScrolledWindow fileSearchResults []string searchEntry *gtk.SearchEntry @@ -98,6 +100,9 @@ var ( statusLabel *gtk.Label status string ignore string + showWindowTrigger bool + desktopTrigger bool + pinnedTrigger bool ) func defaultStringIfBlank(s, fallback string) string { @@ -122,48 +127,79 @@ 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 nameLimit = flag.Int("fslen", 80, "File Search name LENgth Limit") var noCats = flag.Bool("nocats", false, "Disable filtering by category") var noFS = flag.Bool("nofs", false, "Disable file search") +var resident = flag.Bool("r", false, "Leave the program resident in memory") +var debug = flag.Bool("d", false, "Turn on Debug messages") func main() { timeStart := time.Now() flag.Parse() + if *debug { + log.SetLevel(log.DebugLevel) + } + if *displayVersion { fmt.Printf("nwg-drawer version %s\n", version) os.Exit(0) } // Gentle SIGTERM handler thanks to reiki4040 https://gist.github.com/reiki4040/be3705f307d3cd136e85 + // v0.2: we also need to support SIGUSR from now on signalChan := make(chan os.Signal, 1) - signal.Notify(signalChan, syscall.SIGTERM) + signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGUSR1) + go func() { for { s := <-signalChan - if s == syscall.SIGTERM { - log.Info("SIGTERM received, bye bye!") + switch s { + case syscall.SIGTERM: + log.Info("SIGTERM received, bye bye") gtk.MainQuit() + case syscall.SIGUSR1: + if *resident { + // As win.Show() called from inside a goroutine randomly crashes GTK, + // let's just set e helper variable here. We'll be checking it with glib.TimeoutAdd. + log.Debug("SIGUSR1 received, showing the window") + showWindowTrigger = true + } else { + log.Info("SIGUSR1 received, and I'm not resident, bye bye") + gtk.MainQuit() + } + default: + log.Info("Unknown signal") } } }() - // We want the same key/mouse binding to turn the dock off: kill the running instance and exit. - lockFilePath := fmt.Sprintf("%s/nwg-drawer.lock", tempDir()) + // If running instance found, we want it to show the window. The new instance will send SIGUSR1 and die + // (equivalent of `pkill -USR1 nwg-drawer`). + // Otherwise the command may behave in two ways: + // 1. kill the running non-residennt instance and exit; + // 2. die if a resident instance found. + lockFilePath := path.Join(tempDir(), "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 { - log.Info("Running instance found, sending SIGTERM and exiting...") - syscall.Kill(i, syscall.SIGTERM) + if *resident { + log.Warnf("Resident instance already running (PID %v)", i) + } else { + log.Infof("Showing resident instance (PID %v)", i) + syscall.Kill(i, syscall.SIGUSR1) + } } } os.Exit(0) } defer lockFile.Close() + log.Infof("term: %s", *term) + // LANGUAGE if *lang == "" && os.Getenv("LANG") != "" { *lang = strings.Split(os.Getenv("LANG"), ".")[0] @@ -173,6 +209,29 @@ func main() { // ENVIRONMENT configDirectory = configDir() + // Placing the drawer config files in the nwg-panel config directory was a mistake. + // Let's move them to their own location. + oldConfigDirectory, err := oldConfigDir() + if err == nil { + for _, p := range []string{"drawer.css", "preferred-apps.json"} { + if pathExists(path.Join(oldConfigDirectory, p)) { + log.Infof("File %s found in stale location, moving to %s", p, configDirectory) + if !pathExists(path.Join(configDirectory, p)) { + err = os.Rename(path.Join(oldConfigDirectory, p), path.Join(configDirectory, p)) + if err == nil { + log.Info("Success") + } else { + log.Warn(err) + } + } else { + log.Warnf("Failed moving %s to %s: path already exists!", path.Join(oldConfigDirectory, p), path.Join(configDirectory, p)) + } + + } + } + } + + // Copy default style sheet if not found if !pathExists(filepath.Join(configDirectory, "drawer.css")) { copyFile(filepath.Join(getDataHome(), "nwg-drawer/drawer.css"), filepath.Join(configDirectory, "drawer.css")) } @@ -205,12 +264,29 @@ func main() { // For opening files we use xdg-open. As its configuration is PITA, we may override some associations // in the ~/.config/nwg-panel/preferred-apps.json file. - paFile := filepath.Join(configDirectory, "preferred-apps.json") - preferredApps, err = loadPreferredApps(paFile) - if err != nil { - log.Error(fmt.Sprintf("Custom associations file %s not found or invalid", paFile)) + paFile := path.Join(configDirectory, "preferred-apps.json") + if pathExists(paFile) { + preferredApps, err = loadPreferredApps(paFile) + if err != nil { + log.Infof("Custom associations file %s not found or invalid", paFile) + } else { + log.Infof("Found %v associations in %s", len(preferredApps), paFile) + } } else { - log.Info(fmt.Sprintf("Found %v associations in %s", len(preferredApps), paFile)) + 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 @@ -228,7 +304,7 @@ func main() { gtk.AddProviderForScreen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) } - win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL) + win, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL) if err != nil { log.Fatal("Unable to create window:", err) } @@ -277,7 +353,11 @@ func main() { searchEntry.GrabFocus() searchEntry.SetText("") } else { - gtk.MainQuit() + if !*resident { + gtk.MainQuit() + } else { + restoreStateAndHide() + } } return false case gdk.KEY_downarrow, gdk.KEY_Up, gdk.KEY_Down, gdk.KEY_Left, gdk.KEY_Right, gdk.KEY_Tab, @@ -292,18 +372,6 @@ func main() { } }) - // Close the window on leave, but not immediately, to avoid accidental closes - win.Connect("leave-notify-event", func() { - src = glib.TimeoutAdd(uint(500), func() bool { - gtk.MainQuit() - return false - }) - }) - - win.Connect("enter-notify-event", func() { - cancelClose() - }) - /* In case someone REALLY needed to use X11 - for some stupid Zoom meeting or something, this allows the drawer to behave properly on Openbox, and possibly somewhere else. For sure not on i3. @@ -343,13 +411,15 @@ func main() { resultWindow, _ = gtk.ScrolledWindowNew(nil, nil) resultWindow.SetEvents(int(gdk.ALL_EVENTS_MASK)) resultWindow.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - resultWindow.Connect("enter-notify-event", func() { - cancelClose() - }) + resultWindow.Connect("button-release-event", func(sw *gtk.ScrolledWindow, e *gdk.Event) bool { btnEvent := gdk.EventButtonNewFromEvent(e) if btnEvent.Button() == 1 || btnEvent.Button() == 3 { - gtk.MainQuit() + if !*resident { + gtk.MainQuit() + } else { + restoreStateAndHide() + } return true } return false @@ -394,6 +464,7 @@ func main() { statusLineWrapper.PackStart(statusLabel, true, false, 0) win.ShowAll() + if !*noFS { fileSearchResultWrapper.SetSizeRequest(appFlowBox.GetAllocatedWidth(), 1) fileSearchResultWrapper.Hide() @@ -401,8 +472,69 @@ func main() { if !*noCats { categoriesWrapper.SetSizeRequest(1, categoriesWrapper.GetAllocatedHeight()*2) } + if *resident { + win.Hide() + } t := time.Now() 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) + glib.TimeoutAdd(uint(1), func() bool { + if showWindowTrigger && win != nil && !win.IsVisible() { + win.ShowAll() + // focus 1st element + b := appFlowBox.GetChildAtIndex(0) + if b != nil { + button, err := b.GetChild() + if err == nil { + button.ToWidget().GrabFocus() + } + } + } + showWindowTrigger = false + + // some .desktop file changed + if desktopTrigger { + log.Debug(".desktop file changed") + desktopFiles = listDesktopFiles() + status = parseDesktopFiles(desktopFiles) + appFlowBox = setUpAppsFlowBox(nil, "") + desktopTrigger = false + } + + // pinned file changed + if pinnedTrigger { + log.Debug("pinned file changed") + pinnedTrigger = false + pinned, _ = loadTextFile(pinnedFile) + pinnedFlowBox = setUpPinnedFlowBox() + } + return true + }) + + go watchFiles() + gtk.Main() } + +func restoreStateAndHide() { + timeStart1 := time.Now() + win.Hide() + + // clear search + searchEntry.SetText("") + + // clear category filter (in gotk3 it means: rebuild, as we have no filtering here) + appFlowBox = setUpAppsFlowBox(nil, "") + for _, btn := range catButtons { + btn.SetImagePosition(gtk.POS_LEFT) + btn.SetSizeRequest(0, 0) + } + + // scroll to the top + resultWindow.GetVAdjustment().SetValue(0) + + t := time.Now() + log.Debugf(fmt.Sprintf("UI hidden and restored in the backgroud in %v ms", t.Sub(timeStart1).Milliseconds())) +} diff --git a/tools.go b/tools.go index 79eabff..2ef5a44 100644 --- a/tools.go +++ b/tools.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "os" "os/exec" + "path" "path/filepath" "regexp" "sort" @@ -19,7 +20,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/gotk3/gotk3/gdk" - "github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/gtk" "github.com/joshuarubin/go-sway" ) @@ -28,19 +28,6 @@ func wayland() bool { return os.Getenv("WAYLAND_DISPLAY") != "" || os.Getenv("XDG_SESSION_TYPE") == "wayland" } -/* -Window leave-notify-event event quits the program with glib Timeout 500 ms. -We might have left the window by accident, so let's clear the timeout if window re-entered. -Furthermore - hovering a widget triggers window leave-notify-event event, and the timeout -needs to be cleared as well. -*/ -func cancelClose() { - if src > 0 { - glib.SourceRemove(src) - src = 0 - } -} - func createPixbuf(icon string, size int) (*gdk.Pixbuf, error) { iconTheme, err := gtk.IconThemeGetDefault() if err != nil { @@ -163,14 +150,29 @@ func readTextFile(path string) (string, error) { return string(bytes), nil } -func configDir() string { +func oldConfigDir() (string, error) { if os.Getenv("XDG_CONFIG_HOME") != "" { - dir := fmt.Sprintf("%s/nwg-panel", os.Getenv("XDG_CONFIG_HOME")) - createDir(dir) - return (fmt.Sprintf("%s/nwg-panel", os.Getenv("XDG_CONFIG_HOME"))) + dir := path.Join(os.Getenv("XDG_CONFIG_HOME"), "nwg-panel") + return dir, nil + } else if os.Getenv("HOME") != "" { + dir := path.Join(os.Getenv("HOME"), ".config/nwg-panel") + return dir, nil } - dir := fmt.Sprintf("%s/.config/nwg-panel", os.Getenv("HOME")) + + return "", errors.New("old config dir not found") +} + +func configDir() string { + var dir string + if os.Getenv("XDG_CONFIG_HOME") != "" { + dir = path.Join(os.Getenv("XDG_CONFIG_HOME"), "nwg-drawer") + } else if os.Getenv("HOME") != "" { + dir = path.Join(os.Getenv("HOME"), ".config/nwg-drawer") + } + + log.Infof("Config dir: %s", dir) createDir(dir) + return dir } @@ -240,11 +242,17 @@ func getAppDirs() []string { "/var/lib/flatpak/exports/share/applications"} for _, d := range flatpakDirs { - if !isIn(dirs, d) { + if pathExists(d) && !isIn(dirs, d) { dirs = append(dirs, d) } } - return dirs + var confirmedDirs []string + for _, d := range dirs { + if pathExists(d) { + confirmedDirs = append(confirmedDirs, d) + } + } + return confirmedDirs } func loadPreferredApps(path string) (map[string]interface{}, error) { @@ -350,6 +358,7 @@ func setUpCategories() { } func parseDesktopFiles(desktopFiles []string) string { + desktopEntries = nil id2entry = make(map[string]desktopEntry) skipped := 0 hidden := 0 @@ -368,7 +377,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 @@ -461,7 +470,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) } @@ -563,12 +572,13 @@ func launch(command string, terminal bool) { msg := fmt.Sprintf("env vars: %s; command: '%s'; args: %s\n", envVars, elements[cmdIdx], elements[1+cmdIdx:]) log.Info(msg) - go cmd.Run() + cmd.Start() - glib.TimeoutAdd(uint(150), func() bool { + if *resident { + restoreStateAndHide() + } else { gtk.MainQuit() - return false - }) + } } func open(filePath string, xdgOpen bool) { @@ -586,10 +596,15 @@ func open(filePath string, xdgOpen bool) { } else { cmd = exec.Command(*fileManager, filePath) } - fmt.Printf("Executing: %s", cmd) + log.Infof("Executing: %s", cmd) + cmd.Start() - gtk.MainQuit() + if *resident { + restoreStateAndHide() + } else { + gtk.MainQuit() + } } // Returns map output name -> gdk.Monitor diff --git a/uicomponents.go b/uicomponents.go index b7aac1a..f6b59d9 100644 --- a/uicomponents.go +++ b/uicomponents.go @@ -7,6 +7,7 @@ import ( "strings" log "github.com/sirupsen/logrus" + "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gtk" ) @@ -34,19 +35,13 @@ func setUpPinnedFlowBox() *gtk.FlowBox { btn, _ := gtk.ButtonNew() - var pixbuf *gdk.Pixbuf var img *gtk.Image - var err error if entry.Icon != "" { - pixbuf, err = createPixbuf(entry.Icon, *iconSize) + pixbuf, _ := createPixbuf(entry.Icon, *iconSize) + img, _ = gtk.ImageNewFromPixbuf(pixbuf) } else { - pixbuf, err = createPixbuf("image-missing", *iconSize) + img, _ = gtk.ImageNewFromIconName("image-missing", gtk.ICON_SIZE_INVALID) } - if err != nil { - log.Error(err) - pixbuf, _ = createPixbuf("unknown", *iconSize) - } - img, _ = gtk.ImageNewFromPixbuf(pixbuf) btn.SetImage(img) btn.SetAlwaysShowImage(true) @@ -72,7 +67,6 @@ func setUpPinnedFlowBox() *gtk.FlowBox { return true } else if btnEvent.Button() == 3 { unpinItem(entry.DesktopID) - pinnedFlowBox = setUpPinnedFlowBox() return true } return false @@ -92,10 +86,6 @@ func setUpPinnedFlowBox() *gtk.FlowBox { item.(*gtk.Widget).SetCanFocus(false) }) } - flowBox.Connect("enter-notify-event", func() { - cancelClose() - }) - flowBox.ShowAll() return flowBox @@ -115,9 +105,7 @@ func setUpCategoriesButtonBox() *gtk.EventBox { } eventBox, _ := gtk.EventBoxNew() - eventBox.Connect("enter-notify-event", func() { - cancelClose() - }) + hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) eventBox.Add(hBox) button, _ := gtk.ButtonNewWithLabel("All") @@ -138,7 +126,6 @@ func setUpCategoriesButtonBox() *gtk.EventBox { button.SetProperty("name", "category-button") catButtons = append(catButtons, button) button.SetLabel(cat.DisplayName) - // fix #8 button.SetAlwaysShowImage(true) hBox.PackStart(button, false, false, 0) name := cat.Name @@ -246,10 +233,10 @@ func flowBoxButton(entry desktopEntry) *gtk.Button { if entry.Icon != "" { pixbuf, err = createPixbuf(entry.Icon, *iconSize) } else { + log.Warnf("Undefined icon for %s", entry.Name) pixbuf, err = createPixbuf("image-missing", *iconSize) } if err != nil { - log.Error(err) pixbuf, _ = createPixbuf("unknown", *iconSize) } img, _ = gtk.ImageNewFromPixbuf(pixbuf) @@ -275,7 +262,6 @@ func flowBoxButton(entry desktopEntry) *gtk.Button { return true } else if btnEvent.Button() == 3 { pinItem(ID) - pinnedFlowBox = setUpPinnedFlowBox() return true } return false @@ -295,9 +281,6 @@ func setUpFileSearchResultContainer() *gtk.FlowBox { } flowBox, _ := gtk.FlowBoxNew() flowBox.SetProperty("orientation", gtk.ORIENTATION_VERTICAL) - flowBox.Connect("enter-notify-event", func() { - cancelClose() - }) fileSearchResultWrapper.PackStart(flowBox, false, false, 10) return flowBox @@ -309,7 +292,19 @@ func walk(path string, d fs.DirEntry, e error) error { } // don't search leading part of the path, as e.g. '/home/user/Pictures' toSearch := strings.Split(path, ignore)[1] - if strings.Contains(strings.ToLower(toSearch), strings.ToLower(phrase)) { + + // Remaing part of the path (w/o file name) must be checked against being present in excluded dirs + doSearch := true + parts := strings.Split(toSearch, "/") + remainingPart := "" + if len(parts) > 1 { + remainingPart = strings.Join(parts[:len(parts)-1], "/") + } + if remainingPart != "" && isExcluded(remainingPart) { + doSearch = false + } + + if doSearch && strings.Contains(strings.ToLower(toSearch), strings.ToLower(phrase)) { // mark directories if d.IsDir() { fileSearchResults = append(fileSearchResults, fmt.Sprintf("#is_dir#%s", path)) @@ -317,15 +312,16 @@ func walk(path string, d fs.DirEntry, e error) error { fileSearchResults = append(fileSearchResults, path) } } + return nil } func setUpSearchEntry() *gtk.SearchEntry { searchEntry, _ := gtk.SearchEntryNew() searchEntry.SetPlaceholderText("Type to search") - searchEntry.Connect("enter-notify-event", func() { + /*searchEntry.Connect("enter-notify-event", func() { cancelClose() - }) + })*/ searchEntry.Connect("search-changed", func() { for _, btn := range catButtons { btn.SetImagePosition(gtk.POS_LEFT) @@ -380,7 +376,6 @@ func setUpSearchEntry() *gtk.SearchEntry { if w == nil && fileSearchResultFlowBox != nil { f := fileSearchResultFlowBox.GetChildAtIndex(0) if f != nil { - //f.SetCanFocus(false) button, err := f.GetChild() if err == nil { button.ToWidget().SetCanFocus(true) @@ -401,13 +396,19 @@ func setUpSearchEntry() *gtk.SearchEntry { } } }) - /*searchEntry.Connect("focus-in-event", func() { - searchEntry.SetText("") - })*/ return searchEntry } +func isExcluded(dir string) bool { + for _, exclusion := range exclusions { + if strings.Contains(dir, exclusion) { + return true + } + } + return false +} + func searchUserDir(dir string) { fileSearchResults = nil ignore = userDirsMap[dir] @@ -420,8 +421,11 @@ func searchUserDir(dir string) { for _, path := range fileSearchResults { partOfPathToShow := strings.Split(path, userDirsMap[dir])[1] if partOfPathToShow != "" { - btn := setUpUserFileSearchResultButton(partOfPathToShow, path) - fileSearchResultFlowBox.Add(btn) + if !(strings.HasPrefix(path, "#is_dir#") && isExcluded(path)) { + btn := setUpUserFileSearchResultButton(partOfPathToShow, path) + fileSearchResultFlowBox.Add(btn) + } + } } fileSearchResultFlowBox.Hide() diff --git a/watcher.go b/watcher.go new file mode 100644 index 0000000..97f626b --- /dev/null +++ b/watcher.go @@ -0,0 +1,62 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + + log "github.com/sirupsen/logrus" + + "github.com/fsnotify/fsnotify" +) + +// Thanks to Steve Domino https://medium.com/@skdomino/watch-this-file-watching-in-go-5b5a247cf71f +var watcher *fsnotify.Watcher + +func watchFiles() { + + // creates a new file watcher + watcher, _ = fsnotify.NewWatcher() + defer watcher.Close() + + if err := watcher.Add(pinnedFile); err != nil { + log.Errorf("ERROR", err) + } + + for _, fp := range appDirs { + if err := filepath.Walk(fp, watchDir); err != nil { + log.Errorf("ERROR", err) + } + } + + done := make(chan bool) + + go func() { + for { + select { + case event := <-watcher.Events: + if strings.HasSuffix(event.Name, ".desktop") && + (event.Op.String() == "CREATE" || + event.Op.String() == "REMOVE" || + event.Op.String() == "RENAME") { + desktopTrigger = true + } else if event.Name == pinnedFile { + pinnedTrigger = true + } + + case err := <-watcher.Errors: + log.Errorf("ERROR", err) + } + } + }() + + <-done +} + +func watchDir(path string, fi os.FileInfo, err error) error { + if fi.Mode().IsDir() { + return watcher.Add(path) + } + + return nil +}