pub const REPO_URL: &str = "https://github.com/AprilNEA/OpenLogi";
pub const HELP_URL: &str = "https://github.com/AprilNEA/OpenLogi#readme";
pub const RELEASES_URL: &str = "https://github.com/AprilNEA/OpenLogi/releases/latest";
pub const APP_ID: &str = "org.openlogi.openlogi";
pub const AGENT_ID: &str = "org.openlogi.agent";
pub const OVERLAY_ID: &str = "org.openlogi.overlay";
const DEV_SUFFIX: &str = "-dev";
#[must_use]
pub fn dev_id(id: &str) -> String {
format!("{id}{DEV_SUFFIX}")
}
#[must_use]
pub fn is_dev_id(id: &str) -> bool {
[DEV_SUFFIX, LEGACY_DEV_SUFFIX]
.iter()
.any(|suffix| ends_with_ignore_ascii_case(id, suffix))
}
const LEGACY_DEV_SUFFIX: &str = ".dev";
fn ends_with_ignore_ascii_case(haystack: &str, suffix: &str) -> bool {
haystack.len() > suffix.len()
&& haystack
.get(haystack.len() - suffix.len()..)
.is_some_and(|tail| tail.eq_ignore_ascii_case(suffix))
}
#[must_use]
pub fn release_tag_url(version: &str) -> String {
format!("{REPO_URL}/releases/tag/v{version}")
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DeeplinkCommand {
Show,
OpenSettings,
OpenAbout,
CheckForUpdates,
Quit,
}
impl DeeplinkCommand {
pub const SCHEME: &str = "openlogi";
#[must_use]
pub const fn as_name(self) -> &'static str {
match self {
Self::Show => "show",
Self::OpenSettings => "open-settings",
Self::OpenAbout => "open-about",
Self::CheckForUpdates => "check-for-updates",
Self::Quit => "quit",
}
}
#[must_use]
pub fn to_url(self) -> String {
format!("{}://{}", Self::SCHEME, self.as_name())
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name {
"show" => Some(Self::Show),
"open-settings" => Some(Self::OpenSettings),
"open-about" => Some(Self::OpenAbout),
"check-for-updates" => Some(Self::CheckForUpdates),
"quit" => Some(Self::Quit),
_ => None,
}
}
#[must_use]
pub fn parse_url(url: &str) -> Option<Self> {
let rest = url.strip_prefix(Self::SCHEME)?.strip_prefix("://")?;
let name = rest.split(['/', '?']).next().unwrap_or(rest);
Self::from_name(name)
}
}
#[cfg(test)]
mod tests {
use super::{AGENT_ID, APP_ID, DeeplinkCommand, OVERLAY_ID, dev_id, is_dev_id};
const ALL: [DeeplinkCommand; 5] = [
DeeplinkCommand::Show,
DeeplinkCommand::OpenSettings,
DeeplinkCommand::OpenAbout,
DeeplinkCommand::CheckForUpdates,
DeeplinkCommand::Quit,
];
#[test]
fn url_round_trips() {
for cmd in ALL {
assert_eq!(DeeplinkCommand::parse_url(&cmd.to_url()), Some(cmd));
}
}
#[test]
fn parse_url_ignores_trailing_path_and_query() {
assert_eq!(
DeeplinkCommand::parse_url("openlogi://show/"),
Some(DeeplinkCommand::Show)
);
assert_eq!(
DeeplinkCommand::parse_url("openlogi://open-settings?from=tray"),
Some(DeeplinkCommand::OpenSettings)
);
}
#[test]
fn parse_url_rejects_foreign_scheme_and_unknown_command() {
assert_eq!(DeeplinkCommand::parse_url("https://example.com/show"), None);
assert_eq!(DeeplinkCommand::parse_url("openlogi://bogus"), None);
assert_eq!(DeeplinkCommand::parse_url("openlogi://"), None);
}
#[test]
fn dev_ids_round_trip() {
for id in [APP_ID, AGENT_ID, OVERLAY_ID] {
assert!(is_dev_id(&dev_id(id)), "{id} suffixed must read as dev");
assert!(!is_dev_id(id), "{id} is production");
}
}
#[test]
fn the_legacy_dotted_suffix_still_reads_as_dev() {
assert!(is_dev_id("org.openlogi.agent.dev"));
assert!(is_dev_id("org.openlogi.openlogi.dev"));
}
#[test]
fn a_bare_suffix_is_not_a_dev_id() {
assert!(!is_dev_id("-dev"));
assert!(!is_dev_id(".dev"));
assert!(!is_dev_id(""));
}
#[test]
fn matching_ignores_case_but_not_position() {
assert!(is_dev_id("org.openlogi.agent-DEV"));
assert!(!is_dev_id("org.openlogi.dev-agent"));
}
}