cache text during chat sidebar anim + file search with plocate
C++ / fmt (pull_request) Successful in 6s
C++ / build (pull_request) Failing after 2m2s
C++ / clang-tidy (pull_request) Failing after 2m0s
JS/TS / fmt (pull_request) Failing after 11s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 27s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Successful in 1m32s
Rust / fmt (pull_request) Successful in 27s
Rust / clippy (pull_request) Successful in 48s

This commit is contained in:
2026-09-02 19:31:54 +02:00
parent ae5c1fa148
commit dfecaf9cbc
15 changed files with 501 additions and 55 deletions
+18 -1
View File
@@ -25,6 +25,8 @@ CustomListView {
return [0];
case "variant":
return SchemeVariants.query(text);
case "files":
return Files.query(text);
default:
return Apps.search(text);
}
@@ -33,7 +35,7 @@ CustomListView {
function stateForText(text: string): string {
const prefix = Config.launcher.actionPrefix;
if (text.startsWith(prefix)) {
for (const action of ["calc", "scheme", "variant"])
for (const action of ["calc", "scheme", "variant", "files"])
if (text.startsWith(`${prefix}${action} `))
return action;
@@ -163,6 +165,13 @@ CustomListView {
PropertyChanges {
root.delegate: variantItem
}
},
State {
name: "files"
PropertyChanges {
root.delegate: filesItem
}
}
]
transitions: Transition {
@@ -264,6 +273,14 @@ CustomListView {
}
}
Component {
id: filesItem
FilesItem {
list: root
}
}
Connections {
function onTextChanged() {
root.syncDisplayText();
+2
View File
@@ -85,6 +85,8 @@ Item {
} else if (text.startsWith(Config.launcher.actionPrefix)) {
if (text.startsWith(`${Config.launcher.actionPrefix}calc `))
currentItem.onClicked();
else if (text.startsWith(`${Config.launcher.actionPrefix}files `))
currentItem.onClicked();
else
currentItem.modelData.onClicked(list.currentList);
} else {
+1 -1
View File
@@ -16,7 +16,7 @@ Item {
implicitHeight: Config.launcher.sizes.itemHeight
StateLayer {
radius: Tokens.rounding.small
radius: Tokens.rounding.medium
onClicked: {
root.modelData?.onClicked(root.list);
+1 -1
View File
@@ -19,7 +19,7 @@ Item {
implicitHeight: Config.launcher.sizes.itemHeight
StateLayer {
radius: Tokens.rounding.small
radius: Tokens.rounding.medium
onClicked: {
Apps.launch(root.modelData);
+2 -3
View File
@@ -24,7 +24,7 @@ Item {
implicitHeight: Config.launcher.sizes.itemHeight
StateLayer {
radius: Tokens.rounding.small
radius: Tokens.rounding.medium
onClicked: {
root.onClicked();
@@ -97,8 +97,7 @@ Item {
text: qsTr("Open in calculator")
Behavior on opacity {
Anim {
}
Anim {}
}
}
+141
View File
@@ -0,0 +1,141 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import ZShell.Config
import qs.Components
import qs.Helpers
import qs.Modules.Launcher.Services
import qs.Services
Item {
id: root
required property var list
required property var modelData
anchors.left: parent?.left
anchors.right: parent?.right
implicitHeight: Config.launcher.sizes.itemHeight
function onClicked(): void {
root.list.visibilities.launcher = false;
if (root.modelData?.path)
Files.launch(["xdg-open", root.modelData.path]);
}
StateLayer {
id: sLayer
radius: Tokens.rounding.medium
manualHoverOverride: openLocation.hovered
onClicked: {
root.onClicked();
}
}
IconButton {
id: openLocation
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.medium
anchors.verticalCenter: parent.verticalCenter
icon: "open_in_new"
isRound: true
scale: sLayer.containsMouse || sLayer.manualHoverOverride ? 1 : 0.4
opacity: sLayer.containsMouse || sLayer.manualHoverOverride ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on scale {
Anim {
type: Anim.DefaultEffects
}
}
onClicked: {
root.list.visibilities.launcher = false;
if (root.modelData?.path)
Files.showInFolder(root.modelData.path);
}
}
Item {
anchors.fill: parent
anchors.leftMargin: Tokens.padding.small
anchors.margins: Tokens.padding.small
anchors.rightMargin: Tokens.padding.small
IconImage {
id: icon
anchors.verticalCenter: parent.verticalCenter
implicitSize: Tokens.font.size.extraLarge
source: Quickshell.iconPath(root.modelData?.icon) ?? ""
visible: !root.modelData?.isImage
}
Loader {
active: opacity > 0
anchors.verticalCenter: parent.verticalCenter
asynchronous: true
opacity: root.modelData?.isImage && image.status === Image.Loading ? 1 : 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
sourceComponent: LoadingIndicator {
color: Colors.palette.m3primaryContainer
implicitSize: Tokens.font.size.extraLarge
}
}
FadeImage {
id: image
property int implicitSize: Tokens.font.size.extraLarge
width: implicitSize
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
source: root.modelData?.isImage ? root.modelData?.path ?? "" : ""
visible: root.modelData?.isImage
}
Item {
anchors.left: icon.right
anchors.leftMargin: Tokens.spacing.medium
anchors.verticalCenter: icon.verticalCenter
implicitHeight: name.implicitHeight + desc.implicitHeight
implicitWidth: parent.width - icon.width
CustomText {
id: name
font.pointSize: Tokens.font.size.normal
text: root.modelData?.name ?? ""
elide: Text.ElideRight
width: root.width - icon.width - openLocation.implicitWidth - openLocation.anchors.rightMargin - Tokens.spacing.medium - Tokens.rounding.medium * 2
}
CustomText {
id: desc
anchors.top: name.bottom
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
text: root.modelData?.path ?? ""
width: root.width - icon.width - openLocation.implicitWidth - openLocation.anchors.rightMargin - Tokens.spacing.medium - Tokens.rounding.medium * 2
}
}
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ Item {
implicitHeight: Config.launcher.sizes.itemHeight
StateLayer {
radius: Tokens.rounding.small
radius: Tokens.rounding.medium
onClicked: {
root.modelData?.onClicked(root.list);
+194
View File
@@ -0,0 +1,194 @@
// qs/Services/Files.qml
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import ZShell.Config
Singleton {
id: root
property list<var> results: []
property string _pendingQuery: ""
property int _generation: 0
readonly property var _imageExts: ({
"png": true,
"jpg": true,
"jpeg": true,
"gif": true,
"bmp": true,
"webp": true,
"svg": true,
"tiff": true,
"tif": true,
"ico": true,
"heic": true,
"avif": true
})
// freedesktop mimetype-icon names; extend as needed
readonly property var _extIcons: ({
// images
"png": "image-x-generic",
"jpg": "image-x-generic",
"jpeg": "image-x-generic",
"gif": "image-x-generic",
"bmp": "image-x-generic",
"webp": "image-x-generic",
"svg": "image-x-generic",
"tiff": "image-x-generic",
"tif": "image-x-generic",
"ico": "image-x-generic",
"heic": "image-x-generic",
"avif": "image-x-generic",
// documents
"pdf": "application-pdf",
"doc": "x-office-document",
"docx": "x-office-document",
"odt": "x-office-document",
"txt": "text-x-generic",
"md": "text-x-generic",
"xls": "x-office-spreadsheet",
"xlsx": "x-office-spreadsheet",
"ods": "x-office-spreadsheet",
"ppt": "x-office-presentation",
"pptx": "x-office-presentation",
"odp": "x-office-presentation",
// archives
"zip": "package-x-generic",
"tar": "package-x-generic",
"gz": "package-x-generic",
"xz": "package-x-generic",
"7z": "package-x-generic",
"rar": "package-x-generic",
// audio/video
"mp3": "audio-x-generic",
"flac": "audio-x-generic",
"wav": "audio-x-generic",
"ogg": "audio-x-generic",
"mp4": "video-x-generic",
"mkv": "video-x-generic",
"webm": "video-x-generic",
"avi": "video-x-generic",
"mov": "video-x-generic",
// code
"js": "text-x-script",
"ts": "text-x-script",
"py": "text-x-script",
"sh": "text-x-script",
"qml": "text-x-script",
"cpp": "text-x-c++src",
"c": "text-x-csrc",
"h": "text-x-chdr",
"json": "application-json",
"html": "text-html",
"css": "text-css"
})
function showInFolder(path: string): void {
const explorer = Config.general.apps.explorer;
let args = [];
if (explorer.includes("dolphin"))
args = [...explorer, "--select", path];
else
args = [...explorer, path];
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "--", ...args]);
else
Quickshell.execDetached([...args]);
}
function launch(command: list<string>): void {
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "--"].concat(command));
else
Quickshell.execDetached(command);
}
function transformSearch(search: string): string {
return search.slice(Config.launcher.actionPrefix.length + "files ".length);
}
function query(search: string): list<var> {
search = transformSearch(search);
if (search !== _pendingQuery) {
_pendingQuery = search;
debounceTimer.restart();
}
return results;
}
function _extOf(name: string): string {
const i = name.lastIndexOf(".");
// no dot, or dotfile with no real extension (".bashrc" -> treat as no ext)
if (i <= 0)
return "";
return name.slice(i + 1).toLowerCase();
}
function _iconFor(name: string, isDir: bool): string {
if (isDir)
return "folder";
const ext = _extOf(name);
return _extIcons[ext] ?? "text-x-generic";
}
function _buildEntry(path: string): var {
const name = path.split("/").pop();
const ext = _extOf(name);
// trailing slash from plocate's directory output (if -d/dir results ever appear)
const isDir = path.endsWith("/");
return {
path: path,
name: name,
icon: _iconFor(name, isDir),
isImage: !isDir && (_imageExts[ext] ?? false)
};
}
Timer {
id: debounceTimer
interval: 120
onTriggered: root._runQuery(root._pendingQuery)
}
function _runQuery(search: string): void {
_generation++;
if (!search) {
proc.running = false;
results = [];
return;
}
if (proc.running)
proc.running = false;
proc.buffer = [];
proc.myGeneration = _generation;
proc.command = ["plocate", "-i", "-b", "--limit", "50", search];
proc.running = true;
}
Process {
id: proc
property var buffer: []
property int myGeneration: 0
stdout: SplitParser {
onRead: line => proc.buffer.push(root._buildEntry(line))
}
onRunningChanged: {
if (!running && myGeneration === root._generation)
root.results = buffer;
}
}
}