├── .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 | '': 'html',
49 | 'def ': 'python',
50 | 'self ': 'python',
51 | '\":': 'json',
52 | '\\n##': 'md',
53 | '](': 'md',
54 | '\\n* ': 'md',
55 | '#{': 'ruby',
56 | '(@': 'ruby',
57 | ':\\n': 'yaml',
58 | }
59 |
60 | const errorLabel = 'Highlight Code Error'
61 |
62 | function contains(text, what) {
63 | return text.indexOf(what) != -1
64 | }
65 |
66 | function startsWith(text, what) {
67 | return what === text.substring(0, what.length)
68 | }
69 |
70 | function langFromTags(tags) {
71 | for (var tagPart in tagPartToLang) {
72 | if (tags.indexOf(tagPart) != -1)
73 | return tagPartToLang[tagPart]
74 | }
75 | return ''
76 | }
77 |
78 | function langFromPrefix(text) {
79 | for (var prefix in prefixToLang) {
80 | if (startsWith(text, prefix))
81 | return prefixToLang[prefix]
82 | }
83 | return ''
84 | }
85 |
86 | function langFromTextPart(text) {
87 | for (var part in textPartToLang) {
88 | if (text.indexOf(part) != -1)
89 | return textPartToLang[part]
90 | }
91 | return ''
92 | }
93 |
94 | function addHtml(html, tag) {
95 | setData(mimeHtml, html)
96 | if (tag) {
97 | tags = data(mimeTags)
98 | tags = (tags ? str(tags) + ',' : '') + tag
99 | setData(mimeTags, tags)
100 | }
101 | return true
102 | }
103 |
104 | function addHtmlOutput(result, tag) {
105 | if (!result) {
106 | notification(
107 | '.id', 'highlight',
108 | '.message', 'Failed to add syntax highlighting',
109 | )
110 | return false
111 | }
112 |
113 | if (result.exit_code !== 0) {
114 | popup(errorLabel, result.stderr)
115 | return false
116 | }
117 |
118 | return addHtml(result.stdout, tag)
119 | }
120 |
121 | function code(textData, lang) {
122 | result = execute('python3', '-c', pythonCode, lang, null, textData)
123 | return addHtmlOutput(result, 'code::' + lang)
124 | }
125 |
126 | function highlightCode() {
127 | var formats = dataFormats()
128 | if ( formats.indexOf(mimeHidden) != -1
129 | || formats.indexOf(mimeHtml) != -1 ) {
130 | return false
131 | }
132 |
133 | const textData = data(mimeText)
134 | if (textData.length == 0 || textData.length > 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 | 
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 | '' + 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 | 
30 | 
31 | 
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 |
--------------------------------------------------------------------------------