///|
/// Owns one native backend instance and the normalized registration index for it.
///
/// Create a value with `create()`, use its methods to manage global shortcuts,
/// and call `destroy()` when you are finished so the native resources are
/// released promptly.
pub struct GlobalHotkeyManager {
state : GlobalHotkeyState
store : RegistrationStore
}
///|
/// Resolves the backend state unless the manager has already been destroyed.
fn GlobalHotkeyManager::resolve_state(
self : GlobalHotkeyManager,
) -> Result[GlobalHotkeyState, String] {
if self.store.destroyed {
Err(destroyed_error_message())
} else if state_is_null_ffi(self.state) {
Err(create_error_message())
} else {
Ok(self.state)
}
}
///|
/// Converts the support probe into the package's public result shape.
fn ensure_supported_result(
supported : Bool,
error_message : Bytes,
) -> Result[Unit, String] {
if supported {
Ok(())
} else {
Err(decode_error_message(error_message, unsupported_error_message()))
}
}
///|
/// Converts a native create failure into the package's public result shape.
fn create_status_result(
is_null : Bool,
error_message : Bytes,
) -> Result[Unit, String] {
if is_null {
Err(decode_error_message(error_message, create_error_message()))
} else {
Ok(())
}
}
///|
/// Converts a native register status code into the package's public result shape.
fn register_status_result(
status : Int,
error_message : Bytes,
accelerator : String,
) -> Result[Bool, String] {
if status == 0 {
Ok(true)
} else {
Err(
decode_error_message(error_message, register_error_message(accelerator)),
)
}
}
///|
/// Converts a native unregister status code into the package's public result shape.
fn unregister_status_result(
status : Int,
error_message : Bytes,
accelerator : String,
) -> Result[Bool, String] {
if status == 0 {
Ok(true)
} else {
Err(
decode_error_message(error_message, unregister_error_message(accelerator)),
)
}
}
///|
/// Drains repeated trigger reads into one array.
fn drain_with(take : () -> String?) -> Array[String] {
let accelerators : Array[String] = []
while true {
match take() {
Some(accelerator) => accelerators.push(accelerator)
None => break
}
}
accelerators
}
///|
/// Returns `true` when the current native target exposes a usable global hotkey backend.
///
/// This is a lightweight capability probe that does not allocate a manager, so
/// it is safe to call repeatedly during startup. On Linux it checks for an X11
/// session and an X11 backend that can be opened at runtime. On macOS a later
/// `create()` call may still fail until the process has Input Monitoring
/// permission.
///
/// Use this when you only need a boolean answer. If you also want a
/// human-readable failure reason, call `ensure_supported()` instead.
///
/// # Returns
///
/// Returns `true` when this package can open a native backend on the current
/// machine. Returns `false` when the target is unsupported, when Linux lacks an
/// X11 session, or when the runtime backend cannot currently be loaded.
///
/// ```mbt check
/// test "support detection can be queried" {
/// ignore(@global_hotkey.is_supported())
/// }
/// ```
pub fn is_supported() -> Bool {
platform_supported_ffi()
}
///|
/// Verifies that the current native target has a backend this package can use.
///
/// This is useful for fast-fail checks before building a polling loop. Unlike
/// `create()`, it only reports capability and does not retain any native state.
/// When the probe can provide a concrete reason, the returned `Err(String)`
/// contains it; otherwise a generic compatibility message is used.
///
/// Call this when you want an actionable error message for logs or setup flows
/// before attempting real registration work.
///
/// # Returns
///
/// Returns `Ok(())` when a backend is available right now. Returns `Err(String)`
/// with a human-readable reason when the current target cannot provide global
/// hotkeys.
///
/// ```mbt check
/// test "support can be checked without registering anything" {
/// ignore(@global_hotkey.ensure_supported())
/// }
/// ```
pub fn ensure_supported() -> Result[Unit, String] {
ensure_supported_result(is_supported(), last_error_message_ffi())
}
///|
/// Creates a manager that can register, unregister, and poll global hotkeys.
///
/// This allocates the native backend state and initializes the package's
/// normalized registration store. The returned manager owns native resources
/// until `destroy()` is called, so long-lived applications will usually create
/// one manager and reuse it for their polling loop.
///
/// Returns `Err(String)` when the backend cannot be initialized on the current
/// machine, including unsupported platforms, missing Linux X11 support, or
/// missing macOS Input Monitoring permission.
///
/// # Returns
///
/// Returns `Ok(GlobalHotkeyManager)` when the native backend starts
/// successfully. The returned manager is ready to register accelerators, poll
/// for triggered shortcuts, and later release its resources with `destroy()`.
///
/// # Lifecycle
///
/// Each successful call creates an independent manager with its own MoonBit-side
/// registration store. Most applications should create one manager during
/// startup and keep it alive for the duration of their event loop.
///
/// ```mbt check
/// test "manager creation can be attempted safely" {
/// match @global_hotkey.create() {
/// Ok(manager) => manager.destroy()
/// Err(_) => ()
/// }
/// }
/// ```
pub fn create() -> Result[GlobalHotkeyManager, String] {
let state = create_state_ffi()
match
create_status_result(state_is_null_ffi(state), last_error_message_ffi()) {
Err(error) => Err(error)
Ok(_) => Ok({ state, store: new_registration_store() })
}
}
///|
/// Registers one global accelerator like `Ctrl+Shift+K` or `Meta+Space`.
///
/// Supported modifier aliases include `Ctrl` / `Control`, `Alt` / `Option`,
/// and `Meta` / `Cmd` / `Command` / `Win` / `Super`. The accelerator is
/// normalized before registration, so `ctrl + shift + k` is stored as
/// `Ctrl+Shift+K`.
///
/// Every accelerator must contain exactly one non-modifier key. Duplicate
/// modifiers such as `Ctrl+Control+K` and malformed segments such as
/// `Ctrl++K` are rejected before the native backend is called.
///
/// Returns `Ok(true)` when the native backend accepted the registration.
/// Returns `Err(String)` when the accelerator is malformed, the normalized
/// shortcut is already registered by this manager, the manager has already been
/// destroyed, or the native backend rejects the request.
///
/// Successful registrations become visible immediately through `list()`,
/// `take_triggered()`, and `drain_triggered()`.
///
/// # Parameters
///
/// `accelerator` is a user-facing shortcut string such as `Ctrl+Shift+K`,
/// `Meta+Space`, or `Alt+F12`.
///
/// # Returns
///
/// Returns `Ok(true)` when the registration is now active for this manager.
/// Returns `Err(String)` when parsing fails, the normalized shortcut is already
/// present in this manager, the manager has been destroyed, or the OS backend
/// refuses the registration.
///
/// ```mbt check
/// test "register may be attempted on a live manager" {
/// match @global_hotkey.create() {
/// Ok(manager) => {
/// ignore(manager.register("Ctrl+Shift+F24"))
/// ignore(manager.unregister("Ctrl+Shift+F24"))
/// manager.destroy()
/// }
/// Err(_) => ()
/// }
/// }
/// ```
pub fn GlobalHotkeyManager::register(
self : GlobalHotkeyManager,
accelerator : String,
) -> Result[Bool, String] {
match self.resolve_state() {
Err(error) => Err(error)
Ok(state) =>
match self.store.plan_register(accelerator) {
Err(error) => Err(error)
Ok(plan) =>
match
register_status_result(
register_hotkey_ffi(
state,
plan.id,
plan.parsed.modifiers,
@utf8.encode(plan.parsed.key_name),
),
last_error_message_ffi(),
plan.parsed.normalized,
) {
Err(error) => Err(error)
Ok(result) => {
self.store.commit_register(plan)
Ok(result)
}
}
}
}
}
///|
/// Unregisters one previously registered accelerator.
///
/// The accelerator string is normalized with the same rules as `register()`.
/// Callers may therefore use any supported alias or spacing style.
///
/// Returns `Ok(true)` when a registration existed and was removed, `Ok(false)`
/// when nothing matched in this manager, and `Err(String)` when the accelerator
/// is malformed, the manager was destroyed, or the backend refuses the
/// unregistration.
///
/// # Parameters
///
/// `accelerator` may use any supported alias or spacing style. It is normalized
/// before lookup, so `ctrl + k` and `Ctrl+K` refer to the same registration.
///
/// # Returns
///
/// Returns `Ok(true)` when a matching registration existed and has been removed.
/// Returns `Ok(false)` when the manager was alive but no normalized shortcut
/// matched. Returns `Err(String)` when parsing fails, the manager has already
/// been destroyed, or the native backend reports an unregistration error.
pub fn GlobalHotkeyManager::unregister(
self : GlobalHotkeyManager,
accelerator : String,
) -> Result[Bool, String] {
match self.resolve_state() {
Err(error) => Err(error)
Ok(state) =>
match self.store.plan_unregister(accelerator) {
Err(error) => Err(error)
Ok(None) => Ok(false)
Ok(Some(plan)) =>
match
unregister_status_result(
unregister_hotkey_ffi(state, plan.id),
last_error_message_ffi(),
plan.normalized,
) {
Err(error) => Err(error)
Ok(result) => {
self.store.commit_unregister(plan)
Ok(result)
}
}
}
}
}
///|
/// Returns a snapshot of every accelerator currently registered in this manager.
///
/// Each entry uses the canonical normalized form produced by `register()`. This
/// only reads the MoonBit-side registry, so it does not block on the native
/// backend and remains available even when no triggers are pending.
///
/// # Returns
///
/// Returns an array of normalized accelerators in registration order. The
/// returned array is a snapshot, so later `register()` or `unregister()` calls
/// do not mutate previously returned arrays.
pub fn GlobalHotkeyManager::list(self : GlobalHotkeyManager) -> Array[String] {
self.store.list()
}
///|
/// Pops the next triggered accelerator from the native queue, if any.
///
/// This call never blocks. It returns `Some(String)` for the next queued
/// trigger in normalized form and `None` when the queue is empty or when the
/// manager has already been destroyed.
///
/// Poll this method from your application's main loop when you want to react to
/// global shortcuts one event at a time.
///
/// # Returns
///
/// Returns `Some(String)` with the next queued normalized accelerator when one
/// is available. Returns `None` when no trigger is pending or when polling is
/// no longer possible because the manager has been destroyed.
pub fn GlobalHotkeyManager::take_triggered(
self : GlobalHotkeyManager,
) -> String? {
match self.resolve_state() {
Err(_) => None
Ok(state) => self.store.lookup_trigger(take_triggered_id_ffi(state))
}
}
///|
/// Drains every currently queued trigger in the order reported by the backend.
///
/// This is a convenience wrapper over repeated `take_triggered()` calls and is
/// useful when you poll infrequently or want to batch-handle pending shortcuts.
/// It returns an empty array when nothing is queued, and it also becomes empty
/// after `destroy()` because polling is no longer possible.
///
/// # Returns
///
/// Returns an array containing every currently queued normalized accelerator in
/// FIFO order. The queue is empty after this call returns, because all pending
/// trigger ids have been consumed.
pub fn GlobalHotkeyManager::drain_triggered(
self : GlobalHotkeyManager,
) -> Array[String] {
drain_with(fn() { self.take_triggered() })
}
///|
/// Releases every native resource owned by this manager and clears its registry.
///
/// Calling `destroy()` more than once is harmless. After destruction,
/// `register()` and `unregister()` return `Err(String)`, `take_triggered()`
/// returns `None`, and `drain_triggered()` returns `[]`.
///
/// # Lifecycle
///
/// Call this once you no longer need the manager, typically during application
/// shutdown. After destruction, the value may still exist as a MoonBit object,
/// but it no longer owns a usable native backend.
pub fn GlobalHotkeyManager::destroy(self : GlobalHotkeyManager) -> Unit {
if !self.store.destroyed {
destroy_state_ffi(self.state)
self.store.destroy()
}
}