mirror of
https://github.com/SeCherkasov/util-linux-cal.git
synced 2026-03-30 16:01:50 +03:00
Initial commit: Rust calendar utility
This commit is contained in:
214
plugins/holiday_highlighter/src/lib.rs
Normal file
214
plugins/holiday_highlighter/src/lib.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
//! Holiday Highlighter Plugin for cal.
|
||||
//!
|
||||
//! Fetches holiday data from isdayoff.ru API for multiple countries.
|
||||
|
||||
use libc::{c_char, c_int};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
pub const PLUGIN_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
pub const PLUGIN_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
const API_URL_YEAR: &str = "https://isdayoff.ru/api/getdata";
|
||||
const API_URL_MONTH: &str = "https://isdayoff.ru/api/getdata";
|
||||
|
||||
/// Supported countries with their locale mappings.
|
||||
const SUPPORTED_COUNTRIES: &[(&str, &[&str])] = &[
|
||||
("RU", &["ru_RU", "ru_BY", "ru_KZ", "ru_UZ", "ru_LV"]),
|
||||
("BY", &["be_BY", "ru_BY"]),
|
||||
("KZ", &["kk_KZ", "ru_KZ"]),
|
||||
("US", &["en_US", "en"]),
|
||||
("UZ", &["uz_UZ", "ru_UZ"]),
|
||||
("TR", &["tr_TR"]),
|
||||
("LV", &["lv_LV", "ru_LV"]),
|
||||
];
|
||||
|
||||
type CacheKey = (i32, u32, String);
|
||||
static CACHE: LazyLock<Mutex<HashMap<CacheKey, String>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
/// Initialize the plugin (optional, cache is lazily initialized).
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn plugin_init() {}
|
||||
|
||||
/// Get plugin name (do not free returned pointer).
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn plugin_get_name() -> *const c_char {
|
||||
static NAME: LazyLock<CString> = LazyLock::new(|| CString::new(PLUGIN_NAME).unwrap());
|
||||
NAME.as_ptr()
|
||||
}
|
||||
|
||||
/// Get plugin version (do not free returned pointer).
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn plugin_get_version() -> *const c_char {
|
||||
static VERSION: LazyLock<CString> = LazyLock::new(|| CString::new(PLUGIN_VERSION).unwrap());
|
||||
VERSION.as_ptr()
|
||||
}
|
||||
|
||||
/// Get holiday data for a specific month.
|
||||
///
|
||||
/// Returns string where each character represents a day:
|
||||
/// - '0' = working day, '1' = weekend, '2' = shortened, '8' = public holiday
|
||||
///
|
||||
/// # Safety
|
||||
/// `country_code` must be a valid null-terminated C string.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn plugin_get_holidays(
|
||||
year: c_int,
|
||||
month: c_int,
|
||||
country_code: *const c_char,
|
||||
) -> *mut c_char {
|
||||
let country = unsafe { parse_country(country_code) };
|
||||
let key = (year, month as u32, country.clone());
|
||||
|
||||
if let Some(data) = CACHE.lock().unwrap().get(&key) {
|
||||
return CString::new(data.as_str()).unwrap().into_raw();
|
||||
}
|
||||
|
||||
let data = fetch_holidays(year, month as u32, &country).unwrap_or_default();
|
||||
CACHE.lock().unwrap().insert(key, data.clone());
|
||||
|
||||
CString::new(data).unwrap().into_raw()
|
||||
}
|
||||
|
||||
unsafe fn parse_country(country_code: *const c_char) -> String {
|
||||
unsafe {
|
||||
CStr::from_ptr(country_code)
|
||||
.to_str()
|
||||
.unwrap_or("RU")
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Free memory allocated by plugin_get_holidays.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be returned by `plugin_get_holidays`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn plugin_free_holidays(ptr: *mut c_char) {
|
||||
if !ptr.is_null() {
|
||||
let _ = unsafe { CString::from_raw(ptr) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Get country code from system locale.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn plugin_get_country_from_locale() -> *mut c_char {
|
||||
let country = get_country_from_locale();
|
||||
CString::new(country).unwrap().into_raw()
|
||||
}
|
||||
|
||||
/// Free memory allocated by plugin_get_country_from_locale.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must be returned by `plugin_get_country_from_locale`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn plugin_free_country(ptr: *mut c_char) {
|
||||
if !ptr.is_null() {
|
||||
let _ = unsafe { CString::from_raw(ptr) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a specific day is a holiday.
|
||||
///
|
||||
/// Returns: 0=working, 1=weekend, 2=shortened, 8=public, -1=error
|
||||
///
|
||||
/// # Safety
|
||||
/// `country_code` must be a valid null-terminated C string.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn plugin_is_holiday(
|
||||
year: c_int,
|
||||
month: c_int,
|
||||
day: c_int,
|
||||
country_code: *const c_char,
|
||||
) -> c_int {
|
||||
let country = unsafe { parse_country(country_code) };
|
||||
let holidays = fetch_holidays(year, month as u32, &country);
|
||||
|
||||
match holidays {
|
||||
Some(data) => {
|
||||
let day_idx = (day - 1) as usize;
|
||||
if day_idx < data.len() {
|
||||
data.chars()
|
||||
.nth(day_idx)
|
||||
.and_then(|c| c.to_digit(10).map(|d| d as c_int))
|
||||
.unwrap_or(-1)
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get holiday data for entire year.
|
||||
///
|
||||
/// # Safety
|
||||
/// `country_code` must be a valid null-terminated C string.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn plugin_get_year_holidays(
|
||||
year: c_int,
|
||||
country_code: *const c_char,
|
||||
) -> *mut c_char {
|
||||
let country = unsafe { parse_country(country_code) };
|
||||
let data = fetch_holidays_year(year, &country).unwrap_or_default();
|
||||
CString::new(data).unwrap().into_raw()
|
||||
}
|
||||
|
||||
/// Fetch holiday data for a month from isdayoff.ru API.
|
||||
fn fetch_holidays(year: i32, month: u32, _country: &str) -> Option<String> {
|
||||
let url = format!("{}?year={}&month={:02}&pre=1", API_URL_MONTH, year, month);
|
||||
|
||||
match ureq::get(&url).call() {
|
||||
Ok(response) => response.into_string().ok(),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch holiday data for entire year from isdayoff.ru API.
|
||||
pub fn fetch_holidays_year(year: i32, _country: &str) -> Option<String> {
|
||||
let url = format!("{}?year={}&pre=1", API_URL_YEAR, year);
|
||||
|
||||
match ureq::get(&url).call() {
|
||||
Ok(response) => response.into_string().ok(),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine country code from system locale.
|
||||
pub fn get_country_from_locale() -> String {
|
||||
let locale = std::env::var("LC_ALL")
|
||||
.or_else(|_| std::env::var("LC_TIME"))
|
||||
.or_else(|_| std::env::var("LANG"))
|
||||
.unwrap_or_else(|_| "en_US.UTF-8".to_string());
|
||||
|
||||
let locale_name = locale
|
||||
.split('.')
|
||||
.next()
|
||||
.unwrap_or(&locale)
|
||||
.split('@')
|
||||
.next()
|
||||
.unwrap_or(&locale);
|
||||
|
||||
// Match against supported countries
|
||||
for (country, locales) in SUPPORTED_COUNTRIES {
|
||||
for &supported_locale in *locales {
|
||||
if locale_name == supported_locale {
|
||||
return country.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract country code from locale (e.g., "en_US" -> "US")
|
||||
if let Some(underscore_pos) = locale_name.find('_') {
|
||||
let country_code = &locale_name[underscore_pos + 1..];
|
||||
for (country, _) in SUPPORTED_COUNTRIES {
|
||||
if *country == country_code {
|
||||
return country_code.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"RU".to_string()
|
||||
}
|
||||
Reference in New Issue
Block a user