package main import ( "embed" "encoding/json" "flag" "fmt" "log" "sync" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "github.com/sahilm/fuzzy" ) //go:embed data/gitmojis.json data/images var data embed.FS var preferredTheme = flag.String("theme", "", "Specify the theme to be used. dark/light") var linger = flag.String("linger", "", "If specified, linger for the given duration (e.g. 10s) before actually closing.") type Gitmoji struct { Emoji string Code string Description string Name string } type GitmojiData struct { Gitmojis []Gitmoji } type GitmojiEntry struct { Gitmoji Icon fyne.Resource } type keyedEntry struct { widget.Entry onKeyTyped func(keyEvent *fyne.KeyEvent) bool } func (e *keyedEntry) TypedKey(keyEvent *fyne.KeyEvent) { if e.onKeyTyped == nil || !e.onKeyTyped(keyEvent) { e.Entry.TypedKey(keyEvent) } } func newKeyedEntry() *keyedEntry { e := &keyedEntry{} e.ExtendBaseWidget(e) return e } func main() { flag.Parse() gitmojiData, err := loadData() if err != nil { // TODO show dialog panic(err) } a := app.New() switch *preferredTheme { case "dark": a.Settings().SetTheme(theme.DarkTheme()) case "light": a.Settings().SetTheme(theme.LightTheme()) } var lingerDuration *time.Duration = nil if *linger != "" { if d, err := time.ParseDuration(*linger); err != nil { log.Fatalf("invalid linger time: %v\n", err) } else { lingerDuration = &d } } w := a.NewWindow("Gitmoji Picker") w.CenterOnScreen() allEntries := make([]*GitmojiEntry, len(gitmojiData.Gitmojis)) allTerms := make([]string, len(gitmojiData.Gitmojis)) currentId := 0 for i, gitmoji := range gitmojiData.Gitmojis { b, err := data.ReadFile("data/images/" + gitmoji.Name + ".png") if err != nil { panic(err) } allEntries[i] = &GitmojiEntry{ Gitmoji: gitmoji, Icon: fyne.NewStaticResource(gitmoji.Name, b), } allTerms[i] = gitmoji.Name + " " + gitmoji.Description } buildFilteredList := func(filter string) []*GitmojiEntry { r := fuzzy.Find(filter, allTerms) result := make([]*GitmojiEntry, len(r)) for i := range r { result[i] = allEntries[r[i].Index] } return result } var m sync.Mutex currentEntries := allEntries e := newKeyedEntry() l := widget.NewList( func() int { return len(currentEntries) }, func() fyne.CanvasObject { tn := canvas.NewText("Dummy", theme.ForegroundColor()) tn.TextStyle.Bold = true td := canvas.NewText("Dummy", theme.ForegroundColor()) td.TextSize = theme.TextSize() * 8 / 10 return container.NewHBox( widget.NewIcon(allEntries[0].Icon), container.NewVBox(tn, td), ) }, func(id widget.ListItemID, object fyne.CanvasObject) { m.Lock() entry := currentEntries[id] m.Unlock() hb := object.(*fyne.Container) hb.Objects[0].(*widget.Icon).SetResource(entry.Icon) vb := hb.Objects[1].(*fyne.Container) vb.Objects[0].(*canvas.Text).Text = entry.Name vb.Objects[1].(*canvas.Text).Text = entry.Description vb.Objects[0].(*canvas.Text).Refresh() vb.Objects[1].(*canvas.Text).Refresh() }, ) e.OnChanged = func(s string) { m.Lock() if s == "" { currentEntries = allEntries } else { currentEntries = buildFilteredList(s) } if len(currentEntries) > 0 { currentId = 0 } else { currentId = -1 } m.Unlock() l.Select(currentId) l.Refresh() } l.Select(currentId) l.OnSelected = func(id widget.ListItemID) { currentId = id w.Canvas().Focus(e) } w.SetContent(container.NewBorder(e, nil, nil, nil, l)) w.Resize(fyne.NewSize(300, 300)) e.Show() e.onKeyTyped = func(event *fyne.KeyEvent) bool { switch event.Name { case fyne.KeyDown: m.Lock() if currentId < len(currentEntries)-1 { currentId++ } m.Unlock() l.Select(currentId) return true case fyne.KeyUp: m.Lock() if currentId > 0 { currentId-- } m.Unlock() l.Select(currentId) return true case fyne.KeyEscape: w.Close() return true case fyne.KeyReturn, fyne.KeyEnter: m.Lock() defer m.Unlock() if currentId > -1 { w.Clipboard().SetContent(currentEntries[currentId].Emoji) if lingerDuration != nil { w.Hide() time.Sleep(*lingerDuration) } w.Close() return true } } return false } w.Canvas().SetOnTypedKey(func(event *fyne.KeyEvent) { e.onKeyTyped(event) }) w.Canvas().Focus(e) w.ShowAndRun() } func loadData() (*GitmojiData, error) { b, err := data.ReadFile("data/gitmojis.json") if err != nil { return nil, fmt.Errorf("cannot read gitmoji data: %w", err) } var result GitmojiData if err := json.Unmarshal(b, &result); err != nil { return nil, fmt.Errorf("cannot decode gitmoji data: %w", err) } return &result, nil }