Module: Winclip

Defined in:
lib/winclip.rb,
lib/winclip/version.rb,
ext/winclip/winclip.cpp

Overview

winclip — read and write the Windows clipboard: text, images (PNG), and file lists.

Winclip.text = "hello"
Winclip.text                 # => "hello"
Winclip.image = File.binread("pic.png")
File.binwrite("out.png", Winclip.image) if Winclip.has_image?
Winclip.files = ["C:/a.txt", "C:/b.txt"]
Winclip.files                # => ["C:/a.txt", "C:/b.txt"]
Winclip.clear

Defined Under Namespace

Classes: Error

Constant Summary collapse

STANDARD_FORMATS =

Standard clipboard format ids, for Winclip.available?(“CF_…”).

{
  "CF_TEXT" => 1, "CF_BITMAP" => 2, "CF_METAFILEPICT" => 3, "CF_SYLK" => 4,
  "CF_DIF" => 5, "CF_TIFF" => 6, "CF_OEMTEXT" => 7, "CF_DIB" => 8,
  "CF_PALETTE" => 9, "CF_PENDATA" => 10, "CF_RIFF" => 11, "CF_WAVE" => 12,
  "CF_UNICODETEXT" => 13, "CF_ENHMETAFILE" => 14, "CF_HDROP" => 15,
  "CF_LOCALE" => 16, "CF_DIBV5" => 17
}.freeze
VERSION =
"0.1.0"

Class Method Summary collapse

Class Method Details

._available?(vid) ⇒ Boolean

Returns:

  • (Boolean)


531
532
533
534
# File 'ext/winclip/winclip.cpp', line 531

static VALUE wc_available(VALUE self, VALUE vid) {
    UINT id = (UINT)NUM2UINT(vid);
    return IsClipboardFormatAvailable(id) ? Qtrue : Qfalse;
}

._get_filesObject



411
412
413
414
415
416
417
418
419
420
421
422
# File 'ext/winclip/winclip.cpp', line 411

static VALUE wc_get_files(VALUE self) {
    char **arr = NULL;
    int n = clip_get_files(&arr);
    if (n < 0) return Qnil;
    VALUE rary = rb_ary_new_capa(n);
    for (int i = 0; i < n; i++) {
        rb_ary_push(rary, rb_utf8_str_new_cstr(arr[i]));
        LocalFree(arr[i]);
    }
    LocalFree(arr);
    return rary;
}

._get_imageObject



452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
# File 'ext/winclip/winclip.cpp', line 452

static VALUE wc_get_image(VALUE self) {
    if (!IsClipboardFormatAvailable(CF_DIBV5) && !IsClipboardFormatAvailable(CF_DIB))
        return Qnil;

    HRESULT cohr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
    bool com_owned = (cohr == S_OK || cohr == S_FALSE);

    std::vector<BYTE> png;
    HRESULT hr = E_FAIL;
    bool threw = false;
    try { hr = clip_get_image(png); }
    catch (...) { threw = true; } /* e.g. bad_alloc in bgra_to_png (clipboard already closed) */
    if (com_owned) CoUninitialize();

    if (threw) rb_raise(eWinclipError, "winclip: out of memory decoding the clipboard image");
    /* Format present but not decodable (palettized / malformed) -> nil, matching
     * the "getters return nil when there's no usable content" contract. */
    if (FAILED(hr)) return Qnil;

    VALUE s = rb_str_new((char *)png.data(), (long)png.size());
    rb_enc_associate(s, rb_ascii8bit_encoding());
    return s;
}

._get_textObject

Ruby-facing methods =============================


393
394
395
396
397
398
399
# File 'ext/winclip/winclip.cpp', line 393

static VALUE wc_get_text(VALUE self) {
    char *u8 = clip_get_text();
    if (!u8) return Qnil;
    VALUE s = rb_utf8_str_new_cstr(u8);
    LocalFree(u8);
    return s;
}

._register_format(name) ⇒ Object



536
537
538
539
540
541
542
543
544
# File 'ext/winclip/winclip.cpp', line 536

static VALUE wc_register_format(VALUE self, VALUE name) {
    VALUE u8 = rb_str_export_to_enc(StringValue(name), rb_utf8_encoding());
    wchar_t *w = utf8_to_utf16(RSTRING_PTR(u8), -1);
    if (!w) rb_raise(eWinclipError, "winclip: could not encode format name");
    UINT id = RegisterClipboardFormatW(w);
    LocalFree(w);
    if (id == 0) rb_raise(eWinclipError, "winclip: could not register clipboard format");
    return UINT2NUM(id);
}

