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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ User-visible changes to `voltage`, newest first. Each pull request that changes
- An animated spinner on interactive stderr, with the time waited after two seconds; `VOLTAGE_SPINNER=0` keeps it still. A payment wait on a terminal reads as a timeline: one line per status change and a closing `✓` or `✗` line.
- `voltage login` on a terminal shows the verification URL as a clickable link, the code in bold, a countdown to the code's expiry, and a closing `✓ Logged in as EMAIL` line.
- A change described without `--execute` starts, on a terminal, with what it would do and the values it would use (`! Not sent: this would send a payment`) and ends with how to send it, instead of an `error:` line. Exit code 6 is unchanged.
- Human output shows amounts with grouped digits, every BTC amount in sats (`150,000 sats`, `1.5 sats`) and cents as dollars (`$1,234.56`), and table timestamps relative to now (`3 minutes ago`); single resources show the UTC time with the relative time beside it. With color on, headers and field names are bold and known statuses are colored. JSON is unchanged.

## 0.2.0 - 2026-10-22

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ keyring = { version = "=4.2.0", default-features = false, features = ["v1"] }
# Human tables: display-width layout and truncation. Default features stay off for security:
# without `tty` and `custom_styling`, cells cannot carry escape sequences past scrubbing.
comfy-table = { version = "=8.0.1", default-features = false }
# Display widths, to find a table column after layout and color it; comfy-table already
# depends on it, so it adds no crate.
unicode-width = "=0.2.2"
# Reads the terminal width on every platform. Without its `windows` feature, crossterm
# refuses to compile on Windows.
crossterm = { version = "=0.29.0", default-features = false, features = ["windows"] }
Expand Down
5 changes: 3 additions & 2 deletions docs/reference/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ On a terminal, results are readable tables; when piped, they are JSON. `--json`
## Tables

- Tables show up to seven of the fields a list's items share (ID, name, status, amount, and similar) with aligned columns, a result count, and the next cursor.
- Amounts are shown in their base units (`msats` or `cents`).
- Amounts are shown with grouped digits: every BTC amount in sats, with up to three decimals for msats (`150,000 sats`, `1.5 sats`), so a column never mixes units, and cents as dollars (`$1,234.56`). JSON keeps the API's integer and base unit.
- A table wider than the terminal (its width, or `COLUMNS`) hides trailing columns and then shortens text cells with `…`, naming what it hid. IDs, timestamps, and amounts are never shortened.
- Timestamps are shown in UTC to the second, such as `2024-02-27 01:58:51 UTC`; JSON keeps the exact value.
- Table timestamps are relative to now, such as `3 minutes ago` or `in 2 days`; anything within a minute either way reads `just now`. A single resource shows the UTC time to the second with the relative time beside it, such as `2024-02-27 01:58:51 UTC (7 months ago)`. JSON keeps the exact value.
- With color on, table headers and field names are bold, a known status is colored by meaning (`completed` green, `failed` red, `sending` yellow), and so is the outcome line (`accepted`, `dry_run`). Color is added after layout, so it never moves a column; an unknown status stays plain.
- A table starts with the outcome only when it adds something, such as `accepted` or `dry_run`, or when there is no data to show.
- A single resource is shown as aligned `field value` lines, with nested fields joined by dots. `voltage context` shows each setting as `name value (source)`.

Expand Down
35 changes: 24 additions & 11 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ pub struct Output {
format: OutputFormat,
show_secrets: bool,
file: Option<File>,
color: ColorChoice,
}

/// Reserve `--output-file`. It is reserved before any request, so a refusal sent nothing; the
Expand All @@ -212,12 +213,14 @@ impl Output {
format: OutputFormat,
show_secrets: bool,
output_file: Option<&Path>,
color: ColorChoice,
) -> Result<Self> {
let file = output_file.map(reserve_output_file).transpose()?;
Ok(Self {
format,
show_secrets,
file,
color,
})
}

Expand Down Expand Up @@ -247,24 +250,34 @@ impl Output {
if !self.show_secrets {
redact(&mut envelope.data, secrets);
}
let mut bytes = match self.format {
OutputFormat::Table => {
let mut buf = Vec::new();
human::render(&mut buf, &envelope, terminal::width())?;
buf
}
OutputFormat::Json | OutputFormat::Ndjson => serde_json::to_vec(&envelope)?,
};
if self.format != OutputFormat::Table {
bytes.push(b'\n');
if self.format == OutputFormat::Table {
// Styled as for a terminal; the stream strips styles when color is off there.
let view = human::View {
width: terminal::width(),
color: true,
..human::View::plain()
};
let mut buf = Vec::new();
human::render(&mut buf, &envelope, view)?;
return write_stdout_styled(&buf, self.color);
}
let mut bytes = serde_json::to_vec(&envelope)?;
bytes.push(b'\n');
write_stdout(&bytes)
}
}

/// Write rendered output; a closed stdout ends the pipeline successfully.
pub fn write_stdout(bytes: &[u8]) -> Result<()> {
let mut stdout = std::io::stdout().lock();
finish_stdout(std::io::stdout().lock(), bytes)
}

/// `write_stdout` for human output, keeping its styles only when `color` allows them there.
fn write_stdout_styled(bytes: &[u8], color: ColorChoice) -> Result<()> {
finish_stdout(style::stdout(color), bytes)
}

fn finish_stdout(mut stdout: impl Write, bytes: &[u8]) -> Result<()> {
stdout
.write_all(bytes)
.and_then(|()| stdout.flush())
Expand Down
Loading
Loading