diff --git a/Cargo.lock b/Cargo.lock index abd96e1..4df0624 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -761,6 +761,28 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "chrono-tz" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d7b79e99bfaa0d47da0687c43aa3b7381938a62ad3a6498599039321f660b7" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "clang-sys" version = "1.7.0" @@ -2488,6 +2510,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -2500,6 +2531,44 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -3044,6 +3113,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -4200,6 +4275,7 @@ dependencies = [ "ash", "ash-window", "chrono", + "chrono-tz", "cstr", "ctrlc", "env_logger 0.10.2", @@ -4221,6 +4297,7 @@ dependencies = [ "regex", "rodio", "serde", + "serde_json", "serde_yaml", "smallvec", "strum", diff --git a/Cargo.toml b/Cargo.toml index d594b38..c69486c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ anyhow = "1.0.79" ash = "^0.37.2" ash-window = "0.12.0" chrono = "0.4.29" +chrono-tz = "0.8.5" cstr = "0.2.11" ctrlc = { version = "3.4.2", features = ["termination"] } env_logger = "0.10.0" @@ -34,7 +35,8 @@ png = "0.17.10" raw-window-handle = "0.5.2" regex = "1.9.5" rodio = { version = "0.17.1", default-features = false, features = ["wav", "hound"] } -serde = { version = "1.0.188", features = ["derive"] } +serde = { version = "1.0.188", features = ["derive", "rc"] } +serde_json = "1.0.113" serde_yaml = "0.9.25" smallvec = "1.11.0" strum = { version = "0.25.0", features = ["derive"] } diff --git a/src/config.rs b/src/config.rs index 42e065f..732b2ed 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,7 @@ use crate::config_io; use crate::config_io::get_conf_d_path; use crate::load_with_fallback; use crate::overlays::keyboard; +use crate::overlays::watch::WatchConfig; use log::error; use serde::Deserialize; use serde::Serialize; @@ -71,6 +72,11 @@ pub fn load_keyboard() -> keyboard::Layout { serde_yaml::from_str(&yaml_data).expect("Failed to parse keyboard.yaml") } +pub fn load_watch() -> WatchConfig { + let yaml_data = load_with_fallback!("watch.yaml", "res/watch.yaml"); + serde_yaml::from_str(&yaml_data).expect("Failed to parse watch.yaml") +} + pub fn load_general() -> GeneralConfig { let mut yaml_data = String::new(); diff --git a/src/gui/mod.rs b/src/gui/mod.rs index c79df26..4b821bc 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use anyhow::bail; use glam::{Vec2, Vec3, Vec4}; use vulkano::{ command_buffer::CommandBufferUsage, @@ -28,12 +29,20 @@ struct Rect { } // Parses a color from a HTML hex string -pub fn color_parse(html_hex: &str) -> Vec3 { - let mut color = Vec3::ZERO; - color.x = u8::from_str_radix(&html_hex[1..3], 16).unwrap() as f32 / 255.; - color.y = u8::from_str_radix(&html_hex[3..5], 16).unwrap() as f32 / 255.; - color.z = u8::from_str_radix(&html_hex[5..7], 16).unwrap() as f32 / 255.; - color +pub fn color_parse(html_hex: &str) -> anyhow::Result { + if html_hex.len() == 7 { + if let (Ok(r), Ok(g), Ok(b)) = ( + u8::from_str_radix(&html_hex[1..3], 16), + u8::from_str_radix(&html_hex[3..5], 16), + u8::from_str_radix(&html_hex[5..7], 16), + ) { + return Ok(Vec3::new(r as f32 / 255., g as f32 / 255., b as f32 / 255.)); + } + } + bail!( + "Invalid color string: '{}', must be 7 characters long (e.g. #FF00FF)", + &html_hex + ) } pub struct CanvasBuilder { @@ -625,7 +634,7 @@ impl Control { let set0 = canvas.pipeline_fg_glyph.uniform_sampler( 0, ImageView::new_default(tex).unwrap(), - Filter::Nearest, + Filter::Linear, ); let set1 = canvas.pipeline_fg_glyph.uniform_buffer( 1, @@ -670,7 +679,7 @@ impl Control { let set0 = canvas.pipeline_fg_glyph.uniform_sampler( 0, ImageView::new_default(tex).unwrap(), - Filter::Nearest, + Filter::Linear, ); let set1 = canvas.pipeline_fg_glyph.uniform_buffer( 1, diff --git a/src/overlays/keyboard.rs b/src/overlays/keyboard.rs index 4671b1d..ccda35f 100644 --- a/src/overlays/keyboard.rs +++ b/src/overlays/keyboard.rs @@ -52,11 +52,11 @@ where data, ); - canvas.bg_color = color_parse("#101010"); + canvas.bg_color = color_parse("#101010").unwrap(); canvas.panel(0., 0., size.x, size.y); canvas.font_size = 18; - canvas.bg_color = color_parse("#202020"); + canvas.bg_color = color_parse("#202020").unwrap(); let unit_size = size.x / LAYOUT.row_size; let h = unit_size - 2. * BUTTON_PADDING; diff --git a/src/overlays/screen.rs b/src/overlays/screen.rs index ddcd7c6..f79f2c3 100644 --- a/src/overlays/screen.rs +++ b/src/overlays/screen.rs @@ -201,7 +201,7 @@ impl ScreenPipeline { let set0 = self .pipeline - .uniform_sampler(0, mouse_view.clone(), Filter::Nearest); + .uniform_sampler(0, mouse_view.clone(), Filter::Linear); let pass = self.pipeline.create_pass( self.extentf, diff --git a/src/overlays/watch.rs b/src/overlays/watch.rs index 8d2a72b..a40acd2 100644 --- a/src/overlays/watch.rs +++ b/src/overlays/watch.rs @@ -1,7 +1,14 @@ -use std::{sync::Arc, time::Instant}; +use std::{ + io::Read, + process::{self, Stdio}, + sync::Arc, + time::Instant, +}; use chrono::Local; -use glam::{vec2, Affine2}; +use chrono_tz::Tz; +use glam::{vec2, Affine2, Vec3}; +use serde::Deserialize; use crate::{ backend::{ @@ -9,116 +16,209 @@ use crate::{ input::PointerMode, overlay::{OverlayData, OverlayState, RelativeTo}, }, + config::load_watch, gui::{color_parse, CanvasBuilder, Control}, state::AppState, }; use super::{keyboard::KEYBOARD_NAME, toast::Toast}; +const FALLBACK_COLOR: Vec3 = Vec3 { + x: 1., + y: 0., + z: 1., +}; + pub const WATCH_NAME: &str = "watch"; pub fn create_watch(state: &AppState, screens: &[OverlayData]) -> OverlayData where O: Default, { - let mut canvas = CanvasBuilder::new(400, 200, state.graphics.clone(), state.format, ()); + let config = load_watch(); + + let mut canvas = CanvasBuilder::new( + config.watch_size[0] as _, + config.watch_size[1] as _, + state.graphics.clone(), + state.format, + (), + ); let empty_str: Arc = Arc::from(""); - // Background - canvas.bg_color = color_parse("#353535"); - canvas.panel(0., 0., 400., 200.); + for elem in config.watch_elements.into_iter() { + match elem { + WatchElement::Panel { + rect: [x, y, w, h], + bg_color, + } => { + canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR); + canvas.panel(x, y, w, h); + } + WatchElement::Label { + rect: [x, y, w, h], + font_size, + fg_color, + text, + } => { + canvas.font_size = font_size; + canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR); + canvas.label(x, y, w, h, text); + } + WatchElement::Clock { + rect: [x, y, w, h], + font_size, + fg_color, + format, + timezone, + } => { + canvas.font_size = font_size; + canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR); - // Time display - canvas.font_size = 46; - let clock = canvas.label(19., 100., 200., 50., empty_str.clone()); - clock.on_update = Some(|control, _data, _app| { - let date = Local::now(); - control.set_text(&format!("{}", &date.format("%H:%M"))); - }); + let tz: Option = match timezone { + Some(tz) => Some(tz.parse().unwrap_or_else(|_| { + log::error!("Failed to parse timezone '{}'", &tz); + canvas.fg_color = FALLBACK_COLOR; + Tz::UTC + })), + None => None, + }; - canvas.font_size = 14; - let date = canvas.label(20., 125., 200., 50., empty_str.clone()); - date.on_update = Some(|control, _data, _app| { - let date = Local::now(); - control.set_text(&format!("{}", &date.format("%x"))); - }); + let label = canvas.label(x, y, w, h, empty_str.clone()); + label.state = Some(ElemState { + clock: Some(ClockState { + timezone: tz, + format, + }), + ..Default::default() + }); + label.on_update = Some(clock_update); + } + WatchElement::ExecLabel { + rect: [x, y, w, h], + font_size, + fg_color, + exec, + interval, + } => { + canvas.font_size = font_size; + canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR); + let label = canvas.label(x, y, w, h, empty_str.clone()); + label.state = Some(ElemState { + exec: Some(ExecState { + last_exec: Instant::now(), + interval, + exec, + child: None, + }), + button: None, + ..Default::default() + }); + label.on_update = Some(exec_label_update); + } + WatchElement::ExecButton { + rect: [x, y, w, h], + font_size, + bg_color, + fg_color, + exec, + text, + } => { + canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR); + canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR); + canvas.font_size = font_size; + let button = canvas.button(x, y, w, h, text.clone()); + button.state = Some(ElemState { + exec: Some(ExecState { + last_exec: Instant::now(), + interval: 0., + exec, + child: None, + }), + button: Some(WatchButtonState { + pressed_at: Instant::now(), + mode: PointerMode::Left, + overlay: None, + }), + ..Default::default() + }); + button.on_press = Some(exec_button); + } + WatchElement::OverlayList { + rect, + font_size, + kbd_fg_color, + kbd_bg_color, + scr_fg_color, + scr_bg_color, + layout, + } => { + let num_buttons = screens.len() + 1; + let mut button_x = rect[0]; + let mut button_y = rect[1]; + let (button_w, button_h) = match layout { + ListLayout::Horizontal => (rect[2] / (num_buttons as f32), rect[3]), + ListLayout::Vertical => (rect[2], rect[3] / (num_buttons as f32)), + }; - let day_of_week = canvas.label(20., 150., 200., 50., empty_str); - day_of_week.on_update = Some(|control, _data, _app| { - let date = Local::now(); - control.set_text(&format!("{}", &date.format("%A"))); - }); + canvas.bg_color = color_parse(&kbd_bg_color).unwrap_or(FALLBACK_COLOR); + canvas.fg_color = color_parse(&kbd_fg_color).unwrap_or(FALLBACK_COLOR); + canvas.font_size = font_size; - // Volume controls - canvas.bg_color = color_parse("#222222"); - canvas.fg_color = color_parse("#AAAAAA"); - canvas.font_size = 14; + let keyboard = canvas.button( + button_x + 2., + button_y + 2., + button_w - 4., + button_h - 4., + KEYBOARD_NAME.into(), + ); + keyboard.state = Some(ElemState { + button: Some(WatchButtonState { + pressed_at: Instant::now(), + overlay: Some(OverlaySelector::Name(KEYBOARD_NAME.into())), + mode: PointerMode::Left, + }), + ..Default::default() + }); + keyboard.on_press = Some(overlay_button_dn); + keyboard.on_release = Some(overlay_button_up); - canvas.bg_color = color_parse("#303030"); - canvas.fg_color = color_parse("#353535"); + canvas.bg_color = color_parse(&scr_bg_color).unwrap_or(FALLBACK_COLOR); + canvas.fg_color = color_parse(&scr_fg_color).unwrap_or(FALLBACK_COLOR); - let vol_up = canvas.button(327., 116., 46., 32., "+".into()); - vol_up.on_press = Some(|_control, _data, _app, _| { - println!("Volume up!"); //TODO - }); + for screen in screens.iter() { + button_x += match layout { + ListLayout::Horizontal => button_w, + ListLayout::Vertical => 0., + }; + button_y += match layout { + ListLayout::Horizontal => 0., + ListLayout::Vertical => button_h, + }; - let vol_dn = canvas.button(327., 52., 46., 32., "-".into()); - vol_dn.on_press = Some(|_control, _data, _app, _| { - println!("Volume down!"); //TODO - }); + let button = canvas.button( + button_x + 2., + button_y + 2., + button_w - 4., + button_h - 4., + screen.state.name.clone(), + ); + button.state = Some(ElemState { + button: Some(WatchButtonState { + pressed_at: Instant::now(), + overlay: Some(OverlaySelector::Id(screen.state.id)), + mode: PointerMode::Left, + }), + ..Default::default() + }); - canvas.bg_color = color_parse("#303030"); - canvas.fg_color = color_parse("#353535"); - - let settings = canvas.button(2., 162., 36., 36., "☰".into()); - settings.on_press = Some(|_control, _data, _app, _| { - println!("Settings!"); //TODO - }); - - canvas.fg_color = color_parse("#CCBBAA"); - canvas.bg_color = color_parse("#406050"); - // Bottom row - let num_buttons = screens.len() + 1; - let button_width = 360. / num_buttons as f32; - let mut button_x = 40.; - - let keyboard = canvas.button( - button_x + 2., - 162., - button_width - 4., - 36., - KEYBOARD_NAME.into(), - ); - keyboard.state = Some(WatchButtonState { - pressed_at: Instant::now(), - overlay: OverlaySelector::Name(KEYBOARD_NAME.into()), - mode: PointerMode::Left, - }); - - keyboard.on_press = Some(overlay_button_dn); - keyboard.on_release = Some(overlay_button_up); - button_x += button_width; - - canvas.bg_color = color_parse("#405060"); - - for screen in screens.iter() { - let button = canvas.button( - button_x + 2., - 162., - button_width - 4., - 36., - screen.state.name.clone(), - ); - button.state = Some(WatchButtonState { - pressed_at: Instant::now(), - overlay: OverlaySelector::Id(screen.state.id), - mode: PointerMode::Left, - }); - - button.on_press = Some(overlay_button_dn); - button.on_release = Some(overlay_button_up); - button_x += button_width; + button.on_press = Some(overlay_button_dn); + button.on_release = Some(overlay_button_up); + } + } + } } + let interaction_transform = Affine2::from_translation(vec2(0.5, 0.5)) * Affine2::from_scale(vec2(1., -2.0)); @@ -142,84 +242,279 @@ where } } +#[derive(Default)] +struct ElemState { + clock: Option, + exec: Option, + button: Option, +} + +struct ClockState { + timezone: Option, + format: Arc, +} + struct WatchButtonState { pressed_at: Instant, mode: PointerMode, - overlay: OverlaySelector, + overlay: Option, +} + +struct ExecState { + last_exec: Instant, + interval: f32, + exec: Vec>, + child: Option, +} + +fn exec_button( + control: &mut Control<(), ElemState>, + _: &mut (), + _: &mut AppState, + _mode: PointerMode, +) { + let state = control.state.as_mut().unwrap(); + let exec = state.exec.as_mut().unwrap(); + if let Some(child) = &mut exec.child { + match child.try_wait() { + Ok(Some(code)) => { + if !code.success() { + log::error!("Child process exited with code: {}", code); + } + exec.child = None; + } + Ok(None) => { + log::warn!("Unable to launch child process: previous child not exited yet"); + return; + } + Err(e) => { + exec.child = None; + log::error!("Error checking child process: {:?}", e); + } + } + } + let args = exec.exec.iter().map(|s| s.as_ref()).collect::>(); + match process::Command::new(args[0]).args(&args[1..]).spawn() { + Ok(child) => { + exec.child = Some(child); + } + Err(e) => { + log::error!("Failed to spawn process {:?}: {:?}", args, e); + } + }; +} + +fn exec_label_update(control: &mut Control<(), ElemState>, _: &mut (), _: &mut AppState) { + let state = control.state.as_mut().unwrap(); + let exec = state.exec.as_mut().unwrap(); + + if let Some(mut child) = exec.child.take() { + match child.try_wait() { + Ok(Some(code)) => { + if !code.success() { + log::error!("Child process exited with code: {}", code); + } else { + if let Some(mut stdout) = child.stdout.take() { + let mut buf = String::new(); + if let Ok(_) = stdout.read_to_string(&mut buf) { + control.set_text(&buf); + } else { + log::error!("Failed to read stdout for child process"); + return; + } + return; + } else { + log::error!("No stdout for child process"); + return; + } + } + } + Ok(None) => { + exec.child = Some(child); + // not exited yet + return; + } + Err(e) => { + exec.child = None; + log::error!("Error checking child process: {:?}", e); + return; + } + } + } + + if Instant::now() + .saturating_duration_since(exec.last_exec) + .as_secs_f32() + > exec.interval + { + exec.last_exec = Instant::now(); + let args = exec.exec.iter().map(|s| s.as_ref()).collect::>(); + + match process::Command::new(args[0]) + .args(&args[1..]) + .stdout(Stdio::piped()) + .spawn() + { + Ok(child) => { + exec.child = Some(child); + } + Err(e) => { + log::error!("Failed to spawn process {:?}: {:?}", args, e); + } + }; + } +} + +fn clock_update(control: &mut Control<(), ElemState>, _: &mut (), _: &mut AppState) { + let state = control.state.as_mut().unwrap(); + let clock = state.clock.as_mut().unwrap(); + + let fmt = clock.format.clone(); + + if let Some(tz) = clock.timezone { + let date = Local::now().with_timezone(&tz); + control.set_text(&format!("{}", &date.format(&fmt))); + } else { + let date = Local::now(); + control.set_text(&format!("{}", &date.format(&fmt))); + } } fn overlay_button_dn( - control: &mut Control<(), WatchButtonState>, + control: &mut Control<(), ElemState>, _: &mut (), _: &mut AppState, mode: PointerMode, ) { - if let Some(state) = control.state.as_mut() { - state.pressed_at = Instant::now(); - state.mode = mode; + let btn = control.state.as_mut().unwrap().button.as_mut().unwrap(); + btn.pressed_at = Instant::now(); + btn.mode = mode; +} + +fn overlay_button_up(control: &mut Control<(), ElemState>, _: &mut (), app: &mut AppState) { + let btn = control.state.as_mut().unwrap().button.as_mut().unwrap(); + let selector = btn.overlay.as_ref().unwrap().clone(); + if Instant::now() + .saturating_duration_since(btn.pressed_at) + .as_millis() + < 2000 + { + match btn.mode { + PointerMode::Left => { + app.tasks.enqueue(TaskType::Overlay( + selector, + Box::new(|app, o| { + o.want_visible = !o.want_visible; + if o.recenter { + o.show_hide = o.want_visible; + o.reset(app, false); + } + }), + )); + } + PointerMode::Right => { + app.tasks.enqueue(TaskType::Overlay( + selector, + Box::new(|app, o| { + o.recenter = !o.recenter; + o.grabbable = o.recenter; + o.show_hide = o.recenter; + if !o.recenter { + app.tasks.enqueue(TaskType::Toast(Toast::new( + format!("{} is now locked in place!", o.name).into(), + "Right-click again to toggle.".into(), + ))) + } + }), + )); + } + PointerMode::Middle => { + app.tasks.enqueue(TaskType::Overlay( + selector, + Box::new(|app, o| { + o.interactable = !o.interactable; + if !o.interactable { + app.tasks.enqueue(TaskType::Toast(Toast::new( + format!("{} is now non-interactable!", o.name).into(), + "Middle-click again to toggle.".into(), + ))) + } + }), + )); + } + _ => {} + } + } else { + app.tasks.enqueue(TaskType::Overlay( + selector, + Box::new(|app, o| { + o.reset(app, true); + }), + )); } } -fn overlay_button_up(control: &mut Control<(), WatchButtonState>, _: &mut (), app: &mut AppState) { - if let Some(state) = control.state.as_ref() { - let selector = state.overlay.clone(); - if Instant::now() - .saturating_duration_since(state.pressed_at) - .as_millis() - < 2000 - { - match state.mode { - PointerMode::Left => { - app.tasks.enqueue(TaskType::Overlay( - selector, - Box::new(|app, o| { - o.want_visible = !o.want_visible; - if o.recenter { - o.show_hide = o.want_visible; - o.reset(app, false); - } - }), - )); - } - PointerMode::Right => { - app.tasks.enqueue(TaskType::Overlay( - selector, - Box::new(|app, o| { - o.recenter = !o.recenter; - o.grabbable = o.recenter; - o.show_hide = o.recenter; - if !o.recenter { - app.tasks.enqueue(TaskType::Toast(Toast::new( - format!("{} is now locked in place!", o.name).into(), - "Right-click again to toggle.".into(), - ))) - } - }), - )); - } - PointerMode::Middle => { - app.tasks.enqueue(TaskType::Overlay( - selector, - Box::new(|app, o| { - o.interactable = !o.interactable; - if !o.interactable { - app.tasks.enqueue(TaskType::Toast(Toast::new( - format!("{} is now non-interactable!", o.name).into(), - "Middle-click again to toggle.".into(), - ))) - } - }), - )); - } - _ => {} - } - } else { - app.tasks.enqueue(TaskType::Overlay( - selector, - Box::new(|app, o| { - o.reset(app, true); - }), - )); - } - } +#[derive(Deserialize)] +pub struct WatchConfig { + watch_hand: LeftRight, + watch_size: [u32; 2], + watch_elements: Vec, +} + +#[derive(Deserialize)] +#[serde(tag = "type")] +enum WatchElement { + Panel { + rect: [f32; 4], + bg_color: Arc, + }, + Label { + rect: [f32; 4], + font_size: isize, + fg_color: Arc, + text: Arc, + }, + Clock { + rect: [f32; 4], + font_size: isize, + fg_color: Arc, + format: Arc, + timezone: Option>, + }, + ExecLabel { + rect: [f32; 4], + font_size: isize, + fg_color: Arc, + exec: Vec>, + interval: f32, + }, + ExecButton { + rect: [f32; 4], + font_size: isize, + bg_color: Arc, + fg_color: Arc, + exec: Vec>, + text: Arc, + }, + OverlayList { + rect: [f32; 4], + font_size: isize, + kbd_fg_color: Arc, + kbd_bg_color: Arc, + scr_fg_color: Arc, + scr_bg_color: Arc, + layout: ListLayout, + }, +} + +#[derive(Deserialize)] +enum ListLayout { + Horizontal, + Vertical, +} + +#[derive(Deserialize)] +enum LeftRight { + Left, + Right, } diff --git a/src/res/watch.yaml b/src/res/watch.yaml new file mode 100644 index 0000000..a43b2cb --- /dev/null +++ b/src/res/watch.yaml @@ -0,0 +1,115 @@ +# Tips for optimization: +# - try to re-use font sizes, every loaded font size uses additional VRAM. + +# common properties: +# - rect: bounding rectangle [x, y, width, height] +# (x, y) = (0, 0) is top-left +# - bg_color: background color of panels and buttons +# - fg_color: color of text +# - font_size: +# +# element types: +# panel - simple colored rectangle +# rect: bounding rectangle +# bg_color: color of rectangle. quotation marks mandatory. +# +# clock - date/time display with custom formatting +# rect: bounding rectangle +# fg_color: color of text. quotation marks mandatory. +# format: chrono format to print, see https://docs.rs/chrono/latest/chrono/format/strftime/index.html +# timezone: timezone to use, leave empty for local + +# render resolution of the watch + +watch_hand: Left + +watch_offset: [] + +watch_rotation: [] + +watch_size: [400, 200] + +watch_elements: + # background panel + - type: Panel + rect: [0, 0, 400, 200] + bg_color: "#353535" + + # bottom row, of keyboard + overlays + - type: OverlayList + rect: [0, 160, 400, 40] + font_size: 14 + kbd_fg_color: "#FFFFFF" + kbd_bg_color: "#406050" + scr_fg_color: "#FFFFFF" + scr_bg_color: "#405060" + layout: Horizontal + + # main clock with date and day-of-week + - type: Clock + rect: [19, 90, 200, 50] + #format: "%h:%M %p" # 11:59 PM + format: "%H:%M" # 23:59 + font_size: 46 + fg_color: "#ffffff" + - type: Clock + rect: [20, 117, 200, 20] + format: "%x" # local date representation + font_size: 14 + fg_color: "#ffffff" + - type: Clock + rect: [20, 137, 200, 50] + #format: "%a" # Tue + format: "%A" # Tuesday + font_size: 14 + fg_color: "#ffffff" + + # alt clock 1 + - type: Clock + rect: [210, 90, 200, 50] + timezone: "Asia/Tokyo" # change TZ1 here + format: "%H:%M" + font_size: 24 + fg_color: "#99BBAA" + - type: Label + rect: [210, 60, 200, 50] + font_size: 14 + fg_color: "#99BBAA" + text: "Tokyo" # change TZ1 label here + + # alt clock 2 + - type: Clock + rect: [210, 150, 200, 50] + timezone: "America/Chicago" # change TZ2 here + format: "%H:%M" + font_size: 24 + fg_color: "#AA99BB" + - type: Label + rect: [210, 120, 200, 50] + font_size: 14 + fg_color: "#AA99BB" + text: "Chicago" # change TZ2 label here + + # sample + - type: ExecLabel + rect: [50, 20, 200, 50] + font_size: 14 + fg_color: "#FFFFFF" + exec: ["echo", "customize me! see watch.yaml"] + interval: 0 # seconds + + # volume buttons + - type: ExecButton + rect: [327, 52, 46, 32] + font_size: 14 + fg_color: "#FFFFFF" + bg_color: "#505050" + text: "+" + exec: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%" ] + - type: ExecButton + rect: [327, 116, 46, 32] + font_size: 14 + fg_color: "#FFFFFF" + bg_color: "#505050" + text: "-" + exec: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%" ]