♻️ Replace Fyne with Shirei

This commit is contained in:
2026-09-25 22:53:36 +02:00
parent 31af1d8cd0
commit fe89f803d7
3 changed files with 211 additions and 826 deletions
+160 -144
View File
@@ -1,26 +1,29 @@
package main
import (
"bytes"
"embed"
"encoding/json"
"flag"
"fmt"
"image"
"image/draw"
"image/png"
"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"
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.")
@@ -37,25 +40,24 @@ type GitmojiData struct {
type GitmojiEntry struct {
Gitmoji
Icon fyne.Resource
Icon *image.RGBA
}
type keyedEntry struct {
widget.Entry
onKeyTyped func(keyEvent *fyne.KeyEvent) bool
}
var (
allEntries []*GitmojiEntry
allTerms []string
currentEntries []*GitmojiEntry
filter string
appliedFilter string
selectedId int
listKey = new(int)
lingerDuration *time.Duration
shuttingDown 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
}
// 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()
@@ -66,16 +68,16 @@ func main() {
panic(err)
}
a := app.New()
loadEntries(gitmojiData)
currentEntries = allEntries
switch *preferredTheme {
case "dark":
a.Settings().SetTheme(theme.DarkTheme())
SetDarkMode(true)
case "light":
a.Settings().SetTheme(theme.LightTheme())
SetDarkMode(false)
}
var lingerDuration *time.Duration = nil
if *linger != "" {
if d, err := time.ParseDuration(*linger); err != nil {
log.Fatalf("invalid linger time: %v\n", err)
@@ -84,12 +86,14 @@ func main() {
}
}
w := a.NewWindow("Gitmoji Picker")
w.CenterOnScreen()
app.SetupWindow("Gitmoji Picker", 300, 300)
app.SetupIconBytes(iconPNG)
app.Run(RootView)
}
allEntries := make([]*GitmojiEntry, len(gitmojiData.Gitmojis))
allTerms := make([]string, len(gitmojiData.Gitmojis))
currentId := 0
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")
@@ -98,130 +102,142 @@ func main() {
}
allEntries[i] = &GitmojiEntry{
Gitmoji: gitmoji,
Icon: fyne.NewStaticResource(gitmoji.Name, b),
Icon: decodePNG(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
func decodePNG(b []byte) *image.RGBA {
img, err := png.Decode(bytes.NewReader(b))
if err != nil {
panic(err)
}
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()
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
}
l.Select(currentId)
l.OnSelected = func(id widget.ListItemID) {
currentId = id
w.Canvas().Focus(e)
}
func RootView() {
handleKeys()
applyFilterChange()
w.SetContent(container.NewBorder(e, nil, nil, nil, l))
w.Resize(fyne.NewSize(300, 300))
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)
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)
VirtualListView(listKey, len(currentEntries),
func(i int) any { return i },
nil, // auto-height via Measure
rowView,
)
})
}
w.Canvas().Focus(e)
func applyFilterChange() {
if filter == appliedFilter {
return
}
appliedFilter = filter
w.ShowAndRun()
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) {