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 application identifier: the Wayland xdg-toplevel `app_id` (and X11
18/// `WM_CLASS`) the GUI advertises, the root of the macOS bundle-id family
19/// (`org.openlogi.agent`, `org.openlogi.openlogi.dev`), and the value the Linux
20/// `.desktop` file pins as `StartupWMClass`. Defined once here so the window the
21/// compositor sees, the launcher that groups it, and the frontmost backend that
22/// self-identifies OpenLogi can never disagree. The `.desktop` file carries its
23/// own literal copy (it can't reference Rust) — keep the two in sync.
24pub const APP_ID: &str = "org.openlogi.openlogi";
25
26/// The release page for a specific version tag (e.g. the running build).
27#[must_use]
28pub fn release_tag_url(version: &str) -> String {
29 format!("{REPO_URL}/releases/tag/v{version}")
30}
31
32/// A GUI action the agent's tray (or any external caller) requests by opening
33/// an `openlogi://<name>` URL. macOS delivers it to the running GUI via an
34/// Apple Event; the GUI parses it back into this enum and dispatches.
35///
36/// The agent builds URLs with [`DeeplinkCommand::to_url`]; the GUI reads them
37/// with [`DeeplinkCommand::parse_url`]. The command names are defined once, in
38/// [`DeeplinkCommand::as_name`], so the two sides cannot disagree.
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum DeeplinkCommand {
41 /// Show / foreground the main window.
42 Show,
43 /// Open the Settings window.
44 OpenSettings,
45 /// Open Settings on the About page.
46 OpenAbout,
47 /// Run a manual update check and open Settings on the Updates page, where
48 /// its status is rendered.
49 CheckForUpdates,
50 /// Quit the GUI.
51 Quit,
52}
53
54impl DeeplinkCommand {
55 /// The URL scheme OpenLogi registers with LaunchServices.
56 pub const SCHEME: &str = "openlogi";
57
58 /// The wire name for this command — the host component of its URL.
59 #[must_use]
60 pub const fn as_name(self) -> &'static str {
61 match self {
62 Self::Show => "show",
63 Self::OpenSettings => "open-settings",
64 Self::OpenAbout => "open-about",
65 Self::CheckForUpdates => "check-for-updates",
66 Self::Quit => "quit",
67 }
68 }
69
70 /// Build the `openlogi://<name>` URL for this command.
71 #[must_use]
72 pub fn to_url(self) -> String {
73 format!("{}://{}", Self::SCHEME, self.as_name())
74 }
75
76 /// Parse a command from its wire name (the part after `openlogi://`).
77 #[must_use]
78 pub fn from_name(name: &str) -> Option<Self> {
79 match name {
80 "show" => Some(Self::Show),
81 "open-settings" => Some(Self::OpenSettings),
82 "open-about" => Some(Self::OpenAbout),
83 "check-for-updates" => Some(Self::CheckForUpdates),
84 "quit" => Some(Self::Quit),
85 _ => None,
86 }
87 }
88
89 /// Parse a full `openlogi://…` URL. The command lives in the URL's host
90 /// component, so any trailing path or query (`openlogi://show/`,
91 /// `openlogi://show?x=1`) is ignored. Returns `None` for a foreign scheme
92 /// or an unknown command.
93 #[must_use]
94 pub fn parse_url(url: &str) -> Option<Self> {
95 let rest = url.strip_prefix(Self::SCHEME)?.strip_prefix("://")?;
96 let name = rest.split(['/', '?']).next().unwrap_or(rest);
97 Self::from_name(name)
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::DeeplinkCommand;
104
105 const ALL: [DeeplinkCommand; 5] = [
106 DeeplinkCommand::Show,
107 DeeplinkCommand::OpenSettings,
108 DeeplinkCommand::OpenAbout,
109 DeeplinkCommand::CheckForUpdates,
110 DeeplinkCommand::Quit,
111 ];
112
113 #[test]
114 fn url_round_trips() {
115 for cmd in ALL {
116 assert_eq!(DeeplinkCommand::parse_url(&cmd.to_url()), Some(cmd));
117 }
118 }
119
120 #[test]
121 fn parse_url_ignores_trailing_path_and_query() {
122 assert_eq!(
123 DeeplinkCommand::parse_url("openlogi://show/"),
124 Some(DeeplinkCommand::Show)
125 );
126 assert_eq!(
127 DeeplinkCommand::parse_url("openlogi://open-settings?from=tray"),
128 Some(DeeplinkCommand::OpenSettings)
129 );
130 }
131
132 #[test]
133 fn parse_url_rejects_foreign_scheme_and_unknown_command() {
134 assert_eq!(DeeplinkCommand::parse_url("https://example.com/show"), None);
135 assert_eq!(DeeplinkCommand::parse_url("openlogi://bogus"), None);
136 assert_eq!(DeeplinkCommand::parse_url("openlogi://"), None);
137 }
138}