From a4a4f5ab1d7797a5d5cd04913d3a279c5ae466c3 Mon Sep 17 00:00:00 2001 From: Sandro Heinzelmann Date: Sun, 2 May 2021 10:12:39 +0200 Subject: [PATCH] Reload snippets.yml on changes --- Makefile | 5 +- go.mod | 1 + main.go | 160 +++++++++++++----------------------------------- searchWidget.go | 149 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 196 insertions(+), 119 deletions(-) create mode 100644 searchWidget.go diff --git a/Makefile b/Makefile index acd05e5..40614e8 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ build-centos: ensure-centos-image .PHONY: build-windows build-windows: - GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go build ${EXTRA_BUILD_ARGS} + GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go build -ldflags "-H=windowsgui ${EXTRA_WIN_LDFLAGS}" ${EXTRA_WIN_BUILD_ARGS} .PHONY: build-linux build-linux: @@ -75,7 +75,8 @@ print-version: .PHONY: build-all-optimized build-all-optimized: EXTRA_BUILD_ARGS=-ldflags='-s -w' -build-all-optimized: build-linux build-centos build-windows +build-all-optimized: EXTRA_WIN_LDFLAGS=-s -w +build-all-optimized: build-linux build-centos build-windows upx: wget https://github.com/upx/upx/releases/download/v3.96/upx-3.96-amd64_linux.tar.xz diff --git a/go.mod b/go.mod index 5beef7c..1af753e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( fyne.io/fyne/v2 v2.0.3 + github.com/fsnotify/fsnotify v1.4.9 github.com/go-vgo/robotgo v0.93.1 github.com/lithammer/fuzzysearch v1.1.2 github.com/robotn/gohook v0.30.5 diff --git a/main.go b/main.go index 0e837a2..026c78b 100644 --- a/main.go +++ b/main.go @@ -1,23 +1,19 @@ package main import ( - "fmt" "image/color" - "math" + "log" "os" "path/filepath" - "sort" "strings" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" - "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" + "github.com/fsnotify/fsnotify" "github.com/go-vgo/robotgo" - "github.com/lithammer/fuzzysearch/fuzzy" hook "github.com/robotn/gohook" "gopkg.in/yaml.v2" ) @@ -28,7 +24,9 @@ type snippet struct { } func main() { - snippets, snippetsText, err := loadSnippets() + dir, _ := filepath.Abs(filepath.Dir(os.Args[0])) + snippetsFile := filepath.Join(dir, "snippets.yml") + snippets, err := loadSnippets(snippetsFile) if err != nil { panic(err) } @@ -42,8 +40,7 @@ func main() { w = a.NewWindow("") } - list, entry := createSearchWidget(snippets, - snippetsText, + search := newSearchWidget(snippets, func(snippet *snippet) { w.Hide() typeSnippet(snippet.content) @@ -53,11 +50,20 @@ func main() { }, ) - split := container.NewVSplit(entry, list) + go watchSnippets(snippetsFile, func() { + snippets, err := loadSnippets(snippetsFile) + if err != nil { + log.Println("error reloading snippets.yml:", err) + return + } + search.setSnippets(snippets) + }) + + split := container.NewVSplit(search.entry, search.list) split.Offset = 0 w.SetContent(split) w.Resize(fyne.NewSize(400, 400)) - w.Canvas().Focus(entry) + w.Canvas().Focus(search.entry) w.CenterOnScreen() go listenForHotkeys(w) @@ -65,96 +71,56 @@ func main() { w.ShowAndRun() } -func loadSnippets() ([]*snippet, []string, error) { - dir, _ := filepath.Abs(filepath.Dir(os.Args[0])) - bytes, err := os.ReadFile(filepath.Join(dir, "snippets.yml")) +func loadSnippets(snippetsFile string) ([]*snippet, error) { + bytes, err := os.ReadFile(snippetsFile) if err != nil { - return nil, nil, err + return nil, err } var rawSnippets map[string]string err = yaml.Unmarshal(bytes, &rawSnippets) if err != nil { - return nil, nil, err + return nil, err } var snippets []*snippet - var snippetsText []string for k, v := range rawSnippets { snippets = append(snippets, &snippet{ label: k, content: v, }) - snippetsText = append(snippetsText, fmt.Sprintf("%s: %s", k, v)) } - return snippets, snippetsText, nil + return snippets, nil } -func createSearchWidget(snippets []*snippet, snippetsText []string, onSubmit func(snippet *snippet), onCancel func()) (*widget.List, *snippetEntry) { - filteredSnippets := snippets - - list := widget.NewList( - func() int { - return len(filteredSnippets) - }, - func() fyne.CanvasObject { - label := widget.NewLabel("tmpl lbl") - label.TextStyle.Bold = true - content := canvas.NewText("tmpl content", color.RGBA{128, 128, 128, 255}) - content.TextStyle.Monospace = true - return container.NewHBox(label, content) - }, - func(id widget.ListItemID, item fyne.CanvasObject) { - container := item.(*fyne.Container) - label := container.Objects[0].(*widget.Label) - content := container.Objects[1].(*canvas.Text) - label.SetText(filteredSnippets[id].label) - content.Text = strings.ReplaceAll(filteredSnippets[id].content, "\n", "\\n") - ellipsis(container, content) - }, - ) - - var selectedID widget.ListItemID = -1 - list.OnSelected = func(id widget.ListItemID) { - selectedID = id +func watchSnippets(snippetsFile string, onModified func()) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + panic(err) } - list.Select(0) + defer watcher.Close() - entry := newSnippetEntry() - - resetSearch := func() { - entry.Text = "" - entry.OnChanged(entry.Text) - list.Select(0) + err = watcher.Add(snippetsFile) + if err != nil { + panic(err) } - entry.onTypedKey = func(key *fyne.KeyEvent) { - if key.Name == "Down" { - list.Select((selectedID + 1) % len(filteredSnippets)) - } else if key.Name == "Up" { - list.Select((len(filteredSnippets) + selectedID - 1) % len(filteredSnippets)) - } else if key.Name == "Return" { - if selectedID >= 0 && selectedID < len(filteredSnippets) { - onSubmit(filteredSnippets[selectedID]) - resetSearch() + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return } - } else if key.Name == "Escape" { - onCancel() - resetSearch() - } - } - entry.OnChanged = func(s string) { - ranked := fuzzy.RankFindFold(s, snippetsText) - sort.Sort(ranked) - filteredSnippets = make([]*snippet, 0) - for _, r := range ranked { - filteredSnippets = append(filteredSnippets, snippets[r.OriginalIndex]) + if event.Op&fsnotify.Write == fsnotify.Write { + onModified() + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Println("error:", err) } - list.Refresh() - list.Select(0) } - - return list, entry } func listenForHotkeys(w fyne.Window) { @@ -180,46 +146,6 @@ func typeSnippet(content string) { } } -type snippetEntry struct { - widget.Entry - onTypedKey func(key *fyne.KeyEvent) -} - -func newSnippetEntry() *snippetEntry { - e := &snippetEntry{} - e.ExtendBaseWidget(e) - return e -} - -func (e *snippetEntry) TypedKey(key *fyne.KeyEvent) { - e.Entry.TypedKey(key) - if e.onTypedKey != nil { - e.onTypedKey(key) - } -} - -func ellipsis(container *fyne.Container, label *canvas.Text) { - w := fyne.MeasureText(string(label.Text), theme.TextSize(), label.TextStyle).Width - if label.Position().X+w > container.Size().Width { - wellipsis := fyne.MeasureText(string("..."), theme.TextSize(), label.TextStyle).Width - wmax := container.Size().Width - wellipsis - wpc := float64(w) / float64(len(label.Text)) - k := 0 - for label.Position().X+w > wmax { - overlap := label.Position().X + w - wmax - overlapc := int(math.Ceil(math.Max(1, float64(overlap)/wpc))) - if overlapc > len(label.Text) { - break - } - label.Text = label.Text[:len(label.Text)-overlapc] - w = fyne.MeasureText(string(label.Text), theme.TextSize(), label.TextStyle).Width - k++ - } - label.Text += "..." - label.Refresh() - } -} - type myTheme struct{} func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color { diff --git a/searchWidget.go b/searchWidget.go new file mode 100644 index 0000000..5d01c58 --- /dev/null +++ b/searchWidget.go @@ -0,0 +1,149 @@ +package main + +import ( + "fmt" + "image/color" + "math" + "sort" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/lithammer/fuzzysearch/fuzzy" +) + +type searchWidget struct { + snippets []*snippet + snippetsText []string + filteredSnippets []*snippet + selectedID widget.ListItemID + onSubmit func(snippet *snippet) + onCancel func() + list *widget.List + entry *searchEntry +} + +func newSearchWidget(snippets []*snippet, onSubmit func(snippet *snippet), onCancel func()) *searchWidget { + w := &searchWidget{onSubmit: onSubmit, onCancel: onCancel} + w.createList() + w.createEntry() + w.setSnippets(snippets) + return w +} + +func (w *searchWidget) setSnippets(snippets []*snippet) { + var snippetsText []string + for _, s := range snippets { + snippetsText = append(snippetsText, fmt.Sprintf("%s: %s", s.label, s.content)) + } + w.snippetsText = snippetsText + w.snippets = snippets + w.entry.OnChanged(w.entry.Text) +} + +func (w *searchWidget) createList() { + w.list = widget.NewList( + func() int { + return len(w.filteredSnippets) + }, + func() fyne.CanvasObject { + label := widget.NewLabel("tmpl lbl") + label.TextStyle.Bold = true + content := canvas.NewText("tmpl content", color.RGBA{128, 128, 128, 255}) + content.TextStyle.Monospace = true + return container.NewHBox(label, content) + }, + func(id widget.ListItemID, item fyne.CanvasObject) { + container := item.(*fyne.Container) + label := container.Objects[0].(*widget.Label) + content := container.Objects[1].(*canvas.Text) + label.SetText(w.filteredSnippets[id].label) + content.Text = strings.ReplaceAll(w.filteredSnippets[id].content, "\n", "\\n") + ellipsis(container, content) + }, + ) + + w.list.OnSelected = func(id widget.ListItemID) { + w.selectedID = id + } + w.list.Select(0) +} + +func (w *searchWidget) createEntry() { + w.entry = newSearchEntry() + + resetSearch := func() { + w.entry.Text = "" + w.entry.OnChanged(w.entry.Text) + w.list.Select(0) + } + + w.entry.onTypedKey = func(key *fyne.KeyEvent) { + if key.Name == "Down" { + w.list.Select((w.selectedID + 1) % len(w.filteredSnippets)) + } else if key.Name == "Up" { + w.list.Select((len(w.filteredSnippets) + w.selectedID - 1) % len(w.filteredSnippets)) + } else if key.Name == "Return" { + if w.selectedID >= 0 && w.selectedID < len(w.filteredSnippets) { + w.onSubmit(w.filteredSnippets[w.selectedID]) + resetSearch() + } + } else if key.Name == "Escape" { + w.onCancel() + resetSearch() + } + } + w.entry.OnChanged = func(s string) { + ranked := fuzzy.RankFindFold(s, w.snippetsText) + sort.Sort(ranked) + w.filteredSnippets = make([]*snippet, 0) + for _, r := range ranked { + w.filteredSnippets = append(w.filteredSnippets, w.snippets[r.OriginalIndex]) + } + w.list.Refresh() + w.list.Select(0) + } +} + +type searchEntry struct { + widget.Entry + onTypedKey func(key *fyne.KeyEvent) +} + +func newSearchEntry() *searchEntry { + e := &searchEntry{} + e.ExtendBaseWidget(e) + return e +} + +func (e *searchEntry) TypedKey(key *fyne.KeyEvent) { + e.Entry.TypedKey(key) + if e.onTypedKey != nil { + e.onTypedKey(key) + } +} + +func ellipsis(container *fyne.Container, label *canvas.Text) { + w := fyne.MeasureText(string(label.Text), theme.TextSize(), label.TextStyle).Width + if label.Position().X+w > container.Size().Width { + wellipsis := fyne.MeasureText(string("..."), theme.TextSize(), label.TextStyle).Width + wmax := container.Size().Width - wellipsis + wpc := float64(w) / float64(len(label.Text)) + k := 0 + for label.Position().X+w > wmax { + overlap := label.Position().X + w - wmax + overlapc := int(math.Ceil(math.Max(1, float64(overlap)/wpc))) + if overlapc > len(label.Text) { + break + } + label.Text = label.Text[:len(label.Text)-overlapc] + w = fyne.MeasureText(string(label.Text), theme.TextSize(), label.TextStyle).Width + k++ + } + label.Text += "..." + label.Refresh() + } +}