detached 1c70add

feat(config): replace animations.slowdown with animations.speed and make normal faster

commit
1c70add6c2b75447de6a2465160057e4aac2b774
author
brice · · 2026-10-05 07:44 UTC
parent
f6e232f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +39 -17
M README.md +1 -1
@@ -18,7 +18,7 @@
1818
1919 I wanted a compositor I would never have to think about. It takes the newest Wayland protocols and kernel features as they land, gives games the whole GPU, and otherwise stays out of sight.
2020
21-It is small on purpose, and it will stay small. Drawing is kept to simple primitives: no blur, no shadows, no rounded corners, no themes. There is no built-in bar or wallpaper either; for those, bring your own: any layer-shell client works. Animations are experimental and off by default (`animations = on` turns them on); off, every change is instant and touchpad swipes switch on release. With them, three-finger swipes follow the fingers, windows and the view move with short springs (about 300 ms) when you scroll, switch workspaces, move columns or windows, resize them, drop a dragged tile or send a workspace to another monitor; windows fade in and out when they open, close or leave with the stash, the overview zooms its cards, and a brief pulse marks the window you just focused. They stay cheap: nothing runs while the screen is still, frames follow the display, only the region that moves is redrawn, apps are resized once, to their final size, and do not redraw during a transition, and nothing is copied or blurred. About 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change.
21+It is small on purpose, and it will stay small. Drawing is kept to simple primitives: no blur, no shadows, no rounded corners, no themes. There is no built-in bar or wallpaper either; for those, bring your own: any layer-shell client works. Animations are experimental and off by default (`animations = on` turns them on); off, every change is instant and touchpad swipes switch on release. With them, three-finger swipes follow the fingers, windows and the view move with short springs (about 180 ms) when you scroll, switch workspaces, move columns or windows, resize them, drop a dragged tile or send a workspace to another monitor; windows fade in and out when they open, close or leave with the stash, the overview zooms its cards, and a brief pulse marks the window you just focused. They stay cheap: nothing runs while the screen is still, frames follow the display, only the region that moves is redrawn, apps are resized once, to their final size, and do not redraw during a transition, and nothing is copied or blurred. About 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change.
2222
2323 ## OK, but why Go?
2424
@@ -18,7 +18,7 @@
18 18
19 I wanted a compositor I would never have to think about. It takes the newest Wayland protocols and kernel features as they land, gives games the whole GPU, and otherwise stays out of sight.19 I wanted a compositor I would never have to think about. It takes the newest Wayland protocols and kernel features as they land, gives games the whole GPU, and otherwise stays out of sight.
20 20
21-It is small on purpose, and it will stay small. Drawing is kept to simple primitives: no blur, no shadows, no rounded corners, no themes. There is no built-in bar or wallpaper either; for those, bring your own: any layer-shell client works. Animations are experimental and off by default (`animations = on` turns them on); off, every change is instant and touchpad swipes switch on release. With them, three-finger swipes follow the fingers, windows and the view move with short springs (about 300 ms) when you scroll, switch workspaces, move columns or windows, resize them, drop a dragged tile or send a workspace to another monitor; windows fade in and out when they open, close or leave with the stash, the overview zooms its cards, and a brief pulse marks the window you just focused. They stay cheap: nothing runs while the screen is still, frames follow the display, only the region that moves is redrawn, apps are resized once, to their final size, and do not redraw during a transition, and nothing is copied or blurred. About 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change.21+It is small on purpose, and it will stay small. Drawing is kept to simple primitives: no blur, no shadows, no rounded corners, no themes. There is no built-in bar or wallpaper either; for those, bring your own: any layer-shell client works. Animations are experimental and off by default (`animations = on` turns them on); off, every change is instant and touchpad swipes switch on release. With them, three-finger swipes follow the fingers, windows and the view move with short springs (about 180 ms) when you scroll, switch workspaces, move columns or windows, resize them, drop a dragged tile or send a workspace to another monitor; windows fade in and out when they open, close or leave with the stash, the overview zooms its cards, and a brief pulse marks the window you just focused. They stay cheap: nothing runs while the screen is still, frames follow the display, only the region that moves is redrawn, apps are resized once, to their final size, and do not redraw during a transition, and nothing is copied or blurred. About 40–80 MB of RAM with two 4K monitors, and almost no CPU while the screen does not change.
22 22
23 ## OK, but why Go?23 ## OK, but why Go?
24 24
M docs/config.md +1 -1
@@ -45,7 +45,7 @@ NeferWL reads `$XDG_CONFIG_HOME/neferwl/config` (or `~/.config/neferwl/config`).
4545 | `mouse.left-handed` | `off` | Swaps the left and right mouse buttons |
4646 | `cursor.hide-after` | `5s` | Hides the pointer cursor after this long without motion; the next motion shows it. 100ms to 1h, or `off` |
4747 | `animations` | `off` | **Experimental.** `on` animates changes; `off` makes every change instant and the three-finger swipe no longer follows the fingers: swipe landings, scrolling, workspace switches, window moves and resizes, drag drops, workspaces sent to another monitor, the overview, windows opening and closing, the stash showing and hiding, the focus pulse |
48-| `animations.slowdown` | `1` | Multiplies the duration of every spring (not the focus pulse), `0.1` to `10` |
48+| `animations.speed` | `normal` | Speed of every spring (not the focus pulse): `slow`, `normal` or `fast` (a view move lands in about 260, 180 or 130 ms), or a number relative to `normal`, from `0.1` (10× slower) to `10` (10× faster) |
4949 | `focus.follow-move` | `off` | Follow a column moved to another workspace |
5050 | `focus.animation` | `pulse` | Focus indicator on a window that keeps the focus for 150 ms: `pulse` (brief rise and fall) or `off`. Skipped for fullscreen windows, a window alone on screen (e.g. a maximized column), the overview and a window focused as it opens. Off while `animations = off` |
5151 | `focus.effect` | `screen` | What the focus indicator draws: `screen` brightens the window slightly, visible on black too. HDR (PQ) client content is not brightened on HDR outputs |
@@ -45,7 +45,7 @@ NeferWL reads `$XDG_CONFIG_HOME/neferwl/config` (or `~/.config/neferwl/config`).
45 | `mouse.left-handed` | `off` | Swaps the left and right mouse buttons |45 | `mouse.left-handed` | `off` | Swaps the left and right mouse buttons |
46 | `cursor.hide-after` | `5s` | Hides the pointer cursor after this long without motion; the next motion shows it. 100ms to 1h, or `off` |46 | `cursor.hide-after` | `5s` | Hides the pointer cursor after this long without motion; the next motion shows it. 100ms to 1h, or `off` |
47 | `animations` | `off` | **Experimental.** `on` animates changes; `off` makes every change instant and the three-finger swipe no longer follows the fingers: swipe landings, scrolling, workspace switches, window moves and resizes, drag drops, workspaces sent to another monitor, the overview, windows opening and closing, the stash showing and hiding, the focus pulse |47 | `animations` | `off` | **Experimental.** `on` animates changes; `off` makes every change instant and the three-finger swipe no longer follows the fingers: swipe landings, scrolling, workspace switches, window moves and resizes, drag drops, workspaces sent to another monitor, the overview, windows opening and closing, the stash showing and hiding, the focus pulse |
48-| `animations.slowdown` | `1` | Multiplies the duration of every spring (not the focus pulse), `0.1` to `10` |48+| `animations.speed` | `normal` | Speed of every spring (not the focus pulse): `slow`, `normal` or `fast` (a view move lands in about 260, 180 or 130 ms), or a number relative to `normal`, from `0.1` (10× slower) to `10` (10× faster) |
49 | `focus.follow-move` | `off` | Follow a column moved to another workspace |49 | `focus.follow-move` | `off` | Follow a column moved to another workspace |
50 | `focus.animation` | `pulse` | Focus indicator on a window that keeps the focus for 150 ms: `pulse` (brief rise and fall) or `off`. Skipped for fullscreen windows, a window alone on screen (e.g. a maximized column), the overview and a window focused as it opens. Off while `animations = off` |50 | `focus.animation` | `pulse` | Focus indicator on a window that keeps the focus for 150 ms: `pulse` (brief rise and fall) or `off`. Skipped for fullscreen windows, a window alone on screen (e.g. a maximized column), the overview and a window focused as it opens. Off while `animations = off` |
51 | `focus.effect` | `screen` | What the focus indicator draws: `screen` brightens the window slightly, visible on black too. HDR (PQ) client content is not brightened on HDR outputs |51 | `focus.effect` | `screen` | What the focus indicator draws: `screen` brightens the window slightly, visible on black too. HDR (PQ) client content is not brightened on HDR outputs |
M examples/config +3 -2
@@ -77,9 +77,10 @@ focus.follow-move = off
7777 # they open, close or leave with the stash, the overview zooms its cards;
7878 # off: every change is instant and three-finger swipes act when the
7979 # fingers lift.
80-# slowdown multiplies every spring's duration (0.1 to 10).
80+# speed: slow, normal, fast, or a number from 0.1 (10x slower) to 10
81+# (10x faster).
8182 animations = off
82-animations.slowdown = 1
83+animations.speed = normal
8384 # Focus indicator on the newly focused window: an animation (pulse or off)
8485 # of an effect (screen: a slight brightening), peaking at strength
8586 # (0.01 to 0.2; 0.04: black reaches about 10/255).
@@ -77,9 +77,10 @@ focus.follow-move = off
77 # they open, close or leave with the stash, the overview zooms its cards;77 # they open, close or leave with the stash, the overview zooms its cards;
78 # off: every change is instant and three-finger swipes act when the78 # off: every change is instant and three-finger swipes act when the
79 # fingers lift.79 # fingers lift.
80-# slowdown multiplies every spring's duration (0.1 to 10).80+# speed: slow, normal, fast, or a number from 0.1 (10x slower) to 10
81+# (10x faster).
81 animations = off82 animations = off
82-animations.slowdown = 183+animations.speed = normal
83 # Focus indicator on the newly focused window: an animation (pulse or off)84 # Focus indicator on the newly focused window: an animation (pulse or off)
84 # of an effect (screen: a slight brightening), peaking at strength85 # of an effect (screen: a slight brightening), peaking at strength
85 # (0.01 to 0.2; 0.04: black reaches about 10/255).86 # (0.01 to 0.2; 0.04: black reaches about 10/255).
M internal/adapters/config/config.go +19 -6
@@ -171,7 +171,7 @@ func Defaults() ports.Config {
171171 c.Cursor.HideAfter = 5 * time.Second
172172 // Animations are experimental: off unless the config turns them on.
173173 c.Animations.On = false
174- c.Animations.Slowdown = 1
174+ c.Animations.Slowdown = normalSlowdown
175175 c.Focus.Animation = ports.FocusAnimationPulse
176176 c.Focus.Effect = ports.FocusEffectScreen
177177 c.Focus.Strength = 0.04
@@ -758,12 +758,17 @@ func set(c *ports.Config, key, v string) error {
758758 return err
759759 }
760760 c.Animations.On = b
761- case "animations.slowdown":
762- f, err := strconv.ParseFloat(v, 64)
763- if err != nil || !(f >= 0.1 && f <= 10) {
764- return fmt.Errorf("must be between 0.1 and 10")
761+ case "animations.speed":
762+ f, ok := animationSpeeds[v]
763+ if !ok {
764+ var err error
765+ f, err = strconv.ParseFloat(v, 64)
766+ if err != nil || !(f >= 0.1 && f <= 10) {
767+ return fmt.Errorf("must be slow, normal, fast or a number between 0.1 and 10")
768+ }
765769 }
766- c.Animations.Slowdown = f
770+ // Core stretches springs by a duration factor: the inverse.
771+ c.Animations.Slowdown = normalSlowdown / f
767772 case "focus.follow-move":
768773 b, err := onOff(v)
769774 if err != nil {
@@ -1022,6 +1027,14 @@ func positive(dst *int, v string, max int) error {
10221027 return nil
10231028 }
10241029
1030+// normalSlowdown is the spring duration factor of animations.speed =
1031+// normal (1): a view move lands in about 180 ms.
1032+const normalSlowdown = 0.7
1033+
1034+// animationSpeeds are the named animations.speed values, relative to
1035+// normal: a view move lands in about 260 ms at slow and 130 ms at fast.
1036+var animationSpeeds = map[string]float64{"slow": 0.7, "normal": 1, "fast": 1.4}
1037+
10251038 func onOff(v string) (bool, error) {
10261039 switch v {
10271040 case "on":
@@ -171,7 +171,7 @@ func Defaults() ports.Config {
171 c.Cursor.HideAfter = 5 * time.Second171 c.Cursor.HideAfter = 5 * time.Second
172 // Animations are experimental: off unless the config turns them on.172 // Animations are experimental: off unless the config turns them on.
173 c.Animations.On = false173 c.Animations.On = false
174- c.Animations.Slowdown = 1174+ c.Animations.Slowdown = normalSlowdown
175 c.Focus.Animation = ports.FocusAnimationPulse175 c.Focus.Animation = ports.FocusAnimationPulse
176 c.Focus.Effect = ports.FocusEffectScreen176 c.Focus.Effect = ports.FocusEffectScreen
177 c.Focus.Strength = 0.04177 c.Focus.Strength = 0.04
@@ -758,12 +758,17 @@ func set(c *ports.Config, key, v string) error {
758 return err758 return err
759 }759 }
760 c.Animations.On = b760 c.Animations.On = b
761- case "animations.slowdown":761+ case "animations.speed":
762- f, err := strconv.ParseFloat(v, 64)762+ f, ok := animationSpeeds[v]
763- if err != nil || !(f >= 0.1 && f <= 10) {763+ if !ok {
764- return fmt.Errorf("must be between 0.1 and 10")764+ var err error
765+ f, err = strconv.ParseFloat(v, 64)
766+ if err != nil || !(f >= 0.1 && f <= 10) {
767+ return fmt.Errorf("must be slow, normal, fast or a number between 0.1 and 10")
768+ }
765 }769 }
766- c.Animations.Slowdown = f770+ // Core stretches springs by a duration factor: the inverse.
771+ c.Animations.Slowdown = normalSlowdown / f
767 case "focus.follow-move":772 case "focus.follow-move":
768 b, err := onOff(v)773 b, err := onOff(v)
769 if err != nil {774 if err != nil {
@@ -1022,6 +1027,14 @@ func positive(dst *int, v string, max int) error {
1022 return nil1027 return nil
1023 }1028 }
1024 1029
1030+// normalSlowdown is the spring duration factor of animations.speed =
1031+// normal (1): a view move lands in about 180 ms.
1032+const normalSlowdown = 0.7
1033+
1034+// animationSpeeds are the named animations.speed values, relative to
1035+// normal: a view move lands in about 260 ms at slow and 130 ms at fast.
1036+var animationSpeeds = map[string]float64{"slow": 0.7, "normal": 1, "fast": 1.4}
1037+
1025 func onOff(v string) (bool, error) {1038 func onOff(v string) (bool, error) {
1026 switch v {1039 switch v {
1027 case "on":1040 case "on":
M internal/adapters/config/config_test.go +12 -5
@@ -2,6 +2,7 @@ package config
22
33 import (
44 "fmt"
5+ "math"
56 "os"
67 "path/filepath"
78 "reflect"
@@ -80,23 +81,29 @@ func TestFollowMove(t *testing.T) {
8081
8182 func TestAnimations(t *testing.T) {
8283 d := Defaults().Animations
83- if d.On || d.Slowdown != 1 {
84+ if d.On || d.Slowdown != normalSlowdown {
8485 t.Fatal("animations defaults", d)
8586 }
86- c, w := parseString(t, "animations = on\nanimations.slowdown = 2\n")
87- if !c.Animations.On || c.Animations.Slowdown != 2 || len(w) != 0 {
87+ c, w := parseString(t, "animations = on\nanimations.speed = 0.5\n")
88+ if !c.Animations.On || c.Animations.Slowdown != 2*normalSlowdown || len(w) != 0 {
8889 t.Fatal(c.Animations, w)
8990 }
91+ for v, slowdown := range map[string]float64{"slow": 1, "normal": 0.7, "fast": 0.5, "10": 0.07, "0.1": 7} {
92+ c, w = parseString(t, "animations.speed = "+v+"\n")
93+ if math.Abs(c.Animations.Slowdown-slowdown) > 1e-9 || len(w) != 0 {
94+ t.Fatal(v, c.Animations, w)
95+ }
96+ }
9097 c, w = parseString(t, "animations = off\n")
9198 if c.Animations.On || len(w) != 0 {
9299 t.Fatal(c.Animations, w)
93100 }
94- for _, line := range []string{"animations = maybe", "animations.slowdown = 0", "animations.slowdown = 11"} {
101+ for _, line := range []string{"animations = maybe", "animations.speed = 0", "animations.speed = 11", "animations.speed = turbo", "animations.slowdown = 2"} {
95102 c, w = parseString(t, line+"\n")
96103 if c.Animations != d || len(w) != 1 {
97104 t.Fatal(line, c.Animations, w)
98105 }
99- if strings.Contains(line, "slowdown") && !strings.Contains(w[0].Msg, "must be between 0.1 and 10") {
106+ if strings.Contains(line, "speed") && !strings.Contains(w[0].Msg, "must be slow, normal, fast or a number between 0.1 and 10") {
100107 t.Fatal(line, w)
101108 }
102109 }
@@ -2,6 +2,7 @@ package config
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5+ "math"
5 "os"6 "os"
6 "path/filepath"7 "path/filepath"
7 "reflect"8 "reflect"
@@ -80,23 +81,29 @@ func TestFollowMove(t *testing.T) {
80 81
81 func TestAnimations(t *testing.T) {82 func TestAnimations(t *testing.T) {
82 d := Defaults().Animations83 d := Defaults().Animations
83- if d.On || d.Slowdown != 1 {84+ if d.On || d.Slowdown != normalSlowdown {
84 t.Fatal("animations defaults", d)85 t.Fatal("animations defaults", d)
85 }86 }
86- c, w := parseString(t, "animations = on\nanimations.slowdown = 2\n")87+ c, w := parseString(t, "animations = on\nanimations.speed = 0.5\n")
87- if !c.Animations.On || c.Animations.Slowdown != 2 || len(w) != 0 {88+ if !c.Animations.On || c.Animations.Slowdown != 2*normalSlowdown || len(w) != 0 {
88 t.Fatal(c.Animations, w)89 t.Fatal(c.Animations, w)
89 }90 }
91+ for v, slowdown := range map[string]float64{"slow": 1, "normal": 0.7, "fast": 0.5, "10": 0.07, "0.1": 7} {
92+ c, w = parseString(t, "animations.speed = "+v+"\n")
93+ if math.Abs(c.Animations.Slowdown-slowdown) > 1e-9 || len(w) != 0 {
94+ t.Fatal(v, c.Animations, w)
95+ }
96+ }
90 c, w = parseString(t, "animations = off\n")97 c, w = parseString(t, "animations = off\n")
91 if c.Animations.On || len(w) != 0 {98 if c.Animations.On || len(w) != 0 {
92 t.Fatal(c.Animations, w)99 t.Fatal(c.Animations, w)
93 }100 }
94- for _, line := range []string{"animations = maybe", "animations.slowdown = 0", "animations.slowdown = 11"} {101+ for _, line := range []string{"animations = maybe", "animations.speed = 0", "animations.speed = 11", "animations.speed = turbo", "animations.slowdown = 2"} {
95 c, w = parseString(t, line+"\n")102 c, w = parseString(t, line+"\n")
96 if c.Animations != d || len(w) != 1 {103 if c.Animations != d || len(w) != 1 {
97 t.Fatal(line, c.Animations, w)104 t.Fatal(line, c.Animations, w)
98 }105 }
99- if strings.Contains(line, "slowdown") && !strings.Contains(w[0].Msg, "must be between 0.1 and 10") {106+ if strings.Contains(line, "speed") && !strings.Contains(w[0].Msg, "must be slow, normal, fast or a number between 0.1 and 10") {
100 t.Fatal(line, w)107 t.Fatal(line, w)
101 }108 }
102 }109 }
M internal/core/gesture.go +1 -1
@@ -359,7 +359,7 @@ func (c *Core) hasScreen(s *screen) bool {
359359 // animOn reports whether transitions run (animations = on).
360360 func (c *Core) animOn() bool { return c.cfg.Animations.On }
361361
362-// spring starts s at now, slowed down by animations.slowdown. Every motion
362+// spring starts s at now, stretched by animations.speed (Slowdown). Every motion
363363 // core creates goes through it, except a retarget, which keeps the slowdown
364364 // of the motion it replaces.
365365 func (c *Core) spring(s spring, now time.Time) motion {
@@ -359,7 +359,7 @@ func (c *Core) hasScreen(s *screen) bool {
359 // animOn reports whether transitions run (animations = on).359 // animOn reports whether transitions run (animations = on).
360 func (c *Core) animOn() bool { return c.cfg.Animations.On }360 func (c *Core) animOn() bool { return c.cfg.Animations.On }
361 361
362-// spring starts s at now, slowed down by animations.slowdown. Every motion362+// spring starts s at now, stretched by animations.speed (Slowdown). Every motion
363 // core creates goes through it, except a retarget, which keeps the slowdown363 // core creates goes through it, except a retarget, which keeps the slowdown
364 // of the motion it replaces.364 // of the motion it replaces.
365 func (c *Core) spring(s spring, now time.Time) motion {365 func (c *Core) spring(s spring, now time.Time) motion {
M internal/ports/config.go +2 -1
@@ -144,7 +144,8 @@ type Config struct {
144144 // motion; the next motion shows it. 0 never hides it.
145145 Cursor struct{ HideAfter time.Duration }
146146 // Animations: On enables motion (off lands everything instantly);
147- // Slowdown scales every spring's duration (not the focus pulse), 0.1 to 10.
147+ // Slowdown scales every spring's duration (not the focus pulse), 0.1 to
148+ // 10 (the config's animations.speed maps to it).
148149 Animations struct {
149150 On bool
150151 Slowdown float64
@@ -144,7 +144,8 @@ type Config struct {
144 // motion; the next motion shows it. 0 never hides it.144 // motion; the next motion shows it. 0 never hides it.
145 Cursor struct{ HideAfter time.Duration }145 Cursor struct{ HideAfter time.Duration }
146 // Animations: On enables motion (off lands everything instantly);146 // Animations: On enables motion (off lands everything instantly);
147- // Slowdown scales every spring's duration (not the focus pulse), 0.1 to 10.147+ // Slowdown scales every spring's duration (not the focus pulse), 0.1 to
148+ // 10 (the config's animations.speed maps to it).
148 Animations struct {149 Animations struct {
149 On bool150 On bool
150 Slowdown float64151 Slowdown float64

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 24

Tags 10

↑↓ moveenter openesc close