mod app;
mod body;
mod folders;
mod html;
mod icons;
mod list;
mod numbers;
mod rules;
#[cfg(test)]
mod snapshots;
mod startup;
mod status;
#[cfg(test)]
mod test_support;
mod theme;
mod toolbar;
use std::path::Path;
use std::sync::{Arc, OnceLock};
use anyhow::{Result, bail};
use eframe::egui;
use crate::config::Config;
use crate::paths::Paths;
pub use app::App;
pub(crate) const ICON: &[u8] = include_bytes!("../../assets/icon.png");
#[cfg(target_os = "macos")]
const WINDOW_ICON: &[u8] = include_bytes!("../../assets/macos.iconset/icon_512x512@2x.png");
#[cfg(not(target_os = "macos"))]
const WINDOW_ICON: &[u8] = ICON;
const APP_ID: &str = "io.github.postvak_app.postvak";
pub fn run(paths: Result<Paths>) -> Result<()> {
let window: Arc<OnceLock<egui::Context>> = Arc::default();
let error = match paths {
Ok(paths) => match start(&paths, window.clone()) {
Ok((config, connection)) => return open(config, paths, connection, &window),
Err(e) => explain(e, Some(&paths.daemon_log())),
},
Err(e) => explain(e, None),
};
Err(error)
}
fn start(paths: &Paths, window: Arc<OnceLock<egui::Context>>) -> Result<(Config, app::Connection)> {
let config = Config::load(&paths.config_file())?;
if config.accounts.is_empty() {
bail!(startup::NoAccounts);
}
let wake = Box::new(move || {
if let Some(ctx) = window.get() {
ctx.request_repaint();
}
});
Ok((config, app::connect(paths, wake)?))
}
fn native_options(size: [f32; 2]) -> eframe::NativeOptions {
let options = eframe::NativeOptions {
viewport: viewport(size),
..Default::default()
};
#[cfg(target_os = "macos")]
let options = prefer_integrated_gpu(options);
options
}
#[cfg(target_os = "macos")]
fn prefer_integrated_gpu(mut options: eframe::NativeOptions) -> eframe::NativeOptions {
use eframe::wgpu::PowerPreference;
if let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_options.wgpu_setup {
setup.power_preference = PowerPreference::from_env().unwrap_or(PowerPreference::LowPower);
}
options
}
fn viewport(size: [f32; 2]) -> egui::ViewportBuilder {
#[allow(clippy::expect_used)]
let icon =
eframe::icon_data::from_png_bytes(WINDOW_ICON).expect("the window icon is a valid PNG");
egui::ViewportBuilder::default()
.with_icon(icon)
.with_app_id(APP_ID)
.with_inner_size(size)
.with_title("Postvak")
}
fn explain(error: anyhow::Error, log: Option<&Path>) -> anyhow::Error {
let problem = startup::Problem::of(&error, log);
let shown = eframe::run_native(
"Postvak",
explain_options(),
Box::new(|cc| {
theme::install(&cc.egui_ctx);
Ok(Box::new(Explain(problem)))
}),
);
if let Err(e) = shown {
log::debug!("could not show the startup error in a window: {e}");
}
error
}
fn explain_options() -> eframe::NativeOptions {
eframe::NativeOptions {
persist_window: false,
persistence_path: Some(std::path::PathBuf::new()),
..native_options([520.0, 260.0])
}
}
pub(crate) struct Explain(startup::Problem);
impl Explain {
fn show(&mut self, ui: &mut egui::Ui) {
if startup::show(&self.0, ui) {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
}
}
}
impl eframe::App for Explain {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.show(ui);
}
fn persist_egui_memory(&self) -> bool {
false
}
}
fn open(
config: Config,
paths: Paths,
(client, events, states): app::Connection,
window: &OnceLock<egui::Context>,
) -> Result<()> {
let options = eframe::NativeOptions {
persistence_path: Some(paths.state_dir.join("window.ron")),
..native_options([1280.0, 800.0])
};
eframe::run_native(
"Postvak",
options,
Box::new(move |cc| {
let _ = window.set(cc.egui_ctx.clone());
Ok(Box::new(App::new(&config, paths, client, events, states)))
}),
)
.map_err(|e| anyhow::anyhow!("the window failed: {e}"))
}
#[cfg(test)]
mod tests {
#[cfg(target_os = "macos")]
#[test]
fn macos_windows_prefer_the_integrated_gpu() {
use eframe::egui_wgpu::WgpuSetup;
use eframe::wgpu::PowerPreference;
if std::env::var_os("WGPU_POWER_PREF").is_some() {
return;
}
let options = super::native_options([100.0, 100.0]);
let WgpuSetup::CreateNew(setup) = options.wgpu_options.wgpu_setup else {
panic!("eframe's default wgpu setup creates a new instance");
};
assert_eq!(setup.power_preference, PowerPreference::LowPower);
}
#[test]
fn the_icon_is_a_square_png_with_transparent_corners() {
let icon = eframe::icon_data::from_png_bytes(super::ICON).unwrap();
assert_eq!((icon.width, icon.height), (512, 512));
assert_eq!(icon.rgba[3], 0, "the top-left pixel must be transparent");
}
#[test]
fn the_window_icon_is_a_valid_png() {
let icon = eframe::icon_data::from_png_bytes(super::WINDOW_ICON).unwrap();
assert_eq!(icon.width, icon.height);
if cfg!(target_os = "macos") {
let left_middle = (icon.height / 2 * icon.width + icon.width / 20) as usize * 4;
assert_eq!(
icon.rgba[left_middle + 3],
0,
"the Dock icon needs Apple's transparent margin"
);
}
}
#[test]
fn the_error_window_saves_nothing() {
let options = super::explain_options();
assert!(!options.persist_window);
assert_eq!(options.persistence_path, Some(std::path::PathBuf::new()));
let problem = super::startup::Problem::of(&anyhow::anyhow!("no display"), None);
assert!(!eframe::App::persist_egui_memory(&super::Explain(problem)));
}
#[test]
fn the_desktop_entry_names_the_app_id() {
let entry = include_str!("../../packaging/linux/io.github.postvak_app.postvak.desktop");
for key in ["Icon", "StartupWMClass"] {
let line = format!("{key}={}", super::APP_ID);
assert!(
entry.lines().any(|l| l == line),
"the desktop entry lacks {line}"
);
}
}
}