From 3494245d16458b9ba1d6f4427fce702d6ecf179cc6c7221e5199d4350aac392c Mon Sep 17 00:00:00 2001 From: professorwaltwood Date: Sun, 26 Apr 2026 10:08:52 +1000 Subject: [PATCH] Added Todo editor and highlighter. Restructured modules --- samples/todo.txt | 6 +- src/app.rs | 76 +++++++--------- src/main.rs | 1 - src/todo/editor.rs | 99 +++++++++++++++++++++ src/{todo_editor.rs => todo/highlighter.rs} | 2 +- src/todo/mod.rs | 3 + src/{todo.rs => todo/parser.rs} | 41 ++++----- 7 files changed, 153 insertions(+), 75 deletions(-) create mode 100644 src/todo/editor.rs rename src/{todo_editor.rs => todo/highlighter.rs} (99%) create mode 100644 src/todo/mod.rs rename src/{todo.rs => todo/parser.rs} (94%) diff --git a/samples/todo.txt b/samples/todo.txt index 32e314d..f79fba0 100644 --- a/samples/todo.txt +++ b/samples/todo.txt @@ -3,7 +3,7 @@ test having lots of items +planit @tests test having lots of items +planit @tests x 2020-01-01 2020-01-02 test having lots of items +planit @tests 2020-01-01 test having lots of items +planit @tests -(B) test having lots of items +planit @tests +(B) (A) test having lots of items +planit @tests x (A) test having lots of items +planit @tests test having lots of items +planit @tests test having lots of items +planit @tests @@ -18,5 +18,5 @@ test having lots of items +planit @tests test having lots of items +planit @tests test having lots of items +planit @tests test having lots of items +planit @tests -+sda sda sda@ asd @Sasd @dsa a@adsa - 2020-01-01 @asda +asd \ No newline at end of file ++sda sda sda@ asd @Sasd @dsa a@adsa + 2020-01-01 @asda +asd diff --git a/src/app.rs b/src/app.rs index 3ec9317..720ec00 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,7 @@ use crate::config::Config; use crate::fl; -use crate::todo_editor::TodoHighlighter; +use crate::todo::editor::TodoEditor; use cosmic::app::context_drawer; use cosmic::cosmic_config::{self, ConfigSet, CosmicConfigEntry}; use cosmic::iced::alignment::{Horizontal, Vertical}; @@ -11,7 +11,6 @@ use cosmic::iced::{Alignment, Length, Subscription, stream}; use cosmic::prelude::*; use cosmic::widget::text_editor::{self, Content}; use cosmic::widget::{self, about::About, menu}; - use futures_util::SinkExt; use std::collections::HashMap; use std::path; @@ -19,7 +18,7 @@ use std::time::Duration; use cosmic_files::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings}; -use crate::todo::{Todo, TodoPart}; +use crate::todo::parser::Todo; const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY"); const APP_ICON: &[u8] = include_bytes!("../resources/icons/hicolor/scalable/apps/icon.svg"); @@ -44,8 +43,8 @@ pub struct AppModel { watch_is_active: bool, /// Current Todo data todo: Option, + editor: TodoEditor, dialog_opt: Option>, - content: Content, } /// Messages emitted by the application and its widgets. @@ -58,7 +57,7 @@ pub enum Message { OpenFileDialog, DialogMessage(DialogMessage), OpenFileResult(DialogResult), - Edit(text_editor::Action), + Edit(text_editor::Action, usize), } /// Create a COSMIC application from the app model @@ -114,7 +113,8 @@ impl cosmic::Application for AppModel { } }; - let todo = config.current_file.clone().map(|path| Todo::new(path)); + let todo = config_load_todo(&config); + let editor = TodoEditor::new(todo.as_ref()); // Construct the app model with the runtime's core. let mut app = AppModel { @@ -128,8 +128,8 @@ impl cosmic::Application for AppModel { time: 0, watch_is_active: false, todo: todo, + editor: editor, dialog_opt: None, - content: Content::with_text("test"), }; // Create a startup command that sets the window title. @@ -182,37 +182,21 @@ impl cosmic::Application for AppModel { fn view(&self) -> Element<'_, Self::Message> { let space_s = cosmic::theme::spacing().space_s; - let header = widget::row::with_capacity(2) + let header = widget::row::with_capacity(1) .push(widget::text::title1(fl!("welcome"))) - .push( - widget::TextEditor::new(&self.content) - .on_action(Message::Edit) - .highlight_with::((), |hl, _t| hl.to_format()), - ) .align_y(Alignment::End) .spacing(space_s); - let section; - - if let Some(todo) = self.todo.as_ref() { - section = widget::Column::with_children( - todo.todos.iter().map(|item| view_todo(item.get_str())), - ) - .spacing(20); - } else { - section = widget::Column::new() - } - let content: Element<_> = widget::column::with_capacity(2) .push(header) - .push(section) + .push(&self.editor) .spacing(space_s) - .height(Length::Fill) .into(); widget::container(content) - .width(600) + .width(Length::Fill) .height(Length::Fill) + .padding(50) .apply(widget::container) .width(Length::Fill) .align_x(Horizontal::Center) @@ -335,8 +319,10 @@ impl cosmic::Application for AppModel { } DialogResult::Cancel => todo!(), }, - Message::Edit(action) => { - self.content.perform(action); + Message::Edit(action, line) => { + if let Err(err) = self.editor.edit(action, line) { + log::error!("error in update: {:?}", err); + } } } Task::none() @@ -363,23 +349,6 @@ impl AppModel { } } -fn view_todo<'a>(todo: Vec>) -> Element<'a, Message> { - widget::row::with_children(todo.iter().map(|part| view_text(*part).into())).into() -} - -fn view_text<'a>(text: TodoPart<'a>) -> widget::Text<'a, Theme, Renderer> { - let theme = &cosmic::theme::COSMIC_DARK; - let colour = match text { - TodoPart::Context(_) => theme.accent_color(), - TodoPart::Description(_) => theme.on_primary_component_color(), - TodoPart::Project(_) => theme.palette.accent_green, - TodoPart::Tag(_) => theme.palette.accent_purple, - }; - let colour = cosmic::iced::Color::from(colour); - let text: &str = text.into(); - widget::text(text).class(colour) -} - /// The context page to display in the context drawer. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub enum ContextPage { @@ -403,3 +372,18 @@ impl menu::action::MenuAction for MenuAction { } } } + +fn config_load_todo(config: &Config) -> Option { + let path = config.current_file.clone()?; + let mut todo = Todo::new(path); + if let Err(err) = todo.load() { + log::error!( + "Unable to load configured todo file: {:?}\n{:?}", + todo.get_path(), + err + ); + None + } else { + Some(todo) + } +} diff --git a/src/main.rs b/src/main.rs index 97fd025..e55a15a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,6 @@ mod app; mod config; mod i18n; mod todo; -mod todo_editor; fn main() -> cosmic::iced::Result { // Get the system's preferred languages. diff --git a/src/todo/editor.rs b/src/todo/editor.rs new file mode 100644 index 0000000..7d71186 --- /dev/null +++ b/src/todo/editor.rs @@ -0,0 +1,99 @@ +use std::fmt::Debug; + +use crate::app::Message; +use crate::todo::highlighter::TodoHighlighter; +use crate::todo::parser::Todo; + +use cosmic::Element; +use cosmic::iced::Alignment; +use cosmic::iced::Length; +use cosmic::widget::Column; +use cosmic::widget::TextEditor; +use cosmic::widget::scrollable; +use cosmic::widget::text_editor::Action; +use cosmic::widget::text_editor::Content; + +pub enum TodoEditorError { + InactiveEdit, + LineNotFound(usize), +} + +impl Debug for TodoEditorError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InactiveEdit => write!(f, "A line must be active for edit to be valid"), + Self::LineNotFound(line) => { + write!(f, "Error trying to edit line {} - Line not found", line) + } + } + } +} + +#[derive(Debug, Clone, Default)] +struct TodoLine { + content: Content, + dirty: bool, +} + +#[derive(Debug, Clone)] +pub struct TodoEditor { + lines: Vec, + active: Option, +} + +impl TodoEditor { + pub fn new(todo: Option<&Todo>) -> Self { + if let Some(todo) = todo { + let lines = todo + .todos + .iter() + .map(|item| TodoLine { + content: Content::with_text(item.get_desc()), + dirty: false, + }) + .collect(); + Self { + lines, + active: None, + } + } else { + Self { + lines: vec![TodoLine::default()], + active: None, + } + } + } + + pub fn edit(&mut self, edit: Action, line_num: usize) -> Result<(), TodoEditorError> { + let line = self + .lines + .get_mut(line_num) + .ok_or(TodoEditorError::LineNotFound(line_num))?; + + line.content.perform(edit); + line.dirty = true; + Ok(()) + } +} + +impl<'a, 'b> From<&'a TodoEditor> for Element<'b, Message> +where + 'a: 'b, +{ + fn from(value: &'a TodoEditor) -> Self { + let mut col = Column::with_capacity(value.lines.len()) + .spacing(cosmic::theme::spacing().space_s) + .align_x(Alignment::Center) + .width(Length::Fill); + for (i, line) in (&value.lines).iter().enumerate() { + col = col.push( + TextEditor::new(&line.content) + .on_action(move |action| Message::Edit(action, i)) + .highlight_with::((), |hl, _t| hl.to_format()), + ); + } + scrollable(col) + .spacing(cosmic::theme::spacing().space_xs) + .into() + } +} diff --git a/src/todo_editor.rs b/src/todo/highlighter.rs similarity index 99% rename from src/todo_editor.rs rename to src/todo/highlighter.rs index 4586257..5ff06f7 100644 --- a/src/todo_editor.rs +++ b/src/todo/highlighter.rs @@ -10,7 +10,7 @@ static TODOSYNTAX: LazyLock = LazyLock::new(|| { let mut builder = parsing::SyntaxSetBuilder::new(); builder.add( parsing::SyntaxDefinition::load_from_str( - std::include_str!("../resources/todo.sublime-syntax"), + std::include_str!("../../resources/todo.sublime-syntax"), false, None, ) diff --git a/src/todo/mod.rs b/src/todo/mod.rs new file mode 100644 index 0000000..b73b63e --- /dev/null +++ b/src/todo/mod.rs @@ -0,0 +1,3 @@ +pub mod editor; +pub mod highlighter; +pub mod parser; diff --git a/src/todo.rs b/src/todo/parser.rs similarity index 94% rename from src/todo.rs rename to src/todo/parser.rs index 7f618f2..edbe721 100644 --- a/src/todo.rs +++ b/src/todo/parser.rs @@ -40,6 +40,10 @@ impl Todo { } } + pub fn get_path(&self) -> &PathBuf { + &self.location + } + pub fn load(&mut self) -> Result<(), TodoError> { self.reload() } @@ -97,17 +101,6 @@ impl<'a> Into> for TodoPartRaw { } } -impl TodoPartRaw { - fn as_str(&self) -> &str { - match self { - TodoPartRaw::Description(s) => unsafe { s.as_ref() }, - TodoPartRaw::Project(s) => unsafe { s.as_ref() }, - TodoPartRaw::Context(s) => unsafe { s.as_ref() }, - TodoPartRaw::Tag(s) => unsafe { s.as_ref() }, - } - } -} - #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] pub struct TodoItemKey(u64); @@ -175,6 +168,10 @@ pub struct TodoItem { } impl TodoItem { + pub fn get_raw(&self) -> &str { + &*self.raw + } + pub fn is_done(&self) -> bool { self.done } @@ -219,11 +216,7 @@ impl TodoItem { self.date_made = None; } - pub fn get_str<'a>(&'a self) -> Vec> { - self.desc.iter().map(|p| (*p).into()).collect() - } - - fn get_raw_str(&self) -> &str { + pub fn get_desc(&self) -> &str { // Safety // ensure that the pointer is convertible to a reference. // The pointee will always be valid for the pointer's lifetime @@ -242,7 +235,7 @@ impl TodoItem { T: Into, { let tag_pointer = self.tags.get(&tag.into())?; - // Safety: see get_raw_str + // Safety: see get_desc Some(unsafe { tag_pointer.as_ref() }) } } @@ -408,7 +401,7 @@ mod tests { "X (A) 2020-05-01 2020-01-01 help me please".to_string() ); assert_eq!( - todo.get_raw_str(), + todo.get_desc(), "X (A) 2020-05-01 2020-01-01 help me please" ); @@ -431,7 +424,7 @@ mod tests { *todo.raw, "x (A) 2020-05-01 2020-01-01 help me please artist: :music".to_string() ); - assert_eq!(todo.get_raw_str(), "help me please artist: :music"); + assert_eq!(todo.get_desc(), "help me please artist: :music"); assert!(todo.projects.is_empty()); assert!(todo.contexts.is_empty()); @@ -452,7 +445,7 @@ mod tests { *todo.raw, "x (A) 2020-05-01 2020-01-01 help me please a+beatles +\tmusic".to_string() ); - assert_eq!(todo.get_raw_str(), "help me please a+beatles +\tmusic"); + assert_eq!(todo.get_desc(), "help me please a+beatles +\tmusic"); assert!(todo.projects.is_empty()); assert!(todo.contexts.is_empty()); @@ -473,7 +466,7 @@ mod tests { *todo.raw, "x (A) 2020-05-01 2020-01-01 help me please a@beatles @\tmusic".to_string() ); - assert_eq!(todo.get_raw_str(), "help me please a@beatles @\tmusic"); + assert_eq!(todo.get_desc(), "help me please a@beatles @\tmusic"); assert!(todo.projects.is_empty()); assert!(todo.contexts.is_empty()); @@ -496,7 +489,7 @@ mod tests { "x (A) 2020-05-01 2020-01-01 help me please artist:beatles category:music".to_string() ); assert_eq!( - todo.get_raw_str(), + todo.get_desc(), "help me please artist:beatles category:music" ); @@ -521,7 +514,7 @@ mod tests { *todo.raw, "x (A) 2020-05-01 2020-01-01 help me please +beatles +music".to_string() ); - assert_eq!(todo.get_raw_str(), "help me please +beatles +music"); + assert_eq!(todo.get_desc(), "help me please +beatles +music"); let mut projects = HashSet::with_hasher(TodoBuildHasher); projects.insert("+beatles".into()); @@ -547,7 +540,7 @@ mod tests { "x (A) 2020-05-01 2020-01-01 help me please @beatles @music".to_string() ); - assert_eq!(todo.get_raw_str(), "help me please @beatles @music"); + assert_eq!(todo.get_desc(), "help me please @beatles @music"); let mut contexts = HashSet::with_hasher(TodoBuildHasher); contexts.insert("@beatles".into()); contexts.insert("@music".into());