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

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_clearObject

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.

Raises:

  • (TypeError)


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