From 82cf5027566ce6bafe20f5d6550fde31b6ac3f8c Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Sun, 3 May 2026 23:30:12 -0400 Subject: [PATCH] feat: gradient_sample average vs row scanline Default gradient_sample=average preserves full-height column averaging. row mode uses gradient_row 0-100 for horizontal scanline. CLI -gradient-sample/-gradient-row; config.toml keys; TUI sample toggle and row slider when Row mode selected. Co-authored-by: Cursor --- .beads/issues.jsonl | 17 +++-- internal/daemon/daemon.go | 3 + internal/daemon/push.go | 3 +- internal/wallpaper/wallpaper.go | 70 ++++++++++++++++- main.go | 32 +++++++- main_test.go | 60 ++++++++++++++- tui.go | 128 ++++++++++++++++++++++++++------ tui_config.go | 55 +++++++++++--- tui_config_test.go | 12 ++- 9 files changed, 333 insertions(+), 47 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 8805b35..7ea091d 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -10,7 +10,8 @@ {"_type":"issue","id":"omarchy-wled-bgv","title":"Core: send color to WLED via HTTP API","description":"Send RGB color to WLED device using /json/state API endpoint. Accept IP at runtime.","status":"closed","priority":1,"issue_type":"task","assignee":"Eric Dahl","owner":"dahl.eric@gmail.com","created_at":"2026-05-01T03:23:31Z","created_by":"Eric Dahl","updated_at":"2026-05-01T15:38:06Z","started_at":"2026-05-01T03:23:41Z","closed_at":"2026-05-01T03:24:12Z","close_reason":"Closed","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/22","dependencies":[{"issue_id":"omarchy-wled-bgv","depends_on_id":"omarchy-wled-p8g","type":"blocks","created_at":"2026-04-30T23:23:34Z","created_by":"Eric Dahl","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"omarchy-wled-iqx","title":"Core: watch for theme changes and update WLED","description":"Use inotify (watchdog) to detect changes to ~/.config/omarchy/current/theme/colors.toml and push new accent color to WLED automatically.","status":"closed","priority":1,"issue_type":"feature","assignee":"Eric Dahl","owner":"dahl.eric@gmail.com","created_at":"2026-05-01T03:23:30Z","created_by":"Eric Dahl","updated_at":"2026-05-01T15:38:06Z","started_at":"2026-05-01T03:24:13Z","closed_at":"2026-05-01T03:24:17Z","close_reason":"Closed","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/23","dependencies":[{"issue_id":"omarchy-wled-iqx","depends_on_id":"omarchy-wled-bgv","type":"blocks","created_at":"2026-04-30T23:23:35Z","created_by":"Eric Dahl","metadata":"{}"},{"issue_id":"omarchy-wled-iqx","depends_on_id":"omarchy-wled-p8g","type":"blocks","created_at":"2026-04-30T23:23:35Z","created_by":"Eric Dahl","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"omarchy-wled-p8g","title":"Core: read accent color from Omarchy theme","description":"Parse accent color from ~/.config/omarchy/current/theme/colors.toml and return as RGB tuple. This is the foundation all other features depend on.","status":"closed","priority":1,"issue_type":"task","assignee":"Eric Dahl","owner":"dahl.eric@gmail.com","created_at":"2026-05-01T03:23:30Z","created_by":"Eric Dahl","updated_at":"2026-05-01T15:38:07Z","started_at":"2026-05-01T03:23:40Z","closed_at":"2026-05-01T03:24:12Z","close_reason":"Closed","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/24","dependency_count":0,"dependent_count":2,"comment_count":0} -{"_type":"issue","id":"omarchy-wled-jip","title":"tui: fix misleading RGB variable names in View swatch","description":"In tui.go non-gradient preview branch, the third channel is named `c` but holds blue (B). Rename to `r, g, b` (or `b`) so readers do not confuse with generic color. No behavior change.","status":"closed","priority":2,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:26Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:35:26Z","closed_at":"2026-05-04T02:35:26Z","close_reason":"Merged via PRs #46–#49 (readability pass).","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"omarchy-wled-2yf","title":"Gradient row selection (issue #60)","description":"CLI/TUI/config gradient_row 0-100; wallpaper strip from horizontal row","status":"closed","priority":2,"issue_type":"feature","assignee":"Eric Dahl","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T03:05:42Z","created_by":"Eric Dahl","updated_at":"2026-05-04T03:08:17Z","started_at":"2026-05-04T03:05:58Z","closed_at":"2026-05-04T03:08:17Z","close_reason":"Implemented gradient_row / -gradient-row (0–100), TUI slider, single-scanline strip (replaces full-height column average).","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"omarchy-wled-jip","title":"tui: fix misleading RGB variable names in View swatch","description":"In tui.go non-gradient preview branch, the third channel is named `c` but holds blue (B). Rename to `r, g, b` (or `b`) so readers do not confuse with generic color. No behavior change.","status":"closed","priority":2,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:26Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:45:02Z","closed_at":"2026-05-04T02:35:26Z","close_reason":"Merged via PRs #46–#49 (readability pass).","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/51","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"omarchy-wled-5a1","title":"Revisit: Python TUI vs Go core duplication","description":"ADR-0005 keeps Go as core; Python TUI may duplicate colour logic. Options: document intentional drift boundary or have TUI delegate reads to Go binary.","notes":"Direction (2026-05): long-term plan is to port the TUI to a Go-compatible stack so colour/WLED logic stays in one language; short-term drift vs delegate-to-binary is lower priority until that port is scheduled.","status":"closed","priority":2,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-03T04:01:32Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:24:20Z","closed_at":"2026-05-03T04:37:38Z","close_reason":"Go TUI shipped as omarchy-wled tui; Python deprecation is follow-up (R1 docs, R2 remove)","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/39","labels":["architecture"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"omarchy-wled-3fq","title":"Deepen: Sentinel/poll fallback adapter","description":"Align with ADR-0003: explicit adapter for poll path + sentinel callable so new ColorSources cannot silently omit poll support.","status":"closed","priority":2,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-03T04:01:31Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:40:48Z","closed_at":"2026-05-04T02:40:48Z","close_reason":"PollFallback + PreparePushColors/DeliverPreparedColors (ADR-0007).","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/40","labels":["architecture"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"omarchy-wled-u66","title":"Revisit: split main.go into packages","description":"Conflicts CONTEXT.md single-file layout and ADR-0001 spirit for small tools. Proceed only if navigation friction outweighs ADR-0005 simplicity; update CONTEXT + ADR if split happens.","status":"closed","priority":2,"issue_type":"decision","owner":"dahl.eric@gmail.com","created_at":"2026-05-03T04:01:31Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:24:21Z","closed_at":"2026-05-03T05:01:21Z","close_reason":"Go core split into internal/paths, color, config, wallpaper, wled, source, daemon; ADR-0006 + CONTEXT/AGENTS updated. main.go is CLI wiring only.","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/41","labels":["architecture"],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -32,13 +33,13 @@ {"_type":"issue","id":"omarchy-wled-1777649880013-3-2a984396","title":"refactor: remove shallow read_accent_color / read_foreground_color wrappers","description":"## Problem\n\n`read_accent_color` and `read_foreground_color` (L76–83) are thin wrappers over `_read_color_key`. They're only referenced in tests and imply a stable public API that doesn't exist. Deletion test passes — removing them concentrates no complexity.\n\n## Fix\n\nRemove both functions. Tests should use `ThemeColorSource` or `_read_color_key` directly.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-01T15:03:51Z","updated_at":"2026-05-01T15:26:38Z","closed_at":"2026-05-01T15:26:39Z","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/6","source_system":"github:https://github.com/ericdahl-dev/omarchy-wled/issues/6:6","metadata":null,"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"omarchy-wled-1777649879832-2-e2c09327","title":"bug: watch() ignores source.watch_path() — BgColorSource never triggers","description":"## Problem\n\n`watch()` hardcodes `THEME_NAME_FILE.parent` as the watched directory regardless of the active source. `BgColorSource` (and any future source) never fires because the observer watches the wrong directory.\n\n```python\n# L224 — always watches theme dir\nobserver.schedule(Handler(), str(THEME_NAME_FILE.parent), recursive=False)\n# should be:\nobserver.schedule(Handler(), str(source.watch_path().parent), recursive=False)\n```\n\nThe `watch_path()` seam exists on the `ColorSource` protocol precisely for this — it's just unused.\n\n## Fix\n\nReplace the hardcoded path with `source.watch_path().parent`.","status":"closed","priority":2,"issue_type":"bug","assignee":"Copilot","created_at":"2026-05-01T15:03:46Z","updated_at":"2026-05-01T15:20:53Z","closed_at":"2026-05-01T15:20:54Z","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/5","source_system":"github:https://github.com/ericdahl-dev/omarchy-wled/issues/5:5","metadata":null,"labels":["bug"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"omarchy-wled-1777649879686-1-13dd76b4","title":"AUR release","description":"Can we get AUR releases happening from GitHub via tags? Research how, implement and make a PR","status":"closed","priority":2,"issue_type":"task","assignee":"Copilot","created_at":"2026-05-01T11:32:17Z","updated_at":"2026-05-01T14:22:02Z","closed_at":"2026-05-01T14:22:03Z","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/2","source_system":"github:https://github.com/ericdahl-dev/omarchy-wled/issues/2:2","metadata":null,"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"omarchy-wled-6i0","title":"source: clarify Source interface methods for polling vs fsnotify","description":"Expand package or interface doc comments for Sentinel() (poll fingerprint string) and IsTrigger(path) (whether this path should trigger a color refresh). Optional follow-up: consider renaming to ChangeFingerprint / PathTriggersRefresh if willing to touch the interface.","status":"closed","priority":3,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:29Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:35:27Z","closed_at":"2026-05-04T02:35:27Z","close_reason":"Merged via PRs #46–#49 (readability pass).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"omarchy-wled-507","title":"tui: document or rename preview debounce fields (previewGen/previewArm)","description":"tuiModel uses previewGen and previewArm for debounced preview scheduling. Add a short comment block above the fields explaining the generation/token pattern, or rename to clearer names (e.g. previewRequestSeq / previewAcceptedSeq).","status":"closed","priority":3,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:28Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:35:26Z","closed_at":"2026-05-04T02:35:26Z","close_reason":"Merged via PRs #46–#49 (readability pass).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"omarchy-wled-t7t","title":"tui: replace magic index 2 for wallpaper source with named constant","description":"tui.go uses `sourceIx == 2` throughout for wallpaper/Wallpaper avg. Introduce a named constant (e.g. sourceIndexWallpaper) next to sourceMaxIdx/sourceLabels so intent is obvious without counting UI rows.","status":"closed","priority":3,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:28Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:35:26Z","closed_at":"2026-05-04T02:35:26Z","close_reason":"Merged via PRs #46–#49 (readability pass).","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"omarchy-wled-6i0","title":"source: clarify Source interface methods for polling vs fsnotify","description":"Expand package or interface doc comments for Sentinel() (poll fingerprint string) and IsTrigger(path) (whether this path should trigger a color refresh). Optional follow-up: consider renaming to ChangeFingerprint / PathTriggersRefresh if willing to touch the interface.","status":"closed","priority":3,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:29Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:45:11Z","closed_at":"2026-05-04T02:35:27Z","close_reason":"Merged via PRs #46–#49 (readability pass).","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/52","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"omarchy-wled-507","title":"tui: document or rename preview debounce fields (previewGen/previewArm)","description":"tuiModel uses previewGen and previewArm for debounced preview scheduling. Add a short comment block above the fields explaining the generation/token pattern, or rename to clearer names (e.g. previewRequestSeq / previewAcceptedSeq).","status":"closed","priority":3,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:28Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:45:11Z","closed_at":"2026-05-04T02:35:26Z","close_reason":"Merged via PRs #46–#49 (readability pass).","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/53","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"omarchy-wled-t7t","title":"tui: replace magic index 2 for wallpaper source with named constant","description":"tui.go uses `sourceIx == 2` throughout for wallpaper/Wallpaper avg. Introduce a named constant (e.g. sourceIndexWallpaper) next to sourceMaxIdx/sourceLabels so intent is obvious without counting UI rows.","status":"closed","priority":3,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:28Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:45:12Z","closed_at":"2026-05-04T02:35:26Z","close_reason":"Merged via PRs #46–#49 (readability pass).","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/54","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"omarchy-wled-8pr","title":"refactor: extract Handler class from watch() closure for testability","description":"Handler is embedded inside watch() as a closure. Cannot be instantiated in tests. _maybe_push behavior (is_trigger, sleep, push_if_changed) is untested. Extract as top-level class taking source/state/ip/brightness/saturation.","status":"closed","priority":3,"issue_type":"task","assignee":"Eric Dahl","owner":"dahl.eric@gmail.com","created_at":"2026-05-02T20:15:30Z","created_by":"Eric Dahl","updated_at":"2026-05-02T21:25:25Z","started_at":"2026-05-02T20:18:06Z","closed_at":"2026-05-02T20:22:56Z","close_reason":"Closed","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/32","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"omarchy-wled-l8w","title":"refactor: debounce WLED HTTP sends on brightness/saturation keystrokes","description":"Every keystroke in brightness/saturation fields fires a blocking HTTP POST to WLED via _refresh_color_preview. No debounce means typing '128' sends 3 requests. Add ~300ms debounce.","status":"closed","priority":3,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-02T20:15:29Z","created_by":"Eric Dahl","updated_at":"2026-05-02T21:26:24Z","started_at":"2026-05-02T20:18:06Z","closed_at":"2026-05-02T21:26:24Z","close_reason":"Closed","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/33","metadata":null,"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"omarchy-wled-2fk","title":"docs: extend CONTEXT.md with Omarchy paths and WLED/sentinel glossary","description":"Per readability review: add short glossary entries to CONTEXT.md for Omarchy paths (colors.toml, theme.name, background symlink), WLED concepts (seg.i spatial gradient, bri), and watcher vs Sentinel polling so code can stay terse without losing new readers. Only if CONTEXT.md already exists and project wants narrative docs.","status":"closed","priority":4,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:37Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:35:27Z","closed_at":"2026-05-04T02:35:27Z","close_reason":"Merged via PRs #46–#49 (readability pass).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"omarchy-wled-4np","title":"color: unpack regex submatches in NamedHexFromColorsToml","description":"In internal/color/color.go loop over FindAllStringSubmatch, replace opaque m[1]/m[2] with named locals (e.g. key, hexDigits) for scanability. No logic change.","status":"closed","priority":4,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:32Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:35:28Z","closed_at":"2026-05-04T02:35:28Z","close_reason":"Merged via PRs #46–#49 (readability pass).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"omarchy-wled-zhj","title":"daemon/wled: internal naming polish for readability","description":"Small renames/comments: daemon PushCurrentColorIfChanged printf vars `a, z` -\u003e strip endpoints (e.g. firstRGB/lastRGB); gradientSent struct -\u003e clearer last-gradient cache name; wled jsonGETURL / seg.i `iArr` add one-line comment that payload follows WLED seg.i (offset + hex per LED). Keep behavior identical.","status":"closed","priority":4,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:31Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:35:28Z","closed_at":"2026-05-04T02:35:28Z","close_reason":"Merged via PRs #46–#49 (readability pass).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"omarchy-wled-94c","title":"tests: align main_test.go section headers with current API names","description":"main_test.go section comments still reference legacy labels (readColorKey, applySaturation, sendColorToWLED, makeSource). Update headers to match actual symbols (NamedHexFromColorsToml, ScaleSaturation, PostSolidJSON, FromFlag, etc.) for grep/nav consistency.","status":"closed","priority":4,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:30Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:35:28Z","closed_at":"2026-05-04T02:35:28Z","close_reason":"Merged via PRs #46–#49 (readability pass).","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"omarchy-wled-2fk","title":"docs: extend CONTEXT.md with Omarchy paths and WLED/sentinel glossary","description":"Per readability review: add short glossary entries to CONTEXT.md for Omarchy paths (colors.toml, theme.name, background symlink), WLED concepts (seg.i spatial gradient, bri), and watcher vs Sentinel polling so code can stay terse without losing new readers. Only if CONTEXT.md already exists and project wants narrative docs.","status":"closed","priority":4,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:37Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:45:12Z","closed_at":"2026-05-04T02:35:27Z","close_reason":"Merged via PRs #46–#49 (readability pass).","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/55","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"omarchy-wled-4np","title":"color: unpack regex submatches in NamedHexFromColorsToml","description":"In internal/color/color.go loop over FindAllStringSubmatch, replace opaque m[1]/m[2] with named locals (e.g. key, hexDigits) for scanability. No logic change.","status":"closed","priority":4,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:32Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:45:13Z","closed_at":"2026-05-04T02:35:28Z","close_reason":"Merged via PRs #46–#49 (readability pass).","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/56","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"omarchy-wled-zhj","title":"daemon/wled: internal naming polish for readability","description":"Small renames/comments: daemon PushCurrentColorIfChanged printf vars `a, z` -\u003e strip endpoints (e.g. firstRGB/lastRGB); gradientSent struct -\u003e clearer last-gradient cache name; wled jsonGETURL / seg.i `iArr` add one-line comment that payload follows WLED seg.i (offset + hex per LED). Keep behavior identical.","status":"open","priority":4,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:31Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:45:18Z","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/57","metadata":null,"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"omarchy-wled-94c","title":"tests: align main_test.go section headers with current API names","description":"main_test.go section comments still reference legacy labels (readColorKey, applySaturation, sendColorToWLED, makeSource). Update headers to match actual symbols (NamedHexFromColorsToml, ScaleSaturation, PostSolidJSON, FromFlag, etc.) for grep/nav consistency.","status":"closed","priority":4,"issue_type":"task","owner":"dahl.eric@gmail.com","created_at":"2026-05-04T02:28:30Z","created_by":"Eric Dahl","updated_at":"2026-05-04T02:45:14Z","closed_at":"2026-05-04T02:35:28Z","close_reason":"Merged via PRs #46–#49 (readability pass).","external_ref":"https://github.com/ericdahl-dev/omarchy-wled/issues/58","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"memory","key":"always-write-code-that-is-self-documenting-a","value":"always write code that is self documenting. a human should be able to understand the code by reading it."} diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index a7be3ae..b596aa0 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -7,6 +7,7 @@ import ( "time" "github.com/ericdahl-dev/omarchy-wled/internal/source" + "github.com/ericdahl-dev/omarchy-wled/internal/wallpaper" "github.com/fsnotify/fsnotify" ) @@ -14,6 +15,8 @@ import ( type PushOptions struct { WallpaperGradientStrip bool GradientLEDCountOrZero int // 0 → query WLED /json/info + GradientSample wallpaper.GradientSampleKind + GradientRowPercent int // 0–100 when GradientSampleRow } type gradientStripSnapshot struct { diff --git a/internal/daemon/push.go b/internal/daemon/push.go index 004727e..e81d4af 100644 --- a/internal/daemon/push.go +++ b/internal/daemon/push.go @@ -38,7 +38,8 @@ func PreparePushColors(src source.Source, wledIP string, saturation float64, opt if err != nil { return PreparedColors{}, false, err } - stripRGB, err := wallpaper.ColumnStripForLEDCount(wallpaper.CurrentSymlink(), n) + stripRGB, err := wallpaper.ColumnStripForLEDCount( + wallpaper.CurrentSymlink(), n, opts.GradientSample, opts.GradientRowPercent) if err != nil { if strictStripDecode { return PreparedColors{}, false, err diff --git a/internal/wallpaper/wallpaper.go b/internal/wallpaper/wallpaper.go index e7f9cb8..4ad292f 100644 --- a/internal/wallpaper/wallpaper.go +++ b/internal/wallpaper/wallpaper.go @@ -11,6 +11,7 @@ import ( "math" "os" "path/filepath" + "strings" "github.com/ericdahl-dev/omarchy-wled/internal/paths" xdraw "golang.org/x/image/draw" @@ -131,6 +132,26 @@ func LeftRightHalvesLinearAvg(wallpaperSymlinkPath string) (left, right [3]uint8 return left, right, err } +// GradientSampleKind selects how each column is reduced before LED resampling. +type GradientSampleKind byte + +const ( + // GradientSampleAverage averages each column vertically over the full image (legacy behavior). + GradientSampleAverage GradientSampleKind = iota + // GradientSampleRow uses a single horizontal scanline (see RowYFromPercent). + GradientSampleRow +) + +// GradientSampleKindFromString parses config/CLI values; unknown → average. +func GradientSampleKindFromString(s string) GradientSampleKind { + switch strings.TrimSpace(strings.ToLower(s)) { + case "row": + return GradientSampleRow + default: + return GradientSampleAverage + } +} + func columnAverageRow(rgbaFull *image.RGBA) *image.RGBA { b := rgbaFull.Bounds() w, h := b.Dx(), b.Dy() @@ -157,8 +178,43 @@ func columnAverageRow(rgbaFull *image.RGBA) *image.RGBA { return out } +// RowYFromPercent maps rowPercent in [0,100] to a row index in [0, height-1] (0=top, 100=bottom). +func RowYFromPercent(height, rowPercent int) int { + if height <= 1 { + return 0 + } + if rowPercent < 0 { + rowPercent = 0 + } + if rowPercent > 100 { + rowPercent = 100 + } + return int(math.Round(float64(rowPercent) / 100.0 * float64(height-1))) +} + +func horizontalScanlineRGBA(rgbaFull *image.RGBA, rowIndex int) *image.RGBA { + b := rgbaFull.Bounds() + w, h := b.Dx(), b.Dy() + out := image.NewRGBA(image.Rect(0, 0, w, 1)) + if w <= 0 || h <= 0 { + return out + } + if rowIndex < 0 { + rowIndex = 0 + } + if rowIndex >= h { + rowIndex = h - 1 + } + y := b.Min.Y + rowIndex + for x := 0; x < w; x++ { + out.Set(x, 0, rgbaFull.RGBAAt(b.Min.X+x, y)) + } + return out +} + // ColumnStripForLEDCount builds one sample per wallpaper column, then resamples to ledCount LEDs. -func ColumnStripForLEDCount(wallpaperSymlinkPath string, ledCount int) ([][3]uint8, error) { +// kind selects full-height column average vs a single row; rowPercent is used only for GradientSampleRow. +func ColumnStripForLEDCount(wallpaperSymlinkPath string, ledCount int, kind GradientSampleKind, rowPercent int) ([][3]uint8, error) { if ledCount <= 0 { return nil, fmt.Errorf("ledCount must be positive") } @@ -181,7 +237,17 @@ func ColumnStripForLEDCount(wallpaperSymlinkPath string, ledCount int) ([][3]uin } rgbaFull := image.NewRGBA(bounds) draw.Draw(rgbaFull, bounds, decoded, bounds.Min, draw.Src) - rowRgba := columnAverageRow(rgbaFull) + + var rowRgba *image.RGBA + switch kind { + case GradientSampleAverage: + rowRgba = columnAverageRow(rgbaFull) + case GradientSampleRow: + y := RowYFromPercent(bounds.Dy(), rowPercent) + rowRgba = horizontalScanlineRGBA(rgbaFull, y) + default: + return nil, fmt.Errorf("unsupported gradient sample kind") + } pix := rowRgba.Pix for i := 0; i < len(pix); i += 4 { diff --git a/main.go b/main.go index 9250569..dcc112b 100644 --- a/main.go +++ b/main.go @@ -8,11 +8,13 @@ import ( "io" "os" "strconv" + "strings" "github.com/ericdahl-dev/omarchy-wled/internal/config" "github.com/ericdahl-dev/omarchy-wled/internal/daemon" "github.com/ericdahl-dev/omarchy-wled/internal/paths" "github.com/ericdahl-dev/omarchy-wled/internal/source" + "github.com/ericdahl-dev/omarchy-wled/internal/wallpaper" ) // version is set at link time: go build -ldflags "-X main.version=v1.2.3" @@ -29,6 +31,8 @@ type cliOpts struct { wledIP string bgGradient bool gradientLEDs int + gradientSample string // average | row + gradientRow int // 0–100 when row mode } // parseArgs parses argv using the same defaults as loadConfig merge rules. @@ -59,6 +63,16 @@ func parseArgs(args []string, cfg map[string]string, output io.Writer) (*cliOpts defaultGradientLEDs = n } } + defaultGradientSample := cfg["gradient_sample"] + if defaultGradientSample == "" { + defaultGradientSample = "average" + } + defaultGradientRow := 50 + if v, ok := cfg["gradient_row"]; ok { + if n, err := strconv.Atoi(v); err == nil { + defaultGradientRow = n + } + } fs := flag.NewFlagSet("omarchy-wled", flag.ContinueOnError) fs.SetOutput(output) @@ -75,9 +89,13 @@ func parseArgs(args []string, cfg map[string]string, output io.Writer) (*cliOpts "Saturation multiplier (0.0=greyscale, 1.0=unchanged, >1.0=boost;\n"+ "default 1.2 for accent, 1.0 for fg/bg)") gradient := fs.Bool("gradient", defaultGradient, - "Wallpaper left/right averages as strip endpoints (spatial fade via seg.i; requires -source bg)") + "Wallpaper per-column strip (spatial fade via seg.i; requires -source bg; see -gradient-sample)") gradientLEDs := fs.Int("gradient-leds", defaultGradientLEDs, "LED count for spatial fade (0 = fetch from WLED /json/info)") + gradientSample := fs.String("gradient-sample", defaultGradientSample, + "Wallpaper gradient sampling: average (full-height column avg) or row (single scanline)") + gradientRow := fs.Int("gradient-row", defaultGradientRow, + "With gradient-sample=row: vertical position 0–100 (0=top, 100=bottom)") once := fs.Bool("once", false, "Send current color once and exit (no watching)") fs.Usage = func() { fmt.Fprintf(output, "Usage: omarchy-wled [options] [WLED_IP]\n") @@ -96,6 +114,8 @@ func parseArgs(args []string, cfg map[string]string, output io.Writer) (*cliOpts opts.once = *once opts.bgGradient = *gradient opts.gradientLEDs = *gradientLEDs + opts.gradientSample = *gradientSample + opts.gradientRow = *gradientRow opts.wledIP = cfg["ip"] if fs.NArg() > 0 { @@ -108,6 +128,14 @@ func validateCli(opts *cliOpts) error { if opts.bgGradient && opts.sourceName != "bg" { return fmt.Errorf("-gradient requires -source bg") } + switch strings.TrimSpace(strings.ToLower(opts.gradientSample)) { + case "", "average", "row": + default: + return fmt.Errorf("-gradient-sample must be average or row — got %q", opts.gradientSample) + } + if opts.gradientRow < 0 || opts.gradientRow > 100 { + return fmt.Errorf("-gradient-row must be 0–100 — got %d", opts.gradientRow) + } return nil } @@ -161,6 +189,8 @@ func main() { pushOpts := daemon.PushOptions{ WallpaperGradientStrip: opts.bgGradient, GradientLEDCountOrZero: opts.gradientLEDs, + GradientSample: wallpaper.GradientSampleKindFromString(opts.gradientSample), + GradientRowPercent: opts.gradientRow, } if opts.once { diff --git a/main_test.go b/main_test.go index 7065530..9ea84d9 100644 --- a/main_test.go +++ b/main_test.go @@ -460,7 +460,7 @@ func TestWallpaperColumnAverageMapsToLEDs(t *testing.T) { if err := os.Symlink(imgPath, link); err != nil { t.Fatal(err) } - colors, err := wallpaper.ColumnStripForLEDCount(link, 3) + colors, err := wallpaper.ColumnStripForLEDCount(link, 3, wallpaper.GradientSampleAverage, 0) if err != nil { t.Fatal(err) } @@ -475,6 +475,43 @@ func TestWallpaperColumnAverageMapsToLEDs(t *testing.T) { } } +func TestWallpaperGradientScanlineDiffersByRow(t *testing.T) { + dir := t.TempDir() + img := image.NewRGBA(image.Rect(0, 0, 3, 2)) + for x := 0; x < 3; x++ { + img.Set(x, 0, imgcolor.RGBA{R: uint8(50 + x*80), A: 255}) + img.Set(x, 1, imgcolor.RGBA{B: uint8(50 + x*80), A: 255}) + } + imgPath := filepath.Join(dir, "rows.png") + f, err := os.Create(imgPath) + if err != nil { + t.Fatal(err) + } + if err := png.Encode(f, img); err != nil { + f.Close() + t.Fatal(err) + } + f.Close() + link := filepath.Join(dir, "background") + if err := os.Symlink(imgPath, link); err != nil { + t.Fatal(err) + } + top, err := wallpaper.ColumnStripForLEDCount(link, 3, wallpaper.GradientSampleRow, 0) + if err != nil { + t.Fatal(err) + } + bot, err := wallpaper.ColumnStripForLEDCount(link, 3, wallpaper.GradientSampleRow, 100) + if err != nil { + t.Fatal(err) + } + if top[0][0] <= top[0][2] { + t.Errorf("top scanline strip should be red-heavy at left: %v", top[0]) + } + if bot[0][2] <= bot[0][0] { + t.Errorf("bottom scanline strip should be blue-heavy at left: %v", bot[0]) + } +} + func TestReadBgGradientHorizontalHalves(t *testing.T) { dir := t.TempDir() img := image.NewRGBA(image.Rect(0, 0, 4, 2)) @@ -656,8 +693,10 @@ func TestValidateCliGradientRequiresBg(t *testing.T) { func TestParseArgsGradientFromConfig(t *testing.T) { opts, err := parseArgs([]string{}, map[string]string{ - "gradient": "true", - "gradient_leds": "120", + "gradient": "true", + "gradient_leds": "120", + "gradient_sample": "row", + "gradient_row": "33", }, io.Discard) if err != nil { t.Fatal(err) @@ -668,4 +707,19 @@ func TestParseArgsGradientFromConfig(t *testing.T) { if opts.gradientLEDs != 120 { t.Fatalf("gradientLEDs: got %d want 120", opts.gradientLEDs) } + if opts.gradientSample != "row" { + t.Fatalf("gradientSample: got %q want row", opts.gradientSample) + } + if opts.gradientRow != 33 { + t.Fatalf("gradientRow: got %d want 33", opts.gradientRow) + } +} + +func TestValidateCliGradientSampleAndRow(t *testing.T) { + if err := validateCli(&cliOpts{gradientSample: "bogus"}); err == nil { + t.Fatal("expected error for bad gradient-sample") + } + if err := validateCli(&cliOpts{gradientSample: "average", gradientRow: 101}); err == nil { + t.Fatal("expected error for gradient-row > 100") + } } diff --git a/tui.go b/tui.go index d3ccffa..edac7bf 100644 --- a/tui.go +++ b/tui.go @@ -53,6 +53,8 @@ const ( focusIP tuiFocus = iota focusSource focusGradient // wallpaper spatial strip (only when source = Wallpaper) + focusGradientSample + focusGradientRow focusBrightness focusSaturation focusService @@ -105,9 +107,11 @@ type tuiModel struct { brightPct int // 0–100 step 2 satPct int // 0–200 step 2 → saturation = satPct/100 - cfg tuiConfig - serviceOn bool - gradientOn bool // wallpaper-only; persisted as cfg.Gradient when source is bg + cfg tuiConfig + serviceOn bool + gradientOn bool // wallpaper-only; persisted as cfg.Gradient when source is bg + gradientSampleIsRow bool // false = average (full-height column), true = single scanline + gradientRowPct int // 0–100 when gradientSampleIsRow previewTracker *daemon.DedupeTracker previewRGB [3]uint8 @@ -144,17 +148,27 @@ func newTuiModel(initial *tuiConfig) *tuiModel { mt.Focus() grad := initial.Gradient && sourceIdxFromName(initial.Source) == sourceIndexWallpaper + gs := normalizeGradientSample(initial.GradientSample) + gr := initial.GradientRow + if gr < 0 { + gr = 0 + } + if gr > 100 { + gr = 100 + } m := &tuiModel{ - screen: tuiScreenMain, - mainTI: mt, - focus: focusIP, - sourceIx: sourceIdxFromName(initial.Source), - brightPct: brightness255ToPct(initial.Brightness), - satPct: int(initial.Saturation * 100), - cfg: *initial, - gradientOn: grad, - previewTracker: &daemon.DedupeTracker{}, - width: 80, + screen: tuiScreenMain, + mainTI: mt, + focus: focusIP, + sourceIx: sourceIdxFromName(initial.Source), + brightPct: brightness255ToPct(initial.Brightness), + satPct: int(initial.Saturation * 100), + cfg: *initial, + gradientOn: grad, + gradientSampleIsRow: gs == "row", + gradientRowPct: gr, + previewTracker: &daemon.DedupeTracker{}, + width: 80, } if m.satPct > 200 { m.satPct = 200 @@ -247,7 +261,7 @@ func (m *tuiModel) updateSetupKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.errSetup = "" m.cfg = tuiConfig{ IP: ip, Source: tuiDefaultSource, Brightness: tuiDefaultBrightness, Saturation: tuiDefaultSaturation, - Gradient: false, GradientLEDs: 0, + Gradient: false, GradientLEDs: 0, GradientSample: "average", GradientRow: 50, } if err := saveTuiConfig(paths.ConfigPath, &m.cfg); err != nil { m.errSetup = err.Error() @@ -262,6 +276,8 @@ func (m *tuiModel) updateSetupKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.focus = focusIP m.sourceIx = 0 m.gradientOn = false + m.gradientSampleIsRow = false + m.gradientRowPct = 50 m.brightPct = 100 m.satPct = int(tuiDefaultSaturation * 100) m.previewTracker = &daemon.DedupeTracker{} @@ -315,6 +331,9 @@ func (m *tuiModel) updateMainKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } if m.sourceIx != sourceIndexWallpaper { m.gradientOn = false + if m.focus == focusGradient || m.focus == focusGradientSample || m.focus == focusGradientRow { + m.focus = focusSource + } } m.resetPreviewTracker() m.ensureValidFocus() @@ -326,6 +345,9 @@ func (m *tuiModel) updateMainKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } if m.sourceIx != sourceIndexWallpaper { m.gradientOn = false + if m.focus == focusGradient || m.focus == focusGradientSample || m.focus == focusGradientRow { + m.focus = focusSource + } } m.resetPreviewTracker() m.ensureValidFocus() @@ -334,9 +356,35 @@ func (m *tuiModel) updateMainKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case focusGradient: if k == " " { m.gradientOn = !m.gradientOn + if !m.gradientOn && (m.focus == focusGradientSample || m.focus == focusGradientRow) { + m.focus = focusGradient + } + m.resetPreviewTracker() + return m, m.schedulePreview() + } + case focusGradientSample: + switch k { + case " ", "left", "right", "h", "l": + m.gradientSampleIsRow = !m.gradientSampleIsRow + if !m.gradientSampleIsRow && m.focus == focusGradientRow { + m.focus = focusGradientSample + } m.resetPreviewTracker() return m, m.schedulePreview() } + case focusGradientRow: + switch k { + case "left", "down", "h": + if m.gradientRowPct > 0 { + m.gradientRowPct-- + } + return m, m.schedulePreview() + case "right", "up", "l": + if m.gradientRowPct < 100 { + m.gradientRowPct++ + } + return m, m.schedulePreview() + } case focusBrightness: switch k { case "left", "down", "h": @@ -388,6 +436,12 @@ func (m *tuiModel) focusSequence() []tuiFocus { seq := []tuiFocus{focusIP, focusSource} if m.sourceIx == sourceIndexWallpaper { seq = append(seq, focusGradient) + if m.gradientOn { + seq = append(seq, focusGradientSample) + if m.gradientSampleIsRow { + seq = append(seq, focusGradientRow) + } + } } seq = append(seq, focusBrightness, focusSaturation, focusService, focusSave, focusQuit) return seq @@ -410,18 +464,32 @@ func (m *tuiModel) ensureValidFocus() { if m.focus == focusGradient && m.sourceIx != sourceIndexWallpaper { m.focus = focusSource } + if m.focus == focusGradientSample || m.focus == focusGradientRow { + if m.sourceIx != sourceIndexWallpaper || !m.gradientOn { + m.focus = focusGradient + } + } + if m.focus == focusGradientRow && !m.gradientSampleIsRow { + m.focus = focusGradientSample + } } func (m *tuiModel) currentConfigFromForm() tuiConfig { ip := strings.TrimSpace(m.mainTI.Value()) grad := m.gradientOn && m.sourceIx == sourceIndexWallpaper + gs := "average" + if m.gradientSampleIsRow { + gs = "row" + } return tuiConfig{ - IP: ip, - Source: sourceIdxToName(m.sourceIx), - Brightness: brightnessPctTo255(m.brightPct), - Saturation: float64(m.satPct) / 100.0, - Gradient: grad, - GradientLEDs: m.cfg.GradientLEDs, + IP: ip, + Source: sourceIdxToName(m.sourceIx), + Brightness: brightnessPctTo255(m.brightPct), + Saturation: float64(m.satPct) / 100.0, + Gradient: grad, + GradientLEDs: m.cfg.GradientLEDs, + GradientSample: gs, + GradientRow: m.gradientRowPct, } } @@ -458,7 +526,11 @@ func (m *tuiModel) updatePreviewSwatches(cfg *tuiConfig) { if err != nil { return } - strip, err := wallpaper.ColumnStripForLEDCount(wallpaper.CurrentSymlink(), n) + kind := wallpaper.GradientSampleAverage + if normalizeGradientSample(cfg.GradientSample) == "row" { + kind = wallpaper.GradientSampleRow + } + strip, err := wallpaper.ColumnStripForLEDCount(wallpaper.CurrentSymlink(), n, kind, cfg.GradientRow) if err != nil || len(strip) == 0 { return } @@ -573,6 +645,20 @@ func (m *tuiModel) View() string { glabel = "On (left→right strip)" } b.WriteString(m.renderLabeled("Wallpaper gradient", glabel+" (Space)", m.focus == focusGradient) + "\n") + if m.gradientOn { + slabel := "Average (full height)" + if m.gradientSampleIsRow { + slabel = "Row (scanline)" + } + b.WriteString(m.renderLabeled("Gradient sample", slabel+" (←/→ Space)", m.focus == focusGradientSample) + "\n") + if m.gradientSampleIsRow { + b.WriteString(m.renderLabeled( + "Gradient row (0=top·100=bottom)", + fmt.Sprintf("%d%% %s", m.gradientRowPct, m.renderBar(m.gradientRowPct, 100)), + m.focus == focusGradientRow, + ) + "\n") + } + } } b.WriteString(m.renderLabeled(fmt.Sprintf("Brightness: %d%%", m.brightPct), m.renderBar(m.brightPct, 100), m.focus == focusBrightness) + "\n") b.WriteString(m.renderLabeled(fmt.Sprintf("Saturation: %.2f×", float64(m.satPct)/100.0), m.renderBar(m.satPct, 200), m.focus == focusSaturation) + "\n\n") diff --git a/tui_config.go b/tui_config.go index f3bbd3f..0973f12 100644 --- a/tui_config.go +++ b/tui_config.go @@ -12,6 +12,7 @@ import ( "github.com/ericdahl-dev/omarchy-wled/internal/config" "github.com/ericdahl-dev/omarchy-wled/internal/daemon" "github.com/ericdahl-dev/omarchy-wled/internal/source" + "github.com/ericdahl-dev/omarchy-wled/internal/wallpaper" "github.com/ericdahl-dev/omarchy-wled/internal/wled" ) @@ -21,8 +22,18 @@ type tuiConfig struct { Source string Brightness int Saturation float64 - Gradient bool // wallpaper column strip (requires source bg) - GradientLEDs int // 0 = fetch LED count from WLED /json/info + Gradient bool // wallpaper column strip (requires source bg) + GradientLEDs int // 0 = fetch LED count from WLED /json/info + GradientSample string // average | row (full-height column avg vs scanline) + GradientRow int // 0–100 when GradientSample is row +} + +func normalizeGradientSample(s string) string { + s = strings.TrimSpace(strings.ToLower(s)) + if s == "row" { + return "row" + } + return "average" } const ( @@ -62,6 +73,15 @@ func (c *tuiConfig) Validate() error { if c.GradientLEDs < 0 { return fmt.Errorf("gradient_leds must be >= 0 — got %d", c.GradientLEDs) } + g := strings.TrimSpace(strings.ToLower(c.GradientSample)) + switch g { + case "", "average", "row": + default: + return fmt.Errorf("gradient_sample must be average or row — got %q", c.GradientSample) + } + if c.GradientRow < 0 || c.GradientRow > 100 { + return fmt.Errorf("gradient_row must be 0–100 — got %d", c.GradientRow) + } return nil } @@ -128,17 +148,29 @@ func loadTuiConfig(path string) (*tuiConfig, error) { gradLEDs = n } } + gradSample := "average" + if v, ok := m["gradient_sample"]; ok && strings.TrimSpace(v) != "" { + gradSample = normalizeGradientSample(v) + } + gradRow := 50 + if v, ok := m["gradient_row"]; ok { + if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil { + gradRow = n + } + } nsrc := normalizeSource(src) if nsrc != "bg" { grad = false } return &tuiConfig{ - IP: ip, - Source: nsrc, - Brightness: bri, - Saturation: sat, - Gradient: grad, - GradientLEDs: gradLEDs, + IP: ip, + Source: nsrc, + Brightness: bri, + Saturation: sat, + Gradient: grad, + GradientLEDs: gradLEDs, + GradientSample: gradSample, + GradientRow: gradRow, }, nil } @@ -149,6 +181,7 @@ func saveTuiConfig(path string, c *tuiConfig) error { } src := normalizeSource(c.Source) grad := c.Gradient && src == "bg" + gs := normalizeGradientSample(c.GradientSample) content := fmt.Sprintf( `ip = "%s" source = "%s" @@ -156,8 +189,10 @@ brightness = %d saturation = %g gradient = %t gradient_leds = %d +gradient_sample = "%s" +gradient_row = %d `, - c.IP, src, c.Brightness, c.Saturation, grad, c.GradientLEDs, + c.IP, src, c.Brightness, c.Saturation, grad, c.GradientLEDs, gs, c.GradientRow, ) return os.WriteFile(path, []byte(content), 0o644) } @@ -169,6 +204,8 @@ func previewPushTUI(cfg *tuiConfig, tracker *daemon.DedupeTracker) error { opts := daemon.PushOptions{ WallpaperGradientStrip: cfg.Gradient && normalizeSource(cfg.Source) == "bg", GradientLEDCountOrZero: cfg.GradientLEDs, + GradientSample: wallpaper.GradientSampleKindFromString(cfg.GradientSample), + GradientRowPercent: cfg.GradientRow, } prep, skip, err := daemon.PreparePushColors(src, cfg.IP, cfg.Saturation, opts, true, true) if err != nil { diff --git a/tui_config_test.go b/tui_config_test.go index 4592a36..e2b9465 100644 --- a/tui_config_test.go +++ b/tui_config_test.go @@ -53,11 +53,18 @@ func TestTuiConfigValidation(t *testing.T) { if err := (&tuiConfig{IP: "1", Source: "accent", Brightness: 255, Saturation: 1.0, Gradient: true}).Validate(); err == nil { t.Error("gradient without bg") } + bad := &tuiConfig{IP: "1", Source: "bg", Brightness: 255, Saturation: 1.0, Gradient: true, GradientLEDs: 0, GradientSample: "bogus"} + if err := bad.Validate(); err == nil { + t.Error("bad gradient_sample") + } } func TestTuiConfigGradientRoundTrip(t *testing.T) { path := filepath.Join(t.TempDir(), "config.toml") - cfg := &tuiConfig{IP: "10.0.0.2", Source: "bg", Brightness: 200, Saturation: 1.1, Gradient: true, GradientLEDs: 0} + cfg := &tuiConfig{ + IP: "10.0.0.2", Source: "bg", Brightness: 200, Saturation: 1.1, Gradient: true, GradientLEDs: 0, + GradientSample: "row", GradientRow: 17, + } if err := saveTuiConfig(path, cfg); err != nil { t.Fatal(err) } @@ -65,7 +72,8 @@ func TestTuiConfigGradientRoundTrip(t *testing.T) { if err != nil || got == nil { t.Fatalf("load: %v %+v", err, got) } - if !got.Gradient || got.Source != "bg" || got.GradientLEDs != 0 || got.Brightness != 200 { + if !got.Gradient || got.Source != "bg" || got.GradientLEDs != 0 || got.Brightness != 200 || + got.GradientSample != "row" || got.GradientRow != 17 { t.Fatalf("got %+v", got) } }