overview: search

This commit is contained in:
end-4
2025-04-25 20:35:37 +02:00
parent e1359116b8
commit 63c844cdeb
4 changed files with 214 additions and 29 deletions
@@ -197,6 +197,7 @@ Singleton {
property int barPreferredSideSectionWidth: 400
property int sidebarWidth: 450
property int notificationPopupWidth: 410
property int searchWidthCollapsed: 260
property int searchWidth: 450
property int hyprlandGapsOut: 5
property int elevationMargin: 7
@@ -41,6 +41,12 @@ Singleton {
property int updateInterval: 3000
}
property QtObject search: QtObject {
property int nonAppResultDelay: 30 // This prevents lagging when typing
property string engineBaseUrl: "https://www.google.com/search?q="
property list<string> excludedSites: [ "quora.com" ]
}
property QtObject hacks: QtObject {
property int arbitraryRaceConditionDelay: 10 // milliseconds
}
@@ -11,12 +11,17 @@ import Quickshell.Widgets
Button {
id: root
property DesktopEntry desktopEntry
property string itemName: desktopEntry?.name
property string itemIcon: desktopEntry?.icon
property var itemExecute: desktopEntry?.execute
property string itemClickActionName: desktopEntry?.clickActionName
property var entry
property bool entryShown: entry?.shown ?? true
property string itemType: entry?.type
property string itemName: entry?.name
property string itemIcon: entry?.icon ?? ""
property var itemExecute: entry?.execute
property string fontType: entry?.fontType ?? "main"
property string itemClickActionName: entry?.clickActionName
property string materialSymbol: entry?.materialSymbol ?? ""
visible: root.entryShown
property int horizontalMargin: 10
property int buttonHorizontalPadding: 10
property int buttonVerticalPadding: 5
@@ -61,20 +66,46 @@ Button {
anchors.leftMargin: root.horizontalMargin + root.buttonHorizontalPadding
anchors.rightMargin: root.horizontalMargin + root.buttonHorizontalPadding
// Icon
IconImage {
visible: root.materialSymbol == ""
source: Quickshell.iconPath(root.itemIcon);
width: 35
height: 35
}
StyledText {
Layout.fillWidth: true
id: nameText
font.pixelSize: Appearance.font.pixelSize.normal
MaterialSymbol {
visible: root.materialSymbol != ""
text: root.materialSymbol
font.pixelSize: 30
color: Appearance.m3colors.m3onSurface
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
text: root.itemName
// width: 35
// height: 35
}
// Main text
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 0
StyledText {
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colSubtext
visible: root.itemType && root.itemType != "App"
text: root.itemType
}
StyledText {
Layout.fillWidth: true
id: nameText
font.pixelSize: Appearance.font.pixelSize.normal
font.family: Appearance.font.family[root.fontType]
color: Appearance.m3colors.m3onSurface
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
text: root.itemName
}
}
// Action text
StyledText {
Layout.fillWidth: false
visible: (root.hovered || root.focus)
@@ -17,15 +17,106 @@ Item { // Wrapper
implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2
implicitHeight: searchWidgetContent.implicitHeight + Appearance.sizes.elevationMargin * 2
Keys.onPressed: {
// Only handle printable characters (ignore modifiers, arrows, etc.)
if (event.text && event.text.length === 1 && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return) {
property string mathResult: ""
Timer {
id: nonAppResultsTimer
interval: ConfigOptions.search.nonAppResultDelay
onTriggered: {
mathProcess.calculateExpression(root.searchingText);
}
}
Process {
id: mathProcess
property list<string> baseCommand: ["qalc", "-t"]
function calculateExpression(expression) {
// mathProcess.running = false
mathProcess.command = baseCommand.concat(expression)
mathProcess.running = true
}
stdout: SplitParser {
onRead: data => {
root.mathResult = data
if (searchInput.focus) appResults.currentIndex = 0; // Focus the first item
}
}
}
Process {
id: copyText
property list<string> baseCommand: ["wl-copy"]
function copyTextToClipboard(text) {
copyText.running = false
copyText.command = baseCommand.concat(text)
copyText.running = true
}
}
Process {
id: webSearch
property list<string> baseCommand: ["xdg-open"]
function search(query) {
webSearch.running = false
let url = ConfigOptions.search.engineBaseUrl + query
for (let site of ConfigOptions.search.excludedSites) {
url += ` -site:${site}`;
}
webSearch.command = baseCommand.concat(url)
webSearch.running = true
}
}
Keys.onPressed: (event) => {
// Prevent Esc and Backspace from registering
if (event.key === Qt.Key_Escape) return;
// Handle Backspace: focus and delete character if not focused
if (event.key === Qt.Key_Backspace) {
if (!searchInput.activeFocus) {
searchInput.forceActiveFocus();
if (event.modifiers & Qt.ControlModifier) {
// Delete word before cursor
let text = searchInput.text;
let pos = searchInput.cursorPosition;
if (pos > 0) {
// Find the start of the previous word
let left = text.slice(0, pos);
let match = left.match(/(\s*\S+)\s*$/);
let deleteLen = match ? match[0].length : 1;
searchInput.text = text.slice(0, pos - deleteLen) + text.slice(pos);
searchInput.cursorPosition = pos - deleteLen;
}
} else {
// Delete character before cursor if any
if (searchInput.cursorPosition > 0) {
searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition - 1) +
searchInput.text.slice(searchInput.cursorPosition);
searchInput.cursorPosition -= 1;
}
}
// Always move cursor to end after programmatic edit
searchInput.cursorPosition = searchInput.text.length;
event.accepted = true;
}
// If already focused, let TextField handle it
return;
}
// Only handle visible printable characters (ignore control chars, arrows, etc.)
if (
event.text &&
event.text.length === 1 &&
event.key !== Qt.Key_Enter &&
event.key !== Qt.Key_Return &&
event.text.charCodeAt(0) >= 0x20 // ignore control chars like Backspace, Tab, etc.
) {
if (!searchInput.activeFocus) {
searchInput.forceActiveFocus();
// Insert the character at the cursor position
searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition) +
event.text +
searchInput.text.slice(searchInput.cursorPosition);
event.text +
searchInput.text.slice(searchInput.cursorPosition);
searchInput.cursorPosition += 1;
event.accepted = true;
}
@@ -57,7 +148,12 @@ Item { // Wrapper
RowLayout {
id: searchBar
spacing: 5
KeyNavigation.down: appResults
KeyNavigation.down: {
if (appResults.count > 1) {
appResults.currentIndex = 1;
appResults.forceActiveFocus();
}
}
MaterialSymbol {
id: searchIcon
Layout.leftMargin: 15
@@ -75,8 +171,14 @@ Item { // Wrapper
selectedTextColor: Appearance.m3colors.m3onSurface
placeholderText: qsTr("Search")
placeholderTextColor: Appearance.m3colors.m3outline
implicitWidth: Appearance.sizes.searchWidth
implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.animation.elementDecelFast.duration
easing.type: Appearance.animation.elementDecelFast.type
}
}
onTextChanged: root.searchingText = text
Connections {
@@ -125,20 +227,65 @@ Item { // Wrapper
spacing: 0
KeyNavigation.up: searchBar
Connections {
target: root
function onSearchingTextChanged() {
if (appResults.count > 0)
appResults.currentIndex = 0;
}
}
model: ScriptModel {
id: model
values: DesktopEntries.applications.values
.filter((entry) => {
if (root.searchingText == "") return false
return entry.name.toLowerCase().includes(root.searchingText.toLowerCase())
})
.map((entry) => {
entry.clickActionName = "Launch";
return entry;
})
values: {
if(root.searchingText == "") return [];
// Start math and other non-app stuff
nonAppResultsTimer.restart();
// Init result array
let result = [];
// Add filtered application entries
result = result.concat(
DesktopEntries.applications.values
.filter((entry) => {
if (root.searchingText == "") return false
return entry.name.toLowerCase().includes(root.searchingText.toLowerCase())
})
.map((entry) => {
entry.clickActionName = "Launch";
entry.type = "App"
return entry;
})
);
// Add non-app results
result.push({
name: root.mathResult,
clickActionName: "Copy",
type: qsTr("Math result"),
fontType: "monospace",
materialSymbol: 'calculate',
execute: () => {
copyText.copyTextToClipboard(root.mathResult);
}
});
result.push({
name: root.searchingText,
clickActionName: "Search",
type: "Search the web",
materialSymbol: 'travel_explore',
execute: () => {
webSearch.search(root.searchingText);
}
});
return result;
}
}
delegate: SearchItem {
desktopEntry: modelData
entry: modelData
// itemName: modelData.name
// itemIcon: modelData.icon
}