summaryrefslogtreecommitdiffstats
path: root/src/main.rs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-11-04 11:30:10 +0100
committerDaniel Baumann <daniel@debian.org>2024-11-20 07:38:21 +0100
commit34f503aa3bfba930fd7978a0071786884d73749f (patch)
tree7e3c8e2506fdd93e29958d9f8cb36fbed4a5af7d /src/main.rs
parentInitial commit. (diff)
downloadforgejo-cli-34f503aa3bfba930fd7978a0071786884d73749f.tar.xz
forgejo-cli-34f503aa3bfba930fd7978a0071786884d73749f.zip
Adding upstream version 0.1.1.upstream/0.1.1
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs800
1 files changed, 800 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..c26081f
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,800 @@
+use std::io::IsTerminal;
+
+use clap::{Parser, Subcommand};
+use eyre::{eyre, Context, OptionExt};
+use tokio::io::AsyncWriteExt;
+
+mod keys;
+use keys::*;
+
+mod auth;
+mod issues;
+mod prs;
+mod release;
+mod repo;
+mod user;
+mod wiki;
+
+#[derive(Parser, Debug)]
+pub struct App {
+ #[clap(long, short = 'H')]
+ host: Option<String>,
+ #[clap(long)]
+ style: Option<Style>,
+ #[clap(subcommand)]
+ command: Command,
+}
+
+#[derive(Subcommand, Clone, Debug)]
+pub enum Command {
+ #[clap(subcommand)]
+ Repo(repo::RepoCommand),
+ Issue(issues::IssueCommand),
+ Pr(prs::PrCommand),
+ Wiki(wiki::WikiCommand),
+ #[command(name = "whoami")]
+ WhoAmI {
+ #[clap(long, short)]
+ remote: Option<String>,
+ },
+ #[clap(subcommand)]
+ Auth(auth::AuthCommand),
+ Release(release::ReleaseCommand),
+ User(user::UserCommand),
+ Version {
+ /// Checks for updates
+ #[clap(long)]
+ #[cfg(feature = "update-check")]
+ check: bool,
+ },
+}
+
+#[tokio::main]
+async fn main() -> eyre::Result<()> {
+ let args = App::parse();
+
+ let _ = SPECIAL_RENDER.set(SpecialRender::new(args.style.unwrap_or_default()));
+
+ let mut keys = KeyInfo::load().await?;
+
+ let host_name = args.host.as_deref();
+ // let remote = repo::RepoInfo::get_current(host_name, remote_name)?;
+ match args.command {
+ Command::Repo(subcommand) => subcommand.run(&mut keys, host_name).await?,
+ Command::Issue(subcommand) => subcommand.run(&mut keys, host_name).await?,
+ Command::Pr(subcommand) => subcommand.run(&mut keys, host_name).await?,
+ Command::Wiki(subcommand) => subcommand.run(&mut keys, host_name).await?,
+ Command::WhoAmI { remote } => {
+ let url = repo::RepoInfo::get_current(host_name, None, remote.as_deref())
+ .wrap_err("could not find host, try specifying with --host")?
+ .host_url()
+ .clone();
+ let name = keys.get_login(&url)?.username();
+ let host = url
+ .host_str()
+ .ok_or_eyre("instance url does not have host")?;
+ if url.path() == "/" || url.path().is_empty() {
+ println!("currently signed in to {name}@{host}");
+ } else {
+ println!("currently signed in to {name}@{host}{}", url.path());
+ }
+ }
+ Command::Auth(subcommand) => subcommand.run(&mut keys, host_name).await?,
+ Command::Release(subcommand) => subcommand.run(&mut keys, host_name).await?,
+ Command::User(subcommand) => subcommand.run(&mut keys, host_name).await?,
+ Command::Version {
+ #[cfg(feature = "update-check")]
+ check,
+ } => {
+ println!("{}", env!("CARGO_PKG_VERSION"));
+ #[cfg(feature = "update-check")]
+ update_msg(check).await?;
+ }
+ }
+
+ keys.save().await?;
+ Ok(())
+}
+
+#[cfg(feature = "update-check")]
+async fn update_msg(check: bool) -> eyre::Result<()> {
+ use std::cmp::Ordering;
+
+ if check {
+ let url = url::Url::parse("https://codeberg.org/")?;
+ let api = forgejo_api::Forgejo::new(forgejo_api::Auth::None, url)?;
+
+ let latest = api
+ .repo_get_latest_release("Cyborus", "forgejo-cli")
+ .await?;
+ let latest_tag = latest
+ .tag_name
+ .ok_or_eyre("latest release does not have name")?;
+ let latest_ver = latest_tag
+ .strip_prefix("v")
+ .unwrap_or(&latest_tag)
+ .parse::<semver::Version>()?;
+
+ let current_ver = env!("CARGO_PKG_VERSION").parse::<semver::Version>()?;
+
+ match current_ver.cmp(&latest_ver) {
+ Ordering::Less => {
+ let latest_url = latest
+ .html_url
+ .ok_or_eyre("latest release does not have url")?;
+ println!("New version available: {latest_ver}");
+ println!("Get it at {}", latest_url);
+ }
+ Ordering::Equal => {
+ println!("Up to date!");
+ }
+ Ordering::Greater => {
+ println!("You are ahead of the latest published version");
+ }
+ }
+ } else {
+ println!("Check for a new version with `fj version --check`");
+ }
+ Ok(())
+}
+
+async fn readline(msg: &str) -> eyre::Result<String> {
+ use std::io::Write;
+ print!("{msg}");
+ std::io::stdout().flush()?;
+ tokio::task::spawn_blocking(|| {
+ let mut input = String::new();
+ std::io::stdin().read_line(&mut input)?;
+ Ok(input)
+ })
+ .await?
+}
+
+async fn editor(contents: &mut String, ext: Option<&str>) -> eyre::Result<()> {
+ let editor = std::path::PathBuf::from(
+ std::env::var_os("EDITOR").ok_or_else(|| eyre!("unable to locate editor"))?,
+ );
+
+ let (mut file, path) = tempfile(ext).await?;
+ file.write_all(contents.as_bytes()).await?;
+ drop(file);
+
+ // Closure acting as a try/catch block so that the temp file is deleted even
+ // on errors
+ let res = (|| async {
+ eprint!("waiting on editor\r");
+ let flags = get_editor_flags(&editor);
+ let status = tokio::process::Command::new(editor)
+ .args(flags)
+ .arg(&path)
+ .status()
+ .await?;
+ if !status.success() {
+ eyre::bail!("editor exited unsuccessfully");
+ }
+
+ *contents = tokio::fs::read_to_string(&path).await?;
+ eprint!(" \r");
+
+ Ok(())
+ })()
+ .await;
+
+ tokio::fs::remove_file(path).await?;
+ res?;
+ Ok(())
+}
+
+fn get_editor_flags(editor_path: &std::path::Path) -> &'static [&'static str] {
+ let editor_name = match editor_path.file_stem().and_then(|s| s.to_str()) {
+ Some(name) => name,
+ None => return &[],
+ };
+ if editor_name == "code" {
+ return &["--wait"];
+ }
+ &[]
+}
+
+async fn tempfile(ext: Option<&str>) -> tokio::io::Result<(tokio::fs::File, std::path::PathBuf)> {
+ let filename = uuid::Uuid::new_v4();
+ let mut path = std::env::temp_dir().join(filename.to_string());
+ if let Some(ext) = ext {
+ path.set_extension(ext);
+ }
+ let file = tokio::fs::OpenOptions::new()
+ .create(true)
+ .read(true)
+ .write(true)
+ .open(&path)
+ .await?;
+ Ok((file, path))
+}
+
+use std::sync::OnceLock;
+static SPECIAL_RENDER: OnceLock<SpecialRender> = OnceLock::new();
+
+fn special_render() -> &'static SpecialRender {
+ SPECIAL_RENDER
+ .get()
+ .expect("attempted to get special characters before that was initialized")
+}
+
+#[derive(clap::ValueEnum, Clone, Copy, Debug, Default)]
+enum Style {
+ /// Use special characters, and colors.
+ #[default]
+ Fancy,
+ /// No special characters and no colors. Always used in non-terminal contexts (i.e. pipes)
+ Minimal,
+}
+
+struct SpecialRender {
+ fancy: bool,
+
+ dash: char,
+ bullet: char,
+ body_prefix: char,
+ horiz_rule: char,
+
+ // Uncomment these as needed
+ // red: &'static str,
+ bright_red: &'static str,
+ // green: &'static str,
+ bright_green: &'static str,
+ // blue: &'static str,
+ bright_blue: &'static str,
+ // cyan: &'static str,
+ bright_cyan: &'static str,
+ yellow: &'static str,
+ // bright_yellow: &'static str,
+ // magenta: &'static str,
+ bright_magenta: &'static str,
+ black: &'static str,
+ dark_grey: &'static str,
+ light_grey: &'static str,
+ white: &'static str,
+ no_fg: &'static str,
+ reset: &'static str,
+
+ dark_grey_bg: &'static str,
+ // no_bg: &'static str,
+ hide_cursor: &'static str,
+ show_cursor: &'static str,
+ clear_line: &'static str,
+
+ italic: &'static str,
+ bold: &'static str,
+ strike: &'static str,
+ no_italic_bold: &'static str,
+ no_strike: &'static str,
+}
+
+impl SpecialRender {
+ fn new(display: Style) -> Self {
+ let is_tty = std::io::stdout().is_terminal();
+ match display {
+ _ if !is_tty => Self::minimal(),
+ Style::Fancy => Self::fancy(),
+ Style::Minimal => Self::minimal(),
+ }
+ }
+
+ fn fancy() -> Self {
+ Self {
+ fancy: true,
+
+ dash: '—',
+ bullet: '•',
+ body_prefix: '▌',
+ horiz_rule: '─',
+
+ // red: "\x1b[31m",
+ bright_red: "\x1b[91m",
+ // green: "\x1b[32m",
+ bright_green: "\x1b[92m",
+ // blue: "\x1b[34m",
+ bright_blue: "\x1b[94m",
+ // cyan: "\x1b[36m",
+ bright_cyan: "\x1b[96m",
+ yellow: "\x1b[33m",
+ // bright_yellow: "\x1b[93m",
+ // magenta: "\x1b[35m",
+ bright_magenta: "\x1b[95m",
+ black: "\x1b[30m",
+ dark_grey: "\x1b[90m",
+ light_grey: "\x1b[37m",
+ white: "\x1b[97m",
+ no_fg: "\x1b[39m",
+ reset: "\x1b[0m",
+
+ dark_grey_bg: "\x1b[100m",
+ // no_bg: "\x1b[49",
+ hide_cursor: "\x1b[?25l",
+ show_cursor: "\x1b[?25h",
+ clear_line: "\x1b[2K",
+
+ italic: "\x1b[3m",
+ bold: "\x1b[1m",
+ strike: "\x1b[9m",
+ no_italic_bold: "\x1b[23m",
+ no_strike: "\x1b[29m",
+ }
+ }
+
+ fn minimal() -> Self {
+ Self {
+ fancy: false,
+
+ dash: '-',
+ bullet: '-',
+ body_prefix: '>',
+ horiz_rule: '-',
+
+ // red: "",
+ bright_red: "",
+ // green: "",
+ bright_green: "",
+ // blue: "",
+ bright_blue: "",
+ // cyan: "",
+ bright_cyan: "",
+ yellow: "",
+ // bright_yellow: "",
+ // magenta: "",
+ bright_magenta: "",
+ black: "",
+ dark_grey: "",
+ light_grey: "",
+ white: "",
+ no_fg: "",
+ reset: "",
+
+ dark_grey_bg: "",
+ // no_bg: "",
+ hide_cursor: "",
+ show_cursor: "",
+ clear_line: "",
+
+ italic: "",
+ bold: "",
+ strike: "~~",
+ no_italic_bold: "",
+ no_strike: "~~",
+ }
+ }
+}
+
+fn markdown(text: &str) -> String {
+ let SpecialRender {
+ fancy,
+
+ bullet,
+ horiz_rule,
+ bright_blue,
+ dark_grey_bg,
+ body_prefix,
+ ..
+ } = *special_render();
+
+ if !fancy {
+ let mut out = String::new();
+ for line in text.lines() {
+ use std::fmt::Write;
+ let _ = writeln!(&mut out, "{body_prefix} {line}");
+ }
+ return out;
+ }
+
+ let arena = comrak::Arena::new();
+ let mut options = comrak::Options::default();
+ options.extension.strikethrough = true;
+ let root = comrak::parse_document(&arena, text, &options);
+
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ enum Side {
+ Start,
+ End,
+ }
+
+ let mut explore_stack = Vec::new();
+ let mut render_queue = Vec::new();
+
+ explore_stack.extend(root.reverse_children().map(|x| (x, Side::Start)));
+ while let Some((node, side)) = explore_stack.pop() {
+ if side == Side::Start {
+ explore_stack.push((node, Side::End));
+ explore_stack.extend(node.reverse_children().map(|x| (x, Side::Start)));
+ }
+ render_queue.push((node, side));
+ }
+
+ let mut list_numbers = Vec::new();
+
+ let (terminal_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
+ let max_line_len = (terminal_width as usize - 2).min(80);
+
+ let mut links = Vec::new();
+
+ let mut ansi_printer = AnsiPrinter::new(max_line_len);
+ ansi_printer.pause_style();
+ ansi_printer.prefix();
+ ansi_printer.resume_style();
+ let mut iter = render_queue.into_iter().peekable();
+ while let Some((item, side)) = iter.next() {
+ use comrak::nodes::NodeValue;
+ use Side::*;
+ match (&item.data.borrow().value, side) {
+ (NodeValue::Paragraph, Start) => (),
+ (NodeValue::Paragraph, End) => {
+ if iter.peek().is_some_and(|(_, side)| *side == Start) {
+ ansi_printer.newline();
+ ansi_printer.newline();
+ }
+ }
+ (NodeValue::Text(s), Start) => ansi_printer.text(s),
+ (NodeValue::Link(_), Start) => {
+ ansi_printer.start_fg(bright_blue);
+ }
+ (NodeValue::Link(link), End) => {
+ use std::fmt::Write;
+ ansi_printer.stop_fg();
+ links.push(link.url.clone());
+ let _ = write!(&mut ansi_printer, "({})", links.len());
+ }
+ (NodeValue::Image(_), Start) => {
+ ansi_printer.start_fg(bright_blue);
+ }
+ (NodeValue::Image(link), End) => {
+ use std::fmt::Write;
+ ansi_printer.stop_fg();
+ links.push(link.url.clone());
+ let _ = write!(&mut ansi_printer, "({})", links.len());
+ }
+ (NodeValue::Code(code), Start) => {
+ ansi_printer.pause_style();
+ ansi_printer.start_bg(dark_grey_bg);
+ ansi_printer.text(&code.literal);
+ ansi_printer.resume_style();
+ }
+ (NodeValue::CodeBlock(code), Start) => {
+ if ansi_printer.cur_line_len != 0 {
+ ansi_printer.newline();
+ }
+ ansi_printer.pause_style();
+ ansi_printer.start_bg(dark_grey_bg);
+ ansi_printer.text(&code.literal);
+ ansi_printer.newline();
+ ansi_printer.resume_style();
+ ansi_printer.newline();
+ }
+ (NodeValue::BlockQuote, Start) => {
+ ansi_printer.blockquote_depth += 1;
+ ansi_printer.pause_style();
+ ansi_printer.prefix();
+ ansi_printer.resume_style();
+ }
+ (NodeValue::BlockQuote, End) => {
+ ansi_printer.blockquote_depth -= 1;
+ ansi_printer.newline();
+ }
+ (NodeValue::HtmlInline(html), Start) => {
+ ansi_printer.pause_style();
+ ansi_printer.text(html);
+ ansi_printer.resume_style();
+ }
+ (NodeValue::HtmlBlock(html), Start) => {
+ if ansi_printer.cur_line_len != 0 {
+ ansi_printer.newline();
+ }
+ ansi_printer.pause_style();
+ ansi_printer.text(&html.literal);
+ ansi_printer.newline();
+ ansi_printer.resume_style();
+ }
+
+ (NodeValue::Heading(heading), Start) => {
+ ansi_printer.reset();
+ ansi_printer.start_bold();
+ ansi_printer
+ .out
+ .extend(std::iter::repeat('#').take(heading.level as usize));
+ ansi_printer.out.push(' ');
+ ansi_printer.cur_line_len += heading.level as usize + 1;
+ }
+ (NodeValue::Heading(_), End) => {
+ ansi_printer.reset();
+ ansi_printer.newline();
+ ansi_printer.newline();
+ }
+
+ (NodeValue::List(list), Start) => {
+ if list.list_type == comrak::nodes::ListType::Ordered {
+ list_numbers.push(0);
+ }
+ }
+ (NodeValue::List(list), End) => {
+ if list.list_type == comrak::nodes::ListType::Ordered {
+ list_numbers.pop();
+ }
+ ansi_printer.newline();
+ }
+ (NodeValue::Item(list), Start) => {
+ if list.list_type == comrak::nodes::ListType::Ordered {
+ use std::fmt::Write;
+ let number: usize = if let Some(number) = list_numbers.last_mut() {
+ *number += 1;
+ *number
+ } else {
+ 0
+ };
+ let _ = write!(&mut ansi_printer, "{number}. ");
+ } else {
+ ansi_printer.out.push(bullet);
+ ansi_printer.out.push(' ');
+ ansi_printer.cur_line_len += 2;
+ }
+ }
+ (NodeValue::Item(_), End) => {
+ ansi_printer.newline();
+ }
+
+ (NodeValue::LineBreak, Start) => ansi_printer.newline(),
+ (NodeValue::SoftBreak, Start) => ansi_printer.newline(),
+ (NodeValue::ThematicBreak, Start) => {
+ if ansi_printer.cur_line_len != 0 {
+ ansi_printer.newline();
+ }
+ ansi_printer
+ .out
+ .extend(std::iter::repeat(horiz_rule).take(max_line_len));
+ ansi_printer.newline();
+ ansi_printer.newline();
+ }
+
+ (NodeValue::Emph, Start) => ansi_printer.start_italic(),
+ (NodeValue::Emph, End) => ansi_printer.stop_italic(),
+ (NodeValue::Strong, Start) => ansi_printer.start_bold(),
+ (NodeValue::Strong, End) => ansi_printer.stop_bold(),
+ (NodeValue::Strikethrough, Start) => ansi_printer.start_strike(),
+ (NodeValue::Strikethrough, End) => ansi_printer.stop_strike(),
+
+ (NodeValue::Escaped, Start) => (),
+ (_, End) => (),
+ (_, Start) => ansi_printer.text("?TODO?"),
+ }
+ }
+ if !links.is_empty() {
+ ansi_printer.out.push('\n');
+ for (i, url) in links.into_iter().enumerate() {
+ use std::fmt::Write;
+ let _ = writeln!(&mut ansi_printer.out, "({}. {url} )", i + 1);
+ }
+ }
+
+ ansi_printer.out
+}
+
+#[derive(Default)]
+struct RenderStyling {
+ bold: bool,
+ italic: bool,
+ strike: bool,
+
+ fg: Option<&'static str>,
+ bg: Option<&'static str>,
+}
+
+struct AnsiPrinter {
+ special_render: &'static SpecialRender,
+
+ out: String,
+
+ cur_line_len: usize,
+ max_line_len: usize,
+
+ blockquote_depth: usize,
+
+ style_frames: Vec<RenderStyling>,
+}
+
+impl AnsiPrinter {
+ fn new(max_line_len: usize) -> Self {
+ Self {
+ special_render: special_render(),
+
+ out: String::new(),
+
+ cur_line_len: 0,
+ max_line_len,
+
+ blockquote_depth: 0,
+
+ style_frames: vec![RenderStyling::default()],
+ }
+ }
+
+ fn text(&mut self, text: &str) {
+ let mut iter = text.lines().peekable();
+ while let Some(mut line) = iter.next() {
+ loop {
+ let this_len = line.chars().count();
+ if self.cur_line_len + this_len > self.max_line_len {
+ let mut split_at = self.max_line_len - self.cur_line_len;
+ loop {
+ if line.is_char_boundary(split_at) {
+ break;
+ }
+ split_at -= 1;
+ }
+ let split_at = line
+ .split_at(split_at)
+ .0
+ .char_indices()
+ .rev()
+ .find(|(_, c)| c.is_whitespace())
+ .map(|(i, _)| i)
+ .unwrap_or(split_at);
+ let (head, tail) = line.split_at(split_at);
+ self.out.push_str(head);
+ self.cur_line_len += split_at;
+ self.newline();
+ line = tail.trim_start();
+ } else {
+ self.out.push_str(line);
+ self.cur_line_len += this_len;
+ break;
+ }
+ }
+ if iter.peek().is_some() {
+ self.newline();
+ }
+ }
+ }
+
+ // Uncomment if needed
+ // fn current_fg(&self) -> Option<&'static str> {
+ // self.current_style().fg
+ // }
+
+ fn start_fg(&mut self, color: &'static str) {
+ self.current_style_mut().fg = Some(color);
+ self.out.push_str(color);
+ }
+
+ fn stop_fg(&mut self) {
+ self.current_style_mut().fg = None;
+ self.out.push_str(self.special_render.no_fg);
+ }
+
+ fn current_bg(&self) -> Option<&'static str> {
+ self.current_style().bg
+ }
+
+ fn start_bg(&mut self, color: &'static str) {
+ self.current_style_mut().bg = Some(color);
+ self.out.push_str(color);
+ }
+
+ // Uncomment if needed
+ // fn stop_bg(&mut self) {
+ // self.current_style_mut().bg = None;
+ // self.out.push_str(self.special_render.no_bg);
+ // }
+
+ fn is_bold(&self) -> bool {
+ self.current_style().bold
+ }
+
+ fn start_bold(&mut self) {
+ self.current_style_mut().bold = true;
+ self.out.push_str(self.special_render.bold);
+ }
+
+ fn stop_bold(&mut self) {
+ self.current_style_mut().bold = false;
+ self.out.push_str(self.special_render.reset);
+ if self.is_italic() {
+ self.out.push_str(self.special_render.italic);
+ }
+ if self.is_strike() {
+ self.out.push_str(self.special_render.strike);
+ }
+ }
+
+ fn is_italic(&self) -> bool {
+ self.current_style().italic
+ }
+
+ fn start_italic(&mut self) {
+ self.current_style_mut().italic = true;
+ self.out.push_str(self.special_render.italic);
+ }
+
+ fn stop_italic(&mut self) {
+ self.current_style_mut().italic = false;
+ self.out.push_str(self.special_render.no_italic_bold);
+ if self.is_bold() {
+ self.out.push_str(self.special_render.bold);
+ }
+ }
+
+ fn is_strike(&self) -> bool {
+ self.current_style().strike
+ }
+
+ fn start_strike(&mut self) {
+ self.current_style_mut().strike = true;
+ self.out.push_str(self.special_render.strike);
+ }
+
+ fn stop_strike(&mut self) {
+ self.current_style_mut().strike = false;
+ self.out.push_str(self.special_render.no_strike);
+ }
+
+ fn reset(&mut self) {
+ *self.current_style_mut() = RenderStyling::default();
+ self.out.push_str(self.special_render.reset);
+ }
+
+ fn pause_style(&mut self) {
+ self.out.push_str(self.special_render.reset);
+ self.style_frames.push(RenderStyling::default());
+ }
+
+ fn resume_style(&mut self) {
+ self.out.push_str(self.special_render.reset);
+ self.style_frames.pop();
+ if let Some(bg) = self.current_bg() {
+ self.out.push_str(bg);
+ }
+ if self.is_bold() {
+ self.out.push_str(self.special_render.bold);
+ }
+ if self.is_italic() {
+ self.out.push_str(self.special_render.italic);
+ }
+ if self.is_strike() {
+ self.out.push_str(self.special_render.strike);
+ }
+ }
+
+ fn newline(&mut self) {
+ if self.current_bg().is_some() {
+ self.out
+ .extend(std::iter::repeat(' ').take(self.max_line_len - self.cur_line_len));
+ }
+ self.pause_style();
+ self.out.push('\n');
+ self.prefix();
+ for _ in 0..self.blockquote_depth {
+ self.prefix();
+ }
+ self.resume_style();
+ self.cur_line_len = self.blockquote_depth * 2;
+ }
+
+ fn prefix(&mut self) {
+ self.out.push_str(self.special_render.dark_grey);
+ self.out.push(self.special_render.body_prefix);
+ self.out.push(' ');
+ }
+
+ fn current_style(&self) -> &RenderStyling {
+ self.style_frames.last().expect("Ran out of style frames")
+ }
+
+ fn current_style_mut(&mut self) -> &mut RenderStyling {
+ self.style_frames
+ .last_mut()
+ .expect("Ran out of style frames")
+ }
+}
+
+impl std::fmt::Write for AnsiPrinter {
+ fn write_str(&mut self, s: &str) -> std::fmt::Result {
+ self.text(s);
+ Ok(())
+ }
+}