Multi-action keys with tap/long-press/double-tap/hold, gesture timing, display output colors, global font, and display output JSON/color parsing

- Multi-action system: each key supports multiple actions with different triggers
  (tap, long_press, double_tap, hold_start, hold_end)
- GestureEngine: timing-based gesture detection with configurable thresholds
- New config types: KeyAction, TimingConfig, DisplayOutput
- Display output now supports JSON format (text/background/text_color) and
  first-line-as-hex-color format for dynamic key backgrounds
- Display background colors are reflected in the web UI on the grid
- renderUnicodeText/renderUnicodeTextOnImage accept custom text color (fg)
- Global font setting (medium/regular) in Settings UI
- PageManager.defaultFont used as fallback for per-key font
- Alpine.js bundled locally (no CDN dependency)
- Web UI: action tabs (Tap/Long Press/Double Tap/Hold), unified More section
  with appearance + display settings, gesture timing sliders in Settings
- Grid: subtle action type icons in top-left corner matching display indicator style
- Backward compatible: old config 'action' field auto-migrated to 'actions' array
- M+ 1m font files in assets/ for reference
- Updated config.example.json and README with all new features
This commit is contained in:
totmin
2026-06-17 00:09:48 +07:00
parent 35eb27738d
commit f6f3950d28
14 changed files with 1037 additions and 284 deletions
+39 -31
View File
@@ -56,6 +56,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
pageMgrs := make([]*PageManager, len(decks))
for i, d := range decks {
pageMgrs[i] = NewPageManager(d)
pageMgrs[i].defaultFont = cfg.Font
pageMgrs[i].LoadPages(cfg.Pages)
}
primaryPM := pageMgrs[0]
@@ -99,6 +100,21 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
ssCtrl := NewScreensaver(&cfg.Screensaver)
ge := NewGestureEngine(cfg.Timing.LongPressMs, cfg.Timing.DoubleTapMs, func(a *config.Action) {
if a == nil {
return
}
if err := ExecuteAction(a, primaryDeck, primaryPM); err != nil {
slog.Error("execute action", "error", err)
}
if a.Type == "page" {
asm.NotifyManualPage(a.Page)
for _, pm := range pageMgrs[1:] {
pm.ActivatePage(a.Page)
}
}
})
reconnectTicker := time.NewTicker(5 * time.Second)
defer reconnectTicker.Stop()
@@ -146,38 +162,27 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
}
}
for idx, output := range savedOutputs {
primaryPM.ReRenderDisplayKey(idx, output)
}
}
page := primaryPM.ActivePage()
if page == nil {
continue
}
for _, k := range page.Keys {
if k.Index == evt.Index && k.Action != nil {
slog.Debug("key pressed", "index", evt.Index, "action", k.Action.Type)
if k.Action.Type == "page" {
asm.NotifyManualPage(k.Action.Page)
for idx, dout := range savedOutputs {
if dout != nil {
primaryPM.ReRenderDisplayKey(idx, dout.Text)
}
go func(a *config.Action) {
if err := ExecuteAction(a, primaryDeck, primaryPM); err != nil {
slog.Error("execute action", "error", err)
}
if a.Type == "page" {
for _, pm := range pageMgrs[1:] {
pm.ActivatePage(a.Page)
}
}
}(k.Action)
break
}
}
}
page := primaryPM.ActivePage()
if page == nil {
continue
}
for _, k := range page.Keys {
if k.Index == evt.Index {
if len(k.Actions) > 0 {
ge.HandleEvent(evt, k.Actions)
}
break
}
}
case win := <-windowCh:
if page, ok := asm.Evaluate(win, primaryPM.ActivePageName()); ok {
for _, pm := range pageMgrs {
@@ -209,10 +214,12 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error {
for _, d := range decks {
d.SetBrightness(deviceBrightness(cfg, d.Serial()))
}
for _, pm := range pageMgrs {
pm.stopPeriodicKeys()
pm.LoadPages(cfg.Pages)
}
for _, pm := range pageMgrs {
pm.stopPeriodicKeys()
pm.defaultFont = cfg.Font
pm.LoadPages(cfg.Pages)
}
ge.ReloadTiming(cfg.Timing.LongPressMs, cfg.Timing.DoubleTapMs)
asm.Reload(cfg.AutoSwitch)
if len(cfg.AutoSwitch) > 0 && detector == nil {
detector = NewWindowDetector()
@@ -285,6 +292,7 @@ func reconnectDeck(ctx context.Context, cfg *config.Config, pm **PageManager, as
newDeck, err := OpenDeck("")
if err == nil {
*pm = NewPageManager(newDeck)
(*pm).defaultFont = cfg.Font
(*pm).LoadPages(cfg.Pages)
(*pm).ActivatePage(cfg.DefaultPage)
(*pm).startPeriodicKeys()