win_toaster 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 21777567d049b3c1e516133d5f876e82f90d0ca1b44953b82f4161c01d982a55
4
+ data.tar.gz: 1dd039962e19df0c0e5bb849b5ea8a5afd907b2b9deea04d98d6e6d0becb637e
5
+ SHA512:
6
+ metadata.gz: 1493bcb149010cf4b85d85763f3898585580830de78622194f12afdc1acbe7d94ecf8072f70b1151e2b344d4aed03d8d5f6308033dfdf009991768c23bacee19
7
+ data.tar.gz: f5f2ed18c74f7155310b622a2efa4f91f5b249a712a64c87a5e063cd13b112d6768f0abd5ed5fd71dcf366e7e85b40f315667edd9b0fd2ad4f8fb8ae40e6820d
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-06-19
4
+
5
+ - Initial release
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "win_toaster" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["yuuji.yaginuma@gmail.com"](mailto:"yuuji.yaginuma@gmail.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yuji Yaginuma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all 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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # WinToaster
2
+
3
+ Show native Windows toast notifications from **WSL** or **Windows**.
4
+
5
+ `win_toaster` invokes Windows PowerShell (`powershell.exe`) and uses the
6
+ `Windows.UI.Notifications` API to display a toast. It works both from WSL and
7
+ from Ruby running on Windows directly, since it only needs `powershell.exe` on
8
+ `PATH`. The notification text and the PowerShell script are Base64-encoded before
9
+ being handed to PowerShell, so non-ASCII text and shell metacharacters are passed
10
+ through safely.
11
+
12
+ ## Requirements
13
+
14
+ - Windows 10 / 11 with `powershell.exe` available on `PATH`, either:
15
+ - inside WSL (`powershell.exe` is reachable via WSL interop), or
16
+ - on Windows directly
17
+ - Ruby >= 3.2
18
+
19
+ ## Installation
20
+
21
+ Install the gem:
22
+
23
+ ```bash
24
+ gem install win_toaster
25
+ ```
26
+
27
+ Or add it to your `Gemfile`:
28
+
29
+ ```ruby
30
+ gem "win_toaster"
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ### Ruby
36
+
37
+ ```ruby
38
+ require "win_toaster"
39
+
40
+ WinToaster.notify(title: "Build finished", message: "All tests are green")
41
+
42
+ # An optional third line, images, and a custom AppUserModelId:
43
+ WinToaster.notify(
44
+ title: "Deploy finished",
45
+ message: "Production has been updated",
46
+ detail: "took 3m",
47
+ image: "/mnt/c/Users/me/Pictures/icon.png", # small icon (appLogoOverride)
48
+ hero: "/mnt/c/Users/me/Pictures/banner.png", # large banner image
49
+ app_id: '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
50
+ )
51
+ ```
52
+
53
+ `WinToaster.notify` returns `true` on success and raises `WinToaster::Error`
54
+ on failure (e.g. when `powershell.exe` cannot be found).
55
+
56
+ #### Images
57
+
58
+ `image:` is shown as the small icon (`appLogoOverride`) and `hero:` as the large
59
+ banner image. Both take a file path. From WSL you can pass a Linux/WSL path
60
+ (e.g. `/mnt/c/...` or `/home/...`); it is converted to a Windows path with
61
+ `wslpath -w` automatically. On native Windows the path is used as-is. The path
62
+ must be absolute and point to a file Windows can read; an unreadable path is
63
+ silently dropped by Windows (the rest of the toast still shows).
64
+
65
+ ### CLI
66
+
67
+ ```bash
68
+ win_toaster "Build finished" "All tests are green"
69
+ win_toaster "Build finished" "All green" --detail "took 3m"
70
+ win_toaster "Deploy finished" "Production updated" --image /mnt/c/Users/me/Pictures/icon.png
71
+ win_toaster --help
72
+ ```
73
+
74
+ ## Default AppId
75
+
76
+ When you don't pass `app_id`, win_toaster uses Windows PowerShell's AppUserModelId:
77
+
78
+ ```
79
+ {1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe
80
+ ```
81
+
82
+ A toast can only be displayed under an AppUserModelId (AUMID) that is registered
83
+ with the system via a Start menu shortcut — `CreateToastNotifier` fails to show
84
+ the toast otherwise. Windows ships with such a shortcut for Windows PowerShell,
85
+ so this AUMID is already registered and works out of the box without you having
86
+ to register an application of your own. Notifications then appear attributed to
87
+ "Windows PowerShell". The leading GUID `{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}`
88
+ is the well-known `FOLDERID_System` known-folder id (the `System32` folder); it
89
+ is a public constant, not a secret.
90
+
91
+ To show notifications under your own app name/icon, register a custom AUMID and
92
+ pass it via `app_id:` (or `--app-id`).
93
+
94
+ References (official Microsoft docs):
95
+
96
+ - [How to enable desktop toast notifications through an AppUserModelID](https://learn.microsoft.com/en-us/windows/win32/shell/enable-desktop-toast-with-appusermodelid)
97
+ — a Start-menu shortcut carrying an AUMID is required to raise a toast.
98
+ - [ToastNotificationManager.CreateToastNotifier](https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toastnotificationmanager.createtoastnotifier)
99
+ — the API used here; the AUMID must match the shortcut or the toast is not shown.
100
+ - [KNOWNFOLDERID](https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid)
101
+ — `FOLDERID_System` is defined as `{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}`.
102
+
103
+ ## How it works
104
+
105
+ 1. Builds the toast XML (`ToastGeneric` template), XML-escaping each text node.
106
+ 2. Base64-encodes the XML and embeds it in a small PowerShell script.
107
+ 3. Base64-encodes the whole script (UTF-16LE) and runs
108
+ `powershell.exe -NoProfile -NonInteractive -EncodedCommand <script>`.
109
+
110
+ Using `-EncodedCommand` avoids shell-quoting problems and the UNC-path warning
111
+ that `-File` triggers when launched from a WSL working directory. The same
112
+ encoded invocation works whether PowerShell is reached through WSL interop or
113
+ run on Windows directly.
114
+
115
+ ## Development
116
+
117
+ After checking out the repo, run `bin/setup` to install dependencies. Then run
118
+ `rake test` to run the tests. The test suite stubs out PowerShell, so it runs on
119
+ any platform (no `powershell.exe` required).
120
+
121
+ To install this gem onto your local machine, run `bundle exec rake install`.
122
+
123
+ ## Contributing
124
+
125
+ Bug reports and pull requests are welcome on GitHub at https://github.com/y-yagi/win_toaster.
126
+
127
+ ## License
128
+
129
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
data/exe/win_toaster ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "win_toaster"
5
+ require "win_toaster/cli"
6
+
7
+ exit WinToaster::CLI.run(ARGV)
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "../win_toaster"
5
+
6
+ module WinToaster
7
+ # Command-line front-end for win_toaster.
8
+ #
9
+ # win_toaster TITLE MESSAGE [--detail DETAIL] [--app-id APP_ID]
10
+ class CLI
11
+ # Runs the CLI and returns the process exit code.
12
+ def self.run(argv)
13
+ new.run(argv)
14
+ end
15
+
16
+ def run(argv)
17
+ options = { detail: nil, image: nil, hero: nil, app_id: Notifier::DEFAULT_APP_ID }
18
+
19
+ parser = OptionParser.new do |o|
20
+ o.banner = "Usage: win_toaster TITLE MESSAGE [options]"
21
+ o.on("-d", "--detail DETAIL", "Third line shown below the message") { |v| options[:detail] = v }
22
+ o.on("-i", "--image PATH", "Icon image (appLogoOverride)") { |v| options[:image] = v }
23
+ o.on("--hero PATH", "Hero (large banner) image") { |v| options[:hero] = v }
24
+ o.on("-a", "--app-id APP_ID", "AppUserModelId the toast is shown under") { |v| options[:app_id] = v }
25
+ o.on("-v", "--version", "Show version and exit") do
26
+ puts WinToaster::VERSION
27
+ return 0
28
+ end
29
+ o.on("-h", "--help", "Show this help and exit") do
30
+ puts o
31
+ return 0
32
+ end
33
+ end
34
+
35
+ title, message = parser.parse(argv)
36
+
37
+ if title.nil? || message.nil?
38
+ warn parser.banner
39
+ return 1
40
+ end
41
+
42
+ WinToaster.notify(
43
+ title: title, message: message, detail: options[:detail],
44
+ image: options[:image], hero: options[:hero], app_id: options[:app_id]
45
+ )
46
+ 0
47
+ rescue OptionParser::ParseError, WinToaster::Error => e
48
+ warn "win_toaster: #{e.message}"
49
+ 1
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module WinToaster
6
+ # Builds a Windows toast notification and shows it by invoking Windows
7
+ # PowerShell (powershell.exe) from WSL. The toast XML and the PowerShell
8
+ # script are Base64-encoded so that arbitrary text (including Japanese and
9
+ # shell metacharacters) survives the WSL -> Windows boundary without any
10
+ # quoting or injection concerns.
11
+ class Notifier
12
+ # Default AppUserModelId. This is Windows PowerShell's registered id, taken
13
+ # from the reference article. Toasts shown under it appear as coming from
14
+ # "Windows PowerShell". Override via +app_id+ to use your own.
15
+ DEFAULT_APP_ID = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
16
+
17
+ # The Windows PowerShell executable, resolved from PATH inside WSL.
18
+ POWERSHELL = "powershell.exe"
19
+
20
+ attr_reader :title, :message, :detail, :image, :hero, :app_id
21
+
22
+ # +image+ is shown as the small icon (appLogoOverride) and +hero+ as the
23
+ # large banner image. Both take a file path; from WSL a Linux/WSL path
24
+ # (e.g. /mnt/c/... or /home/...) is accepted and converted to a Windows
25
+ # path automatically.
26
+ def initialize(title:, message:, detail: nil, image: nil, hero: nil, app_id: DEFAULT_APP_ID)
27
+ @title = title
28
+ @message = message
29
+ @detail = detail
30
+ @image = image
31
+ @hero = hero
32
+ @app_id = app_id
33
+ end
34
+
35
+ # Builds the toast XML using the ToastGeneric template. Text nodes are
36
+ # XML-escaped. The +detail+ line and image nodes are omitted when nil.
37
+ def build_xml
38
+ nodes = [title, message, detail].compact.map { |t| "<text>#{escape_xml(t)}</text>" }
39
+ nodes << image_node("appLogoOverride", image) if image
40
+ nodes << image_node("hero", hero) if hero
41
+ %(<toast><visual><binding template="ToastGeneric">#{nodes.join}</binding></visual></toast>)
42
+ end
43
+
44
+ # The PowerShell script that decodes the XML and shows the toast.
45
+ def powershell_script
46
+ xml_b64 = [build_xml.encode("UTF-8")].pack("m0")
47
+ app_id_literal = app_id.gsub("'", "''")
48
+ <<~PS
49
+ $ErrorActionPreference = 'Stop'
50
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
51
+ [Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
52
+ [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
53
+ $xmlText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('#{xml_b64}'))
54
+ $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
55
+ $xml.LoadXml($xmlText)
56
+ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
57
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('#{app_id_literal}').Show($toast)
58
+ PS
59
+ end
60
+
61
+ # The argv used to launch PowerShell with the script as an EncodedCommand.
62
+ # Using -EncodedCommand avoids shell quoting issues and the UNC-path warning
63
+ # that -File triggers when run from a WSL working directory.
64
+ def powershell_command
65
+ encoded = [powershell_script.encode("UTF-16LE")].pack("m0")
66
+ [POWERSHELL, "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded]
67
+ end
68
+
69
+ # Builds and shows the toast. Returns true on success and raises
70
+ # WinToaster::Error on any failure.
71
+ def deliver
72
+ _stdout, stderr, status = Open3.capture3(*powershell_command)
73
+ unless status.success?
74
+ raise Error, "failed to show toast (exit #{status.exitstatus}): #{stderr.strip}"
75
+ end
76
+
77
+ true
78
+ rescue Errno::ENOENT
79
+ raise Error, "#{POWERSHELL} not found. win_toaster requires Windows PowerShell, " \
80
+ "so run it from WSL or from Ruby on Windows."
81
+ end
82
+
83
+ private
84
+
85
+ def image_node(placement, path)
86
+ %(<image placement="#{placement}" src="#{escape_xml(windows_path(path))}"/>)
87
+ end
88
+
89
+ # Converts a file path to a Windows path that the toast can load. From WSL,
90
+ # `wslpath -w` maps the path (e.g. /mnt/c/... or /home/...) to its Windows
91
+ # form. On native Windows wslpath is absent, so the absolute path is used
92
+ # as-is.
93
+ def windows_path(path)
94
+ expanded = File.expand_path(path)
95
+ out, _err, status = Open3.capture3("wslpath", "-w", expanded)
96
+ status.success? ? out.strip : expanded
97
+ rescue Errno::ENOENT
98
+ expanded
99
+ end
100
+
101
+ def escape_xml(text)
102
+ text.to_s
103
+ .gsub("&", "&amp;")
104
+ .gsub("<", "&lt;")
105
+ .gsub(">", "&gt;")
106
+ .gsub('"', "&quot;")
107
+ .gsub("'", "&apos;")
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WinToaster
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "win_toaster/version"
4
+
5
+ module WinToaster
6
+ class Error < StandardError; end
7
+
8
+ # Show a Windows toast notification from WSL.
9
+ #
10
+ # WinToaster.notify(title: "Build finished", message: "All green")
11
+ #
12
+ # +image+ (small icon) and +hero+ (large banner) take a file path; from WSL a
13
+ # Linux/WSL path is accepted and converted to a Windows path automatically.
14
+ #
15
+ # Returns true on success and raises WinToaster::Error on failure.
16
+ def self.notify(title:, message:, detail: nil, image: nil, hero: nil, app_id: Notifier::DEFAULT_APP_ID)
17
+ Notifier.new(
18
+ title: title, message: message, detail: detail,
19
+ image: image, hero: hero, app_id: app_id
20
+ ).deliver
21
+ end
22
+ end
23
+
24
+ require_relative "win_toaster/notifier"
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: win_toaster
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yuji Yaginuma
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: win_toaster shows native Windows toast notifications by invoking Windows
13
+ PowerShell and the Windows.UI.Notifications API. It works both from WSL and from
14
+ Ruby running on Windows directly.
15
+ email:
16
+ - yuuji.yaginuma@gmail.com
17
+ executables:
18
+ - win_toaster
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - CHANGELOG.md
23
+ - CODE_OF_CONDUCT.md
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - exe/win_toaster
28
+ - lib/win_toaster.rb
29
+ - lib/win_toaster/cli.rb
30
+ - lib/win_toaster/notifier.rb
31
+ - lib/win_toaster/version.rb
32
+ homepage: https://github.com/y-yagi/win_toaster
33
+ licenses:
34
+ - MIT
35
+ metadata:
36
+ allowed_push_host: https://rubygems.org
37
+ homepage_uri: https://github.com/y-yagi/win_toaster
38
+ source_code_uri: https://github.com/y-yagi/win_toaster
39
+ changelog_uri: https://github.com/y-yagi/win_toaster/blob/main/CHANGELOG.md
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.2.0
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 4.0.14
55
+ specification_version: 4
56
+ summary: Show Windows toast notifications from WSL or native Windows.
57
+ test_files: []