wintoast 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 +7 -0
- data/CHANGELOG.md +40 -0
- data/LICENSE.txt +21 -0
- data/README.md +335 -0
- data/ext/wintoast/extconf.rb +36 -0
- data/ext/wintoast/wintoast.cpp +501 -0
- data/lib/wintoast/payload.rb +195 -0
- data/lib/wintoast/version.rb +5 -0
- data/lib/wintoast.rb +270 -0
- metadata +127 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3a5ffc4e1ef7ddebe12e6096aa293b1d70f96a8bb8c7fdba4e6c807dc80578f6
|
|
4
|
+
data.tar.gz: d7d1357b903a3582b4e51b5616a83e13f4a1c0dbbc94c273c9bf3d8234c1daef
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1f0271faa0e9e3ee302a7e54d74eff648c75538432ae821b58fc338b2eb25c5da1f0cff9e034da31ad94ad44c515967975b05d420ee9784e1cdd7f106c90e14c
|
|
7
|
+
data.tar.gz: 48714a218686fdfb01849299231162881f70302e8ae9278ce1dc3f6858ab8505da5a88fdcc87aa4acff32ab287effe9844d0f1234f5c5ff32411ad1282adfa91
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-06-27
|
|
10
|
+
|
|
11
|
+
Initial release.
|
|
12
|
+
|
|
13
|
+
- `Wintoast.toast(title, body = nil, ...)` — fire-and-forget toast notification
|
|
14
|
+
via the inbox WinRT API (no App SDK, no packaging, no COM server). Supports
|
|
15
|
+
`aumid:`, `attribution:`, `image:`/`hero:` (local absolute paths) with
|
|
16
|
+
`circle:`, `audio:` (system sounds or `false` for silent), `duration:`,
|
|
17
|
+
`scenario:` (`:alarm`/`:incoming_call`/`:urgent`), `expires_at:`/`expires_in:`,
|
|
18
|
+
and `tag:`/`group:` replacement keys. Returns `nil` (accepted, not necessarily
|
|
19
|
+
displayed — silent suppression is undetectable by design of the platform).
|
|
20
|
+
- `Wintoast.register!(aumid:, display_name:, icon: nil)` /
|
|
21
|
+
`Wintoast.unregister!(aumid:)` — opt-in, reversible, per-user branding that
|
|
22
|
+
writes only `HKCU\Software\Classes\AppUserModelId\<aumid>`. No elevation, no
|
|
23
|
+
Start-Menu shortcut, no HKLM, no COM activator. `unregister!` is idempotent
|
|
24
|
+
(`false` when the key was absent).
|
|
25
|
+
- `Wintoast.progress(value = nil, of: 100, state: nil)` /
|
|
26
|
+
`Wintoast.progress_clear` — taskbar (`ITaskbarList3`) and Windows Terminal tab
|
|
27
|
+
(OSC 9;4) progress on every call; exactly one renders per host. Returns
|
|
28
|
+
`true`/`false` (accepted, not necessarily visible); environmental failure is a
|
|
29
|
+
`false`, never an exception. Never writes OSC bytes to a redirected stdout.
|
|
30
|
+
- `Wintoast::Payload.build(title, body = nil, ...)` — the pure-Ruby
|
|
31
|
+
`ToastGeneric` XML builder, exposed for debugging and as the escaping /
|
|
32
|
+
audio-mapping test seam.
|
|
33
|
+
- `Wintoast::POWERSHELL_AUMID` — the registered-everywhere default AUMID, so
|
|
34
|
+
`Wintoast.toast("hi")` works on a stock machine (branded "Windows PowerShell").
|
|
35
|
+
- `Wintoast::Error` / `Wintoast::OSError` (with `#code`) error hierarchy.
|
|
36
|
+
|
|
37
|
+
Windows MSVC (mswin) Ruby only; Windows 10 1809+ / Windows 11, x64.
|
|
38
|
+
|
|
39
|
+
[Unreleased]: https://github.com/main-path/wintoast/compare/v0.1.0...HEAD
|
|
40
|
+
[0.1.0]: https://github.com/main-path/wintoast/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ned
|
|
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 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,
|
|
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# wintoast
|
|
2
|
+
|
|
3
|
+
**Fire-and-forget Windows toast notifications and taskbar/terminal progress for Ruby — inbox WinRT and shell APIs only, no App SDK, no packaging, no COM server.**
|
|
4
|
+
|
|
5
|
+
Scripts deserve native notifications. When a backup finishes, a build breaks, or
|
|
6
|
+
a long job crosses 50%, a plain `ruby.exe` should be able to pop a real Windows
|
|
7
|
+
toast and light up the taskbar — without bundling a runtime or registering a COM
|
|
8
|
+
activation server.
|
|
9
|
+
|
|
10
|
+
That gap is real. PowerShell's [BurntToast] exists, but there is nothing
|
|
11
|
+
native-and-thin for Ruby; the [Windows App SDK] notification path drags in a
|
|
12
|
+
NuGet runtime, an MSIX singleton package, and a bootstrapper — against this
|
|
13
|
+
suite's OS-libraries-only rule. `wintoast` uses the **inbox** WinRT notification
|
|
14
|
+
API (`Windows.UI.Notifications`, shipped with Windows since 10240) via
|
|
15
|
+
`RoGetActivationFactory`, plus `ITaskbarList3` and the terminal OSC 9;4 progress
|
|
16
|
+
sequence. Nothing to install but the gem.
|
|
17
|
+
|
|
18
|
+
> Not to be confused with [mohabouje/WinToast], a C++ library — different
|
|
19
|
+
> ecosystem, same idea.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "wintoast"
|
|
23
|
+
|
|
24
|
+
Wintoast.toast("Backup finished", "1,204 files in 38 s")
|
|
25
|
+
# => nil (a banner pops; it persists in the Notification Center)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| What | API |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Pop a toast | `Wintoast.toast(title, body, ...)` |
|
|
31
|
+
| Brand it (opt-in) | `Wintoast.register!` / `Wintoast.unregister!` |
|
|
32
|
+
| Taskbar + tab progress | `Wintoast.progress` / `Wintoast.progress_clear` |
|
|
33
|
+
| Inspect the XML | `Wintoast::Payload.build` |
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- **Windows 10 1809+ or Windows 11** with a native **MSVC (mswin)** Ruby
|
|
38
|
+
(`x64-mswin64`). On a MinGW/UCRT Ruby this gem is not supported — its
|
|
39
|
+
`extconf.rb` will say so.
|
|
40
|
+
- Visual Studio 2017+ or the Build Tools with the **Desktop development with
|
|
41
|
+
C++** workload (for `cl.exe` + the Windows SDK headers/libs).
|
|
42
|
+
- x64. arm64-mswin is expected to work (all code is arch-neutral) but is
|
|
43
|
+
untested and unsupported until an arm64-mswin Ruby distribution exists.
|
|
44
|
+
|
|
45
|
+
> Building uses [`vcvars`](https://rubygems.org/gems/vcvars) to load the MSVC
|
|
46
|
+
> toolchain automatically — no "Developer Command Prompt" needed. If a build
|
|
47
|
+
> fails, run `vcvars doctor`.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
gem install wintoast
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
**Zero setup — works on a stock machine (branded "Windows PowerShell"):**
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
require "wintoast"
|
|
61
|
+
|
|
62
|
+
Wintoast.toast("Backup finished", "1,204 files in 38 s")
|
|
63
|
+
# => nil (a banner pops; it persists in the Notification Center)
|
|
64
|
+
|
|
65
|
+
# Soften the borrowed branding with an attribution line:
|
|
66
|
+
Wintoast.toast("Backup finished", "1,204 files in 38 s", attribution: "via backup.rb")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Own branding — one-time, per-user, reversible, no admin:**
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
AUMID = Wintoast.register!(aumid: "Acme.BackupTool", display_name: "Acme Backup",
|
|
73
|
+
icon: "C:/Acme/backup.png") # => "Acme.BackupTool"
|
|
74
|
+
|
|
75
|
+
Wintoast.toast("Backup finished", aumid: AUMID,
|
|
76
|
+
image: "C:/Acme/backup.png", circle: true,
|
|
77
|
+
audio: :mail, duration: :long,
|
|
78
|
+
expires_in: 3600, tag: "backup", group: "acme")
|
|
79
|
+
# A later toast with tag: "backup", group: "acme" REPLACES this one in Action Center.
|
|
80
|
+
|
|
81
|
+
Wintoast.unregister!(aumid: "Acme.BackupTool") # => true
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Progress around a work loop (taskbar under conhost, tab ring under Windows Terminal):**
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
begin
|
|
88
|
+
files.each_with_index do |f, i|
|
|
89
|
+
process(f)
|
|
90
|
+
Wintoast.progress(i + 1, of: files.size)
|
|
91
|
+
end
|
|
92
|
+
Wintoast.toast("Done", "#{files.size} files processed")
|
|
93
|
+
rescue => e
|
|
94
|
+
Wintoast.progress(100, state: :error) # red bar
|
|
95
|
+
Wintoast.toast("Failed", e.message, audio: :reminder)
|
|
96
|
+
raise
|
|
97
|
+
ensure
|
|
98
|
+
Wintoast.progress_clear
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Failure paths, demonstrated:**
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
Wintoast.toast("hi", image: "logo.png")
|
|
106
|
+
# => ArgumentError: wintoast: image: must be an absolute path to an existing file
|
|
107
|
+
|
|
108
|
+
Wintoast.toast("hi", aumid: "Not.Registered.Anywhere")
|
|
109
|
+
# => nil — and NOTHING is displayed. Unregistered AUMIDs are silently dropped by
|
|
110
|
+
# Windows (no error, no Action Center entry). Use register! or the default AUMID.
|
|
111
|
+
|
|
112
|
+
Wintoast.progress(50, state: :indeterminate)
|
|
113
|
+
# => ArgumentError: wintoast: state: :indeterminate ignores value — pass value nil
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## The one trap you must know about
|
|
117
|
+
|
|
118
|
+
**An unregistered AUMID is silently dropped by Windows.** `Show()` returns
|
|
119
|
+
success, but no banner pops, nothing lands in the Notification Center, and there
|
|
120
|
+
is no error, no event-log breadcrumb — nothing. This is the platform's behavior,
|
|
121
|
+
not the gem's, and it is structurally undetectable.
|
|
122
|
+
|
|
123
|
+
What "registered" means — one of:
|
|
124
|
+
|
|
125
|
+
- a **Start-Menu shortcut** carrying an `AppUserModelID` (what most installed
|
|
126
|
+
apps have), **or**
|
|
127
|
+
- a per-user **registry key** `HKCU\Software\Classes\AppUserModelId\<aumid>`
|
|
128
|
+
with a `DisplayName` — exactly what `Wintoast.register!` writes, **or**
|
|
129
|
+
- **package identity** (MSIX) — out of scope here.
|
|
130
|
+
|
|
131
|
+
`SetCurrentProcessExplicitAppUserModelID` is **not** registration: it stamps the
|
|
132
|
+
process AUMID for taskbar grouping / Jump Lists, and does nothing for
|
|
133
|
+
notifications. (A future `winshell` concern, not this gem's.)
|
|
134
|
+
|
|
135
|
+
That is why `Wintoast.toast` defaults to **Windows PowerShell's** AUMID
|
|
136
|
+
(`Wintoast::POWERSHELL_AUMID`): it is registered on every Windows box via
|
|
137
|
+
PowerShell's Start-Menu shortcut, so `Wintoast.toast("hi")` visibly works on a
|
|
138
|
+
stock machine. The trade-offs of borrowing it:
|
|
139
|
+
|
|
140
|
+
- the toast header reads **"Windows PowerShell"** (soften it with
|
|
141
|
+
`attribution:`, or switch to your own AUMID via `register!`);
|
|
142
|
+
- the per-app Settings toggle is **shared** — turning off "Windows PowerShell"
|
|
143
|
+
notifications kills every tool borrowing the default. `register!` gives you a
|
|
144
|
+
private toggle.
|
|
145
|
+
|
|
146
|
+
`Wintoast.toast` **never touches the registry.** Registration is an explicit,
|
|
147
|
+
consented, reversible act. There is deliberately **no `registered?`** query: it
|
|
148
|
+
could only check the registry route and would lie `false` for the common
|
|
149
|
+
shortcut-registered case (including the default AUMID).
|
|
150
|
+
|
|
151
|
+
## Toasts
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
Wintoast.toast(title, body = nil,
|
|
155
|
+
aumid: Wintoast::POWERSHELL_AUMID,
|
|
156
|
+
attribution: nil, # small "via ..." line
|
|
157
|
+
image: nil, # absolute path -> appLogoOverride
|
|
158
|
+
hero: nil, # absolute path -> hero (364x180 @100%)
|
|
159
|
+
circle: false, # hint-crop="circle" on image:
|
|
160
|
+
audio: :default, # see table; false => silent
|
|
161
|
+
duration: :short, # :short | :long
|
|
162
|
+
scenario: nil, # nil | :alarm | :incoming_call | :urgent
|
|
163
|
+
expires_at: nil, # Time -> ExpirationTime (OS caps at 3 days)
|
|
164
|
+
expires_in: nil, # Numeric seconds from now (mutually exclusive)
|
|
165
|
+
tag: nil, # String <= 64 chars (dedup/replace key)
|
|
166
|
+
group: nil) -> nil # String <= 64 chars
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**A normal return (`nil`) means the OS ACCEPTED the toast — NOT that it was
|
|
170
|
+
displayed.** Banners can be suppressed undetectably by Focus Assist /
|
|
171
|
+
Do-Not-Disturb (the toast still lands in the Notification Center), the per-app
|
|
172
|
+
toggle, or the `NoToastApplicationNotification` group policy. The return value is
|
|
173
|
+
`nil`, not `true`/`self`, precisely because the gem cannot honestly promise
|
|
174
|
+
display.
|
|
175
|
+
|
|
176
|
+
**Audio** (`audio:`) — only the system sounds Windows honors for unpackaged
|
|
177
|
+
apps; arbitrary file paths silently fall back to the default sound and are not
|
|
178
|
+
accepted:
|
|
179
|
+
|
|
180
|
+
| value | effect |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `:default` | OS default sound (no `<audio>` element) |
|
|
183
|
+
| `false` | silent |
|
|
184
|
+
| `:im` `:mail` `:reminder` `:sms` | the matching `ms-winsoundevent` sound |
|
|
185
|
+
| `:alarm`, `:alarm2`..`:alarm10` | looping alarm sounds |
|
|
186
|
+
| `:call`, `:call2`..`:call10` | looping call sounds |
|
|
187
|
+
|
|
188
|
+
Looping sounds only *audibly* loop when the toast is pinned by
|
|
189
|
+
`scenario: :alarm` / `:incoming_call` (a short alarm sound is still valid).
|
|
190
|
+
|
|
191
|
+
**Scenario** (`scenario:`) — `:alarm` and `:incoming_call` pin the toast and
|
|
192
|
+
loop audio; `:urgent` breaks through Do-Not-Disturb (Windows 11 build 22546+;
|
|
193
|
+
older builds ignore the attribute and show a normal toast). `:reminder` is
|
|
194
|
+
deliberately **not** accepted — without a button it is ignored by Windows, i.e.
|
|
195
|
+
useless for fire-and-forget.
|
|
196
|
+
|
|
197
|
+
**Expiration** (`expires_at:` Time / `expires_in:` seconds, mutually exclusive)
|
|
198
|
+
maps to `ExpirationTime` (removal from the Notification Center). Local toasts are
|
|
199
|
+
retained **at most 3 days** regardless; larger values pass through and are
|
|
200
|
+
clamped by the OS. `expires_in <= 0` raises; a past `expires_at` passes through
|
|
201
|
+
(the OS treats it as already expired).
|
|
202
|
+
|
|
203
|
+
**Tag / group** (`tag:`, `group:`, each ≤ 64 chars) map to
|
|
204
|
+
`IToastNotification2.Tag/Group`: a later toast with the same tag (+ group) under
|
|
205
|
+
the same AUMID **replaces** the earlier one in the Action Center. Pure
|
|
206
|
+
pass-through; there is no history/removal API in v1.
|
|
207
|
+
|
|
208
|
+
**Images** (`image:`, `hero:`) are **local absolute paths only** (http(s) images
|
|
209
|
+
need package identity and are rejected by the absolute-path check). A
|
|
210
|
+
relative/missing path raises `ArgumentError` so a typo fails loudly instead of
|
|
211
|
+
rendering an imageless toast. Paths are emitted as plain absolute paths (no
|
|
212
|
+
`file://` URI), sidestepping percent-encoding of spaces and Unicode.
|
|
213
|
+
|
|
214
|
+
Desktop apps **cannot schedule** toasts (`ScheduledToastNotification` needs
|
|
215
|
+
package identity) — out of scope.
|
|
216
|
+
|
|
217
|
+
## Progress
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
Wintoast.progress(value = nil, of: 100, state: nil) -> true | false
|
|
221
|
+
Wintoast.progress_clear -> true | false
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Every call drives **both** progress surfaces, unconditionally — exactly one
|
|
225
|
+
takes effect per host, the other no-ops (the strategy blessed in
|
|
226
|
+
[microsoft/terminal#14268]):
|
|
227
|
+
|
|
228
|
+
1. **OSC 9;4** written to the console — the Windows Terminal tab ring + WT-window
|
|
229
|
+
taskbar progress (also ConEmu and a growing set of terminals). Emitted **only
|
|
230
|
+
when STDOUT is a real console** (never pollutes redirected/piped output).
|
|
231
|
+
2. **`ITaskbarList3`** on `GetConsoleWindow()` — classic-conhost taskbar
|
|
232
|
+
progress.
|
|
233
|
+
|
|
234
|
+
| call | meaning |
|
|
235
|
+
|---|---|
|
|
236
|
+
| `progress(50)` / `progress(7, of: 23)` | determinate, green |
|
|
237
|
+
| `progress(50, state: :error)` | determinate, red |
|
|
238
|
+
| `progress(50, state: :paused)` | determinate, yellow |
|
|
239
|
+
| `progress(state: :indeterminate)` | marquee / ring (value must be nil) |
|
|
240
|
+
| `progress(nil)` / `progress(:clear)` / `progress_clear` | remove |
|
|
241
|
+
|
|
242
|
+
Host matrix:
|
|
243
|
+
|
|
244
|
+
| host | taskbar (ITaskbarList3) | tab/taskbar (OSC 9;4) |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| classic conhost | ✓ | swallowed |
|
|
247
|
+
| Windows Terminal | accepts, invisible (hidden ConPTY window) | ✓ |
|
|
248
|
+
| Windows Terminal + redirected stdout | accepts, invisible | ✗ (no OSC to a pipe) |
|
|
249
|
+
| ConEmu | — | ✓ |
|
|
250
|
+
| no console (rubyw / detached) | — | — → returns `false` |
|
|
251
|
+
|
|
252
|
+
**The return value means accepted, not visible** — the same honesty rule as
|
|
253
|
+
`toast`'s `nil`. `true` means at least one OS surface accepted the update (OSC
|
|
254
|
+
bytes reached a real console, or every `ITaskbarList3` call succeeded against a
|
|
255
|
+
non-NULL console window). Under a ConPTY host the taskbar leg accepts against a
|
|
256
|
+
hidden window that can never render, so e.g. running under Windows Terminal with
|
|
257
|
+
stdout redirected returns `true` with nothing visible. `false` is guaranteed
|
|
258
|
+
only when no surface accepted at all.
|
|
259
|
+
|
|
260
|
+
**Environmental failure is a `false`, never an exception** — a progress bar must
|
|
261
|
+
not be able to crash the app. Only argument misuse raises `ArgumentError`
|
|
262
|
+
(`of: 0`, a missing value for a determinate state, a value for `:indeterminate`,
|
|
263
|
+
an unknown state, a non-Numeric value). Overshoot like `progress(101)` is
|
|
264
|
+
**clamped**, not raised.
|
|
265
|
+
|
|
266
|
+
**Clearing on exit is the caller's job** — put `Wintoast.progress_clear` in an
|
|
267
|
+
`ensure`. v1 installs no `at_exit` hook.
|
|
268
|
+
|
|
269
|
+
Two console side effects, both standard CLI behavior: progress enables
|
|
270
|
+
`ENABLE_VIRTUAL_TERMINAL_PROCESSING` once when missing and leaves it on
|
|
271
|
+
(per-call restore would race other writers); and under a **classic conhost in
|
|
272
|
+
select mode** (the user is holding a text selection) the OSC write can block.
|
|
273
|
+
The GVL is released so every *other* Ruby thread keeps running, but the parked
|
|
274
|
+
write itself is **not interruptible** (`Thread#kill` / `Timeout` will not break
|
|
275
|
+
it) — exact parity with `Kernel#puts`, whose console write is equally stuck.
|
|
276
|
+
|
|
277
|
+
## Library API
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
Wintoast.toast(title, body = nil, **opts) # => nil (accepted, not necessarily shown)
|
|
281
|
+
Wintoast.register!(aumid:, display_name:, icon: nil) # => aumid String (HKCU branding)
|
|
282
|
+
Wintoast.unregister!(aumid:) # => true | false (false = wasn't there)
|
|
283
|
+
Wintoast.progress(value = nil, of: 100, state: nil) # => true | false (accepted, not visible)
|
|
284
|
+
Wintoast.progress_clear # => true | false
|
|
285
|
+
Wintoast::Payload.build(title, body = nil, **opts) # => String (debugging: the exact XML sent)
|
|
286
|
+
Wintoast::VERSION # => "0.1.0"
|
|
287
|
+
Wintoast::POWERSHELL_AUMID # the registered-everywhere default AUMID
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## How it works
|
|
291
|
+
|
|
292
|
+
Toasts are sent through the **inbox** WinRT API activated directly via
|
|
293
|
+
`RoGetActivationFactory` — no Windows App SDK, no C++/WinRT codegen, no NuGet.
|
|
294
|
+
The C++ extension assembles the `ToastGeneric` XML in Ruby, hands it to
|
|
295
|
+
`Windows.Data.Xml.Dom.XmlDocument.LoadXml`, builds a `ToastNotification`, and
|
|
296
|
+
calls `ToastNotifier.Show` — all synchronous, milliseconds.
|
|
297
|
+
|
|
298
|
+
`register!` writes **only** `HKCU\Software\Classes\AppUserModelId\<aumid>`
|
|
299
|
+
(`DisplayName`, optional `IconUri`) — per-user, no elevation, no Start-Menu
|
|
300
|
+
shortcut, no HKLM, no COM activator. `unregister!` deletes that key tree.
|
|
301
|
+
|
|
302
|
+
Progress drives both `ITaskbarList3` and OSC 9;4 every call.
|
|
303
|
+
|
|
304
|
+
Every native operation is **stateless and complete within one call** — WinRT/COM
|
|
305
|
+
is initialized and uninitialized per call, owning no resources across calls. That
|
|
306
|
+
makes every API **idempotent and thread-safe**: call them from any thread,
|
|
307
|
+
concurrently, with no locks.
|
|
308
|
+
|
|
309
|
+
## Errors
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
StandardError
|
|
313
|
+
└─ Wintoast::Error misuse / non-OS failures (e.g. invalid UTF-8)
|
|
314
|
+
└─ Wintoast::OSError OS API failures; #code => Integer
|
|
315
|
+
(HRESULT for COM/WinRT, Win32 code for registry)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Plain argument-shape problems raise Ruby's own `ArgumentError` / `TypeError`.
|
|
319
|
+
|
|
320
|
+
These are **not** errors (by design — the platform cannot report them):
|
|
321
|
+
|
|
322
|
+
- a toast sent to an unregistered AUMID is silently dropped;
|
|
323
|
+
- Focus Assist / Do-Not-Disturb suppresses the banner (the toast still reaches
|
|
324
|
+
the Notification Center);
|
|
325
|
+
- a per-app or policy toggle disables toasts;
|
|
326
|
+
- `progress` returns `false` when no console surface accepted.
|
|
327
|
+
|
|
328
|
+
## License
|
|
329
|
+
|
|
330
|
+
[MIT](LICENSE.txt).
|
|
331
|
+
|
|
332
|
+
[BurntToast]: https://github.com/Windos/BurntToast
|
|
333
|
+
[Windows App SDK]: https://learn.microsoft.com/windows/apps/windows-app-sdk/
|
|
334
|
+
[mohabouje/WinToast]: https://github.com/mohabouje/WinToast
|
|
335
|
+
[microsoft/terminal#14268]: https://github.com/microsoft/terminal/discussions/14268
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# extconf.rb for the wintoast C++ extension (WinRT toasts + ITaskbarList3 +
|
|
4
|
+
# OSC 9;4 progress + HKCU AppUserModelId registration).
|
|
5
|
+
#
|
|
6
|
+
# wintoast is Windows-MSVC-only: it activates the inbox WinRT notification API
|
|
7
|
+
# via RoGetActivationFactory, drives COM (ITaskbarList3), and writes the registry
|
|
8
|
+
# — all with cl.exe. mkmf auto-discovers the .cpp source and compiles it as C++
|
|
9
|
+
# (its CXX_EXT list includes "cpp"). The guard is arch-neutral (/mswin/ only), so
|
|
10
|
+
# it passes unchanged on a future arm64-mswin Ruby (target_os stays "mswin64").
|
|
11
|
+
|
|
12
|
+
require "mkmf"
|
|
13
|
+
|
|
14
|
+
unless RbConfig::CONFIG["target_os"] =~ /mswin/
|
|
15
|
+
abort <<~MSG
|
|
16
|
+
wintoast requires a native Windows MSVC (mswin) Ruby — it binds the inbox
|
|
17
|
+
WinRT toast notification API (RoGetActivationFactory), ITaskbarList3, the
|
|
18
|
+
console VT/OSC path, and the registry, and is built with cl.exe. Your Ruby
|
|
19
|
+
is "#{RbConfig::CONFIG['arch']}". On a MinGW/UCRT Ruby this gem is not supported.
|
|
20
|
+
MSG
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# C++ (WinRT ABI interfaces, std::wstring, COM patterns); enable exceptions.
|
|
24
|
+
$CXXFLAGS << " -EHsc"
|
|
25
|
+
|
|
26
|
+
# System import libs (bare "NAME.lib" tokens on mswin; no -l prefix):
|
|
27
|
+
# runtimeobject - WinRT (RoInitialize / RoGetActivationFactory /
|
|
28
|
+
# WindowsCreateStringReference / WindowsCreateString)
|
|
29
|
+
# ole32 - COM (CoInitializeEx / CoCreateInstance for ITaskbarList3)
|
|
30
|
+
# uuid - CLSID_TaskbarList / interface IIDs
|
|
31
|
+
# advapi32 - registry (RegCreateKeyExW / RegSetValueExW / RegDeleteTreeW)
|
|
32
|
+
# kernel32 (console APIs, GetConsoleWindow) is linked by default and not listed.
|
|
33
|
+
$libs = [$libs, "runtimeobject.lib", "ole32.lib", "uuid.lib", "advapi32.lib"].join(" ")
|
|
34
|
+
|
|
35
|
+
# Produces lib/wintoast/wintoast.so, required as "wintoast/wintoast".
|
|
36
|
+
create_makefile("wintoast/wintoast")
|