src/share/say.bend

ezx@1.4.0.0 · 184 lines · 0 definitions

1# share/say: the words a long step says, and a spinner for the wait.
2#
3# A clone, a hub fetch or a native build can sit there for half a minute, and
4# a step that prints nothing looks stopped. The line is printed before the
5# work. On a terminal it is drawn again in place, with a mark that moves,
6# until the step finishes; the mark is a child, because the work itself is
7# one program waited on to the end and nothing can be printed while it runs.
8# That child is started with its own session and its standard streams closed,
9# so it opens the terminal by the path this process's own stderr or stdout
10# already has. A pipe is not a terminal: the line is printed once and left,
11# which is what a log wants.
12#
13# The words and the scripts the mark runs are here; what starts and stops the
14# mark is share/spin, which runs programs and so reaches foreign code a law
15# may not import.
16import Base
17import ezx@1.4.0.0/src/pkg/path.bend as P
18
19# a spinner that is running, or none. An empty pid means the line was already
20# printed and nothing has to be stopped. `msg` is what the terminal is owed
21# when the mark is cleared.
22type Spin is Data:
23  Spin{dev: String, pid: String, msg: String}
24
25# the first twelve characters of a commit, which is enough to tell one from
26# the next. A shorter name is already short.
27def brief.go(short: Bool, +rev: String) -> String:
28  match short:
29    case True{}:
30      rev
31    case False{}:
32      String.take(rev, 12n)
33
34# a commit as a line shows it
35def brief(+rev: String) -> String:
36  brief.go(Nat.is_le(String.length(rev), 12n), rev)
37
38# a package's name when its entry has one, otherwise the hash. `main` is kept:
39# it is the name the file has.
40def label.go(empty: Bool, +hash: String, +name: String) -> String:
41  match empty:
42    case True{}:
43      hash
44    case False{}:
45      name
46
47# what a line calls a package
48def label(+hash: String, +entry: String) -> String:
49  +name = P.unbend(P.base(entry))
50  label.go(String.is_empty(name), hash, name)
51
52# `dep sha256: git fetch (https://github.com/…)`
53def dep.git(+name: String, +url: String) -> String:
54  "dep " ++ name ++ ": git fetch (" ++ url ++ ")"
55
56# a package taken from the hub
57def dep.hub(+name: String) -> String:
58  "dep " ++ name ++ ": hub fetch"
59
60# a binary already built for this commit
61def reused(+name: String) -> String:
62  name ++ ": cached"
63
64# the lock resolving every dependency
65def resolving() -> String:
66  "resolving deps"
67
68def resolving.at(+name: String) -> String:
69  "resolving " ++ name
70
71def git.fetch(+url: String) -> String:
72  "git fetch (" ++ url ++ ")"
73
74# a binary being built
75def building(+name: String) -> String:
76  "building " ++ name
77
78# a binary being linked into place
79def linking(+at: String) -> String:
80  "linking " ++ at
81
82# a binary built and linked
83def installed(+name: String, +at: String) -> String:
84  "installed " ++ name ++ " -> " ++ at
85
86# a binary linked, with nothing to build
87def linked(+name: String, +at: String) -> String:
88  "linked " ++ name ++ " -> " ++ at
89
90# a commit's tree being weighed for its NAR hash
91def hashing(+rev: String) -> String:
92  "hashing " ++ brief(rev)
93
94# a device path that is a terminal. A pipe and `/dev/null` are not.
95def tty.at(+path: String) -> Bool:
96  Bool.or(String.starts_with(path, "/dev/pts/"),
97    Bool.or(String.eq(path, "/dev/console"), String.starts_with(path, "/dev/tty")))
98
99def tty.out(on: Bool, +out: String) -> String:
100  match on:
101    case True{}:
102      out
103    case False{}:
104      ""
105
106# stderr's terminal when it is one, otherwise stdout's, otherwise none
107def tty.pick.go(on: Bool, +err: String, +out: String) -> String:
108  match on:
109    case True{}:
110      err
111    case False{}:
112      tty.out(tty.at(out), out)
113
114def tty.pick(+err: String, +out: String) -> String:
115  tty.pick.go(tty.at(err), err, out)
116
117# the hub answered 404 for a manifest. A missing file is a different miss:
118# its path is not `/manifest`.
119def hub.miss(+why: String) -> Bool:
120  Bool.and(String.contains(why, "/manifest:"),
121    String.ends_with(why, "the server answered 404"))
122
123def hub.explain.go(miss: Bool, +hash: String, +url: String, +why: String) -> String:
124  match miss:
125    case True{}:
126      "ez: " ++ hash ++ ": the hub has no manifest for this package (" ++ url ++
127        " answered 404). Re-lock with `ez lock`, fix the dependency, or vendor it offline (`ez add`)."
128    case False{}:
129      "ez: " ++ why
130
131def hub.explain(+hash: String, +url: String, +why: String) -> String:
132  hub.explain.go(hub.miss(why), hash, url, why)
133
134# the commit the words name. A tag is what a person pinned; a bare rev is not.
135def drift.where(bare: Bool, +tag: String, +rev: String) -> String:
136  match bare:
137    case True{}:
138      "commit " ++ rev
139    case False{}:
140      "tagged commit " ++ tag ++ " (" ++ rev ++ ")"
141
142# the tagged commit's tree no longer hashes to the pin. Upgrade will not
143# invent a new hash for that: the pin is what the ledger says, and a drift
144# is the ledger and the tree disagreeing.
145def drift(+name: String, +tag: String, +rev: String, +old: String, +new: String) -> String:
146  "ez: " ++ name ++ ": the tree hash of " ++
147    drift.where(String.is_empty(tag), tag, rev) ++
148    " no longer matches the pin (the package moved, or the hash is wrong: " ++
149    old ++ " on the pin, " ++ new ++
150    " in the tree). `ez lock --upgrade` cannot fix a drifted pin. Update the dependency hash, re-add the package, or check the tag."
151
152# a drift judged before the tree was weighed, so there are not two hashes to show
153def stuck(+name: String) -> String:
154  "ez: " ++ name ++
155    ": the pin drifted. `ez lock --upgrade` cannot fix a drifted pin. Update the dependency hash, re-add the package, or check the tag."
156
157# a `[tools.` section in the ledger text. The parser is not asked: a ledger
158# that does not parse can still have named the command.
159def has.tools(+text: String) -> Bool:
160  String.contains(text, "[tools.")
161
162# `ez sync` is not a command. A ledger that pins tools already has the one
163# that builds them; a ledger that does not is pointed at lock and install.
164def hint(tools: Bool) -> String:
165  match tools:
166    case True{}:
167      "hint: `sync` is not a command; use `ez tool sync`"
168    case False{}:
169      "hint: `sync` is not a command. Use `ez lock` to resolve dependencies, or `ez tool install <target>` to link a binary."
170
171# the parent's fd, by number. The child cannot look at its own: that one was
172# replaced with a pipe before this ran. `$1` is the number.
173def probe() -> String:
174  "ppid=; while read -r k v; do case $k in PPid:) ppid=$v;; esac; done < /proc/self/status; readlink /proc/$ppid/fd/$1 2>/dev/null"
175
176# a mark on the terminal while the parent is alive. `$1` is the terminal,
177# `$2` the line. The mark walks ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏. An empty parent
178# id exits rather than spinning on `/proc`.
179def frames() -> String:
180  "ppid=; while read -r k v; do case $k in PPid:) ppid=$v;; esac; done < /proc/self/status; [ -n \"$ppid\" ] || exit 0; while [ -d /proc/$ppid ]; do printf '\\r%s %s' '⠋' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠙' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠹' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠸' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠼' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠴' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠦' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠧' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠇' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠏' \"$2\" >\"$1\"; sleep 0.08; done"
181
182# the mark wiped, and the line left in its place
183def settle() -> String:
184  "printf '\\r\\033[K%s\\n' \"$2\" >\"$1\""