use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OptionsAction {
Customize,
Settings,
Refresh,
Detect,
OpenTui,
ToggleStartup,
CheckUpdates,
About,
Quit,
}
impl OptionsAction {
#[cfg_attr(any(windows, target_os = "macos"), allow(dead_code))]
pub(crate) const ALL: [OptionsAction; 9] = [
OptionsAction::Customize,
OptionsAction::Settings,
OptionsAction::Refresh,
OptionsAction::Detect,
OptionsAction::OpenTui,
OptionsAction::ToggleStartup,
OptionsAction::CheckUpdates,
OptionsAction::About,
OptionsAction::Quit,
];
pub(crate) fn id(self) -> &'static str {
match self {
OptionsAction::Customize => "options-customize",
OptionsAction::Settings => "options-settings",
OptionsAction::Refresh => "options-refresh",
OptionsAction::Detect => "options-detect",
OptionsAction::OpenTui => "options-open-tui",
OptionsAction::ToggleStartup => "options-startup",
OptionsAction::CheckUpdates => "options-check-updates",
OptionsAction::About => "options-about",
OptionsAction::Quit => "options-quit",
}
}
pub(crate) fn from_id(id: &str) -> Option<Self> {
match id {
"options-customize" => Some(OptionsAction::Customize),
"options-settings" => Some(OptionsAction::Settings),
"options-refresh" => Some(OptionsAction::Refresh),
"options-detect" => Some(OptionsAction::Detect),
"options-open-tui" => Some(OptionsAction::OpenTui),
"options-startup" => Some(OptionsAction::ToggleStartup),
"options-check-updates" => Some(OptionsAction::CheckUpdates),
"options-about" => Some(OptionsAction::About),
"options-quit" => Some(OptionsAction::Quit),
_ => None,
}
}
pub(crate) fn popover_action(self) -> Option<&'static str> {
match self {
OptionsAction::Customize => Some("customize"),
OptionsAction::Settings => Some("settings"),
OptionsAction::CheckUpdates => Some("check-updates"),
OptionsAction::About => Some("about"),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct OptionsLabels {
pub(crate) customize: String,
pub(crate) settings: String,
pub(crate) refresh: String,
pub(crate) detect: String,
pub(crate) open_tui: String,
pub(crate) start_at_login: String,
pub(crate) check_for_updates: String,
pub(crate) about: String,
pub(crate) quit: String,
}
impl Default for OptionsLabels {
fn default() -> Self {
Self {
customize: "Customize".into(),
settings: "Settings".into(),
refresh: "Refresh".into(),
detect: "Detect Providers".into(),
open_tui: "Open TUI".into(),
start_at_login: "Start at Login".into(),
check_for_updates: "Check for Updates…".into(),
about: "About".into(),
quit: "Quit".into(),
}
}
}
impl OptionsLabels {
pub(crate) fn merged(&self, value: &Value) -> Self {
Self {
customize: usable(value, "customize").unwrap_or_else(|| self.customize.clone()),
settings: usable(value, "settings").unwrap_or_else(|| self.settings.clone()),
refresh: usable(value, "refresh").unwrap_or_else(|| self.refresh.clone()),
detect: usable(value, "detect").unwrap_or_else(|| self.detect.clone()),
open_tui: usable(value, "openTui").unwrap_or_else(|| self.open_tui.clone()),
start_at_login: usable(value, "startAtLogin")
.unwrap_or_else(|| self.start_at_login.clone()),
check_for_updates: usable(value, "checkForUpdates")
.unwrap_or_else(|| self.check_for_updates.clone()),
about: usable(value, "about").unwrap_or_else(|| self.about.clone()),
quit: usable(value, "quit").unwrap_or_else(|| self.quit.clone()),
}
}
}
fn usable(value: &Value, key: &str) -> Option<String> {
value
.get(key)
.and_then(Value::as_str)
.map(str::trim)
.filter(|text| !text.is_empty())
.filter(|text| text.chars().count() <= 64)
.filter(|text| !text.chars().any(char::is_control))
.map(str::to_owned)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum OptionsEntry {
Item {
action: OptionsAction,
label: String,
},
Check {
action: OptionsAction,
label: String,
checked: bool,
},
Separator,
}
pub(crate) fn options_entries(
labels: &OptionsLabels,
native: bool,
startup_enabled: bool,
) -> Vec<OptionsEntry> {
let mut entries = Vec::new();
if !native {
entries.push(OptionsEntry::Item {
action: OptionsAction::Customize,
label: labels.customize.clone(),
});
}
entries.push(OptionsEntry::Item {
action: OptionsAction::Settings,
label: labels.settings.clone(),
});
entries.push(OptionsEntry::Separator);
entries.push(OptionsEntry::Item {
action: OptionsAction::Refresh,
label: labels.refresh.clone(),
});
entries.push(OptionsEntry::Item {
action: OptionsAction::Detect,
label: labels.detect.clone(),
});
entries.push(OptionsEntry::Item {
action: OptionsAction::OpenTui,
label: labels.open_tui.clone(),
});
entries.push(OptionsEntry::Separator);
entries.push(OptionsEntry::Check {
action: OptionsAction::ToggleStartup,
label: labels.start_at_login.clone(),
checked: startup_enabled,
});
entries.push(OptionsEntry::Separator);
entries.push(OptionsEntry::Item {
action: OptionsAction::CheckUpdates,
label: labels.check_for_updates.clone(),
});
entries.push(OptionsEntry::Item {
action: OptionsAction::About,
label: labels.about.clone(),
});
entries.push(OptionsEntry::Item {
action: OptionsAction::Quit,
label: labels.quit.clone(),
});
entries
}
#[cfg(any(windows, target_os = "macos"))]
pub(crate) fn fill_menu(menu: &tray_icon::menu::Menu, entries: &[OptionsEntry]) {
use tray_icon::menu::{CheckMenuItem, MenuItem, PredefinedMenuItem};
while menu.remove_at(0).is_some() {}
for entry in entries {
let _ = match entry {
OptionsEntry::Item { action, label } => {
menu.append(&MenuItem::with_id(action.id(), label, true, None))
}
OptionsEntry::Check {
action,
label,
checked,
} => menu.append(&CheckMenuItem::with_id(
action.id(),
label,
true,
*checked,
None,
)),
OptionsEntry::Separator => menu.append(&PredefinedMenuItem::separator()),
};
}
}
#[cfg(test)]
mod tests {
use super::*;
fn item(action: OptionsAction, label: &str) -> OptionsEntry {
OptionsEntry::Item {
action,
label: label.into(),
}
}
#[test]
fn classic_menu_matches_the_popover_options_order() {
assert_eq!(
options_entries(&OptionsLabels::default(), false, false),
vec![
item(OptionsAction::Customize, "Customize"),
item(OptionsAction::Settings, "Settings"),
OptionsEntry::Separator,
item(OptionsAction::Refresh, "Refresh"),
item(OptionsAction::Detect, "Detect Providers"),
item(OptionsAction::OpenTui, "Open TUI"),
OptionsEntry::Separator,
OptionsEntry::Check {
action: OptionsAction::ToggleStartup,
label: "Start at Login".into(),
checked: false,
},
OptionsEntry::Separator,
item(OptionsAction::CheckUpdates, "Check for Updates…"),
item(OptionsAction::About, "About"),
item(OptionsAction::Quit, "Quit"),
]
);
}
#[test]
fn native_menu_omits_customize() {
let classic = options_entries(&OptionsLabels::default(), false, false);
let native = options_entries(&OptionsLabels::default(), true, false);
assert_eq!(native, classic[1..].to_vec());
}
#[test]
fn startup_check_follows_the_login_item() {
let checked = |entries: &[OptionsEntry]| {
entries.iter().find_map(|entry| match entry {
OptionsEntry::Check {
action: OptionsAction::ToggleStartup,
checked,
..
} => Some(*checked),
_ => None,
})
};
assert_eq!(
checked(&options_entries(&OptionsLabels::default(), false, false)),
Some(false)
);
assert_eq!(
checked(&options_entries(&OptionsLabels::default(), false, true)),
Some(true)
);
}
#[test]
fn ids_round_trip_and_do_not_collide_with_the_fallback_menu() {
for action in OptionsAction::ALL {
assert_eq!(OptionsAction::from_id(action.id()), Some(action));
}
let ids: Vec<&str> = OptionsAction::ALL
.iter()
.map(|action| action.id())
.collect();
assert_eq!(
ids.iter().collect::<std::collections::HashSet<_>>().len(),
ids.len()
);
assert_eq!(OptionsAction::from_id("fallback-refresh"), None);
assert_eq!(OptionsAction::from_id("refresh"), None);
assert_eq!(OptionsAction::from_id(""), None);
}
#[test]
fn popover_actions_cover_only_screen_entries() {
assert_eq!(OptionsAction::Customize.popover_action(), Some("customize"));
assert_eq!(OptionsAction::Settings.popover_action(), Some("settings"));
assert_eq!(
OptionsAction::CheckUpdates.popover_action(),
Some("check-updates")
);
assert_eq!(OptionsAction::About.popover_action(), Some("about"));
for action in [
OptionsAction::Refresh,
OptionsAction::Detect,
OptionsAction::OpenTui,
OptionsAction::ToggleStartup,
OptionsAction::Quit,
] {
assert_eq!(action.popover_action(), None);
}
}
#[test]
fn merged_takes_usable_labels_and_keeps_the_rest() {
let merged = OptionsLabels::default().merged(&serde_json::json!({
"cmd": "menu-labels",
"settings": "Configurações",
"quit": " Sair ",
"refresh": "",
"detect": "a\nb",
"about": 42,
"openTui": "a".repeat(65),
}));
assert_eq!(merged.settings, "Configurações");
assert_eq!(merged.quit, "Sair");
assert_eq!(merged.refresh, "Refresh");
assert_eq!(merged.detect, "Detect Providers");
assert_eq!(merged.about, "About");
assert_eq!(merged.open_tui, "Open TUI");
assert_eq!(merged.customize, "Customize");
assert_eq!(merged.start_at_login, "Start at Login");
assert_eq!(merged.check_for_updates, "Check for Updates…");
}
}