Skip to content

Commit 56c23ad

Browse files
kradalbyclaude
andcommitted
muninkit: unify reading progress under ConsoleKit
ReadingProgress used raw ANSI (`\u{1B}[1;36m`, `\r`, `\u{1B}[2K`) and hand-rolled isatty gating because ConsoleKit's LoadingBar has no running-count primitive. Introduce `ReadingActivity`, a value-type `ActivityIndicatorType` conformance, and wrap it in the generic `ActivityIndicator<_>` the same way WritingProgress wraps ProgressBar. ConsoleKit now owns the refresh loop, ephemeral-line handling, and non-TTY fallback, and the raw escapes / FileHandle.write plumbing are gone. Gallery.swift call sites are unchanged (`ReadingProgress(header:)`, `update(count:text:)`, `complete()`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a6b78c7 commit 56c23ad

2 files changed

Lines changed: 42 additions & 37 deletions

File tree

FUTURES.md

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,17 +136,7 @@ return (previously dropped with `swiftpm2nix`).
136136

137137
---
138138

139-
## 9. Progress UI refinement
140-
141-
Writing progress uses `vapor/console-kit`'s `ProgressBar`. Reading
142-
progress is a minimal in-tree ANSI impl in `ProgressAnimation.swift`
143-
because ConsoleKit's `LoadingBar` doesn't naturally show a running count.
144-
A proper `ActivityBar` conformance would unify both under ConsoleKit and
145-
drop the small ANSI helper.
146-
147-
---
148-
149-
## 10. Test-suite paper cuts
139+
## 9. Test-suite paper cuts
150140

151141
- Several of the deep GalleryTests assertions (57 photos / 9 albums / 47
152142
changed) were hard-coded based on specific fixture counts that have

Sources/MuninKit/ProgressAnimation.swift

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,47 +41,62 @@ public final class WritingProgress {
4141
}
4242
}
4343

44+
/// `ActivityIndicatorType` that renders a running count + current item.
45+
/// Drives `ReadingProgress` through ConsoleKit's `ActivityIndicator<_>`
46+
/// so both read and write paths share the same rendering, ephemeral-line
47+
/// handling, and non-TTY graceful degradation.
48+
struct ReadingActivity: ActivityIndicatorType {
49+
var title: String
50+
var count: Int = 0
51+
var text: String = ""
52+
53+
func outputActivityIndicator(to console: any Console, state: ActivityIndicatorState) {
54+
switch state {
55+
case .ready:
56+
console.output(title.consoleText(.info))
57+
case .active:
58+
console.output("Found: [\(count)] \(text)".consoleText(.plain))
59+
case .success:
60+
console.output("Found: [\(count)]".consoleText(.success))
61+
case .failure:
62+
console.output("Read failed after \(count)".consoleText(.error))
63+
}
64+
}
65+
}
66+
4467
/// Indeterminate progress display (running count + current item) used during
45-
/// input scanning.
46-
///
47-
/// Minimal in-tree implementation: prints a colored header on first use, then
48-
/// repeatedly clears the line and re-emits the current count and item text.
49-
/// Silent when stdout is not a TTY.
68+
/// input scanning. Wraps ``ReadingActivity`` in ConsoleKit's generic
69+
/// `ActivityIndicator` so the refresh loop, ANSI handling, and TTY fallback
70+
/// match ``WritingProgress``.
5071
public final class ReadingProgress {
51-
private let out: FileHandle
52-
private let header: String
53-
private let isInteractive: Bool
54-
private var hasDisplayedHeader = false
72+
private let indicator: ActivityIndicator<ReadingActivity>
73+
private var started = false
5574
private let lock = NSLock()
5675

57-
public init(fileHandle: FileHandle = .standardOutput, header: String) {
58-
self.out = fileHandle
59-
self.header = header
60-
self.isInteractive = isatty(fileHandle.fileDescriptor) != 0
76+
public init(terminal: Terminal = Terminal(), header: String) {
77+
self.indicator = ReadingActivity(title: header).newActivity(for: terminal)
6178
}
6279

6380
/// Update the display with the current count and current item name.
6481
public func update(count: Int, text: String) {
65-
guard isInteractive else { return }
6682
lock.lock()
6783
defer { lock.unlock() }
68-
if !hasDisplayedHeader {
69-
write("\u{1B}[1;36m\(header)\u{1B}[0m\n")
70-
hasDisplayedHeader = true
84+
if !started {
85+
indicator.start()
86+
started = true
7187
}
72-
// CR + clear line + re-emit
73-
write("\r\u{1B}[2KFound: [\(count)] \(text)")
88+
var snapshot = indicator.activity
89+
snapshot.count = count
90+
snapshot.text = text
91+
indicator.activity = snapshot
7492
}
7593

76-
/// Finalize the display (terminates the in-progress line).
94+
/// Finalize the display. Safe to call multiple times.
7795
public func complete() {
78-
guard isInteractive else { return }
7996
lock.lock()
8097
defer { lock.unlock() }
81-
write("\n")
82-
}
83-
84-
private func write(_ s: String) {
85-
out.write(Data(s.utf8))
98+
guard started else { return }
99+
indicator.succeed()
100+
started = false
86101
}
87102
}

0 commit comments

Comments
 (0)