Support low-power mode in config (#250)

* Suspend ALSA speaker on stop/pause to save ~2% CPU

* Reduce renderer wakeups in low-power mode

* Support low-power mode in config
This commit is contained in:
Eric van Riet Paap
2026-05-27 20:23:42 +02:00
committed by GitHub
parent 139b2c02c2
commit c9c6faf655
12 changed files with 67 additions and 13 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ func buildApp() *cli.Command {
&cli.StringFlag{Name: "audio-device", Usage: "audio output device (use 'list' to show)"},
&cli.StringFlag{Name: "playlist", Usage: "load a local TOML playlist by name and start playing"},
&cli.StringFlag{Name: "log-level", Usage: "log level: debug, info, warn, error"},
&cli.BoolFlag{Name: "low-power", Usage: "low-power mode: disable visualizer to minimize CPU"},
&cli.BoolFlag{Name: "low-power", Usage: "low-power mode: reduce CPU by lowering UI cadence and disabling visualization"},
&cli.BoolFlag{Name: "daemon", Aliases: []string{"d"}, Usage: "run headless (no TUI), serving IPC for scripts/Waybar"},
}
+3
View File
@@ -55,6 +55,9 @@ eq = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# Visualizer mode: Bars, BarsDot, Rain, BarsOutline, Bricks, Columns, ClassicPeak, Wave, Scatter, Flame, Retro, Pulse, Matrix, Binary, Sakura, Firework, Bubbles, Logo, Terrain, Scope, Heartbeat, Butterfly, Ascii, Firefly, Mosaic, Sand, or None
# visualizer = "Bars"
# Reduce CPU usage by lowering UI cadence and disabling visualization.
# low_power = false
# Log level: "debug", "info", "warn", or "error" (default "info")
# Logs are written to ~/.config/cliamp/cliamp.log
# log_level = "info"
+6 -1
View File
@@ -252,7 +252,7 @@ type Config struct {
NetEase NetEaseConfig // NetEase Cloud Music provider (opt-in via enabled = true)
Plugins map[string]map[string]string // per-plugin config from [plugins.*] sections
LogLevel string // log level: debug, info, warn, error (default "info")
LowPower bool // runtime-only: set by --low-power, not loaded from config
LowPower bool // reduce CPU by lowering UI cadence and disabling visualization
}
// defaultConfig returns a Config with sensible defaults.
@@ -523,6 +523,8 @@ func Load() (Config, error) {
case "debug", "info", "warn", "warning", "error":
cfg.LogLevel = lvl
}
case "low_power":
cfg.LowPower = strings.ToLower(val) == "true"
}
}
}
@@ -733,6 +735,9 @@ func (c *Config) clamp() {
c.Spotify.Bitrate = clampSpotifyBitrate(c.Spotify.Bitrate)
c.PaddingH = max(min(c.PaddingH, 10), 0)
c.PaddingV = max(min(c.PaddingV, 5), 0)
if c.LowPower {
c.Visualizer = "none"
}
}
// nearestAllowed returns the value in allowed closest to v.
+24
View File
@@ -69,3 +69,27 @@ func TestSeekStepLargeDuration(t *testing.T) {
t.Fatalf("SeekStepLargeDuration = %v, want %v", got, want)
}
}
func TestLoadLowPower(t *testing.T) {
t.Setenv("HOME", t.TempDir())
path := filepath.Join(os.Getenv("HOME"), ".config", "cliamp", "config.toml")
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
data := []byte("visualizer = \"Bars\"\nlow_power = true\n")
if err := os.WriteFile(path, data, 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if !cfg.LowPower {
t.Fatal("LowPower = false, want true")
}
if cfg.Visualizer != "none" {
t.Fatalf("Visualizer = %q, want none", cfg.Visualizer)
}
}
+15
View File
@@ -610,6 +610,21 @@ func TestOverridesApplyClamps(t *testing.T) {
}
}
func TestOverridesApplyLowPower(t *testing.T) {
cfg := defaultConfig()
cfg.Visualizer = "Bars"
lowPower := true
Overrides{LowPower: &lowPower}.Apply(&cfg)
if !cfg.LowPower {
t.Fatal("LowPower = false, want true")
}
if cfg.Visualizer != "none" {
t.Fatalf("Visualizer = %q, want none", cfg.Visualizer)
}
}
// Mock player for ApplyPlayer tests
type mockPlayer struct {
volumeMin float64
-3
View File
@@ -77,9 +77,6 @@ func (o Overrides) Apply(cfg *Config) {
}
if o.LowPower != nil && *o.LowPower {
cfg.LowPower = true
// Low-power mode disables the visualizer entirely. With Mode = None,
// the model also uses lower UI cadences to reduce idle wakeups.
cfg.Visualizer = "none"
}
cfg.clamp()
}
+9 -3
View File
@@ -41,10 +41,16 @@ Logs are written to `~/.config/cliamp/cliamp.log`. Levels: `debug`, `info` (defa
## Low-power mode
```sh
cliamp --low-power track.mp3 # minimize CPU: visualizer off
cliamp --low-power track.mp3 # reduce CPU load for this session
```
Forces the visualizer to `none` so the TUI ticks at 5 FPS for the time/seek display only. Useful on battery, slow terminals, or SSH sessions. Press `v` in the player to cycle visualizers back on at any time.
Reduces CPU load by lowering UI/render cadence and forcing the visualizer to `none`. Useful on battery, slow terminals, or SSH sessions. Press `v` in the player to cycle visualizers back on at any time.
To make this persistent, set it in `~/.config/cliamp/config.toml`:
```toml
low_power = true
```
## Headless daemon mode
@@ -102,7 +108,7 @@ cliamp track.mp3 --repeat all --mono ~/Music
| `--bit-depth` | int | 16 | 16, 32 |
| `--playlist` | string | | local TOML playlist name |
| `--log-level` | string | info | debug, info, warn, error |
| `--low-power` | bool | false | disables visualizer; cuts redraw + FFT cost |
| `--low-power` | bool | false | lowers UI cadence; disables visualization |
| `--daemon` / `-d` | bool | false | run headless; IPC only, no TUI |
CLI flags override config file values for the current session only. They are not persisted.
+4
View File
@@ -60,6 +60,10 @@ visualizer = "Bars"
# even at very low volume levels.
vis_volume_linked = true
# Reduce CPU usage by lowering UI cadence and disabling visualization.
# This has the same effect as starting with --low-power.
low_power = false
# Compact mode: cap UI width at 80 columns (default: fluid/full-width)
compact = false
+1 -1
View File
@@ -2090,7 +2090,7 @@ user_id = "your-account-user-id"</code></pre>
<div class="key-row"><kbd>--resample-quality &lt;n&gt;</kbd><span>14</span></div>
<div class="key-row"><kbd>--bit-depth &lt;n&gt;</kbd><span>PCM: 16 or 32</span></div>
<div class="key-row"><kbd>--compact</kbd><span>Cap width at 80 cols</span></div>
<div class="key-row"><kbd>--low-power</kbd><span>Disable visualizer to save CPU</span></div>
<div class="key-row"><kbd>--low-power</kbd><span>Lower UI cadence and disable visualization</span></div>
<div class="key-row"><kbd>--daemon / -d</kbd><span>Run headless (IPC only, no TUI)</span></div>
<div class="key-row"><kbd>--playlist &lt;name&gt;</kbd><span>Load TOML playlist</span></div>
<div class="key-row"><kbd>--provider &lt;name&gt;</kbd><span>Default provider</span></div>
+2 -2
View File
@@ -84,7 +84,7 @@ func (m *Model) findProviderWith(check func(playlist.Provider) bool) playlist.Pr
// SetAutoPlay makes the player start playback immediately on Init.
func (m *Model) SetAutoPlay(v bool) { m.autoPlay = v }
// SetLowPower lowers UI cadences for --low-power without affecting normal mode.
// SetLowPower lowers UI cadences without affecting normal mode.
func (m *Model) SetLowPower(v bool) { m.lowPower = v }
// SetCompact enables compact mode which caps the frame width at 80 columns.
@@ -137,7 +137,7 @@ func (m *Model) SetVisualizer(name string) bool {
m.vis.RequestRefresh()
m.refreshChrome()
// Skip the terminal-title intro animation when the visualizer is disabled
// (e.g. --low-power); the user opted out of visual flair, so the 3-second
// (e.g. low-power mode); the user opted out of visual flair, so the 3-second
// TickFast intro burn (~20 FPS UI rendering) is just wasted CPU.
if mode == ui.VisNone {
m.termTitle.introActive = false
+1 -1
View File
@@ -271,7 +271,7 @@ type Model struct {
fullVis bool
autoPlay bool // start playing immediately on launch
lowPower bool // lower UI/render cadences; set only by --low-power
lowPower bool // lower UI/render cadences in low-power mode
compact bool // compact mode: cap frame width at 80 columns
heightExpanded bool // tracks whether manual 'x' expansion is active
+1 -1
View File
@@ -10,7 +10,7 @@ const (
TickAnalyze = 33 * time.Millisecond // ~30 Hz — FFT analysis cadence (independent of animation)
TickSlow = 200 * time.Millisecond // 5 FPS — visualizer off or overlay
// TickLowPowerPlaying keeps playback bookkeeping responsive while avoiding
// the 20 FPS time/seek refresh cost when --low-power is explicitly enabled.
// the 20 FPS time/seek refresh cost when low-power mode is enabled.
TickLowPowerPlaying = 500 * time.Millisecond // 2 FPS — low-power playback UI cadence
// TickIdle is used when the player is stopped or paused with nothing
// animating (no overlay, no buffering, no pending status / reconnect).