mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 03:25:50 +00:00
macos: implement notify on command finish
This commit is contained in:
committed by
Mitchell Hashimoto
parent
9d6a8d0fc1
commit
3b5a7b77d3
@@ -632,9 +632,13 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_SEARCH_SELECTED:
|
||||
searchSelected(app, target: target, v: action.action.search_selected)
|
||||
|
||||
case GHOSTTY_ACTION_COMMAND_FINISHED:
|
||||
commandFinished(app, target: target, v: action.action.command_finished)
|
||||
|
||||
case GHOSTTY_ACTION_PRESENT_TERMINAL:
|
||||
return presentTerminal(app, target: target)
|
||||
|
||||
|
||||
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
||||
fallthrough
|
||||
case GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS:
|
||||
@@ -1353,7 +1357,7 @@ extension Ghostty {
|
||||
n: ghostty_action_desktop_notification_s) {
|
||||
switch target.tag {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("toggle split zoom does nothing with an app target")
|
||||
Ghostty.logger.warning("desktop notification does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
@@ -1361,17 +1365,82 @@ extension Ghostty {
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
guard let title = String(cString: n.title!, encoding: .utf8) else { return }
|
||||
guard let body = String(cString: n.body!, encoding: .utf8) else { return }
|
||||
showDesktopNotification(surfaceView, title: title, body: body)
|
||||
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.alert, .sound]) { _, error in
|
||||
if let error = error {
|
||||
Ghostty.logger.error("Error while requesting notification authorization: \(error)")
|
||||
}
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func showDesktopNotification(
|
||||
_ surfaceView: SurfaceView,
|
||||
title: String,
|
||||
body: String) {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.alert, .sound]) { _, error in
|
||||
if let error = error {
|
||||
Ghostty.logger.error("Error while requesting notification authorization: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
center.getNotificationSettings { settings in
|
||||
guard settings.authorizationStatus == .authorized else { return }
|
||||
surfaceView.showUserNotification(title: title, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
private static func commandFinished(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_command_finished_s
|
||||
) {
|
||||
switch target.tag {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("command finished does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
|
||||
guard let appState = (NSApplication.shared.delegate as? AppDelegate)?.ghostty else { return }
|
||||
let config = appState.config
|
||||
|
||||
let mode = config.notifyOnCommandFinish
|
||||
if mode == .never { return }
|
||||
if mode == .unfocused && surfaceView.focused { return }
|
||||
|
||||
let durationMs = v.duration / 1_000_000
|
||||
if durationMs < config.notifyOnCommandFinishAfter { return }
|
||||
|
||||
let actions = config.notifyOnCommandFinishAction
|
||||
|
||||
if actions.contains(.bell) {
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyBellDidRing,
|
||||
object: surfaceView
|
||||
)
|
||||
}
|
||||
|
||||
center.getNotificationSettings { settings in
|
||||
guard settings.authorizationStatus == .authorized else { return }
|
||||
surfaceView.showUserNotification(title: title, body: body)
|
||||
if actions.contains(.notify) {
|
||||
let title: String
|
||||
if v.exit_code < 0 {
|
||||
title = "Command Finished"
|
||||
} else if v.exit_code == 0 {
|
||||
title = "Command Succeeded"
|
||||
} else {
|
||||
title = "Command Failed"
|
||||
}
|
||||
|
||||
let body: String
|
||||
let formattedDuration = Self.formatDuration(ns: v.duration)
|
||||
if v.exit_code < 0 {
|
||||
body = "Command took \(formattedDuration)."
|
||||
} else {
|
||||
body = "Command took \(formattedDuration) and exited with code \(v.exit_code)."
|
||||
}
|
||||
|
||||
showDesktopNotification(surfaceView, title: title, body: body)
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -1379,6 +1448,26 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func formatDuration(ns: UInt64) -> String {
|
||||
let totalSeconds = ns / 1_000_000_000
|
||||
let ms = (ns / 1_000_000) % 1000
|
||||
|
||||
if totalSeconds == 0 {
|
||||
return "\(ms)ms"
|
||||
}
|
||||
|
||||
let seconds = totalSeconds % 60
|
||||
let minutes = (totalSeconds / 60) % 60
|
||||
let hours = totalSeconds / 3600
|
||||
|
||||
var parts: [String] = []
|
||||
if hours > 0 { parts.append("\(hours)h") }
|
||||
if minutes > 0 { parts.append("\(minutes)m") }
|
||||
if seconds > 0 || (hours == 0 && minutes == 0) { parts.append("\(seconds)s") }
|
||||
|
||||
return parts.joined(separator: " ")
|
||||
}
|
||||
|
||||
private static func toggleFloatWindow(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
||||
@@ -134,6 +134,31 @@ extension Ghostty {
|
||||
return .init(rawValue: v)
|
||||
}
|
||||
|
||||
var notifyOnCommandFinish: NotifyOnCommandFinish {
|
||||
guard let config = self.config else { return .never }
|
||||
var v: UnsafePointer<Int8>?
|
||||
let key = "notify-on-command-finish"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .never }
|
||||
guard let ptr = v else { return .never }
|
||||
return NotifyOnCommandFinish(rawValue: String(cString: ptr)) ?? .never
|
||||
}
|
||||
|
||||
var notifyOnCommandFinishAction: NotifyOnCommandFinishAction {
|
||||
guard let config = self.config else { return .bell }
|
||||
var v: CUnsignedInt = 0
|
||||
let key = "notify-on-command-finish-action"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .bell }
|
||||
return .init(rawValue: v)
|
||||
}
|
||||
|
||||
var notifyOnCommandFinishAfter: UInt {
|
||||
guard let config = self.config else { return 5000 }
|
||||
var v: UInt = 0
|
||||
let key = "notify-on-command-finish-after"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
|
||||
return v
|
||||
}
|
||||
|
||||
var splitPreserveZoom: SplitPreserveZoom {
|
||||
guard let config = self.config else { return .init() }
|
||||
var v: CUnsignedInt = 0
|
||||
@@ -842,4 +867,17 @@ extension Ghostty.Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NotifyOnCommandFinish: String {
|
||||
case never
|
||||
case unfocused
|
||||
case always
|
||||
}
|
||||
|
||||
struct NotifyOnCommandFinishAction: OptionSet {
|
||||
let rawValue: CUnsignedInt
|
||||
|
||||
static let bell = NotifyOnCommandFinishAction(rawValue: 1 << 0)
|
||||
static let notify = NotifyOnCommandFinishAction(rawValue: 1 << 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1207,8 +1207,6 @@ command: ?Command = null,
|
||||
/// notifications for a single command, overriding the `never` and `unfocused`
|
||||
/// options.
|
||||
///
|
||||
/// GTK only.
|
||||
///
|
||||
/// Available since 1.3.0.
|
||||
@"notify-on-command-finish": NotifyOnCommandFinish = .never,
|
||||
|
||||
@@ -1223,8 +1221,6 @@ command: ?Command = null,
|
||||
/// Options can be combined by listing them as a comma separated list. Options
|
||||
/// can be negated by prefixing them with `no-`. For example `no-bell,notify`.
|
||||
///
|
||||
/// GTK only.
|
||||
///
|
||||
/// Available since 1.3.0.
|
||||
@"notify-on-command-finish-action": NotifyOnCommandFinishAction = .{
|
||||
.bell = true,
|
||||
@@ -1262,8 +1258,6 @@ command: ?Command = null,
|
||||
/// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any
|
||||
/// value larger than this will be clamped to the maximum value.
|
||||
///
|
||||
/// GTK only.
|
||||
///
|
||||
/// Available since 1.3.0
|
||||
@"notify-on-command-finish-after": Duration = .{ .duration = 5 * std.time.ns_per_s },
|
||||
|
||||
|
||||
Reference in New Issue
Block a user