use std::collections::HashMap;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use crate::agent::AgentClient;
use crate::error::Result;
use crate::gh::GhClient;
use crate::git::cli::GitCli;
pub struct Stream {
writer: Box<dyn Write + Send>,
is_tty: bool,
}
impl Stream {
pub fn new(writer: Box<dyn Write + Send>, is_tty: bool) -> Self {
Self { writer, is_tty }
}
pub fn is_tty(&self) -> bool {
self.is_tty
}
pub fn line(&mut self, s: &str) -> Result<()> {
writeln!(self.writer, "{s}")?;
Ok(())
}
pub fn text(&mut self, s: &str) -> Result<()> {
write!(self.writer, "{s}")?;
Ok(())
}
pub fn flush(&mut self) -> Result<()> {
self.writer.flush()?;
Ok(())
}
}
pub trait Input {
fn read_line(&mut self) -> Result<String>;
}
pub struct StdinInput;
impl Input for StdinInput {
fn read_line(&mut self) -> Result<String> {
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
Ok(line)
}
}
pub struct SilentInput;
impl Input for SilentInput {
fn read_line(&mut self) -> Result<String> {
Ok(String::new())
}
}
#[derive(Clone)]
pub struct Env {
vars: HashMap<String, String>,
}
impl Env {
pub fn from_map(vars: HashMap<String, String>) -> Self {
Self { vars }
}
pub fn from_real() -> Self {
Self {
vars: std::env::vars().collect(),
}
}
pub fn get(&self, key: &str) -> Option<&str> {
self.vars.get(key).map(String::as_str)
}
pub fn is_set_nonempty(&self, key: &str) -> bool {
self.get(key).is_some_and(|v| !v.is_empty())
}
}
pub struct Cx {
pub out: Stream,
pub err: Stream,
pub env: Env,
pub cwd: PathBuf,
pub git: Arc<dyn GitCli + Send + Sync>,
pub gh: Arc<dyn GhClient + Send + Sync>,
pub agent: Arc<dyn AgentClient + Send + Sync>,
pub input: Box<dyn Input + Send>,
pub color_flag: Option<crate::output::color::ColorChoice>,
pub no_pager: bool,
pub verbose: u8,
}
impl Cx {
#[allow(clippy::too_many_arguments)]
pub fn new(
out: Stream,
err: Stream,
env: Env,
cwd: PathBuf,
git: Arc<dyn GitCli + Send + Sync>,
gh: Arc<dyn GhClient + Send + Sync>,
agent: Arc<dyn AgentClient + Send + Sync>,
input: Box<dyn Input + Send>,
) -> Self {
Self {
out,
err,
env,
cwd,
git,
gh,
agent,
input,
color_flag: None,
no_pager: false,
verbose: 0,
}
}
pub fn color_enabled(&self, ui_color: crate::output::color::ColorChoice) -> bool {
crate::output::color::resolve_color(
self.color_flag,
self.env.is_set_nonempty("NO_COLOR"),
Some(ui_color),
self.out.is_tty(),
)
}
pub fn color_enabled_err(&self, ui_color: crate::output::color::ColorChoice) -> bool {
crate::output::color::resolve_color(
self.color_flag,
self.env.is_set_nonempty("NO_COLOR"),
Some(ui_color),
self.err.is_tty(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil::{SharedBuf, test_cx};
#[test]
fn stream_writes_line_and_text() {
let buf = SharedBuf::new();
let mut s = Stream::new(Box::new(buf.clone()), false);
s.text("a").unwrap();
s.line("b").unwrap();
s.flush().unwrap();
assert_eq!(buf.contents(), "ab\n");
assert!(!s.is_tty());
}
#[test]
fn stream_reports_tty_flag() {
let s = Stream::new(Box::new(SharedBuf::new()), true);
assert!(s.is_tty());
}
#[test]
fn silent_input_reports_eof() {
assert_eq!(SilentInput.read_line().unwrap(), "");
}
#[test]
fn env_get_and_nonempty() {
let env = Env::from_map(
[
("A".to_string(), "1".to_string()),
("E".to_string(), String::new()),
]
.into_iter()
.collect(),
);
assert_eq!(env.get("A"), Some("1"));
assert_eq!(env.get("MISSING"), None);
assert!(env.is_set_nonempty("A"));
assert!(!env.is_set_nonempty("E"));
assert!(!env.is_set_nonempty("MISSING"));
}
#[test]
fn color_enabled_err_follows_stderr_tty() {
use crate::output::color::ColorChoice;
let mut t = test_cx(&[], "/work");
t.cx.err = Stream::new(Box::new(SharedBuf::new()), true);
assert!(t.cx.color_enabled_err(ColorChoice::Auto));
assert!(!t.cx.color_enabled(ColorChoice::Auto));
assert!(!t.cx.color_enabled_err(ColorChoice::Never));
t.cx.color_flag = Some(ColorChoice::Always);
assert!(t.cx.color_enabled_err(ColorChoice::Never));
}
#[test]
fn color_enabled_err_honors_no_color() {
use crate::output::color::ColorChoice;
let mut t = test_cx(&[("NO_COLOR", "1")], "/work");
t.cx.err = Stream::new(Box::new(SharedBuf::new()), true);
assert!(!t.cx.color_enabled_err(ColorChoice::Always));
}
#[test]
fn cx_exposes_streams_env_cwd() {
let mut t = test_cx(&[("X", "y")], "/work");
t.cx.out.line("path").unwrap();
t.cx.err.line("note").unwrap();
assert_eq!(t.out.contents(), "path\n");
assert_eq!(t.err.contents(), "note\n");
assert_eq!(t.cx.env.get("X"), Some("y"));
assert_eq!(t.cx.cwd, PathBuf::from("/work"));
}
}