diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/cli/mod.rs | 35 | ||||
-rw-r--r-- | src/db/categories.rs | 4 | ||||
-rw-r--r-- | src/db/event-color.rs | 7 | ||||
-rw-r--r-- | src/db/event_color.rs | 14 | ||||
-rw-r--r-- | src/db/event_colors.rs | 12 | ||||
-rw-r--r-- | src/db/events.rs | 64 | ||||
-rw-r--r-- | src/db/migrations/01-init.sql (renamed from src/db/migrations/1-init.sql) | 0 | ||||
-rw-r--r-- | src/db/migrations/02-categories.sql (renamed from src/db/migrations/2-categories.sql) | 0 | ||||
-rw-r--r-- | src/db/migrations/03-event-color.sql (renamed from src/db/migrations/3-event-color.sql) | 0 | ||||
-rw-r--r-- | src/db/migrations/04-strict-tables.sql | 57 | ||||
-rw-r--r-- | src/db/mod.rs | 24 | ||||
-rw-r--r-- | src/gui/app.rs | 9 | ||||
-rw-r--r-- | src/gui/form/mod.rs | 8 | ||||
-rw-r--r-- | src/gui/form/repetition.rs | 2 | ||||
-rw-r--r-- | src/main.rs | 20 | ||||
-rw-r--r-- | src/model/event.rs | 10 | ||||
-rw-r--r-- | src/validation/mod.rs | 4 |
17 files changed, 189 insertions, 81 deletions
diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 3674a08..8069a10 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,14 +1,25 @@ use anyhow::Result; -use chrono::{Local, NaiveDate, NaiveDateTime, TimeZone}; +use chrono::{Local, NaiveDate, NaiveDateTime, TimeDelta, TimeZone}; use rusqlite::Connection; +use std::ops::{Add, Sub}; use crate::model::event::Event; +use crate::model::category::Category; use crate::{db, model::event}; -pub fn today(conn: &Connection) -> Result<String> { - let today = Local::now().date_naive(); - let events = between_inclusive(conn, today, today)?; - Ok(format_events(events)) +pub fn parse_date(s: String) -> Option<NaiveDate> { + match s.as_str() { + "yesterday" => Some(Local::now().sub(TimeDelta::days(1)).date_naive()), + "today" => Some(Local::now().date_naive()), + "tomorrow" => Some(Local::now().add(TimeDelta::days(1)).date_naive()), + _ => NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok(), + } +} + +pub fn started_at_date(conn: &Connection, date: NaiveDate) -> Result<String> { + let events = between_inclusive(conn, date, date)?; + let categories = db::categories::list(conn)?; + Ok(format_events(events, categories)) } pub fn parse_timestamp_range(s: String) -> Option<(NaiveDateTime, NaiveDateTime)> { @@ -41,7 +52,8 @@ pub fn start_between(conn: &Connection, from: NaiveDateTime, to: NaiveDateTime) }) .cloned() .collect::<Vec<Event>>(); - Ok(format_events(events)) + let categories = db::categories::list(conn)?; + Ok(format_events(events, categories)) } fn between_inclusive(conn: &Connection, from: NaiveDate, to: NaiveDate) -> Result<Vec<Event>> { @@ -60,12 +72,19 @@ fn between_inclusive(conn: &Connection, from: NaiveDate, to: NaiveDate) -> Resul Ok(events) } -fn format_events(events: Vec<Event>) -> String { +fn format_events(events: Vec<Event>, categories: Vec<Category>) -> String { let mut events = events; events.sort_by_key(|e| e.local_timestamp()); events .iter() - .map(|e| format!("{}\n", e.pprint())) + .map(|e| { + let category = e.category.and_then(|c| categories.clone().into_iter().find(|c2| c == c2.id)); + let category_str = match category { + Some(c) => format!("[{}] ", c.name), + None => "".to_string() + }; + return format!("{}{}\n", category_str, e.pprint()) + }) .collect::<Vec<String>>() .join("") } diff --git a/src/db/categories.rs b/src/db/categories.rs index ebefb6d..f81b855 100644 --- a/src/db/categories.rs +++ b/src/db/categories.rs @@ -5,7 +5,8 @@ use uuid::Uuid; use crate::model::category::Category; pub fn list(conn: &Connection) -> Result<Vec<Category>> { - let mut stmt = conn.prepare("SELECT id, name, color FROM categories")?; + let query = r#"SELECT id, name, color FROM categories"#; + let mut stmt = conn.prepare(query)?; let iter = stmt.query_map([], |row| { Ok(read_category(row.get(0)?, row.get(1)?, row.get(2)?)) @@ -20,6 +21,5 @@ pub fn list(conn: &Connection) -> Result<Vec<Category>> { fn read_category(id: String, name: String, color: String) -> Result<Category> { let id = Uuid::parse_str(&id)?; - Ok(Category { id, name, color }) } diff --git a/src/db/event-color.rs b/src/db/event-color.rs deleted file mode 100644 index 18612e4..0000000 --- a/src/db/event-color.rs +++ /dev/null @@ -1,7 +0,0 @@ - - -pub fn get_default_color(conn: &Connection) -> Result<String> { -} - -// pub fn set_default_color(conon: &Connection, color: &str) -> Result<()> { -// } diff --git a/src/db/event_color.rs b/src/db/event_color.rs deleted file mode 100644 index 33d350b..0000000 --- a/src/db/event_color.rs +++ /dev/null @@ -1,14 +0,0 @@ -use anyhow::Result; -use rusqlite::Connection; - -pub fn get_default_color(conn: &Connection) -> Result<String> { - let mut stmt = conn.prepare("SELECT * FROM event_color LIMIT 1")?; - - let iter = stmt.query_map([], |row| row.get(0))?; - - let mut res = vec![]; - for color in iter { - res.push(color?) - } - Ok(res.first().unwrap_or(&"blue".to_string()).clone()) -} diff --git a/src/db/event_colors.rs b/src/db/event_colors.rs new file mode 100644 index 0000000..bf7f541 --- /dev/null +++ b/src/db/event_colors.rs @@ -0,0 +1,12 @@ +use anyhow::Result; +use rusqlite::Connection; + +pub fn get_default_color(conn: &Connection) -> Result<String> { + let mut stmt = conn.prepare("SELECT * FROM event_colors LIMIT 1")?; + let mut iter = stmt.query_map([], |row| row.get(0))?; + + match iter.next() { + Some(Ok(color)) => Ok(color), + _ => Ok("blue".to_string()), + } +} diff --git a/src/db/events.rs b/src/db/events.rs index 1721967..7f09466 100644 --- a/src/db/events.rs +++ b/src/db/events.rs @@ -1,11 +1,16 @@ use anyhow::Result; use chrono::{NaiveDate, NaiveTime}; -use rusqlite::{params, Connection}; +use rusqlite::{named_params, Connection}; use uuid::Uuid; use crate::model::event::Event; pub fn insert(conn: &Connection, event: &Event) -> Result<()> { + let query = r#" + INSERT INTO events (id, date, start, end, name, repetition, category, created, updated) + VALUES (:id, :date, :start, :end, :name, :repetition, :category, datetime(), datetime()) + "#; + let repetition = match &event.repetition { Some(r) => Some(serde_json::to_string(&r)?), None => None, @@ -14,14 +19,35 @@ pub fn insert(conn: &Connection, event: &Event) -> Result<()> { let category = event.category.map(|id| id.hyphenated().to_string()); conn.execute( - "INSERT INTO events (id, date, start, end, name, repetition, category, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?, datetime(), datetime())", - params![event.id.hyphenated().to_string(), event.date, event.start, event.end, event.name, repetition, category] + query, + named_params![ + ":id": event.id.hyphenated().to_string(), + ":date": event.date, + ":start": event.start, + ":end": event.end, + ":name": event.name, + ":repetition": repetition, + ":category": category + ], )?; Ok(()) } pub fn update(conn: &Connection, event: &Event) -> Result<()> { + let query = r#" + UPDATE events + SET + date = :date, + start = :start, + end = :end, + name = :name, + repetition = :repetition, + category = :category, + updated = datetime() + WHERE id = :id + "#; + let repetition = match &event.repetition { Some(r) => Some(serde_json::to_string(&r)?), None => None, @@ -30,29 +56,35 @@ pub fn update(conn: &Connection, event: &Event) -> Result<()> { let category = event.category.map(|id| id.hyphenated().to_string()); conn.execute( - "UPDATE events SET date = ?, start = ?, end = ?, name = ?, repetition = ?, category = ?, updated = datetime() WHERE id = ?", - params![event.date, event.start, event.end, event.name, repetition, category, event.id.hyphenated().to_string()] + query, + named_params![ + ":date": event.date, + ":start": event.start, + ":end": event.end, + ":name": event.name, + ":repetition": repetition, + ":category": category, + ":id": event.id.hyphenated().to_string() + ], )?; Ok(()) } pub fn delete(conn: &Connection, id: &Uuid) -> Result<()> { - conn.execute( - "DELETE FROM events WHERE id = ?", - params![id.hyphenated().to_string()], - )?; - + let query = r#"DELETE FROM events WHERE id = :id"#; + conn.execute(query, named_params![":id": id.hyphenated().to_string()])?; Ok(()) } pub fn list_recurring(conn: &Connection) -> Result<Vec<Event>> { - let mut stmt = conn.prepare( - " + let query = r#" SELECT id, date, start, end, name, repetition, category FROM events - WHERE repetition IS NOT NULL", - )?; + WHERE repetition IS NOT NULL + "#; + + let mut stmt = conn.prepare(query)?; let iter = stmt.query_map([], |row| { Ok(read_event( @@ -82,8 +114,8 @@ pub fn list_non_recurring_between( " SELECT id, date, start, end, name, category FROM events - WHERE - repetition IS NULL + WHERE + repetition IS NULL AND date >= ? AND date <= ? ", diff --git a/src/db/migrations/1-init.sql b/src/db/migrations/01-init.sql index 467e481..467e481 100644 --- a/src/db/migrations/1-init.sql +++ b/src/db/migrations/01-init.sql diff --git a/src/db/migrations/2-categories.sql b/src/db/migrations/02-categories.sql index 0b373d0..0b373d0 100644 --- a/src/db/migrations/2-categories.sql +++ b/src/db/migrations/02-categories.sql diff --git a/src/db/migrations/3-event-color.sql b/src/db/migrations/03-event-color.sql index ec589ea..ec589ea 100644 --- a/src/db/migrations/3-event-color.sql +++ b/src/db/migrations/03-event-color.sql diff --git a/src/db/migrations/04-strict-tables.sql b/src/db/migrations/04-strict-tables.sql new file mode 100644 index 0000000..7a10b31 --- /dev/null +++ b/src/db/migrations/04-strict-tables.sql @@ -0,0 +1,57 @@ +-- Categories + +ALTER TABLE "categories" RENAME TO "categories_non_strict"; + +CREATE TABLE IF NOT EXISTS "categories" ( + "id" TEXT PRIMARY KEY, /* UUID */ + "name" TEXT NOT NULL UNIQUE, + "color" TEXT NOT NULL, /* COLOR */ + "created" TEXT NOT NULL, /* DATETIME */ + "updated" TEXT NOT NULL /* DATETIME */ +) STRICT; + +INSERT INTO categories (id, name, color, created, updated) + SELECT id, name, color, created, updated + FROM categories_non_strict; + +DROP TABLE categories_non_strict; + +-- Event color + +CREATE TABLE IF NOT EXISTS "event_colors" ( + "color" TEXT NOT NULL /* COLOR */ +) STRICT; + +INSERT INTO event_colors (color) + SELECT color + FROM event_color; + +DROP TABLE event_color; + +-- Events + +ALTER TABLE "events" RENAME TO "events_non_strict"; + +CREATE TABLE IF NOT EXISTS "events" ( + "id" TEXT PRIMARY KEY, /* UUID */ + "date" TEXT NOT NULL, /* DATE */ + "start" TEXT NULL, /* TIME */ + "end" TEXT NULL, /* TIME */ + "name" TEXT NOT NULL, + "repetition" TEXT NULL, /* JSON */ + "created" TEXT NOT NULL, /* DATETIME */ + "updated" TEXT NOT NULL, /* DATETIME */ + "category" TEXT REFERENCES "categories" ("id") +) STRICT; + +INSERT INTO events (id, date, start, end, name, repetition, created, updated, category) + SELECT id, date, start, end, name, repetition, created, updated, category + FROM events_non_strict; + +DROP TABLE events_non_strict; + +DROP INDEX IF EXISTS events_date_index; +CREATE INDEX events_date_index on events (date); + +DROP INDEX IF EXISTS events_repetition_index; +CREATE INDEX events_repetition_index on events (repetition); diff --git a/src/db/mod.rs b/src/db/mod.rs index 20e7f81..0e73f30 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,5 +1,5 @@ pub mod categories; -pub mod event_color; +pub mod event_colors; pub mod events; use anyhow::Result; @@ -8,11 +8,23 @@ use rusqlite_migration::{Migrations, M}; pub fn init(db_path: &str) -> Result<Connection> { let mut conn = Connection::open(db_path)?; + apply_migrations(&mut conn)?; + set_pragma(&conn, "foreign_keys", "ON")?; + set_pragma(&conn, "journal_mode", "wal")?; + Ok(conn) +} + +fn apply_migrations(conn: &mut Connection) -> Result<()> { let migrations = Migrations::new(vec![ - M::up(include_str!("migrations/1-init.sql")), - M::up(include_str!("migrations/2-categories.sql")), - M::up(include_str!("migrations/3-event-color.sql")), + M::up(include_str!("migrations/01-init.sql")), + M::up(include_str!("migrations/02-categories.sql")), + M::up(include_str!("migrations/03-event-color.sql")), + M::up(include_str!("migrations/04-strict-tables.sql")), ]); - migrations.to_latest(&mut conn)?; - Ok(conn) + migrations.to_latest(conn)?; + Ok(()) +} + +fn set_pragma(conn: &Connection, key: &str, value: &str) -> Result<()> { + Ok(conn.pragma_update(None, key, value)?) } diff --git a/src/gui/app.rs b/src/gui/app.rs index 5469e53..826f051 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -2,7 +2,7 @@ use gtk4 as gtk; use anyhow::Result; use async_channel::Sender; -use chrono::{Datelike, Duration, NaiveDate, Weekday}; +use chrono::{Datelike, Duration, NaiveDate}; use rusqlite::Connection; use std::rc::Rc; @@ -30,16 +30,13 @@ pub struct App { impl App { pub fn new(conn: Rc<Connection>, app: >k::Application, tx: Sender<Msg>) -> Result<Self> { let today = chrono::offset::Local::now().naive_local().date(); - // TODO: error handling - let start_date = - NaiveDate::from_isoywd_opt(today.year(), today.iso_week().week(), Weekday::Mon) - .unwrap(); + let start_date = today - Duration::days(today.weekday().num_days_from_monday().into()); let end_date = start_date + Duration::days(7 * 4 - 1); let events = db::events::list_non_recurring_between(&conn, start_date, end_date)?; let recurring_events = db::events::list_recurring(&conn)?; let categories = db::categories::list(&conn)?; - let default_color = db::event_color::get_default_color(&conn)?; + let default_color = db::event_colors::get_default_color(&conn)?; let calendar = calendar::create( tx.clone(), diff --git a/src/gui/form/mod.rs b/src/gui/form/mod.rs index 197bc14..a14fb82 100644 --- a/src/gui/form/mod.rs +++ b/src/gui/form/mod.rs @@ -256,10 +256,10 @@ pub async fn show(app: &App, target: Target) { match event::validate( id, - date.buffer().text().to_string(), - name.buffer().text().to_string(), - start.buffer().text().to_string(), - end.buffer().text().to_string(), + &date.buffer().text(), + &name.buffer().text(), + &start.buffer().text(), + &end.buffer().text(), repetition, category, ) { diff --git a/src/gui/form/repetition.rs b/src/gui/form/repetition.rs index 4ed9803..6e7a13f 100644 --- a/src/gui/form/repetition.rs +++ b/src/gui/form/repetition.rs @@ -195,7 +195,7 @@ pub fn validate( // Check until let until = (if frequency.is_some() { - match validation::non_empty(model.until.buffer().text().to_string()) { + match validation::non_empty(&model.until.buffer().text()) { Some(until) => match NaiveDate::parse_from_str(&until, event::DATE_FORMAT) { Ok(until) => Ok(Some(until)), Err(_) => Err(format!("Can’t parse date from {}", until)), diff --git a/src/main.rs b/src/main.rs index 8eafd77..e61a771 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,11 +14,11 @@ struct Opt { #[clap(long = "database", default_value = "database.sqlite3")] db_path: String, - /// List today’s events as plain text - #[clap(long = "list-today")] - list_today: bool, + /// Started at events as plain text + #[clap(long = "date")] + date: Option<String>, - /// Start between <timestamp..timestamp> events as plain text + /// Started between <timestamp..timestamp> events as plain text #[clap(long = "start-between")] start_between: Option<String>, } @@ -26,17 +26,17 @@ struct Opt { fn main() -> Result<()> { let Opt { db_path, - list_today, + date, start_between, } = Opt::parse(); let conn = db::init(&db_path)?; - if list_today { - print!("{}", cli::today(&conn)?); - } else { - match start_between.and_then(cli::parse_timestamp_range) { + + match date.and_then(cli::parse_date) { + Some(date) => print!("{}", cli::started_at_date(&conn, date)?), + None => match start_between.and_then(cli::parse_timestamp_range) { Some((from, to)) => print!("{}", cli::start_between(&conn, from, to)?), None => gui::run(conn), - } + }, }; Ok(()) } diff --git a/src/model/event.rs b/src/model/event.rs index 98ce416..62e5b12 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -65,10 +65,10 @@ pub fn repetitions_between( pub fn validate( id: Uuid, - date: String, - name: String, - start: String, - end: String, + date: &str, + name: &str, + start: &str, + end: &str, repetition: Option<Repetition>, category: Option<Uuid>, ) -> Option<Event> { @@ -82,7 +82,7 @@ pub fn validate( Some(Event { id, - date: NaiveDate::parse_from_str(&date, DATE_FORMAT).ok()?, + date: NaiveDate::parse_from_str(date, DATE_FORMAT).ok()?, name: validation::non_empty(name)?, start, end, diff --git a/src/validation/mod.rs b/src/validation/mod.rs index 07a7c4c..029726d 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -2,7 +2,7 @@ use chrono::NaiveTime; use crate::model::time; -pub fn time(time: String) -> Option<Option<NaiveTime>> { +pub fn time(time: &str) -> Option<Option<NaiveTime>> { let time = time.trim(); if time.is_empty() { Some(None) @@ -11,7 +11,7 @@ pub fn time(time: String) -> Option<Option<NaiveTime>> { } } -pub fn non_empty(str: String) -> Option<String> { +pub fn non_empty(str: &str) -> Option<String> { let str = str.trim(); if str.is_empty() { None |