//! The line a long action draws while it works (§4, "never leave the person //! wondering"): one line on stderr, redrawn in place and cleared when the step //! ends, and only when stderr is a terminal, so pipes, logs and `--json` never //! see it. module cli import os import src.fmt import src.remote import src.snapshot import src.view fn working(text string) { if os.is_atty(2) == 0 { return } width := line_width() shown := fmt.fit(' ${text}', width) // Padded with spaces, not cleared with an escape code, so a plain terminal // and NO_COLOR stay free of control sequences. eprint('\r${fmt.dim(shown)}${' '.repeat(width - fmt.visible_width(shown))}') flush_stderr() } fn done_working() { if os.is_atty(2) != 0 { eprint('\r${' '.repeat(line_width())}\r') flush_stderr() } } // line_width stays one column short of the terminal, so the line never wraps. fn line_width() int { _, columns := view.terminal_size() return if columns > 1 { columns - 1 } else { 79 } } // reading is the walk's line: how many files it has had to read and hash. fn reading() snapshot.Hashing { return snapshot.Hashing{ on: fn (done int, total int) { if done % 100 == 1 { working('reading files · ${done}') } } } } // Watched counts what an offer, steal or strip moves through a backend, so the // line moves with the transfer. Exchange stays unaware of it. struct Watched { label string // whole is a steal of a whole repo, which fetches every key it lists, so a // listing is the total for that kind: files, saves or events. whole bool mut: inner remote.Backend received int sent int listed map[string]int fetched map[string]int kind string } // watched_backend is this machine's backend, counted on the line under `label`. fn watched_backend(label string) !remote.Backend { return remote.Backend(watch(label, this_machine_backend()!)) } fn watch(label string, backend remote.Backend) Watched { return Watched{ label: label inner: backend } } fn (mut w Watched) show() { mut text := w.label if w.kind != '' { text += ' · ${w.kind} ${w.fetched[w.kind]} of ${w.listed[w.kind]}' } if w.sent > 0 { text += ' · ${w.sent} sent' } if w.kind == '' && w.received > 0 { text += ' · ${w.received} received' } working(text) } fn (mut w Watched) get(key string) !remote.Object { object := w.inner.get(key)! kind := key_kind(os.dir(key)) if kind in w.listed { w.kind = kind w.fetched[kind]++ } else { w.received++ } w.show() return object } fn (mut w Watched) list(prefix string) ![]string { keys := w.inner.list(prefix)! if w.whole { w.listed[key_kind(prefix)] += keys.len } return keys } // key_kind names what a key folder such as `repo/objects` holds, in the words // the person knows: an object is a file's contents. fn key_kind(folder string) string { kind := os.base(folder) return if kind == 'objects' { 'files' } else { kind } } fn (mut w Watched) put_if_absent(key string, body []u8) !bool { put := w.inner.put_if_absent(key, body)! w.sent++ w.show() return put } fn (mut w Watched) put_if_match(key string, body []u8, tag string) !bool { put := w.inner.put_if_match(key, body, tag)! w.sent++ w.show() return put } fn (mut w Watched) delete(key string) ! { w.inner.delete(key)! w.sent++ w.show() }