From c0179c8a87d723505dc73d49ac3b199178392e63 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 12 Jan 2026 16:50:25 -0500 Subject: [PATCH] [gnome] Add clipboard manager extension --- .../clipboard-history@alexsaveau.dev/LICENSE | 25 + .../README.md | 68 + .../confirmDialog.js | 77 + .../dataStructures.js | 417 +++++ .../extension.js | 1352 +++++++++++++++++ .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 1058 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 976 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 2548 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 2705 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 1053 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 2702 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 1296 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 1493 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 899 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 2641 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 1345 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 1395 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 3002 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 1886 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 1311 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 6882 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 2789 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 3336 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 1174 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 1329 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 3405 bytes .../clipboard-history@alexsaveau.dev.mo | Bin 0 -> 2567 bytes .../metadata.json | 16 + .../clipboard-history@alexsaveau.dev/prefs.js | 444 ++++++ .../schemas/gschemas.compiled | Bin 0 -> 1576 bytes ...extensions.clipboard-indicator.gschema.xml | 140 ++ .../settingsFields.js | 19 + .../clipboard-history@alexsaveau.dev/store.js | 512 +++++++ .../stylesheet.css | 34 + 34 files changed, 3104 insertions(+) create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/LICENSE create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/README.md create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/confirmDialog.js create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/dataStructures.js create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/extension.js create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/ar/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/ca/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/cs/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/de/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/el/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/es/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/eu/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/fa/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/fi/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/fr/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/hu/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/it/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/ja/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/nl/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/oc/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/pl/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/pt_BR/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/ru/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/sk/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/tr/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/uk/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/zh_CN/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/metadata.json create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/prefs.js create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/schemas/gschemas.compiled create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/schemas/org.gnome.shell.extensions.clipboard-indicator.gschema.xml create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/settingsFields.js create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/store.js create mode 100644 gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/stylesheet.css diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/LICENSE b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/LICENSE new file mode 100644 index 0000000..a2d684f --- /dev/null +++ b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/LICENSE @@ -0,0 +1,25 @@ +====================== +The MIT License (MIT) +====================== + +Copyright (c) 2022, Alex Saveau +Copyright (c) 2014, Yotam Bar-On +----------------------------------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +*The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.* + +**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.** diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/README.md b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/README.md new file mode 100644 index 0000000..e2edeb1 --- /dev/null +++ b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/README.md @@ -0,0 +1,68 @@ +# Gnome Clipboard History + +[Gnome Clipboard History](https://extensions.gnome.org/extension/4839/clipboard-history/) is a +clipboard manager GNOME extension that saves what you've copied into an easily accessible, +searchable history panel. + +The extension is a rewrite of +[Clipboard Indicator](https://github.com/Tudmotu/gnome-shell-extension-clipboard-indicator) with +vastly improved performance, new features, and +[bug fixes](https://github.com/Tudmotu/gnome-shell-extension-clipboard-indicator/pull/338). + +A technical overview is available at https://alexsaveau.dev/blog/gch. + +## Project status: replaced by Ringboard + +Gnome Clipboard History is now in maintenance mode as it is being replaced by +[Ringboard](https://github.com/SUPERCILEX/clipboard-history). I'm still accepting PRs for small +improvements and bug fixes (such as supporting the latest Gnome version), but no new development +will take place. + +## Download + +[Get it on GNOME Extensions](https://extensions.gnome.org/extension/4839/clipboard-history/) + +## Tips + +![Tutorial screenshot](tutorial-screenshot.png) + +- Open the panel from anywhere with Super + Shift + V. +- Modify shortcuts in settings or delete them by hitting backspace while editing a shortcut. +- Use the `Only save favorites to disk` feature to wipe your non-favorited items on shutdown. +- Use `Private mode` to temporarily stop processing copied items. +- Use keyboard shortcuts while the panel is open: + - Ctrl + N where `N` is a number from 1 to 9 to select the Nth + non-favorited entry. + - Super + Ctrl + N where `N` is a number from 1 to 9 to select + the Nth favorited entry. + - Ctrl + p/n to navigate to the previous/next page. + - Ctrl + Alt + S to open settings. + - / to search. + - F to (un)favorite a highlighted item. +- Search uses case-insensitive [regex](https://regex101.com/?flavor=javascript). + +## Install from source + +A note on versioning: + +- The `master` branch and `1.4.x` tags support GNOME 45. +- The `pre-45` branch and `1.3.x` (or earlier) tags support GNOME 40-44. + +### Build + +```shell +cd ~/.local/share/gnome-shell/extensions/ && \ + git clone https://github.com/SUPERCILEX/gnome-clipboard-history.git clipboard-history@alexsaveau.dev && \ + cd clipboard-history@alexsaveau.dev && \ + make +``` + +### Restart GNOME + +Alt + F2 then type `r`. + +### Install + +```shell +gnome-extensions enable clipboard-history@alexsaveau.dev +``` diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/confirmDialog.js b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/confirmDialog.js new file mode 100644 index 0000000..1b06006 --- /dev/null +++ b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/confirmDialog.js @@ -0,0 +1,77 @@ +import Clutter from 'gi://Clutter'; +import St from 'gi://St'; +import GObject from 'gi://GObject'; + +import * as ModalDialog from 'resource:///org/gnome/shell/ui/modalDialog.js'; + +let _openDialog; + +export function openConfirmDialog( + title, + message, + sub_message, + ok_label, + cancel_label, + callback, +) { + if (!_openDialog) { + _openDialog = new ConfirmDialog( + title, + message + '\n' + sub_message, + ok_label, + cancel_label, + callback, + ).open(); + } +} + +const ConfirmDialog = GObject.registerClass( + class ConfirmDialog extends ModalDialog.ModalDialog { + _init(title, desc, ok_label, cancel_label, callback) { + super._init(); + + let main_box = new St.BoxLayout({ + vertical: false, + }); + this.contentLayout.add_child(main_box); + + let message_box = new St.BoxLayout({ + vertical: true, + }); + main_box.add_child(message_box); + + let subject_label = new St.Label({ + style: 'font-weight: bold', + x_align: Clutter.ActorAlign.CENTER, + text: title, + }); + message_box.add_child(subject_label); + + let desc_label = new St.Label({ + style: 'padding-top: 12px', + x_align: Clutter.ActorAlign.CENTER, + text: desc, + }); + message_box.add_child(desc_label); + + this.setButtons([ + { + label: cancel_label, + action: () => { + this.close(); + _openDialog = null; + }, + key: Clutter.Escape, + }, + { + label: ok_label, + action: () => { + this.close(); + callback(); + _openDialog = null; + }, + }, + ]); + } + }, +); diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/dataStructures.js b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/dataStructures.js new file mode 100644 index 0000000..4ea4bb9 --- /dev/null +++ b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/dataStructures.js @@ -0,0 +1,417 @@ +// Derived from +// https://github.com/wooorm/linked-list/blob/d2390fe1cab9f780cfd34fa31c8fa8ede4ad674d/index.js + +export const TYPE_TEXT = 'text'; + +// Creates a new `Iterator` for looping over the `List`. +class Iterator { + constructor(item) { + this.item = item; + } + + // Move the `Iterator` to the next item. + next() { + this.value = this.item; + this.done = !this.item; + this.item = this.item ? this.item.next : undefined; + return this; + } +} + +// Creates a new `Item`: +// An item is a bit like DOM node: It knows only about its "parent" (`list`), +// the item before it (`prev`), and the item after it (`next`). +export class LLNode { + // Prepends the given item *before* the item operated on. + prepend(item) { + const list = this.list; + + if (!item || !item.append || !item.prepend || !item.detach) { + throw new Error( + 'An argument without append, prepend, or detach methods was given to `Item#prepend`.', + ); + } + + // If self is detached, return false. + if (!list) { + return false; + } + if (this === item) { + return false; + } + + // Detach the prependee. + const transient = this.list === item.list; + item.detach(transient); + + // If self has a previous item... + if (this.prev) { + item.prev = this.prev; + this.prev.next = item; + } + + // Connect the prependee. + item.next = this; + item.list = list; + + // Set the previous item of self to the prependee. + this.prev = item; + + // If self is the first item in the parent list, link the lists first item to + // the prependee. + if (this === list.head) { + list.head = item; + } + + // If the the parent list has no last item, link the lists last item to self. + if (!list.tail) { + list.tail = this; + } + + list.length++; + if (!transient) { + item._addToIndex(); + } + + return item; + } + + // Appends the given item *after* the item operated on. + append(item) { + const list = this.list; + + if (!item || !item.append || !item.prepend || !item.detach) { + throw new Error( + 'An argument without append, prepend, or detach methods was given to `Item#append`.', + ); + } + + if (!list) { + return false; + } + if (this === item) { + return false; + } + + // Detach the appendee. + const transient = this.list === item.list; + item.detach(transient); + + // If self has a next item... + if (this.next) { + item.next = this.next; + this.next.prev = item; + } + + // Connect the appendee. + item.prev = this; + item.list = list; + + // Set the next item of self to the appendee. + this.next = item; + + // If the the parent list has no last item or if self is the parent lists last + // item, link the lists last item to the appendee. + if (this === list.tail || !list.tail) { + list.tail = item; + } + + list.length++; + if (!transient) { + item._addToIndex(); + } + + return item; + } + + // Detaches the item operated on from its parent list. + detach(transient) { + const list = this.list; + + if (!list) { + return this; + } + if (!transient) { + this._removeFromIndex(); + } + + // If self is the last item in the parent list, link the lists last item to + // the previous item. + if (list.tail === this) { + list.tail = this.prev; + } + + // If self is the first item in the parent list, link the lists first item to + // the next item. + if (list.head === this) { + list.head = this.next; + } + + // If both the last and first items in the parent list are the same, remove + // the link to the last item. + if (list.tail === list.head) { + list.tail = null; + } + + // If a previous item exists, link its next item to selfs next item. + if (this.prev) { + this.prev.next = this.next; + } + + // If a next item exists, link its previous item to selfs previous item. + if (this.next) { + this.next.prev = this.prev; + } + + // Remove links from self to both the next and previous items, and to the + // parent list. + this.prev = this.next = this.list = null; + + list.length--; + + return this; + } + + nextCyclic() { + return this.next || this.list.head; + } + + prevCyclic() { + return this.prev || this.list.last(); + } + + _addToIndex() { + const hash = this._hash(); + if (hash === undefined || hash === null) { + return; + } + + if (this.type === TYPE_TEXT) { + this.list.bytes += this.text.length; + } + + let entries = this.list.invertedIndex[hash]; + if (!entries) { + entries = []; + this.list.invertedIndex[hash] = entries; + } + entries.push(this.id); + this.list.idsToItems[this.id] = this; + } + + _removeFromIndex() { + const hash = this._hash(); + if (hash === undefined || hash === null) { + return; + } + + if (this.type === TYPE_TEXT) { + this.list.bytes -= this.text.length; + } + + const entries = this.list.invertedIndex[hash]; + if (entries.length === 1) { + delete this.list.invertedIndex[hash]; + } else { + entries.splice(entries.indexOf(this.id), 1); + } + delete this.list.idsToItems[this.id]; + } + + _hash() { + if (this.type === TYPE_TEXT) { + return _hashText(this.text); + } else { + return null; + } + } +} + +LLNode.prototype.next = LLNode.prototype.prev = LLNode.prototype.list = null; + +// Creates a new List: A linked list is a bit like an Array, but knows nothing +// about how many items are in it, and knows only about its first (`head`) and +// last (`tail`) items. +// Each item (e.g. `head`, `tail`, &c.) knows which item comes before or after +// it (its more like the implementation of the DOM in JavaScript). +export class LinkedList { + // Creates a new list from the arguments (each a list item) passed in. + static of(...items) { + return appendAll(new this(), items); + } + + // Creates a new list from the given array-like object (each a list item) passed + // in. + static from(items) { + return appendAll(new this(), items); + } + + constructor(...items) { + appendAll(this, items); + this.idsToItems = {}; + this.invertedIndex = {}; + /** Note: this isn't an accurate count because of UTF encoding and other JS mumbo jumbo. */ + this.bytes = 0; + } + + // Returns the list's items as an array. + // This does *not* detach the items. + toArray() { + let item = this.head; + const result = []; + + while (item) { + result.push(item); + item = item.next; + } + + return result; + } + + // Prepends the given item to the list. + // `item` will be the new first item (`head`). + prepend(item) { + if (!item) { + return false; + } + + if (!item.append || !item.prepend || !item.detach) { + throw new Error( + 'An argument without append, prepend, or detach methods was given to `List#prepend`.', + ); + } + + if (this.head) { + return this.head.prepend(item); + } + + item.detach(); + item.list = this; + this.head = item; + this.length++; + + item._addToIndex(); + + return item; + } + + // Appends the given item to the list. + // `item` will be the new last item (`tail`) if the list had a first item, and + // its first item (`head`) otherwise. + append(item) { + if (!item) { + return false; + } + + if (!item.append || !item.prepend || !item.detach) { + throw new Error( + 'An argument without append, prepend, or detach methods was given to `List#append`.', + ); + } + + // If self has a last item, defer appending to the last items append method, + // and return the result. + if (this.tail) { + return this.tail.append(item); + } + + // If self has a first item, defer appending to the first items append method, + // and return the result. + if (this.head) { + return this.head.append(item); + } + + // ...otherwise, there is no `tail` or `head` item yet. + item.detach(); + item.list = this; + this.head = item; + this.length++; + + item._addToIndex(); + + return item; + } + + last() { + return this.tail || this.head; + } + + findById(id) { + return this.idsToItems[id]; + } + + findTextItem(text) { + const entries = this.invertedIndex[_hashText(text)]; + if (!entries) { + return null; + } + + for (let i = entries.length - 1; i >= 0; i--) { + const item = this.idsToItems[entries[i]]; + if (item.type === TYPE_TEXT && item.text === text) { + return item; + } + } + return null; + } + + // Creates an iterator from the list. + [Symbol.iterator]() { + return new Iterator(this.head); + } +} + +LinkedList.prototype.length = 0; +LinkedList.prototype.tail = LinkedList.prototype.head = null; + +// Creates a new list from the items passed in. +export function appendAll(list, items) { + let index; + let item; + let iterator; + + if (!items) { + return list; + } + + if (items[Symbol.iterator]) { + iterator = items[Symbol.iterator](); + item = {}; + + while (!item.done) { + item = iterator.next(); + list.append(item && item.value); + } + } else { + index = -1; + + while (++index < items.length) { + list.append(items[index]); + } + } + + return list; +} + +function _hashText(text) { + // The goal of this hash function is to be extremely fast while minimizing collisions. To do + // this, we make an assumption about our data. If users copy text, the guess is that there is + // a very low likelihood of collisions when the text is very long. For example, why would + // someone copy two different pieces of text that are exactly 29047 characters long? However, for + // smaller pieces of text, it's very easy to get length collisions. For example, I can copy "the" + // and "123" to cause a collision. Thus, our hash function returns the string length for longer + // strings while using an ok-ish hash for short strings. + + if (text.length > 500) { + return text.length; + } + + // Copied from https://stackoverflow.com/a/7616484/4548500 + let hash = 0; + for (let i = 0; i < text.length; i++) { + let chr = text.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; // Convert to integer + } + return hash; +} diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/extension.js b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/extension.js new file mode 100644 index 0000000..9a3a1e6 --- /dev/null +++ b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/extension.js @@ -0,0 +1,1352 @@ +import Clutter from 'gi://Clutter'; +import GObject from 'gi://GObject'; +import GLib from 'gi://GLib'; +import Meta from 'gi://Meta'; +import Shell from 'gi://Shell'; +import St from 'gi://St'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js'; +import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; +import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; + +import { + Extension, + gettext as _, +} from 'resource:///org/gnome/shell/extensions/extension.js'; + +import { ensureActorVisibleInScrollView } from 'resource:///org/gnome/shell/misc/animationUtils.js'; + +import * as Store from './store.js'; +import * as DS from './dataStructures.js'; +import { openConfirmDialog } from './confirmDialog.js'; +import SettingsFields from './settingsFields.js'; + +const Clipboard = St.Clipboard.get_default(); +const VirtualKeyboard = (() => { + let VirtualKeyboard; + return () => { + if (!VirtualKeyboard) { + VirtualKeyboard = Clutter.get_default_backend() + .get_default_seat() + .create_virtual_device(Clutter.InputDeviceType.KEYBOARD_DEVICE); + } + return VirtualKeyboard; + }; +})(); + +const SETTING_KEY_CLEAR_HISTORY = 'clear-history'; +const SETTING_KEY_PREV_ENTRY = 'prev-entry'; +const SETTING_KEY_NEXT_ENTRY = 'next-entry'; +const SETTING_KEY_TOGGLE_MENU = 'toggle-menu'; +const SETTING_KEY_PRIVATE_MODE = 'toggle-private-mode'; +const INDICATOR_ICON = 'edit-paste-symbolic'; + +const PAGE_SIZE = 50; +const MAX_VISIBLE_CHARS = 200; + +let MAX_REGISTRY_LENGTH; +let MAX_BYTES; +let WINDOW_WIDTH_PERCENTAGE; +let CACHE_ONLY_FAVORITES; +let MOVE_ITEM_FIRST; +let ENABLE_KEYBINDING; +let PRIVATE_MODE; +let NOTIFY_ON_COPY; +let CONFIRM_ON_CLEAR; +let MAX_TOPBAR_LENGTH; +let TOPBAR_DISPLAY_MODE; // 0 - only icon, 1 - only clipboard content, 2 - both, 3 - none +let DISABLE_DOWN_ARROW; +let STRIP_TEXT; +let PASTE_ON_SELECTION; +let PROCESS_PRIMARY_SELECTION; +let IGNORE_PASSWORD_MIMES; + +class ClipboardIndicator extends PanelMenu.Button { + _init(extension) { + super._init(0, extension.indicatorName, false); + + this.extension = extension; + this.settings = extension.getSettings(); + + this._shortcutsBindingIds = []; + + const hbox = new St.BoxLayout({ + style_class: 'panel-status-menu-box clipboard-indicator-hbox', + }); + this.icon = new St.Icon({ + icon_name: INDICATOR_ICON, + style_class: 'system-status-icon clipboard-indicator-icon', + }); + hbox.add_child(this.icon); + this._buttonText = new St.Label({ + text: '', + y_align: Clutter.ActorAlign.CENTER, + }); + hbox.add_child(this._buttonText); + this._downArrow = PopupMenu.arrowIcon(St.Side.BOTTOM); + hbox.add_child(this._downArrow); + this.add_child(hbox); + + this._fetchSettings(); + this._buildMenu(); + this._updateTopbarLayout(); + } + + destroy() { + this._disconnectSettings(); + this._unbindShortcuts(); + this._disconnectSelectionListener(); + + if (this._searchFocusHackCallbackId) { + GLib.Source.source_remove(this._searchFocusHackCallbackId); + this._searchFocusHackCallbackId = undefined; + } + if (this._pasteHackCallbackId) { + GLib.Source.source_remove(this._pasteHackCallbackId); + this._pasteHackCallbackId = undefined; + } + + super.destroy(); + } + + _buildMenu() { + this.searchEntry = new St.Entry({ + name: 'searchEntry', + style_class: 'search-entry ci-history-search-entry', + can_focus: true, + hint_text: _('Search clipboard history…'), + track_hover: true, + x_expand: true, + y_expand: true, + }); + + const entryItem = new PopupMenu.PopupBaseMenuItem({ + style_class: 'ci-history-search-section', + reactive: false, + can_focus: false, + }); + entryItem.add_child(this.searchEntry); + this.menu.addMenuItem(entryItem); + + this.menu.connect('open-state-changed', (self, open) => { + if (open) { + this._setMenuWidth(); + this.searchEntry.set_text(''); + this._searchFocusHackCallbackId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 1, + () => { + global.stage.set_key_focus(this.searchEntry); + this._searchFocusHackCallbackId = undefined; + return false; + }, + ); + } + }); + + // Create menu sections for items + // Favorites + this.favoritesSection = new PopupMenu.PopupMenuSection(); + + this.scrollViewFavoritesMenuSection = new PopupMenu.PopupMenuSection(); + const favoritesScrollView = new St.ScrollView({ + style_class: 'ci-history-menu-section', + overlay_scrollbars: true, + }); + favoritesScrollView.add_child(this.favoritesSection.actor); + + this.scrollViewFavoritesMenuSection.actor.add_child(favoritesScrollView); + this.menu.addMenuItem(this.scrollViewFavoritesMenuSection); + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + // History + this.historySection = new PopupMenu.PopupMenuSection(); + + this.scrollViewMenuSection = new PopupMenu.PopupMenuSection(); + this.historyScrollView = new St.ScrollView({ + style_class: 'ci-history-menu-section', + overlay_scrollbars: true, + }); + this.historyScrollView.add_child(this.historySection.actor); + + this.scrollViewMenuSection.actor.add_child(this.historyScrollView); + + this.menu.addMenuItem(this.scrollViewMenuSection); + + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + const actionsSection = new PopupMenu.PopupMenuSection(); + const actionsBox = new St.BoxLayout({ + style_class: 'ci-history-actions-section', + vertical: false, + }); + + actionsSection.actor.add_child(actionsBox); + this.menu.addMenuItem(actionsSection); + + const prevPage = new PopupMenu.PopupBaseMenuItem(); + prevPage.add_child( + new St.Icon({ + icon_name: 'go-previous-symbolic', + style_class: 'popup-menu-icon', + }), + ); + prevPage.connect('activate', this._navigatePrevPage.bind(this)); + actionsBox.add_child(prevPage); + + const nextPage = new PopupMenu.PopupBaseMenuItem(); + nextPage.add_child( + new St.Icon({ + icon_name: 'go-next-symbolic', + style_class: 'popup-menu-icon', + }), + ); + nextPage.connect('activate', this._navigateNextPage.bind(this)); + actionsBox.add_child(nextPage); + + actionsBox.add_child(new St.BoxLayout({ x_expand: true })); + + this.privateModeMenuItem = new PopupMenu.PopupSwitchMenuItem( + _('Private mode'), + PRIVATE_MODE, + { reactive: true }, + ); + this.privateModeMenuItem.connect('toggled', () => { + this.settings.set_boolean( + SettingsFields.PRIVATE_MODE, + this.privateModeMenuItem.state, + ); + }); + actionsBox.add_child(this.privateModeMenuItem); + this._updatePrivateModeState(); + + const clearMenuItem = new PopupMenu.PopupBaseMenuItem(); + clearMenuItem.add_child( + new St.Icon({ + icon_name: 'edit-delete-symbolic', + style_class: 'popup-menu-icon', + }), + ); + actionsBox.add_child(clearMenuItem); + + const settingsMenuItem = new PopupMenu.PopupBaseMenuItem(); + settingsMenuItem.add_child( + new St.Icon({ + icon_name: 'emblem-system-symbolic', + style_class: 'popup-menu-icon', + }), + ); + settingsMenuItem.connect('activate', this._openSettings.bind(this)); + actionsBox.add_child(settingsMenuItem); + + if (ENABLE_KEYBINDING) { + this._bindShortcuts(); + } + this.menu.actor.connect('key-press-event', (_, event) => + this._handleGlobalKeyEvent(event), + ); + + Store.buildClipboardStateFromLog( + (entries, favoriteEntries, nextId, nextDiskId) => { + /** + * This field stores the number of items in the historySection to avoid calling _getMenuItems + * since that method is slow. + */ + this.activeHistoryMenuItems = 0; + /** + * These two IDs are extremely important: making a mistake with either one breaks the + * extension. Both IDs are globally unique within compaction intervals. The normal ID is + * *always* present and valid -- it allows us to build an inverted index so we can find + * previously copied items in O(1) time. The Disk ID is only present when we cache all + * entries. This additional complexity is needed to know what the ID of an item is on disk as + * compared to in memory when we're only caching favorites. + */ + this.nextId = nextId; + this.nextDiskId = nextDiskId || nextId; + /** + * DS.LinkedList is the actual clipboard history and source of truth. Never use historySection + * or favoritesSection as the source of truth as these may get outdated during pagination. + * + * Entries *may* have a menuItem attached, meaning they are currently visible. On the other + * hand, menu items must always have an entry attached. + */ + this.entries = entries; + this.favoriteEntries = favoriteEntries; + + this.currentlySelectedEntry = entries.last(); + this._restoreFavoritedEntries(); + this._maybeRestoreMenuPages(); + + this._settingsChangedId = this.settings.connect( + 'changed', + this._onSettingsChange.bind(this), + ); + + this.searchEntry + .get_clutter_text() + .connect('text-changed', this._onSearchTextChanged.bind(this)); + clearMenuItem.connect('activate', this._removeAll.bind(this)); + + this._setupSelectionChangeListener(); + }, + ); + } + + _setMenuWidth() { + const display = global.display; + const screen_width = display.get_monitor_geometry( + display.get_primary_monitor(), + ).width; + + this.menu.actor.width = screen_width * (WINDOW_WIDTH_PERCENTAGE / 100); + } + + _handleGlobalKeyEvent(event) { + this._handleCtrlSelectKeyEvent(event); + this._handleSettingsKeyEvent(event); + this._handleNavigationKeyEvent(event); + this._handleFocusSearchKeyEvent(event); + } + + _handleCtrlSelectKeyEvent(event) { + if (!event.has_control_modifier()) { + return; + } + + const index = parseInt(event.get_key_unicode()); // Starts at 1 + if (isNaN(index) || index <= 0) { + return; + } + + const items = + event.get_state() === 68 // Ctrl + Super + ? this.favoritesSection._getMenuItems() + : this.historySection._getMenuItems(); + if (index > items.length) { + return; + } + + this._onMenuItemSelectedAndMenuClose(items[index - 1]); + } + + _handleSettingsKeyEvent(event) { + if (event.get_state() !== 12 || event.get_key_unicode() !== 's') { + return; + } + + this._openSettings(); + } + + _handleNavigationKeyEvent(event) { + if (!event.has_control_modifier()) { + return; + } + + if (event.get_key_unicode() === 'n') { + this._navigateNextPage(); + } else if (event.get_key_unicode() === 'p') { + this._navigatePrevPage(); + } + } + + _handleFocusSearchKeyEvent(event) { + if (event.get_key_unicode() !== '/') { + return; + } + + global.stage.set_key_focus(this.searchEntry); + } + + _addEntry(entry, selectEntry, updateClipboard, insertIndex) { + if (!entry.favorite && this.activeHistoryMenuItems >= PAGE_SIZE) { + const items = this.historySection._getMenuItems(); + const item = items[items.length - 1]; + this._rewriteMenuItem(item, entry); + this.historySection.moveMenuItem(item, 0); + + if (selectEntry) { + this._selectEntry(entry, updateClipboard); + } + return; + } + + const menuItem = new PopupMenu.PopupMenuItem('', { hover: false }); + menuItem.setOrnament(PopupMenu.Ornament.NONE); + + menuItem.entry = entry; + entry.menuItem = menuItem; + + menuItem.connect( + 'activate', + this._onMenuItemSelectedAndMenuClose.bind(this), + ); + menuItem.connect('key-press-event', (_, event) => + this._handleMenuItemKeyEvent(event, menuItem), + ); + + this._setEntryLabel(menuItem); + + // Favorite button + const icon_name = entry.favorite + ? 'starred-symbolic' + : 'non-starred-symbolic'; + const iconfav = new St.Icon({ + icon_name: icon_name, + style_class: 'system-status-icon', + }); + + const icofavBtn = new St.Button({ + style_class: 'ci-action-btn', + can_focus: true, + child: iconfav, + x_align: Clutter.ActorAlign.END, + x_expand: true, + y_expand: true, + }); + + menuItem.actor.add_child(icofavBtn); + icofavBtn.connect('clicked', () => { + this._favoriteToggle(menuItem); + }); + + // Delete button + const icon = new St.Icon({ + icon_name: 'edit-delete-symbolic', + style_class: 'system-status-icon', + }); + + const icoBtn = new St.Button({ + style_class: 'ci-action-btn', + can_focus: true, + child: icon, + x_align: Clutter.ActorAlign.END, + x_expand: false, + y_expand: true, + }); + + menuItem.actor.add_child(icoBtn); + icoBtn.connect('clicked', () => { + this._deleteEntryAndRestoreLatest(menuItem.entry); + }); + + menuItem.connect('destroy', () => { + delete menuItem.entry.menuItem; + if (!menuItem.entry.favorite) { + this.activeHistoryMenuItems--; + } + }); + menuItem.connect('key-focus-in', () => { + if (!menuItem.entry.favorite) { + ensureActorVisibleInScrollView(this.historyScrollView, menuItem); + } + }); + + if (entry.favorite) { + this.favoritesSection.addMenuItem(menuItem, insertIndex); + } else { + this.historySection.addMenuItem(menuItem, insertIndex); + + this.activeHistoryMenuItems++; + } + + if (selectEntry) { + this._selectEntry(entry, updateClipboard); + } + } + + _handleMenuItemKeyEvent(event, menuItem) { + if (event.get_key_unicode() === 'f') { + this._favoriteToggle(menuItem); + } + if (event.get_key_code() === 119) { + const next = menuItem.entry.prev || menuItem.entry.next; + if (next?.menuItem) { + global.stage.set_key_focus(next.menuItem); + } + this._deleteEntryAndRestoreLatest(menuItem.entry); + } + } + + _updateButtonText(entry) { + if ( + !(TOPBAR_DISPLAY_MODE === 1 || TOPBAR_DISPLAY_MODE === 2) || + (entry && entry.type !== DS.TYPE_TEXT) + ) { + return; + } + + if (PRIVATE_MODE) { + this._buttonText.set_text('…'); + } else if (entry) { + this._buttonText.set_text(this._truncated(entry.text, MAX_TOPBAR_LENGTH)); + } else { + this._buttonText.set_text(''); + } + } + + _setEntryLabel(menuItem) { + const entry = menuItem.entry; + if (entry.type === DS.TYPE_TEXT) { + menuItem.label.set_text(this._truncated(entry.text, MAX_VISIBLE_CHARS)); + } else { + throw new TypeError('Unknown type: ' + entry.type); + } + } + + _favoriteToggle(menuItem) { + const entry = menuItem.entry; + const wasSelected = this.currentlySelectedEntry?.id === entry.id; + + // Move to front (end of list) + (entry.favorite ? this.entries : this.favoriteEntries).append(entry); + this._removeEntry(entry); + entry.favorite = !entry.favorite; + this._addEntry(entry, wasSelected, false, 0); + this._maybeRestoreMenuPages(); + global.stage.set_key_focus(entry.menuItem); + + if (CACHE_ONLY_FAVORITES && !entry.favorite) { + if (entry.diskId) { + Store.deleteTextEntry(entry.diskId, true); + delete entry.diskId; + } + return; + } + + if (entry.diskId) { + Store.updateFavoriteStatus(entry.diskId, entry.favorite); + } else { + entry.diskId = this.nextDiskId++; + + Store.storeTextEntry(entry.text); + Store.updateFavoriteStatus(entry.diskId, true); + } + } + + _removeAll() { + if (CONFIRM_ON_CLEAR) { + this._confirmRemoveAll(); + } else { + this._clearHistory(); + } + } + + _confirmRemoveAll() { + const title = _('Clear all?'); + const message = _('Are you sure you want to delete all clipboard items?'); + const sub_message = _('This operation cannot be undone.'); + + openConfirmDialog( + title, + message, + sub_message, + _('Clear'), + _('Cancel'), + () => { + this._clearHistory(); + }, + ); + } + + _clearHistory() { + if (this.currentlySelectedEntry && !this.currentlySelectedEntry.favorite) { + this._resetSelectedMenuItem(true); + } + + // Favorites aren't touched when clearing history + this.entries = new DS.LinkedList(); + this.historySection.removeAll(); + + Store.resetDatabase(this._currentStateBuilder.bind(this)); + } + + _removeEntry(entry, fullyDelete, humanGenerated) { + if (fullyDelete) { + entry.detach(); + + if (entry.diskId) { + Store.deleteTextEntry(entry.diskId, entry.favorite); + } + } + + if (entry.id === this.currentlySelectedEntry?.id) { + this._resetSelectedMenuItem(humanGenerated); + } + entry.menuItem?.destroy(); + if (fullyDelete) { + this._maybeRestoreMenuPages(); + } + } + + _pruneOldestEntries() { + let entry = this.entries.head; + while ( + entry && + (this.entries.length > MAX_REGISTRY_LENGTH || + this.entries.bytes > MAX_BYTES) + ) { + const next = entry.next; + this._removeEntry(entry, true); + entry = next; + } + + Store.maybePerformLogCompaction(this._currentStateBuilder.bind(this)); + } + + _selectEntry(entry, updateClipboard, triggerPaste) { + this.currentlySelectedEntry?.menuItem?.setOrnament(PopupMenu.Ornament.NONE); + this.currentlySelectedEntry = entry; + + entry.menuItem?.setOrnament(PopupMenu.Ornament.DOT); + this._updateButtonText(entry); + if (updateClipboard !== false) { + if (entry.type === DS.TYPE_TEXT) { + this._setClipboardText(entry.text); + } else { + throw new TypeError('Unknown type: ' + entry.type); + } + + if (PASTE_ON_SELECTION && triggerPaste) { + this._triggerPasteHack(); + } + } + } + + _setClipboardText(text) { + if (this._debouncing !== undefined) { + this._debouncing++; + } + + Clipboard.set_text(St.ClipboardType.CLIPBOARD, text); + Clipboard.set_text(St.ClipboardType.PRIMARY, text); + } + + _triggerPasteHack() { + this._pasteHackCallbackId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 1, // Just post to the end of the event loop + () => { + const SHIFT_L = 42; + const INSERT = 110; + + const eventTime = Clutter.get_current_event_time() * 1000; + VirtualKeyboard().notify_key( + eventTime, + SHIFT_L, + Clutter.KeyState.PRESSED, + ); + VirtualKeyboard().notify_key( + eventTime, + INSERT, + Clutter.KeyState.PRESSED, + ); + VirtualKeyboard().notify_key( + eventTime, + INSERT, + Clutter.KeyState.RELEASED, + ); + VirtualKeyboard().notify_key( + eventTime, + SHIFT_L, + Clutter.KeyState.RELEASED, + ); + + this._pasteHackCallbackId = undefined; + return false; + }, + ); + } + + _onMenuItemSelectedAndMenuClose(menuItem) { + this._moveEntryFirst(menuItem.entry); + this._selectEntry(menuItem.entry, true, true); + this.menu.close(); + } + + _resetSelectedMenuItem(resetClipboard) { + this.currentlySelectedEntry = undefined; + this._updateButtonText(); + if (resetClipboard) { + this._setClipboardText(''); + } + } + + _restoreFavoritedEntries() { + for (let entry = this.favoriteEntries.last(); entry; entry = entry.prev) { + this._addEntry(entry); + } + } + + _maybeRestoreMenuPages() { + if (this.activeHistoryMenuItems > 0) { + return; + } + + for ( + let entry = this.entries.last(); + entry && this.activeHistoryMenuItems < PAGE_SIZE; + entry = entry.prev + ) { + this._addEntry(entry, this.currentlySelectedEntry === entry); + } + } + + /** + * Our pagination implementation is purposefully "broken." The idea is simply to do no unnecessary + * work. As a consequence, if a user navigates to some page and then starts copying/moving items, + * those items will appear on the currently visible page even though they don't belong there. This + * could kind of be considered a feature since it means you can go back to some cluster of copied + * items and start copying stuff from the same cluster and have it all show up together. + * + * Note that over time (as the user copies items), the page reclamation process will morph the + * current page into the first page. This is the only way to make the user-visible state match our + * backing store after changing pages. + * + * Also note that the use of `last` and `next` is correct. Menu items are ordered from latest to + * oldest whereas `entries` is ordered from oldest to latest. + */ + _navigatePrevPage() { + if (this.searchEntryFront) { + this.populateSearchResults(this.searchEntry.get_text(), false); + return; + } + + const items = this.historySection._getMenuItems(); + if (items.length === 0) { + return; + } + + const start = items[0].entry; + for ( + let entry = start.nextCyclic(), i = items.length - 1; + entry !== start && i >= 0; + entry = entry.nextCyclic() + ) { + this._rewriteMenuItem(items[i--], entry); + } + } + + _navigateNextPage() { + if (this.searchEntryFront) { + this.populateSearchResults(this.searchEntry.get_text(), true); + return; + } + + const items = this.historySection._getMenuItems(); + if (items.length === 0) { + return; + } + + const start = items[items.length - 1].entry; + for ( + let entry = start.prevCyclic(), i = 0; + entry !== start && i < items.length; + entry = entry.prevCyclic() + ) { + this._rewriteMenuItem(items[i++], entry); + } + } + + _rewriteMenuItem(item, entry) { + if (item.entry.id === this.currentlySelectedEntry?.id) { + item.setOrnament(PopupMenu.Ornament.NONE); + } + + item.entry = entry; + entry.menuItem = item; + + this._setEntryLabel(item); + if (entry.id === this.currentlySelectedEntry?.id) { + item.setOrnament(PopupMenu.Ornament.DOT); + } + } + + _onSearchTextChanged() { + const query = this.searchEntry.get_text(); + + if (!query) { + this.historySection.removeAll(); + this.favoritesSection.removeAll(); + + this.searchEntryFront = this.searchEntryBack = undefined; + this._restoreFavoritedEntries(); + this._maybeRestoreMenuPages(); + return; + } + + this.searchEntryFront = this.searchEntryBack = this.entries.last(); + this.populateSearchResults(query); + } + + populateSearchResults(query, forward) { + if (!this.searchEntryFront) { + return; + } + + this.historySection.removeAll(); + this.favoritesSection.removeAll(); + + if (typeof forward !== 'boolean') { + forward = true; + } + + query = query.toLowerCase(); + let searchExp; + try { + searchExp = new RegExp(query, 'i'); + } catch {} + const start = forward ? this.searchEntryFront : this.searchEntryBack; + let entry = start; + + while (this.activeHistoryMenuItems < PAGE_SIZE) { + if (entry.type === DS.TYPE_TEXT) { + let match = entry.text.toLowerCase().indexOf(query); + if (searchExp && match < 0) { + match = entry.text.search(searchExp); + } + if (match >= 0) { + this._addEntry( + entry, + entry === this.currentlySelectedEntry, + false, + forward ? undefined : 0, + ); + entry.menuItem.label.set_text( + this._truncated( + entry.text, + match - 40, + match + MAX_VISIBLE_CHARS - 40, + ), + ); + } + } else { + throw new TypeError('Unknown type: ' + entry.type); + } + + entry = forward ? entry.prevCyclic() : entry.nextCyclic(); + if (entry === start) { + break; + } + } + + if (forward) { + this.searchEntryBack = this.searchEntryFront.nextCyclic(); + this.searchEntryFront = entry; + } else { + this.searchEntryFront = this.searchEntryBack.prevCyclic(); + this.searchEntryBack = entry; + } + } + + _shouldAbortClipboardQuery(kind) { + if (PRIVATE_MODE) { + return true; + } + + if ( + IGNORE_PASSWORD_MIMES && + Clipboard.get_mimetypes(kind).includes( + // Note that we should check for the value "secret" but there don't appear to be any other + // values so it's not worth the trouble right now. + 'x-kde-passwordManagerHint', + ) + ) { + console.log(this.uuid, 'Ignoring password entry.'); + return true; + } + + return false; + } + + _queryClipboard() { + if (this._shouldAbortClipboardQuery(St.Clipboard.CLIPBOARD)) { + return; + } + + Clipboard.get_text(St.ClipboardType.CLIPBOARD, (_, text) => { + this._processClipboardContent(text, true); + }); + } + + _queryPrimaryClipboard() { + if (this._shouldAbortClipboardQuery(St.Clipboard.PRIMARY)) { + return; + } + + Clipboard.get_text(St.ClipboardType.PRIMARY, (_, text) => { + const last = this.entries.last(); + text = this._processClipboardContent(text, false); + if ( + last && + text && + text.length !== last.text.length && + (text.endsWith(last.text) || + text.startsWith(last.text) || + last.text.endsWith(text) || + last.text.startsWith(text)) + ) { + this._removeEntry(last, true); + } + }); + } + + _processClipboardContent(text, selectEntry) { + if (this._debouncing > 0) { + this._debouncing--; + return; + } + + if (STRIP_TEXT && text) { + text = text.trim(); + } + if (!text) { + return; + } + + let entry = + this.entries.findTextItem(text) || + this.favoriteEntries.findTextItem(text); + if (entry) { + const isFirst = + entry === this.entries.last() || entry === this.favoriteEntries.last(); + if (!isFirst) { + this._moveEntryFirst(entry); + } + if (selectEntry && (!isFirst || entry !== this.currentlySelectedEntry)) { + this._selectEntry(entry, false); + } + } else { + entry = new DS.LLNode(); + entry.id = this.nextId++; + entry.diskId = CACHE_ONLY_FAVORITES ? undefined : this.nextDiskId++; + entry.type = DS.TYPE_TEXT; + entry.text = text; + entry.favorite = false; + this.entries.append(entry); + this._addEntry(entry, selectEntry, false, 0); + + if (!CACHE_ONLY_FAVORITES) { + Store.storeTextEntry(text); + } + this._pruneOldestEntries(); + } + + if (NOTIFY_ON_COPY) { + this._showNotification(_('Copied to clipboard'), null, (notif) => { + notif.addAction(_('Cancel'), () => + this._deleteEntryAndRestoreLatest(this.currentlySelectedEntry), + ); + }); + } + + return text; + } + + _moveEntryFirst(entry) { + if (!MOVE_ITEM_FIRST) { + return; + } + + let menu; + let entries; + if (entry.favorite) { + menu = this.favoritesSection; + entries = this.favoriteEntries; + } else { + menu = this.historySection; + entries = this.entries; + } + + if (entry.menuItem) { + menu.moveMenuItem(entry.menuItem, 0); + } else { + this._addEntry(entry, false, false, 0); + } + + entries.append(entry); + if (entry.diskId) { + Store.moveEntryToEnd(entry.diskId); + } + } + + _currentStateBuilder() { + const state = []; + + this.nextDiskId = 1; + for (const entry of this.favoriteEntries) { + entry.diskId = this.nextDiskId++; + state.push(entry); + } + for (const entry of this.entries) { + if (CACHE_ONLY_FAVORITES) { + delete entry.diskId; + } else { + entry.diskId = this.nextDiskId++; + state.push(entry); + } + } + + return state; + } + + _setupSelectionChangeListener() { + this._debouncing = 0; + + this.selection = Shell.Global.get().get_display().get_selection(); + this._selectionOwnerChangedId = this.selection.connect( + 'owner-changed', + (_, selectionType) => { + if (selectionType === Meta.SelectionType.SELECTION_CLIPBOARD) { + this._queryClipboard(); + } else if ( + PROCESS_PRIMARY_SELECTION && + selectionType === Meta.SelectionType.SELECTION_PRIMARY + ) { + this._queryPrimaryClipboard(); + } + }, + ); + } + + _disconnectSelectionListener() { + if (!this._selectionOwnerChangedId) { + return; + } + + this.selection.disconnect(this._selectionOwnerChangedId); + this.selection = undefined; + this._selectionOwnerChangedId = undefined; + } + + _deleteEntryAndRestoreLatest(entry) { + this._removeEntry(entry, true, true); + + if (!this.currentlySelectedEntry) { + const nextEntry = this.entries.last(); + if (nextEntry) { + this._selectEntry(nextEntry, true); + } + } + } + + _initNotifSource() { + if (this._notifSource) { + return; + } + + this._notifSource = new MessageTray.Source({ + title: this.extension.indicatorName, + iconName: INDICATOR_ICON, + }); + this._notifSource.connect('destroy', () => { + this._notifSource = undefined; + }); + Main.messageTray.add(this._notifSource); + } + + _showNotification(title, message, transformFn) { + const dndOn = () => + !Main.panel.statusArea.dateMenu._indicator._settings.get_boolean( + 'show-banners', + ); + if (PRIVATE_MODE || dndOn()) { + return; + } + + this._initNotifSource(); + + let notification; + if (this._notifSource.count === 0) { + notification = new MessageTray.Notification({ + source: this._notifSource, + title, + body: message, + isTransient: true, + }); + } else { + notification = this._notifSource.notifications[0]; + notification.set({ + title, + body: message, + }); + notification.clearActions(); + } + + if (typeof transformFn === 'function') { + transformFn(notification); + } + + this._notifSource.addNotification(notification); + } + + _updatePrivateModeState() { + // We hide the history in private mode because it will be out of sync + // (selected item will not reflect clipboard) + this.scrollViewMenuSection.actor.visible = !PRIVATE_MODE; + this.scrollViewFavoritesMenuSection.actor.visible = !PRIVATE_MODE; + + if (PRIVATE_MODE) { + this.icon.add_style_class_name('private-mode'); + this._updateButtonText(); + } else { + this.icon.remove_style_class_name('private-mode'); + if (this.currentlySelectedEntry) { + this._selectEntry(this.currentlySelectedEntry, true); + } else { + this._resetSelectedMenuItem(true); + } + } + } + + _fetchSettings() { + MAX_REGISTRY_LENGTH = this.settings.get_int(SettingsFields.HISTORY_SIZE); + MAX_BYTES = + (1 << 20) * this.settings.get_int(SettingsFields.CACHE_FILE_SIZE); + WINDOW_WIDTH_PERCENTAGE = this.settings.get_int( + SettingsFields.WINDOW_WIDTH_PERCENTAGE, + ); + CACHE_ONLY_FAVORITES = this.settings.get_boolean( + SettingsFields.CACHE_ONLY_FAVORITES, + ); + MOVE_ITEM_FIRST = this.settings.get_boolean(SettingsFields.MOVE_ITEM_FIRST); + NOTIFY_ON_COPY = this.settings.get_boolean(SettingsFields.NOTIFY_ON_COPY); + CONFIRM_ON_CLEAR = this.settings.get_boolean( + SettingsFields.CONFIRM_ON_CLEAR, + ); + ENABLE_KEYBINDING = this.settings.get_boolean( + SettingsFields.ENABLE_KEYBINDING, + ); + MAX_TOPBAR_LENGTH = this.settings.get_int( + SettingsFields.TOPBAR_PREVIEW_SIZE, + ); + TOPBAR_DISPLAY_MODE = this.settings.get_int( + SettingsFields.TOPBAR_DISPLAY_MODE_ID, + ); + DISABLE_DOWN_ARROW = this.settings.get_boolean( + SettingsFields.DISABLE_DOWN_ARROW, + ); + STRIP_TEXT = this.settings.get_boolean(SettingsFields.STRIP_TEXT); + PRIVATE_MODE = this.settings.get_boolean(SettingsFields.PRIVATE_MODE); + PASTE_ON_SELECTION = this.settings.get_boolean( + SettingsFields.PASTE_ON_SELECTION, + ); + PROCESS_PRIMARY_SELECTION = this.settings.get_boolean( + SettingsFields.PROCESS_PRIMARY_SELECTION, + ); + IGNORE_PASSWORD_MIMES = this.settings.get_boolean( + SettingsFields.IGNORE_PASSWORD_MIMES, + ); + } + + _onSettingsChange() { + const prevCacheOnlyFavorites = CACHE_ONLY_FAVORITES; + const prevPrivateMode = PRIVATE_MODE; + + this._fetchSettings(); + + if ( + prevCacheOnlyFavorites !== undefined && + CACHE_ONLY_FAVORITES !== prevCacheOnlyFavorites + ) { + if (CACHE_ONLY_FAVORITES) { + Store.resetDatabase(this._currentStateBuilder.bind(this)); + } else { + for (const entry of this.entries) { + entry.diskId = this.nextDiskId++; + Store.storeTextEntry(entry.text); + } + } + } + + if (prevPrivateMode !== undefined && PRIVATE_MODE !== prevPrivateMode) { + this._updatePrivateModeState(); + } + + // Remove old entries in case the registry size changed + this._pruneOldestEntries(); + + // Re-set menu-items labels in case preview size changed + const resetLabel = (item) => this._setEntryLabel(item); + this.favoritesSection._getMenuItems().forEach(resetLabel); + this.historySection._getMenuItems().forEach(resetLabel); + + this._updateTopbarLayout(); + if (this.currentlySelectedEntry) { + this._updateButtonText(this.currentlySelectedEntry); + } + this._setMenuWidth(); + + if (ENABLE_KEYBINDING) { + this._bindShortcuts(); + } else { + this._unbindShortcuts(); + } + } + + _bindShortcuts() { + this._unbindShortcuts(); + this._bindShortcut(SETTING_KEY_CLEAR_HISTORY, () => { + if (this.entries) { + this._removeAll(); + } + }); + this._bindShortcut(SETTING_KEY_PREV_ENTRY, () => { + if (this.entries) { + this._previousEntry(); + } + }); + this._bindShortcut(SETTING_KEY_NEXT_ENTRY, () => { + if (this.entries) { + this._nextEntry(); + } + }); + this._bindShortcut(SETTING_KEY_TOGGLE_MENU, () => this.menu.toggle()); + this._bindShortcut(SETTING_KEY_PRIVATE_MODE, () => + this.privateModeMenuItem.toggle(), + ); + } + + _unbindShortcuts() { + this._shortcutsBindingIds.forEach((id) => Main.wm.removeKeybinding(id)); + + this._shortcutsBindingIds = []; + } + + _bindShortcut(name, cb) { + const ModeType = Shell.hasOwnProperty('ActionMode') + ? Shell.ActionMode + : Shell.KeyBindingMode; + + Main.wm.addKeybinding( + name, + this.settings, + Meta.KeyBindingFlags.NONE, + ModeType.ALL, + cb.bind(this), + ); + + this._shortcutsBindingIds.push(name); + } + + _updateTopbarLayout() { + if (TOPBAR_DISPLAY_MODE === 3) { + this.icon.visible = false; + this._buttonText.visible = false; + + this._style_class = this.style_class; + this.style_class = ''; + } else if (this._style_class) { + this.style_class = this._style_class; + } + + if (TOPBAR_DISPLAY_MODE === 0) { + this.icon.visible = true; + this._buttonText.visible = false; + } + if (TOPBAR_DISPLAY_MODE === 1) { + this.icon.visible = false; + this._buttonText.visible = true; + } + if (TOPBAR_DISPLAY_MODE === 2) { + this.icon.visible = true; + this._buttonText.visible = true; + } + this._downArrow.visible = !DISABLE_DOWN_ARROW; + } + + _disconnectSettings() { + if (!this._settingsChangedId) { + return; + } + + this.settings.disconnect(this._settingsChangedId); + this._settingsChangedId = undefined; + } + + _openSettings() { + this.extension.openPreferences(); + this.menu.close(); + } + + _previousEntry() { + this._selectNextPrevEntry( + this.currentlySelectedEntry.nextCyclic() || this.entries.head, + ); + } + + _nextEntry() { + this._selectNextPrevEntry( + this.currentlySelectedEntry.prevCyclic() || this.entries.last(), + ); + } + + _selectNextPrevEntry(entry) { + if (!entry) { + return; + } + + this._selectEntry(entry, true); + if (entry.type === DS.TYPE_TEXT) { + this._showNotification(_('Copied'), entry.text); + } + } + + _truncated(s, start, end) { + if (start < 0) { + start = 0; + } + if (!end) { + end = start; + start = 0; + } + if (end > s.length) { + end = s.length; + } + + const includesStart = start === 0; + const includesEnd = end === s.length; + const isMiddle = !includesStart && !includesEnd; + const length = end - start; + const overflow = s.length > length; + + // Reduce regex search space. If the string is mostly whitespace, + // we might end up removing too many characters, but oh well. + s = s.substring(start, end + 100); + + // Remove new lines and extra spaces so the text fits nicely on one line + s = s.replace(/\s+/g, ' ').trim(); + + if (includesStart && overflow) { + s = s.substring(0, length - 1) + '…'; + } + if (includesEnd && overflow) { + s = '…' + s.substring(1, length); + } + if (isMiddle) { + s = '…' + s.substring(1, length - 1) + '…'; + } + + return s; + } +} + +const ClipboardIndicatorObj = GObject.registerClass(ClipboardIndicator); + +export default class ClipboardHistoryExtension extends Extension { + enable() { + this.indicatorName = `${this.metadata.name} Indicator`; + + Store.init(this.uuid); + + this.clipboardIndicator = new ClipboardIndicatorObj(this); + Main.panel.addToStatusArea(this.indicatorName, this.clipboardIndicator, 1); + } + + disable() { + this.clipboardIndicator.destroy(); + this.clipboardIndicator = undefined; + + Store.destroy(); + } +} diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/ar/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/ar/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo new file mode 100644 index 0000000000000000000000000000000000000000..75db88b8a0614489c2a688dd6d0ddead94e7a651 GIT binary patch literal 1058 zcmZvYO-~a+7{^D!xAhf>CML!V(SsfawipO0)(8lODB55(Mo-Ij*e==5)ZHoABTxjn zdGG@m5_yTx5Y)iIZ{W?#%*D^*f4aO}ob=h>Jn#SM-}cte1m`^L228=)V83DZTsc8V zE7%5J1h0V|;O&EWKiCdG2RdLLyac`lFM!kFb#NBE41NON!M?z3zN3YZs~`n$fqkHj zzX3bJ0JQxd54Z~2`d?r-_y@cRwm>9C61dHC_JHTW)37tJQwK2%tJS{CzWeZ;w3i{N zN@O_VUQA12qGVhn!?F_cB2|+5QCOLjUR)$&z7*sMuQffIlw_>@s!GT+{$5co6x#AJ zp9y6(Ida1pPjOn7MNVFn>!jTPA| z9w#Wx(m=>EXGw`i5yL(%Y?vL1W1%S8u`+fhcm>MXcoKv~HdGA~R*+fhx&A8c&%iKQKb)I^OVujcfNo2=m>0qghe`2s!_5{7}S;whfjz0s2Hx^#aZc+$&?>p`WF`=Zueyj~u@x)8#tDQs5ETRI5OW!jrlykr#>q z>X*DY;VSpCFv|Lm>#U+F9kEbX5Rm6MOEu@|vX8JCkR6_uu&@gQSPG6OmycjCl)8%&cIm9fOx?VF&W>K#Ze74LogoLNb0woo~z_w)wQt7gT_n`M;1S<~BkQ~v;W?1Z#=!K~8Qa^J!*T|J3_w$FANnwqEaKY`c5eefQ52;KsZ!2{?I=w;Yz}GjY0}b zedWBdL$+ZCN@nB@rG*-Fjj$QpNsVT&<#23~>l^FS$~*Q}e(@x=$F;VV2g+1#W~u|> zC6y+V?0w%Hk~UuTR4TkOn((D&Fk~H*=Y=HSN2JshvY36DdcKqKJ!u_66UwzIC3k&U z6dd)W4sJecjw_vow8loea)8cw%jGKL%_?`iV-kwm@Axj78(-T*QwgoawK|WV@LEi@ zIH|=vUXA1Ejc}fKtkA9q=@MGcOwlv+N(@U$O<}FVtj5u^p7LhD`=aVr+E>e|DWBuA z&MT2i-jSk2(NjC2R791=)w{|^?V_@x;M>NQE+IV_o3634Mw4UXp{At<)gG-yANfnErJan{ zHZoJ5R+i)C)rhT^-E4H?ShG@@pyJ=rx5+jrQLy*H65+X&2#-y&pzxaox6+HEaO3R| zY}3kATIndmd&(5QkZoZz1QI%#kA7j=nL}WBtBhqWtOpHj*cF@|!l}|?wkxR`orIF( S;ufn}mX#Nw18mFaC;Ja#Lj;%r literal 0 HcmV?d00001 diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/cs/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/cs/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo new file mode 100644 index 0000000000000000000000000000000000000000..0849a18e5b023352a6cac21ce0ec9dad4bafda22 GIT binary patch literal 2548 zcmZvcOKjX!6oxNQUg2FRP#*0qP}+((O{SGvg-4o(Qlc=)sF|jy5{o>!N0+AaL)sb?En|RhrkB-Ab1WO13v+u09|k= zn1QtKbMRU4OOT%buDX8>%it17@jdWK@FMs) zxB>13uYj+D*DL-G(sTcUyTP3h&B1Yybk)J9Kmoo4ehNMfUIIBh`39`v{3eRAz}q0z zwHG3ZZ-S447D#c9fu!$rb$%WsU)Dg1e*r{D_7zC^FN3dxJ&<(%2A%-_0V(dpBaFQO z9tZb;jfw_*9_L?x&w<~Ar29I!AN&=hxVOQ5;4TEC=Z}F0z!i}4T?9$j6_9lQ1d>m` zfX{%pK-f?&x=)(PU&;kb%NW!rS`?ppL3;35IalP-aG|8qd&)

