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

func buildMonthSelect(dateToShow time.Time, owningDialog *dialog.Dialog) *fyne.Container {
	// Calculate the days shown
	startOfMonth, _ := time.Parse("2006-January-03", fmt.Sprintf("%s-%s", dateToShow.Format("2006-January"), "01"))
	startOfMonthDisplay := startOfMonth
	startOffset := int(startOfMonth.Weekday())
	if startOffset != 6 {
		startOfMonthDisplay = startOfMonthDisplay.AddDate(0, 0, -1*int(startOfMonth.Weekday()))
	} else {
		startOffset = 0
	}
	totalDays := startOffset + startOfMonth.AddDate(0, 1, -1).Day()
	remainder := totalDays % 7
	if remainder > 0 {
		totalDays += 7 - totalDays%7
	}

	days := []fyne.CanvasObject{
		widget.NewLabelWithStyle("S", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
		widget.NewLabelWithStyle("M", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
		widget.NewLabelWithStyle("T", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
		widget.NewLabelWithStyle("W", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
		widget.NewLabelWithStyle("T", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
		widget.NewLabelWithStyle("F", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
		widget.NewLabelWithStyle("S", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
	}
	thisDay := startOfMonthDisplay
	todayString := time.Now().Format("01/02/2006")
	fmt.Printf("Today is %s\n", todayString)
	for i := 0; i < totalDays; i++ {
		mike := thisDay
		bg := canvas.NewRectangle(color.NRGBA{R: 220, G: 220, B: 220, A: 0})
		if thisDay.Format("01/02/2006") == todayString {
			bg = canvas.NewRectangle(color.NRGBA{R: 100, G: 200, B: 150, A: 255})
		}
		days = append(days, container.NewMax(bg, widget.NewButton(fmt.Sprintf("%d", thisDay.Day()), func() {
			x, _ := appStatus.CurrentZettleDKB.Get()
			saveZettle(markdownInput.Text, x)

			appStatus.CurrentZettleDBDate = mike
			appStatus.CurrentZettleDKB.Set(zettleFileName(appStatus.CurrentZettleDBDate))
			x, _ = appStatus.CurrentZettleDKB.Get()
			markdownInput.Text = getFileContentsAndCreateIfMissing(path.Join(appPreferences.ZettlekastenHome, x))
			markdownInput.Refresh()
			(*owningDialog).Hide()
		})))
		thisDay = thisDay.AddDate(0, 0, 1)
	}
	return container.NewGridWithColumns(7,
		days...)
}

func createDatePicker(dateToShow time.Time, owningDialog *dialog.Dialog) fyne.CanvasObject {
	var calendarWidget *fyne.Container
	var monthSelect *widget.Label
	var monthDisplay *fyne.Container
	var backMonth *widget.Button
	var forwardMonth *widget.Button

	monthSelect = widget.NewLabel(dateToShow.Format("January 2006"))

	monthDisplay = buildMonthSelect(dateToShow, owningDialog)

	backMonth = widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func() {
		dateToShow = dateToShow.AddDate(0, -1, 0)
		monthSelect = widget.NewLabel(dateToShow.Format("January 2006"))
		monthDisplay = buildMonthSelect(dateToShow, owningDialog)
		calendarWidget.RemoveAll()
		calendarWidget.Add(container.NewBorder(
			container.NewHBox(
				backMonth,
				layout.NewSpacer(),
				monthSelect,
				layout.NewSpacer(),
				forwardMonth,
			),
			nil,
			nil,
			nil,
			monthDisplay))
		calendarWidget.Refresh()
	})
	forwardMonth = widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func() {
		dateToShow = dateToShow.AddDate(0, 1, 0)
		fmt.Printf("Date to show %s\n", dateToShow)
		monthSelect = widget.NewLabel(dateToShow.Format("January 2006"))
		monthDisplay = buildMonthSelect(dateToShow, owningDialog)
		calendarWidget.RemoveAll()
		calendarWidget.Add(container.NewBorder(
			container.NewHBox(
				backMonth,
				layout.NewSpacer(),
				monthSelect,
				layout.NewSpacer(),
				forwardMonth,
			),
			nil,
			nil,
			nil,
			monthDisplay))
		calendarWidget.Refresh()
	})
	// Build the UI
	// Note: RemoveAll/Add required so the above back/Forward months look the same
	calendarWidget = container.NewHBox(widget.NewLabel("Loading"))
	calendarWidget.RemoveAll()
	calendarWidget.Add(container.NewBorder(
		container.NewHBox(
			backMonth,
			layout.NewSpacer(),
			monthSelect,
			layout.NewSpacer(),
			forwardMonth,
		),
		nil,
		nil,
		nil,
		monthDisplay))
	return calendarWidget
}

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.