wintoast 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a5ffc4e1ef7ddebe12e6096aa293b1d70f96a8bb8c7fdba4e6c807dc80578f6
4
- data.tar.gz: d7d1357b903a3582b4e51b5616a83e13f4a1c0dbbc94c273c9bf3d8234c1daef
3
+ metadata.gz: 779c2f38a8971d52dcd72fd4790bb390349e7e75bc121360d969cdef2726a7a7
4
+ data.tar.gz: 6bec442db8cfd2cf320198afe50feffb524d167382c96e08d4af87c94f9be467
5
5
  SHA512:
6
- metadata.gz: 1f0271faa0e9e3ee302a7e54d74eff648c75538432ae821b58fc338b2eb25c5da1f0cff9e034da31ad94ad44c515967975b05d420ee9784e1cdd7f106c90e14c
7
- data.tar.gz: 48714a218686fdfb01849299231162881f70302e8ae9278ce1dc3f6858ab8505da5a88fdcc87aa4acff32ab287effe9844d0f1234f5c5ff32411ad1282adfa91
6
+ metadata.gz: c2530fb41929d590ffdc3fd35e478c20f04ed27ce2cd96ca20bebbe698bcc25d26c95e3871055c1eb460f0f3a1b403b2c5f423d4052e0d7f1bef281c66a6727c
7
+ data.tar.gz: a9eecb5b2b46e459c8c83ac460fc6289d2749d032b95c281a8d8bd94569b0c9c0df96b4e10c8cc72b65ed8b1aa0709c1dc32e77d6402cca77e21d78de2c0be33
data/README.md CHANGED
@@ -1,335 +1,11 @@
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
1
+ # wintoast (discontinued)
2
+
3
+ **wintoast is no longer maintained.** This final release (0.1.1) contains no
4
+ code: installing it compiles nothing, and `require "wintoast"` raises a
5
+ `LoadError` explaining the discontinuation.
6
+
7
+ Use win_toaster instead.
8
+
9
+ Earlier releases remain on RubyGems only because RubyGems does not allow
10
+ versions older than 30 days to be yanked. They are unmaintained and not
11
+ recommended.
data/lib/wintoast.rb CHANGED
@@ -1,270 +1,4 @@
1
- # frozen_string_literal: true
2
-
3
- require "wintoast/version"
4
- require "wintoast/wintoast" # native ext: defines _show/_register/_unregister/_progress + errors
5
- require "wintoast/payload"
6
-
7
- # wintoast — fire-and-forget Windows toast notifications and taskbar/terminal
8
- # progress, built on the inbox WinRT and shell APIs that already ship with
9
- # Windows. No Windows App SDK, no packaging, no COM activation server, no
10
- # elevation, nothing to install but the gem.
11
- #
12
- # require "wintoast"
13
- #
14
- # Wintoast.toast("Backup finished", "1,204 files in 38 s") # => nil (a banner pops)
15
- # Wintoast.progress(50) # => true/false
16
- # Wintoast.progress_clear # => true/false
17
- #
18
- # A normal return from #toast means the OS ACCEPTED the toast — NOT that it was
19
- # displayed. Silent suppression (unregistered AUMID, Focus Assist, per-app
20
- # toggle, group policy) is undetectable by design of the platform; the gem never
21
- # pretends otherwise. See the README "The one trap you must know about".
22
- module Wintoast
23
- # Misuse / non-OS failures (e.g. invalid UTF-8). OS API failures are OSError.
24
- class Error < StandardError; end
25
-
26
- # OS API failure. #code is an Integer: the HRESULT for COM/WinRT, or the Win32
27
- # error code for registry calls. Attached in C via rb_iv_set(exc, "@code", ...).
28
- class OSError < Error
29
- def code = @code
30
- end
31
-
32
- # The AUMID of Windows PowerShell's Start-Menu shortcut — registered on every
33
- # supported Windows box, so toasts sent with it always render (branded
34
- # "Windows PowerShell"). The zero-setup default for Wintoast.toast.
35
- POWERSHELL_AUMID = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe"
36
-
37
- module_function
38
-
39
- # Fire-and-forget toast notification. Returns nil, always — a normal return
40
- # means the OS accepted the toast, not that it was displayed (silent drops are
41
- # undetectable). See §2.2 of the spec / the README for every kwarg.
42
- #
43
- # Raises ArgumentError / TypeError on bad arguments, Wintoast::Error on invalid
44
- # UTF-8, and Wintoast::OSError (with #code = HRESULT) on a WinRT API failure.
45
- def toast(title, body = nil,
46
- aumid: POWERSHELL_AUMID,
47
- attribution: nil,
48
- image: nil,
49
- hero: nil,
50
- circle: false,
51
- audio: :default,
52
- duration: :short,
53
- scenario: nil,
54
- expires_at: nil,
55
- expires_in: nil,
56
- tag: nil,
57
- group: nil)
58
- # Build the XML first: it validates title/body/attribution/image/hero/
59
- # circle/audio/duration/scenario and normalizes + UTF-8-checks every text
60
- # field, all in pure Ruby BEFORE any C bridge runs (the §3.5 regime).
61
- xml = Payload.build(title, body,
62
- attribution: attribution, image: image, hero: hero,
63
- circle: circle, audio: audio, duration: duration,
64
- scenario: scenario)
65
-
66
- aumid_u8 = validate_aumid_arg(aumid)
67
- expire_ms = validate_expiry(expires_at, expires_in)
68
- tag_u8 = validate_label(tag, "tag")
69
- group_u8 = validate_label(group, "group")
70
-
71
- _show(aumid_u8, xml, expire_ms, tag_u8, group_u8)
72
- nil
73
- end
74
-
75
- # Opt-in, reversible, per-user branding: writes ONLY
76
- # HKCU\Software\Classes\AppUserModelId\<aumid> {DisplayName, IconUri?}.
77
- # No elevation, no Start-Menu shortcut, no HKLM, no COM activator. Returns the
78
- # aumid String. Raises ArgumentError on format violations, Wintoast::OSError
79
- # (#code = Win32 error) on registry failures.
80
- def register!(aumid:, display_name:, icon: nil)
81
- aumid_u8 = validate_registry_aumid(aumid)
82
-
83
- dn = String.try_convert(display_name)
84
- raise TypeError, "wintoast: display_name: must be a String" if dn.nil?
85
-
86
- dn = normalize_text(dn, "display_name")
87
- raise ArgumentError, "wintoast: display_name: must be non-empty" if dn.empty?
88
-
89
- icon_u8 =
90
- if icon.nil?
91
- nil
92
- else
93
- ic = normalize_text(icon, "icon")
94
- unless File.absolute_path?(ic) && File.file?(ic)
95
- raise ArgumentError, "wintoast: icon: must be an absolute path to an existing file"
96
- end
97
- ic
98
- end
99
-
100
- _register(aumid_u8, dn, icon_u8)
101
- aumid_u8
102
- end
103
-
104
- # Delete the HKCU AppUserModelId key tree for the given aumid. Idempotent:
105
- # returns false (no raise) when the key was not present. Only unregister AUMIDs
106
- # YOU registered — deleting another app's HKCU key breaks its toasts.
107
- def unregister!(aumid:)
108
- _unregister(validate_registry_aumid(aumid))
109
- end
110
-
111
- # Drive taskbar + terminal progress. Returns true if at least one OS surface
112
- # accepted the update, false if none did (no console at all, or a console whose
113
- # taskbar leg also failed). Accepted != visible. Environmental failure is a
114
- # false, never an exception — only argument misuse raises ArgumentError.
115
- #
116
- # progress(50) determinate, green
117
- # progress(7, of: 23) determinate from a ratio
118
- # progress(50, state: :error) determinate, red
119
- # progress(50, state: :paused) determinate, yellow
120
- # progress(state: :indeterminate) marquee/ring (value must be nil)
121
- # progress(nil) / progress(:clear) remove
122
- def progress(value = nil, of: 100, state: nil)
123
- unless of.is_a?(Numeric) && of.positive?
124
- raise ArgumentError, "wintoast: of: must be a positive number, got #{of.inspect}"
125
- end
126
-
127
- # :indeterminate is value-less (the marquee ignores any percent). value must
128
- # be nil; a supplied value (Numeric or :clear) is misuse. Checked first so
129
- # progress(state: :indeterminate) is NOT mistaken for a clear.
130
- if state == :indeterminate
131
- unless value.nil?
132
- raise ArgumentError, "wintoast: state: :indeterminate ignores value — pass value nil"
133
- end
134
- return _progress(3, 0)
135
- end
136
-
137
- # Clearing: value nil or :clear, and (because clear has no color) no state:.
138
- if value.nil? || value == :clear
139
- unless state.nil?
140
- raise ArgumentError, "wintoast: clearing progress takes no state:, got #{state.inspect}"
141
- end
142
- return _progress(0, 0)
143
- end
144
-
145
- # Determinate: requires a Numeric value.
146
- unless value.is_a?(Numeric)
147
- raise ArgumentError, "wintoast: progress value must be a number, got #{value.inspect}"
148
- end
149
- state_int =
150
- case state
151
- when nil, :normal then 1
152
- when :error then 2
153
- when :paused then 4
154
- else
155
- raise ArgumentError, "wintoast: unknown state: #{state.inspect}"
156
- end
157
-
158
- _progress(state_int, pct(value, of))
159
- end
160
-
161
- # Remove progress. Equivalent to progress(nil).
162
- def progress_clear = _progress(0, 0)
163
-
164
- # ---- validation helpers (private) ----------------------------------------
165
-
166
- # Normalize any String to UTF-8 and reject invalid UTF-8 in pure Ruby (§3.5).
167
- def normalize_text(value, label)
168
- str = String.try_convert(value)
169
- raise TypeError, "wintoast: #{label} must be a String, got #{value.class}" if str.nil?
170
-
171
- str = str.encode(Encoding::UTF_8) unless str.encoding == Encoding::UTF_8
172
- unless str.valid_encoding?
173
- raise Wintoast::Error, "wintoast: invalid UTF-8 in #{label}"
174
- end
175
- str
176
- rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
177
- raise Wintoast::Error, "wintoast: invalid UTF-8 in #{label}"
178
- end
179
-
180
- # aumid: for toast() — must be a non-empty String (StringValue semantics).
181
- def validate_aumid_arg(aumid)
182
- str = String.try_convert(aumid)
183
- raise TypeError, "wintoast: aumid: must be a String, got #{aumid.class}" if str.nil?
184
-
185
- str = normalize_text(str, "aumid")
186
- raise ArgumentError, "wintoast: aumid: must not be empty" if str.empty?
187
- str
188
- end
189
-
190
- # aumid: for register!/unregister! — the documented AUMID format: 1..128
191
- # chars, no spaces, no control chars/NUL. Backslashes allowed (nested keys).
192
- def validate_registry_aumid(aumid)
193
- str = String.try_convert(aumid)
194
- raise TypeError, "wintoast: aumid: must be a String, got #{aumid.class}" if str.nil?
195
-
196
- str = normalize_text(str, "aumid")
197
- if str.empty? || str.length > 128
198
- raise ArgumentError, "wintoast: aumid: must be 1..128 characters, got #{str.length}"
199
- end
200
- if str.include?(" ")
201
- raise ArgumentError, "wintoast: aumid: must not contain spaces"
202
- end
203
- if str.match?(/[\x00-\x1f]/)
204
- raise ArgumentError, "wintoast: aumid: must not contain control characters"
205
- end
206
- str
207
- end
208
-
209
- # tag:/group: -> nil or a 1..64-char String. Empty or > 64 raises.
210
- def validate_label(value, label)
211
- return nil if value.nil?
212
-
213
- str = String.try_convert(value)
214
- raise TypeError, "wintoast: #{label}: must be a String, got #{value.class}" if str.nil?
215
-
216
- str = normalize_text(str, label)
217
- if str.empty? || str.length > 64
218
- raise ArgumentError, "wintoast: #{label}: must be 1..64 characters, got #{str.length}"
219
- end
220
- str
221
- end
222
-
223
- # The epoch-ms whose WinRT DateTime tick — (ms + 11644473600000) * 10000, in
224
- # 100-ns units since 1601 — still fits a signed int64. The C bridge performs
225
- # exactly that multiply (wintoast.cpp), so any ms past this point would overflow
226
- # int64 (undefined behavior) and hand the OS a garbage UniversalTime. The OS
227
- # caps toast retention at 3 days anyway, so a far-future expiry is already
228
- # meaningless; clamping here is observably correct and removes the UB window.
229
- MAX_EXPIRE_MS = (2**63 - 1) / 10_000 - 11_644_473_600_000
230
- private_constant :MAX_EXPIRE_MS
231
-
232
- # expires_at:/expires_in: -> the absolute Unix epoch milliseconds, or nil.
233
- # Mutually exclusive. expires_in must be a positive Numeric (seconds from now);
234
- # expires_at must be a Time (past times pass through — the OS treats them as
235
- # already expired; clocks skew, so we don't police it). The result is clamped
236
- # to MAX_EXPIRE_MS so the C-side tick multiply can never overflow int64.
237
- def validate_expiry(expires_at, expires_in)
238
- if expires_at && expires_in
239
- raise ArgumentError, "wintoast: pass only one of expires_at:/expires_in:"
240
- end
241
-
242
- if expires_in
243
- unless expires_in.is_a?(Numeric) && expires_in.positive?
244
- raise ArgumentError, "wintoast: expires_in: must be a positive number of seconds"
245
- end
246
- ms = ((Time.now.to_f + expires_in.to_f) * 1000.0).round
247
- return ms.clamp(0, MAX_EXPIRE_MS)
248
- end
249
-
250
- if expires_at
251
- unless expires_at.is_a?(Time)
252
- raise ArgumentError, "wintoast: expires_at: must be a Time, got #{expires_at.class}"
253
- end
254
- ms = (expires_at.to_f * 1000.0).round
255
- return ms.clamp(0, MAX_EXPIRE_MS)
256
- end
257
-
258
- nil
259
- end
260
-
261
- # Percent = round(value/of * 100), clamped 0..100. Overshoot is a routine
262
- # rounding artifact in app loops, so it is clamped, not raised.
263
- def pct(value, of)
264
- ((value.to_f / of.to_f) * 100.0).round.clamp(0, 100)
265
- end
266
-
267
- private_class_method :_show, :_register, :_unregister, :_progress
268
- private_class_method :normalize_text, :validate_aumid_arg, :validate_registry_aumid,
269
- :validate_label, :validate_expiry, :pct
270
- end
1
+ # frozen_string_literal: true
2
+
3
+ raise LoadError, "wintoast is discontinued and this release contains no code. Use win_toaster instead." \
4
+ " Earlier wintoast releases are unmaintained and not recommended."