Skip to content
Draft
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 @@ -24,6 +24,7 @@ User-visible changes to `voltage`, newest first. Each pull request that changes
- A payment that settles on a terminal closes with `⚡ Payment ID completed` instead of a check mark, printed after the payment's record so it stays on screen.
- `voltage payments list --watch` follows payments until Ctrl-C: a live table that marks new and changed payments on a terminal, or one `snapshot`, `new`, or `updated` event per payment when piped or with `--json`. `--interval` sets the refresh (default 5 seconds).
- `voltage` with no command, on a terminal, greets you with the wordmark, who you are signed in as, your saved scope, and commands to try; off a terminal it is still a usage error. An empty wallet, payment, or webhook list, and a new profile, end with a `›` line naming the next command.
- The hidden API-key prompt of `voltage auth import-key` is shown on stderr, accepts Ctrl-U to clear what was typed, and restores the terminal after Ctrl-C on every platform. Ctrl-C there exits 130 with `Interrupted before any resource change was submitted`, as it did on Unix.

## 0.2.0 - 2026-10-22

Expand Down
22 changes: 0 additions & 22 deletions Cargo.lock

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

9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ rustls = { version = "=0.23.46", default-features = false, features = ["ring", "
uuid = { version = "=1.28.0", features = ["v4", "serde"] }
sha2 = "=0.11.1"
hex = "=0.4.3"
rpassword = "=7.5.4"
webbrowser = "=1.2.4"
futures-util = "=0.3.34"
url = "=2.5.8"
Expand All @@ -48,9 +47,11 @@ 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"] }
# The terminal backend: width, raw mode for hidden prompts (it saves and restores the
# terminal's modes), key events, and the screen and cursor commands. Without its `windows`
# feature, crossterm refuses to compile on Windows. `events` and `bracketed-paste` are
# already on through inquire.
crossterm = { version = "=0.29.0", default-features = false, features = ["windows", "events", "bracketed-paste"] }
# Pickers for a missing organization, environment, or wallet. Its default filter matches a
# substring of a name or ID. It draws through crossterm, so the CLI keeps one terminal backend.
inquire = { version = "=0.9.4", default-features = false, features = ["crossterm"] }
Expand Down
4 changes: 0 additions & 4 deletions docs/quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,6 @@ an existing simpler pattern cannot satisfy it.
- `check_owner` in `config.rs` calls `libc::geteuid` in an `unsafe` block
because std has no safe effective-UID accessor; the block is one call with a
`SAFETY:` comment.
- `TerminalModes` in `terminal.rs` calls `libc::tcgetattr` and `libc::tcsetattr`
in `unsafe` blocks because std cannot save or restore terminal attributes. It
restores echo when Ctrl-C cancels a hidden prompt whose read cannot return;
each block is one call with a `SAFETY:` comment.
- `config`, `registry`, and `secret` are public modules. The integration
suite seeds saved credentials through the real persistence boundary and
walks the operation contract to exercise every route; no other item is
Expand Down
25 changes: 22 additions & 3 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ use crate::{
terminal::{self, scrub_line},
};
use clap::ValueEnum;
use crossterm::{
cursor::{Hide, MoveTo, Show},
terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use serde::Serialize;
use serde_json::{Value, json};
use std::{
Expand Down Expand Up @@ -347,7 +351,12 @@ impl Output {
frame.push_str(&format!(" … {more} more, below the terminal's height\n"));
}
// Home and clear are screen control, not color, so they skip the color stream.
finish_stdout(std::io::stdout().lock(), b"\x1b[H\x1b[2J")?;
let home = format!(
"{}{}",
terminal::ansi(MoveTo(0, 0)),
terminal::ansi(Clear(ClearType::All))
);
finish_stdout(std::io::stdout().lock(), home.as_bytes())?;
write_stdout_styled(frame.as_bytes(), self.color)
}

Expand Down Expand Up @@ -404,14 +413,24 @@ pub struct LiveScreen;

impl LiveScreen {
pub fn enter() -> Self {
let _ = finish_stdout(std::io::stdout().lock(), b"\x1b[?1049h\x1b[?25l");
let enter = format!(
"{}{}",
terminal::ansi(EnterAlternateScreen),
terminal::ansi(Hide)
);
let _ = finish_stdout(std::io::stdout().lock(), enter.as_bytes());
Self
}
}

impl Drop for LiveScreen {
fn drop(&mut self) {
let _ = finish_stdout(std::io::stdout().lock(), b"\x1b[?25h\x1b[?1049l");
let leave = format!(
"{}{}",
terminal::ansi(Show),
terminal::ansi(LeaveAlternateScreen)
);
let _ = finish_stdout(std::io::stdout().lock(), leave.as_bytes());
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/progress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{
style::{Glyphs, Role, paint},
terminal,
};
use crossterm::terminal::{Clear, ClearType};
use std::{
io::Write,
sync::{Arc, Mutex, PoisonError},
Expand All @@ -16,6 +17,11 @@ const FRAME: Duration = Duration::from_millis(100);
/// A quick wait shows no time; one the caller times shows it from the start.
const SHOW_WAITED_AFTER: Duration = Duration::from_secs(2);

/// A carriage return to the line's start, then the line cleared.
fn clear_line() -> String {
format!("\r{}", terminal::ansi(Clear(ClearType::CurrentLine)))
}

/// How the status line is drawn on this stderr.
#[derive(Clone, Copy, Debug)]
pub struct Look {
Expand Down Expand Up @@ -130,13 +136,13 @@ impl StatusLine {
.expires
.map(|expires| expires.saturating_duration_since(Instant::now()));
let text = self.text(frame, self.since.elapsed(), left, self.width);
(self.write)(format!("\r\x1b[2K{text}").as_bytes());
(self.write)(format!("{}{text}", clear_line()).as_bytes());
}

fn clear(&self) {
let mut cleared = self.cleared.lock().unwrap_or_else(PoisonError::into_inner);
*cleared = true;
(self.write)(b"\r\x1b[2K");
(self.write)(clear_line().as_bytes());
}

/// The line for one frame, cut to fit `width` so clearing never leaves a wrapped remnant.
Expand Down
Loading
Loading