Skip to main content

openlogi_core/
brand.rs

1//! Brand constants shared across the workspace: the project's public URLs and
2//! the `openlogi://` deep-link command vocabulary.
3//!
4//! Both live here, in the platform-free core crate, so the agent (which *emits*
5//! tray deep links and renders help links) and the GUI (which *parses* the deep
6//! links and renders the same help links) share a single source of truth — the
7//! command names can't drift across the process boundary, and a repo move
8//! touches one file instead of three.
9
10/// The OpenLogi GitHub repository.
11pub const REPO_URL: &str = "https://github.com/AprilNEA/OpenLogi";
12/// The README, used as the in-app "Help" link.
13pub const HELP_URL: &str = "https://github.com/AprilNEA/OpenLogi#readme";
14/// The "latest release" page.
15pub const RELEASES_URL: &str = "https://github.com/AprilNEA/OpenLogi/releases/latest";
16
17/// The release page for a specific version tag (e.g. the running build).
18#[must_use]
19pub fn release_tag_url(version: &str) -> String {
20    format!("{REPO_URL}/releases/tag/v{version}")
21}
22
23/// A GUI action the agent's tray (or any external caller) requests by opening
24/// an `openlogi://<name>` URL. macOS delivers it to the running GUI via an
25/// Apple Event; the GUI parses it back into this enum and dispatches.
26///
27/// The agent builds URLs with [`DeeplinkCommand::to_url`]; the GUI reads them
28/// with [`DeeplinkCommand::parse_url`]. The command names are defined once, in
29/// [`DeeplinkCommand::as_name`], so the two sides cannot disagree.
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub enum DeeplinkCommand {
32    /// Show / foreground the main window.
33    Show,
34    /// Open the Settings window.
35    OpenSettings,
36    /// Open Settings on the About page.
37    OpenAbout,
38    /// Run a manual update check and open Settings on the Updates page, where
39    /// its status is rendered.
40    CheckForUpdates,
41    /// Quit the GUI.
42    Quit,
43}
44
45impl DeeplinkCommand {
46    /// The URL scheme OpenLogi registers with LaunchServices.
47    pub const SCHEME: &str = "openlogi";
48
49    /// The wire name for this command — the host component of its URL.
50    #[must_use]
51    pub const fn as_name(self) -> &'static str {
52        match self {
53            Self::Show => "show",
54            Self::OpenSettings => "open-settings",
55            Self::OpenAbout => "open-about",
56            Self::CheckForUpdates => "check-for-updates",
57            Self::Quit => "quit",
58        }
59    }
60
61    /// Build the `openlogi://<name>` URL for this command.
62    #[must_use]
63    pub fn to_url(self) -> String {
64        format!("{}://{}", Self::SCHEME, self.as_name())
65    }
66
67    /// Parse a command from its wire name (the part after `openlogi://`).
68    #[must_use]
69    pub fn from_name(name: &str) -> Option<Self> {
70        match name {
71            "show" => Some(Self::Show),
72            "open-settings" => Some(Self::OpenSettings),
73            "open-about" => Some(Self::OpenAbout),
74            "check-for-updates" => Some(Self::CheckForUpdates),
75            "quit" => Some(Self::Quit),
76            _ => None,
77        }
78    }
79
80    /// Parse a full `openlogi://…` URL. The command lives in the URL's host
81    /// component, so any trailing path or query (`openlogi://show/`,
82    /// `openlogi://show?x=1`) is ignored. Returns `None` for a foreign scheme
83    /// or an unknown command.
84    #[must_use]
85    pub fn parse_url(url: &str) -> Option<Self> {
86        let rest = url.strip_prefix(Self::SCHEME)?.strip_prefix("://")?;
87        let name = rest.split(['/', '?']).next().unwrap_or(rest);
88        Self::from_name(name)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::DeeplinkCommand;
95
96    const ALL: [DeeplinkCommand; 5] = [
97        DeeplinkCommand::Show,
98        DeeplinkCommand::OpenSettings,
99        DeeplinkCommand::OpenAbout,
100        DeeplinkCommand::CheckForUpdates,
101        DeeplinkCommand::Quit,
102    ];
103
104    #[test]
105    fn url_round_trips() {
106        for cmd in ALL {
107            assert_eq!(DeeplinkCommand::parse_url(&cmd.to_url()), Some(cmd));
108        }
109    }
110
111    #[test]
112    fn parse_url_ignores_trailing_path_and_query() {
113        assert_eq!(
114            DeeplinkCommand::parse_url("openlogi://show/"),
115            Some(DeeplinkCommand::Show)
116        );
117        assert_eq!(
118            DeeplinkCommand::parse_url("openlogi://open-settings?from=tray"),
119            Some(DeeplinkCommand::OpenSettings)
120        );
121    }
122
123    #[test]
124    fn parse_url_rejects_foreign_scheme_and_unknown_command() {
125        assert_eq!(DeeplinkCommand::parse_url("https://example.com/show"), None);
126        assert_eq!(DeeplinkCommand::parse_url("openlogi://bogus"), None);
127        assert_eq!(DeeplinkCommand::parse_url("openlogi://"), None);
128    }
129}