Compare commits

...

5 commits

Author SHA1 Message Date
7f1a1afaf7 chore(deps): lock file maintenance
All checks were successful
Continuous Integration / Lint, Check & Test (push) Successful in 2m35s
Continuous Integration / Build Package (push) Successful in 2m45s
2026-04-13 19:38:22 +02:00
b8a73bfb3e
docs: document file_type prefix convention and streamd daily command
All checks were successful
Release / Build and Release (push) Successful in 6s
Continuous Integration / Build Package (push) Successful in 44s
Continuous Integration / Lint, Check & Test (push) Successful in 1m9s
2026-04-13 19:33:19 +02:00
bf700a15af
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.
2026-04-13 19:31:32 +02:00
b653590c36
feat(localize): extract file_type from filename prefix
Add `extract_file_type_from_file_name` to parse prefixes like `_daily`
from filenames (e.g. `20260412-123456_daily.md` → `"daily"`).

Insert the result into `initial_location` in `localize_stream_file` so
all localized shards carry a `file_type` dimension value.

Also register the `file_type` dimension in `TaskConfiguration` so the
propagation contract is documented.
2026-04-13 19:30:59 +02:00
e15e6f1053
fix: broken tasks extraction
All checks were successful
Release / Build and Release (push) Successful in 6s
Continuous Integration / Lint, Check & Test (push) Successful in 1m33s
Continuous Integration / Build Package (push) Successful in 1m43s
2026-04-13 19:26:31 +02:00
13 changed files with 316 additions and 36 deletions

28
Cargo.lock generated
View file

