From b075b72eeaf2fa7bd9c9dd021c5b6a731660b278 Mon Sep 17 00:00:00 2001 From: Maksim Totmin Date: Wed, 17 Jun 2026 10:48:31 +0700 Subject: [PATCH] fix: keep active page on config save, add Cyrillic preview support, fix grid sizing - daemon.go: save/restore active page on config reload instead of always activating default_page (fixes page switch on save) - render.go: replace basicfont.Face7x13 with parseDisplayFace() for unicode/Cyrillic support in edit key preview - web.go: add SSE endpoint (GET /api/events) for real-time page_changed events and POST /api/activate-page API - app.js: persist active page via localStorage, listen for SSE page_changed events - styles.css, index.html: fix grid sizing (width 100%, minmax(0, 1fr), aspect-ratio on key buttons, max-width 525px) --- daemon.go | 32 ++++++++++---- render.go | 30 ++++++++++--- static/app.js | 35 +++++++++++++++- static/index.html | 6 +-- static/styles.css | 8 +++- web.go | 104 +++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 195 insertions(+), 20 deletions(-) diff --git a/daemon.go b/daemon.go index 7215087..eabf455 100644 --- a/daemon.go +++ b/daemon.go @@ -74,6 +74,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error { web.SetDecks(decks) web.SetPageManager(primaryPM) + web.SetExtraPageManagers(pageMgrs[1:]) for _, pm := range pageMgrs { if err := pm.ActivatePage(cfg.DefaultPage); err != nil { @@ -104,14 +105,19 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error { if a == nil { return } + oldPage := primaryPM.ActivePageName() 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) + newPage := primaryPM.ActivePageName() + if newPage != oldPage { + if a.Type == "page" { + asm.NotifyManualPage(a.Page) } + for _, pm := range pageMgrs[1:] { + pm.ActivatePage(newPage) + } + web.BroadcastPageChange(newPage) } }) @@ -191,6 +197,7 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error { } pm.startPeriodicKeys() } + web.BroadcastPageChange(page) } case <-configTicker.C: @@ -214,7 +221,9 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error { for _, d := range decks { d.SetBrightness(deviceBrightness(cfg, d.Serial())) } + var activePages []string for _, pm := range pageMgrs { + activePages = append(activePages, pm.ActivePageName()) pm.stopPeriodicKeys() pm.defaultFont = cfg.Font pm.LoadPages(cfg.Pages) @@ -225,9 +234,18 @@ func Run(ctx context.Context, cfg *config.Config, opts RunOptions) error { detector = NewWindowDetector() windowCh, _ = detector.Start(ctx) } - for _, pm := range pageMgrs { - if err := pm.ActivatePage(cfg.DefaultPage); err != nil { - slog.Warn("reload: activate default page", "error", err) + for i, pm := range pageMgrs { + page := cfg.DefaultPage + if i < len(activePages) && activePages[i] != "" { + for _, p := range cfg.Pages { + if p.Name == activePages[i] { + page = activePages[i] + break + } + } + } + if err := pm.ActivatePage(page); err != nil { + slog.Warn("reload: activate page", "error", err) } pm.startPeriodicKeys() } diff --git a/render.go b/render.go index 9c69b71..e0ba49a 100644 --- a/render.go +++ b/render.go @@ -42,7 +42,11 @@ func RenderKeyToImage(k *config.KeyConfig, keySize int) image.Image { } } if k.Label != "" { - return composeImageWithLabel(img, k.Label, keySize) + fontSize := 10.0 + if k.FontSize != nil { + fontSize = *k.FontSize + } + return composeImageWithLabel(img, k.Label, keySize, fontSize) } g := gift.New(gift.Resize(keySize, keySize, gift.LanczosResampling)) rgba := image.NewRGBA(image.Rect(0, 0, keySize, keySize)) @@ -62,7 +66,11 @@ func RenderKeyToImage(k *config.KeyConfig, keySize int) image.Image { return blankImage(keySize, bg) } } - return renderTextImage(k.Label, keySize) + fontSize := 12.0 + if k.FontSize != nil { + fontSize = *k.FontSize + } + return renderTextImage(k.Label, keySize, fontSize) } func blankImage(size int, c color.Color) image.Image { @@ -71,7 +79,7 @@ func blankImage(size int, c color.Color) image.Image { return img } -func composeImageWithLabel(src image.Image, text string, keySize int) image.Image { +func composeImageWithLabel(src image.Image, text string, keySize int, fontSize float64) image.Image { g := gift.New(gift.Resize(keySize, keySize, gift.LanczosResampling)) rgba := image.NewRGBA(image.Rect(0, 0, keySize, keySize)) g.Draw(rgba, src) @@ -84,7 +92,12 @@ func composeImageWithLabel(src image.Image, text string, keySize int) image.Imag draw.Draw(rgba, barRect, &image.Uniform{color.RGBA{0, 0, 0, 180}}, image.Point{}, draw.Over) if text != "" { - face := basicfont.Face7x13 + face, err := parseDisplayFace(fontSize) + if err != nil { + face = basicfont.Face7x13 + } else { + defer face.Close() + } textW := font.MeasureString(face, text).Ceil() posX := (keySize - textW) / 2 if posX < 2 { @@ -107,11 +120,16 @@ func composeImageWithLabel(src image.Image, text string, keySize int) image.Imag return rgba } -func renderTextImage(text string, keySize int) image.Image { +func renderTextImage(text string, keySize int, fontSize float64) image.Image { rgba := blankImage(keySize, color.RGBA{0, 0, 0, 255}).(*image.RGBA) if text != "" { - face := basicfont.Face7x13 + face, err := parseDisplayFace(fontSize) + if err != nil { + face = basicfont.Face7x13 + } else { + defer face.Close() + } textW := font.MeasureString(face, text).Ceil() posX := (keySize - textW) / 2 if posX < 2 { diff --git a/static/app.js b/static/app.js index 4b45b99..a3c9cb2 100644 --- a/static/app.js +++ b/static/app.js @@ -51,6 +51,28 @@ document.addEventListener('alpine:init', () => { await this.loadConfig() await this.loadDecks() setInterval(() => this.pollDisplayOutputs(), 3000) + this.connectSSE() + }, + + connectSSE() { + if (this._sse) this._sse.close() + const es = new EventSource('/api/events') + this._sse = es + es.addEventListener('page_changed', (e) => { + try { + const data = JSON.parse(e.data) + if (data.page && this.pages.find(p => p.name === data.page)) { + this.activePage = data.page + localStorage.setItem('sd_active_page', data.page) + this.displayOutputs = {} + } + } catch (_) {} + }) + es.onerror = () => { + es.close() + this._sse = null + setTimeout(() => this.connectSSE(), 3000) + } }, async loadDecks() { @@ -79,7 +101,12 @@ document.addEventListener('alpine:init', () => { this.config = await res.json() this.pages = this.config.pages if (this.pages.length > 0) { - this.activePage = this.config.default_page || this.pages[0].name + const saved = localStorage.getItem('sd_active_page') + if (saved && this.pages.find(p => p.name === saved)) { + this.activePage = saved + } else { + this.activePage = this.config.default_page || this.pages[0].name + } } this.syncSettings() } catch (e) { @@ -460,7 +487,13 @@ document.addEventListener('alpine:init', () => { // ── Page management ── switchPage(name) { this.activePage = name + localStorage.setItem('sd_active_page', name) this.displayOutputs = {} + fetch('/api/activate-page', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ page: name }) + }).catch(() => {}) }, async addPage() { diff --git a/static/index.html b/static/index.html index 94765e7..7f68c6a 100644 --- a/static/index.html +++ b/static/index.html @@ -14,7 +14,7 @@
- @@ -53,7 +53,7 @@ x-text="d.model + ' (' + d.keys_x + '\u00d7' + d.keys_y + ')'"> -
+