#![allow(dead_code)]
mod arcs;
mod ascii;
mod clock;
mod digits;
mod professional;
pub use arcs::ArcsFace;
pub use ascii::AsciiFace;
pub use clock::ClockFace;
pub use digits::DigitsFace;
pub use professional::ProfessionalFace;
use crate::rendering::Canvas;
use crate::sensors::data::SystemData;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::f32::consts::PI;
#[derive(Debug, Clone, Copy)]
pub struct Theme {
pub primary: u32,
pub secondary: u32,
pub text: u32,
pub background: u32,
}
impl Default for Theme {
fn default() -> Self {
Self::from_preset("default")
}
}
impl Theme {
pub fn from_preset(name: &str) -> Self {
match name.to_lowercase().as_str() {
"hacker" => Self {
primary: 0x00FF00, secondary: 0x00DD00, text: 0x00FF00, background: 0x000000,
},
"ember" | "fire" => Self {
primary: 0xFF6B35, secondary: 0xFF4444, text: 0xFFEEDD, background: 0x1A0A00,
},
"solarized-light" | "solarized_light" => Self {
primary: 0x268BD2, secondary: 0x859900, text: 0x073642, background: 0xFDF6E3, },
"solarized-dark" | "solarized_dark" => Self {
primary: 0x268BD2, secondary: 0x2AA198, text: 0xEEE8D5, background: 0x002B36, },
"nord" => Self {
primary: 0x88C0D0, secondary: 0x81A1C1, text: 0xECEFF4, background: 0x2E3440, },
"tokyonight" | "tokyo-night" | "tokyo_night" => Self {
primary: 0x7AA2F7, secondary: 0xBB9AF7, text: 0xE0E0FF, background: 0x1A1B26,
},
_ => Self::from_preset("nord"),
}
}
}
fn lighten_color(color: u32, factor: f32) -> u32 {
let r = ((color >> 16) & 0xFF) as f32;
let g = ((color >> 8) & 0xFF) as f32;
let b = (color & 0xFF) as f32;
let r_light = r + (255.0 - r) * factor;
let g_light = g + (255.0 - g) * factor;
let b_light = b + (255.0 - b) * factor;
((r_light as u32) << 16) | ((g_light as u32) << 8) | (b_light as u32)
}
#[allow(clippy::too_many_arguments)]
pub fn draw_mini_analog_clock(
canvas: &mut Canvas,
cx: i32,
cy: i32,
radius: u32,
hour: u8,
minute: u8,
primary_color: u32,
hand_color: u32,
) {
let radius_f = radius as f32;
canvas.draw_arc(cx, cy, radius, 0.0, 2.0 * PI, 1.5, primary_color);
let minute_angle = (minute as f32) * PI / 30.0 - PI / 2.0;
let hour_angle = ((hour % 12) as f32 + minute as f32 / 60.0) * PI / 6.0 - PI / 2.0;
let hour_length = radius_f * 0.5;
let hour_x = cx as f32 + hour_length * hour_angle.cos();
let hour_y = cy as f32 + hour_length * hour_angle.sin();
canvas.draw_line(cx, cy, hour_x as i32, hour_y as i32, 2.5, hand_color);
let minute_color = lighten_color(hand_color, 0.4);
let minute_length = radius_f * 0.7;
let minute_x = cx as f32 + minute_length * minute_angle.cos();
let minute_y = cy as f32 + minute_length * minute_angle.sin();
canvas.draw_line(cx, cy, minute_x as i32, minute_y as i32, 1.5, minute_color);
canvas.fill_circle(cx, cy, 2, primary_color);
}
#[derive(Debug, Clone)]
pub struct ThemeInfo {
pub id: &'static str,
pub display_name: &'static str,
}
pub fn available_themes() -> Vec<ThemeInfo> {
vec![
ThemeInfo {
id: "ember",
display_name: "Ember",
},
ThemeInfo {
id: "hacker",
display_name: "Hacker",
},
ThemeInfo {
id: "nord",
display_name: "Nord",
},
ThemeInfo {
id: "solarized-dark",
display_name: "Solarized Dark",
},
ThemeInfo {
id: "solarized-light",
display_name: "Solarized Light",
},
ThemeInfo {
id: "tokyonight",
display_name: "Tokyo Night",
},
]
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ComplicationOptionType {
Choice(Vec<ComplicationChoice>),
Boolean,
Range {
min: f32,
max: f32,
step: f32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ComplicationChoice {
pub value: String,
pub label: String,
}
impl ComplicationChoice {
pub fn new(value: &str, label: &str) -> Self {
Self {
value: value.to_string(),
label: label.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ComplicationOption {
pub id: String,
pub name: String,
pub description: String,
pub option_type: ComplicationOptionType,
pub default_value: String,
}
impl ComplicationOption {
pub fn choice(
id: &str,
name: &str,
description: &str,
choices: Vec<ComplicationChoice>,
default: &str,
) -> Self {
Self {
id: id.to_string(),
name: name.to_string(),
description: description.to_string(),
option_type: ComplicationOptionType::Choice(choices),
default_value: default.to_string(),
}
}
pub fn range(
id: &str,
name: &str,
description: &str,
min: f32,
max: f32,
step: f32,
default: f32,
) -> Self {
Self {
id: id.to_string(),
name: name.to_string(),
description: description.to_string(),
option_type: ComplicationOptionType::Range { min, max, step },
default_value: default.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Complication {
pub id: String,
pub name: String,
pub description: String,
pub default_enabled: bool,
#[serde(default)]
pub options: Vec<ComplicationOption>,
}
impl Complication {
pub fn new(id: &str, name: &str, description: &str, default_enabled: bool) -> Self {
Self {
id: id.to_string(),
name: name.to_string(),
description: description.to_string(),
default_enabled,
options: Vec::new(),
}
}
pub fn with_options(
id: &str,
name: &str,
description: &str,
default_enabled: bool,
options: Vec<ComplicationOption>,
) -> Self {
Self {
id: id.to_string(),
name: name.to_string(),
description: description.to_string(),
default_enabled,
options,
}
}
}
pub mod complication_names {
pub const TIME: &str = "time";
pub const DATE: &str = "date";
pub const NETWORK: &str = "network";
pub const DISK_IO: &str = "disk_io";
pub const CPU_TEMP: &str = "cpu_temp";
pub const IP_ADDRESS: &str = "ip_address";
}
pub mod complication_options {
pub const TIME_FORMAT: &str = "format";
pub const DATE_FORMAT: &str = "format";
pub const IP_TYPE: &str = "ip_type";
pub const INTERFACE: &str = "interface";
pub const SIZE: &str = "size";
}
pub mod time_formats {
pub const DIGITAL_24H: &str = "digital-24h";
pub const DIGITAL_12H: &str = "digital-12h";
pub const ANALOGUE: &str = "analogue";
}
pub mod date_formats {
pub const ISO: &str = "iso"; pub const US: &str = "us"; pub const EU: &str = "eu"; pub const SHORT: &str = "short"; pub const LONG: &str = "long"; pub const WEEKDAY: &str = "weekday"; }
pub mod complications {
use super::*;
pub fn time(default_enabled: bool) -> Complication {
Complication::with_options(
complication_names::TIME,
"Time",
"Display the current time",
default_enabled,
vec![ComplicationOption::choice(
complication_options::TIME_FORMAT,
"Format",
"Time display format",
vec![
ComplicationChoice::new(time_formats::DIGITAL_24H, "Digital (24h)"),
ComplicationChoice::new(time_formats::DIGITAL_12H, "Digital (12h)"),
ComplicationChoice::new(time_formats::ANALOGUE, "Analogue"),
],
time_formats::DIGITAL_24H,
)],
)
}
pub fn date(default_enabled: bool, default_format: &str) -> Complication {
Complication::with_options(
complication_names::DATE,
"Date",
"Display the current date",
default_enabled,
vec![ComplicationOption::choice(
complication_options::DATE_FORMAT,
"Format",
"Date display format",
vec![
ComplicationChoice::new(date_formats::ISO, "ISO (2024-01-15)"),
ComplicationChoice::new(date_formats::US, "US (01/15/2024)"),
ComplicationChoice::new(date_formats::EU, "EU (15/01/2024)"),
ComplicationChoice::new(date_formats::SHORT, "Short (Jan 15)"),
ComplicationChoice::new(date_formats::LONG, "Long (January 15, 2024)"),
ComplicationChoice::new(date_formats::WEEKDAY, "Weekday (Mon, Jan 15)"),
],
default_format,
)],
)
}
pub fn ip_address(default_enabled: bool) -> Complication {
Complication::with_options(
complication_names::IP_ADDRESS,
"IP Address",
"Display network IP address",
default_enabled,
vec![ComplicationOption::choice(
complication_options::IP_TYPE,
"IP Type",
"Type of IP address to display",
vec![
ComplicationChoice::new("ipv6-gua", "IPv6 Global"),
ComplicationChoice::new("ipv6-lla", "IPv6 Link-Local"),
ComplicationChoice::new("ipv6-ula", "IPv6 ULA"),
ComplicationChoice::new("ipv4", "IPv4"),
],
"ipv6-gua",
)],
)
}
pub fn network(default_enabled: bool) -> Complication {
Complication::with_options(
complication_names::NETWORK,
"Network",
"Display network activity graph",
default_enabled,
vec![ComplicationOption::choice(
complication_options::INTERFACE,
"Interface",
"Network interface to monitor",
vec![ComplicationChoice::new("auto", "Auto-detect")],
"auto",
)],
)
}
pub fn disk_io(default_enabled: bool) -> Complication {
Complication::new(
complication_names::DISK_IO,
"Disk I/O",
"Display disk read/write activity graph",
default_enabled,
)
}
pub fn cpu_temp(default_enabled: bool) -> Complication {
Complication::new(
complication_names::CPU_TEMP,
"CPU Temperature",
"Display CPU temperature",
default_enabled,
)
}
pub fn hostname(default_enabled: bool) -> Complication {
Complication::new(
"hostname",
"Hostname",
"Display the system hostname",
default_enabled,
)
}
pub fn digital_time(default_enabled: bool) -> Complication {
Complication::with_options(
"digital_time",
"Digital Time",
"Display the current time in digital format",
default_enabled,
vec![ComplicationOption::range(
complication_options::SIZE,
"Size",
"Size of the digital clock display",
32.0, 96.0, 4.0, 32.0, )],
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ComplicationConfig {
pub enabled: bool,
#[serde(default)]
pub options: HashMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EnabledComplications {
#[serde(default)]
face_complications: HashMap<String, HashMap<String, ComplicationConfig>>,
}
impl EnabledComplications {
pub fn new() -> Self {
Self::default()
}
pub fn is_enabled(&self, face: &str, complication_id: &str, default: bool) -> bool {
if let Some(configs) = self.face_complications.get(face) {
configs
.get(complication_id)
.map(|c| c.enabled)
.unwrap_or(default)
} else {
default
}
}
pub fn set_enabled(&mut self, face: &str, complication_id: &str, enabled: bool) {
let face_map = self.face_complications.entry(face.to_string()).or_default();
let config = face_map.entry(complication_id.to_string()).or_default();
config.enabled = enabled;
}
pub fn init_from_defaults(&mut self, face: &dyn Face) {
let face_name = face.name();
if !self.face_complications.contains_key(face_name) {
let mut configs = HashMap::new();
for comp in face.available_complications() {
let mut config = ComplicationConfig {
enabled: comp.default_enabled,
options: HashMap::new(),
};
for opt in &comp.options {
config
.options
.insert(opt.id.clone(), opt.default_value.clone());
}
configs.insert(comp.id.clone(), config);
}
self.face_complications
.insert(face_name.to_string(), configs);
}
}
pub fn get_enabled(&self, face: &str) -> std::collections::HashSet<String> {
self.face_complications
.get(face)
.map(|configs| {
configs
.iter()
.filter(|(_, c)| c.enabled)
.map(|(id, _)| id.clone())
.collect()
})
.unwrap_or_default()
}
pub fn get_option(
&self,
face: &str,
complication_id: &str,
option_id: &str,
) -> Option<&String> {
self.face_complications
.get(face)
.and_then(|configs| configs.get(complication_id))
.and_then(|config| config.options.get(option_id))
}
pub fn set_option(
&mut self,
face: &str,
complication_id: &str,
option_id: &str,
value: String,
) {
let face_map = self.face_complications.entry(face.to_string()).or_default();
let config = face_map.entry(complication_id.to_string()).or_default();
config.options.insert(option_id.to_string(), value);
}
pub fn get_config(&self, face: &str, complication_id: &str) -> Option<&ComplicationConfig> {
self.face_complications
.get(face)
.and_then(|configs| configs.get(complication_id))
}
}
pub trait Face: Send + Sync {
fn name(&self) -> &str;
fn available_complications(&self) -> Vec<Complication>;
fn render(
&self,
canvas: &mut Canvas,
data: &SystemData,
theme: &Theme,
complications: &EnabledComplications,
);
}
pub fn create_face(name: &str) -> Option<Box<dyn Face>> {
match name.to_lowercase().as_str() {
"arcs" => Some(Box::new(ArcsFace::new())),
"ascii" => Some(Box::new(AsciiFace::new())),
"clock" => Some(Box::new(ClockFace::new())),
"digits" => Some(Box::new(DigitsFace::new())),
"professional" => Some(Box::new(ProfessionalFace::new())),
_ => None,
}
}
#[derive(Debug, Clone)]
pub struct FaceInfo {
pub id: &'static str,
pub display_name: &'static str,
}
pub fn available_faces() -> Vec<FaceInfo> {
vec![
FaceInfo {
id: "arcs",
display_name: "Arcs",
},
FaceInfo {
id: "ascii",
display_name: "ASCII",
},
FaceInfo {
id: "clock",
display_name: "Clock",
},
FaceInfo {
id: "digits",
display_name: "Digits",
},
FaceInfo {
id: "professional",
display_name: "Professional",
},
]
}
pub fn face_complications(name: &str) -> Vec<Complication> {
create_face(name)
.map(|f| f.available_complications())
.unwrap_or_default()
}