Module: TerminalNotifier
- Defined in:
- lib/terminal-notifier.rb,
lib/terminal-notifier/cli.rb,
lib/terminal-notifier/version.rb
Overview
Sends macOS User Notifications by shelling out to the bundled terminal-notifier application.
See TerminalNotifier.notify, .remove and .list for the public API.
Defined Under Namespace
Modules: CLI Classes: BinaryNotFoundError, UnsupportedPlatformError
Constant Summary collapse
- MINIMUM_MACOS_VERSION =
Minimum macOS version supported by the bundled binary. UNUserNotificationCenter, which the tool is built on, is not available before this.
'10.14'- TIMEOUT_EXIT_CODE =
Exit code the binary uses when -timeout expires waiting for -action or -reply. The notification was delivered correctly and the tool reported @TIMEOUT, so this is an outcome to pass on rather than a failure.
6- BIN_PATH =
Path to the binary inside the vendored application bundle. Overridable so that a Homebrew-installed or self-built copy can be used instead.
ENV.fetch( 'TERMINAL_NOTIFIER_BIN', File.( '../vendor/terminal-notifier/terminal-notifier.app/Contents/MacOS/terminal-notifier', __dir__ ) )
- LIST_FIELDS =
%i[group title subtitle message delivered_at].freeze
- VERSION =
The version of this gem, which tracks the version of the bundled terminal-notifier binary.
Note: this is not TerminalNotifier.version, which returns the macOS product version the tool is running on.
'3.0.0'
Class Method Summary collapse
-
.available? ⇒ Boolean
Returns whether the current platform is macOS 10.14 or higher.
-
.command_for(options) ⇒ Object
Builds the argument vector for the binary.
-
.ensure_usable! ⇒ Object
Raises unless this platform and installation can actually run the binary.
-
.escape_value(value) ⇒ Object
Prepares a single option value for the binary.
-
.execute(verbose, options) ⇒ Object
Runs the binary and returns its standard output.
-
.last_status ⇒ Object
The exit status of the most recent execute call on this thread.
-
.list(group = 'ALL', verbose = false) ⇒ Object
If a ‘group’ ID is given, and a notification for that group exists, returns a hash with details about the notification.
- .macos? ⇒ Boolean
-
.notify(message, options = {}, verbose = false, always_string = false) ⇒ Object
Sends a User Notification and returns whether or not it was a success.
-
.notify_result(result, options, always_string = false) ⇒ Object
Cleans up the result of a notification, making it easier to work it.
-
.parse_list_row(line) ⇒ Object
Turns one tab-separated -list row into a hash.
- .parse_time(value) ⇒ Object
-
.presence(value) ⇒ Object
3.0.0 emits an empty field for an absent value; earlier binaries emitted the literal string "(null)".
-
.remove(group = 'ALL', verbose = false) ⇒ Object
Removes a notification that was previously sent with the specified ‘group’ ID, if one exists.
-
.reported? ⇒ Boolean
True when the binary ran and reported an outcome, including a timeout.
- .succeeded? ⇒ Boolean
-
.version ⇒ Object
The macOS product version, or nil when not running on macOS.
Class Method Details
.available? ⇒ Boolean
Returns whether the current platform is macOS 10.14 or higher.
37 38 39 40 41 42 43 |
# File 'lib/terminal-notifier.rb', line 37 def self.available? # Memoise with defined? rather than ||=, so that a false result is cached # too and we don't re-shell out on every single call. return @available if defined?(@available) @available = macos? && Gem::Version.new(version) >= Gem::Version.new(MINIMUM_MACOS_VERSION) end |
.command_for(options) ⇒ Object
Builds the argument vector for the binary.
A value of true becomes a bare flag and false omits the option entirely,
so switches like :reply and :ignoreDnD don't end up with a stray "true" as
their value (which -reply would show as its placeholder text).
66 67 68 69 70 71 72 73 74 75 |
# File 'lib/terminal-notifier.rb', line 66 def self.command_for() arguments = .flat_map do |key, value| case value when true then ["-#{key}"] when false, nil then [] else ["-#{key}", escape_value(value.to_s)] end end [BIN_PATH, *arguments] end |
.ensure_usable! ⇒ Object
Raises unless this platform and installation can actually run the binary.
114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/terminal-notifier.rb', line 114 def self.ensure_usable! unless available? raise UnsupportedPlatformError, "terminal-notifier is only supported on macOS #{MINIMUM_MACOS_VERSION} or higher." end return if File.executable?(BIN_PATH) raise BinaryNotFoundError, "terminal-notifier binary not found at #{BIN_PATH}. " \ 'Set TERMINAL_NOTIFIER_BIN to point at it.' end |
.escape_value(value) ⇒ Object
Prepares a single option value for the binary.
This is not shell escaping — IO.popen is handed an argv array, so no shell is ever involved. It is the escaping the tool's own README documents as ‘[’: the binary reads its arguments through NSUserDefaults, which tries to parse each value as a property list, so a value beginning with '[', '(', '{', a quote, a space and so on comes back as nil and the notification is never sent. Prefixing a backslash makes NSUserDefaults treat it as a plain string, and the binary strips that backslash again (see objectForKeyedSubscript: in AppDelegate.m).
Shellwords.escape is used purely as a convenient test of "would this character be taken as syntax", which is the same set that matters here.
90 91 92 93 94 95 96 97 |
# File 'lib/terminal-notifier.rb', line 90 def self.escape_value(value) return value if value.empty? # A value starting with '-' would be read as the next flag; the binary # strips this leading space back off. prefix = value.start_with?('-') ? ' ' : '' "#{prefix}#{Shellwords.escape(value[0, 1])}#{value[1..]}" end |
.execute(verbose, options) ⇒ Object
Runs the binary and returns its standard output.
Raises UnsupportedPlatformError when not on a supported macOS, and BinaryNotFoundError when the vendored binary is missing.
103 104 105 106 107 108 109 110 111 |
# File 'lib/terminal-notifier.rb', line 103 def self.execute(verbose, ) ensure_usable! output = IO.popen(command_for(), &:read) # Remember the status so callers aren't exposed to races on the global $?. Thread.current[:terminal_notifier_last_status] = $? $stdout.print output if verbose output end |
.last_status ⇒ Object
The exit status of the most recent execute call on this thread.
128 129 130 |
# File 'lib/terminal-notifier.rb', line 128 def self.last_status Thread.current[:terminal_notifier_last_status] end |
.list(group = 'ALL', verbose = false) ⇒ Object
If a ‘group’ ID is given, and a notification for that group exists, returns a hash with details about the notification.
If no ‘group’ ID is given, an array of hashes describing all notifications.
If no information is available this will return nil.
231 232 233 234 235 236 237 238 239 |
# File 'lib/terminal-notifier.rb', line 231 def list(group = 'ALL', verbose = false) output = TerminalNotifier.execute(verbose, list: group) return if output.nil? || output.strip.empty? # Drop the header row. notifications = output.split("\n")[1..].to_a.map { |line| TerminalNotifier.parse_list_row(line) } group == 'ALL' ? notifications : notifications.first end |
.macos? ⇒ Boolean
55 56 57 58 59 |
# File 'lib/terminal-notifier.rb', line 55 def self.macos? return @macos if defined?(@macos) @macos = !(RbConfig::CONFIG['host_os'] =~ /darwin|mac os/).nil? end |
.notify(message, options = {}, verbose = false, always_string = false) ⇒ Object
Sends a User Notification and returns whether or not it was a success.
The available options are :title, :subtitle, :group, :activate,
:open, :execute, :contentImage, and :sound. For a description of
each option see:
https://github.com/julienXX/terminal-notifier/blob/master/README.markdown
Examples are:
TerminalNotifier.notify('Hello World')
TerminalNotifier.notify('Hello World', :title => 'Ruby')
TerminalNotifier.notify('Hello World', :group => Process.pid)
TerminalNotifier.notify('Hello World', :activate => 'com.apple.Safari')
TerminalNotifier.notify('Hello World', :open => 'http://twitter.com/julienXX')
TerminalNotifier.notify('Hello World', :execute => 'say "OMG"')
TerminalNotifier.notify('Hello World', :sound => 'default')
Raises UnsupportedPlatformError if not supported on the current platform.
202 203 204 205 206 207 |
# File 'lib/terminal-notifier.rb', line 202 def notify(, = {}, verbose = false, always_string = false) result = TerminalNotifier.execute(verbose, .merge(message: )) # A timeout counts as a reported outcome: without this it would come back as # a bare false, indistinguishable from the notification failing to send. TerminalNotifier.reported? && notify_result(result, , always_string) end |
.notify_result(result, options, always_string = false) ⇒ Object
Cleans up the result of a notification, making it easier to work it
The result of a notification is downcased, then groups of 1 or more non-word characters are replaced with an underscore, before being symbolised.
If the reply option was given, then instead of going through the above process, the result is returned with no changes as a string.
If the always_string param is set to true, a the result is returned with no changes as a string, like above.
Examples are:
notify_result('Test', {}) #=> :test notify_result('No, sir', {}) #=> :no_sir notify_result('@timeout', {}) #=> :_timeout notify_result('@closeaction', {}) #=> :_closeaction notify_result('I like pie', true) #=> 'I like pie' notify_result('I do not like pie', => true) #=> 'I do not like pie' notify_result('@timeout', => true) #=> '@timeout' notify_result('I may like pie', {}) #=> :i_may_like_pie
167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/terminal-notifier.rb', line 167 def notify_result(result, , always_string = false) # The binary terminates its response with a newline. Left on, a reply comes # back as "text\n" and an action title symbolises to :title_ because the # newline is a non-word character. result = result.to_s.chomp if [:reply] || ['reply'] || always_string result else # NOTE: an empty result yields `true`, not a symbol, because the `||` # short-circuits. Odd, but it is the long-standing public contract. result.empty? || result.downcase.gsub(/\W+/, '_').to_sym end end |
.parse_list_row(line) ⇒ Object
Turns one tab-separated -list row into a hash.
243 244 245 246 247 |
# File 'lib/terminal-notifier.rb', line 243 def self.parse_list_row(line) LIST_FIELDS.zip(line.split("\t")).each_with_object({}) do |(key, value), hash| hash[key] = key == :delivered_at ? parse_time(value) : presence(value) end end |
.parse_time(value) ⇒ Object
249 250 251 252 253 |
# File 'lib/terminal-notifier.rb', line 249 def self.parse_time(value) Time.parse(value.to_s) rescue ArgumentError, TypeError nil end |
.presence(value) ⇒ Object
3.0.0 emits an empty field for an absent value; earlier binaries emitted the literal string "(null)".
257 258 259 |
# File 'lib/terminal-notifier.rb', line 257 def self.presence(value) value unless value.nil? || value.empty? || value == '(null)' end |
.remove(group = 'ALL', verbose = false) ⇒ Object
Removes a notification that was previously sent with the specified ‘group’ ID, if one exists.
If no ‘group’ ID is given, all notifications are removed.
rubocop:disable Naming/PredicateMethod -- remove is the public API name
215 216 217 218 |
# File 'lib/terminal-notifier.rb', line 215 def remove(group = 'ALL', verbose = false) TerminalNotifier.execute(verbose, remove: group) TerminalNotifier.succeeded? end |
.reported? ⇒ Boolean
True when the binary ran and reported an outcome, including a timeout.
138 139 140 141 142 143 |
# File 'lib/terminal-notifier.rb', line 138 def self.reported? status = last_status return false if status.nil? status.success? || status.exitstatus == TIMEOUT_EXIT_CODE end |
.succeeded? ⇒ Boolean
132 133 134 135 |
# File 'lib/terminal-notifier.rb', line 132 def self.succeeded? status = last_status !status.nil? && status.success? end |
.version ⇒ Object
The macOS product version, or nil when not running on macOS.
46 47 48 49 50 51 52 53 |
# File 'lib/terminal-notifier.rb', line 46 def self.version return @version if defined?(@version) # Only shell out when we already know this is macOS. RbConfig reports the # Darwin kernel version, which doesn't map cleanly onto product versions, # so sw_vers is still needed for the number itself. @version = macos? ? `sw_vers -productVersion`.strip : nil end |