2026-07-21 18:55:02 -04:00
pragma ComponentBehavior: Bound
import Qt . labs . synchronizer
import QtQuick
import QtQuick . Layouts
import Quickshell
import qs
import qs . services
import qs . modules . common
import qs . modules . common . widgets
import qs . modules . common . functions
Item { // Wrapper
id: root
readonly property string xdgConfigHome: Directories . config
readonly property int typingDebounceInterval: 200
readonly property int typingResultLimit: 15 // Should be enough to cover the whole view
property string searchingText: LauncherSearch . query
property bool showResults: searchingText != ""
implicitWidth: searchWidgetContent . implicitWidth + Appearance . sizes . elevationMargin * 2
implicitHeight: searchWidgetContent . implicitHeight + searchBar . verticalPadding * 2 + Appearance . sizes . elevationMargin * 2
function focusFirstItem ( ) {
appResults . currentIndex = 0 ;
}
function focusSearchInput ( ) {
searchBar . forceFocus ( ) ;
}
function disableExpandAnimation ( ) {
searchBar . animateWidth = false ;
}
function cancelSearch ( ) {
searchBar . searchInput . selectAll ( ) ;
LauncherSearch . query = "" ;
searchBar . animateWidth = true ;
}
function setSearchingText ( text ) {
searchBar . searchInput . text = text ;
LauncherSearch . query = text ;
}
shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
function debugState ( ) {
return {
showResults: root . showResults ,
width: root . width ,
height: root . height ,
contentWidth: searchWidgetContent . width ,
contentHeight: searchWidgetContent . height ,
columnWidth: columnLayout . width ,
columnHeight: columnLayout . height ,
listVisible: appResults . visible ,
listCount: appResults . count ,
listWidth: appResults . width ,
listHeight: appResults . height ,
listImplicitHeight: appResults . implicitHeight ,
listContentHeight: appResults . contentHeight ,
modelCount: resultModel . count
} ;
}
2026-07-21 18:55:02 -04:00
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 ( ! searchBar . searchInput . activeFocus ) {
root . focusSearchInput ( ) ;
if ( event . modifiers & Qt . ControlModifier ) {
// Delete word before cursor
let text = searchBar . searchInput . text ;
let pos = searchBar . 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 ;
searchBar . searchInput . text = text . slice ( 0 , pos - deleteLen ) + text . slice ( pos ) ;
searchBar . searchInput . cursorPosition = pos - deleteLen ;
}
} else {
// Delete character before cursor if any
if ( searchBar . searchInput . cursorPosition > 0 ) {
searchBar . searchInput . text = searchBar . searchInput . text . slice ( 0 , searchBar . searchInput . cursorPosition - 1 ) + searchBar . searchInput . text . slice ( searchBar . searchInput . cursorPosition ) ;
searchBar . searchInput . cursorPosition -= 1 ;
}
}
// Always move cursor to end after programmatic edit
searchBar . searchInput . cursorPosition = searchBar . 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 . key !== Qt . Key_Delete && event . text . charCodeAt ( 0 ) >= 0x20 ) // ignore control chars like Backspace, Tab, etc.
{
if ( ! searchBar . searchInput . activeFocus ) {
root . focusSearchInput ( ) ;
// Insert the character at the cursor position
searchBar . searchInput . text = searchBar . searchInput . text . slice ( 0 , searchBar . searchInput . cursorPosition ) + event . text + searchBar . searchInput . text . slice ( searchBar . searchInput . cursorPosition ) ;
searchBar . searchInput . cursorPosition += 1 ;
event . accepted = true ;
root . focusFirstItem ( ) ;
}
}
// Ctrl+N / Ctrl+P navigation
if ( event . modifiers & Qt . ControlModifier ) {
if ( event . key === Qt . Key_N ) {
if ( appResults . visible && appResults . count > 0 ) {
// Wrap around the list rather than the default arrow key behaviour
appResults . currentIndex = appResults . count - 1 == appResults . currentIndex ? 0 : appResults . currentIndex + 1
event . accepted = true ;
return ;
}
} else if ( event . key === Qt . Key_P ) {
if ( appResults . visible && appResults . count > 0 ) {
// Wrap around too
appResults . currentIndex = 0 == appResults . currentIndex ? appResults . count - 1 : appResults . currentIndex - 1
event . accepted = true ;
return ;
}
}
}
}
StyledRectangularShadow {
target: searchWidgetContent
}
Rectangle { // Background
id: searchWidgetContent
anchors {
top: parent . top
horizontalCenter: parent . horizontalCenter
topMargin: Appearance . sizes . elevationMargin
}
clip: true
implicitWidth: columnLayout . implicitWidth
implicitHeight: columnLayout . implicitHeight
radius: searchBar . height / 2 + searchBar . verticalPadding
color: Appearance . colors . colBackgroundSurfaceContainer
Behavior on implicitHeight {
id: searchHeightBehavior
enabled: GlobalStates . overviewOpen && root . showResults
animation: Appearance . animation . elementMove . numberAnimation . createObject ( this )
}
ColumnLayout {
id: columnLayout
anchors {
top: parent . top
horizontalCenter: parent . horizontalCenter
}
spacing: 0
SearchBar {
id: searchBar
property real verticalPadding: 4
Layout.fillWidth: true
Layout.leftMargin: 10
Layout.rightMargin: 4
Layout.topMargin: verticalPadding
Layout.bottomMargin: verticalPadding
Synchronizer on searchingText {
property alias source: root . searchingText
}
}
Rectangle {
// Separator
visible: root . showResults
Layout.fillWidth: true
height: 1
color: Appearance . colors . colOutlineVariant
}
ListView { // App results
id: appResults
visible: root . showResults
Layout.fillWidth: true
implicitHeight: Math . min ( 600 , appResults . contentHeight + topMargin + bottomMargin )
clip: true
topMargin: 10
bottomMargin: 10
spacing: 2
KeyNavigation.up: searchBar
highlightMoveDuration: 100
onFocusChanged: {
if ( focus )
appResults . currentIndex = 1 ;
}
Connections {
target: root
function onSearchingTextChanged ( ) {
if ( appResults . count > 0 )
appResults . currentIndex = 0 ;
}
}
Timer {
id: debounceTimer
interval: root . typingDebounceInterval
onTriggered: {
resultModel . values = LauncherSearch . results ? ? [ ] ;
}
}
Connections {
target: LauncherSearch
function onResultsChanged ( ) {
resultModel . values = LauncherSearch . results . slice ( 0 , root . typingResultLimit ) ;
root . focusFirstItem ( ) ;
debounceTimer . restart ( ) ;
}
}
model: ScriptModel {
id: resultModel
objectProp: "key"
}
delegate: SearchItem {
id: searchItem
// The selectable item for each search result
required property var modelData
anchors.left: parent ? . left
anchors.right: parent ? . right
entry: modelData
query: StringUtils . cleanOnePrefix ( root . searchingText , [ Config . options . search . prefix . action , Config . options . search . prefix . app , Config . options . search . prefix . clipboard , Config . options . search . prefix . emojis , Config . options . search . prefix . math , Config . options . search . prefix . shellCommand , Config . options . search . prefix . webSearch ] )
Keys.onPressed: event = > {
if ( event . key === Qt . Key_Tab ) {
if ( LauncherSearch . results . length === 0 )
return ;
const tabbedText = searchItem . modelData . name ;
LauncherSearch . query = tabbedText ;
searchBar . searchInput . text = tabbedText ;
event . accepted = true ;
root . focusSearchInput ( ) ;
}
}
}
}
}
}
}