package main import ( "bytes" "embed" "encoding/json" "flag" "fmt" "image" "image/draw" "image/png" "log" "time" "github.com/sahilm/fuzzy" app "go.hasen.dev/shirei/app" . "go.hasen.dev/shirei" . "go.hasen.dev/shirei/widgets" ) //go:embed data/gitmojis.json data/images var data embed.FS //go:embed Icon.png var iconPNG []byte 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.") var x11 = flag.Bool("x11", false, "Force the X11 backend (Linux, XWayland): uses the window manager's decorations instead of shirei's client-side titlebar") type Gitmoji struct { Emoji string Code string Description string Name string } type GitmojiData struct { Gitmojis []Gitmoji } type GitmojiEntry struct { Gitmoji Icon *image.RGBA } var ( allEntries []*GitmojiEntry allTerms []string currentEntries []*GitmojiEntry filter string appliedFilter string selectedId int listKey = new(int) lingerDuration *time.Duration shuttingDown bool ) // minimum time to keep the process alive after a copy, so the current frame // can finish and the backend can flush the clipboard request const minQuitDelay = 100 * time.Millisecond func main() { flag.Parse() gitmojiData, err := loadData() if err != nil { // TODO show dialog panic(err) } loadEntries(gitmojiData) currentEntries = allEntries switch *preferredTheme { case "dark": SetDarkMode(true) case "light": SetDarkMode(false) } if *linger != "" { if d, err := time.ParseDuration(*linger); err != nil { log.Fatalf("invalid linger time: %v\n", err) } else { lingerDuration = &d } } setupWindow("Gitmoji Picker", 300, 300) runWindow(RootView) } func loadEntries(gitmojiData *GitmojiData) { allEntries = make([]*GitmojiEntry, len(gitmojiData.Gitmojis)) allTerms = make([]string, len(gitmojiData.Gitmojis)) 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: decodePNG(b), } allTerms[i] = gitmoji.Name + " " + gitmoji.Description } } func decodePNG(b []byte) *image.RGBA { img, err := png.Decode(bytes.NewReader(b)) if err != nil { panic(err) } if rgba, ok := img.(*image.RGBA); ok { return rgba } rgba := image.NewRGBA(img.Bounds()) draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src) return rgba } func RootView() { handleKeys() applyFilterChange() scheme := CurrentColorScheme Container(Attrs(Viewport, BackgroundVec(scheme.Surfaces.Canvas.Background)), func() { attrs := DefaultTextInputAttrs() attrs.NoUpDownLineEdges = true // leave Up/Down for list navigation TextInputExt(&filter, attrs) VirtualListView(listKey, len(currentEntries), func(i int) any { return i }, nil, // auto-height via Measure rowView, ) }) } func applyFilterChange() { if filter == appliedFilter { return } appliedFilter = filter if filter == "" { currentEntries = allEntries } else { r := fuzzy.Find(filter, allTerms) currentEntries = make([]*GitmojiEntry, len(r)) for i := range r { currentEntries[i] = allEntries[r[i].Index] } } if len(currentEntries) > 0 { selectedId = 0 VirtualListScrollIntoView(listKey, selectedId) } else { selectedId = -1 } } func handleKeys() { if shuttingDown { return } switch GetFrameInput().Key { case KeyDown: if selectedId < len(currentEntries)-1 { selectEntry(selectedId + 1) } case KeyUp: if selectedId > 0 { selectEntry(selectedId - 1) } case KeyEscape: app.Quit() case KeyEnter: if selectedId > -1 { pickGitmoji() } } } func selectEntry(idx int) { selectedId = idx VirtualListScrollIntoView(listKey, idx) } func rowView(idx int, width float32) { entry := currentEntries[idx] icon := UseImage("gitmoji:"+entry.Name, entry.Icon) scheme := CurrentColorScheme nameColor := scheme.Surfaces.Canvas.Text descColor := nameColor descColor[3] = 0.65 hoverColor := nameColor hoverColor[3] = 0.08 selectedColor := scheme.Buttons.Primary.Normal.Background selectedColor[3] = 0.35 Container(Attrs(Row, CrossMid, Gap(8), Pad2(5, 10), Expand, Clip), func() { if idx == selectedId { ModAttrs(BackgroundVec(selectedColor)) } else if IsHovered() { ModAttrs(BackgroundVec(hoverColor)) } if IsClicked() { selectEntry(idx) } ImageView(icon, Vec2{20, 20}) Container(Attrs(Expand), func() { Label(entry.Name, FontWeight(WeightBold), TextColorVec(nameColor)) Label(entry.Description, FontSize(DefaultTextSize*8/10), TextColorVec(descColor)) }) }) } func pickGitmoji() { if shuttingDown { return } shuttingDown = true RequestTextCopy(currentEntries[selectedId].Emoji) delay := minQuitDelay if lingerDuration != nil { delay = *lingerDuration } go func() { time.Sleep(delay) app.Quit() }() } 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 }