clipboard history using cliphist
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 15s
Python / test (pull_request) Successful in 29s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m6s

This commit is contained in:
2026-06-11 14:35:42 +02:00
parent e90f1facb7
commit 130e613eb5
14 changed files with 492 additions and 70 deletions
-6
View File
@@ -17,21 +17,15 @@ Item {
implicitWidth: content.implicitWidth
visible: width > 0 && height > 0
x: {
if (content.isDetached)
return (parent.width - content.nonAnimWidth) / 2;
const off = content.currentCenter - Config.barConfig.border - content.nonAnimWidth / 2;
const diff = parent.width - Math.floor(off + content.nonAnimWidth);
if (diff < 0)
return off + diff;
return Math.floor(Math.max(off, 0));
}
y: content.isDetached ? (parent.height - content.nonAnimHeight) / 2 : 0
Behavior on offsetScale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on x {
+148
View File
@@ -0,0 +1,148 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Helpers
import qs.Config
Item {
id: root
required property ShellScreen screen
required property PersistentProperties visibilities
implicitHeight: screen.height / 2
implicitWidth: 500
Component.onCompleted: {
if (!ClipHistory.entries.length > 0)
ClipHistory.refresh();
searchField.forceActiveFocus();
}
CustomClippingRect {
id: search
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: 50
radius: Appearance.rounding.full
MaterialIcon {
id: searchIcon
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.verticalCenter: parent.verticalCenter
text: "search"
}
CustomTextField {
id: searchField
anchors.bottom: parent.bottom
anchors.left: searchIcon.right
anchors.leftMargin: Appearance.spacing.small
anchors.right: parent.right
anchors.top: parent.top
color: DynamicColors.palette.m3onSurface
placeholderText: "Search clipboard history..."
}
}
CustomClippingRect {
id: entries
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.top: search.bottom
anchors.topMargin: Appearance.spacing.normal
radius: Appearance.rounding.normal
ListView {
id: view
anchors.fill: parent
spacing: Appearance.spacing.normal
delegate: RowLayout {
id: clipItem
required property string modelData
height: 50
width: view.width
CustomClippingRect {
id: textRect
Layout.fillHeight: true
Layout.fillWidth: true
// Layout.preferredWidth: implicitWidth + (textLayer.pressed ? 18 : 0)
// implicitWidth: 250
radius: textLayer.pressed ? (Appearance.rounding.small / 2) : Appearance.rounding.small
Behavior on Layout.preferredWidth {
Anim {
type: Anim.FastEffects
}
}
Behavior on radius {
Anim {
type: Anim.FastEffects
}
}
CustomText {
id: text
anchors.left: parent.left
anchors.margins: Appearance.padding.normal
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
text: clipItem.modelData
}
StateLayer {
id: textLayer
onClicked: ClipHistory.copy(clipItem.modelData)
}
}
IconButton {
Layout.fillHeight: true
Layout.margins: Appearance.padding.smallest
Layout.preferredWidth: height
icon: "content_copy"
isToggle: false
// implicitWidth: 30
}
IconButton {
Layout.fillHeight: true
Layout.margins: Appearance.padding.smallest
Layout.preferredWidth: height
icon: "delete"
inactiveColor: Qt.alpha(DynamicColors.palette.m3error, 0.8)
inactiveOnColor: DynamicColors.palette.m3onError
isToggle: false
}
}
model: ScriptModel {
values: {
const entries = ClipHistory.entries;
const search = searchField.text;
var regex = new RegExp(search, "i");
return entries.filter(n => regex.test(n));
}
}
}
}
}
+42
View File
@@ -0,0 +1,42 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Components
import qs.Config
Item {
id: root
property int contentHeight
property real offsetScale: shouldBeActive ? 0 : 1
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.clipboard
required property PersistentProperties visibilities
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight + Appearance.padding.normal * 2
implicitWidth: content.implicitWidth + Appearance.padding.normal * 2 || 400
opacity: 1 - offsetScale
visible: offsetScale < 1
Behavior on offsetScale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Loader {
id: content
active: root.shouldBeActive || root.visible
anchors.centerIn: parent
asynchronous: true
sourceComponent: Content {
screen: root.screen
visibilities: root.visibilities
}
}
}
@@ -267,8 +267,8 @@ CustomRect {
checked: Recorder.paused
font.pointSize: Appearance.font.size.large
icon: Recorder.paused ? "play_arrow" : "pause"
isToggle: true
label.animate: true
toggle: true
type: IconButton.Tonal
onClicked: {
@@ -280,8 +280,8 @@ CustomRect {
IconButton {
font.pointSize: Appearance.font.size.large
icon: "stop"
inactiveColour: DynamicColors.palette.m3error
inactiveOnColour: DynamicColors.palette.m3onError
inactiveColor: DynamicColors.palette.m3error
inactiveOnColor: DynamicColors.palette.m3onError
onClicked: Recorder.stop()
}
@@ -101,11 +101,11 @@ CustomRect {
component Toggle: IconButton {
Layout.fillWidth: true
Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? 18 : internalChecked ? 7 : 0)
inactiveColour: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
inactiveColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
isToggle: true
radius: stateLayer.pressed ? 6 / 2 : internalChecked ? 6 : 8
radiusAnim.duration: MaterialEasing.expressiveEffectsTime
radiusAnim.easing.bezierCurve: MaterialEasing.expressiveEffects
toggle: true
Behavior on Layout.preferredWidth {
Anim {
@@ -221,11 +221,11 @@ Item {
font: Appearance.font.family.sans
// icon: root.iconForId(modelData.entry.id)
icon: root.labelForId(modelData.entry.id)
inactiveColour: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
inactiveColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2)
isToggle: true
radius: stateLayer.pressed ? 6 / 2 : internalChecked ? 6 : 8
radiusAnim.duration: MaterialEasing.expressiveEffectsTime
radiusAnim.easing.bezierCurve: MaterialEasing.expressiveEffects
toggle: true
visible: !["spacer", "upower", "dash", "audio"].some(prefix => modelData.entry.id.startsWith(prefix))
Behavior on Layout.preferredWidth {
+9
View File
@@ -59,4 +59,13 @@ Scope {
visibilities.settings = !visibilities.settings;
}
}
CustomShortcut {
name: "toggle-clipboard"
onPressed: {
const visibilities = Visibilities.getForActive();
visibilities.clipboard = !visibilities.clipboard;
}
}
}
-1
View File
@@ -27,7 +27,6 @@ Item {
detachedMode = "";
}
focus: hasCurrent
implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth