├── .gitignore ├── Application ├── QuickSave.ini ├── README.md ├── auto-preview.ini ├── clear-clipboard-tab.ini ├── copy-paste-items-as-json.ini ├── decrypt-and-type.ini ├── edit-files.ini ├── filter.ini ├── frequent-items-tab.ini ├── hide-item-content.ini ├── highlight-text.ini ├── join-selected-items.ini ├── mark-selected-items.ini ├── next-previous.ini ├── paste-and-forget.ini ├── paste-formatted-json.ini ├── qr-code.ini ├── remove-background-and-text-colors.ini ├── remove-carriage-return-linefeed-and-multiple-space-then-paste.ini ├── render-html.ini ├── save-item-clipboard-to-file.ini ├── search-all-tabs.ini ├── search-and-replace.ini ├── select-nth-item.ini ├── sort-items.ini ├── sort-tabs.ini ├── tab-alt-navigation.ini ├── tab-key-select.ini ├── tab-switcher.ini ├── toggle-simple-items.ini ├── toggle-tag.ini ├── translate-to-english.ini ├── treefy.ini └── undoable-move-to-trash.ini ├── Automatic ├── README.md ├── big-data-tab.ini ├── copy-a-secret-if-modifier-held.ini ├── copy-clipboard-to-windows-tab.ini ├── ignore-images-when-text-is-available.ini ├── ignore-passwords-tokens.ini ├── image-tab.ini ├── import-commands-after-copied.ini ├── keepassxc-protector.ini ├── linkify.ini ├── mouse-selections-tab.ini ├── play-sound-when-copying-to-clipboard-linux.ini ├── play-sound-when-copying-to-clipboard-windows.ini ├── show-window-title.ini ├── store-copy-time.ini ├── synchronize-clipboard-with-other-sessions.ini └── tab-for-urls-with-title-and-icon.ini ├── Display ├── README.md ├── highlight-code.ini ├── preview-image-files.ini ├── render-markdown.ini └── toggle-show-as-plain-text.ini ├── Global ├── README.md ├── convert-markdown.ini ├── copy-a-secret.ini ├── copy-and-search-on-web.ini ├── copy-text-in-image.ini ├── cycle-items.ini ├── diff-latest-items.ini ├── disable-clipboard-monitoring-state-permanently.ini ├── edit-and-paste.ini ├── images │ └── cmd_show-char-code.png ├── paste-current-date-time-in-iso-8601.ini ├── paste-current-date-time.ini ├── paste-new-uuid.ini ├── paste-nminus1th-item.ini ├── push-pop-stack.ini ├── quick-cycle-items.ini ├── quickly-show-current-clipboard-content.ini ├── replace-all-occurences-in-selected-text.ini ├── screenshot-cutout.ini ├── screenshot.ini ├── select-nth-item.ini ├── show-char-code.ini ├── show-clipboard.ini ├── snippets.ini ├── stopwatch.ini ├── tabs-navigation.ini ├── to-title-case.ini ├── toggle-clipboard-storing.ini └── toggle-upper-lower-case-of-selected-text.ini ├── README.md ├── Scripts ├── README.md ├── backup-on-exit.ini ├── blocklisted_texts.ini ├── bookmarks.ini ├── clear-clipboard-after-interval.ini ├── clipboard-notification.ini ├── full-clipboard-in-title.ini ├── ignore-non-mouse-text-selection.ini ├── indicate-copy-in-icon.ini ├── keep-item-in-clipboard.ini ├── make-selected-tab-clipboard.ini ├── no-clipboard-in-title-and-tooltip.ini ├── remeber-clipboard-storing-state.ini ├── reset-empty-clipboard.ini ├── show-on-start.ini ├── top-item-to-clipboard.ini ├── wayland-support.ini └── write-clipboard-to-file.ini ├── Templates ├── README.md ├── modify-selected-items.ini └── modify-selected-text.ini ├── images ├── copy-command-link.png ├── import-command-notification.png └── select-category.png ├── tests ├── Global │ └── snippets.js ├── Scripts │ ├── reset-empty-clipboard.js │ └── show-on-start.js ├── session.js └── test_functions.js └── utils └── tests.sh /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hluk/copyq-commands/544c88c5e5c121e2bd9a650140ac9fe4e024208a/.gitignore -------------------------------------------------------------------------------- /Application/QuickSave.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=QuickSave 3 | Command=" 4 | copyq: 5 | // To save a clipboard item as file to a preset path using tags as it's file name. 6 | // Avoid a dialogue for user input 7 | 8 | // Initial user setup: 9 | currentPath('C:/abc/xyz') 10 | // Set your default path here for once: 11 | 12 | var words = 3 13 | // number of words to use from tags (not number of tags) 14 | 15 | var defaultname = 'clip' 16 | // default file name if there are no tags. 17 | 18 | var suffices = { 19 | 'image/svg': 'svg', 20 | 'image/png': 'png', 21 | 'image/jpeg': 'jpg', 22 | 'image/jpg': 'jpg', 23 | 'image/bmp': 'bmp', 24 | 'text/html': 'html', 25 | 'text/plain' : 'txt', 26 | } 27 | 28 | function addSuffix(fileName, format) 29 | { 30 | var suffix = suffices[format] 31 | return suffix ? fileName + \".\" + suffix : fileName 32 | } 33 | 34 | function filterFormats(format) 35 | { 36 | return /^[a-z]/.test(format) && !/^application\\/x/.test(format) 37 | } 38 | 39 | function itemFormats(row) 40 | { 41 | return str(read('?', row)) 42 | .split('\\n') 43 | .filter(filterFormats) 44 | } 45 | 46 | function formatPriority(format) 47 | { 48 | var k = Object.keys(suffices); 49 | var i = k.indexOf(format); 50 | return i === -1 ? k.length : i 51 | } 52 | 53 | function reorderFormats(formats) 54 | { 55 | formats.sort(function(lhs, rhs){ 56 | var i = formatPriority(lhs); 57 | var j = formatPriority(rhs); 58 | return i === j ? lhs.localeCompare(rhs) : i - j; 59 | }) 60 | } 61 | 62 | if (selectedtab()) tab(selectedtab()) 63 | var row = selectedtab() ? currentitem() : -1 64 | var formats = itemFormats(row) 65 | reorderFormats(formats) 66 | 67 | 68 | var tags = str(data('application/x-copyq-tags')) 69 | var nametag = tags.trim().replace(/[^a-z0-9]+/gi, '_').split('_',words).join('_') 70 | // simplyfy & only take first two words from tags. can be modified. 71 | var defaultFileName = currentPath()+'/'+(tags=='' ? defaultname : nametag) 72 | // that's without ext 73 | var id = index()+1 74 | // just to start from 1. 75 | 76 | var format = formats[0] 77 | 78 | // name incrementally to avoid overwriting. 79 | do { 80 | var filename = defaultFileName +'-'+ id 81 | var filenameExt = addSuffix(filename, format) 82 | var f = File(filenameExt) 83 | id++ 84 | } 85 | while (f.exists()) 86 | 87 | 88 | if (!f.open()) { 89 | popup('Failed to open \"' + f.fileName() + '\"', f.errorString()) 90 | abort() 91 | } 92 | 93 | f.write(selectedtab() ? getitem(currentitem())[format] : clipboard(format)) 94 | 95 | f.close() 96 | popup(\"Item Saved\", 'Item saved as \"' + f.fileName() + '\".')" 97 | 98 | InMenu=true 99 | Icon=\xf56d 100 | Shortcut=ctrl+alt+s -------------------------------------------------------------------------------- /Application/README.md: -------------------------------------------------------------------------------- 1 | This section contains commands which can be executed from tool bar, menu or with shortcut. 2 | 3 | ### [Clear Clipboard Tab](clear-clipboard-tab.ini) 4 | 5 | Remove all items from clipboard tab using menu item (or custom shortcut). 6 | 7 | ### [Copy and Paste Items in JSON Format](copy-paste-items-as-json.ini) 8 | 9 | Allows to easily share items in readable text format. 10 | 11 | ### [Edit Files](edit-files.ini) 12 | 13 | Opens files referenced by selected item in external editor (uses "External editor command" from "History" config tab). 14 | 15 | Works with following path formats (some editors may not support all of these). 16 | 17 | - `C:/...` 18 | - `file://...` 19 | - `~...` (some shells) 20 | - `%...%...` (Windows environment variables) 21 | - `$...` (environment variables) 22 | - `/c/...` (gitbash) 23 | 24 | ### [Decrypt and Type](decrypt-and-type.ini) 25 | 26 | Safely types in decrypted text of selected item instead of using clipboard. 27 | 28 | Requires "xdotool" utility which works only on Linux/X11. 29 | 30 | ### [Navigate Tabs With Alt+Number](tab-alt-navigation.ini) 31 | 32 | Enables Alt+1 .. Alt+9, Alt+0 to navigate to the tab based on the order 33 | (instead of the default Ctrl+Number navigation). 34 | 35 | ### [Next/Previous](next-previous.ini) 36 | 37 | Remaps Up/Down arrows for browsing items to Ctrl+P/Ctrl+N or other custom 38 | shortcuts. 39 | 40 | ### [Tab for Frequent Items](frequent-items-tab.ini) 41 | 42 | Two commands to add frequent items to special tab and to show frequent items with global shortcut. 43 | 44 | ### [Hide/Show Item Content](hide-item-content.ini) 45 | 46 | Hides (or shows if hidden) item content except notes and tags. 47 | 48 | ### [Highlight Text](highlight-text.ini) 49 | 50 | Highlights all occurrences of a text (change `x = "text"` to match something else than `text`). 51 | 52 | ### [Join Selected Items](join-selected-items.ini) 53 | 54 | Creates new item containing concatenated text of selected items. 55 | 56 | Change the `separator` variable to separate the merged items with a different 57 | string than line break `\n`. 58 | 59 | ### [Mark Selected Items](mark-selected-items.ini) 60 | 61 | Toggles highlighting of selected items. 62 | 63 | ### [Paste and Forget](paste-and-forget.ini) 64 | 65 | Pastes selected items and clear clipboard. 66 | 67 | ### [Remove Background and Text Colors](remove-background-and-text-colors.ini) 68 | 69 | Removes background and text colors from rich text (e.g. text copied from web pages). 70 | 71 | Command can be both automatically applied on text copied to clipboard and invoked from menu (or using custom shortcut). 72 | 73 | ### [Remove Carriage Return, Line Feed and Multiple Space Then Paste](remove-carriage-return-linefeed-and-multiple-space-then-paste.ini) 74 | 75 | Removes carriage return, line feed and multiple space in the clipboard, then paste it. 76 | 77 | Useful when copy texts from PDF. 78 | 79 | ### [Render HTML](render-html.ini) 80 | 81 | Converts text item with HTML code to HTML item. 82 | 83 | ### [Save Item/Clipboard To a File](save-item-clipboard-to-file.ini) 84 | 85 | Opens dialog for saving selected item data to a file. 86 | 87 | ### [Select Nth Item](select-nth-item.ini) 88 | 89 | Overrides main window shortcuts Ctrl+1..9 to quickly select items in specific row. 90 | 91 | ### [Quick Save](QuickSave.ini) 92 | 93 | Saves an item as file to a preset path using available tags as it's file name, without overwriting. There is no user input dialog. 94 | This works great along the script [show window title](../Automatic/show-window-title.ini) which saves source window title to tags while adding to clipboard. 95 | After installation, you *must edit default folder (xyz) path*: `currentPath('C:/abc/xyz')` 96 | 97 | Other options: 98 | Number of words to use from tags (not number of tags): `var words = 3` 99 | Default file name if there are no tags: `var defaultname = 'clip'` 100 | 101 | ### [Search & Replace](search-and-replace.ini) 102 | 103 | Search and replace specified text in the currently selected items or all items 104 | in the current tab. 105 | 106 | ### [Search All Tabs](search-all-tabs.ini) 107 | 108 | Searches an text in all tabs and stores found items in "Search" tab. 109 | 110 | ### [Toggle Simple Items](toggle-simple-items.ini) 111 | 112 | Show/hide more compact items (one line of text or thumbnail). 113 | 114 | ### [Toggle Tag](toggle-tag.ini) 115 | 116 | Instead of two commands, one to tag and other to untag selected items, and see 117 | two actions in toolbar, you can use this command to toggle a tag. 118 | 119 | ### [Translate to English](translate-to-english.ini) 120 | 121 | Passes text to [Google Translate](https://translate.google.com/). 122 | 123 | ### [Undoable Move to Trash](undoable-move-to-trash.ini) 124 | 125 | Two commands to move items to trash and to undo removals. 126 | 127 | ### [Paste Formatted Json](paste-formatted-json.ini) 128 | 129 | Pastes selected Json text as a formatted Json text. 130 | If not Json, just pastes the text as is. 131 | 132 | ### [QR Code](qr-code.ini) 133 | 134 | From currently selected text items, creates a new item with the QR code for the 135 | text. 136 | 137 | Requires [qrcode](https://github.com/lincolnloop/python-qrcode) utility. 138 | 139 | ### [Sort Tabs](sort-tabs.ini) 140 | 141 | Sorts tabs by name. 142 | 143 | ### [Tab Key to Select Next/Previous](tab-key-select.ini) 144 | 145 | Use Tab and Shift+Tab to select next/previous item. 146 | 147 | ### [Tab Switcher](tab-switcher.ini) 148 | 149 | Use a shortcut or a toolbar action to quickly find a tab and focus it on Enter 150 | key or double click. 151 | 152 | This uses a separate CopyQ session/app-instance to show the tab list. 153 | 154 | ### [Treefy](treefy.ini) 155 | 156 | Convert indented line block (tabs, spaces or markdown headers) to an ASCII directory tree with the possibility to add a root element. 157 | 158 | Example 159 | 160 | ``` 161 | A1 162 | B1 163 | C1 164 | D1 165 | E1 166 | D2 167 | E2 168 | Z1 169 | Y1 170 | X1 171 | ``` 172 | 173 | Output (with root) 174 | 175 | ``` 176 | . 177 | ├─ A1 178 | │ └─ B1 179 | │ └─ C1 180 | │ ├─ D1 181 | │ │ └─ E1 182 | │ └─ D2 183 | │ └─ E2 184 | └─ Z1 185 | └─ Y1 186 | └─ X1 187 | ``` 188 | 189 | Output (without root) 190 | 191 | ``` 192 | A1 193 | └─ B1 194 | └─ C1 195 | ├─ D1 196 | │ └─ E1 197 | └─ D2 198 | └─ E2 199 | Z1 200 | └─ Y1 201 | └─ X1 202 | ``` 203 | 204 | 205 | ### [Sort Items](sort-items.ini) 206 | Sort items based on copy time, size, pinned status, etc. 207 | 208 | - When selecting single item, sort the entire tab 209 | - When selecting multiple items, only sort the selected items 210 | 211 | ### [Auto Preview Image or Long-text](auto-preview.ini) 212 | Automatically preview images and long-text, and support manual preview with `Space` key. You can set the number of lines and characters for long text. 213 | 214 | ``` 215 | // More than 100 characters and 2 lines are considered as long text 216 | var LongTextCharacters = 100 217 | var LongTextLines = 2 218 | ``` 219 | 220 | ### [Filter](filter.ini) 221 | Add a filter menu to quickly filter images, URLs, files, etc. 222 | 223 | You can customize the filters: 224 | 225 | ``` 226 | var filter1= { 227 | [mimeText]: 'File ---------------- F', //Text displayed on the menu 228 | [mimeIcon]: '', 229 | filter: '(?=^file://)', //Regular expression of the filter 230 | shortcut: 'f' //You need to set the menu shortcut of this command at the same time. 231 | } 232 | 233 | var filters = [filter1, filter2, filter3, ...] //Add filters to the list 234 | ``` -------------------------------------------------------------------------------- /Application/auto-preview.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Auto preview image or long-text 3 | MatchCommand=" 4 | copyq: 5 | if (visible()) { 6 | if (settings('AutoPreview') == 1) { 7 | 8 | if (selectedItems().length > 1) { 9 | preview(false) 10 | abort() 11 | } 12 | 13 | var preview_image = true 14 | var preview_longText = true 15 | 16 | // The characters count of long-text 17 | var LongTextCharacters = 100 18 | // The lines count of long-text 19 | var LongTextLines = 2 20 | 21 | var format = str(dataFormats()) 22 | var content = str(data(mimeUriList)) || str(data(mimeText)) 23 | 24 | icon_on() 25 | 26 | preview(condition()) 27 | } else { 28 | icon_off() 29 | } 30 | } 31 | function condition() { 32 | return ( 33 | preview_image && (/^image\\/.*/.test(format) || /^file:.*(png|jpe?g|bmp|svg|gif|ico|webp)$/.test(content)) 34 | || 35 | preview_longText && (content.length > LongTextCharacters || (content.split(/\\n/)).length > LongTextLines) 36 | ) 37 | } 38 | function icon_on() { 39 | menuItem['text'] = 'Auto preview OFF' 40 | menuItem['icon'] = '' 41 | } 42 | function icon_off() { 43 | menuItem['text'] = 'Auto preview ON' 44 | menuItem['icon'] = '' 45 | }" 46 | Command=" 47 | copyq: 48 | if (str(data(mimeShortcut))=='space') { 49 | preview(!preview()) 50 | abort() 51 | } 52 | if (settings('AutoPreview') == 1) { 53 | settings('AutoPreview', 0) 54 | popup('Auto preview❎','',1200) 55 | } else { 56 | settings('AutoPreview', 1) 57 | popup('Auto preview✅','',1200) 58 | }" 59 | InMenu=true 60 | Icon= 61 | Shortcut=f7, space -------------------------------------------------------------------------------- /Application/clear-clipboard-tab.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | tab(config('clipboard_tab')) 5 | items = Array 6 | .apply(0, Array(size())) 7 | .map(function(x,i){return i}) 8 | remove.apply(this, items)" 9 | Icon=\xf2ed 10 | InMenu=true 11 | MatchCommand=copyq: size() || fail() 12 | Name=Clear Clipboard Tab 13 | -------------------------------------------------------------------------------- /Application/copy-paste-items-as-json.ini: -------------------------------------------------------------------------------- 1 | [Commands] 2 | 1\Command=" 3 | copyq: 4 | var indent = 4 5 | 6 | function fromData(data) 7 | { 8 | var text = str(data) 9 | if ( data.equals(new ByteArray(text)) ) { 10 | if (text.indexOf('\\n') == -1) 11 | return text 12 | return { lines: text.split('\\n') } 13 | } 14 | return { base64: toBase64(data) } 15 | } 16 | 17 | var itemsData = selectedItemsData() 18 | for (var i in itemsData) { 19 | var itemData = itemsData[i] 20 | for (var format in itemData) 21 | itemData[format] = fromData(itemData[format]) 22 | } 23 | 24 | var text = JSON.stringify(itemsData, null, indent) 25 | copy('{ \"copyq_items\": ' + text + ' }')" 26 | 1\Icon=\xf08b 27 | 1\InMenu=true 28 | 1\Name=Copy Items as JSON 29 | 2\Command=" 30 | copyq: 31 | function toData(data) 32 | { 33 | if (typeof data === 'string') 34 | return new ByteArray(data) 35 | 36 | if (data.lines) 37 | return new ByteArray( 38 | data.lines.join('\\n') 39 | ) 40 | 41 | return fromBase64(data.base64) 42 | } 43 | 44 | var text = str(clipboard()) 45 | var itemsData = JSON.parse(text).copyq_items 46 | for (var i in itemsData) { 47 | itemData = itemsData[i] 48 | for (var format in itemData) 49 | itemData[format] = toData(itemData[format]) 50 | setItem(i, itemData) 51 | }" 52 | 2\Icon=\xf090 53 | 2\InMenu=true 54 | 2\MatchCommand=copyq: str(clipboard()).match(/^{ \"copyq_items\": \\[\\n/) || fail() 55 | 2\Name=Paste Items from JSON 56 | size=2 57 | 58 | -------------------------------------------------------------------------------- /Application/decrypt-and-type.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | // Safely types in decrypted item text without using clipboard 5 | // (simulates key presses). 6 | // Requires \"xdotool\". 7 | var maxChars = 1 8 | var delayBeetweenKeystrokesMs = 10 9 | var notificationTimeoutMs = 8000 10 | 11 | function typeText(slice) { 12 | var result = execute( 13 | 'xdotool', 14 | 'type', 15 | '--file', '-', 16 | '--delay', delayBeetweenKeystrokesMs, 17 | null, 18 | slice) 19 | 20 | if (!result) { 21 | throw 'Failed to type text' 22 | } 23 | 24 | if (result.exit_code != 0) { 25 | throw 'Failed to type text: ' + result.stderr 26 | } 27 | } 28 | 29 | function isModifierPressed() { 30 | var modifiers = queryKeyboardModifiers() 31 | return modifiers.length > 0 32 | } 33 | 34 | function notify(title, message) { 35 | notification( 36 | '.id', 'decrypt-type', 37 | '.time', notificationTimeoutMs, 38 | '.icon', '\xf13e', 39 | '.title', title, 40 | '.message', message || '' 41 | ) 42 | } 43 | 44 | while (isModifierPressed()) { 45 | sleep(20) 46 | } 47 | 48 | var data = plugins.itemencrypted.decrypt(input()) 49 | var item = unpack(data) 50 | var text = str(item[mimeText]) 51 | if (!text) { 52 | abort() 53 | } 54 | 55 | hide() 56 | // Wait to focus a target window. 57 | sleep(200) 58 | 59 | try { 60 | notify('Typing password...') 61 | for (var i = 0; i < text.length; i += maxChars) { 62 | if (isModifierPressed()) { 63 | throw 'Typing interrupted' 64 | } 65 | 66 | typeText( text.slice(i, i + maxChars) ) 67 | } 68 | notify('Password typed') 69 | } catch (e) { 70 | notify('Failed to type password', e) 71 | }" 72 | Icon=\xf13e 73 | InMenu=true 74 | Input=application/x-copyq-encrypted 75 | Name=Decrypt and Type 76 | Shortcut=return 77 | -------------------------------------------------------------------------------- /Application/edit-files.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Edit Files 3 | Match=^([a-zA-Z]:[\\\\/]|~|file://|%\\w+%|$\\w+|/) 4 | Command=" 5 | copyq: 6 | var editor = config('editor') 7 | .replace(/ %1/, '') 8 | 9 | var filePaths = str(input()) 10 | .replace(/^file:\\/{2}/gm, '') 11 | .replace(/^\\/(\\w):?\\//gm, '$1:/') 12 | .split('\\n') 13 | 14 | var args = [editor].concat(filePaths) 15 | 16 | execute.apply(this, args)" 17 | Input=text/plain 18 | InMenu=true 19 | Icon=\xf040 20 | Shortcut=f4 21 | -------------------------------------------------------------------------------- /Application/filter.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Filter 3 | Command=" 4 | copyq: 5 | var image = { 6 | [mimeText]: 'Image ---------------- I', 7 | [mimeIcon]: '', 8 | filter: '(^image/.*)|(?=^file\\:.*\\.(png|jpe?g|bmp|svg|gif|ico|webp)$)', 9 | shortcut: 'i' 10 | } 11 | 12 | var file = { 13 | [mimeText]: 'File ---------------- F', 14 | [mimeIcon]: '', 15 | filter: '(?=^file://)', 16 | shortcut: 'f' 17 | } 18 | 19 | var url = { 20 | [mimeText]: 'URL ---------------- U', 21 | [mimeIcon]: '', 22 | filter: '^(?=(https?|ftps?|smb|mailto)://)', 23 | shortcut: 'u' 24 | } 25 | 26 | var html = { 27 | [mimeText]: 'HTML', 28 | [mimeIcon]: '', 29 | filter: '^text/html$', 30 | shortcut: 'h' 31 | } 32 | 33 | var PhoneMail = { 34 | [mimeText]: 'Phone/Email', 35 | [mimeIcon]: '', 36 | filter: '(^0{0,1}(13[0-9]|15[7-9]|153|156|18[7-9])[0-9]{8}$)|(^([a-zA-Z0-9]+[_|\\_|\\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\\_|\\.]?)*[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$)', 37 | shortcut: 'p' 38 | } 39 | 40 | var filters = [image, file, url, html, PhoneMail] 41 | var selectedFilter = '' 42 | var shortcut = str(data(mimeShortcut)) 43 | 44 | if (shortcut) { 45 | for (let f in filters) { 46 | if (filters[f].shortcut === shortcut) { 47 | selectedFilter = filters[f].filter 48 | filter_toggle(selectedFilter) 49 | abort() 50 | } 51 | } 52 | } 53 | 54 | var selectedIndex = menuItems(filters) 55 | 56 | if (selectedIndex != -1) { 57 | selectedFilter = filters[selectedIndex].filter 58 | filter_toggle(selectedFilter) 59 | } else { 60 | filter('') 61 | } 62 | 63 | function filter_toggle(filter_) { 64 | if (filter() == filter_) { 65 | filter('') 66 | } 67 | else { 68 | filter('') // Necessary to switching between different filters 69 | filter(filter_) 70 | } 71 | }" 72 | InMenu=true 73 | Icon= 74 | Shortcut=shift+f, f, i, u -------------------------------------------------------------------------------- /Application/frequent-items-tab.ini: -------------------------------------------------------------------------------- 1 | [Commands] 2 | 1\Name=Activate and Add to Frequent 3 | 1\Command=" 4 | copyq: 5 | tab_name = \"Frequent\" 6 | 7 | source = selectedtab() 8 | tab(source) 9 | items = selecteditems() 10 | p = \"application/x-copyq-\" 11 | freq_mime = p + \"user-frequency\" 12 | ignored = [ 13 | freq_mime, 14 | p + \"owner\", 15 | p + \"owner-window-title\", 16 | ] 17 | 18 | function items_equal(item, i) { 19 | for (var mime in item) { 20 | if ( str(read(mime, i)) !== str(item[mime]) ) 21 | return false 22 | } 23 | return true 24 | } 25 | 26 | function index_of_item(item) { 27 | for (var i = 0; i < size(); ++i) { 28 | if (items_equal(item, i)) 29 | return i 30 | } 31 | return -1 32 | } 33 | 34 | function get_freq(i) { 35 | return parseInt(str(read(freq_mime, i))) || 0 36 | } 37 | 38 | function find_index_for_frequency(freq) { 39 | for (var i = 0; i < size(); ++i) { 40 | if (freq >= get_freq(i)) 41 | return i 42 | } 43 | return size() 44 | } 45 | 46 | for (i in items) { 47 | item = getitem(items[i]) 48 | for (j in ignored) 49 | delete item[ignored[j]]; 50 | tab(tab_name) 51 | j = index_of_item(item) 52 | if (j !== -1) { 53 | item[freq_mime] = freq = get_freq(j) + 1 54 | remove(j) 55 | j = find_index_for_frequency(freq) 56 | } else { 57 | j = size() 58 | } 59 | setitem(j, item) 60 | tab(source) 61 | } 62 | 63 | select(items) 64 | 65 | tab(tab_name) 66 | selectitems([0]) 67 | 68 | if ( config(\"activate_closes\") == \"true\" ) hide() 69 | if ( config(\"activate_pastes\") == \"true\" ) paste()" 70 | 1\InMenu=true 71 | 1\Icon=\xf004 72 | 1\Shortcut=Return, Enter 73 | 2\Name=Show Frequent 74 | 2\Command=copyq menu \"Frequent\" 75 | 2\Icon=\xf004 76 | 2\GlobalShortcut=Meta+Shift+F 77 | size=2 78 | -------------------------------------------------------------------------------- /Application/hide-item-content.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | if ( dataFormats().indexOf(mimeHidden) != -1 ) 5 | removeData(mimeHidden) 6 | else 7 | setData(mimeHidden, 1)" 8 | Icon=\xf070 9 | InMenu=true 10 | Name=Hide/Show Item Content 11 | -------------------------------------------------------------------------------- /Application/highlight-text.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Highlight Text 3 | Command=" 4 | copyq: 5 | x = 'text' 6 | style = 'background: yellow; text-decoration: underline' 7 | 8 | text = str(input()) 9 | x = x.toLowerCase() 10 | lowertext = text.toLowerCase() 11 | html = '' 12 | a = 0 13 | esc = function(a, b) { 14 | return escapeHTML( text.substr(a, b - a) ) 15 | } 16 | 17 | while (1) { 18 | b = lowertext.indexOf(x, a) 19 | if (b != -1) { 20 | html += esc(a, b) + '' + esc(b, b + x.length) + '' 21 | } else { 22 | html += esc(a, text.length) 23 | break 24 | } 25 | a = b + x.length; 26 | } 27 | 28 | tab( selectedtab() ) 29 | write( 30 | index(), 31 | 'text/plain', text, 32 | 'text/html', 33 | '' 36 | + html + 37 | '' 38 | )" 39 | Input=text/plain 40 | Wait=true 41 | InMenu=true 42 | -------------------------------------------------------------------------------- /Application/join-selected-items.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | const separator = '\\n'; 5 | var sel = new ItemSelection().current(); 6 | const texts = sel.itemsFormat(mimeText); 7 | sel.selectAll(); 8 | add(texts.join(separator)); 9 | sel.invert(); 10 | selectItems(sel.rows()[0]);" 11 | Icon=\xf0c1 12 | InMenu=true 13 | Name=Join Selected Items 14 | Shortcut=space 15 | -------------------------------------------------------------------------------- /Application/mark-selected-items.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var color = 'rgba(255, 255, 0, 0.5)' 5 | var currentColor = str(selectedItemData(0)[mimeColor]); 6 | if (currentColor != color) { 7 | setData(mimeColor, color) 8 | } else { 9 | removeData(mimeColor) 10 | }" 11 | Icon=\xf591 12 | InMenu=true 13 | MatchCommand=" 14 | copyq: 15 | var color = 'rgba(255, 255, 0, 0.5)' 16 | var currentColor = str(selectedItemData(0)[mimeColor]) 17 | if (currentColor != color) { 18 | menuItem['text'] = 'Mark Items' 19 | menuItem['tag'] = '__' 20 | menuItem['color'] = color.replace(/\\d+\\.\\d+/, 1) 21 | } else { 22 | menuItem['text'] = 'Unmark Items' 23 | menuItem['tag'] = 'x' 24 | menuItem['color'] = 'white' 25 | } 26 | menuItem['icon'] = '\xf591'" 27 | Name=Mark/Unmark Items 28 | Shortcut=ctrl+m 29 | -------------------------------------------------------------------------------- /Application/next-previous.ini: -------------------------------------------------------------------------------- 1 | [Commands] 2 | 1\Command=" 3 | copyq: 4 | selectItems(currentItem() + 1)" 5 | 1\Icon=\xf0ab 6 | 1\InMenu=true 7 | 1\Name=Next 8 | 1\Shortcut=ctrl+n 9 | 2\Command=" 10 | copyq: 11 | selectItems(currentItem() - 1)" 12 | 2\Icon=\xf0aa 13 | 2\InMenu=true 14 | 2\Name=Previous 15 | 2\Shortcut=ctrl+p 16 | size=2 -------------------------------------------------------------------------------- /Application/paste-and-forget.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Paste and Forget 3 | Command=" 4 | copyq: 5 | tab(selectedtab()) 6 | items = selecteditems() 7 | if (items.length > 1) { 8 | text = '' 9 | for (i in items) 10 | text += read(items[i]); 11 | copy(text) 12 | } else { 13 | select(items[0]) 14 | } 15 | 16 | hide() 17 | sleep(100) 18 | paste() 19 | sleep(1000) 20 | copy('')" 21 | InMenu=true 22 | Icon=\xf0ea 23 | Shortcut=Ctrl+Return 24 | -------------------------------------------------------------------------------- /Application/paste-formatted-json.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Paste formatted Json 3 | Command=" 4 | copyq: 5 | var text = str(input()) 6 | try { 7 | var json = JSON.parse(text) 8 | // Remplace '\\t' with a number to indent 9 | // with this amount of spaces 10 | text = JSON.stringify(json, null, '\\t') 11 | } catch (e) { 12 | } 13 | copy(text) 14 | paste() 15 | " 16 | Input=text/plain 17 | InMenu=true 18 | HideWindow=true 19 | Icon=\xf121 20 | -------------------------------------------------------------------------------- /Application/qr-code.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | const result = execute('qrcode', '--factory=pil', null, input()); 5 | if (!result) { 6 | popup('Failed to run qrcode'); 7 | abort(); 8 | } 9 | if (result.exit_code != 0) { 10 | popup('Failed to run qrcode', result.stderr); 11 | abort(); 12 | } 13 | write('image/png', result.stdout);" 14 | Icon=\xf029 15 | InMenu=true 16 | Input=text/plain 17 | Name=QR Code 18 | -------------------------------------------------------------------------------- /Application/remove-background-and-text-colors.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | var html = str(input()) 6 | html = html.replace(/color\\s*:/g, 'xxx:') 7 | setData('text/html', html)" 8 | Icon=\xf042 9 | InMenu=true 10 | Input=text/html 11 | Name=Remove Background and Text Colors 12 | -------------------------------------------------------------------------------- /Application/remove-carriage-return-linefeed-and-multiple-space-then-paste.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Remove Carriage Return, Line Feed and Multiple Space Then Paste 3 | Command=" 4 | copyq: 5 | 6 | var text = str(clipboard()) 7 | 8 | if (text) { 9 | 10 | text = text.replace(new RegExp(/[\\r\\n]+/g), ' ') 11 | text = text.replace(new RegExp(/\\s+/g), ' ') 12 | 13 | copy(text) 14 | paste() 15 | 16 | }" 17 | IsGlobalShortcut=true 18 | Icon=\xf0c1 19 | GlobalShortcut=ctrl+shift+v 20 | -------------------------------------------------------------------------------- /Application/render-html.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Render HTML 3 | Match=^\\s*<(!|html) 4 | Command=" 5 | copyq: 6 | tab(selectedtab()) 7 | write(index() + 1, 'text/html', input())" 8 | Input=text/plain 9 | InMenu=true 10 | -------------------------------------------------------------------------------- /Application/save-item-clipboard-to-file.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var suffices = { 5 | 'image/svg': 'svg', 6 | 'image/png': 'png', 7 | 'image/jpeg': 'jpg', 8 | 'image/jpg': 'jpg', 9 | 'image/bmp': 'bmp', 10 | 'text/html': 'html', 11 | 'text/plain' : 'txt', 12 | } 13 | 14 | function hasSuffix(fileName) 15 | { 16 | return /\\.[0-9a-zA-z]+$/.test(fileName); 17 | } 18 | 19 | function addSuffix(fileName, format) 20 | { 21 | var suffix = suffices[format] 22 | return suffix ? fileName + \".\" + suffix : fileName 23 | } 24 | 25 | function filterFormats(format) 26 | { 27 | return /^[a-z]/.test(format) && !/^application\\/x/.test(format) 28 | } 29 | 30 | function itemFormats(row) 31 | { 32 | return str(read('?', row)) 33 | .split('\\n') 34 | .filter(filterFormats) 35 | } 36 | 37 | function formatPriority(format) 38 | { 39 | var k = Object.keys(suffices); 40 | var i = k.indexOf(format); 41 | return i === -1 ? k.length : i 42 | } 43 | 44 | function reorderFormats(formats) 45 | { 46 | formats.sort(function(lhs, rhs){ 47 | var i = formatPriority(lhs); 48 | var j = formatPriority(rhs); 49 | return i === j ? lhs.localeCompare(rhs) : i - j; 50 | }) 51 | } 52 | 53 | if (selectedtab()) tab(selectedtab()) 54 | var row = selectedtab() ? currentitem() : -1 55 | var formats = itemFormats(row) 56 | reorderFormats(formats) 57 | 58 | currentpath(Dir().homePath()) 59 | var defaultFileName = 'untitled' 60 | 61 | var keyFormat = 'Format' 62 | var keyFileName = 'File' 63 | var defaultFormat = formats[0] 64 | 65 | var result = dialog( 66 | '.title', 'Save Item As...', 67 | '.width', 250, 68 | keyFormat, [defaultFormat].concat(formats), 69 | keyFileName, File(defaultFileName) 70 | ) || abort() 71 | 72 | var fileName = result[keyFileName] 73 | var format = result[keyFormat] 74 | 75 | if (!format || !fileName) 76 | abort() 77 | 78 | if (!hasSuffix(fileName)) 79 | fileName = addSuffix(fileName, format) 80 | 81 | var f = File(fileName) 82 | if (!f.open()) { 83 | popup('Failed to open \"' + f.fileName() + '\"', f.errorString()) 84 | abort() 85 | } 86 | 87 | f.write(selectedtab() ? getitem(currentitem())[format] : clipboard(format)) 88 | popup(\"Item Saved\", 'Item saved as \"' + f.fileName() + '\".')" 89 | Icon=\xf0c7 90 | InMenu=true 91 | Name=Save As... 92 | Shortcut=Ctrl+S 93 | -------------------------------------------------------------------------------- /Application/search-all-tabs.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Search All Tabs 3 | Command=" 4 | copyq: 5 | // Name for tab for storing matching items. 6 | var search_tab_name = \"Search\" 7 | 8 | // Returns true iff item at index matches regex. 9 | function item_matches(item_index, re) 10 | { 11 | var item = getitem(item_index) 12 | var note = str(item[mimeItemNotes]) 13 | var text = str(item[mimeText]) 14 | try { 15 | var tag = str(plugins.itemtags.tags(item_index)) 16 | } catch (e) { 17 | var tag = '' 18 | } 19 | return text && (re.test(text) || re.test(note) || re.test(tag)) 20 | } 21 | 22 | // Ask for search expression. 23 | var match = dialog(\"Search\") 24 | if (!match) 25 | abort() 26 | var re = new RegExp(match,'i') // 'i' case-insensitive 27 | 28 | // Clear tab with results. 29 | try { 30 | removeTab(search_tab_name) 31 | } catch (e) {} 32 | 33 | // Search all tabs. 34 | var tab_names = tab() 35 | for (var i in tab_names) { 36 | var tab_name = tab_names[i] 37 | tab(tab_name) 38 | var item_count = count() 39 | 40 | // Search all items in tab. 41 | for (var j = 0; j < item_count; ++j) { 42 | // Add matching item to tab with results. 43 | if (item_matches(j, re)) { 44 | var item = getItem(j) 45 | tab(search_tab_name) 46 | setItem(j, item) 47 | tab(tab_name) 48 | } 49 | } 50 | } 51 | 52 | show(search_tab_name)" 53 | InMenu=true 54 | Icon=\xf002 55 | Shortcut=Ctrl+Shift+F 56 | -------------------------------------------------------------------------------- /Application/search-and-replace.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | const lastFindConfig = 'search-replace-last-find'; 5 | const lastReplaceConfig = 'search-replace-last-replace'; 6 | var lastFind = settings(lastFindConfig) || []; 7 | var lastReplace = settings(lastReplaceConfig) || []; 8 | var input = dialog( 9 | 'Find', lastFind, 10 | 'Replace', lastReplace, 11 | 'In All Items', 12 | false, 13 | ); 14 | const find = input && input['Find']; 15 | const replace = input && input['Replace']; 16 | if (!find) { 17 | abort(); 18 | } 19 | 20 | lastFind.unshift(find); 21 | lastReplace.unshift(replace); 22 | settings(lastFindConfig, lastFind); 23 | settings(lastReplaceConfig, lastReplace); 24 | 25 | var sel = ItemSelection(); 26 | if (input['In All Items']) { 27 | sel = sel.selectAll(); 28 | } else { 29 | sel = sel.current(); 30 | } 31 | 32 | for (var index = 0; index < sel.length; ++index) { 33 | var item = sel.itemAtIndex(index); 34 | const text = str(item[mimeText]); 35 | const newText = text.replace(find, replace); 36 | if (text != newText) { 37 | item[mimeText] = newText; 38 | delete item[mimeHtml]; 39 | sel.setItemAtIndex(index, item); 40 | } 41 | }" 42 | Icon=\xe521 43 | InMenu=true 44 | Name=Search & Replace 45 | Shortcut=ctrl+f3 46 | -------------------------------------------------------------------------------- /Application/select-nth-item.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var n = str(data(mimeShortcut)).slice(-1) 5 | if (config('row_index_from_one') === 'true') { 6 | n -= 1 7 | } 8 | 9 | if (config('activate_closes') === 'true') { 10 | hide() 11 | } 12 | 13 | selectItems(n) 14 | select(n)" 15 | Icon=\xf0cb 16 | InMenu=true 17 | Name=Select Nth Item 18 | Shortcut=ctrl+0, ctrl+1, ctrl+2, ctrl+3, ctrl+4, ctrl+5, ctrl+6, ctrl+7, ctrl+8, ctrl+9 19 | -------------------------------------------------------------------------------- /Application/sort-items.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Sort 3 | Command=" 4 | copyq: 5 | // When selecting single item, sort the entire tab 6 | // When selecting multiple items, only sort the selected items 7 | 8 | var sort_top_pinned = true 9 | 10 | var date_reverse = { 11 | [mimeText]: 'Latest First', 12 | [mimeIcon]: '', 13 | format: \"application/x-copyq-user-time\", 14 | reverse: true 15 | } 16 | 17 | var date = { 18 | [mimeText]: 'Oldest First', 19 | [mimeIcon]: '', 20 | format: \"application/x-copyq-user-time\", 21 | reverse: false 22 | } 23 | 24 | var pinned = { 25 | [mimeText]: 'Pinned to the top', 26 | [mimeIcon]: '', 27 | format: \"application/x-copyq-item-pinned\", 28 | reverse: false 29 | } 30 | 31 | var alphabet = { 32 | [mimeText]: 'Alphabetical', 33 | [mimeIcon]: '', 34 | format: mimeText, 35 | reverse: false 36 | } 37 | var alphabet_reverse = { 38 | [mimeText]: 'Alphabetical reverse', 39 | [mimeIcon]: '', 40 | format: mimeText, 41 | reverse: true 42 | } 43 | 44 | var size_reverse = { 45 | [mimeText]: 'Biggest First', 46 | [mimeIcon]: '', 47 | format: \"ItemSize\", 48 | reverse: true 49 | } 50 | 51 | var size = { 52 | [mimeText]: 'Smallest First', 53 | [mimeIcon]: '', 54 | format: \"ItemSize\", 55 | reverse: false 56 | } 57 | 58 | // sorting menu list 59 | var sorts = [ 60 | date_reverse, 61 | date, 62 | pinned, 63 | alphabet, 64 | alphabet_reverse, 65 | size_reverse, 66 | size 67 | ] 68 | // Show menu 69 | var selectedIndex = menuItems(sorts) 70 | 71 | var sel = ItemSelection().current() 72 | if (sel.length <= 1) { 73 | if (length() <=1 ) abort() 74 | // When only one item is selected, the selection is set to the entire tab 75 | tab(selectedTab()) 76 | sel = ItemSelection().selectAll() 77 | if (sort_top_pinned) handle_pinned(sel.length) 78 | } else { 79 | // Check if the selection starts from row 0. 80 | // Yes: Process pinned items; No: No operation. 81 | if (sel.rows()[0] == 0 && sort_top_pinned) { 82 | handle_pinned(sel.length) 83 | } 84 | } 85 | 86 | const rows = sel.rows() 87 | var order = '' 88 | if (selectedIndex != -1) { 89 | // Get the selected sort by 90 | const selectedFormat = sorts[selectedIndex].format 91 | switch (selectedFormat) { 92 | case 'ItemSize': 93 | // When sorting by item size, it takes ~3ms to calculate the size of each item 94 | popup('Sorting ⏳', '', rows.length * 3) 95 | order = sizeList() 96 | break; 97 | case \"application/x-copyq-item-pinned\": 98 | order = sel.itemsFormat(selectedFormat).map((item) => item === undefined); 99 | break; 100 | default: 101 | order = sel.itemsFormat(selectedFormat) 102 | } 103 | } else { 104 | abort() 105 | } 106 | 107 | // sorting 108 | if (sorts[selectedIndex].reverse) { 109 | sel.sort((i, j) => order[i] > order[j]); 110 | } else { 111 | sel.sort((i, j) => order[i] < order[j]); 112 | } 113 | popup('Sorting completed✅', '', 1000) 114 | 115 | // Obtain the byte size of each item in the sel selection 116 | function sizeList() { 117 | var items = sel.items() 118 | var sizes = [] 119 | for (let row in rows) { 120 | var itemSize = 0 121 | var item = items[row] 122 | for (var format in item) { 123 | itemSize += read(format, row).size() 124 | } 125 | sizes.push(itemSize) 126 | } 127 | return sizes 128 | } 129 | 130 | // The first pinned item on the top of the tab can not be sorted 131 | // so need to move the continuous pinned items down n+1 rows 132 | function handle_pinned(sel_length) { 133 | var pinned = [] 134 | // Get continuous pinned items starting from row 0 135 | for (var i = 0; i < sel_length; i++) { 136 | if (plugins.itempinned.isPinned(i)) { 137 | pinned.push(i) 138 | } else { 139 | break 140 | } 141 | } 142 | if (0 < pinned.length < sel_length) { 143 | var selAll = ItemSelection().selectAll() 144 | selAll.deselectIndexes(pinned) 145 | selAll.invert().move(pinned.length + 1) 146 | } 147 | }" 148 | InMenu=true 149 | Icon= 150 | Shortcut=shift+s -------------------------------------------------------------------------------- /Application/sort-tabs.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | let tabs=config('tabs'); 5 | const normalizeTab = (name) => name.replace(/&/g, ''); 6 | const orderTabs = (a, b) => normalizeTab(a).localeCompare(normalizeTab(b)); 7 | tabs.sort(orderTabs); 8 | config('tabs', tabs);" 9 | Icon= 10 | InMenu=true 11 | Name=Sort Tabs 12 | -------------------------------------------------------------------------------- /Application/tab-alt-navigation.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | const tabs = tab(); 5 | const maxHotkeySize = 10; 6 | const hotkeyNumber = str(data(mimeShortcut)).slice(-1); 7 | const actualTabIndex = (hotkeyNumber - 1 + maxHotkeySize) % maxHotkeySize; 8 | 9 | if (tabs.length > actualTabIndex) { 10 | setCurrentTab(tabs[actualTabIndex]); 11 | }" 12 | Icon= 13 | InMenu=true 14 | Name=Navigate Tabs With Alt+Number 15 | Shortcut=alt+1, alt+2, alt+3, alt+4, alt+5, alt+6, alt+7, alt+8, alt+9, alt+0 16 | -------------------------------------------------------------------------------- /Application/tab-key-select.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var selected = selectedItems(); 5 | var len = size(); 6 | var i = selected.length > 0 ? selected[0] : len; 7 | var shortcut = str(data(mimeShortcut)); 8 | if (shortcut.search(/shift/i) != -1) { 9 | i--; 10 | if (i < 0) { 11 | i = len - 1; 12 | } 13 | } else { 14 | i++; 15 | if (i >= len) { 16 | i = 0; 17 | } 18 | } 19 | selectItems(i)" 20 | Icon=\xf362 21 | InMenu=true 22 | Name=Tab Key to Select Next/Previous 23 | Shortcut=tab, shift+tab 24 | -------------------------------------------------------------------------------- /Application/tab-switcher.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | function tabsSession() { 5 | execute( 6 | 'copyq', '-s', 'tabs', '--start-server', 7 | 'tab', 'Tabs', ...arguments 8 | ) 9 | } 10 | 11 | var cmd = ` 12 | config( 13 | 'check_clipboard', false, 14 | 'check_selection', false, 15 | 'copy_clipboard', false, 16 | 'copy_selection', false, 17 | 'disable_tray', true, 18 | 'hide_tabs', true, 19 | 'hide_toolbar', true, 20 | 'hide_main_window', true, 21 | ); 22 | removeTab('Tabs') 23 | setCommands([{ 24 | name: 'Show Tab', 25 | inMenu: true, 26 | hideWindow: true, 27 | shortcuts: ['Enter', 'Return'], 28 | cmd: 'copyq -s \"%SESSION%\" show %1' 29 | }]); 30 | `; 31 | var session = str(env('COPYQ_SESSION_NAME')); 32 | cmd = cmd.replace('%SESSION%', session); 33 | tabsSession(cmd); 34 | tabsSession('add', ...tab().reverse()) 35 | tabsSession('show', 'Tabs')" 36 | Icon=\xf022 37 | InMenu=true 38 | Name=Tab Switcher 39 | Shortcut=alt+f1 40 | -------------------------------------------------------------------------------- /Application/toggle-simple-items.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var on = config(\"show_simple_items\") === \"true\" 5 | config(\"show_simple_items\", !on)" 6 | Icon=\xf016 7 | InMenu=true 8 | Name=Toggle Simple Items 9 | Shortcut=f8 10 | -------------------------------------------------------------------------------- /Application/toggle-tag.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var tag = decodeURIComponent('important') 5 | if (plugins.itemtags.hasTag(tag)) 6 | plugins.itemtags.untag(tag) 7 | else 8 | plugins.itemtags.tag(tag)" 9 | Icon=\xf02b 10 | InMenu=true 11 | Name=Toggle tag \x201cimportant\x201d 12 | -------------------------------------------------------------------------------- /Application/translate-to-english.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | fromLanguage = 'auto' 5 | toLanguage = 'en' 6 | 7 | text = str(input()) 8 | url = 'https://translate.google.com/#view=home&op=translate' 9 | + '&sl=' + fromLanguage 10 | + '&tl=' + toLanguage 11 | + '&text=' + encodeURIComponent(text) 12 | open(url)" 13 | Icon=\xf558 14 | InMenu=true 15 | Input=text/plain 16 | Name=Translate to English 17 | -------------------------------------------------------------------------------- /Application/treefy.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Treefy 3 | Command=" 4 | copyq: 5 | /** 6 | * Normalize text replacing initial characters to tabs 7 | * Replace first '#' with '' 8 | * Then convert 2 spaces to tab and '#' to tab 9 | * @param text Text 10 | */ 11 | function normalizeText(text) { 12 | text = text.replace(/^\\# ?/gm, ''); 13 | text = text.replace(/( |\\# ?)/gm, '\\t'); 14 | return text; 15 | } 16 | 17 | /** 18 | * Get number of tabs 19 | * @param line Line 20 | */ 21 | function getTabs(line) { 22 | return (line.match(/^\\t+/g) || []).toString().length; 23 | } 24 | 25 | /** 26 | * Search last node with specific key and value 27 | * @param nodes Array of nodes 28 | * @param key Key 29 | * @param value Value 30 | */ 31 | function lastIndexOf(nodes, key, value) { 32 | for (var i = nodes.length - 1; i >= 0; i--) { 33 | if (nodes[i][key] == value) { 34 | return i; 35 | } 36 | } 37 | return -1; 38 | } 39 | 40 | /** 41 | * Print father's children 42 | * @param children Array of children 43 | * @param tabs Initial indentation 44 | */ 45 | function printChildren(children, tabs) { 46 | var numberChildren = children.length; 47 | var child = {}, childTabs = '', string = ''; 48 | 49 | if (numberChildren == 0) 50 | return; 51 | 52 | for (var i = 0; i < numberChildren; i++) { 53 | child = nodes[children[i]]; 54 | 55 | // Print tabs 56 | string = tabs; 57 | 58 | // Check if is the last child 59 | // Print tree and get child tabs 60 | if (i == numberChildren - 1) { 61 | string += lastElement; 62 | childTabs = tabs + ' '; 63 | } else { 64 | string += intermediateElement; 65 | childTabs = tabs + verticalElement + ' '; 66 | } 67 | 68 | // Print node string 69 | string += child.string + '\\n'; 70 | 71 | tree += string; 72 | 73 | printChildren(child.children, childTabs) 74 | } 75 | } 76 | 77 | var verticalElement = '\x2502'; 78 | var lastElement = '\x2514\x2500 '; 79 | var intermediateElement = '\x251c\x2500 '; 80 | 81 | var tree = ''; 82 | var text = str(read(currentItem())); 83 | var lines = [], line = ''; 84 | var nodes = [], node = {}; 85 | var length = 0, tabs = -1, father = {}, fatherTabs = -1; 86 | var rootElement = dialog('.title', 'Root element', 'Root element name?', '.'); 87 | var offset = 0; 88 | 89 | // Normalize text 90 | text = normalizeText(text); 91 | 92 | // Get lines 93 | lines = text.split(/\\r?\\n/); 94 | 95 | // Add root element 96 | if (rootElement) { 97 | offset = 1; 98 | nodes.push({ 99 | index: 0, 100 | tabs: 0, 101 | father: null, 102 | children: [], 103 | string: rootElement 104 | }); 105 | } 106 | 107 | // Main loop 108 | for (var i = 0; i < lines.length; i++) { 109 | // Clear 110 | node = {}; 111 | tabs = -1; 112 | father = null; 113 | fatherTabs = -1; 114 | line = lines[i]; 115 | 116 | // Get number of tabs 117 | tabs = getTabs(line) + offset; 118 | 119 | // Search father 120 | if (tabs > 0) { 121 | // Father's tabs 122 | fatherTabs = tabs - 1; 123 | 124 | // Read last node that have the number of tabs 125 | father = nodes[lastIndexOf(nodes, 'tabs', fatherTabs)]; 126 | 127 | // Add child index to fathers node 128 | if (father !== undefined) { 129 | father.children.push(i + offset); 130 | } 131 | } 132 | 133 | // Add node 134 | node.index = i + offset; 135 | node.tabs = tabs; 136 | node.father = father ? father.index : null; 137 | node.children = []; 138 | node.string = line.trim(); 139 | nodes.push(node); 140 | } 141 | 142 | // Print tree 143 | for (var i = 0; i < nodes.length; i++) { 144 | node = nodes[i]; 145 | if(node.tabs !== 0) 146 | continue; 147 | tree += node.string + '\\n'; 148 | printChildren(node.children, ''); 149 | } 150 | 151 | // Remove whitespace characters 152 | tree = tree.trim(); 153 | 154 | // Create html 155 | var html = '
' + escapeHtml(tree) + '
'; 156 | 157 | // Write 158 | write(mimeText, tree, mimeHtml, html);" 159 | InMenu=true 160 | Icon=\xf1bb -------------------------------------------------------------------------------- /Application/undoable-move-to-trash.ini: -------------------------------------------------------------------------------- 1 | [Commands] 2 | 1\Command=" 3 | const onItemsRemoved_ = global.onItemsRemoved; 4 | global.onItemsRemoved = function() { 5 | const trash_tab = '(trash)'; 6 | const tab_mime = 'application/x-copyq-user-tab'; 7 | const index_mime = 'application/x-copyq-user-index'; 8 | const time_mime = 'application/x-copyq-user-time'; 9 | const source_tab = selectedtab(); 10 | if (source_tab == trash_tab) { 11 | serverLog(`Removing items from ${source_tab}`); 12 | } else { 13 | serverLog(`Removing items from ${source_tab} to ${trash_tab}`); 14 | const time = (new Date).toISOString(); 15 | const sel = ItemSelection().current(); 16 | const rows = sel.rows(); 17 | let trashed = sel.items().map((item, i) => { 18 | item[tab_mime] = source_tab; 19 | item[index_mime] = rows[i]; 20 | item[time_mime] = time; 21 | return item; 22 | }); 23 | 24 | tab(trash_tab); 25 | write(0, trashed); 26 | tab(source_tab); 27 | } 28 | 29 | onItemsRemoved_(); 30 | }" 31 | 1\Icon= 32 | 1\IsScript=true 33 | 1\Name=Move to Trash (Undoable) 34 | 2\Command=" 35 | copyq: 36 | const trash_tab = '(trash)'; 37 | const tab_mime = 'application/x-copyq-user-tab'; 38 | const index_mime = 'application/x-copyq-user-index'; 39 | const time_mime = 'application/x-copyq-user-time'; 40 | 41 | const remove_mime = [tab_mime, index_mime, time_mime]; 42 | tab(trash_tab); 43 | 44 | if (length() == 0) { 45 | popup('Nothing to undo'); 46 | abort(); 47 | } 48 | 49 | let item = getItem(0); 50 | const target_tab = str(item[tab_mime]) || selectedTab(); 51 | const time = str(item[time_mime]); 52 | 53 | tab(trash_tab); 54 | 55 | let target_index = 999999; 56 | let i = 0; 57 | let items = []; 58 | while (true) { 59 | target_index = Math.min(target_index, item[index_mime] || 0) 60 | 61 | for (let j in remove_mime) { 62 | delete item[remove_mime[j]]; 63 | } 64 | 65 | items.push(item); 66 | 67 | item = getItem(++i) 68 | if ( !time || time !== str(item[time_mime]) ) { 69 | break; 70 | } 71 | } 72 | 73 | let select_items = []; 74 | let remove_items = []; 75 | for (let j = 0; j < i; ++j) { 76 | select_items.push(target_index + j); 77 | remove_items.push(j); 78 | } 79 | 80 | show(target_tab); 81 | tab(target_tab); 82 | 83 | items.unshift(target_index); 84 | insert.apply(this, items); 85 | 86 | selectItems.apply(this, select_items); 87 | 88 | tab(trash_tab); 89 | remove.apply(this, remove_items);" 90 | 2\Icon= 91 | 2\InMenu=true 92 | 2\Name=Undo Delete 93 | 2\Shortcut=ctrl+z 94 | size=2 95 | -------------------------------------------------------------------------------- /Automatic/README.md: -------------------------------------------------------------------------------- 1 | This section contains commands which are executed automatically whenever something is copied to clipboard. 2 | 3 | ### [Big Data Tab](big-data-tab.ini) 4 | 5 | Automatically moves larger amount of data copied to clipboard to a special tab 6 | (see the command variables to set the output tab and size limit) to keep the 7 | access to primary clipboard tab swift. 8 | 9 | ### [Copy a Secret If a Modifier Held](copy-a-secret-if-modifier-held.ini) 10 | 11 | If any keyboard modifier (Ctrl, Shift, Alt etc) is held for a second after 12 | copying a content, it will not be stored or shown in GUI. 13 | 14 | ### [Copy Clipboard to Window Tabs](copy-clipboard-to-windows-tab.ini) 15 | 16 | Automatically adds new clipboard to tab with same name as title of the window where copy operation was performed. 17 | 18 | ### [Ignore Images when Text is Available](ignore-images-when-text-is-available.ini) 19 | 20 | This is useful for ignoring cells copied as images from Microsoft Excel and LibreOffice Calc. 21 | 22 | ### [Ignore Passwords/Tokens](ignore-passwords-tokens.ini) 23 | 24 | Ignore the clipboard if it contains a password or token based on the text 25 | characteristics (length, uppercase letters, digits). 26 | 27 | ### [Image Tab](image-tab.ini) 28 | 29 | Automatically store images copied to clipboard in a separate tab. 30 | 31 | ### [Import Commands after Copied to Clipboard](import-commands-after-copied.ini) 32 | 33 | Shows notification allowing to easily import commands just copied to clipboard. 34 | 35 | The copied text can be either link to a command on github or starts with `[Command]` or `[Commands]`. 36 | 37 | ### [Linkify](linkify.ini) 38 | 39 | Creates interactive link from plain text. 40 | 41 | ### Play Sound when Copying to Clipboard ([Windows](play-sound-when-copying-to-clipboard-windows.ini), [Linux](play-sound-when-copying-to-clipboard-linux.ini)) 42 | 43 | Following command will play an audio file whenever something is copied clipboard. 44 | 45 | ### [Store Copy Time](store-copy-time.ini) 46 | 47 | Store the copy time of new items in a **tag**. 48 | 49 | To show tags in the item list the **Tags** plugin is required. 50 | 51 | Optionally you can change the appearence of the copy time tag. See this example: 52 | 53 | `Tag Name`: `\1` 54 | 55 | `Match`: `(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})` 56 | 57 | `Style Sheet`: `font-size: 9pt; color: #fff; border: 1px solid #000; background: #000; padding: 0 2px` 58 | 59 | 60 | ### [Show Window Title](show-window-title.ini) 61 | 62 | Shows source application window title for new items in tag. 63 | 64 | Requires "Tags" plugin. 65 | 66 | ### [Store Mouse Selections in Separate Tab](mouse-selections-tab.ini) 67 | 68 | Stores Linux/X11 mouse selections in a separate tab. 69 | 70 | ### [Synchronize Clipboard with Other Sessions](synchronize-clipboard-with-other-sessions.ini) 71 | 72 | Synchronizes clipboard with other application session running on different X11 servers. 73 | 74 | ### [Tab for URLs with Title and Icon](tab-for-urls-with-title-and-icon.ini) 75 | 76 | For copied URLS tries to get web page title and icon and stores it with item in "url" tab. 77 | 78 | ### [KeePassXC protector](keepassxc-protector.ini) 79 | 80 | A plugin that prevents saving data from the [KeePassXC](https://github.com/keepassxreboot/keepassxc) password manager. 81 | -------------------------------------------------------------------------------- /Automatic/big-data-tab.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | var tabName = 'BIG' 6 | var minBytes = 250*1000 7 | 8 | function hasBigData() { 9 | var itemSize = 0 10 | var formats = dataFormats() 11 | for (var i in formats) { 12 | itemSize += data(formats[i]).size() 13 | if (itemSize >= minBytes) 14 | return true 15 | } 16 | return false 17 | } 18 | 19 | if (hasBigData()) { 20 | setData(mimeOutputTab, tabName) 21 | }" 22 | Icon=\xf1c0 23 | Name=Big Data Tab 24 | -------------------------------------------------------------------------------- /Automatic/copy-a-secret-if-modifier-held.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | var waitMs = 1000; 6 | 7 | var notificationId = 'copy-secret-if-modifier-held'; 8 | notification( 9 | '.id', notificationId, 10 | '.message', 'Hold a modifier key to copy as a secret', 11 | '.time', waitMs + 1000); 12 | 13 | var start = Date.now(); 14 | while ( 15 | queryKeyboardModifiers().length > 0 16 | && Date.now() - start < waitMs) {} 17 | 18 | if (queryKeyboardModifiers().length > 0) { 19 | notification( 20 | '.id', notificationId, 21 | '.message', 'Copied as a secret', 22 | '.time', waitMs + 1000); 23 | ignore(); 24 | } else { 25 | notification('.id', notificationId, '.time', 0); 26 | }" 27 | Icon=\xf070 28 | Name=Copy a Secret If a Modifier Held 29 | -------------------------------------------------------------------------------- /Automatic/copy-clipboard-to-windows-tab.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | const windowTitle = str(data(mimeWindowTitle)); 6 | if (windowTitle) { 7 | // Remove the part of window title before dash 8 | // (it's usually document name or URL). 9 | const tabName = windowTitle.replace(/.* (-|–|—) /, '').trim(); 10 | setData(mimeOutputTab, `Windows/${tabName}`); 11 | }" 12 | Icon= 13 | Name=Window Tabs 14 | -------------------------------------------------------------------------------- /Automatic/ignore-images-when-text-is-available.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | var text = data('text/plain') 6 | copy(text) 7 | add(text)" 8 | Icon=\xf031 9 | Input=image/bmp 10 | MatchCommand="copyq: if ( str(data('text/plain')) == '' ) fail()" 11 | Name=Ignore Images when Text Copied 12 | Remove=true 13 | -------------------------------------------------------------------------------- /Automatic/ignore-passwords-tokens.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | const notificationTimeout = config('item_popup_interval') * 1000; 6 | const passwordLengthRange = [16, 128]; 7 | const passwordMustNotContainSpaces = true; 8 | const passwordMustContainNumber = true; 9 | const passwordMustContainLowercaseLetter = true; 10 | const passwordMustContainUppercaseLetter = true; 11 | const allowList = [/^https?:\\/\\//] 12 | 13 | // Assume that HTML text does not contain secrets 14 | if (data(mimeHtml).length !== 0) abort(); 15 | 16 | const textData = data(mimeText); 17 | if (textData.length < passwordLengthRange[0]) abort(); 18 | if (textData.length > passwordLengthRange[1]) abort(); 19 | 20 | const text = str(textData); 21 | if (passwordMustNotContainSpaces && /\\s/.test(text)) abort(); 22 | 23 | if (passwordMustContainNumber && !/\\d/.test(text)) abort(); 24 | if (passwordMustContainLowercaseLetter && !/[a-z]/.test(text)) abort(); 25 | if (passwordMustContainUppercaseLetter && !/[A-Z]/.test(text)) abort(); 26 | 27 | for (const re of allowList) { 28 | if (re.test(text)) abort(); 29 | } 30 | 31 | notification( 32 | '.title', 'Ignoring secret in the clipboard', 33 | '.id', 'secrets', 34 | '.icon', '\xf084', 35 | '.time', notificationTimeout, 36 | ); 37 | ignore();" 38 | Icon=\xf084 39 | Name=Ignore Passwords/Tokens 40 | -------------------------------------------------------------------------------- /Automatic/image-tab.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | const imageTab = '&Images'; 6 | 7 | function hasImageFormat(formats) { 8 | for (const format of formats.values()) { 9 | if (format.startsWith('image/')) 10 | return true; 11 | } 12 | return false; 13 | } 14 | 15 | const formats = dataFormats(); 16 | if (hasImageFormat(formats)) { 17 | setData(mimeOutputTab, imageTab); 18 | }" 19 | Icon=\xf302 20 | Name=Image Tab 21 | -------------------------------------------------------------------------------- /Automatic/import-commands-after-copied.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | // Imports raw commands code (starting with [Command] or [Commands]) 6 | // or from a link ('https://github.com/**/copyq-commands/blob/**.ini'). 7 | const timeOutSeconds = 10 8 | const rawDataUrlPrefix = 'https://raw.githubusercontent.com' 9 | 10 | // Don't run this when mouse selection changes. 11 | if ( dataFormats().indexOf(mimeClipboardMode) != -1 ) 12 | abort() 13 | 14 | function importCommandsFromUrl(url) 15 | { 16 | let m = url.match(/^https?:\\/\\/github\\.com(\\/.*)\\/blob(\\/.*\\.ini)/) 17 | if (!m) 18 | return; 19 | 20 | let rawDataUrl = rawDataUrlPrefix + m[1] + m[2] 21 | let reply = networkGet(rawDataUrl) 22 | let commandsData = str(reply.data) 23 | if (reply.status != 200) { 24 | throw 'Failed to fetch commands.' 25 | + '\\nStatus code: ' + reply.status 26 | + '\\nError: ' + reply.error 27 | } 28 | if ( !commandsData.match(/^\\[Commands?\\]/) ) 29 | return; 30 | 31 | return importCommands(commandsData); 32 | } 33 | 34 | const ini = dataFormats().includes(mimeTextUtf8) ? data(mimeTextUtf8) : input(); 35 | const cmds = importCommandsFromUrl(str(ini)) || importCommands(ini) 36 | 37 | let list = '' 38 | for (const cmd of cmds) { 39 | let cmdType = 40 | cmd.automatic ? 'automatic' : 41 | cmd.inMenu ? 'menu/shortcut' : 42 | cmd.globalShortcuts ? 'global shortcut' : '???'; 43 | list += cmd.name + ' (' + cmdType + ')\\n' 44 | } 45 | 46 | notification( 47 | '.title', 'Import CopyQ commands from clipboard?', 48 | '.message', list, 49 | '.time', timeOutSeconds * 1000, 50 | '.icon', '', 51 | '.id', 'CopyQ_commands_in_clipboard', 52 | '.button', 'Cancel', '', '', 53 | '.button', 'Import', 'copyq: add(input()); addCommands( importCommands(input()) )', exportCommands(cmds) 54 | )" 55 | Icon= 56 | Input=text/plain 57 | Match=^\\[Commands?\\]|https?://github\\.com/.*/copyq-commands/blob/.*\\.ini 58 | Name=Notification for Copied Commands 59 | -------------------------------------------------------------------------------- /Automatic/keepassxc-protector.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Icon=\xf21b 4 | Input=x-kde-passwordManagerHint 5 | Name=KeePassXC protector v3 6 | Remove=true 7 | -------------------------------------------------------------------------------- /Automatic/linkify.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | var url = str(input()) 6 | var html = '###'.replace(/###/g, url) 7 | setData(mimeText, url) 8 | setData(mimeHtml, html)" 9 | Icon=\xf127 10 | Input=text/plain 11 | Match=^(https?|ftps?|file|mailto):// 12 | Name=Linkify 13 | -------------------------------------------------------------------------------- /Automatic/mouse-selections-tab.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | var tabName = 'Selections' 6 | setData(mimeOutputTab, tabName)" 7 | Icon=\xf245 8 | MatchCommand="copyq: dataFormats().indexOf(mimeClipboardMode) == -1 && fail()" 9 | Name=Store Mouse Selections in Separate Tab 10 | -------------------------------------------------------------------------------- /Automatic/play-sound-when-copying-to-clipboard-linux.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Play Sound on Copy 3 | Automatic=true 4 | Command=" 5 | copyq: 6 | if ( isClipboard() ) 7 | execute('mpv', '/usr/share/kmousetool/sounds/mousetool_tap.wav')" 8 | Icon=\xf028 9 | -------------------------------------------------------------------------------- /Automatic/play-sound-when-copying-to-clipboard-windows.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Play Sound on Copy 3 | Command=" 4 | powershell: 5 | (New-Object Media.SoundPlayer \"C:\\Users\\copy.wav\").PlaySync()" 6 | Automatic=true 7 | Icon=\xf028 8 | -------------------------------------------------------------------------------- /Automatic/show-window-title.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | const tagsMime = 'application/x-copyq-tags' 6 | const window = data(mimeWindowTitle) 7 | const oldTags = data(tagsMime) 8 | const tags = `${oldTags}, ${window}` 9 | setData(tagsMime, tags)" 10 | Icon= 11 | Name=Store Window Title 12 | -------------------------------------------------------------------------------- /Automatic/store-copy-time.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | const tagsMime = 'application/x-copyq-tags' 6 | const time = dateString('yyyy-MM-dd hh:mm:ss') 7 | const oldTags = data(tagsMime) 8 | const tags = `${oldTags}, ${time}` 9 | setData(tagsMime, tags)" 10 | Icon= 11 | Name=Store Copy Time 12 | -------------------------------------------------------------------------------- /Automatic/synchronize-clipboard-with-other-sessions.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | // Select session to send data to. 6 | var sessions = [ 7 | 'my_session_2', 8 | 'my_session_3', 9 | 'my_session_4', 10 | ] 11 | 12 | function passInput(session_name, command) { 13 | execute( 14 | 'copyq', '-s', session_name, 15 | command, mimeItems, '-', 16 | null, input()) 17 | } 18 | 19 | function syncNewItem(session_name) { 20 | passInput(session_name, 'write') 21 | } 22 | 23 | function syncClipboard(session_name) { 24 | passInput(session_name, 'clipboard') 25 | } 26 | 27 | for (var i in sessions) { 28 | // Set clipboard in each session. 29 | //syncClipboard(sessions[i]) 30 | 31 | // Add new item in each session. 32 | syncNewItem(sessions[i]) 33 | }" 34 | Icon=\xf1e1 35 | Input=text/plain 36 | Name=Synchronize Clipboard with Other Sessions 37 | -------------------------------------------------------------------------------- /Automatic/tab-for-urls-with-title-and-icon.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | var tabName = '&URLs'; 6 | function lower(data) { 7 | return str(data).toLowerCase() 8 | } 9 | 10 | function findHeader(reply, headerName) { 11 | reply.data // fetches data and headers 12 | var headers = reply.headers 13 | for (var i in headers) { 14 | var header = headers[i] 15 | if (lower(header[0]) === headerName) 16 | return header[1] 17 | } 18 | return '' 19 | } 20 | 21 | function fetchContent(url, maxRedirects) { 22 | if (maxRedirects === undefined) 23 | maxRedirects = 4 24 | 25 | serverLog('Fetching: ' + url) 26 | var reply = networkGet(url) 27 | if (maxRedirects == 0) 28 | return reply 29 | 30 | var header = findHeader(reply, 'location') 31 | if (header) 32 | return fetchContent(header, maxRedirects - 1) 33 | 34 | return reply 35 | } 36 | 37 | function decodeHtml(html) { 38 | return html.replace(/&#(\\d+);/g, function(match, charCode) { 39 | return String.fromCharCode(charCode); 40 | }); 41 | } 42 | 43 | function isHtml(reply) { 44 | var headers = reply.headers 45 | for (var i in headers) { 46 | var header = headers[i] 47 | if (lower(header[0]) === 'content-type') 48 | return lower(header[1]).indexOf(mimeHtml) === 0 49 | } 50 | return false 51 | } 52 | 53 | function grep(content, re) { 54 | return content ? (re.exec(content) || [])[1] : '' 55 | } 56 | 57 | function getTitle(content) { 58 | var title = grep(content, /]*>([^<]*)<\\/title>/i) 59 | return title ? decodeHtml(title.trim()) : '' 60 | } 61 | 62 | function getFavicon(content) { 63 | var iconLine = grep(content, /]*rel=[\"'](?:shortcut )?icon[\"'][^>]*)/i) 64 | var icon = grep(iconLine, /href=[\"']([^\"']*)/i) 65 | 66 | if (!icon) 67 | return '' 68 | 69 | // Icon path can be complete URL. 70 | if (icon.indexOf('://') != -1) 71 | return fetchContent(icon).data 72 | 73 | // Icon path can be missing protocol. 74 | if (icon.substr(0, 2) === '//') { 75 | var i = url.search(/\\/\\//) 76 | var protocol = (i == -1) ? 'http:' : url.substr(0, i) 77 | return fetchContent(protocol + icon).data 78 | } 79 | 80 | // Icon path can be relative to host URL. 81 | if (icon[0] === '/') { 82 | var baseUrl = url.substr(0, url.search(/[^\\/:](\\/|$)/) + 1) 83 | return fetchContent(baseUrl + icon).data 84 | } 85 | 86 | // Icon path can be relative to current URL. 87 | var baseUrl = url.substr(0, url.lastIndexOf('/') + 1) 88 | return fetchContent(baseUrl + icon).data 89 | } 90 | 91 | var url = str(input()).trim() 92 | serverLog('Fetching icon and title: ' + url) 93 | 94 | // URL already added? (Just check the top of the list.) 95 | if (url === str(read(0))) 96 | abort() 97 | 98 | // Fetch HTML. 99 | var reply = fetchContent(url) 100 | if (!isHtml(reply)) 101 | abort() 102 | 103 | var content = str(reply.data) 104 | 105 | var title = getTitle(content) 106 | var icon = getFavicon(content) 107 | 108 | setData(mimeText, url) 109 | setData(mimeItemNotes, title || '') 110 | setData(mimeIcon, icon) 111 | setData(mimeOutputTab, tabName)" 112 | Icon=\xf0c1 113 | Input=text/plain 114 | Match=^https?:// 115 | Name=Tab for URLs with Title and Icon 116 | -------------------------------------------------------------------------------- /Display/README.md: -------------------------------------------------------------------------------- 1 | This section contains commands which change appearance of items. 2 | 3 | ### [Highlight Code](highlight-code.ini) 4 | 5 | Highlights syntax for recognized code. 6 | 7 | Requires Python and [Pygments](https://pygments.org/). 8 | 9 | ### [Preview Image Files](preview-image-files.ini) 10 | 11 | Shows images instead of just a path (works with `file://...`). 12 | 13 | ### [Render Markdown](render-markdown.ini) 14 | 15 | Renders markdown if recognized. 16 | 17 | Requires [marked](https://marked.js.org/). 18 | 19 | ### [Toggle Show As Plain Text](toggle-show-as-plain-text.ini) 20 | 21 | Display command combined with a shortcut that changes how selected items are 22 | displayed: as plain text or rich text (HTML). 23 | -------------------------------------------------------------------------------- /Display/highlight-code.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | const mimeTags = 'application/x-copyq-tags' 5 | 6 | const pythonCode = ` 7 | import sys 8 | from pygments import highlight 9 | from pygments.lexers import get_lexer_by_name 10 | from pygments.formatters import HtmlFormatter 11 | 12 | code = sys.stdin.read() 13 | lexer = get_lexer_by_name(sys.argv[1]) 14 | formatter = HtmlFormatter(noclasses=True, style='tango', encoding='utf-8') 15 | formatter.style.background_color = 'none' 16 | print(highlight(code, lexer, formatter).decode()) 17 | ` 18 | 19 | const tagPartToLang = { 20 | 'CopyQ Commands': 'js', 21 | '.cpp': 'cpp', 22 | '.h ': 'cpp', 23 | '.py': 'python', 24 | '.rs': 'rust', 25 | '.rb': 'ruby', 26 | '.yaml': 'yaml', 27 | '.yml': 'yaml', 28 | '@': 'md', 29 | } 30 | 31 | const prefixToLang = { 32 | '[Command]': 'ini', 33 | 'copyq:': 'js', 34 | '#!/bin/bash': 'bash', 35 | 'SELECT': 'sql', 36 | } 37 | 38 | const textPartToLang = { 39 | '; then\\n': 'bash', 40 | '; do\\n': 'bash', 41 | '`': 'md', 42 | ' function(': 'js', 43 | 'fn ': 'rust', 44 | 'if (': 'cpp', 45 | 'for (': 'cpp', 46 | 'while (': 'cpp', 47 | '#include ': 'cpp', 48 | ' 10000) 135 | return false 136 | 137 | const text = str(textData) 138 | 139 | tags = str(data(mimeTags)) 140 | 141 | try { 142 | var lang = langFromTags(tags) 143 | || langFromPrefix(text) 144 | || langFromTextPart(text) 145 | if (lang) 146 | return code(textData, lang) 147 | } catch(e) { 148 | popup(errorLabel, e) 149 | serverLog(errorLabel + ': ' + e) 150 | } 151 | 152 | return false 153 | } 154 | 155 | highlightCode()" 156 | Display=true 157 | Icon=\xf121 158 | Name=Highlight Code 159 | -------------------------------------------------------------------------------- /Display/preview-image-files.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var prefix = 'file://' 5 | var suffixToMime = { 6 | 'png': 'image/png', 7 | 'jpg': 'image/jpeg', 8 | 'jpeg': 'image/jpeg', 9 | 'bmp': 'image/bmp', 10 | 'svg': 'image/svg+xml', 11 | 'ico': 'image/png', 12 | 'webp': 'image/png', 13 | 14 | } 15 | 16 | function startsWith(text, what) { 17 | return what === text.substring(0, what.length) 18 | } 19 | 20 | function tryShowImage(mime) { 21 | var text = str(data(mime)) 22 | if ( !startsWith(text, prefix) ) 23 | return false 24 | 25 | var i = text.lastIndexOf('.') 26 | if (i == -1) 27 | return false 28 | 29 | var suffix = text.substring(i + 1) 30 | var imageMime = suffixToMime[suffix] 31 | if (imageMime === undefined) 32 | return false 33 | 34 | var path = text.substring(prefix.length) 35 | 36 | var f = new File(path) 37 | if ( !f.openReadOnly() ) 38 | return false 39 | 40 | var imageData = f.readAll() 41 | f.close() 42 | if ( imageData.size() === 0 ) 43 | return false 44 | 45 | setData(mimeItemNotes, path) 46 | setData(imageMime, imageData) 47 | return true 48 | } 49 | 50 | tryShowImage(mimeText) 51 | || tryShowImage(mimeUriList)" 52 | Display=true 53 | Icon=\xf1c5 54 | Name=Preview Image Files 55 | -------------------------------------------------------------------------------- /Display/render-markdown.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | const markdownTag = 'markdown' 5 | const mimeTags = 'application/x-copyq-tags' 6 | const tagFragments = ['@'] 7 | const textFragments = [ 8 | '`', 9 | '\\n##', 10 | '](', 11 | '\\n* ', 12 | ] 13 | const errorLabel = 'Markdown Error' 14 | 15 | function contains(text, what) { 16 | return text.indexOf(what) != -1 17 | } 18 | 19 | function startsWith(text, what) { 20 | return what === text.substring(0, what.length) 21 | } 22 | 23 | function matchesAnyOf(text, fragments) { 24 | return fragments.find(e => text.indexOf(e) != -1) 25 | } 26 | 27 | function addHtml(html, tag) { 28 | setData(mimeHtml, html) 29 | if (tag) { 30 | tags = data(mimeTags) 31 | tags = (tags ? str(tags) + ',' : '') + tag 32 | setData(mimeTags, tags) 33 | } 34 | return true 35 | } 36 | 37 | function addHtmlOutput(result, tag) { 38 | if (!result) { 39 | notification( 40 | '.id', 'highlight', 41 | '.message', 'Failed to add syntax highlighting', 42 | ) 43 | return false 44 | } 45 | 46 | if (result.exit_code !== 0) { 47 | popup(errorLabel, result.stderr) 48 | return false 49 | } 50 | 51 | return addHtml(result.stdout, tag) 52 | } 53 | 54 | function markdown(textData) { 55 | result = execute('marked', null, textData) 56 | return addHtmlOutput(result, markdownTag) 57 | } 58 | 59 | function highlightCode() { 60 | var formats = dataFormats() 61 | if ( formats.indexOf(mimeHidden) != -1 62 | || formats.indexOf(mimeHtml) != -1 ) { 63 | return false 64 | } 65 | 66 | var textData = data(mimeText) 67 | var text = str(textData) 68 | if (!text) 69 | return false 70 | 71 | tags = str(data(mimeTags)) 72 | 73 | try { 74 | if ( startsWith(text, 'http') ) 75 | return markdown(textData, 'md') 76 | 77 | if ( matchesAnyOf(tags, tagFragments) 78 | || matchesAnyOf(text, textFragments) ) { 79 | return markdown(textData) 80 | } 81 | } catch(e) { 82 | popup(errorLabel, e) 83 | serverLog(errorLabel + ': ' + e) 84 | } 85 | 86 | return false 87 | } 88 | 89 | highlightCode()" 90 | Display=true 91 | Icon=\xf60f 92 | Name=Render Markdown 93 | -------------------------------------------------------------------------------- /Display/toggle-show-as-plain-text.ini: -------------------------------------------------------------------------------- 1 | [Commands] 2 | 1\Command=" 3 | copyq: 4 | const mime = 'application/x-copyq-show-plain'; 5 | const sel = ItemSelection().current(); 6 | const enabled = str(sel.itemAtIndex(0)[mime]) == '1'; 7 | sel.setItemsFormat(mime, enabled ? undefined : '1');" 8 | 1\Icon=A 9 | 1\InMenu=true 10 | 1\Name=Toggle Show As Plain Text 11 | 1\Shortcut=ctrl+shift+x 12 | 2\Command=" 13 | copyq: 14 | if (data('application/x-copyq-show-plain') == '1') 15 | setData(mimeHtml, undefined);" 16 | 2\Display=true 17 | 2\Icon=A 18 | 2\Name=Toggle Show As Plain Text 19 | size=2 20 | -------------------------------------------------------------------------------- /Global/README.md: -------------------------------------------------------------------------------- 1 | This section contains commands which can be executed with global/system shortcut 2 | (even when the main application window is not active). 3 | 4 | ### [Copy a Secret](copy-a-secret.ini) 5 | 6 | Copies (Ctrl+C) from current window without storing or showing the data in GUI. 7 | 8 | ### [Copy and Search on Web](copy-and-search-on-web.ini) 9 | 10 | Copies currently selected text and opens selection menu to search the text on 11 | some well-known websites. New search queries can be easily defined. 12 | 13 | ### [Copy Text in Image](copy-text-in-image.ini) 14 | 15 | Takes screenshot of selected part of the screen and tries to recognize text. 16 | 17 | Requires [GraphicsMagick](http://www.graphicsmagick.org/download.html) 18 | and [Tesseract](https://github.com/tesseract-ocr/tesseract/wiki/Downloads). 19 | 20 | ### [Cycle Items](cycle-items.ini) 21 | 22 | Pops up the main window (if the shortcut is pressed once), cycles through items 23 | (if the shortcut is pressed again) and pastes selected item when the shortcut 24 | is released. 25 | 26 | See: https://github.com/hluk/CopyQ/issues/1948 27 | 28 | ### [Cycle Items - Quick](quick-cycle-items.ini) 29 | 30 | Like Cycle Items command but previews items to copy in popups without showing 31 | the main window. 32 | 33 | ### [Disable Monitoring State Permanently](disable-clipboard-monitoring-state-permanently.ini) 34 | 35 | Disables clipboard monitoring permanently, i.e. the state is restored when clipboard changes even after application is restarted. 36 | 37 | ### [Edit and Paste](edit-and-paste.ini) 38 | 39 | Following command allows to edit current clipboard text before pasting it. 40 | 41 | If the editing is canceled the text won't be pasted. 42 | 43 | ### [Paste Current Date and Time](paste-current-date-time.ini) 44 | 45 | Copies current date/time text to clipboard and pastes to current window on global shortcut Win+Alt+T. 46 | 47 | ### [Paste Current Date and Time in ISO8601 Format](paste-current-date-time-in-iso-8601.ini) 48 | 49 | Copies current date/time in ISO8601 format to clipboard, adds it to the clipboard history, and then pastes it to the current window. 50 | 51 | ### [Paste new UUID](paste-new-uuid.ini) 52 | 53 | Generates a new RFC4122 version 4 compliant UUID, adds it to the clipboard history, copies it to the clipboard and pastes it to the current window. 54 | Full credit for UUID generation code goes to Jeff Ward (jcward.com), link: https://stackoverflow.com/a/21963136/11820711 55 | 56 | ### [Push/Pop Items](push-pop-stack.ini) 57 | 58 | A global shortcut to copy selected text/HTML/image as a new top item in "Stack" 59 | tab and another shortcut to paste the top item and remove it from the tab. 60 | 61 | See: https://github.com/hluk/CopyQ/issues/597 62 | 63 | ### [Quickly Show Current Clipboard Content](quickly-show-current-clipboard-content.ini) 64 | 65 | Quickly pops up notification with text in clipboard using `Win+Alt+C` system shortcut. 66 | 67 | ### [Replace All Occurrences in Selected Text](replace-all-occurences-in-selected-text.ini) 68 | 69 | ### [Screenshot](screenshot.ini) 70 | 71 | Take screenshot of the screen. 72 | 73 | ### [Screenshot Cutout](screenshot-cutout.ini) 74 | 75 | Take screenshot of selected part of the screen. 76 | 77 | ### [Select Nth Item](select-nth-item.ini) 78 | 79 | Quick shortcuts to activate items 0 to 9 (copy, move to top and paste depending 80 | on preferences in History configuration tab). 81 | 82 | ### [Paste Nth Item](paste-nminus1th-item.ini) 83 | 84 | Paste items 1-9 in history using ctrl+1 through ctrl+9 shortcuts. 85 | 86 | ### [Show Clipboard](show-clipboard.ini) 87 | 88 | Shows notification with current clipboard content (text or image). 89 | 90 | ### [Snippets](snippets.ini) 91 | 92 | Shows dialog with snippets to paste. 93 | 94 | Snippets are loaded from "Snippets" tab. Item notes are used as snippet name. 95 | 96 | Items can contain placeholders like: 97 | - `${Name}` (default text is empty), 98 | - `${Name:value}` (default text is "value"), 99 | - `${Name:value1,value2,value3}` (default text is "value1"; allows to select from multiple values), 100 | - `${Name:\n}` (multi-line text field). 101 | 102 | When such snippet is selected, user is prompted to replace these placeholders with custom content. 103 | 104 | To create your first snippet: 105 | 106 | 1. create "Snippets" tab (Ctrl+T), 107 | 2. add new item (Ctrl+N) with a snippet: 108 | 109 | You picked ${Fruit:apples,oranges,pears}! 110 | 111 | 3. set optional snippet name (Ctrl+F2), e.g. "Fruit". 112 | 113 | Triggering the Snippets command (with a global shortcut) will show a simple 114 | dialog where you can pick the snippet by its name. 115 | 116 | To pick different tab name, you have to change the command's code. 117 | 118 | var snippetsTabName = '&Snippets' 119 | 120 | ### [Stopwatch](stopwatch.ini) 121 | 122 | Restarts stopwatch and copies elapsed time since last started. 123 | 124 | ### [Tabs navigation](tabs-navigation.ini) 125 | 126 | Global shortcuts to select tabs. It is possible to select the Nth tab by order and the next or previous tab. 127 | 128 | ### [Toggle Clipboard Storing](toggle-clipboard-storing.ini) 129 | 130 | Toggles clipboard storing/monitoring with global shortcut or from menu/toolbar. 131 | 132 | ### [Capitalize Selected Text for Titles](to-title-case.ini) 133 | 134 | E.g. changes "Do androids dream of electric sheep?" to "Do Androids Dream of Electric Sheep?". 135 | 136 | ### [Change Upper/Lower Case of Selected Text](toggle-upper-lower-case-of-selected-text.ini) 137 | 138 | Toggles between upper- and lower-case letters in selected text. 139 | 140 | ### [Diff Latest Items](diff-latest-items.ini) 141 | 142 | Compares two clipboard history items with your preferred diff tool. 143 | 144 | The latest two items get compared when the command is run as a global command. 145 | You can also run the command on any two items selected in the main window. 146 | 147 | By default, this command launches [Beyond Compare 4](https://www.scootersoftware.com/download.php) 148 | for doing the comparison. 149 | You can find examples of launching other tools like [WinMerge](https://winmerge.org/downloads) directly in the command's source code. 150 | 151 | ### [Convert Markdown to ...](convert-markdown.ini) 152 | 153 | Converts text written in [Markdown syntax](https://daringfireball.net/projects/markdown/syntax) 154 | to desired format, which can be for example: 155 | 156 | * HTML 157 | * [Jira markup](https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=all) 158 | * JSON (AST) (JSON representation of the parsed text; useful rather for contributors than users) 159 | * [LaTeX](https://en.wikipedia.org/wiki/LaTeX) 160 | 161 | The command can be run on any text selection via a global shortcut or over items selected 162 | in the main window. 163 | 164 | #### Installation 165 | 166 | This script relies on the [mistletoe](https://github.com/miyuchina/mistletoe) project to do the 167 | actual Markdown parsing and conversion. 168 | This in turn requires that [Python](https://www.python.org/downloads/) is installed on the user computer. 169 | 170 | See mistletoe's page linked above for the various possibilities of its installation. 171 | 172 | For output format "HTML + code highlighting", an additional Python package needs to be installed: 173 | 174 | pip3 install pygments 175 | 176 | ### [Show Character Code](show-char-code.ini) 177 | 178 | Shows Unicode code info for the first characters of any text. An example of how this looks like: 179 | 180 | ![Show Character Code Dialog](images/cmd_show-char-code.png) 181 | -------------------------------------------------------------------------------- /Global/convert-markdown.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Convert Markdown to ... 3 | Command=" 4 | copyq: 5 | // # get input text to be converted 6 | 7 | var markdown = str(input()); 8 | if (!markdown) { 9 | copy(); 10 | markdown = clipboard(); 11 | } 12 | 13 | // # get conversion options from user 14 | 15 | var renderers = { 16 | 'HTML': 'mistletoe.html_renderer.HTMLRenderer', 17 | 'HTML + code highlighting': 'mistletoe.contrib.pygments_renderer.PygmentsRenderer', // requires: `pip3 install pygments` 18 | 'HTML + GitHub Wiki': 'mistletoe.contrib.github_wiki.GithubWikiRenderer', 19 | 'HTML + MathJax': 'mistletoe.contrib.mathjax.MathJaxRenderer', 20 | 'Jira': 'mistletoe.contrib.jira_renderer.JIRARenderer', 21 | 'JSON (AST)': 'mistletoe.ast_renderer.ASTRenderer', 22 | 'LaTeX': 'mistletoe.latex_renderer.LaTeXRenderer', 23 | 'XWiki Syntax 2.0': 'mistletoe.contrib.xwiki20_renderer.XWiki20Renderer', 24 | } 25 | 26 | var settingsPrefix = 'convert-markdown/'; 27 | var optFormat = 'Target format'; 28 | var optAddToHistory = 'Add result to clipboard history'; 29 | var optHtmlAsSourceOnly = 'Output HTML as source code only'; 30 | 31 | var format = settings(settingsPrefix + optFormat); 32 | var addToHistory = settings(settingsPrefix + optAddToHistory) == 'true'; 33 | var htmlAsSourceOnly = settings(settingsPrefix + optHtmlAsSourceOnly) == 'true'; 34 | 35 | var options = dialog( 36 | '.title', 'Convert Markdown to ...', 37 | '.defaultChoice', format, 38 | optFormat, Object.keys(renderers), 39 | optAddToHistory, addToHistory, 40 | optHtmlAsSourceOnly, htmlAsSourceOnly 41 | ); 42 | 43 | if (!options) { 44 | abort(); 45 | } 46 | 47 | // # parse and store the options 48 | 49 | format = options[optFormat]; 50 | addToHistory = options[optAddToHistory]; 51 | htmlAsSourceOnly = options[optHtmlAsSourceOnly]; 52 | 53 | settings(settingsPrefix + optFormat, format); 54 | settings(settingsPrefix + optAddToHistory, addToHistory); 55 | settings(settingsPrefix + optHtmlAsSourceOnly, htmlAsSourceOnly); 56 | 57 | // # do the conversion 58 | 59 | function tempFile(content) { 60 | var file = new TemporaryFile(); 61 | file.openWriteOnly(); 62 | file.write(content); 63 | file.close(); 64 | return file; 65 | } 66 | 67 | var mdFile = tempFile(markdown); 68 | 69 | var cmdRes = execute('python', '-m', 'mistletoe', mdFile.fileName(), '--renderer', renderers[format]); 70 | 71 | if (!cmdRes || cmdRes.exit_code != 0) { 72 | popup('', 'Conversion failed: ' + (cmdRes ? str(cmdRes.stderr) : 'Python executable is probably not available?'), -1); 73 | fail(); 74 | } 75 | 76 | // # store conversion result 77 | 78 | var output = str(cmdRes.stdout); 79 | 80 | function html2text(html) { 81 | // strip tags 82 | var text = html.replace(/<[^>]*>?/gm, ''); 83 | // replace known entities 84 | text = text.replace(/&([^;]+);/g, function (match, p1) { 85 | var chars = { 86 | 'lt': '<', 87 | 'gt': '>', 88 | 'amp': '&', 89 | '#39': '\\'', 90 | 'nbsp': '\\xa0', 91 | } 92 | return chars[p1] || p1; 93 | }); 94 | return text; 95 | } 96 | 97 | var item = {}; 98 | if (htmlAsSourceOnly || format.indexOf('HTML') == -1) { 99 | item[mimeText] = output; 100 | } else { 101 | item[mimeHtml] = output; 102 | item[mimeText] = html2text(output); 103 | } 104 | 105 | function copyItem(item) { 106 | args = []; 107 | for (prop in item) { 108 | args.push(prop, item[prop]); 109 | } 110 | 111 | // copy() signature: copy(mimeType, data, [mimeType, data]...) 112 | copy.apply(this, args); 113 | } 114 | 115 | if (addToHistory) { 116 | add(item); 117 | } 118 | copyItem(item); 119 | 120 | popup('', 'Markdown successfully converted to ' + format + '!'); 121 | " 122 | InMenu=true 123 | IsGlobalShortcut=true 124 | Icon=\xf103 125 | GlobalShortcut=meta+alt+c 126 | -------------------------------------------------------------------------------- /Global/copy-a-secret.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var wasMonitoring = monitoring() 5 | if (wasMonitoring) 6 | disable() 7 | 8 | try { 9 | copy() 10 | } catch (e) { 11 | } 12 | 13 | if (wasMonitoring) 14 | enable()" 15 | GlobalShortcut=ctrl+shift+c 16 | Icon=\xf6fa 17 | IsGlobalShortcut=true 18 | Name=Copy a Secret 19 | -------------------------------------------------------------------------------- /Global/copy-and-search-on-web.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var searchEngines = [ 5 | { 6 | 'name': 'Stackoverflow', 7 | 'url': 'https://stackoverflow.com/search?q=%s', 8 | 'icon': '\xf16c', 9 | }, 10 | { 11 | 'name': 'Github', 12 | 'url': 'https://github.com/search?q=%s', 13 | 'icon': '\xf09b', 14 | }, 15 | { 16 | 'name': 'DuckDuckGo', 17 | 'url': 'https://duckduckgo.com/?q=%s', 18 | }, 19 | ] 20 | 21 | // Copy selected text. 22 | copy() 23 | var text = str(clipboard()) 24 | if (!text) 25 | abort() 26 | popup('Search Text', text) 27 | 28 | var items = [] 29 | for (var i in searchEngines) { 30 | var engine = searchEngines[i] 31 | var item = {} 32 | item[mimeText] = engine['name'] 33 | item[mimeIcon] = engine['icon'] 34 | items.push(item) 35 | } 36 | 37 | var i = menuItems(items) 38 | if (i == -1) 39 | abort() 40 | 41 | text = encodeURIComponent(text) 42 | var urlTemplate = searchEngines[i] 43 | var url = urlTemplate['url'].replace('%s', text) 44 | open(url)" 45 | GlobalShortcut=meta+shift+f 46 | Icon=\xf002 47 | Input=text/plain 48 | IsGlobalShortcut=true 49 | Name=Copy and Search on Web 50 | -------------------------------------------------------------------------------- /Global/copy-text-in-image.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq screenshotSelect | gm convert png:- -colorspace Gray -depth 8 -resample 200x200 tif:- | copyq: 4 | var language = 'eng' 5 | 6 | var imageData = input() 7 | if (imageData.size() == 0) { 8 | popup('No image area specified') 9 | abort() 10 | } 11 | 12 | var result = execute('tesseract', '--list-langs') 13 | if (!result) 14 | throw 'Failed to run tesseract utility' 15 | if (result.exit_code != 0) { 16 | throw 'Failed to get languages from tesseract: ' 17 | + str(result.stderr) + str(result.stdout) 18 | } 19 | 20 | var languages = str(result.stdout).split('\\n').slice(1) 21 | 22 | language = dialog( 23 | '.title', 'Pick Language', 24 | '.defaultChoice', language, 25 | 'Language', languages) 26 | if (!language) 27 | abort() 28 | 29 | result = execute( 30 | 'tesseract', 31 | // OCR Engine mode: 32 | // 3 - Default, based on what is available. 33 | '--oem', '3', 34 | // Page segmentation mode: 35 | // 6 - Assume a single uniform block of text. 36 | '--psm', '6', 37 | '-l', language.trim(), 38 | 'stdin', 'stdout', 39 | null, imageData) 40 | if (!result) 41 | throw 'Failed to run tesseract utility' 42 | if (result.exit_code != 0) { 43 | throw 'Failed to run tesseract OCR: ' 44 | + str(result.stderr) + str(result.stdout) 45 | } 46 | 47 | var text = str(result.stdout).trim() 48 | add(text) 49 | copy(text)" 50 | GlobalShortcut=meta+ctrl+t 51 | Icon=\xf1ea 52 | IsGlobalShortcut=true 53 | Name=Copy Text in Image 54 | -------------------------------------------------------------------------------- /Global/cycle-items.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | // Pops up the main window (if the shortcut is pressed once), cycles through items 5 | // (if the shortcut is pressed again) and pastes selected item when the shortcut 6 | // is released. 7 | const selectedRowOption = 'cycleItemsSelectedRow'; 8 | const selectedTabOption = 'cycleItemsSelectedTab'; 9 | 10 | if (focused()) { 11 | const sel = ItemSelection().current(); 12 | const rows = sel.rows(); 13 | const row = rows.length > 0 ? (rows[0] + 1) % length() : 0; 14 | settings(selectedRowOption, row); 15 | settings(selectedTabOption, selectedTab()); 16 | selectItems(row); 17 | } else { 18 | settings(selectedRowOption, -1); 19 | selectItems(0); 20 | show(); 21 | 22 | // Wait for shortcut modifiers to be released. 23 | while (queryKeyboardModifiers().length > 0) { 24 | sleep(20); 25 | } 26 | 27 | const row = settings(selectedRowOption) 28 | if (row != -1) { 29 | tab(settings(selectedTabOption)); 30 | select(row); 31 | hide(); 32 | paste(); 33 | } 34 | }" 35 | GlobalShortcut="ctrl+;" 36 | Icon=\xf1b8 37 | InMenu=true 38 | IsGlobalShortcut=true 39 | Name=Cycle Items 40 | -------------------------------------------------------------------------------- /Global/diff-latest-items.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var selectedItem1 = selectedItemData(0)[mimeText] 5 | var selectedItem2 = selectedItemData(1)[mimeText] 6 | 7 | var item1 = null 8 | var item2 = null 9 | 10 | if (selectedItem2 == undefined) { 11 | // the selected item either doesn't contain text 12 | // or the command is run as global shortcut. 13 | // select the last two clipboard in this case. 14 | item1 = read(1) 15 | item2 = read(0) 16 | } else { 17 | item1 = selectedItem1 18 | item2 = selectedItem2 19 | } 20 | 21 | function tempFile(content) { 22 | var file = new TemporaryFile() 23 | file.openWriteOnly() 24 | file.write(content) 25 | file.close() 26 | return file 27 | } 28 | 29 | var f1 = tempFile(item1) 30 | var f2 = tempFile(item2) 31 | var name1 = f1.fileName() 32 | var name2 = f2.fileName() 33 | 34 | // Choose your favorite diff tool (leave just one execute(...) uncommented): 35 | 36 | // === Beyond Compare === 37 | // reference: https://www.scootersoftware.com/v4help/command_line_reference.html 38 | // If it doesn't work, try using the full path, eg: 39 | // execute('/usr/local/bin/compare', name1, name2) 40 | execute('bcompare', name1, name2) 41 | 42 | // === WinMerge === 43 | // reference: https://manual.winmerge.org/en/Command_line.html 44 | // execute('winmergeu', '/e', '/x', '/u', '/fl', '/dl', 'item1', '/dr', 'item2', name1, name2) 45 | 46 | // Wait few seconds before exiting script and deleting temporary files, 47 | // because the command may be executed in background. 48 | sleep(5000)" 49 | GlobalShortcut=meta+alt+d 50 | Icon=\xf0db 51 | InMenu=true 52 | IsGlobalShortcut=true 53 | Name=Diff Latest Items 54 | Shortcut=ctrl+d 55 | -------------------------------------------------------------------------------- /Global/disable-clipboard-monitoring-state-permanently.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Automatic=true 3 | Command=" 4 | copyq: 5 | var option = 'disable_monitoring' 6 | var disabled = str(settings(option)) === 'true' 7 | 8 | if (str(data(mimeShortcut))) { 9 | disabled = !disabled 10 | settings(option, disabled) 11 | popup('', disabled ? 'Monitoring disabled' : 'Monitoring enabled') 12 | } 13 | 14 | if (disabled) { 15 | disable() 16 | ignore() 17 | } else { 18 | enable() 19 | }" 20 | GlobalShortcut=meta+alt+x 21 | Icon=\xf05e 22 | Name=Toggle Monitoring 23 | -------------------------------------------------------------------------------- /Global/edit-and-paste.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var text = dialog('paste', str(clipboard())) 5 | if (text) { 6 | copy(text) 7 | copySelection(text) 8 | paste() 9 | }" 10 | GlobalShortcut=ctrl+shift+v 11 | Icon=\xf0ea 12 | Name=Edit and Paste 13 | -------------------------------------------------------------------------------- /Global/images/cmd_show-char-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hluk/copyq-commands/544c88c5e5c121e2bd9a650140ac9fe4e024208a/Global/images/cmd_show-char-code.png -------------------------------------------------------------------------------- /Global/paste-current-date-time-in-iso-8601.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Paste current date/time in ISO8601 format 3 | Command=" 4 | copyq: 5 | var dateTime = new Date(); 6 | var isoDateTime = dateTime.toISOString(); 7 | add(isoDateTime); 8 | copy(isoDateTime) 9 | paste()" 10 | IsGlobalShortcut=true 11 | Icon=\xf017 12 | GlobalShortcut=meta+ctrl+t 13 | -------------------------------------------------------------------------------- /Global/paste-current-date-time.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var time = dateString('yyyy-MM-dd hh:mm:ss') 5 | copy('Current date/time is ' + time) 6 | paste()" 7 | GlobalShortcut=meta+alt+t 8 | Icon=\xf017 9 | Name=Paste Current Time 10 | -------------------------------------------------------------------------------- /Global/paste-new-uuid.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Paste new UUID 3 | Command=" 4 | copyq: 5 | // RFC4122 version 4 compliant UUID generator 6 | // Author credit: Jeff Ward (jcward.com) 7 | // Link: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136 8 | 9 | function generateUuid() { 10 | var lut = []; 11 | for (var i=0; i<256; i++) { 12 | lut[i] = (i<16?'0':'')+(i).toString(16); 13 | } 14 | 15 | var d0 = Math.random()*0xffffffff|0; 16 | var d1 = Math.random()*0xffffffff|0; 17 | var d2 = Math.random()*0xffffffff|0; 18 | var d3 = Math.random()*0xffffffff|0; 19 | return lut[d0&0xff]+lut[d0>>8&0xff]+lut[d0>>16&0xff]+lut[d0>>24&0xff]+'-'+ 20 | lut[d1&0xff]+lut[d1>>8&0xff]+'-'+lut[d1>>16&0x0f|0x40]+lut[d1>>24&0xff]+'-'+ 21 | lut[d2&0x3f|0x80]+lut[d2>>8&0xff]+'-'+lut[d2>>16&0xff]+lut[d2>>24&0xff]+ 22 | lut[d3&0xff]+lut[d3>>8&0xff]+lut[d3>>16&0xff]+lut[d3>>24&0xff]; 23 | } 24 | 25 | var uuid = generateUuid(); 26 | add(uuid); 27 | copy(uuid); 28 | paste();" 29 | IsGlobalShortcut=true 30 | Icon=\xf6cf 31 | GlobalShortcut=meta+ctrl+u 32 | -------------------------------------------------------------------------------- /Global/paste-nminus1th-item.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var n = str(data(mimeShortcut)).slice(-1) 5 | select(n-1) 6 | paste()" 7 | GlobalShortcut=ctrl+1, ctrl+2, ctrl+3, ctrl+4, ctrl+5, ctrl+6, ctrl+7, ctrl+8, ctrl+9 8 | Icon=\xf0cb 9 | IsGlobalShortcut=true 10 | Name=Paste Nth Item 11 | -------------------------------------------------------------------------------- /Global/push-pop-stack.ini: -------------------------------------------------------------------------------- 1 | [Commands] 2 | 1\Command=" 3 | copyq: 4 | tab('Stack') 5 | copy() 6 | var item = {} 7 | for (const format of clipboardFormatsToSave()) { 8 | var data = clipboard(format) 9 | if (data.length) { 10 | item[format] = data; 11 | } 12 | } 13 | add(item)" 14 | 1\GlobalShortcut=ctrl+shift+c 15 | 1\Icon=\xf078 16 | 1\IsGlobalShortcut=true 17 | 1\Name=Push Item 18 | 2\Command=" 19 | copyq: 20 | tab('Stack') 21 | const item = getItem(0) 22 | copy(item) 23 | paste() 24 | remove(0)" 25 | 2\GlobalShortcut=ctrl+shift+v 26 | 2\Icon=\xf077 27 | 2\IsGlobalShortcut=true 28 | 2\Name=Pop Item 29 | size=2 30 | -------------------------------------------------------------------------------- /Global/quick-cycle-items.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | const selectedRowOption = 'quickCycleItemsSelectedRow'; 5 | 6 | const row = parseInt(settings(selectedRowOption) || -1) + 1; 7 | settings(selectedRowOption, row) 8 | popup(row, read(row)); 9 | 10 | // Wait for shortcut modifiers to be released. 11 | while ( 12 | queryKeyboardModifiers().length > 0 13 | && settings(selectedRowOption) == row 14 | ) { 15 | sleep(20); 16 | } 17 | 18 | if ( settings(selectedRowOption) == row ) { 19 | settings(selectedRowOption, -1); 20 | select(row); 21 | paste(); 22 | }" 23 | GlobalShortcut="ctrl+;" 24 | Icon=\xf1b8 25 | IsGlobalShortcut=true 26 | Name=Quick Cycle Items 27 | -------------------------------------------------------------------------------- /Global/quickly-show-current-clipboard-content.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Show clipboard 3 | Command=" 4 | copyq: 5 | seconds = 2; 6 | popup(\"\", clipboard(), seconds * 1000)" 7 | GlobalShortcut=Meta+Alt+C 8 | -------------------------------------------------------------------------------- /Global/replace-all-occurences-in-selected-text.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Replace in Selection 3 | Command=" 4 | copyq: 5 | // Copy without changing X11 selection (on Windows you can use "copy" instead). 6 | function copy2() { 7 | try { 8 | var x = config('copy_clipboard') 9 | config('copy_clipboard', false) 10 | try { 11 | copy.apply(this, arguments) 12 | } finally { 13 | config('copy_clipboard', x) 14 | } 15 | } catch(e) { 16 | copy.apply(this, arguments) 17 | } 18 | } 19 | 20 | copy2() 21 | var text = str(clipboard()) 22 | 23 | if (text) { 24 | var r1 = 'Text' 25 | var r2 = 'Replace with' 26 | var reply = dialog(r1, '', r2, '') 27 | 28 | if (reply) { 29 | copy2(text.replace(new RegExp(reply[r1], 'g'), reply[r2])) 30 | paste() 31 | } 32 | }" 33 | Icon=\xf040 34 | GlobalShortcut=Meta+Alt+R 35 | -------------------------------------------------------------------------------- /Global/screenshot-cutout.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: copy('image/png', screenshotSelect())" 4 | GlobalShortcut=ctrl+print 5 | Icon=\xf083 6 | Name=Screenshot Cutout 7 | -------------------------------------------------------------------------------- /Global/screenshot.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: copy('image/png', screenshot())" 4 | GlobalShortcut=print 5 | Icon=\xf083 6 | Name=Screenshot 7 | -------------------------------------------------------------------------------- /Global/select-nth-item.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var n = str(data(mimeShortcut)).slice(-1) 5 | select(n)" 6 | GlobalShortcut=ctrl+shift+0, ctrl+shift+1, ctrl+shift+2, ctrl+shift+3, ctrl+shift+4, ctrl+shift+5, ctrl+shift+6, ctrl+shift+7, ctrl+shift+8, ctrl+shift+9 7 | Icon=\xf0cb 8 | IsGlobalShortcut=true 9 | Name=Select Nth Item 10 | -------------------------------------------------------------------------------- /Global/show-char-code.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Show Character Code 3 | Command=" 4 | copyq: 5 | var maxChars = 18; 6 | 7 | function padStart(str, len, c) { 8 | str = str ? str.toString() : ''; 9 | 10 | if (str.length >= len) { 11 | return str; 12 | } 13 | 14 | var rpt = c || ' '; 15 | for (var i = 1; i < len - str.length; i++) { 16 | rpt += c; 17 | } 18 | 19 | return rpt + str; 20 | } 21 | 22 | function getCodeInfo(code) { 23 | var chunks = [ 24 | 'U+' + padStart(code.toString(16).toUpperCase(), 4, '0'), 25 | '&#x' + code.toString(16) + ';', 26 | code.toString(16), 27 | '&#' + code + ';', 28 | code, 29 | ]; 30 | 31 | return chunks.join(' | '); 32 | } 33 | 34 | var toLabelMap = { 35 | '&' : '&&', // see https://doc.qt.io/qt-5/qlabel.html 36 | ' ' : ' ', 37 | '<' : '<', 38 | '>' : '>' 39 | }; 40 | 41 | function toLabel(c) { 42 | return '' + (toLabelMap[c] || c) + ' '; 43 | } 44 | 45 | // # main 46 | 47 | var text = str(input()) || str(clipboard()); 48 | 49 | do { 50 | var charData = []; 51 | 52 | for (var i = 0, max = Math.min(maxChars, text.length); i < max; i++) { 53 | var c = text[i]; 54 | charData.push(toLabel(c)); 55 | charData.push(getCodeInfo(c.charCodeAt(0))); 56 | } 57 | 58 | var options = dialog.apply(this, 59 | [ 60 | // Note: 'courier new' seems to be necessary on Windows 61 | // - see https://stackoverflow.com/questions/1468022/how-to-specify-monospace-fonts-for-cross-platform-qt-applications 62 | '.style', 'font-family: courier new, monospace', 63 | '.title', 'Show Character Code', 64 | '.label', 'Shows Unicode code info for the first ' + maxChars + ' characters of the given text.' 65 | + '
Info format: <unicodeNotation> | <xmlRefHex> | <hexCode> | <xmlRefDec> | <decCode>', 66 | 'Text', text 67 | ].concat(charData) 68 | ); 69 | 70 | if (options) { 71 | text = options['Text'] || options; 72 | } 73 | } while (options); 74 | " 75 | Input=text/plain 76 | InMenu=true 77 | IsGlobalShortcut=true 78 | Icon=\xf002 79 | -------------------------------------------------------------------------------- /Global/show-clipboard.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | // Shows notification with current clipboard content. 5 | var timeout = 8000 6 | 7 | function showImage(mime, suffix) { 8 | var image = clipboard(mime) 9 | if (image.size() == 0) 10 | return false 11 | 12 | var fileTemplate = Dir().temp().absoluteFilePath( 13 | 'copyq-XXXXXX.' + suffix) 14 | var file = new TemporaryFile() 15 | file.setFileTemplate(fileTemplate) 16 | file.openWriteOnly() 17 | file.write(image) 18 | file.close() 19 | 20 | var filePath = file.fileName() 21 | notification( 22 | '.icon', filePath, 23 | '.time', timeout 24 | ) 25 | sleep(timeout) 26 | return true 27 | } 28 | 29 | function showText() { 30 | var text = clipboard() 31 | notification( 32 | '.message', text, 33 | '.time', timeout 34 | ) 35 | sleep(timeout) 36 | return true 37 | } 38 | 39 | showImage('image/png', 'png') || 40 | showImage('image/bmp', 'bmp') || 41 | showText() 42 | " 43 | GlobalShortcut=ctrl+shift+q 44 | Icon=\xf27a 45 | IsGlobalShortcut=true 46 | Name=Show Clipboard 47 | -------------------------------------------------------------------------------- /Global/snippets.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var snippetsTabName = 'Snippets' 5 | // List snippets instead of search combo box? 6 | var listSnippets = false 7 | 8 | function newVarRe(content) { 9 | return new RegExp('\\\\${' + content + '}', 'g') 10 | } 11 | 12 | function getText(item, format) { 13 | return str(item[format] || '') 14 | } 15 | 16 | function assignPlaceholder(snippet, placeholder, value) { 17 | return snippet.replace(newVarRe(placeholder + ':?.*?'), value) 18 | } 19 | 20 | function fuzzyIndexOf(snippetNames, snippetName) { 21 | var re = new RegExp(snippetName, 'i') 22 | for (var i in snippetNames) { 23 | if (snippetNames[i].match(re)) 24 | return i; 25 | } 26 | return -1 27 | } 28 | 29 | function loadSnippets(snippetNames, snippets) 30 | { 31 | var tabs = tab() 32 | for (var i in tabs) { 33 | var tabName = tabs[i]; 34 | if (tabName != snippetsTabName && tabName.indexOf(snippetsTabName + '/') != 0) 35 | continue; 36 | 37 | tab(tabName) 38 | var prefix = tabName.substring(snippetsTabName.length + 1) 39 | if (prefix) 40 | prefix += ': ' 41 | for (var j = 0; j < size(); ++j) { 42 | var snippet = getitem(j) 43 | var snippetName = getText(snippet, mimeItemNotes) 44 | || getText(snippet, mimeText) 45 | || getText(snippet, mimeHtml) 46 | snippetNames.push(prefix + snippetName) 47 | snippets.push(snippet) 48 | } 49 | } 50 | } 51 | 52 | function askForSnippet(snippetNames, snippets) { 53 | var list = listSnippets ? '.list:' : '' 54 | 55 | var settingsPrefix = 'snippets/' 56 | 57 | var optSnippet = 'Snippet' 58 | var snippetName = settings(settingsPrefix + optSnippet) 59 | 60 | var snippet = dialog( 61 | '.title', 'Select Snippet', 62 | '.defaultChoice', snippetName, 63 | list + optSnippet, snippetNames 64 | ) 65 | 66 | if (snippet === undefined) { 67 | abort() 68 | } 69 | 70 | settings(settingsPrefix + optSnippet, listSnippets ? snippetNames[snippet] : snippet) 71 | 72 | if (listSnippets) 73 | return snippets[snippet] 74 | 75 | var i = snippetNames.indexOf(snippet) 76 | if (i != -1) 77 | return snippets[i] 78 | 79 | i = fuzzyIndexOf(snippetNames, snippet) 80 | if (i != -1) 81 | return snippets[i] 82 | 83 | popup( 84 | 'Snippet Not Found', 85 | 'No matching snippet found for \"' + snippetName + '\"!' 86 | ) 87 | abort() 88 | } 89 | 90 | function getPlaceholders(snippet) { 91 | var placeholders = {} 92 | var m 93 | var reVar = newVarRe('([^:}]*):?(.*?)') 94 | while ((m = reVar.exec(snippet)) !== null) { 95 | if (!(m[1] in placeholders)) 96 | placeholders[m[1]] = m[2].replace('\\\\n', '\\n') 97 | } 98 | 99 | return placeholders 100 | } 101 | 102 | function assignPlaceholders(text, values) { 103 | if (!(values instanceof Object)) { 104 | text = assignPlaceholder(text, '.*?', values) 105 | } else { 106 | for (var name in values) 107 | text = assignPlaceholder(text, name, values[name]) 108 | } 109 | 110 | return text 111 | } 112 | 113 | function askToAssignPlaceholders(snippet, format, values) { 114 | var text = getText(snippet, format) 115 | var placeholders = getPlaceholders(text) 116 | 117 | if (Object.keys(placeholders).length < 1) 118 | return 119 | 120 | if (values) { 121 | snippet[format] = assignPlaceholders(text, values) 122 | return values 123 | } 124 | 125 | var label = escapeHtml(text) 126 | .replace(newVarRe('([^:}]*).*?'), '$1') 127 | 128 | var dialogVars = [ 129 | '.title', 'Set Snippet Values', 130 | '.label', label 131 | ] 132 | 133 | for (var name in placeholders) { 134 | var values = placeholders[name].split(',') 135 | dialogVars.push(name) 136 | dialogVars.push((values.length == 1) ? values[0] : values) 137 | } 138 | 139 | var values = dialog.apply(this, dialogVars) || abort() 140 | snippet[format] = assignPlaceholders(text, values) 141 | return values 142 | } 143 | 144 | function pasteSnippet(mime, content) { 145 | copy(mime, content) 146 | copySelection(mime, content) 147 | paste() 148 | } 149 | 150 | var snippetNames = [] 151 | var snippets = [] 152 | loadSnippets(snippetNames, snippets) 153 | 154 | var snippet = askForSnippet(snippetNames, snippets) 155 | 156 | values = askToAssignPlaceholders(snippet, mimeText) 157 | askToAssignPlaceholders(snippet, mimeHtml, values) 158 | 159 | pasteSnippet(mimeItems, pack(snippet))" 160 | GlobalShortcut=meta+alt+q 161 | Icon=\xf1fb 162 | IsGlobalShortcut=true 163 | Name=Snippets 164 | -------------------------------------------------------------------------------- /Global/stopwatch.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | var now = Date.now(); 5 | var optionName = 'stopwatchLastTime'; 6 | 7 | function padWithZero(n) { 8 | if (n < 10) 9 | return '0' + n; 10 | return n; 11 | } 12 | 13 | function millisecondsToTimeString(milliseconds) { 14 | var seconds = Math.floor(milliseconds / 1000); 15 | var s = seconds % 60; 16 | var minutes = Math.floor(seconds / 60); 17 | var m = minutes % 60; 18 | var hours = Math.floor(minutes / 60); 19 | return padWithZero(hours) + ':' + padWithZero(m) + ':' + padWithZero(s); 20 | } 21 | 22 | var lastTime = Number(settings(optionName)); 23 | if (lastTime) { 24 | var time = millisecondsToTimeString(now - lastTime); 25 | popup('Elapsed time', time); 26 | copy(time); 27 | } 28 | 29 | settings(optionName, now);" 30 | GlobalShortcut=meta+return 31 | Icon=\xf2f2 32 | IsGlobalShortcut=true 33 | Name=Stopwatch 34 | -------------------------------------------------------------------------------- /Global/tabs-navigation.ini: -------------------------------------------------------------------------------- 1 | [Commands] 2 | 1\Name=Navigate to tab by hotkey with number 3 | 1\Command=" 4 | copyq: 5 | const tabs = tab(); 6 | const maxHotkeySize = 10; 7 | const hotkeyNumber = str(data(mimeShortcut)).slice(-1); 8 | //Get shift tab position 9 | const actualTabIndex = (hotkeyNumber - 1 + maxHotkeySize) % maxHotkeySize; 10 | 11 | if (tabs.length > actualTabIndex) { 12 | setCurrentTab(tabs[actualTabIndex]); 13 | }" 14 | 1\InMenu=true 15 | 1\IsGlobalShortcut=true 16 | 1\Icon=\xf2f2 17 | 1\GlobalShortcut=ctrl+alt+shift+1, ctrl+alt+shift+2, ctrl+alt+shift+3, ctrl+alt+shift+4, ctrl+alt+shift+5, ctrl+alt+shift+6, ctrl+alt+shift+7, ctrl+alt+shift+8, ctrl+alt+shift+9, ctrl+alt+shift+0 18 | 2\Name=Select next tab 19 | 2\Command=" 20 | copyq: 21 | const tabs = tab(); 22 | const currentTabIndex = tabs.indexOf(selectedTab()); 23 | const newTab = tabs[(currentTabIndex - 1 + tabs.length) % tabs.length]; 24 | 25 | setCurrentTab(newTab);" 26 | 2\InMenu=true 27 | 2\IsGlobalShortcut=true 28 | 2\Icon=\xf2f2 29 | 2\GlobalShortcut=ctrl+alt+shift+left 30 | 3\Name=Select previous tab 31 | 3\Command=" 32 | copyq: 33 | const tabs = tab(); 34 | const currentTabIndex = tabs.indexOf(selectedTab()); 35 | const newTab = tabs[(currentTabIndex + 1) % tabs.length]; 36 | 37 | setCurrentTab(newTab);" 38 | 3\InMenu=true 39 | 3\IsGlobalShortcut=true 40 | 3\Icon=\xf2f2 41 | 3\GlobalShortcut=ctrl+alt+shift+right 42 | size=3 43 | -------------------------------------------------------------------------------- /Global/to-title-case.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | if (!copy()) 5 | abort() 6 | 7 | // http://stackoverflow.com/a/6475125/454171 8 | String.prototype.toTitleCase = function() { 9 | var i, j, str, lowers, uppers; 10 | str = this.replace(/([^\\W_]+[^\\s-]*) */g, function(txt) { 11 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 12 | }); 13 | 14 | // Certain minor words should be left lowercase unless 15 | // they are the first or last words in the string 16 | lowers = ['A', 'An', 'The', 'And', 'But', 'Or', 'For', 'Nor', 'As', 'At', 17 | 'By', 'For', 'From', 'In', 'Into', 'Near', 'Of', 'On', 'Onto', 'To', 'With']; 18 | for (i = 0, j = lowers.length; i < j; i++) 19 | str = str.replace(new RegExp('\\\\s' + lowers[i] + '\\\\s', 'g'), 20 | function(txt) { 21 | return txt.toLowerCase(); 22 | }); 23 | 24 | // Certain words such as initialisms or acronyms should be left uppercase 25 | uppers = ['Id', 'Tv']; 26 | for (i = 0, j = uppers.length; i < j; i++) 27 | str = str.replace(new RegExp('\\\\b' + uppers[i] + '\\\\b', 'g'), 28 | uppers[i].toUpperCase()); 29 | 30 | return str; 31 | } 32 | 33 | var text = str(clipboard()) 34 | 35 | var newText = text.toTitleCase(); 36 | if (text == newText) 37 | abort(); 38 | 39 | copy(newText) 40 | paste()" 41 | GlobalShortcut=meta+alt+t 42 | Icon=\xf034 43 | Name=To Title Case 44 | -------------------------------------------------------------------------------- /Global/toggle-clipboard-storing.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | if (monitoring()) 5 | disable() 6 | else 7 | enable()" 8 | GlobalShortcut=meta+alt+x 9 | Icon=\xf070 10 | InMenu=true 11 | IsGlobalShortcut=true 12 | Name=Toggle Clipboard Storing 13 | -------------------------------------------------------------------------------- /Global/toggle-upper-lower-case-of-selected-text.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | if (!copy()) 5 | abort() 6 | 7 | var text = str(clipboard()) 8 | 9 | var newText = text.toUpperCase() 10 | if (text == newText) 11 | newText = text.toLowerCase() 12 | 13 | if (text == newText) 14 | abort(); 15 | 16 | copy(newText) 17 | paste()" 18 | GlobalShortcut=meta+ctrl+u 19 | Icon=\xf034 20 | Name=Toggle Upper/Lower Case 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Useful commands for [CopyQ clipboard manager](https://github.com/hluk/CopyQ). 2 | 3 | You can share your commands and ideas here. 4 | Just open pull request or an issue. 5 | 6 | # Categories 7 | 8 | - [Application](https://github.com/hluk/copyq-commands/tree/master/Application) - Commands which can be executed from tool bar, menu or with shortcut 9 | - [Automatic](https://github.com/hluk/copyq-commands/tree/master/Automatic) - Commands which are executed automatically whenever something is copied to clipboard 10 | - [Display](https://github.com/hluk/copyq-commands/tree/master/Display) - Scripts for changing appearance of items 11 | - [Global](https://github.com/hluk/copyq-commands/tree/master/Global) - Commands which can be executed with global/system shortcut 12 | - [Scripts](https://github.com/hluk/copyq-commands/tree/master/Scripts) - Scripts for changing application behavior, extending command line and adding functionality 13 | - [Templates](https://github.com/hluk/copyq-commands/tree/master/Templates) - Templates for new commands 14 | 15 | # Add a Command to CopyQ 16 | 17 | To add a command to CopyQ: 18 | 19 | - copy the command code (starts with `[Command]` or `[Commands]` for multiple commands), 20 | - open CopyQ, 21 | - open Command dialog (F6 shortcut), 22 | - click "Paste Commands" button (or Ctrl+V), 23 | - apply changes. 24 | 25 | To **simplify this** add [command](Automatic/import-commands-after-copied.ini) 26 | which shows notification with button to import all commands copied to clipboard. 27 | This also works if you just copy a link with commands. 28 | 29 | ![Select Category](images/select-category.png) 30 | ![Copy Command Link](images/copy-command-link.png) 31 | ![Import Command Notification](images/import-command-notification.png) 32 | 33 | # Write new Commands 34 | 35 | See following documentation about writing commands and scripting. 36 | 37 | - [Writing Commands and Adding Functionality](https://copyq.readthedocs.io/en/latest/writing-commands-and-adding-functionality.html) 38 | - [Scripting](https://copyq.readthedocs.io/en/latest/scripting.html) 39 | - [Scripting API](https://copyq.readthedocs.io/en/latest/scripting-api.html) 40 | 41 | Submit new pull request in this repository if you want to share a command. 42 | 43 | -------------------------------------------------------------------------------- /Scripts/README.md: -------------------------------------------------------------------------------- 1 | This section contains commands which modify or extend default application behavior. 2 | 3 | ### [Backup On Exit](backup-on-exit.ini) 4 | 5 | Backs up items and configuration on exit. 6 | 7 | ### [Blocklisted Texts](blocklisted_texts.ini) 8 | 9 | Blocklists clipboard text to omit adding it in item list and avoid running 10 | automatic commands on it. 11 | 12 | Only checksum of the text (salted) is stored in the blocklist so this can be 13 | safely used with passwords (the texts are not stored anywhere). 14 | 15 | ### [Bookmarks](bookmarks.ini) 16 | 17 | Allows you to set a mark on an item, then later restore that mark to the clipboard. 18 | 19 | The implementation uses special tags with a "mark:" prefix, and when a mark is set, removes that tag from any items that contain that tag. 20 | 21 | ### [Clear Clipboard After Interval](clear-clipboard-after-interval.ini) 22 | 23 | Clears clipboard after an interval (30 seconds by default). 24 | 25 | ### [Clipboard Notification](clipboard-notification.ini) 26 | 27 | Persistently displays notification with clipboard (and X11 selection) content. 28 | 29 | ### [Full Clipboard Text in Title and Tooltip](full-clipboard-in-title.ini) 30 | 31 | Shows full clipboard text in window title and tray tooltip. 32 | 33 | ### [Ignore Non-Mouse Text Selection](ignore-non-mouse-text-selection.ini) 34 | 35 | Linux/X11 only. Some web or other applications can automatically set X11 mouse 36 | selection buffer. This can be quiet annoying so this command tries to reset the 37 | buffer to previous content when this happens. 38 | 39 | ### [Indicate Copy in Icon](indicate-copy-in-icon.ini) 40 | 41 | Indicates a copy operation by changing the icon tag. 42 | 43 | ### [Keep Item in Clipboard](keep-item-in-clipboard.ini) 44 | 45 | Keeps the first item (can be pinned) in clipboard at start and after a copy 46 | operation (after custom interval). 47 | 48 | ### [Make a selected tab a clipboard tab and put the first item on the clipboard](make-selected-tab-clipboard.ini) 49 | 50 | Make the selected tab a clipboard tab and put the first item on the clipboard. 51 | 52 | ### [No Clipboard in Title and Tool Tip](no-clipboard-in-title-and-tooltip.ini) 53 | 54 | Stop showing current clipboard content in window title and tray tool tip. 55 | 56 | ### [Remember Clipboard Storing State](remeber-clipboard-storing-state.ini) 57 | 58 | Normally, if "Clipboard Storing" is disabled from File menu, it will be 59 | re-enabled automatically on the application start next time. 60 | 61 | This command makes the last set state persistent between application launches. 62 | 63 | ### [Reset Empty Clipboard/Selection](reset-empty-clipboard.ini) 64 | 65 | Resets last clipboard text (or X11 selection) if it's cleared. 66 | 67 | ### [Show on Start](show-on-start.ini) 68 | 69 | Show main window after application starts. 70 | 71 | ### [Top Item to Clipboard](top-item-to-clipboard.ini) 72 | 73 | Whenever a new top item is added to the clipboard tab or is changed, it is also 74 | copied to the system clipboard. 75 | 76 | ### [Wayland Support](wayland-support.ini) 77 | 78 | Adds support for some features under Wayland compositors in KDE, Sway, Hyprland 79 | and possibly others. 80 | 81 | Command "Paste Items when Activated" pastes items when activated (on 82 | double-click or Enter key) depending on application configuration (History 83 | configuration tab). 84 | 85 | Paste behaviour is implemented with Shift+Insert shortcut. It works in most 86 | applications by default, but you may need to enable it for some (for example, 87 | for terminal emulators). 88 | Exact configuration changes vary by application. For example, for alacritty 89 | you should modify your `alacritty.yml` with next line: 90 | ```yaml 91 | - { key: Insert, mods: Shift, action: Paste } 92 | ``` 93 | 94 | Getting window title is currently implemented only for KDE, Sway and Hyprland. 95 | 96 | Requirements: 97 | 98 | - [kdotool](https://github.com/jinliu/kdotool) for getting window titles on KDE 99 | - [ydotool](https://github.com/ReimuNotMoe/ydotool) for copy/paste commands 100 | - [gnome-screenshot](https://gitlab.gnome.org/GNOME/gnome-screenshot) for 101 | taking screenshots in Gnome 102 | - [grim](https://github.com/emersion/grim) and 103 | [slurp](https://github.com/emersion/slurp) for taking screenshots in Sway and 104 | Hyprland 105 | - [spectacle](https://invent.kde.org/graphics/spectacle) for screenshots in 106 | other environments 107 | 108 | ### [Write Clipboard to File](write-clipboard-to-file.ini) 109 | 110 | Stores clipboard continuously to a "clipboard.txt" (in home directory). 111 | -------------------------------------------------------------------------------- /Scripts/backup-on-exit.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | var backupPath = 'Documents/copyq-backups' 4 | var backupsToKeep = 15 5 | 6 | function backupDirectory() { 7 | var dir = Dir().home() 8 | 9 | dir.mkpath(backupPath) 10 | if ( !dir.cd(backupPath) ) 11 | throw 'Error: Failed to create backup directory.' 12 | 13 | return dir 14 | } 15 | 16 | function removeOldBackups() { 17 | var dir = backupDirectory() 18 | var backups = dir 19 | .entryList(['*.cpq']) 20 | .sort() 21 | .slice(0, -backupsToKeep) 22 | 23 | for (var i in backups) { 24 | var path = dir.absoluteFilePath(backups[i]) 25 | var file = File(path) 26 | file.remove() 27 | } 28 | } 29 | 30 | function createBackup() { 31 | var dir = backupDirectory() 32 | var fileName = dir.absoluteFilePath( 33 | dateString('yyyy-MM-dd-hh-mm-ss.cpq')) 34 | exportData(fileName) 35 | } 36 | 37 | global.backup = function() { 38 | createBackup() 39 | removeOldBackups() 40 | } 41 | 42 | var onExitPrevious = global.onExit 43 | global.onExit = function() { 44 | onExitPrevious() 45 | global.backup() 46 | }" 47 | Icon=\xf0a0 48 | IsScript=true 49 | Name=Backup On Exit 50 | -------------------------------------------------------------------------------- /Scripts/blocklisted_texts.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | /* 4 | Shows notification to blocklist copied text. 5 | 6 | Blocklisted text will be removed from clipboard tab and ignored 7 | (unless allowlisted again). 8 | */ 9 | var blocklistConfigKey = 'blocklisted-hashes' 10 | var blocklistNotificationId = 'blocklist-notification' 11 | var notificationTimeoutSeconds = 4 12 | 13 | function blocklistedHashes() { 14 | return settings(blocklistConfigKey) || [] 15 | } 16 | 17 | function setBlocklistedHashes(hashes) { 18 | return settings(blocklistConfigKey, hashes) 19 | } 20 | 21 | function removeItemHash(hash) { 22 | for (var i = 2; i >= 0; --i) { 23 | var text = read(mimeText, i) 24 | if ( hash == calculateTextHash(text) ) 25 | remove(i) 26 | } 27 | } 28 | 29 | function notifyClipboardBlocklisted(hash) { 30 | notification( 31 | '.id', blocklistNotificationId, 32 | '.time', notificationTimeoutSeconds * 1000, 33 | '.title', 'Clipboard Blocklisted', 34 | '.button', 'Allowlist', 'copyq allowlistHash ' + hash 35 | ) 36 | } 37 | 38 | function notifyClipboardToBlocklist(hash) { 39 | if ( !isClipboard() ) 40 | return; 41 | 42 | notification( 43 | '.id', blocklistNotificationId, 44 | '.time', notificationTimeoutSeconds * 1000, 45 | '.title', 'Blocklist?', 46 | '.button', 'Blocklist', 'copyq blocklistHash ' + hash 47 | ) 48 | } 49 | 50 | global.isHashBlocklisted = function(hash) { 51 | return blocklistedHashes().indexOf(hash) !== -1 52 | } 53 | 54 | global.blocklistHash = function(hash) { 55 | hash = str(hash) 56 | var hashes = blocklistedHashes() 57 | if ( hashes.indexOf(hash) !== -1 ) 58 | return; 59 | 60 | hashes.push(hash) 61 | setBlocklistedHashes(hashes) 62 | removeItemHash(hash) 63 | setTitle() 64 | } 65 | 66 | global.allowlistHash = function(hash) { 67 | hash = str(hash) 68 | var hashes = blocklistedHashes() 69 | var i = hashes.indexOf(hash) 70 | if (i === -1) 71 | return; 72 | 73 | hashes.splice(i, 1) 74 | setBlocklistedHashes(hashes) 75 | } 76 | 77 | global.calculateTextHash = function(text) { 78 | var salt = 'This is just some random salt prefix.' 79 | var saltedText = salt + str(text) 80 | return sha256sum(saltedText) 81 | } 82 | 83 | var onClipboardChanged_ = global.onClipboardChanged 84 | global.onClipboardChanged = function() { 85 | var hash = calculateTextHash(data(mimeText)) 86 | if ( isHashBlocklisted(hash) ) { 87 | notifyClipboardBlocklisted(hash) 88 | } else { 89 | onClipboardChanged_() 90 | notifyClipboardToBlocklist(hash) 91 | } 92 | } 93 | 94 | var onOwnClipboardChanged_ = global.onOwnClipboardChanged 95 | global.onOwnClipboardChanged = function() { 96 | var hash = calculateTextHash(data(mimeText)) 97 | if ( !isHashBlocklisted(hash) ) { 98 | onOwnClipboardChanged_() 99 | } 100 | }" 101 | Icon=\xf05e 102 | Input=text/plain 103 | IsScript=true 104 | Name=Blocklisted Texts 105 | Remove=true 106 | -------------------------------------------------------------------------------- /Scripts/bookmarks.ini: -------------------------------------------------------------------------------- 1 | [Commands] 2 | 1\Command=" 3 | const prefix = 'mark:'; 4 | global.setMark = function(name) { 5 | if (!name) { 6 | name = dialog('.title', 'Assign to register', 'Name', ''); 7 | } 8 | const tag = prefix + name; 9 | // Filter to only items that have a mark tag 10 | const sel = ItemSelection().select(/mark:/, plugins.itemtags.mimeTags); 11 | plugins.itemtags.untag(tag, ...sel.rows()); 12 | 13 | // TODO: allow selecting multiple items? 14 | plugins.itemtags.tag(tag, currentItem()); 15 | }; 16 | 17 | global.listMarks = function() { 18 | const s = new Set(); 19 | // Support multiple items? 20 | for (let i = 0; i < size(); i++) { 21 | for (const tag of plugins.itemtags.tags(i)) { 22 | if (tag.startsWith(prefix)) { 23 | s.add(tag.slice(prefix.length)); 24 | } 25 | } 26 | } 27 | return Array.from(s); 28 | }; 29 | 30 | global.copyMark = function(name) { 31 | if (!name) { 32 | name = menuItems.apply(global, listMarks()); 33 | } 34 | const tag = prefix + name; 35 | const len = size(); 36 | for (let i = 0; i < len; i++) { 37 | if (plugins.itemtags.hasTag(tag, i)) { 38 | select(i); 39 | return; 40 | } 41 | } 42 | }" 43 | 1\Icon=\xe0bb 44 | 1\IsScript=true 45 | 1\Name=Bookmarks 46 | 2\Command=" 47 | copyq setMark" 48 | 2\Icon=\xf15b 49 | 2\InMenu=true 50 | 2\Name=set mark 51 | 3\Command=" 52 | copyq copyMark" 53 | 3\Icon=\xf15b 54 | 3\InMenu=true 55 | 3\IsGlobalShortcut=true 56 | 3\Name=Restore mark 57 | size=3 58 | -------------------------------------------------------------------------------- /Scripts/clear-clipboard-after-interval.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | var timeoutSeconds = 30; 4 | 5 | function option() { 6 | return isClipboard() 7 | ? 'clear_clipboard/clipboard_change_counter' 8 | : 'clear_clipboard/selection_change_counter' 9 | } 10 | 11 | function getCount() { 12 | return Number(settings(option())) || 0 13 | } 14 | 15 | function bumpCounter() { 16 | var counter = getCount() + 1 17 | settings(option(), counter) 18 | return counter 19 | } 20 | 21 | function resetLater(counter) { 22 | for (var i = 0; i < timeoutSeconds && counter == getCount(); ++i) { 23 | sleep(1000) 24 | } 25 | 26 | if (counter != getCount()) 27 | return 28 | 29 | if (isClipboard()) 30 | copy('') 31 | else 32 | copySelection('') 33 | } 34 | 35 | var onClipboardChanged_ = onClipboardChanged 36 | onClipboardChanged = function() { 37 | var counter = bumpCounter() 38 | onClipboardChanged_() 39 | resetLater(counter) 40 | } 41 | 42 | var onOwnClipboardChanged_ = onOwnClipboardChanged 43 | onOwnClipboardChanged = function() { 44 | var counter = bumpCounter() 45 | onOwnClipboardChanged_() 46 | resetLater(counter) 47 | }" 48 | Icon=\xf2f2 49 | IsScript=true 50 | Name=Clear Clipboard After Interval 51 | -------------------------------------------------------------------------------- /Scripts/clipboard-notification.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | const notificationTimeout = config('item_popup_interval') * 1000 4 | 5 | function clipboardNotification(owns, hidden) { 6 | var id = isClipboard() ? 'clipboard' : 'selection' 7 | var icon = isClipboard() ? '\\uf0ea' : '\\uf246' 8 | var owner = owns ? 'CopyQ' : str(data(mimeWindowTitle)) 9 | var title = id + ' - ' + owner 10 | var message = hidden ? '' : data(mimeText).left(100) 11 | notification( 12 | '.id', id, 13 | '.title', title, 14 | '.message', message, 15 | '.icon', icon, 16 | '.time', notificationTimeout, 17 | ) 18 | } 19 | 20 | var onClipboardChanged_ = onClipboardChanged 21 | onClipboardChanged = function() { 22 | clipboardNotification(false, false) 23 | onClipboardChanged_() 24 | } 25 | 26 | var onOwnClipboardChanged_ = onOwnClipboardChanged 27 | onOwnClipboardChanged = function() { 28 | clipboardNotification(true, false) 29 | onOwnClipboardChanged_() 30 | } 31 | 32 | var onHiddenClipboardChanged_ = onHiddenClipboardChanged 33 | onHiddenClipboardChanged = function() { 34 | clipboardNotification(true, true) 35 | onHiddenClipboardChanged_() 36 | }" 37 | Icon=\xf075 38 | IsScript=true 39 | Name=Clipboard Notification 40 | 41 | -------------------------------------------------------------------------------- /Scripts/full-clipboard-in-title.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | global.updateTitle = function() { 4 | var text = str(data(mimeText)) 5 | setTitle(text) 6 | } 7 | " 8 | Icon=\xf2d0 9 | IsScript=true 10 | Name=Full Clipboard Text in Title and Tooltip 11 | -------------------------------------------------------------------------------- /Scripts/ignore-non-mouse-text-selection.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | var onClipboardChanged_ = onClipboardChanged 4 | global.onClipboardChanged = function() { 5 | var positionKey = 'lastPointerPosition' 6 | var selectionKey = 'lastPointerSelection' 7 | var selectionTimeKey = 'lastPointerSelectionTime' 8 | var position = str(pointerPosition()) 9 | if (isClipboard() 10 | || position != settings(positionKey) 11 | || hasSelectionFormat('_VIM_TEXT') 12 | || Date.now() - settings(selectionTimeKey) < 1000) 13 | { 14 | settings(positionKey, position) 15 | settings(selectionTimeKey, Date.now()) 16 | if (!isClipboard()) 17 | settings(selectionKey, data(mimeText)) 18 | onClipboardChanged_() 19 | } else { 20 | serverLog('Ignoring non-mouse text selection') 21 | copySelection(settings(selectionKey)) 22 | } 23 | }" 24 | Icon=\xf245 25 | IsScript=true 26 | Name=Ignore Non-Mouse Text Selection 27 | -------------------------------------------------------------------------------- /Scripts/indicate-copy-in-icon.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | var timeMs = 300; 4 | var iconTags = [ 5 | '\xf111', 6 | ' \xf111', 7 | ' \xf111', 8 | '', 9 | ] 10 | 11 | function clipboardNotification() { 12 | var id = Number(settings('icon-activation-id') || 0) + 1; 13 | settings('icon-activation-id', id); 14 | 15 | iconTagColor('red'); 16 | for (const tag of iconTags.values()) { 17 | if ( settings('icon-activation-id') != id ) 18 | break; 19 | iconTag(tag); 20 | sleep(timeMs); 21 | } 22 | } 23 | 24 | onClipboardChanged_ = onClipboardChanged 25 | onClipboardChanged = function() { 26 | onClipboardChanged_() 27 | clipboardNotification() 28 | } 29 | 30 | onOwnClipboardChanged_ = onOwnClipboardChanged 31 | onOwnClipboardChanged = function() { 32 | onOwnClipboardChanged_() 33 | clipboardNotification() 34 | } 35 | 36 | onHiddenClipboardChanged_ = onHiddenClipboardChanged 37 | onHiddenClipboardChanged = function() { 38 | onHiddenClipboardChanged_() 39 | clipboardNotification() 40 | }" 41 | Icon=\xf0c4 42 | IsScript=true 43 | Name=Indicate Copy in Icon 44 | -------------------------------------------------------------------------------- /Scripts/keep-item-in-clipboard.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | // Keeps the first item (can be pinned) in clipboard at start 4 | // and after a copy operation (after custom interval). 5 | var copyIntervalInSeconds = 5 6 | 7 | function copyBookmarkAfterInterval() { 8 | var oldClipboard = str(data(mimeText)) 9 | var wasClipboard = isClipboard() 10 | var toCopy = str(read(0)) 11 | 12 | if (oldClipboard == toCopy) 13 | return 14 | 15 | function copyFunction() { 16 | var currentClipboard = str(wasClipboard ? clipboard() : selection()) 17 | if (oldClipboard == currentClipboard) 18 | wasClipboard ? copy(toCopy) : copySelection(toCopy) 19 | } 20 | afterMilliseconds(copyIntervalInSeconds * 1000, copyFunction) 21 | } 22 | 23 | function overrideClipboardFunction(functionName) { 24 | var originalFunction = global[functionName] 25 | global[functionName] = function() { 26 | copyBookmarkAfterInterval() 27 | originalFunction() 28 | } 29 | } 30 | 31 | var monitorClipboard_ = monitorClipboard 32 | monitorClipboard = function() { 33 | copy(read(0)) 34 | 35 | overrideClipboardFunction('onClipboardChanged') 36 | overrideClipboardFunction('onOwnClipboardChanged') 37 | overrideClipboardFunction('onHiddenClipboardChanged') 38 | 39 | monitorClipboard_() 40 | }" 41 | Icon=\xf02e 42 | IsScript=true 43 | Name=Keep Item in Clipboard 44 | -------------------------------------------------------------------------------- /Scripts/make-selected-tab-clipboard.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Name=Make a selected tab a clipboard tab and put the first item to the clipboard 3 | Command=" 4 | const onTabSelected_ = global.onTabSelected; 5 | global.onTabSelected = function () { 6 | settings('clipboard_tab', selectedTab()); 7 | select(0); 8 | return onTabSelected_(); 9 | }" 10 | Separator=" " 11 | IsScript=true 12 | Icon=\xf070 -------------------------------------------------------------------------------- /Scripts/no-clipboard-in-title-and-tooltip.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | updateTitle = function() {}" 4 | Icon=\xf070 5 | IsScript=true 6 | Name=No Clipboard in Title and Tool Tip 7 | -------------------------------------------------------------------------------- /Scripts/remeber-clipboard-storing-state.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | const option = 'disable_monitoring_at_start'; 4 | 5 | const onStart_ = global.onStart; 6 | global.onStart = function() { 7 | var disabled = str(settings(option)) === 'true'; 8 | if (disabled) 9 | disable(); 10 | onStart_(); 11 | } 12 | 13 | const onExit_ = global.onExit; 14 | global.onExit = function() { 15 | onExit_(); 16 | settings(option, !monitoring()) 17 | }" 18 | Icon=\xf05e 19 | IsScript=true 20 | Name=Remeber Clipboard Storing State 21 | -------------------------------------------------------------------------------- /Scripts/reset-empty-clipboard.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | const timeoutMilliseconds = 500; 4 | 5 | function reset(key, getFn, setFn) { 6 | if (hasData()) { 7 | settings(key, [data(mimeText), str(Date.now())]); 8 | return false; 9 | } 10 | 11 | const last = settings(key); 12 | if (!last) { 13 | return false; 14 | } 15 | 16 | afterMilliseconds(timeoutMilliseconds, function() { 17 | if (!str(getFn()) && last[1] == settings(key)[1]) { 18 | serverLog('Reset from ' + key); 19 | setFn(mimeText, last[0], mimeHidden, 1); 20 | } 21 | }); 22 | return true; 23 | } 24 | 25 | function resetClipboard() { 26 | return reset('lastClipboard', clipboard, copy); 27 | } 28 | 29 | function resetSelection() { 30 | return reset('lastSelection', selection, copySelection); 31 | } 32 | 33 | let onClipboardChanged_ = onClipboardChanged; 34 | onClipboardChanged = function() { 35 | onClipboardChanged_(); 36 | const wait = isClipboard() ? resetClipboard() : resetSelection(); 37 | if (wait) { 38 | sleep(timeoutMilliseconds + 1000); 39 | } 40 | }" 41 | Icon=\xf246 42 | IsScript=true 43 | Name=Reset Empty Clipboard/Selection 44 | -------------------------------------------------------------------------------- /Scripts/show-on-start.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | var onStartPrevious = global.onStart 4 | global.onStart = function() { 5 | onStartPrevious() 6 | show() 7 | }" 8 | Icon=\xf2d0 9 | IsScript=true 10 | Name=Show On Start 11 | -------------------------------------------------------------------------------- /Scripts/top-item-to-clipboard.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | var onItemsAdded_ = onItemsAdded; 4 | onItemsAdded = function() { 5 | onItemsAdded_(); 6 | syncClipboard(); 7 | } 8 | 9 | var onItemsChanged_ = onItemsChanged; 10 | onItemsChanged = function() { 11 | onItemsChanged_(); 12 | syncClipboard(); 13 | } 14 | 15 | function syncClipboard() { 16 | if (selectedTab() != config('clipboard_tab')) 17 | return; 18 | 19 | var sel = ItemSelection().current(); 20 | const i = sel.rows().indexOf(0); 21 | if (i == -1) 22 | return; 23 | 24 | const item = sel.itemAtIndex(i); 25 | copy(mimeItems, pack(item)); 26 | }" 27 | Icon= 28 | IsScript=true 29 | Name=Top Item to Clipboard 30 | -------------------------------------------------------------------------------- /Scripts/wayland-support.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | /* 4 | This adds support for KDE, Gnome, Sway and Hyprland Wayland sessions. 5 | 6 | For Sway and Hyprland, this requires: 7 | - `kdotool` to get window title in KDE 8 | - `ydotool` utility to send copy/paste shortcuts to applications 9 | - `gnome-screenshot` for taking screenshots in Gnome 10 | - `grim` for taking screenshot in Sway and Hyprland 11 | - `spectacle` for taking screenshots in non-Gnome environments 12 | - `slurp` for selecting screenshot area 13 | 14 | For KDE, this requires Spectacle for taking screenshots. 15 | 16 | Global shortcut commands can be triggered with: 17 | 18 | copyq triggerGlobalShortcut {COMMAND_NAME} 19 | 20 | On Gnome, clipboard monitor is executed as X11 app using XWayland. 21 | */ 22 | 23 | function isSway() { 24 | return env('SWAYSOCK').length != 0 25 | } 26 | 27 | function isHyprland() { 28 | return env('HYPRLAND_CMD').length != 0 29 | } 30 | 31 | function isKde() { 32 | return env('XDG_CURRENT_DESKTOP') == 'KDE' 33 | } 34 | 35 | function isGnome() { 36 | return str(env('XAUTHORITY')).includes('mutter-Xwayland') 37 | } 38 | 39 | function run() { 40 | const p = execute.apply(this, arguments) 41 | if (!p) { 42 | throw 'Failed to start ' + arguments[0] 43 | } 44 | if (p.exit_code !== 0) { 45 | throw 'Failed command ' + arguments[0] + ': ' + str(p.stderr) 46 | } 47 | return p.stdout 48 | } 49 | 50 | function swayGetTree() { 51 | const tree = run('swaymsg', '--raw', '--type', 'get_tree') 52 | return JSON.parse(str(tree)) 53 | } 54 | 55 | function swayFindFocused(tree) { 56 | const nodes = tree['nodes'].concat(tree['floating_nodes']) 57 | for (const node of nodes) { 58 | if (node['focused']) 59 | return node 60 | const focusedNode = swayFindFocused(node) 61 | if (focusedNode) 62 | return focusedNode 63 | } 64 | return undefined 65 | } 66 | 67 | function hyprlandFindFocused() { 68 | const window = run('hyprctl', '-j', 'activewindow') 69 | return JSON.parse(str(window)) 70 | } 71 | 72 | function kdeFocused() { 73 | return str(run('kdotool', 'getactivewindow', 'getwindowname')) 74 | } 75 | 76 | function sendShortcut(...shortcut) { 77 | sleep(100) 78 | run('ydotool', 'key', ...shortcut) 79 | } 80 | 81 | global.currentWindowTitle = function() { 82 | if (isSway()) { 83 | const tree = swayGetTree() 84 | const focusedNode = swayFindFocused(tree) 85 | return focusedNode ? focusedNode['name'] : '' 86 | } 87 | 88 | if (isHyprland()) { 89 | const focusedWindow = hyprlandFindFocused() 90 | return focusedWindow ? focusedWindow['title'] : '' 91 | } 92 | 93 | if (isKde()) { 94 | return kdeFocused() 95 | } 96 | 97 | return '' 98 | } 99 | 100 | global.paste = function() { 101 | sendShortcut('42:1', '110:1', '110:0', '42:0') 102 | } 103 | 104 | const copy_ = global.copy 105 | global.copy = function() { 106 | if (arguments.length == 0) { 107 | sendShortcut('29:1', '46:1', '46:0', '29:0') 108 | } else { 109 | copy_.apply(this, arguments) 110 | } 111 | } 112 | 113 | global.focusPrevious = function() { 114 | hide() 115 | } 116 | 117 | function overrideToRunInXWayland(fn) { 118 | const oldFn = global[fn] 119 | global[fn] = function() { 120 | if (isGnome() && env('QT_QPA_PLATFORM') != 'xcb') { 121 | serverLog(`Starting XWayland ${fn}`) 122 | setEnv('QT_QPA_PLATFORM', 'xcb') 123 | execute('copyq', '--clipboard-access', fn) 124 | serverLog(`Stopping XWayland ${fn}`) 125 | return 126 | } 127 | return oldFn() 128 | } 129 | } 130 | overrideToRunInXWayland('monitorClipboard') 131 | overrideToRunInXWayland('synchronizeFromSelection') 132 | overrideToRunInXWayland('synchronizeToSelection') 133 | 134 | const onClipboardChanged_ = onClipboardChanged 135 | onClipboardChanged = function() { 136 | const title = currentWindowTitle() 137 | if (title) 138 | setData(mimeWindowTitle, title) 139 | onClipboardChanged_() 140 | } 141 | 142 | function gnomeScreenshot(arg) { 143 | const tmpFile = TemporaryFile() 144 | tmpFile.setFileTemplate(Dir().temp().absoluteFilePath('copyq-XXXXXX.png')) 145 | tmpFile.openWriteOnly() 146 | tmpFile.close() 147 | run( 148 | 'gnome-screenshot', 149 | arg || '--delay=0', 150 | '--include-pointer', 151 | '--file', 152 | tmpFile.fileName(), 153 | ) 154 | const file = File(tmpFile.fileName()) 155 | file.openReadOnly() 156 | const stdout = File('/dev/stdout') 157 | stdout.openWriteOnly() 158 | stdout.write(file.readAll()) 159 | } 160 | 161 | screenshot = function(format, screenName) { 162 | if (isSway() || isHyprland()) 163 | return run('grim', '-t', format || 'png', '-') 164 | if (isGnome()) 165 | return gnomeScreenshot() 166 | return run( 167 | 'spectacle', 168 | '--background', 169 | '--nonotify', 170 | '--pointer', 171 | '--output', 172 | '/dev/stdout', 173 | ) 174 | } 175 | 176 | screenshotSelect = function(format, screenName) { 177 | if (isSway() || isHyprland()) { 178 | let geometry = run('slurp') 179 | geometry = str(geometry).trim() 180 | return run('grim', '-c', '-g', geometry, '-t', format || 'png', '-') 181 | } 182 | if (isGnome()) 183 | return gnomeScreenshot('--area') 184 | return run( 185 | 'spectacle', 186 | '--background', 187 | '--nonotify', 188 | '--pointer', 189 | '--region', 190 | '--output', 191 | '/dev/stdout', 192 | ) 193 | } 194 | 195 | global.triggerGlobalShortcut = function(commandName) { 196 | const cmds = commands() 197 | for (const cmd of cmds) { 198 | if (cmd.isGlobalShortcut && cmd.enable && cmd.name == commandName) 199 | return action(cmd.cmd) 200 | } 201 | throw 'Failed to find enabled global command with given name' 202 | }" 203 | Icon= 204 | IsScript=true 205 | Name=Wayland Support 206 | -------------------------------------------------------------------------------- /Scripts/write-clipboard-to-file.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | var filePath = Dir().homePath() + '/clipboard.txt' 4 | var itemSeparator = '\\n\\n\\n\\n' 5 | 6 | function storeClipboard() { 7 | var text = data(mimeText) 8 | if (text.size() == 0) { 9 | return; 10 | } 11 | 12 | var f = File(filePath) 13 | if ( !f.openAppend() ) { 14 | popup( 15 | 'Failed saving clipboard', 16 | 'Cannot open file: ' + filePath 17 | + '\\n' + f.errorString()) 18 | return; 19 | } 20 | 21 | if ( (f.size() != 0 && f.write(itemSeparator) == 0) || f.write(text) != text.size() ) { 22 | popup( 23 | 'Failed saving clipboard', 24 | 'Cannot write to file: ' + filePath 25 | + '\\n' + f.errorString()) 26 | return; 27 | } 28 | 29 | f.close() 30 | } 31 | 32 | var onClipboardChanged_ = global.onClipboardChanged 33 | global.onClipboardChanged = function() { 34 | onClipboardChanged_() 35 | storeClipboard() 36 | }" 37 | Icon=\xf56d 38 | IsScript=true 39 | Name=Write Clipboard to File 40 | -------------------------------------------------------------------------------- /Templates/README.md: -------------------------------------------------------------------------------- 1 | This section contains templates for new commands. 2 | 3 | ### [Modify Selected Items](modify-selected-items.ini) 4 | 5 | ### [Modify Selected Text](modify-selected-text.ini) 6 | 7 | -------------------------------------------------------------------------------- /Templates/modify-selected-items.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | function modifySelectedItemData(itemData) 5 | { 6 | // TODO: Modify item data here! 7 | if (mimeText in itemData) { 8 | itemData[mimeText] = str(itemData[mimeText]).replace('\\n', ';'); 9 | delete itemData[mimeHtml] 10 | } 11 | } 12 | 13 | var itemsData = selectedItemsData() 14 | for (var i in itemsData) { 15 | var itemData = itemsData[i] 16 | modifySelectedItemData(itemsData[i]) 17 | } 18 | setSelectedItemsData(itemsData)" 19 | Icon=\xf016 20 | InMenu=true 21 | Name=Template: Modify Selected Items 22 | Shortcut=ctrl+shift+m 23 | 24 | -------------------------------------------------------------------------------- /Templates/modify-selected-text.ini: -------------------------------------------------------------------------------- 1 | [Command] 2 | Command=" 3 | copyq: 4 | function modifyText(text) 5 | { 6 | // TODO: Modify text here! 7 | return text.replace(/\\n/g, ';') 8 | } 9 | 10 | copy() 11 | 12 | var text = str(clipboard()) 13 | var newText = modifyText(text) 14 | if (text == newText) 15 | abort() 16 | 17 | copy(newText) 18 | paste()" 19 | GlobalShortcut=ctrl+shift+s 20 | Icon=\xf15b 21 | IsGlobalShortcut=true 22 | Name=Template: Modify Selected Text 23 | -------------------------------------------------------------------------------- /images/copy-command-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hluk/copyq-commands/544c88c5e5c121e2bd9a650140ac9fe4e024208a/images/copy-command-link.png -------------------------------------------------------------------------------- /images/import-command-notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hluk/copyq-commands/544c88c5e5c121e2bd9a650140ac9fe4e024208a/images/import-command-notification.png -------------------------------------------------------------------------------- /images/select-category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hluk/copyq-commands/544c88c5e5c121e2bd9a650140ac9fe4e024208a/images/select-category.png -------------------------------------------------------------------------------- /tests/Global/snippets.js: -------------------------------------------------------------------------------- 1 | tab('Snippets') 2 | add('Snippet 1') 3 | 4 | keys('Ctrl+F1', 'focus:ComboBox', 'ENTER') 5 | 6 | test.clipboardTextEquals('Snippet 1') 7 | -------------------------------------------------------------------------------- /tests/Scripts/reset-empty-clipboard.js: -------------------------------------------------------------------------------- 1 | function lastClipboard() { 2 | const v = settings('lastClipboard'); 3 | if (v) { 4 | return v[0]; 5 | } 6 | return undefined; 7 | } 8 | test.assertEquals(undefined, lastClipboard()) 9 | 10 | const testText = ByteArray('Test'); 11 | test.copy(testText) 12 | test.clipboardTextEquals(testText, 'clipboard after copy') 13 | test.waitForEquals(testText, lastClipboard, 'lastClipboard settings set') 14 | 15 | test.copy(ByteArray()) 16 | test.waitForEquals(testText, lastClipboard, 'lastClipboard settings unchanged') 17 | test.clipboardTextEquals(testText, 'clipboard after reset') 18 | -------------------------------------------------------------------------------- /tests/Scripts/show-on-start.js: -------------------------------------------------------------------------------- 1 | // tests: noshow restart 2 | test.waitForEquals(true, visible, 'window state') 3 | -------------------------------------------------------------------------------- /tests/session.js: -------------------------------------------------------------------------------- 1 | testSession = { 2 | sessions: 0, 3 | 4 | session: function() { 5 | return str(env('COPYQ_SESSION')) 6 | }, 7 | 8 | start: function() { 9 | test.sessions += 1 10 | var session = test.session() + '-' + test.sessions 11 | action('copyq -s ' + session) 12 | return session 13 | }, 14 | 15 | execute: function(session, cmd) { 16 | return test.execute('copyq', '-s', session, cmd) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/test_functions.js: -------------------------------------------------------------------------------- 1 | test = { 2 | objectEquals: function(lhs, rhs) { 3 | return typeof(lhs) == "object" && lhs.equals && lhs.equals(rhs); 4 | }, 5 | 6 | equals: function(expected, actual) { 7 | if (expected == actual) { 8 | return true; 9 | } 10 | 11 | if (test.objectEquals(expected, actual)) { 12 | return true; 13 | } 14 | 15 | if (test.objectEquals(actual, expected)) { 16 | return true; 17 | } 18 | 19 | return false; 20 | }, 21 | 22 | assertEquals: function(expected, actual, label) { 23 | if (test.equals(expected, actual)) { 24 | return; 25 | } 26 | 27 | error = `❌ Failed comparison [${label}]:` 28 | + `\n Expected: ${expected} [type=${typeof(expected)}]` 29 | + `\n Actual: ${actual} [type=${typeof(actual)}]`; 30 | throw error; 31 | }, 32 | 33 | assertTrue: function(actual, label) { 34 | test.assertEquals(true, actual); 35 | }, 36 | 37 | assertFalse: function(actual, label) { 38 | test.assertEquals(false, actual); 39 | }, 40 | 41 | waitForEquals: function(expected, getter, label) { 42 | let actual = getter(); 43 | for (let i = 0; !test.equals(actual, expected) && i < 10; ++i) { 44 | sleep(500); 45 | actual = getter(); 46 | } 47 | 48 | test.assertEquals(expected, actual, label); 49 | }, 50 | 51 | clipboardTextEquals: function(expected) { 52 | test.waitForEquals(expected, clipboard, 'clipboard text'); 53 | }, 54 | 55 | importCommands: function(ini) { 56 | serverLog('Importing: ' + ini); 57 | 58 | let commandConfigFile = new File(ini); 59 | if (!commandConfigFile.openReadOnly()) { 60 | throw 'Failed to open ini file: ' + ini; 61 | } 62 | 63 | const commandConfigContent = commandConfigFile.readAll(); 64 | commandConfigFile.close(); 65 | 66 | const commands = importCommands(commandConfigContent); 67 | if (commands.length == 0) { 68 | throw 'Failed to load ini file: ' + ini; 69 | } 70 | 71 | for (var i in commands) { 72 | let command = commands[i]; 73 | 74 | // Set global shortcut commands to application shortcut Ctrl+F1. 75 | if (command.isGlobalShortcut) { 76 | command.isGlobalShortcut = false; 77 | command.shortcuts = ['Ctrl+F1']; 78 | command.inMenu = true; 79 | } 80 | } 81 | 82 | setCommands(commands); 83 | 84 | test.waitForEquals(true, isClipboardMonitorRunning, 'wait for monitor'); 85 | }, 86 | 87 | importCommandsForTest: function(js) { 88 | const ini = str(js).replace("tests/", "").replace(".js", ".ini"); 89 | if (File(ini).exists()) { 90 | test.importCommands(ini); 91 | } 92 | }, 93 | 94 | run: function(js) { 95 | // Fail after an interval. 96 | afterMilliseconds(10000, fail); 97 | source(js); 98 | }, 99 | 100 | execute: function() { 101 | const result = execute.apply(this, arguments); 102 | const cmd = Array.prototype.slice.call(arguments).join(" "); 103 | if (!result) { 104 | throw 'Failed to execute: ' + cmd; 105 | } 106 | 107 | if (result.exit_code != 0) { 108 | throw 'Non-zero exit code (' + result.exit_code + ') from command: ' + cmd; 109 | } 110 | 111 | return result.stdout; 112 | }, 113 | 114 | // Simulate non-owned copying which would trigger onClipboardChanged 115 | // instead of onOwnClipboardChanged callback triggered after calling 116 | // copy(text). 117 | copy: function(text) { 118 | global.copy(mimeText, text, mimeOwner, '') 119 | }, 120 | } 121 | -------------------------------------------------------------------------------- /utils/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Executes tests for commands (*/test_*.js). 3 | set -euo pipefail 4 | 5 | export COPYQ_SESSION=command-tests 6 | export COPYQ_SESSION_COLOR=red 7 | export COPYQ_SETTINGS_PATH=/tmp/copyq-command-tests-config 8 | export COPYQ_LOG_FILE=/tmp/copyq-command-tests.log 9 | export COPYQ_LOG_LEVEL=${COPYQ_LOG_LEVEL:-WARNING} 10 | export COPYQ=${COPYQ:-copyq} 11 | 12 | copyq_pid="" 13 | failed_count=0 14 | 15 | run_copyq() { 16 | echo "--- Test Command: $COPYQ $*" >> "$COPYQ_LOG_FILE" 17 | "$COPYQ" "$@" 2>> "$COPYQ_LOG_FILE" 18 | } 19 | 20 | stop_server() { 21 | if [[ -n "$copyq_pid" ]]; then 22 | if kill "$copyq_pid" 2> /dev/null; then 23 | wait "$copyq_pid" 24 | fi 25 | copyq_pid="" 26 | fi 27 | rm -rf "$COPYQ_SETTINGS_PATH" 28 | } 29 | 30 | start_server() { 31 | mkdir -p "$COPYQ_SETTINGS_PATH" 32 | "$COPYQ" 2>> "$COPYQ_LOG_FILE" & 33 | copyq_pid=$! 34 | run_copyq copy '' > /dev/null 35 | show_if_needed 36 | } 37 | 38 | run_script() { 39 | run_copyq source tests/test_functions.js test.importCommandsForTest "$js" 40 | 41 | restart_if_needed 42 | 43 | if ! run_copyq source tests/test_functions.js test.run "$js"; then 44 | cat "$COPYQ_LOG_FILE" 45 | echo "Failed! See whole log: $COPYQ_LOG_FILE" 46 | echo 47 | failed_count=$((failed_count + 1)) 48 | fi 49 | } 50 | 51 | show_if_needed() { 52 | if ! grep -q '^// tests:.*noshow' "$js"; then 53 | run_copyq show 54 | fi 55 | } 56 | 57 | restart_if_needed() { 58 | if grep -q '^// tests:.*restart' "$js"; then 59 | run_copyq exit 60 | start_server 61 | fi 62 | } 63 | 64 | trap stop_server QUIT TERM INT HUP EXIT 65 | 66 | if [[ $# == 0 ]]; then 67 | exec "$0" tests/*/*.js 68 | fi 69 | 70 | for js in "$@"; do 71 | echo "Test: $js" 72 | 73 | rm -f "$COPYQ_LOG_FILE" 74 | echo "*** Starting: $js" >> "$COPYQ_LOG_FILE" 75 | 76 | stop_server 77 | start_server 78 | 79 | run_script "$js" 80 | 81 | echo "*** Finished: $js" >> "$COPYQ_LOG_FILE" 82 | done 83 | 84 | if [[ $failed_count -gt 0 ]]; then 85 | echo 86 | echo "$failed_count test(s) failed" 87 | exit 1 88 | else 89 | echo "All OK" 90 | fi 91 | --------------------------------------------------------------------------------