Ut4Bj2MH=G9P(G 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 yEOHc19v6-`r5-YBn literal 0 HcmV?d00001 diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/el/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/el/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo new file mode 100644 index 0000000000000000000000000000000000000000..9ef3647e1cc19728a478cd399b4beea6d44af391 GIT binary patch literal 1053 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/nl/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/nl/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo new file mode 100644 index 0000000000000000000000000000000000000000..f80e11bccee14bf8c352b607cc452792f9795e95 GIT binary patch literal 1886 zcmYk6%Wqpn6vn4e9yNtRdB3D#QAmZZW4lxohtTHL5WuaKn1m(c>*MQ*=gwF&bDgH_ zvO;V?LTuP|M+Jy~012TikXW!nELpKYVoP@{_>O&@WTd&j`#5vXch1bYf1W@6mB4r& z^Cir`FfU+!cM2DbYxfE92Dk*C2FKuo;3wb%-~m_xKLsBJzbwDs0iVS4Hz2S35j+q6 z3`+3#^7}pTH9ViWUx`Rs0$&7IL0vGWa>j+P?=S_zTE!{|3*2 z|AFrzm$MMj!1E47v+mDuIRkzHaz0;!tp5(k>wf@Q$05jd`3+?If51n;dtd^d!r~{u zd61u%KztGoWSs^0DtG`s4c-OW?@u86`4!|m{s3P9{{=6A=THpRaT$CH+yHq?*!MG- zoEv|i4J@0~nSIX&*AwZBhcTauH4U@sOo(<0TSxl}w53Uan&X;qW zj3rNU-xV?x<;|Y5QAQ_wWGIqU#>mv@QP(NolR8o!u8L(B2VzCpl#Ez0r2O>C##hVR zfez96eX-(3ntCPJ?~yaHkvc2hr~RqxFmOJmMGT^)ZXH|6(vb}MHp#h`UcNXnutnY_ zFI|7CNwnOU8j9OxkaBg~>#>4O?s_CPDQ6vf19mb}DeC84VS6$noY)$;J!xIk zeVr=y#f~9#Uv!YAbR+W9*Qv5FlUnv48$7Pw}1WyDj-*IL+*#g4cE;J?^y=0qw zfd8hfY|;pIO8_tU`WMHfoJVQfnlQ%d*~RUV8KOTD?}Q ze1QCuj#oAqh5c;GcS$3d+={N>Clln`a1vD!Lb%q4ZLYMbrY?U6D{WS&jFJwi91&;F zO=(S~wb5ET7Fn;>Dl5)LY)8`BA3+S4Ykp*uwhJ;HC?6;;e%QI5TsgL5O?~ponoV6# z+pHP2rVEt~;Lxgc?mA!(wAWPHz|ItbAor)-NtpK1$Zfa>+Tbwxgbo@=~={ ztyjbjQIv(eO+%cqp5l9)ZIVMndDnSZq7USrHW8N)$f74m;{YsKTNv_W#m?ltSi&;4 zoEoiDNC)T#ve@tGOR-KvKC9$=CvK%x%F6*%%tDH`PNU1b%Egu?6CFjwtmyc0x ziOZ3Bb~4hOwsO`vRjJV7%9MSxhPcQvI}$-<&vbE+A;Ldm6C7#D3Genr+o<6XZ>%a?X;P$Zv|Bdy!+msk7hO~Y)Gqo$ z=jbZ$&;U8k!w?$Eg0;x-hEFB2L$2T9w_Nm9Y~c`^Ep(!}GU{;#d)P6A&L~%E7^`TM YqflIo`9eq0LI+Jf&X39C*8=(Ce^eRtHvj+t literal 0 HcmV?d00001 diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/oc/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/oc/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo new file mode 100644 index 0000000000000000000000000000000000000000..29f84762d748bdaef6976edfb1388c0e69f53553 GIT binary patch literal 1311 zcmY+DKa3ki6vijHK#0SiBqWLufAl0u*gN}@A~HlL=QyGhF&uZB2+_^WzOx6dXErml zKAb9LQlv})HAqK=bR`ANc1TF{NEGQP5hX(6duzuhMjrom$8X;I=FR6nE}eO*FfL-g zh4~%kRm}HK;S1vtcpZEUo&kRbUj!TQ1@ITJ1O5iS0{#xZ4*m&B{J-EO@bu|<+!gRu zyc75`xCXureh9t+ehCtoz}LX1;0khl4{qcAhZUvHf`2dg&w?u`Ch_M%ssAP@dDlSE zI|MI-Id}oIpy+x4ijF7XI`}R4F8C|>4)_-+dN1IxkfF|FN*&25d87yDFkiwHUD8KL zS`NuALwY3rk%6PMIg$Eg3LlNv(dt-k{&L?|djZW4(!pHb(rgCT^ zUvZc(8|@HA`%KvD9j~g~k47a^8lw=q8Y28xY*MyeWcN5k{dOB zS?;mC^I5hF$r6-(1gU*m@2>Zm zZH|IQZxZ}c1v{|;H;Y@K(&MU1mYwXH7wyupImQ;82Od*<&kxofN&JP3`z6dCNw=b~#|PJ*GBroA+yGWXx8Wnb ube<9CaqB#bf0P>BubX44n&U*3j?^5*$bod*i?pf+W#ZtF108IBQvU%?!evwd literal 0 HcmV?d00001 diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/pl/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/pl/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo new file mode 100644 index 0000000000000000000000000000000000000000..ea757847b08afa76ecfafa80849accb1482f31ae GIT binary patch literal 6882 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/pt_BR/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/pt_BR/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo new file mode 100644 index 0000000000000000000000000000000000000000..b1e6f9957c0cbc00d5a8322109f0b450f669b638 GIT binary patch literal 2789 zcma)-O>Epm6vqczzC!tGDbVuu3I$RrPPb_fWlLI;&{kEm$tqn47gWvKlif+|8E3{$ zvN<45NEKA#f8r4A-02O!MnjGco#Si4uNlh4}e{8 zC%6K#-34$L_yNfCFIVGVf`s>PtMQ+~m+*cAya(I^V_tV3_#8L_J_*i(y#5mSAow=8 z1H1@63w{DV4PLAG6Ug)a0QZ195j24#Ajj1J9|9$~7hDD(2HyvNhbEtb(|Estpk4%j z1o27y3Em5C!w=`V1LQb%fscR$?gz&}&SxIv`R75le;4FDE`ra4pMf0T_aK4SL4+*+ z0y&=T_m%qW2HAcbd;*jpkFSI5|2=RR{21hVepkKU0LSpY7oxJB4UpqWL7w*!h(B=| zKdj?7;BN2-5Hb`O$IF_rzC}zRYs>TadImSI^(bz*EA|!VI=EN|!p%DK^*CljCI*J}DS0>9pxkSNqJmo|v>*S4>G0 zs#r|%VEM}{pQs)Orc)b^uyUqM=EamvwTdcmTpPiNGcXcoRj&-qcdg6998(%{ZPRKH z26?@%Xt<##4-^I_Puj|nT_|%B4ZET!b!tW1+{Znn#(v8|cAIq-kct_l@#w&{j1VCb zoY`0^>`EuYOgT^5kk4e6duk)&S%W3Lgo_2aVjX1kg~r-1iKg_>83KAJ9?9n<`g+4yAz^wgbVdHEL9}Y^M{mx?9qy zzOk%{GP#)V$|4k$@#c)?HP%_BRUKtr8h+sbEojz>eA9LGe{$z?|cEhUpWO**-*V=WCe z5l-I8l~?~`cAe7f{N&tF<4oh^W{!tOYeQ4z!3bKt6v1-=52mry<{0tj^C}yEy>%)$ zx@pHbEGQS8G@*@fF6(r(t+SywgVV}z$vC;w*+!;xqy{g|ICnA*PFa`uI+?V1_2Xm5 zsC*nBHgud09l$b8t0J8RlhUhHX5BiCjvg%jHpj|s)x>B}LuO7b_|ZlGuj7a6gp`!w z0*B;Wtagj${u zC}Jl!nRJ9Eha-puW1XaUFV8^#s?(tr6G_{m^As%$hj+R#sX$pCS#-cub})?(PBp10 zO#fPh&UxDiiZ&)AJ0QWeq}!YKFQUaMF_Fo*iv+QDroxyPXvsvHuBD`ZX-y~axix{R z&CT?$CCY8FQPlsMH7c83`CK$Qtmw;F;K8O=%*aUMnDwt>aVU*8#~%g@LbkAKbP&-< zZgI%N=QtJGIy9Mhg*8rTcNQ65rUfUbCAaAhNRSR)#37CvJoNEBug98yh{eBrQ1=;Xha*=~`w7F|)#W z-5L?cyrs@f<(G5q5&S>C|5w!;) v_lN{hDjZ>*Nfd62787lU?O$}y+2UX%13$M^WLje5LoRI1MQKzli$3}{gEkdp literal 0 HcmV?d00001 diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/ru/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/ru/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo new file mode 100644 index 0000000000000000000000000000000000000000..8c4c35008c8be3af5d4581e6e3141b0762e03073 GIT binary patch literal 3336 zcma)-U2GiH6~`}yw$%Lq(ogz9Z}_x}$Lu-*Sx%fZarlUWiIo^qA@yOrd+i;vJG0Ep z+A%`NAx%>hlh9TO2>}tJg7yKi32|afvU%X8k9S_GzO)bROX)*YUU=^B+?n-Pg2a_( z|8wu0d(Qv-&$)O1x#^}?6=O5cPkHXXL8%A8{;%-E_z$=h{1og5@3>K^o4|eG*T8Y` ztKcE92mCeoZLkJz0%t(6dkOq5_zEcd|5SYc3#gf2D!%^%ypQ>3;Mc+1F&4j@!8^fz z@F(DYQ2ZYTzXQGiehYjV{5kkK_%m?1z`ujC?{n~Wa1*38*bfS?G4K}92Y(Dc3*HJY zfXC6~P4F4!cR}iD@DwO{eF}=*{|da}W~F|_d=n@g5vML;G^Kp-&D#2$H6_| z3*axnPe7^bRq!G3E`kh$PlKY*tKbgsgJOOaLrwaP<=nx`P&SL-J9( zVx#AxmQGs5%d8)!I*oMI)J8H|k|2jR4?1xXskb82^#*3_sUR!m)u%E$ayJnt-CyQZTnf$*G~Fiq5G)C^3u z0O7SlM(xE&?K5*ZwWJosX{E)MG5^S_RhNcNg6E9x9Se5%IfJm(m^87Drt+NBSTtjF zGd43p)Jp1eq$Y9mj}kVm8K9}2FafVIcqG*@B*7{n6~CprjqlKN>Y(UWAn zKg4C?6JpAriDG0-oW?|oG(BAxb=Ra(tck1*6SmZ$n(wGk#*G;r z3h*BO6eSRUW+9iXUU zHgA|QmN;0s^Cq;ewfsK+ICp)s++@!A68>MX6g+; z)Z1A((s1uXL%*DE_(6T35;b#J!5;v9$sZx9w_yU=B@2b z%r#*nC3v7&_k(bomi9?ZI`ZViBi_)eosgO`vG;ITiK?8eVLdb%q&?&HR_xckN20iq z4C}Dzrpd^bZ90D)=?zCl%6g|RX+GamF6|f@)c4-&Ubl~ImDgTbE^RND^z+ZVb+IY+ zv8T+Q!M<%hzw#b6p;T5R8IMdgNcEP1(m=UK**|0_b#~02vy0ihU9^|8ll?k7k-ca; zIy-JVye#o7ip_c3wjawhThR7gc2rhhW@XV{aywtdl|7ps%YNgWwYBZov-vmM-l^={ z_6jlD+3)NU@!GkcD;S+8LQdcUK_Eep)7oAsIGx8)c7nB|*ezrKkuD}3{4LofW#1xD z8^FME$yv%mcb}tu%|ej}orea5|nm0fnTZ2Ia-D=ZI?C8VLwv#Oi@CK{ZdhKN9Qu% zX`iFlkx59C+^Q#7qCwsv*>Ppx%!Aqwb48^A&(n3MTm<@NWoNda?CaNIS!e`d5}-O# z07=2sYB}fCaXEFe(|I>a={ni*oI4e8X8=~3rl2uNgKRi7PIi<*XLy_~a;5fmQO=s^ zlvm6-kn>PvcR|!)Q$F30#2M@)$&)To5prXiVo{u`)v7OA*Q?6DlQVIwi$oVmeK|Wu z>XM+n_@y*>XZ;-@^yP@-OO8Gwa#KhP3a(QOjfz`?6vUqAa+m1JCUGHZvVG-bpCN)1 z*nM2RUFeWCw~PF@ktLT1?mM)49>ATFLgZ`Dqq{lYhZrizS(O{Go&A<@r=;T~s|9;e zq?T)N&Al?P+SnXH8jmC3RD_g^1`(;Il$|D(16Y(oE-A@q*`3JZ3Ur?3j(JZKmwT@; Zpx!I@H)g&6#iL+Xr6u~D+LR4C_- zorw)oE6&Lq%1Tx58EJE|nHep%c((M&6~_9k@lIUhPdu^q_-tT#s!Zelan)3Mrpn|j zcDXWBro%#ccD2lyq&1C36MQd*g*hZ`yxLcp^vY<$FEjN_jLdjkGWh~^SU2cl4mk7C zW*%K<>yVmKXP0ZpiMHKX<SNzMqnt z+#GIowl}x0oT^K@ac9G5k2{J+vl=SG$Zyo8RCrcRyY7Me-*}P| zMr0jsKIDj$6JErB=Lra_tXRu^3KF-QKolDs}rE<%V2V*!D>C&)LqxcsWU$%jKVCDsjW?SviMxYHBk#R&!8aTSfnp QeLHlnb>lY}T7{`aUKr!z{1tC%CL0mwvu495!ISFZF+7K0kl`bvfg2uZ~)`MqfRx`5+ zn=b%-fO6ns4j``NQZ5k}I~Pt!91(ElLm=^=I>aN7fAh>dFaKvW``4ul_XWoDn6F@d zkNFbjTaVxm<126#{2IIf{sKM*&cH{(U%?jm8~8N%2lxW`4;cOb1uuc;&Mo{dgU`Sw z@G0;rcm-SsUj{z~2@K$~;C*m}96x|P*cXvB#`VFdV+cMD-UBa#pMkNL8jN+n1f$Mx z!56{rz^L=*Q=WiN!2TUX8Sy9h8u&LD^_)j=?B@y?&!5DMx?=6;Fk|i*e-Sh4h_%mS z{y(DDIN}|}x^bYDhj)My%W=aoHMS;SpDqrh2^1{ltT`}Attr#08d>RbQh`gqDYk4F zi@r1&Yth#%-QqtwZ=Np4$_MKj(YKZ2JSIHLB6c!s#BFXCIsMqWkk!G95BZBgYy!fD zj`x+V{h6uuCDfEQXR*tr-Df%&LyfOw29eIz21O7r?u_jL85`7|%4AU15Pq4h8j(U1 z*^1p_JCg=(G-5)mHQW}dEeg#P#wf(5h6wvPXCc|ile_FZa`mXS%N6t{17D~-*{Tbl zq!!!5kCI(Tj!wyYklLeer`t(ZI!SjG%icSkH#={4I;{_+4@v5z@mk`7dNioVl5S|_ zJ*}5m%ecH=lv3$-X3KXGVTxK7oTMzv9;LOLC_kok!;RrR_SfrCZ9<&{*ESrM{yhq= zHP{*4T#8?5cUpaG0`53Tn+hW1m0hj0RA!B`v2>op#>eTcgDU_Ql@bIK_4N|!t#JR$hbr`S5o*$|h?P>DN+y(!*jM+#S=@KnY8rz%J- zQn(yy##x1ZJl1F#1R!ivZ1e2LQk_iCPQ(zE8%Yy952>Ze=SRpiiLY~hq~(6YG?^cH z*;uWm!{HrkoSY~sl*^TveS%L%lW9;+{0H}zZGiv) literal 0 HcmV?d00001 diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/uk/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/uk/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo new file mode 100644 index 0000000000000000000000000000000000000000..f42e7cfdce4e47043dfa751d4c3241824e0f4ed3 GIT binary patch literal 3405 zcmai#U2Ggz6~}L&Eno_T^8J0ONU%wFyLJ*aaqNc1q$L8kR^n2s3c+~ydUxvGnPq0y z_Ckg18ude}T!B?}gehNMSH5`JH*LUE3@cZy2oQI!=ZTKPhN4S7C ze}$twKSy#ya1Ba*|Admyf1s?t@4ZTW5^5;Bp~U+glyz^xLHKt#3=h9wsk5*MPrJ%-zm-s;Q`&ef{`YE32URcXr(tq`6 zWOOrZ=(y3jnel=|C!sExs!5FYs#RUA`t^y>i%Qy0Of5d69*w8;WEkmU7)<(6%}e|+ z&=ZCs!(!t_eo)pGKTg7^sm_K;MU8kt(NxulOlE)OKmq+r50DhC@D7BGV0B4uj;0uWB&(6_m28!``m({QJXN44kxpk)Mz+u zbUiZDe%OesO-hrv*`$O`DhAZm6UOH@g0qPxLkfIi>!@PIi@aiDqFDQZjuS6w#Cn2? zpA0xM_6RZQO@|RO#!h2Do>F68jLrxcqj*uIRAZeyIswKaf7)YBEi4&z!PF$xQaBT6 zFN(sMJ)t`DGZhI^_lmSI3L8O5CuFQHN)A~ds*@qfqpSZ#x>Ky|7w^w=SJlM|p@J~+ zC;ej9a8gF4rW$9v4(le$hKpXnGVGv<8U`k>#=~;CD(dbiqcbM5)=bb)<55#GK^?#3 zjui{ly*Qo;qf)GUrvfRs9*Qe`tkcvJjdHo!r=G5Ot_J$QSBj^75}465ekrNw-ot(9 z8a{`_x$~vmw`dYl59*!^rjB!RqjA|U<<2(Bac(?hc7Kua$iIJK|N3yIFTz1 zs6D7XY;X%rv0M% zYPsh5)qF9mJ;KJI-0;dKH*UNd0lUwGx-r!=dVciWc8>k|LeEI{M&!nub#jr;JyfrH zesEfg>td4(KQ;bX?&S75NomqVxpP4=ETQJ0J~`nhJ!92I2-> z>+E*8Hy9r7*JpG=Gkv(f@W}9hK61p}4h8q4Q2ir``0~BG> zS9f%rsTE3a-E6tYuWa|FlkXm(T9PMynnvvfq?gSc)-aFJVS}$@KNE>0F7c&b{q)K|6CxUc_Bb7g@h9 ze%xbo=S6x(i&*uV_)bfipR{Cq3z_WkocLlReHkI!`zH@Jr2O}io${93o4eh#$uKX)j&B62qaG#| zk(pT(AxRFImA#rS$_FSDe>r_o=A~$y%J2>8HOnbn-m6`ol)Su`|F11=cYy44{lD#u z!Z+kimlPkq9m#h-k}|l%eEYM;JoM1@DRXh}&RoTYQXhWhef^DmSVWS&N=aYcIj&ac U{9og6smIO8cctxa<=^ao0ZVPYkN^Mx literal 0 HcmV?d00001 diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/zh_CN/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/locale/zh_CN/LC_MESSAGES/clipboard-history@alexsaveau.dev.mo new file mode 100644 index 0000000000000000000000000000000000000000..ee405d98ee2f5991e18be7d2bc33a55f635b4d28 GIT binary patch literal 2567 zcmZvcTWl0n7{`waUMh;>6)$)&5J5X-w?RS{5NNrCU||DQNQ^Pl-P7HH-I;Y}wxv9n zEn3o!VhX^T);p&(r=G^MRA=##$rqKOacgZ9ksJ`rQ^RsY}4v<2j()8EWF-}V2U z_V)#IIvLhtJTKr`at~uG!18jM9)AJJ?;mg>xBx|SupFeiYQXuR3O)zE4?YHd4E_vFPJ*xE+d@$k=W~$y z8u$2}#~;9FvHv4T^%OvgdlP&JockbSPl5A6@?Q$lemS@VTm@2J?|_Rz9VELBknFm^ zMPNTjb$km_zF)obn;_-=7bO3Aa3WTKWFG{{J_b@9E#7x8NO^}r^8X&BJU@Y?&)*q8_Ffq+lEm=}hYxJX8~1Ih4&F$3u4rlCy{L zOfQ=2)6*Xy&7BtYL5p;zUZ)qXP-)S;*+x_FCL_tMr1#RO>Nd9x9u_fS3$DguJQULs zb%tt&xn_&FwVrLX_Vaqfg=%Vgghw^YHq0hgW!OX}4{Yzy7CShbPRMA!q>HB$`R1|zmzG`Xc&QNy%DNx0Og2WG9N z%ixxFQ1E3nTGjHBfu4-l36mT3ZcnVnXb?PMiU!R{TCpayMtR*sN^3_2kYGE6hF23j zU~^Q626xPa3Pn{@4cWr9xTbT(KE|9U(0hDeBb*!-R}hNn_30&w{E2ogttF z#X}?|3wm{U34*59pu#3@gaz9r;#6wbXw}h z8r($aj<%?dY5jL&I-%(Q#rq?5j_r;jm2TKty%ur@j*_8L6RU+gHxj~hw?nE9Gh7Fb ztcETsSgjF>#7Ny++VJ*B*0|7tKLyo zv#nb#D641fsPuYaDx39?5k~C+ zzPe7c%Yw0_sm7Eoh8ed4Tu+oft#$r2+)*+TLoBvd;Z;1#o*?ga{BTk!?x&#B(3*k|4K+#lfRLg+LKvSIoDoXqtU&Na)n)5Q~6 zC!KQo(lULqFg)y>yzZRr&-dnJN7s#sQ_LA{$zK?kpB$E%%QD>#ALr{s@Gs=DOtuUZ zKOA5(bKDteBTK~c;D#*ZhU8@DT_UlWWb$x}%#AU5^|H*i%eIl(H`z8cW5s0eHRp6I zlh?m=+OHOmx>hWIWW-4|JEt>F_qBX>Jm2$?9PG+pIyW6L zb8+hWNjcc<92+TgWe^UP=g&6hd%IcwayK-UeFMc*+Btf`Z5H_v-K&8Wdph#xo1KAv zIgw!~1RBmHoEjZOHdNvqP3LoiGCS;9ILBL^zDrZ1*}}CkSYSf=6O(RL%qu3HqMRi= zQnI8Hb2|F+!)In>b$97sjw^-JHRASIO!eeXeZic;tn*p>)MOt7k(sZYPqP^GpQ%Gk xwzWF#XKpK9PNZbZl|rUZws$ggd`A;*T<7>$sm+UBGILhuCgu1!Olv94>ObgVg^mCK literal 0 HcmV?d00001 diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/metadata.json b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/metadata.json new file mode 100644 index 0000000..fa25d4f --- /dev/null +++ b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/metadata.json @@ -0,0 +1,16 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Gnome Clipboard History is a clipboard manager GNOME extension that saves items you've copied into an easily accessible, searchable history panel.", + "gettext-domain": "clipboard-history@alexsaveau.dev", + "name": "Clipboard History", + "settings-schema": "org.gnome.shell.extensions.clipboard-history", + "shell-version": [ + "46", + "47", + "48", + "49" + ], + "url": "https://github.com/SUPERCILEX/gnome-clipboard-history", + "uuid": "clipboard-history@alexsaveau.dev", + "version": 47 +} \ No newline at end of file diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/prefs.js b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/prefs.js new file mode 100644 index 0000000..5868498 --- /dev/null +++ b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/prefs.js @@ -0,0 +1,444 @@ +import GObject from 'gi://GObject'; +import Gtk from 'gi://Gtk'; +import Gio from 'gi://Gio'; +import Adw from 'gi://Adw'; + +import { + ExtensionPreferences, + gettext as _, +} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; + +import Fields from './settingsFields.js'; + +export default class ClipboardHistoryPrefs extends ExtensionPreferences { + // fillPreferencesWindow() is passed a Adw.PreferencesWindow, + // we need to wrap our widget in a Adw.PreferencesPage and Adw.PreferencesGroup + // ourselves. + // It would be great to port the preferences to standard Adw widgets. + // https://gjs.guide/extensions/development/preferences.html#prefs-js + fillPreferencesWindow(window) { + const settings = this.getSettings(); + + const main = new Gtk.Grid({ + margin_top: 10, + margin_bottom: 10, + margin_start: 10, + margin_end: 10, + row_spacing: 12, + column_spacing: 18, + column_homogeneous: false, + row_homogeneous: false, + }); + const field_size = new Gtk.SpinButton({ + adjustment: new Gtk.Adjustment({ + lower: 1, + upper: 100_000, + step_increment: 100, + }), + }); + const window_width_percentage = new Gtk.SpinButton({ + adjustment: new Gtk.Adjustment({ + lower: 0, + upper: 100, + step_increment: 5, + }), + }); + const field_cache_size = new Gtk.SpinButton({ + adjustment: new Gtk.Adjustment({ + lower: 1, + upper: 1024, + step_increment: 5, + }), + }); + const field_topbar_preview_size = new Gtk.SpinButton({ + adjustment: new Gtk.Adjustment({ + lower: 1, + upper: 100, + step_increment: 10, + }), + }); + const field_display_mode = new Gtk.ComboBox({ + model: this._create_display_mode_options(), + }); + + const rendererText = new Gtk.CellRendererText(); + field_display_mode.pack_start(rendererText, false); + field_display_mode.add_attribute(rendererText, 'text', 0); + const field_disable_down_arrow = new Gtk.Switch(); + const field_cache_disable = new Gtk.Switch(); + const field_notification_toggle = new Gtk.Switch(); + const field_confirm_clear_toggle = new Gtk.Switch(); + const field_strip_text = new Gtk.Switch(); + const field_paste_on_selection = new Gtk.Switch(); + const field_process_primary_selection = new Gtk.Switch(); + const field_ignore_password_mimes = new Gtk.Switch(); + const field_move_item_first = new Gtk.Switch(); + const field_keybinding = createKeybindingWidget(settings); + addKeybinding( + field_keybinding.model, + settings, + 'toggle-menu', + _('Toggle the menu'), + ); + addKeybinding( + field_keybinding.model, + settings, + 'clear-history', + _('Clear history'), + ); + addKeybinding( + field_keybinding.model, + settings, + 'prev-entry', + _('Previous entry'), + ); + addKeybinding( + field_keybinding.model, + settings, + 'next-entry', + _('Next entry'), + ); + addKeybinding( + field_keybinding.model, + settings, + 'toggle-private-mode', + _('Toggle private mode'), + ); + + const field_keybinding_activation = new Gtk.Switch(); + field_keybinding_activation.connect('notify::active', (widget) => { + field_keybinding.set_sensitive(widget.active); + }); + + const sizeLabel = new Gtk.Label({ + label: _('Max number of items'), + hexpand: true, + halign: Gtk.Align.START, + }); + const windowWidthPercentageLabel = new Gtk.Label({ + label: _('Window width (%)'), + hexpand: true, + halign: Gtk.Align.START, + }); + const cacheSizeLabel = new Gtk.Label({ + label: _('Max clipboard history size (MiB)'), + hexpand: true, + halign: Gtk.Align.START, + }); + const cacheDisableLabel = new Gtk.Label({ + label: _('Only save favorites to disk'), + hexpand: true, + halign: Gtk.Align.START, + }); + const notificationLabel = new Gtk.Label({ + label: _('Show notification on copy'), + hexpand: true, + halign: Gtk.Align.START, + }); + const confirmClearLabel = new Gtk.Label({ + label: _('Ask for confirmation before clearing history'), + hexpand: true, + halign: Gtk.Align.START, + }); + const moveFirstLabel = new Gtk.Label({ + label: _('Move previously copied items to the top'), + hexpand: true, + halign: Gtk.Align.START, + }); + const keybindingLabel = new Gtk.Label({ + label: _('Keyboard shortcuts'), + hexpand: true, + halign: Gtk.Align.START, + }); + const topbarPreviewLabel = new Gtk.Label({ + label: _('Number of characters in status bar'), + hexpand: true, + halign: Gtk.Align.START, + }); + const displayModeLabel = new Gtk.Label({ + label: _('What to show in status bar'), + hexpand: true, + halign: Gtk.Align.START, + }); + const disableDownArrowLabel = new Gtk.Label({ + label: _('Remove down arrow in status bar'), + hexpand: true, + halign: Gtk.Align.START, + }); + const stripTextLabel = new Gtk.Label({ + label: _('Remove whitespace around text'), + hexpand: true, + halign: Gtk.Align.START, + }); + const pasteOnSelectionLabel = new Gtk.Label({ + label: _('Paste on selection'), + hexpand: true, + halign: Gtk.Align.START, + }); + const processPrimarySelection = new Gtk.Label({ + label: _('Save selected text to history'), + hexpand: true, + halign: Gtk.Align.START, + }); + const ignorePasswordMimes = new Gtk.Label({ + label: _('Try to avoid copying passwords (known potentially buggy)'), + hexpand: true, + halign: Gtk.Align.START, + }); + + const addRow = ((main) => { + let row = 0; + return (label, input) => { + let inputWidget = input; + + if (input instanceof Gtk.Switch) { + inputWidget = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + }); + inputWidget.append(input); + } + + if (label) { + main.attach(label, 0, row, 1, 1); + main.attach(inputWidget, 1, row, 1, 1); + } else { + main.attach(inputWidget, 0, row, 2, 1); + } + + row++; + }; + })(main); + + addRow(windowWidthPercentageLabel, window_width_percentage); + addRow(sizeLabel, field_size); + addRow(cacheSizeLabel, field_cache_size); + addRow(cacheDisableLabel, field_cache_disable); + addRow(moveFirstLabel, field_move_item_first); + addRow(stripTextLabel, field_strip_text); + addRow(pasteOnSelectionLabel, field_paste_on_selection); + addRow(processPrimarySelection, field_process_primary_selection); + addRow(ignorePasswordMimes, field_ignore_password_mimes); + addRow(displayModeLabel, field_display_mode); + addRow(disableDownArrowLabel, field_disable_down_arrow); + addRow(topbarPreviewLabel, field_topbar_preview_size); + addRow(notificationLabel, field_notification_toggle); + addRow(confirmClearLabel, field_confirm_clear_toggle); + addRow(keybindingLabel, field_keybinding_activation); + addRow(null, field_keybinding); + + settings.bind( + Fields.HISTORY_SIZE, + field_size, + 'value', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.WINDOW_WIDTH_PERCENTAGE, + window_width_percentage, + 'value', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.CACHE_FILE_SIZE, + field_cache_size, + 'value', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.CACHE_ONLY_FAVORITES, + field_cache_disable, + 'active', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.NOTIFY_ON_COPY, + field_notification_toggle, + 'active', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.CONFIRM_ON_CLEAR, + field_confirm_clear_toggle, + 'active', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.MOVE_ITEM_FIRST, + field_move_item_first, + 'active', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.TOPBAR_DISPLAY_MODE_ID, + field_display_mode, + 'active', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.DISABLE_DOWN_ARROW, + field_disable_down_arrow, + 'active', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.TOPBAR_PREVIEW_SIZE, + field_topbar_preview_size, + 'value', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.STRIP_TEXT, + field_strip_text, + 'active', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.PASTE_ON_SELECTION, + field_paste_on_selection, + 'active', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.PROCESS_PRIMARY_SELECTION, + field_process_primary_selection, + 'active', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.IGNORE_PASSWORD_MIMES, + field_ignore_password_mimes, + 'active', + Gio.SettingsBindFlags.DEFAULT, + ); + settings.bind( + Fields.ENABLE_KEYBINDING, + field_keybinding_activation, + 'active', + Gio.SettingsBindFlags.DEFAULT, + ); + + const group = new Adw.PreferencesGroup(); + group.add(main); + + const page = new Adw.PreferencesPage(); + page.add(group); + + window.add(page); + } + + _create_display_mode_options() { + const options = [ + { name: _('Icon') }, + { name: _('Clipboard contents') }, + { name: _('Both') }, + { name: _('Neither') }, + ]; + const liststore = new Gtk.ListStore(); + liststore.set_column_types([GObject.TYPE_STRING]); + for (let i = 0; i < options.length; i++) { + const option = options[i]; + const iter = liststore.append(); + liststore.set(iter, [0], [option.name]); + } + return liststore; + } +} + +//binding widgets +////////////////////////////////// +const COLUMN_ID = 0; +const COLUMN_DESCRIPTION = 1; +const COLUMN_KEY = 2; +const COLUMN_MODS = 3; + +function addKeybinding(model, settings, id, description) { + // Get the current accelerator. + const accelerator = settings.get_strv(id)[0]; + let key, mods; + if (accelerator == null) { + [key, mods] = [0, 0]; + } else { + [, key, mods] = Gtk.accelerator_parse(settings.get_strv(id)[0]); + } + + // Add a row for the keybinding. + const row = model.insert(100); // Erm... + model.set( + row, + [COLUMN_ID, COLUMN_DESCRIPTION, COLUMN_KEY, COLUMN_MODS], + [id, description, key, mods], + ); +} + +function createKeybindingWidget(Settings) { + const model = new Gtk.ListStore(); + + model.set_column_types([ + GObject.TYPE_STRING, // COLUMN_ID + GObject.TYPE_STRING, // COLUMN_DESCRIPTION + GObject.TYPE_INT, // COLUMN_KEY + GObject.TYPE_INT, + ]); // COLUMN_MODS + + const treeView = new Gtk.TreeView(); + treeView.model = model; + treeView.headers_visible = false; + + let column, renderer; + + // Description column. + renderer = new Gtk.CellRendererText(); + + column = new Gtk.TreeViewColumn(); + column.expand = true; + column.pack_start(renderer, true); + column.add_attribute(renderer, 'text', COLUMN_DESCRIPTION); + + treeView.append_column(column); + + // Key binding column. + renderer = new Gtk.CellRendererAccel(); + renderer.accel_mode = Gtk.CellRendererAccelMode.GTK; + renderer.editable = true; + + renderer.connect( + 'accel-edited', + function (renderer, path, key, mods, hwCode) { + const [ok, iter] = model.get_iter_from_string(path); + if (!ok) { + return; + } + + // Update the UI. + model.set(iter, [COLUMN_KEY, COLUMN_MODS], [key, mods]); + + // Update the stored setting. + const id = model.get_value(iter, COLUMN_ID); + const accelString = Gtk.accelerator_name(key, mods); + Settings.set_strv(id, [accelString]); + }, + ); + + renderer.connect('accel-cleared', function (renderer, path) { + const [ok, iter] = model.get_iter_from_string(path); + if (!ok) { + return; + } + + // Update the UI. + model.set(iter, [COLUMN_KEY, COLUMN_MODS], [0, 0]); + + // Update the stored setting. + const id = model.get_value(iter, COLUMN_ID); + Settings.set_strv(id, []); + }); + + column = new Gtk.TreeViewColumn(); + column.pack_end(renderer, false); + column.add_attribute(renderer, 'accel-key', COLUMN_KEY); + column.add_attribute(renderer, 'accel-mods', COLUMN_MODS); + + treeView.append_column(column); + + return treeView; +} diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/schemas/gschemas.compiled b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..95071e690b48198592d0a094905c7b84c3b0500a GIT binary patch literal 1576 zcmZWpU8qb^7~Ot`%f)r?_4`IqXUd)tdGM2;DE9%WTT&8x&g?t0yK~MyoqcA;5F&}9 zBoE#Y#f!Qx!h`an2z4XnQAi@ngD8o$&Yb(lt~G0&HG6;i+vj}y+rCG|MumyU#v<_E zw)Q4x-X!p<`wMQ6`QHlBCr*LqP7xwEUx+^#%cj8)I|SJSY+NZ+LL2CUtLj>pTUh8VDge>hS}f|bV1JrdNTBa=K%|V zj=LA0k(u*i(GV-aYXCK^`v`sNneaD*_W?t_{qN{g_rQ<9b>RKE)gFE7UifFg*MUFl zhE~w0=KOo$2f*<{|9<+^v)~VdKLd>eZ+FnA=6oJF2Ylc4;t_r7h47bv*8^YH$`ACZ z8K)2I17`>8SLjnS{|HH zXS|kYOg^92p(@q19Mr9n3Ckv|xO5)YCpIdn7<=f3(akgvYkDLOV~Vjjk#dQaHk^>l zB!M{D{b^^yMM8W;J&nYwbjK}d$UWgafR7(c7oK78`hZM%|>yp?+GC>ER&%8sMn1L3L4ig} literal 0 HcmV?d00001 diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/schemas/org.gnome.shell.extensions.clipboard-indicator.gschema.xml b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/schemas/org.gnome.shell.extensions.clipboard-indicator.gschema.xml new file mode 100644 index 0000000..9484485 --- /dev/null +++ b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/schemas/org.gnome.shell.extensions.clipboard-indicator.gschema.xml @@ -0,0 +1,140 @@ + + + + + 1000 +

The maximum number of items to remember + + + + + 0 + What to display in top bar + + + + + true + Remove down arrow in top bar + + + + 33 + Window width (%) + + The width of the clipboard panel as a percentage of screen width. + + + + + + 10 + Number of visible characters in top bar + + The number of characters to display for the current clipboard item in the top bar. + + + + + + 100 + The maximum clipboard history size (MiB) + + Note that this is the maximum number of clipboard item bytes to store, not the maximum file + size on disk. The clipboard history on disk may be larger than this limit due to storage + inefficiencies. + + + + + + false + Only save favorites to disk + + Non-favorite items will still be saved, but only in-memory. Restarting the Gnome Shell will + result in the loss of those items. + + + + + false + Show a notification on copy + + If true, a notification is shown when content is copied to clipboard with an undo button. + + + + + true + Show confirmation dialog on Clear History + + If true, a confirmation dialog is shown when attempting to Clear History. + + + + + false + Remove whitespace around copied plaintext items + + + + true + Paste selected items into the previously active window + + + + false + Save the currently selected text to the clipboard history + + If true, both the contents from the "CLIPBOARD" clipboard and the "PRIMARY" clipboard are added to the history. + For more info, see https://wiki.archlinux.org/title/clipboard#Selections. + + + + + true + Move previously copied items to the top of the list + + + + false + Enable private mode + + If true, copied items are not saved in the clipboard history (be that in memory or on disk). + + + + + true + Enable keyboard shortcuts + + + + Shortcut to clear history + + + + Shortcut to cycle to the previous clipboard entry + + + + + + Shortcut to cycle to the next clipboard entry + + + V']]]> + Shortcut to open the clipboard history + + + P']]]> + Toggle private mode + + + true + Ignore selections containing the x-kde-passwordManagerHint mime type. + + + diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/settingsFields.js b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/settingsFields.js new file mode 100644 index 0000000..de7a063 --- /dev/null +++ b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/settingsFields.js @@ -0,0 +1,19 @@ +const SettingsFields = { + HISTORY_SIZE: 'history-size', + WINDOW_WIDTH_PERCENTAGE: 'window-width-percentage', + CACHE_FILE_SIZE: 'cache-size', + CACHE_ONLY_FAVORITES: 'cache-only-favorites', + NOTIFY_ON_COPY: 'notify-on-copy', + CONFIRM_ON_CLEAR: 'confirm-clear', + MOVE_ITEM_FIRST: 'move-item-first', + ENABLE_KEYBINDING: 'enable-keybindings', + TOPBAR_PREVIEW_SIZE: 'topbar-preview-size', + TOPBAR_DISPLAY_MODE_ID: 'display-mode', + DISABLE_DOWN_ARROW: 'disable-down-arrow', + STRIP_TEXT: 'strip-text', + PRIVATE_MODE: 'private-mode', + PASTE_ON_SELECTION: 'paste-on-selection', + PROCESS_PRIMARY_SELECTION: 'process-primary-selection', + IGNORE_PASSWORD_MIMES: 'ignore-password-mimes', +}; +export default SettingsFields; diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/store.js b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/store.js new file mode 100644 index 0000000..0cc9c77 --- /dev/null +++ b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/store.js @@ -0,0 +1,512 @@ +import GLib from 'gi://GLib'; +import Gio from 'gi://Gio'; +import * as DS from './dataStructures.js'; + +let EXTENSION_UUID; +let CACHE_DIR; +const OLD_REGISTRY_FILE = GLib.build_filenamev([ + GLib.get_user_cache_dir(), + 'clipboard-indicator@tudmotu.com', + 'registry.txt', +]); + +/** + * Stores our compacting log implementation. Here are its key ideas: + * - We only ever append to the log. + * - This means there will be operations that cancel each other out. These are wasted/useless ops + * that must be occasionally pruned. MAX_WASTED_OPS limits the number of useless ops. + * - The available operations are listed in the OP_TYPE_* constants. + * - An add op never moves (until compaction), allowing us to derive globally unique entry IDs based + * on the order in which these add ops are discovered. + */ +let DATABASE_FILE; +const BYTE_ORDER = Gio.DataStreamByteOrder.LITTLE_ENDIAN; + +// Don't use zero b/c DataInputStream uses 0 as its error value +const OP_TYPE_SAVE_TEXT = 1; +const OP_TYPE_DELETE_TEXT = 2; +const OP_TYPE_FAVORITE_ITEM = 3; +const OP_TYPE_UNFAVORITE_ITEM = 4; +const OP_TYPE_MOVE_ITEM_TO_END = 5; + +const MAX_WASTED_OPS = 500; +let uselessOpCount; + +let opQueue = new DS.LinkedList(); +let opInProgress = false; +let writeStream; + +export function init(uuid) { + EXTENSION_UUID = uuid; + CACHE_DIR = GLib.build_filenamev([GLib.get_user_cache_dir(), EXTENSION_UUID]); + DATABASE_FILE = GLib.build_filenamev([CACHE_DIR, 'database.log']); + + if (GLib.mkdir_with_parents(CACHE_DIR, 0o775) !== 0) { + console.log( + EXTENSION_UUID, + "Failed to create cache dir, extension likely won't work", + CACHE_DIR, + ); + } +} + +export function destroy() { + _pushToOpQueue((resolve) => { + if (writeStream) { + writeStream.close_async(0, null, (src, res) => { + src.close_finish(res); + resolve(); + }); + writeStream = undefined; + } else { + resolve(); + } + }); +} + +export function buildClipboardStateFromLog(callback) { + if (typeof callback !== 'function') { + throw TypeError('`callback` must be a function'); + } + uselessOpCount = 0; + + Gio.File.new_for_path(DATABASE_FILE).read_async(0, null, (src, res) => { + try { + _parseLog(src.read_finish(res), callback); + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) { + _readAndConsumeOldFormat(callback); + } else { + throw e; + } + } + }); +} + +function _parseLog(stream, callback) { + stream = Gio.DataInputStream.new(stream); + stream.set_byte_order(BYTE_ORDER); + + const state = { + entries: new DS.LinkedList(), + favorites: new DS.LinkedList(), + nextId: 1, + }; + _consumeStream(stream, state, callback); +} + +function _consumeStream(stream, state, callback) { + const finish = () => { + callback(state.entries, state.favorites, state.nextId); + }; + const forceFill = (minBytes, fillCallback) => { + stream.fill_async(/*count=*/ -1, 0, null, (src, res) => { + if (src.fill_finish(res) < minBytes) { + finish(); + } else { + fillCallback(); + } + }); + }; + + let parseAvailableAware; + + function loop() { + if (stream.get_available() === 0) { + forceFill(1, loop); + return; + } + + const opType = stream.read_byte(null); + if (opType === OP_TYPE_SAVE_TEXT) { + stream.read_upto_async( + /*stop_chars=*/ '\0', + /*stop_chars_len=*/ 1, + 0, + null, + (src, res) => { + const [text] = src.read_upto_finish(res); + src.read_byte(null); + + const node = new DS.LLNode(); + node.diskId = node.id = state.nextId++; + node.type = DS.TYPE_TEXT; + node.text = text || ''; + node.favorite = false; + state.entries.append(node); + + loop(); + }, + ); + } else if (opType === OP_TYPE_DELETE_TEXT) { + uselessOpCount += 2; + parseAvailableAware(4, () => { + const id = stream.read_uint32(null); + (state.entries.findById(id) || state.favorites.findById(id)).detach(); + }); + } else if (opType === OP_TYPE_FAVORITE_ITEM) { + parseAvailableAware(4, () => { + const id = stream.read_uint32(null); + const entry = state.entries.findById(id); + + entry.favorite = true; + state.favorites.append(entry); + }); + } else if (opType === OP_TYPE_UNFAVORITE_ITEM) { + uselessOpCount += 2; + parseAvailableAware(4, () => { + const id = stream.read_uint32(null); + const entry = state.favorites.findById(id); + + entry.favorite = false; + state.entries.append(entry); + }); + } else if (opType === OP_TYPE_MOVE_ITEM_TO_END) { + uselessOpCount++; + parseAvailableAware(4, () => { + const id = stream.read_uint32(null); + const entry = + state.entries.findById(id) || state.favorites.findById(id); + + if (entry.favorite) { + state.favorites.append(entry); + } else { + state.entries.append(entry); + } + }); + } else { + console.log(EXTENSION_UUID, 'Unknown op type, aborting load.', opType); + finish(); + } + } + + parseAvailableAware = (minBytes, parse) => { + const safeParse = (cont) => { + try { + parse(); + cont(); + } catch (e) { + console.log(EXTENSION_UUID, 'Parsing error'); + console.error(e); + + const entries = new DS.LinkedList(); + let nextId = 1; + const addEntry = (text) => { + const node = new DS.LLNode(); + node.id = nextId++; + node.type = DS.TYPE_TEXT; + node.text = text; + node.favorite = false; + entries.prepend(node); + }; + + addEntry('Your clipboard data has been corrupted and was moved to:'); + addEntry('~/.cache/clipboard-history@alexsaveau.dev/corrupted.log'); + addEntry('Please file a bug report at:'); + addEntry( + 'https://github.com/SUPERCILEX/gnome-clipboard-history/issues/new?assignees=&labels=bug&template=1-bug.md', + ); + + try { + if ( + !Gio.File.new_for_path(DATABASE_FILE).move( + Gio.File.new_for_path( + GLib.build_filenamev([CACHE_DIR, 'corrupted.log']), + ), + Gio.FileCopyFlags.OVERWRITE, + null, + null, + ) + ) { + console.log(EXTENSION_UUID, 'Failed to move database file'); + } + } catch (e) { + console.log(EXTENSION_UUID, 'Crash moving database file'); + console.error(e); + } + callback(entries, new DS.LinkedList(), nextId, 1); + } + }; + + if (stream.get_available() < minBytes) { + forceFill(minBytes, () => { + safeParse(loop); + }); + } else { + safeParse(loop); + } + }; + + loop(); +} + +function _readAndConsumeOldFormat(callback) { + Gio.File.new_for_path(OLD_REGISTRY_FILE).load_contents_async( + null, + (src, res) => { + const entries = new DS.LinkedList(); + const favorites = new DS.LinkedList(); + let id = 1; + + let contents; + try { + [, contents] = src.load_contents_finish(res); + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) { + callback(entries, favorites, id); + return; + } else { + throw e; + } + } + + let registry = []; + try { + registry = JSON.parse(GLib.ByteArray.toString(contents)); + } catch (e) { + console.error(e); + } + + for (const entry of registry) { + const node = new DS.LLNode(); + + node.diskId = node.id = id; + node.type = DS.TYPE_TEXT; + if (typeof entry === 'string') { + node.text = entry; + node.favorite = false; + + entries.append(node); + } else { + node.text = entry.contents; + node.favorite = entry.favorite; + + favorites.append(node); + } + + id++; + } + + resetDatabase(() => entries.toArray().concat(favorites.toArray())); + Gio.File.new_for_path(OLD_REGISTRY_FILE).trash_async( + 0, + null, + (src, res) => { + src.trash_finish(res); + }, + ); + + callback(entries, favorites, id); + }, + ); +} + +export function maybePerformLogCompaction(currentStateBuilder) { + if (uselessOpCount >= MAX_WASTED_OPS) { + resetDatabase(currentStateBuilder); + } +} + +export function resetDatabase(currentStateBuilder) { + uselessOpCount = 0; + + const state = currentStateBuilder(); + _pushToOpQueue((resolve) => { + // Sigh, can't use truncate because it doesn't have an async variant. Instead, nuke the stream + // and let the next append re-create it. Note that we can't use this stream because it tries to + // apply our operations atomically and therefore writes to a temporary file instead of the one + // we asked for. + writeStream = undefined; + + const priority = -10; + Gio.File.new_for_path(DATABASE_FILE).replace_async( + /*etag=*/ null, + /*make_backup=*/ false, + Gio.FileCreateFlags.PRIVATE, + priority, + null, + (src, res) => { + const stream = _intoDataStream(src.replace_finish(res)); + const finish = () => { + stream.close_async(priority, null, (src, res) => { + src.close_finish(res); + resolve(); + }); + }; + + if (state.length === 0) { + finish(); + return; + } + + let i = 0; + _writeToStream(stream, priority, finish, (dataStream) => { + do { + const entry = state[i]; + + if (entry.type === DS.TYPE_TEXT) { + _storeTextOp(entry.text)(dataStream); + } else { + throw new TypeError('Unknown type: ' + entry.type); + } + if (entry.favorite) { + _updateFavoriteStatusOp(entry.diskId, true)(dataStream); + } + + i++; + } while (i % 1000 !== 0 && i < state.length); + + // Flush the buffer every 1000 entries + return i >= state.length; + }); + }, + ); + }); +} + +export function storeTextEntry(text) { + _appendBytesToLog(_storeTextOp(text), -5); +} + +function _storeTextOp(text) { + return (dataStream) => { + dataStream.put_byte(OP_TYPE_SAVE_TEXT, null); + dataStream.put_string(text, null); + dataStream.put_byte(0, null); // NUL terminator + return true; + }; +} + +export function deleteTextEntry(id, isFavorite) { + _appendBytesToLog(_deleteTextOp(id), 5); + uselessOpCount += 2; + if (isFavorite) { + uselessOpCount++; + } +} + +function _deleteTextOp(id) { + return (dataStream) => { + dataStream.put_byte(OP_TYPE_DELETE_TEXT, null); + dataStream.put_uint32(id, null); + return true; + }; +} + +export function updateFavoriteStatus(id, favorite) { + _appendBytesToLog(_updateFavoriteStatusOp(id, favorite)); + + if (!favorite) { + uselessOpCount += 2; + } +} + +function _updateFavoriteStatusOp(id, favorite) { + return (dataStream) => { + dataStream.put_byte( + favorite ? OP_TYPE_FAVORITE_ITEM : OP_TYPE_UNFAVORITE_ITEM, + null, + ); + dataStream.put_uint32(id, null); + return true; + }; +} + +export function moveEntryToEnd(id) { + _appendBytesToLog(_moveToEndOp(id)); + uselessOpCount++; +} + +function _moveToEndOp(id) { + return (dataStream) => { + dataStream.put_byte(OP_TYPE_MOVE_ITEM_TO_END, null); + dataStream.put_uint32(id, null); + return true; + }; +} + +function _appendBytesToLog(callback, priority) { + priority = priority || 0; + _pushToOpQueue((resolve) => { + const runUnsafe = () => { + _writeToStream(writeStream, priority, resolve, callback); + }; + + if (writeStream === undefined) { + Gio.File.new_for_path(DATABASE_FILE).append_to_async( + Gio.FileCreateFlags.PRIVATE, + priority, + null, + (src, res) => { + writeStream = _intoDataStream(src.append_to_finish(res)); + runUnsafe(); + }, + ); + } else { + runUnsafe(); + } + }); +} + +function _writeToStream(stream, priority, resolve, callback) { + _writeCallbackBytesAsyncHack(callback, stream, priority, () => { + stream.flush_async(priority, null, (src, res) => { + src.flush_finish(res); + resolve(); + }); + }); +} + +/** + * This garbage code is here to keep disk writes off the main thread. DataOutputStream doesn't have + * async method variants, so we write to a memory buffer and then flush it asynchronously. We're + * basically trying to balance memory allocations with disk writes. + */ +function _writeCallbackBytesAsyncHack( + dataCallback, + stream, + priority, + callback, +) { + if (dataCallback(stream)) { + callback(); + } else { + stream.flush_async(priority, null, (src, res) => { + src.flush_finish(res); + _writeCallbackBytesAsyncHack(dataCallback, stream, priority, callback); + }); + } +} + +function _intoDataStream(stream) { + const bufStream = Gio.BufferedOutputStream.new(stream); + bufStream.set_auto_grow(true); // Blocks flushing, needed for hack + const ioStream = Gio.DataOutputStream.new(bufStream); + ioStream.set_byte_order(BYTE_ORDER); + return ioStream; +} + +function _pushToOpQueue(op) { + const consumeOp = () => { + const resolve = () => { + opInProgress = false; + + const next = opQueue.head; + if (next) { + next.detach(); + next.op(); + } + }; + + opInProgress = true; + op(resolve); + }; + + if (opInProgress) { + const node = new DS.LLNode(); + node.op = consumeOp; + opQueue.append(node); + } else { + consumeOp(); + } +} diff --git a/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/stylesheet.css b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/stylesheet.css new file mode 100644 index 0000000..824aee7 --- /dev/null +++ b/gnome/.local/share/gnome-shell/extensions/clipboard-history@alexsaveau.dev/stylesheet.css @@ -0,0 +1,34 @@ +.clipboard-indicator-icon.private-mode { + color: rgba(255, 255, 255, 0.3); +} + +.ci-notification-label { + font-weight: bold; + color: #ffffff; + background-color: rgba(10, 10, 10, 0.7); + border-radius: 6px; + font-size: 2em; + padding: 0.5em; + width: 400px; +} + +.popup-menu-item .ci-action-btn StIcon { + icon-size: 16px; +} + +.ci-history-menu-section { + max-height: 450px; +} + +.ci-history-search-section { + padding-top: 0; +} + +.ci-history-search-section .popup-menu-ornament, +.ci-history-actions-section .popup-menu-ornament { + width: auto; +} + +.ci-history-search-entry { + width: 5em; +}