Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions executables/shell/graphical/src/layout.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
use crate::error::{Error, Result};
use alloc::{format, string::String};
use alloc::string::String;
use core::ffi::CStr;
use core::ptr::null_mut;
use core::time::Duration;
use xila::file_system::{AccessFlags, Path};
use xila::graphics::{self, EventKind, lvgl, symbol, theme};
use xila::log;
use xila::network::InterfaceKind;
use xila::shared::unix_to_human_time;
use xila::virtual_file_system::{Directory, File};
use xila::{network, time, virtual_file_system};
use xila::{internationalization, network, time, virtual_file_system};

const KEYBOARD_SIZE_RATIO: f64 = 3.0 / 1.0;

Expand Down Expand Up @@ -217,10 +216,10 @@ impl Layout {
}

async fn update_clock(&mut self, current_time: Duration) {
let (_, _, _, hour, minute, _) = unix_to_human_time(current_time.as_secs() as i64);

graphics::lock!({
self.clock_string = format!("{hour:02}:{minute:02}\0");
self.clock_string =
internationalization::format_unix_timestamp(current_time.as_secs() as i64, "%H:%M");
self.clock_string.push('\0');

unsafe {
lvgl::lv_label_set_text_static(self.clock, self.clock_string.as_ptr() as *const i8);
Expand Down
5 changes: 3 additions & 2 deletions modules/file_system/src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use core::fmt::{self, Display, Formatter};
use core::time::Duration;
use shared::unix_to_human_time;
use shared::decompose_unix_timestamp;

/// Represents a point in time for file system operations.
///
Expand Down Expand Up @@ -100,7 +100,8 @@ impl From<Time> for Duration {

impl Display for Time {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let (year, month, day, hour, minute, second) = unix_to_human_time(self.seconds as i64);
let (year, month, day, hour, minute, second) =
decompose_unix_timestamp(self.seconds as i64);

write!(
f,
Expand Down
1 change: 1 addition & 0 deletions modules/internationalization/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2024"

[dependencies]
internationalization_macros = { path = "./macros" }
shared = { path = "../shared" }

[features]
default = []
Expand Down
2 changes: 2 additions & 0 deletions modules/internationalization/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ extern crate alloc;
extern crate std;

mod range;
mod time;

pub use range::*;
pub use time::*;

pub use internationalization_macros::translate;

Expand Down
90 changes: 90 additions & 0 deletions modules/internationalization/src/time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use alloc::format;
use alloc::string::String;

/// Formats a Unix timestamp using Python-like `strftime` tokens.
///
/// Supported tokens:
/// - `%Y` year with century (e.g. `2026`)
/// - `%m` month as zero-padded decimal (`01`..`12`)
/// - `%d` day of month as zero-padded decimal (`01`..`31`)
/// - `%H` hour (24-hour clock) as zero-padded decimal (`00`..`23`)
/// - `%I` hour (12-hour clock) as zero-padded decimal (`01`..`12`)
/// - `%M` minute as zero-padded decimal (`00`..`59`)
/// - `%S` second as zero-padded decimal (`00`..`59`)
/// - `%p` locale-independent `AM`/`PM`
/// - `%%` literal `%`
pub fn format_unix_timestamp(unix_timestamp: i64, pattern: &str) -> String {
let (year, month, day, hour, minute, second) = shared::decompose_unix_timestamp(unix_timestamp);

let mut output = String::with_capacity(pattern.len() + 16);
let mut characters = pattern.chars();

while let Some(character) = characters.next() {
if character != '%' {
output.push(character);
continue;
}

match characters.next() {
Some('Y') => output.push_str(&format!("{:04}", year)),
Some('m') => output.push_str(&format!("{:02}", month)),
Some('d') => output.push_str(&format!("{:02}", day)),
Some('H') => output.push_str(&format!("{:02}", hour)),
Some('I') => output.push_str(&format!("{:02}", hour_12(hour))),
Some('M') => output.push_str(&format!("{:02}", minute)),
Some('S') => output.push_str(&format!("{:02}", second)),
Some('p') => output.push_str(if hour < 12 { "AM" } else { "PM" }),
Some('%') => output.push('%'),
Some(other) => {
output.push('%');
output.push(other);
}
None => output.push('%'),
}
}

output
}

const fn hour_12(hour_24: u8) -> u8 {
match hour_24 % 12 {
0 => 12,
value => value,
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn format_24_hour_time() {
let timestamp = 13 * 3600 + 5 * 60;
assert_eq!(format_unix_timestamp(timestamp, "%H:%M"), "13:05");
}

#[test]
fn format_12_hour_time_with_am_pm() {
let midnight = 0;
let afternoon = 13 * 3600 + 5 * 60;

assert_eq!(format_unix_timestamp(midnight, "%I:%M %p"), "12:00 AM");
assert_eq!(format_unix_timestamp(afternoon, "%I:%M %p"), "01:05 PM");
}

#[test]
fn format_date_and_time() {
assert_eq!(
format_unix_timestamp(0, "%Y-%m-%d %H:%M:%S"),
"1970-01-01 00:00:00"
);
}

#[test]
fn format_negative_unix_time() {
assert_eq!(
format_unix_timestamp(-1, "%Y-%m-%d %H:%M:%S"),
"1969-12-31 23:59:59"
);
}
}
70 changes: 58 additions & 12 deletions modules/shared/src/time.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
pub fn unix_to_human_time(unix_timestamp: i64) -> (u16, u8, u8, u8, u8, u8) {
pub fn decompose_unix_timestamp(unix_timestamp: i64) -> (u16, u8, u8, u8, u8, u8) {
// Constants for calculations
const SECONDS_IN_MINUTE: i64 = 60;
const SECONDS_IN_HOUR: i64 = 60 * SECONDS_IN_MINUTE;
const SECONDS_IN_DAY: i64 = 24 * SECONDS_IN_HOUR;
const DAYS_IN_YEAR: i64 = 365;
const DAYS_IN_LEAP_YEAR: i64 = 366;

// Start from 1970
let mut year = 1970;
let mut days_since_epoch = unix_timestamp / SECONDS_IN_DAY;
let mut remaining_seconds = unix_timestamp % SECONDS_IN_DAY;
// Start from 1970.
let mut year: i64 = 1970;
let mut days_since_epoch = unix_timestamp.div_euclid(SECONDS_IN_DAY);
let mut remaining_seconds = unix_timestamp.rem_euclid(SECONDS_IN_DAY);

if remaining_seconds < 0 {
// Handle negative Unix timestamps
days_since_epoch -= 1;
remaining_seconds += SECONDS_IN_DAY;
}

// Determine the current year
// Determine the current year for timestamps on/after and before epoch.
while days_since_epoch
>= if is_leap_year(year) {
DAYS_IN_LEAP_YEAR
Expand All @@ -33,6 +27,15 @@ pub fn unix_to_human_time(unix_timestamp: i64) -> (u16, u8, u8, u8, u8, u8) {
year += 1;
}

while days_since_epoch < 0 {
year -= 1;
days_since_epoch += if is_leap_year(year) {
DAYS_IN_LEAP_YEAR
} else {
DAYS_IN_YEAR
};
}

// Determine the current month and day
let mut month = 0;
while days_since_epoch >= days_in_month(year, month) {
Expand Down Expand Up @@ -74,3 +77,46 @@ pub fn days_in_month(year: i64, month: usize) -> i64 {
DAYS_IN_MONTH[month]
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn unix_epoch_is_correct() {
assert_eq!(decompose_unix_timestamp(0), (1970, 1, 1, 0, 0, 0));
}

#[test]
fn one_second_before_epoch_is_correct() {
assert_eq!(decompose_unix_timestamp(-1), (1969, 12, 31, 23, 59, 59));
}

#[test]
fn one_day_before_epoch_is_correct() {
assert_eq!(decompose_unix_timestamp(-86_400), (1969, 12, 31, 0, 0, 0));
}

#[test]
fn leap_day_2024_is_correct() {
// 2024-02-29 00:00:00 UTC
assert_eq!(
decompose_unix_timestamp(1_709_164_800),
(2024, 2, 29, 0, 0, 0)
);
}

#[test]
fn leap_year_rules_are_correct() {
assert!(is_leap_year(2000));
assert!(!is_leap_year(1900));
assert!(is_leap_year(2024));
assert!(!is_leap_year(2023));
}

#[test]
fn february_days_are_correct() {
assert_eq!(days_in_month(2024, 1), 29);
assert_eq!(days_in_month(2023, 1), 28);
}
}
6 changes: 3 additions & 3 deletions modules/synchronization/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ name = "synchronization"
version = "0.1.0"
edition = "2024"

[dependencies]
embassy-sync = { workspace = true }

[target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))'.dependencies]
embassy-sync = { workspace = true, features = ["std"] }

[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
embassy-sync = { workspace = true }

[target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))'.dev-dependencies]
critical-section = { workspace = true, features = ["std"] }
Loading