From 4ded06748ba3e084caa59b8069062a030de95010 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Tue, 7 Apr 2026 13:49:12 +0200 Subject: [PATCH 1/8] fix: cross-platform compatibility for Windows support - Use directories::BaseDirs for config path fallback instead of hardcoded Unix path - Default to notepad on Windows instead of vi for editor commands - Skip +N line argument for notepad in todo edit (notepad doesn't support it) --- src/cli/commands/edit.rs | 8 +++++++- src/cli/commands/new.rs | 8 +++++++- src/cli/commands/todo.rs | 18 ++++++++++++------ src/config.rs | 4 +++- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/cli/commands/edit.rs b/src/cli/commands/edit.rs index a8761f3..4c15017 100644 --- a/src/cli/commands/edit.rs +++ b/src/cli/commands/edit.rs @@ -67,7 +67,13 @@ pub fn run(number: i32) -> Result<(), StreamdError> { }; if let Some(file_path) = sorted_shards[selected_index].location.get("file") { - let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); + let editor = std::env::var("EDITOR").unwrap_or_else(|_| { + if cfg!(windows) { + "notepad".to_string() + } else { + "vi".to_string() + } + }); Command::new(&editor).arg(file_path).status()?; } diff --git a/src/cli/commands/new.rs b/src/cli/commands/new.rs index de7d351..5c3eecb 100644 --- a/src/cli/commands/new.rs +++ b/src/cli/commands/new.rs @@ -24,7 +24,13 @@ pub fn run() -> Result<(), StreamdError> { drop(file); // Open in editor - let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); + let editor = std::env::var("EDITOR").unwrap_or_else(|_| { + if cfg!(windows) { + "notepad".to_string() + } else { + "vi".to_string() + } + }); let status = Command::new(&editor).arg(&preliminary_path).status()?; if !status.success() { diff --git a/src/cli/commands/todo.rs b/src/cli/commands/todo.rs index 17bfba2..e54d1e2 100644 --- a/src/cli/commands/todo.rs +++ b/src/cli/commands/todo.rs @@ -92,13 +92,19 @@ pub fn run_edit(number: usize) -> Result<(), StreamdError> { .get("file") .ok_or(StreamdError::MissingFilePath)?; - let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); - let line_arg = format!("+{}", task.start_line); + let editor = std::env::var("EDITOR").unwrap_or_else(|_| { + if cfg!(windows) { + "notepad".to_string() + } else { + "vi".to_string() + } + }); - let status = Command::new(&editor) - .arg(&line_arg) - .arg(file_path) - .status()?; + let mut cmd = Command::new(&editor); + if !editor.to_lowercase().contains("notepad") { + cmd.arg(format!("+{}", task.start_line)); + } + let status = cmd.arg(file_path).status()?; if !status.success() { return Err(StreamdError::IoError(std::io::Error::other( diff --git a/src/config.rs b/src/config.rs index c828ba9..417e8f0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -37,8 +37,10 @@ impl Settings { fn config_path() -> PathBuf { if let Some(proj_dirs) = ProjectDirs::from("", "", "streamd") { proj_dirs.config_dir().join("config.toml") + } else if let Some(base_dirs) = directories::BaseDirs::new() { + base_dirs.config_dir().join("streamd").join("config.toml") } else { - PathBuf::from("~/.config/streamd/config.toml") + PathBuf::from("streamd_config.toml") } } } From ca2ebd594951567de046732c0b8feda30925335f Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Tue, 7 Apr 2026 13:49:45 +0200 Subject: [PATCH 2/8] feat: add Windows cross-compilation and release artifacts - Add mkWindowsCraneLib using x86_64-pc-windows-gnu target - Add mkStreamdWindows using mingw-w64 toolchain for cross-compilation - Export streamd-windows package from flake - Add Windows build step and .exe artifact to release workflow --- .forgejo/workflows/release.yml | 5 +++++ flake.nix | 35 +++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 7c93504..bef91f5 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -49,12 +49,17 @@ jobs: if: steps.version.outputs.SKIP != 'true' run: nix build .#streamd-musl -o result-musl + - name: Build Windows binary + if: steps.version.outputs.SKIP != 'true' + run: nix build .#streamd-windows -o result-windows + - name: Prepare release artifacts if: steps.version.outputs.SKIP != 'true' run: | mkdir -p release cp result-deb release/streamd_${{ steps.version.outputs.VERSION }}_amd64.deb cp result-musl/bin/streamd release/streamd-${{ steps.version.outputs.VERSION }}-linux-x86_64 + cp result-windows/bin/streamd.exe release/streamd-${{ steps.version.outputs.VERSION }}-windows-x86_64.exe - name: Create release if: steps.version.outputs.SKIP != 'true' diff --git a/flake.nix b/flake.nix index 38723ae..12cdf69 100644 --- a/flake.nix +++ b/flake.nix @@ -132,6 +132,38 @@ in craneLib.buildPackage (commonArgs // { inherit cargoArtifacts; }); + mkWindowsCraneLib = + system: + let + pkgs = mkPkgs system; + toolchain = pkgs.rust-bin.stable.latest.default.override { + targets = [ "x86_64-pc-windows-gnu" ]; + }; + in + (crane.mkLib pkgs).overrideToolchain toolchain; + + mkStreamdWindows = + system: + let + pkgs = mkPkgs system; + pkgsCross = pkgs.pkgsCross.mingwW64; + craneLib = mkWindowsCraneLib system; + commonArgs = { + src = craneLib.path ./.; + pname = "streamd"; + inherit version; + strictDeps = true; + CARGO_BUILD_TARGET = "x86_64-pc-windows-gnu"; + CC_x86_64_pc_windows_gnu = "${pkgsCross.stdenv.cc}/bin/x86_64-w64-mingw32-gcc"; + CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER = "${pkgsCross.stdenv.cc}/bin/x86_64-w64-mingw32-gcc"; + nativeBuildInputs = [ pkgsCross.stdenv.cc ]; + buildInputs = [ pkgsCross.windows.pthreads ]; + doCheck = false; + }; + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + in + craneLib.buildPackage (commonArgs // { inherit cargoArtifacts; }); + mkStreamdDeb = system: let @@ -181,9 +213,10 @@ streamd = mkStreamd system; streamd-musl = mkStreamdMusl system; streamd-deb = mkStreamdDeb system; + streamd-windows = mkStreamdWindows system; in { - inherit streamd streamd-musl streamd-deb; + inherit streamd streamd-musl streamd-deb streamd-windows; default = streamd; } ); From e15e6f105313dc373650ad6a51a3390b4921c296 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Mon, 13 Apr 2026 19:26:09 +0200 Subject: [PATCH 3/8] fix: broken tasks extraction --- src/extract/parser.rs | 68 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/src/extract/parser.rs b/src/extract/parser.rs index 1326371..b1d0374 100644 --- a/src/extract/parser.rs +++ b/src/extract/parser.rs @@ -12,6 +12,8 @@ struct BlockInfo { end_line: usize, block_type: BlockType, events: Vec>, + /// Nested list items contained within this block (for ListItem blocks with sub-lists). + nested_items: Vec, } #[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 { let mut blocks = Vec::new(); let mut current_block: Option = None; - let _current_events: Vec> = Vec::new(); let mut depth = 0; let mut list_items: Vec = 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, Vec, usize)> = Vec::new(); + // Pre-compute line starts for offset-to-line mapping let line_starts: Vec = 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 { 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 { 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 { } } 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 { } 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 { 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 { 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 { 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 = 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( From b653590c366a818b4781b41bd0aff11f1f80db50 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Mon, 13 Apr 2026 19:30:59 +0200 Subject: [PATCH 4/8] feat(localize): extract file_type from filename prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/localize/datetime.rs | 72 +++++++++++++++++++++++++++++++++++ src/localize/mod.rs | 2 +- src/localize/preconfigured.rs | 6 +++ src/localize/shard.rs | 8 +++- 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/localize/datetime.rs b/src/localize/datetime.rs index 9f6d2ec..f5fd601 100644 --- a/src/localize/datetime.rs +++ b/src/localize/datetime.rs @@ -9,6 +9,11 @@ use std::path::Path; static FILE_NAME_REGEX: Lazy = Lazy::new(|| Regex::new(r"^(?P\d{8})(?:-(?P