diff --git a/src/main.rs b/src/main.rs index b12ac0b..be634a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -257,7 +257,10 @@ pub fn main() -> ExitCode { match iced_result { Ok(()) => ExitCode::SUCCESS, - Err(_) => ExitCode::from(42), // Custom error code + Err(ex) => { + println!("ICED Error: {}", ex); + ExitCode::from(42) + }, // Custom error code } } diff --git a/src/stages/keyboard/mod.rs b/src/stages/keyboard/mod.rs new file mode 100644 index 0000000..7090ec2 --- /dev/null +++ b/src/stages/keyboard/mod.rs @@ -0,0 +1,258 @@ +// +// Copyright (C) <2026> + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/* + This is TimeZone stage, used to select timezone +*/ + +use crate::stage::{ConfigValue, StageAction, StageResult}; +use iced::{Alignment, Length, widget}; +use rust_i18n::t; +use std::collections::HashMap; + +mod scroll_list; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TimeZoneData { + region: String, + zone: String, +} + +impl TimeZoneData { + fn from_str(s: &str) -> Option { + let idx = s.find('/')?; + let (region, zone) = s.split_at(idx); + Some(Self { + region: region.into(), + zone: zone.trim_start_matches('/').into(), + }) + } +} + +impl std::fmt::Display for TimeZoneData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}", self.region, self.zone) + } +} + +#[derive(Debug, Clone)] +pub struct TimeZoneStage { + time_zones: Result>, String>, + regions: Vec, + region_id: Option, + zones: Vec, + zone_id: Option, + selected_zone: Option, + selected_zone_text: String, +} + +#[derive(Debug, Clone)] +pub enum Message { + SelectRegion(scroll_list::ListViewMessage), + SelectZone(scroll_list::ListViewMessage), + Next, + Back, +} + +/// Getting list of awailable timezones from system. +fn get_timezones_blocking() -> Result, String> { + use std::process::Command; + //timedatectl list-timezones --no-pager + match Command::new("timedatectl") + .arg("list-timezones") + .arg("--no-pager") + .output() + { + Ok(cmd_output) => { + if cmd_output.status.success() { + //Etc/UTC + match String::from_utf8(cmd_output.stdout) { + Ok(str_tz_list) => Ok(str_tz_list + .split("\n") + .filter(|sb| sb.contains('/')) + .filter_map(|str_t_zone| TimeZoneData::from_str(str_t_zone.trim())) + .collect::>()), + Err(ex) => Err(format!("Exception while converting to UTF8 {}", ex)), + } + } else { + Err(format!( + "Error getting timezones list: {}", + String::from_utf8(cmd_output.stderr).unwrap_or("UNKNOWN".into()) + )) + } + } + Err(ex) => Err(format!("Exception while trying to list time zones {}", ex)), + } +} + +impl TimeZoneStage { + pub fn new() -> Self { + match get_timezones_blocking() { + Ok(time_zones_data_list) => { + let mut time_zones_map: HashMap> = HashMap::new(); + for tz_data in &time_zones_data_list { + if let Some(zones) = time_zones_map.get_mut(&tz_data.region) { + zones.push(tz_data.zone.clone()); + } else { + time_zones_map.insert(tz_data.region.clone(), vec![tz_data.zone.clone()]); + } + } + // sort zones alphabetically + time_zones_map.values_mut().for_each(|v| v.sort()); + // sort region names alphabetically + let mut regions: Vec = time_zones_map.keys().cloned().collect(); + regions.sort(); + + Self { + regions: regions, + zones: Vec::new(), + time_zones: Ok(time_zones_map), + region_id: None, + zone_id: None, + selected_zone: None, + selected_zone_text: String::new(), + } + } + Err(ex) => { + println!( + "Exception while trying to get time zones list from system {}", + ex + ); + Self { + time_zones: Err(ex), + regions: Vec::new(), + zones: Vec::new(), + region_id: None, + zone_id: None, + selected_zone: None, + selected_zone_text: String::new(), + } + } + } + } + + fn gen_result(&self) -> StageResult { + if let Some(time_zone) = &self.selected_zone { + StageResult { + name: "time_zone".to_string(), + config: Some(HashMap::from([ + ( + "name".to_string(), + ConfigValue::String(time_zone.to_string()), + ), + ( + "region".to_string(), + ConfigValue::String(time_zone.region.clone()), + ), + ( + "zone".to_string(), + ConfigValue::String(time_zone.zone.clone()), + ), + ])), + resuts: None, + error: None, + } + } else { + StageResult { + name: "time_zone".to_string(), + config: None, + resuts: None, + error: None, + } + } + } + + pub fn update(&mut self, message: Message) -> StageAction { + match message { + Message::SelectRegion(scroll_list::ListViewMessage::Select(region_id)) => { + let region = self.regions.get(region_id).unwrap(); + self.zones = self + .time_zones + .as_ref() + .unwrap() + .get(region) + .unwrap() + .clone(); + self.region_id = Some(region_id); + self.zone_id = None; + //self.selected_zone_text.clear(); + StageAction::None + } + Message::SelectZone(scroll_list::ListViewMessage::Select(zone_id)) => { + let zone = TimeZoneData { + region: self.regions[self.region_id.unwrap()].clone(), + zone: self.zones[zone_id].clone(), + }; + self.selected_zone_text = + format!("{} {}", t!("timezone.selected_timezone"), zone.to_string()); + self.zone_id = Some(zone_id); + self.selected_zone = Some(zone); + StageAction::None + } + Message::Next => StageAction::Next(self.gen_result()), + Message::Back => StageAction::Back, + } + } + + pub fn view(&self) -> iced::Element<'_, Message> { + let next_button = if self.zone_id.is_some() { + widget::button(widget::text(t!("button.next"))).on_press(Message::Next) + } else { + widget::button(widget::text(t!("button.next"))) + }; + let main_content = match &self.time_zones { + Ok(_) => widget::container( + widget::column![ + widget::text(t!("timezone.select_timezone")), + widget::row![ + scroll_list::list_view(&self.regions, self.region_id, Length::Shrink) + .map(Message::SelectRegion), + scroll_list::list_view(&self.zones, self.zone_id, Length::FillPortion(1)) + .map(Message::SelectZone), + ] + .padding(20) + .spacing(10) + .height(Length::Shrink), + widget::text(self.selected_zone_text.clone()) + ] + .spacing(10) + .align_x(Alignment::Center), + ), + + Err(_) => widget::container(widget::text(t!("timezone.init_error"))), + }; + let back_button = widget::button(widget::text(t!("button.back"))).on_press(Message::Back); + + widget::column![ + main_content + .height(iced::Length::Fill) + .width(iced::Length::Fill) + .align_x(Alignment::Center) + .align_y(Alignment::Center), + widget::row![ + back_button, + widget::space::horizontal(), // Pushes the right button to the far right + next_button + ] + .width(iced::Length::Fill) + .align_y(Alignment::End) + .padding(10), + ] + .align_x(Alignment::Center) + .spacing(10) + .into() + } +} diff --git a/src/stages/keyboard/scroll_list.rs b/src/stages/keyboard/scroll_list.rs new file mode 100644 index 0000000..4c268e3 --- /dev/null +++ b/src/stages/keyboard/scroll_list.rs @@ -0,0 +1,110 @@ +use std::fmt::Display; + +use iced::widget::{Column, button, container, scrollable, text}; +use iced::{Alignment, Color, Element, Length, Theme}; + +#[derive(Debug, Clone)] +pub enum ListViewMessage { + Select(usize), +} + +fn unselected_button_style(theme: &Theme, status: button::Status) -> button::Style { + //let palette = theme.extended_palette(); + use iced::{Border, border::Radius}; + + match status { + button::Status::Hovered => button::Style { + border: Border { + color: Color::from_rgb(0.8, 0.3, 0.3), + width: 2.0, + radius: Radius::new(5.0), + }, + ..button::primary(theme, status) + }, + button::Status::Active => button::Style { + background: Some(iced::Background::Color(crate::kira_theming::change_lightnes( + &theme.palette().primary, + -0.1, + ))), + ..button::primary(theme, status) + }, + _ => button::primary(theme, status), + } +} + +fn selected_button_style(theme: &Theme, status: button::Status) -> button::Style { + //let palette = theme.extended_palette(); + + match status { + button::Status::Active => button::Style { + background: Some(iced::Background::Color(Color::from_rgb(0.8, 0.3, 0.3))), + ..button::primary(theme, status) + }, + _ => button::Style { + background: Some(iced::Background::Color(Color::from_rgb(0.8, 0.3, 0.3))), + ..button::primary(theme, status) + }, + } +} + +// fn view() -> container::Style { +// container::Style { +// text_color: None, +// background: None, +// border: Border { +// color: Color::BLACK, +// width: 2.0, +// radius: Radius::new(5.0), +// }, +// shadow: None, +// snap: false, +// } +// } + +// fn border_style(theme: &Theme) -> container::Style { +// use iced::{Border, border::Radius}; + +// container::Style { +// border: Border { +// color: Color::from_rgb(0.8, 0.3, 0.3), +// width: 2.0, +// radius: Radius::new(5.0), +// }, +// ..container::bordered_box(theme) +// } +// } + +// Assuming your Item struct or type implements Display or has a text representation +pub fn list_view( + items: &Vec, + selected_id: Option, + container_height: Length, +) -> Element<'_, ListViewMessage> { + let mut column = Column::new() + .spacing(2) + .align_x(Alignment::Center) + .width(Length::Fill); + + for (index, value) in items.iter().enumerate() { + // Create a row for each item, possibly with buttons or other controls + let list_item = if let Some(id) = selected_id + && id == index + { + button(text(value.to_string())).style(selected_button_style) + } else { + button(text(value.to_string())).style(unselected_button_style) + }; + let list_item = list_item + .width(Length::Fill) + .on_press(ListViewMessage::Select(index)); + + column = column.push(list_item); + } + + // Wrap the column in a scrollable widget + container(scrollable(column).width(Length::Fill)) + .style(container::bordered_box) + .height(container_height) + .padding(8) + .into() +} diff --git a/src/stages/timezone/scroll_list.rs b/src/stages/timezone/scroll_list.rs index 4c268e3..30afbf6 100644 --- a/src/stages/timezone/scroll_list.rs +++ b/src/stages/timezone/scroll_list.rs @@ -9,42 +9,50 @@ pub enum ListViewMessage { } fn unselected_button_style(theme: &Theme, status: button::Status) -> button::Style { - //let palette = theme.extended_palette(); use iced::{Border, border::Radius}; + let palette = theme.palette(); + + let mut res_stile = button::Style { + background: Some(iced::Background::Color(Color::TRANSPARENT)), + text_color: palette.text, + ..button::primary(theme, button::Status::Active) + }; + match status { - button::Status::Hovered => button::Style { - border: Border { + button::Status::Hovered => { + res_stile.border = Border { color: Color::from_rgb(0.8, 0.3, 0.3), width: 2.0, radius: Radius::new(5.0), - }, - ..button::primary(theme, status) + }; }, - button::Status::Active => button::Style { - background: Some(iced::Background::Color(crate::kira_theming::change_lightnes( - &theme.palette().primary, - -0.1, - ))), - ..button::primary(theme, status) - }, - _ => button::primary(theme, status), + _ => (), } + + return res_stile; } fn selected_button_style(theme: &Theme, status: button::Status) -> button::Style { - //let palette = theme.extended_palette(); + use iced::{Border, border::Radius}; + + //let palette = theme.palette(); + + let mut res_stile = button::primary(theme, button::Status::Active); + match status { - button::Status::Active => button::Style { - background: Some(iced::Background::Color(Color::from_rgb(0.8, 0.3, 0.3))), - ..button::primary(theme, status) - }, - _ => button::Style { - background: Some(iced::Background::Color(Color::from_rgb(0.8, 0.3, 0.3))), - ..button::primary(theme, status) + button::Status::Hovered => { + res_stile.border = Border { + color: Color::from_rgb(0.8, 0.3, 0.3), + width: 2.0, + radius: Radius::new(5.0), + }; }, + _ => (), } + + return res_stile; } // fn view() -> container::Style { @@ -102,9 +110,9 @@ pub fn list_view( } // Wrap the column in a scrollable widget - container(scrollable(column).width(Length::Fill)) + container(scrollable(column).width(Length::Fill).spacing(5)) .style(container::bordered_box) .height(container_height) - .padding(8) + .padding(5) .into() }