Compare commits

..

No commits in common. "b8a73bfb3e2c393e9cf608f0b5235e56c903422b" and "e15e6f105313dc373650ad6a51a3390b4921c296" have entirely different histories.

10 changed files with 4 additions and 228 deletions

View file

@ -49,20 +49,11 @@ Markdown files are named with a timestamp: `YYYYMMDD-HHMMSS [markers].md`
For example: `20260131-210000 Task Streamd.md` For example: `20260131-210000 Task Streamd.md`
An optional `_file_type` segment can follow the timestamp to classify the file:
```
YYYYMMDD-HHMMSS_<file_type> [markers].md
```
For example: `20260413-083000_daily.md` — the `daily` prefix is stored as the `file_type` dimension and propagates to all child shards.
Within files, `@`-prefixed markers at the beginning of paragraphs or headings define how a shard is categorized. Within files, `@`-prefixed markers at the beginning of paragraphs or headings define how a shard is categorized.
## Commands ## Commands
- `streamd` / `streamd new` — Create a new timestamped markdown entry, opening your editor - `streamd` / `streamd new` — Create a new timestamped markdown entry, opening your editor
- `streamd daily [YYYYMMDD]` — Open today's daily file (or create it if missing); pass a date to open that day's file instead
- `streamd todo` — Show all open tasks (shards with `@Task` markers), numbered for easy reference - `streamd todo` — Show all open tasks (shards with `@Task` markers), numbered for easy reference
- `streamd todo N edit` — Edit task N in your editor, jumping to the task's line - `streamd todo N edit` — Edit task N in your editor, jumping to the task's line
- `streamd todo N done` — Mark task N as done by inserting `@Done` after `@Task` - `streamd todo N done` — Mark task N as done by inserting `@Done` after `@Task`

View file

@ -275,18 +275,13 @@ This allows conditional placements to override base placements.
### R15: File Name Format ### R15: File Name Format
Files follow the pattern: `YYYYMMDD-HHMMSS[_file_type] [markers].md` Files follow the pattern: `YYYYMMDD-HHMMSS [markers].md`
- `YYYYMMDD`: Date (8 digits, required) - `YYYYMMDD`: Date (8 digits, required)
- `HHMMSS`: Time (4-6 digits, optional, pads with zeros) - `HHMMSS`: Time (4-6 digits, optional, pads with zeros)
- `_file_type`: Optional alphanumeric prefix identifying the file type (e.g. `_daily`)
- `[markers]`: Space-separated marker names extracted from file content - `[markers]`: Space-separated marker names extracted from file content
**Extraction regex for datetime:** `^(?P<date>\d{8})(?:-(?P<time>\d{4,6}))?.+\.md$` **Extraction regex:** `^(?P<date>\d{8})(?:-(?P<time>\d{4,6}))?.+\.md$`
**Extraction regex for file type:** `^\d{8}(?:-\d{4,6})?_([a-zA-Z0-9]+)`
When a `_file_type` prefix is present it is stored in the `file_type` dimension of the root shard and propagates to all child shards.
### R16: Temporal Markers ### R16: Temporal Markers
@ -392,7 +387,6 @@ Provide recursive search through the shard tree:
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `streamd new` | Create new timestamped file, open editor, rename with markers on close | | `streamd new` | Create new timestamped file, open editor, rename with markers on close |
| `streamd daily [YYYYMMDD]` | Open the earliest daily file for the given date (default: today in configured timezone), or create a new `_daily` file if none exists |
| `streamd todo` | List all shards with `task: "open"`, numbered, hiding future tasks | | `streamd todo` | List all shards with `task: "open"`, numbered, hiding future tasks |
| `streamd todo --show-future` | Include tasks with future dates in the todo listing | | `streamd todo --show-future` | Include tasks with future dates in the todo listing |
| `streamd todo N edit` | Edit task N in editor, cursor positioned at task line | | `streamd todo N edit` | Edit task N in editor, cursor positioned at task line |
@ -401,23 +395,6 @@ Provide recursive search through the shard tree:
| `streamd timesheet` | Generate formatted timesheet report with expected/actual hours | | `streamd timesheet` | Generate formatted timesheet report with expected/actual hours |
| `streamd completions <shell>` | Generate shell completions (bash, zsh, fish, elvish, powershell) | | `streamd completions <shell>` | Generate shell completions (bash, zsh, fish, elvish, powershell) |
### R21a: Daily Command Behavior
`streamd daily [YYYYMMDD]` provides quick access to the daily journal entry for a given date.
**Date resolution:**
- If a `YYYYMMDD` argument is provided, it is parsed as the target date.
- If no argument is given, today's date is used, interpreted in the repository timezone (from `.streamd.toml`, defaulting to UTC).
**File lookup:**
- All markdown files in the base folder are localized.
- Files with `file_type = "daily"` whose root shard `moment` falls within the target date (in the configured timezone) are collected.
- The file with the earliest `moment` is opened in `$EDITOR` (defaults to `vi`).
**File creation:**
- If no matching file is found, a new file is created at `<now_local>_daily.md` (e.g. `20260413-083000_daily.md`) containing `# ` and opened in the editor.
- The `_daily` suffix is permanent — it identifies the file type and is not renamed after editing.
### R21: Todo Command Behavior ### R21: Todo Command Behavior
**Task Numbering:** **Task Numbering:**

