adding important subcommands

This commit is contained in:
2026-03-02 21:55:37 +00:00
parent 1dea592fb2
commit c0a71bc285
4 changed files with 217 additions and 30 deletions

41
CLAUDE.md Normal file
View File

@@ -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 -- <cmd> # run a subcommand (e.g. cargo run -- init)
cargo test # run all tests
cargo test <name> # 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 <files> -m <msg>` (repeat) → `tour end -m <msg>`
**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/<N>/` — one numbered directory per step
- `.tour/session` — tracks current reader position as `STEP=<n>`
**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).

138
src/info.rs Normal file
View File

@@ -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
}

View File

@@ -1,25 +1,20 @@
// Create a .tour folder // Create a .tour folder
// Populate with tour/steps/0/message // Create directory ./.tour/steps that stores tutorial steps
// Populate with tour/steps/0/files/file1 // Create file ./.tour/session that logs sessions information
// Populate with tour/steps/0/files/file2
use crate::TOUR_DIR; use crate::TOUR_DIR;
use crate::utils::copy_files; use std::fs::DirBuilder;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
pub fn init(files: Vec<PathBuf>, message: String) -> Result<(), std::io::Error> { // Creates the directory for tour
// // Convert PathBuf to &Path pub fn init() -> Result<(), std::io::Error> {
// let files = files.iter().map(|p| p.as_path()).collect(); let tour_dir = PathBuf::from(TOUR_DIR);
//
// // Check TOUR_DIR exists (it shouldn't because user calls init)
// if fs::exists(TOUR_DIR)? {
// panic!("{} folder exists", TOUR_DIR);
// }
// // Create dir recursively
Ok(()) DirBuilder::new()
.recursive(true)
.create(tour_dir.join("steps"))?;
std::fs::File::create(tour_dir.join("session"))?;
crate::info::set_info()
} }

View File

@@ -4,13 +4,14 @@ use std::path::PathBuf;
mod commit; mod commit;
mod end; mod end;
mod error;
mod init; mod init;
mod next; mod step;
mod prev;
mod utils; mod utils;
mod info;
const TOUR_DIR: &str = "./.tour"; const TOUR_DIR: &str = "./.tour";
const DEFAULT_SESSION: &str = "./.tour/session"; const SESSION_PATH: &str = "./.tour/session";
#[derive(Parser)] #[derive(Parser)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@@ -22,12 +23,8 @@ struct Args {
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
// Create a new tour // Create a new tour
Init { Init,
#[arg(value_name = "FILES")]
files: Vec<PathBuf>,
#[arg(short, value_name = "MESSAGE")]
message: String,
},
// Add steps to the tour // Add steps to the tour
Commit { Commit {
files: Vec<PathBuf>, files: Vec<PathBuf>,
@@ -35,6 +32,7 @@ enum Commands {
#[arg(short, long, value_name = "MESSAGE")] #[arg(short, long, value_name = "MESSAGE")]
message: String, message: String,
}, },
// Finish the tour // Finish the tour
End { End {
#[arg(short, long, value_name = "MESSAGE")] #[arg(short, long, value_name = "MESSAGE")]
@@ -46,21 +44,36 @@ enum Commands {
#[arg(short, value_name = "NUM STEPS")] #[arg(short, value_name = "NUM STEPS")]
n: Option<i32>, n: Option<i32>,
}, },
// Go to previous step of tour // Go to previous step of tour
Prev { Prev {
#[arg(short, value_name = "NUM STEPS")] #[arg(short, value_name = "NUM STEPS")]
n: Option<i32>, n: Option<i32>,
}, },
// 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<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse(); let args = Args::parse();
match args.command { 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::Commit { files, message }) => crate::commit::commit(files, message)?,
Some(Commands::End { message }) => crate::end::end(message)?, Some(Commands::End { message }) => crate::end::end(message)?,
Some(Commands::Next { n }) => crate::next::next(n)?, Some(Commands::Next { n }) => crate::step::next(n)?,
Some(Commands::Prev { n }) => crate::prev::prev(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"), _ => println!("command not found"),
} }
Ok(()) Ok(())