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.
- A Systray icon to pop up a menu
- 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.
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 commandgrep
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 caselookfor
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.


