Skip to content

Commit db091da

Browse files
authored
feat: add automatic paste feature to clipboard history and update related settings (#66)
1 parent 687ca2c commit db091da

9 files changed

Lines changed: 205 additions & 121 deletions

data/po/pt_BR.po

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ msgid ""
66
msgstr ""
77
"Project-Id-Version: aurora-shell\n"
88
"Report-Msgid-Bugs-To: https://github.com/luminusOS/aurora-shell/issues\n"
9-
"POT-Creation-Date: 2026-07-11 22:48-0300\n"
9+
"POT-Creation-Date: 2026-07-14 13:44-0300\n"
1010
"PO-Revision-Date: 2026-03-09 18:00-0300\n"
1111
"Last-Translator: Aurora Shell Contributors\n"
1212
"Language-Team: Portuguese (Brazil)\n"
@@ -33,10 +33,18 @@ msgid "Keyboard shortcut to open the clipboard history panel"
3333
msgstr ""
3434

3535
#: dist/clipboard/clipboardHistory.manifest.js:19
36+
msgid "Paste Automatically"
37+
msgstr "Colar automaticamente"
38+
39+
#: dist/clipboard/clipboardHistory.manifest.js:20
40+
msgid "Insert the selected text into the previously focused input"
41+
msgstr "Insere o texto selecionado no campo que estava em foco anteriormente"
42+
43+
#: dist/clipboard/clipboardHistory.manifest.js:25
3644
msgid "Poll Interval (ms)"
3745
msgstr ""
3846

39-
#: dist/clipboard/clipboardHistory.manifest.js:20
47+
#: dist/clipboard/clipboardHistory.manifest.js:26
4048
msgid "How often to check the clipboard for changes"
4149
msgstr ""
4250

@@ -69,7 +77,7 @@ msgstr "O histórico da área de transferência está vazio"
6977
msgid "Copy text, links, code, or images to see them here."
7078
msgstr "Copie textos, links, códigos ou imagens para vê-los aqui."
7179

72-
#: dist/clipboard/clipboardPanel.js:35
80+
#: dist/clipboard/clipboardPanel.js:38
7381
msgid "Search…"
7482
msgstr "Pesquisar…"
7583

@@ -250,15 +258,15 @@ msgid "No recent items"
250258
msgstr ""
251259

252260
#: dist/panel/auroraMenu.js:339 dist/panel/auroraMenu.js:352
253-
#: dist/panel/auroraMenu.js:371 dist/panel/auroraMenu.manifest.js:8
261+
#: dist/panel/auroraMenu.js:374 dist/panel/auroraMenu.manifest.js:8
254262
msgid "Aurora Menu"
255263
msgstr ""
256264

257265
#: dist/panel/auroraMenu.js:339 dist/panel/auroraMenu.js:352
258266
msgid "Could not launch the selected command."
259267
msgstr ""
260268

261-
#: dist/panel/auroraMenu.js:371
269+
#: dist/panel/auroraMenu.js:374
262270
msgid "Could not open the selected recent item."
263271
msgstr ""
264272

data/schemas/org.gnome.shell.extensions.aurora-shell.gschema.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@
307307
<summary>Clipboard History shortcut</summary>
308308
<description>Keyboard shortcut to open the clipboard history panel</description>
309309
</key>
310+
<key name="clipboard-history-auto-paste" type="b">
311+
<default>true</default>
312+
<summary>Insert clipboard text automatically</summary>
313+
<description>Insert the selected clipboard text into the input that was focused before opening the panel</description>
314+
</key>
310315
<key name="clipboard-history-poll-interval" type="i">
311316
<range min="250" max="5000"/>
312317
<default>1000</default>

src/clipboard/clipboardHistory.manifest.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ export const manifest: ModuleManifest = {
1414
subtitle: _('Keyboard shortcut to open the clipboard history panel'),
1515
type: 'shortcut',
1616
},
17+
{
18+
key: 'clipboard-history-auto-paste',
19+
title: _('Paste Automatically'),
20+
subtitle: _('Insert the selected text into the previously focused input'),
21+
type: 'switch',
22+
},
1723
{
1824
key: 'clipboard-history-poll-interval',
1925
title: _('Poll Interval (ms)'),

src/clipboard/clipboardHistory.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { ClipboardMonitor } from '~/clipboard/clipboardMonitor.ts';
1919
import { ClipboardPanel } from '~/clipboard/clipboardPanel.ts';
2020

2121
const KEYBINDING_KEY = 'clipboard-history-shortcut';
22+
const AUTO_PASTE_KEY = 'clipboard-history-auto-paste';
23+
const AUTO_PASTE_DELAY_MS = 100;
2224
const LOG_PREFIX = 'ClipboardHistory';
2325

2426
// @ts-ignore - _promisify is a GJS extension not reflected in .d.ts
@@ -30,6 +32,8 @@ export class ClipboardHistory extends Module {
3032
private _panel: ClipboardPanel | null = null;
3133
private _cleanup: CleanupBag | null = null;
3234
private _startupIdleId: number = 0;
35+
private _autoPasteTimeoutId: number = 0;
36+
private _pasteTargetWindow: Meta.Window | null = null;
3337

3438
constructor(context: ExtensionContext) {
3539
super(context);
@@ -102,6 +106,9 @@ export class ClipboardHistory extends Module {
102106
}
103107

104108
override disable(): void {
109+
this._cancelScheduledAutoPaste();
110+
this._pasteTargetWindow = null;
111+
105112
this._cleanup?.dispose();
106113
this._cleanup = null;
107114

@@ -118,13 +125,15 @@ export class ClipboardHistory extends Module {
118125

119126
openPanel(): boolean {
120127
if (!this._panel) return false;
128+
if (!this._panel.isOpen) this._pasteTargetWindow = global.display.focus_window;
121129
this._panel.open();
122130
return true;
123131
}
124132

125133
closePanel(): boolean {
126134
if (!this._panel) return false;
127135
this._panel.close();
136+
this._pasteTargetWindow = null;
128137
return true;
129138
}
130139

@@ -165,8 +174,12 @@ export class ClipboardHistory extends Module {
165174
return;
166175
}
167176

177+
const pasteTarget = this.context.settings.getBoolean(AUTO_PASTE_KEY)
178+
? this._pasteTargetWindow
179+
: null;
168180
St.Clipboard.get_default().set_text(St.ClipboardType.CLIPBOARD, entry.text);
169181
this.closePanel();
182+
if (pasteTarget) this._scheduleAutoPaste(pasteTarget, entry.text);
170183
logger.debug(`Restored clipboard entry: ${entry.text.slice(0, 40)}`, { prefix: LOG_PREFIX });
171184
}
172185

@@ -182,6 +195,34 @@ export class ClipboardHistory extends Module {
182195
}
183196
}
184197

198+
private _scheduleAutoPaste(targetWindow: Meta.Window, text: string): void {
199+
if (!this._cleanup) return;
200+
201+
this._cancelScheduledAutoPaste();
202+
try {
203+
targetWindow.activate(global.get_current_time());
204+
} catch (e) {
205+
logger.warn(
206+
'Failed to restore focus before automatic paste:',
207+
{ prefix: LOG_PREFIX },
208+
e as Error,
209+
);
210+
return;
211+
}
212+
213+
this._autoPasteTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, AUTO_PASTE_DELAY_MS, () => {
214+
this._autoPasteTimeoutId = 0;
215+
if (this._cleanup && Main.inputMethod.currentFocus) Main.inputMethod.commit(text);
216+
return GLib.SOURCE_REMOVE;
217+
});
218+
}
219+
220+
private _cancelScheduledAutoPaste(): void {
221+
if (this._autoPasteTimeoutId === 0) return;
222+
GLib.source_remove(this._autoPasteTimeoutId);
223+
this._autoPasteTimeoutId = 0;
224+
}
225+
185226
private _onRemove(id: string): void {
186227
this._store?.remove(id);
187228
this._panel?.refresh();

src/clipboard/clipboardPanel.ts

Lines changed: 16 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@ import { gettext as _ } from 'gettext';
44
import St from '@girs/st-18';
55
import GObject from '@girs/gobject-2.0';
66
import Clutter from '@girs/clutter-18';
7-
import type Meta from '@girs/meta-18';
87
import * as Main from '@girs/gnome-shell/ui/main';
98

109
import type { ClipboardEntry, ClipboardStore } from '~/clipboard/clipboardStore.ts';
1110
import { ClipboardList } from '~/clipboard/clipboardList.ts';
1211
import {
12+
findClipboardPanelMonitorAtPoint,
1313
placeClipboardPanelNearPointer,
14-
resolveClipboardPanelAnchor,
15-
type ClipboardPanelWindowPlacement,
1614
} from '~/clipboard/clipboardPosition.ts';
1715

1816
const PANEL_WIDTH = 360;
@@ -101,10 +99,10 @@ export class ClipboardPanel extends St.BoxLayout {
10199
return Clutter.EVENT_STOP;
102100
});
103101

104-
Main.uiGroup.add_child(this._overlay);
105-
Main.uiGroup.add_child(this); // panel sits above overlay
102+
Main.layoutManager.addTopChrome(this._overlay, { trackFullscreen: false });
103+
Main.layoutManager.addTopChrome(this, { trackFullscreen: false }); // panel sits above overlay
106104

107-
this._positionNearCurrentAnchor();
105+
this._positionNearPointer();
108106

109107
this.show();
110108
this._isOpen = true;
@@ -149,12 +147,12 @@ export class ClipboardPanel extends St.BoxLayout {
149147
}
150148

151149
if (this._overlay) {
152-
Main.uiGroup.remove_child(this._overlay);
150+
Main.layoutManager.removeChrome(this._overlay);
153151
this._overlay.destroy();
154152
this._overlay = null;
155153
}
156154

157-
Main.uiGroup.remove_child(this);
155+
Main.layoutManager.removeChrome(this);
158156
this.hide();
159157
this._isOpen = false;
160158
}
@@ -235,22 +233,13 @@ export class ClipboardPanel extends St.BoxLayout {
235233
}
236234
}
237235

238-
private _positionNearCurrentAnchor(): void {
236+
private _positionNearPointer(): void {
239237
const [pointerX, pointerY] = global.get_pointer();
240-
const pointerMonitorIndex = this._findMonitorIndexAt(pointerX, pointerY);
241-
const anchor = resolveClipboardPanelAnchor(
242-
pointerX,
243-
pointerY,
244-
pointerMonitorIndex,
245-
this._getFocusedWindowPlacement(),
246-
);
247-
const monitorIndex = this._isMonitorValid(anchor.monitorIndex)
248-
? anchor.monitorIndex
249-
: pointerMonitorIndex;
238+
const monitorIndex = this._findMonitorIndexAt(pointerX, pointerY);
250239
const workArea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
251240
const bounds = placeClipboardPanelNearPointer(
252-
anchor.x,
253-
anchor.y,
241+
pointerX,
242+
pointerY,
254243
workArea,
255244
PANEL_WIDTH,
256245
PANEL_HEIGHT,
@@ -263,43 +252,11 @@ export class ClipboardPanel extends St.BoxLayout {
263252
}
264253

265254
private _findMonitorIndexAt(x: number, y: number): number {
266-
const monitors = Main.layoutManager.monitors;
267-
for (let i = 0; i < monitors.length; i++) {
268-
const monitor = monitors[i]!;
269-
if (
270-
x >= monitor.x &&
271-
x < monitor.x + monitor.width &&
272-
y >= monitor.y &&
273-
y < monitor.y + monitor.height
274-
) {
275-
return i;
276-
}
277-
}
278-
279-
return Main.layoutManager.primaryIndex;
280-
}
281-
282-
private _getFocusedWindowPlacement(): ClipboardPanelWindowPlacement | null {
283-
const win = global.display.focus_window as Meta.Window | null;
284-
if (!win || win.minimized) return null;
285-
286-
const monitorIndex = win.get_monitor();
287-
if (!this._isMonitorValid(monitorIndex)) return null;
288-
289-
const frame = win.get_frame_rect();
290-
return {
291-
monitorIndex,
292-
frame: {
293-
x: frame.x,
294-
y: frame.y,
295-
width: frame.width,
296-
height: frame.height,
297-
},
298-
};
299-
}
300-
301-
private _isMonitorValid(monitorIndex: number): boolean {
302-
const monitors = Main.layoutManager.monitors ?? [];
303-
return monitorIndex >= 0 && monitorIndex < monitors.length;
255+
return findClipboardPanelMonitorAtPoint(
256+
x,
257+
y,
258+
Main.layoutManager.monitors,
259+
Main.layoutManager.primaryIndex,
260+
);
304261
}
305262
}

src/clipboard/clipboardPosition.ts

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,21 @@ export type ClipboardPanelBounds = {
55
height: number;
66
};
77

8-
export type ClipboardPanelAnchor = {
9-
x: number;
10-
y: number;
11-
monitorIndex: number;
12-
};
13-
14-
export type ClipboardPanelWindowPlacement = {
15-
monitorIndex: number;
16-
frame: ClipboardPanelBounds;
17-
};
18-
19-
export function resolveClipboardPanelAnchor(
20-
pointerX: number,
21-
pointerY: number,
22-
pointerMonitorIndex: number,
23-
focusedWindow: ClipboardPanelWindowPlacement | null,
24-
): ClipboardPanelAnchor {
25-
if (focusedWindow && focusedWindow.monitorIndex >= 0 && hasUsableFrame(focusedWindow.frame)) {
26-
return {
27-
x: focusedWindow.frame.x + Math.floor(focusedWindow.frame.width / 2),
28-
y: focusedWindow.frame.y + Math.floor(focusedWindow.frame.height / 2),
29-
monitorIndex: focusedWindow.monitorIndex,
30-
};
31-
}
32-
33-
return { x: pointerX, y: pointerY, monitorIndex: pointerMonitorIndex };
8+
export function findClipboardPanelMonitorAtPoint(
9+
x: number,
10+
y: number,
11+
monitors: readonly ClipboardPanelBounds[],
12+
fallbackMonitorIndex: number,
13+
): number {
14+
const monitorIndex = monitors.findIndex(
15+
(monitor) =>
16+
x >= monitor.x &&
17+
x < monitor.x + monitor.width &&
18+
y >= monitor.y &&
19+
y < monitor.y + monitor.height,
20+
);
21+
22+
return monitorIndex >= 0 ? monitorIndex : fallbackMonitorIndex;
3423
}
3524

3625
export function placeClipboardPanelNearPointer(
@@ -71,7 +60,3 @@ function clamp(value: number, min: number, max: number): number {
7160
if (max < min) return min;
7261
return Math.max(min, Math.min(max, value));
7362
}
74-
75-
function hasUsableFrame(frame: ClipboardPanelBounds): boolean {
76-
return frame.width > 0 && frame.height > 0;
77-
}

0 commit comments

Comments
 (0)