diff --git a/README.md b/README.md new file mode 100644 index 0000000..e455e9c --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +## alfred-pangu-workflow + +![盘古之白](https://tva1.sinaimg.cn/large/006y8mN6ly1g95xl43qg2j303l03lq2v.jpg) + +[下载链接]() + +**前言:** + +在使用 [alfred-clipboard-ocr](https://github.com/oott123/alfred-clipboard-ocr) 的过程中,ocr 识别的文字排版不美观,每次都要手动添加空格,想着利用 Workflow 「一步到位」,所以写了这个简单的 Workflow + +**简介:** + +格式化剪贴板文字,为文字中的中文与英文、数字之间加上空格。 + +**使用方法**: + +复制到剪贴板后,打开 Alfred,输入 `pangu` 关键字,回车。格式后的文本会复制到剪贴板中,此时直接 Ctrl+V 粘贴即可 + +**示例:** + +![image-20191121202655416](https://tva1.sinaimg.cn/large/006y8mN6ly1g95xmme437j30zy09stc3.jpg) + +**提示:** + +[中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines) +[為什麼你們就是不能加個空格呢?](https://github.com/vinta/pangu.js) + +**拓展:** + +[alfred-clipboard-ocr](https://github.com/oott123/alfred-clipboard-ocr) 是一个对剪贴板中的图片内容调用百度云 API 做 OCR 识别的 Alfred 工作流。对于不能选择复制的文字,可利用截屏后 OCR 识别,大大提高生产力。可在 alfred-clipboard-ocr 工作流中,加入 alfred-pangu-workflow 的脚本。 + +![image-20191121203616998](https://tva1.sinaimg.cn/large/006y8mN6ly1g95xwd8pbsj313608y77v.jpg) \ No newline at end of file diff --git a/format.py b/format.py new file mode 100755 index 0000000..18f22c5 --- /dev/null +++ b/format.py @@ -0,0 +1,11 @@ +#!/usr/bin/python3 +# coding=utf-8 +import pangu +import pyperclip + +text = pyperclip.paste() +print(text) +new_text = pangu.spacing_text(text) +pyperclip.copy(new_text) +print(new_text) + diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..223ec08 Binary files /dev/null and b/icon.png differ diff --git a/info.plist b/info.plist new file mode 100644 index 0000000..4a9ddf2 --- /dev/null +++ b/info.plist @@ -0,0 +1,148 @@ + + + + + bundleid + com.deppwang.alfred-format-workflow + category + Tools + connections + + 7910EB1E-72C2-40CB-86D9-3C009AA296DA + + + destinationuid + 3549F4CA-4434-4EF7-9B68-B5988BF0027A + modifiers + 0 + modifiersubtext + + vitoclose + + + + F81BE267-5FB9-45E5-89F9-0D0936A17AFC + + + destinationuid + 7910EB1E-72C2-40CB-86D9-3C009AA296DA + modifiers + 0 + modifiersubtext + + vitoclose + + + + + createdby + DeppWang + description + 格式化剪贴板文字,为文字中的中文与英文、数字之间加上空格。 + disabled + + name + pangu + objects + + + config + + argumenttype + 2 + keyword + pangu + subtext + + text + 格式化剪贴板文字 + withspace + + + type + alfred.workflow.input.keyword + uid + F81BE267-5FB9-45E5-89F9-0D0936A17AFC + version + 1 + + + config + + concurrently + + escaping + 68 + script + python3 test.py + scriptargtype + 1 + scriptfile + ./format.py + type + 8 + + type + alfred.workflow.action.script + uid + 7910EB1E-72C2-40CB-86D9-3C009AA296DA + version + 2 + + + config + + lastpathcomponent + + onlyshowifquerypopulated + + removeextension + + text + + title + 已完成格式化,请查看剪贴板,或直接使用 Ctrl+V 复制 + + type + alfred.workflow.output.notification + uid + 3549F4CA-4434-4EF7-9B68-B5988BF0027A + version + 1 + + + readme + 格式化剪贴板文字,为文字中的中文与英文、数字之间加上空格。 +具体排版规范请参考: +「中文文案排版指北」(https://github.com/sparanoid/chinese-copywriting-guidelines) +「為什麼你們就是不能加個空格呢?」(https://github.com/vinta/pangu.js) + uidata + + 3549F4CA-4434-4EF7-9B68-B5988BF0027A + + xpos + 470 + ypos + 140 + + 7910EB1E-72C2-40CB-86D9-3C009AA296DA + + xpos + 310 + ypos + 140 + + F81BE267-5FB9-45E5-89F9-0D0936A17AFC + + xpos + 140 + ypos + 140 + + + version + 1.0 + webaddress + https://github.com/DeppWang/alfred-pangu-workflow + + diff --git a/main.py b/main.py new file mode 100755 index 0000000..18f22c5 --- /dev/null +++ b/main.py @@ -0,0 +1,11 @@ +#!/usr/bin/python3 +# coding=utf-8 +import pangu +import pyperclip + +text = pyperclip.paste() +print(text) +new_text = pangu.spacing_text(text) +pyperclip.copy(new_text) +print(new_text) + diff --git a/pangu.py b/pangu.py new file mode 100644 index 0000000..b564afb --- /dev/null +++ b/pangu.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python +# coding: utf-8 +""" +Paranoid text spacing for good readability, to automatically insert whitespace between CJK (Chinese, Japanese, Korean) and half-width characters (alphabetical letters, numerical digits and symbols). +>>> import pangu +>>> nwe_text = pangu.spacing_text('當你凝視著bug,bug也凝視著你') +>>> print(nwe_text) +'當你凝視著 bug,bug 也凝視著你' +>>> nwe_content = pangu.spacing_file('path/to/file.txt') +>>> print(nwe_content) +'與 PM 戰鬥的人,應當小心自己不要成為 PM' +""" + +import argparse +import os +import re +import sys + +__version__ = '4.0.6.1' +__all__ = ['spacing_text', 'spacing_file', 'spacing', 'cli'] + +CJK = r'\u2e80-\u2eff\u2f00-\u2fdf\u3040-\u309f\u30a0-\u30fa\u30fc-\u30ff\u3100-\u312f\u3200-\u32ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff' + +ANY_CJK = re.compile(r'[{CJK}]'.format(CJK=CJK)) + +CONVERT_TO_FULLWIDTH_CJK_SYMBOLS_CJK = re.compile('([{CJK}])([ ]*(?:[\\:]+|\\.)[ ]*)([{CJK}])'.format( + CJK=CJK)) # there is an extra non-capturing group compared to JavaScript version +CONVERT_TO_FULLWIDTH_CJK_SYMBOLS = re.compile('([{CJK}])[ ]*([~\\!;,\\?]+)[ ]*'.format(CJK=CJK)) +DOTS_CJK = re.compile('([\\.]{{2,}}|\u2026)([{CJK}])'.format(CJK=CJK)) # need to escape { } +FIX_CJK_COLON_ANS = re.compile('([{CJK}])\\:([A-Z0-9\\(\\)])'.format(CJK=CJK)) + +CJK_QUOTE = re.compile('([{CJK}])([`"\u05f4])'.format(CJK=CJK)) # no need to escape ` +QUOTE_CJK = re.compile('([`"\u05f4])([{CJK}])'.format(CJK=CJK)) # no need to escape ` +# FIX_QUOTE_ANY_QUOTE = re.compile(r'([`"\u05f4]+)(\s*)(.+?)(\s*)([`"\u05f4]+)') + +CJK_SINGLE_QUOTE_BUT_POSSESSIVE = re.compile("([{CJK}])('[^s])".format(CJK=CJK)) +SINGLE_QUOTE_CJK = re.compile("(')([{CJK}])".format(CJK=CJK)) +FIX_POSSESSIVE_SINGLE_QUOTE = re.compile("([{CJK}A-Za-z0-9])( )('s)".format(CJK=CJK)) + +HASH_ANS_CJK_HASH = re.compile('([{CJK}])(#)([{CJK}]+)(#)([{CJK}])'.format(CJK=CJK)) +CJK_HASH = re.compile('([{CJK}])(#([^ ]))'.format(CJK=CJK)) +HASH_CJK = re.compile('(([^ ])#)([{CJK}])'.format(CJK=CJK)) + +CJK_OPERATOR_ANS = re.compile('([{CJK}])([\\+\\-\\*\\/=&\\|])([A-Za-z0-9])'.format(CJK=CJK)) +ANS_OPERATOR_CJK = re.compile('([A-Za-z0-9])([\\+\\-\\*\\/=&\\|<>])([{CJK}])'.format(CJK=CJK)) + +FIX_SLASH_AS = re.compile(r'([/]) ([a-z\-_\./]+)') +FIX_SLASH_AS_SLASH = re.compile(r'([/\.])([A-Za-z\-_\./]+) ([/])') + +CJK_LEFT_BRACKET = re.compile('([{CJK}])([\\(\\[\\{{<>\u201c])'.format(CJK=CJK)) # need to escape { +RIGHT_BRACKET_CJK = re.compile('([\\)\\]\\}}<>\u201d])([{CJK}])'.format(CJK=CJK)) # need to escape } +# FIX_LEFT_BRACKET_ANY_RIGHT_BRACKET = re.compile( +# r'([\(\[\{<\u201c]+)(\s*)(.+?)(\s*)([\)\]\}>\u201d]+)') # need to escape { } +ANS_CJK_LEFT_BRACKET_ANY_RIGHT_BRACKET = re.compile( + '([A-Za-z0-9{CJK}])[ ]*([\u201c])([A-Za-z0-9{CJK}\\-_ ]+)([\u201d])'.format(CJK=CJK)) +LEFT_BRACKET_ANY_RIGHT_BRACKET_ANS_CJK = re.compile( + '([\u201c])([A-Za-z0-9{CJK}\\-_ ]+)([\u201d])[ ]*([A-Za-z0-9{CJK}])'.format(CJK=CJK)) + +# AN_LEFT_BRACKET = re.compile(r'([A-Za-z0-9])([\(\[\{])') +RIGHT_BRACKET_AN = re.compile(r'([\)\]\}])([A-Za-z0-9])') + +CJK_ANS = re.compile( + '([{CJK}])([A-Za-z\u0370-\u03ff0-9@\\$%\\^&\\-\\+\\\\=\\|/\u00a1-\u00ff\u2150-\u218f\u2700—\u27bf])'.format( + CJK=CJK)) +ANS_CJK = re.compile( + '([A-Za-z\u0370-\u03ff0-9~\\!\\$%\\^&\\-\\+\\\\=\\|;:,\\./\\?\u00a1-\u00ff\u2150-\u218f\u2700—\u27bf])([{CJK}])'.format( + CJK=CJK)) + +S_A = re.compile(r'(%)([A-Za-z])') + +MIDDLE_DOT = re.compile(r'([ ]*)([\u00b7\u2022\u2027])([ ]*)') + +# Python version only +TILDES = re.compile(r'~+') +EXCLAMATION_MARKS = re.compile(r'!+') +SEMICOLONS = re.compile(r';+') +COLONS = re.compile(r':+') +COMMAS = re.compile(r',+') +PERIODS = re.compile(r'\.+') +QUESTION_MARKS = re.compile(r'\?+') + + +def convert_to_fullwidth(symbols): + symbols = TILDES.sub('~', symbols) + symbols = EXCLAMATION_MARKS.sub('!', symbols) + symbols = SEMICOLONS.sub(';', symbols) + symbols = COLONS.sub(':', symbols) + symbols = COMMAS.sub(',', symbols) + symbols = PERIODS.sub('。', symbols) + symbols = QUESTION_MARKS.sub('?', symbols) + return symbols.strip() + + +def spacing(text): + """ + Perform paranoid text spacing on text. + """ + if len(text) <= 1 or not ANY_CJK.search(text): + return text + + new_text = text + + # TODO: refactoring + matched = CONVERT_TO_FULLWIDTH_CJK_SYMBOLS_CJK.search(new_text) + while matched: + start, end = matched.span() + new_text = ''.join( + (new_text[:start + 1], convert_to_fullwidth(new_text[start + 1:end - 1]), new_text[end - 1:])) + matched = CONVERT_TO_FULLWIDTH_CJK_SYMBOLS_CJK.search(new_text) + + matched = CONVERT_TO_FULLWIDTH_CJK_SYMBOLS.search(new_text) + while matched: + start, end = matched.span() + new_text = ''.join( + (new_text[:start + 1].strip(), convert_to_fullwidth(new_text[start + 1:end]), new_text[end:].strip())) + matched = CONVERT_TO_FULLWIDTH_CJK_SYMBOLS.search(new_text) + + new_text = DOTS_CJK.sub(r'\1 \2', new_text) + new_text = FIX_CJK_COLON_ANS.sub(r'\1:\2', new_text) + + new_text = CJK_QUOTE.sub(r'\1 \2', new_text) + new_text = QUOTE_CJK.sub(r'\1 \2', new_text) + # new_text = FIX_QUOTE_ANY_QUOTE.sub(r'\1\3\5', new_text) + + new_text = CJK_SINGLE_QUOTE_BUT_POSSESSIVE.sub(r'\1 \2', new_text) + new_text = SINGLE_QUOTE_CJK.sub(r'\1 \2', new_text) + new_text = FIX_POSSESSIVE_SINGLE_QUOTE.sub(r"\1's", new_text) + + new_text = HASH_ANS_CJK_HASH.sub(r'\1 \2\3\4 \5', new_text) + new_text = CJK_HASH.sub(r'\1 \2', new_text) + new_text = HASH_CJK.sub(r'\1 \3', new_text) + + new_text = CJK_OPERATOR_ANS.sub(r'\1 \2 \3', new_text) + new_text = ANS_OPERATOR_CJK.sub(r'\1 \2 \3', new_text) + + new_text = FIX_SLASH_AS.sub(r'\1\2', new_text) + new_text = FIX_SLASH_AS_SLASH.sub(r'\1\2\3', new_text) + + new_text = CJK_LEFT_BRACKET.sub(r'\1 \2', new_text) + new_text = RIGHT_BRACKET_CJK.sub(r'\1 \2', new_text) + # new_text = FIX_LEFT_BRACKET_ANY_RIGHT_BRACKET.sub(r'\1\3\5', new_text) + new_text = ANS_CJK_LEFT_BRACKET_ANY_RIGHT_BRACKET.sub(r'\1 \2\3\4', new_text) + new_text = LEFT_BRACKET_ANY_RIGHT_BRACKET_ANS_CJK.sub(r'\1\2\3 \4', new_text) + + # new_text = AN_LEFT_BRACKET.sub(r'\1 \2', new_text) + # new_text = RIGHT_BRACKET_AN.sub(r'\1 \2', new_text) + + new_text = CJK_ANS.sub(r'\1 \2', new_text) + new_text = ANS_CJK.sub(r'\1 \2', new_text) + + new_text = S_A.sub(r'\1 \2', new_text) + + new_text = MIDDLE_DOT.sub('・', new_text) + + return new_text.strip() + + +def spacing_text(text): + """ + Perform paranoid text spacing on text. An alias of `spacing()`. + """ + return spacing(text) + + +def spacing_file(path): + """ + Perform paranoid text spacing from file. + """ + # TODO: read line by line + with open(os.path.abspath(path), 'r', encoding='UTF-8') as f: + return spacing_text(f.read()) + + +def cli(args=None): + if not args: + args = sys.argv[1:] + + parser = argparse.ArgumentParser( + prog='pangu', + description='pangu.py -- Paranoid text spacing for good readability, to automatically insert whitespace between CJK and half-width characters (alphabetical letters, numerical digits and symbols).', + ) + parser.add_argument('-v', '--version', action='version', version=__version__) + parser.add_argument('-t', '--text', action='store_true', dest='is_text', required=False, + help='specify the input value is a text') + parser.add_argument('-f', '--file', action='store_true', dest='is_file', required=False, + help='specify the input value is a file path') + parser.add_argument('text_or_path', action='store', type=str, help='the text or file path to apply spacing') + + if not sys.stdin.isatty(): + print(spacing_text(sys.stdin.read())) # noqa: T003 + else: + args = parser.parse_args(args) + if args.is_text: + print(spacing_text(args.text_or_path)) # noqa: T003 + elif args.is_file: + print(spacing_file(args.text_or_path)) # noqa: T003 + else: + print(spacing_text(args.text_or_path)) # noqa: T003 + + +if __name__ == '__main__': + cli() diff --git a/push.sh b/push.sh new file mode 100644 index 0000000..4f1742d --- /dev/null +++ b/push.sh @@ -0,0 +1,6 @@ +#!/bin/bash +current="`date +'%Y-%m-%d %H:%M:%S'`" +msg="Updated: $current" +git ci -m "$msg" +git st +git push origin master diff --git a/pyperclip/__init__.py b/pyperclip/__init__.py new file mode 100644 index 0000000..3c2ea6c --- /dev/null +++ b/pyperclip/__init__.py @@ -0,0 +1,653 @@ +""" +Pyperclip + +A cross-platform clipboard module for Python, with copy & paste functions for plain text. +By Al Sweigart al@inventwithpython.com +BSD License + +Usage: + import pyperclip + pyperclip.copy('The text to be copied to the clipboard.') + spam = pyperclip.paste() + + if not pyperclip.is_available(): + print("Copy functionality unavailable!") + +On Windows, no additional modules are needed. +On Mac, the pyobjc module is used, falling back to the pbcopy and pbpaste cli + commands. (These commands should come with OS X.). +On Linux, install xclip or xsel via package manager. For example, in Debian: + sudo apt-get install xclip + sudo apt-get install xsel + +Otherwise on Linux, you will need the gtk or PyQt5/PyQt4 modules installed. + +gtk and PyQt4 modules are not available for Python 3, +and this module does not work with PyGObject yet. + +Note: There seems to be a way to get gtk on Python 3, according to: + https://askubuntu.com/questions/697397/python3-is-not-supporting-gtk-module + +Cygwin is currently not supported. + +Security Note: This module runs programs with these names: + - which + - where + - pbcopy + - pbpaste + - xclip + - xsel + - klipper + - qdbus +A malicious user could rename or add programs with these names, tricking +Pyperclip into running them with whatever permissions the Python process has. + +""" +__version__ = '1.7.0' + +import contextlib +import ctypes +import os +import platform +import subprocess +import sys +import time +import warnings + +from ctypes import c_size_t, sizeof, c_wchar_p, get_errno, c_wchar + + +# `import PyQt4` sys.exit()s if DISPLAY is not in the environment. +# Thus, we need to detect the presence of $DISPLAY manually +# and not load PyQt4 if it is absent. +HAS_DISPLAY = os.getenv("DISPLAY", False) + +EXCEPT_MSG = """ + Pyperclip could not find a copy/paste mechanism for your system. + For more information, please visit https://pyperclip.readthedocs.io/en/latest/introduction.html#not-implemented-error """ + +PY2 = sys.version_info[0] == 2 + +STR_OR_UNICODE = unicode if PY2 else str # For paste(): Python 3 uses str, Python 2 uses unicode. + +ENCODING = 'utf-8' + +# The "which" unix command finds where a command is. +if platform.system() == 'Windows': + WHICH_CMD = 'where' +else: + WHICH_CMD = 'which' + +def _executable_exists(name): + return subprocess.call([WHICH_CMD, name], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 + + + +# Exceptions +class PyperclipException(RuntimeError): + pass + +class PyperclipWindowsException(PyperclipException): + def __init__(self, message): + message += " (%s)" % ctypes.WinError() + super(PyperclipWindowsException, self).__init__(message) + + +def _stringifyText(text): + if PY2: + acceptedTypes = (unicode, str, int, float, bool) + else: + acceptedTypes = (str, int, float, bool) + if not isinstance(text, acceptedTypes): + raise PyperclipException('only str, int, float, and bool values can be copied to the clipboard, not %s' % (text.__class__.__name__)) + return STR_OR_UNICODE(text) + + +def init_osx_pbcopy_clipboard(): + + def copy_osx_pbcopy(text): + text = _stringifyText(text) # Converts non-str values to str. + p = subprocess.Popen(['pbcopy', 'w'], + stdin=subprocess.PIPE, close_fds=True) + p.communicate(input=text.encode(ENCODING)) + + def paste_osx_pbcopy(): + p = subprocess.Popen(['pbpaste', 'r'], + stdout=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + return stdout.decode(ENCODING) + + return copy_osx_pbcopy, paste_osx_pbcopy + + +def init_osx_pyobjc_clipboard(): + def copy_osx_pyobjc(text): + '''Copy string argument to clipboard''' + text = _stringifyText(text) # Converts non-str values to str. + newStr = Foundation.NSString.stringWithString_(text).nsstring() + newData = newStr.dataUsingEncoding_(Foundation.NSUTF8StringEncoding) + board = AppKit.NSPasteboard.generalPasteboard() + board.declareTypes_owner_([AppKit.NSStringPboardType], None) + board.setData_forType_(newData, AppKit.NSStringPboardType) + + def paste_osx_pyobjc(): + "Returns contents of clipboard" + board = AppKit.NSPasteboard.generalPasteboard() + content = board.stringForType_(AppKit.NSStringPboardType) + return content + + return copy_osx_pyobjc, paste_osx_pyobjc + + +def init_gtk_clipboard(): + global gtk + import gtk + + def copy_gtk(text): + global cb + text = _stringifyText(text) # Converts non-str values to str. + cb = gtk.Clipboard() + cb.set_text(text) + cb.store() + + def paste_gtk(): + clipboardContents = gtk.Clipboard().wait_for_text() + # for python 2, returns None if the clipboard is blank. + if clipboardContents is None: + return '' + else: + return clipboardContents + + return copy_gtk, paste_gtk + + +def init_qt_clipboard(): + global QApplication + # $DISPLAY should exist + + # Try to import from qtpy, but if that fails try PyQt5 then PyQt4 + try: + from qtpy.QtWidgets import QApplication + except: + try: + from PyQt5.QtWidgets import QApplication + except: + from PyQt4.QtGui import QApplication + + app = QApplication.instance() + if app is None: + app = QApplication([]) + + def copy_qt(text): + text = _stringifyText(text) # Converts non-str values to str. + cb = app.clipboard() + cb.setText(text) + + def paste_qt(): + cb = app.clipboard() + return STR_OR_UNICODE(cb.text()) + + return copy_qt, paste_qt + + +def init_xclip_clipboard(): + DEFAULT_SELECTION='c' + PRIMARY_SELECTION='p' + + def copy_xclip(text, primary=False): + text = _stringifyText(text) # Converts non-str values to str. + selection=DEFAULT_SELECTION + if primary: + selection=PRIMARY_SELECTION + p = subprocess.Popen(['xclip', '-selection', selection], + stdin=subprocess.PIPE, close_fds=True) + p.communicate(input=text.encode(ENCODING)) + + def paste_xclip(primary=False): + selection=DEFAULT_SELECTION + if primary: + selection=PRIMARY_SELECTION + p = subprocess.Popen(['xclip', '-selection', selection, '-o'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=True) + stdout, stderr = p.communicate() + # Intentionally ignore extraneous output on stderr when clipboard is empty + return stdout.decode(ENCODING) + + return copy_xclip, paste_xclip + + +def init_xsel_clipboard(): + DEFAULT_SELECTION='-b' + PRIMARY_SELECTION='-p' + + def copy_xsel(text, primary=False): + text = _stringifyText(text) # Converts non-str values to str. + selection_flag = DEFAULT_SELECTION + if primary: + selection_flag = PRIMARY_SELECTION + p = subprocess.Popen(['xsel', selection_flag, '-i'], + stdin=subprocess.PIPE, close_fds=True) + p.communicate(input=text.encode(ENCODING)) + + def paste_xsel(primary=False): + selection_flag = DEFAULT_SELECTION + if primary: + selection_flag = PRIMARY_SELECTION + p = subprocess.Popen(['xsel', selection_flag, '-o'], + stdout=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + return stdout.decode(ENCODING) + + return copy_xsel, paste_xsel + + +def init_klipper_clipboard(): + def copy_klipper(text): + text = _stringifyText(text) # Converts non-str values to str. + p = subprocess.Popen( + ['qdbus', 'org.kde.klipper', '/klipper', 'setClipboardContents', + text.encode(ENCODING)], + stdin=subprocess.PIPE, close_fds=True) + p.communicate(input=None) + + def paste_klipper(): + p = subprocess.Popen( + ['qdbus', 'org.kde.klipper', '/klipper', 'getClipboardContents'], + stdout=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + + # Workaround for https://bugs.kde.org/show_bug.cgi?id=342874 + # TODO: https://github.com/asweigart/pyperclip/issues/43 + clipboardContents = stdout.decode(ENCODING) + # even if blank, Klipper will append a newline at the end + assert len(clipboardContents) > 0 + # make sure that newline is there + assert clipboardContents.endswith('\n') + if clipboardContents.endswith('\n'): + clipboardContents = clipboardContents[:-1] + return clipboardContents + + return copy_klipper, paste_klipper + + +def init_dev_clipboard_clipboard(): + def copy_dev_clipboard(text): + text = _stringifyText(text) # Converts non-str values to str. + if text == '': + warnings.warn('Pyperclip cannot copy a blank string to the clipboard on Cygwin. This is effectively a no-op.') + if '\r' in text: + warnings.warn('Pyperclip cannot handle \\r characters on Cygwin.') + + fo = open('/dev/clipboard', 'wt') + fo.write(text) + fo.close() + + def paste_dev_clipboard(): + fo = open('/dev/clipboard', 'rt') + content = fo.read() + fo.close() + return content + + return copy_dev_clipboard, paste_dev_clipboard + + +def init_no_clipboard(): + class ClipboardUnavailable(object): + + def __call__(self, *args, **kwargs): + raise PyperclipException(EXCEPT_MSG) + + if PY2: + def __nonzero__(self): + return False + else: + def __bool__(self): + return False + + return ClipboardUnavailable(), ClipboardUnavailable() + + + + +# Windows-related clipboard functions: +class CheckedCall(object): + def __init__(self, f): + super(CheckedCall, self).__setattr__("f", f) + + def __call__(self, *args): + ret = self.f(*args) + if not ret and get_errno(): + raise PyperclipWindowsException("Error calling " + self.f.__name__) + return ret + + def __setattr__(self, key, value): + setattr(self.f, key, value) + + +def init_windows_clipboard(): + global HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND, HINSTANCE, HMENU, BOOL, UINT, HANDLE + from ctypes.wintypes import (HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND, + HINSTANCE, HMENU, BOOL, UINT, HANDLE) + + windll = ctypes.windll + msvcrt = ctypes.CDLL('msvcrt') + + safeCreateWindowExA = CheckedCall(windll.user32.CreateWindowExA) + safeCreateWindowExA.argtypes = [DWORD, LPCSTR, LPCSTR, DWORD, INT, INT, + INT, INT, HWND, HMENU, HINSTANCE, LPVOID] + safeCreateWindowExA.restype = HWND + + safeDestroyWindow = CheckedCall(windll.user32.DestroyWindow) + safeDestroyWindow.argtypes = [HWND] + safeDestroyWindow.restype = BOOL + + OpenClipboard = windll.user32.OpenClipboard + OpenClipboard.argtypes = [HWND] + OpenClipboard.restype = BOOL + + safeCloseClipboard = CheckedCall(windll.user32.CloseClipboard) + safeCloseClipboard.argtypes = [] + safeCloseClipboard.restype = BOOL + + safeEmptyClipboard = CheckedCall(windll.user32.EmptyClipboard) + safeEmptyClipboard.argtypes = [] + safeEmptyClipboard.restype = BOOL + + safeGetClipboardData = CheckedCall(windll.user32.GetClipboardData) + safeGetClipboardData.argtypes = [UINT] + safeGetClipboardData.restype = HANDLE + + safeSetClipboardData = CheckedCall(windll.user32.SetClipboardData) + safeSetClipboardData.argtypes = [UINT, HANDLE] + safeSetClipboardData.restype = HANDLE + + safeGlobalAlloc = CheckedCall(windll.kernel32.GlobalAlloc) + safeGlobalAlloc.argtypes = [UINT, c_size_t] + safeGlobalAlloc.restype = HGLOBAL + + safeGlobalLock = CheckedCall(windll.kernel32.GlobalLock) + safeGlobalLock.argtypes = [HGLOBAL] + safeGlobalLock.restype = LPVOID + + safeGlobalUnlock = CheckedCall(windll.kernel32.GlobalUnlock) + safeGlobalUnlock.argtypes = [HGLOBAL] + safeGlobalUnlock.restype = BOOL + + wcslen = CheckedCall(msvcrt.wcslen) + wcslen.argtypes = [c_wchar_p] + wcslen.restype = UINT + + GMEM_MOVEABLE = 0x0002 + CF_UNICODETEXT = 13 + + @contextlib.contextmanager + def window(): + """ + Context that provides a valid Windows hwnd. + """ + # we really just need the hwnd, so setting "STATIC" + # as predefined lpClass is just fine. + hwnd = safeCreateWindowExA(0, b"STATIC", None, 0, 0, 0, 0, 0, + None, None, None, None) + try: + yield hwnd + finally: + safeDestroyWindow(hwnd) + + @contextlib.contextmanager + def clipboard(hwnd): + """ + Context manager that opens the clipboard and prevents + other applications from modifying the clipboard content. + """ + # We may not get the clipboard handle immediately because + # some other application is accessing it (?) + # We try for at least 500ms to get the clipboard. + t = time.time() + 0.5 + success = False + while time.time() < t: + success = OpenClipboard(hwnd) + if success: + break + time.sleep(0.01) + if not success: + raise PyperclipWindowsException("Error calling OpenClipboard") + + try: + yield + finally: + safeCloseClipboard() + + def copy_windows(text): + # This function is heavily based on + # http://msdn.com/ms649016#_win32_Copying_Information_to_the_Clipboard + + text = _stringifyText(text) # Converts non-str values to str. + + with window() as hwnd: + # http://msdn.com/ms649048 + # If an application calls OpenClipboard with hwnd set to NULL, + # EmptyClipboard sets the clipboard owner to NULL; + # this causes SetClipboardData to fail. + # => We need a valid hwnd to copy something. + with clipboard(hwnd): + safeEmptyClipboard() + + if text: + # http://msdn.com/ms649051 + # If the hMem parameter identifies a memory object, + # the object must have been allocated using the + # function with the GMEM_MOVEABLE flag. + count = wcslen(text) + 1 + handle = safeGlobalAlloc(GMEM_MOVEABLE, + count * sizeof(c_wchar)) + locked_handle = safeGlobalLock(handle) + + ctypes.memmove(c_wchar_p(locked_handle), c_wchar_p(text), count * sizeof(c_wchar)) + + safeGlobalUnlock(handle) + safeSetClipboardData(CF_UNICODETEXT, handle) + + def paste_windows(): + with clipboard(None): + handle = safeGetClipboardData(CF_UNICODETEXT) + if not handle: + # GetClipboardData may return NULL with errno == NO_ERROR + # if the clipboard is empty. + # (Also, it may return a handle to an empty buffer, + # but technically that's not empty) + return "" + return c_wchar_p(handle).value + + return copy_windows, paste_windows + + +def init_wsl_clipboard(): + def copy_wsl(text): + text = _stringifyText(text) # Converts non-str values to str. + p = subprocess.Popen(['clip.exe'], + stdin=subprocess.PIPE, close_fds=True) + p.communicate(input=text.encode(ENCODING)) + + def paste_wsl(): + p = subprocess.Popen(['powershell.exe', '-command', 'Get-Clipboard'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=True) + stdout, stderr = p.communicate() + # WSL appends "\r\n" to the contents. + return stdout[:-2].decode(ENCODING) + + return copy_wsl, paste_wsl + + +# Automatic detection of clipboard mechanisms and importing is done in deteremine_clipboard(): +def determine_clipboard(): + ''' + Determine the OS/platform and set the copy() and paste() functions + accordingly. + ''' + + global Foundation, AppKit, gtk, qtpy, PyQt4, PyQt5 + + # Setup for the CYGWIN platform: + if 'cygwin' in platform.system().lower(): # Cygwin has a variety of values returned by platform.system(), such as 'CYGWIN_NT-6.1' + # FIXME: pyperclip currently does not support Cygwin, + # see https://github.com/asweigart/pyperclip/issues/55 + if os.path.exists('/dev/clipboard'): + warnings.warn('Pyperclip\'s support for Cygwin is not perfect, see https://github.com/asweigart/pyperclip/issues/55') + return init_dev_clipboard_clipboard() + + # Setup for the WINDOWS platform: + elif os.name == 'nt' or platform.system() == 'Windows': + return init_windows_clipboard() + + if platform.system() == 'Linux': + with open('/proc/version', 'r') as f: + if "Microsoft" in f.read(): + return init_wsl_clipboard() + + # Setup for the MAC OS X platform: + if os.name == 'mac' or platform.system() == 'Darwin': + try: + import Foundation # check if pyobjc is installed + import AppKit + except ImportError: + return init_osx_pbcopy_clipboard() + else: + return init_osx_pyobjc_clipboard() + + # Setup for the LINUX platform: + if HAS_DISPLAY: + try: + import gtk # check if gtk is installed + except ImportError: + pass # We want to fail fast for all non-ImportError exceptions. + else: + return init_gtk_clipboard() + + if _executable_exists("xsel"): + return init_xsel_clipboard() + if _executable_exists("xclip"): + return init_xclip_clipboard() + if _executable_exists("klipper") and _executable_exists("qdbus"): + return init_klipper_clipboard() + + try: + # qtpy is a small abstraction layer that lets you write applications using a single api call to either PyQt or PySide. + # https://pypi.python.org/pypi/QtPy + import qtpy # check if qtpy is installed + except ImportError: + # If qtpy isn't installed, fall back on importing PyQt4. + try: + import PyQt5 # check if PyQt5 is installed + except ImportError: + try: + import PyQt4 # check if PyQt4 is installed + except ImportError: + pass # We want to fail fast for all non-ImportError exceptions. + else: + return init_qt_clipboard() + else: + return init_qt_clipboard() + else: + return init_qt_clipboard() + + + return init_no_clipboard() + + +def set_clipboard(clipboard): + ''' + Explicitly sets the clipboard mechanism. The "clipboard mechanism" is how + the copy() and paste() functions interact with the operating system to + implement the copy/paste feature. The clipboard parameter must be one of: + - pbcopy + - pbobjc (default on Mac OS X) + - gtk + - qt + - xclip + - xsel + - klipper + - windows (default on Windows) + - no (this is what is set when no clipboard mechanism can be found) + ''' + global copy, paste + + clipboard_types = {'pbcopy': init_osx_pbcopy_clipboard, + 'pyobjc': init_osx_pyobjc_clipboard, + 'gtk': init_gtk_clipboard, + 'qt': init_qt_clipboard, # TODO - split this into 'qtpy', 'pyqt4', and 'pyqt5' + 'xclip': init_xclip_clipboard, + 'xsel': init_xsel_clipboard, + 'klipper': init_klipper_clipboard, + 'windows': init_windows_clipboard, + 'no': init_no_clipboard} + + if clipboard not in clipboard_types: + raise ValueError('Argument must be one of %s' % (', '.join([repr(_) for _ in clipboard_types.keys()]))) + + # Sets pyperclip's copy() and paste() functions: + copy, paste = clipboard_types[clipboard]() + + +def lazy_load_stub_copy(text): + ''' + A stub function for copy(), which will load the real copy() function when + called so that the real copy() function is used for later calls. + + This allows users to import pyperclip without having determine_clipboard() + automatically run, which will automatically select a clipboard mechanism. + This could be a problem if it selects, say, the memory-heavy PyQt4 module + but the user was just going to immediately call set_clipboard() to use a + different clipboard mechanism. + + The lazy loading this stub function implements gives the user a chance to + call set_clipboard() to pick another clipboard mechanism. Or, if the user + simply calls copy() or paste() without calling set_clipboard() first, + will fall back on whatever clipboard mechanism that determine_clipboard() + automatically chooses. + ''' + global copy, paste + copy, paste = determine_clipboard() + return copy(text) + + +def lazy_load_stub_paste(): + ''' + A stub function for paste(), which will load the real paste() function when + called so that the real paste() function is used for later calls. + + This allows users to import pyperclip without having determine_clipboard() + automatically run, which will automatically select a clipboard mechanism. + This could be a problem if it selects, say, the memory-heavy PyQt4 module + but the user was just going to immediately call set_clipboard() to use a + different clipboard mechanism. + + The lazy loading this stub function implements gives the user a chance to + call set_clipboard() to pick another clipboard mechanism. Or, if the user + simply calls copy() or paste() without calling set_clipboard() first, + will fall back on whatever clipboard mechanism that determine_clipboard() + automatically chooses. + ''' + global copy, paste + copy, paste = determine_clipboard() + return paste() + + +def is_available(): + return copy != lazy_load_stub_copy and paste != lazy_load_stub_paste + + +# Initially, copy() and paste() are set to lazy loading wrappers which will +# set `copy` and `paste` to real functions the first time they're used, unless +# set_clipboard() or determine_clipboard() is called first. +copy, paste = lazy_load_stub_copy, lazy_load_stub_paste + + +__all__ = ['copy', 'paste', 'set_clipboard', 'determine_clipboard'] + + diff --git a/pyperclip/__main__.py b/pyperclip/__main__.py new file mode 100644 index 0000000..c7702be --- /dev/null +++ b/pyperclip/__main__.py @@ -0,0 +1,12 @@ +import pyperclip +import sys + +if len(sys.argv) > 1 and sys.argv[1] in ('-c', '--copy'): + pyperclip.copy(sys.stdin.read()) +elif len(sys.argv) > 1 and sys.argv[1] in ('-p', '--paste'): + sys.stdout.write(pyperclip.paste()) +else: + print('Usage: python -m pyperclip [-c | --copy] | [-p | --paste]') + print() + print('When copying, stdin will be placed on the clipboard.') + print('When pasting, the clipboard will be written to stdout.') \ No newline at end of file