pub mod async_ops;
pub mod icon;
pub mod message;
pub mod state;
pub mod subscription;
pub mod update;
pub mod view;
use crate::config::{self, AppTheme, Config};
use crate::context::{AppContext, StandardContext};
use crate::gui::message::Message;
use crate::gui::state::GuiApp;
use crate::system::spawn_alarm_actor;
use iced::futures::SinkExt;
use iced::futures::channel::mpsc::Sender;
use iced::stream;
use iced::window::icon as window_icon;
use iced::{Element, Subscription, Task, Theme, font, window};
use std::path::PathBuf;
use std::sync::Arc;
pub fn run() -> iced::Result {
run_with_ics_file(None, None, false)
}
pub fn run_with_ics_file(
ics_file_path: Option<String>,
override_root: Option<PathBuf>,
force_ssd: bool,
) -> iced::Result {
let window_icon =
window_icon::from_file_data(include_bytes!("../../assets/autogen/cfait.png"), None).ok();
let init_ctx = Arc::new(StandardContext::new(override_root.clone()));
let init_config = Config::load(init_ctx.as_ref()).unwrap_or_default();
let width = init_config.window_width.max(400.0);
let height = init_config.window_height.max(300.0);
iced::application(
move || GuiApp::new_with_ics(ics_file_path.clone(), override_root.clone(), force_ssd),
GuiApp::update,
GuiApp::view,
)
.title(GuiApp::title)
.subscription(GuiApp::subscription)
.theme(GuiApp::theme)
.scale_factor(|state: &GuiApp| state.ui_scale)
.style(|state: &GuiApp, _theme: &Theme| {
let theme_binding = state.theme();
let palette = theme_binding.extended_palette();
iced::theme::Style {
background_color: if state.force_ssd {
palette.background.base.color
} else {
iced::Color::TRANSPARENT
},
text_color: palette.background.base.text,
}
})
.window(window::Settings {
size: iced::Size::new(width, height),
min_size: Some(iced::Size::new(400.0, 300.0)),
icon: window_icon,
decorations: force_ssd,
transparent: !force_ssd,
platform_specific: window::settings::PlatformSpecific {
#[cfg(target_os = "linux")]
application_id: String::from("cfait"),
..Default::default()
},
..Default::default()
})
.run()
}
fn alarm_stream() -> impl iced::futures::Stream<Item = Message> {
stream::channel(1000, |mut output: Sender<Message>| async move {
let (gui_tx, mut gui_rx) = tokio::sync::mpsc::channel(1000);
let actor_tx = spawn_alarm_actor(Some(gui_tx));
let _ = output.send(Message::InitAlarmActor(actor_tx)).await;
while let Some(msg) = gui_rx.recv().await {
let _ = output
.send(Message::AlarmSignalReceived(Arc::new(msg)))
.await;
}
std::future::pending::<()>().await;
})
}
impl GuiApp {
fn new_with_ics(
ics_file_path: Option<String>,
override_root: Option<PathBuf>,
force_ssd: bool,
) -> (Self, Task<Message>) {
let ctx: Arc<dyn AppContext> = Arc::new(StandardContext::new(override_root));
config::init_locale(ctx.as_ref());
let ctx_clone = ctx.clone();
crate::system::init_keyring();
let mut tasks = vec![
Task::perform(
async move {
let ctx_ref = ctx_clone.clone();
match tokio::task::spawn_blocking(move || {
Config::load_with_credentials(ctx_ref.as_ref())
})
.await
{
Ok(Ok(cfg)) => Ok(Box::new(cfg)),
Ok(Err(e)) => Err(e.to_string()),
Err(e) => Err(format!("Task panicked: {}", e)),
}
},
Message::ConfigLoaded,
),
font::load(icon::FONT_BYTES).map(|_| Message::FontLoaded(Ok(()))),
font::load(iced_aw::ICED_AW_FONT_BYTES).map(|_| Message::FontLoaded(Ok(()))),
];
if let Some(path) = ics_file_path {
tasks.push(Task::perform(
async move {
std::fs::read_to_string(&path)
.map(|content| (path, content))
.map_err(|e| e.to_string())
},
|result| match result {
Ok((path, content)) => Message::IcsFileLoaded(Ok((path, content))),
Err(e) => Message::IcsFileLoaded(Err(e)),
},
));
}
let app = Self {
force_ssd,
ctx,
..Self::default()
};
(app, Task::batch(tasks))
}
fn view(&self) -> Element<'_, Message> {
view::root_view(self)
}
fn title(&self) -> String {
rust_i18n::t!("window_title").to_string()
}
fn theme(&self) -> Theme {
fn create_rusty_dark_theme() -> Theme {
let mut palette = iced::Theme::Dark.palette();
palette.background = iced::Color::from_rgb8(0x21, 0x1e, 0x1e);
palette.text = iced::Color::WHITE;
palette.primary = iced::Color::from_rgb8(0xFF, 0xA5, 0x00); palette.success = iced::Color::from_rgb8(0xA3, 0xBE, 0x8C); palette.danger = iced::Color::from_rgb8(0xBF, 0x61, 0x6A); Theme::custom("Rusty Dark", palette)
}
let effective_theme = if self.current_theme == AppTheme::Random {
self.resolved_random_theme
} else {
self.current_theme
};
match effective_theme {
AppTheme::Light => Theme::Light,
AppTheme::Dark => Theme::Dark,
AppTheme::Dracula => Theme::Dracula,
AppTheme::Nord => Theme::Nord,
AppTheme::SolarizedLight => Theme::SolarizedLight,
AppTheme::SolarizedDark => Theme::SolarizedDark,
AppTheme::GruvboxLight => Theme::GruvboxLight,
AppTheme::GruvboxDark => Theme::GruvboxDark,
AppTheme::CatppuccinLatte => Theme::CatppuccinLatte,
AppTheme::CatppuccinFrappe => Theme::CatppuccinFrappe,
AppTheme::CatppuccinMacchiato => Theme::CatppuccinMacchiato,
AppTheme::CatppuccinMocha => Theme::CatppuccinMocha,
AppTheme::TokyoNight => Theme::TokyoNight,
AppTheme::TokyoNightStorm => Theme::TokyoNightStorm,
AppTheme::TokyoNightLight => Theme::TokyoNightLight,
AppTheme::KanagawaWave => Theme::KanagawaWave,
AppTheme::KanagawaDragon => Theme::KanagawaDragon,
AppTheme::KanagawaLotus => Theme::KanagawaLotus,
AppTheme::Moonfly => Theme::Moonfly,
AppTheme::Nightfly => Theme::Nightfly,
AppTheme::Oxocarbon => Theme::Oxocarbon,
AppTheme::Ferra => Theme::Ferra,
AppTheme::RustyDark => create_rusty_dark_theme(),
AppTheme::Random => create_rusty_dark_theme(),
}
}
fn subscription(&self) -> Subscription<Message> {
let subs = subscription::subscription(self);
let alarm_sub = Subscription::run(alarm_stream);
Subscription::batch(vec![subs, alarm_sub])
}
fn update(&mut self, message: Message) -> Task<Message> {
update::update(self, message)
}
}