diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ec8e628 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,41 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```sh +cargo build # build the project +cargo run -- # run a subcommand (e.g. cargo run -- init) +cargo test # run all tests +cargo test # run a single test by name +cargo clippy # lint +``` + +## What This Project Is + +`tour` is a CLI tool for creating and navigating code tutorials as a series of snapshots. Authors create tours by committing file snapshots with explanations; readers step through them with `next`/`prev`. + +**Author workflow:** `tour init` → `tour commit -m ` (repeat) → `tour end -m ` + +**Reader workflow:** `tour start` → `tour next [n]` / `tour prev [n]` + +## Architecture + +Entry point is `main.rs`, which uses clap's derive macro to parse subcommands and dispatch to per-command modules. + +**On-disk format** (stored in `.tour/` in the project being toured): +- `.tour/steps//` — one numbered directory per step +- `.tour/session` — tracks current reader position as `STEP=` + +**Module layout:** +- `init.rs` — creates `.tour/steps/` and `.tour/session` +- `commit.rs` — validates files, then saves them as a new numbered step +- `end.rs` — finalizes the tour +- `next.rs` / `prev.rs` — advance/retreat the session step +- `utils.rs` — shared helpers: `copy_files`, `get_session_step`, `get_tour_step`, path validation +- `error.rs` — custom error types (currently `CommitError`) + +Constants `TOUR_DIR` and `SESSION_PATH` are defined in `main.rs` and imported via `crate::`. + +**Status:** Early development. `next`, `prev`, and `end` are stubbed. `commit` validates paths but hasn't yet written the step to disk. `utils::get_tour_step` has a dead `Ok(0)` after a `match` expression (unreachable code). diff --git a/src/info.rs b/src/info.rs new file mode 100644 index 0000000..0b562b1 --- /dev/null +++ b/src/info.rs @@ -0,0 +1,138 @@ +use std::fs; +use std::io::{self, Write}; +use std::time::{SystemTime, UNIX_EPOCH}; + +const INFO_PATH: &str = "./.tour/info"; + +struct TourInfo { + author: String, + description: String, + language: String, + created: String, + updated: String, +} + +impl TourInfo { + fn parse(content: &str) -> Self { + let mut author = String::new(); + let mut description = String::new(); + let mut language = String::new(); + let mut created = String::new(); + let mut updated = String::new(); + + for line in content.lines() { + if let Some((key, value)) = line.split_once('=') { + match key { + "author" => author = value.to_string(), + "description" => description = value.to_string(), + "language" => language = value.to_string(), + "created" => created = value.to_string(), + "updated" => updated = value.to_string(), + _ => {} + } + } + } + + TourInfo { author, description, language, created, updated } + } + + fn serialize(&self) -> String { + format!( + "author={}\ndescription={}\nlanguage={}\ncreated={}\nupdated={}\n", + self.author, self.description, self.language, self.created, self.updated + ) + } +} + +pub fn set_info() -> Result<(), io::Error> { + macro_rules! prompt { + ($msg:expr) => {{ + print!($msg); + io::stdout().flush()?; + let mut buf = String::new(); + io::stdin().read_line(&mut buf)?; + buf.trim().to_string() + }}; + } + + let author = prompt!("Author: "); + let description = prompt!("Description: "); + let language = prompt!("Language: "); + let today = current_date(); + + let info = TourInfo { + author, + description, + language, + created: today.clone(), + updated: today, + }; + + fs::write(INFO_PATH, info.serialize()) +} + +pub fn get_info() -> Result<(), io::Error> { + let content = fs::read_to_string(INFO_PATH).map_err(|_| { + io::Error::new( + io::ErrorKind::NotFound, + "No tour info found. Run `tour init` to set up a tour.", + ) + })?; + let info = TourInfo::parse(&content); + println!("Author: {}", info.author); + println!("Description: {}", info.description); + println!("Language: {}", info.language); + println!("Created: {}", info.created); + println!("Updated: {}", info.updated); + Ok(()) +} + +pub fn update_last_modified() -> Result<(), io::Error> { + let content = fs::read_to_string(INFO_PATH).unwrap_or_default(); + let mut info = TourInfo::parse(&content); + info.updated = current_date(); + fs::write(INFO_PATH, info.serialize()) +} + +pub fn info() -> Result<(), io::Error> { + get_info() +} + +fn current_date() -> String { + let secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let (y, m, d) = days_to_ymd((secs / 86400) as u32); + format!("{:04}-{:02}-{:02}", y, m, d) +} + +fn days_to_ymd(mut days: u32) -> (u32, u32, u32) { + let mut year = 1970u32; + loop { + let days_in_year = if is_leap(year) { 366 } else { 365 }; + if days < days_in_year { + break; + } + days -= days_in_year; + year += 1; + } + let month_days = if is_leap(year) { + [31u32, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + } else { + [31u32, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + }; + let mut month = 1u32; + for (i, &md) in month_days.iter().enumerate() { + if days < md { + month = i as u32 + 1; + break; + } + days -= md; + } + (year, month, days + 1) +} + +fn is_leap(year: u32) -> bool { + (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 +} diff --git a/src/init.rs b/src/init.rs index 38ef83e..3d5c0f8 100644 --- a/src/init.rs +++ b/src/init.rs @@ -1,25 +1,20 @@ // Create a .tour folder -// Populate with tour/steps/0/message -// Populate with tour/steps/0/files/file1 -// Populate with tour/steps/0/files/file2 +// Create directory ./.tour/steps that stores tutorial steps +// Create file ./.tour/session that logs sessions information use crate::TOUR_DIR; -use crate::utils::copy_files; - -use std::fs; -use std::fs::File; -use std::io::Write; +use std::fs::DirBuilder; use std::path::PathBuf; -pub fn init(files: Vec, message: String) -> Result<(), std::io::Error> { - // // Convert PathBuf to &Path - // let files = files.iter().map(|p| p.as_path()).collect(); - // - // // Check TOUR_DIR exists (it shouldn't because user calls init) - // if fs::exists(TOUR_DIR)? { - // panic!("{} folder exists", TOUR_DIR); - // } - // // Create dir recursively +// Creates the directory for tour +pub fn init() -> Result<(), std::io::Error> { + let tour_dir = PathBuf::from(TOUR_DIR); - Ok(()) + DirBuilder::new() + .recursive(true) + .create(tour_dir.join("steps"))?; + + std::fs::File::create(tour_dir.join("session"))?; + + crate::info::set_info() } diff --git a/src/main.rs b/src/main.rs index 316f4d4..7b36ead 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,13 +4,14 @@ use std::path::PathBuf; mod commit; mod end; +mod error; mod init; -mod next; -mod prev; +mod step; mod utils; +mod info; const TOUR_DIR: &str = "./.tour"; -const DEFAULT_SESSION: &str = "./.tour/session"; +const SESSION_PATH: &str = "./.tour/session"; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -22,12 +23,8 @@ struct Args { #[derive(Subcommand)] enum Commands { // Create a new tour - Init { - #[arg(value_name = "FILES")] - files: Vec, - #[arg(short, value_name = "MESSAGE")] - message: String, - }, + Init, + // Add steps to the tour Commit { files: Vec, @@ -35,6 +32,7 @@ enum Commands { #[arg(short, long, value_name = "MESSAGE")] message: String, }, + // Finish the tour End { #[arg(short, long, value_name = "MESSAGE")] @@ -46,21 +44,36 @@ enum Commands { #[arg(short, value_name = "NUM STEPS")] n: Option, }, + // Go to previous step of tour Prev { #[arg(short, value_name = "NUM STEPS")] n: Option, }, + + // Go to a specific step of tour + Step { + #[arg(value_name = "STEP")] + n: i32, + }, + + // Go to beginning of tour + Start, + + Info, } fn main() -> Result<(), Box> { let args = Args::parse(); match args.command { - Some(Commands::Init { files, message }) => crate::init::init(files, message)?, + Some(Commands::Init) => crate::init::init()?, Some(Commands::Commit { files, message }) => crate::commit::commit(files, message)?, Some(Commands::End { message }) => crate::end::end(message)?, - Some(Commands::Next { n }) => crate::next::next(n)?, - Some(Commands::Prev { n }) => crate::prev::prev(n)?, + Some(Commands::Next { n }) => crate::step::next(n)?, + Some(Commands::Prev { n }) => crate::step::prev(n)?, + Some(Commands::Step { n }) => crate::step::step_n(n)?, + Some(Commands::Start) => crate::step::step_n(0)?, + Some(Commands::Info) => crate::info::info()?, _ => println!("command not found"), } Ok(())