1package main 2 3import ( 4 "flag" 5 "fmt" 6 "log" 7 "os" 8 "os/signal" 9 "path/filepath" 10 "strconv" 11 "strings" 12 "syscall" 13 "time" 14 15 "github.com/allan-simon/go-singleinstance" 16 "github.com/dlasky/gotk3-layershell/layershell" 17 "github.com/gotk3/gotk3/gdk" 18 "github.com/gotk3/gotk3/glib" 19 "github.com/gotk3/gotk3/gtk" 20) 21 22const version = "0.1.1" 23 24var ( 25 appDirs []string 26 configDirectory string 27 pinnedFile string 28 pinned []string 29 leftBox *gtk.Box 30 rightBox *gtk.Box 31 src glib.SourceHandle 32 id2entry map[string]desktopEntry 33) 34 35var categoryNames = [...]string{ 36 "utility", 37 "development", 38 "game", 39 "graphics", 40 "internet-and-network", 41 "office", 42 "audio-video", 43 "system-tools", 44 "other", 45} 46 47type category struct { 48 Name string 49 DisplayName string 50 Icon string 51} 52 53var categories []category 54 55type desktopEntry struct { 56 DesktopID string 57 Name string 58 NameLoc string 59 Comment string 60 CommentLoc string 61 Icon string 62 Exec string 63 Terminal bool 64 NoDisplay bool 65} 66 67// slices below will hold DesktopID strings 68var ( 69 listUtility []string 70 listDevelopment []string 71 listGame []string 72 listGraphics []string 73 listInternetAndNetwork []string 74 listOffice []string 75 listAudioVideo []string 76 listSystemTools []string 77 listOther []string 78) 79 80var desktopEntries []desktopEntry 81 82// UI elements 83var ( 84 categoriesListBox *gtk.ListBox 85 userDirsListBox *gtk.ListBox 86 pinnedListBox *gtk.ListBox 87 resultWrapper *gtk.Box 88 resultWindow *gtk.ScrolledWindow 89 fileSearchResults map[string]string 90 fileSearchResultWindow *gtk.ScrolledWindow 91 backButton *gtk.Box 92 searchEntry *gtk.SearchEntry 93 phrase string 94 resultListBox *gtk.ListBox 95 fileSearchResultListBox *gtk.ListBox 96 buttonsWrapper *gtk.Box 97 buttonBox *gtk.EventBox 98 confirmationBox *gtk.Box 99 userDirsMap map[string]string 100) 101 102// Flags 103var cssFileName = flag.String("s", "menu-start.css", "Styling: css file name") 104var targetOutput = flag.String("o", "", "name of the Output to display the menu on") 105var displayVersion = flag.Bool("v", false, "display Version information") 106var autohide = flag.Bool("d", false, "auto-hiDe: close window when left") 107var valign = flag.String("va", "bottom", "Vertical Alignment: \"bottom\" or \"top\"") 108var halign = flag.String("ha", "left", "Horizontal Alignment: \"left\" or \"right\"") 109var marginTop = flag.Int("mt", 0, "Margin Top") 110var marginLeft = flag.Int("ml", 0, "Margin Left") 111var marginRight = flag.Int("mr", 0, "Margin Right") 112var marginBottom = flag.Int("mb", 0, "Margin Bottom") 113var iconSizeLarge = flag.Int("isl", 32, "Icon Size Large") 114var iconSizeSmall = flag.Int("iss", 16, "Icon Size Small") 115var sLen = flag.Int("slen", 80, "Search result length Limit") 116var itemPadding = flag.Uint("padding", 2, "vertical item padding") 117var lang = flag.String("lang", "", "force lang, e.g. \"en\", \"pl\"") 118var fileManager = flag.String("fm", "thunar", "File Manager") 119var term = flag.String("term", "alacritty", "Terminal emulator") 120var cmdLock = flag.String("cmd-lock", "swaylock -f -c 000000", "screen lock command") 121var cmdLogout = flag.String("cmd-logout", "swaymsg exit", "logout command") 122var cmdRestart = flag.String("cmd-restart", "shutdown -r now", "reboot command") 123var cmdShutdown = flag.String("cmd-shutdown", "shutdown -p now", "shutdown command") 124 125func main() { 126 timeStart := time.Now() 127 flag.Parse() 128 129 if *displayVersion { 130 fmt.Printf("nwg-menu version %s\n", version) 131 os.Exit(0) 132 } 133 134 // Gentle SIGTERM handler thanks to reiki4040 https://gist.github.com/reiki4040/be3705f307d3cd136e85 135 signalChan := make(chan os.Signal, 1) 136 signal.Notify(signalChan, syscall.SIGTERM) 137 go func() { 138 for { 139 s := <-signalChan 140 if s == syscall.SIGTERM { 141 println("SIGTERM received, bye bye!") 142 gtk.MainQuit() 143 } 144 } 145 }() 146 147 // We want the same key/mouse binding to turn the dock off: kill the running instance and exit. 148 lockFilePath := fmt.Sprintf("%s/nwg-menu.lock", tempDir()) 149 lockFile, err := singleinstance.CreateLockFile(lockFilePath) 150 if err != nil { 151 pid, err := readTextFile(lockFilePath) 152 if err == nil { 153 i, err := strconv.Atoi(pid) 154 if err == nil { 155 /*if !*autohide { 156 println("Running instance found, sending SIGTERM and exiting...") 157 syscall.Kill(i, syscall.SIGTERM) 158 } else { 159 println("Already running") 160 }*/ 161 println("Running instance found, sending SIGTERM and exiting...") 162 syscall.Kill(i, syscall.SIGTERM) 163 } 164 } 165 os.Exit(0) 166 } 167 defer lockFile.Close() 168 169 // LANGUAGE 170 if *lang == "" && os.Getenv("LANG") != "" { 171 *lang = strings.Split(os.Getenv("LANG"), ".")[0] 172 } 173 println(fmt.Sprintf("lang: %s", *lang)) 174 175 // ENVIRONMENT 176 configDirectory = configDir() 177 178 if !pathExists(filepath.Join(configDirectory, "menu-start.css")) { 179 copyFile("/usr/local/share/nwg-menu/menu-start.css", filepath.Join(configDirectory, "menu-start.css")) 180 } 181 182 cacheDirectory := cacheDir() 183 if cacheDirectory == "" { 184 log.Panic("Couldn't determine cache directory location") 185 } 186 187 // DATA 188 pinnedFile = filepath.Join(cacheDirectory, "nwg-pin-cache") 189 pinned, err = loadTextFile(pinnedFile) 190 if err != nil { 191 pinned = nil 192 } 193 194 cssFile := filepath.Join(configDirectory, *cssFileName) 195 196 appDirs = getAppDirs() 197 198 setUpCategories() 199 200 desktopFiles := listDesktopFiles() 201 println(fmt.Sprintf("Found %v desktop files", len(desktopFiles))) 202 203 parseDesktopFiles(desktopFiles) 204 205 // USER INTERFACE 206 gtk.Init(nil) 207 208 cssProvider, _ := gtk.CssProviderNew() 209 210 err = cssProvider.LoadFromPath(cssFile) 211 if err != nil { 212 println(fmt.Sprintf("ERROR: %s css file not found or erroneous. Using GTK styling.", cssFile)) 213 println(fmt.Sprintf(">>> %s", err)) 214 } else { 215 println(fmt.Sprintf("Using style from %s", cssFile)) 216 screen, _ := gdk.ScreenGetDefault() 217 gtk.AddProviderForScreen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 218 } 219 220 win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL) 221 if err != nil { 222 log.Fatal("Unable to create window:", err) 223 } 224 225 layershell.InitForWindow(win) 226 227 var output2mon map[string]*gdk.Monitor 228 if *targetOutput != "" { 229 // We want to assign layershell to a monitor, but we only know the output name! 230 output2mon, err = mapOutputs() 231 if err == nil { 232 monitor := output2mon[*targetOutput] 233 layershell.SetMonitor(win, monitor) 234 235 } else { 236 println(err) 237 } 238 } 239 240 if *valign == "bottom" { 241 layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_BOTTOM, true) 242 } else { 243 layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_TOP, true) 244 } 245 246 if *halign == "left" { 247 layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_LEFT, true) 248 } else { 249 layershell.SetAnchor(win, layershell.LAYER_SHELL_EDGE_RIGHT, true) 250 } 251 252 layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_TOP) 253 254 layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_TOP, *marginTop) 255 layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_LEFT, *marginLeft) 256 layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_RIGHT, *marginRight) 257 layershell.SetMargin(win, layershell.LAYER_SHELL_EDGE_BOTTOM, *marginBottom) 258 259 layershell.SetKeyboardMode(win, layershell.LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE) 260 261 win.Connect("destroy", func() { 262 gtk.MainQuit() 263 }) 264 265 win.Connect("key-release-event", func(window *gtk.Window, event *gdk.Event) { 266 key := &gdk.EventKey{Event: event} 267 if key.KeyVal() == gdk.KEY_Escape { 268 s, _ := searchEntry.GetText() 269 if s != "" { 270 clearSearchResult() 271 searchEntry.GrabFocus() 272 searchEntry.SetText("") 273 } else { 274 if resultWindow == nil || !resultWindow.GetVisible() { 275 gtk.MainQuit() 276 } else { 277 clearSearchResult() 278 } 279 } 280 } 281 }) 282 283 // Close the window on leave, but not immediately, to avoid accidental closes 284 win.Connect("leave-notify-event", func() { 285 if *autohide { 286 src = glib.TimeoutAdd(uint(1000), func() bool { 287 gtk.MainQuit() 288 return false 289 }) 290 } 291 }) 292 293 win.Connect("enter-notify-event", func() { 294 cancelClose() 295 }) 296 297 outerBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) 298 win.Add(outerBox) 299 300 alignmentBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) 301 //alignmentBox.SetHomogeneous(true) 302 outerBox.PackStart(alignmentBox, true, true, 0) 303 304 leftBox, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) 305 alignmentBox.PackStart(leftBox, false, false, 10) 306 307 leftColumn, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) 308 leftBox.PackStart(leftColumn, false, false, 0) 309 310 searchEntry = setUpSearchEntry() 311 if *valign == "top" { 312 leftColumn.PackStart(searchEntry, false, false, 10) 313 } 314 315 pinnedListBox = setUpPinnedListBox() 316 leftColumn.PackStart(pinnedListBox, false, false, 10) 317 318 categoriesListBox = setUpCategoriesListBox() 319 leftColumn.PackStart(categoriesListBox, false, false, 10) 320 321 if *valign != "top" { 322 leftColumn.PackEnd(searchEntry, false, false, 10) 323 } 324 325 rightBox, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) 326 alignmentBox.PackStart(rightBox, true, true, 10) 327 328 rightColumn, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) 329 330 rightBox.PackStart(rightColumn, true, true, 0) 331 332 userDirsListBox = setUpUserDirsList() 333 rightColumn.PackStart(userDirsListBox, false, true, 10) 334 335 backButton = setUpBackButton() 336 rightColumn.PackStart(backButton, false, false, 10) 337 338 resultWrapper, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) 339 rightColumn.PackStart(resultWrapper, true, true, 0) 340 341 buttonsWrapper, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) 342 343 buttonBox = setUpButtonBox() 344 buttonsWrapper.PackStart(buttonBox, false, false, 10) 345 rightColumn.PackEnd(buttonsWrapper, false, true, 0) 346 347 //win.SetSizeRequest(0, *windowHeigth) 348 349 win.ShowAll() 350 351 backButton.Hide() 352 353 pinnedListBox.UnselectAll() 354 categoriesListBox.UnselectAll() 355 searchEntry.GrabFocus() 356 t := time.Now() 357 println(fmt.Sprintf("UI created in %v ms. Thanks for watching.", t.Sub(timeStart).Milliseconds())) 358 gtk.Main() 359} 360