feat: add streamd daily command
`streamd daily [YYYYMMDD]` opens the earliest existing daily file (file_type=daily) whose timestamp falls within the target date. If no such file exists it creates `<timestamp>_daily.md` and opens it. Date defaults to today in the repository timezone.
This commit is contained in:
parent
b653590c36
commit
bf700a15af
4 changed files with 108 additions and 0 deletions
|
|
@ -60,6 +60,12 @@ pub enum Commands {
|
||||||
debug: bool,
|
debug: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Open or create the daily entry for a given date
|
||||||
|
Daily {
|
||||||
|
/// Date in YYYYMMDD format (defaults to today in configured timezone)
|
||||||
|
date: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Generate shell completions
|
/// Generate shell completions
|
||||||
Completions {
|
Completions {
|
||||||
/// Shell to generate completions for
|
/// Shell to generate completions for
|
||||||
|
|
|
||||||
100
src/cli/commands/daily.rs
Normal file
100
src/cli/commands/daily.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use chrono::{Days, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
|
||||||
|
use chrono_tz::Tz;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
use crate::config::Settings;
|
||||||
|
use crate::error::StreamdError;
|
||||||
|
use crate::extract::parse_markdown_file;
|
||||||
|
use crate::localize::localize_stream_file;
|
||||||
|
use crate::models::{LocalizedShard, RepositoryConfiguration};
|
||||||
|
use crate::timesheet::load_repository_config;
|
||||||
|
|
||||||
|
fn load_all_shards(base_folder: &Path, tz: Tz) -> Result<Vec<LocalizedShard>, StreamdError> {
|
||||||
|
let config = RepositoryConfiguration::new();
|
||||||
|
let mut shards = Vec::new();
|
||||||
|
|
||||||
|
for entry in WalkDir::new(base_folder)
|
||||||
|
.max_depth(1)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
{
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().map(|e| e == "md").unwrap_or(false) {
|
||||||
|
let file_name = path.to_string_lossy().to_string();
|
||||||
|
let content = fs::read_to_string(path)?;
|
||||||
|
let stream_file = parse_markdown_file(&file_name, &content);
|
||||||
|
|
||||||
|
if let Ok(shard) = localize_stream_file(&stream_file, &config, tz) {
|
||||||
|
shards.push(shard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(shards)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(date: Option<String>) -> Result<(), StreamdError> {
|
||||||
|
let settings = Settings::load()?;
|
||||||
|
let base_folder = Path::new(&settings.base_folder);
|
||||||
|
|
||||||
|
let repo_config = load_repository_config(base_folder)?;
|
||||||
|
let tz: Tz = repo_config
|
||||||
|
.timezone
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(chrono_tz::UTC);
|
||||||
|
|
||||||
|
let target_date: NaiveDate = match date {
|
||||||
|
Some(s) => NaiveDate::parse_from_str(&s, "%Y%m%d").map_err(|_| {
|
||||||
|
StreamdError::ConfigError("Invalid date format, expected YYYYMMDD".into())
|
||||||
|
})?,
|
||||||
|
None => Utc::now().with_timezone(&tz).date_naive(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let day_start = tz
|
||||||
|
.from_local_datetime(&NaiveDateTime::new(target_date, NaiveTime::MIN))
|
||||||
|
.earliest()
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&Utc);
|
||||||
|
let day_end = tz
|
||||||
|
.from_local_datetime(&NaiveDateTime::new(
|
||||||
|
target_date + Days::new(1),
|
||||||
|
NaiveTime::MIN,
|
||||||
|
))
|
||||||
|
.earliest()
|
||||||
|
.unwrap()
|
||||||
|
.with_timezone(&Utc);
|
||||||
|
|
||||||
|
let all_shards = load_all_shards(base_folder, tz)?;
|
||||||
|
let mut daily_shards: Vec<_> = all_shards
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| {
|
||||||
|
s.location
|
||||||
|
.get("file_type")
|
||||||
|
.map(|v| v == "daily")
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.filter(|s| s.moment >= day_start && s.moment < day_end)
|
||||||
|
.collect();
|
||||||
|
daily_shards.sort_by_key(|s| s.moment);
|
||||||
|
|
||||||
|
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
|
||||||
|
|
||||||
|
if let Some(shard) = daily_shards.first() {
|
||||||
|
let file_path = shard.location.get("file").unwrap();
|
||||||
|
Command::new(&editor).arg(file_path).status()?;
|
||||||
|
} else {
|
||||||
|
let now_local = Utc::now().with_timezone(&tz);
|
||||||
|
let file_name = now_local.format("%Y%m%d-%H%M%S_daily.md").to_string();
|
||||||
|
let file_path = base_folder.join(&file_name);
|
||||||
|
fs::write(&file_path, "# ")?;
|
||||||
|
Command::new(&editor).arg(&file_path).status()?;
|
||||||
|
println!("Created {}", file_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod completions;
|
pub mod completions;
|
||||||
|
pub mod daily;
|
||||||
pub mod edit;
|
pub mod edit;
|
||||||
pub mod new;
|
pub mod new;
|
||||||
pub mod timesheet;
|
pub mod timesheet;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ fn main() -> miette::Result<()> {
|
||||||
Some(Commands::Timesheet { decimal, debug }) => {
|
Some(Commands::Timesheet { decimal, debug }) => {
|
||||||
streamd::cli::commands::timesheet::run(decimal, debug)?
|
streamd::cli::commands::timesheet::run(decimal, debug)?
|
||||||
}
|
}
|
||||||
|
Some(Commands::Daily { date }) => streamd::cli::commands::daily::run(date)?,
|
||||||
Some(Commands::Completions { shell }) => {
|
Some(Commands::Completions { shell }) => {
|
||||||
streamd::cli::commands::completions::run(shell);
|
streamd::cli::commands::completions::run(shell);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue