Added Todo parser, file open dialog
This commit is contained in:
Generated
+9428
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -7,14 +7,17 @@ description = "A COSMIC todo.txt application"
|
|||||||
repository = "https://github.com/professorwaltwood/planit.git"
|
repository = "https://github.com/professorwaltwood/planit.git"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
cosmic-files = { git = "https://github.com/pop-os/cosmic-files.git", version = "1.0.8" }
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
i18n-embed = { version = "0.16", features = [
|
i18n-embed = { version = "0.16", features = [
|
||||||
"fluent-system",
|
"fluent-system",
|
||||||
"desktop-requester",
|
"desktop-requester",
|
||||||
] }
|
] }
|
||||||
i18n-embed-fl = "0.10"
|
i18n-embed-fl = "0.10"
|
||||||
|
log = "0.4.29"
|
||||||
open = "5.3.2"
|
open = "5.3.2"
|
||||||
rust-embed = "8.8.0"
|
rust-embed = "8.8.0"
|
||||||
|
time = { version = "0.3.47", features = ["macros", "parsing"] }
|
||||||
tokio = { version = "1.48.0", features = ["full"] }
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
|
|
||||||
[dependencies.libcosmic]
|
[dependencies.libcosmic]
|
||||||
@@ -22,7 +25,7 @@ git = "https://github.com/pop-os/libcosmic.git"
|
|||||||
# See https://github.com/pop-os/libcosmic/blob/master/Cargo.toml for available features.
|
# See https://github.com/pop-os/libcosmic/blob/master/Cargo.toml for available features.
|
||||||
features = [
|
features = [
|
||||||
# Accessibility support
|
# Accessibility support
|
||||||
"a11y",
|
#"a11y",
|
||||||
# About widget for the app
|
# About widget for the app
|
||||||
"about",
|
"about",
|
||||||
# Uses cosmic-settings-daemon to watch for config file changes
|
# Uses cosmic-settings-daemon to watch for config file changes
|
||||||
@@ -30,7 +33,7 @@ features = [
|
|||||||
# Support creating additional application windows.
|
# Support creating additional application windows.
|
||||||
"multi-window",
|
"multi-window",
|
||||||
# On app startup, focuses an existing instance if the app is already open
|
# On app startup, focuses an existing instance if the app is already open
|
||||||
"single-instance",
|
#"single-instance",
|
||||||
# Uses tokio as the executor for the runtime
|
# Uses tokio as the executor for the runtime
|
||||||
"tokio",
|
"tokio",
|
||||||
# Windowing support for X11, Windows, Mac, & Redox
|
# Windowing support for X11, Windows, Mac, & Redox
|
||||||
|
|||||||
+3
-1
@@ -2,6 +2,8 @@ app-title = Planit
|
|||||||
about = About
|
about = About
|
||||||
repository = Repository
|
repository = Repository
|
||||||
view = View
|
view = View
|
||||||
welcome = Welcome to COSMIC! ✨
|
welcome = Welcome to Planit!
|
||||||
page-id = Page { $num }
|
page-id = Page { $num }
|
||||||
git-description = Git commit {$hash} on {$date}
|
git-description = Git commit {$hash} on {$date}
|
||||||
|
file = File
|
||||||
|
open = Open File
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
test todo list +planit @tests
|
||||||
|
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
|
||||||
|
x (A) 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
|
||||||
|
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 due:2020-02-01
|
||||||
|
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
|
||||||
|
test having lots of items +planit @tests
|
||||||
|
test having lots of items +planit @tests
|
||||||
+105
-104
@@ -5,13 +5,19 @@ use crate::fl;
|
|||||||
use cosmic::app::context_drawer;
|
use cosmic::app::context_drawer;
|
||||||
use cosmic::cosmic_config::{self, CosmicConfigEntry};
|
use cosmic::cosmic_config::{self, CosmicConfigEntry};
|
||||||
use cosmic::iced::alignment::{Horizontal, Vertical};
|
use cosmic::iced::alignment::{Horizontal, Vertical};
|
||||||
|
use cosmic::iced::futures::channel::mpsc::Sender;
|
||||||
use cosmic::iced::{Alignment, Length, Subscription};
|
use cosmic::iced::{Alignment, Length, Subscription};
|
||||||
use cosmic::widget::{self, about::About, icon, menu, nav_bar};
|
use cosmic::widget::{self, about::About, menu};
|
||||||
use cosmic::{iced_futures, prelude::*};
|
use cosmic::{Theme, iced_futures, prelude::*};
|
||||||
use futures_util::SinkExt;
|
use futures_util::SinkExt;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use cosmic_files::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings};
|
||||||
|
|
||||||
|
use crate::todo::{Todo, TodoPart};
|
||||||
|
|
||||||
const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
|
const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
|
||||||
const APP_ICON: &[u8] = include_bytes!("../resources/icons/hicolor/scalable/apps/icon.svg");
|
const APP_ICON: &[u8] = include_bytes!("../resources/icons/hicolor/scalable/apps/icon.svg");
|
||||||
|
|
||||||
@@ -24,8 +30,6 @@ pub struct AppModel {
|
|||||||
context_page: ContextPage,
|
context_page: ContextPage,
|
||||||
/// The about page for this app.
|
/// The about page for this app.
|
||||||
about: About,
|
about: About,
|
||||||
/// Contains items assigned to the nav bar panel.
|
|
||||||
nav: nav_bar::Model,
|
|
||||||
/// Key bindings for the application's menu bar.
|
/// Key bindings for the application's menu bar.
|
||||||
key_binds: HashMap<menu::KeyBind, MenuAction>,
|
key_binds: HashMap<menu::KeyBind, MenuAction>,
|
||||||
/// Configuration data that persists between application runs.
|
/// Configuration data that persists between application runs.
|
||||||
@@ -34,6 +38,9 @@ pub struct AppModel {
|
|||||||
time: u32,
|
time: u32,
|
||||||
/// Toggle the watch subscription
|
/// Toggle the watch subscription
|
||||||
watch_is_active: bool,
|
watch_is_active: bool,
|
||||||
|
/// Current Todo data
|
||||||
|
todo: Option<Todo>,
|
||||||
|
dialog_opt: Option<Dialog<Message>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Messages emitted by the application and its widgets.
|
/// Messages emitted by the application and its widgets.
|
||||||
@@ -41,9 +48,11 @@ pub struct AppModel {
|
|||||||
pub enum Message {
|
pub enum Message {
|
||||||
LaunchUrl(String),
|
LaunchUrl(String),
|
||||||
ToggleContextPage(ContextPage),
|
ToggleContextPage(ContextPage),
|
||||||
ToggleWatch,
|
|
||||||
UpdateConfig(Config),
|
UpdateConfig(Config),
|
||||||
WatchTick(u32),
|
WatchTick(u32),
|
||||||
|
OpenFileDialog,
|
||||||
|
DialogMessage(DialogMessage),
|
||||||
|
OpenFileResult(DialogResult),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a COSMIC application from the app model
|
/// Create a COSMIC application from the app model
|
||||||
@@ -58,7 +67,7 @@ impl cosmic::Application for AppModel {
|
|||||||
type Message = Message;
|
type Message = Message;
|
||||||
|
|
||||||
/// Unique identifier in RDNN (reverse domain name notation) format.
|
/// Unique identifier in RDNN (reverse domain name notation) format.
|
||||||
const APP_ID: &'static str = "dev.mmurphy.Test";
|
const APP_ID: &'static str = "dev.waltwood.planit";
|
||||||
|
|
||||||
fn core(&self) -> &cosmic::Core {
|
fn core(&self) -> &cosmic::Core {
|
||||||
&self.core
|
&self.core
|
||||||
@@ -73,25 +82,6 @@ impl cosmic::Application for AppModel {
|
|||||||
core: cosmic::Core,
|
core: cosmic::Core,
|
||||||
_flags: Self::Flags,
|
_flags: Self::Flags,
|
||||||
) -> (Self, Task<cosmic::Action<Self::Message>>) {
|
) -> (Self, Task<cosmic::Action<Self::Message>>) {
|
||||||
// Create a nav bar with three page items.
|
|
||||||
let mut nav = nav_bar::Model::default();
|
|
||||||
|
|
||||||
nav.insert()
|
|
||||||
.text(fl!("page-id", num = 1))
|
|
||||||
.data::<Page>(Page::Page1)
|
|
||||||
.icon(icon::from_name("applications-science-symbolic"))
|
|
||||||
.activate();
|
|
||||||
|
|
||||||
nav.insert()
|
|
||||||
.text(fl!("page-id", num = 2))
|
|
||||||
.data::<Page>(Page::Page2)
|
|
||||||
.icon(icon::from_name("applications-system-symbolic"));
|
|
||||||
|
|
||||||
nav.insert()
|
|
||||||
.text(fl!("page-id", num = 3))
|
|
||||||
.data::<Page>(Page::Page3)
|
|
||||||
.icon(icon::from_name("applications-games-symbolic"));
|
|
||||||
|
|
||||||
// Create the about widget
|
// Create the about widget
|
||||||
let about = About::default()
|
let about = About::default()
|
||||||
.name(fl!("app-title"))
|
.name(fl!("app-title"))
|
||||||
@@ -105,7 +95,6 @@ impl cosmic::Application for AppModel {
|
|||||||
core,
|
core,
|
||||||
context_page: ContextPage::default(),
|
context_page: ContextPage::default(),
|
||||||
about,
|
about,
|
||||||
nav,
|
|
||||||
key_binds: HashMap::new(),
|
key_binds: HashMap::new(),
|
||||||
// Optional configuration file for an application.
|
// Optional configuration file for an application.
|
||||||
config: cosmic_config::Config::new(Self::APP_ID, Config::VERSION)
|
config: cosmic_config::Config::new(Self::APP_ID, Config::VERSION)
|
||||||
@@ -122,6 +111,8 @@ impl cosmic::Application for AppModel {
|
|||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
time: 0,
|
time: 0,
|
||||||
watch_is_active: false,
|
watch_is_active: false,
|
||||||
|
todo: None,
|
||||||
|
dialog_opt: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a startup command that sets the window title.
|
// Create a startup command that sets the window title.
|
||||||
@@ -132,22 +123,26 @@ impl cosmic::Application for AppModel {
|
|||||||
|
|
||||||
/// Elements to pack at the start of the header bar.
|
/// Elements to pack at the start of the header bar.
|
||||||
fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
|
fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
|
||||||
let menu_bar = menu::bar(vec![menu::Tree::with_children(
|
let menu_bar = menu::bar(vec![
|
||||||
|
menu::Tree::with_children(
|
||||||
|
menu::root(fl!("file")).apply(Element::from),
|
||||||
|
menu::items(
|
||||||
|
&self.key_binds,
|
||||||
|
vec![menu::Item::Button(fl!("open"), None, MenuAction::OpenFile)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
menu::Tree::with_children(
|
||||||
menu::root(fl!("view")).apply(Element::from),
|
menu::root(fl!("view")).apply(Element::from),
|
||||||
menu::items(
|
menu::items(
|
||||||
&self.key_binds,
|
&self.key_binds,
|
||||||
vec![menu::Item::Button(fl!("about"), None, MenuAction::About)],
|
vec![menu::Item::Button(fl!("about"), None, MenuAction::About)],
|
||||||
),
|
),
|
||||||
)]);
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
vec![menu_bar.into()]
|
vec![menu_bar.into()]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enables the COSMIC application to create a nav bar with this model.
|
|
||||||
fn nav_model(&self) -> Option<&nav_bar::Model> {
|
|
||||||
Some(&self.nav)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Display a context drawer if the context page is requested.
|
/// Display a context drawer if the context page is requested.
|
||||||
fn context_drawer(&self) -> Option<context_drawer::ContextDrawer<'_, Self::Message>> {
|
fn context_drawer(&self) -> Option<context_drawer::ContextDrawer<'_, Self::Message>> {
|
||||||
if !self.core.window.show_context {
|
if !self.core.window.show_context {
|
||||||
@@ -169,62 +164,29 @@ impl cosmic::Application for AppModel {
|
|||||||
/// events received by widgets will be passed to the update method.
|
/// events received by widgets will be passed to the update method.
|
||||||
fn view(&self) -> Element<'_, Self::Message> {
|
fn view(&self) -> Element<'_, Self::Message> {
|
||||||
let space_s = cosmic::theme::spacing().space_s;
|
let space_s = cosmic::theme::spacing().space_s;
|
||||||
let content: Element<_> = match self.nav.active_data::<Page>().unwrap() {
|
|
||||||
Page::Page1 => {
|
|
||||||
let header = widget::row::with_capacity(2)
|
let header = widget::row::with_capacity(2)
|
||||||
.push(widget::text::title1(fl!("welcome")))
|
.push(widget::text::title1(fl!("welcome")))
|
||||||
.push(widget::text::title3(fl!("page-id", num = 1)))
|
|
||||||
.align_y(Alignment::End)
|
.align_y(Alignment::End)
|
||||||
.spacing(space_s);
|
.spacing(space_s);
|
||||||
|
|
||||||
let counter_label = ["Watch: ", self.time.to_string().as_str()].concat();
|
let section;
|
||||||
let section = cosmic::widget::settings::section().add(
|
|
||||||
cosmic::widget::settings::item::builder(counter_label).control(
|
|
||||||
widget::button::text(if self.watch_is_active {
|
|
||||||
"Stop"
|
|
||||||
} else {
|
|
||||||
"Start"
|
|
||||||
})
|
|
||||||
.on_press(Message::ToggleWatch),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
widget::column::with_capacity(2)
|
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(header)
|
||||||
.push(section)
|
.push(section)
|
||||||
.spacing(space_s)
|
.spacing(space_s)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into();
|
||||||
}
|
|
||||||
|
|
||||||
Page::Page2 => {
|
|
||||||
let header = widget::row::with_capacity(2)
|
|
||||||
.push(widget::text::title1(fl!("welcome")))
|
|
||||||
.push(widget::text::title3(fl!("page-id", num = 2)))
|
|
||||||
.align_y(Alignment::End)
|
|
||||||
.spacing(space_s);
|
|
||||||
|
|
||||||
widget::column::with_capacity(1)
|
|
||||||
.push(header)
|
|
||||||
.spacing(space_s)
|
|
||||||
.height(Length::Fill)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
Page::Page3 => {
|
|
||||||
let header = widget::row::with_capacity(2)
|
|
||||||
.push(widget::text::title1(fl!("welcome")))
|
|
||||||
.push(widget::text::title3(fl!("page-id", num = 3)))
|
|
||||||
.align_y(Alignment::End)
|
|
||||||
.spacing(space_s);
|
|
||||||
|
|
||||||
widget::column::with_capacity(1)
|
|
||||||
.push(header)
|
|
||||||
.spacing(space_s)
|
|
||||||
.height(Length::Fill)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
widget::container(content)
|
widget::container(content)
|
||||||
.width(600)
|
.width(600)
|
||||||
@@ -260,7 +222,7 @@ impl cosmic::Application for AppModel {
|
|||||||
// Conditionally enables a timer that emits a message every second.
|
// Conditionally enables a timer that emits a message every second.
|
||||||
if self.watch_is_active {
|
if self.watch_is_active {
|
||||||
subscriptions.push(Subscription::run(|| {
|
subscriptions.push(Subscription::run(|| {
|
||||||
iced_futures::stream::channel(1, |mut emitter| async move {
|
iced_futures::stream::channel(1, |mut emitter: Sender<Message>| async move {
|
||||||
let mut time = 1;
|
let mut time = 1;
|
||||||
let mut interval = tokio::time::interval(Duration::from_secs(1));
|
let mut interval = tokio::time::interval(Duration::from_secs(1));
|
||||||
|
|
||||||
@@ -285,11 +247,6 @@ impl cosmic::Application for AppModel {
|
|||||||
Message::WatchTick(time) => {
|
Message::WatchTick(time) => {
|
||||||
self.time = time;
|
self.time = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::ToggleWatch => {
|
|
||||||
self.watch_is_active = !self.watch_is_active;
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::ToggleContextPage(context_page) => {
|
Message::ToggleContextPage(context_page) => {
|
||||||
if self.context_page == context_page {
|
if self.context_page == context_page {
|
||||||
// Close the context drawer if the toggled context page is the same.
|
// Close the context drawer if the toggled context page is the same.
|
||||||
@@ -300,39 +257,71 @@ impl cosmic::Application for AppModel {
|
|||||||
self.core.window.show_context = true;
|
self.core.window.show_context = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::UpdateConfig(config) => {
|
Message::UpdateConfig(config) => {
|
||||||
self.config = config;
|
self.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::LaunchUrl(url) => match open::that_detached(&url) {
|
Message::LaunchUrl(url) => match open::that_detached(&url) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("failed to open {url:?}: {err}");
|
eprintln!("failed to open {url:?}: {err}");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Message::OpenFileDialog => {
|
||||||
|
if self.dialog_opt.is_none() {
|
||||||
|
let (dialog, command) = Dialog::new(
|
||||||
|
DialogSettings::new().kind(DialogKind::OpenFile),
|
||||||
|
Message::DialogMessage,
|
||||||
|
Message::OpenFileResult,
|
||||||
|
);
|
||||||
|
self.dialog_opt = Some(dialog);
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::DialogMessage(dialog_message) => {
|
||||||
|
if let Some(dialog) = &mut self.dialog_opt {
|
||||||
|
return dialog.update(dialog_message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::OpenFileResult(dialog_result) => match dialog_result {
|
||||||
|
DialogResult::Open(paths) => {
|
||||||
|
for path in paths {
|
||||||
|
let cpath = match std::fs::canonicalize(&path) {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(err) => match path::absolute(&path) {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(_) => {
|
||||||
|
log::error!("failed to canonicalise path {:?}: {}", path, err);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut todo = Todo::new(cpath);
|
||||||
|
// Todo: this might need to be more async
|
||||||
|
if let Err(err) = todo.load() {
|
||||||
|
log::error!("failed to load file {:?} {:?}", path, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.todo = Some(todo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DialogResult::Cancel => todo!(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called when a nav item is selected.
|
fn view_window(&self, id: cosmic::iced::window::Id) -> Element<'_, Self::Message> {
|
||||||
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<cosmic::Action<Self::Message>> {
|
match &self.dialog_opt {
|
||||||
// Activate the page in the model.
|
Some(dialog) => dialog.view(id),
|
||||||
self.nav.activate(id);
|
None => widget::text("Unknown window ID").into(),
|
||||||
|
}
|
||||||
self.update_title()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppModel {
|
impl AppModel {
|
||||||
/// Updates the header and window titles.
|
/// Updates the header and window titles.
|
||||||
pub fn update_title(&mut self) -> Task<cosmic::Action<Message>> {
|
pub fn update_title(&mut self) -> Task<cosmic::Action<Message>> {
|
||||||
let mut window_title = fl!("app-title");
|
let window_title = fl!("app-title");
|
||||||
|
|
||||||
if let Some(page) = self.nav.text(self.nav.active()) {
|
|
||||||
window_title.push_str(" — ");
|
|
||||||
window_title.push_str(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(id) = self.core.main_window_id() {
|
if let Some(id) = self.core.main_window_id() {
|
||||||
self.set_window_title(window_title, id)
|
self.set_window_title(window_title, id)
|
||||||
@@ -342,11 +331,21 @@ impl AppModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The page to display in the application.
|
fn view_todo<'a>(todo: Vec<TodoPart<'a>>) -> Element<'a, Message> {
|
||||||
pub enum Page {
|
widget::row::with_children(todo.iter().map(|part| view_text(*part).into())).into()
|
||||||
Page1,
|
}
|
||||||
Page2,
|
|
||||||
Page3,
|
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.
|
/// The context page to display in the context drawer.
|
||||||
@@ -359,6 +358,7 @@ pub enum ContextPage {
|
|||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum MenuAction {
|
pub enum MenuAction {
|
||||||
About,
|
About,
|
||||||
|
OpenFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl menu::action::MenuAction for MenuAction {
|
impl menu::action::MenuAction for MenuAction {
|
||||||
@@ -367,6 +367,7 @@ impl menu::action::MenuAction for MenuAction {
|
|||||||
fn message(&self) -> Self::Message {
|
fn message(&self) -> Self::Message {
|
||||||
match self {
|
match self {
|
||||||
MenuAction::About => Message::ToggleContextPage(ContextPage::About),
|
MenuAction::About => Message::ToggleContextPage(ContextPage::About),
|
||||||
|
MenuAction::OpenFile => Message::OpenFileDialog,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ pub static LANGUAGE_LOADER: LazyLock<FluentLanguageLoader> = LazyLock::new(|| {
|
|||||||
loader
|
loader
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/// Request a localized string by ID from the i18n/ directory.
|
/// Request a localized string by ID from the i18n/ directory.
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! fl {
|
macro_rules! fl {
|
||||||
@@ -49,4 +48,3 @@ macro_rules! fl {
|
|||||||
i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *)
|
i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *)
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod config;
|
mod config;
|
||||||
mod i18n;
|
mod i18n;
|
||||||
|
mod todo;
|
||||||
|
|
||||||
fn main() -> cosmic::iced::Result {
|
fn main() -> cosmic::iced::Result {
|
||||||
// Get the system's preferred languages.
|
// Get the system's preferred languages.
|
||||||
|
|||||||
+643
@@ -0,0 +1,643 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
fs,
|
||||||
|
hash::{BuildHasher, DefaultHasher, Hash, Hasher},
|
||||||
|
io::{self, BufRead, BufReader},
|
||||||
|
path::PathBuf,
|
||||||
|
pin::Pin,
|
||||||
|
ptr::NonNull,
|
||||||
|
};
|
||||||
|
use time::{Date, macros::format_description};
|
||||||
|
|
||||||
|
const DATE_FMT: time::format_description::StaticFormatDescription =
|
||||||
|
format_description!("[year]-[month]-[day]");
|
||||||
|
|
||||||
|
pub struct Todo {
|
||||||
|
/// Location of todo.txt file
|
||||||
|
location: PathBuf,
|
||||||
|
|
||||||
|
/// Vec of todo items (i.e., parsed lines of the todo.txt)
|
||||||
|
pub todos: Vec<TodoItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TodoError {
|
||||||
|
Parse,
|
||||||
|
Io(io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for TodoError {
|
||||||
|
fn from(value: io::Error) -> Self {
|
||||||
|
Self::Io(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Todo {
|
||||||
|
pub fn new(loc: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
location: loc,
|
||||||
|
todos: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(&mut self) -> Result<(), TodoError> {
|
||||||
|
self.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reload(&mut self) -> Result<(), TodoError> {
|
||||||
|
let file = fs::File::open(&self.location)?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
|
let mut todos = Vec::new();
|
||||||
|
for line in reader.lines() {
|
||||||
|
// Pin the string so it can be referenced but cruicially not modified
|
||||||
|
let line = Pin::new(line?);
|
||||||
|
todos.push(line.try_into()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.todos = todos;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum TodoPart<'a> {
|
||||||
|
Description(&'a str),
|
||||||
|
Project(&'a str),
|
||||||
|
Context(&'a str),
|
||||||
|
Tag(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Into<&'a str> for TodoPart<'a> {
|
||||||
|
fn into(self) -> &'a str {
|
||||||
|
match self {
|
||||||
|
TodoPart::Description(s) => s,
|
||||||
|
TodoPart::Project(s) => s,
|
||||||
|
TodoPart::Context(s) => s,
|
||||||
|
TodoPart::Tag(s) => s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum TodoPartRaw {
|
||||||
|
Description(NonNull<str>),
|
||||||
|
Project(NonNull<str>),
|
||||||
|
Context(NonNull<str>),
|
||||||
|
Tag(NonNull<str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Into<TodoPart<'a>> for TodoPartRaw {
|
||||||
|
fn into<'b>(self) -> TodoPart<'a> {
|
||||||
|
match self {
|
||||||
|
TodoPartRaw::Description(s) => TodoPart::Description(unsafe { s.as_ref() }),
|
||||||
|
TodoPartRaw::Project(s) => TodoPart::Project(unsafe { s.as_ref() }),
|
||||||
|
TodoPartRaw::Context(s) => TodoPart::Context(unsafe { s.as_ref() }),
|
||||||
|
TodoPartRaw::Tag(s) => TodoPart::Tag(unsafe { s.as_ref() }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
impl From<&str> for TodoItemKey {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
let mut s = DefaultHasher::new();
|
||||||
|
value.hash(&mut s);
|
||||||
|
Self(s.finish())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for TodoItemKey {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
state.write_u64(self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TodoHasher(u64);
|
||||||
|
|
||||||
|
impl Hasher for TodoHasher {
|
||||||
|
fn finish(&self) -> u64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
fn write(&mut self, _: &[u8]) {}
|
||||||
|
|
||||||
|
fn write_u64(&mut self, i: u64) {
|
||||||
|
self.0 = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct TodoBuildHasher;
|
||||||
|
|
||||||
|
impl BuildHasher for TodoBuildHasher {
|
||||||
|
type Hasher = TodoHasher;
|
||||||
|
|
||||||
|
fn build_hasher(&self) -> Self::Hasher {
|
||||||
|
TodoHasher(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TodoItem {
|
||||||
|
/// TodoItem owns/stores the whole here
|
||||||
|
#[allow(unused)]
|
||||||
|
raw: Pin<String>,
|
||||||
|
/// Optional - Marks Completion x
|
||||||
|
done: bool,
|
||||||
|
/// Optional - Marks Priority (A)
|
||||||
|
prio: Option<char>,
|
||||||
|
/// Optional - Completion Date
|
||||||
|
date_done: Option<Date>,
|
||||||
|
/// Optional - Creation Date (must be sepecified if completion date is)
|
||||||
|
date_made: Option<Date>,
|
||||||
|
/// Description - raw str slice for description
|
||||||
|
raw_desc: NonNull<str>,
|
||||||
|
/// Formatted Description - list of description components
|
||||||
|
desc: Vec<TodoPartRaw>,
|
||||||
|
/// Project tags
|
||||||
|
projects: HashSet<TodoItemKey, TodoBuildHasher>,
|
||||||
|
/// Context tags
|
||||||
|
contexts: HashSet<TodoItemKey, TodoBuildHasher>,
|
||||||
|
/// Special Key/Value tags
|
||||||
|
tags: HashMap<TodoItemKey, NonNull<str>, TodoBuildHasher>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TodoItem {
|
||||||
|
pub fn is_done(&self) -> bool {
|
||||||
|
self.done
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete(&mut self) {
|
||||||
|
self.done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_priority(&self) -> Option<char> {
|
||||||
|
self.prio
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_priority(&mut self, prio: char) {
|
||||||
|
self.prio = Some(prio);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_priority(&mut self) {
|
||||||
|
self.prio = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_date_completed(&self) -> Option<Date> {
|
||||||
|
self.date_done
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_date_completed(&mut self, date: Date) {
|
||||||
|
self.date_done = Some(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_date_completed(&mut self) {
|
||||||
|
self.date_done = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_date_created(&self) -> Option<Date> {
|
||||||
|
self.date_made
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_date_created(&mut self, date: Date) {
|
||||||
|
self.date_made = Some(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_date_created(&mut self) {
|
||||||
|
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 {
|
||||||
|
// Safety
|
||||||
|
// ensure that the pointer is convertible to a reference.
|
||||||
|
// The pointee will always be valid for the pointer's lifetime
|
||||||
|
// as it points to a pinned string owned by the same object.
|
||||||
|
// i.e., the String will only be dropped when the pointer is
|
||||||
|
// Additionally the String is never mutated.
|
||||||
|
unsafe { self.raw_desc.as_ref() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_project(&self, project: TodoItemKey) -> bool {
|
||||||
|
self.projects.contains(&project)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tag<T>(&self, tag: T) -> Option<&str>
|
||||||
|
where
|
||||||
|
T: Into<TodoItemKey>,
|
||||||
|
{
|
||||||
|
let tag_pointer = self.tags.get(&tag.into())?;
|
||||||
|
// Safety: see get_raw_str
|
||||||
|
Some(unsafe { tag_pointer.as_ref() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Pin<String>> for TodoItem {
|
||||||
|
type Error = TodoError;
|
||||||
|
|
||||||
|
fn try_from(value: Pin<String>) -> Result<Self, Self::Error> {
|
||||||
|
let raw = value;
|
||||||
|
|
||||||
|
let mut pos = 0;
|
||||||
|
|
||||||
|
let done = parse_done(&raw[pos..], &mut pos);
|
||||||
|
let prio = parse_prio(&raw[pos..], &mut pos);
|
||||||
|
let (date_done, date_made) = parse_dates(&raw[pos..], &mut pos);
|
||||||
|
|
||||||
|
if !raw[pos..].contains(|c: char| !c.is_whitespace()) {
|
||||||
|
return Err(TodoError::Parse);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut projects = HashSet::with_hasher(TodoBuildHasher);
|
||||||
|
let mut contexts = HashSet::with_hasher(TodoBuildHasher);
|
||||||
|
let mut tags = HashMap::with_hasher(TodoBuildHasher);
|
||||||
|
let raw_desc = NonNull::from_ref(&raw[pos..]);
|
||||||
|
let mut desc = Vec::new();
|
||||||
|
|
||||||
|
while let Some((size, word)) = get_word(&raw[pos..]) {
|
||||||
|
if word.starts_with('@') {
|
||||||
|
if word.len() > 1 {
|
||||||
|
contexts.insert(word.into());
|
||||||
|
desc.push(TodoPartRaw::Context(NonNull::from_ref(word)));
|
||||||
|
}
|
||||||
|
} else if word.starts_with('+') {
|
||||||
|
if word.len() > 1 {
|
||||||
|
projects.insert(word.into());
|
||||||
|
desc.push(TodoPartRaw::Project(NonNull::from_ref(word)));
|
||||||
|
}
|
||||||
|
} else if let Some((k, v)) = word.split_once(':') {
|
||||||
|
if !(k.contains(':') || v.contains(':') || k.len() == 0 || v.len() == 0) {
|
||||||
|
tags.insert(k.into(), NonNull::from_ref(v));
|
||||||
|
desc.push(TodoPartRaw::Tag(NonNull::from_ref(word)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
desc.push(TodoPartRaw::Description(NonNull::from_ref(word)));
|
||||||
|
}
|
||||||
|
pos += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
raw,
|
||||||
|
done,
|
||||||
|
prio,
|
||||||
|
date_done,
|
||||||
|
date_made,
|
||||||
|
raw_desc,
|
||||||
|
desc,
|
||||||
|
projects,
|
||||||
|
contexts,
|
||||||
|
tags,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_word(raw: &str) -> Option<(usize, &str)> {
|
||||||
|
let mut chars = raw.char_indices();
|
||||||
|
let (next_char, _) = chars.find(|(_, c)| !c.is_whitespace())?;
|
||||||
|
let next_ws = chars.find(|(_, c)| c.is_whitespace());
|
||||||
|
|
||||||
|
match next_ws {
|
||||||
|
Some((end, _)) => {
|
||||||
|
let next_word = chars.find(|(_, c)| !c.is_whitespace());
|
||||||
|
match next_word {
|
||||||
|
Some((nw_start, _)) => Some((nw_start, &raw[next_char..end])),
|
||||||
|
None => Some((end, &raw[next_char..end])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Some((raw.len(), &raw[next_char..])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_done(raw: &str, pos: &mut usize) -> bool {
|
||||||
|
let next_word = get_word(raw);
|
||||||
|
|
||||||
|
match next_word {
|
||||||
|
Some((size, word)) => {
|
||||||
|
let done = word == "x";
|
||||||
|
if done {
|
||||||
|
*pos += size;
|
||||||
|
}
|
||||||
|
done
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_prio(raw: &str, pos: &mut usize) -> Option<char> {
|
||||||
|
let next_word = get_word(raw)?;
|
||||||
|
let (size, word) = next_word;
|
||||||
|
let mut word = word.chars();
|
||||||
|
|
||||||
|
if word.next()? == '('
|
||||||
|
&& let char = word.next()?
|
||||||
|
&& char.is_ascii_uppercase()
|
||||||
|
&& word.next()? == ')'
|
||||||
|
&& word.next().is_none()
|
||||||
|
{
|
||||||
|
*pos += size;
|
||||||
|
Some(char)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_dates(raw: &str, pos: &mut usize) -> (Option<Date>, Option<Date>) {
|
||||||
|
let mut date_pos = 0;
|
||||||
|
let first = parse_date(raw, &mut date_pos);
|
||||||
|
if first.is_some() {
|
||||||
|
let second = parse_date(&raw[date_pos..], &mut date_pos);
|
||||||
|
*pos += date_pos;
|
||||||
|
if second.is_some() {
|
||||||
|
(first, second)
|
||||||
|
} else {
|
||||||
|
(None, first)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_date(raw: &str, pos: &mut usize) -> Option<Date> {
|
||||||
|
let (size, word) = get_word(raw)?;
|
||||||
|
|
||||||
|
if let Ok(date) = Date::parse(word, &DATE_FMT) {
|
||||||
|
*pos += size;
|
||||||
|
Some(date)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use time::macros::date;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_from_file() {
|
||||||
|
let mut todo = Todo::new("samples/todo.txt".into());
|
||||||
|
todo.load().expect("Error loading todo file");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_todo_incorrect_done() {
|
||||||
|
let todo_str = Pin::new("X (A) 2020-05-01 2020-01-01 help me please".to_string());
|
||||||
|
let todo: TodoItem = todo_str.try_into().expect("todo parse");
|
||||||
|
|
||||||
|
assert_eq!(todo.done, false);
|
||||||
|
assert_eq!(todo.prio, None);
|
||||||
|
assert_eq!(todo.date_done, None);
|
||||||
|
assert_eq!(todo.date_made, None);
|
||||||
|
assert_eq!(
|
||||||
|
*todo.raw,
|
||||||
|
"X (A) 2020-05-01 2020-01-01 help me please".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
todo.get_raw_str(),
|
||||||
|
"X (A) 2020-05-01 2020-01-01 help me please"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(todo.projects.is_empty());
|
||||||
|
assert!(todo.contexts.is_empty());
|
||||||
|
assert!(todo.tags.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_todo_invalid_tags() {
|
||||||
|
let todo_str =
|
||||||
|
Pin::new("x (A) 2020-05-01 2020-01-01 help me please artist: :music".to_string());
|
||||||
|
let todo: TodoItem = todo_str.try_into().expect("todo parse");
|
||||||
|
|
||||||
|
assert_eq!(todo.done, true);
|
||||||
|
assert_eq!(todo.prio, Some('A'));
|
||||||
|
assert_eq!(todo.date_done, Some(date!(2020 - 05 - 01)));
|
||||||
|
assert_eq!(todo.date_made, Some(date!(2020 - 01 - 01)));
|
||||||
|
assert_eq!(
|
||||||
|
*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!(todo.projects.is_empty());
|
||||||
|
assert!(todo.contexts.is_empty());
|
||||||
|
assert!(todo.tags.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_todo_invalid_projects() {
|
||||||
|
let todo_str =
|
||||||
|
Pin::new("x (A) 2020-05-01 2020-01-01 help me please a+beatles +\tmusic".to_string());
|
||||||
|
let todo: TodoItem = todo_str.try_into().expect("todo parse");
|
||||||
|
|
||||||
|
assert_eq!(todo.done, true);
|
||||||
|
assert_eq!(todo.prio, Some('A'));
|
||||||
|
assert_eq!(todo.date_done, Some(date!(2020 - 05 - 01)));
|
||||||
|
assert_eq!(todo.date_made, Some(date!(2020 - 01 - 01)));
|
||||||
|
assert_eq!(
|
||||||
|
*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!(todo.projects.is_empty());
|
||||||
|
assert!(todo.contexts.is_empty());
|
||||||
|
assert!(todo.tags.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_todo_invalid_contexts() {
|
||||||
|
let todo_str =
|
||||||
|
Pin::new("x (A) 2020-05-01 2020-01-01 help me please a@beatles @\tmusic".to_string());
|
||||||
|
let todo: TodoItem = todo_str.try_into().expect("todo parse");
|
||||||
|
|
||||||
|
assert_eq!(todo.done, true);
|
||||||
|
assert_eq!(todo.prio, Some('A'));
|
||||||
|
assert_eq!(todo.date_done, Some(date!(2020 - 05 - 01)));
|
||||||
|
assert_eq!(todo.date_made, Some(date!(2020 - 01 - 01)));
|
||||||
|
assert_eq!(
|
||||||
|
*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!(todo.projects.is_empty());
|
||||||
|
assert!(todo.contexts.is_empty());
|
||||||
|
assert!(todo.tags.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_todo_valid_tags() {
|
||||||
|
let todo_str = Pin::new(
|
||||||
|
"x (A) 2020-05-01 2020-01-01 help me please artist:beatles category:music".to_string(),
|
||||||
|
);
|
||||||
|
let todo: TodoItem = todo_str.try_into().expect("todo parse");
|
||||||
|
|
||||||
|
assert_eq!(todo.done, true);
|
||||||
|
assert_eq!(todo.prio, Some('A'));
|
||||||
|
assert_eq!(todo.date_done, Some(date!(2020 - 05 - 01)));
|
||||||
|
assert_eq!(todo.date_made, Some(date!(2020 - 01 - 01)));
|
||||||
|
assert_eq!(
|
||||||
|
*todo.raw,
|
||||||
|
"x (A) 2020-05-01 2020-01-01 help me please artist:beatles category:music".to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
todo.get_raw_str(),
|
||||||
|
"help me please artist:beatles category:music"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(todo.projects.is_empty());
|
||||||
|
assert!(todo.contexts.is_empty());
|
||||||
|
|
||||||
|
assert_eq!(todo.get_tag("artist").expect("artist in tags"), "beatles");
|
||||||
|
assert_eq!(todo.get_tag("category").expect("category in tags"), "music");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_todo_valid_projects() {
|
||||||
|
let todo_str =
|
||||||
|
Pin::new("x (A) 2020-05-01 2020-01-01 help me please +beatles +music".to_string());
|
||||||
|
let todo: TodoItem = todo_str.try_into().expect("todo parse");
|
||||||
|
|
||||||
|
assert_eq!(todo.done, true);
|
||||||
|
assert_eq!(todo.prio, Some('A'));
|
||||||
|
assert_eq!(todo.date_done, Some(date!(2020 - 05 - 01)));
|
||||||
|
assert_eq!(todo.date_made, Some(date!(2020 - 01 - 01)));
|
||||||
|
assert_eq!(
|
||||||
|
*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");
|
||||||
|
|
||||||
|
let mut projects = HashSet::with_hasher(TodoBuildHasher);
|
||||||
|
projects.insert("+beatles".into());
|
||||||
|
projects.insert("+music".into());
|
||||||
|
|
||||||
|
assert_eq!(todo.projects, projects);
|
||||||
|
assert!(todo.contexts.is_empty());
|
||||||
|
assert!(todo.tags.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_todo_valid_contexts() {
|
||||||
|
let todo_str =
|
||||||
|
Pin::new("x (A) 2020-05-01 2020-01-01 help me please @beatles @music".to_string());
|
||||||
|
let todo: TodoItem = todo_str.try_into().expect("todo parse");
|
||||||
|
|
||||||
|
assert_eq!(todo.done, true);
|
||||||
|
assert_eq!(todo.prio, Some('A'));
|
||||||
|
assert_eq!(todo.date_done, Some(date!(2020 - 05 - 01)));
|
||||||
|
assert_eq!(todo.date_made, Some(date!(2020 - 01 - 01)));
|
||||||
|
assert_eq!(
|
||||||
|
*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");
|
||||||
|
let mut contexts = HashSet::with_hasher(TodoBuildHasher);
|
||||||
|
contexts.insert("@beatles".into());
|
||||||
|
contexts.insert("@music".into());
|
||||||
|
|
||||||
|
assert_eq!(todo.contexts, contexts);
|
||||||
|
assert!(todo.projects.is_empty());
|
||||||
|
assert!(todo.tags.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_prio_invalid() {
|
||||||
|
let todo = "(a) hello".to_string();
|
||||||
|
let mut pos = 0;
|
||||||
|
let prio = parse_prio(&todo, &mut pos);
|
||||||
|
|
||||||
|
assert_eq!(prio, None);
|
||||||
|
assert_eq!(pos, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_prio_valid() {
|
||||||
|
let todo = "(A) hello".to_string();
|
||||||
|
let mut pos = 0;
|
||||||
|
let prio = parse_prio(&todo, &mut pos);
|
||||||
|
|
||||||
|
assert_eq!(prio, Some('A'));
|
||||||
|
assert_eq!(get_word(&todo[pos..]), Some((5, "hello")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_dates_not_done_invalid() {
|
||||||
|
let dates = "2020-05-2 hello there".to_string();
|
||||||
|
let mut pos = 0;
|
||||||
|
let (completed, created) = parse_dates(&dates, &mut pos);
|
||||||
|
|
||||||
|
assert_eq!(completed, None);
|
||||||
|
assert_eq!(created, None);
|
||||||
|
assert_eq!(pos, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_dates_not_done_valid() {
|
||||||
|
let dates = "2020-05-02 hello there";
|
||||||
|
let mut pos = 0;
|
||||||
|
let (completed, created) = parse_dates(&dates, &mut pos);
|
||||||
|
|
||||||
|
assert_eq!(completed, None);
|
||||||
|
assert_eq!(created, Some(date!(2020 - 05 - 02)));
|
||||||
|
assert_eq!(get_word(&dates[pos..]), Some((6, "hello")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_dates_done_valid() {
|
||||||
|
let dates = "2020-05-03 2020-05-02";
|
||||||
|
let mut pos = 0;
|
||||||
|
let (completed, created) = parse_dates(&dates, &mut pos);
|
||||||
|
|
||||||
|
assert_eq!(completed, Some(date!(2020 - 05 - 03)));
|
||||||
|
assert_eq!(created, Some(date!(2020 - 05 - 02)));
|
||||||
|
assert!(get_word(&dates[pos..]).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_dates_done_invalid() {
|
||||||
|
let dates = "d2020-05-03 2020-05-02";
|
||||||
|
let mut pos = 0;
|
||||||
|
let (completed, created) = parse_dates(&dates, &mut pos);
|
||||||
|
|
||||||
|
assert_eq!(completed, None);
|
||||||
|
assert_eq!(created, None);
|
||||||
|
assert_eq!(pos, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_date_valid() {
|
||||||
|
let date = "2020-05-02";
|
||||||
|
let mut pos = 0;
|
||||||
|
let parsed = parse_date(&date, &mut pos);
|
||||||
|
|
||||||
|
assert_eq!(parsed, Some(date!(2020 - 05 - 02)));
|
||||||
|
assert_eq!(get_word(&date[pos..]), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_date_invalid() {
|
||||||
|
let date = "2020-05-2";
|
||||||
|
let mut pos = 0;
|
||||||
|
let parsed = parse_date(&date, &mut pos);
|
||||||
|
|
||||||
|
assert_eq!(parsed, None);
|
||||||
|
assert_eq!(pos, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user