import GLib from 'gi://GLib'; import Gio from 'gi://Gio'; import St from 'gi://St'; import { PrefsFields } from './constants.js'; const FileQueryInfoFlags = Gio.FileQueryInfoFlags; const FileCopyFlags = Gio.FileCopyFlags; const FileTest = GLib.FileTest; export class Registry { constructor ({ settings, uuid }) { this.uuid = uuid; this.settings = settings; this.REGISTRY_FILE = 'registry.txt'; this.REGISTRY_DIR = GLib.get_user_cache_dir() + '/' + this.uuid; this.REGISTRY_PATH = this.REGISTRY_DIR + '/' + this.REGISTRY_FILE; this.BACKUP_REGISTRY_PATH = this.REGISTRY_PATH + '~'; } write (entries) { const registryContent = []; for (let entry of entries) { const item = { favorite: entry.isFavorite(), mimetype: entry.mimetype() }; registryContent.push(item); if (entry.isText()) { item.contents = entry.getStringValue(); } else if (entry.isImage()) { const filename = this.getEntryFilename(entry); item.contents = filename; this.writeEntryFile(entry); } } this.writeToFile(registryContent); } writeToFile (registry) { let json = JSON.stringify(registry); let contents = new GLib.Bytes(json); // Make sure dir exists GLib.mkdir_with_parents(this.REGISTRY_DIR, parseInt('0775', 8)); // Write contents to file asynchronously let file = Gio.file_new_for_path(this.REGISTRY_PATH); file.replace_async(null, false, Gio.FileCreateFlags.NONE, GLib.PRIORITY_DEFAULT, null, (obj, res) => { let stream = obj.replace_finish(res); stream.write_bytes_async(contents, GLib.PRIORITY_DEFAULT, null, (w_obj, w_res) => { w_obj.write_bytes_finish(w_res); stream.close(null); }); }); } async read () { return new Promise(resolve => { if (GLib.file_test(this.REGISTRY_PATH, FileTest.EXISTS)) { let file = Gio.file_new_for_path(this.REGISTRY_PATH); let CACHE_FILE_SIZE = this.settings.get_int(PrefsFields.CACHE_FILE_SIZE); file.query_info_async('*', FileQueryInfoFlags.NONE, GLib.PRIORITY_DEFAULT, null, (src, res) => { // Check if file size is larger than CACHE_FILE_SIZE // If so, make a backup of file, and resolve with empty array let file_info = src.query_info_finish(res); if (file_info.get_size() >= CACHE_FILE_SIZE * 1024 * 1024) { let destination = Gio.file_new_for_path(this.BACKUP_REGISTRY_PATH); file.move(destination, FileCopyFlags.OVERWRITE, null, null); resolve([]); return; } file.load_contents_async(null, (obj, res) => { let [success, contents] = obj.load_contents_finish(res); if (success) { let max_size = this.settings.get_int(PrefsFields.HISTORY_SIZE); const registry = JSON.parse(new TextDecoder().decode(contents)); const entriesPromises = registry.map( jsonEntry => { return ClipboardEntry.fromJSON(jsonEntry) } ); Promise.all(entriesPromises).then(clipboardEntries => { clipboardEntries = clipboardEntries .filter(entry => entry !== null); let registryNoFavorite = clipboardEntries .filter(entry => entry.isFavorite()); while (registryNoFavorite.length > max_size) { let oldestNoFavorite = registryNoFavorite.shift(); let itemIdx = clipboardEntries.indexOf(oldestNoFavorite); clipboardEntries.splice(itemIdx,1); registryNoFavorite = clipboardEntries.filter( entry => entry.isFavorite() ); } resolve(clipboardEntries); }).catch(e => { console.error(e); }); } else { console.error('Clipboard Indicator: failed to open registry file'); } }); }); } else { resolve([]); } }); } #entryFileExists (entry) { const filename = this.getEntryFilename(entry); return GLib.file_test(filename, FileTest.EXISTS); } async getEntryAsImage (entry) { const filename = this.getEntryFilename(entry); if (entry.isImage() === false) return; if (this.#entryFileExists(entry) == false) { await this.writeEntryFile(entry); } const gicon = Gio.icon_new_for_string(this.getEntryFilename(entry)); const stIcon = new St.Icon({ gicon }); return stIcon; } getEntryFilename (entry) { return `${this.REGISTRY_DIR}/${entry.asBytes().hash()}`; } async writeEntryFile (entry) { if (this.#entryFileExists(entry)) return; let file = Gio.file_new_for_path(this.getEntryFilename(entry)); return new Promise(resolve => { file.replace_async(null, false, Gio.FileCreateFlags.NONE, GLib.PRIORITY_DEFAULT, null, (obj, res) => { let stream = obj.replace_finish(res); stream.write_bytes_async(entry.asBytes(), GLib.PRIORITY_DEFAULT, null, (w_obj, w_res) => { w_obj.write_bytes_finish(w_res); stream.close(null); resolve(); }); }); }); } async deleteEntryFile (entry) { const file = Gio.file_new_for_path(this.getEntryFilename(entry)); try { await file.delete_async(GLib.PRIORITY_DEFAULT, null); } catch (e) { console.error(e); } } } export class ClipboardEntry { #mimetype; #bytes; #favorite; static #decode (contents) { return Uint8Array.from(contents.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); } static async fromJSON (jsonEntry) { const mimetype = jsonEntry.mimetype || 'text/plain;charset=utf-8'; const favorite = jsonEntry.favorite; let bytes; if (mimetype.startsWith('text/')) { bytes = new TextEncoder().encode(jsonEntry.contents); } else { const filename = jsonEntry.contents; if (!GLib.file_test(filename, FileTest.EXISTS)) return null; let file = Gio.file_new_for_path(filename); bytes = await new Promise((resolve, reject) => file.load_contents_async(null, (obj, res) => { let [success, contents] = obj.load_contents_finish(res); if (success) { resolve(contents); } else { reject( new Error('Clipboard Indicator: could not read image file from cache') ); } })); } return new ClipboardEntry(mimetype, bytes, favorite); } constructor (mimetype, bytes, favorite) { this.#mimetype = mimetype; this.#bytes = bytes; this.#favorite = favorite; } #encode () { if (this.isText()) { return this.getStringValue(); } return [...this.#bytes] .map(x => x.toString(16).padStart(2, '0')) .join(''); } getStringValue () { if (this.isImage()) { return `[Image ${this.asBytes().hash()}]`; } return new TextDecoder().decode(this.#bytes); } mimetype () { return this.#mimetype; } isFavorite () { return this.#favorite; } set favorite (val) { this.#favorite = !!val; } isText () { return this.#mimetype.startsWith('text/'); } isImage () { return this.#mimetype.startsWith('image/'); } asBytes () { return GLib.Bytes.new(this.#bytes); } equals (otherEntry) { return this.getStringValue() === otherEntry.getStringValue(); // this.asBytes().equal(otherEntry.asBytes()); } }