├── .gitignore ├── .rubocop.yml ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── lib ├── notifier.rb └── notifier │ ├── hud.rb │ ├── kdialog.rb │ ├── knotify.rb │ ├── notify_send.rb │ ├── osd_cat.rb │ ├── placebo.rb │ ├── snarl.rb │ ├── terminal_notifier.rb │ └── version.rb ├── notifier.gemspec └── test ├── notifier └── snarl_test.rb ├── notifier_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | .DS_Store 5 | *.lock 6 | /coverage 7 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | inherit_gem: 3 | rubocop-fnando: .rubocop.yml 4 | 5 | AllCops: 6 | TargetRubyVersion: 2.7 7 | NewCops: enable 8 | Exclude: 9 | - vendor/**/* 10 | - gemfiles/**/* 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "http://rubygems.org" 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Nando Vieira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the 'Software'), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notifier 2 | 3 | [![Gem](https://img.shields.io/gem/v/notifier.svg)](https://rubygems.org/gems/notifier) 4 | [![Gem](https://img.shields.io/gem/dt/notifier.svg)](https://rubygems.org/gems/notifier) 5 | 6 | Send system notifications on several platforms with a simple and unified API. 7 | Currently supports: 8 | 9 | - terminal-notifier (Notification Center wrapper for Mac OS X) 10 | - [HUD](https://fnando.gumroad.com/l/hud-macos) 11 | - Kdialog (Linux/KDE) 12 | - Knotify (Linux/KDE) 13 | - OSD Cat (Linux) 14 | - Libnotify (Linux) 15 | - Snarl (Windows) 16 | 17 | ## Installation 18 | 19 | gem install notifier 20 | 21 | ### Mac OS X 22 | 23 | #### terminal-notifier 24 | 25 | - Install terminal-notifier - https://github.com/alloy/terminal-notifier 26 | 27 | terminal-notifier also supports two additional flags: 28 | 29 | - `subtitle` 30 | - `sound` 31 | 32 | See terminal-notifier's help for additional information. 33 | 34 | #### hud 35 | 36 | - Install HUD - https://fnando.gumroad.com/l/hud-macos 37 | 38 | ### Linux 39 | 40 | If you're a linux guy, you can choose one of these methods: 41 | 42 | - Install libnotify-bin and its dependencies: 43 | `sudo aptitude install libnotify-bin` 44 | - Install xosd-bin: `sudo aptitude install xosd-bin` 45 | - KDE users don't need to install anything: Test Notifier will use +knotify+ or 46 | +kdialog+. 47 | 48 | ### Windows 49 | 50 | - Install Snarl: download from http://www.fullphat.net 51 | - Install ruby-snarl: `gem install ruby-snarl` 52 | 53 | ## Usage 54 | 55 | Notifier will try to detect which notifiers are available in your system. So you 56 | can just send a message: 57 | 58 | ```ruby 59 | Notifier.notify( 60 | image: "image.png", 61 | title: "Testing Notifier", 62 | message: "Sending an important message!" 63 | ) 64 | ``` 65 | 66 | Not all notifiers support the image option, therefore it will be ignored. 67 | 68 | If your system support more than one notifier, you can specify which one you 69 | prefer: 70 | 71 | ```ruby 72 | Notifier.default_notifier = :notify_send 73 | ``` 74 | 75 | Alternatively, you can set the default notifier by using the `NOTIFIER` env var. 76 | The following example assumes `test_notifier` is configured on this Rails 77 | project. The env var has precedence of `Notifier.default_notifier`. 78 | 79 | ```console 80 | $ NOTIFIER=hud rails test 81 | ``` 82 | 83 | The available names are `terminal_notifier`, `kdialog`, `knotify`, 84 | `notify_send`, `osd_cat`, and `snarl`. 85 | 86 | There are several helper methods that you can use in order to retrieve 87 | notifiers. 88 | 89 | - `Notifier.notifier`: return the first supported notifier 90 | - `Notifier.notifiers`: return all notifiers 91 | - `Notifier.supported_notifiers`: return only supported notifiers 92 | - `Notifier.from_name(name)`: find notifier by its name 93 | - `Notifier.supported_notifier_from_name(name)`: find a supported notifier by 94 | its name 95 | 96 | ## Creating custom notifiers 97 | 98 | To create a new notifier, just create a module on `Notifier` namespace that 99 | implements the following interface: 100 | 101 | ```ruby 102 | module Notifier 103 | module MyCustomNotifier 104 | def self.supported? 105 | end 106 | 107 | def self.notify(options) 108 | end 109 | end 110 | end 111 | ``` 112 | 113 | ## Maintainer 114 | 115 | - Nando Vieira - https://nandovieira.com 116 | 117 | ## Contributors 118 | 119 | https://github.com/fnando/notifier/graphs/contributors 120 | 121 | ## License 122 | 123 | (The MIT License) 124 | 125 | Permission is hereby granted, free of charge, to any person obtaining a copy of 126 | this software and associated documentation files (the 'Software'), to deal in 127 | the Software without restriction, including without limitation the rights to 128 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 129 | the Software, and to permit persons to whom the Software is furnished to do so, 130 | subject to the following conditions: 131 | 132 | The above copyright notice and this permission notice shall be included in all 133 | copies or substantial portions of the Software. 134 | 135 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 136 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 137 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 138 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 139 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 140 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 141 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | require "rubocop/rake_task" 6 | 7 | Rake::TestTask.new(:test) do |t| 8 | t.libs << "test" 9 | t.test_files = FileList["test/**/*_test.rb"] 10 | t.warning = false 11 | end 12 | 13 | RuboCop::RakeTask.new 14 | 15 | task default: %i[test rubocop] 16 | -------------------------------------------------------------------------------- /lib/notifier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "open3" 4 | require "socket" 5 | require "digest/md5" 6 | require "timeout" 7 | require "rbconfig" 8 | require "English" 9 | require "net/http" 10 | 11 | module Notifier 12 | require "notifier/snarl" 13 | require "notifier/hud" 14 | require "notifier/osd_cat" 15 | require "notifier/knotify" 16 | require "notifier/kdialog" 17 | require "notifier/notify_send" 18 | require "notifier/placebo" 19 | require "notifier/terminal_notifier" 20 | require "notifier/version" 21 | 22 | extend self 23 | 24 | class << self 25 | attr_accessor :default_notifier 26 | end 27 | 28 | def skip_constants 29 | @skip_constants ||= %w[Placebo Adapters Version] 30 | end 31 | 32 | def notifier 33 | supported_notifier_from_name(ENV.fetch("NOTIFIER", default_notifier)) || 34 | supported_notifiers.first 35 | end 36 | 37 | def notify(options) 38 | notifier.notify(options) 39 | end 40 | 41 | def notifiers 42 | notifiers = constants.filter_map do |name| 43 | const_get(name) unless skip_constants.include?(name.to_s) 44 | end 45 | 46 | notifiers.sort_by(&:name) + [Placebo] 47 | end 48 | 49 | def supported_notifiers 50 | notifiers.select(&:supported?) 51 | end 52 | 53 | def from_name(name) 54 | const_get(classify(name.to_s)) 55 | rescue StandardError 56 | nil 57 | end 58 | 59 | def supported_notifier_from_name(name) 60 | notifier = from_name(name) 61 | notifier&.supported? ? notifier : nil 62 | end 63 | 64 | def os?(regex) 65 | RUBY_PLATFORM =~ regex || RbConfig::CONFIG["host_os"] =~ regex 66 | end 67 | 68 | private def classify(string) 69 | string.gsub(/_(.)/sm) { Regexp.last_match(1).upcase.to_s } 70 | .gsub(/^(.)/) { Regexp.last_match(1).upcase.to_s } 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/notifier/hud.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Notifier 4 | module Hud 5 | extend self 6 | 7 | def supported? 8 | Notifier.os?(/darwin/) && hud_bin 9 | end 10 | 11 | def hud_bin 12 | @hud_bin ||= [ 13 | "/Applications/hud.app/Contents/MacOS/cli", 14 | "~/Applications/hud.app/Contents/MacOS/cli" 15 | ].first {|path| File.expand_path(path) } 16 | end 17 | 18 | def notify(options) 19 | command = [ 20 | hud_bin, 21 | "--title", 22 | options[:title].to_s, 23 | "--message", 24 | options[:message].to_s, 25 | "--symbol-name", 26 | options[:image].to_s 27 | ] 28 | 29 | Thread.new { system(*command) }.join 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/notifier/kdialog.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Notifier 4 | module Kdialog 5 | extend self 6 | 7 | def supported? 8 | Notifier.os?(/(linux|freebsd)/) && 9 | `which kdialog > /dev/null` && 10 | $CHILD_STATUS == 0 11 | end 12 | 13 | def notify(options) 14 | command = [ 15 | "kdialog", 16 | "--title", options[:title].to_s, 17 | "--passivepopup", options[:message].to_s, 18 | "5" 19 | ] 20 | 21 | Thread.new { system(*command) }.join 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/notifier/knotify.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Notifier 4 | module Knotify 5 | extend self 6 | 7 | def supported? 8 | Notifier.os?(/(linux|freebsd)/) && 9 | `ps -Al | grep dcop` && 10 | $CHILD_STATUS == 0 11 | end 12 | 13 | def notify(options) 14 | command = [ 15 | "dcop", 16 | "knotify", 17 | "default", 18 | "notify", 19 | "eventname", 20 | options[:title].to_s, 21 | options[:message].to_s, 22 | "", 23 | "", 24 | "16", 25 | "2" 26 | ] 27 | 28 | Thread.new { system(*command) }.join 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/notifier/notify_send.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Notifier 4 | module NotifySend 5 | extend self 6 | 7 | def supported? 8 | Notifier.os?(/(linux|freebsd)/) && 9 | `which notify-send > /dev/null` && 10 | $CHILD_STATUS == 0 11 | end 12 | 13 | def notify(options) 14 | command = [ 15 | "notify-send", "-i", 16 | options[:image].to_s, 17 | options[:title].to_s, 18 | options[:message].to_s 19 | ] 20 | 21 | Thread.new { system(*command) }.join 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/notifier/osd_cat.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Notifier 4 | module OsdCat 5 | extend self 6 | 7 | def supported? 8 | Notifier.os?(/(linux|freebsd)/) && 9 | `which osd_cat > /dev/null` && 10 | $CHILD_STATUS == 0 11 | end 12 | 13 | def notify(options) 14 | color = options.fetch(:color, "white").to_s 15 | 16 | command = [ 17 | "osd_cat", 18 | "--shadow", "0", 19 | "--colour", color, 20 | "--pos", "top", 21 | "--offset", "10", 22 | "--align", "center", 23 | "--font", "-bitstream-bitstream charter-bold-r-*-*-*-350-*-*-*-*-*-*", 24 | "--delay", "5", 25 | "--outline", "4" 26 | ] 27 | 28 | Thread.new do 29 | Open3.popen3(*command) do |stdin, _stdout, _stderr| 30 | stdin.puts options[:message] 31 | stdin.close 32 | end 33 | end.join 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/notifier/placebo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Notifier 4 | module Placebo 5 | extend self 6 | 7 | def supported? 8 | true 9 | end 10 | 11 | def notify(options) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/notifier/snarl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Notifier 4 | module Snarl 5 | extend self 6 | 7 | def supported? 8 | return false unless Notifier.os?(/(mswin|mingw)/) 9 | 10 | begin 11 | require "snarl" unless defined?(::Snarl) 12 | true 13 | rescue LoadError 14 | false 15 | end 16 | end 17 | 18 | def notify(options) 19 | ::Snarl.show_message(options[:title], options[:message], options[:image]) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/notifier/terminal_notifier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Notifier 4 | module TerminalNotifier 5 | extend self 6 | 7 | def supported? 8 | Notifier.os?(/darwin/) && `which terminal-notifier` && $CHILD_STATUS == 0 9 | end 10 | 11 | def notify(options) 12 | command = [ 13 | "terminal-notifier", 14 | "-group", "notifier-rubygems", 15 | "-title", options[:title].to_s, 16 | "-appIcon", options.fetch(:image, "").to_s, 17 | "-message", options[:message].to_s, 18 | "-subtitle", options.fetch(:subtitle, "").to_s 19 | ] 20 | 21 | # -sound with an empty string (e.g., "") will trigger the 22 | # default sound so we need to check for it. 23 | if options[:sound] 24 | command.concat([ 25 | "-sound", 26 | options.fetch(:sound, "default").to_s 27 | ]) 28 | end 29 | 30 | Thread.new do 31 | Open3.popen3(*command) do |_stdin, _stdout, _stderr| 32 | # noop 33 | end 34 | end.join 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/notifier/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Notifier 4 | module Version 5 | MAJOR = 1 6 | MINOR = 2 7 | PATCH = 2 8 | STRING = "#{MAJOR}.#{MINOR}.#{PATCH}" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /notifier.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "./lib/notifier/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "notifier" 7 | s.version = Notifier::Version::STRING 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Nando Vieira"] 10 | s.email = ["me@fnando.com"] 11 | s.homepage = "http://rubygems.org/gems/notifier" 12 | s.summary = "Send system notifications on several platforms with a " \ 13 | "simple and unified API. Currently supports Libnotify, " \ 14 | "OSD, KDE (Knotify and Kdialog) and Snarl" 15 | s.description = s.summary 16 | s.required_ruby_version = ">= 2.7" 17 | s.metadata["rubygems_mfa_required"] = "true" 18 | 19 | s.files = `git ls-files`.split("\n") 20 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 21 | s.executables = `git ls-files -- bin/*`.split("\n").map do |f| 22 | File.basename(f) 23 | end 24 | s.require_paths = ["lib"] 25 | 26 | s.requirements << "terminal-notifier, Libnotify, OSD, KDE (Knotify and " \ 27 | "Kdialog) or Snarl" 28 | 29 | s.add_development_dependency "minitest-utils" 30 | s.add_development_dependency "mocha" 31 | s.add_development_dependency "rake" 32 | s.add_development_dependency "rubocop" 33 | s.add_development_dependency "rubocop-fnando" 34 | s.add_development_dependency "simplecov" 35 | s.add_development_dependency "test_notifier" 36 | end 37 | -------------------------------------------------------------------------------- /test/notifier/snarl_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class SnarlTest < Minitest::Test 6 | test "sends notification" do 7 | Snarl.expects(:show_message).with("TITLE", "MESSAGE", "IMAGE") 8 | 9 | Notifier::Snarl.notify(title: "TITLE", message: "MESSAGE", image: "IMAGE") 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/notifier_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class NotifierTest < Minitest::Test 6 | setup do 7 | Notifier.notifiers.each do |notifier| 8 | unless notifier == Notifier::Placebo 9 | notifier.stubs(:supported?).returns(false) 10 | end 11 | end 12 | 13 | Notifier.default_notifier = nil 14 | ENV.delete("NOTIFIER") 15 | end 16 | 17 | teardown do 18 | Notifier.default_notifier = :hud 19 | end 20 | 21 | test "retrieves list of supported notifiers" do 22 | Notifier::Snarl.stubs(:supported?).returns(true) 23 | Notifier::Knotify.stubs(:supported?).returns(true) 24 | 25 | assert_equal 3, Notifier.supported_notifiers.size 26 | end 27 | 28 | test "returns first available notifier" do 29 | Notifier::Snarl.stubs(:supported?).returns(true) 30 | Notifier::Knotify.stubs(:supported?).returns(true) 31 | Notifier::Hud.stubs(:supported?).returns(true) 32 | 33 | assert_equal Notifier::Hud, Notifier.notifier 34 | end 35 | 36 | test "prefers default notifier" do 37 | Notifier::Snarl.stubs(:supported?).returns(true) 38 | Notifier::Knotify.stubs(:supported?).returns(true) 39 | 40 | Notifier.default_notifier = :knotify 41 | 42 | assert_equal Notifier::Knotify, Notifier.notifier 43 | end 44 | 45 | test "prefers default notifier using env var" do 46 | ENV["NOTIFIER"] = "knotify" 47 | 48 | Notifier::Snarl.stubs(:supported?).returns(true) 49 | Notifier::Knotify.stubs(:supported?).returns(true) 50 | 51 | Notifier.default_notifier = :snarl 52 | 53 | assert_equal Notifier::Knotify, Notifier.notifier 54 | end 55 | 56 | test "sends notification" do 57 | params = { 58 | title: "Some title", 59 | message: "Some message", 60 | image: "image.png" 61 | } 62 | 63 | Notifier::Snarl.stubs(:supported?).returns(true) 64 | Notifier::Snarl.expects(:notify).with(params) 65 | 66 | Notifier.notify(params) 67 | end 68 | 69 | test "retrieves list of all notifiers" do 70 | assert_equal 8, Notifier.notifiers.size 71 | end 72 | 73 | test "considers Placebo as fallback notifier" do 74 | assert_equal Notifier::Placebo, Notifier.supported_notifiers.last 75 | end 76 | 77 | test "returns notifier by its name" do 78 | assert_equal Notifier::OsdCat, Notifier.from_name(:osd_cat) 79 | assert_equal Notifier::NotifySend, Notifier.from_name(:notify_send) 80 | assert_equal Notifier::Hud, Notifier.from_name(:hud) 81 | end 82 | 83 | test "returns notifier by its name when supported" do 84 | Notifier::Snarl.expects(:supported?).returns(true) 85 | assert_equal Notifier::Snarl, Notifier.supported_notifier_from_name(:snarl) 86 | end 87 | 88 | test "returns nil when have no supported notifiers" do 89 | assert_nil Notifier.supported_notifier_from_name(:snarl) 90 | end 91 | 92 | test "returns nil when an invalid notifier name is provided" do 93 | assert_nil Notifier.from_name(:invalid) 94 | assert_nil Notifier.supported_notifier_from_name(:invalid) 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV.delete("NOTIFIER") 4 | 5 | require "simplecov" 6 | SimpleCov.start 7 | 8 | require "bundler/setup" 9 | require "notifier" 10 | require "minitest/utils" 11 | require "minitest/autorun" 12 | 13 | module Snarl 14 | def self.show_message(*) 15 | end 16 | end 17 | --------------------------------------------------------------------------------