._set_files(ary) ⇒ Object



424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'ext/winclip/winclip.cpp', line 424

static VALUE wc_set_files(VALUE self, VALUE ary) {
    Check_Type(ary, T_ARRAY);
    long n = RARRAY_LEN(ary);
    if (n <= 0) rb_raise(rb_eArgError, "winclip: files list must not be empty");

    /* Convert all paths to UTF-16 up front (raises here, before any clipboard
     * work or HGLOBAL exists). */
    wchar_t **wp = (wchar_t **)LocalAlloc(LPTR, (size_t)n * sizeof(wchar_t *));
    if (!wp) rb_raise(rb_eNoMemError, "winclip: out of memory");
    int built = 0;
    int failed = 0;
    for (long i = 0; i < n; i++) {
        VALUE e = rb_ary_entry(ary, i);
        if (!RB_TYPE_P(e, T_STRING)) { failed = 1; break; }
        VALUE u8 = rb_str_export_to_enc(e, rb_utf8_encoding());
        wp[i] = utf8_to_utf16(RSTRING_PTR(u8), -1);
        if (!wp[i]) { failed = 1; break; }
        built++;
    }
    int ok = 0;
    if (!failed) ok = clip_set_files((const wchar_t **)wp, (int)n);
    for (int i = 0; i < built; i++) LocalFree(wp[i]);
    LocalFree(wp);
    if (failed) rb_raise(rb_eArgError, "winclip: every file path must be a String");
    if (!ok) rb_raise(eWinclipError, "winclip: failed to set clipboard files");
    return Qnil;
}

._set_image(png) ⇒ Object



476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'ext/winclip/winclip.cpp', line 476

static VALUE wc_set_image(VALUE self, VALUE png) {
    StringValue(png);
    HRESULT cohr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
    bool com_owned = (cohr == S_OK || cohr == S_FALSE);

    HRESULT hr = E_FAIL;
    bool threw = false;
    try {
        std::vector<BYTE> in((BYTE *)RSTRING_PTR(png), (BYTE *)RSTRING_PTR(png) + RSTRING_LEN(png));
        hr = clip_set_image(in.data(), in.size());
    } catch (...) { threw = true; } /* e.g. bad_alloc decoding a huge PNG (no clipboard held) */
    if (com_owned) CoUninitialize();

    if (threw) rb_raise(eWinclipError, "winclip: out of memory encoding the clipboard image");
    if (FAILED(hr))
        rb_raise(eWinclipError, "winclip: failed to set clipboard image "
                 "(not a valid PNG?) (hr=0x%08lX)", (unsigned long)hr);
    return Qnil;
}

._set_text(str) ⇒ Object



401
402
403
404
405
406
407
408
409
# File 'ext/winclip/winclip.cpp', line 401

static VALUE wc_set_text(VALUE self, VALUE str) {
    VALUE u8 = rb_str_export_to_enc(StringValue(str), rb_utf8_encoding());
    wchar_t *w = utf8_to_utf16(RSTRING_PTR(u8), -1);
    if (!w) rb_raise(eWinclipError, "winclip: could not encode text");
    int ok = clip_set_text(w);
    LocalFree(w);
    if (!ok) rb_raise(eWinclipError, "winclip: failed to set clipboard text");
    return Qnil;
}

.available?(format) ⇒ Boolean

Is the given clipboard format present? Accepts an Integer format id, a standard format name (“CF_UNICODETEXT”), a registered format name (“PNG”), or one of :text / :image / :files.

Returns:

  • (Boolean)


78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/winclip.rb', line 78

def available?(format)
  case format
  when Integer then _available?(format)
  when :text   then has_text?
  when :image  then has_image?
  when :files  then has_files?
  else
    name = format.to_s
    if (id = STANDARD_FORMATS[name])
      _available?(id)
    else
      # Resolve a custom/registered format by name WITHOUT registering it as a
      # side effect (RegisterClipboardFormat would create unknown names).
      # `formats` reports what's actually present — exactly the answer needed.
      formats.include?(name)
    end
  end
end

.clearObject



496
497
498
499
500
501
# File 'ext/winclip/winclip.cpp', line 496

