Add weather API
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Isaac Mills 2025-07-28 15:15:55 -06:00
parent 3a6b9713f9
commit 265f8d8dc3
Signed by: fnmain
GPG key ID: B67D7410F33A0F61
5 changed files with 154 additions and 44 deletions

View file

@ -4,3 +4,4 @@ Cargo.lock
bin/ bin/
pkg/ pkg/
wasm-pack.log wasm-pack.log
.env

View file

@ -17,10 +17,12 @@ path = "src/main.rs"
required-features = ["ratatui/default"] required-features = ["ratatui/default"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.47.0", features = ["rt", "macros"] }
ratatui = { version = "0.29.0", default-features = false } ratatui = { version = "0.29.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2.84" wasm-bindgen = "0.2.84"
wasm-bindgen-futures = "0.4.50"
# The `console_error_panic_hook` crate provides better debugging of panics by # The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires # logging them with `console.error`. This is great for development, but requires
@ -37,9 +39,13 @@ wasm-bindgen-test = "0.3.34"
opt-level = "s" opt-level = "s"
[dependencies] [dependencies]
dotenvy_macro = "0.15.7"
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"
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"

View file

@ -1,18 +1,24 @@
use std::{ use std::{
future::Future,
rc::Rc, rc::Rc,
sync::atomic::{AtomicUsize, Ordering}, sync::{
atomic::{AtomicUsize, Ordering},
mpsc::{self, Receiver, Sender},
},
}; };
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 ratatui::{ use ratatui::{
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, 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;
@ -61,12 +67,43 @@ impl Shader for SelectedTab {
fn filter(&mut self, _filter: tachyonfx::CellFilter) {} fn filter(&mut self, _filter: tachyonfx::CellFilter) {}
} }
pub struct App { pub trait AppExecutor {
fn execute<T: 'static>(&self, future: impl Future<Output = T> + 'static, sender: Sender<T>);
}
pub struct Pending<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> {
tabs: [&'static str; 3], tabs: [&'static str; 3],
transition_instant: Instant, transition_instant: Instant,
selected_tab: SelectedTab, selected_tab: SelectedTab,
password_locker: PasswordLocker, password_locker: PasswordLocker,
current_effect: Effect, current_effect: Effect,
weather: Pending<WeatherInfo>,
executor: E,
} }
const SPLASH: &str = r#" const SPLASH: &str = r#"
@ -109,14 +146,27 @@ const TABS: [&'static str; 10] = [
"Goobers", "Goobers",
]; ];
impl App { impl<E: AppExecutor> App<E> {
pub fn new() -> Self { pub fn new(executor: E) -> Self {
let (weather_tx, weather_rx) = mpsc::channel();
executor.execute(async move {
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")))
.await
.unwrap()
.json()
.await
.unwrap();
weather_info
},
weather_tx);
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),
executor,
} }
} }
@ -162,13 +212,32 @@ impl App {
} }
pub fn draw_first_tab(&mut self, frame: &mut Frame, layout: Rect) { pub fn draw_first_tab(&mut self, frame: &mut Frame, layout: Rect) {
let layout = Layout::new(
Direction::Horizontal,
[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
)
.split(layout);
frame.render_widget(
Paragraph::new(
self.weather
.resolved()
.as_ref()
.map(|weather| weather.name.as_str())
.unwrap_or("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)
.fg(Color::Rgb(226, 190, 89)) .fg(Color::Rgb(226, 190, 89))
.bg(Color::Black) .bg(Color::Black)
.block(Block::new().borders(Borders::all())), .block(Block::new().borders(Borders::all())),
layout, layout[1],
); );
} }

View file

@ -1,37 +1,63 @@
use std::time::Duration; use std::{future::Future, time::Duration};
use futures_util::FutureExt;
use ratatui::crossterm::event; use ratatui::crossterm::event;
use thecockpit::app::App; use thecockpit::app::{App, AppExecutor};
struct TokioExecutor;
impl AppExecutor for TokioExecutor {
fn execute<T: 'static>(
&self,
future: impl Future<Output = T> + 'static,
sender: std::sync::mpsc::Sender<T>,
) {
tokio::task::spawn_local(future.map(move |output| sender.send(output).unwrap()));
}
}
fn main() { fn main() {
let mut terminal = ratatui::init(); let rt = tokio::runtime::Builder::new_current_thread()
let mut app = App::new(); .enable_all()
.build()
.unwrap();
loop { let local = tokio::task::LocalSet::new();
terminal.draw(|frame| app.draw(frame)).unwrap();
if event::poll(Duration::from_secs(0)).unwrap() { local.spawn_local(async {
match event::read().unwrap() { let mut terminal = ratatui::init();
event::Event::Key(event::KeyEvent { let mut app = App::new(TokioExecutor);
code: event::KeyCode::Char('q'),
.. loop {
}) => break, terminal.draw(|frame| app.draw(frame)).unwrap();
event::Event::Key(event::KeyEvent {
code: event::KeyCode::Left, if event::poll(Duration::from_secs(0)).unwrap() {
.. match event::read().unwrap() {
}) => app.prev_tab(), event::Event::Key(event::KeyEvent {
event::Event::Key(event::KeyEvent { code: event::KeyCode::Char('q'),
code: event::KeyCode::Right, ..
.. }) => break,
}) => app.next_tab(), event::Event::Key(event::KeyEvent {
event::Event::Key(event::KeyEvent { code: event::KeyCode::Left,
code: event::KeyCode::Char(c), ..
.. }) => app.prev_tab(),
}) => app.add_char_to_number_guess(c), event::Event::Key(event::KeyEvent {
_ => {} code: event::KeyCode::Right,
..
}) => app.next_tab(),
event::Event::Key(event::KeyEvent {
code: event::KeyCode::Char(c),
..
}) => app.add_char_to_number_guess(c),
_ => {}
}
} }
}
}
ratatui::restore(); tokio::task::yield_now().await;
}
ratatui::restore();
});
rt.block_on(local);
} }

View file

@ -1,28 +1,36 @@
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, future::Future, rc::Rc};
use futures_util::FutureExt;
use ratzilla::{ use ratzilla::{
backend::canvas::CanvasBackendOptions, backend::canvas::CanvasBackendOptions, event::KeyCode, ratatui::Terminal, CanvasBackend,
event::KeyCode, WebRenderer,
ratatui::{
widgets::{Block, Borders},
Terminal,
},
web_sys, CanvasBackend, WebRenderer,
}; };
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
mod utils; mod utils;
use crate::app::App; use crate::app::{App, AppExecutor};
#[wasm_bindgen] #[wasm_bindgen]
extern "C" { extern "C" {
fn alert(s: &str); fn alert(s: &str);
} }
struct WasmBindgenExecutor;
impl AppExecutor for WasmBindgenExecutor {
fn execute<T: 'static>(
&self,
future: impl Future<Output = T> + 'static,
sender: std::sync::mpsc::Sender<T>,
) {
wasm_bindgen_futures::spawn_local(future.map(move |output| sender.send(output).unwrap()));
}
}
#[wasm_bindgen] #[wasm_bindgen]
pub fn run(grid_id: &str) { pub fn run(grid_id: &str) {
console_error_panic_hook::set_once(); utils::set_panic_hook();
let backend = CanvasBackend::new_with_options( let backend = CanvasBackend::new_with_options(
CanvasBackendOptions::new() CanvasBackendOptions::new()
@ -32,7 +40,7 @@ pub fn run(grid_id: &str) {
.unwrap(); .unwrap();
let terminal = Terminal::new(backend).unwrap(); let terminal = Terminal::new(backend).unwrap();
let app = Rc::new(RefCell::new(App::new())); let app = Rc::new(RefCell::new(App::new(WasmBindgenExecutor)));
terminal.on_key_event({ terminal.on_key_event({
let app = Rc::clone(&app); let app = Rc::clone(&app);