use std::fmt::Write as _;
use clap::ValueEnum;
use crate::cli::DocTopic;
use crate::markdown::wrap::wrap_markdown;
const AGENTS: &str = include_str!("../../docs/agents.md");
const SCRIPTING: &str = include_str!("../../docs/scripting.md");
const SHOW: &str = include_str!("../../docs/show.md");
const CONFIG: &str = include_str!("../../docs/config.md");
const TEMPLATE: &str = include_str!("../../docs/template.md");
const AUTH: &str = include_str!("../../docs/auth.md");
const FALLBACK_WIDTH: usize = 80;
const MAX_WIDTH: usize = 100;
pub(crate) fn source(topic: DocTopic) -> &'static str {
match topic {
DocTopic::Agents => AGENTS,
DocTopic::Scripting => SCRIPTING,
DocTopic::Show => SHOW,
DocTopic::Config => CONFIG,
DocTopic::Template => TEMPLATE,
DocTopic::Auth => AUTH,
}
}
pub(crate) fn index() -> String {
let mut out = String::from("stakk documentation topics:\n\n");
for topic in DocTopic::value_variants() {
let value = topic
.to_possible_value()
.expect("every DocTopic variant has a possible value");
let help = value
.get_help()
.map_or_else(String::new, ToString::to_string);
writeln!(out, " {:<10} {help}", value.get_name())
.expect("writing to a String cannot fail");
}
out.push_str("\nRun `stakk docs <topic>` to print one.\n");
out.push_str("The same documents live in docs/ at https://github.com/glennib/stakk\n");
out
}
pub(crate) fn render(topic: DocTopic, width: Option<usize>) -> String {
match width {
None => source(topic).to_string(),
Some(width) => wrap_markdown(source(topic), width),
}
}
pub(crate) fn print(topic: Option<DocTopic>) {
let Some(topic) = topic else {
print!("{}", index());
return;
};
if console::Term::stdout().is_term() {
println!("{}", render(topic, Some(terminal_width())));
} else {
print!("{}", render(topic, None));
}
}
fn terminal_width() -> usize {
if let Ok(value) = std::env::var("COLUMNS")
&& let Ok(columns) = value.trim().parse::<usize>()
&& columns > 0
{
return columns.min(MAX_WIDTH);
}
console::Term::stdout()
.size_checked()
.map_or(FALLBACK_WIDTH, |(_, columns)| columns as usize)
.min(MAX_WIDTH)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::markdown::unwrap::fence_marker;
const MAX_FENCE_WIDTH: usize = 76;
fn all_topics() -> Vec<DocTopic> {
DocTopic::value_variants().to_vec()
}
fn fenced_lines(text: &str) -> Vec<String> {
let mut open: Option<String> = None;
let mut lines = Vec::new();
for line in text.lines() {
match &open {
Some(marker) => {
if fence_marker(line).as_ref() == Some(marker) {
open = None;
} else {
lines.push(line.to_string());
}
}
None => open = fence_marker(line),
}
}
lines
}
#[test]
fn redirected_output_is_byte_identical_to_the_source() {
for topic in all_topics() {
assert!(!source(topic).is_empty(), "{topic:?} source is empty");
assert_eq!(
render(topic, None),
source(topic),
"{topic:?} is not reproduced verbatim when redirected"
);
}
}
#[test]
fn terminal_output_is_reflowed() {
for topic in all_topics() {
assert_ne!(
render(topic, Some(80)),
source(topic),
"{topic:?} was not re-flowed at width 80"
);
}
}
#[test]
fn fenced_content_survives_wrapping_verbatim() {
for topic in all_topics() {
let src = source(topic);
let expected = fenced_lines(src);
for width in [40, 60, 80, 120] {
let wrapped = wrap_markdown(src, width);
assert_eq!(
fenced_lines(&wrapped),
expected,
"{topic:?} fenced content changed at width {width}"
);
}
}
}
#[test]
fn fenced_lines_fit_a_narrow_terminal() {
for topic in all_topics() {
for line in fenced_lines(source(topic)) {
assert!(
line.chars().count() <= MAX_FENCE_WIDTH,
"{topic:?}: fenced line is {} chars (max {MAX_FENCE_WIDTH}): {line}",
line.chars().count()
);
}
}
}
#[test]
fn index_lists_every_topic() {
let index = index();
for topic in all_topics() {
let name = topic
.to_possible_value()
.expect("variant has a possible value")
.get_name()
.to_string();
assert!(index.contains(&name), "index is missing {name}");
}
}
#[test]
fn index_describes_a_topic_for_coding_agents() {
assert!(
index().contains("coding agents"),
"one index entry should name coding agents, so an agent scanning the index picks it"
);
}
#[test]
fn every_docs_file_is_a_topic() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/docs");
let bundled: Vec<&str> = all_topics().into_iter().map(source).collect();
for entry in std::fs::read_dir(dir).expect("docs/ exists") {
let path = entry.expect("readable dir entry").path();
if path.extension().is_none_or(|ext| ext != "md") {
continue;
}
let contents = std::fs::read_to_string(&path).expect("readable doc");
assert!(
bundled.contains(&contents.as_str()),
"{} is not reachable from `stakk docs`: add a `DocTopic` variant and a `source` \
arm, or the file ships unlisted",
path.display(),
);
}
}
#[test]
fn columns_overrides_the_detected_width() {
unsafe { std::env::set_var("COLUMNS", "37") };
let width = terminal_width();
unsafe { std::env::remove_var("COLUMNS") };
assert_eq!(width, 37);
}
}