mod app;
#[cfg(test)]
mod dialog_style;
mod editor;
mod environments;
mod filepick;
mod icons;
mod menu;
mod postman;
mod probe;
#[cfg(test)]
mod probe_test_support;
mod remote;
mod report_editor;
mod report_run;
mod report_wizard;
mod reports;
mod requests;
mod response;
mod theme;
mod widgets;
pub use app::GuiApp;
use eframe::egui;
pub const DEFAULT_WINDOW: (f32, f32) = (1280.0, 820.0);
pub const MIN_WINDOW: (f32, f32) = (760.0, 500.0);
pub fn run() -> Result<(), String> {
install_desktop_integration();
let saved = crate::persistence::load_state()
.map(|s| s.gui)
.unwrap_or_default();
let (w, h) = saved
.window
.filter(|(w, h)| *w >= MIN_WINDOW.0 && *h >= MIN_WINDOW.1)
.unwrap_or(DEFAULT_WINDOW);
let mut viewport = egui::ViewportBuilder::default()
.with_inner_size([w, h])
.with_min_inner_size([MIN_WINDOW.0, MIN_WINDOW.1])
.with_title("PaperBoy")
.with_app_id("paperboy");
if let Some(icon) = app::load_app_icon() {
viewport = viewport.with_icon(std::sync::Arc::new(icon));
}
let native_options = eframe::NativeOptions {
viewport,
..Default::default()
};
eframe::run_native(
"PaperBoy",
native_options,
Box::new(|cc| Ok(Box::new(GuiApp::new(cc)))),
)
.map_err(|e| e.to_string())
}
#[cfg(target_os = "linux")]
fn install_desktop_integration() {
use std::path::PathBuf;
let home = std::env::var_os("HOME").map(PathBuf::from);
let data_home = std::env::var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| home.as_ref().map(|h| h.join(".local/share")));
let Some(data_home) = data_home else {
return;
};
let icon_dir = data_home.join("paperboy");
let icon_path = icon_dir.join("paperboy_logo.png");
if !icon_path.exists() {
let _ = std::fs::create_dir_all(&icon_dir);
let _ = std::fs::write(&icon_path, app::LOGO_PNG);
}
let apps_dir = data_home.join("applications");
let desktop_path = apps_dir.join("paperboy.desktop");
if !desktop_path.exists() {
let exec = std::env::current_exe()
.ok()
.and_then(|p| p.to_str().map(str::to_string))
.unwrap_or_else(|| "paperboy".to_string());
let icon = icon_path.to_string_lossy();
let entry = format!(
"[Desktop Entry]\n\
Type=Application\n\
Name=PaperBoy\n\
Comment=Rust-native API client\n\
Exec={exec} -g\n\
Icon={icon}\n\
Terminal=false\n\
StartupWMClass=paperboy\n\
Categories=Development;Utility;\n"
);
let _ = std::fs::create_dir_all(&apps_dir);
let _ = std::fs::write(&desktop_path, entry);
}
}
#[cfg(not(target_os = "linux"))]
fn install_desktop_integration() {}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Focus {
Tabs,
List,
Main,
GlobalEnv,
Response,
}
impl Focus {
pub const ORDER: [Focus; 5] = [
Focus::Tabs,
Focus::List,
Focus::Main,
Focus::GlobalEnv,
Focus::Response,
];
pub fn cycle(self, forward: bool) -> Focus {
let n = Self::ORDER.len();
let cur = Self::ORDER.iter().position(|f| *f == self).unwrap_or(0);
let next = if forward {
(cur + 1) % n
} else {
(cur + n - 1) % n
};
Self::ORDER[next]
}
}
pub struct ShortcutSection<'a> {
pub title: &'a str,
pub rows: Vec<(&'a str, &'a str)>,
}
#[cfg_attr(not(feature = "gui"), allow(dead_code))]
pub fn shortcut_help_sections(s: &crate::i18n::Strings) -> Vec<ShortcutSection<'_>> {
vec![
ShortcutSection {
title: s.gui_shortcuts_group_panels,
rows: vec![(
s.gui_shortcut_cycle_panels,
s.gui_shortcut_cycle_panels_desc,
)],
},
ShortcutSection {
title: s.gui_shortcuts_group_list,
rows: vec![
(s.gui_shortcut_list_move, s.gui_shortcut_list_move_desc),
(s.gui_shortcut_list_ends, s.gui_shortcut_list_ends_desc),
(s.gui_shortcut_list_run, s.gui_shortcut_list_run_desc),
(s.gui_shortcut_list_rename, s.gui_shortcut_list_rename_desc),
(s.gui_shortcut_list_delete, s.gui_shortcut_list_delete_desc),
],
},
ShortcutSection {
title: s.gui_shortcuts_group_run,
rows: vec![
(s.gui_shortcut_run, s.gui_shortcut_run_desc),
(s.gui_shortcut_save, s.gui_shortcut_save_desc),
(s.gui_shortcut_save_as, s.gui_shortcut_save_as_desc),
(s.gui_shortcut_close_tab_key, s.gui_shortcut_close_tab_desc),
(s.gui_shortcut_undo_delete, s.gui_shortcut_undo_delete_desc),
],
},
ShortcutSection {
title: s.gui_shortcuts_group_report,
rows: vec![(s.gui_shortcut_find, s.gui_shortcut_find_desc)],
},
ShortcutSection {
title: s.gui_shortcuts_group_help,
rows: vec![
(s.gui_shortcut_help, s.gui_shortcut_help_desc),
(s.gui_shortcut_escape, s.gui_shortcut_escape_desc),
],
},
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::i18n::{Language, Strings};
#[test]
fn the_shortcut_overlay_is_complete_in_every_language() {
for lang in [Language::English, Language::French, Language::Danish] {
let s = Strings::for_language(&lang);
let sections = shortcut_help_sections(&s);
assert!(!sections.is_empty(), "{lang:?}: the overlay has content");
let mut rows = 0;
for section in §ions {
assert!(
!section.title.trim().is_empty(),
"{lang:?}: every group has a heading"
);
for (keys, desc) in §ion.rows {
assert!(!keys.trim().is_empty(), "{lang:?}: every row names a key");
assert!(
!desc.trim().is_empty(),
"{lang:?}: every row explains itself"
);
rows += 1;
}
}
assert!(
rows >= 10,
"{lang:?}: the overlay documents the vocabulary, not a placeholder ({rows} rows)"
);
}
}
#[test]
fn the_shortcut_overlay_names_the_keys_that_exist() {
let s = Strings::for_language(&Language::English);
let keys: Vec<&str> = shortcut_help_sections(&s)
.into_iter()
.flat_map(|sec| sec.rows.into_iter().map(|(k, _)| k))
.collect();
for expected in [
"Tab / Shift+Tab",
"Ctrl+Enter / F5",
"Ctrl+S",
"Ctrl+Shift+S",
"Ctrl+W",
"Ctrl+Z",
"F2",
"Delete",
"Ctrl+F",
"F1",
] {
assert!(
keys.contains(&expected),
"the overlay should list {expected:?}, has {keys:?}"
);
}
}
}