WILT: Golang OS specificity and Windows Findstr

I appear to have many itches to scratch. I should see if there's a code topical ointment.

In keeping notes for work and home, I've adopted Zettlekasten. I've gone into that before. VSCode is good for markdown, but I wanted something that was always a click away to take a note. I'd also found my router's parental controls a pain, and wanted an easier way to turn it off for a time.

I'd previously experimented in using Fyne and Systray together, so I started making a tool.

The Application.

  1. A Systray icon to pop up a menu
  2. Menu has Note for markdown and Internet to turn off the parent controls for a time

Simples.

The Problems

Systray and Fyne

Systray is the Golang library to put an icon in the systray. Fyne is a good Golang library for GUI windows. Both of them want to be the last thing executed and hold the main loop. I'd previously documented a workaround I found - but Golang has since incorporated Systray directly into the library. Use that.

Router and JS

The router actually has some decent security concepts - the username and password are encrypted in the browser by some JS before being sent to the router for logging in. Annoying. I tried using the Webview thing but it got annoying. In the end I just grabbed Selenium and made an automated browser thing. Works alright

Markdown preview

Markdown was easy to do, it's just a text editor. It would be nice to have a lovely rendered version of it. Fyne ships with a Markdown preview, but it's not great. Not sure what to do here, wondering if I can use another Webview and roll the Markdown through the https://github.com/yuin/goldmark library.

Date dialog

Interestingly, Fyne doesn't come with a Date selector. It was a fun exercise to figure out how to build a date selector in Fyne. In the below, buildMonthSelect is given a date to show and builds a month calendar view. createDatePicker wraps that in a modal window with navigation buttons for shifting months.

