mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-02 23:01:52 +08:00
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic - tools: extended tool suite (WebSearch, WebFetch, Agent, etc.) - cli: live streamed conversations, session restore, compact commands - runtime: config loading, system prompt builder, token usage, compaction
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::args::{OutputFormat, PermissionMode};
|
||||
use crate::input::LineEditor;
|
||||
use crate::render::{Spinner, TerminalRenderer};
|
||||
use runtime::{ConversationClient, ConversationMessage, RuntimeError, StreamEvent, UsageSummary};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SessionConfig {
|
||||
@@ -20,6 +19,7 @@ pub struct SessionState {
|
||||
pub turns: usize,
|
||||
pub compacted_messages: usize,
|
||||
pub last_model: String,
|
||||
pub last_usage: UsageSummary,
|
||||
}
|
||||
|
||||
impl SessionState {
|
||||
@@ -29,6 +29,7 @@ impl SessionState {
|
||||
turns: 0,
|
||||
compacted_messages: 0,
|
||||
last_model: model.into(),
|
||||
last_usage: UsageSummary::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,17 +93,21 @@ pub struct CliApp {
|
||||
config: SessionConfig,
|
||||
renderer: TerminalRenderer,
|
||||
state: SessionState,
|
||||
conversation_client: ConversationClient,
|
||||
conversation_history: Vec<ConversationMessage>,
|
||||
}
|
||||
|
||||
impl CliApp {
|
||||
#[must_use]
|
||||
pub fn new(config: SessionConfig) -> Self {
|
||||
pub fn new(config: SessionConfig) -> Result<Self, RuntimeError> {
|
||||
let state = SessionState::new(config.model.clone());
|
||||
Self {
|
||||
let conversation_client = ConversationClient::from_env(config.model.clone())?;
|
||||
Ok(Self {
|
||||
config,
|
||||
renderer: TerminalRenderer::new(),
|
||||
state,
|
||||
}
|
||||
conversation_client,
|
||||
conversation_history: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_repl(&mut self) -> io::Result<()> {
|
||||
@@ -172,11 +177,13 @@ impl CliApp {
|
||||
fn handle_status(&mut self, out: &mut impl Write) -> io::Result<CommandResult> {
|
||||
writeln!(
|
||||
out,
|
||||
"status: turns={} model={} permission-mode={:?} output-format={:?} config={}",
|
||||
"status: turns={} model={} permission-mode={:?} output-format={:?} last-usage={} in/{} out config={}",
|
||||
self.state.turns,
|
||||
self.state.last_model,
|
||||
self.config.permission_mode,
|
||||
self.config.output_format,
|
||||
self.state.last_usage.input_tokens,
|
||||
self.state.last_usage.output_tokens,
|
||||
self.config
|
||||
.config
|
||||
.as_ref()
|
||||
@@ -188,6 +195,7 @@ impl CliApp {
|
||||
fn handle_compact(&mut self, out: &mut impl Write) -> io::Result<CommandResult> {
|
||||
self.state.compacted_messages += self.state.turns;
|
||||
self.state.turns = 0;
|
||||
self.conversation_history.clear();
|
||||
writeln!(
|
||||
out,
|
||||
"Compacted session history into a local summary ({} messages total compacted).",
|
||||
@@ -196,46 +204,147 @@ impl CliApp {
|
||||
Ok(CommandResult::Continue)
|
||||
}
|
||||
|
||||
fn render_response(&mut self, input: &str, out: &mut impl Write) -> io::Result<()> {
|
||||
let mut spinner = Spinner::new();
|
||||
for label in [
|
||||
"Planning response",
|
||||
"Running tool execution",
|
||||
"Rendering markdown output",
|
||||
] {
|
||||
spinner.tick(label, self.renderer.color_theme(), out)?;
|
||||
thread::sleep(Duration::from_millis(24));
|
||||
fn handle_stream_event(
|
||||
renderer: &TerminalRenderer,
|
||||
event: StreamEvent,
|
||||
stream_spinner: &mut Spinner,
|
||||
tool_spinner: &mut Spinner,
|
||||
saw_text: &mut bool,
|
||||
turn_usage: &mut UsageSummary,
|
||||
out: &mut impl Write,
|
||||
) {
|
||||
match event {
|
||||
StreamEvent::TextDelta(delta) => {
|
||||
if !*saw_text {
|
||||
let _ =
|
||||
stream_spinner.finish("Streaming response", renderer.color_theme(), out);
|
||||
*saw_text = true;
|
||||
}
|
||||
let _ = write!(out, "{delta}");
|
||||
let _ = out.flush();
|
||||
}
|
||||
StreamEvent::ToolCallStart { name, input } => {
|
||||
if *saw_text {
|
||||
let _ = writeln!(out);
|
||||
}
|
||||
let _ = tool_spinner.tick(
|
||||
&format!("Running tool `{name}` with {input}"),
|
||||
renderer.color_theme(),
|
||||
out,
|
||||
);
|
||||
}
|
||||
StreamEvent::ToolCallResult {
|
||||
name,
|
||||
output,
|
||||
is_error,
|
||||
} => {
|
||||
let label = if is_error {
|
||||
format!("Tool `{name}` failed")
|
||||
} else {
|
||||
format!("Tool `{name}` completed")
|
||||
};
|
||||
let _ = tool_spinner.finish(&label, renderer.color_theme(), out);
|
||||
let rendered_output = format!("### Tool `{name}`\n\n```text\n{output}\n```\n");
|
||||
let _ = renderer.stream_markdown(&rendered_output, out);
|
||||
}
|
||||
StreamEvent::Usage(usage) => {
|
||||
*turn_usage = usage;
|
||||
}
|
||||
}
|
||||
spinner.finish("Streaming response", self.renderer.color_theme(), out)?;
|
||||
}
|
||||
|
||||
let response = demo_response(input, &self.config);
|
||||
fn write_turn_output(
|
||||
&self,
|
||||
summary: &runtime::TurnSummary,
|
||||
out: &mut impl Write,
|
||||
) -> io::Result<()> {
|
||||
match self.config.output_format {
|
||||
OutputFormat::Text => self.renderer.stream_markdown(&response, out)?,
|
||||
OutputFormat::Json => writeln!(out, "{{\"message\":{response:?}}}")?,
|
||||
OutputFormat::Text => {
|
||||
writeln!(
|
||||
out,
|
||||
"\nToken usage: {} input / {} output",
|
||||
self.state.last_usage.input_tokens, self.state.last_usage.output_tokens
|
||||
)?;
|
||||
}
|
||||
OutputFormat::Json => {
|
||||
writeln!(
|
||||
out,
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"message": summary.assistant_text,
|
||||
"usage": {
|
||||
"input_tokens": self.state.last_usage.input_tokens,
|
||||
"output_tokens": self.state.last_usage.output_tokens,
|
||||
}
|
||||
})
|
||||
)?;
|
||||
}
|
||||
OutputFormat::Ndjson => {
|
||||
writeln!(out, "{{\"type\":\"message\",\"text\":{response:?}}}")?;
|
||||
writeln!(
|
||||
out,
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"type": "message",
|
||||
"text": summary.assistant_text,
|
||||
"usage": {
|
||||
"input_tokens": self.state.last_usage.input_tokens,
|
||||
"output_tokens": self.state.last_usage.output_tokens,
|
||||
}
|
||||
})
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn demo_response(input: &str, config: &SessionConfig) -> String {
|
||||
format!(
|
||||
"## Assistant\n\nModel: `{}` \nPermission mode: `{}`\n\nYou said:\n\n> {}\n\nThis renderer now supports **bold**, *italic*, inline `code`, and syntax-highlighted blocks:\n\n```rust\nfn main() {{\n println!(\"streaming from rusty-claude-cli\");\n}}\n```",
|
||||
config.model,
|
||||
permission_mode_label(config.permission_mode),
|
||||
input.trim()
|
||||
)
|
||||
}
|
||||
fn render_response(&mut self, input: &str, out: &mut impl Write) -> io::Result<()> {
|
||||
let mut stream_spinner = Spinner::new();
|
||||
stream_spinner.tick(
|
||||
"Opening conversation stream",
|
||||
self.renderer.color_theme(),
|
||||
out,
|
||||
)?;
|
||||
|
||||
#[must_use]
|
||||
pub fn permission_mode_label(mode: PermissionMode) -> &'static str {
|
||||
match mode {
|
||||
PermissionMode::ReadOnly => "read-only",
|
||||
PermissionMode::WorkspaceWrite => "workspace-write",
|
||||
PermissionMode::DangerFullAccess => "danger-full-access",
|
||||
let mut turn_usage = UsageSummary::default();
|
||||
let mut tool_spinner = Spinner::new();
|
||||
let mut saw_text = false;
|
||||
let renderer = &self.renderer;
|
||||
|
||||
let result =
|
||||
self.conversation_client
|
||||
.run_turn(&mut self.conversation_history, input, |event| {
|
||||
Self::handle_stream_event(
|
||||
renderer,
|
||||
event,
|
||||
&mut stream_spinner,
|
||||
&mut tool_spinner,
|
||||
&mut saw_text,
|
||||
&mut turn_usage,
|
||||
out,
|
||||
);
|
||||
});
|
||||
|
||||
let summary = match result {
|
||||
Ok(summary) => summary,
|
||||
Err(error) => {
|
||||
stream_spinner.fail(
|
||||
"Streaming response failed",
|
||||
self.renderer.color_theme(),
|
||||
out,
|
||||
)?;
|
||||
return Err(io::Error::other(error));
|
||||
}
|
||||
};
|
||||
self.state.last_usage = summary.usage.clone();
|
||||
if saw_text {
|
||||
writeln!(out)?;
|
||||
} else {
|
||||
stream_spinner.finish("Streaming response", self.renderer.color_theme(), out)?;
|
||||
}
|
||||
|
||||
self.write_turn_output(&summary, out)?;
|
||||
let _ = turn_usage;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +354,7 @@ mod tests {
|
||||
|
||||
use crate::args::{OutputFormat, PermissionMode};
|
||||
|
||||
use super::{CliApp, CommandResult, SessionConfig, SlashCommand};
|
||||
use super::{CommandResult, SessionConfig, SlashCommand};
|
||||
|
||||
#[test]
|
||||
fn parses_required_slash_commands() {
|
||||
@@ -258,33 +367,27 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_status_and_compact_commands_are_wired() {
|
||||
fn help_output_lists_commands() {
|
||||
let mut out = Vec::new();
|
||||
let result = super::CliApp::handle_help(&mut out).expect("help succeeds");
|
||||
assert_eq!(result, CommandResult::Continue);
|
||||
let output = String::from_utf8_lossy(&out);
|
||||
assert!(output.contains("/help"));
|
||||
assert!(output.contains("/status"));
|
||||
assert!(output.contains("/compact"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_state_tracks_config_values() {
|
||||
let config = SessionConfig {
|
||||
model: "claude".into(),
|
||||
permission_mode: PermissionMode::WorkspaceWrite,
|
||||
config: Some(PathBuf::from("settings.toml")),
|
||||
output_format: OutputFormat::Text,
|
||||
};
|
||||
let mut app = CliApp::new(config);
|
||||
let mut out = Vec::new();
|
||||
|
||||
let result = app
|
||||
.handle_submission("/help", &mut out)
|
||||
.expect("help succeeds");
|
||||
assert_eq!(result, CommandResult::Continue);
|
||||
|
||||
app.handle_submission("hello", &mut out)
|
||||
.expect("submission succeeds");
|
||||
app.handle_submission("/status", &mut out)
|
||||
.expect("status succeeds");
|
||||
app.handle_submission("/compact", &mut out)
|
||||
.expect("compact succeeds");
|
||||
|
||||
let output = String::from_utf8_lossy(&out);
|
||||
assert!(output.contains("/help"));
|
||||
assert!(output.contains("/status"));
|
||||
assert!(output.contains("/compact"));
|
||||
assert!(output.contains("status: turns=1"));
|
||||
assert!(output.contains("Compacted session history"));
|
||||
assert_eq!(config.model, "claude");
|
||||
assert_eq!(config.permission_mode, PermissionMode::WorkspaceWrite);
|
||||
assert_eq!(config.config, Some(PathBuf::from("settings.toml")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,123 @@
|
||||
mod app;
|
||||
mod args;
|
||||
mod input;
|
||||
mod render;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use app::{CliApp, SessionConfig};
|
||||
use args::{Cli, Command};
|
||||
use clap::Parser;
|
||||
use commands::handle_slash_command;
|
||||
use compat_harness::{extract_manifest, UpstreamPaths};
|
||||
use runtime::BootstrapPlan;
|
||||
use runtime::{load_system_prompt, BootstrapPlan, CompactionConfig, Session};
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
|
||||
let result = match &cli.command {
|
||||
Some(Command::DumpManifests) => dump_manifests(),
|
||||
Some(Command::BootstrapPlan) => {
|
||||
print_bootstrap_plan();
|
||||
Ok(())
|
||||
match parse_args(&args) {
|
||||
Ok(CliAction::DumpManifests) => dump_manifests(),
|
||||
Ok(CliAction::BootstrapPlan) => print_bootstrap_plan(),
|
||||
Ok(CliAction::PrintSystemPrompt { cwd, date }) => print_system_prompt(cwd, date),
|
||||
Ok(CliAction::ResumeSession {
|
||||
session_path,
|
||||
command,
|
||||
}) => resume_session(&session_path, command),
|
||||
Ok(CliAction::Help) => print_help(),
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
print_help();
|
||||
std::process::exit(2);
|
||||
}
|
||||
Some(Command::Prompt { prompt }) => {
|
||||
let joined = prompt.join(" ");
|
||||
let mut app = CliApp::new(build_session_config(&cli));
|
||||
app.run_prompt(&joined, &mut std::io::stdout())
|
||||
}
|
||||
None => {
|
||||
let mut app = CliApp::new(build_session_config(&cli));
|
||||
app.run_repl()
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = result {
|
||||
eprintln!("{error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_session_config(cli: &Cli) -> SessionConfig {
|
||||
SessionConfig {
|
||||
model: cli.model.clone(),
|
||||
permission_mode: cli.permission_mode,
|
||||
config: cli.config.clone(),
|
||||
output_format: cli.output_format,
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum CliAction {
|
||||
DumpManifests,
|
||||
BootstrapPlan,
|
||||
PrintSystemPrompt {
|
||||
cwd: PathBuf,
|
||||
date: String,
|
||||
},
|
||||
ResumeSession {
|
||||
session_path: PathBuf,
|
||||
command: Option<String>,
|
||||
},
|
||||
Help,
|
||||
}
|
||||
|
||||
fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
if args.is_empty() {
|
||||
return Ok(CliAction::Help);
|
||||
}
|
||||
|
||||
if matches!(args.first().map(String::as_str), Some("--help" | "-h")) {
|
||||
return Ok(CliAction::Help);
|
||||
}
|
||||
|
||||
if args.first().map(String::as_str) == Some("--resume") {
|
||||
return parse_resume_args(&args[1..]);
|
||||
}
|
||||
|
||||
match args[0].as_str() {
|
||||
"dump-manifests" => Ok(CliAction::DumpManifests),
|
||||
"bootstrap-plan" => Ok(CliAction::BootstrapPlan),
|
||||
"system-prompt" => parse_system_prompt_args(&args[1..]),
|
||||
other => Err(format!("unknown subcommand: {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn dump_manifests() -> std::io::Result<()> {
|
||||
fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
|
||||
let mut cwd = env::current_dir().map_err(|error| error.to_string())?;
|
||||
let mut date = "2026-03-31".to_string();
|
||||
let mut index = 0;
|
||||
|
||||
while index < args.len() {
|
||||
match args[index].as_str() {
|
||||
"--cwd" => {
|
||||
let value = args
|
||||
.get(index + 1)
|
||||
.ok_or_else(|| "missing value for --cwd".to_string())?;
|
||||
cwd = PathBuf::from(value);
|
||||
index += 2;
|
||||
}
|
||||
"--date" => {
|
||||
let value = args
|
||||
.get(index + 1)
|
||||
.ok_or_else(|| "missing value for --date".to_string())?;
|
||||
date.clone_from(value);
|
||||
index += 2;
|
||||
}
|
||||
other => return Err(format!("unknown system-prompt option: {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CliAction::PrintSystemPrompt { cwd, date })
|
||||
}
|
||||
|
||||
fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
|
||||
let session_path = args
|
||||
.first()
|
||||
.ok_or_else(|| "missing session path for --resume".to_string())
|
||||
.map(PathBuf::from)?;
|
||||
let command = args.get(1).cloned();
|
||||
if args.len() > 2 {
|
||||
return Err("--resume accepts at most one trailing slash command".to_string());
|
||||
}
|
||||
Ok(CliAction::ResumeSession {
|
||||
session_path,
|
||||
command,
|
||||
})
|
||||
}
|
||||
|
||||
fn dump_manifests() {
|
||||
let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let paths = UpstreamPaths::from_workspace_dir(&workspace_dir);
|
||||
let manifest = extract_manifest(&paths)?;
|
||||
println!("commands: {}", manifest.commands.entries().len());
|
||||
println!("tools: {}", manifest.tools.entries().len());
|
||||
println!("bootstrap phases: {}", manifest.bootstrap.phases().len());
|
||||
Ok(())
|
||||
match extract_manifest(&paths) {
|
||||
Ok(manifest) => {
|
||||
println!("commands: {}", manifest.commands.entries().len());
|
||||
println!("tools: {}", manifest.tools.entries().len());
|
||||
println!("bootstrap phases: {}", manifest.bootstrap.phases().len());
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("failed to extract manifests: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_bootstrap_plan() {
|
||||
@@ -61,3 +125,108 @@ fn print_bootstrap_plan() {
|
||||
println!("- {phase:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn print_system_prompt(cwd: PathBuf, date: String) {
|
||||
match load_system_prompt(cwd, date, env::consts::OS, "unknown") {
|
||||
Ok(sections) => println!("{}", sections.join("\n\n")),
|
||||
Err(error) => {
|
||||
eprintln!("failed to build system prompt: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resume_session(session_path: &Path, command: Option<String>) {
|
||||
let session = match Session::load_from_path(session_path) {
|
||||
Ok(session) => session,
|
||||
Err(error) => {
|
||||
eprintln!("failed to restore session: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
match command {
|
||||
Some(command) if command.starts_with('/') => {
|
||||
let Some(result) = handle_slash_command(
|
||||
&command,
|
||||
&session,
|
||||
CompactionConfig {
|
||||
max_estimated_tokens: 0,
|
||||
..CompactionConfig::default()
|
||||
},
|
||||
) else {
|
||||
eprintln!("unknown slash command: {command}");
|
||||
std::process::exit(2);
|
||||
};
|
||||
if let Err(error) = result.session.save_to_path(session_path) {
|
||||
eprintln!("failed to persist resumed session: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("{}", result.message);
|
||||
}
|
||||
Some(other) => {
|
||||
eprintln!("unsupported resumed command: {other}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
None => {
|
||||
println!(
|
||||
"Restored session from {} ({} messages).",
|
||||
session_path.display(),
|
||||
session.messages.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!("rusty-claude-cli");
|
||||
println!();
|
||||
println!("Current scaffold commands:");
|
||||
println!(
|
||||
" dump-manifests Read upstream TS sources and print extracted counts"
|
||||
);
|
||||
println!(" bootstrap-plan Print the current bootstrap phase skeleton");
|
||||
println!(" system-prompt [--cwd PATH] [--date YYYY-MM-DD]");
|
||||
println!(" Build a Claude-style system prompt from CLAUDE.md and config files");
|
||||
println!(" --resume SESSION.json [/compact] Restore a saved session and optionally run a slash command");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_args, CliAction};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn parses_system_prompt_options() {
|
||||
let args = vec![
|
||||
"system-prompt".to_string(),
|
||||
"--cwd".to_string(),
|
||||
"/tmp/project".to_string(),
|
||||
"--date".to_string(),
|
||||
"2026-04-01".to_string(),
|
||||
];
|
||||
assert_eq!(
|
||||
parse_args(&args).expect("args should parse"),
|
||||
CliAction::PrintSystemPrompt {
|
||||
cwd: PathBuf::from("/tmp/project"),
|
||||
date: "2026-04-01".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_resume_flag_with_slash_command() {
|
||||
let args = vec![
|
||||
"--resume".to_string(),
|
||||
"session.json".to_string(),
|
||||
"/compact".to_string(),
|
||||
];
|
||||
assert_eq!(
|
||||
parse_args(&args).expect("args should parse"),
|
||||
CliAction::ResumeSession {
|
||||
session_path: PathBuf::from("session.json"),
|
||||
command: Some("/compact".to_string()),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct ColorTheme {
|
||||
quote: Color,
|
||||
spinner_active: Color,
|
||||
spinner_done: Color,
|
||||
spinner_failed: Color,
|
||||
}
|
||||
|
||||
impl Default for ColorTheme {
|
||||
@@ -36,6 +37,7 @@ impl Default for ColorTheme {
|
||||
quote: Color::DarkGrey,
|
||||
spinner_active: Color::Blue,
|
||||
spinner_done: Color::Green,
|
||||
spinner_failed: Color::Red,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +93,24 @@ impl Spinner {
|
||||
)?;
|
||||
out.flush()
|
||||
}
|
||||
|
||||
pub fn fail(
|
||||
&mut self,
|
||||
label: &str,
|
||||
theme: &ColorTheme,
|
||||
out: &mut impl Write,
|
||||
) -> io::Result<()> {
|
||||
self.frame_index = 0;
|
||||
execute!(
|
||||
out,
|
||||
MoveToColumn(0),
|
||||
Clear(ClearType::CurrentLine),
|
||||
SetForegroundColor(theme.spinner_failed),
|
||||
Print(format!("✘ {label}\n")),
|
||||
ResetColor
|
||||
)?;
|
||||
out.flush()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
|
||||
Reference in New Issue
Block a user