Add streamd daily. #87

Closed
opened 2026-04-13 19:02:06 +02:00 by kfickel · 2 comments
Owner
  1. If there is a prefix in the filename, add it as the dimension file_type. e.g. 20260412-123456_dailyshould have file_typedaily`.
  2. Add the command streamd daily <date, e.g. 20260312>. This should open up the earliest entry with file_type=daily and moment within date. If no date given, today (in timezone from config) should be used.
    If no such file exist, a daily-file for right now should be created.
1. If there is a prefix in the filename, add it as the dimension `file_type`. e.g. 20260412-123456_daily` should have file_type `daily`. 2. Add the command `streamd daily <date, e.g. 20260312>`. This should open up the earliest entry with `file_type`=`daily` and moment within date. If no date given, today (in timezone from config) should be used. If no such file exist, a daily-file for right now should be created.
kfickel added the
planned
label 2026-04-13 19:06:16 +02:00
Author
Owner

Updated Implementation Plan

Change from previous plan: timezone already exists in RepositoryConfig (in src/timesheet/config.rs), loaded from .streamd.toml via load_repository_config(). Step 1 is dropped — no changes to Settings needed.

Step 1: Add file_type dimension to TaskConfiguration (src/localize/preconfigured.rs)

.with_dimension(
    "file_type",
    Dimension::new("File Type")
        .with_comment("Type of file derived from filename prefix (e.g. 'daily')")
        .with_propagate(true),
)

Propagating ensures child shards (sub-entries) inherit the file type.


Step 2: Extract file_type from filename (src/localize/datetime.rs + src/localize/shard.rs)

Add a helper in datetime.rs:

/// Extracts the file-type prefix from filenames like `20260412-123456_daily.md`.
/// Returns `Some("daily")` for that example, `None` if no `_prefix` is present.
pub fn extract_file_type_from_file_name(file_name: &str) -> Option<String> {
    // regex: after timestamp, look for _word before space or .md
    // e.g. "20260412-123456_daily.md" → "daily"
    //      "20260412-123456_daily Some Title.md" → "daily"
    //      "20260412-123456 Some Title.md" → None
}

Use the regex ^\d{8}(?:-\d{4,6})?_([a-zA-Z0-9]+) on the base filename.

In localize_stream_file (shard.rs), after inserting "file":

if let Some(file_type) = extract_file_type_from_file_name(&stream_file.file_name) {
    initial_location.insert("file_type".to_string(), file_type);
}

Add tests for the new helper covering: with prefix, without prefix, with title after prefix, with full path.


Step 3: Add Daily command variant to src/cli/args.rs

/// Open or create the daily entry for a given date
Daily {
    /// Date in YYYYMMDD format (defaults to today in configured timezone)
    date: Option<String>,
},

Step 4: Implement src/cli/commands/daily.rs

Uses the existing load_repository_config() + RepositoryConfig.timezone (same as timesheet command):

pub fn run(date: Option<String>) -> Result<(), StreamdError> {
    let settings = Settings::load()?;
    let base_folder = Path::new(&settings.base_folder);

    // Reuse existing RepositoryConfig for timezone
    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);

    // Determine target date
    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(),
    };

    // Compute UTC day bounds
    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);

    // Load and filter shards
    let all_shards = all_files(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);

    if let Some(shard) = daily_shards.first() {
        // Open existing file
        open_in_editor(shard.location.get("file").unwrap())?;
    } else {
        // Create new daily file
        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, "# ")?;
        open_in_editor(&file_path.to_string_lossy())?;
        println!("Created {}", file_name);
    }
    Ok(())
}

Key design decision: the _daily suffix is permanent (not renamed away like _wip in new.rs), because the prefix is what identifies the file type.


Step 5: Wire up in src/main.rs and src/cli/commands/mod.rs

In mod.rs: pub mod daily;

In main.rs:

Some(Commands::Daily { date }) => streamd::cli::commands::daily::run(date)?,

Step 6: Update REQUIREMENTS.md and README.md

Document:

  • The _<file_type> filename prefix convention
  • The streamd daily [YYYYMMDD] command (timezone comes from existing .streamd.toml timezone field)

Files to create/modify

