256 lines
5.1 KiB
Go
256 lines
5.1 KiB
Go
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.")
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
app.SetupWindow("Gitmoji Picker", 300, 300)
|
|
app.SetupIconBytes(iconPNG)
|
|
app.Run(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
|
|
}
|