#' Send a CLI beep / desktop notification #' #' Plays an audible beep in the terminal and/or sends a desktop notification #' when a long-running R script completes or reaches a milestone. #' #' @param msg Character. Optional message to include in the notification. #' When `type = "notify"`, this becomes the notification body. #' @param type Character. Notification method: #' - `"beep"` (default): emit a terminal bell character (`\007`). #' Works in any terminal that supports the bell. #' - `"notify"`: send a desktop notification via `notify-send` (Linux) or #' `osascript` (macOS). Falls back to `"beep"` if neither tool is found. #' - `"webhook"`: POST a JSON payload to a URL. Requires `url` argument. #' - `"all"`: play beep + send desktop notification (webhook only if `url` #' is provided). #' @param url Character. Webhook URL for `type = "webhook"` or `"all"`. #' A JSON payload is POSTed with keys `message`, `status`, and `timestamp`. #' @param status Character. Status label for the notification (default `"done"`). #' Used in the notification title and webhook payload. #' @param timeout Numeric. Seconds to wait for the webhook POST to complete #' (default `5`). Ignored for non-webhook types. #' @param quiet Logical. If `TRUE`, suppress the terminal beep even when #' `type` includes `"beep"`. Useful for silent background runs. #' #' @return Invisible `NULL`. #' #' @section Requirements: #' - `type = "notify"` requires `notify-send` (Linux) or `osascript` (macOS). #' - `type = "webhook"` requires network access to the provided URL. #' #' @section Examples: #' \preformatted{ #' # Simple terminal beep #' beep() #' #' # Desktop notification with message #' beep("Analysis complete!", type = "notify") #' #' # Send to a webhook (e.g., Slack, Discord, custom endpoint) #' beep("Job finished", type = "webhook", #' url = "https://hooks.slack.com/services/...") #' #' # Beep + desktop notification #' beep("Processing done", type = "all") #' } #' #' @export #' beep <- function(msg = "done", type = c("beep", "notify", "webhook", "all"), url = NULL, status = "done", timeout = 5, quiet = FALSE) { type <- match.arg(type) # -- Terminal beep ---------------------------------------------------------- if (!quiet && grepl("beep", type)) { cat("\007") flush.console() } # -- Desktop notification --------------------------------------------------- if (grepl("notify", type)) { .send_desktop_notify(msg, status) } # -- Webhook ---------------------------------------------------------------- if (grepl("webhook", type) && !is.null(url)) { .send_webhook(url, msg, status, timeout) } invisible(NULL) } # -- Internal helpers --------------------------------------------------------- #' Send a desktop notification via notify-send or osascript #' #' @param msg Message body #' @param status Status label for the title #' @keywords internal .send_desktop_notify <- function(msg, status) { # Linux: notify-send if (.has_command("notify-send")) { cmd <- paste0("notify-send '", status, "' '", gsub("'", "'\\''", msg), "'") system(cmd, ignore.stdout = TRUE, ignore.stderr = TRUE) return(invisible(NULL)) } # macOS: osascript if (.has_command("osascript")) { escaped_msg <- gsub('"', '\\"', msg) escaped_status <- gsub('"', '\\"', status) cmd <- paste0( "osascript -e 'display notification \"", escaped_msg, "\" with title \"", escaped_status, "\"'" ) system(cmd, ignore.stdout = TRUE, ignore.stderr = TRUE) return(invisible(NULL)) } # Neither tool available — silently skip invisible(NULL) } #' POST a JSON payload to a webhook URL #' #' @param url Webhook URL #' @param msg Message body #' @param status Status label #' @param timeout Seconds to wait for the request #' @keywords internal .send_webhook <- function(url, msg, status, timeout) { payload <- jsonlite::toJSON(list( message = msg, status = status, timestamp = format(Sys.time(), "%Y-%m-%dT%H:%M:%S%z") ), auto_unbox = TRUE) # Use curl via system() for maximum compatibility (no extra R deps) if (.has_command("curl")) { cmd <- paste0( "curl -s -X POST -H 'Content-Type: application/json' ", "-d '", payload, "' '", url, "' ", "--max-time ", timeout ) system(cmd, ignore.stdout = TRUE, ignore.stderr = TRUE) } else if (.has_command("wget")) { cmd <- paste0( "wget -q -O /dev/null --post-data='", payload, "' --header='Content-Type: application/json' ", "--timeout=", timeout, " '", url, "'" ) system(cmd, ignore.stdout = TRUE, ignore.stderr = TRUE) } invisible(NULL) } #' Check if a command exists on the system PATH #' #' @param cmd Command name #' @return Logical #' @keywords internal .has_command <- function(cmd) { Sys.which(cmd) != "" }