Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ for the up to date details!
- Parsing the output generated from `makeHive` now supports reading the schema version in semver
format alongside the previous integer-based system. Currently, it still reports `1` to maintain
backwards compatibility with v1.3.0.
- A list of nodes that are currently deploying, failed, or waiting on a specific
step are displayed below the status bar.

### Changed

Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/hive/steps/activate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub struct SwitchToConfiguration {

impl Display for SwitchToConfiguration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "switch-to-configuration")
write!(f, "Run switch-to-configuration")
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/hive/steps/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub struct Build {

impl Display for Build {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Build the node")
write!(f, "Build")
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/hive/steps/evaluate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub struct Evaluate {

impl Display for Evaluate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Evaluate the node")
write!(f, "Evaluate")
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/hive/steps/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ impl Display for Keys {

impl Display for PushKeyAgent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Push the key agent")
write!(f, "Push key agent")
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/hive/steps/ping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub struct Ping {

impl Display for Ping {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Ping node")
write!(f, "Ping")
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/core/src/hive/steps/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ pub struct PushBuildOutput {

impl Display for PushEvaluatedOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Push the evaluated output")
write!(f, "Push evaluated output")
}
}

impl Display for PushBuildOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Push the build output")
write!(f, "Push build output")
}
}

Expand Down
111 changes: 94 additions & 17 deletions crates/core/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use std::{
sync::OnceLock,
time::{Duration, Instant},
};
use termion::{clear, cursor};
use strip_ansi_escapes::strip_str;
use termion::{clear, cursor, terminal_size};
use tokio::sync::{
mpsc::{self, UnboundedReceiver},
oneshot,
Expand All @@ -18,13 +19,15 @@ use crate::hive::node::Name;

use std::collections::HashMap;

#[derive(Default)]
// Statuses are ordered deliberately such that failed nodes are at the top of
// the list and Succeeded nodes are at the bottom / never shown.
#[derive(Default, PartialEq, PartialOrd, Ord, Eq)]
pub enum NodeStatus {
Failed,
Running(String),
#[default]
Pending,
Running(String),
Succeeded,
Failed,
}

pub enum UiMessage {
Expand All @@ -46,19 +49,23 @@ pub enum UiMessage {
}

pub struct Status {
statuses: HashMap<String, NodeStatus>,
statuses: HashMap<Name, NodeStatus>,
began: Instant,
show_progress: bool,
previous_number_lines: usize,
}

pub static UI_SENDER: OnceLock<mpsc::UnboundedSender<UiMessage>> = OnceLock::new();
const MAX_NODE_NAME_LENGTH: usize = 20;
const FALLBACK_TERMINAL_ROWS: usize = 24;

impl Status {
fn new() -> Self {
Self {
statuses: HashMap::default(),
began: Instant::now(),
show_progress: false,
previous_number_lines: 0,
}
}

Expand Down Expand Up @@ -119,6 +126,70 @@ impl Status {

let _ = write!(&mut msg, " {}s", self.began.elapsed().as_secs());

let mut entries: Vec<(String, &NodeStatus)> = self
.statuses
.iter()
.filter_map(|(name, status)| {
if matches!(status, NodeStatus::Succeeded) {
return None;
}

let truncated = if name.0.len() <= MAX_NODE_NAME_LENGTH {
name.0.to_string()
} else {
format!(
"{}...",
name.0
.chars()
.take(MAX_NODE_NAME_LENGTH)
.collect::<String>()
)
};

Some((truncated, status))
})
.collect();

let max_name_len = entries.iter().map(|(t, _)| t.len()).max().unwrap_or(0);

// sort by status priority then sort by name
entries.sort_by(|(na, sa), (nb, sb)| sa.cmp(sb).then_with(|| na.cmp(nb)));

// cap the displayed node lines to half the terminal height.
// this keeps the status bar from overflowing.
let rows = terminal_size().map_or(FALLBACK_TERMINAL_ROWS, |(_, r)| r as usize);
let cap = rows.saturating_sub(1).max(1) / 2;

let mut shown = 0;
for (truncated, status) in &entries {
if shown >= cap {
break;
}

let line = format!(
"\n {}{} {}",
truncated.bold(),
" ".repeat(max_name_len.saturating_sub(truncated.len())),
match status {
NodeStatus::Pending => "Waiting".dimmed().to_string(),
NodeStatus::Running(task) => {
format!("Running {}", task.blue())
.on_default_color()
.to_string()
}
NodeStatus::Succeeded => unreachable!("filtered above"),
NodeStatus::Failed => "Failed".red().to_string(),
}
);
let _ = write!(&mut msg, "{line}");
shown += 1;
}

let hidden = entries.len().saturating_sub(shown);
if hidden > 0 {
let _ = write!(&mut msg, "\n ... and {hidden} more");
}

msg
}

Expand All @@ -127,10 +198,12 @@ impl Status {
return;
}

let _ = write!(writer, "{}", cursor::Save);
// let _ = write!(writer, "{}", cursor::Down(1));
let _ = write!(writer, "{}", cursor::Left(999));
let _ = write!(writer, "{}", clear::CurrentLine);
for _ in 0..self.previous_number_lines {
let _ = write!(writer, "\r{}{}", clear::CurrentLine, cursor::Up(1));
}

let _ = write!(writer, "\r{}", clear::CurrentLine);
let _ = writer.flush();
}

/// used when there is an interactive prompt
Expand All @@ -139,16 +212,20 @@ impl Status {
return;
}

let _ = write!(writer, "{}", cursor::Save);
let _ = write!(writer, "{}", cursor::Left(999));
let _ = write!(writer, "{}", clear::CurrentLine);
let _ = writer.flush();
self.clear(writer);
}

pub fn write_status<T: std::io::Write>(&mut self, writer: &mut T) {
if self.show_progress {
let _ = write!(writer, "{}", self.get_msg());
if !self.show_progress {
return;
}

let msg = self.get_msg();

let _ = write!(writer, "{msg}");
let _ = writer.flush();

self.previous_number_lines = msg.lines().count().saturating_sub(1);
Comment on lines 218 to +228

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first
ast-grep outline crates/core/src/status.rs --view expanded

# Show the relevant section with line numbers
sed -n '1,320p' crates/core/src/status.rs | cat -n

# Search for cursor movement / clearing logic and terminal width usage
rg -n "cursor::Up|previous_number_lines|terminal width|width|wrap|truncate|clear" crates/core/src/status.rs crates/core/src -S

# Lightweight behavioral probe: inspect whether rendered status text is width-bounded
python3 - <<'PY'
from pathlib import Path
p = Path("crates/core/src/status.rs")
text = p.read_text()

# Print the specific functions around write_status and clear if present
for name in ["write_status", "clear", "get_msg"]:
    idx = text.find(f"fn {name}")
    if idx != -1:
        start = max(0, text.rfind("\n", 0, idx-1)-1)
        end = min(len(text), text.find("\n    pub fn ", idx+1) if text.find("\n    pub fn ", idx+1) != -1 else len(text))
        print(f"\n--- {name} ---")
        print(text[idx:end])
PY

Repository: forallsys/wire

Length of output: 28968


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for any width-aware cursor clearing or wrapping logic elsewhere
rg -n "terminal_size\(|cursor::Up\(|clear::CurrentLine|strip_str\(|wrap|truncate|width" crates/core/src -S

# Estimate the maximum visible length of the summary line and a typical status line from the code.
python3 - <<'PY'
from pathlib import Path
import re

text = Path("crates/core/src/status.rs").read_text()

# Extract the get_msg function body roughly
m = re.search(r'pub fn get_msg\(&self\) -> String \{([\s\S]*?)^\s*\}\s*$', text, re.M)
body = m.group(1) if m else text

# Visible summary skeleton examples based on the code
examples = [
    "[0 / 0] 0s",
    "[123 / 456] (12 Failed, 34 Deploying) 999s",
    "[123 / 456] (12 Failed) 999s",
    "[123 / 456] (34 Deploying) 999s",
]
for s in examples:
    print(len(s), s)
PY

Repository: forallsys/wire

Length of output: 6310


Track rendered rows, not msg.lines()
previous_number_lines only counts logical \ns, but clear() moves up one physical row per count. Any wrapped status line will leave stale rows behind on narrow terminals. Count rendered rows with the terminal width, or keep each status line to a single visual row.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/core/src/status.rs` around lines 218 - 228, `write_status` is tracking
logical newline count instead of the number of rendered terminal rows, which
breaks `clear()` on wrapped status text. Update `Status::write_status` so
`previous_number_lines` reflects physical rows based on terminal width (or
ensure the status message always fits on one visual row), and keep the clearing
logic in sync with that row count.

}
}

Expand Down Expand Up @@ -177,11 +254,11 @@ pub async fn status_tick_worker(mut rx: UnboundedReceiver<UiMessage>, show_progr
status.statuses.extend(
names
.iter()
.map(|name| (name.0.to_string(), NodeStatus::Pending)),
.map(|name| (name.clone(), NodeStatus::Pending)),
);
},
UiMessage::SetStatus(name, value) => {
status.statuses.insert(name.0.to_string(), value);
status.statuses.insert(name.clone(), value);
},
UiMessage::Takeover(tx) => {
taken_over = true;
Expand Down
Loading