diff --git a/Cargo.lock b/Cargo.lock index f02baf5..5128401 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -756,6 +756,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "dimensioned" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b0a86c5d31c93238ff4b694fa31f3acdf67440770dc314c57d90e433914397" +dependencies = [ + "generic-array 0.14.7", + "num-traits", + "serde 1.0.188", + "typenum", +] + [[package]] name = "displaydoc" version = "0.2.4" @@ -807,7 +819,7 @@ version = "0.6.0" dependencies = [ "chrono", "chrono-tz", - "dimensioned", + "dimensioned 0.7.0", "serde 1.0.188", "serde_derive", "serde_json", @@ -960,6 +972,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +[[package]] +name = "fitnesstrax" +version = "0.1.0" +dependencies = [ + "gio", + "glib", + "glib-build-tools 0.18.0", + "gtk4", + "libadwaita", + "tokio", +] + [[package]] name = "flate2" version = "1.0.27" @@ -1104,6 +1128,18 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "ft-core" +version = "0.1.0" +dependencies = [ + "chrono", + "chrono-tz", + "dimensioned 0.8.0", + "emseries", + "serde 1.0.188", + "tempfile", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 1957e1d..73f671e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ members = [ "dashboard", "emseries", "file-service", + "fitnesstrax/core", + "fitnesstrax/app", "fluent-ergonomics", "geo-types", "gm-control-panel", diff --git a/build.sh b/build.sh index dcaed56..8311bd5 100755 --- a/build.sh +++ b/build.sh @@ -11,6 +11,7 @@ RUST_ALL_TARGETS=( "dashboard" "emseries" "file-service" + "fitnesstrax" "fluent-ergonomics" "geo-types" "gm-control-panel" diff --git a/emseries/src/series.rs b/emseries/src/series.rs index 71e089f..98f0cb1 100644 --- a/emseries/src/series.rs +++ b/emseries/src/series.rs @@ -42,7 +42,7 @@ where { /// Open a time series database at the specified path. `path` is the full path and filename for /// the database. - pub fn open(path: &str) -> Result, EmseriesReadError> { + pub fn open>(path: P) -> Result, EmseriesReadError> { let f = OpenOptions::new() .read(true) .append(true) diff --git a/emseries/tests/test_io.rs b/emseries/tests/test_io.rs index c599369..3f26f16 100644 --- a/emseries/tests/test_io.rs +++ b/emseries/tests/test_io.rs @@ -99,8 +99,8 @@ mod test { { let tmp_file = tempfile::NamedTempFile::new().expect("temporary path created"); let tmp_path = tmp_file.into_temp_path(); - let ts: Series = Series::open(&tmp_path.to_string_lossy()) - .expect("the time series should open correctly"); + let ts: Series = + Series::open(&tmp_path).expect("the time series should open correctly"); test(ts); } @@ -136,8 +136,8 @@ mod test { pub fn can_search_for_an_entry_with_exact_time() { run_test(|path| { let trips = mk_trips(); - let mut ts: Series = Series::open(&path.to_string_lossy()) - .expect("expect the time series to open correctly"); + let mut ts: Series = + Series::open(&path).expect("expect the time series to open correctly"); for trip in &trips[0..=4] { ts.put(trip.clone()).expect("expect a successful put"); @@ -157,8 +157,8 @@ mod test { pub fn can_get_entries_in_time_range() { run_test(|path| { let trips = mk_trips(); - let mut ts: Series = Series::open(&path.to_string_lossy()) - .expect("expect the time series to open correctly"); + let mut ts: Series = + Series::open(&path).expect("expect the time series to open correctly"); for trip in &trips[0..=4] { ts.put(trip.clone()).expect("expect a successful put"); @@ -186,8 +186,8 @@ mod test { let trips = mk_trips(); { - let mut ts: Series = Series::open(&path.to_string_lossy()) - .expect("expect the time series to open correctly"); + let mut ts: Series = + Series::open(&path).expect("expect the time series to open correctly"); for trip in &trips[0..=4] { ts.put(trip.clone()).expect("expect a successful put"); @@ -195,8 +195,8 @@ mod test { } { - let ts: Series = Series::open(&path.to_string_lossy()) - .expect("expect the time series to open correctly"); + let ts: Series = + Series::open(&path).expect("expect the time series to open correctly"); let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted( time_range( DateTimeTz(UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0).unwrap()).into(), @@ -220,8 +220,8 @@ mod test { let trips = mk_trips(); { - let mut ts: Series = Series::open(&path.to_string_lossy()) - .expect("expect the time series to open correctly"); + let mut ts: Series = + Series::open(&path).expect("expect the time series to open correctly"); for trip in &trips[0..=2] { ts.put(trip.clone()).expect("expect a successful put"); @@ -229,8 +229,8 @@ mod test { } { - let mut ts: Series = Series::open(&path.to_string_lossy()) - .expect("expect the time series to open correctly"); + let mut ts: Series = + Series::open(&path).expect("expect the time series to open correctly"); let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted( time_range( DateTimeTz(UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0).unwrap()).into(), @@ -248,8 +248,8 @@ mod test { } { - let ts: Series = Series::open(&path.to_string_lossy()) - .expect("expect the time series to open correctly"); + let ts: Series = + Series::open(&path).expect("expect the time series to open correctly"); let v: Vec<(&UniqueId, &BikeTrip)> = ts.search_sorted( time_range( DateTimeTz(UTC.with_ymd_and_hms(2011, 10, 31, 0, 0, 0).unwrap()).into(), @@ -273,8 +273,8 @@ mod test { run_test(|path| { let trips = mk_trips(); - let mut ts: Series = Series::open(&path.to_string_lossy()) - .expect("expect the time series to open correctly"); + let mut ts: Series = + Series::open(&path).expect("expect the time series to open correctly"); ts.put(trips[0].clone()).expect("expect a successful put"); ts.put(trips[1].clone()).expect("expect a successful put"); @@ -310,8 +310,8 @@ mod test { let trips = mk_trips(); { - let mut ts: Series = Series::open(&path.to_string_lossy()) - .expect("expect the time series to open correctly"); + let mut ts: Series = + Series::open(&path).expect("expect the time series to open correctly"); ts.put(trips[0].clone()).expect("expect a successful put"); ts.put(trips[1].clone()).expect("expect a successful put"); @@ -327,8 +327,8 @@ mod test { } { - let ts: Series = Series::open(&path.to_string_lossy()) - .expect("expect the time series to open correctly"); + let ts: Series = + Series::open(&path).expect("expect the time series to open correctly"); let trips: Vec<(&UniqueId, &BikeTrip)> = ts.records().collect(); assert_eq!(trips.len(), 3); @@ -356,8 +356,8 @@ mod test { let trips = mk_trips(); { - let mut ts: Series = Series::open(&path.to_string_lossy()) - .expect("expect the time series to open correctly"); + let mut ts: Series = + Series::open(&path).expect("expect the time series to open correctly"); let trip_id = ts.put(trips[0].clone()).expect("expect a successful put"); ts.put(trips[1].clone()).expect("expect a successful put"); ts.put(trips[2].clone()).expect("expect a successful put"); @@ -368,8 +368,8 @@ mod test { } { - let ts: Series = Series::open(&path.to_string_lossy()) - .expect("expect the time series to open correctly"); + let ts: Series = + Series::open(&path).expect("expect the time series to open correctly"); let recs: Vec<(&UniqueId, &BikeTrip)> = ts.records().collect(); assert_eq!(recs.len(), 2); } diff --git a/fitnesstrax/app/Cargo.toml b/fitnesstrax/app/Cargo.toml new file mode 100644 index 0000000..4692329 --- /dev/null +++ b/fitnesstrax/app/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "fitnesstrax" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] } +gio = { version = "0.18" } +glib = { version = "0.18" } +gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] } +tokio = { version = "1.34", features = [ "full" ] } + +[build-dependencies] +glib-build-tools = "0.18" + diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs new file mode 100644 index 0000000..3204d89 --- /dev/null +++ b/fitnesstrax/app/src/main.rs @@ -0,0 +1,38 @@ +use fitnesstrax; +use gtk::prelude::*; +use std::env; + +struct AppState {} + +struct AppWindow { + window: adw::ApplicationWindow, +} + +fn main() { + println!("Hello, world!"); + + let app = adw::Application::builder() + .application_id("com.luminescent-dreams.fitnesstrax") + .resource_base_path("/com/luminescent-dreams/fitnesstrax") + .build(); + + /* + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + */ + + let app = adw::Application::builder() + .application_id("com.luminescent-dreams.fitnesstrax") + .resource_base_path("/com/luminescent-dreams/fitnesstrax") + .build(); + + app.connect_activate(move |app| { + let window = adw::ApplicationWindow::new(app); + window.present(); + }); + + let args: Vec = env::args().collect(); + ApplicationExtManual::run_with_args(&app, &args); +} diff --git a/fitnesstrax/core/Cargo.toml b/fitnesstrax/core/Cargo.toml new file mode 100644 index 0000000..520b69c --- /dev/null +++ b/fitnesstrax/core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ft-core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = { version = "0.4" } +chrono-tz = { version = "0.8" } +dimensioned = { version = "0.8", features = [ "serde" ] } +emseries = { path = "../../emseries" } +serde = { version = "1", features = [ "derive" ] } + +[dev-dependencies] +tempfile = "*" + + diff --git a/fitnesstrax/core/src/legacy.rs b/fitnesstrax/core/src/legacy.rs new file mode 100644 index 0000000..0a79713 --- /dev/null +++ b/fitnesstrax/core/src/legacy.rs @@ -0,0 +1,24 @@ +use crate::types; + +#[cfg(test)] +mod test { + #[test] + fn read_a_legacy_set_rep_record() { + unimplemented!() + } + + #[test] + fn read_a_legacy_steps_record() { + unimplemented!() + } + + #[test] + fn read_a_legacy_time_distance_record() { + unimplemented!() + } + + #[test] + fn read_a_legacy_weight_record() { + unimplemented!() + } +} diff --git a/fitnesstrax/core/src/lib.rs b/fitnesstrax/core/src/lib.rs new file mode 100644 index 0000000..b591639 --- /dev/null +++ b/fitnesstrax/core/src/lib.rs @@ -0,0 +1,6 @@ +use chrono::NaiveDate; +use dimensioned::si; +use emseries::DateTimeTz; + +mod legacy; +mod types; diff --git a/fitnesstrax/core/src/types.rs b/fitnesstrax/core/src/types.rs new file mode 100644 index 0000000..4c332d1 --- /dev/null +++ b/fitnesstrax/core/src/types.rs @@ -0,0 +1,106 @@ +use chrono::NaiveDate; +use dimensioned::si; +use emseries::{DateTimeTz, Recordable, Timestamp}; +use serde::{Deserialize, Serialize}; + +/// SetRep represents workouts like pushups or situps, which involve doing a "set" of a number of +/// actions, resting, and then doing another set. +pub struct SetRep { + /// I assume that a set/rep workout is only done once in a day. + date: NaiveDate, + /// Each set entry represents the number of times that the action was performed in a set. So, a + /// pushup workout that involved five sets would have five entries. Each entry would be x + /// number of pushups. A viable workout would be something like [6, 6, 4, 4, 5]. + sets: Vec, + comments: Option, +} + +/// The number of steps one takes in a single day. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Steps { + date: NaiveDate, + count: u32, +} + +/// TimeDistance represents workouts characterized by a duration and a distance travelled. These +/// sorts of workouts can occur many times a day, depending on how one records things. I might +/// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km +/// workouts if I am out running errands. Distance and Duration are both optional because different +/// people have different priorities and may choose to measure different things. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct TimeDistance { + /// The precise time (and the relevant timezone) of the workout. One of the edge cases that I + /// account for is that a ride which occurred at 11pm in one timezone would then count as 1am + /// if one moved two timezones to the east. This is kind of nonsensical from a human + /// perspective, so the DateTimeTz keeps track of the precise time in UTC, but also the + /// timezone in which the event was recorded. + datetime: DateTimeTz, + /// The distance travelled. This is optional because such a workout makes sense even without + /// the distance. + distance: Option>, + /// The duration of the workout, which is also optional. Some people may keep track of the + /// amount of distance travelled without tracking the duration. + duration: Option>, + comments: Option, +} + +/// A singular daily weight measurement. Weight changes slowly enough that it seems unlikely to +/// need to track more than a single weight in a day. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Weight { + date: NaiveDate, + weight: si::Kilogram, +} + +/// The unified data structure for all records that are part of the app. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum TraxRecord { + BikeRide(TimeDistance), + Row(TimeDistance), + Run(TimeDistance), + Steps(Steps), + Swim(TimeDistance), + Walk(TimeDistance), + Weight(Weight), +} + +impl Recordable for TraxRecord { + fn timestamp(&self) -> Timestamp { + match self { + TraxRecord::BikeRide(rec) => Timestamp::DateTime(rec.datetime.clone()), + TraxRecord::Row(rec) => Timestamp::DateTime(rec.datetime.clone()), + TraxRecord::Run(rec) => Timestamp::DateTime(rec.datetime.clone()), + TraxRecord::Steps(rec) => Timestamp::Date(rec.date), + TraxRecord::Swim(rec) => Timestamp::DateTime(rec.datetime.clone()), + TraxRecord::Walk(rec) => Timestamp::DateTime(rec.datetime.clone()), + TraxRecord::Weight(rec) => Timestamp::Date(rec.date), + } + } + + fn tags(&self) -> Vec { + vec![] + } +} + +#[cfg(test)] +mod test { + use super::*; + use emseries::Series; + + #[test] + fn can_record_records() { + let file = tempfile::NamedTempFile::new().expect("a temporary file"); + let path = file.into_temp_path(); + let mut series: Series = Series::open(&path).unwrap(); + + let record = TraxRecord::Steps(Steps { + date: NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(), + count: 1000, + }); + + let id = series.put(record.clone()).unwrap(); + + let record_ = series.get(&id).unwrap(); + assert_eq!(record_, record); + } +}