diff --git a/bin/nwg-drawer b/bin/nwg-drawer index 154f699..5d654c5 100755 Binary files a/bin/nwg-drawer and b/bin/nwg-drawer differ diff --git a/main.go b/main.go index c0e5dde..3a0d778 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{} + //src glib.SourceHandle + id2entry map[string]desktopEntry + preferredApps map[string]interface{} ) 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,7 @@ var ( statusLabel *gtk.Label status string ignore string + showWindowTrigger bool ) func defaultStringIfBlank(s, fallback string) string { @@ -125,45 +128,74 @@ var term = flag.String("term", defaultStringIfBlank(os.Getenv("TERM"), "alacritt 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.Debug("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.Debug("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 and running residently, we want it to refresh and show the window. + // Otherwise we want the same command to terminate the drawer: kill the running instance and exit. + //lockFilePath := fmt.Sprintf("%s/nwg-drawer.lock", tempDir()) + 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] @@ -226,7 +258,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) } @@ -275,7 +307,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, @@ -290,18 +326,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. @@ -341,13 +365,17 @@ 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() { + /*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 @@ -392,6 +420,7 @@ func main() { statusLineWrapper.PackStart(statusLabel, true, false, 0) win.ShowAll() + if !*noFS { fileSearchResultWrapper.SetSizeRequest(appFlowBox.GetAllocatedWidth(), 1) fileSearchResultWrapper.Hide() @@ -399,8 +428,50 @@ 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 + return true + }) + gtk.Main() } + +func restoreStateAndHide() { + timeStart1 := time.Now() + win.Hide() + + // 1. clear search + searchEntry.SetText("") + + // 2. 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) + } + + // 3. 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..741ec34 100644 --- a/tools.go +++ b/tools.go @@ -19,7 +19,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" ) @@ -34,12 +33,12 @@ We might have left the window by accident, so let's clear the timeout if window Furthermore - hovering a widget triggers window leave-notify-event event, and the timeout needs to be cleared as well. */ -func cancelClose() { +/*func cancelClose() { if src > 0 { glib.SourceRemove(src) src = 0 } -} +}*/ func createPixbuf(icon string, size int) (*gdk.Pixbuf, error) { iconTheme, err := gtk.IconThemeGetDefault() @@ -563,12 +562,20 @@ 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() + + if *resident { + restoreStateAndHide() + } else { + gtk.MainQuit() + } + + /*go cmd.Run() glib.TimeoutAdd(uint(150), func() bool { gtk.MainQuit() return false - }) + })*/ } func open(filePath string, xdgOpen bool) { @@ -586,10 +593,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..c4ff2c6 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) @@ -92,11 +87,11 @@ func setUpPinnedFlowBox() *gtk.FlowBox { item.(*gtk.Widget).SetCanFocus(false) }) } - flowBox.Connect("enter-notify-event", func() { + /*flowBox.Connect("enter-notify-event", func() { cancelClose() - }) + })*/ - flowBox.ShowAll() + //flowBox.ShowAll() return flowBox } @@ -115,9 +110,9 @@ func setUpCategoriesButtonBox() *gtk.EventBox { } eventBox, _ := gtk.EventBoxNew() - eventBox.Connect("enter-notify-event", func() { + /*eventBox.Connect("enter-notify-event", func() { cancelClose() - }) + })*/ hBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) eventBox.Add(hBox) button, _ := gtk.ButtonNewWithLabel("All") @@ -246,6 +241,7 @@ 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 { @@ -295,9 +291,9 @@ func setUpFileSearchResultContainer() *gtk.FlowBox { } flowBox, _ := gtk.FlowBoxNew() flowBox.SetProperty("orientation", gtk.ORIENTATION_VERTICAL) - flowBox.Connect("enter-notify-event", func() { + /*flowBox.Connect("enter-notify-event", func() { cancelClose() - }) + })*/ fileSearchResultWrapper.PackStart(flowBox, false, false, 10) return flowBox @@ -323,9 +319,9 @@ func walk(path string, d fs.DirEntry, e error) error { 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)