Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions clipboard/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,34 @@ Item {
// Id of the decode currently in flight ("" when idle).
property string decodingId: ""

readonly property int minAutoPasteDelayMs: 100
readonly property int maxAutoPasteDelayMs: 2000
readonly property int fallbackAutoPasteDelayMs: 500

function configuredAutoPasteDelayMs() {
const cfg = pluginApi?.pluginSettings || ({});
const defaults = pluginApi?.manifest?.metadata?.defaultSettings || ({});
const configuredDelay = Number(cfg.autoPasteDelayMs ?? defaults.autoPasteDelayMs ?? root.fallbackAutoPasteDelayMs);
if (isNaN(configuredDelay))
return root.fallbackAutoPasteDelayMs;
return Math.max(root.minAutoPasteDelayMs, Math.min(root.maxAutoPasteDelayMs, configuredDelay));
}

// Sends Ctrl+V after the panel has closed and focus has returned to the
// previously active window. The explicit Wayland environment keeps wtype
// working when Noctalia is launched as a user service.
function pasteIntoPreviousWindow() {
if (pasteProc.running)
return;
const delayMs = root.configuredAutoPasteDelayMs();
const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR") || "";
const waylandDisplay = Quickshell.env("WAYLAND_DISPLAY") || "";
pasteProc.command = ["bash", "-c",
'sleep "$1"; XDG_RUNTIME_DIR="$2" WAYLAND_DISPLAY="$3" wtype -M ctrl -P v -p v -m ctrl',
"--", String(delayMs / 1000), runtimeDir, waylandDisplay];
pasteProc.running = true;
Comment thread
alepspizzetti marked this conversation as resolved.
}

// Pending ids waiting for decodeProc to become free. FIFO — delegates
// are rendered top-to-bottom so the first enqueued id is the most
// visible one.
Expand Down Expand Up @@ -408,6 +436,21 @@ Item {
stderr: StdioCollector {}
}

// Sends the paste shortcut to the window focused after the panel closes.
Process {
id: pasteProc

stdout: StdioCollector {}
stderr: StdioCollector {}

onExited: exitCode => {
if (exitCode !== 0) {
Logger.w("Clipboard Plugin", "automatic paste failed:",
String(pasteProc.stderr.text).trim() || "exit " + exitCode);
}
}
}

// Deletes a specific entry, then refreshes the list on success.
Process {
id: removeProc
Expand Down Expand Up @@ -1139,8 +1182,8 @@ Item {

// --- Lifecycle -----------------------------------------------------------

// Seed the list once the shell has injected pluginApi so maxHistorySize
// resolves to the user's actual setting (not the fallback 100).
// Seed or clear the list once the shell has injected pluginApi so startup
// behavior resolves to the user's actual settings.
//
// Also ensure the pinned-file parent directory exists. FileView.setText
// does not create missing parent directories, so a fresh install with
Expand All @@ -1150,7 +1193,13 @@ Item {
// copied-at.json have a parent to write into.
onPluginApiChanged: {
if (pluginApi) {
root.refresh();
const cfg = pluginApi?.pluginSettings || ({});
const defaults = pluginApi?.manifest?.metadata?.defaultSettings || ({});
if (cfg.clearHistoryOnStartup ?? defaults.clearHistoryOnStartup ?? false) {
root.wipe();
} else {
root.refresh();
}
if (root.dataDir) {
pinnedDirProc.command = ["mkdir", "-p", root.dataDir];
pinnedDirProc.running = true;
Expand All @@ -1165,6 +1214,8 @@ Item {
listProc.running = false;
if (copyProc.running)
copyProc.running = false;
if (pasteProc.running)
pasteProc.running = false;
if (removeProc.running)
removeProc.running = false;
if (wipeProc.running)
Expand Down
31 changes: 27 additions & 4 deletions clipboard/Panel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ Item {

// Cached settings for reactive use inside bindings.
readonly property bool showImagePreviews: pluginApi?.pluginSettings?.showImagePreviews ?? true
function shouldAutoPasteOnSelect() {
const cfg = pluginApi?.pluginSettings || ({});
const defaults = pluginApi?.manifest?.metadata?.defaultSettings || ({});
return cfg.autoPasteOnSelect ?? defaults.autoPasteOnSelect ?? false;
}
readonly property real densitySpacing: {
const d = pluginApi?.pluginSettings?.density ?? "comfortable";
if (d === "compact") return Style.marginXS;
Expand All @@ -116,6 +121,12 @@ Item {
// which clamps to [0, length-1] instead of resetting so the user's
// focus stays on the row that slid up into the deleted position.
property int selectedIndex: -1
property bool pasteAfterClose: false

function closeAfterCopy() {
root.pasteAfterClose = root.shouldAutoPasteOnSelect();
closePanelTimer.restart();
}

// Sentinel index to restore AFTER a delete-triggered refresh. -1
// means "no pending clamp" (normal reset semantics apply). Any
Expand Down Expand Up @@ -271,15 +282,19 @@ Item {
Logger.w("Clipboard Plugin", "activateSelection: no entry at index", idx);
return;
}
root.pluginApi?.mainInstance?.copy(entry.id);
if (entry.pinned && entry.pinnedIndex >= 0) {
root.pluginApi?.mainInstance?.copyPinned(entry.pinnedIndex);
} else {
root.pluginApi?.mainInstance?.copy(entry.id);
}
const typeSlug = entry.type === "file" ? "file"
: entry.type === "image" ? "image"
: "text";
ToastService.showNotice(
root.pluginApi?.tr("toast.item-copied-" + typeSlug + "-title"),
root.pluginApi?.tr("toast.item-copied-" + typeSlug + "-body")
);
closePanelTimer.restart();
root.closeAfterCopy();
}

// Pin / unpin the selected row. Toggles based on the entry's
Expand Down Expand Up @@ -650,6 +665,10 @@ Item {
// the Quickshell log without flooding on repeat attempts.
Logger.w("Clipboard Plugin", "closePanel returned false — screen:", targetScreen?.name ?? "null");
}
if (ok && root.pasteAfterClose) {
root.pluginApi?.mainInstance?.pasteIntoPreviousWindow();
}
root.pasteAfterClose = false;
}
}

Expand Down Expand Up @@ -1152,7 +1171,9 @@ Item {
// (reuseItems: true) cannot carry a stale screen
// reference through the deferred-close timer — see the
// closePanelTimer commentary above.
onCopied: closePanelTimer.restart()
onCopied: {
root.closeAfterCopy();
}
Comment thread
alepspizzetti marked this conversation as resolved.
}

// A subtle scrollbar keeps the user oriented in a long list.
Expand Down Expand Up @@ -1203,7 +1224,9 @@ Item {
// background Rectangle) — so grid cells highlight
// consistently on the Images tab.
selected: index === root.selectedIndex
onCopied: closePanelTimer.restart()
onCopied: {
root.closeAfterCopy();
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions clipboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ A clipboard history panel for the [Noctalia](https://github.com/noctalia-dev/noc
- Noctalia `4.1.2`+
- [cliphist](https://github.com/sentriz/cliphist)
- `wl-clipboard`
- `wtype` (optional, required for automatic paste after selection)

## Installation

Expand All @@ -42,6 +43,9 @@ Settings are stored in `~/.config/noctalia/plugins/clipboard/settings.json` (cre
|---|---|---|
| `maxHistorySize` | `100` | Maximum number of entries retained in history. |
| `showImagePreviews` | `true` | Show image thumbnails inline in the history panel. |
| `clearHistoryOnStartup` | `false` | Clear regular history whenever the shell starts; pinned items remain. |
| `autoPasteOnSelect` | `false` | Paste the selected entry into the previously focused window. Requires `wtype`. |
| `autoPasteDelayMs` | `500` | Delay before automatic paste (100-2000 ms), allowing focus to return to the previous window. |
| `density` | `"comfortable"` | Visual density of the list. `"comfortable"` or `"compact"`. |

## IPC
Expand Down
44 changes: 40 additions & 4 deletions clipboard/Settings.qml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import qs.Widgets
// - docs/specs/plugin-qml-idioms.md (two-phase save pattern,
// null-guarding, widget library, i18n via pluginApi?.tr)
//
// Three controls stacked vertically map to the keys in
// Controls stacked vertically map to the keys in
// manifest.json metadata.defaultSettings:
// maxHistorySize — NSpinBox, 20–500, default 100
// showImagePreviews — NToggle, default true
Expand All @@ -29,6 +29,8 @@ ColumnLayout {
// Injected by the shell after instantiation. Always null-guard every
// access with optional chaining (?.) — per docs/specs/plugin-qml-idioms.md.
property var pluginApi: null
property var cfg: pluginApi?.pluginSettings || ({})
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})

// --- Pending state ------------------------------------------------------
//
Expand All @@ -38,9 +40,12 @@ ColumnLayout {
// exactly — drift between the two is a real bug (a plugin instance that
// predates a defaults change falls back to the QML literal here, not
// the manifest value).
property int valueMaxHistorySize: pluginApi?.pluginSettings?.maxHistorySize ?? 100
property bool valueShowImagePreviews: pluginApi?.pluginSettings?.showImagePreviews ?? true
property string valueDensity: pluginApi?.pluginSettings?.density ?? "comfortable"
property int valueMaxHistorySize: cfg.maxHistorySize ?? defaults.maxHistorySize ?? 100
property bool valueShowImagePreviews: cfg.showImagePreviews ?? defaults.showImagePreviews ?? true
property bool valueClearHistoryOnStartup: cfg.clearHistoryOnStartup ?? defaults.clearHistoryOnStartup ?? false
property bool valueAutoPasteOnSelect: cfg.autoPasteOnSelect ?? defaults.autoPasteOnSelect ?? false
property int valueAutoPasteDelayMs: cfg.autoPasteDelayMs ?? defaults.autoPasteDelayMs ?? 500
property string valueDensity: cfg.density ?? defaults.density ?? "comfortable"

// --- Controls -----------------------------------------------------------

Expand Down Expand Up @@ -69,6 +74,34 @@ ColumnLayout {
onToggled: checked => { root.valueShowImagePreviews = checked; }
}

NToggle {
Layout.fillWidth: true
label: pluginApi?.tr("settings.clear-history-on-startup")
description: pluginApi?.tr("settings.clear-history-on-startup-description")
checked: root.valueClearHistoryOnStartup
onToggled: checked => { root.valueClearHistoryOnStartup = checked; }
}

NToggle {
Layout.fillWidth: true
label: pluginApi?.tr("settings.auto-paste-on-select")
description: pluginApi?.tr("settings.auto-paste-on-select-description")
checked: root.valueAutoPasteOnSelect
onToggled: checked => { root.valueAutoPasteOnSelect = checked; }
}

NSpinBox {
Layout.fillWidth: true
visible: root.valueAutoPasteOnSelect
label: pluginApi?.tr("settings.auto-paste-delay")
description: pluginApi?.tr("settings.auto-paste-delay-description")
from: 100
to: 2000
stepSize: 100
value: root.valueAutoPasteDelayMs
onValueChanged: root.valueAutoPasteDelayMs = value
}

// density — enum of "compact" / "comfortable" / "spacious". The
// NComboBox model uses { key, name } entries; we bind selection through
// currentKey so the stored value is the enum string itself (matching
Expand Down Expand Up @@ -102,6 +135,9 @@ ColumnLayout {
}
pluginApi.pluginSettings.maxHistorySize = root.valueMaxHistorySize;
pluginApi.pluginSettings.showImagePreviews = root.valueShowImagePreviews;
pluginApi.pluginSettings.clearHistoryOnStartup = root.valueClearHistoryOnStartup;
pluginApi.pluginSettings.autoPasteOnSelect = root.valueAutoPasteOnSelect;
pluginApi.pluginSettings.autoPasteDelayMs = root.valueAutoPasteDelayMs;
pluginApi.pluginSettings.density = root.valueDensity;
pluginApi.saveSettings();
}
Expand Down
6 changes: 6 additions & 0 deletions clipboard/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
"max-history-size-description": "Number of clipboard entries kept in history. Takes effect on the next refresh.",
"show-image-previews": "Show image previews",
"show-image-previews-description": "Decode and display thumbnails for image entries. Disable on low-memory systems.",
"clear-history-on-startup": "Clear history on startup",
"clear-history-on-startup-description": "Clear regular history whenever Noctalia Shell starts or restarts. Pinned items are preserved.",
"auto-paste-on-select": "Paste automatically on selection",
"auto-paste-on-select-description": "Close the panel and send Ctrl+V to the previous window after selecting an entry.",
"auto-paste-delay": "Automatic paste delay",
"auto-paste-delay-description": "Milliseconds to wait for focus to return before sending Ctrl+V (100-2000 ms).",
"density": "Density",
"density-description": "Spacing of clipboard entries in the panel.",
"density-compact": "Compact",
Expand Down
48 changes: 27 additions & 21 deletions clipboard/i18n/pt.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
{
"bar": {
"tooltip": "Historico da area de transferencia"
"tooltip": "Histórico da área de transferência"
},
"context": {
"toggle": "Alternar historico da area de transferencia",
"settings": "Abrir configuracoes"
"toggle": "Alternar histórico da área de transferência",
"settings": "Abrir configurações"
},
"panel": {
"title": "Historico da area de transferencia",
"empty": "Nenhum historico de area de transferencia ainda",
"no-matches": "Sem correspondencias",
"title": "Histórico da área de transferência",
"empty": "Nenhum histórico da área de transferência ainda",
"no-matches": "Sem correspondências",
"search-placeholder": "Pesquisar...",
"delete": "Excluir entrada",
"pin": "Fixar entrada",
"unpin": "Desfixar entrada",
"wipe": "Limpar historico",
"wipe": "Limpar histórico",
"close": "Fechar",
"settings": "Configuracoes",
"settings": "Configurações",
"filter-text": "Texto",
"filter-images": "Imagens",
"filter-files": "Arquivos",
Expand All @@ -26,31 +26,37 @@
},
"settings": {
"tab-general": "Geral",
"max-history-size": "Tamanho maximo do historico",
"max-history-size-description": "Numero de entradas da area de transferencia no historico. Entra em vigor na proxima atualizacao.",
"show-image-previews": "Mostrar pre-visualizacoes de imagens",
"show-image-previews-description": "Decodificar e exibir miniaturas para entradas de imagem. Desativar em sistemas com pouca memoria.",
"max-history-size": "Tamanho máximo do histórico",
"max-history-size-description": "Número de entradas da área de transferência no histórico. Entra em vigor na próxima atualização.",
"show-image-previews": "Mostrar pré-visualizações de imagens",
"show-image-previews-description": "Decodificar e exibir miniaturas para entradas de imagem. Desativar em sistemas com pouca memória.",
"clear-history-on-startup": "Limpar histórico ao iniciar",
"clear-history-on-startup-description": "Limpar o histórico comum sempre que o Noctalia Shell iniciar ou reiniciar. Itens fixados são preservados.",
"auto-paste-on-select": "Colar automaticamente ao selecionar",
"auto-paste-on-select-description": "Fechar o painel e enviar Ctrl+V para a janela anterior depois de selecionar uma entrada.",
"auto-paste-delay": "Atraso da colagem automática",
"auto-paste-delay-description": "Milissegundos de espera para o foco retornar antes de enviar Ctrl+V (100-2000 ms).",
"density": "Densidade",
"density-description": "Espacamento das entradas da area de transferencia no painel.",
"density-description": "Espaçamento das entradas da área de transferência no painel.",
"density-compact": "Compacto",
"density-comfortable": "Confortavel",
"density-spacious": "Espacoso"
"density-comfortable": "Confortável",
"density-spacious": "Espaçoso"
},
"toast": {
"item-copied-text-title": "Texto copiado",
"item-copied-text-body": "Voce pode colar o texto da area de transferencia.",
"item-copied-text-body": "Você pode colar o texto da área de transferência.",
"item-copied-image-title": "Imagem copiada",
"item-copied-image-body": "Voce pode colar a imagem da area de transferencia.",
"item-copied-image-body": "Você pode colar a imagem da área de transferência.",
"item-copied-file-title": "Arquivo copiado",
"item-copied-file-body": "Voce pode colar o arquivo da area de transferencia.",
"item-copied-file-body": "Você pode colar o arquivo da área de transferência.",
"item-pinned": "Entrada fixada",
"item-unpinned": "Entrada desfixada",
"history-cleared": "Historico da area de transferencia limpo"
"history-cleared": "Histórico da área de transferência limpo"
},
"time": {
"just-now": "agora mesmo",
"minutes-ago": "ha {minutes}m",
"hours-ago": "ha {hours}h",
"minutes-ago": " {minutes}m",
"hours-ago": " {hours}h",
"yesterday": "ontem"
}
}
5 changes: 4 additions & 1 deletion clipboard/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "clipboard",
"name": "Clipboard",
"version": "1.0.3",
"version": "1.1.0",
"minNoctaliaVersion": "4.1.2",
"author": "yanekyuk",
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
Expand All @@ -25,6 +25,9 @@
"defaultSettings": {
"maxHistorySize": 100,
"showImagePreviews": true,
"clearHistoryOnStartup": false,
"autoPasteOnSelect": false,
"autoPasteDelayMs": 500,
"density": "comfortable"
}
}
Expand Down