| 123 |
123 |
|
/// ports (the dashboard, the day view, the board) each said the fact its own
|
| 124 |
124 |
|
/// JS counterpart said at port time, and none said this one.
|
| 125 |
125 |
|
///
|
| 126 |
|
- |
/// # What the description cannot carry
|
|
126 |
+ |
/// # The detail behind each label
|
| 127 |
127 |
|
///
|
| 128 |
|
- |
/// The shipped badges put the detail in a `title`: the block depth behind
|
| 129 |
|
- |
/// "Blocked", the freed count's wording behind "Unblocks N", the repair
|
| 130 |
|
- |
/// instruction behind "Cycle". A [`Tag`] has a label and a tone and no hint, so
|
| 131 |
|
- |
/// the labels here are the shipped visible text and the detail has nowhere to
|
| 132 |
|
- |
/// go. Whether tokens should carry a hint is a vocabulary question and is not
|
| 133 |
|
- |
/// answered by this type quietly dropping it.
|
|
128 |
+ |
/// The shipped badges put it in a `title`: the block depth behind "Blocked",
|
|
129 |
+ |
/// the freed count's wording behind "Unblocks N", the repair instruction behind
|
|
130 |
+ |
/// "Cycle". [`Tag`] carried a label and a tone and no hint until quasicoherent
|
|
131 |
+ |
/// `436bc223`, so the described cards said the short thing and dropped the long
|
|
132 |
+ |
/// one. They carry both now, through [`Tag::hinted`].
|
|
133 |
+ |
///
|
|
134 |
+ |
/// A renderer may still drop a hint -- quasi-tui does, having nowhere to put
|
|
135 |
+ |
/// one -- so nothing here may live only in a hint. Each of the three is a
|
|
136 |
+ |
/// longer form of a label that is already on screen, which is what makes that
|
|
137 |
+ |
/// safe: the badge alone is the fact, and the hint is the same fact said at
|
|
138 |
+ |
/// length.
|
| 134 |
139 |
|
#[derive(Clone, Copy)]
|
| 135 |
140 |
|
pub(crate) struct Availability {
|
| 136 |
141 |
|
/// Something unfinished is in this task's way.
|
| 137 |
142 |
|
blocked: bool,
|
|
143 |
+ |
/// How many sequential steps stand between this task and being startable.
|
|
144 |
+ |
///
|
|
145 |
+ |
/// The longest chain, not the shortest, because a task waits for every
|
|
146 |
+ |
/// blocker it has. Read for the "Blocked" hint and nothing else, which is
|
|
147 |
+ |
/// why it is not itself a badge: a number on a card competes with the
|
|
148 |
+ |
/// label, and the label is what a reader scans for.
|
|
149 |
+ |
depth: u32,
|
| 138 |
150 |
|
/// It sits on a cycle, so it can never open.
|
| 139 |
151 |
|
in_cycle: bool,
|
| 140 |
152 |
|
/// How many tasks finishing this one would free.
|
| 146 |
158 |
|
pub(crate) fn of(task: &Task) -> Self {
|
| 147 |
159 |
|
Self {
|
| 148 |
160 |
|
blocked: task.is_blocked(),
|
|
161 |
+ |
depth: task.graph.block_depth,
|
| 149 |
162 |
|
in_cycle: task.graph.in_cycle,
|
| 150 |
163 |
|
unblocks: task.graph.unblocks_count,
|
| 151 |
164 |
|
}
|
| 159 |
172 |
|
pub(crate) fn reported(task: &crate::commands::TaskResponse) -> Self {
|
| 160 |
173 |
|
Self {
|
| 161 |
174 |
|
blocked: task.is_blocked,
|
|
175 |
+ |
depth: task.block_depth,
|
| 162 |
176 |
|
in_cycle: task.in_cycle,
|
| 163 |
177 |
|
unblocks: task.unblocks_count,
|
| 164 |
178 |
|
}
|
| 172 |
186 |
|
/// nothing downstream is the ordinary case and carries nothing at all.
|
| 173 |
187 |
|
pub(crate) fn marker(self) -> Option<Tag> {
|
| 174 |
188 |
|
if self.in_cycle {
|
| 175 |
|
- |
return Some(Tag::badge("Cycle").tone(Tone::Danger));
|
|
189 |
+ |
return Some(
|
|
190 |
+ |
Tag::badge("Cycle")
|
|
191 |
+ |
.tone(Tone::Danger)
|
|
192 |
+ |
.hinted("On a dependency cycle, so it can never open. Remove an edge."),
|
|
193 |
+ |
);
|
| 176 |
194 |
|
}
|
| 177 |
195 |
|
if self.blocked {
|
| 178 |
|
- |
return Some(Tag::badge("Blocked").tone(Tone::Warning));
|
|
196 |
+ |
return Some(
|
|
197 |
+ |
Tag::badge("Blocked")
|
|
198 |
+ |
.tone(Tone::Warning)
|
|
199 |
+ |
.hinted(steps_away(self.depth)),
|
|
200 |
+ |
);
|
| 179 |
201 |
|
}
|
| 180 |
202 |
|
self.frees_marker()
|
| 181 |
203 |
|
}
|
| 188 |
210 |
|
/// names the blocker instead. What the gate cannot say is which task is
|
| 189 |
211 |
|
/// worth scheduling first, which is what this says.
|
| 190 |
212 |
|
pub(crate) fn frees_marker(self) -> Option<Tag> {
|
| 191 |
|
- |
(!self.blocked && !self.in_cycle && self.unblocks > 0)
|
| 192 |
|
- |
.then(|| Tag::badge(format!("Unblocks {}", self.unblocks)).tone(Tone::Info))
|
|
213 |
+ |
(!self.blocked && !self.in_cycle && self.unblocks > 0).then(|| {
|
|
214 |
+ |
Tag::badge(format!("Unblocks {}", self.unblocks))
|
|
215 |
+ |
.tone(Tone::Info)
|
|
216 |
+ |
.hinted(format!(
|
|
217 |
+ |
"Finishing this frees {}.",
|
|
218 |
+ |
match self.unblocks {
|
|
219 |
+ |
1 => "1 other task".to_owned(),
|
|
220 |
+ |
many => format!("{many} other tasks"),
|
|
221 |
+ |
}
|
|
222 |
+ |
))
|
|
223 |
+ |
})
|
|
224 |
+ |
}
|
|
225 |
+ |
}
|
|
226 |
+ |
|
|
227 |
+ |
/// How far a blocked task is from being startable, in words.
|
|
228 |
+ |
///
|
|
229 |
+ |
/// The depth is the longest chain ahead of the task, so "1 step away" means one
|
|
230 |
+ |
/// completion and nothing else stands in the way. A depth of zero cannot reach
|
|
231 |
+ |
/// here -- `is_blocked` is `block_depth > 0` -- and is said rather than
|
|
232 |
+ |
/// asserted, because the two facts are cached columns that a merge could in
|
|
233 |
+ |
/// principle disagree about, and a badge is not the place to panic.
|
|
234 |
+ |
fn steps_away(depth: u32) -> String {
|
|
235 |
+ |
match depth {
|
|
236 |
+ |
0 => "Waiting on something unfinished.".to_owned(),
|
|
237 |
+ |
1 => "1 step away: one task has to finish first.".to_owned(),
|
|
238 |
+ |
many => format!("{many} steps away, counting the longest chain of blockers."),
|
| 193 |
239 |
|
}
|
| 194 |
240 |
|
}
|
| 195 |
241 |
|
|