static VALUE wc_clear(VALUE self) {
    if (!clip_open_retry()) rb_raise(eWinclipError, "winclip: could not open the clipboard");
    EmptyClipboard();
    CloseClipboard();
    return Qnil;
}

.copy(string) ⇒ Object

Copy text to the clipboard (alias for text=). Returns the string.



66
67
68
# File 'lib/winclip.rb', line 66

def copy(string)
  self.text = string
end

.filesObject

The clipboard file list as an Array<String> of paths, or nil if none.



52
53
54
# File 'lib/winclip.rb', line 52

def files
  _get_files
end

.files=(paths) ⇒ Object

Put a file list on the clipboard (CF_HDROP). Accepts an Array of paths or a single path String. Returns the value.

Raises:

  • (ArgumentError)


58
59
60
61
62
63
# File 'lib/winclip.rb', line 58

def files=(paths)
  list = Array(paths).map(&:to_s)
  raise ArgumentError, "winclip: provide at least one file path" if list.empty?
  _set_files(list)
  paths
end

.formatsObject



503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
# File 'ext/winclip/winclip.cpp', line 503

static VALUE wc_formats(VALUE self) {
    if (!clip_open_retry()) rb_raise(eWinclipError, "winclip: could not open the clipboard");

    /* Collect ids while open; build Ruby objects only after closing (so an
     * allocation that raises can't leave the clipboard open). std::vector grows
     * as needed — no silent cap on the number of formats reported. */
    std::vector<UINT> ids;
    UINT f = 0;
    while ((f = EnumClipboardFormats(f)) != 0) ids.push_back(f);
    CloseClipboard();

    VALUE ary = rb_ary_new_capa((long)ids.size());
    for (size_t i = 0; i < ids.size(); i++) {
        const char *nm = clip_std_name(ids[i]);
        if (nm) {
            rb_ary_push(ary, rb_str_new_cstr(nm));
        } else {
            wchar_t wbuf[256];
            int wn = GetClipboardFormatNameW(ids[i], wbuf, 256);  /* no open needed */
            if (wn > 0) {
                char *u8 = utf16_to_utf8(wbuf, wn);
                if (u8) { rb_ary_push(ary, rb_utf8_str_new_cstr(u8)); LocalFree(u8); }
            }
        }
    }
    return ary;
}

.has_files?Boolean

Returns:

  • (Boolean)


555
556
557
# File 'ext/winclip/winclip.cpp', line 555

static VALUE wc_has_files(VALUE self) {
    return IsClipboardFormatAvailable(CF_HDROP) ? Qtrue : Qfalse;
}

.has_image?Boolean

Returns:

  • (Boolean)


550
551
552
553
554
# File 'ext/winclip/winclip.cpp', line 550

static VALUE wc_has_image(VALUE self) {
    return (IsClipboardFormatAvailable(CF_DIBV5) ||
            IsClipboardFormatAvailable(CF_DIB) ||
            IsClipboardFormatAvailable(CF_BITMAP)) ? Qtrue : Qfalse;
}

.has_text?Boolean

Returns:

  • (Boolean)


546
547
548
549
# File 'ext/winclip/winclip.cpp', line 546

static VALUE wc_has_text(VALUE self) {
    return (IsClipboardFormatAvailable(CF_UNICODETEXT) ||
            IsClipboardFormatAvailable(CF_TEXT)) ? Qtrue : Qfalse;
}

.imageObject

The clipboard image as PNG bytes (binary String), or nil if no image is present. Pairs with windraw: ‘File.binwrite(“x.png”, Winclip.image)`.



40
41
42
# File 'lib/winclip.rb', line 40

def image
  _get_image
end

.image=(png) ⇒ Object

Put an image on the clipboard from PNG bytes (a binary String). Returns the value. Sets both CF_DIBV5 (alpha-aware) and CF_DIB for broad compatibility.



46
47
48
49
# File 'lib/winclip.rb', line 46

def image=(png)
  _set_image(png)
  png
end

.pasteObject

Read text from the clipboard (alias for #text).



71
72
73
# File 'lib/winclip.rb', line 71

def paste
  text
end

.textObject

Clipboard text as a UTF-8 String, or nil if no text is present.



28
29
30
# File 'lib/winclip.rb', line 28

def text
  _get_text
end

.text=(value) ⇒ Object

Set the clipboard to the given text (coerced with #to_s). Returns the value.



33
34
35
36
# File 'lib/winclip.rb', line 33

def text=(value)
  _set_text(value.to_s)
  value
end