Date picker

  1func buildMonthSelect(dateToShow time.Time, owningDialog *dialog.Dialog) *fyne.Container {
  2	// Calculate the days shown
  3	startOfMonth, _ := time.Parse("2006-January-03", fmt.Sprintf("%s-%s", dateToShow.Format("2006-January"), "01"))
  4	startOfMonthDisplay := startOfMonth
  5	startOffset := int(startOfMonth.Weekday())
  6	if startOffset != 6 {
  7		startOfMonthDisplay = startOfMonthDisplay.AddDate(0, 0, -1*int(startOfMonth.Weekday()))
  8	} else {
  9		startOffset = 0
 10	}
 11	totalDays := startOffset + startOfMonth.AddDate(0, 1, -1).Day()
 12	remainder := totalDays % 7
 13	if remainder > 0 {
 14		totalDays += 7 - totalDays%7
 15	}
 16
 17	days := []fyne.CanvasObject{
 18		widget.NewLabelWithStyle("S", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
 19		widget.NewLabelWithStyle("M", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
 20		widget.NewLabelWithStyle("T", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
 21		widget.NewLabelWithStyle("W", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
 22		widget.NewLabelWithStyle("T", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
 23		widget.NewLabelWithStyle("F", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
 24		widget.NewLabelWithStyle("S", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
 25	}
 26	thisDay := startOfMonthDisplay
 27	todayString := time.Now().Format("01/02/2006")
 28	fmt.Printf("Today is %s\n", todayString)
 29	for i := 0; i < totalDays; i++ {
 30		mike := thisDay
 31		bg := canvas.NewRectangle(color.NRGBA{R: 220, G: 220, B: 220, A: 0})
 32		if thisDay.Format("01/02/2006") == todayString {
 33			bg = canvas.NewRectangle(color.NRGBA{R: 100, G: 200, B: 150, A: 255})
 34		}
 35		days = append(days, container.NewMax(bg, widget.NewButton(fmt.Sprintf("%d", thisDay.Day()), func() {
 36			x, _ := appStatus.CurrentZettleDKB.Get()
 37			saveZettle(markdownInput.Text, x)
 38
 39			appStatus.CurrentZettleDBDate = mike
 40			appStatus.CurrentZettleDKB.Set(zettleFileName(appStatus.CurrentZettleDBDate))
 41			x, _ = appStatus.CurrentZettleDKB.Get()
 42			markdownInput.Text = getFileContentsAndCreateIfMissing(path.Join(appPreferences.ZettlekastenHome, x))
 43			markdownInput.Refresh()
 44			(*owningDialog).Hide()
 45		})))
 46		thisDay = thisDay.AddDate(0, 0, 1)
 47	}
 48	return container.NewGridWithColumns(7,
 49		days...)
 50}
 51
 52func createDatePicker(dateToShow time.Time, owningDialog *dialog.Dialog) fyne.CanvasObject {
 53	var calendarWidget *fyne.Container
 54	var monthSelect *widget.Label
 55	var monthDisplay *fyne.Container
 56	var backMonth *widget.Button
 57	var forwardMonth *widget.Button
 58
 59	monthSelect = widget.NewLabel(dateToShow.Format("January 2006"))
 60
 61	monthDisplay = buildMonthSelect(dateToShow, owningDialog)
 62
 63	backMonth = widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func() {
 64		dateToShow = dateToShow.AddDate(0, -1, 0)
 65		monthSelect = widget.NewLabel(dateToShow.Format("January 2006"))
 66		monthDisplay = buildMonthSelect(dateToShow, owningDialog)
 67		calendarWidget.RemoveAll()
 68		calendarWidget.Add(container.NewBorder(
 69			container.NewHBox(
 70				backMonth,
 71				layout.NewSpacer(),
 72				monthSelect,
 73				layout.NewSpacer(),
 74				forwardMonth,
 75			),
 76			nil,
 77			nil,
 78			nil,
 79			monthDisplay))
 80		calendarWidget.Refresh()
 81	})
 82	forwardMonth = widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func() {
 83		dateToShow = dateToShow.AddDate(0, 1, 0)
 84		fmt.Printf("Date to show %s\n", dateToShow)
 85		monthSelect = widget.NewLabel(dateToShow.Format("January 2006"))
 86		monthDisplay = buildMonthSelect(dateToShow, owningDialog)
 87		calendarWidget.RemoveAll()
 88		calendarWidget.Add(container.NewBorder(
 89			container.NewHBox(
 90				backMonth,
 91				layout.NewSpacer(),
 92				monthSelect,
 93				layout.NewSpacer(),
 94				forwardMonth,
 95			),
 96			nil,
 97			nil,
 98			nil,
 99			monthDisplay))
100		calendarWidget.Refresh()
101	})
102	// Build the UI
103	// Note: RemoveAll/Add required so the above back/Forward months look the same
104	calendarWidget = container.NewHBox(widget.NewLabel("Loading"))
105	calendarWidget.RemoveAll()
106	calendarWidget.Add(container.NewBorder(
107		container.NewHBox(
108			backMonth,
109			layout.NewSpacer(),
110			monthSelect,
111			layout.NewSpacer(),
112			forwardMonth,
113		),
114		nil,
115		nil,
116		nil,
117		monthDisplay))
118	return calendarWidget
119}

Learnings

  • Fyne buttons can't have backgrounds or anything outside the global Theme. To colour a button, colour a rectangle behind it.
  • Fyne has systray built in it now
  • To build code specific to different OS's, there's code comments you put at the top of the file.
    • //go:build !windows to not include this file for Windows compiles
    • //go:build windows to include this file for Windows compiles
  • Webview is tricky to control through JS thrown at it
  • Fyne markdown preview was built in Fyne following only the basic spec. No tables!
  • Fyne doesn't have a date picker

Searching for text in files

To add a search to the application, I looked into how to integrate with Windows and Finder indexes and cheat my way out. But I couldn't find out how to do it. I fell back to command line options. Find and Grep are instinctively what I reach for in Unix. But what about Windows?

Grep and Find

cmdVariables := []string{"/bin/sh", "-c", "find . -type f \( -name '*.markdown' -o -name '*.md' \) -exec grep -li '" + lookfor + "' {} \;"}

  • bin/sh -c invokes the linux shell. Since Golang is very aggressive in quoting parameters, the find commands of \( and \; end up getting swallowed so find doesn't work. So I have to wrap it in a single command passed to bash.
  • find is a standard Unix command for finding files/ directories based on 'things'.
  • -type f tells find to only look for Files, ignoring Directories. It still goes down any subdir it finds though
  • \( -name "*.markdown" -o -name "*.md" \) a few things going on here. -o is "Or", a way of combining multiple selectors. name "*.markdown" is to find any files with the extension markdown.
  • -exec run a command
  • grep this is the command we've asked find to run on every file that matches *.markdown or *.md. Grep looks in a file for things
  • -li the l flag is just show me the filename, the i flag is ignore the case
  • lookfor is the variable holding the string to look for
  • {} find changes that symbol combination to be the name of the matched file, so grep will search the file found
  • \; that tells find that's the end of the command

Updated 20220808: The original command here worked in Windows Bash, but not in OSX Zsh, so I've updated it to work there.

There might be a better way of doing that, please tell me if there is.

Findstr

cmdVariables := []string{"findstr", fmt.Sprintf("/simc:%s", lookfor), "*.markdown", "*.md"}

  • findstr is a standard Windows command for looking for strings in files, so combining find and grep in one
  • /simc:[lookfor] the s flag says look in sub-directories, the i flag says to ignore the case, the m flag says only show the file name, c says look for exactly the text in [lookfor].
  • *.markdown *.md you then add in the filemasks that you want to look in.

Learnings

  • Findstr is finicky. If you don't have end a file with a carriage return and you search with the flags to return the contents of the file, and you have a match on the last line of the file, you don't have a carriage return between found files. There's no other codes to then see where that line ends and the next filename found starts; so I had to do filename only matching.
  • Find included the leading './' in the file path, so I had to go through and trim all of those to get a joinable URL.