Added Todo editor and highlighter. Restructured modules
This commit is contained in:
+3
-3
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod editor;
|
||||
pub mod highlighter;
|
||||
pub mod parser;
|
||||
@@ -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());
|
||||
Reference in New Issue
Block a user