initial commit
This commit is contained in:
commit
2d5299c8fa
57
ActivateLinux.qml
Normal file
57
ActivateLinux.qml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import "root:/bar"
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: activateLinux
|
||||||
|
|
||||||
|
required property ShellScreen modelData
|
||||||
|
|
||||||
|
screen: modelData
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
|
margins {
|
||||||
|
right: 50
|
||||||
|
bottom: 50
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: content.width
|
||||||
|
implicitHeight: content.height
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
// Give the window an empty click mask so all clicks pass through it.
|
||||||
|
mask: Region {}
|
||||||
|
|
||||||
|
// Use the wlroots specific layer property to ensure it displays over
|
||||||
|
// fullscreen windows.
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
|
||||||
|
Pill {
|
||||||
|
Text {
|
||||||
|
text: "Ahhh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Activate Linux"
|
||||||
|
color: "#50ffffff"
|
||||||
|
font.pointSize: 22
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Go to Settings to activate Linux"
|
||||||
|
color: "#50ffffff"
|
||||||
|
font.pointSize: 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
README.md
Normal file
13
README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
|
||||||
|
# Usefull links
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
https://doc.qt.io/qt-6/qml-qtquick-repeater.html
|
||||||
|
https://quickshell.outfoxxed.me/
|
||||||
|
|
||||||
|
## Example dotfiles
|
||||||
|
https://github.com/caelestia-dots/shell
|
||||||
|
https://github.com/end-4/dots-hyprland
|
||||||
|
https://github.com/Ly-sec/nixos/tree/main/home/quickshell
|
||||||
|
|
30
bar/Music.qml
Normal file
30
bar/Music.qml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
|
||||||
|
Text {
|
||||||
|
required property MprisPlayer player
|
||||||
|
text: `${player.trackTitle} - ${player.trackArtist || "Unknown Artist"}`
|
||||||
|
|
||||||
|
color: "white"
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
onClicked: {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
parent.player.canGoNext && parent.player.next()
|
||||||
|
} else if (mouse.button === Qt.LeftButton) {
|
||||||
|
parent.player.canTogglePlaying && parent.player.togglePlaying()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheel: (event) => {
|
||||||
|
if (!parent.player.volumeSupported || !parent.player.canControl){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.player.volume = parent.player.volume + (event.angleDelta.y / 120 * 0.05)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
bar/Pill.qml
Normal file
24
bar/Pill.qml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: pill
|
||||||
|
default property alias content: container.data
|
||||||
|
|
||||||
|
// Center in the middle of the bar
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
implicitWidth: container.childrenRect.width + 15
|
||||||
|
implicitHeight: container.childrenRect.height + 12
|
||||||
|
|
||||||
|
color: "#939ab7"
|
||||||
|
radius: 18
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: container
|
||||||
|
width: childrenRect.width
|
||||||
|
height: childrenRect.height
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
}
|
54
bar/StatusBar.qml
Normal file
54
bar/StatusBar.qml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import "root:/singeltons"
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
required property ShellScreen modelData
|
||||||
|
screen: modelData
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
|
||||||
|
height: 30
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Pill {
|
||||||
|
Workspaces {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Pill {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: Time.time
|
||||||
|
|
||||||
|
color: "white"
|
||||||
|
font.pixelSize: 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Pill {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.right: music.left
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
Volume {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Pill {
|
||||||
|
id: music
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
Music {
|
||||||
|
player: Mpris.players.values[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
bar/Volume.qml
Normal file
16
bar/Volume.qml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import QtQuick
|
||||||
|
import "root:singeltons"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: `${Pipewire.volumePercent}% ${Pipewire.micMuted}`
|
||||||
|
|
||||||
|
color: "white"
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
onWheel: (event) => {
|
||||||
|
Pipewire.setVolume(Pipewire.volume + (event.angleDelta.y / 120 * 0.05))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
bar/Workspaces.qml
Normal file
51
bar/Workspaces.qml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
|
||||||
|
implicitWidth: content.implicitWidth
|
||||||
|
implicitHeight: content.implicitHeight
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: content
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: Hyprland.workspaces.values.filter(e => e.id >= 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
required property HyprlandWorkspace modelData
|
||||||
|
property bool hovered: false
|
||||||
|
|
||||||
|
width: 25
|
||||||
|
height: 20
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.name
|
||||||
|
color: modelData.active ? "#09608c" : "#cccccc"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
font.pixelSize: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
modelData.activate()
|
||||||
|
parent.hovered = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntered: parent.hovered = !modelData.active
|
||||||
|
onExited: parent.hovered = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1751741127,
|
||||||
|
"narHash": "sha256-t75Shs76NgxjZSgvvZZ9qOmz5zuBE8buUaYD28BMTxg=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "29e290002bfff26af1db6f64d070698019460302",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-25.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1749285348,
|
||||||
|
"narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "3e3afe5174c561dee0df6f2c2b2236990146329f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quickshell": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1751880110,
|
||||||
|
"narHash": "sha256-5fQ2cetL3rTHqXe2VM3puawL/8u5j6ujBr6Gdt7Iues=",
|
||||||
|
"owner": "quickshell-mirror",
|
||||||
|
"repo": "quickshell",
|
||||||
|
"rev": "5d7e07508ae3e5487edb1ac5a152120434f091d5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "quickshell-mirror",
|
||||||
|
"repo": "quickshell",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"quickshell": "quickshell"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
24
flake.nix
Normal file
24
flake.nix
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
description = "Dev shell with quickshell";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||||
|
quickshell.url = "github:quickshell-mirror/quickshell";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, quickshell, ... }:
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
in {
|
||||||
|
devShells.${system}.default = pkgs.mkShell {
|
||||||
|
packages = [
|
||||||
|
quickshell.packages.${system}.default
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
exec zsh
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
136
launcher/Launcher.qml
Normal file
136
launcher/Launcher.qml
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import "root:/singeltons"
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool show: true
|
||||||
|
|
||||||
|
implicitWidth: 600
|
||||||
|
implicitHeight: 400
|
||||||
|
visible: show
|
||||||
|
focusable: true
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||||
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
|
|
||||||
|
HyprlandWindow.opacity: 0.9
|
||||||
|
|
||||||
|
// Main window
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
color: Qt.hsla(0, 0, 1, 0.5)
|
||||||
|
border.color: Qt.hsla(0, 0, 1, 0.2)
|
||||||
|
border.width: 2
|
||||||
|
radius: 12
|
||||||
|
|
||||||
|
// Main window layout
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 16
|
||||||
|
spacing: 20
|
||||||
|
|
||||||
|
// Search bar
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 40
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: searchInput
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
focus: true
|
||||||
|
placeholderText: "Search..."
|
||||||
|
font.pixelSize: 20
|
||||||
|
background: null
|
||||||
|
color: "white"
|
||||||
|
selectByMouse: true
|
||||||
|
|
||||||
|
Keys.onPressed: (event) => {
|
||||||
|
switch (event.key){
|
||||||
|
case (Qt.Key_Escape):
|
||||||
|
root.show = false
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case (Qt.Key_Return):
|
||||||
|
case (Qt.Key_Enter):
|
||||||
|
if (resultsList.currentIndex >= 0) {
|
||||||
|
const item = resultsList.model[resultsList.currentIndex]
|
||||||
|
DesktopApps.launch(item)
|
||||||
|
}
|
||||||
|
root.show = false
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case (Qt.Key_Down):
|
||||||
|
case (Qt.Key_Tab):
|
||||||
|
resultsList.currentIndex = Math.min(resultsList.count - 1, resultsList.currentIndex + 1)
|
||||||
|
event.accepted = true
|
||||||
|
break
|
||||||
|
case (Qt.Key_Up):
|
||||||
|
case (Qt.Key_Backtab):
|
||||||
|
resultsList.currentIndex = Math.max(0, resultsList.currentIndex - 1)
|
||||||
|
break
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Results
|
||||||
|
ListView {
|
||||||
|
id: resultsList
|
||||||
|
model: DesktopApps.fuzzySearch(searchInput.text)
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
spacing: 4
|
||||||
|
keyNavigationWraps: true
|
||||||
|
|
||||||
|
// Result item
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property DesktopEntry modelData
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: 6
|
||||||
|
color: ListView.isCurrentItem ? "#3a3a3c" : "transparent"
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: parent.parent.modelData.icon !== "" ? Quickshell.iconPath(parent.parent.modelData.icon,true) : ""
|
||||||
|
visible: parent.parent.modelData.icon !== ""
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
sourceSize.width: 30
|
||||||
|
sourceSize.height: 30
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: parent.parent.modelData.name
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
font.pixelSize: 18
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
21
shell.qml
Normal file
21
shell.qml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
import "./bar"
|
||||||
|
import "./launcher/"
|
||||||
|
|
||||||
|
ShellRoot{
|
||||||
|
// Variants{
|
||||||
|
// model: Quickshell.screens
|
||||||
|
//
|
||||||
|
// ActivateLinux {}
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
Variants {
|
||||||
|
model: Quickshell.screens
|
||||||
|
|
||||||
|
StatusBar {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launcher {}
|
||||||
|
}
|
18
singeltons/DesktopApps.qml
Normal file
18
singeltons/DesktopApps.qml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import "root:/utils/fuzzySearch.js" as FuzzySearch
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property list<DesktopEntry> all: DesktopEntries.applications.values
|
||||||
|
|
||||||
|
function launch(entry: DesktopEntry): void {
|
||||||
|
entry.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
function fuzzySearch(query: string): var {
|
||||||
|
return FuzzySearch.fuzzySearch(query, all);
|
||||||
|
}
|
||||||
|
}
|
30
singeltons/Pipewire.qml
Normal file
30
singeltons/Pipewire.qml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||||
|
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||||
|
|
||||||
|
readonly property bool muted: sink?.audio?.muted ?? false
|
||||||
|
readonly property real volume: sink?.audio?.volume ?? 0
|
||||||
|
|
||||||
|
readonly property int volumePercent: Math.round(volume * 100)
|
||||||
|
|
||||||
|
|
||||||
|
readonly property bool micMuted: source?.audio?.muted ?? false
|
||||||
|
|
||||||
|
function setVolume(volume: real): void {
|
||||||
|
if (sink?.ready && sink?.audio) {
|
||||||
|
sink.audio.muted = false;
|
||||||
|
sink.audio.volume = volume;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PwObjectTracker {
|
||||||
|
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
|
||||||
|
}
|
||||||
|
}
|
17
singeltons/Time.qml
Normal file
17
singeltons/Time.qml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Time.qml
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
readonly property string time: {
|
||||||
|
Qt.formatDateTime(clock.date, "hh:mm")
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemClock {
|
||||||
|
id: clock
|
||||||
|
precision: SystemClock.Seconds
|
||||||
|
}
|
||||||
|
}
|
70
utils/fuzzySearch.js
Normal file
70
utils/fuzzySearch.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Fuzzy search for list of objects.
|
||||||
|
* - Each object in `items` must have a `value`. Or set the search key in options.
|
||||||
|
* - Returns an array of original objects ordered by their relevance.
|
||||||
|
* - Implements a cutoff for non-matching strings based on minimum relevance. Can be set in the options.
|
||||||
|
*
|
||||||
|
* @param {string} query - The search string.
|
||||||
|
* @param {Object[]} items - The array of objects to search. Each must have a 'value' property (string).
|
||||||
|
* @param {object} [options] - Optional settings.
|
||||||
|
* @param {number} [options.cutoff=0.2] - Minimum relevance threshold (0 to 1).
|
||||||
|
* @param {string} [options.key='value'] - The property name to match against (default: 'value').
|
||||||
|
* @param {boolean} [options.allOnEmpty=true] - Return all items on empty query.
|
||||||
|
* @param {Object} [options.boostMap] - An object mapping item keys (by 'value') to normalized boost values (0 to 1).
|
||||||
|
* @param {number} [options.boostWeight=0.1] - The maximum boost to apply for boostMap value 1.0.
|
||||||
|
* @returns {Object[]} - Array of matching objects sorted by relevance.
|
||||||
|
*/
|
||||||
|
function fuzzySearch(query, items, options = {}) {
|
||||||
|
const cutoff = options.cutoff ?? 0.2;
|
||||||
|
const key = options.key ?? 'name';
|
||||||
|
const boostMap = options.boostMap || {};
|
||||||
|
const boostWeight = options.boostWeight ?? 0.1;
|
||||||
|
const allOnEmpty = options.allOnEmpty == undefined || options.allOnEmpty == true;
|
||||||
|
|
||||||
|
if (!query) return allOnEmpty ? items : []
|
||||||
|
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
|
||||||
|
return items
|
||||||
|
.map(item => {
|
||||||
|
const text = (item[key] || '').toLowerCase();
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
// Perfect match
|
||||||
|
if (text === q) score = 1.0;
|
||||||
|
// Starts with
|
||||||
|
else if (text.startsWith(q)) score = 0.9 + 0.01 * (1 - q.length / (text.length || 1));
|
||||||
|
// Substring match
|
||||||
|
else {
|
||||||
|
const idx = text.indexOf(q);
|
||||||
|
if (idx !== -1) {
|
||||||
|
const startScore = 0.7 - 0.1 * (idx / (text.length || 1));
|
||||||
|
const lengthScore = 0.2 * (q.length / (text.length || 1));
|
||||||
|
score = startScore + lengthScore;
|
||||||
|
} else {
|
||||||
|
// Fuzzy: all query chars in order
|
||||||
|
let lastIdx = -1;
|
||||||
|
let found = true;
|
||||||
|
for (let c of q) {
|
||||||
|
lastIdx = text.indexOf(c, lastIdx + 1);
|
||||||
|
if (lastIdx === -1) {
|
||||||
|
found = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) {
|
||||||
|
score = 0.5 * (q.length / (text.length || 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boost: expects normalized boostMap (values from 0 to 1)
|
||||||
|
const boost = Math.max(0, Math.min(1, boostMap[item[key]] || 0)) * boostWeight;
|
||||||
|
|
||||||
|
return { item, score: score + boost };
|
||||||
|
})
|
||||||
|
.filter(result => result.score >= cutoff)
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.map(result => result.item);
|
||||||
|
}
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user