diff --git a/strata@edu4rdshl.dev/extension.js b/strata@edu4rdshl.dev/extension.js index 43f7dea..ded632f 100644 --- a/strata@edu4rdshl.dev/extension.js +++ b/strata@edu4rdshl.dev/extension.js @@ -58,7 +58,7 @@ export default class StrataExtension extends Extension { _pendingSignalId = null; /** @type {boolean} Re-entrancy guard for signal processing */ - #busy = false; + _busy = false; /** @type {number | null} Keyboard shortcut binding ID */ _shortcutId = null; @@ -90,22 +90,11 @@ export default class StrataExtension extends Extension { // inert until the panel toggles the `.strata-theme-light` class (panel.js). this._loadThemeStylesheet(); - // 1. Top-bar indicator icon. this._addIndicator(); - - // 2. Spawn the Rust daemon. this._spawnDaemon(); - - // 3. Connect D-Bus proxy (async - doesn't block if daemon isn't ready yet). this._connectProxy(); - - // 4. Track focused window (lightweight - no clipboard I/O). this._connectFocusTracking(); - - // 5. Monitor clipboard via Meta.Selection (GNOME-native, no Wayland protocol needed). this._connectClipboardMonitor(); - - // 6. Register keyboard shortcut. this._registerShortcut(); } @@ -261,9 +250,7 @@ export default class StrataExtension extends Extension { ]; for (const want of PREFERRED) if (mimes.includes(want)) return want; - // Allowlist only: see comment in pick_mime (daemon). Reading unknown - // mime types could pull a 1 GB blob into Shell memory before we can - // size-check it. + // Allowlist only: see pick_mime in the daemon. Unknown mime types are skipped. return null; } @@ -288,10 +275,8 @@ export default class StrataExtension extends Extension { if (this._shuttingDown) return; try { _conn.call_finish(result); - // Name already owned - daemon managed externally (systemd etc). - console.log('[Strata] daemon already running, skipping spawn'); + // Name already owned (e.g. systemd user service) - don't spawn a second instance. } catch (_) { - // Name not owned - spawn it ourselves. this._doSpawnDaemon(); } } @@ -351,10 +336,7 @@ export default class StrataExtension extends Extension { const lifetimeMs = (GLib.get_monotonic_time() / 1000) - this._daemonSpawnTime; this._daemon = null; - if (this._shuttingDown) { - console.log(`[Strata] daemon exited cleanly during shutdown (status=${exit})`); - return; - } + if (this._shuttingDown) return; // Reset attempt counter if the daemon ran long enough to be considered healthy. if (lifetimeMs >= 5000) { @@ -380,7 +362,6 @@ export default class StrataExtension extends Extension { if (this._shuttingDown) return; // Exponential backoff: 1s, 2s, 4s, 8s, 16s const backoffMs = 1000 * Math.pow(2, Math.max(0, this._daemonRestartAttempts - 1)); - console.log(`[Strata] respawning daemon in ${backoffMs}ms`); this._daemonRestartTimerId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, backoffMs, () => { this._daemonRestartTimerId = null; this._spawnDaemon(); @@ -549,8 +530,8 @@ export default class StrataExtension extends Extension { } async _processItemAdded(params) { - if (this.#busy) return; - this.#busy = true; + if (this._busy) return; + this._busy = true; try { const [id, mimeType, preview] = params.deepUnpack(); @@ -570,7 +551,7 @@ export default class StrataExtension extends Extension { return GLib.SOURCE_REMOVE; }); } finally { - this.#busy = false; + this._busy = false; } } @@ -614,17 +595,9 @@ export default class StrataExtension extends Extension { } - /** Load light.css into the global St theme context ONCE. It is scoped under - * `.strata-theme-light`, so it stays inert until the panel adds that class - * (dark/light switching is the panel's class toggle, independent of this). - * - * We deliberately do NOT subscribe to the theme context's 'changed' - * signal: load_stylesheet itself emits 'changed', so reloading on it feeds - * back into itself and hits "too much recursion" (it fired on screen - * unlock, which restyles widgets). The only thing a one-time load gives up - * is re-applying after a GNOME Shell *theme* switch (which replaces the - * St.Theme and drops this sheet) - a rare action, recoverable by toggling - * the extension. That trade is worth never touching the signal. */ + /** Load light.css once. It is scoped under `.strata-theme-light` and stays + * inert until the panel adds that class. We do not subscribe to the theme + * context's 'changed' signal because load_stylesheet itself emits it. */ _loadThemeStylesheet() { try { const themeContext = St.ThemeContext.get_for_stage(global.stage); diff --git a/strata@edu4rdshl.dev/metadata.json b/strata@edu4rdshl.dev/metadata.json index 95d56a2..88e126a 100644 --- a/strata@edu4rdshl.dev/metadata.json +++ b/strata@edu4rdshl.dev/metadata.json @@ -1,6 +1,6 @@ { "name": "Strata", - "description": "A fast, stutter-free clipboard manager. All I/O runs in a Rust daemon - GNOME Shell is never blocked.", + "description": "A fast clipboard manager. Storage, search and image decoding run in a separate Rust daemon.", "uuid": "strata@edu4rdshl.dev", "version": 8, "shell-version": ["45", "46", "47", "48", "49", "50"], diff --git a/strata@edu4rdshl.dev/prefs.js b/strata@edu4rdshl.dev/prefs.js index fc5d8bd..fccf52b 100644 --- a/strata@edu4rdshl.dev/prefs.js +++ b/strata@edu4rdshl.dev/prefs.js @@ -1,10 +1,4 @@ -/** - * prefs.js - Strata preferences window (GNOME 45+ / Adw). - * - * Pages: - * General: max-history SpinRow, keyboard-shortcut ShortcutRow - * Privacy: excluded-apps ExpanderRow + StringList (one entry per line) - */ +/* prefs.js - Strata preferences window. */ import Adw from 'gi://Adw'; import Gtk from 'gi://Gtk'; @@ -22,17 +16,12 @@ export default class StrataPreferences extends ExtensionPreferences { window.add(this._buildPrivacyPage(settings)); } - // ------------------------------------------------------------------------- - // General page - // ------------------------------------------------------------------------- - _buildGeneralPage(settings) { const page = new Adw.PreferencesPage({ title: 'General', icon_name: 'preferences-system-symbolic', }); - // ── History ────────────────────────────────────────────────────────── const historyGroup = new Adw.PreferencesGroup({ title: 'History' }); const maxHistoryRow = new Adw.SpinRow({ @@ -89,7 +78,6 @@ export default class StrataPreferences extends ExtensionPreferences { page.add(historyGroup); - // ── Appearance ─────────────────────────────────────────────────────── const appearanceGroup = new Adw.PreferencesGroup({ title: 'Appearance' }); const themes = [ @@ -180,7 +168,6 @@ export default class StrataPreferences extends ExtensionPreferences { page.add(appearanceGroup); - // ── Keyboard ───────────────────────────────────────────────────────── const kbGroup = new Adw.PreferencesGroup({ title: 'Keyboard' }); const shortcutRow = new Adw.ActionRow({ @@ -208,10 +195,6 @@ export default class StrataPreferences extends ExtensionPreferences { return page; } - // ------------------------------------------------------------------------- - // Privacy page - // ------------------------------------------------------------------------- - _buildPrivacyPage(settings) { const page = new Adw.PreferencesPage({ title: 'Privacy', @@ -223,7 +206,6 @@ export default class StrataPreferences extends ExtensionPreferences { description: 'Items copied while these apps have focus will not be stored in history. Enter a partial app name (case-insensitive).', }); - // We use a StringList model bound to excluded-apps. const model = new Gtk.StringList(); const currentApps = settings.get_strv('excluded-apps'); for (const app of currentApps) @@ -239,7 +221,6 @@ export default class StrataPreferences extends ExtensionPreferences { settings.set_strv('excluded-apps', apps); }; - // Each item in the list: an EditableLabel + Remove button. const listBox = new Gtk.ListBox({ selection_mode: Gtk.SelectionMode.NONE, css_classes: ['boxed-list'], @@ -280,7 +261,6 @@ export default class StrataPreferences extends ExtensionPreferences { listBox.append(row); } - // Add-new row. const addRow = new Adw.ActionRow({ activatable: false }); const addEntry = new Gtk.Entry({ placeholder_text: 'App name…', @@ -314,10 +294,6 @@ export default class StrataPreferences extends ExtensionPreferences { return page; } - // ------------------------------------------------------------------------- - // Keyboard shortcut dialog - // ------------------------------------------------------------------------- - _showShortcutDialog(parent, settings) { const dialog = new Adw.MessageDialog({ heading: 'Set Keyboard Shortcut', diff --git a/strata@edu4rdshl.dev/ui/clipboardItem.js b/strata@edu4rdshl.dev/ui/clipboardItem.js index d7182a4..554d34e 100644 --- a/strata@edu4rdshl.dev/ui/clipboardItem.js +++ b/strata@edu4rdshl.dev/ui/clipboardItem.js @@ -48,13 +48,8 @@ export const ClipboardItem = GObject.registerClass({ this._id = id; this._mimeType = mimeType; - /** D-Bus proxy used for lazy thumbnail fetch. */ this._proxy = opts.proxy ?? null; - /** Optional shared LRU cache (Map) - populated on first fetch. */ this._thumbCache = opts.thumbCache ?? null; - /** Stored on the actor so panel.js can filter by it. */ - this.actor = this; - this._strataPreview = mimeType.startsWith('image/') ? '' : preview; const row = new St.BoxLayout({ style_class: 'strata-item-row', @@ -112,10 +107,6 @@ export const ClipboardItem = GObject.registerClass({ return icon; } - /** Build an image thumbnail container. - * - If already in shared cache, apply immediately. - * - Else fetch via GetThumbnailRemote, write to ~/.cache/strata/thumbnails/{id}.png, - * then apply. The decode happens off-thread (CSS background-image loader). */ _buildThumbnail(id) { const container = new St.Widget({ width: THUMB_SIZE, @@ -135,18 +126,15 @@ export const ClipboardItem = GObject.registerClass({ }; try { - // Fast path: already cached this session. if (this._thumbCache?.has(id)) { applyStyle(); return container; } - // Second fast path: file exists on disk from a previous session. if (GLib.file_test(cachePath, GLib.FileTest.EXISTS)) { this._thumbCache?.set(id, cachePath); applyStyle(); return container; } - // Slow path: ask the daemon for the bytes, then write file async. GLib.mkdir_with_parents(cacheDir, 0o755); if (!this._proxy) { this._fallbackIcon(container); diff --git a/strata@edu4rdshl.dev/ui/panel.js b/strata@edu4rdshl.dev/ui/panel.js index b0a3d5d..7867356 100644 --- a/strata@edu4rdshl.dev/ui/panel.js +++ b/strata@edu4rdshl.dev/ui/panel.js @@ -298,7 +298,7 @@ export class StrataPanel { const widget = this._makeItemWidget(id, mimeType, preview); this._widgets.set(id, widget); - this._itemList.insert_child_at_index(widget.actor, 0); + this._itemList.insert_child_at_index(widget, 0); this._setActiveWidget(widget); } catch (e) { console.error(`[Strata] prependItem failed for id=${id} mime=${mimeType}:`, e); @@ -425,7 +425,7 @@ export class StrataPanel { this._items.push({ id, mimeType, preview }); const widget = this._makeItemWidget(id, mimeType, preview); this._widgets.set(id, widget); - this._itemList.add_child(widget.actor); + this._itemList.add_child(widget); } catch (e) { console.error(`[Strata] _appendItemFromMeta failed for id=${meta?.id}:`, e); } @@ -475,12 +475,8 @@ export class StrataPanel { for (let j = i; j < e; j++) this._appendItemFromMeta(this._searchResults[j]); }; - // The very first chunk of a fresh render (start === 0, right after - // _clearListDom) is rendered synchronously, in the same frame as - // the clear, so the list never paints empty in between - that - // empty frame is the "blink" seen while typing a search. Later - // chunks (and scroll-driven pages) yield to idle to keep frames - // light. + // First chunk runs synchronously so the list never paints empty + // between clear and first append. Later chunks yield to idle. if (i === 0) { renderChunk(); } else {