use std::path::{Path, PathBuf};
use clap::Parser;
use crate::{
app::{App, status},
cli::{Cli, Command},
};
mod action_palette;
mod app;
mod cli;
mod components;
mod config;
mod degrade;
mod edit_buffer;
mod editor;
mod elapsed;
mod errors;
mod filter_line;
mod footer;
mod glyphs;
mod header;
mod help;
mod keys;
mod launcher;
mod launcher_palette;
mod list_viewport;
mod logging;
mod management;
mod message;
mod notice;
mod scroll;
mod selection;
mod set_picker;
mod sets;
mod sort;
mod state;
mod status_row;
#[cfg(test)]
mod test_support;
mod theme;
mod tui;
mod unwind;
mod warnings;
fn main() -> color_eyre::Result<()> {
let args = Cli::parse();
#[cfg(debug_assertions)]
if args.panic_after_tui_enter {
return panic_after_tui_enter();
}
#[cfg(debug_assertions)]
if args.launcher_marker_after_tui_enter {
return launcher_marker_after_tui_enter();
}
#[cfg(debug_assertions)]
if args.editor_marker_after_tui_enter {
return editor_marker_after_tui_enter();
}
#[cfg(debug_assertions)]
if args.panic_after_launcher_handoff {
return panic_after_launcher_handoff();
}
#[cfg(debug_assertions)]
if args.unspawnable_launcher_after_tui_enter {
return unspawnable_launcher_after_tui_enter();
}
#[cfg(debug_assertions)]
if args.redraw_marker_after_suspend_for_child {
return redraw_marker_after_suspend_for_child("true");
}
#[cfg(debug_assertions)]
if args.redraw_marker_after_unspawnable_child {
return redraw_marker_after_suspend_for_child("repon-no-such-program-on-any-PATH-anywhere");
}
#[cfg(debug_assertions)]
if args.write_raw_stderr_after_tui_enter {
return write_raw_stderr_after_tui_enter();
}
#[cfg(debug_assertions)]
if args.exit_after_delay_once_tui_entered {
return exit_after_delay_once_tui_entered();
}
#[cfg(debug_assertions)]
if let Some(new_value) = &args.reprint_config_path_after_env_change {
return reprint_config_path_after_env_change(new_value);
}
if let Some(command) = &args.command {
return run_command(command, args.config, args.set, args.no_fetch);
}
errors::init()?;
logging::init()?;
config::init(args.config);
App::new(
args.tick_rate,
args.frame_rate,
args.theme,
args.set,
args.filter,
args.no_fetch,
)?
.run()
}
#[cfg(debug_assertions)]
fn panic_after_tui_enter() -> color_eyre::Result<()> {
errors::init()?;
let mut tui = tui::Tui::new()?;
tui.enter()?;
panic!("repon: test-triggered panic after claiming the terminal");
}
#[cfg(debug_assertions)]
fn synthetic_entity() -> repon_core::EntityState {
let cwd: std::sync::Arc<std::path::Path> = std::sync::Arc::from(std::env::temp_dir().as_path());
repon_core::EntityState::new(
repon_core::EntityKey::new(std::sync::Arc::clone(&cwd)),
std::sync::Arc::from("synthetic"),
cwd,
repon_core::Kind::Repo,
)
}
#[cfg(debug_assertions)]
fn launcher_marker_after_tui_enter() -> color_eyre::Result<()> {
errors::init()?;
config::init(None);
let config = config::Config::new()?;
let resolved = launcher::resolve(&config.document);
let test_launcher = resolved
.iter()
.find(|launcher| launcher.name == "test")
.expect("the config this flag is run against must declare a [[launcher]] named `test`");
let mut tui = tui::Tui::new()?;
tui.enter()?;
launcher::run(&mut tui, test_launcher, &synthetic_entity())?;
Ok(())
}
#[cfg(debug_assertions)]
fn editor_marker_after_tui_enter() -> color_eyre::Result<()> {
errors::init()?;
unsafe {
std::env::set_var(
"EDITOR",
r#"sh -c 'printf EDITOR_HANDOFF_MARKER > "$1"' --"#,
);
}
let mut tui = tui::Tui::new()?;
tui.enter()?;
let edited = editor::edit(&mut tui, "before the handoff\n")?;
tui.exit()?;
println!("EDITED:{edited}");
Ok(())
}
#[cfg(debug_assertions)]
fn panic_after_launcher_handoff() -> color_eyre::Result<()> {
errors::init()?;
let mut tui = tui::Tui::new()?;
tui.enter()?;
let synthetic_launcher = launcher::Launcher {
name: "test".to_string(),
source: launcher::Source::Args(vec!["true".to_string()]),
shell: false,
interactive: false,
takes_terminal: true,
env: Default::default(),
};
launcher::run(&mut tui, &synthetic_launcher, &synthetic_entity())?;
panic!("repon: test-triggered panic after a Launcher handoff completed");
}
#[cfg(debug_assertions)]
fn unspawnable_launcher_after_tui_enter() -> color_eyre::Result<()> {
errors::init()?;
let mut tui = tui::Tui::new()?;
tui.enter()?;
let synthetic_launcher = launcher::Launcher {
name: "test".to_string(),
source: launcher::Source::Args(vec!["repon-test-binary-that-does-not-exist".to_string()]),
shell: false,
interactive: false,
takes_terminal: true,
env: Default::default(),
};
launcher::run(&mut tui, &synthetic_launcher, &synthetic_entity())?;
Ok(())
}
#[cfg(debug_assertions)]
fn redraw_marker_after_suspend_for_child(program: &str) -> color_eyre::Result<()> {
errors::init()?;
let mut tui = tui::Tui::new()?;
tui.enter()?;
let draw_marker = |tui: &mut tui::Tui| -> color_eyre::Result<()> {
tui.draw(|frame| {
frame.render_widget(
ratatui::widgets::Paragraph::new("REDRAW_MARKER_CONTENT"),
frame.area(),
);
})?;
Ok(())
};
draw_marker(&mut tui)?;
let mut command = std::process::Command::new(program);
let _ = tui.suspend_for_child(&mut command);
draw_marker(&mut tui)?;
tui.exit()?;
Ok(())
}
#[cfg(debug_assertions)]
fn write_raw_stderr_after_tui_enter() -> color_eyre::Result<()> {
errors::init()?;
let mut tui = tui::Tui::new()?;
tui.enter()?;
std::thread::spawn(|| {
use std::io::Write as _;
let _ = writeln!(std::io::stderr(), "STDERR_REDIRECT_MARKER");
})
.join()
.expect("the stderr-writing thread must not panic");
tui.exit()?;
Ok(())
}
#[cfg(debug_assertions)]
fn exit_after_delay_once_tui_entered() -> color_eyre::Result<()> {
errors::init()?;
let mut tui = tui::Tui::new()?;
tui.enter()?;
std::thread::sleep(std::time::Duration::from_millis(500));
tui.exit()?;
println!("EXIT_AFTER_DELAY_MARKER");
Ok(())
}
#[cfg(debug_assertions)]
fn reprint_config_path_after_env_change(new_value: &Path) -> color_eyre::Result<()> {
config::init(None);
println!("{}", config::config_file().display());
unsafe {
std::env::set_var("REPON_CONFIG", new_value);
}
println!("{}", config::config_file().display());
Ok(())
}
fn run_command(
command: &Command,
flag_config_file: Option<PathBuf>,
flag_set: Option<String>,
flag_no_fetch: bool,
) -> color_eyre::Result<()> {
match command {
Command::Config { example: true } => {
print!("{}", config::document::annotated_example());
}
Command::Config { example: false } => {
config::init(flag_config_file);
print_config_paths();
}
Command::Sets => {
config::init(flag_config_file);
let config = config::Config::new()?;
sets::print(&config.document);
}
Command::Status => {
config::init(flag_config_file);
let config = config::Config::new()?;
status::run(&config, flag_set.as_deref(), flag_no_fetch)?;
}
}
Ok(())
}
fn print_config_paths() {
let config_file = config::config_file();
let log_file = logging::log_file_path();
println!(
"config file: {} ({})",
config_file.display(),
existence(&config_file)
);
println!("themes dir: {}", config::themes_dir().display());
println!("data dir: {}", config::data_dir().display());
println!(
"log file: {} ({})",
log_file.display(),
existence(&log_file)
);
}
fn existence(path: &Path) -> &'static str {
if path.exists() { "exists" } else { "missing" }
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use crate::test_support::{
SourceLine, all_lines_where, production_lines_under_containing, production_source_at,
rust_source_files, workspace_crate_src_dirs, workspace_rust_source_dirs,
};
const WALL_CLOCK_READS: [&str; 2] = [".elapsed()", "Instant::now()"];
#[derive(Clone, Copy)]
enum Comparison {
Less,
LessOrEqual,
Greater,
GreaterOrEqual,
}
impl Comparison {
const ALL: [Self; 4] = [
Self::Less,
Self::LessOrEqual,
Self::Greater,
Self::GreaterOrEqual,
];
fn spelling(self) -> &'static str {
match self {
Self::Less => "<",
Self::LessOrEqual => "<=",
Self::Greater => ">",
Self::GreaterOrEqual => ">=",
}
}
}
const ARROWS: [&str; 2] = ["=>", "->"];
fn is_a_deadline_shape(line: &str) -> bool {
if !WALL_CLOCK_READS.iter().any(|read| line.contains(read)) {
return false;
}
let mut without_arrows = line.to_string();
for arrow in ARROWS {
without_arrows = without_arrows.replace(arrow, " ");
}
Comparison::ALL
.iter()
.any(|comparison| without_arrows.contains(comparison.spelling()))
}
type NonWaitDeadline = (String, &'static str, &'static str, &'static str);
fn non_wait_deadlines() -> Vec<NonWaitDeadline> {
let elapsed_ge = format!("{}{}", WALL_CLOCK_READS[0], " >=");
vec![
(
format!("if set_at{elapsed_ge} self.document.notice_timeout {{"),
"repon/src/app.rs",
"notice",
"production: a Notice expiring on screen",
),
(
format!("&& at{elapsed_ge} threshold"),
"repon-core/src/cell.rs",
"age_into_stale",
"production: a status Cell ageing into Stale",
),
(
format!("if started{elapsed_ge} abandon_after {{"),
"repon-core/src/discovery.rs",
"walk",
"production: discovery abandoning a walk that will not finish",
),
(
format!("assert!(past{elapsed_ge} Duration::from_secs(90));"),
"repon-core/src/cell.rs",
"elapsed_reads_a_positive_duration_for_a_timestamp_in_the_past",
"a claim about a fabricated Timestamp's own arithmetic, not a wait on anything",
),
(
format!("if start{elapsed_ge} deadline {{"),
"repon-core/src/liveness.rs",
"wait_within",
"`liveness::wait_within`, the one polling wait every other wait goes through",
),
]
}
fn enclosing_item(path: &Path, number: usize) -> Option<String> {
let source = std::fs::read_to_string(path).expect("read a workspace source file");
let lines: Vec<&str> = source.lines().collect();
lines[..number.min(lines.len())]
.iter()
.rev()
.filter(|line| !line.trim_start().starts_with("//"))
.find_map(|line| {
let after = line.split_once("fn ")?.1;
let name = after
.split(['(', '<', ' ', '\t'])
.next()
.filter(|name| !name.is_empty())?;
Some(name.to_string())
})
}
fn is_the_allowed_deadline(line: &SourceLine, allowed: &NonWaitDeadline) -> bool {
let (text, file, item, _reason) = allowed;
*text == line.text
&& line.path.ends_with(file)
&& enclosing_item(&line.path, line.number).as_deref() == Some(*item)
}
#[test]
fn no_test_owns_a_wall_clock_deadline_of_its_own() {
let dirs = workspace_rust_source_dirs();
let allowed = non_wait_deadlines();
let offending: Vec<String> = all_lines_where(&dirs, is_a_deadline_shape)
.into_iter()
.filter(|line| {
!allowed
.iter()
.any(|entry| is_the_allowed_deadline(line, entry))
})
.map(|line| format!("{}:{}: {}", line.path.display(), line.number, line.text))
.collect();
assert!(
offending.is_empty(),
"found a wall-clock deadline outside `repon_core::liveness` and outside the \
allowlist of deadlines that bound something other than a test's wait: \
{offending:?}. Wait through `liveness::wait_for` (or take `liveness::BACKSTOP` \
for a loop with cleanup of its own); add an entry to `non_wait_deadlines` only \
for a deadline that is not a test waiting on a liveness property."
);
}
#[derive(Clone, Copy)]
enum PublicItem {
Function,
Constant,
Module,
}
impl PublicItem {
const ALL: [Self; 3] = [Self::Function, Self::Constant, Self::Module];
fn declaration(self) -> &'static str {
match self {
Self::Function => "pub fn ",
Self::Constant => "pub const ",
Self::Module => "pub mod ",
}
}
fn use_shapes(self, name: &str) -> Vec<String> {
match self {
Self::Function => vec![format!(".{name}("), format!("::{name}(")],
Self::Constant | Self::Module => vec![format!("::{name}")],
}
}
}
fn undocumented_test_only_public_items(source: &str) -> Vec<(PublicItem, String)> {
let lines: Vec<&str> = source.lines().collect();
let mut items = Vec::new();
for (index, line) in lines.iter().enumerate() {
let trimmed = line.trim_start();
let Some((kind, rest)) = PublicItem::ALL
.into_iter()
.find_map(|kind| Some((kind, trimmed.strip_prefix(kind.declaration())?)))
else {
continue;
};
if is_test_gated_above(&lines, index) || !doc_comment_names_test_above(&lines, index) {
continue;
}
let name = rest
.split(['(', '<', ':', ';', ' ', '='])
.next()
.unwrap_or(rest)
.trim();
if !name.is_empty() {
items.push((kind, name.to_string()));
}
}
items
}
fn is_test_gated_above(lines: &[&str], index: usize) -> bool {
let mut cursor = index;
while cursor > 0 {
cursor -= 1;
let above = lines[cursor].trim();
if above.starts_with("///") {
continue;
}
if above.starts_with("#[") {
if above.contains("cfg(") && above.contains("test") {
return true;
}
continue;
}
break;
}
false
}
fn doc_comment_names_test_above(lines: &[&str], index: usize) -> bool {
let mut cursor = index;
let mut doc = String::new();
while cursor > 0 {
cursor -= 1;
let above = lines[cursor].trim();
if let Some(rest) = above.strip_prefix("///") {
doc.push_str(rest);
doc.push(' ');
continue;
}
if above.starts_with("#[") {
continue;
}
break;
}
doc.split(|c: char| !c.is_alphanumeric())
.any(|word| word.eq_ignore_ascii_case("test") || word.eq_ignore_ascii_case("tests"))
}
fn is_used_under(dirs: &[PathBuf], kind: PublicItem, name: &str) -> bool {
kind.use_shapes(name)
.iter()
.any(|shape| !production_lines_under_containing(dirs, shape).is_empty())
}
#[test]
fn every_pub_item_documented_as_test_only_is_either_gated_or_has_a_production_use_site() {
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let core_src = manifest_dir.join("../repon-core/src");
let production_dirs = workspace_crate_src_dirs();
let mut unguarded = Vec::new();
for path in rust_source_files(&core_src) {
for (kind, name) in undocumented_test_only_public_items(&production_source_at(&path)) {
if !is_used_under(&production_dirs, kind, &name) {
unguarded.push(format!("{}: {name}", path.display()));
}
}
}
assert!(
unguarded.is_empty(),
"found a public repon-core item whose own doc comment names a test as its \
reason to exist, with no production use site anywhere in the workspace, and no \
`cfg(test)` or `feature = \"test-util\"` gate keeping it off the default build: \
{unguarded:?}"
);
}
#[test]
fn repon_core_dependency_enables_test_util_from_dev_dependencies_only() {
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let manifest = std::fs::read_to_string(manifest_dir.join("Cargo.toml"))
.expect("read this crate's own Cargo.toml");
let dependencies_section = manifest
.split("\n[dependencies]")
.nth(1)
.and_then(|rest| rest.split("\n[").next())
.expect("manifest must have a [dependencies] section");
let repon_core_dependency_line = dependencies_section
.lines()
.find(|line| line.trim_start().starts_with("repon-core"))
.expect("[dependencies] must declare repon-core");
assert!(
!repon_core_dependency_line.contains("test-util"),
"the production [dependencies] entry for repon-core must never request \
`test-util`, or a default build would carry it: {repon_core_dependency_line}"
);
let dev_dependencies_section = manifest
.split("\n[dev-dependencies]")
.nth(1)
.and_then(|rest| rest.split("\n[").next())
.expect("manifest must have a [dev-dependencies] section");
let repon_core_dev_dependency_line = dev_dependencies_section
.lines()
.find(|line| line.trim_start().starts_with("repon-core"))
.expect(
"[dev-dependencies] must declare repon-core with test-util, or this crate's \
own tests could not call Timestamp::at",
);
assert!(
repon_core_dev_dependency_line.contains("test-util"),
"the [dev-dependencies] entry for repon-core must request `test-util`: \
{repon_core_dev_dependency_line}"
);
}
}