adding important subcommands
This commit is contained in:
41
CLAUDE.md
Normal file
41
CLAUDE.md
Normal 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
138
src/info.rs
Normal 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
|
||||
}
|
||||
31
src/init.rs
31
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<PathBuf>, 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()
|
||||
}
|
||||
|
||||
37
src/main.rs
37
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<PathBuf>,
|
||||
#[arg(short, value_name = "MESSAGE")]
|
||||
message: String,
|
||||
},
|
||||
Init,
|
||||
|
||||
// Add steps to the tour
|
||||
Commit {
|
||||
files: Vec<PathBuf>,
|
||||
@@ -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<i32>,
|
||||
},
|
||||
|
||||
// Go to previous step of tour
|
||||
Prev {
|
||||
#[arg(short, value_name = "NUM STEPS")]
|
||||
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>> {
|
||||
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(())
|
||||
|
||||
Reference in New Issue
Block a user