@ -135,9 +135,9 @@ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "cc"
version = "1.2.59"
version = "1.2.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
dependencies = [
"find-msvc-tools",
"shlex",
@ -432,9 +432,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.94"
version = "0.3.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
dependencies = [
"once_cell",
"wasm-bindgen",
@ -454,9 +454,9 @@ checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
name = "libredox"
version = "0.1.15"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
dependencies = [
"libc",
]
@ -1016,9 +1016,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.117"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
dependencies = [
"cfg-if",
"once_cell",
@ -1029,9 +1029,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.117"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -1039,9 +1039,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.117"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
dependencies = [
"bumpalo",
"proc-macro2",
@ -1052,9 +1052,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.117"
version = "0.2.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
dependencies = [
"unicode-ident",
]

View file

@ -49,11 +49,20 @@ Markdown files are named with a timestamp: `YYYYMMDD-HHMMSS [markers].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.
## Commands
- `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 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`

View file

@ -275,13 +275,18 @@ This allows conditional placements to override base placements.
### R15: File Name Format
Files follow the pattern: `YYYYMMDD-HHMMSS [markers].md`
Files follow the pattern: `YYYYMMDD-HHMMSS[_file_type] [markers].md`
- `YYYYMMDD`: Date (8 digits, required)
- `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
**Extraction regex:** `^(?P<date>\d{8})(?:-(?P<time>\d{4,6}))?.+\.md$`
**Extraction regex for datetime:** `^(?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
@ -387,6 +392,7 @@ Provide recursive search through the shard tree:
| Command | Description |
|---------|-------------|
| `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 --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 |
@ -395,6 +401,23 @@ Provide recursive search through the shard tree:
| `streamd timesheet` | Generate formatted timesheet report with expected/actual hours |
| `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
**Task Numbering:**

24
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"crane": {
"locked": {
"lastModified": 1775236976,
"narHash": "sha256-gCgX+AXN7K1gAIEqcLcZHxmC+QoZcwn9m6Z9r2Az+N8=",
"lastModified": 1775839657,
"narHash": "sha256-SPm9ck7jh3Un9nwPuMGbRU04UroFmOHjLP56T10MOeM=",
"owner": "ipetkov",
"repo": "crane",
"rev": "6c23998526351a53ce734f0ac84940da988ccef1",
"rev": "7cf72d978629469c4bd4206b95c402514c1f6000",
"type": "github"
},
"original": {
@ -40,11 +40,11 @@
]
},
"locked": {
"lastModified": 1775036584,
"narHash": "sha256-zW0lyy7ZNNT/x8JhzFHBsP2IPx7ATZIPai4FJj12BgU=",
"lastModified": 1775585728,
"narHash": "sha256-8Psjt+TWvE4thRKktJsXfR6PA/fWWsZ04DVaY6PUhr4=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "4e0eb042b67d863b1b34b3f64d52ceb9cd926735",
"rev": "580633fa3fe5fc0379905986543fd7495481913d",
"type": "github"
},
"original": {
@ -76,11 +76,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"lastModified": 1775710090,
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
"type": "github"
},
"original": {
@ -105,11 +105,11 @@
]
},
"locked": {
"lastModified": 1775358767,
"narHash": "sha256-f2eC+WIfhjevCPQILuV08i/kmKZzYZpUvkom/33VxCA=",
"lastModified": 1775963625,
"narHash": "sha256-OmwF0Rd/HDbEGC0ZcBS2jPMvmCcn3HDqUypjXrR7KfM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "20fd44bc663daa53a2575e01293e24e681d62244",
"rev": "573a61faa8ec910a6b8576cc3c145844245574f3",
"type": "github"
},
"original": {

View file

@ -60,6 +60,12 @@ pub enum Commands {
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
Completions {
/// Shell to generate completions for

100
src/cli/commands/daily.rs Normal file
View 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(())
}

View file

@ -1,4 +1,5 @@
pub mod completions;
pub mod daily;
pub mod edit;
pub mod new;
pub mod timesheet;

View file

@ -12,6 +12,8 @@ struct BlockInfo {
end_line: usize,
block_type: BlockType,
events: Vec<Event<'static>>,
/// Nested list items contained within this block (for ListItem blocks with sub-lists).
nested_items: Vec<BlockInfo>,
}
#[derive(Debug, Clone, PartialEq)]
@ -110,12 +112,14 @@ pub fn parse_markdown_file(file_name: &str, file_content: &str) -> StreamFile {
fn collect_blocks(content: &str, parser: Parser) -> Vec<BlockInfo> {
let mut blocks = Vec::new();
let mut current_block: Option<BlockInfo> = None;
let _current_events: Vec<Event<'static>> = Vec::new();
let mut depth = 0;
let mut list_items: Vec<BlockInfo> = Vec::new();
let mut in_list = false;
let mut list_start_line = 0;
// Stack for nested lists: (saved current_block, saved list_items, saved list_start_line)
let mut list_nesting_stack: Vec<(Option<BlockInfo>, Vec<BlockInfo>, usize)> = Vec::new();
// Pre-compute line starts for offset-to-line mapping
let line_starts: Vec<usize> = std::iter::once(0)
.chain(content.match_indices('\n').map(|(i, _)| i + 1))
@ -135,6 +139,7 @@ fn collect_blocks(content: &str, parser: Parser) -> Vec<BlockInfo> {
end_line: line,
block_type: BlockType::Paragraph,
events: Vec::new(),
nested_items: Vec::new(),
});
}
depth += 1;
@ -166,6 +171,7 @@ fn collect_blocks(content: &str, parser: Parser) -> Vec<BlockInfo> {
end_line: line,
block_type: BlockType::Heading(heading_level),
events: Vec::new(),
nested_items: Vec::new(),
});
}
depth += 1;
@ -186,7 +192,15 @@ fn collect_blocks(content: &str, parser: Parser) -> Vec<BlockInfo> {
}
}
Event::Start(Tag::List(_)) => {
if !in_list {
if in_list {
// Entering a nested list: save current list item and collected items
list_nesting_stack.push((
current_block.take(),
std::mem::take(&mut list_items),
list_start_line,
));
list_start_line = line;
} else {
in_list = true;
list_start_line = line;
list_items.clear();
@ -195,7 +209,18 @@ fn collect_blocks(content: &str, parser: Parser) -> Vec<BlockInfo> {
}
Event::End(TagEnd::List(_)) => {
depth -= 1;
if depth == 0 && in_list {
if let Some((parent_block, parent_items, parent_start_line)) =
list_nesting_stack.pop()
{
// Nested list ended: attach collected items as nested children of parent item
let nested = std::mem::take(&mut list_items);
list_start_line = parent_start_line;
list_items = parent_items;
current_block = parent_block.map(|mut item| {
item.nested_items = nested;
item
});
} else if depth == 0 && in_list {
in_list = false;
// Create a list block containing all list items
if !list_items.is_empty() {
@ -204,6 +229,7 @@ fn collect_blocks(content: &str, parser: Parser) -> Vec<BlockInfo> {
end_line: line,
block_type: BlockType::List,
events: vec![], // List events are handled through list_items
nested_items: vec![],
});
// Store list items for later processing
for item in list_items.drain(..) {
@ -222,6 +248,7 @@ fn collect_blocks(content: &str, parser: Parser) -> Vec<BlockInfo> {
end_line: line,
block_type: BlockType::ListItem,
events: Vec::new(),
nested_items: Vec::new(),
});
}
}
@ -240,6 +267,7 @@ fn collect_blocks(content: &str, parser: Parser) -> Vec<BlockInfo> {
end_line: line,
block_type: BlockType::CodeBlock,
events: Vec::new(),
nested_items: Vec::new(),
});
}
depth += 1;
@ -507,13 +535,21 @@ fn parse_single_block_shard(
}
}
BlockType::List | BlockType::ListItem => {
// List handling is complex - for now, extract any markers/tags
let (markers, tags) = extract_block_markers_and_tags(block);
if markers.is_empty() {
// Recursively build child shards from nested list items
let children: Vec<Shard> = block
.nested_items
.iter()
.filter_map(|item| {
let (child, _) = parse_single_block_shard(item, item.start_line, item.end_line);
child
})
.collect();
if markers.is_empty() && children.is_empty() {
(None, tags)
} else {
(
Some(build_shard(start_line, end_line, markers, tags, vec![])),
Some(build_shard(start_line, end_line, markers, tags, children)),
vec![],
)
}
@ -716,6 +752,26 @@ mod tests {
);
}
#[test]
fn test_parse_nested_list_creates_three_shards() {
let content = "* @Task 1\n * @Task 2\n* @Task 3";
let result = parse_markdown_file(&make_file_name(), content);
let root = result.shard.unwrap();
// The root shard should have two top-level children: @Task 1 and @Task 3
assert_eq!(root.children.len(), 2, "expected 2 top-level shards");
let task1 = &root.children[0];
let task3 = &root.children[1];
// @Task 1 must carry its marker and contain @Task 2 as a child
assert_eq!(task1.markers, vec!["Task"], "@Task 1 marker");
assert_eq!(task1.children.len(), 1, "@Task 1 should have one child");
let task2 = &task1.children[0];
assert_eq!(task2.markers, vec!["Task"], "@Task 2 marker");
assert!(task2.children.is_empty(), "@Task 2 should have no children");
// @Task 3 is a sibling of @Task 1
assert_eq!(task3.markers, vec!["Task"], "@Task 3 marker");
assert!(task3.children.is_empty(), "@Task 3 should have no children");
}
#[test]
fn test_parse_continues_looking_for_markers_after_first_link_marker() {
let result = parse_markdown_file(

View file

@ -9,6 +9,11 @@ use std::path::Path;
static FILE_NAME_REGEX: Lazy<Regex> =
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).
static DATETIME_MARKER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\d{14}$").unwrap());
@ -62,6 +67,28 @@ pub fn extract_datetime_from_file_name(file_name: &str, tz: Tz) -> Option<DateTi
.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.
fn parse_naive_datetime_from_marker(marker: &str) -> Option<NaiveDateTime> {
if !DATETIME_MARKER_REGEX.is_match(marker) {
@ -155,6 +182,51 @@ mod tests {
use chrono::TimeZone;
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]
fn test_extract_date_from_file_name_valid() {
let file_name = "20230101-123456 Some Text.md";

View file

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

View file

@ -20,6 +20,12 @@ pub static TaskConfiguration: Lazy<RepositoryConfiguration> = Lazy::new(|| {
.with_comment("Project the task is attached to")
.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(
"Task",
Marker::new("Task").with_placements(vec![

View file

@ -5,7 +5,10 @@ use indexmap::{IndexMap, IndexSet};
use crate::error::StreamdError;
use crate::models::{LocalizedShard, RepositoryConfiguration, Shard, StreamFile};
use super::datetime::{extract_datetime_from_file_name, extract_datetime_from_marker_list};
use super::datetime::{
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.
///
@ -102,6 +105,9 @@ pub fn localize_stream_file(
let mut initial_location = IndexMap::new();
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(
shard,

View file

@ -18,6 +18,7 @@ fn main() -> miette::Result<()> {
Some(Commands::Timesheet { decimal, debug }) => {
streamd::cli::commands::timesheet::run(decimal, debug)?
}
Some(Commands::Daily { date }) => streamd::cli::commands::daily::run(date)?,
Some(Commands::Completions { shell }) => {
streamd::cli::commands::completions::run(shell);
}