diff --git a/Cargo.lock b/Cargo.lock index 7e5d422..83b54a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1722,6 +1722,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "thiserror", "tokio", ] diff --git a/modules/desktop-environment/home/panels/eww/configDir/bottomBar/bottomBar.yuck b/modules/desktop-environment/home/panels/eww/configDir/bottomBar/bottomBar.yuck index 6c31965..2034740 100644 --- a/modules/desktop-environment/home/panels/eww/configDir/bottomBar/bottomBar.yuck +++ b/modules/desktop-environment/home/panels/eww/configDir/bottomBar/bottomBar.yuck @@ -1,3 +1,6 @@ +(include "bottomBar/workspaces.yuck") +(include "bottomBar/traveldings.yuck") + (defwindow bottomBar :monitor 0 :stacking "fg" @@ -19,7 +22,7 @@ ) (box :halign "center" - (label :text "mid") + (traveldings) ) (box :halign "end" @@ -39,21 +42,3 @@ ) ) ) - -(defwidget workspaceWidget [] - (box - :class "workspaces" - (for workspace in workspaces - (button - :class "${workspace.urgent ? "urgent" : ""} ${workspace.focused ? "focused" : 0}" - :onclick "swaymsg workspace ${workspace.name}" - (label :text "${workspace.name}") - ) - ) - ) -) - -(deflisten workspaces - :initial "[]" - "bar-ws-monitor" -) diff --git a/modules/desktop-environment/home/panels/eww/configDir/bottomBar/traveldings.yuck b/modules/desktop-environment/home/panels/eww/configDir/bottomBar/traveldings.yuck new file mode 100644 index 0000000..0e13b28 --- /dev/null +++ b/modules/desktop-environment/home/panels/eww/configDir/bottomBar/traveldings.yuck @@ -0,0 +1,50 @@ +(defwidget traveldings [] + (revealer + :class "traveldings" + :transition "crossfade" + :reveal { traveldings_data != "null" } + (traveldingsBarWidget) + ) +) + +(defwidget traveldingsBarWidget [] + (overlay + (box + :width 640 + (label + :halign "start" + :text "${traveldings_data.line} -> ${strlength(traveldings_data.arrival_station) > 24 ? "${substring(traveldings_data.arrival_station, 0, 24)}…" : traveldings_data.arrival_station}${traveldings_data.arrival_platform_data_available ? " (Gl. ${traveldings_data.arrival_platform_real})" : ""}" + ) + (label + :halign "end" + :text { traveldings_data.time_left >= 3600 ? formattime(traveldings_data.time_left, "noch %-Hh %-Mmin", "Etc/UTC") : formattime(traveldings_data.time_left, "noch %-Mmin", "Etc/UTC") } + ) + ) + (box + (progress + :value {traveldings_data.progress * 100} + :orientation "horizontal" + ) + ) + ) +) + +(defwindow traveldingsWindow + :monitor 0 + :stacking "overlay" + :exclusive "false" + :geometry (geometry + :x "0%" + :y "6.6%" + :width "30%" + :height "20%" + :anchor "bottom center") + (box + :class "traveldingsWindow" + (label :text "TODO")) +) + +(deflisten traveldings_data + :initial "null" + "traveldings current" +) diff --git a/modules/desktop-environment/home/panels/eww/configDir/bottomBar/workspaces.yuck b/modules/desktop-environment/home/panels/eww/configDir/bottomBar/workspaces.yuck new file mode 100644 index 0000000..4c5495f --- /dev/null +++ b/modules/desktop-environment/home/panels/eww/configDir/bottomBar/workspaces.yuck @@ -0,0 +1,17 @@ +(defwidget workspaceWidget [] + (box + :class "workspaces" + (for workspace in workspaces + (button + :class "${workspace.urgent ? "urgent" : ""} ${workspace.focused ? "focused" : 0}" + :onclick "swaymsg workspace ${workspace.name}" + (label :text "${workspace.name}") + ) + ) + ) +) + +(deflisten workspaces + :initial "[]" + "bar-ws-monitor" +) diff --git a/modules/desktop-environment/home/panels/eww/configDir/eww.css b/modules/desktop-environment/home/panels/eww/configDir/eww.css index 848f77a..f512917 100644 --- a/modules/desktop-environment/home/panels/eww/configDir/eww.css +++ b/modules/desktop-environment/home/panels/eww/configDir/eww.css @@ -13,4 +13,21 @@ label { .workspaces button.focused { background-color: #504935; +} + +.traveldings progressbar trough { + border: none; + background-color: #3c3836; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.traveldings progressbar progress { + background-color: #79740e; + border-bottom-left-radius: 0; +} + + +.traveldingsWindow { + border-radius: 15px; } \ No newline at end of file diff --git a/programs/traveldings/Cargo.toml b/programs/traveldings/Cargo.toml index 3b8ad29..c21a6d1 100644 --- a/programs/traveldings/Cargo.toml +++ b/programs/traveldings/Cargo.toml @@ -8,6 +8,7 @@ serde = { version = "1.0.209", features = ["derive"] } serde_json = "1.0.128" reqwest = {version = "0.12.7", default-features = false, features = ["rustls-tls", "charset", "http2"]} tokio = { version = "1", features = ["full"] } +thiserror = "1" anyhow = "1" chrono = { version = "0.4", features = ["serde"]} clap = { version = "4.5", features = ["derive"]} diff --git a/programs/traveldings/src/commands/current_journey.rs b/programs/traveldings/src/commands/current_journey.rs index ee57973..b0864bf 100644 --- a/programs/traveldings/src/commands/current_journey.rs +++ b/programs/traveldings/src/commands/current_journey.rs @@ -1,9 +1,184 @@ -use crate::traewelling::TraewellingClient; +use std::time::Duration; + +use chrono::Local; +use reqwest::StatusCode; +use serde::Serialize; +use tokio::time::sleep; + +use crate::traewelling::{ + model::{JsonableData, Status, StopJourneyPart}, + RequestErr, TraewellingClient, +}; pub async fn get_current_journey() -> anyhow::Result<()> { let client = TraewellingClient::new()?; - println!("active: {:#?}", client.get_active_checkin().await?); + let mut state; + let mut cur_active_checkin = None; + + loop { + match client.get_active_checkin().await { + Ok(status) => { + cur_active_checkin = Some(status); + state = State::Live; + } + Err(err) => { + if err == RequestErr::WithStatus(StatusCode::NOT_FOUND) { + state = State::NoCheckin; + cur_active_checkin = None; + } else { + state = State::NoConnectionOrSomethingElseDoesntWork; + } + } + }; + + match (state, &cur_active_checkin) { + (State::Live | State::NoConnectionOrSomethingElseDoesntWork, Some(status)) => { + let live = state == State::Live; + let out = CurrentJourneyOutput::new(&status, live); + + println!( + "{}", + serde_json::to_string(&out) + .expect("serde should not make you sad but it does because it's serde") + ); + sleep(Duration::from_secs(20)).await; + } + (_, None) | (State::NoCheckin, Some(_)) => { + println!("null"); + sleep(Duration::from_secs(60)).await; + } + } + } Ok(()) } + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +enum State { + Live, + NoConnectionOrSomethingElseDoesntWork, + NoCheckin, +} + +#[derive(Serialize)] +struct CurrentJourneyOutput { + live: bool, + // Journey progress, 0.0-1.0 + progress: Option, + time_left: Option, + icon: String, + line: String, + + // Invalid data received? + departure_err: bool, + departure_planned: Option, + departure_real: Option, + departure_station: String, + departure_ril100: Option, + departure_platform_data_available: bool, + departure_platform_planned: Option, + departure_platform_real: Option, + + // Invalid data received? + arrival_err: bool, + arrival_planned: Option, + arrival_real: Option, + arrival_station: String, + arrival_ril100: Option, + arrival_platform_data_available: bool, + arrival_platform_planned: Option, + arrival_platform_real: Option, +} + +impl CurrentJourneyOutput { + fn new(checkin: &Status, live: bool) -> Self { + let JsonableData { + time_err: departure_err, + time_planned: departure_planned, + time_real: departure_real, + station: departure_station, + ril100: departure_ril100, + platform_data_available: departure_platform_data_available, + platform_planned: departure_platform_planned, + platform_real: departure_platform_real, + } = checkin.train.origin.get_time_data(StopJourneyPart::Origin); + let JsonableData { + time_err: arrival_err, + time_planned: arrival_planned, + time_real: arrival_real, + station: arrival_station, + ril100: arrival_ril100, + platform_data_available: arrival_platform_data_available, + platform_planned: arrival_platform_planned, + platform_real: arrival_platform_real, + } = checkin + .train + .destination + .get_time_data(StopJourneyPart::Destination); + + let (progress, time_left) = if !departure_err && !arrival_err { + let departure = departure_real.unwrap_or(departure_planned.unwrap()); + let arrival = arrival_real.unwrap_or(arrival_planned.unwrap()); + let dur = arrival - departure; + + let now = Local::now().timestamp(); + + let progress = ((now - departure) as f32) / dur as f32; + let time_left = arrival - now; + + (Some(progress), Some(time_left)) + } else { + (None, None) + }; + + let icon = match checkin.train.category.as_str() { + "nationalExpress" | "national" => "longDistanceTrans", + "regionalExp" | "regional" => "regionalTrans", + "suburban" => "localTrans", + "subway" => "subTrans", + "bus" => "bus", + "tram" => "tram", + "ferry" => "ferry", + _ => "other", + } + .to_string(); + + CurrentJourneyOutput { + live, + progress, + time_left, + icon, + line: checkin.train.line_name.clone(), + departure_err, + departure_planned, + departure_real, + departure_station, + departure_ril100, + departure_platform_data_available, + departure_platform_planned, + departure_platform_real, + arrival_err, + arrival_planned, + arrival_real, + arrival_station, + arrival_ril100, + arrival_platform_data_available, + arrival_platform_planned, + arrival_platform_real, + } + } +} + +enum TransportType { + // FV, ob jetzt NJ, IC, ICE... egal + LongDistanceTrans, + RegionalTrans, + // S-bahn... + LocalTrans, + // U-bahn + SubTrans, + Bus, + Tram, + Ferry, +} diff --git a/programs/traveldings/src/traewelling.rs b/programs/traveldings/src/traewelling.rs index 3f55f5a..e1537e8 100644 --- a/programs/traveldings/src/traewelling.rs +++ b/programs/traveldings/src/traewelling.rs @@ -3,7 +3,7 @@ use std::{fmt, fs}; use model::{Container, Status}; use reqwest::{ header::{self, HeaderMap}, - Client, ClientBuilder, + Client, ClientBuilder, StatusCode, }; const KEY_PATH: &str = "/home/jade/Docs/traveldings-key"; @@ -19,7 +19,6 @@ impl TraewellingClient { let mut headers = HeaderMap::new(); let token = fs::read_to_string(KEY_PATH)?; let key = header::HeaderValue::from_str(&format!("Bearer {token}"))?; - println!("meow"); headers.insert("Authorization", key); headers.insert( header::ACCEPT, @@ -33,16 +32,17 @@ impl TraewellingClient { }) } - pub async fn get_active_checkin(&self) -> anyhow::Result { - let txt = self + pub async fn get_active_checkin(&self) -> Result { + let res = self .client .get(Self::fmt_url("user/statuses/active")) .send() - .await? - .text() .await?; + if res.status() != StatusCode::OK { + return Err(RequestErr::WithStatus(res.status())); + } - println!("{txt}"); + let txt = res.text().await?; let res: Container = serde_json::de::from_str(&txt)?; Ok(res.data) @@ -53,4 +53,35 @@ impl TraewellingClient { } } +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +pub enum RequestErr { + #[error("Couldn't deserialize the json :(")] + DeserializationError, + #[error("an error related to connect happened!!")] + RelatedToConnect, + #[error("error haz status: {0}")] + WithStatus(StatusCode), + #[error("fuck if i know what went wrong :333 am silly ")] + Other, +} + +impl From for RequestErr { + fn from(value: serde_json::Error) -> Self { + eprintln!("serde error: {value:?}"); + Self::DeserializationError + } +} + +impl From for RequestErr { + fn from(value: reqwest::Error) -> Self { + if let Some(status) = value.status() { + Self::WithStatus(status) + } else if value.is_connect() { + Self::RelatedToConnect + } else { + Self::Other + } + } +} + pub mod model; diff --git a/programs/traveldings/src/traewelling/model.rs b/programs/traveldings/src/traewelling/model.rs index b40554a..090d36f 100644 --- a/programs/traveldings/src/traewelling/model.rs +++ b/programs/traveldings/src/traewelling/model.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, FixedOffset}; +use chrono::{DateTime, FixedOffset, Timelike}; use serde::Deserialize; #[derive(Deserialize, Debug)] @@ -9,39 +9,88 @@ pub struct Container { #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Status { - train: TransportResource, + pub train: TransportResource, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct TransportResource { - category: String, - line_name: String, - distance: u32, - duration: u32, - operator: OperatorResource, - origin: StopOverResource, - destination: StopOverResource, + pub category: String, + pub line_name: String, + pub distance: u32, + pub duration: u32, + pub operator: Option, + pub origin: StopOverResource, + pub destination: StopOverResource, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct StopOverResource { - name: String, - ril_identifier: Option, - arrival: Option>, - arrival_planned: Option>, - arrival_real: Option>, - departure: Option>, - departure_planned: Option>, - departure_real: Option>, - platform: Option, - departure_platform_planned: Option, - departure_platform_real: Option, + pub name: String, + pub ril_identifier: Option, + pub arrival_planned: Option>, + pub arrival_real: Option>, + pub departure_planned: Option>, + pub departure_real: Option>, + pub platform: Option, + pub departure_platform_planned: Option, + pub departure_platform_real: Option, + pub arrival_platform_planned: Option, + pub arrival_platform_real: Option, +} + +// ???? +pub struct JsonableData { + pub time_err: bool, + pub time_planned: Option, + pub time_real: Option, + pub station: String, + pub ril100: Option, + pub platform_data_available: bool, + pub platform_planned: Option, + pub platform_real: Option, +} + +// What the meaning of the stop in the journey is +pub enum StopJourneyPart { + Origin, + Destination, +} +impl StopOverResource { + pub fn get_time_data(&self, journey_part: StopJourneyPart) -> JsonableData { + let (time_planned, time_real, platform_planned, platform_real) = match journey_part { + StopJourneyPart::Origin => ( + self.departure_planned, + self.departure_real, + self.departure_platform_planned.clone(), + self.departure_platform_real.clone(), + ), + StopJourneyPart::Destination => ( + self.arrival_planned, + self.arrival_real, + self.arrival_platform_planned.clone(), + self.arrival_platform_real.clone(), + ), + }; + + let time_err = time_planned == None; + + JsonableData { + time_err, + time_planned: time_planned.map(|ts| ts.timestamp()), + time_real: time_real.map(|ts| ts.timestamp()), + station: self.name.clone(), + ril100: self.ril_identifier.clone(), + platform_data_available: platform_planned.is_some() || platform_real.is_some(), + platform_planned, + platform_real, + } + } } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct OperatorResource { - name: String, + pub name: String, }