View file

@ -60,12 +60,6 @@ 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

View file

@ -1,100 +0,0 @@
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(())
}

View file

@ -1,5 +1,4 @@
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;

View file

@ -9,11 +9,6 @@ use std::path::Path;
static FILE_NAME_REGEX: Lazy<Regex> = static FILE_NAME_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?P<date>\d{8})(?:-(?P<time>\d{4,6}))?.+\.md$").unwrap()); Lazy::new(|| Regex::new(r"^(?P<date>\d{8})(?:-(?P<time>\d{4,6}))?.+\.md$").unwrap());
/// Regex for extracting a file-type prefix from file names.
/// Matches filenames like `20260412-123456_daily.md` or `20260412_daily Some Title.md`.
static FILE_TYPE_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\d{8}(?:-\d{4,6})?_([a-zA-Z0-9]+)").unwrap());
/// Regex for validating datetime marker format (14 digits). /// Regex for validating datetime marker format (14 digits).
static DATETIME_MARKER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\d{14}$").unwrap()); static DATETIME_MARKER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\d{14}$").unwrap());
@ -67,28 +62,6 @@ pub fn extract_datetime_from_file_name(file_name: &str, tz: Tz) -> Option<DateTi
.and_then(|dt| naive_to_utc(dt, tz)) .and_then(|dt| naive_to_utc(dt, tz))
} }
/// Extract the file-type prefix from a filename.
///
/// Filenames with a `_prefix` segment after the timestamp (and optional time component)
/// are recognised. The prefix must consist of alphanumeric characters only.
///
/// # Examples
/// - `"20260412-123456_daily.md"` → `Some("daily")`
/// - `"20260412_daily Some Title.md"` → `Some("daily")`
/// - `"20260412-123456 Some Title.md"` → `None`
/// - `"/path/to/20260412-123456_daily.md"` → `Some("daily")`
pub fn extract_file_type_from_file_name(file_name: &str) -> Option<String> {
let base_name = Path::new(file_name)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(file_name);
FILE_TYPE_REGEX
.captures(base_name)
.and_then(|c| c.get(1))
.map(|m| m.as_str().to_string())
}
/// Parse a 14-digit marker string as a NaiveDateTime without timezone conversion. /// Parse a 14-digit marker string as a NaiveDateTime without timezone conversion.
fn parse_naive_datetime_from_marker(marker: &str) -> Option<NaiveDateTime> { fn parse_naive_datetime_from_marker(marker: &str) -> Option<NaiveDateTime> {
if !DATETIME_MARKER_REGEX.is_match(marker) { if !DATETIME_MARKER_REGEX.is_match(marker) {
@ -182,51 +155,6 @@ mod tests {
use chrono::TimeZone; use chrono::TimeZone;
use chrono_tz::UTC; use chrono_tz::UTC;
#[test]
fn test_extract_file_type_with_time() {
assert_eq!(
extract_file_type_from_file_name("20260412-123456_daily.md"),
Some("daily".to_string())
);
}
#[test]
fn test_extract_file_type_with_time_and_title() {
assert_eq!(
extract_file_type_from_file_name("20260412-123456_daily Some Title.md"),
Some("daily".to_string())
);
}
#[test]
fn test_extract_file_type_without_time() {
assert_eq!(
extract_file_type_from_file_name("20260412_daily.md"),
Some("daily".to_string())
);
}
#[test]
fn test_extract_file_type_without_prefix() {
assert_eq!(
extract_file_type_from_file_name("20260412-123456 Some Title.md"),
None
);
}
#[test]
fn test_extract_file_type_with_full_path() {
assert_eq!(
extract_file_type_from_file_name("/path/to/20260412-123456_daily.md"),
Some("daily".to_string())
);
}
#[test]
fn test_extract_file_type_no_timestamp() {
assert_eq!(extract_file_type_from_file_name("notes.md"), None);
}
#[test] #[test]
fn test_extract_date_from_file_name_valid() { fn test_extract_date_from_file_name_valid() {
let file_name = "20230101-123456 Some Text.md"; let file_name = "20230101-123456 Some Text.md";

View file

@ -9,7 +9,7 @@ pub use configuration::{
}; };
pub use datetime::{ pub use datetime::{
extract_date_from_marker, extract_datetime_from_file_name, extract_datetime_from_marker, extract_date_from_marker, extract_datetime_from_file_name, extract_datetime_from_marker,
extract_datetime_from_marker_list, extract_file_type_from_file_name, extract_time_from_marker, extract_datetime_from_marker_list, extract_time_from_marker,
}; };
pub use preconfigured::TaskConfiguration; pub use preconfigured::TaskConfiguration;
pub use shard::{localize_shard, localize_stream_file}; pub use shard::{localize_shard, localize_stream_file};

View file

@ -20,12 +20,6 @@ pub static TaskConfiguration: Lazy<RepositoryConfiguration> = Lazy::new(|| {
.with_comment("Project the task is attached to") .with_comment("Project the task is attached to")
.with_propagate(true), .with_propagate(true),
) )
.with_dimension(
"file_type",
Dimension::new("File Type")
.with_comment("Type of file derived from filename prefix (e.g. 'daily')")
.with_propagate(true),
)
.with_marker( .with_marker(
"Task", "Task",
Marker::new("Task").with_placements(vec![ Marker::new("Task").with_placements(vec![

View file

@ -5,10 +5,7 @@ use indexmap::{IndexMap, IndexSet};
use crate::error::StreamdError; use crate::error::StreamdError;
use crate::models::{LocalizedShard, RepositoryConfiguration, Shard, StreamFile}; use crate::models::{LocalizedShard, RepositoryConfiguration, Shard, StreamFile};
use super::datetime::{ use super::datetime::{extract_datetime_from_file_name, extract_datetime_from_marker_list};
extract_datetime_from_file_name, extract_datetime_from_marker_list,
extract_file_type_from_file_name,
};
/// Localize a shard within the repository's coordinate system. /// Localize a shard within the repository's coordinate system.
/// ///
@ -105,9 +102,6 @@ pub fn localize_stream_file(
let mut initial_location = IndexMap::new(); let mut initial_location = IndexMap::new();
initial_location.insert("file".to_string(), stream_file.file_name.clone()); initial_location.insert("file".to_string(), stream_file.file_name.clone());
if let Some(file_type) = extract_file_type_from_file_name(&stream_file.file_name) {
initial_location.insert("file_type".to_string(), file_type);
}
Ok(localize_shard( Ok(localize_shard(
shard, shard,

View file

@ -18,7 +18,6 @@ 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);
} }