refactor/kvconf 3421f16

policy.go RawHistoryFolder

1// Package core owns pure absence policy. Call Handle on one owner goroutine;
2// adapters perform returned effects. Times are explicit, not inferred idle age.
3package core
4
5import (
6	"fmt"
7	"slices"
8	"time"
9
10	"github.com/bnema/neferafk/internal/ports"
11)
12
13type deadline struct {
14	after        time.Duration
15	notification uint64
16	done, due    bool
17}
18type registration struct {
19	id         uint64
20	generation ports.Generation
21	after      time.Duration
22	actions    []ports.Action
23}
24type Policy struct {
25	cfg                     ports.Config
26	generation              ports.Generation
27	nextNotification        uint64
28	deadlines               [4]deadline
29	registrations           []registration
30	acquiring, protected    bool
31	lockGeneration          ports.Generation
32	faded, off, beforeSleep bool
33}
34type State struct {
35	Generation                       ports.Generation
36	Acquiring, Protected, Faded, Off bool
37}
38
39func (p *Policy) State() State { return State{p.generation, p.acquiring, p.protected, p.faded, p.off} }
40func afters(c ports.Config) [4]time.Duration {
41	return [4]time.Duration{c.LockAfter, c.FadeAfter, c.OffAfter, c.SleepAfter}
42}
43
44func New(cfg ports.Config, now time.Time) (*Policy, []ports.PolicyEffect, error) {
45	if err := cfg.Validate(); err != nil {
46		return nil, nil, err
47	}
48	p := &Policy{cfg: cfg, generation: 1}
49	for a, d := range afters(cfg) {
50		p.deadlines[a] = deadline{after: d}
51	}
52	return p, p.arm(now), nil
53}
54
55func (p *Policy) arm(_ time.Time) []ports.PolicyEffect {
56	old := p.registrations
57	p.registrations = nil
58	for a := range p.deadlines {
59		d := &p.deadlines[a]
60		if d.after == 0 || d.done || d.due {
61			continue
62		}
63		i := slices.IndexFunc(p.registrations, func(r registration) bool {
64			return d.notification != 0 && r.id == d.notification || d.notification == 0 && r.generation == p.generation && r.after == d.after
65		})
66		if i < 0 {
67			r := registration{after: d.after, generation: p.generation}
68			if d.notification != 0 {
69				j := slices.IndexFunc(old, func(r registration) bool { return r.id == d.notification })
70				if j >= 0 {
71					r = old[j]
72					r.actions = nil
73				}
74			}
75			if r.id == 0 {
76				p.nextNotification++
77				r.id = p.nextNotification
78			}
79			r.actions = []ports.Action{ports.Action(a)}
80			p.registrations = append(p.registrations, r)
81			d.notification = r.id
82		} else {
83			p.registrations[i].actions = append(p.registrations[i].actions, ports.Action(a))
84			d.notification = p.registrations[i].id
85		}
86	}
87	slices.SortFunc(p.registrations, func(a, b registration) int {
88		if a.after < b.after {
89			return -1
90		}
91		if a.after > b.after {
92			return 1
93		}
94		return 0
95	})
96	n := make([]ports.IdleNotification, 0, len(p.registrations))
97	for _, r := range p.registrations {
98		n = append(n, ports.IdleNotification{ID: r.id, After: r.after})
99	}
100	return []ports.PolicyEffect{ports.ArmIdle{Generation: p.generation, Notifications: n}}
101}
102
103func (p *Policy) acquire(out *[]ports.PolicyEffect) {
104	if p.acquiring || p.protected {
105		return
106	}
107	p.acquiring = true
108	p.lockGeneration = p.generation
109	*out = append(*out, ports.AcquireLock{Generation: p.lockGeneration})
110}
111
112func (p *Policy) dispatch() []ports.PolicyEffect {
113	var out []ports.PolicyEffect
114	// Due flags for one notification are established before dispatch; lock is
115	// first only for simultaneous due actions, never because enabled/later.
116	for a := ports.Lock; a <= ports.Sleep; a++ {
117		d := &p.deadlines[a]
118		if !d.due || d.done || d.after == 0 {
119			continue
120		}
121		if (a == ports.OutputOff || a == ports.Sleep) && p.acquiring {
122			continue
123		}
124		d.done = true
125		switch a {
126		case ports.Lock:
127			p.acquire(&out)
128		case ports.Fade:
129			p.faded = true
130			out = append(out, ports.SetFade{Black: true, Duration: p.cfg.FadeDuration})
131		case ports.OutputOff:
132			p.off = true
133			out = append(out, ports.SetOutputPower{On: false})
134		case ports.Sleep:
135			out = append(out, ports.Suspend{})
136		}
137	}
138	return out
139}
140
141func (p *Policy) Handle(now time.Time, event ports.PolicyEvent) ([]ports.PolicyEffect, error) {
142	switch v := event.(type) {
143	case ports.IdleDue:
144		i := slices.IndexFunc(p.registrations, func(r registration) bool {
145			return r.generation == v.Generation && (v.Notification != 0 && r.id == v.Notification || v.Notification == 0 && r.after == v.After)
146		})
147		if i < 0 {
148			return nil, nil
149		}
150		for _, a := range p.registrations[i].actions {
151			p.deadlines[a].due = true
152		}
153		return p.dispatch(), nil
154	case ports.Activity:
155		p.generation++
156		var out []ports.PolicyEffect
157		if p.faded {
158			p.faded = false
159			out = append(out, ports.SetFade{Black: false, Duration: p.cfg.FadeDuration})
160		}
161		if p.off {
162			p.off = false
163			out = append(out, ports.SetOutputPower{On: true})
164		}
165		for a, d := range afters(p.cfg) {
166			p.deadlines[a] = deadline{after: d}
167		}
168		return append(out, p.arm(now)...), nil
169	case ports.Reload:
170		if err := v.Config.Validate(); err != nil {
171			return nil, err
172		}
173		if v.Config == p.cfg {
174			return nil, nil
175		}
176		for a, d := range afters(v.Config) {
177			old := &p.deadlines[a]
178			if old.after != d {
179				// Enablement changes even for completed actions; completion is
180				// pinned for this cycle, but old due/proxy identity is retired.
181				*old = deadline{after: d, done: old.done}
182			}
183		}
184		p.cfg = v.Config
185		p.generation++
186		return p.arm(now), nil
187	case ports.LockConfirmed:
188		if !p.acquiring || v.Generation != p.lockGeneration {
189			return nil, nil
190		}
191		p.acquiring = false
192		p.protected = true
193		out := p.dispatch()
194		if p.beforeSleep {
195			p.beforeSleep = false
196			out = append(out, ports.SleepReady{})
197		}
198		return out, nil
199	case ports.LockReleased:
200		if !p.protected || v.Generation != p.lockGeneration {
201			return nil, nil
202		}
203		p.protected = false // only authoritative successful visual-owner release
204		return nil, nil
205	case ports.LockRequested:
206		var out []ports.PolicyEffect
207		p.acquire(&out)
208		return out, nil
209	case ports.BeforeSleep:
210		if p.protected {
211			return []ports.PolicyEffect{ports.SleepReady{}}, nil
212		}
213		if p.cfg.LockAfter == 0 && !p.acquiring {
214			return []ports.PolicyEffect{ports.SleepReady{}}, nil
215		}
216		p.beforeSleep = true
217		var out []ports.PolicyEffect
218		p.acquire(&out)
219		return out, nil
220	default:
221		return nil, fmt.Errorf("unknown policy event %T", event)
222	}
223}

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 2

Tags 9

↑↓ moveenter openesc close