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