From 921f3e74c5bb4b8ec0d0e78aab998fe8da7b314f Mon Sep 17 00:00:00 2001 From: landon Date: Tue, 15 Sep 2026 14:27:30 -0500 Subject: [PATCH] feat: Add 'New Scene' button to file list menu for creating linked planning+draft files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added create_scene_pair() method to workspace.rs - Added '+ New Scene' button in file_list.rs UI - Button creates both planning file (05-Plot/act_N/scene_XX.md) and draft file (06-First Draft/Act N/sceneXX.md) - Files are automatically linked via YAML frontmatter (draft_file and planning_template fields) - Auto-detects current act from folder context - Auto-generates next scene number - Selects the draft file after creation (ready to edit) - Includes hover text explaining the feature Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- src/app/file_list.rs | 10 ++ src/app/workspace.rs | 317 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 327 insertions(+) diff --git a/src/app/file_list.rs b/src/app/file_list.rs index 6d8e744..8ec1ec2 100644 --- a/src/app/file_list.rs +++ b/src/app/file_list.rs @@ -539,6 +539,16 @@ impl App { if ui.button("+ New").clicked() { self.create_file(); } + if ui + .button("+ New Scene") + .on_hover_text( + "Create a planning file in 05-Plot and a linked draft file \ + in 06-First Draft with automatic cross-references", + ) + .clicked() + { + self.create_scene_pair(); + } }); if ui .button("+ New from template") diff --git a/src/app/workspace.rs b/src/app/workspace.rs index ccc372a..d885b73 100644 --- a/src/app/workspace.rs +++ b/src/app/workspace.rs @@ -600,6 +600,323 @@ impl App { self.rebuild_field_names(); } + /// Create a scene pair: both a planning file (in 05-Plot) and a draft file + /// (in 06-First Draft), linked to each other. Used for the "New Scene" button. + pub(super) fn create_scene_pair(&mut self) { + use super::today_utc; + + // Determine which folder we're in + let current_dir = self + .selected + .and_then(|i| self.files.get(i)) + .map(|path| parent_dir(path).to_string()) + .unwrap_or_default(); + + // Check if we're in a First Draft folder (Act 1, Act 2, etc.) + let in_draft_folder = current_dir.starts_with("06-First Draft/Act") + || current_dir.starts_with("06-First Draft/"); + + // Check if we're in a Plot folder (act_1, act_2, etc.) + let in_plot_folder = current_dir.starts_with("05-Plot/act_") + || current_dir.starts_with("05-Plot/"); + + // Extract act number from current directory + let act_num = if in_draft_folder { + // Parse "06-First Draft/Act N" or "06-First Draft/Act N/" + current_dir.split('/').nth(1).and_then(|s| { + s.strip_prefix("Act ").and_then(|n| n.parse::().ok()) + }) + } else if in_plot_folder { + // Parse "05-Plot/act_N" or "05-Plot/act_N/" + current_dir.split('/').nth(1).and_then(|s| { + s.strip_prefix("act_").and_then(|n| n.parse::().ok()) + }) + } else { + // Default to Act 1 + Some(1) + }; + + let act = act_num.unwrap_or(1); + let today = today_utc(); + + // Find next available scene number in this act + let existing_scenes: Vec = self.files.iter() + .filter(|f| { + let path_lower = f.to_lowercase(); + if in_draft_folder || in_plot_folder { + // Check files in current act + path_lower.contains(&format!("act_{}/", act)) + || path_lower.contains(&format!("Act {}/", act)) + } else { + // Check both folders + path_lower.contains(&format!("act_{}/scene_", act)) + || path_lower.contains(&format!("Act {}/scene", act)) + } + }) + .filter_map(|f| { + // Extract scene number from filename + let filename = Path::new(f).file_name().and_then(|n| n.to_str()).unwrap_or(""); + let num_str = filename.trim_start_matches("scene").trim_start_matches("_").trim_end_matches(".md"); + num_str.parse::().ok() + }) + .collect(); + + let scene_num = (1..=1000).find(|n| !existing_scenes.contains(n)).unwrap_or(1); + + // Default chapter is 1 + let chapter = 1; + + // Generate scene title + let title = format!("Chapter {}: Scene {}", chapter, scene_num); + let slug = format!("chapter-{}--scene-{}", chapter, scene_num); + + // Determine where to create files + let draft_dir = format!("06-First Draft/Act {}", act); + let plot_dir = format!("05-Plot/act_{:02}", act); + + // Create planning file + let planning_name = format!("{}/scene_{:02}.md", plot_dir, scene_num); + let draft_name = format!("{}/scene{:02}.md", draft_dir, scene_num); + + // Planning file content + let planning_id = format!("scene_{:02d}_{:02d}_{:02d}", act, chapter, scene_num); + let planning_content = format!(r#"--- +id: {} +title: "{}" +slug: {} +act: {} +chapter: {} +scene: {} +type: scene +status: planning +word_count_target: 2000 +word_count_min: 1800 +word_count_max: 2200 +pov: "" +characters: [] +setting: "" +date: "" +time: "" +conflict: "" +theme: "" +tone: "" +draft_file: "../../06-First Draft/Act {}/scene{:02}.md" +order: {} +created: "{}" +updated: "{}" +--- + +# {} + +**Planning Template** | **ID:** {} | **Status:** Planning + +--- + +## Planning Metadata + +| Attribute | Value | +|-----------|-------| +| **Act** | {} | +| **Chapter** | {} | +| **Scene** | {} | +| **Type** | scene | +| **Order** | {} | +| **Draft File** | [Open Draft](../../06-First Draft/Act {}/scene{:02}.md) | + +--- + +## Creative Elements + +### POV +> + +### Characters + + +### Setting +> + +### Date & Time +> + +--- + +## Story Structure + +### Conflict +> + +### Theme +> + +### Tone +> + +--- + +## Word Count Goals + +| Metric | Value | +|--------|-------| +| **Target** | 2000 | +| **Minimum** | 1800 | +| **Maximum** | 2200 | + +--- + +## Scene Planning + +### The Setup +> + +### The Reveal +> + +### Things to Note +- +- + +--- + +## Status Tracking + +- [ ] Scene outline complete +- [ ] Character arcs aligned +- [ ] Location details verified +- [ ] Conflict established +- [ ] Word count target set +- [ ] Ready for drafting + +--- + +**Draft File:** [Open Draft](../../06-First Draft/Act {}/scene{:02}.md) | **Created:** {} | **Updated:** {} +"#, + planning_id, title, slug, act, chapter, scene_num, + act, scene_num, scene_num, + today, today, + title, planning_id, + act, chapter, scene_num, scene_num, + act, scene_num, + act, chapter, scene_num, + today, today + ); + + // Draft file content + let draft_id = format!("draft_scene_{:02d}_{:02d}_{:02d}", act, chapter, scene_num); + let draft_content = format!(r#"--- +id: {} +title: "{}" +slug: {} +planning_template: "../../05-Plot/act_{:02}/scene_{:02}.md" +act: {} +chapter: {} +scene: {} +type: draft +status: not_started +word_count: 0 +word_count_updated: "" +session_word_count: 0 +last_session: "" +total_sessions: 0 +pov: "" +version: 1 +created: "{}" +updated: "{}" +--- + +# {} + +**Draft File** | **ID:** {} | **Status:** Not Started + +--- + +## Planning Reference + +> **Linked to:** [Planning Template](../../05-Plot/act_{:02}/scene_{:02}.md) + +--- + +## Scene Metadata + +| Attribute | Value | +|-----------|-------| +| **Act** | {} | +| **Chapter** | {} | +| **Scene** | {} | +| **Type** | draft | +| **POV** | Not set | +| **Version** | 1 | + +--- + +## Word Count Tracking + +| Metric | Value | +|--------|-------| +| **Target** | 2000 (from planning) | +| **Current** | 0 | +| **This Session** | 0 | +| **Last Updated** | Never | + +--- + +## Draft Content + +### Rough Draft: + +[Write your scene content here...] + +--- + +## Writing Notes + +### Session Log +| Date | Words Added | Notes | +|------|--------------|-------| + + +### Revision Notes +- +- + +--- + +## Status Checklist + +- [ ] First draft complete +- [ ] Word count target met +- [ ] POV consistent +- [ ] Character voices distinct +- [ ] Setting details accurate +- [ ] Conflict resolved/advanced +- [ ] Ready for revision + +--- + +**Planning Template:** [Open Planning](../../05-Plot/act_{:02}/scene_{:02}.md) | **Created:** {} | **Updated:** {} +"#, + draft_id, title, slug, + act, scene_num, + act, chapter, scene_num, + today, today, + title, draft_id, + act, scene_num, + act, chapter, scene_num, + act, scene_num, + today, today + ); + + // Create both files + self.insert_new_file(planning_name, planning_content); + self.insert_new_file(draft_name, draft_content); + + // Select the draft file (more likely what user wants to edit) + let draft_idx = self.files.iter().position(|f| *f == draft_name).unwrap_or(0); + self.selected = Some(draft_idx); + self.buffer.clear(); + self.dirty = false; + } + /// Write `contents` to a new file, add it to the manuscript order and select /// it. Shared by the plain and template-backed create paths. fn insert_new_file(&mut self, name: String, contents: String) {