Module: Wintoast
- Defined in:
- lib/wintoast.rb,
lib/wintoast/payload.rb,
lib/wintoast/version.rb,
ext/wintoast/wintoast.cpp
Overview
wintoast — fire-and-forget Windows toast notifications and taskbar/terminal progress, built on the inbox WinRT and shell APIs that already ship with Windows. No Windows App SDK, no packaging, no COM activation server, no elevation, nothing to install but the gem.
require "wintoast"
Wintoast.toast("Backup finished", "1,204 files in 38 s") # => nil (a banner pops)
Wintoast.progress(50) # => true/false
Wintoast.progress_clear # => true/false
A normal return from #toast means the OS ACCEPTED the toast — NOT that it was displayed. Silent suppression (unregistered AUMID, Focus Assist, per-app toggle, group policy) is undetectable by design of the platform; the gem never pretends otherwise. See the README "The one trap you must know about".
Defined Under Namespace
Modules: Payload Classes: Error, OSError
Constant Summary collapse
- POWERSHELL_AUMID =
The AUMID of Windows PowerShell's Start-Menu shortcut — registered on every supported Windows box, so toasts sent with it always render (branded "Windows PowerShell"). The zero-setup default for Wintoast.toast.
"{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe"- VERSION =
"0.1.0"
Class Method Summary collapse
- ._progress(vstate, vpercent) ⇒ Object
- ._register(vaumid, vdn, vicon) ⇒ Object
- ._show(vaumid, vxml, vexpire, vtag, vgroup) ⇒ Object
- ._unregister(vaumid) ⇒ Object
-
.progress(value = nil, of: 100, state: nil) ⇒ Object
Drive taskbar + terminal progress.
-
.progress_clear ⇒ Object
Remove progress.
-
.register!(aumid:, display_name:, icon: nil) ⇒ Object
Opt-in, reversible, per-user branding: writes ONLY HKCUSoftwareClassesAppUserModelId<aumid> IconUri?.
-
.toast(title, body = nil, aumid: POWERSHELL_AUMID, attribution: nil, image: nil, hero: nil, circle: false, audio: :default, duration: :short, scenario: nil, expires_at: nil, expires_in: nil, tag: nil, group: nil) ⇒ Object
Fire-and-forget toast notification.
-
.unregister!(aumid:) ⇒ Object
Delete the HKCU AppUserModelId key tree for the given aumid.
Class Method Details
._progress(vstate, vpercent) ⇒ Object
477 478 479 480 481 482 483 484 485 486 487 488 |
# File 'ext/wintoast/wintoast.cpp', line 477
static VALUE wintoast_progress(VALUE self, VALUE vstate, VALUE vpercent) {
(void)self;
unsigned state = static_cast<unsigned>(NUM2UINT(vstate));
unsigned percent = static_cast<unsigned>(NUM2UINT(vpercent));
/* Both legs always run; exactly one is visible per host (or neither). The
* console leg releases the GVL for the WriteConsoleW; the taskbar leg holds
* it (quick COM work). Neither raises on environmental failure. */
bool did_console = console_leg(state, percent);
bool did_taskbar = taskbar_leg(state, percent);
return (did_console || did_taskbar) ? Qtrue : Qfalse;
}
|
._register(vaumid, vdn, vicon) ⇒ Object
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 |
# File 'ext/wintoast/wintoast.cpp', line 332
static VALUE wintoast_register(VALUE self, VALUE vaumid, VALUE vdn, VALUE vicon) {
(void)self;
bool has_icon = !NIL_P(vicon);
LSTATUS st = ERROR_SUCCESS;
const char* what = NULL;
bool oom = false;
{
try {
std::wstring aumid = to_utf16(vaumid);
std::wstring dn = to_utf16(vdn);
std::wstring icon = has_icon ? to_utf16(vicon) : std::wstring();
st = do_register(aumid, dn, has_icon, icon, &what);
} catch (...) {
oom = true;
}
}
if (oom) rb_raise(rb_eNoMemError, "wintoast: out of memory in register!");
if (st != ERROR_SUCCESS) raise_os(what ? what : "RegCreateKeyEx", static_cast<long>(st), /*is_hr*/ false);
return Qtrue;
}
|
._show(vaumid, vxml, vexpire, vtag, vgroup) ⇒ Object
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'ext/wintoast/wintoast.cpp', line 272
static VALUE wintoast_show(VALUE self, VALUE vaumid, VALUE vxml, VALUE vexpire,
VALUE vtag, VALUE vgroup) {
(void)self;
/* Outer frame: no C++ objects, no try. Read scalars only. */
bool has_expire = !NIL_P(vexpire);
long long expire_ms = has_expire ? NUM2LL(vexpire) : 0;
bool has_tag = !NIL_P(vtag);
bool has_group = !NIL_P(vgroup);
HRESULT hr = S_OK;
const char* what = NULL;
bool oom = false;
{ /* inner C++ scope: every std::wstring + interface pointer lives here */
try {
std::wstring aumid = to_utf16(vaumid);
std::wstring xml = to_utf16(vxml);
std::wstring tag = has_tag ? to_utf16(vtag) : std::wstring();
std::wstring group = has_group ? to_utf16(vgroup) : std::wstring();
hr = show_toast(aumid, xml, has_expire, expire_ms,
has_tag, tag, has_group, group, &what);
} catch (...) {
oom = true; /* std::bad_alloc from a wstring: flag only */
}
} /* scope closes: destructors run, COM balanced, try no longer active */
if (oom) rb_raise(rb_eNoMemError, "wintoast: out of memory building toast");
if (FAILED(hr)) raise_os(what ? what : "Show", static_cast<long>(hr), /*is_hr*/ true);
return Qnil;
}
|
._unregister(vaumid) ⇒ Object
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 |
# File 'ext/wintoast/wintoast.cpp', line 355
static VALUE wintoast_unregister(VALUE self, VALUE vaumid) {
(void)self;
LSTATUS st = ERROR_SUCCESS;
bool oom = false;
{
try {
std::wstring path = L"Software\\Classes\\AppUserModelId\\" + to_utf16(vaumid);
st = RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
} catch (...) {
oom = true;
}
}
if (oom) rb_raise(rb_eNoMemError, "wintoast: out of memory in unregister!");
if (st == ERROR_SUCCESS) return Qtrue;
if (st == ERROR_FILE_NOT_FOUND) return Qfalse;
raise_os("RegDeleteTree", static_cast<long>(st), /*is_hr*/ false);
return Qnil; /* unreachable */
}
|
.progress(value = nil, of: 100, state: nil) ⇒ Object
Drive taskbar + terminal progress. Returns true if at least one OS surface accepted the update, false if none did (no console at all, or a console whose taskbar leg also failed). Accepted != visible. Environmental failure is a false, never an exception — only argument misuse raises ArgumentError.
progress(50) determinate, green
progress(7, of: 23) determinate from a ratio
progress(50, state: :error) determinate, red
progress(50, state: :paused) determinate, yellow
progress(state: :indeterminate) marquee/ring (value must be nil)
progress(nil) / progress(:clear) remove
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/wintoast.rb', line 122 def progress(value = nil, of: 100, state: nil) unless of.is_a?(Numeric) && of.positive? raise ArgumentError, "wintoast: of: must be a positive number, got #{of.inspect}" end # :indeterminate is value-less (the marquee ignores any percent). value must # be nil; a supplied value (Numeric or :clear) is misuse. Checked first so # progress(state: :indeterminate) is NOT mistaken for a clear. if state == :indeterminate unless value.nil? raise ArgumentError, "wintoast: state: :indeterminate ignores value — pass value nil" end return _progress(3, 0) end # Clearing: value nil or :clear, and (because clear has no color) no state:. if value.nil? || value == :clear unless state.nil? raise ArgumentError, "wintoast: clearing progress takes no state:, got #{state.inspect}" end return _progress(0, 0) end # Determinate: requires a Numeric value. unless value.is_a?(Numeric) raise ArgumentError, "wintoast: progress value must be a number, got #{value.inspect}" end state_int = case state when nil, :normal then 1 when :error then 2 when :paused then 4 else raise ArgumentError, "wintoast: unknown state: #{state.inspect}" end _progress(state_int, pct(value, of)) end |
.progress_clear ⇒ Object
Remove progress. Equivalent to progress(nil).
162 |
# File 'lib/wintoast.rb', line 162 def progress_clear = _progress(0, 0) |
.register!(aumid:, display_name:, icon: nil) ⇒ Object
Opt-in, reversible, per-user branding: writes ONLY HKCUSoftwareClassesAppUserModelId<aumid> IconUri?. No elevation, no Start-Menu shortcut, no HKLM, no COM activator. Returns the aumid String. Raises ArgumentError on format violations, Wintoast::OSError (#code = Win32 error) on registry failures.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/wintoast.rb', line 80 def register!(aumid:, display_name:, icon: nil) aumid_u8 = validate_registry_aumid(aumid) dn = String.try_convert(display_name) raise TypeError, "wintoast: display_name: must be a String" if dn.nil? dn = normalize_text(dn, "display_name") raise ArgumentError, "wintoast: display_name: must be non-empty" if dn.empty? icon_u8 = if icon.nil? nil else ic = normalize_text(icon, "icon") unless File.absolute_path?(ic) && File.file?(ic) raise ArgumentError, "wintoast: icon: must be an absolute path to an existing file" end ic end _register(aumid_u8, dn, icon_u8) aumid_u8 end |
.toast(title, body = nil, aumid: POWERSHELL_AUMID, attribution: nil, image: nil, hero: nil, circle: false, audio: :default, duration: :short, scenario: nil, expires_at: nil, expires_in: nil, tag: nil, group: nil) ⇒ Object
Fire-and-forget toast notification. Returns nil, always — a normal return means the OS accepted the toast, not that it was displayed (silent drops are undetectable). See §2.2 of the spec / the README for every kwarg.
Raises ArgumentError / TypeError on bad arguments, Wintoast::Error on invalid UTF-8, and Wintoast::OSError (with #code = HRESULT) on a WinRT API failure.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/wintoast.rb', line 45 def toast(title, body = nil, aumid: POWERSHELL_AUMID, attribution: nil, image: nil, hero: nil, circle: false, audio: :default, duration: :short, scenario: nil, expires_at: nil, expires_in: nil, tag: nil, group: nil) # Build the XML first: it validates title/body/attribution/image/hero/ # circle/audio/duration/scenario and normalizes + UTF-8-checks every text # field, all in pure Ruby BEFORE any C bridge runs (the §3.5 regime). xml = Payload.build(title, body, attribution: attribution, image: image, hero: hero, circle: circle, audio: audio, duration: duration, scenario: scenario) aumid_u8 = validate_aumid_arg(aumid) expire_ms = validate_expiry(expires_at, expires_in) tag_u8 = validate_label(tag, "tag") group_u8 = validate_label(group, "group") _show(aumid_u8, xml, expire_ms, tag_u8, group_u8) nil end |
.unregister!(aumid:) ⇒ Object
Delete the HKCU AppUserModelId key tree for the given aumid. Idempotent: returns false (no raise) when the key was not present. Only unregister AUMIDs YOU registered — deleting another app's HKCU key breaks its toasts.
107 108 109 |
# File 'lib/wintoast.rb', line 107 def unregister!(aumid:) _unregister(validate_registry_aumid(aumid)) end |