This commit is contained in:
parent
265f8d8dc3
commit
450183c06b
5 changed files with 419 additions and 70 deletions
|
@ -43,9 +43,9 @@ dotenvy_macro = "0.15.7"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
getrandom = { version = "0.3.3", features = ["wasm_js"] }
|
getrandom = { version = "0.3.3", features = ["wasm_js"] }
|
||||||
heapless = "0.8.0"
|
heapless = "0.8.0"
|
||||||
|
openweathermap_lib = { git = "https://github.com/StratusFearMe21/rusty-openweathermap-library", branch = "wasm_deps" }
|
||||||
|
phf = { version = "0.12.1", features = ["macros"] }
|
||||||
rand = { version = "0.9.2", default-features = false, features = ["os_rng", "std"] }
|
rand = { version = "0.9.2", default-features = false, features = ["os_rng", "std"] }
|
||||||
reqwest = { version = "0.12.22", features = ["json"] }
|
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
|
||||||
tachyonfx = { version = "0.16.0", default-features = false, features = ["web-time"] }
|
tachyonfx = { version = "0.16.0", default-features = false, features = ["web-time"] }
|
||||||
web-time = "1.1.0"
|
web-time = "1.1.0"
|
||||||
|
|
||||||
|
|
|
@ -1,30 +1,35 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
cell::OnceCell,
|
||||||
future::Future,
|
future::Future,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::atomic::{AtomicUsize, Ordering},
|
||||||
atomic::{AtomicUsize, Ordering},
|
|
||||||
mpsc::{self, Receiver, Sender},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use openweathermap_lib::{
|
||||||
|
location::Location,
|
||||||
|
weather::{WeatherClient, WeatherResponse},
|
||||||
|
};
|
||||||
use rand::{rngs::OsRng, seq::IndexedRandom, TryRngCore};
|
use rand::{rngs::OsRng, seq::IndexedRandom, TryRngCore};
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use ratzilla::ratatui;
|
use ratzilla::ratatui;
|
||||||
|
|
||||||
use dotenvy_macro::dotenv;
|
use dotenvy_macro::dotenv;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Margin, Rect},
|
||||||
style::{Color, Style, Stylize},
|
style::{Color, Style, Stylize},
|
||||||
widgets::{Block, Borders, Paragraph, Tabs},
|
widgets::{Block, Borders, Paragraph, Tabs},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
|
||||||
use tachyonfx::{fx, Effect, Interpolation, Shader};
|
use tachyonfx::{fx, Effect, Interpolation, Shader};
|
||||||
use web_time::Instant;
|
use web_time::Instant;
|
||||||
|
|
||||||
use crate::app::password_locker::PasswordLocker;
|
use crate::app::{
|
||||||
|
password_locker::PasswordLocker,
|
||||||
|
weather_icon::{WeatherIcon, DAY_ICONS},
|
||||||
|
};
|
||||||
|
|
||||||
mod password_locker;
|
mod password_locker;
|
||||||
|
mod weather_icon;
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
struct SelectedTab {
|
struct SelectedTab {
|
||||||
|
@ -68,32 +73,11 @@ impl Shader for SelectedTab {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AppExecutor {
|
pub trait AppExecutor {
|
||||||
fn execute<T: 'static>(&self, future: impl Future<Output = T> + 'static, sender: Sender<T>);
|
fn execute<T: 'static>(
|
||||||
}
|
&self,
|
||||||
|
future: impl Future<Output = T> + 'static,
|
||||||
pub struct Pending<T> {
|
output_cell: Rc<OnceCell<T>>,
|
||||||
rx: Receiver<T>,
|
);
|
||||||
resolved: Option<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Pending<T> {
|
|
||||||
pub fn new(rx: Receiver<T>) -> Self {
|
|
||||||
Self { rx, resolved: None }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resolved(&mut self) -> &Option<T> {
|
|
||||||
if self.resolved.is_some() {
|
|
||||||
return &self.resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.resolved = self.rx.try_recv().ok();
|
|
||||||
&self.resolved
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
pub struct WeatherInfo {
|
|
||||||
name: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App<E: AppExecutor> {
|
pub struct App<E: AppExecutor> {
|
||||||
|
@ -102,7 +86,7 @@ pub struct App<E: AppExecutor> {
|
||||||
selected_tab: SelectedTab,
|
selected_tab: SelectedTab,
|
||||||
password_locker: PasswordLocker,
|
password_locker: PasswordLocker,
|
||||||
current_effect: Effect,
|
current_effect: Effect,
|
||||||
weather: Pending<WeatherInfo>,
|
weather: Rc<OnceCell<WeatherResponse>>,
|
||||||
executor: E,
|
executor: E,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,24 +132,31 @@ const TABS: [&'static str; 10] = [
|
||||||
|
|
||||||
impl<E: AppExecutor> App<E> {
|
impl<E: AppExecutor> App<E> {
|
||||||
pub fn new(executor: E) -> Self {
|
pub fn new(executor: E) -> Self {
|
||||||
let (weather_tx, weather_rx) = mpsc::channel();
|
let weather = Rc::new(OnceCell::new());
|
||||||
executor.execute(async move {
|
executor.execute(
|
||||||
let weather_info: WeatherInfo = reqwest::get(concat!("https://api.openweathermap.org/data/2.5/weather?lat=40.7660851712019&lon=-111.89066476757807&appid=", dotenv!("OPENWEATHERMAP_API_KEY")))
|
async move {
|
||||||
.await
|
let weather_client = WeatherClient::new(
|
||||||
.unwrap()
|
Location {
|
||||||
.json()
|
zip: String::from("84111"),
|
||||||
.await
|
name: String::from("Salt Lake City"),
|
||||||
.unwrap();
|
lat: 40.7660851712019,
|
||||||
weather_info
|
lon: -111.89066476757807,
|
||||||
},
|
country: String::from("US"),
|
||||||
weather_tx);
|
},
|
||||||
|
"metric".to_owned(),
|
||||||
|
dotenv!("OPENWEATHERMAP_API_KEY").to_owned(),
|
||||||
|
);
|
||||||
|
weather_client.get_current_weather().await.unwrap()
|
||||||
|
},
|
||||||
|
Rc::clone(&weather),
|
||||||
|
);
|
||||||
Self {
|
Self {
|
||||||
tabs: TABS.choose_multiple_array(&mut OsRng.unwrap_err()).unwrap(),
|
tabs: TABS.choose_multiple_array(&mut OsRng.unwrap_err()).unwrap(),
|
||||||
transition_instant: Instant::now(),
|
transition_instant: Instant::now(),
|
||||||
selected_tab: SelectedTab::default(),
|
selected_tab: SelectedTab::default(),
|
||||||
password_locker: PasswordLocker::default(),
|
password_locker: PasswordLocker::default(),
|
||||||
current_effect: fx::fade_from_fg(Color::Black, (0, Interpolation::CircIn)),
|
current_effect: fx::fade_from_fg(Color::Black, (0, Interpolation::CircIn)),
|
||||||
weather: Pending::new(weather_rx),
|
weather,
|
||||||
executor,
|
executor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,26 +202,85 @@ impl<E: AppExecutor> App<E> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_first_tab(&mut self, frame: &mut Frame, layout: Rect) {
|
pub fn draw_first_tab(&mut self, frame: &mut Frame, layout_outer: Rect) {
|
||||||
let layout = Layout::new(
|
let layout = Layout::new(
|
||||||
Direction::Horizontal,
|
if layout_outer.width < (layout_outer.height * 2) {
|
||||||
|
Direction::Vertical
|
||||||
|
} else {
|
||||||
|
Direction::Horizontal
|
||||||
|
},
|
||||||
[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
|
[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
|
||||||
)
|
)
|
||||||
.split(layout);
|
.split(layout_outer);
|
||||||
frame.render_widget(
|
if let Some(weather) = self.weather.get() {
|
||||||
Paragraph::new(
|
frame.render_widget(
|
||||||
self.weather
|
Block::new()
|
||||||
.resolved()
|
.title("Weather")
|
||||||
.as_ref()
|
.borders(Borders::all())
|
||||||
.map(|weather| weather.name.as_str())
|
.fg(Color::Rgb(226, 190, 89))
|
||||||
.unwrap_or("Loading"),
|
.bg(Color::Black),
|
||||||
|
layout[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
let layout = Layout::new(
|
||||||
|
Direction::Vertical,
|
||||||
|
[Constraint::Length(12), Constraint::Min(0)],
|
||||||
)
|
)
|
||||||
.alignment(ratatui::layout::Alignment::Center)
|
.split(layout[0].inner(Margin::new(2, 2)));
|
||||||
.fg(Color::Rgb(226, 190, 89))
|
|
||||||
.bg(Color::Black)
|
let layout_upper = Layout::new(
|
||||||
.block(Block::new().title("Weather").borders(Borders::all())),
|
Direction::Horizontal,
|
||||||
layout[0],
|
[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
|
||||||
);
|
)
|
||||||
|
.split(layout[0]);
|
||||||
|
|
||||||
|
frame.render_widget(
|
||||||
|
WeatherIcon(DAY_ICONS.get(&weather.weather[0].icon).unwrap()),
|
||||||
|
layout_upper[0],
|
||||||
|
);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(format!(
|
||||||
|
"{} {}°C\n~{}°C -{}°C +{}°C\n{} {} {}m/s SSE\nVisibility: {}km",
|
||||||
|
weather.name,
|
||||||
|
weather.main.temp.unwrap_or(f64::NAN),
|
||||||
|
weather.main.feels_like.unwrap_or(f64::NAN),
|
||||||
|
weather.main.temp_min.unwrap_or(f64::NAN),
|
||||||
|
weather.main.temp_max.unwrap_or(f64::NAN),
|
||||||
|
weather.weather[0].main,
|
||||||
|
match weather.wind.deg {
|
||||||
|
0..=45 => "🡓",
|
||||||
|
46..=90 => "🡗",
|
||||||
|
91..=135 => "🡐",
|
||||||
|
136..=180 => "🡔",
|
||||||
|
181..=225 => "🡑",
|
||||||
|
226..=270 => "🡕",
|
||||||
|
271..=315 => "🡒",
|
||||||
|
_ => "🡖",
|
||||||
|
},
|
||||||
|
weather.wind.speed,
|
||||||
|
weather.visibility as f64 / 1000.0,
|
||||||
|
))
|
||||||
|
.fg(Color::Rgb(226, 190, 89))
|
||||||
|
.bg(Color::Black),
|
||||||
|
layout_upper[1],
|
||||||
|
);
|
||||||
|
frame.render_widget(
|
||||||
|
Block::new()
|
||||||
|
.borders(Borders::all())
|
||||||
|
.fg(Color::Rgb(226, 190, 89))
|
||||||
|
.bg(Color::Black),
|
||||||
|
layout[1],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new("Loading")
|
||||||
|
.alignment(ratatui::layout::Alignment::Center)
|
||||||
|
.fg(Color::Rgb(226, 190, 89))
|
||||||
|
.bg(Color::Black)
|
||||||
|
.block(Block::new().title("Weather").borders(Borders::all())),
|
||||||
|
layout[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(SPLASH)
|
Paragraph::new(SPLASH)
|
||||||
.alignment(ratatui::layout::Alignment::Center)
|
.alignment(ratatui::layout::Alignment::Center)
|
||||||
|
|
293
thecockpit/src/app/weather_icon.rs
Normal file
293
thecockpit/src/app/weather_icon.rs
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
use phf::phf_map;
|
||||||
|
use ratatui::{style::Color, widgets::Widget};
|
||||||
|
|
||||||
|
pub const DAY_ICONS: phf::Map<&'static str, [&'static str; 3]> = phf_map! {
|
||||||
|
"01d" => [
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
r#"
|
||||||
|
+==========
|
||||||
|
================
|
||||||
|
+===================
|
||||||
|
+=====================
|
||||||
|
+=======================
|
||||||
|
=========================
|
||||||
|
=========================
|
||||||
|
========================+
|
||||||
|
========================
|
||||||
|
=======================
|
||||||
|
=====================
|
||||||
|
+=================
|
||||||
|
============+
|
||||||
|
"#,
|
||||||
|
],
|
||||||
|
"02d" => [
|
||||||
|
"",
|
||||||
|
r#"
|
||||||
|
......
|
||||||
|
..........
|
||||||
|
............
|
||||||
|
.................
|
||||||
|
......................
|
||||||
|
........................
|
||||||
|
.............................
|
||||||
|
..............................
|
||||||
|
..............................
|
||||||
|
.............................
|
||||||
|
.........................
|
||||||
|
"#,
|
||||||
|
r#"
|
||||||
|
==========
|
||||||
|
==============
|
||||||
|
=+=============
|
||||||
|
-=============+
|
||||||
|
:+========
|
||||||
|
+=======
|
||||||
|
+++====
|
||||||
|
+=
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"#,
|
||||||
|
],
|
||||||
|
"03d" => [
|
||||||
|
"",
|
||||||
|
r#"
|
||||||
|
.........
|
||||||
|
............
|
||||||
|
..................
|
||||||
|
....................
|
||||||
|
........................
|
||||||
|
............................
|
||||||
|
..............................
|
||||||
|
..............................
|
||||||
|
.............................
|
||||||
|
........................
|
||||||
|
"#,
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
"04d" => [
|
||||||
|
r#"
|
||||||
|
########
|
||||||
|
%#########
|
||||||
|
:#############
|
||||||
|
#############
|
||||||
|
=:..:##########
|
||||||
|
=##########
|
||||||
|
-%#########
|
||||||
|
+#####
|
||||||
|
%
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"#,
|
||||||
|
r#"
|
||||||
|
|
||||||
|
:...
|
||||||
|
.........
|
||||||
|
............
|
||||||
|
............
|
||||||
|
:....................
|
||||||
|
.......................
|
||||||
|
............................
|
||||||
|
..............................
|
||||||
|
..............................
|
||||||
|
............................
|
||||||
|
........................
|
||||||
|
"#,
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
"09d" => [
|
||||||
|
r#"
|
||||||
|
#######
|
||||||
|
-%#########
|
||||||
|
+##########
|
||||||
|
-..=########
|
||||||
|
:%#######
|
||||||
|
:-######
|
||||||
|
-###
|
||||||
|
|
||||||
|
:=
|
||||||
|
## %
|
||||||
|
## ## ##
|
||||||
|
### ###
|
||||||
|
### ##
|
||||||
|
"#,
|
||||||
|
r#"
|
||||||
|
|
||||||
|
.....
|
||||||
|
.........
|
||||||
|
..........
|
||||||
|
.................
|
||||||
|
....................
|
||||||
|
.......................
|
||||||
|
........................
|
||||||
|
......:=..............
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"#,
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
"10d" => [
|
||||||
|
r#"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## %
|
||||||
|
## ## ##
|
||||||
|
### *##
|
||||||
|
### ##
|
||||||
|
"#,
|
||||||
|
r#"
|
||||||
|
|
||||||
|
......
|
||||||
|
.........
|
||||||
|
..........:..
|
||||||
|
..................
|
||||||
|
.....................
|
||||||
|
........................
|
||||||
|
........................
|
||||||
|
......:+..............
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"#,
|
||||||
|
r#"
|
||||||
|
=========+
|
||||||
|
+===========
|
||||||
|
-+==========
|
||||||
|
:+======
|
||||||
|
+=====
|
||||||
|
:=+=+
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"#,
|
||||||
|
],
|
||||||
|
"11d" => [
|
||||||
|
r#"
|
||||||
|
######
|
||||||
|
#########
|
||||||
|
%###########
|
||||||
|
+############
|
||||||
|
*-:-##########
|
||||||
|
-##########
|
||||||
|
:%%########
|
||||||
|
+#####
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"#,
|
||||||
|
r#"
|
||||||
|
|
||||||
|
|
||||||
|
.......
|
||||||
|
..........
|
||||||
|
............
|
||||||
|
..................
|
||||||
|
......................
|
||||||
|
...........................
|
||||||
|
.............................
|
||||||
|
......... ...............
|
||||||
|
....... ................
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"#,
|
||||||
|
r#"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
+=+:
|
||||||
|
+=+
|
||||||
|
+====+
|
||||||
|
===
|
||||||
|
+=+
|
||||||
|
+=
|
||||||
|
"#,
|
||||||
|
],
|
||||||
|
"13d" => [
|
||||||
|
r#"
|
||||||
|
##
|
||||||
|
# ######## #
|
||||||
|
##### ###### ### #
|
||||||
|
###### ## ######
|
||||||
|
####################
|
||||||
|
# %## ### #
|
||||||
|
####################
|
||||||
|
##### ## #####
|
||||||
|
##### #### #####
|
||||||
|
##############
|
||||||
|
## ## ##
|
||||||
|
"#,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
"50d" => [
|
||||||
|
r#"
|
||||||
|
##########
|
||||||
|
|
||||||
|
#################
|
||||||
|
####################
|
||||||
|
%##################
|
||||||
|
#####################
|
||||||
|
|
||||||
|
#####################
|
||||||
|
##############
|
||||||
|
"#,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct WeatherIcon(pub &'static [&'static str; 3]);
|
||||||
|
|
||||||
|
impl Widget for WeatherIcon {
|
||||||
|
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
for (layer, color) in [
|
||||||
|
(0, Color::Rgb(72, 72, 72)),
|
||||||
|
(1, Color::Rgb(242, 242, 242)),
|
||||||
|
(2, Color::Rgb(236, 110, 76)),
|
||||||
|
] {
|
||||||
|
for (y, line) in self.0[layer].lines().skip(1).enumerate() {
|
||||||
|
for (x, byte) in line.bytes().enumerate() {
|
||||||
|
if byte != b' ' {
|
||||||
|
let cell = buf
|
||||||
|
.cell_mut((area.x + x as u16, area.y + y as u16))
|
||||||
|
.unwrap();
|
||||||
|
cell.fg = color;
|
||||||
|
cell.set_symbol(unsafe { std::str::from_utf8_unchecked(&[byte]) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{future::Future, time::Duration};
|
use std::{cell::OnceCell, future::Future, rc::Rc, time::Duration};
|
||||||
|
|
||||||
use futures_util::FutureExt;
|
use futures_util::FutureExt;
|
||||||
use ratatui::crossterm::event;
|
use ratatui::crossterm::event;
|
||||||
|
@ -10,9 +10,9 @@ impl AppExecutor for TokioExecutor {
|
||||||
fn execute<T: 'static>(
|
fn execute<T: 'static>(
|
||||||
&self,
|
&self,
|
||||||
future: impl Future<Output = T> + 'static,
|
future: impl Future<Output = T> + 'static,
|
||||||
sender: std::sync::mpsc::Sender<T>,
|
output_cell: Rc<OnceCell<T>>,
|
||||||
) {
|
) {
|
||||||
tokio::task::spawn_local(future.map(move |output| sender.send(output).unwrap()));
|
tokio::task::spawn_local(future.map(move |output| output_cell.set(output)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
use std::{cell::RefCell, future::Future, rc::Rc};
|
use std::{
|
||||||
|
cell::{OnceCell, RefCell},
|
||||||
|
future::Future,
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
use futures_util::FutureExt;
|
use futures_util::FutureExt;
|
||||||
use ratzilla::{
|
use ratzilla::{
|
||||||
|
@ -22,9 +26,11 @@ impl AppExecutor for WasmBindgenExecutor {
|
||||||
fn execute<T: 'static>(
|
fn execute<T: 'static>(
|
||||||
&self,
|
&self,
|
||||||
future: impl Future<Output = T> + 'static,
|
future: impl Future<Output = T> + 'static,
|
||||||
sender: std::sync::mpsc::Sender<T>,
|
output_cell: Rc<OnceCell<T>>,
|
||||||
) {
|
) {
|
||||||
wasm_bindgen_futures::spawn_local(future.map(move |output| sender.send(output).unwrap()));
|
wasm_bindgen_futures::spawn_local(future.map(move |output| {
|
||||||
|
let _ = output_cell.set(output);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue