290 lines
5.7 KiB
Go
290 lines
5.7 KiB
Go
package command
|
|
|
|
import (
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
|
|
"aego/core/errs"
|
|
"aego/core/log"
|
|
"aego/platform/input"
|
|
)
|
|
|
|
type Mods uint8
|
|
|
|
const (
|
|
ModPrimary Mods = 1 << iota
|
|
ModShift
|
|
ModAlt
|
|
)
|
|
|
|
type Shortcut struct {
|
|
Key input.Scancode
|
|
Mods Mods
|
|
}
|
|
|
|
func Key(k input.Scancode, m Mods) Shortcut {
|
|
return Shortcut{Key: k, Mods: m}
|
|
}
|
|
|
|
func (s Shortcut) IsZero() bool {
|
|
return s.Key == input.KeyUnknown
|
|
}
|
|
|
|
func (s Shortcut) String() string {
|
|
if s.IsZero() {
|
|
return ""
|
|
}
|
|
var b strings.Builder
|
|
mac := runtime.GOOS == "darwin"
|
|
if s.Mods&ModPrimary != 0 {
|
|
if mac {
|
|
b.WriteString("⌘")
|
|
} else {
|
|
b.WriteString("Ctrl+")
|
|
}
|
|
}
|
|
if s.Mods&ModShift != 0 {
|
|
if mac {
|
|
b.WriteString("⇧")
|
|
} else {
|
|
b.WriteString("Shift+")
|
|
}
|
|
}
|
|
if s.Mods&ModAlt != 0 {
|
|
if mac {
|
|
b.WriteString("⌥")
|
|
} else {
|
|
b.WriteString("Alt+")
|
|
}
|
|
}
|
|
b.WriteString(s.Key.String())
|
|
return b.String()
|
|
}
|
|
|
|
func FromInput(m input.Mods) Mods {
|
|
var out Mods
|
|
if m.Primary() {
|
|
out |= ModPrimary
|
|
}
|
|
if m.Has(input.ModShift) {
|
|
out |= ModShift
|
|
}
|
|
if m.Has(input.ModAlt) {
|
|
out |= ModAlt
|
|
}
|
|
return out
|
|
}
|
|
|
|
func ParseShortcut(s string) (Shortcut, error) {
|
|
parts := strings.Split(s, "+")
|
|
var sc Shortcut
|
|
for i, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if i < len(parts)-1 {
|
|
switch strings.ToLower(p) {
|
|
case "primary", "ctrl", "cmd", "command":
|
|
sc.Mods |= ModPrimary
|
|
case "shift":
|
|
sc.Mods |= ModShift
|
|
case "alt", "option":
|
|
sc.Mods |= ModAlt
|
|
default:
|
|
return Shortcut{}, errs.New(errs.EditorCommand, "unknown modifier", log.F("modifier", p))
|
|
}
|
|
continue
|
|
}
|
|
for k := input.Scancode(1); k < input.ScancodeCount; k++ {
|
|
if strings.EqualFold(k.String(), p) {
|
|
sc.Key = k
|
|
break
|
|
}
|
|
}
|
|
if sc.Key == input.KeyUnknown {
|
|
return Shortcut{}, errs.New(errs.EditorCommand, "unknown key", log.F("key", p))
|
|
}
|
|
}
|
|
return sc, nil
|
|
}
|
|
|
|
func isModifier(k input.Scancode) bool {
|
|
return k >= input.KeyLCtrl && k <= input.KeyRGUI
|
|
}
|
|
|
|
type Desc struct {
|
|
ID string
|
|
Name string
|
|
Category string
|
|
Scope string
|
|
Shortcut Shortcut
|
|
Enabled func() bool
|
|
Run func() error
|
|
}
|
|
|
|
type Registry struct {
|
|
OnError func(id string, err error)
|
|
byID map[string]*Desc
|
|
order []string
|
|
}
|
|
|
|
func New() *Registry {
|
|
return &Registry{byID: map[string]*Desc{}}
|
|
}
|
|
|
|
func (r *Registry) Register(d Desc) error {
|
|
if d.ID == "" || d.Run == nil {
|
|
return errs.New(errs.EditorCommand, "command needs an id and a run function", log.F("command", d.ID))
|
|
}
|
|
if _, dup := r.byID[d.ID]; dup {
|
|
return errs.New(errs.EditorCommand, "duplicate command id", log.F("command", d.ID))
|
|
}
|
|
if err := r.checkConflict(d.ID, d.Shortcut, d.Scope); err != nil {
|
|
return err
|
|
}
|
|
if d.Name == "" {
|
|
d.Name = d.ID
|
|
}
|
|
cp := d
|
|
r.byID[d.ID] = &cp
|
|
r.order = append(r.order, d.ID)
|
|
return nil
|
|
}
|
|
|
|
func (r *Registry) checkConflict(id string, sc Shortcut, scope string) error {
|
|
if sc.IsZero() {
|
|
return nil
|
|
}
|
|
for _, other := range r.byID {
|
|
if other.ID != id && other.Shortcut == sc && other.Scope == scope {
|
|
return errs.New(errs.EditorCommand, "shortcut is already bound",
|
|
log.F("command", id), log.F("other", other.ID), log.F("shortcut", sc.String()))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Registry) Rebind(id string, sc Shortcut) error {
|
|
d, ok := r.byID[id]
|
|
if !ok {
|
|
return errs.New(errs.EditorCommand, "unknown command", log.F("command", id))
|
|
}
|
|
if err := r.checkConflict(id, sc, d.Scope); err != nil {
|
|
return err
|
|
}
|
|
d.Shortcut = sc
|
|
return nil
|
|
}
|
|
|
|
func (r *Registry) Unregister(id string) {
|
|
if _, ok := r.byID[id]; !ok {
|
|
return
|
|
}
|
|
delete(r.byID, id)
|
|
for i, o := range r.order {
|
|
if o == id {
|
|
r.order = append(r.order[:i], r.order[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Registry) Get(id string) (Desc, bool) {
|
|
d, ok := r.byID[id]
|
|
if !ok {
|
|
return Desc{}, false
|
|
}
|
|
return *d, true
|
|
}
|
|
|
|
func (r *Registry) Enabled(id string) bool {
|
|
d, ok := r.byID[id]
|
|
return ok && (d.Enabled == nil || d.Enabled())
|
|
}
|
|
|
|
func (r *Registry) Run(id string) error {
|
|
d, ok := r.byID[id]
|
|
if !ok {
|
|
return errs.New(errs.EditorCommand, "unknown command", log.F("command", id))
|
|
}
|
|
if d.Enabled != nil && !d.Enabled() {
|
|
return errs.New(errs.EditorCommand, "command is disabled", log.F("command", id))
|
|
}
|
|
err := d.Run()
|
|
if err != nil && r.OnError != nil {
|
|
r.OnError(id, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r *Registry) Lookup(sc Shortcut, scope string) (Desc, bool) {
|
|
var global *Desc
|
|
for _, id := range r.order {
|
|
d := r.byID[id]
|
|
if d.Shortcut != sc {
|
|
continue
|
|
}
|
|
if scope != "" && d.Scope == scope {
|
|
return *d, true
|
|
}
|
|
if d.Scope == "" && global == nil {
|
|
global = d
|
|
}
|
|
}
|
|
if global != nil {
|
|
return *global, true
|
|
}
|
|
return Desc{}, false
|
|
}
|
|
|
|
func (r *Registry) Dispatch(in *input.State, scope string) (string, bool) {
|
|
if in == nil {
|
|
return "", false
|
|
}
|
|
mods := FromInput(in.Mods())
|
|
hit := ""
|
|
in.EachDown(func(k input.Scancode) {
|
|
if hit != "" || isModifier(k) || !in.KeyRepeated(k) {
|
|
return
|
|
}
|
|
d, ok := r.Lookup(Shortcut{Key: k, Mods: mods}, scope)
|
|
if !ok || (d.Enabled != nil && !d.Enabled()) {
|
|
return
|
|
}
|
|
hit = d.ID
|
|
})
|
|
if hit == "" {
|
|
return "", false
|
|
}
|
|
r.Run(hit)
|
|
return hit, true
|
|
}
|
|
|
|
func (r *Registry) All() []Desc {
|
|
out := make([]Desc, 0, len(r.order))
|
|
for _, id := range r.order {
|
|
out = append(out, *r.byID[id])
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
if out[i].Category != out[j].Category {
|
|
return out[i].Category < out[j].Category
|
|
}
|
|
return out[i].Name < out[j].Name
|
|
})
|
|
return out
|
|
}
|
|
|
|
func (r *Registry) Search(q string) []Desc {
|
|
q = strings.ToLower(strings.TrimSpace(q))
|
|
all := r.All()
|
|
if q == "" {
|
|
return all
|
|
}
|
|
out := all[:0]
|
|
for _, d := range all {
|
|
if strings.Contains(strings.ToLower(d.Name), q) || strings.Contains(strings.ToLower(d.ID), q) {
|
|
out = append(out, d)
|
|
}
|
|
}
|
|
return out
|
|
}
|