File Action
src/localize/preconfigured.rs Add file_type dimension
src/localize/datetime.rs Add extract_file_type_from_file_name()
src/localize/shard.rs Use new helper in localize_stream_file()
src/cli/args.rs Add Daily variant
src/cli/commands/daily.rs New file — full command implementation
src/cli/commands/mod.rs Export daily module
src/main.rs Add match arm
REQUIREMENTS.md Document new features
README.md Document new features
## Updated Implementation Plan > **Change from previous plan:** `timezone` already exists in `RepositoryConfig` (in `src/timesheet/config.rs`), loaded from `.streamd.toml` via `load_repository_config()`. Step 1 is dropped — no changes to `Settings` needed. ### Step 1: Add `file_type` dimension to `TaskConfiguration` (`src/localize/preconfigured.rs`) ```rust .with_dimension( "file_type", Dimension::new("File Type") .with_comment("Type of file derived from filename prefix (e.g. 'daily')") .with_propagate(true), ) ``` Propagating ensures child shards (sub-entries) inherit the file type. --- ### Step 2: Extract `file_type` from filename (`src/localize/datetime.rs` + `src/localize/shard.rs`) Add a helper in `datetime.rs`: ```rust /// Extracts the file-type prefix from filenames like `20260412-123456_daily.md`. /// Returns `Some("daily")` for that example, `None` if no `_prefix` is present. pub fn extract_file_type_from_file_name(file_name: &str) -> Option<String> { // regex: after timestamp, look for _word before space or .md // e.g. "20260412-123456_daily.md" → "daily" // "20260412-123456_daily Some Title.md" → "daily" // "20260412-123456 Some Title.md" → None } ``` Use the regex `^\d{8}(?:-\d{4,6})?_([a-zA-Z0-9]+)` on the base filename. In `localize_stream_file` (`shard.rs`), after inserting `"file"`: ```rust if let Some(file_type) = extract_file_type_from_file_name(&stream_file.file_name) { initial_location.insert("file_type".to_string(), file_type); } ``` Add tests for the new helper covering: with prefix, without prefix, with title after prefix, with full path. --- ### Step 3: Add `Daily` command variant to `src/cli/args.rs` ```rust /// Open or create the daily entry for a given date Daily { /// Date in YYYYMMDD format (defaults to today in configured timezone) date: Option<String>, }, ``` --- ### Step 4: Implement `src/cli/commands/daily.rs` Uses the existing `load_repository_config()` + `RepositoryConfig.timezone` (same as `timesheet` command): ```rust pub fn run(date: Option<String>) -> Result<(), StreamdError> { let settings = Settings::load()?; let base_folder = Path::new(&settings.base_folder); // Reuse existing RepositoryConfig for timezone 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); // Determine target date 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(), }; // Compute UTC day bounds 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); // Load and filter shards let all_shards = all_files(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); if let Some(shard) = daily_shards.first() { // Open existing file open_in_editor(shard.location.get("file").unwrap())?; } else { // Create new daily file 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, "# ")?; open_in_editor(&file_path.to_string_lossy())?; println!("Created {}", file_name); } Ok(()) } ``` Key design decision: the `_daily` suffix is permanent (not renamed away like `_wip` in `new.rs`), because the prefix is what identifies the file type. --- ### Step 5: Wire up in `src/main.rs` and `src/cli/commands/mod.rs` In `mod.rs`: `pub mod daily;` In `main.rs`: ```rust Some(Commands::Daily { date }) => streamd::cli::commands::daily::run(date)?, ``` --- ### Step 6: Update `REQUIREMENTS.md` and `README.md` Document: - The `_<file_type>` filename prefix convention - The `streamd daily [YYYYMMDD]` command (timezone comes from existing `.streamd.toml` `timezone` field) --- ### Files to create/modify | File | Action | |---|---| | `src/localize/preconfigured.rs` | Add `file_type` dimension | | `src/localize/datetime.rs` | Add `extract_file_type_from_file_name()` | | `src/localize/shard.rs` | Use new helper in `localize_stream_file()` | | `src/cli/args.rs` | Add `Daily` variant | | `src/cli/commands/daily.rs` | New file — full command implementation | | `src/cli/commands/mod.rs` | Export `daily` module | | `src/main.rs` | Add match arm | | `REQUIREMENTS.md` | Document new features | | `README.md` | Document new features |
Author
Owner

Implementation Summary

Implemented in PR #88.

Duration: ~25 minutes
Tokens: ~50k (estimate based on session length)

What was done

  1. file_type extraction — Added extract_file_type_from_file_name in localize/datetime.rs using regex ^\d{8}(?:-\d{4,6})?_([a-zA-Z0-9]+). Wired into localize_stream_file so the value is inserted into initial_location and propagates to all children automatically (no config change needed for propagation, since initial_location values flow through the propagated map). Also registered the dimension in TaskConfiguration for documentation.

  2. streamd daily command — New file src/cli/commands/daily.rs. Reuses load_repository_config from the timesheet module for timezone. Loads all .md files, filters to file_type=daily within the day's UTC bounds, sorts by moment, and opens the earliest match. Creates <timestamp>_daily.md with # content if no match.

Key findings

  • The initial_location map passed to localize_shard as propagated means filename-derived values (like file_type) propagate to all child shards for free — no dimension entry in RepositoryConfiguration is needed for propagation (though it was added for documentation).
  • The _daily prefix is permanent (unlike _wip in new.rs) because it's the mechanism for identifying the file type.
  • 174 tests pass, nix flake check clean.
## Implementation Summary Implemented in PR #88. **Duration:** ~25 minutes **Tokens:** ~50k (estimate based on session length) ### What was done 1. **`file_type` extraction** — Added `extract_file_type_from_file_name` in `localize/datetime.rs` using regex `^\d{8}(?:-\d{4,6})?_([a-zA-Z0-9]+)`. Wired into `localize_stream_file` so the value is inserted into `initial_location` and propagates to all children automatically (no config change needed for propagation, since initial_location values flow through the propagated map). Also registered the dimension in `TaskConfiguration` for documentation. 2. **`streamd daily` command** — New file `src/cli/commands/daily.rs`. Reuses `load_repository_config` from the timesheet module for timezone. Loads all `.md` files, filters to `file_type=daily` within the day's UTC bounds, sorts by moment, and opens the earliest match. Creates `<timestamp>_daily.md` with `# ` content if no match. ### Key findings - The `initial_location` map passed to `localize_shard` as `propagated` means filename-derived values (like `file_type`) propagate to all child shards for free — no dimension entry in `RepositoryConfiguration` is needed for propagation (though it was added for documentation). - The `_daily` prefix is permanent (unlike `_wip` in `new.rs`) because it's the mechanism for identifying the file type. - 174 tests pass, `nix flake check` clean.
Sign in to join this conversation.
No labels
planned
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference: kfickel/streamd#87
No description provided.