Added Todo editor and highlighter. Restructured modules

This commit is contained in:
2026-04-26 10:08:52 +10:00
committed by professor
parent 84e7aa2a87
commit 3494245d16
7 changed files with 153 additions and 75 deletions
+3 -3
View File
@@ -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
+sda sda sda@ asd @Sasd @dsa a@adsa
2020-01-01 @asda +asd
+30 -46
View File
@@ -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<Todo>,
editor: TodoEditor,
dialog_opt: Option<Dialog<Message>>,
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::<TodoHighlighter>((), |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<TodoPart<'a>>) -> 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<Todo> {
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)
}
}
-1
View File
@@ -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.
+99
View File
@@ -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<TodoLine>,
active: Option<usize>,
}
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::<TodoHighlighter>((), |hl, _t| hl.to_format()),
);
}
scrollable(col)
.spacing(cosmic::theme::spacing().space_xs)
.into()
}
}
@@ -10,7 +10,7 @@ static TODOSYNTAX: LazyLock<parsing::SyntaxSet> = 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,
)
+3
View File
@@ -0,0 +1,3 @@
pub mod editor;
pub mod highlighter;
pub mod parser;
+17 -24
View File
@@ -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<TodoPart<'a>> 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<TodoPart<'a>> {
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<TodoItemKey>,
{
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());