337 lines
8.2 KiB
Go
337 lines
8.2 KiB
Go
//go:build !noautofill
|
|
|
|
package autofill
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"os/user"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gioui.org/app"
|
|
"gioui.org/font/gofont"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/system"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/op/clip"
|
|
"gioui.org/op/paint"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget"
|
|
"gioui.org/widget/material"
|
|
"github.com/shirou/gopsutil/v3/process"
|
|
)
|
|
|
|
type AutofillEntry struct {
|
|
Username string
|
|
Name string
|
|
UUID string
|
|
}
|
|
|
|
var autofillEntries = []AutofillEntry{}
|
|
var onAutofill func(string, chan bool)
|
|
var selectedEntry = 0
|
|
|
|
type scoredAutofillEntry struct {
|
|
autofillEntry AutofillEntry
|
|
score int
|
|
}
|
|
|
|
func getUserProcs() []string {
|
|
procNames := []string{}
|
|
|
|
procs, err := process.Processes()
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
for _, proc := range procs {
|
|
user, err := user.Current()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
procuser, err := proc.Username()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if procuser == user.Username {
|
|
procName, err := proc.Name()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
procNames = append(procNames, strings.ToLower(procName))
|
|
}
|
|
|
|
}
|
|
|
|
return procNames
|
|
}
|
|
|
|
func GetFilteredAutofillEntries(entries []AutofillEntry, filter string) []AutofillEntry {
|
|
if len(filter) == 0 {
|
|
return []AutofillEntry{}
|
|
}
|
|
|
|
filter = strings.ToLower(filter)
|
|
|
|
// filter entrien by whether they contain the filter string
|
|
filteredEntries := []AutofillEntry{}
|
|
for _, entry := range entries {
|
|
name := strings.ToLower(entry.Name)
|
|
if strings.HasPrefix(name, filter) {
|
|
filteredEntries = append(filteredEntries, entry)
|
|
}
|
|
}
|
|
|
|
processes := getUserProcs()
|
|
|
|
scoredEntries := []scoredAutofillEntry{}
|
|
for _, entry := range filteredEntries {
|
|
score := 0
|
|
name := strings.ToLower(entry.Name)
|
|
filter = strings.ToLower(filter)
|
|
|
|
maxProcessScore := 0
|
|
|
|
for _, process := range processes {
|
|
score := 0
|
|
|
|
sharedPrefixLen := 0
|
|
for i := 0; i < len(process) && i < len(entry.Name); i++ {
|
|
if process[i] == name[i] {
|
|
sharedPrefixLen++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
sharedPrefixLenPercent := float32(sharedPrefixLen) / float32(math.Min(float64(len(process)), float64(len(name))))
|
|
|
|
if sharedPrefixLen > 0 {
|
|
score += int(sharedPrefixLenPercent * 7)
|
|
}
|
|
|
|
if score > maxProcessScore {
|
|
maxProcessScore = score
|
|
}
|
|
}
|
|
|
|
score += maxProcessScore
|
|
scoredEntries = append(scoredEntries, scoredAutofillEntry{entry, score})
|
|
}
|
|
|
|
sort.Slice(scoredEntries, func(i, j int) bool {
|
|
return scoredEntries[i].score > scoredEntries[j].score
|
|
})
|
|
|
|
var filteredEntries1 []AutofillEntry
|
|
for _, scoredEntry := range scoredEntries {
|
|
if scoredEntry.score == 0 {
|
|
continue
|
|
}
|
|
filteredEntries1 = append(filteredEntries1, scoredEntry.autofillEntry)
|
|
}
|
|
|
|
return filteredEntries1
|
|
}
|
|
|
|
func RunAutofill(entries []AutofillEntry, onAutofillFunc func(string, chan bool)) {
|
|
autofillEntries = entries
|
|
onAutofill = onAutofillFunc
|
|
|
|
go func() {
|
|
w := app.NewWindow()
|
|
w.Option(app.Size(unit.Dp(600), unit.Dp(800)))
|
|
w.Option(app.Decorated(false))
|
|
w.Perform(system.ActionCenter)
|
|
w.Perform(system.ActionRaise)
|
|
lineEditor.Focus()
|
|
if err := loop(w); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}()
|
|
app.Main()
|
|
}
|
|
|
|
var lineEditor = &widget.Editor{
|
|
SingleLine: true,
|
|
Submit: true,
|
|
}
|
|
|
|
var (
|
|
unselected = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF}
|
|
unselectedText = color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}
|
|
background = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF}
|
|
selected = color.NRGBA{R: 0x65, G: 0x1F, B: 0xFF, A: 0xFF}
|
|
selectedText = color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}
|
|
)
|
|
|
|
var th = material.NewTheme(gofont.Collection())
|
|
var list = layout.List{Axis: layout.Vertical}
|
|
|
|
func doLayout(gtx layout.Context) layout.Dimensions {
|
|
var filteredEntries []AutofillEntry = GetFilteredAutofillEntries(autofillEntries, lineEditor.Text())
|
|
|
|
if selectedEntry >= 10 || selectedEntry >= len(filteredEntries) {
|
|
selectedEntry = 0
|
|
}
|
|
|
|
return Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return Background{Color: background, CornerRadius: unit.Dp(0)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
searchBox := material.Editor(th, lineEditor, "Search query")
|
|
searchBox.Color = selectedText
|
|
border := widget.Border{Color: selectedText, CornerRadius: unit.Dp(8), Width: unit.Dp(2)}
|
|
return border.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.UniformInset(unit.Dp(8)).Layout(gtx, searchBox.Layout)
|
|
})
|
|
})
|
|
})
|
|
}),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
|
|
return list.Layout(gtx, len(filteredEntries), func(gtx layout.Context, i int) layout.Dimensions {
|
|
entry := filteredEntries[i]
|
|
|
|
return layout.Inset{Bottom: unit.Dp(10)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
isSelected := i == selectedEntry
|
|
var color color.NRGBA
|
|
if isSelected {
|
|
color = selected
|
|
} else {
|
|
color = unselected
|
|
}
|
|
|
|
return Background{Color: color, CornerRadius: unit.Dp(8)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
dimens := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
t := material.H6(th, entry.Name)
|
|
if isSelected {
|
|
t.Color = selectedText
|
|
} else {
|
|
t.Color = unselectedText
|
|
}
|
|
return t.Layout(gtx)
|
|
}),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
t := material.Body1(th, entry.Username)
|
|
if isSelected {
|
|
t.Color = selectedText
|
|
} else {
|
|
t.Color = unselectedText
|
|
}
|
|
|
|
return t.Layout(gtx)
|
|
}),
|
|
)
|
|
return dimens
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
}))
|
|
})
|
|
}
|
|
|
|
func loop(w *app.Window) error {
|
|
var ops op.Ops
|
|
for {
|
|
e := <-w.Events()
|
|
switch e := e.(type) {
|
|
case system.DestroyEvent:
|
|
return e.Err
|
|
case system.FrameEvent:
|
|
gtx := layout.NewContext(&ops, e)
|
|
|
|
key.InputOp{
|
|
Keys: key.Set(key.NameReturn + "|" + key.NameEscape + "|" + key.NameDownArrow),
|
|
Tag: 0,
|
|
}.Add(gtx.Ops)
|
|
t := lineEditor.Events()
|
|
for _, ev := range t {
|
|
switch ev.(type) {
|
|
case widget.SubmitEvent:
|
|
entries := GetFilteredAutofillEntries(autofillEntries, lineEditor.Text())
|
|
if len(entries) == 0 {
|
|
fmt.Println("no entries")
|
|
continue
|
|
} else {
|
|
w.Perform(system.ActionMinimize)
|
|
c := make(chan bool)
|
|
go onAutofill(entries[selectedEntry].UUID, c)
|
|
go func() {
|
|
<-c
|
|
os.Exit(0)
|
|
}()
|
|
}
|
|
}
|
|
}
|
|
|
|
test := gtx.Events(0)
|
|
for _, ev := range test {
|
|
switch ev := ev.(type) {
|
|
case key.Event:
|
|
switch ev.Name {
|
|
case key.NameReturn:
|
|
fmt.Println("uncaught submit")
|
|
return nil
|
|
case key.NameDownArrow:
|
|
if ev.State == key.Press {
|
|
selectedEntry++
|
|
if selectedEntry >= 10 {
|
|
selectedEntry = 0
|
|
}
|
|
}
|
|
case key.NameEscape:
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
doLayout(gtx)
|
|
e.Frame(gtx.Ops)
|
|
}
|
|
}
|
|
}
|
|
|
|
type Background struct {
|
|
Color color.NRGBA
|
|
CornerRadius unit.Dp
|
|
}
|
|
|
|
func (b Background) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
|
m := op.Record(gtx.Ops)
|
|
dims := w(gtx)
|
|
size := dims.Size
|
|
call := m.Stop()
|
|
if r := gtx.Dp(b.CornerRadius); r > 0 {
|
|
defer clip.RRect{
|
|
Rect: image.Rect(0, 0, size.X, size.Y),
|
|
NE: r, NW: r, SE: r, SW: r,
|
|
}.Push(gtx.Ops).Pop()
|
|
}
|
|
fill{b.Color}.Layout(gtx, size)
|
|
call.Add(gtx.Ops)
|
|
return dims
|
|
}
|
|
|
|
type fill struct {
|
|
col color.NRGBA
|
|
}
|
|
|
|
func (f fill) Layout(gtx layout.Context, sz image.Point) layout.Dimensions {
|
|
defer clip.Rect(image.Rectangle{Max: sz}).Push(gtx.Ops).Pop()
|
|
paint.ColorOp{Color: f.col}.Add(gtx.Ops)
|
|
paint.PaintOp{}.Add(gtx.Ops)
|
|
return layout.Dimensions{Size: sz}
|
|
}
|