1// Package uierrors holds the error helper shared by the NeferGUI adapters.
2package uierrors
3
4import (
5 "context"
6 "errors"
7)
8
9// Drop removes from err, and recursively from errors.Join trees, every leaf
10// that is exactly one of targets. Only bare leaves match: a real failure that
11// merely wraps a target (fmt.Errorf("x: %w", context.Canceled)) is kept, and so
12// is every sibling of a dropped leaf. It returns nil when nothing else remains.
13//
14// Use it to discard the context.Canceled a NeferGUI Run reports after the
15// adapter itself ended it, without hiding a real error joined to it.
16func Drop(err error, targets ...error) error {
17 if err == nil {
18 return nil
19 }
20 if j, ok := err.(interface{ Unwrap() []error }); ok {
21 var keep []error
22 for _, e := range j.Unwrap() {
23 if e = Drop(e, targets...); e != nil {
24 keep = append(keep, e)
25 }
26 }
27 switch len(keep) {
28 case 0:
29 return nil
30 case 1:
31 return keep[0]
32 }
33 return errors.Join(keep...)
34 }
35 for _, t := range targets {
36 if err == t {
37 return nil
38 }
39 }
40 return err
41}
42
43// Ended classifies how a NeferGUI Run ended. It is pure: the caller snapshots
44// its state once and passes it, so a context that ends between two reads cannot
45// flip the answer.
46//
47// explained says that this adapter or its caller ended the run on purpose (a
48// decision was made, or the parent context is done). parent is the parent
49// context's error at that snapshot, nil when it is still live. closed is
50// returned when the run ended without an explanation.
51//
52// - explained: a bare context.Canceled (and bare parent) is the expected echo
53// of our own cancellation and is dropped. Anything else, including a real
54// error joined with it or an error that merely wraps Canceled, is kept.
55// - not explained: the run ended on its own. When something real remains
56// (an initial authorization, capability or font failure, say), exactly that
57// is returned: calling it a compositor close would mislead. Only when
58// nothing real remains, including a bare Canceled, is closed returned.
59func Ended(runErr error, explained bool, parent, closed error) error {
60 if explained {
61 return Drop(runErr, context.Canceled, parent)
62 }
63 if rest := Drop(runErr, context.Canceled, parent); rest != nil {
64 return rest
65 }
66 return closed
67}
68
69// IsCancellation reports whether err is non-nil and made only of
70// context.Canceled leaves, possibly wrapped with %w or joined. A failure
71// joined with, or merely mentioning, a cancellation is not one. Unlike Drop,
72// wrapped Canceled counts: it is how a caller's own cancellation surfaces
73// through layers that add context. DeadlineExceeded is deliberately not
74// included, so a bounded-operation timeout stays a real failure.
75func IsCancellation(err error) bool {
76 switch e := err.(type) {
77 case nil:
78 return false
79 case interface{ Unwrap() []error }:
80 children := e.Unwrap()
81 for _, child := range children {
82 if !IsCancellation(child) {
83 return false
84 }
85 }
86 return len(children) > 0
87 case interface{ Unwrap() error }:
88 return err == context.Canceled || IsCancellation(e.Unwrap())
89 default:
90 return err == context.Canceled
91 }
92}