diff --git a/src/main.rs b/src/main.rs index fd912fe..b191ff4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ use std::net::SocketAddr; use crate::routes::{ - absolute_handler, fallback_handler, implicit_handler, index_handler, relative_handler, + absolute_handler, fallback_handler, favicon_handler, implicit_handler, index_handler, + relative_handler, }; use axum::{routing::get, Router}; use config::Configuration; @@ -34,6 +35,7 @@ async fn main() { let app = Router::new() .route("/", get(index_handler)) + .route("/favicon.ico", get(favicon_handler)) .route("/{path}", get(implicit_handler)) .route("/rel/{path}", get(relative_handler)) .route("/relative/{path}", get(relative_handler)) diff --git a/src/routes.rs b/src/routes.rs index 4c7d616..e4e5fe4 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -3,8 +3,10 @@ use crate::error::{get_error_response, TimeBannerError}; use crate::render::render_time_response; use crate::template::OutputForm; use crate::utils::parse_path; +use axum::extract::ConnectInfo; use axum::extract::Path; use axum::response::IntoResponse; +use std::net::SocketAddr; /// Root handler - redirects to current time in relative format. pub async fn index_handler() -> impl IntoResponse { @@ -49,6 +51,19 @@ pub async fn implicit_handler(Path(path): Path) -> impl IntoResponse { render_time_response(time, OutputForm::Absolute, extension).into_response() } +/// Handles `/favicon.ico` - generates a dynamic clock favicon showing the current time. +/// +/// Logs the client IP address and returns a PNG image of an analog clock. +pub async fn favicon_handler(ConnectInfo(addr): ConnectInfo) -> impl IntoResponse { + let now = chrono::Utc::now(); + + // Log the IP address for the favicon request + tracing::info!("Favicon request from IP: {}", addr.ip()); + + // Generate clock favicon showing current time + render_time_response(now, OutputForm::Clock, "png").into_response() +} + /// Fallback handler for unmatched routes. pub async fn fallback_handler() -> impl IntoResponse { get_error_response(TimeBannerError::NotFound).into_response() diff --git a/src/template.rs b/src/template.rs index ca554ed..99a021a 100644 --- a/src/template.rs +++ b/src/template.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Timelike, Utc}; use lazy_static::lazy_static; use tera::{Context, Tera}; use timeago::Formatter; @@ -36,9 +36,11 @@ pub enum OutputForm { Relative, /// Absolute display: "2025-01-17 14:30:00 UTC" Absolute, + /// Clock display: analog clock with hands showing the time + Clock, } -/// Timezone specification formats. +/// Timezone specification formats (currently unused but reserved for future features). pub enum TzForm { Abbreviation(String), // e.g. "CST" Iso(String), // e.g. "America/Chicago" @@ -58,19 +60,70 @@ pub struct RenderContext { pub now: Option, } +/// Calculates clock hand positions for a given time. +/// +/// Returns (hour_x, hour_y, minute_x, minute_y) coordinates for SVG rendering. +/// Clock center is at (16, 16) with appropriate hand lengths for a 32x32 favicon. +fn calculate_clock_hands(time: DateTime) -> (f64, f64, f64, f64) { + let hour = time.hour() as f64; + let minute = time.minute() as f64; + + // Calculate angles (12 o'clock = 0°, clockwise) + let hour_angle = ((hour % 12.0) + minute / 60.0) * 30.0; // 30° per hour + let minute_angle = minute * 6.0; // 6° per minute + + // Convert to radians and adjust for SVG coordinate system (0° at top) + let hour_rad = (hour_angle - 90.0).to_radians(); + let minute_rad = (minute_angle - 90.0).to_radians(); + + // Clock center and hand lengths + let center_x = 16.0; + let center_y = 16.0; + let hour_length = 7.0; // Shorter hour hand + let minute_length = 11.0; // Longer minute hand + + // Calculate end positions + let hour_x = center_x + hour_length * hour_rad.cos(); + let hour_y = center_y + hour_length * hour_rad.sin(); + let minute_x = center_x + minute_length * minute_rad.cos(); + let minute_y = center_y + minute_length * minute_rad.sin(); + + (hour_x, hour_y, minute_x, minute_y) +} + /// Renders a time value using the appropriate template. +/// +/// Uses different templates based on output form: +/// - Relative/Absolute: "basic.svg" with text content +/// - Clock: "clock.svg" with calculated hand positions pub fn render_template(context: RenderContext) -> Result { let mut template_context = Context::new(); - let formatter = Formatter::new(); - template_context.insert( - "text", - match context.output_form { - OutputForm::Relative => formatter.convert_chrono(context.value, Utc::now()), - OutputForm::Absolute => context.value.to_rfc3339(), + match context.output_form { + OutputForm::Relative => { + let formatter = Formatter::new(); + template_context.insert("text", &formatter.convert_chrono(context.value, Utc::now())); + TEMPLATES.render("basic.svg", &template_context) } - .as_str(), - ); + OutputForm::Absolute => { + template_context.insert("text", &context.value.to_rfc3339()); + TEMPLATES.render("basic.svg", &template_context) + } + OutputForm::Clock => { + let (hour_x, hour_y, minute_x, minute_y) = calculate_clock_hands(context.value); - TEMPLATES.render("basic.svg", &template_context) + // Format to 2 decimal places to avoid precision issues + let hour_x_str = format!("{:.2}", hour_x); + let hour_y_str = format!("{:.2}", hour_y); + let minute_x_str = format!("{:.2}", minute_x); + let minute_y_str = format!("{:.2}", minute_y); + + template_context.insert("hour_x", &hour_x_str); + template_context.insert("hour_y", &hour_y_str); + template_context.insert("minute_x", &minute_x_str); + template_context.insert("minute_y", &minute_y_str); + + TEMPLATES.render("clock.svg", &template_context) + } + } } diff --git a/src/templates/clock.svg b/src/templates/clock.svg new file mode 100644 index 0000000..b57898b --- /dev/null +++ b/src/templates/clock.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file