├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.markdown ├── Ruby ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.markdown ├── Rakefile ├── bin │ └── terminal-notifier ├── lib │ └── terminal-notifier.rb ├── spec │ └── terminal-notifier_spec.rb └── terminal-notifier.gemspec ├── Terminal Notifier.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── Terminal Notifier.xccheckout └── xcshareddata │ └── xcschemes │ └── Terminal Notifier.xcscheme ├── Terminal Notifier ├── AppDelegate.h ├── AppDelegate.m ├── Terminal Notifier-Info.plist ├── Terminal Notifier-Prefix.pch ├── en.lproj │ ├── Credits.rtf │ ├── InfoPlist.strings │ └── MainMenu.xib └── main.m ├── Terminal.icns └── assets ├── Example_1.png ├── Example_2.png ├── Example_3.png ├── Example_4.png ├── Example_5.png └── System_prefs.png /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Please check everything that applies to your issue: 2 | - [ ] I looked in closed issues and it has not already been answered 3 | - [ ] My issue appeared with a terminal-notifier update 4 | - [ ] I'm using a tool that uses terminal-notifier (guard-notifier, node-notifier...) 5 | 6 | # To help us debug your issue please include: 7 | - the macOS version you use 8 | - terminal-notifier version 9 | - how did you install terminal-notifier (binary download, homebrew, ruby gem...)? 10 | - step-by-step reproduction instructions 11 | 12 | # Common issues and solution: 13 | - I'm using tmux -> see #115 14 | - I'm using iterm2 -> see #147 15 | - I want to change the icon -> see https://github.com/julienXX/terminal-notifier/issues/197#issuecomment-301305576 16 | - I want sticky notifications or action buttons -> please use [alerter](https://github.com/vjeantet/alerter) 17 | 18 | Thanks! 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw? 2 | .DS_Store 3 | DerivedData 4 | xcuserdata 5 | Ruby/*.zip 6 | Ruby/*.gem 7 | Ruby/vendor 8 | build 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at julien@sideburns.eu. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | I like to encourage you to contribute to the repository. 4 | This should be as easy as possible for you but there are a few things to consider when contributing. 5 | The following guidelines for contribution should be followed if you want to submit a pull request. 6 | 7 | ## How to prepare 8 | 9 | * You need a [GitHub account](https://github.com/signup/free) 10 | * Submit an [issue ticket](https://github.com/julienXX/terminal-notifier/issues) for your issue if there is no one yet. 11 | * Describe the issue and include steps to reproduce if it's a bug. 12 | * Ensure to mention the earliest version that you know is affected. 13 | * If you are able and want to fix this, fork the repository on GitHub 14 | 15 | ## Make Changes 16 | 17 | * In your forked repository, create a topic branch for your upcoming patch. (e.g. `feature--autoplay` or `bugfix--ios-crash`) 18 | * Usually this is based on the master branch. 19 | * Create a branch based on master; `git branch 20 | fix/master/my_contribution master` then checkout the new branch with `git 21 | checkout fix/master/my_contribution`. Please avoid working directly on the `master` branch. 22 | * Make sure you stick to the coding style that is used already. 23 | * Make commits of logical units and describe them properly. 24 | * Check for unnecessary whitespace with `git diff --check` before committing. 25 | 26 | * If possible, submit tests to your patch / new feature so it can be tested easily. 27 | * Assure nothing is broken by running all the tests. 28 | 29 | ## Submit Changes 30 | 31 | * Push your changes to a topic branch in your fork of the repository. 32 | * Open a pull request to the original repository and choose the right original branch you want to patch. 33 | _Advanced users may install the `hub` gem and use the [`hub pull-request` command](https://hub.github.com/hub.1.html)._ 34 | * If not done in commit messages (which you really should do) please reference and update your issue with the code changes. But _please do not close the issue yourself_. 35 | _Notice: You can [turn your previously filed issues into a pull-request here](http://issue2pr.herokuapp.com/)._ 36 | * Even if you have write access to the repository, do not directly push or merge pull-requests. Let another team member review your pull request and approve. 37 | 38 | # Additional Resources 39 | 40 | * [General GitHub documentation](http://help.github.com/) 41 | * [GitHub pull request documentation](https://help.github.com/articles/about-pull-requests/) 42 | * [Read the Issue Guidelines by @necolas](https://github.com/necolas/issue-guidelines/blob/master/CONTRIBUTING.md) for more details 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All the works are available under the MIT license. Except for ‘Terminal.icns’, which is a copy of Apple’s Terminal.app icon and as such is copyright of Apple. 2 | 3 | Copyright (C) 2012-2016 Eloy Durán eloy.de.enige@gmail.com, Julien Blanchard julien@sideburns.eu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # terminal-notifier 2 | 3 | [![GitHub release](https://img.shields.io/github/release/julienXX/terminal-notifier.svg)](https://github.com/julienXX/terminal-notifier/releases) 4 | 5 | terminal-notifier is a command-line tool to send macOS User Notifications, 6 | which are available on macOS 10.10 and higher. 7 | 8 | 9 | ## News 10 | 11 | [alerter](https://github.com/vjeantet/alerter) features were merged in terminal-notifier 1.7. This led to some issues and even more issues in the 1.8 release. We decided with [Valère Jeantet](https://github.com/vjeantet) to rollback this merge. 12 | 13 | From now on terminal-notifier won't have the sticky notification feature nor the actions buttons. If you need them please use [alerter](https://github.com/vjeantet/alerter). I also want to follow [semver](http://semver.org) hence this latest version starts at 2.0.0. 14 | 15 | Sticking to two smaller specialized tools will hopefully make them easier to maintain and less error prone. 16 | 17 | 18 | ## Caveats 19 | 20 | * It is currently packaged as an application bundle, because `NSUserNotification` 21 | does not work from a ‘Foundation tool’. [radar://11956694](radar://11956694) 22 | 23 | * If you intend to package terminal-notifier with your app to distribute it on the Mac App Store, please use 1.5.2; version 1.6.0+ uses a private method override, which is not allowed in the App Store Guidelines. 24 | 25 | * If you're using macOS < 10.10 you should use terminal-notifier 1.6.3. 26 | 27 | * If you're looking for sticky notifications or more actions on a notification please use [alerter](https://github.com/vjeantet/alerter) 28 | 29 | ## Download 30 | 31 | Prebuilt binaries are available from the 32 | [releases section](https://github.com/julienXX/terminal-notifier/releases). 33 | 34 | Or if you want to use this from 35 | [Ruby](https://github.com/julienXX/terminal-notifier/tree/master/Ruby), you can 36 | install it through RubyGems: 37 | 38 | ``` 39 | $ [sudo] gem install terminal-notifier 40 | ``` 41 | 42 | You can also install it via [Homebrew](https://github.com/mxcl/homebrew): 43 | ``` 44 | $ brew install terminal-notifier 45 | ``` 46 | 47 | ## Usage 48 | 49 | ``` 50 | $ ./terminal-notifier.app/Contents/MacOS/terminal-notifier -[message|group|list] [VALUE|ID|ID] [options] 51 | ``` 52 | 53 | In order to use terminal-notifier, you have to call the binary _inside_ the 54 | application bundle. 55 | 56 | The Ruby gem, which wraps this tool, _does_ have a bin wrapper. If installed 57 | you can simply do: 58 | 59 | ``` 60 | $ terminal-notifier -[message|group|list] [VALUE|ID|ID] [options] 61 | ``` 62 | 63 | This will obviously be a bit slower than using the tool without the wrapper. 64 | 65 | If you'd like notifications to stay on the screen until dismissed, go to System Preferences -> Notifications -> terminal-notifier and change the style from Banners to Alerts. You cannot do this on a per-notification basis. 66 | 67 | 68 | ### Example Uses 69 | 70 | Display piped data with a sound: 71 | ``` 72 | $ echo 'Piped Message Data!' | terminal-notifier -sound default 73 | ``` 74 | 75 | ![Example 1](assets/Example_1.png) 76 | 77 | Use a custom icon: 78 | ``` 79 | $ terminal-notifier -title ProjectX -subtitle "new tag detected" -message "Finished" -appIcon http://vjeantet.fr/images/logo.png 80 | ``` 81 | 82 | ![Example 3](assets/Example_3.png) 83 | 84 | Open an URL when the notification is clicked: 85 | ``` 86 | $ terminal-notifier -title '💰' -message 'Check your Apple stock!' -open 'http://finance.yahoo.com/q?s=AAPL' 87 | ``` 88 | 89 | ![Example 4](assets/Example_4.png) 90 | 91 | Open an app when the notification is clicked: 92 | ``` 93 | $ terminal-notifier -group 'address-book-sync' -title 'Address Book Sync' -subtitle 'Finished' -message 'Imported 42 contacts.' -activate 'com.apple.AddressBook' 94 | ``` 95 | 96 | ![Example 5](assets/Example_5.png) 97 | 98 | 99 | ### Options 100 | 101 | At a minimum, you must specify either the `-message` , the `-remove`, or the 102 | `-list` option. 103 | 104 | ------------------------------------------------------------------------------- 105 | 106 | `-message VALUE` **[required]** 107 | 108 | The message body of the notification. 109 | 110 | If you pipe data into terminal-notifier, you can omit this option, 111 | and the piped data will become the message body instead. 112 | 113 | ------------------------------------------------------------------------------- 114 | 115 | `-title VALUE` 116 | 117 | The title of the notification. This defaults to ‘Terminal’. 118 | 119 | ------------------------------------------------------------------------------- 120 | 121 | `-subtitle VALUE` 122 | 123 | The subtitle of the notification. 124 | 125 | ------------------------------------------------------------------------------- 126 | 127 | `-sound NAME` 128 | 129 | Play the `NAME` sound when the notification appears. 130 | Sound names are listed in `/System/Library/Sounds`. 131 | 132 | Use the special `NAME` “default” for the default notification sound. 133 | 134 | ------------------------------------------------------------------------------- 135 | 136 | `-group ID` 137 | 138 | Specifies the notification’s ‘group’. For any ‘group’, only _one_ 139 | notification will ever be shown, replacing previously posted notifications. 140 | 141 | A notification can be explicitly removed with the `-remove` option (see 142 | below). 143 | 144 | Example group IDs: 145 | 146 | * The sender’s name (to scope the notifications by tool). 147 | * The sender’s process ID (to scope the notifications by a unique process). 148 | * The current working directory (to scope notifications by project). 149 | 150 | ------------------------------------------------------------------------------- 151 | 152 | `-remove ID` **[required]** 153 | 154 | Remove a previous notification from the `ID` ‘group’, if one exists. 155 | 156 | Use the special `ID` “ALL” to remove all messages. 157 | 158 | ------------------------------------------------------------------------------- 159 | 160 | `-list ID` **[required]** 161 | 162 | Lists details about the specified ‘group’ `ID`. 163 | 164 | Use the special `ID` “ALL” to list details about all currently active messages. 165 | 166 | The output of this command is tab-separated, which makes it easy to parse. 167 | 168 | ------------------------------------------------------------------------------- 169 | 170 | `-activate ID` 171 | 172 | Activate the application specified by `ID` when the user clicks the 173 | notification. 174 | 175 | You can find the bundle identifier (`CFBundleIdentifier`) of an application in its `Info.plist` file 176 | _inside_ the application bundle. 177 | 178 | Examples application IDs are: 179 | 180 | * `com.apple.Terminal` to activate Terminal.app 181 | * `com.apple.Safari` to activate Safari.app 182 | 183 | ------------------------------------------------------------------------------- 184 | 185 | `-sender ID` 186 | 187 | Fakes the sender application of the notification. This uses the specified 188 | application’s icon, and will launch it when the notification is clicked. 189 | 190 | Using this option fakes the sender application, so that the notification system 191 | will launch that application when the notification is clicked. Because of this 192 | it is important to note that you cannot combine this with options like 193 | `-execute` and `-activate` which depend on the sender of the notification to be 194 | ‘terminal-notifier’ to perform its work. 195 | 196 | For information on the `ID`, see the `-activate` option. 197 | 198 | ------------------------------------------------------------------------------- 199 | 200 | `-appIcon PATH` 201 | 202 | Specify an image `PATH` to display instead of the application icon. 203 | 204 | **WARNING: This option is subject to change, since it relies on a private method.** 205 | 206 | ------------------------------------------------------------------------------- 207 | 208 | `-contentImage PATH` 209 | 210 | Specify an image `PATH` to attach inside of the notification. 211 | 212 | **WARNING: This option is subject to change since it relies on a private method.** 213 | 214 | ------------------------------------------------------------------------------- 215 | 216 | `-open URL` 217 | 218 | Open `URL` when the user clicks the notification. This can be a web or file URL, 219 | or any custom URL scheme. 220 | 221 | ------------------------------------------------------------------------------- 222 | 223 | `-execute COMMAND` 224 | 225 | Run the shell command `COMMAND` when the user clicks the notification. 226 | 227 | ------------------------------------------------------------------------------- 228 | 229 | `-ignoreDnD` 230 | 231 | Ignore Do Not Disturb settings and unconditionally show the notification. 232 | 233 | **WARNING: This option is subject to change since it relies on a private method.** 234 | 235 | ## License 236 | 237 | All the works are available under the MIT license. **Except** for 238 | ‘Terminal.icns’, which is a copy of Apple’s Terminal.app icon and as such is 239 | copyright of Apple. 240 | 241 | Copyright (C) 2012-2017 Eloy Durán , Julien Blanchard 242 | 243 | 244 | Permission is hereby granted, free of charge, to any person obtaining a copy of 245 | this software and associated documentation files (the "Software"), to deal in 246 | the Software without restriction, including without limitation the rights to 247 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 248 | of the Software, and to permit persons to whom the Software is furnished to do 249 | so, subject to the following conditions: 250 | 251 | The above copyright notice and this permission notice shall be included in all 252 | copies or substantial portions of the Software. 253 | 254 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 255 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 256 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 257 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 258 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 259 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 260 | SOFTWARE. 261 | -------------------------------------------------------------------------------- /Ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /Ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | terminal-notifier (1.5.1) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | bacon (1.1.0) 10 | metaclass (0.0.1) 11 | mocha (0.11.4) 12 | metaclass (~> 0.0.1) 13 | mocha-on-bacon (0.2.0) 14 | mocha (>= 0.9.8) 15 | 16 | PLATFORMS 17 | ruby 18 | 19 | DEPENDENCIES 20 | bacon 21 | mocha 22 | mocha-on-bacon 23 | terminal-notifier! 24 | -------------------------------------------------------------------------------- /Ruby/LICENSE: -------------------------------------------------------------------------------- 1 | All the works are available under the MIT license. **Except** for 2 | ‘Terminal.icns’, which is a copy of Apple’s Terminal.app icon and as such is 3 | copyright of Apple. 4 | 5 | Copyright (C) 2012-2013 Eloy Durán , Julien Blanchard 6 | 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | this software and associated documentation files (the "Software"), to deal in 10 | the Software without restriction, including without limitation the rights to 11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 12 | of the Software, and to permit persons to whom the Software is furnished to do 13 | so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /Ruby/README.markdown: -------------------------------------------------------------------------------- 1 | # TerminalNotifier 2 | 3 | A simple Ruby wrapper around the [`terminal-notifier`][HOMEPAGE] command-line 4 | tool, which allows you to send User Notifications to the Notification Center on 5 | macOS 10.10, or higher. 6 | 7 | 8 | ## Installation 9 | 10 | ``` 11 | $ gem install terminal-notifier 12 | ``` 13 | 14 | 15 | ## Usage 16 | 17 | For full information on all the options, see the tool’s [README][README]. 18 | 19 | Examples are: 20 | 21 | ```ruby 22 | TerminalNotifier.notify('Hello World') 23 | TerminalNotifier.notify('Hello World', :title => 'Ruby', :subtitle => 'Programming Language') 24 | TerminalNotifier.notify('Hello World', :activate => 'com.apple.Safari') 25 | TerminalNotifier.notify('Hello World', :open => 'http://twitter.com/julienXX') 26 | TerminalNotifier.notify('Hello World', :execute => 'say "OMG"') 27 | TerminalNotifier.notify('Hello World', :group => Process.pid) 28 | TerminalNotifier.notify('Hello World', :sender => 'com.apple.Safari') 29 | TerminalNotifier.notify('Hello World', :sound => 'default') 30 | 31 | TerminalNotifier.remove(Process.pid) 32 | 33 | TerminalNotifier.list(Process.pid) 34 | TerminalNotifier.list 35 | ``` 36 | 37 | 38 | ## License 39 | 40 | All the works are available under the MIT license. **Except** for 41 | ‘Terminal.icns’, which is a copy of Apple’s Terminal.app icon and as such is 42 | copyright of Apple. 43 | 44 | See [LICENSE][LICENSE] for details. 45 | 46 | [HOMEPAGE]: https://github.com/julienXX/terminal-notifier 47 | [README]: https://github.com/julienXX/terminal-notifier/blob/master/README.markdown 48 | [LICENSE]: https://github.com/julienXX/terminal-notifier/blob/master/Ruby/LICENSE 49 | -------------------------------------------------------------------------------- /Ruby/Rakefile: -------------------------------------------------------------------------------- 1 | def version 2 | @version ||= begin 3 | plist = File.expand_path('../../Terminal Notifier/Terminal Notifier-Info.plist', __FILE__) 4 | `/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' '#{plist}'`.strip 5 | end 6 | end 7 | 8 | def filename 9 | "terminal-notifier-#{version}" 10 | end 11 | 12 | def zipfile 13 | "#{version}.zip" 14 | end 15 | 16 | task :clean do 17 | rm zipfile 18 | rm_rf 'vendor' 19 | end 20 | 21 | desc 'Fetch latest build from the GitHub releases' 22 | task :update_build do 23 | unless File.exist?(zipfile) 24 | sh "curl -L -O 'https://github.com/julienXX/terminal-notifier/archive/#{zipfile}'" 25 | end 26 | 27 | rm_rf 'vendor' 28 | mkdir 'vendor' 29 | 30 | sh "unzip -o -d vendor #{zipfile}" 31 | mv "vendor/#{filename}", 'vendor/terminal-notifier' 32 | 33 | sh 'cd .. && xcodebuild -target terminal-notifier SYMROOT=build -verbose && cd -' 34 | mv '../build/Release/terminal-notifier.app', 'vendor/terminal-notifier/' 35 | chmod 0755, 'vendor/terminal-notifier/terminal-notifier.app/Contents/MacOS/terminal-notifier' 36 | end 37 | 38 | desc 'Build gem' 39 | task :gem => :update_build do 40 | sh 'gem build terminal-notifier.gemspec' 41 | end 42 | 43 | desc 'Run specs' 44 | task :spec do 45 | sh 'bundle exec ruby spec/terminal-notifier_spec.rb' 46 | end 47 | 48 | task :default => :spec 49 | -------------------------------------------------------------------------------- /Ruby/bin/terminal-notifier: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | if $0 == __FILE__ 4 | $:.unshift File.expand_path('../../lib', __FILE__) 5 | end 6 | 7 | require 'terminal-notifier' 8 | 9 | if !ARGV.include?("-message") && !STDIN.tty? 10 | ARGV.push(*["-message", STDIN.read.chomp]) 11 | end 12 | 13 | exec TerminalNotifier::BIN_PATH, *ARGV 14 | -------------------------------------------------------------------------------- /Ruby/lib/terminal-notifier.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require 'shellwords' 3 | require 'rbconfig' 4 | 5 | module TerminalNotifier 6 | BIN_PATH = File.expand_path('../../vendor/terminal-notifier/terminal-notifier.app/Contents/MacOS/terminal-notifier', __FILE__) 7 | 8 | class UnsupportedPlatformError < StandardError; end 9 | # Returns wether or not the current platform is macOS 10.10, or higher. 10 | def self.available? 11 | @available ||= (/darwin|mac os/ =~ RbConfig::CONFIG['host_os']) && Gem::Version.new(version) > Gem::Version.new('10.10') 12 | end 13 | 14 | def self.version 15 | @version ||= `uname`.strip == 'Darwin' && `sw_vers -productVersion`.strip 16 | end 17 | 18 | def self.execute(verbose, options) 19 | if available? 20 | command = [BIN_PATH, *options.map { |k,v| v = v.to_s; ["-#{k}", "#{v[0] == "-" ? " " : ""}#{Shellwords.escape(v[0,1])}#{v[1..-1]}"] }.flatten] 21 | command = Shellwords.join(command) if RUBY_VERSION < '1.9' 22 | result = '' 23 | IO.popen(command) do |stdout| 24 | output = stdout.read 25 | STDOUT.print output if verbose 26 | result << output 27 | end 28 | result 29 | else 30 | STDERR.print "terminal-notifier is only supported on macOS 10.10, or higher." 31 | end 32 | end 33 | 34 | # Cleans up the result of a notification, making it easier to work it 35 | # 36 | # The result of a notification is downcased, then groups of 1 or more 37 | # non-word characters are replaced with an underscore, before being 38 | # symbolised. 39 | # 40 | # If the reply option was given, then instead of going through the 41 | # above process, the result is returned with no changes as a string. 42 | # 43 | # If the always_string param is set to true, a the result is returned 44 | # with no changes as a string, like above. 45 | # 46 | # Examples are: 47 | # 48 | # notify_result('Test', {}) #=> :test 49 | # notify_result('No, sir', {}) #=> :no_sir 50 | # notify_result('@timeout', {}) #=> :_timeout 51 | # notify_result('@closeaction', {}) #=> :_closeaction 52 | # notify_result('I like pie', {reply: true}) #=> 'I like pie' 53 | # notify_result('I do not like pie', {'reply' => true}) #=> 'I do not like pie' 54 | # notify_result('@timeout', {'reply' => true}) #=> '@timeout' 55 | # notify_result('I may like pie', {}) #=> :i_may_like_pie 56 | def notify_result(result, options, always_string = false) 57 | if options[:reply] || options['reply'] || always_string 58 | result 59 | else 60 | result.length == 0 || result.downcase.gsub(/\W+/,'_').to_sym 61 | end 62 | end 63 | module_function :notify_result 64 | 65 | # Sends a User Notification and returns whether or not it was a success. 66 | # 67 | # The available options are `:title`, `:group`, `:activate`, `:open`, 68 | # `:execute`, `:sender`, and `:sound`. For a description of each option see: 69 | # 70 | # https://github.com/julienXX/terminal-notifier/blob/master/README.markdown 71 | # 72 | # Examples are: 73 | # 74 | # TerminalNotifier.notify('Hello World') 75 | # TerminalNotifier.notify('Hello World', :title => 'Ruby') 76 | # TerminalNotifier.notify('Hello World', :group => Process.pid) 77 | # TerminalNotifier.notify('Hello World', :activate => 'com.apple.Safari') 78 | # TerminalNotifier.notify('Hello World', :open => 'http://twitter.com/julienXX') 79 | # TerminalNotifier.notify('Hello World', :execute => 'say "OMG"') 80 | # TerminalNotifier.notify('Hello World', :sender => 'com.apple.Safari') 81 | # TerminalNotifier.notify('Hello World', :sound => 'default') 82 | # 83 | # Raises if not supported on the current platform. 84 | def notify(message, options = {}, verbose = false, always_string = false) 85 | result = TerminalNotifier.execute(verbose, options.merge(:message => message)) 86 | $? && $?.success? && notify_result(result, options, always_string) 87 | end 88 | module_function :notify 89 | 90 | # Removes a notification that was previously sent with the specified 91 | # ‘group’ ID, if one exists. 92 | # 93 | # If no ‘group’ ID is given, all notifications are removed. 94 | def remove(group = 'ALL', verbose = false) 95 | TerminalNotifier.execute(verbose, :remove => group) 96 | $? && $?.success? 97 | end 98 | module_function :remove 99 | 100 | LIST_FIELDS = [:group, :title, :subtitle, :message, :delivered_at].freeze 101 | 102 | # If a ‘group’ ID is given, and a notification for that group exists, 103 | # returns a hash with details about the notification. 104 | # 105 | # If no ‘group’ ID is given, an array of hashes describing all 106 | # notifications. 107 | # 108 | # If no information is available this will return `nil`. 109 | def list(group = 'ALL', verbose = false) 110 | output = TerminalNotifier.execute(verbose, :list => group) 111 | return if output.strip.empty? 112 | 113 | require 'time' 114 | notifications = output.split("\n")[1..-1].map do |line| 115 | LIST_FIELDS.zip(line.split("\t")).inject({}) do |hash, (key, value)| 116 | hash[key] = key == :delivered_at ? Time.parse(value) : (value unless value == '(null)') 117 | hash 118 | end 119 | end 120 | 121 | group == 'ALL' ? notifications : notifications.first 122 | end 123 | module_function :list 124 | end 125 | -------------------------------------------------------------------------------- /Ruby/spec/terminal-notifier_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bacon' 3 | require 'mocha' 4 | require 'mocha-on-bacon' 5 | 6 | Bacon.summary_at_exit 7 | 8 | $:.unshift File.expand_path('../../lib', __FILE__) 9 | require 'terminal-notifier' 10 | 11 | describe "TerminalNotifier" do 12 | it "executes the tool with the given options and properly escapes the message" do 13 | command = [TerminalNotifier::BIN_PATH, '-message', '\[ZOMG] "OH YEAH"'] 14 | command = Shellwords.join(command) if RUBY_VERSION < '1.9' 15 | IO.expects(:popen).with(command).yields(StringIO.new('output')) 16 | TerminalNotifier.execute(false, :message => '[ZOMG] "OH YEAH"') 17 | end 18 | 19 | it "correctly escapes arguments that start with a dash" do 20 | command = [TerminalNotifier::BIN_PATH, '-message', ' -kittens', '-title', ' -rule'] 21 | command = Shellwords.join(command) if RUBY_VERSION < '1.9' 22 | IO.expects(:popen).with(command).yields(StringIO.new('output')) 23 | TerminalNotifier.execute(false, :message => '-kittens', :title => '-rule') 24 | end 25 | 26 | it "returns the result output of the command" do 27 | TerminalNotifier.execute(false, 'help' => '').should == `'#{TerminalNotifier::BIN_PATH}' -help` 28 | end 29 | 30 | it "sends a notification" do 31 | TerminalNotifier.expects(:execute).with(false, :message => 'ZOMG', :group => 'important stuff') 32 | TerminalNotifier.notify('ZOMG', :group => 'important stuff') 33 | end 34 | 35 | it "removes a notification" do 36 | TerminalNotifier.expects(:execute).with(false, :remove => 'important stuff') 37 | TerminalNotifier.remove('important stuff') 38 | end 39 | 40 | it "by default removes all the notifications" do 41 | TerminalNotifier.expects(:execute).with(false, :remove => 'ALL') 42 | TerminalNotifier.remove 43 | end 44 | 45 | it "returns `nil` if no notification was found to list info for" do 46 | TerminalNotifier.expects(:execute).with(false, :list => 'important stuff').returns('') 47 | TerminalNotifier.list('important stuff').should == nil 48 | end 49 | 50 | it "returns info about a notification posted in a specific group" do 51 | TerminalNotifier.expects(:execute).with(false, :list => 'important stuff'). 52 | returns("GroupID\tTitle\tSubtitle\tMessage\tDelivered At\n" \ 53 | "important stuff\tTerminal\t(null)\tExecute: rake spec\t2012-08-06 19:45:30 +0000") 54 | TerminalNotifier.list('important stuff').should == { 55 | :group => 'important stuff', 56 | :title => 'Terminal', :subtitle => nil, :message => 'Execute: rake spec', 57 | :delivered_at => Time.parse('2012-08-06 19:45:30 +0000') 58 | } 59 | end 60 | 61 | it "by default returns a list of all notification" do 62 | TerminalNotifier.expects(:execute).with(false, :list => 'ALL'). 63 | returns("GroupID\tTitle\tSubtitle\tMessage\tDelivered At\n" \ 64 | "important stuff\tTerminal\t(null)\tExecute: rake spec\t2012-08-06 19:45:30 +0000\n" \ 65 | "(null)\t(null)\tSubtle\tBe subtle!\t2012-08-07 19:45:30 +0000") 66 | TerminalNotifier.list.should == [ 67 | { 68 | :group => 'important stuff', 69 | :title => 'Terminal', :subtitle => nil, :message => 'Execute: rake spec', 70 | :delivered_at => Time.parse('2012-08-06 19:45:30 +0000') 71 | }, 72 | { 73 | :group => nil, 74 | :title => nil, :subtitle => 'Subtle', :message => 'Be subtle!', 75 | :delivered_at => Time.parse('2012-08-07 19:45:30 +0000') 76 | } 77 | ] 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /Ruby/terminal-notifier.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | plist = File.expand_path('../../Terminal Notifier/Terminal Notifier-Info.plist', __FILE__) 3 | # Also run on non-OSX machines, otherwise bundle installs directly from the repo will fail. 4 | # version = `/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' '#{plist}'`.strip 5 | version = File.read(plist).match(%r{(\d+\.\d+\.\d+)})[1] 6 | 7 | 8 | Gem::Specification.new do |gem| 9 | gem.name = "terminal-notifier" 10 | gem.version = version 11 | gem.summary = 'Send User Notifications on macOS 10.10 or higher.' 12 | gem.authors = ["Eloy Duran", "Julien Blanchard"] 13 | gem.email = ["eloy.de.enige@gmail.com", "julien@sideburns.eu"] 14 | gem.homepage = 'https://github.com/julienXX/terminal-notifier' 15 | gem.license = 'MIT' 16 | 17 | gem.executables = ['terminal-notifier'] 18 | gem.files = ['bin/terminal-notifier', 'lib/terminal-notifier.rb'] + Dir.glob('vendor/terminal-notifier/**/*') 19 | gem.require_paths = ['lib'] 20 | 21 | gem.extra_rdoc_files = ['README.markdown'] 22 | 23 | gem.add_development_dependency 'bacon' 24 | gem.add_development_dependency 'mocha' 25 | gem.add_development_dependency 'mocha-on-bacon' 26 | end 27 | -------------------------------------------------------------------------------- /Terminal Notifier.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5199791915B1F92B003AFC57 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5199791815B1F92B003AFC57 /* Cocoa.framework */; }; 11 | 5199792315B1F92B003AFC57 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5199792115B1F92B003AFC57 /* InfoPlist.strings */; }; 12 | 5199792515B1F92B003AFC57 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5199792415B1F92B003AFC57 /* main.m */; }; 13 | 5199792915B1F92B003AFC57 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 5199792715B1F92B003AFC57 /* Credits.rtf */; }; 14 | 5199792C15B1F92B003AFC57 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5199792B15B1F92B003AFC57 /* AppDelegate.m */; }; 15 | 5199792F15B1F92B003AFC57 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5199792D15B1F92B003AFC57 /* MainMenu.xib */; }; 16 | 5199794215B2F908003AFC57 /* ScriptingBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5199794115B2F908003AFC57 /* ScriptingBridge.framework */; }; 17 | 5199794C15B302F1003AFC57 /* Terminal.icns in Resources */ = {isa = PBXBuildFile; fileRef = 5199794B15B302F1003AFC57 /* Terminal.icns */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 5199791415B1F92B003AFC57 /* terminal-notifier.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "terminal-notifier.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 5199791815B1F92B003AFC57 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 23 | 5199791B15B1F92B003AFC57 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 24 | 5199791C15B1F92B003AFC57 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 25 | 5199791D15B1F92B003AFC57 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 26 | 5199792015B1F92B003AFC57 /* Terminal Notifier-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Terminal Notifier-Info.plist"; sourceTree = ""; }; 27 | 5199792215B1F92B003AFC57 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 28 | 5199792415B1F92B003AFC57 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 29 | 5199792615B1F92B003AFC57 /* Terminal Notifier-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Terminal Notifier-Prefix.pch"; sourceTree = ""; }; 30 | 5199792815B1F92B003AFC57 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 31 | 5199792A15B1F92B003AFC57 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 32 | 5199792B15B1F92B003AFC57 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 33 | 5199792E15B1F92B003AFC57 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; 34 | 5199794115B2F908003AFC57 /* ScriptingBridge.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ScriptingBridge.framework; path = System/Library/Frameworks/ScriptingBridge.framework; sourceTree = SDKROOT; }; 35 | 5199794B15B302F1003AFC57 /* Terminal.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = Terminal.icns; sourceTree = ""; }; 36 | /* End PBXFileReference section */ 37 | 38 | /* Begin PBXFrameworksBuildPhase section */ 39 | 5199791115B1F92B003AFC57 /* Frameworks */ = { 40 | isa = PBXFrameworksBuildPhase; 41 | buildActionMask = 2147483647; 42 | files = ( 43 | 5199794215B2F908003AFC57 /* ScriptingBridge.framework in Frameworks */, 44 | 5199791915B1F92B003AFC57 /* Cocoa.framework in Frameworks */, 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | /* End PBXFrameworksBuildPhase section */ 49 | 50 | /* Begin PBXGroup section */ 51 | 5199790915B1F92B003AFC57 = { 52 | isa = PBXGroup; 53 | children = ( 54 | 5199794B15B302F1003AFC57 /* Terminal.icns */, 55 | 5199794115B2F908003AFC57 /* ScriptingBridge.framework */, 56 | 5199791E15B1F92B003AFC57 /* Terminal Notifier */, 57 | 5199791715B1F92B003AFC57 /* Frameworks */, 58 | 5199791515B1F92B003AFC57 /* Products */, 59 | ); 60 | sourceTree = ""; 61 | }; 62 | 5199791515B1F92B003AFC57 /* Products */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 5199791415B1F92B003AFC57 /* terminal-notifier.app */, 66 | ); 67 | name = Products; 68 | sourceTree = ""; 69 | }; 70 | 5199791715B1F92B003AFC57 /* Frameworks */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 5199791815B1F92B003AFC57 /* Cocoa.framework */, 74 | 5199791A15B1F92B003AFC57 /* Other Frameworks */, 75 | ); 76 | name = Frameworks; 77 | sourceTree = ""; 78 | }; 79 | 5199791A15B1F92B003AFC57 /* Other Frameworks */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 5199791B15B1F92B003AFC57 /* AppKit.framework */, 83 | 5199791C15B1F92B003AFC57 /* CoreData.framework */, 84 | 5199791D15B1F92B003AFC57 /* Foundation.framework */, 85 | ); 86 | name = "Other Frameworks"; 87 | sourceTree = ""; 88 | }; 89 | 5199791E15B1F92B003AFC57 /* Terminal Notifier */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 5199792A15B1F92B003AFC57 /* AppDelegate.h */, 93 | 5199792B15B1F92B003AFC57 /* AppDelegate.m */, 94 | 5199792D15B1F92B003AFC57 /* MainMenu.xib */, 95 | 5199791F15B1F92B003AFC57 /* Supporting Files */, 96 | ); 97 | path = "Terminal Notifier"; 98 | sourceTree = ""; 99 | }; 100 | 5199791F15B1F92B003AFC57 /* Supporting Files */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 5199792015B1F92B003AFC57 /* Terminal Notifier-Info.plist */, 104 | 5199792115B1F92B003AFC57 /* InfoPlist.strings */, 105 | 5199792415B1F92B003AFC57 /* main.m */, 106 | 5199792615B1F92B003AFC57 /* Terminal Notifier-Prefix.pch */, 107 | 5199792715B1F92B003AFC57 /* Credits.rtf */, 108 | ); 109 | name = "Supporting Files"; 110 | sourceTree = ""; 111 | }; 112 | /* End PBXGroup section */ 113 | 114 | /* Begin PBXNativeTarget section */ 115 | 5199791315B1F92B003AFC57 /* terminal-notifier */ = { 116 | isa = PBXNativeTarget; 117 | buildConfigurationList = 5199793215B1F92B003AFC57 /* Build configuration list for PBXNativeTarget "terminal-notifier" */; 118 | buildPhases = ( 119 | 5199791015B1F92B003AFC57 /* Sources */, 120 | 5199791115B1F92B003AFC57 /* Frameworks */, 121 | 5199791215B1F92B003AFC57 /* Resources */, 122 | ); 123 | buildRules = ( 124 | ); 125 | dependencies = ( 126 | ); 127 | name = "terminal-notifier"; 128 | productName = "Terminal Notifier"; 129 | productReference = 5199791415B1F92B003AFC57 /* terminal-notifier.app */; 130 | productType = "com.apple.product-type.application"; 131 | }; 132 | /* End PBXNativeTarget section */ 133 | 134 | /* Begin PBXProject section */ 135 | 5199790B15B1F92B003AFC57 /* Project object */ = { 136 | isa = PBXProject; 137 | attributes = { 138 | LastUpgradeCheck = 0440; 139 | ORGANIZATIONNAME = "Eloy Durán"; 140 | }; 141 | buildConfigurationList = 5199790E15B1F92B003AFC57 /* Build configuration list for PBXProject "Terminal Notifier" */; 142 | compatibilityVersion = "Xcode 3.2"; 143 | developmentRegion = English; 144 | hasScannedForEncodings = 0; 145 | knownRegions = ( 146 | en, 147 | ); 148 | mainGroup = 5199790915B1F92B003AFC57; 149 | productRefGroup = 5199791515B1F92B003AFC57 /* Products */; 150 | projectDirPath = ""; 151 | projectRoot = ""; 152 | targets = ( 153 | 5199791315B1F92B003AFC57 /* terminal-notifier */, 154 | ); 155 | }; 156 | /* End PBXProject section */ 157 | 158 | /* Begin PBXResourcesBuildPhase section */ 159 | 5199791215B1F92B003AFC57 /* Resources */ = { 160 | isa = PBXResourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | 5199792315B1F92B003AFC57 /* InfoPlist.strings in Resources */, 164 | 5199792915B1F92B003AFC57 /* Credits.rtf in Resources */, 165 | 5199792F15B1F92B003AFC57 /* MainMenu.xib in Resources */, 166 | 5199794C15B302F1003AFC57 /* Terminal.icns in Resources */, 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | /* End PBXResourcesBuildPhase section */ 171 | 172 | /* Begin PBXSourcesBuildPhase section */ 173 | 5199791015B1F92B003AFC57 /* Sources */ = { 174 | isa = PBXSourcesBuildPhase; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | 5199792515B1F92B003AFC57 /* main.m in Sources */, 178 | 5199792C15B1F92B003AFC57 /* AppDelegate.m in Sources */, 179 | ); 180 | runOnlyForDeploymentPostprocessing = 0; 181 | }; 182 | /* End PBXSourcesBuildPhase section */ 183 | 184 | /* Begin PBXVariantGroup section */ 185 | 5199792115B1F92B003AFC57 /* InfoPlist.strings */ = { 186 | isa = PBXVariantGroup; 187 | children = ( 188 | 5199792215B1F92B003AFC57 /* en */, 189 | ); 190 | name = InfoPlist.strings; 191 | sourceTree = ""; 192 | }; 193 | 5199792715B1F92B003AFC57 /* Credits.rtf */ = { 194 | isa = PBXVariantGroup; 195 | children = ( 196 | 5199792815B1F92B003AFC57 /* en */, 197 | ); 198 | name = Credits.rtf; 199 | sourceTree = ""; 200 | }; 201 | 5199792D15B1F92B003AFC57 /* MainMenu.xib */ = { 202 | isa = PBXVariantGroup; 203 | children = ( 204 | 5199792E15B1F92B003AFC57 /* en */, 205 | ); 206 | name = MainMenu.xib; 207 | sourceTree = ""; 208 | }; 209 | /* End PBXVariantGroup section */ 210 | 211 | /* Begin XCBuildConfiguration section */ 212 | 5199793015B1F92B003AFC57 /* Debug */ = { 213 | isa = XCBuildConfiguration; 214 | buildSettings = { 215 | ALWAYS_SEARCH_USER_PATHS = NO; 216 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 217 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 218 | CLANG_ENABLE_OBJC_ARC = YES; 219 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 220 | COPY_PHASE_STRIP = NO; 221 | GCC_C_LANGUAGE_STANDARD = gnu99; 222 | GCC_DYNAMIC_NO_PIC = NO; 223 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 224 | GCC_OPTIMIZATION_LEVEL = 0; 225 | GCC_PREPROCESSOR_DEFINITIONS = ( 226 | "DEBUG=1", 227 | "$(inherited)", 228 | ); 229 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 230 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 231 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 232 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 233 | GCC_WARN_UNUSED_VARIABLE = YES; 234 | MACOSX_DEPLOYMENT_TARGET = 10.10; 235 | ONLY_ACTIVE_ARCH = YES; 236 | SDKROOT = macosx; 237 | }; 238 | name = Debug; 239 | }; 240 | 5199793115B1F92B003AFC57 /* Release */ = { 241 | isa = XCBuildConfiguration; 242 | buildSettings = { 243 | ALWAYS_SEARCH_USER_PATHS = NO; 244 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 245 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 248 | COPY_PHASE_STRIP = YES; 249 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 250 | GCC_C_LANGUAGE_STANDARD = gnu99; 251 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 252 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 253 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 254 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 255 | GCC_WARN_UNUSED_VARIABLE = YES; 256 | MACOSX_DEPLOYMENT_TARGET = 10.10; 257 | SDKROOT = macosx; 258 | }; 259 | name = Release; 260 | }; 261 | 5199793315B1F92B003AFC57 /* Debug */ = { 262 | isa = XCBuildConfiguration; 263 | buildSettings = { 264 | COMBINE_HIDPI_IMAGES = YES; 265 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 266 | GCC_PREFIX_HEADER = "Terminal Notifier/Terminal Notifier-Prefix.pch"; 267 | INFOPLIST_FILE = "Terminal Notifier/Terminal Notifier-Info.plist"; 268 | MACOSX_DEPLOYMENT_TARGET = 10.10; 269 | PRODUCT_NAME = "$(TARGET_NAME)"; 270 | PROVISIONING_PROFILE = ""; 271 | SDKROOT = macosx; 272 | WRAPPER_EXTENSION = app; 273 | }; 274 | name = Debug; 275 | }; 276 | 5199793415B1F92B003AFC57 /* Release */ = { 277 | isa = XCBuildConfiguration; 278 | buildSettings = { 279 | COMBINE_HIDPI_IMAGES = YES; 280 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 281 | GCC_PREFIX_HEADER = "Terminal Notifier/Terminal Notifier-Prefix.pch"; 282 | INFOPLIST_FILE = "Terminal Notifier/Terminal Notifier-Info.plist"; 283 | MACOSX_DEPLOYMENT_TARGET = 10.10; 284 | PRODUCT_NAME = "$(TARGET_NAME)"; 285 | PROVISIONING_PROFILE = ""; 286 | SDKROOT = macosx; 287 | WRAPPER_EXTENSION = app; 288 | }; 289 | name = Release; 290 | }; 291 | /* End XCBuildConfiguration section */ 292 | 293 | /* Begin XCConfigurationList section */ 294 | 5199790E15B1F92B003AFC57 /* Build configuration list for PBXProject "Terminal Notifier" */ = { 295 | isa = XCConfigurationList; 296 | buildConfigurations = ( 297 | 5199793015B1F92B003AFC57 /* Debug */, 298 | 5199793115B1F92B003AFC57 /* Release */, 299 | ); 300 | defaultConfigurationIsVisible = 0; 301 | defaultConfigurationName = Release; 302 | }; 303 | 5199793215B1F92B003AFC57 /* Build configuration list for PBXNativeTarget "terminal-notifier" */ = { 304 | isa = XCConfigurationList; 305 | buildConfigurations = ( 306 | 5199793315B1F92B003AFC57 /* Debug */, 307 | 5199793415B1F92B003AFC57 /* Release */, 308 | ); 309 | defaultConfigurationIsVisible = 0; 310 | defaultConfigurationName = Release; 311 | }; 312 | /* End XCConfigurationList section */ 313 | }; 314 | rootObject = 5199790B15B1F92B003AFC57 /* Project object */; 315 | } 316 | -------------------------------------------------------------------------------- /Terminal Notifier.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Terminal Notifier.xcodeproj/project.xcworkspace/xcshareddata/Terminal Notifier.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | A55309C1-2ABD-460A-B080-5A322DCAF4B5 9 | IDESourceControlProjectName 10 | Terminal Notifier 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 9F67DD19109869B0F5A85172DA9B0D3E8F7EF383 14 | github.com:julienXX/terminal-notifier.git 15 | 16 | IDESourceControlProjectPath 17 | Terminal Notifier.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | 9F67DD19109869B0F5A85172DA9B0D3E8F7EF383 21 | ../.. 22 | 23 | IDESourceControlProjectURL 24 | github.com:julienXX/terminal-notifier.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | 9F67DD19109869B0F5A85172DA9B0D3E8F7EF383 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | 9F67DD19109869B0F5A85172DA9B0D3E8F7EF383 36 | IDESourceControlWCCName 37 | terminal-notifier 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Terminal Notifier.xcodeproj/xcshareddata/xcschemes/Terminal Notifier.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 51 | 52 | 58 | 59 | 60 | 61 | 64 | 65 | 68 | 69 | 72 | 73 | 76 | 77 | 80 | 81 | 84 | 85 | 88 | 89 | 92 | 93 | 96 | 97 | 100 | 101 | 104 | 105 | 108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | 123 | 124 | 130 | 131 | 132 | 133 | 135 | 136 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /Terminal Notifier/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface AppDelegate : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /Terminal Notifier/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | #import 3 | #import 4 | 5 | NSString * const TerminalNotifierBundleID = @"fr.julienxx.oss.terminal-notifier"; 6 | NSString * const NotificationCenterUIBundleID = @"com.apple.notificationcenterui"; 7 | 8 | NSString *_fakeBundleIdentifier = nil; 9 | 10 | @implementation NSBundle (FakeBundleIdentifier) 11 | 12 | // Overriding bundleIdentifier works, but overriding NSUserNotificationAlertStyle does not work. 13 | 14 | - (NSString *)__bundleIdentifier; 15 | { 16 | if (self == [NSBundle mainBundle]) { 17 | return _fakeBundleIdentifier ? _fakeBundleIdentifier : TerminalNotifierBundleID; 18 | } else { 19 | return [self __bundleIdentifier]; 20 | } 21 | } 22 | 23 | @end 24 | 25 | static BOOL 26 | InstallFakeBundleIdentifierHook() 27 | { 28 | Class class = objc_getClass("NSBundle"); 29 | if (class) { 30 | method_exchangeImplementations(class_getInstanceMethod(class, @selector(bundleIdentifier)), 31 | class_getInstanceMethod(class, @selector(__bundleIdentifier))); 32 | return YES; 33 | } 34 | return NO; 35 | } 36 | 37 | @implementation NSUserDefaults (SubscriptAndUnescape) 38 | - (id)objectForKeyedSubscript:(id)key; 39 | { 40 | id obj = [self objectForKey:key]; 41 | if ([obj isKindOfClass:[NSString class]] && [(NSString *)obj hasPrefix:@"\\"]) { 42 | obj = [(NSString *)obj substringFromIndex:1]; 43 | } 44 | return obj; 45 | } 46 | @end 47 | 48 | 49 | @implementation AppDelegate 50 | 51 | +(void)initializeUserDefaults 52 | { 53 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 54 | 55 | // initialize the dictionary with default values depending on OS level 56 | NSDictionary *appDefaults; 57 | appDefaults = @{@"sender": @"com.apple.Terminal"}; 58 | 59 | // and set them appropriately 60 | [defaults registerDefaults:appDefaults]; 61 | } 62 | 63 | - (void)printHelpBanner; 64 | { 65 | const char *appName = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"] UTF8String]; 66 | const char *appVersion = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] UTF8String]; 67 | printf("%s (%s) is a command-line tool to send macOS User Notifications.\n" \ 68 | "\n" \ 69 | "Usage: %s -[message|list|remove] [VALUE|ID|ID] [options]\n" \ 70 | "\n" \ 71 | " Either of these is required (unless message data is piped to the tool):\n" \ 72 | "\n" \ 73 | " -help Display this help banner.\n" \ 74 | " -version Display terminal-notifier version.\n" \ 75 | " -message VALUE The notification message.\n" \ 76 | " -remove ID Removes a notification with the specified ‘group’ ID.\n" \ 77 | " -list ID If the specified ‘group’ ID exists show when it was delivered,\n" \ 78 | " or use ‘ALL’ as ID to see all notifications.\n" \ 79 | " The output is a tab-separated list.\n" 80 | "\n" \ 81 | " Optional:\n" \ 82 | "\n" \ 83 | " -title VALUE The notification title. Defaults to ‘Terminal’.\n" \ 84 | " -subtitle VALUE The notification subtitle.\n" \ 85 | " -sound NAME The name of a sound to play when the notification appears. The names are listed\n" \ 86 | " in Sound Preferences. Use 'default' for the default notification sound.\n" \ 87 | " -group ID A string which identifies the group the notifications belong to.\n" \ 88 | " Old notifications with the same ID will be removed.\n" \ 89 | " -activate ID The bundle identifier of the application to activate when the user clicks the notification.\n" \ 90 | " -sender ID The bundle identifier of the application that should be shown as the sender, including its icon.\n" \ 91 | " -appIcon URL The URL of a image to display instead of the application icon (Mavericks+ only)\n" \ 92 | " -contentImage URL The URL of a image to display attached to the notification (Mavericks+ only)\n" \ 93 | " -open URL The URL of a resource to open when the user clicks the notification.\n" \ 94 | " -execute COMMAND A shell command to perform when the user clicks the notification.\n" \ 95 | " -ignoreDnD Send notification even if Do Not Disturb is enabled.\n" \ 96 | "\n" \ 97 | "When the user activates a notification, the results are logged to the system logs.\n" \ 98 | "Use Console.app to view these logs.\n" \ 99 | "\n" \ 100 | "Note that in some circumstances the first character of a message has to be escaped in order to be recognized.\n" \ 101 | "An example of this is when using an open bracket, which has to be escaped like so: ‘\\[’.\n" \ 102 | "\n" \ 103 | "For more information see https://github.com/julienXX/terminal-notifier.\n", 104 | appName, appVersion, appName); 105 | } 106 | 107 | - (void)printVersion; 108 | { 109 | const char *appName = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"] UTF8String]; 110 | const char *appVersion = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] UTF8String]; 111 | printf("%s %s.\n", appName, appVersion); 112 | } 113 | 114 | - (void)applicationDidFinishLaunching:(NSNotification *)notification; 115 | { 116 | NSUserNotification *userNotification = notification.userInfo[NSApplicationLaunchUserNotificationKey]; 117 | if (userNotification) { 118 | [self userActivatedNotification:userNotification]; 119 | 120 | } else { 121 | if ([[[NSProcessInfo processInfo] arguments] indexOfObject:@"-help"] != NSNotFound) { 122 | [self printHelpBanner]; 123 | exit(0); 124 | } 125 | 126 | if ([[[NSProcessInfo processInfo] arguments] indexOfObject:@"-version"] != NSNotFound) { 127 | [self printVersion]; 128 | exit(0); 129 | } 130 | 131 | NSArray *runningProcesses = [[[NSWorkspace sharedWorkspace] runningApplications] valueForKey:@"bundleIdentifier"]; 132 | if ([runningProcesses indexOfObject:NotificationCenterUIBundleID] == NSNotFound) { 133 | NSLog(@"[!] Unable to post a notification for the current user (%@), as it has no running NotificationCenter instance.", NSUserName()); 134 | exit(1); 135 | } 136 | 137 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 138 | 139 | NSString *subtitle = defaults[@"subtitle"]; 140 | NSString *message = defaults[@"message"]; 141 | NSString *remove = defaults[@"remove"]; 142 | NSString *list = defaults[@"list"]; 143 | NSString *sound = defaults[@"sound"]; 144 | 145 | // If there is no message and data is piped to the application, use that 146 | // instead. 147 | if (message == nil && !isatty(STDIN_FILENO)) { 148 | NSData *inputData = [NSData dataWithData:[[NSFileHandle fileHandleWithStandardInput] readDataToEndOfFile]]; 149 | message = [[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding]; 150 | } 151 | 152 | if (message == nil && remove == nil && list == nil) { 153 | [self printHelpBanner]; 154 | exit(1); 155 | } 156 | 157 | if (list) { 158 | [self listNotificationWithGroupID:list]; 159 | exit(0); 160 | } 161 | 162 | // Install the fake bundle ID hook so we can fake the sender. This also 163 | // needs to be done to be able to remove a message. 164 | if (defaults[@"sender"]) { 165 | @autoreleasepool { 166 | if (InstallFakeBundleIdentifierHook()) { 167 | _fakeBundleIdentifier = defaults[@"sender"]; 168 | } 169 | } 170 | } 171 | 172 | if (remove) { 173 | [self removeNotificationWithGroupID:remove]; 174 | if (message == nil || ([message length] == 0)) { 175 | exit(0); 176 | } 177 | } 178 | 179 | if (message) { 180 | NSMutableDictionary *options = [NSMutableDictionary dictionary]; 181 | if (defaults[@"activate"]) options[@"bundleID"] = defaults[@"activate"]; 182 | if (defaults[@"group"]) options[@"groupID"] = defaults[@"group"]; 183 | if (defaults[@"execute"]) options[@"command"] = defaults[@"execute"]; 184 | if (defaults[@"appIcon"]) options[@"appIcon"] = defaults[@"appIcon"]; 185 | if (defaults[@"contentImage"]) options[@"contentImage"] = defaults[@"contentImage"]; 186 | 187 | if (defaults[@"open"]) { 188 | NSURL *url = [NSURL URLWithString:defaults[@"open"]]; 189 | if ((url && url.scheme && url.host) || [url isFileURL]) { 190 | options[@"open"] = defaults[@"open"]; 191 | }else{ 192 | NSLog(@"'%@' is not a valid URI.", defaults[@"open"]); 193 | exit(1); 194 | } 195 | } 196 | 197 | if([[[NSProcessInfo processInfo] arguments] containsObject:@"-ignoreDnD"] == true) { 198 | options[@"ignoreDnD"] = @YES; 199 | } 200 | 201 | [self deliverNotificationWithTitle:defaults[@"title"] ?: @"Terminal" 202 | subtitle:subtitle 203 | message:message 204 | options:options 205 | sound:sound]; 206 | } 207 | } 208 | } 209 | 210 | - (NSImage*)getImageFromURL:(NSString *) url; 211 | { 212 | NSURL *imageURL = [NSURL URLWithString:url]; 213 | if([[imageURL scheme] length] == 0){ 214 | // Prefix 'file://' if no scheme 215 | imageURL = [NSURL fileURLWithPath:url]; 216 | } 217 | return [[NSImage alloc] initWithContentsOfURL:imageURL]; 218 | } 219 | 220 | /** 221 | * Decode fragment identifier 222 | * 223 | * @see http://tools.ietf.org/html/rfc3986#section-2.1 224 | * @see http://en.wikipedia.org/wiki/URI_scheme 225 | */ 226 | - (NSString*)decodeFragmentInURL:(NSString *) encodedURL fragment:(NSString *) framgent 227 | { 228 | NSString *beforeStr = [@"%23" stringByAppendingString:framgent]; 229 | NSString *afterStr = [@"#" stringByAppendingString:framgent]; 230 | NSString *decodedURL = [encodedURL stringByReplacingOccurrencesOfString:beforeStr withString:afterStr]; 231 | return decodedURL; 232 | } 233 | 234 | - (void)deliverNotificationWithTitle:(NSString *)title 235 | subtitle:(NSString *)subtitle 236 | message:(NSString *)message 237 | options:(NSDictionary *)options 238 | sound:(NSString *)sound; 239 | { 240 | // First remove earlier notification with the same group ID. 241 | if (options[@"groupID"]) [self removeNotificationWithGroupID:options[@"groupID"]]; 242 | 243 | NSUserNotification *userNotification = [NSUserNotification new]; 244 | userNotification.title = title; 245 | userNotification.subtitle = subtitle; 246 | userNotification.informativeText = message; 247 | userNotification.userInfo = options; 248 | 249 | if(options[@"appIcon"]){ 250 | [userNotification setValue:[self getImageFromURL:options[@"appIcon"]] forKey:@"_identityImage"]; 251 | [userNotification setValue:@(false) forKey:@"_identityImageHasBorder"]; 252 | } 253 | if(options[@"contentImage"]){ 254 | userNotification.contentImage = [self getImageFromURL:options[@"contentImage"]]; 255 | } 256 | 257 | if (sound != nil) { 258 | userNotification.soundName = [sound isEqualToString: @"default"] ? NSUserNotificationDefaultSoundName : sound ; 259 | } 260 | 261 | if(options[@"ignoreDnD"]){ 262 | [userNotification setValue:@YES forKey:@"_ignoresDoNotDisturb"]; 263 | } 264 | 265 | NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; 266 | center.delegate = self; 267 | [center scheduleNotification:userNotification]; 268 | } 269 | 270 | - (void)removeNotificationWithGroupID:(NSString *)groupID; 271 | { 272 | NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; 273 | for (NSUserNotification *userNotification in center.deliveredNotifications) { 274 | if ([@"ALL" isEqualToString:groupID] || [userNotification.userInfo[@"groupID"] isEqualToString:groupID]) { 275 | NSString *deliveredAt = [userNotification.actualDeliveryDate description]; 276 | printf("* Removing previously sent notification, which was sent on: %s\n", [deliveredAt UTF8String]); 277 | [center removeDeliveredNotification:userNotification]; 278 | } 279 | } 280 | } 281 | 282 | - (void)listNotificationWithGroupID:(NSString *)listGroupID; 283 | { 284 | NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; 285 | 286 | NSMutableArray *lines = [NSMutableArray array]; 287 | for (NSUserNotification *userNotification in center.deliveredNotifications) { 288 | NSString *deliveredgroupID = userNotification.userInfo[@"groupID"]; 289 | NSString *title = userNotification.title; 290 | NSString *subtitle = userNotification.subtitle; 291 | NSString *message = userNotification.informativeText; 292 | NSString *deliveredAt = [userNotification.actualDeliveryDate description]; 293 | if ([@"ALL" isEqualToString:listGroupID] || [deliveredgroupID isEqualToString:listGroupID]) { 294 | [lines addObject:[NSString stringWithFormat:@"%@\t%@\t%@\t%@\t%@", deliveredgroupID, title, subtitle, message, deliveredAt]]; 295 | } 296 | } 297 | 298 | if (lines.count > 0) { 299 | printf("GroupID\tTitle\tSubtitle\tMessage\tDelivered At\n"); 300 | for (NSString *line in lines) { 301 | printf("%s\n", [line UTF8String]); 302 | } 303 | } 304 | } 305 | 306 | 307 | - (void)userActivatedNotification:(NSUserNotification *)userNotification; 308 | { 309 | [[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification:userNotification]; 310 | 311 | NSString *groupID = userNotification.userInfo[@"groupID"]; 312 | NSString *bundleID = userNotification.userInfo[@"bundleID"]; 313 | NSString *command = userNotification.userInfo[@"command"]; 314 | NSString *open = userNotification.userInfo[@"open"]; 315 | 316 | NSLog(@"User activated notification:"); 317 | NSLog(@" group ID: %@", groupID); 318 | NSLog(@" title: %@", userNotification.title); 319 | NSLog(@" subtitle: %@", userNotification.subtitle); 320 | NSLog(@" message: %@", userNotification.informativeText); 321 | NSLog(@"bundle ID: %@", bundleID); 322 | NSLog(@" command: %@", command); 323 | NSLog(@" open: %@", open); 324 | 325 | BOOL success = YES; 326 | if (bundleID) success &= [self activateAppWithBundleID:bundleID]; 327 | if (command) success &= [self executeShellCommand:command]; 328 | if (open) success &= [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:open]]; 329 | 330 | exit(success ? 0 : 1); 331 | } 332 | 333 | - (BOOL)activateAppWithBundleID:(NSString *)bundleID; 334 | { 335 | id app = [SBApplication applicationWithBundleIdentifier:bundleID]; 336 | if (app) { 337 | [app activate]; 338 | return YES; 339 | 340 | } else { 341 | NSLog(@"Unable to find an application with the specified bundle indentifier."); 342 | return NO; 343 | } 344 | } 345 | 346 | - (BOOL)executeShellCommand:(NSString *)command; 347 | { 348 | NSPipe *pipe = [NSPipe pipe]; 349 | NSFileHandle *fileHandle = [pipe fileHandleForReading]; 350 | 351 | NSTask *task = [NSTask new]; 352 | task.launchPath = @"/bin/sh"; 353 | task.arguments = @[@"-c", command]; 354 | task.standardOutput = pipe; 355 | task.standardError = pipe; 356 | [task launch]; 357 | 358 | NSData *data = nil; 359 | NSMutableData *accumulatedData = [NSMutableData data]; 360 | while ((data = [fileHandle availableData]) && [data length]) { 361 | [accumulatedData appendData:data]; 362 | } 363 | 364 | [task waitUntilExit]; 365 | NSLog(@"command output:\n%@", [[NSString alloc] initWithData:accumulatedData encoding:NSUTF8StringEncoding]); 366 | return [task terminationStatus] == 0; 367 | } 368 | 369 | - (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center 370 | shouldPresentNotification:(NSUserNotification *)userNotification; 371 | { 372 | return YES; 373 | } 374 | 375 | // Once the notification is delivered we can exit. 376 | - (void)userNotificationCenter:(NSUserNotificationCenter *)center 377 | didDeliverNotification:(NSUserNotification *)userNotification; 378 | { 379 | exit(0); 380 | } 381 | 382 | @end 383 | -------------------------------------------------------------------------------- /Terminal Notifier/Terminal Notifier-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | Terminal 11 | CFBundleIdentifier 12 | fr.julienxx.oss.terminal-notifier 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 2.0.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 15 25 | LSMinimumSystemVersion 26 | 10.10 27 | LSUIElement 28 | 29 | NSHumanReadableCopyright 30 | Copyright © 2012-2017 Eloy Durán, Julien Blanchard. All rights reserved. 31 | NSMainNibFile 32 | MainMenu 33 | NSPrincipalClass 34 | NSApplication 35 | NSAppTransportSecurity 36 | 37 | NSAllowsArbitraryLoads 38 | 39 | 40 | NSUserNotificationAlertStyle 41 | banner 42 | 43 | 44 | -------------------------------------------------------------------------------- /Terminal Notifier/Terminal Notifier-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'Terminal Notifier' target in the 'Terminal Notifier' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /Terminal Notifier/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /Terminal Notifier/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Terminal Notifier/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | int main(int argc, char *argv[]) 4 | { 5 | return NSApplicationMain(argc, (const char **)argv); 6 | } 7 | -------------------------------------------------------------------------------- /Terminal.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/julienXX/terminal-notifier/1e09b8b287ce89658cfa60060667e3ae8a4cae2a/Terminal.icns -------------------------------------------------------------------------------- /assets/Example_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/julienXX/terminal-notifier/1e09b8b287ce89658cfa60060667e3ae8a4cae2a/assets/Example_1.png -------------------------------------------------------------------------------- /assets/Example_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/julienXX/terminal-notifier/1e09b8b287ce89658cfa60060667e3ae8a4cae2a/assets/Example_2.png -------------------------------------------------------------------------------- /assets/Example_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/julienXX/terminal-notifier/1e09b8b287ce89658cfa60060667e3ae8a4cae2a/assets/Example_3.png -------------------------------------------------------------------------------- /assets/Example_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/julienXX/terminal-notifier/1e09b8b287ce89658cfa60060667e3ae8a4cae2a/assets/Example_4.png -------------------------------------------------------------------------------- /assets/Example_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/julienXX/terminal-notifier/1e09b8b287ce89658cfa60060667e3ae8a4cae2a/assets/Example_5.png -------------------------------------------------------------------------------- /assets/System_prefs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/julienXX/terminal-notifier/1e09b8b287ce89658cfa60060667e3ae8a4cae2a/assets/System_prefs.png --------------------------------------------------------------------------------