use std::path::Path;
use crate::cli::Format;
use crate::exit::ExitCode;
use crate::output::{OutputEnvelope, SCHEMA_VERSION, Streams};
const ART: &str = " ,-~-. ,-~-. ,-~-.
( o.o ) ( o.o ) ( o.o ) shep {version}
`-^-' `-^-' `-^-' flock at {home}
\" \" \" \" \" \"
/\\ /\\
( o o )--, the shepherd keeps them running
`--..--' |
| | '
";
const QUICK_START: &str = "\
Getting started
shep start server.js start it and keep it alive
shep flock see what's running
shep bleats server follow its output
shep save remember this flock across reboots
shep startup bring it back after a reboot
shep welcome show this again
";
pub(crate) fn render(home: &Path) -> String {
let home = home.display().to_string();
let art = ART
.replace("{version}", env!("CARGO_PKG_VERSION"))
.replace("{home}", &home);
format!(
"{art}\nSet up {home}. Logs, pids and the shepherd's socket live here.\n\n{QUICK_START}"
)
}
#[derive(Debug, serde::Serialize)]
struct WelcomeData {
text: String,
}
pub(crate) fn on_first_run(streams: &mut Streams<'_>, home: &Path, stderr_is_terminal: bool) {
if streams.fmt == Format::Json || !stderr_is_terminal {
return;
}
let _ = write!(streams.err, "{}", render(home));
}
pub(crate) fn welcome(streams: &mut Streams<'_>, home: &Path) -> ExitCode {
let text = render(home);
let wrote = match streams.fmt {
Format::Table => write!(streams.out, "{text}"),
Format::Json => {
let envelope = OutputEnvelope {
schema_version: SCHEMA_VERSION,
command: "welcome",
data: WelcomeData { text },
};
serde_json::to_writer(&mut *streams.out, &envelope)
.map_err(std::io::Error::other)
.and_then(|()| writeln!(streams.out))
}
};
match wrote {
Ok(()) => ExitCode::Success,
Err(_) => ExitCode::Internal,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_welcome_renders_exactly_this() {
let rendered = render(Path::new("/home/rin/.shep"));
let expected = format!(
" ,-~-. ,-~-. ,-~-.
( o.o ) ( o.o ) ( o.o ) shep {version}
`-^-' `-^-' `-^-' flock at /home/rin/.shep
\" \" \" \" \" \"
/\\ /\\
( o o )--, the shepherd keeps them running
`--..--' |
| | '
Set up /home/rin/.shep. Logs, pids and the shepherd's socket live here.
Getting started
shep start server.js start it and keep it alive
shep flock see what's running
shep bleats server follow its output
shep save remember this flock across reboots
shep startup bring it back after a reboot
shep welcome show this again
",
version = env!("CARGO_PKG_VERSION"),
);
assert_eq!(rendered, expected);
}
#[test]
fn the_home_path_is_substituted_everywhere_it_appears() {
let rendered = render(Path::new("/srv/api"));
assert_eq!(
rendered.matches("/srv/api").count(),
2,
"both the art's caption and the prose line name the home:\n{rendered}"
);
assert!(
!rendered.contains("~/.shep"),
"no hardcoded default leaks through:\n{rendered}"
);
}
#[test]
fn the_welcome_copy_has_no_em_dashes() {
let rendered = render(Path::new("/home/rin/.shep"));
assert!(
!rendered.contains('\u{2014}'),
"em dash in user-facing copy"
);
assert!(
!rendered.contains('\u{2013}'),
"en dash in user-facing copy"
);
}
fn drain(fmt: Format, f: impl FnOnce(&mut Streams<'_>)) -> (String, String) {
let mut out = Vec::new();
let mut err = Vec::new();
{
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt,
};
f(&mut streams);
}
(
String::from_utf8(out).unwrap(),
String::from_utf8(err).unwrap(),
)
}
#[test]
fn the_first_run_welcome_goes_to_stderr() {
let (out, err) = drain(Format::Table, |s| {
on_first_run(s, Path::new("/home/rin/.shep"), true);
});
assert!(out.is_empty(), "stdout must stay clean: {out}");
assert!(
err.contains("Getting started"),
"stderr must carry it: {err}"
);
}
#[test]
fn the_first_run_welcome_is_suppressed_for_json_and_for_pipes() {
let (_, json) = drain(Format::Json, |s| {
on_first_run(s, Path::new("/x"), true);
});
assert!(json.is_empty(), "--format json must suppress it: {json}");
let (_, piped) = drain(Format::Table, |s| {
on_first_run(s, Path::new("/x"), false);
});
assert!(
piped.is_empty(),
"a non-terminal stderr suppresses it: {piped}"
);
}
#[test]
fn the_welcome_verb_prints_to_stdout_even_when_piped() {
let (out, err) = drain(Format::Table, |s| {
welcome(s, Path::new("/home/rin/.shep"));
});
assert!(
out.contains("Getting started"),
"stdout must carry it: {out}"
);
assert!(err.is_empty(), "nothing belongs on stderr here: {err}");
}
#[test]
fn the_welcome_verb_answers_json_with_an_envelope() {
let (out, _) = drain(Format::Json, |s| {
welcome(s, Path::new("/home/rin/.shep"));
});
let parsed: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
assert_eq!(parsed["command"], "welcome");
assert_eq!(parsed["schema_version"], SCHEMA_VERSION);
assert!(
parsed["data"]["text"]
.as_str()
.unwrap()
.contains("Getting started"),
"the envelope carries the text: {out}"
);
}
#[test]
fn the_welcome_fits_an_eighty_column_terminal() {
let rendered = render(Path::new("/home/rin/.shep"));
for line in rendered.lines() {
assert!(
line.chars().count() <= 80,
"line is {} columns: {line:?}",
line.chars().count()
);
}
}
}