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.1"

Class Method Summary collapse

Class Method Details

._available?(vid) ⇒ Boolean

Returns:

  • (Boolean)


604
605
606
607
# File 'ext/winclip/winclip.cpp', line 604

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

._get_filesObject



488
489
490
491
492
493
494
495
496
497
498
499
# File 'ext/winclip/winclip.cpp', line 488

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



529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'ext/winclip/winclip.cpp', line 529

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

    wc_ensure_com();   /* one process-lifetime STA; never CoUninitialize'd */

    /* Pure-POD frame: all C++/EH is inside get_image_guarded, so the rb_raise
     * below never unwinds through a frame carrying C++ exception state. */
    BYTE *buf = NULL; size_t buflen = 0; HRESULT hr = E_FAIL;
    int rc = get_image_guarded(&buf, &buflen, &hr);

    if (rc == 1) 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 (rc == 2) return Qnil;

    VALUE s = rb_str_new((char *)buf, (long)buflen);
    LocalFree(buf);
    rb_enc_associate(s, rb_ascii8bit_encoding());
    return s;
}

._get_textObject

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


470
471
472
473
474
475
476
# File 'ext/winclip/winclip.cpp', line 470

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



609
610
611
612
613
614
615
616
617
# File 'ext/winclip/winclip.cpp', line 609

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



501
502
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
# File 'ext/winclip/winclip.cpp', line 501

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



551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'ext/winclip/winclip.cpp', line 551

static VALUE wc_set_image(VALUE self, VALUE png) {
    StringValue(png);
    wc_ensure_com();   /* one process-lifetime STA; never CoUninitialize'd */

    /* Pure-POD frame: the byte-vector and try/catch live in set_image_guarded so
     * the rb_raise calls below never unwind through a C++ EH frame. */
    const BYTE *p = (const BYTE *)RSTRING_PTR(png);
    size_t n = (size_t)RSTRING_LEN(png);
    HRESULT hr = E_FAIL;
    int threw = set_image_guarded(p, n, &hr);

    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



478
479
480
481
482
483
484
485
486
# File 'ext/winclip/winclip.cpp', line 478

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



569
570
571
572
573
574
# File 'ext/winclip/winclip.cpp', line 569

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 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



576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
# File 'ext/winclip/winclip.cpp', line 576

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)


628
629
630
# File 'ext/winclip/winclip.cpp', line 628

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

.has_image?Boolean

Returns:

  • (Boolean)


623
624
625
626
627
# File 'ext/winclip/winclip.cpp', line 623

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

.has_text?Boolean

Returns:

  • (Boolean)


619
620
621
622
# File 'ext/winclip/winclip.cpp', line 619

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