aboutsummaryrefslogtreecommitdiff
path: root/karld
diff options
context:
space:
mode:
Diffstat (limited to 'karld')
-rw-r--r--karld/Cargo.lock314
-rw-r--r--karld/Cargo.toml14
-rw-r--r--karld/protocol.d.ts75
-rw-r--r--karld/src/condition.rs364
-rw-r--r--karld/src/interface.rs69
-rw-r--r--karld/src/main.rs64
-rw-r--r--karld/src/protocol.rs34
7 files changed, 934 insertions, 0 deletions
diff --git a/karld/Cargo.lock b/karld/Cargo.lock
new file mode 100644
index 0000000..2b4b0b9
--- /dev/null
+++ b/karld/Cargo.lock
@@ -0,0 +1,314 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "time",
+ "winapi",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
+dependencies = [
+ "cfg-if",
+ "lazy_static",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "itoa"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
+
+[[package]]
+name = "karld"
+version = "0.1.1"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "crossbeam-channel",
+ "env_logger",
+ "lazy_static",
+ "log",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
+
+[[package]]
+name = "ryu"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
+
+[[package]]
+name = "serde"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.96"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "time"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
+dependencies = [
+ "libc",
+ "wasi",
+ "winapi",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/karld/Cargo.toml b/karld/Cargo.toml
new file mode 100644
index 0000000..4c2221d
--- /dev/null
+++ b/karld/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "karld"
+version = "0.1.1"
+edition = "2021"
+
+[dependencies]
+serde = { version = "1.0.137", features = ["derive"] }
+anyhow = "1.0.57"
+log = "0.4.17"
+env_logger = "0.9.0"
+crossbeam-channel = "0.5.4"
+serde_json = "1.0.81"
+chrono = "0.4.19"
+lazy_static = "1.4.0"
diff --git a/karld/protocol.d.ts b/karld/protocol.d.ts
new file mode 100644
index 0000000..a592146
--- /dev/null
+++ b/karld/protocol.d.ts
@@ -0,0 +1,75 @@
+
+// { type: "handshake", version: "10"}
+// { type: "handshake", data: {version: "10"}}
+//! { "handshake": {version: "10"}}
+
+export type ServerboundPacket = Download | UpdateTask | RemoveTask
+export type ClientboundPacket = Handshake | DownloadResponse
+
+interface Handshake {
+ type: "handshake"
+ data: { version: string }
+}
+
+interface Download {
+ type: "download",
+ data: null
+}
+interface DownloadResponse {
+ type: "download_response",
+ data: { tasks: Task[] }
+}
+
+interface UpdateTask {
+ type: "update_task",
+ data: Task
+}
+interface RemoveTask {
+ type: "remove_task",
+ data: Task
+}
+
+interface Task {
+ id: number
+ name: string,
+ description: string,
+
+ tags: string[],
+ priority: number,
+
+ completed?: number,
+ scheduled?: number,
+
+ occurence?: Condition,
+ deadline?: Condition,
+}
+
+export type Condition = { from?: Condition }
+ | { or?: Condition[] }
+ | { and?: Condition[] }
+ | { equal?: { prop: Thing, value: number, mod?: number } }
+ | { range?: { prop: Thing, min: number, max: number, mod?: number } }
+
+type Thing = "year"
+ | "monthofyear"
+ | "weekofmonth"
+ | "dayofyear"
+ | "dayofmonth"
+ | "dayofweek"
+ | "hour"
+ | "minute"
+ | "second"
+ | "unix"
+
+/*
+ examples:
+
+ 11:00 - 12:00 every first monday of the month
+
+ and: [
+ { range: { prop: "hour", min: 11, max: 12 } },
+ { equal: { prop: "dayofweek", value: 0 } },
+ { equal: { prop: "weekofmonth", value: 0 } }
+ ]
+
+*/
diff --git a/karld/src/condition.rs b/karld/src/condition.rs
new file mode 100644
index 0000000..830e8ae
--- /dev/null
+++ b/karld/src/condition.rs
@@ -0,0 +1,364 @@
+use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
+use serde::{Deserialize, Serialize};
+use std::cmp::{max, min};
+use Direction::*;
+use Edge::*;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum Condition {
+ From(Box<Condition>),
+
+ Or(Vec<Condition>),
+ And(Vec<Condition>),
+ Invert(Box<Condition>),
+
+ Equal {
+ prop: Property,
+ value: i64,
+ modulus: Option<i64>,
+ },
+ Range {
+ prop: Property,
+ min: i64,
+ max: i64,
+ modulus: Option<i64>,
+ },
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum Property {
+ Year,
+ Monthofyear,
+ Weekofmonth,
+ Dayofyear,
+ Dayofmonth,
+ Dayofweek,
+ Hour,
+ Minute,
+ Second,
+ Unix,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Range<T>(T, T);
+impl<T: PartialOrd + PartialEq> Range<T> {
+ pub fn includes(&self, a: T) -> bool {
+ a > self.0 && a < self.1
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Edge {
+ Start,
+ End,
+}
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Direction {
+ Forward,
+ Backward,
+}
+
+impl Condition {
+ pub fn find(
+ &self,
+ edge: Edge,
+ dir: Direction,
+ mut from: NaiveDateTime,
+ ) -> Option<NaiveDateTime> {
+ match self {
+ Condition::And(cs) => loop {
+ // TODO improve efficiency for backward search
+ let last_start = cs
+ .iter()
+ .map(|c| c.find(Start, dir, from))
+ .reduce(|a, b| Some(max(a?, b?)))?;
+ let first_end = cs
+ .iter()
+ .map(|c| c.find(End, dir, from))
+ .reduce(|a, b| Some(min(a?, b?)))?;
+ match last_start {
+ Some(start) => match first_end {
+ Some(end) => {
+ if end > start {
+ break Some(match edge {
+ Start => start,
+ End => end,
+ });
+ } else {
+ from = start - Duration::seconds(10); // TODO proper fix
+ }
+ }
+ None => break Some(start),
+ },
+ None => break None,
+ }
+ },
+ Condition::Or(cs) => {
+ cs.iter()
+ .filter_map(|c| c.find(edge, dir, from))
+ .reduce(match dir {
+ Forward => min,
+ Backward => max,
+ })
+ }
+ Condition::From(_c) => todo!(),
+ Condition::Invert(c) => c.find(edge.invert(), dir, from),
+ Condition::Equal {
+ prop,
+ value,
+ modulus: _,
+ } => {
+ let value = *value;
+ let off: i64 = match edge {
+ Start => 0,
+ End => 1,
+ };
+ let dir_off = match dir {
+ Forward => 0,
+ Backward => -1,
+ };
+ match prop {
+ Property::Year => {
+ let geq = match dir {
+ Forward => |a, b| a >= b,
+ Backward => |a, b| a < b,
+ };
+
+ if geq(from.year(), (value + off) as i32) {
+ None
+ } else {
+ Some(NaiveDateTime::new(
+ NaiveDate::from_ymd((value + off) as i32, 1, 1),
+ NaiveTime::from_hms(0, 0, 0),
+ ))
+ }
+ }
+ Property::Monthofyear => {
+ let rollover = (value + off) / 12;
+ let value = (value + off) % 12;
+ // month still coming for this year
+ if from.month0() < value as u32 {
+ Some(NaiveDateTime::new(
+ NaiveDate::from_ymd(
+ from.year() + (rollover + dir_off) as i32,
+ value as u32 + 1,
+ 1,
+ ),
+ NaiveTime::from_hms(0, 0, 0),
+ ))
+ } else {
+ Some(NaiveDateTime::new(
+ NaiveDate::from_ymd(
+ from.year() + (rollover + dir_off + 1) as i32,
+ value as u32 + 1,
+ 1,
+ ),
+ NaiveTime::from_hms(0, 0, 0),
+ ))
+ }
+ }
+ Property::Weekofmonth => todo!(),
+ Property::Dayofyear => {
+ todo!()
+ }
+ Property::Dayofmonth => {
+ // let mut target = NaiveDateTime::new(
+ // NaiveDate::from_ymd(from.year(), from.month(), value as u32),
+ // NaiveTime::from_hms(0, 0, 0),
+ // );
+ // if edge == End {
+ // target += Duration::days(1)
+ // }
+ // fn increment_month(d: NaiveDateTime) -> NaiveDateTime {
+ // NaiveDateTime::new(
+ // NaiveDate::from_ymd(
+ // d.year() + (d.month() as i32 / 12),
+ // (d.month() + 1) % 12,
+ // d.day(),
+ // ),
+ // NaiveTime::from_hms(d.hour(), d.minute(), d.second()),
+ // )
+ // }
+ // let dir_off = match dir {
+ // Forward => |d| d,
+ // Backward => increment_month,
+ // };
+ // if target > from {
+ // Some(dir_off(target))
+ // } else {
+ // Some(increment_month(dir_off(target)))
+ // }
+ todo!()
+ }
+ Property::Dayofweek => todo!(),
+ Property::Hour => {
+ let mut target = NaiveDateTime::new(
+ NaiveDate::from_ymd(from.year(), from.month(), from.day()),
+ NaiveTime::from_hms(value as u32, 0, 0),
+ );
+ if edge == End {
+ target += Duration::hours(1)
+ }
+ let dir_off = match dir {
+ Forward => Duration::zero(),
+ Backward => Duration::days(-1),
+ };
+ if target > from {
+ Some(target + dir_off)
+ } else {
+ Some(target + dir_off + Duration::days(1))
+ }
+ }
+ Property::Minute => {
+ let mut target = NaiveDateTime::new(
+ NaiveDate::from_ymd(from.year(), from.month(), from.day()),
+ NaiveTime::from_hms(from.hour(), value as u32, 0),
+ );
+ if edge == End {
+ target += Duration::minutes(1)
+ }
+ let dir_off = match dir {
+ Forward => Duration::zero(),
+ Backward => Duration::hours(-1),
+ };
+ if target > from {
+ Some(target + dir_off)
+ } else {
+ Some(target + dir_off + Duration::hours(1))
+ }
+ }
+ Property::Second => {
+ let mut target = NaiveDateTime::new(
+ NaiveDate::from_ymd(from.year(), from.month(), from.day()),
+ NaiveTime::from_hms(from.hour(), from.minute(), value as u32),
+ );
+ if edge == End {
+ target += Duration::seconds(1)
+ }
+ let dir_off = match dir {
+ Forward => Duration::zero(),
+ Backward => Duration::minutes(-1),
+ };
+ if target > from {
+ Some(target + dir_off)
+ } else {
+ Some(target + dir_off + Duration::minutes(1))
+ }
+ }
+ Property::Unix => {
+ let geq = match dir {
+ Forward => |a, b| a >= b,
+ Backward => |a, b| a < b,
+ };
+ if geq(from.timestamp(), (value + off) as i64) {
+ None
+ } else {
+ Some(NaiveDateTime::from_timestamp(value, 0))
+ }
+ }
+ }
+ }
+ Condition::Range {
+ prop: _,
+ min: _,
+ max: _,
+ modulus: _,
+ } => todo!(),
+ }
+ }
+}
+
+impl Edge {
+ pub fn invert(self) -> Self {
+ match self {
+ Edge::Start => Edge::End,
+ Edge::End => Edge::Start,
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::{Condition, Direction, Edge, Property};
+ use chrono::{NaiveDateTime, Utc};
+ use std::str::FromStr;
+ use Direction::*;
+ use Edge::*;
+
+ #[test]
+ fn blub() {
+ let cond = Condition::And(vec![
+ Condition::Equal {
+ modulus: None,
+ prop: Property::Monthofyear,
+ value: 1,
+ },
+ Condition::Equal {
+ modulus: None,
+ prop: Property::Hour,
+ value: 12,
+ },
+ ]);
+ // let cond = Condition::Equal {
+ // modulus: None,
+ // prop: Property::Hour,
+ // value: 12,
+ // };
+ let dt = Utc::now().naive_utc();
+ println!("START FORWARD => {:?}", cond.find(Start, Forward, dt));
+ println!("END FORWARD => {:?}", cond.find(End, Forward, dt));
+ println!("START BACKWARD => {:?}", cond.find(Start, Backward, dt));
+ println!("END BACKWARD => {:?}", cond.find(End, Backward, dt));
+ }
+
+ #[test]
+ fn year_equal() {
+ let cond = Condition::Equal {
+ modulus: None,
+ prop: Property::Year,
+ value: 2023,
+ };
+
+ let dt = NaiveDateTime::from_str("2022-06-07T13:37:48").unwrap();
+ assert_eq!(
+ cond.find(Edge::Start, Direction::Forward, dt).unwrap(),
+ NaiveDateTime::from_str("2023-01-01T00:00:00").unwrap(),
+ );
+ assert_eq!(
+ cond.find(Edge::End, Direction::Forward, dt).unwrap(),
+ NaiveDateTime::from_str("2024-01-01T00:00:00").unwrap(),
+ );
+ assert_eq!(cond.find(Edge::End, Direction::Backward, dt), None);
+ assert_eq!(cond.find(Edge::Start, Direction::Backward, dt), None);
+ }
+
+ #[test]
+ fn month_equal() {
+ let cond = Condition::Equal {
+ modulus: None,
+ prop: Property::Monthofyear,
+ value: 3,
+ };
+
+ let dt = NaiveDateTime::from_str("2022-06-07T13:37:48").unwrap();
+
+ assert_eq!(
+ cond.find(Edge::Start, Direction::Forward, dt),
+ Some(NaiveDateTime::from_str("2023-04-01T00:00:00").unwrap())
+ );
+ assert_eq!(
+ cond.find(Edge::End, Direction::Forward, dt),
+ Some(NaiveDateTime::from_str("2023-05-01T00:00:00").unwrap())
+ );
+ assert_eq!(
+ cond.find(Edge::Start, Direction::Backward, dt),
+ Some(NaiveDateTime::from_str("2022-04-01T00:00:00").unwrap())
+ );
+ assert_eq!(
+ cond.find(Edge::End, Direction::Backward, dt),
+ Some(NaiveDateTime::from_str("2022-05-01T00:00:00").unwrap())
+ );
+ }
+}
diff --git a/karld/src/interface.rs b/karld/src/interface.rs
new file mode 100644
index 0000000..e3d2ba3
--- /dev/null
+++ b/karld/src/interface.rs
@@ -0,0 +1,69 @@
+use super::protocol::{ClientboundPacket, ServerboundPacket};
+use crate::handle_packet;
+use log::{debug, error, info, warn};
+use std::io;
+use std::io::{BufRead, BufReader, ErrorKind, Write};
+use std::os::unix::net::{UnixListener, UnixStream};
+use std::thread;
+
+pub fn network_loop() {
+ let listener = UnixListener::bind("/run/user/1000/calendar").unwrap();
+ info!("listening.");
+ let mut id_counter = 0;
+
+ loop {
+ let (stream, addr) = listener.accept().unwrap();
+ let id = id_counter;
+ id_counter += 1;
+ thread::spawn(move || {
+ info!("client connected: {:?}", addr);
+ if let Err(err) = handle_connection(id, stream) {
+ warn!("client dropped: {:?} ({})", addr, err);
+ } else {
+ info!("client dropped: {:?}", addr);
+ }
+ });
+ }
+}
+
+fn handle_connection(id: u32, mut stream: UnixStream) -> io::Result<()> {
+ let mut reader = BufReader::new(stream.try_clone()?);
+ let (responder, responses) = crossbeam_channel::unbounded();
+ responder
+ .send(ClientboundPacket::Handshake {
+ version: env!("CARGO_PKG_VERSION").to_string(),
+ })
+ .unwrap();
+ thread::spawn(move || {
+ for m in responses {
+ debug!("{id} -> {m:?}");
+ match stream
+ .write_fmt(format_args!("{}\n", serde_json::to_string(&m).unwrap()))
+ .map_err(|e| e.kind())
+ {
+ Ok(_) => (),
+ Err(ErrorKind::BrokenPipe) => break,
+ Err(e) => error!("network error: {:?}", e),
+ }
+ }
+ });
+ {
+ let mut buf = String::new();
+ loop {
+ if reader.read_line(&mut buf)? == 0 {
+ break Ok(());
+ };
+ match serde_json::from_str::<ServerboundPacket>(buf.as_str()) {
+ Ok(packet) => {
+ debug!("{id} <- {packet:?}");
+ handle_packet(id, packet, responder.clone());
+ }
+ Err(err) => responder
+ .send(ClientboundPacket::Error(format!("{}", &err)))
+ .map_err(|_| io::Error::from(ErrorKind::InvalidInput))?,
+ }
+
+ buf.clear();
+ }
+ }
+}
diff --git a/karld/src/main.rs b/karld/src/main.rs
new file mode 100644
index 0000000..ae49ad3
--- /dev/null
+++ b/karld/src/main.rs
@@ -0,0 +1,64 @@
+pub mod condition;
+pub mod interface;
+pub mod protocol;
+
+use std::{collections::HashMap, sync::RwLock};
+
+use crate::{
+ condition::{Condition, Property},
+ protocol::Task,
+};
+use crossbeam_channel::Sender;
+use interface::network_loop;
+use protocol::{ClientboundPacket, ServerboundPacket};
+
+fn main() {
+ env_logger::init();
+ TASKS.write().unwrap().insert(
+ 0,
+ Task {
+ id: 0,
+ name: "blub".to_string(),
+ description: "blob".to_string(),
+ tags: vec![],
+ priority: 69.0,
+ completed: None,
+ scheduled: None,
+ occurence: Some(Condition::And(vec![
+ Condition::Equal {
+ modulus: None,
+ prop: Property::Monthofyear,
+ value: 1,
+ },
+ Condition::Equal {
+ modulus: None,
+ prop: Property::Hour,
+ value: 12,
+ },
+ ])),
+ deadline: None,
+ },
+ );
+ network_loop();
+}
+
+lazy_static::lazy_static! {
+ static ref TASKS: RwLock<HashMap<u64, Task>> = RwLock::new(HashMap::new());
+}
+
+pub fn handle_packet(client: u32, packet: ServerboundPacket, responder: Sender<ClientboundPacket>) {
+ println!("{:?}, {:?}, {:?}", client, packet, responder);
+ match packet {
+ ServerboundPacket::Download => {
+ let _ = responder.send(ClientboundPacket::DownloadResponse(
+ TASKS.read().unwrap().values().map(|e| e.clone()).collect(),
+ ));
+ }
+ ServerboundPacket::UpdateTask(t) => {
+ TASKS.write().unwrap().insert(t.id, t);
+ }
+ ServerboundPacket::RemoveTask(i) => {
+ TASKS.write().unwrap().remove(&i);
+ }
+ }
+}
diff --git a/karld/src/protocol.rs b/karld/src/protocol.rs
new file mode 100644
index 0000000..40ab0b2
--- /dev/null
+++ b/karld/src/protocol.rs
@@ -0,0 +1,34 @@
+use crate::condition::Condition;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "type", content = "data", rename_all = "snake_case")]
+pub enum ClientboundPacket {
+ Handshake { version: String },
+ Error(String),
+ DownloadResponse(Vec<Task>),
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "type", content = "data", rename_all = "snake_case")]
+pub enum ServerboundPacket {
+ Download,
+ UpdateTask(Task),
+ RemoveTask(u64),
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Task {
+ pub id: u64,
+ pub name: String,
+ pub description: String,
+
+ pub tags: Vec<String>,
+ pub priority: f64,
+
+ pub completed: Option<u64>,
+ pub scheduled: Option<u64>,
+
+ pub occurence: Option<Condition>,
+ pub deadline: Option<Condition>,
+}