diff options
Diffstat (limited to 'config')
61 files changed, 3760 insertions, 1792 deletions
diff --git a/config/ags/config.js b/config/ags/config.js deleted file mode 100644 index 278bf351..00000000 --- a/config/ags/config.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; -import GLib from 'gi://GLib'; -import App from 'resource:///com/github/Aylur/ags/app.js' -import userOptions from './modules/.configuration/user_options.js'; -import Overview from './modules/overview/main.js'; - -const COMPILED_STYLE_DIR = `${GLib.get_user_config_dir()}/ags/user/` - -async function applyStyle() { - - App.resetCss(); - App.applyCss(`${COMPILED_STYLE_DIR}/style.css`); - console.log('[LOG] Styles loaded') -} -applyStyle().catch(print); - -const Windows = () => [ - Overview() -]; -const CLOSE_ANIM_TIME = 210; -App.config({ - css: `${COMPILED_STYLE_DIR}/style.css`, - stackTraceOnError: true, - closeWindowDelay: { - 'sideright': CLOSE_ANIM_TIME, - 'sideleft': CLOSE_ANIM_TIME, - 'osk': CLOSE_ANIM_TIME, - }, - windows: Windows().flat(1), -}); - diff --git a/config/ags/modules/.configuration/user_options.js b/config/ags/modules/.configuration/user_options.js deleted file mode 100644 index 242c0575..00000000 --- a/config/ags/modules/.configuration/user_options.js +++ /dev/null @@ -1,127 +0,0 @@ - -import userOverrides from '../../user_options.js'; - -// Defaults -let configOptions = { - // General stuff - 'ai': { - 'defaultGPTProvider': "openai", - 'defaultTemperature': 0.9, - 'enhancements': true, - 'useHistory': true, - 'writingCursor': " ...", // Warning: Using weird characters can mess up Markdown rendering - }, - 'animations': { - 'choreographyDelay': 35, - 'durationSmall': 110, - 'durationLarge': 180, - }, - 'appearance': { - 'keyboardUseFlag': false, // Use flag emoji instead of abbreviation letters - }, - 'apps': { - 'imageViewer': "loupe", - 'terminal': "foot", // This is only for shell actions - }, - 'battery': { - 'low': 20, - 'critical': 10, - }, - 'music': { - 'preferredPlayer': "plasma-browser-integration", - }, - 'onScreenKeyboard': { - 'layout': "qwerty_full", // See modules/onscreenkeyboard/onscreenkeyboard.js for available layouts - }, - 'overview': { - 'scale': 0.18, // Relative to screen size - 'numOfRows': 2, - 'numOfCols': 5, - 'wsNumScale': 0.09, - 'wsNumMarginScale': 0.07, - }, - 'sidebar': { - 'imageColumns': 2, - 'imageBooruCount': 20, - 'imageAllowNsfw': false, - }, - 'search': { - 'engineBaseUrl': "https://www.google.com/search?q=", - 'excludedSites': [], //add site to exclude from result. eg: "quora.com" - }, - 'time': { - // See https://docs.gtk.org/glib/method.DateTime.format.html - // Here's the 12h format: "%I:%M%P" - // For seconds, add "%S" and set interval to 1000 - 'format': "%H:%M", - 'interval': 5000, - 'dateFormatLong': "%A, %d/%m", // On bar - 'dateInterval': 5000, - 'dateFormat': "%d/%m", // On notif time - }, - 'weather': { - 'city': "", - }, - 'workspaces': { - 'shown': 10, - }, - // Longer stuff - 'icons': { - substitutions: { - 'codium-url-handler': "vscodium", - 'codium': "vscodium", - 'code-url-handler': "visual-studio-code", - 'Code': "visual-studio-code", - 'GitHub Desktop': "github-desktop", - 'Minecraft* 1.20.1': "minecraft", - 'gnome-tweaks': "org.gnome.tweaks", - 'pavucontrol-qt': "pavucontrol", - 'eu.betterbird.Betterbird' : "thunderbird", - 'thunderbird-esr': "thunderbird", - 'wps': "wps-office2019-kprometheus", - 'wpsoffice': "wps-office2019-kprometheus", - 'firefox-esr': "firefox", - 'soffice' : "libreoffice", - '': "image-missing", - } - }, - 'keybinds': { - // Format: Mod1+Mod2+key. CaSe SeNsItIvE! - // Modifiers: Shift Ctrl Alt Hyper Meta - // See https://docs.gtk.org/gdk3/index.html#constants for the other keys (they are listed as KEY_key) - 'overview': { - 'altMoveLeft': "Ctrl+b", - 'altMoveRight': "Ctrl+f", - 'deleteToEnd': "Ctrl+k", - }, - 'sidebar': { - 'apis': { - 'nextTab': "Page_Down", - 'prevTab': "Page_Up", - }, - 'options': { // Right sidebar - 'nextTab': "Page_Down", - 'prevTab': "Page_Up", - }, - 'pin': "Ctrl+p", - 'cycleTab': "Ctrl+Tab", - 'nextTab': "Ctrl+Page_Down", - 'prevTab': "Ctrl+Page_Up", - }, - }, -} - -// Override defaults with user's options -function overrideConfigRecursive(userOverrides, configOptions = {}) { - for (const [key, value] of Object.entries(userOverrides)) { - if (typeof value === 'object') { - overrideConfigRecursive(value, configOptions[key]); - } else { - configOptions[key] = value; - } - } -} -overrideConfigRecursive(userOverrides, configOptions); - -globalThis['userOptions'] = configOptions; -export default configOptions;
\ No newline at end of file diff --git a/config/ags/modules/.miscutils/icons.js b/config/ags/modules/.miscutils/icons.js deleted file mode 100644 index fb1e20da..00000000 --- a/config/ags/modules/.miscutils/icons.js +++ /dev/null @@ -1,13 +0,0 @@ -const { Gtk } = imports.gi; - -export function iconExists(iconName) { - let iconTheme = Gtk.IconTheme.get_default(); - return iconTheme.has_icon(iconName); -} - -export function substitute(str) { - if(userOptions.icons.substitutions[str]) return userOptions.icons.substitutions[str]; - - if (!iconExists(str)) str = str.toLowerCase().replace(/\s+/g, '-'); // Turn into kebab-case - return str; -}
\ No newline at end of file diff --git a/config/ags/modules/.miscutils/mathfuncs.js b/config/ags/modules/.miscutils/mathfuncs.js deleted file mode 100644 index ba1c0b59..00000000 --- a/config/ags/modules/.miscutils/mathfuncs.js +++ /dev/null @@ -1,4 +0,0 @@ - -export function clamp(x, min, max) { - return Math.min(Math.max(x, min), max); -}
\ No newline at end of file diff --git a/config/ags/modules/.miscutils/system.js b/config/ags/modules/.miscutils/system.js deleted file mode 100644 index ef7d15c3..00000000 --- a/config/ags/modules/.miscutils/system.js +++ /dev/null @@ -1,54 +0,0 @@ -const { GLib } = imports.gi; -import Variable from 'resource:///com/github/Aylur/ags/variable.js'; -import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; -const { execAsync, exec } = Utils; - -export const distroID = exec(`bash -c 'cat /etc/os-release | grep "^ID=" | cut -d "=" -f 2 | sed "s/\\"//g"'`).trim(); -export const isDebianDistro = (distroID == 'linuxmint' || distroID == 'ubuntu' || distroID == 'debian' || distroID == 'zorin' || distroID == 'popos' || distroID == 'raspbian' || distroID == 'kali'); -export const isArchDistro = (distroID == 'arch' || distroID == 'endeavouros' || distroID == 'cachyos'); -export const hasFlatpak = !!exec(`bash -c 'command -v flatpak'`); - -const LIGHTDARK_FILE_LOCATION = `${GLib.get_user_cache_dir()}/ags/user/colormode.txt`; -const colorMode = Utils.exec('bash -c "sed -n \'1p\' $HOME/.cache/ags/user/colormode.txt"'); -export let darkMode = Variable(!(Utils.readFile(LIGHTDARK_FILE_LOCATION).split('\n')[0].trim() == 'light')); -export const hasPlasmaIntegration = !!Utils.exec('bash -c "command -v plasma-browser-integration-host"'); - -export const getDistroIcon = () => { - // Arches - if(distroID == 'arch') return 'arch-symbolic'; - if(distroID == 'endeavouros') return 'endeavouros-symbolic'; - if(distroID == 'cachyos') return 'cachyos-symbolic'; - // Funny flake - if(distroID == 'nixos') return 'nixos-symbolic'; - // Cool thing - if(distroID == 'fedora') return 'fedora-symbolic'; - // Debians - if(distroID == 'linuxmint') return 'ubuntu-symbolic'; - if(distroID == 'ubuntu') return 'ubuntu-symbolic'; - if(distroID == 'debian') return 'debian-symbolic'; - if(distroID == 'zorin') return 'ubuntu-symbolic'; - if(distroID == 'popos') return 'ubuntu-symbolic'; - if(distroID == 'raspbian') return 'debian-symbolic'; - if(distroID == 'kali') return 'debian-symbolic'; - return 'linux-symbolic'; -} - -export const getDistroName = () => { - // Arches - if(distroID == 'arch') return 'Arch Linux'; - if(distroID == 'endeavouros') return 'EndeavourOS'; - if(distroID == 'cachyos') return 'CachyOS'; - // Funny flake - if(distroID == 'nixos') return 'NixOS'; - // Cool thing - if(distroID == 'fedora') return 'Fedora'; - // Debians - if(distroID == 'linuxmint') return 'Linux Mint'; - if(distroID == 'ubuntu') return 'Ubuntu'; - if(distroID == 'debian') return 'Debian'; - if(distroID == 'zorin') return 'Zorin'; - if(distroID == 'popos') return 'Pop!_OS'; - if(distroID == 'raspbian') return 'Raspbian'; - if(distroID == 'kali') return 'Kali Linux'; - return 'Linux'; -} diff --git a/config/ags/modules/.widgethacks/advancedrevealers.js b/config/ags/modules/.widgethacks/advancedrevealers.js deleted file mode 100644 index 3f127931..00000000 --- a/config/ags/modules/.widgethacks/advancedrevealers.js +++ /dev/null @@ -1,86 +0,0 @@ -import Widget from 'resource:///com/github/Aylur/ags/widget.js'; - -const { Revealer, Scrollable } = Widget; - -export const MarginRevealer = ({ - transition = 'slide_down', - child, - revealChild, - showClass = 'element-show', // These are for animation curve, they don't really hide - hideClass = 'element-hide', // Don't put margins in these classes! - extraSetup = () => { }, - ...rest -}) => { - const widget = Scrollable({ - ...rest, - attribute: { - 'revealChild': true, // It'll be set to false after init if it's supposed to hide - 'transition': transition, - 'show': () => { - if (widget.attribute.revealChild) return; - widget.hscroll = 'never'; - widget.vscroll = 'never'; - child.toggleClassName(hideClass, false); - child.toggleClassName(showClass, true); - widget.attribute.revealChild = true; - child.css = 'margin: 0px;'; - }, - 'hide': () => { - if (!widget.attribute.revealChild) return; - child.toggleClassName(hideClass, true); - child.toggleClassName(showClass, false); - widget.attribute.revealChild = false; - if (widget.attribute.transition == 'slide_left') - child.css = `margin-right: -${child.get_allocated_width()}px;`; - else if (widget.attribute.transition == 'slide_right') - child.css = `margin-left: -${child.get_allocated_width()}px;`; - else if (widget.attribute.transition == 'slide_up') - child.css = `margin-bottom: -${child.get_allocated_height()}px;`; - else if (widget.attribute.transition == 'slide_down') - child.css = `margin-top: -${child.get_allocated_height()}px;`; - }, - 'toggle': () => { - if (widget.attribute.revealChild) widget.attribute.hide(); - else widget.attribute.show(); - }, - }, - child: child, - hscroll: `${revealChild ? 'never' : 'always'}`, - vscroll: `${revealChild ? 'never' : 'always'}`, - setup: (self) => { - extraSetup(self); - } - }); - child.toggleClassName(`${revealChild ? showClass : hideClass}`, true); - return widget; -} - -// TODO: Allow reveal update. Currently this just helps at declaration -export const DoubleRevealer = ({ - transition1 = 'slide_right', - transition2 = 'slide_left', - duration1 = 150, - duration2 = 150, - child, - revealChild, - ...rest -}) => { - const r2 = Revealer({ - transition: transition2, - transitionDuration: duration2, - revealChild: revealChild, - child: child, - }); - const r1 = Revealer({ - transition: transition1, - transitionDuration: duration1, - revealChild: revealChild, - child: r2, - ...rest, - }) - r1.toggleRevealChild = (value) => { - r1.revealChild = value; - r2.revealChild = value; - } - return r1; -} diff --git a/config/ags/modules/.widgethacks/popupwindow.js b/config/ags/modules/.widgethacks/popupwindow.js deleted file mode 100644 index 26dad59c..00000000 --- a/config/ags/modules/.widgethacks/popupwindow.js +++ /dev/null @@ -1,32 +0,0 @@ -import App from 'resource:///com/github/Aylur/ags/app.js'; -import Widget from 'resource:///com/github/Aylur/ags/widget.js'; -const { Box, Window } = Widget; - - -export default ({ - name, - child, - showClassName = "", - hideClassName = "", - ...props -}) => { - return Window({ - name, - visible: false, - layer: 'overlay', - ...props, - - child: Box({ - setup: (self) => { - self.hook(App, (self, currentName, visible) => { - if (currentName === name) { - self.toggleClassName(hideClassName, !visible); - } - }).keybind("Escape", () => App.closeWindow(name)) - if (showClassName !== "" && hideClassName !== "") - self.className = `${showClassName} ${hideClassName}`; - }, - child: child, - }), - }); -}
\ No newline at end of file diff --git a/config/ags/modules/.widgetutils/clickthrough.js b/config/ags/modules/.widgetutils/clickthrough.js deleted file mode 100644 index 505f1412..00000000 --- a/config/ags/modules/.widgetutils/clickthrough.js +++ /dev/null @@ -1,4 +0,0 @@ -import Cairo from 'gi://cairo?version=1.0'; - -export const dummyRegion = new Cairo.Region(); -export const enableClickthrough = (self) => self.input_shape_combine_region(dummyRegion);
\ No newline at end of file diff --git a/config/ags/modules/.widgetutils/cursorhover.js b/config/ags/modules/.widgetutils/cursorhover.js deleted file mode 100644 index 89be913b..00000000 --- a/config/ags/modules/.widgetutils/cursorhover.js +++ /dev/null @@ -1,57 +0,0 @@ -const { Gdk } = imports.gi; - -export function setupCursorHover(button) { // Hand pointing cursor on hover - const display = Gdk.Display.get_default(); - button.connect('enter-notify-event', () => { - const cursor = Gdk.Cursor.new_from_name(display, 'pointer'); - button.get_window().set_cursor(cursor); - }); - - button.connect('leave-notify-event', () => { - const cursor = Gdk.Cursor.new_from_name(display, 'default'); - button.get_window().set_cursor(cursor); - }); - -} - -export function setupCursorHoverAim(button) { // Crosshair cursor on hover - button.connect('enter-notify-event', () => { - const display = Gdk.Display.get_default(); - const cursor = Gdk.Cursor.new_from_name(display, 'crosshair'); - button.get_window().set_cursor(cursor); - }); - - button.connect('leave-notify-event', () => { - const display = Gdk.Display.get_default(); - const cursor = Gdk.Cursor.new_from_name(display, 'default'); - button.get_window().set_cursor(cursor); - }); -} - -export function setupCursorHoverGrab(button) { // Hand ready to grab on hover - button.connect('enter-notify-event', () => { - const display = Gdk.Display.get_default(); - const cursor = Gdk.Cursor.new_from_name(display, 'grab'); - button.get_window().set_cursor(cursor); - }); - - button.connect('leave-notify-event', () => { - const display = Gdk.Display.get_default(); - const cursor = Gdk.Cursor.new_from_name(display, 'default'); - button.get_window().set_cursor(cursor); - }); -} - -export function setupCursorHoverInfo(button) { // "?" mark cursor on hover - const display = Gdk.Display.get_default(); - button.connect('enter-notify-event', () => { - const cursor = Gdk.Cursor.new_from_name(display, 'help'); - button.get_window().set_cursor(cursor); - }); - - button.connect('leave-notify-event', () => { - const cursor = Gdk.Cursor.new_from_name(display, 'default'); - button.get_window().set_cursor(cursor); - }); -} - diff --git a/config/ags/modules/.widgetutils/keybind.js b/config/ags/modules/.widgetutils/keybind.js deleted file mode 100644 index eda7877b..00000000 --- a/config/ags/modules/.widgetutils/keybind.js +++ /dev/null @@ -1,25 +0,0 @@ -const { Gdk } = imports.gi; - -const MODS = { - 'Shift': Gdk.ModifierType.SHIFT_MASK, - 'Ctrl': Gdk.ModifierType.CONTROL_MASK, - 'Alt': Gdk.ModifierType.ALT_MASK, - 'Hyper': Gdk.ModifierType.HYPER_MASK, - 'Meta': Gdk.ModifierType.META_MASK -} - -export const checkKeybind = (event, keybind) => { - const pressedModMask = event.get_state()[1]; - const pressedKey = event.get_keyval()[1]; - const keys = keybind.split('+'); - for (let i = 0; i < keys.length; i++) { - if (keys[i] in MODS) { - if (!(pressedModMask & MODS[keys[i]])) { - return false; - } - } else if (pressedKey !== Gdk[`KEY_${keys[i]}`]) { - return false; - } - } - return true; -} diff --git a/config/ags/modules/overview/actions.js b/config/ags/modules/overview/actions.js deleted file mode 100644 index 766cf454..00000000 --- a/config/ags/modules/overview/actions.js +++ /dev/null @@ -1,28 +0,0 @@ -import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; -import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js'; - -function moveClientToWorkspace(address, workspace) { - Utils.execAsync(['bash', '-c', `hyprctl dispatch movetoworkspacesilent ${workspace},address:${address} &`]); -} - -export function dumpToWorkspace(from, to) { - if (from == to) return; - Hyprland.clients.forEach(client => { - if (client.workspace.id == from) { - moveClientToWorkspace(client.address, to); - } - }); -} - -export function swapWorkspace(workspaceA, workspaceB) { - if (workspaceA == workspaceB) return; - const clientsA = []; - const clientsB = []; - Hyprland.clients.forEach(client => { - if (client.workspace.id == workspaceA) clientsA.push(client.address); - if (client.workspace.id == workspaceB) clientsB.push(client.address); - }); - - clientsA.forEach((address) => moveClientToWorkspace(address, workspaceB)); - clientsB.forEach((address) => moveClientToWorkspace(address, workspaceA)); -}
\ No newline at end of file diff --git a/config/ags/modules/overview/main.js b/config/ags/modules/overview/main.js deleted file mode 100644 index 1f5348d9..00000000 --- a/config/ags/modules/overview/main.js +++ /dev/null @@ -1,18 +0,0 @@ -import Widget from 'resource:///com/github/Aylur/ags/widget.js'; -import { SearchAndWindows } from "./windowcontent.js"; -import PopupWindow from '../.widgethacks/popupwindow.js'; - -export default (id = '') => PopupWindow({ - name: `overview${id}`, - exclusivity: 'ignore', - keymode: 'exclusive', - visible: false, - // anchor: ['middle'], - layer: 'overlay', - child: Widget.Box({ - vertical: true, - children: [ - SearchAndWindows(), - ] - }), -}) diff --git a/config/ags/modules/overview/miscfunctions.js b/config/ags/modules/overview/miscfunctions.js deleted file mode 100644 index 187ee6ec..00000000 --- a/config/ags/modules/overview/miscfunctions.js +++ /dev/null @@ -1,155 +0,0 @@ -const { Gio, GLib } = imports.gi; -import App from 'resource:///com/github/Aylur/ags/app.js'; -import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; -const { execAsync, exec } = Utils; -// import Todo from "../../services/todo.js"; -import { darkMode } from '../.miscutils/system.js'; - -export function hasUnterminatedBackslash(inputString) { - // Use a regular expression to match a trailing odd number of backslashes - const regex = /\\+$/; - return regex.test(inputString); -} - -export function launchCustomCommand(command) { - const args = command.toLowerCase().split(' '); - if (args[0] == '>raw') { // Mouse raw input - Utils.execAsync('hyprctl -j getoption input:accel_profile') - .then((output) => { - const value = JSON.parse(output)["str"].trim(); - if (value != "[[EMPTY]]" && value != "") { - execAsync(['bash', '-c', `hyprctl keyword input:accel_profile '[[EMPTY]]'`]).catch(print); - } - else { - execAsync(['bash', '-c', `hyprctl keyword input:accel_profile flat`]).catch(print); - } - }) - } - else if (args[0] == '>img') { // Change wallpaper - execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchwall.sh`, `&`]).catch(print); - } - else if (args[0] == '>color') { // Generate colorscheme from color picker - execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh --pick`, `&`]).catch(print); - } - else if (args[0] == '>light') { // Light mode - darkMode.value = false; - execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_cache_dir()}/ags/user && sed -i "1s/.*/light/" ${GLib.get_user_cache_dir()}/ags/user/colormode.txt`]) - .then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`])) - .catch(print); - } - else if (args[0] == '>dark') { // Dark mode - darkMode.value = true; - execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_cache_dir()}/ags/user && sed -i "1s/.*/dark/" ${GLib.get_user_cache_dir()}/ags/user/colormode.txt`]) - .then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`])) - .catch(print); - } - else if (args[0] == '>badapple') { // Black and white - execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_cache_dir()}/ags/user && sed -i "3s/.*/monochrome/" ${GLib.get_user_cache_dir()}/ags/user/colormode.txt`]) - .then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`])) - .catch(print); - } - else if (args[0] == '>material') { // Use material colors - execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_cache_dir()}/ags/user && echo "material" > ${GLib.get_user_cache_dir()}/ags/user/colorbackend.txt`]).catch(print) - .then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchwall.sh --noswitch`]).catch(print)) - .catch(print); - } - else if (args[0] == '>pywal') { // Use Pywal (ik it looks shit but I'm not removing) - execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_cache_dir()}/ags/user && echo "pywal" > ${GLib.get_user_cache_dir()}/ags/user/colorbackend.txt`]).catch(print) - .then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchwall.sh --noswitch`]).catch(print)) - .catch(print); - } - else if (args[0] == '>todo') { // Todo - Todo.add(args.slice(1).join(' ')); - } - else if (args[0] == '>shutdown') { // Shut down - execAsync([`bash`, `-c`, `systemctl poweroff || loginctl poweroff`]).catch(print); - } - else if (args[0] == '>reboot') { // Reboot - execAsync([`bash`, `-c`, `systemctl reboot || loginctl reboot`]).catch(print); - } - else if (args[0] == '>sleep') { // Sleep - execAsync([`bash`, `-c`, `systemctl suspend || loginctl suspend`]).catch(print); - } - else if (args[0] == '>logout') { // Log out - execAsync([`bash`, `-c`, `pkill Hyprland || pkill sway`]).catch(print); - } -} - -export function execAndClose(command, terminal) { - App.closeWindow('overview'); - if (terminal) { - execAsync([`bash`, `-c`, `${userOptions.apps.terminal} fish -C "${command}"`, `&`]).catch(print); - } - else - execAsync(command).catch(print); -} - -export function couldBeMath(str) { - const regex = /^[0-9.+*/-]/; - return regex.test(str); -} - -export function expandTilde(path) { - if (path.startsWith('~')) { - return GLib.get_home_dir() + path.slice(1); - } else { - return path; - } -} - -function getFileIcon(fileInfo) { - let icon = fileInfo.get_icon(); - if (icon) { - // Get the icon's name - return icon.get_names()[0]; - } else { - // Default icon for files - return 'text-x-generic'; - } -} - -export function ls({ path = '~', silent = false }) { - let contents = []; - try { - let expandedPath = expandTilde(path); - if (expandedPath.endsWith('/')) - expandedPath = expandedPath.slice(0, -1); - let folder = Gio.File.new_for_path(expandedPath); - - let enumerator = folder.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null); - let fileInfo; - while ((fileInfo = enumerator.next_file(null)) !== null) { - let fileName = fileInfo.get_display_name(); - let fileType = fileInfo.get_file_type(); - - let item = { - parentPath: expandedPath, - name: fileName, - type: fileType === Gio.FileType.DIRECTORY ? 'folder' : 'file', - icon: getFileIcon(fileInfo), - }; - - // Add file extension for files - if (fileType === Gio.FileType.REGULAR) { - let fileExtension = fileName.split('.').pop(); - item.type = `${fileExtension}`; - } - - contents.push(item); - contents.sort((a, b) => { - const aIsFolder = a.type.startsWith('folder'); - const bIsFolder = b.type.startsWith('folder'); - if (aIsFolder && !bIsFolder) { - return -1; - } else if (!aIsFolder && bIsFolder) { - return 1; - } else { - return a.name.localeCompare(b.name); // Sort alphabetically within folders and files - } - }); - } - } catch (e) { - if (!silent) console.log(e); - } - return contents; -} diff --git a/config/ags/modules/overview/overview_hyprland.js b/config/ags/modules/overview/overview_hyprland.js deleted file mode 100644 index 034d6c81..00000000 --- a/config/ags/modules/overview/overview_hyprland.js +++ /dev/null @@ -1,423 +0,0 @@ -// TODO -// - Make client destroy/create not destroy and recreate the whole thing -// - Active ws hook optimization: only update when moving to next group -// -const { Gdk, Gtk } = imports.gi; -const { Gravity } = imports.gi.Gdk; -import { SCREEN_HEIGHT, SCREEN_WIDTH } from '../../variables.js'; -import App from 'resource:///com/github/Aylur/ags/app.js'; -import Variable from 'resource:///com/github/Aylur/ags/variable.js'; -import Widget from 'resource:///com/github/Aylur/ags/widget.js'; -import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; - -import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js'; -const { execAsync, exec } = Utils; -import { setupCursorHoverGrab } from '../.widgetutils/cursorhover.js'; -import { dumpToWorkspace, swapWorkspace } from "./actions.js"; -import { substitute } from "../.miscutils/icons.js"; - -const NUM_OF_WORKSPACES_SHOWN = userOptions.overview.numOfCols * userOptions.overview.numOfRows; -const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)]; -const POPUP_CLOSE_TIME = 100; // ms - -const overviewTick = Variable(false); - -export default () => { - const clientMap = new Map(); - let workspaceGroup = 0; - const ContextMenuWorkspaceArray = ({ label, actionFunc, thisWorkspace }) => Widget.MenuItem({ - label: `${label}`, - setup: (menuItem) => { - let submenu = new Gtk.Menu(); - submenu.className = 'menu'; - - const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; - const startWorkspace = offset + 1; - const endWorkspace = startWorkspace + NUM_OF_WORKSPACES_SHOWN - 1; - for (let i = startWorkspace; i <= endWorkspace; i++) { - let button = new Gtk.MenuItem({ - label: `Workspace ${i}` - }); - button.connect("activate", () => { - // execAsync([`${onClickBinary}`, `${thisWorkspace}`, `${i}`]).catch(print); - actionFunc(thisWorkspace, i); - overviewTick.setValue(!overviewTick.value); - }); - submenu.append(button); - } - menuItem.set_reserve_indicator(true); - menuItem.set_submenu(submenu); - } - }) - - const Window = ({ address, at: [x, y], size: [w, h], workspace: { id, name }, class: c, title, xwayland }, screenCoords) => { - const revealInfoCondition = (Math.min(w, h) * userOptions.overview.scale > 70); - if (w <= 0 || h <= 0 || (c === '' && title === '') || c.endsWith('-dropterm')) return null; - // Non-primary monitors - if (screenCoords.x != 0) x -= screenCoords.x; - if (screenCoords.y != 0) y -= screenCoords.y; - // Other offscreen adjustments - if (x + w <= 0) x += (Math.floor(x / SCREEN_WIDTH) * SCREEN_WIDTH); - else if (x < 0) { w = x + w; x = 0; } - if (y + h <= 0) x += (Math.floor(y / SCREEN_HEIGHT) * SCREEN_HEIGHT); - else if (y < 0) { h = y + h; y = 0; } - // Truncate if offscreen - if (x + w > SCREEN_WIDTH) w = SCREEN_WIDTH - x; - if (y + h > SCREEN_HEIGHT) h = SCREEN_HEIGHT - y; - - const appIcon = Widget.Icon({ - icon: substitute(c), - size: Math.min(w, h) * userOptions.overview.scale / 2.5, - }); - return Widget.Button({ - attribute: { - address, x, y, w, h, ws: id, - updateIconSize: (self) => { - appIcon.size = Math.min(self.attribute.w, self.attribute.h) * userOptions.overview.scale / 2.5; - }, - }, - className: 'overview-tasks-window', - hpack: 'start', - vpack: 'start', - css: ` - margin-left: ${Math.round(x * userOptions.overview.scale)}px; - margin-top: ${Math.round(y * userOptions.overview.scale)}px; - margin-right: -${Math.round((x + w) * userOptions.overview.scale)}px; - margin-bottom: -${Math.round((y + h) * userOptions.overview.scale)}px; - `, - onClicked: (self) => { - App.closeWindow('overview'); - Utils.timeout(POPUP_CLOSE_TIME, () => Hyprland.messageAsync(`dispatch focuswindow address:${address}`)); - }, - onMiddleClickRelease: () => Hyprland.messageAsync(`dispatch closewindow address:${address}`), - onSecondaryClick: (button) => { - button.toggleClassName('overview-tasks-window-selected', true); - const menu = Widget.Menu({ - className: 'menu', - children: [ - Widget.MenuItem({ - child: Widget.Label({ - xalign: 0, - label: "Close (Middle-click)", - }), - onActivate: () => Hyprland.messageAsync(`dispatch closewindow address:${address}`), - }), - ContextMenuWorkspaceArray({ - label: "Dump windows to workspace", - actionFunc: dumpToWorkspace, - thisWorkspace: Number(id) - }), - ContextMenuWorkspaceArray({ - label: "Swap windows with workspace", - actionFunc: swapWorkspace, - thisWorkspace: Number(id) - }), - ], - }); - menu.connect("deactivate", () => { - button.toggleClassName('overview-tasks-window-selected', false); - }) - menu.connect("selection-done", () => { - button.toggleClassName('overview-tasks-window-selected', false); - }) - menu.popup_at_widget(button.get_parent(), Gravity.SOUTH, Gravity.NORTH, null); // Show menu below the button - button.connect("destroy", () => menu.destroy()); - }, - child: Widget.Box({ - homogeneous: true, - child: Widget.Box({ - vertical: true, - vpack: 'center', - className: 'spacing-v-5', - children: [ - appIcon, - // TODO: Add xwayland tag instead of just having italics - Widget.Revealer({ - transition: 'slide_down', - revealChild: revealInfoCondition, - child: Widget.Label({ - maxWidthChars: 10, // Doesn't matter what number - truncate: 'end', - className: `${xwayland ? 'txt txt-italic' : 'txt'}`, - css: ` - font-size: ${Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * userOptions.overview.scale / 14.6}px; - margin: 0px ${Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * userOptions.overview.scale / 10}px; - `, - // If the title is too short, include the class - label: (title.length <= 1 ? `${c}: ${title}` : title), - }) - }) - ] - }) - }), - tooltipText: `${c}: ${title}`, - setup: (button) => { - setupCursorHoverGrab(button); - - button.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, TARGET, Gdk.DragAction.MOVE); - button.drag_source_set_icon_name(substitute(c)); - // button.drag_source_set_icon_gicon(icon); - - button.connect('drag-begin', (button) => { // On drag start, add the dragging class - button.toggleClassName('overview-tasks-window-dragging', true); - }); - button.connect('drag-data-get', (_w, _c, data) => { // On drag finish, give address - data.set_text(address, address.length); - button.toggleClassName('overview-tasks-window-dragging', false); - }); - }, - }); - } - - const Workspace = (index) => { - // const fixed = Widget.Fixed({ - // attribute: { - // put: (widget, x, y) => { - // fixed.put(widget, x, y); - // }, - // move: (widget, x, y) => { - // fixed.move(widget, x, y); - // }, - // } - // }); - const fixed = Widget.Box({ - attribute: { - put: (widget, x, y) => { - if (!widget.attribute) return; - // Note: x and y are already multiplied by userOptions.overview.scale - const newCss = ` - margin-left: ${Math.round(x)}px; - margin-top: ${Math.round(y)}px; - margin-right: -${Math.round(x + (widget.attribute.w * userOptions.overview.scale))}px; - margin-bottom: -${Math.round(y + (widget.attribute.h * userOptions.overview.scale))}px; - `; - widget.css = newCss; - fixed.pack_start(widget, false, false, 0); - }, - move: (widget, x, y) => { - if (!widget) return; - if (!widget.attribute) return; - // Note: x and y are already multiplied by userOptions.overview.scale - const newCss = ` - margin-left: ${Math.round(x)}px; - margin-top: ${Math.round(y)}px; - margin-right: -${Math.round(x + (widget.attribute.w * userOptions.overview.scale))}px; - margin-bottom: -${Math.round(y + (widget.attribute.h * userOptions.overview.scale))}px; - `; - widget.css = newCss; - }, - } - }) - const WorkspaceNumber = ({ index, ...rest }) => Widget.Label({ - className: 'overview-tasks-workspace-number', - label: `${index}`, - css: ` - margin: ${Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * userOptions.overview.scale * userOptions.overview.wsNumMarginScale}px; - font-size: ${SCREEN_HEIGHT * userOptions.overview.scale * userOptions.overview.wsNumScale}px; - `, - setup: (self) => self.hook(Hyprland.active.workspace, (self) => { - // Update when going to new ws group - const currentGroup = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN); - self.label = `${currentGroup * NUM_OF_WORKSPACES_SHOWN + index}`; - }), - ...rest, - }) - const widget = Widget.Box({ - className: 'overview-tasks-workspace', - vpack: 'center', - css: ` - min-width: ${SCREEN_WIDTH * userOptions.overview.scale}px; - min-height: ${SCREEN_HEIGHT * userOptions.overview.scale}px; - `, - children: [Widget.EventBox({ - hexpand: true, - vexpand: true, - onPrimaryClick: () => { - App.closeWindow('overview'); - Utils.timeout(POPUP_CLOSE_TIME, () => Hyprland.messageAsync(`dispatch workspace ${index}`)); - }, - setup: (eventbox) => { - eventbox.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY); - eventbox.connect('drag-data-received', (_w, _c, _x, _y, data) => { - const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; - Hyprland.messageAsync(`dispatch movetoworkspacesilent ${index + offset},address:${data.get_text()}`) - overviewTick.setValue(!overviewTick.value); - }); - }, - child: Widget.Overlay({ - child: Widget.Box({}), - overlays: [ - WorkspaceNumber({ index: index, hpack: 'start', vpack: 'start' }), - fixed - ] - }), - })], - }); - const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; - fixed.attribute.put(WorkspaceNumber(offset + index), 0, 0); - widget.clear = () => { - const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; - clientMap.forEach((client, address) => { - if (!client) return; - if ((client.attribute.ws <= offset || client.attribute.ws > offset + NUM_OF_WORKSPACES_SHOWN) || - (client.attribute.ws == offset + index)) { - client.destroy(); - client = null; - clientMap.delete(address); - } - }); - } - widget.set = (clientJson, screenCoords) => { - let c = clientMap.get(clientJson.address); - if (c) { - if (c.attribute?.ws !== clientJson.workspace.id) { - c.destroy(); - c = null; - clientMap.delete(clientJson.address); - } - else if (c) { - c.attribute.w = clientJson.size[0]; - c.attribute.h = clientJson.size[1]; - c.attribute.updateIconSize(c); - fixed.attribute.move(c, - Math.max(0, clientJson.at[0] * userOptions.overview.scale), - Math.max(0, clientJson.at[1] * userOptions.overview.scale) - ); - return; - } - } - const newWindow = Window(clientJson, screenCoords); - if (newWindow === null) return; - // clientMap.set(clientJson.address, newWindow); - fixed.attribute.put(newWindow, - Math.max(0, newWindow.attribute.x * userOptions.overview.scale), - Math.max(0, newWindow.attribute.y * userOptions.overview.scale) - ); - clientMap.set(clientJson.address, newWindow); - }; - widget.unset = (clientAddress) => { - const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; - let c = clientMap.get(clientAddress); - if (!c) return; - c.destroy(); - c = null; - clientMap.delete(clientAddress); - }; - widget.show = () => { - fixed.show_all(); - } - return widget; - }; - - const arr = (s, n) => { - const array = []; - for (let i = 0; i < n; i++) - array.push(s + i); - - return array; - }; - - const OverviewRow = ({ startWorkspace, workspaces, windowName = 'overview' }) => Widget.Box({ - children: arr(startWorkspace, workspaces).map(Workspace), - attribute: { - monitorMap: [], - getMonitorMap: (box) => { - execAsync('hyprctl -j monitors').then(monitors => { - box.attribute.monitorMap = JSON.parse(monitors).reduce((acc, item) => { - acc[item.id] = { x: item.x, y: item.y }; - return acc; - }, {}); - }); - }, - update: (box) => { - const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; - if (!App.getWindow(windowName).visible) return; - Hyprland.messageAsync('j/clients').then(clients => { - const allClients = JSON.parse(clients); - const kids = box.get_children(); - kids.forEach(kid => kid.clear()); - for (let i = 0; i < allClients.length; i++) { - const client = allClients[i]; - const childID = client.workspace.id - (offset + startWorkspace); - if (offset + startWorkspace <= client.workspace.id && - client.workspace.id <= offset + startWorkspace + workspaces) { - const screenCoords = box.attribute.monitorMap[client.monitor]; - if (kids[childID]) { - kids[childID].set(client, screenCoords); - } - continue; - } - } - kids.forEach(kid => kid.show()); - }).catch(print); - }, - updateWorkspace: (box, id) => { - const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; - if (!( // Not in range, ignore - offset + startWorkspace <= id && - id <= offset + startWorkspace + workspaces - )) return; - // if (!App.getWindow(windowName).visible) return; - Hyprland.messageAsync('j/clients').then(clients => { - const allClients = JSON.parse(clients); - const kids = box.get_children(); - for (let i = 0; i < allClients.length; i++) { - const client = allClients[i]; - if (client.workspace.id != id) continue; - const screenCoords = box.attribute.monitorMap[client.monitor]; - kids[id - (offset + startWorkspace)]?.set(client, screenCoords); - } - kids[id - (offset + startWorkspace)]?.show(); - }).catch(print); - }, - }, - setup: (box) => { - box.attribute.getMonitorMap(box); - box - .hook(overviewTick, (box) => box.attribute.update(box)) - .hook(Hyprland, (box, clientAddress) => { - const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; - const kids = box.get_children(); - const client = Hyprland.getClient(clientAddress); - if (!client) return; - const id = client.workspace.id; - - box.attribute.updateWorkspace(box, id); - kids[id - (offset + startWorkspace)]?.unset(clientAddress); - }, 'client-removed') - .hook(Hyprland, (box, clientAddress) => { - const client = Hyprland.getClient(clientAddress); - if (!client) return; - box.attribute.updateWorkspace(box, client.workspace.id); - }, 'client-added') - .hook(Hyprland.active.workspace, (box) => { - // Full update when going to new ws group - const previousGroup = box.attribute.workspaceGroup; - const currentGroup = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN); - if (currentGroup !== previousGroup) { - box.attribute.update(box); - box.attribute.workspaceGroup = currentGroup; - } - }) - .hook(App, (box, name, visible) => { // Update on open - if (name == 'overview' && visible) box.attribute.update(box); - }) - }, - }); - - return Widget.Revealer({ - revealChild: true, - transition: 'slide_down', - transitionDuration: userOptions.animations.durationLarge, - child: Widget.Box({ - vertical: true, - className: 'overview-tasks', - children: Array.from({ length: userOptions.overview.numOfRows }, (_, index) => - OverviewRow({ - startWorkspace: 1 + index * userOptions.overview.numOfCols, - workspaces: userOptions.overview.numOfCols, - }) - ) - }), - }); -}
\ No newline at end of file diff --git a/config/ags/modules/overview/searchbuttons.js b/config/ags/modules/overview/searchbuttons.js deleted file mode 100644 index f5892f31..00000000 --- a/config/ags/modules/overview/searchbuttons.js +++ /dev/null @@ -1,163 +0,0 @@ -const { Gtk } = imports.gi; -import App from 'resource:///com/github/Aylur/ags/app.js'; -import Widget from 'resource:///com/github/Aylur/ags/widget.js'; -import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; -const { execAsync, exec } = Utils; -import { searchItem } from './searchitem.js'; -import { execAndClose, couldBeMath, launchCustomCommand } from './miscfunctions.js'; - -export const DirectoryButton = ({ parentPath, name, type, icon }) => { - const actionText = Widget.Revealer({ - revealChild: false, - transition: "crossfade", - transitionDuration: userOptions.animations.durationLarge, - child: Widget.Label({ - className: 'overview-search-results-txt txt txt-small txt-action', - label: 'Open', - }) - }); - const actionTextRevealer = Widget.Revealer({ - revealChild: false, - transition: "slide_left", - transitionDuration: userOptions.animations.durationSmall, - child: actionText, - }); - return Widget.Button({ - className: 'overview-search-result-btn', - onClicked: () => { - App.closeWindow('overview'); - execAsync(['bash', '-c', `xdg-open '${parentPath}/${name}'`, `&`]).catch(print); - }, - child: Widget.Box({ - children: [ - Widget.Box({ - vertical: false, - children: [ - Widget.Box({ - className: 'overview-search-results-icon', - homogeneous: true, - child: Widget.Icon({ - icon: icon, - }), - }), - Widget.Label({ - className: 'overview-search-results-txt txt txt-norm', - label: name, - }), - Widget.Box({ hexpand: true }), - actionTextRevealer, - ] - }) - ] - }), - setup: (self) => self - .on('focus-in-event', (button) => { - actionText.revealChild = true; - actionTextRevealer.revealChild = true; - }) - .on('focus-out-event', (button) => { - actionText.revealChild = false; - actionTextRevealer.revealChild = false; - }) - , - }) -} - -export const CalculationResultButton = ({ result, text }) => searchItem({ - materialIconName: ' ', - name: `Math result`, - actionName: "Copy", - content: `${result}`, - onActivate: () => { - App.closeWindow('overview'); - execAsync(['wl-copy', `${result}`]).catch(print); - }, -}); - -export const DesktopEntryButton = (app) => { - const actionText = Widget.Revealer({ - revealChild: false, - transition: "crossfade", - transitionDuration: userOptions.animations.durationLarge, - child: Widget.Label({ - className: 'overview-search-results-txt txt txt-small txt-action', - label: 'Launch', - }) - }); - const actionTextRevealer = Widget.Revealer({ - revealChild: false, - transition: "slide_left", - transitionDuration: userOptions.animations.durationSmall, - child: actionText, - }); - return Widget.Button({ - className: 'overview-search-result-btn', - onClicked: () => { - App.closeWindow('overview'); - app.launch(); - }, - child: Widget.Box({ - children: [ - Widget.Box({ - vertical: false, - children: [ - Widget.Box({ - className: 'overview-search-results-icon', - homogeneous: true, - child: Widget.Icon({ - icon: app.iconName, - }), - }), - Widget.Label({ - className: 'overview-search-results-txt txt txt-norm', - label: app.name, - }), - Widget.Box({ hexpand: true }), - actionTextRevealer, - ] - }) - ] - }), - setup: (self) => self - .on('focus-in-event', (button) => { - actionText.revealChild = true; - actionTextRevealer.revealChild = true; - }) - .on('focus-out-event', (button) => { - actionText.revealChild = false; - actionTextRevealer.revealChild = false; - }) - , - }) -} - -export const ExecuteCommandButton = ({ command, terminal = false }) => searchItem({ - materialIconName: `${terminal ? 'terminal' : ' '}`, - name: `Run command`, - actionName: `Execute ${terminal ? 'in terminal' : ''}`, - content: `${command}`, - onActivate: () => execAndClose(command, terminal), - extraClassName: 'techfont', -}) - -export const CustomCommandButton = ({ text = '' }) => searchItem({ - materialIconName: ' ', - name: 'Action', - actionName: 'Run', - content: `${text}`, - onActivate: () => { - App.closeWindow('overview'); - launchCustomCommand(text); - }, -}); - -export const SearchButton = ({ text = '' }) => searchItem({ - materialIconName: ' ', - name: 'Search the web', - actionName: 'Go', - content: `${text}`, - onActivate: () => { - App.closeWindow('overview'); - execAsync(['bash', '-c', `xdg-open '${userOptions.search.engineBaseUrl}${text} ${['', ...userOptions.search.excludedSites].join(' -site:')}' &`]).catch(print); - }, -});
\ No newline at end of file diff --git a/config/ags/modules/overview/searchitem.js b/config/ags/modules/overview/searchitem.js deleted file mode 100644 index 2a3303a4..00000000 --- a/config/ags/modules/overview/searchitem.js +++ /dev/null @@ -1,65 +0,0 @@ -import Widget from 'resource:///com/github/Aylur/ags/widget.js'; - -export const searchItem = ({ materialIconName, name, actionName, content, onActivate, extraClassName = '', ...rest }) => { - const actionText = Widget.Revealer({ - revealChild: false, - transition: "crossfade", - transitionDuration: userOptions.animations.durationLarge, - child: Widget.Label({ - className: 'overview-search-results-txt txt txt-small txt-action', - label: `${actionName}`, - }) - }); - const actionTextRevealer = Widget.Revealer({ - revealChild: false, - transition: "slide_left", - transitionDuration: userOptions.animations.durationSmall, - child: actionText, - }) - return Widget.Button({ - className: `overview-search-result-btn txt ${extraClassName}`, - onClicked: onActivate, - child: Widget.Box({ - children: [ - Widget.Box({ - vertical: false, - children: [ - Widget.Label({ - className: `icon-material overview-search-results-icon`, - label: `${materialIconName}`, - }), - Widget.Box({ - vertical: true, - children: [ - Widget.Label({ - hpack: 'start', - className: 'overview-search-results-txt txt-smallie txt-subtext', - label: `${name}`, - truncate: "end", - }), - Widget.Label({ - hpack: 'start', - className: 'overview-search-results-txt txt-norm', - label: `${content}`, - truncate: "end", - }), - ] - }), - Widget.Box({ hexpand: true }), - actionTextRevealer, - ], - }) - ] - }), - setup: (self) => self - .on('focus-in-event', (button) => { - actionText.revealChild = true; - actionTextRevealer.revealChild = true; - }) - .on('focus-out-event', (button) => { - actionText.revealChild = false; - actionTextRevealer.revealChild = false; - }) - , - }); -} diff --git a/config/ags/modules/overview/windowcontent.js b/config/ags/modules/overview/windowcontent.js deleted file mode 100644 index 7a19dd3c..00000000 --- a/config/ags/modules/overview/windowcontent.js +++ /dev/null @@ -1,262 +0,0 @@ -const { Gdk, Gtk } = imports.gi; -import App from 'resource:///com/github/Aylur/ags/app.js'; -import Widget from 'resource:///com/github/Aylur/ags/widget.js'; -import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; - -import Applications from 'resource:///com/github/Aylur/ags/service/applications.js'; -const { execAsync, exec } = Utils; -import { execAndClose, expandTilde, hasUnterminatedBackslash, couldBeMath, launchCustomCommand, ls } from './miscfunctions.js'; -import { - CalculationResultButton, CustomCommandButton, DirectoryButton, - DesktopEntryButton, ExecuteCommandButton, SearchButton -} from './searchbuttons.js'; -import { checkKeybind } from '../.widgetutils/keybind.js'; - -// Add math funcs -const { abs, sin, cos, tan, cot, asin, acos, atan, acot } = Math; -const pi = Math.PI; -// trigonometric funcs for deg -const sind = x => sin(x * pi / 180); -const cosd = x => cos(x * pi / 180); -const tand = x => tan(x * pi / 180); -const cotd = x => cot(x * pi / 180); -const asind = x => asin(x) * 180 / pi; -const acosd = x => acos(x) * 180 / pi; -const atand = x => atan(x) * 180 / pi; -const acotd = x => acot(x) * 180 / pi; - -const MAX_RESULTS = 10; -const OVERVIEW_SCALE = 0.18; // = overview workspace box / screen size -const OVERVIEW_WS_NUM_SCALE = 0.0; -const OVERVIEW_WS_NUM_MARGIN_SCALE = 0.07; -const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)]; - -function iconExists(iconName) { - let iconTheme = Gtk.IconTheme.get_default(); - return iconTheme.has_icon(iconName); -} - -const OptionalOverview = async () => { - try { - return (await import('./overview_hyprland.js')).default(); - } catch { - return Widget.Box({}); - // return (await import('./overview_hyprland.js')).default(); - } -}; - -const overviewContent = await OptionalOverview(); - -export const SearchAndWindows = () => { - var _appSearchResults = []; - - const ClickToClose = ({ ...props }) => Widget.EventBox({ - ...props, - onPrimaryClick: () => App.closeWindow('overview'), - onSecondaryClick: () => App.closeWindow('overview'), - onMiddleClick: () => App.closeWindow('overview'), - }); - const resultsBox = Widget.Box({ - className: 'overview-search-results', - vertical: true, - vexpand: true, - }); - const resultsRevealer = Widget.Revealer({ - transitionDuration: userOptions.animations.durationLarge, - revealChild: false, - transition: 'slide_down', - // duration: 200, - hpack: 'center', - child: resultsBox, - }); - const entryPromptRevealer = Widget.Revealer({ - transition: 'crossfade', - transitionDuration: userOptions.animations.durationLarge, - revealChild: true, - hpack: 'center', - child: Widget.Label({ - className: 'overview-search-prompt txt-small txt', - label: 'Type to search' - }), - }); - - const entryIconRevealer = Widget.Revealer({ - transition: 'crossfade', - transitionDuration: userOptions.animations.durationLarge, - revealChild: false, - hpack: 'end', - child: Widget.Label({ - className: 'txt txt-large icon-material overview-search-icon', - label: ' ', - }), - }); - - const entryIcon = Widget.Box({ - className: 'overview-search-prompt-box', - setup: box => box.pack_start(entryIconRevealer, true, true, 0), - }); - - const entry = Widget.Entry({ - className: 'overview-search-box txt-small txt', - hpack: 'center', - onAccept: (self) => { // This is when you hit Enter - const text = self.text; - if (text.length == 0) return; - const isAction = text.startsWith('>'); - const isDir = (['/', '~'].includes(entry.text[0])); - - if (couldBeMath(text)) { // Eval on typing is dangerous, this is a workaround - try { - const fullResult = eval(text.replace(/\^/g, "**")); - // copy - execAsync(['wl-copy', `${fullResult}`]).catch(print); - App.closeWindow('overview'); - return; - } catch (e) { - // console.log(e); - } - } - if (isDir) { - App.closeWindow('overview'); - execAsync(['bash', '-c', `xdg-open "${expandTilde(text)}"`, `&`]).catch(print); - return; - } - if (_appSearchResults.length > 0) { - App.closeWindow('overview'); - _appSearchResults[0].launch(); - return; - } - else if (text[0] == '>') { // Custom commands - App.closeWindow('overview'); - launchCustomCommand(text); - return; - } - // Fallback: Execute command - if (!isAction && exec(`bash -c "command -v ${text.split(' ')[0]}"`) != '') { - if (text.startsWith('sudo')) - execAndClose(text, true); - else - execAndClose(text, false); - } - - else { - App.closeWindow('overview'); - execAsync(['bash', '-c', `xdg-open '${userOptions.search.engineBaseUrl}${text} ${['', ...userOptions.search.excludedSites].join(' -site:')}' &`]).catch(print); - } - }, - onChange: (entry) => { // this is when you type - const isAction = entry.text[0] == '>'; - const isDir = (['/', '~'].includes(entry.text[0])); - resultsBox.get_children().forEach(ch => ch.destroy()); - - // check empty if so then dont do stuff - if (entry.text == '') { - resultsRevealer.revealChild = false; - overviewContent.revealChild = true; - entryPromptRevealer.revealChild = true; - entryIconRevealer.revealChild = false; - entry.toggleClassName('overview-search-box-extended', false); - return; - } - const text = entry.text; - resultsRevealer.revealChild = true; - overviewContent.revealChild = false; - entryPromptRevealer.revealChild = false; - entryIconRevealer.revealChild = true; - entry.toggleClassName('overview-search-box-extended', true); - _appSearchResults = Applications.query(text); - - // Calculate - if (couldBeMath(text)) { // Eval on typing is dangerous; this is a small workaround. - try { - const fullResult = eval(text.replace(/\^/g, "**")); - resultsBox.add(CalculationResultButton({ result: fullResult, text: text })); - } catch (e) { - // console.log(e); - } - } - if (isDir) { - var contents = []; - contents = ls({ path: text, silent: true }); - contents.forEach((item) => { - resultsBox.add(DirectoryButton(item)); - }) - } - if (isAction) { // Eval on typing is dangerous, this is a workaround. - resultsBox.add(CustomCommandButton({ text: entry.text })); - } - // Add application entries - let appsToAdd = MAX_RESULTS; - _appSearchResults.forEach(app => { - if (appsToAdd == 0) return; - resultsBox.add(DesktopEntryButton(app)); - appsToAdd--; - }); - - // Fallbacks - // if the first word is an actual command - if (!isAction && !hasUnterminatedBackslash(text) && exec(`bash -c "command -v ${text.split(' ')[0]}"`) != '') { - resultsBox.add(ExecuteCommandButton({ command: entry.text, terminal: entry.text.startsWith('sudo') })); - } - - // Add fallback: search - resultsBox.add(SearchButton({ text: entry.text })); - resultsBox.show_all(); - }, - }); - return Widget.Box({ - vertical: true, - children: [ - ClickToClose({ // Top margin. Also works as a click-outside-to-close thing - child: Widget.Box({ - className: 'bar-height', - }) - }), - Widget.Box({ - hpack: 'center', - children: [ - entry, - Widget.Box({ - className: 'overview-search-icon-box', - setup: (box) => { - box.pack_start(entryPromptRevealer, true, true, 0) - }, - }), - entryIcon, - ] - }), - overviewContent, - resultsRevealer, - ], - setup: (self) => self - .hook(App, (_b, name, visible) => { - if (name == 'overview' && !visible) { - resultsBox.children = []; - entry.set_text(''); - } - }) - .on('key-press-event', (widget, event) => { // Typing - const keyval = event.get_keyval()[1]; - const modstate = event.get_state()[1]; - if (checkKeybind(event, userOptions.keybinds.overview.altMoveLeft)) - entry.set_position(Math.max(entry.get_position() - 1, 0)); - else if (checkKeybind(event, userOptions.keybinds.overview.altMoveRight)) - entry.set_position(Math.min(entry.get_position() + 1, entry.get_text().length)); - else if (checkKeybind(event, userOptions.keybinds.overview.deleteToEnd)) { - const text = entry.get_text(); - const pos = entry.get_position(); - const newText = text.slice(0, pos); - entry.set_text(newText); - entry.set_position(newText.length); - } - else if (!(modstate & Gdk.ModifierType.CONTROL_MASK)) { // Ctrl not held - if (keyval >= 32 && keyval <= 126 && widget != entry) { - Utils.timeout(1, () => entry.grab_focus()); - entry.set_text(entry.text + String.fromCharCode(keyval)); - entry.set_position(-1); - } - } - }) - , - }); -}; diff --git a/config/ags/user/style.css b/config/ags/user/style.css deleted file mode 100644 index 13410ea4..00000000 --- a/config/ags/user/style.css +++ /dev/null @@ -1,197 +0,0 @@ -*:not(popover) { - all: unset; -} - -@import '../../../.config/waybar/wallust/colors-waybar.css'; - -/* define some colors */ -@define-color border-color @color12; -@define-color border-color-alt @color9; -@define-color text-color rgba(255, 255, 255, 0.7); -@define-color noti-bg rgba(0, 0, 0, 0.4); -@define-color noti-bg-alt #111111; - -widget { - border-radius: 0.818rem; - -gtk-outline-radius: 0.818rem; -} - -.overview-window { - margin-top: 2.727rem; -} - -.overview-search-box { - transition: 300ms cubic-bezier(0, 0.55, 0.45, 1); - border-radius: 1.705rem; - -gtk-outline-radius: 1.705rem; - border-top: 4px solid @border-color; - border-left: 1px solid @border-color-alt; - border-right: 1px solid @border-color-alt; - border-bottom: 4px solid @border-color; - box-shadow: 0px 2px 3px alpha(@color12, 0.45); - margin: 0.476rem; - min-width: 13.636rem; - min-height: 3.409rem; - padding: 0rem 1.364rem; - padding-right: 2.864rem; - background-color: @noti-bg; - color: @text-color; - caret-color: inherit; - font-weight: bolder; -} -.overview-search-box selection { - background-color: @noti-bg; - color: @text-color; -} - -.overview-search-box-extended { - min-width: 25.909rem; - caret-color: #FDD9FD; -} - -.overview-search-prompt { - color: @text-color; -} - -.overview-search-icon { - margin: 0rem 1.023rem; -} - -.overview-search-prompt-box { - margin-left: -18.545rem; - margin-right: 0.544rem; -} - -.overview-search-icon-box { - margin-left: -18.545rem; - margin-right: 0.544rem; -} - -.overview-search-results { - border-radius: 1.705rem; - -gtk-outline-radius: 1.705rem; - border-top: 4px solid @border-color; - border-left: 1px solid @border-color-alt; - border-right: 1px solid @border-color-alt; - border-bottom: 4px solid @border-color; - box-shadow: 0px 2px 3px @color9; - margin: 0.476rem; - min-width: 28.773rem; - padding: 0.682rem; - background-color: @noti-bg; - color: @text-color; - font-weight: bold; -} - -.overview-search-results-icon { - margin: 0rem 0.682rem; - font-size: 2.386rem; - min-width: 2.386rem; - min-height: 2.386rem; -} - -.overview-search-results-txt { - margin-right: 0.682rem; -} - -.overview-search-results-txt-cmd { - margin-right: 0.682rem; - font-family: "JetBrains Mono NF", "JetBrains Mono Nerd Font", "JetBrains Mono NL", "SpaceMono NF", "SpaceMono Nerd Font", monospace; - font-size: 1.227rem; -} - -.overview-search-result-btn { - border-radius: 1.159rem; - -gtk-outline-radius: 1.159rem; - padding: 0.341rem; - min-width: 2.386rem; - min-height: 2.386rem; - caret-color: transparent; -} - -.overview-search-result-btn:hover, -.overview-search-result-btn:focus { - background-color: alpha(@color7, 0.9); - color: alpha(@color0, 0.7); -} - -.overview-search-result-btn:active { - background-color: alpha(@color7, 0.9); - color: @color4; -} - -.overview-tasks { - border-radius: 1.705rem; - -gtk-outline-radius: 1.705rem; - border-top: 4px solid @border-color; - border-left: 1px solid @border-color-alt; - border-right: 1px solid @border-color-alt; - border-bottom: 4px solid @border-color; - box-shadow: 0px 2px 3px @color5; - margin: 0.476rem; - padding: 0.341rem; - /* background-color: rgba(49, 50, 68, 0.8); */ - background-color: @noti-bg; - color: #EBDFED; -} - -.overview-tasks-workspace { - border-radius: 1.159rem; - -gtk-outline-radius: 1.159rem; - margin: 0.341rem; - /* background-color: #26233A; */ - background-image: url('../../rofi/.current_wallpaper'); - background-size: cover; - background-position: center; - border: 0.068rem solid alpha(@color4, 0.5); -} - -.overview-tasks-workspace-number { - font-family: "Open Sans", "Noto Sans", sans-serif; - color: #CFC2D3; -} - -.overview-tasks-window { - border-radius: 1.159rem; - -gtk-outline-radius: 1.159rem; - transition: 300ms cubic-bezier(0.1, 1, 0, 1); - background-color: alpha(@color3, .7); - /* background-color: @color_a3; */ - /* background-color: rgba(46, 40, 50, 0.8); */ - color: #EBDFED; - border: 0.068rem solid @color7; -} - -.overview-tasks-window:hover, -.overview-tasks-window:focus { - background-color: alpha(@color9, 0.8); -} - -.overview-tasks-window:active { - background-color: alpha(@color9, 0.8); -} - -.overview-tasks-window-selected { - background-color: alpha(@color9, 0.8); -} - -.overview-tasks-window-dragging { - opacity: 0.2; -} - -.growingRadial { - transition: 300ms cubic-bezier(0.2, 0, 0, 1); -} - -.fadingRadial { - transition: 50ms cubic-bezier(0.2, 0, 0, 1); -} - -.sidebar-pinned { - margin: 0rem; - border-radius: 0rem; - border-bottom-right-radius: 1.705rem; - border: 0rem solid; -} - -/*# sourceMappingURL=style.css.map */ diff --git a/config/ags/user_options.js b/config/ags/user_options.js deleted file mode 100644 index 9105decb..00000000 --- a/config/ags/user_options.js +++ /dev/null @@ -1,21 +0,0 @@ - -const userConfigOptions = { - // For every option, see ~/.config/ags/modules/.configuration/user_options.js - // (vscode users ctrl+click this: file://./modules/.configuration/user_options.js) - // (vim users: `:vsp` to split window, move cursor to this path, press `gf`. `Ctrl-w` twice to switch between) - // options listed in this file will override the default ones in the above file - // Here's an example - 'overview':{ - 'scale': 0.15, - 'numOfRows': 2 - }, - 'keybinds': { - 'sidebar': { - 'pin': "Ctrl+p", - 'nextTab': "Ctrl+Page_Down", - 'prevTab': "Ctrl+Page_Up", - }, - }, -} - -export default userConfigOptions; diff --git a/config/ags/variables.js b/config/ags/variables.js deleted file mode 100644 index 645a5807..00000000 --- a/config/ags/variables.js +++ /dev/null @@ -1,21 +0,0 @@ -const { Gtk } = imports.gi; -import Variable from 'resource:///com/github/Aylur/ags/variable.js'; -import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; -const { exec, execAsync } = Utils; - -Gtk.IconTheme.get_default().append_search_path(`${App.configDir}/assets/icons`); - -// Screen size -export const SCREEN_WIDTH = Number(exec(`bash -c "hyprctl monitors -j | jq '.[0].width / .[0].scale'"`)); -export const SCREEN_HEIGHT = Number(exec(`bash -c "hyprctl monitors -j | jq '.[0].height / .[0].scale'"`)); - -// Mode switching -export const currentShellMode = Variable('normal', {}) // normal, focus -globalThis['currentMode'] = currentShellMode; -globalThis['cycleMode'] = () => { - if (currentShellMode.value === 'normal') { - currentShellMode.value = 'focus'; - } else { - currentShellMode.value = 'normal'; - } -} diff --git a/config/hypr/UserConfigs/Startup_Apps.conf b/config/hypr/UserConfigs/Startup_Apps.conf index 702f9a90..91ff9cd0 100644 --- a/config/hypr/UserConfigs/Startup_Apps.conf +++ b/config/hypr/UserConfigs/Startup_Apps.conf @@ -30,6 +30,7 @@ exec-once = swaync #exec-once = blueman-applet #exec-once = rog-control-center exec-once = waybar +exec-once = qs #clipboard manager exec-once = wl-paste --type text --watch cliphist store diff --git a/config/hypr/UserConfigs/UserKeybinds.conf b/config/hypr/UserConfigs/UserKeybinds.conf index ce501d85..c92352fc 100644 --- a/config/hypr/UserConfigs/UserKeybinds.conf +++ b/config/hypr/UserConfigs/UserKeybinds.conf @@ -18,7 +18,7 @@ source= $UserConfigs/01-UserDefaults.conf #bindr = $mainMod, $mainMod_L, exec, pkill rofi || rofi -show drun -modi drun,filebrowser,run,window # Super Key to Launch rofi menu bind = $mainMod, D, exec, pkill rofi || true && rofi -show drun -modi drun,filebrowser,run,window # Main Menu (APP Launcher) bind = $mainMod, B, exec, xdg-open "https://" # default browser -bind = $mainMod, A, exec, pkill rofi || true && ags -t 'overview' # desktop overview (if installed) +bind = $mainMod, A, global, quickshell:overviewToggle # desktop overview (if installed) bind = $mainMod, Return, exec, $term #terminal bind = $mainMod, E, exec, $files #file manager diff --git a/config/hypr/UserConfigs/UserSettings.conf b/config/hypr/UserConfigs/UserSettings.conf index 29dbc572..1f8c23c3 100644 --- a/config/hypr/UserConfigs/UserSettings.conf +++ b/config/hypr/UserConfigs/UserSettings.conf @@ -104,8 +104,6 @@ xwayland { } render { - #explicit_sync = 2 - #explicit_sync_kms = 2 direct_scanout = 0 } diff --git a/config/hypr/UserConfigs/WindowRules.conf b/config/hypr/UserConfigs/WindowRules.conf index 099cd2c8..4485a8f5 100644 --- a/config/hypr/UserConfigs/WindowRules.conf +++ b/config/hypr/UserConfigs/WindowRules.conf @@ -211,6 +211,10 @@ layerrule = blur, rofi layerrule = ignorezero, rofi layerrule = blur, notifications layerrule = ignorezero, notifications +layerrule = blur, quickshell:overview +layerrule = ignorezero, quickshell:overview +layerrule = ignorealpha 0.5, quickshell:overview + #layerrule = ignorealpha 0.5, tag:notif* #layerrule = ignorezero, class:^([Rr]ofi)$ diff --git a/config/hypr/scripts/Refresh.sh b/config/hypr/scripts/Refresh.sh index 2d7887a5..d04570b1 100755 --- a/config/hypr/scripts/Refresh.sh +++ b/config/hypr/scripts/Refresh.sh @@ -25,8 +25,8 @@ done # added since wallust sometimes not applying killall -SIGUSR2 waybar -# quit ags & relaunch ags -#ags -q && ags & +# quit quickshell & relaunch quickshell +#pkill qs && qs & # some process to kill for pid in $(pidof waybar rofi swaync ags swaybg); do diff --git a/config/hypr/scripts/RefreshNoWaybar.sh b/config/hypr/scripts/RefreshNoWaybar.sh index e5a0835e..cdbb82db 100755 --- a/config/hypr/scripts/RefreshNoWaybar.sh +++ b/config/hypr/scripts/RefreshNoWaybar.sh @@ -26,7 +26,7 @@ for _prs in "${_ps[@]}"; do done # quit ags & relaunch ags -#ags -q && ags & +#pkill qs && qs & # Wallust refresh ${SCRIPTSDIR}/WallustSwww.sh & diff --git a/config/quickshell/GlobalStates.qml b/config/quickshell/GlobalStates.qml new file mode 100644 index 00000000..84b53c03 --- /dev/null +++ b/config/quickshell/GlobalStates.qml @@ -0,0 +1,10 @@ +import "root:/modules/common/" +import QtQuick +import Quickshell +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + id: root + property bool overviewOpen: false +}
\ No newline at end of file diff --git a/config/quickshell/config.json b/config/quickshell/config.json new file mode 100644 index 00000000..286985f5 --- /dev/null +++ b/config/quickshell/config.json @@ -0,0 +1,53 @@ +{ + "ui": { + "theme": "dark", + "scale": 1.25 + }, + "features": { + "animations": true + }, + "appearance": { + "fakeScreenRounding": 1 + }, + "overview": { + "scale": 0.15, + "numOfRows": 2, + "numOfCols": 5, + "showXwaylandIndicator": true, + "windowPadding": 6, + "position": 1, + "workspaceNumberSize": 220 + }, + "resources": { + "updateInterval": 3000 + }, + "hacks": { + "arbitraryRaceConditionDelay": 20 + }, + "search": { + "searchEnabled": true, + "nonAppResultDelay": 30, + "prefix": { + "action": "/", + "clipboard": ";", + "emojis": ":" + } + }, + "bar": { + "bottom": false + }, + "font": { + "family": { + "uiFont": "Open Sans", + "iconFont": "FiraConde Nerd Font", + "codeFont": "JetBrains Mono NF" + }, + "pixelSize": { + "textSmall": 13, + "textBase": 15, + "textMedium": 16, + "textLarge": 19, + "iconLarge": 22 + } + } +}
\ No newline at end of file diff --git a/config/quickshell/modules/common/Appearance.qml b/config/quickshell/modules/common/Appearance.qml new file mode 100644 index 00000000..675f1d1e --- /dev/null +++ b/config/quickshell/modules/common/Appearance.qml @@ -0,0 +1,201 @@ +import QtQuick +import Quickshell +import "root:/modules/common/functions/color_utils.js" as ColorUtils +pragma Singleton +pragma ComponentBehavior: Bound + + +Singleton { + id: root + property QtObject m3colors + property QtObject animation + property QtObject animationCurves + property QtObject colors + property QtObject rounding + property QtObject font + property QtObject sizes + + property real transparency: 0.5 + property real contentTransparency: 0.1 + property real workpaceTransparency: 0.8 + property string background_image: Directories.config + "/rofi/.current_wallpaper" + + m3colors: QtObject { + property bool darkmode: true + property bool transparent: true + + property color m3windowBackground: "#161217" + property color m3primaryText: "#EAE0E7" + property color m3layerBackground1: "#1F1A1F" + property color m3layerBackground2: "#231E23" + property color m3layerBackground3: "#2D282E" + property color m3surfaceText: "#EAE0E7" + property color m3secondaryText: "#CFC3CD" + property color m3borderPrimary: "#cba6f7" + property color m3shadowColor: "#000000" + property color m3accentPrimary: "#E5B6F2" + property color m3accentSecondary: "#D5C0D7" + property color m3selectionBackground: "#534457" + property color m3accentPrimaryText: "#452152" + property color m3selectionText: "#F2DCF3" + property color m3borderSecondary: "#4C444D" + + property color colTooltip: "#1e1e2e" + property color colOnTooltip: "#F8F9FA" + } + + colors: QtObject { + property color colSubtext: m3colors.m3borderPrimary + property color colLayer0: ColorUtils.transparentize(m3colors.m3windowBackground, root.transparency) + property color colLayer1: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3layerBackground1, m3colors.m3windowBackground, 0.7), root.contentTransparency); + property color colOnLayer1: m3colors.m3secondaryText; + property color colLayer2: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3layerBackground2, m3colors.m3layerBackground3, 0.55), root.contentTransparency) + property color colOnLayer2: m3colors.m3surfaceText; + property color colLayer1Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.92), root.contentTransparency) + property color colLayer1Active: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.85), root.contentTransparency); + property color colLayer2Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.90), root.contentTransparency) + property color colLayer2Active: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.80), root.contentTransparency); + property color colPrimary: m3colors.m3accentPrimary + property color colPrimaryHover: ColorUtils.mix(colors.colPrimary, colLayer1Hover, 0.87) + property color colPrimaryActive: ColorUtils.mix(colors.colPrimary, colLayer1Active, 0.7) + property color colShadow: ColorUtils.transparentize(m3colors.m3shadowColor, 0.7) + } + + rounding: QtObject { + property int unsharpen: 2 + property int verysmall: 8 + property int small: 12 + property int normal: 17 + property int large: 23 + property int verylarge: 30 + property int veryverylarge: 60 + property int full: 9999 + property int screenRounding: veryverylarge + property int windowRounding: veryverylarge + } + + font: QtObject { + property QtObject family: QtObject { + property string uiFont: "Open Sans" + property string iconFont: "FiraConde Nerd Font" + property string codeFont: "JetBrains Mono NF" + } + property QtObject pixelSize: QtObject { + property int textSmall: 13 + property int textBase: 15 + property int textMedium: 16 + property int textLarge: 19 + property int iconLarge: 22 + } + } + + animationCurves: QtObject { + readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.90, 1, 1] // Default, 350ms + readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1] // Default, 500ms + readonly property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] // Default, 650ms + readonly property list<real> expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1] // Default, 200ms + readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] + readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] + readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] + readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1] + readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1] + readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1] + } + + animation: QtObject { + property QtObject elementMove: QtObject { + property int duration: 500 + property int type: Easing.BezierSpline + property list<real> bezierCurve: animationCurves.expressiveDefaultSpatial + property int velocity: 650 + property Component numberAnimation: Component { + NumberAnimation { + duration: root.animation.elementMove.duration + easing.type: root.animation.elementMove.type + easing.bezierCurve: root.animation.elementMove.bezierCurve + } + } + property Component colorAnimation: Component { + ColorAnimation { + duration: root.animation.elementMove.duration + easing.type: root.animation.elementMove.type + easing.bezierCurve: root.animation.elementMove.bezierCurve + } + } + } + property QtObject elementMoveEnter: QtObject { + property int duration: 400 + property int type: Easing.BezierSpline + property list<real> bezierCurve: animationCurves.emphasizedDecel + property int velocity: 650 + property Component numberAnimation: Component { + NumberAnimation { + duration: root.animation.elementMoveEnter.duration + easing.type: root.animation.elementMoveEnter.type + easing.bezierCurve: root.animation.elementMoveEnter.bezierCurve + } + } + } + property QtObject elementMoveExit: QtObject { + property int duration: 200 + property int type: Easing.BezierSpline + property list<real> bezierCurve: animationCurves.emphasizedAccel + property int velocity: 650 + property Component numberAnimation: Component { + NumberAnimation { + duration: root.animation.elementMoveExit.duration + easing.type: root.animation.elementMoveExit.type + easing.bezierCurve: root.animation.elementMoveExit.bezierCurve + } + } + } + property QtObject elementMoveFast: QtObject { + property int duration: 200 + property int type: Easing.BezierSpline + property list<real> bezierCurve: animationCurves.expressiveEffects + property int velocity: 850 + property Component colorAnimation: Component { ColorAnimation { + duration: root.animation.elementMoveFast.duration + easing.type: root.animation.elementMoveFast.type + easing.bezierCurve: root.animation.elementMoveFast.bezierCurve + }} + property Component numberAnimation: Component { NumberAnimation { + duration: root.animation.elementMoveFast.duration + easing.type: root.animation.elementMoveFast.type + easing.bezierCurve: root.animation.elementMoveFast.bezierCurve + }} + } + + property QtObject clickBounce: QtObject { + property int duration: 200 + property int type: Easing.BezierSpline + property list<real> bezierCurve: animationCurves.expressiveFastSpatial + property int velocity: 850 + property Component numberAnimation: Component { NumberAnimation { + duration: root.animation.clickBounce.duration + easing.type: root.animation.clickBounce.type + easing.bezierCurve: root.animation.clickBounce.bezierCurve + }} + } + property QtObject scroll: QtObject { + property int duration: 400 + property int type: Easing.BezierSpline + property list<real> bezierCurve: animationCurves.standardDecel + } + property QtObject menuDecel: QtObject { + property int duration: 350 + property int type: Easing.OutExpo + } + } + + sizes: QtObject { + property real barHeight: 40 + property real notificationPopupWidth: 410 + property real searchWidthCollapsed: 260 + property real searchWidth: 450 + property real hyprlandGapsOut: 5 + property real elevationMargin: 10 + property real fabShadowRadius: 5 + property real fabHoveredShadowRadius: 7 + } +} diff --git a/config/quickshell/modules/common/ConfigOptions.qml b/config/quickshell/modules/common/ConfigOptions.qml new file mode 100644 index 00000000..25f0de05 --- /dev/null +++ b/config/quickshell/modules/common/ConfigOptions.qml @@ -0,0 +1,43 @@ +import QtQuick +import Quickshell +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + + property QtObject appearance: QtObject { + property int fakeScreenRounding: 1 // 0: None | 1: Always | 2: When not fullscreen + } + + property QtObject overview: QtObject { + property real scale: 0.15 // Relative to screen size + property real numOfRows: 2 + property real numOfCols: 5 + property bool showXwaylandIndicator: true + property real windowPadding: 6 + property real position: 1 // 0: top | 1: middle | 2: bottom + property real workspaceNumberSize: 120 // Set 0, dynamic calculation based on monitor size + } + + property QtObject resources: QtObject { + property int updateInterval: 3000 + } + + property QtObject hacks: QtObject { + property int arbitraryRaceConditionDelay: 20 // milliseconds + } + + property QtObject search: QtObject { + property bool searchEnabled: false + property int nonAppResultDelay: 30 // This prevents lagging when typing + property QtObject prefix: QtObject { + property string action: "/" + property string clipboard: ";" + property string emojis: ":" + } + } + + property QtObject bar: QtObject { + property bool bottom: false // Instead of top + } +} diff --git a/config/quickshell/modules/common/Directories.qml b/config/quickshell/modules/common/Directories.qml new file mode 100644 index 00000000..f4a6bf83 --- /dev/null +++ b/config/quickshell/modules/common/Directories.qml @@ -0,0 +1,20 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import "root:/modules/common/functions/file_utils.js" as FileUtils +import Qt.labs.platform +import QtQuick +import Quickshell +import Quickshell.Hyprland + +Singleton { + // XDG Dirs, with "file://" + readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0] + readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0] + readonly property string gen_cache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0] + + // Other dirs used by the shell, without "file://" + property string shellConfig: FileUtils.trimFileProtocol(`${Directories.config}/quickshell`) + property string shellConfigPath: `${Directories.shellConfig}/config.json` + property string generatedMaterialThemePath: `${Directories.shellConfig}/qml_color.json` +} diff --git a/config/quickshell/modules/common/functions/color_utils.js b/config/quickshell/modules/common/functions/color_utils.js new file mode 100644 index 00000000..c0ccfda9 --- /dev/null +++ b/config/quickshell/modules/common/functions/color_utils.js @@ -0,0 +1,85 @@ +// This module provides high level utility functions for color manipulation. + +/** + * Returns a color with the hue of color2 and the saturation, value, and alpha of color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The color to take hue from. + * @returns {Qt.rgba} The resulting color. + */ +function colorWithHueOf(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + // Qt.color hsvHue/hsvSaturation/hsvValue/alpha return 0-1 + var hue = c2.hsvHue; + var sat = c1.hsvSaturation; + var val = c1.hsvValue; + var alpha = c1.a; + + return Qt.hsva(hue, sat, val, alpha); +} + +/** + * Returns a color with the saturation of color2 and the hue/value/alpha of color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The color to take saturation from. + * @returns {Qt.rgba} The resulting color. + */ +function colorWithSaturationOf(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + var hue = c1.hsvHue; + var sat = c2.hsvSaturation; + var val = c1.hsvValue; + var alpha = c1.a; + + return Qt.hsva(hue, sat, val, alpha); +} + +/** + * Adapts color1 to the accent (hue and saturation) of color2 using HSL, keeping lightness and alpha from color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The accent color. + * @returns {Qt.rgba} The resulting color. + */ +function adaptToAccent(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + var hue = c2.hslHue; + var sat = c2.hslSaturation; + var light = c1.hslLightness; + var alpha = c1.a; + + return Qt.hsla(hue, sat, light, alpha); +} + +/** + * Mixes two colors by a given percentage. + * + * @param {string} color1 - The first color (any Qt.color-compatible string). + * @param {string} color2 - The second color. + * @param {number} percentage - The mix ratio (0-1). 1 = all color1, 0 = all color2. + * @returns {Qt.rgba} The resulting mixed color. + */ +function mix(color1, color2, percentage) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + return Qt.rgba(percentage * c1.r + (1 - percentage) * c2.r, percentage * c1.g + (1 - percentage) * c2.g, percentage * c1.b + (1 - percentage) * c2.b, percentage * c1.a + (1 - percentage) * c2.a); +} + +/** + * Transparentizes a color by a given percentage. + * + * @param {string} color - The color (any Qt.color-compatible string). + * @param {number} percentage - The amount to transparentize (0-1). + * @returns {Qt.rgba} The resulting color. + */ +function transparentize(color, percentage = 1) { + var c = Qt.color(color); + return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage)); +} diff --git a/config/quickshell/modules/common/functions/file_utils.js b/config/quickshell/modules/common/functions/file_utils.js new file mode 100644 index 00000000..758950de --- /dev/null +++ b/config/quickshell/modules/common/functions/file_utils.js @@ -0,0 +1,9 @@ +/** + * Trims the File protocol off the input string + * @param {string} str + * @returns {string} + */ +function trimFileProtocol(str) { + return str.startsWith("file://") ? str.slice(7) : str; +} + diff --git a/config/quickshell/modules/common/functions/fuzzysort.js b/config/quickshell/modules/common/functions/fuzzysort.js new file mode 100644 index 00000000..1c1d9b9d --- /dev/null +++ b/config/quickshell/modules/common/functions/fuzzysort.js @@ -0,0 +1,682 @@ +.pragma library + +// https://github.com/farzher/fuzzysort +// License: MIT | Copyright (c) 2018 Stephen Kamenar +// A copy of the license is available in the `licenses` folder of this repository + +var single = (search, target) => { + if(!search || !target) return NULL + + var preparedSearch = getPreparedSearch(search) + if(!isPrepared(target)) target = getPrepared(target) + + var searchBitflags = preparedSearch.bitflags + if((searchBitflags & target._bitflags) !== searchBitflags) return NULL + + return algorithm(preparedSearch, target) +} + +var go = (search, targets, options) => { + if(!search) return options?.all ? all(targets, options) : noResults + + var preparedSearch = getPreparedSearch(search) + var searchBitflags = preparedSearch.bitflags + var containsSpace = preparedSearch.containsSpace + + var threshold = denormalizeScore( options?.threshold || 0 ) + var limit = options?.limit || INFINITY + + var resultsLen = 0; var limitedCount = 0 + var targetsLen = targets.length + + function push_result(result) { + if(resultsLen < limit) { q.add(result); ++resultsLen } + else { + ++limitedCount + if(result._score > q.peek()._score) q.replaceTop(result) + } + } + + // This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys] + + // options.key + if(options?.key) { + var key = options.key + for(var i = 0; i < targetsLen; ++i) { var obj = targets[i] + var target = getValue(obj, key) + if(!target) continue + if(!isPrepared(target)) target = getPrepared(target) + + if((searchBitflags & target._bitflags) !== searchBitflags) continue + var result = algorithm(preparedSearch, target) + if(result === NULL) continue + if(result._score < threshold) continue + + result.obj = obj + push_result(result) + } + + // options.keys + } else if(options?.keys) { + var keys = options.keys + var keysLen = keys.length + + outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i] + + { // early out based on bitflags + var keysBitflags = 0 + for (var keyI = 0; keyI < keysLen; ++keyI) { + var key = keys[keyI] + var target = getValue(obj, key) + if(!target) { tmpTargets[keyI] = noTarget; continue } + if(!isPrepared(target)) target = getPrepared(target) + tmpTargets[keyI] = target + + keysBitflags |= target._bitflags + } + + if((searchBitflags & keysBitflags) !== searchBitflags) continue + } + + if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) keysSpacesBestScores[i] = NEGATIVE_INFINITY + + for (var keyI = 0; keyI < keysLen; ++keyI) { + target = tmpTargets[keyI] + if(target === noTarget) { tmpResults[keyI] = noTarget; continue } + + tmpResults[keyI] = algorithm(preparedSearch, target, /*allowSpaces=*/false, /*allowPartialMatch=*/containsSpace) + if(tmpResults[keyI] === NULL) { tmpResults[keyI] = noTarget; continue } + + // todo: this seems weird and wrong. like what if our first match wasn't good. this should just replace it instead of averaging with it + // if our second match isn't good we ignore it instead of averaging with it + if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) { + if(allowPartialMatchScores[i] > -1000) { + if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) { + var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/ + if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp + } + } + if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i] + } + } + + if(containsSpace) { + for(let i=0; i<preparedSearch.spaceSearches.length; i++) { if(keysSpacesBestScores[i] === NEGATIVE_INFINITY) continue outer } + } else { + var hasAtLeast1Match = false + for(let i=0; i < keysLen; i++) { if(tmpResults[i]._score !== NEGATIVE_INFINITY) { hasAtLeast1Match = true; break } } + if(!hasAtLeast1Match) continue + } + + var objResults = new KeysResult(keysLen) + for(let i=0; i < keysLen; i++) { objResults[i] = tmpResults[i] } + + if(containsSpace) { + var score = 0 + for(let i=0; i<preparedSearch.spaceSearches.length; i++) score += keysSpacesBestScores[i] + } else { + // todo could rewrite this scoring to be more similar to when there's spaces + // if we match multiple keys give us bonus points + var score = NEGATIVE_INFINITY + for(let i=0; i<keysLen; i++) { + var result = objResults[i] + if(result._score > -1000) { + if(score > NEGATIVE_INFINITY) { + var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/ + if(tmp > score) score = tmp + } + } + if(result._score > score) score = result._score + } + } + + objResults.obj = obj + objResults._score = score + if(options?.scoreFn) { + score = options.scoreFn(objResults) + if(!score) continue + score = denormalizeScore(score) + objResults._score = score + } + + if(score < threshold) continue + push_result(objResults) + } + + // no keys + } else { + for(var i = 0; i < targetsLen; ++i) { var target = targets[i] + if(!target) continue + if(!isPrepared(target)) target = getPrepared(target) + + if((searchBitflags & target._bitflags) !== searchBitflags) continue + var result = algorithm(preparedSearch, target) + if(result === NULL) continue + if(result._score < threshold) continue + + push_result(result) + } + } + + if(resultsLen === 0) return noResults + var results = new Array(resultsLen) + for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll() + results.total = resultsLen + limitedCount + return results +} + + +// this is written as 1 function instead of 2 for minification. perf seems fine ... +// except when minified. the perf is very slow +var highlight = (result, open='<b>', close='</b>') => { + var callback = typeof open === 'function' ? open : undefined + + var target = result.target + var targetLen = target.length + var indexes = result.indexes + var highlighted = '' + var matchI = 0 + var indexesI = 0 + var opened = false + var parts = [] + + for(var i = 0; i < targetLen; ++i) { var char = target[i] + if(indexes[indexesI] === i) { + ++indexesI + if(!opened) { opened = true + if(callback) { + parts.push(highlighted); highlighted = '' + } else { + highlighted += open + } + } + + if(indexesI === indexes.length) { + if(callback) { + highlighted += char + parts.push(callback(highlighted, matchI++)); highlighted = '' + parts.push(target.substr(i+1)) + } else { + highlighted += char + close + target.substr(i+1) + } + break + } + } else { + if(opened) { opened = false + if(callback) { + parts.push(callback(highlighted, matchI++)); highlighted = '' + } else { + highlighted += close + } + } + } + highlighted += char + } + + return callback ? parts : highlighted +} + + +var prepare = (target) => { + if(typeof target === 'number') target = ''+target + else if(typeof target !== 'string') target = '' + var info = prepareLowerInfo(target) + return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags}) +} + +var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() } + + +// Below this point is only internal code +// Below this point is only internal code +// Below this point is only internal code +// Below this point is only internal code + + +class Result { + get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) } + set ['indexes'](indexes) { return this._indexes = indexes } + ['highlight'](open, close) { return highlight(this, open, close) } + get ['score']() { return normalizeScore(this._score) } + set ['score'](score) { this._score = denormalizeScore(score) } +} + +class KeysResult extends Array { + get ['score']() { return normalizeScore(this._score) } + set ['score'](score) { this._score = denormalizeScore(score) } +} + +var new_result = (target, options) => { + const result = new Result() + result['target'] = target + result['obj'] = options.obj ?? NULL + result._score = options._score ?? NEGATIVE_INFINITY + result._indexes = options._indexes ?? [] + result._targetLower = options._targetLower ?? '' + result._targetLowerCodes = options._targetLowerCodes ?? NULL + result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL + result._bitflags = options._bitflags ?? 0 + return result +} + + +var normalizeScore = score => { + if(score === NEGATIVE_INFINITY) return 0 + if(score > 1) return score + return Math.E ** ( ((-score + 1)**.04307 - 1) * -2) +} +var denormalizeScore = normalizedScore => { + if(normalizedScore === 0) return NEGATIVE_INFINITY + if(normalizedScore > 1) return normalizedScore + return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307) +} + + +var prepareSearch = (search) => { + if(typeof search === 'number') search = ''+search + else if(typeof search !== 'string') search = '' + search = search.trim() + var info = prepareLowerInfo(search) + + var spaceSearches = [] + if(info.containsSpace) { + var searches = search.split(/\s+/) + searches = [...new Set(searches)] // distinct + for(var i=0; i<searches.length; i++) { + if(searches[i] === '') continue + var _info = prepareLowerInfo(searches[i]) + spaceSearches.push({lowerCodes:_info.lowerCodes, _lower:searches[i].toLowerCase(), containsSpace:false}) + } + } + + return {lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches} +} + + + +var getPrepared = (target) => { + if(target.length > 999) return prepare(target) // don't cache huge targets + var targetPrepared = preparedCache.get(target) + if(targetPrepared !== undefined) return targetPrepared + targetPrepared = prepare(target) + preparedCache.set(target, targetPrepared) + return targetPrepared +} +var getPreparedSearch = (search) => { + if(search.length > 999) return prepareSearch(search) // don't cache huge searches + var searchPrepared = preparedSearchCache.get(search) + if(searchPrepared !== undefined) return searchPrepared + searchPrepared = prepareSearch(search) + preparedSearchCache.set(search, searchPrepared) + return searchPrepared +} + + +var all = (targets, options) => { + var results = []; results.total = targets.length // this total can be wrong if some targets are skipped + + var limit = options?.limit || INFINITY + + if(options?.key) { + for(var i=0;i<targets.length;i++) { var obj = targets[i] + var target = getValue(obj, options.key) + if(target == NULL) continue + if(!isPrepared(target)) target = getPrepared(target) + var result = new_result(target.target, {_score: target._score, obj: obj}) + results.push(result); if(results.length >= limit) return results + } + } else if(options?.keys) { + for(var i=0;i<targets.length;i++) { var obj = targets[i] + var objResults = new KeysResult(options.keys.length) + for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) { + var target = getValue(obj, options.keys[keyI]) + if(!target) { objResults[keyI] = noTarget; continue } + if(!isPrepared(target)) target = getPrepared(target) + target._score = NEGATIVE_INFINITY + target._indexes.len = 0 + objResults[keyI] = target + } + objResults.obj = obj + objResults._score = NEGATIVE_INFINITY + results.push(objResults); if(results.length >= limit) return results + } + } else { + for(var i=0;i<targets.length;i++) { var target = targets[i] + if(target == NULL) continue + if(!isPrepared(target)) target = getPrepared(target) + target._score = NEGATIVE_INFINITY + target._indexes.len = 0 + results.push(target); if(results.length >= limit) return results + } + } + + return results +} + + +var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => { + if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch) + + var searchLower = preparedSearch._lower + var searchLowerCodes = preparedSearch.lowerCodes + var searchLowerCode = searchLowerCodes[0] + var targetLowerCodes = prepared._targetLowerCodes + var searchLen = searchLowerCodes.length + var targetLen = targetLowerCodes.length + var searchI = 0 // where we at + var targetI = 0 // where you at + var matchesSimpleLen = 0 + + // very basic fuzzy match; to remove non-matching targets ASAP! + // walk through target. find sequential matches. + // if all chars aren't found then exit + for(;;) { + var isMatch = searchLowerCode === targetLowerCodes[targetI] + if(isMatch) { + matchesSimple[matchesSimpleLen++] = targetI + ++searchI; if(searchI === searchLen) break + searchLowerCode = searchLowerCodes[searchI] + } + ++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI + } + + var searchI = 0 + var successStrict = false + var matchesStrictLen = 0 + + var nextBeginningIndexes = prepared._nextBeginningIndexes + if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target) + targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1] + + // Our target string successfully matched all characters in sequence! + // Let's try a more advanced and strict test to improve the score + // only count it as a match if it's consecutive or a beginning character! + var backtrackCount = 0 + if(targetI !== targetLen) for(;;) { + if(targetI >= targetLen) { + // We failed to find a good spot for this search char, go back to the previous search char and force it forward + if(searchI <= 0) break // We failed to push chars forward for a better match + + ++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match + + --searchI + var lastMatch = matchesStrict[--matchesStrictLen] + targetI = nextBeginningIndexes[lastMatch] + + } else { + var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI] + if(isMatch) { + matchesStrict[matchesStrictLen++] = targetI + ++searchI; if(searchI === searchLen) { successStrict = true; break } + ++targetI + } else { + targetI = nextBeginningIndexes[targetI] + } + } + } + + // check if it's a substring match + var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow + var isSubstring = !!~substringIndex + var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex + + // if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score + if(isSubstring && !isSubstringBeginning) { + for(var i=0; i<nextBeginningIndexes.length; i=nextBeginningIndexes[i]) { + if(i <= substringIndex) continue + + for(var s=0; s<searchLen; s++) if(searchLowerCodes[s] !== prepared._targetLowerCodes[i+s]) break + if(s === searchLen) { substringIndex = i; isSubstringBeginning = true; break } + } + } + + // tally up the score & keep track of matches for highlighting later + // if it's a simple match, we'll switch to a substring match if a substring exists + // if it's a strict match, we'll switch to a substring match only if that's a better score + + var calculateScore = matches => { + var score = 0 + + var extraMatchGroupCount = 0 + for(var i = 1; i < searchLen; ++i) { + if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount} + } + var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1) + + score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups + + if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning + + if(!successStrict) { + score *= 1000 + } else { + // successStrict on a target with too many beginning indexes loses points for being a bad target + var uniqueBeginningIndexes = 1 + for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes + + if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ... + } + + score -= (targetLen - searchLen)/2 // penality for longer targets + + if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring + if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex + + score -= (targetLen - searchLen)/2 // penality for longer targets + + return score + } + + if(!successStrict) { + if(isSubstring) for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches + var matchesBest = matchesSimple + var score = calculateScore(matchesBest) + } else { + if(isSubstringBeginning) { + for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches + var matchesBest = matchesSimple + var score = calculateScore(matchesSimple) + } else { + var matchesBest = matchesStrict + var score = calculateScore(matchesStrict) + } + } + + prepared._score = score + + for(var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i] + prepared._indexes.len = searchLen + + const result = new Result() + result.target = prepared.target + result._score = prepared._score + result._indexes = prepared._indexes + return result +} +var algorithmSpaces = (preparedSearch, target, allowPartialMatch) => { + var seen_indexes = new Set() + var score = 0 + var result = NULL + + var first_seen_index_last_search = 0 + var searches = preparedSearch.spaceSearches + var searchesLen = searches.length + var changeslen = 0 + + // Return _nextBeginningIndexes back to its normal state + var resetNextBeginningIndexes = () => { + for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1] + } + + var hasAtLeast1Match = false + for(var i=0; i<searchesLen; ++i) { + allowPartialMatchScores[i] = NEGATIVE_INFINITY + var search = searches[i] + + result = algorithm(search, target) + if(allowPartialMatch) { + if(result === NULL) continue + hasAtLeast1Match = true + } else { + if(result === NULL) {resetNextBeginningIndexes(); return NULL} + } + + // if not the last search, we need to mutate _nextBeginningIndexes for the next search + var isTheLastSearch = i === searchesLen - 1 + if(!isTheLastSearch) { + var indexes = result._indexes + + var indexesIsConsecutiveSubstring = true + for(let i=0; i<indexes.len-1; i++) { + if(indexes[i+1] - indexes[i] !== 1) { + indexesIsConsecutiveSubstring = false; break; + } + } + + if(indexesIsConsecutiveSubstring) { + var newBeginningIndex = indexes[indexes.len-1] + 1 + var toReplace = target._nextBeginningIndexes[newBeginningIndex-1] + for(let i=newBeginningIndex-1; i>=0; i--) { + if(toReplace !== target._nextBeginningIndexes[i]) break + target._nextBeginningIndexes[i] = newBeginningIndex + nextBeginningIndexesChanges[changeslen*2 + 0] = i + nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace + changeslen++ + } + } + } + + score += result._score / searchesLen + allowPartialMatchScores[i] = result._score / searchesLen + + // dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h + if(result._indexes[0] < first_seen_index_last_search) { + score -= (first_seen_index_last_search - result._indexes[0]) * 2 + } + first_seen_index_last_search = result._indexes[0] + + for(var j=0; j<result._indexes.len; ++j) seen_indexes.add(result._indexes[j]) + } + + if(allowPartialMatch && !hasAtLeast1Match) return NULL + + resetNextBeginningIndexes() + + // allows a search with spaces that's an exact substring to score well + var allowSpacesResult = algorithm(preparedSearch, target, /*allowSpaces=*/true) + if(allowSpacesResult !== NULL && allowSpacesResult._score > score) { + if(allowPartialMatch) { + for(var i=0; i<searchesLen; ++i) { + allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen + } + } + return allowSpacesResult + } + + if(allowPartialMatch) result = target + result._score = score + + var i = 0 + for (let index of seen_indexes) result._indexes[i++] = index + result._indexes.len = i + + return result +} + +// we use this instead of just .normalize('NFD').replace(/[\u0300-\u036f]/g, '') because that screws with japanese characters +var remove_accents = (str) => str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '') + +var prepareLowerInfo = (str) => { + str = remove_accents(str) + var strLen = str.length + var lower = str.toLowerCase() + var lowerCodes = [] // new Array(strLen) sparse array is too slow + var bitflags = 0 + var containsSpace = false // space isn't stored in bitflags because of how searching with a space works + + for(var i = 0; i < strLen; ++i) { + var lowerCode = lowerCodes[i] = lower.charCodeAt(i) + + if(lowerCode === 32) { + containsSpace = true + continue // it's important that we don't set any bitflags for space + } + + var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet + : lowerCode>=48&&lowerCode<=57 ? 26 // numbers + // 3 bits available + : lowerCode<=127 ? 30 // other ascii + : 31 // other utf8 + bitflags |= 1<<bit + } + + return {lowerCodes:lowerCodes, bitflags:bitflags, containsSpace:containsSpace, _lower:lower} +} +var prepareBeginningIndexes = (target) => { + var targetLen = target.length + var beginningIndexes = []; var beginningIndexesLen = 0 + var wasUpper = false + var wasAlphanum = false + for(var i = 0; i < targetLen; ++i) { + var targetCode = target.charCodeAt(i) + var isUpper = targetCode>=65&&targetCode<=90 + var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57 + var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum + wasUpper = isUpper + wasAlphanum = isAlphanum + if(isBeginning) beginningIndexes[beginningIndexesLen++] = i + } + return beginningIndexes +} +var prepareNextBeginningIndexes = (target) => { + target = remove_accents(target) + var targetLen = target.length + var beginningIndexes = prepareBeginningIndexes(target) + var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow + var lastIsBeginning = beginningIndexes[0] + var lastIsBeginningI = 0 + for(var i = 0; i < targetLen; ++i) { + if(lastIsBeginning > i) { + nextBeginningIndexes[i] = lastIsBeginning + } else { + lastIsBeginning = beginningIndexes[++lastIsBeginningI] + nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning + } + } + return nextBeginningIndexes +} + +var preparedCache = new Map() +var preparedSearchCache = new Map() + +// the theory behind these being globals is to reduce garbage collection by not making new arrays +var matchesSimple = []; var matchesStrict = [] +var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search +var keysSpacesBestScores = []; var allowPartialMatchScores = [] +var tmpTargets = []; var tmpResults = [] + +// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop] +// prop = 'key1.key2' 10ms +// prop = ['key1', 'key2'] 27ms +// prop = obj => obj.tags.join() ??ms +var getValue = (obj, prop) => { + var tmp = obj[prop]; if(tmp !== undefined) return tmp + if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower + var segs = prop + if(!Array.isArray(prop)) segs = prop.split('.') + var len = segs.length + var i = -1 + while (obj && (++i < len)) obj = obj[segs[i]] + return obj +} + +var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' } +var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY +var noResults = []; noResults.total = 0 +var NULL = null + +var noTarget = prepare('') + +// Hacked version of https://github.com/lemire/FastPriorityQueue.js +var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c<o;){var s=c+1;a=c,s<o&&e[s]._score<e[c]._score&&(a=s),e[a-1>>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score<e[f]._score;f=(a=f)-1>>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score<e[v]._score;v=(a=v)-1>>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a} +var q = fastpriorityqueue() // reuse this diff --git a/config/quickshell/modules/common/functions/levendist.js b/config/quickshell/modules/common/functions/levendist.js new file mode 100644 index 00000000..90180d21 --- /dev/null +++ b/config/quickshell/modules/common/functions/levendist.js @@ -0,0 +1,141 @@ +// Original code from https://github.com/koeqaife/hyprland-material-you +// Original code license: GPLv3 +// Translated to Js from Cython with an LLM and reviewed + +function min3(a, b, c) { + return a < b && a < c ? a : b < c ? b : c; +} + +function max3(a, b, c) { + return a > b && a > c ? a : b > c ? b : c; +} + +function min2(a, b) { + return a < b ? a : b; +} + +function max2(a, b) { + return a > b ? a : b; +} + +function levenshteinDistance(s1, s2) { + let len1 = s1.length; + let len2 = s2.length; + + if (len1 === 0) return len2; + if (len2 === 0) return len1; + + if (len2 > len1) { + [s1, s2] = [s2, s1]; + [len1, len2] = [len2, len1]; + } + + let prev = new Array(len2 + 1); + let curr = new Array(len2 + 1); + + for (let j = 0; j <= len2; j++) { + prev[j] = j; + } + + for (let i = 1; i <= len1; i++) { + curr[0] = i; + for (let j = 1; j <= len2; j++) { + let cost = s1[i - 1] === s2[j - 1] ? 0 : 1; + curr[j] = min3(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost); + } + [prev, curr] = [curr, prev]; + } + + return prev[len2]; +} + +function partialRatio(shortS, longS) { + let lenS = shortS.length; + let lenL = longS.length; + let best = 0.0; + + if (lenS === 0) return 1.0; + + for (let i = 0; i <= lenL - lenS; i++) { + let sub = longS.slice(i, i + lenS); + let dist = levenshteinDistance(shortS, sub); + let score = 1.0 - (dist / lenS); + if (score > best) best = score; + } + + return best; +} + +function computeScore(s1, s2) { + if (s1 === s2) return 1.0; + + let dist = levenshteinDistance(s1, s2); + let maxLen = max2(s1.length, s2.length); + if (maxLen === 0) return 1.0; + + let full = 1.0 - (dist / maxLen); + let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1); + + let score = 0.85 * full + 0.15 * part; + + if (s1 && s2 && s1[0] !== s2[0]) { + score -= 0.05; + } + + let lenDiff = Math.abs(s1.length - s2.length); + if (lenDiff >= 3) { + score -= 0.05 * lenDiff / maxLen; + } + + let commonPrefixLen = 0; + let minLen = min2(s1.length, s2.length); + for (let i = 0; i < minLen; i++) { + if (s1[i] === s2[i]) { + commonPrefixLen++; + } else { + break; + } + } + score += 0.02 * commonPrefixLen; + + if (s1.includes(s2) || s2.includes(s1)) { + score += 0.06; + } + + return Math.max(0.0, Math.min(1.0, score)); +} + +function computeTextMatchScore(s1, s2) { + if (s1 === s2) return 1.0; + + let dist = levenshteinDistance(s1, s2); + let maxLen = max2(s1.length, s2.length); + if (maxLen === 0) return 1.0; + + let full = 1.0 - (dist / maxLen); + let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1); + + let score = 0.4 * full + 0.6 * part; + + let lenDiff = Math.abs(s1.length - s2.length); + if (lenDiff >= 10) { + score -= 0.02 * lenDiff / maxLen; + } + + let commonPrefixLen = 0; + let minLen = min2(s1.length, s2.length); + for (let i = 0; i < minLen; i++) { + if (s1[i] === s2[i]) { + commonPrefixLen++; + } else { + break; + } + } + score += 0.01 * commonPrefixLen; + + if (s1.includes(s2) || s2.includes(s1)) { + score += 0.2; + } + + return Math.max(0.0, Math.min(1.0, score)); +} diff --git a/config/quickshell/modules/common/functions/object_utils.js b/config/quickshell/modules/common/functions/object_utils.js new file mode 100644 index 00000000..96c632fd --- /dev/null +++ b/config/quickshell/modules/common/functions/object_utils.js @@ -0,0 +1,45 @@ +function toPlainObject(qtObj) { + if (qtObj === null || typeof qtObj !== "object") return qtObj; + + // Handle arrays + if (Array.isArray(qtObj)) { + return qtObj.map(toPlainObject); + } + + const result = ({}); + for (let key in qtObj) { + if ( + typeof qtObj[key] !== "function" && + !key.startsWith("objectName") && + !key.startsWith("children") && + !key.startsWith("object") && + !key.startsWith("parent") && + !key.startsWith("metaObject") && + !key.startsWith("destroyed") && + !key.startsWith("reloadableId") + ) { + result[key] = toPlainObject(qtObj[key]); + } + } + return result; +} + +function applyToQtObject(qtObj, jsonObj) { + if (!qtObj || typeof jsonObj !== "object" || jsonObj === null) return; + + for (let key in jsonObj) { + if (!qtObj.hasOwnProperty(key)) continue; + + // Check if the property is a QtObject (not a value) + const value = qtObj[key]; + const jsonValue = jsonObj[key]; + + // If it's an object and not an array, recurse + if (value && typeof value === "object" && !Array.isArray(value)) { + applyToQtObject(value, jsonValue); + } else { + // Otherwise, assign the value + qtObj[key] = jsonValue; + } + } +} diff --git a/config/quickshell/modules/common/functions/string_utils.js b/config/quickshell/modules/common/functions/string_utils.js new file mode 100644 index 00000000..c31edf49 --- /dev/null +++ b/config/quickshell/modules/common/functions/string_utils.js @@ -0,0 +1,53 @@ +/** + * Formats a string according to the args that are passed in + * @param { string } str + * @param {...any} args + * @returns + */ +function format(str, ...args) { + return str.replace(/{(\d+)}/g, (match, index) => + typeof args[index] !== 'undefined' ? args[index] : match + ); +} + +/** + * Returns the domain of the passed in url or null + * @param { string } url + * @returns { string| null } + */ +function getDomain(url) { + const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/); + return match ? match[1] : null; +} + +/** + * Returns the base url of the passed in url or null + * @param { string } url + * @returns { string | null } + */ +function getBaseUrl(url) { + const match = url.match(/^(https?:\/\/[^\/]+)(\/.*)?$/); + return match ? match[1] : null; +} + +/** + * Escapes single quotes in shell commands + * @param { string } str + * @returns { string } + */ +function shellSingleQuoteEscape(str) { + // escape single quotes + return String(str) + // .replace(/\\/g, '\\\\') + .replace(/'/g, "'\\''"); +} + +function escapeHtml(str) { + if (typeof str !== 'string') return str; + return str + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/config/quickshell/modules/common/widgets/CliphistImage.qml b/config/quickshell/modules/common/widgets/CliphistImage.qml new file mode 100644 index 00000000..9de34450 --- /dev/null +++ b/config/quickshell/modules/common/widgets/CliphistImage.qml @@ -0,0 +1,101 @@ +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/services" +import "root:/modules/common/functions/string_utils.js" as StringUtils +import "root:/modules/common/functions/file_utils.js" as FileUtils +import Qt5Compat.GraphicalEffects +import Qt.labs.platform +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Io +import Quickshell.Widgets +import Quickshell.Hyprland + +Rectangle { + id: root + property string entry + property real maxWidth + property real maxHeight + + property string imageDecodePath: Directories.cliphistDecode + property string imageDecodeFileName: `${entryNumber}` + property string imageDecodeFilePath: `${imageDecodePath}/${imageDecodeFileName}` + property string source + + property int entryNumber: { + if (!root.entry) return 0 + const match = root.entry.match(/^(\d+)\t/) + return match ? parseInt(match[1]) : 0 + } + property int imageWidth: { + if (!root.entry) return 0 + const match = root.entry.match(/(\d+)x(\d+)/) + return match ? parseInt(match[1]) : 0 + } + property int imageHeight: { + if (!root.entry) return 0 + const match = root.entry.match(/(\d+)x(\d+)/) + return match ? parseInt(match[2]) : 0 + } + property real scale: { + return Math.min( + root.maxWidth / imageWidth, + root.maxHeight / imageHeight, + 1 + ) + } + + color: Appearance.colors.colLayer1 + radius: Appearance.rounding.small + implicitHeight: imageHeight * scale + implicitWidth: imageWidth * scale + + Component.onCompleted: { + decodeImageProcess.running = true + } + + Process { + id: decodeImageProcess + command: ["bash", "-c", + `[ -f ${imageDecodeFilePath} ] || echo '${StringUtils.shellSingleQuoteEscape(root.entry)}' | cliphist decode > '${imageDecodeFilePath}'` + ] + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + root.source = imageDecodeFilePath + } else { + console.error("[CliphistImage] Failed to decode image for entry:", root.entry) + root.source = "" + } + } + } + + Component.onDestruction: { + Hyprland.dispatch(`exec bash -c "[ -f '${imageDecodeFilePath}' ] && rm -f '${imageDecodeFilePath}'"`) + } + + Image { + id: image + anchors.fill: parent + + source: Qt.resolvedUrl(root.source) + fillMode: Image.PreserveAspectFit + antialiasing: true + asynchronous: true + + width: root.imageWidth * root.scale + height: root.imageHeight * root.scale + sourceSize.width: width + sourceSize.height: height + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: image.width + height: image.height + radius: root.radius + } + } + } +} + diff --git a/config/quickshell/modules/common/widgets/DialogButton.qml b/config/quickshell/modules/common/widgets/DialogButton.qml new file mode 100644 index 00000000..b799336a --- /dev/null +++ b/config/quickshell/modules/common/widgets/DialogButton.qml @@ -0,0 +1,38 @@ +import "root:/modules/common" +import "root:/modules/common/functions/color_utils.js" as ColorUtils +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io + +/** + * Material 3 dialog button. See https://m3.material.io/components/dialogs/overview + */ +RippleButton { + id: button + + property string buttonText + implicitHeight: 30 + implicitWidth: buttonTextWidget.implicitWidth + 15 * 2 + buttonRadius: Appearance?.rounding.full ?? 9999 + + property color colEnabled: Appearance?.colors.colPrimary + property color colDisabled: Appearance?.m3colors.m3borderPrimary + + contentItem: StyledText { + id: buttonTextWidget + anchors.fill: parent + anchors.leftMargin: 15 + anchors.rightMargin: 15 + text: buttonText + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Appearance?.font.pixelSize.textBase ?? 12 + color: button.enabled ? button.colEnabled : button.colDisabled + + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + } + +} diff --git a/config/quickshell/modules/common/widgets/MaterialSymbol.qml b/config/quickshell/modules/common/widgets/MaterialSymbol.qml new file mode 100644 index 00000000..214f838e --- /dev/null +++ b/config/quickshell/modules/common/widgets/MaterialSymbol.qml @@ -0,0 +1,27 @@ +import "root:/modules/common/" +import QtQuick + +Text { + id: root + property real iconSize: Appearance?.font.pixelSize.textBase ?? 16 + property real fill: 0 + renderType: Text.NativeRendering + font.hintingPreference: Font.PreferFullHinting + verticalAlignment: Text.AlignVCenter + font.family: Appearance?.font.family.iconFont ?? "Material Symbols Rounded" + font.pixelSize: iconSize + color: Appearance.m3colors.m3primaryText + + Behavior on fill { + NumberAnimation { + duration: Appearance?.animation.elementMoveFast.duration ?? 200 + easing.type: Appearance?.animation.elementMoveFast.type ?? Easing.BezierSpline + easing.bezierCurve: Appearance?.animation.elementMoveFast.bezierCurve ?? [0.34, 0.80, 0.34, 1.00, 1, 1] + } + } + + font.variableAxes: { + "FILL": fill, + "opsz": iconSize, + } +} diff --git a/config/quickshell/modules/common/widgets/PointingHandInteraction.qml b/config/quickshell/modules/common/widgets/PointingHandInteraction.qml new file mode 100644 index 00000000..cf8b065f --- /dev/null +++ b/config/quickshell/modules/common/widgets/PointingHandInteraction.qml @@ -0,0 +1,7 @@ +import QtQuick + +MouseArea { + anchors.fill: parent + onPressed: (mouse) => mouse.accepted = false + cursorShape: Qt.PointingHandCursor +}
\ No newline at end of file diff --git a/config/quickshell/modules/common/widgets/RippleButton.qml b/config/quickshell/modules/common/widgets/RippleButton.qml new file mode 100644 index 00000000..cd7762b9 --- /dev/null +++ b/config/quickshell/modules/common/widgets/RippleButton.qml @@ -0,0 +1,185 @@ +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/color_utils.js" as ColorUtils +import Qt5Compat.GraphicalEffects +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Io +import Quickshell.Widgets + +/** + * A button with ripple effect similar to in Material Design. + */ +Button { + id: root + property bool toggled + property string buttonText + property real buttonRadius: Appearance?.rounding?.small ?? 4 + property real buttonRadiusPressed: buttonRadius + property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius + property int rippleDuration: 1200 + property bool rippleEnabled: true + property var downAction // When left clicking (down) + property var releaseAction // When left clicking (release) + property var altAction // When right clicking + property var middleClickAction // When middle clicking + + property color colBackground: ColorUtils.transparentize(Appearance?.colors.colLayer1Hover, 1) || "transparent" + property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED" + property color colBackgroundToggled: Appearance?.colors.colPrimary ?? "#65558F" + property color colBackgroundToggledHover: Appearance?.colors.colPrimaryHover ?? "#77699C" + property color colRipple: Appearance?.colors.colLayer1Active ?? "#D6CEE2" + property color colRippleToggled: Appearance?.colors.colPrimaryActive ?? "#D6CEE2" + + property color buttonColor: root.enabled ? (root.toggled ? + (root.hovered ? colBackgroundToggledHover : + colBackgroundToggled) : + (root.hovered ? colBackgroundHover : + colBackground)) : colBackground + property color rippleColor: root.toggled ? colRippleToggled : colRipple + + function startRipple(x, y) { + const stateY = buttonBackground.y; + rippleAnim.x = x; + rippleAnim.y = y - stateY; + + const dist = (ox,oy) => ox*ox + oy*oy + const stateEndY = stateY + buttonBackground.height + rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY))) + + rippleFadeAnim.complete(); + rippleAnim.restart(); + } + + component RippleAnim: NumberAnimation { + duration: rippleDuration + easing.type: Appearance?.animation.elementMoveEnter.type + easing.bezierCurve: Appearance?.animationCurves.standardDecel + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onPressed: (event) => { + if(event.button === Qt.RightButton) { + if (root.altAction) root.altAction(); + return; + } + if(event.button === Qt.MiddleButton) { + if (root.middleClickAction) root.middleClickAction(); + return; + } + root.down = true + if (root.downAction) root.downAction(); + if (!root.rippleEnabled) return; + const {x,y} = event + startRipple(x, y) + } + onReleased: (event) => { + root.down = false + if (event.button != Qt.LeftButton) return; + if (root.releaseAction) root.releaseAction(); + root.click() // Because the MouseArea already consumed the event + if (!root.rippleEnabled) return; + rippleFadeAnim.restart(); + } + onCanceled: (event) => { + root.down = false + if (!root.rippleEnabled) return; + rippleFadeAnim.restart(); + } + } + + RippleAnim { + id: rippleFadeAnim + target: ripple + property: "opacity" + to: 0 + } + + SequentialAnimation { + id: rippleAnim + + property real x + property real y + property real radius + + PropertyAction { + target: ripple + property: "x" + value: rippleAnim.x + } + PropertyAction { + target: ripple + property: "y" + value: rippleAnim.y + } + PropertyAction { + target: ripple + property: "opacity" + value: 1 + } + ParallelAnimation { + RippleAnim { + target: ripple + properties: "implicitWidth,implicitHeight" + from: 0 + to: rippleAnim.radius * 2 + } + } + } + + background: Rectangle { + id: buttonBackground + radius: root.buttonEffectiveRadius + implicitHeight: 50 + + color: root.buttonColor + Behavior on color { + animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: buttonBackground.width + height: buttonBackground.height + radius: root.buttonEffectiveRadius + } + } + + Item { + id: ripple + width: ripple.implicitWidth + height: ripple.implicitHeight + opacity: 0 + + property real implicitWidth: 0 + property real implicitHeight: 0 + + Behavior on opacity { + animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this) + } + + RadialGradient { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: root.rippleColor } + GradientStop { position: 0.3; color: root.rippleColor } + GradientStop { position: 0.5; color: Qt.rgba(root.rippleColor.r, root.rippleColor.g, root.rippleColor.b, 0) } + } + } + + transform: Translate { + x: -ripple.width / 2 + y: -ripple.height / 2 + } + } + } + + contentItem: StyledText { + text: root.buttonText + } +} diff --git a/config/quickshell/modules/common/widgets/RoundCorner.qml b/config/quickshell/modules/common/widgets/RoundCorner.qml new file mode 100644 index 00000000..c9a2827a --- /dev/null +++ b/config/quickshell/modules/common/widgets/RoundCorner.qml @@ -0,0 +1,64 @@ +import QtQuick 2.9 + +Item { + id: root + + property int size: 25 + property color color: "#000000" + + onColorChanged: { + canvas.requestPaint(); + } + + property QtObject cornerEnum: QtObject { + property int topLeft: 0 + property int topRight: 1 + property int bottomLeft: 2 + property int bottomRight: 3 + } + + property int corner: cornerEnum.topLeft // Default to TopLeft + + width: size + height: size + + Canvas { + id: canvas + + anchors.fill: parent + antialiasing: true + + onPaint: { + var ctx = getContext("2d"); + var r = root.size; + + ctx.beginPath(); + switch (root.corner) { + case cornerEnum.topLeft: + ctx.arc(r, r, r, Math.PI, 3 * Math.PI / 2); + ctx.lineTo(0, 0); + break; + case cornerEnum.topRight: + ctx.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI); + ctx.lineTo(r, 0); + break; + case cornerEnum.bottomLeft: + ctx.arc(r, 0, r, Math.PI / 2, Math.PI); + ctx.lineTo(0, r); + break; + case cornerEnum.bottomRight: + ctx.arc(0, 0, r, 0, Math.PI / 2); + ctx.lineTo(r, r); + break; + } + ctx.closePath(); + ctx.fillStyle = root.color; + ctx.fill(); + } + } + + Behavior on size { + animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) + } + +} diff --git a/config/quickshell/modules/common/widgets/StyledRectangularShadow.qml b/config/quickshell/modules/common/widgets/StyledRectangularShadow.qml new file mode 100644 index 00000000..6e1f2e16 --- /dev/null +++ b/config/quickshell/modules/common/widgets/StyledRectangularShadow.qml @@ -0,0 +1,13 @@ +import QtQuick +import QtQuick.Effects +import "root:/modules/common" + +RectangularShadow { + required property var target + anchors.fill: target + radius: target.radius + blur: 1.2 * Appearance.sizes.elevationMargin + spread: 1 + color: Appearance.colors.colShadow + cached: true +} diff --git a/config/quickshell/modules/common/widgets/StyledText.qml b/config/quickshell/modules/common/widgets/StyledText.qml new file mode 100644 index 00000000..988c136d --- /dev/null +++ b/config/quickshell/modules/common/widgets/StyledText.qml @@ -0,0 +1,13 @@ +import "root:/modules/common" +import QtQuick + +Text { + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + font { + hintingPreference: Font.PreferFullHinting + family: Appearance?.font.family.uiFont ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.textBase ?? 15 + } + color: Appearance?.m3colors.m3primaryText ?? "black" +} diff --git a/config/quickshell/modules/common/widgets/StyledTextArea.qml b/config/quickshell/modules/common/widgets/StyledTextArea.qml new file mode 100644 index 00000000..af3cf34d --- /dev/null +++ b/config/quickshell/modules/common/widgets/StyledTextArea.qml @@ -0,0 +1,15 @@ +import "root:/modules/common" +import QtQuick +import QtQuick.Controls + +TextArea { + renderType: Text.NativeRendering + selectedTextColor: Appearance.m3colors.m3selectionText + selectionColor: Appearance.m3colors.m3selectionBackground + placeholderTextColor: Appearance.m3colors.m3borderPrimary + font { + family: Appearance?.font.family.uiFont ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.textBase ?? 15 + hintingPreference: Font.PreferFullHinting + } +} diff --git a/config/quickshell/modules/common/widgets/StyledToolTip.qml b/config/quickshell/modules/common/widgets/StyledToolTip.qml new file mode 100644 index 00000000..aaaad813 --- /dev/null +++ b/config/quickshell/modules/common/widgets/StyledToolTip.qml @@ -0,0 +1,60 @@ +import "root:/modules/common" +import "root:/modules/common/widgets" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +ToolTip { + id: root + property string content + property bool extraVisibleCondition: true + property bool alternativeVisibleCondition: false + property bool internalVisibleCondition: { + const ans = (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition + return ans + } + verticalPadding: 5 + horizontalPadding: 10 + opacity: internalVisibleCondition ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) + } + + background: null + + contentItem: Item { + id: contentItemBackground + implicitWidth: tooltipTextObject.width + 2 * root.horizontalPadding + implicitHeight: tooltipTextObject.height + 2 * root.verticalPadding + + Rectangle { + id: backgroundRectangle + anchors.bottom: contentItemBackground.bottom + anchors.horizontalCenter: contentItemBackground.horizontalCenter + color: Appearance?.m3colors.colTooltip ?? "#3C4043" + radius: Appearance?.rounding.verysmall ?? 7 + width: internalVisibleCondition ? (tooltipTextObject.width + 2 * padding) : 0 + height: internalVisibleCondition ? (tooltipTextObject.height + 2 * padding) : 0 + clip: true + + Behavior on width { + animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) + } + Behavior on height { + animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) + } + + StyledText { + id: tooltipTextObject + anchors.centerIn: parent + text: content + font.pixelSize: Appearance?.font.pixelSize.textSmall ?? 14 + font.hintingPreference: Font.PreferNoHinting // Prevent shaky text + color: Appearance?.m3colors.colOnTooltip ?? "#FFFFFF" + wrapMode: Text.Wrap + } + } + } +}
\ No newline at end of file diff --git a/config/quickshell/modules/overview/Overview.qml b/config/quickshell/modules/overview/Overview.qml new file mode 100644 index 00000000..ef5a49c3 --- /dev/null +++ b/config/quickshell/modules/overview/Overview.qml @@ -0,0 +1,279 @@ +import "root:/" +import "root:/services" +import "root:/modules/common" +import "root:/modules/common/widgets" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +Scope { + id: overviewScope + property bool dontAutoCancelSearch: false + property bool searchEnabled: ConfigOptions.search.searchEnabled + + Variants { + id: overviewVariants + model: Quickshell.screens + PanelWindow { + id: root + required property var modelData + property string searchingText: "" + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor.id) + screen: modelData + visible: GlobalStates.overviewOpen + + WlrLayershell.namespace: "quickshell:overview" + WlrLayershell.layer: WlrLayer.Overlay + color: "transparent" + + mask: Region { + item: GlobalStates.overviewOpen ? columnLayout : null + } + HyprlandWindow.visibleMask: Region { + item: GlobalStates.overviewOpen ? columnLayout : null + } + + anchors { + top: true + left: true + right: true + bottom: true + } + + HyprlandFocusGrab { + id: grab + windows: [ root ] + property bool canBeActive: root.monitorIsFocused + active: false + onCleared: () => { + if (!active) GlobalStates.overviewOpen = false + } + } + + Connections { + target: GlobalStates + function onOverviewOpenChanged() { + if (!GlobalStates.overviewOpen) { + if (overviewScope.searchEnabled && searchWidget) { + searchWidget.disableExpandAnimation() + } + overviewScope.dontAutoCancelSearch = false; + } else { + if (!overviewScope.dontAutoCancelSearch && overviewScope.searchEnabled && searchWidget) { + searchWidget.cancelSearch() + } + delayedGrabTimer.start() + } + } + } + + Timer { + id: delayedGrabTimer + interval: ConfigOptions.hacks.arbitraryRaceConditionDelay + repeat: false + onTriggered: { + if (!grab.canBeActive) return + grab.active = GlobalStates.overviewOpen + } + } + + implicitWidth: columnLayout.implicitWidth + implicitHeight: columnLayout.implicitHeight + + function setSearchingText(text) { + if (overviewScope.searchEnabled && searchWidget) { + searchWidget.setSearchingText(text); + } + } + + ColumnLayout { + id: columnLayout + visible: GlobalStates.overviewOpen + anchors { + horizontalCenter: parent.horizontalCenter + top: ConfigOptions.overview.position === 0 ? parent.top : undefined + verticalCenter: ConfigOptions.overview.position === 1 ? parent.verticalCenter : undefined + bottom: ConfigOptions.overview.position === 2 ? parent.bottom : undefined + } + + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) { + GlobalStates.overviewOpen = false; + } + } + + Item { + height: 1 + width: 1 + } + + // Conditionally render SearchWidget - only exists when searchEnabled is true + SearchWidget { + id: searchWidget + Layout.alignment: Qt.AlignHCenter + visible: overviewScope.searchEnabled + height: overviewScope.searchEnabled ? implicitHeight : 0 + Layout.preferredHeight: overviewScope.searchEnabled ? implicitHeight : 0 + onSearchingTextChanged: (text) => { + root.searchingText = searchingText + } + } + + Item { + Layout.preferredHeight: overviewScope.searchEnabled ? 0 : 20 + Layout.fillWidth: true + visible: !overviewScope.searchEnabled + } + + Loader { + id: overviewLoader + active: GlobalStates.overviewOpen + sourceComponent: OverviewWidget { + panelWindow: root + // Show OverviewWidget when search is disabled OR when search text is empty + visible: !overviewScope.searchEnabled || (root.searchingText == "") + } + } + } + } + } + + IpcHandler { + target: "overview" + + function toggle() { + GlobalStates.overviewOpen = !GlobalStates.overviewOpen + } + function close() { + GlobalStates.overviewOpen = false + } + function open() { + GlobalStates.overviewOpen = true + } + function toggleReleaseInterrupt() { + GlobalStates.superReleaseMightTrigger = false + } + // Add function to control search + function toggleSearch() { + overviewScope.searchEnabled = !overviewScope.searchEnabled + } + function enableSearch() { + overviewScope.searchEnabled = true + } + function disableSearch() { + overviewScope.searchEnabled = false + } + } + + GlobalShortcut { + name: "overviewToggle" + description: qsTr("Toggles overview on press") + + onPressed: { + GlobalStates.overviewOpen = !GlobalStates.overviewOpen + } + } + + GlobalShortcut { + name: "overviewClose" + description: qsTr("Closes overview") + + onPressed: { + GlobalStates.overviewOpen = false + } + } + + GlobalShortcut { + name: "overviewToggleRelease" + description: qsTr("Toggles overview on release") + + onPressed: { + GlobalStates.superReleaseMightTrigger = true + } + + onReleased: { + if (!GlobalStates.superReleaseMightTrigger) { + GlobalStates.superReleaseMightTrigger = true + return + } + GlobalStates.overviewOpen = !GlobalStates.overviewOpen + } + } + + GlobalShortcut { + name: "overviewToggleReleaseInterrupt" + description: qsTr("Interrupts possibility of overview being toggled on release. ") + + qsTr("This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. ") + + qsTr("To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything.") + + onPressed: { + GlobalStates.superReleaseMightTrigger = false + } + } + + // Only enable clipboard/emoji shortcuts when search is enabled + GlobalShortcut { + name: "overviewClipboardToggle" + description: qsTr("Toggle clipboard query on overview widget") + + onPressed: { + if (!overviewScope.searchEnabled) return; // Skip if search disabled + + if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) { + GlobalStates.overviewOpen = false; + return; + } + for (let i = 0; i < overviewVariants.instances.length; i++) { + let panelWindow = overviewVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + overviewScope.dontAutoCancelSearch = true; + panelWindow.setSearchingText( + ConfigOptions.search.prefix.clipboard + ); + GlobalStates.overviewOpen = true; + return + } + } + } + } + + GlobalShortcut { + name: "overviewEmojiToggle" + description: qsTr("Toggle emoji query on overview widget") + + onPressed: { + if (!overviewScope.searchEnabled) return; // Skip if search disabled + + if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) { + GlobalStates.overviewOpen = false; + return; + } + for (let i = 0; i < overviewVariants.instances.length; i++) { + let panelWindow = overviewVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + overviewScope.dontAutoCancelSearch = true; + panelWindow.setSearchingText( + ConfigOptions.search.prefix.emojis + ); + GlobalStates.overviewOpen = true; + return + } + } + } + } + + // Optional: Add shortcut to toggle search functionality + GlobalShortcut { + name: "overviewToggleSearch" + description: qsTr("Toggle search functionality in overview") + + onPressed: { + overviewScope.searchEnabled = !overviewScope.searchEnabled + } + } +}
\ No newline at end of file diff --git a/config/quickshell/modules/overview/OverviewWidget.qml b/config/quickshell/modules/overview/OverviewWidget.qml new file mode 100644 index 00000000..2ea8d58a --- /dev/null +++ b/config/quickshell/modules/overview/OverviewWidget.qml @@ -0,0 +1,343 @@ +import "root:/" +import "root:/services/" +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/color_utils.js" as ColorUtils +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Hyprland + +Item { + id: root + required property var panelWindow + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen) + readonly property var toplevels: ToplevelManager.toplevels + readonly property int workspacesShown: ConfigOptions.overview.numOfRows * ConfigOptions.overview.numOfCols + readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor.id) + property var windows: HyprlandData.windowList + property var windowByAddress: HyprlandData.windowByAddress + property var windowAddresses: HyprlandData.addresses + property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor.id) + property real scale: ConfigOptions.overview.scale + property color activeBorderColor: Appearance.m3colors.m3accentSecondary + + property real workspaceImplicitWidth: Math.max(100, (monitorData?.transform % 2 === 1) ? + ((monitor.height - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale) : + ((monitor.width - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale)) + property real workspaceImplicitHeight: Math.max(60, (monitorData?.transform % 2 === 1) ? + ((monitor.width - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale) : + ((monitor.height - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale)) + + property real workspaceNumberMargin: 80 + property real workspaceNumberSize: (ConfigOptions.overview.workspaceNumberSize > 0) + ? ConfigOptions.overview.workspaceNumberSize + : Math.min(workspaceImplicitHeight, workspaceImplicitWidth) * monitor.scale + property int workspaceZ: 0 + property int windowZ: 1 + property int windowDraggingZ: 99999 + property real workspaceSpacing: 5 + + property int draggingFromWorkspace: -1 + property int draggingTargetWorkspace: -1 + + implicitWidth: overviewBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 + implicitHeight: overviewBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 + + property Component windowComponent: OverviewWindow {} + property list<OverviewWindow> windowWidgets: [] + + // Shared wallpaper image - loaded once and reused + Image { + id: sharedWallpaper + source: Appearance.background_image || "" + visible: false // Hidden as it's only used as a source + cache: true + asynchronous: true + smooth: true + opacity: Appearance.workpaceTransparency // Adds slight transparency (0.0 = fully transparent, 1.0 = fully opaque) + } + + StyledRectangularShadow { + target: overviewBackground + } + Rectangle { // Background + id: overviewBackground + property real padding: 10 + anchors.fill: parent + anchors.margins: Appearance.sizes.elevationMargin + border.color : ColorUtils.transparentize(Appearance.m3colors.m3borderPrimary, 0.2) + border.width : 2 + + implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2 + implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2 + radius: Appearance.rounding.screenRounding * root.scale + padding + color: Appearance.colors.colLayer0 + + + ColumnLayout { // Workspaces + id: workspaceColumnLayout + + z: root.workspaceZ + anchors.centerIn: parent + spacing: workspaceSpacing + Repeater { + model: ConfigOptions.overview.numOfRows + delegate: RowLayout { + id: row + property int rowIndex: index + spacing: workspaceSpacing + + Repeater { // Workspace repeater + model: ConfigOptions.overview.numOfCols + Rectangle { // Workspace + id: workspace + property int colIndex: index + property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * ConfigOptions.overview.numOfCols + colIndex + 1 + property color defaultWorkspaceColor: Appearance.colors.colLayer1 + property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1) + property color hoveredBorderColor: Appearance.colors.colLayer2Hover + property bool hoveredWhileDragging: false + readonly property int padding: ConfigOptions.overview.windowPadding + + Layout.preferredWidth: root.workspaceImplicitWidth + Layout.preferredHeight: root.workspaceImplicitHeight + Layout.minimumWidth: 100 + Layout.minimumHeight: 60 + + width: root.workspaceImplicitWidth + height: root.workspaceImplicitHeight + color: "transparent" + radius: Appearance.rounding.screenRounding * root.scale + clip: true + opacity: Appearance.workpaceTransparency // Adds slight transparency (0.0 = fully transparent, 1.0 = fully opaque) + + + // Efficient wallpaper using ShaderEffectSource + Rectangle { + id: wallpaperContainer + anchors.fill: parent + anchors.margins: 2 // Leave space for border + radius: workspace.radius - 2 + color: workspace.defaultWorkspaceColor // Fallback color + clip: true + + ShaderEffectSource { + id: wallpaperSource + anchors.fill: parent + sourceItem: sharedWallpaper + visible: sharedWallpaper.status === Image.Ready + smooth: true + + // Scale to fill while preserving aspect ratio + transform: Scale { + property real aspectRatio: sharedWallpaper.implicitWidth / Math.max(1, sharedWallpaper.implicitHeight) + property real containerRatio: wallpaperContainer.width / Math.max(1, wallpaperContainer.height) + + xScale: aspectRatio > containerRatio ? + wallpaperContainer.height * aspectRatio / wallpaperContainer.width : 1 + yScale: aspectRatio > containerRatio ? + 1 : wallpaperContainer.width / (wallpaperContainer.height * aspectRatio) + + origin.x: wallpaperContainer.width / 2 + origin.y: wallpaperContainer.height / 2 + } + } + + // Fallback when image fails to load or isn't ready + Rectangle { + anchors.fill: parent + color: workspace.defaultWorkspaceColor + visible: sharedWallpaper.status !== Image.Ready + } + + // Optional: Add overlay for better text readability and hover effects + Rectangle { + anchors.fill: parent + color: hoveredWhileDragging ? hoveredWorkspaceColor : "black" + opacity: hoveredWhileDragging ? 0.3 : 0.1 + } + } + + // Border overlay - on top of wallpaper + Rectangle { + anchors.fill: parent + color: "transparent" + radius: parent.radius + border.width: 1 + border.color: hoveredWhileDragging ? hoveredBorderColor : ColorUtils.transparentize(Appearance.m3colors.m3borderPrimary, 0.6) + z: 10 // Ensure it's on top + } + + StyledText { + // Position in top-left corner with padding + anchors.top: parent.top + anchors.left: parent.left + anchors.topMargin: 12 // Padding from top edge + anchors.leftMargin: 12 // Padding from left edge + + text: workspaceValue + font.pixelSize: root.workspaceNumberSize * root.scale + font.weight: Font.DemiBold + color: ColorUtils.transparentize(Appearance.colors.colOnLayer1, 0.8) + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignTop + z: 15 // Above border + } + + MouseArea { + id: workspaceArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + z: 20 // Above all visual elements + onClicked: { + if (root.draggingTargetWorkspace === -1) { + GlobalStates.overviewOpen = false + Hyprland.dispatch(`workspace ${workspaceValue}`) + } + } + } + + DropArea { + anchors.fill: parent + z: 20 // Same level as MouseArea + onEntered: { + root.draggingTargetWorkspace = workspaceValue + if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return; + hoveredWhileDragging = true + } + onExited: { + hoveredWhileDragging = false + if (root.draggingTargetWorkspace == workspaceValue) root.draggingTargetWorkspace = -1 + } + } + + } + } + } + } + } + + Item { // Windows & focused workspace indicator + id: windowSpace + anchors.centerIn: parent + implicitWidth: workspaceColumnLayout.implicitWidth + implicitHeight: workspaceColumnLayout.implicitHeight + + Repeater { // Window repeater + model: ScriptModel { + values: windowAddresses.filter((address) => { + var win = windowByAddress[address] + return (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown) + }) + } + delegate: OverviewWindow { + id: window + windowData: windowByAddress[modelData] + monitorData: root.monitorData + scale: root.scale + availableWorkspaceWidth: root.workspaceImplicitWidth + availableWorkspaceHeight: root.workspaceImplicitHeight + + property bool atInitPosition: (initX == x && initY == y) + restrictToWorkspace: Drag.active || atInitPosition + + property int workspaceColIndex: (windowData?.workspace.id - 1) % ConfigOptions.overview.numOfCols + property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / ConfigOptions.overview.numOfCols) + xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex + yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex + + Timer { + id: updateWindowPosition + interval: ConfigOptions.hacks.arbitraryRaceConditionDelay + repeat: false + running: false + onTriggered: { + window.x = Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset + window.y = Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset + } + } + + z: atInitPosition ? root.windowZ : root.windowDraggingZ + Drag.hotSpot.x: targetWindowWidth / 2 + Drag.hotSpot.y: targetWindowHeight / 2 + MouseArea { + id: dragArea + anchors.fill: parent + hoverEnabled: true + onEntered: hovered = true + onExited: hovered = false + acceptedButtons: Qt.LeftButton | Qt.MiddleButton + drag.target: parent + onPressed: { + root.draggingFromWorkspace = windowData?.workspace.id + window.pressed = true + window.Drag.active = true + window.Drag.source = window + } + onReleased: { + const targetWorkspace = root.draggingTargetWorkspace + window.pressed = false + window.Drag.active = false + root.draggingFromWorkspace = -1 + if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) { + Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${window.windowData?.address}`) + updateWindowPosition.restart() + } + else { + window.x = window.initX + window.y = window.initY + } + } + onClicked: (event) => { + if (!windowData) return; + + if (event.button === Qt.LeftButton) { + GlobalStates.overviewOpen = false + Hyprland.dispatch(`focuswindow address:${windowData.address}`) + event.accepted = true + } else if (event.button === Qt.MiddleButton) { + Hyprland.dispatch(`closewindow address:${windowData.address}`) + event.accepted = true + } + } + + StyledToolTip { + extraVisibleCondition: false + alternativeVisibleCondition: dragArea.containsMouse && !window.Drag.active + content: `${windowData.title}\n[${windowData.class}] ${windowData.xwayland ? "[XWayland] " : ""}\n` + } + } + } + } + + Rectangle { // Focused workspace indicator + id: focusedWorkspaceIndicator + property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown) + property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / ConfigOptions.overview.numOfCols) + property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % ConfigOptions.overview.numOfCols + x: (root.workspaceImplicitWidth + workspaceSpacing) * activeWorkspaceColIndex + y: (root.workspaceImplicitHeight + workspaceSpacing) * activeWorkspaceRowIndex + z: root.windowZ + width: Math.max(100, root.workspaceImplicitWidth) + height: Math.max(60, root.workspaceImplicitHeight) + color: "transparent" + radius: Appearance.rounding.screenRounding * root.scale + border.width: 2 + border.color: root.activeBorderColor + visible: width > 0 && height > 0 && activeWorkspaceInGroup > 0 + Behavior on x { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + Behavior on y { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + } + } + } +}
\ No newline at end of file diff --git a/config/quickshell/modules/overview/OverviewWindow.qml b/config/quickshell/modules/overview/OverviewWindow.qml new file mode 100644 index 00000000..449a98c4 --- /dev/null +++ b/config/quickshell/modules/overview/OverviewWindow.qml @@ -0,0 +1,94 @@ +import "root:/services/" +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/color_utils.js" as ColorUtils +import Qt5Compat.GraphicalEffects +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Io +import Quickshell.Hyprland + +Rectangle { // Window + id: root + property var windowData + property var monitorData + property var scale + property var availableWorkspaceWidth + property var availableWorkspaceHeight + property bool restrictToWorkspace: true + property real initX: Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset + property real initY: Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset + property real xOffset: 0 + property real yOffset: 0 + + property var targetWindowWidth: windowData?.size[0] * scale + property var targetWindowHeight: windowData?.size[1] * scale + property bool hovered: false + property bool pressed: false + + property var iconToWindowRatio: 0.35 + property var xwaylandIndicatorToIconRatio: 0.35 + property var iconToWindowRatioCompact: 0.6 + property var iconPath: Quickshell.iconPath(AppSearch.guessIcon(windowData?.class), "image-missing") + property bool compactMode: Appearance.font.pixelSize.textSmall * 4 > targetWindowHeight || Appearance.font.pixelSize.textSmall * 4 > targetWindowWidth + + property bool indicateXWayland: (ConfigOptions.overview.showXwaylandIndicator && windowData?.xwayland) ?? false + + x: initX + y: initY + width: Math.min(windowData?.size[0] * root.scale, (restrictToWorkspace ? windowData?.size[0] : availableWorkspaceWidth - x + xOffset)) + height: Math.min(windowData?.size[1] * root.scale, (restrictToWorkspace ? windowData?.size[1] : availableWorkspaceHeight - y + yOffset)) + + radius: Appearance.rounding.windowRounding * root.scale + color: pressed ? Appearance.colors.colLayer2Active : hovered ? Appearance.colors.colLayer2Hover : Appearance.colors.colLayer2 + // border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.9) + border.color : ColorUtils.transparentize(Appearance.m3colors.m3borderPrimary, 0.4) + border.pixelAligned : false + border.width : 2 + + Behavior on x { + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) + } + Behavior on y { + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) + } + Behavior on width { + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) + } + Behavior on height { + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) + } + + ColumnLayout { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.font.pixelSize.textSmall * 0.5 + + IconImage { + id: windowIcon + Layout.alignment: Qt.AlignHCenter + source: root.iconPath + implicitSize: Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) + + Behavior on implicitSize { + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) + } + } + + StyledText { + Layout.leftMargin: 10 + Layout.rightMargin: 10 + visible: !compactMode + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Appearance.font.pixelSize.textSmall + font.italic: indicateXWayland ? true : false + elide: Text.ElideRight + text: windowData?.title ?? "" + } + } +}
\ No newline at end of file diff --git a/config/quickshell/modules/overview/SearchItem.qml b/config/quickshell/modules/overview/SearchItem.qml new file mode 100644 index 00000000..1357d03c --- /dev/null +++ b/config/quickshell/modules/overview/SearchItem.qml @@ -0,0 +1,220 @@ +// pragma NativeMethodBehavior: AcceptThisObject +import "root:/" +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/color_utils.js" as ColorUtils +import "root:/modules/common/functions/string_utils.js" as StringUtils +import "root:/modules/common/functions/fuzzysort.js" as Fuzzy +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import Quickshell.Hyprland + +RippleButton { + id: root + property var entry + property string query + 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 ?? "uiFont" + property string itemClickActionName: entry?.clickActionName + property string bigText: entry?.bigText ?? "" + property string materialSymbol: entry?.materialSymbol ?? "" + property string cliphistRawString: entry?.cliphistRawString ?? "" + + property string highlightPrefix: `<u><font color="${Appearance.colors.colPrimary}">` + property string highlightSuffix: `</font></u>` + function highlightContent(content, query) { + if (!query || query.length === 0 || content == query || fontType === "codeFont") + return StringUtils.escapeHtml(content); + + let contentLower = content.toLowerCase(); + let queryLower = query.toLowerCase(); + + let result = ""; + let lastIndex = 0; + let qIndex = 0; + + for (let i = 0; i < content.length && qIndex < query.length; i++) { + if (contentLower[i] === queryLower[qIndex]) { + // Add non-highlighted part (escaped) + if (i > lastIndex) + result += StringUtils.escapeHtml(content.slice(lastIndex, i)); + // Add highlighted character (escaped) + result += root.highlightPrefix + StringUtils.escapeHtml(content[i]) + root.highlightSuffix; + lastIndex = i + 1; + qIndex++; + } + } + // Add the rest of the string (escaped) + if (lastIndex < content.length) + result += StringUtils.escapeHtml(content.slice(lastIndex)); + + return result; + } + property string displayContent: highlightContent(root.itemName, root.query) + + property list<string> urls: { + if (!root.itemName) return []; + // Regular expression to match URLs + const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi; + const matches = root.itemName?.match(urlRegex) + ?.filter(url => !url.includes("…")) // Elided = invalid + return matches ? matches : []; + } + + visible: root.entryShown + property int horizontalMargin: 10 + property int buttonHorizontalPadding: 10 + property int buttonVerticalPadding: 5 + property bool keyboardDown: false + + implicitHeight: rowLayout.implicitHeight + root.buttonVerticalPadding * 2 + implicitWidth: rowLayout.implicitWidth + root.buttonHorizontalPadding * 2 + buttonRadius: Appearance.rounding.normal + colBackground: (root.down || root.keyboardDown) ? Appearance.colors.colLayer1Active : + ((root.hovered || root.focus) ? Appearance.colors.colLayer1Hover : + ColorUtils.transparentize(Appearance.m3colors.m3layerBackground3, 1)) + colBackgroundHover: Appearance.colors.colLayer1Hover + colRipple: Appearance.colors.colLayer1Active + + background { + anchors.fill: root + anchors.leftMargin: root.horizontalMargin + anchors.rightMargin: root.horizontalMargin + } + + PointingHandInteraction {} + onClicked: { + root.itemExecute() + Hyprland.dispatch("global quickshell:overviewClose") + } + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + root.keyboardDown = true + root.clicked() + event.accepted = true; + } + } + Keys.onReleased: (event) => { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + root.keyboardDown = false + event.accepted = true; + } + } + + RowLayout { + id: rowLayout + spacing: iconLoader.sourceComponent === null ? 0 : 10 + anchors.fill: parent + anchors.leftMargin: root.horizontalMargin + root.buttonHorizontalPadding + anchors.rightMargin: root.horizontalMargin + root.buttonHorizontalPadding + + // Icon + Loader { + id: iconLoader + active: true + sourceComponent: root.materialSymbol !== "" ? materialSymbolComponent : + root.bigText ? bigTextComponent : + root.itemIcon !== "" ? iconImageComponent : + null + } + + Component { + id: iconImageComponent + IconImage { + source: Quickshell.iconPath(root.itemIcon, "image-missing") + width: 35 + height: 35 + } + } + + Component { + id: materialSymbolComponent + MaterialSymbol { + text: root.materialSymbol + iconSize: 30 + color: Appearance.m3colors.m3surfaceText + } + } + + Component { + id: bigTextComponent + StyledText { + text: root.bigText + font.pixelSize: Appearance.font.pixelSize.textLarge + color: Appearance.m3colors.m3surfaceText + } + } + + // Main text + ColumnLayout { + id: contentColumn + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 0 + StyledText { + font.pixelSize: Appearance.font.pixelSize.textSmall + color: Appearance.colors.colSubtext + visible: root.itemType && root.itemType != qsTr("App") + text: root.itemType + } + RowLayout { + Loader { // Checkmark for copied clipboard entry + visible: itemName == Quickshell.clipboardText && root.cliphistRawString + active: itemName == Quickshell.clipboardText && root.cliphistRawString + sourceComponent: Rectangle { + implicitWidth: activeText.implicitHeight + implicitHeight: activeText.implicitHeight + radius: Appearance.rounding.full + color: Appearance.colors.colPrimary + MaterialSymbol { + id: activeText + anchors.centerIn: parent + text: "check" + font.pixelSize: Appearance.font.pixelSize.textMedium + color: Appearance.m3colors.m3accentPrimaryText + } + } + } + StyledText { // Item name/content + Layout.fillWidth: true + id: nameText + textFormat: Text.StyledText // RichText also works, but StyledText ensures elide work + font.pixelSize: Appearance.font.pixelSize.textBase + font.family: Appearance.font.family[root.fontType] + color: Appearance.m3colors.m3surfaceText + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + text: `${root.displayContent}` + } + } + Loader { // Clipboard image preview + active: root.cliphistRawString && /^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(root.cliphistRawString) + sourceComponent: CliphistImage { + Layout.fillWidth: true + entry: root.cliphistRawString + maxWidth: contentColumn.width + maxHeight: 140 + } + } + } + + // Action text + StyledText { + Layout.fillWidth: false + visible: (root.hovered || root.focus) + id: clickAction + font.pixelSize: Appearance.font.pixelSize.textMedium + color: Appearance.colors.colSubtext + horizontalAlignment: Text.AlignRight + text: root.itemClickActionName + } + } +} diff --git a/config/quickshell/modules/overview/SearchWidget.qml b/config/quickshell/modules/overview/SearchWidget.qml new file mode 100644 index 00000000..f84aa558 --- /dev/null +++ b/config/quickshell/modules/overview/SearchWidget.qml @@ -0,0 +1,425 @@ +import "root:/" +import "root:/services/" +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/string_utils.js" as StringUtils +import Qt5Compat.GraphicalEffects +import Qt.labs.platform +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland + +Item { // Wrapper + id: root + readonly property string xdgConfigHome: Directories.config + property string searchingText: "" + property bool showResults: searchingText != "" + property real searchBarHeight: searchBar.height + Appearance.sizes.elevationMargin * 2 + implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2 + implicitHeight: searchWidgetContent.implicitHeight + Appearance.sizes.elevationMargin * 2 + + property string mathResult: "" + + function disableExpandAnimation() { + searchWidthBehavior.enabled = false; + } + + function cancelSearch() { + searchInput.selectAll() + root.searchingText = "" + searchWidthBehavior.enabled = true; + } + + function setSearchingText(text) { + searchInput.text = text; + root.searchingText = text; + } + + property var searchActions: [ + { + action: "img", + execute: () => { + executor.executeCommand(Directories.wallpaperSwitchScriptPath) + } + }, + { + action: "dark", + execute: () => { + executor.executeCommand(`${Directories.wallpaperSwitchScriptPath} --mode dark --noswitch`) + } + }, + { + action: "light", + execute: () => { + executor.executeCommand(`${Directories.wallpaperSwitchScriptPath} --mode light --noswitch`) + } + }, + { + action: "accentcolor", + execute: (args) => { + executor.executeCommand( + `${Directories.wallpaperSwitchScriptPath} --noswitch --color ${args != '' ? ("'"+args+"'") : ""}` + ) + } + }, + { + action: "todo", + execute: (args) => { + Todo.addTask(args) + } + }, + ] + + function focusFirstItemIfNeeded() { + if (searchInput.focus) appResults.currentIndex = 0; // Focus the first item + } + + 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 + root.focusFirstItemIfNeeded() + } + } + } + + Process { + id: executor + property list<string> baseCommand: ["bash", "-c"] + function executeCommand(command) { + executor.command = baseCommand.concat( + `${command} || ${ConfigOptions.apps.terminal} fish -C 'echo "${qsTr("Searching for package with that command")}..." && pacman -F ${command}'` + ) + executor.startDetached() + } + } + + 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); + searchInput.cursorPosition += 1; + event.accepted = true; + } + } + } + + StyledRectangularShadow { + target: searchWidgetContent + } + Rectangle { // Background + id: searchWidgetContent + anchors.centerIn: parent + implicitWidth: columnLayout.implicitWidth + implicitHeight: columnLayout.implicitHeight + radius: Appearance.rounding.large + color: Appearance.colors.colLayer0 + + ColumnLayout { + id: columnLayout + anchors.centerIn: parent + spacing: 0 + + // clip: true + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: searchWidgetContent.width + height: searchWidgetContent.width + radius: searchWidgetContent.radius + } + } + + RowLayout { + id: searchBar + spacing: 5 + MaterialSymbol { + id: searchIcon + Layout.leftMargin: 15 + iconSize: Appearance.font.pixelSize.iconLarge + color: Appearance.m3colors.m3surfaceText + text: root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard) ? 'content_paste_search' : '' + } + TextField { // Search box + id: searchInput + + focus: GlobalStates.overviewOpen + Layout.rightMargin: 15 + padding: 15 + renderType: Text.NativeRendering + font { + family: Appearance?.font.family.uiFont ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.textBase ?? 15 + hintingPreference: Font.PreferFullHinting + } + color: activeFocus ? Appearance.m3colors.m3surfaceText : Appearance.m3colors.m3secondaryText + selectedTextColor: Appearance.m3colors.m3selectionText + selectionColor: Appearance.m3colors.m3selectionBackground + placeholderText: qsTr("Search, calculate or run") + placeholderTextColor: Appearance.m3colors.m3borderPrimary + implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth + + Behavior on implicitWidth { + id: searchWidthBehavior + enabled: false + NumberAnimation { + duration: 300 + easing.type: Appearance.animation.elementMove.type + easing.bezierCurve: Appearance.animation.elementMove.bezierCurve + } + } + + onTextChanged: root.searchingText = text + + onAccepted: { + if (appResults.count > 0) { + // Get the first visible delegate and trigger its click + let firstItem = appResults.itemAtIndex(0); + if (firstItem && firstItem.clicked) { + firstItem.clicked(); + } + } + } + + background: null + + cursorDelegate: Rectangle { + width: 1 + color: searchInput.activeFocus ? Appearance.colors.colPrimary : "transparent" + radius: 1 + } + } + } + + Rectangle { // Separator + visible: root.showResults + Layout.fillWidth: true + height: 1 + color: Appearance.m3colors.m3borderSecondary + } + + 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; + } + } + + model: ScriptModel { + id: model + values: { // Search results are handled here + ////////////////// Skip? ////////////////// + if(root.searchingText == "") return []; + + ///////////// Special cases /////////////// + if (root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard)) { // Clipboard + const searchString = root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length); + return Cliphist.fuzzyQuery(searchString).map(entry => { + return { + cliphistRawString: entry, + name: entry.replace(/^\s*\S+\s+/, ""), + clickActionName: "", + type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`, + execute: () => { + Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(entry)}' | cliphist decode | wl-copy`); + } + }; + }).filter(Boolean); + } + if (root.searchingText.startsWith(ConfigOptions.search.prefix.emojis)) { // Clipboard + const searchString = root.searchingText.slice(ConfigOptions.search.prefix.emojis.length); + return Emojis.fuzzyQuery(searchString).map(entry => { + return { + cliphistRawString: entry, + bigText: entry.match(/^\s*(\S+)/)?.[1] || "", + name: entry.replace(/^\s*\S+\s+/, ""), + clickActionName: "", + type: "Emoji", + execute: () => { + Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(entry.match(/^\s*(\S+)/)?.[1])}'`); + } + }; + }).filter(Boolean); + } + + + ////////////////// Init /////////////////// + nonAppResultsTimer.restart(); + const mathResultObject = { + name: root.mathResult, + clickActionName: qsTr("Copy"), + type: qsTr("Math result"), + fontType: "monospace", + materialSymbol: 'calculate', + execute: () => { + Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(root.mathResult)}'`) + } + } + const commandResultObject = { + name: searchingText.replace("file://", ""), + clickActionName: qsTr("Run"), + type: qsTr("Run command"), + fontType: "monospace", + materialSymbol: 'terminal', + execute: () => { + executor.executeCommand(searchingText.startsWith('sudo') ? `${ConfigOptions.apps.terminal} fish -C '${root.searchingText.replace("file://", "")}'` : root.searchingText.replace("file://", "")); + } + } + const launcherActionObjects = root.searchActions + .map(action => { + const actionString = `${ConfigOptions.search.prefix.action}${action.action}`; + if (actionString.startsWith(root.searchingText) || root.searchingText.startsWith(actionString)) { + return { + name: root.searchingText.startsWith(actionString) ? root.searchingText : actionString, + clickActionName: qsTr("Run"), + type: qsTr("Action"), + materialSymbol: 'settings_suggest', + execute: () => { + action.execute(root.searchingText.split(" ").slice(1).join(" ")) + }, + }; + } + return null; + }) + .filter(Boolean); + + let result = []; + + //////////////// Apps ////////////////// + result = result.concat( + AppSearch.fuzzyQuery(root.searchingText) + .map((entry) => { + entry.clickActionName = qsTr("Launch"); + entry.type = qsTr("App"); + return entry; + }) + ); + + ////////// Launcher actions //////////// + result = result.concat(launcherActionObjects); + + /////////// Math result & command ////////// + const startsWithNumber = /^\d/.test(root.searchingText); + if (startsWithNumber) { + result.push(mathResultObject); + result.push(commandResultObject); + } else { + result.push(commandResultObject); + result.push(mathResultObject); + } + + ///////////////// Web search //////////////// + result.push({ + name: root.searchingText, + clickActionName: qsTr("Search"), + type: qsTr("Search the web"), + materialSymbol: 'travel_explore', + execute: () => { + let url = ConfigOptions.search.engineBaseUrl + root.searchingText + for (let site of ConfigOptions.search.excludedSites) { + url += ` -site:${site}`; + } + Qt.openUrlExternally(url); + } + }); + + return result; + } + } + + delegate: SearchItem { // The selectable item for each search result + required property var modelData + anchors.left: parent?.left + anchors.right: parent?.right + entry: modelData + query: root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard) ? + root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length) : + root.searchingText; + } + } + + } + } +}
\ No newline at end of file diff --git a/config/quickshell/qml_color.json b/config/quickshell/qml_color.json new file mode 100644 index 00000000..888ba3e6 --- /dev/null +++ b/config/quickshell/qml_color.json @@ -0,0 +1,17 @@ +{ + "windowBackground": "#0f0f15", + "primaryText": "#bac2de", + "layerBackground1": "#1F1A1F", + "layerBackground2": "#231E23", + "layerBackground3": "#2D282E", + "surfaceText": "#EAE0E7", + "secondaryText": "#CFC3CD", + "borderPrimary": "#cba6f7", + "shadowColor": "#000000", + "accentPrimary": "#6750A4", + "accentSecondary": "#D5C0D7", + "selectionBackground": "#534457", + "accentPrimaryText": "#FFFFFF", + "selectionText": "#F2DCF3", + "borderSecondary": "#4C444D" +}
\ No newline at end of file diff --git a/config/quickshell/services/AppSearch.qml b/config/quickshell/services/AppSearch.qml new file mode 100644 index 00000000..876df183 --- /dev/null +++ b/config/quickshell/services/AppSearch.qml @@ -0,0 +1,116 @@ +pragma Singleton + +import "root:/modules/common" +import "root:/modules/common/functions/fuzzysort.js" as Fuzzy +import "root:/modules/common/functions/levendist.js" as Levendist +import Quickshell +import Quickshell.Io + +/** + * - Eases fuzzy searching for applications by name + * - Guesses icon name for window class name + */ +Singleton { + id: root + property bool sloppySearch: ConfigOptions?.search.sloppy ?? false + property real scoreThreshold: 0.2 + property var substitutions: ({ + "code-url-handler": "visual-studio-code", + "Code": "visual-studio-code", + "gnome-tweaks": "org.gnome.tweaks", + "pavucontrol-qt": "pavucontrol", + "wps": "wps-office2019-kprometheus", + "wpsoffice": "wps-office2019-kprometheus", + "footclient": "foot", + "zen": "zen-browser", + }) + property var regexSubstitutions: [ + { + "regex": /^steam_app_(\\d+)$/, + "replace": "steam_icon_$1" + }, + { + "regex": /Minecraft.*/, + "replace": "minecraft" + }, + { + "regex": /.*polkit.*/, + "replace": "system-lock-screen" + }, + { + "regex": /gcr.prompter/, + "replace": "system-lock-screen" + } + ] + + readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values) + .sort((a, b) => a.name.localeCompare(b.name)) + + readonly property var preppedNames: list.map(a => ({ + name: Fuzzy.prepare(`${a.name} `), + entry: a + })) + + function fuzzyQuery(search: string): var { // Idk why list<DesktopEntry> doesn't work + if (root.sloppySearch) { + const results = list.map(obj => ({ + entry: obj, + score: Levendist.computeScore(obj.name.toLowerCase(), search.toLowerCase()) + })).filter(item => item.score > root.scoreThreshold) + .sort((a, b) => b.score - a.score) + return results + .map(item => item.entry) + } + + return Fuzzy.go(search, preppedNames, { + all: true, + key: "name" + }).map(r => { + return r.obj.entry + }); + } + + function iconExists(iconName) { + return (Quickshell.iconPath(iconName, true).length > 0) + && !iconName.includes("image-missing"); + } + + function guessIcon(str) { + if (!str || str.length == 0) return "image-missing"; + + // Normal substitutions + if (substitutions[str]) + return substitutions[str]; + + // Regex substitutions + for (let i = 0; i < regexSubstitutions.length; i++) { + const substitution = regexSubstitutions[i]; + const replacedName = str.replace( + substitution.regex, + substitution.replace, + ); + if (replacedName != str) return replacedName; + } + + // If it gets detected normally, no need to guess + if (iconExists(str)) return str; + + let guessStr = str; + // Guess: Take only app name of reverse domain name notation + guessStr = str.split('.').slice(-1)[0].toLowerCase(); + if (iconExists(guessStr)) return guessStr; + // Guess: normalize to kebab case + guessStr = str.toLowerCase().replace(/\s+/g, "-"); + if (iconExists(guessStr)) return guessStr; + // Guess: First fuzze desktop entry match + const searchResults = root.fuzzyQuery(str); + if (searchResults.length > 0) { + const firstEntry = searchResults[0]; + guessStr = firstEntry.icon + if (iconExists(guessStr)) return guessStr; + } + + // Give up + return str; + } +} diff --git a/config/quickshell/services/ConfigLoader.qml b/config/quickshell/services/ConfigLoader.qml new file mode 100644 index 00000000..d3fb4e26 --- /dev/null +++ b/config/quickshell/services/ConfigLoader.qml @@ -0,0 +1,146 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import "root:/modules/common" +import "root:/modules/common/functions/file_utils.js" as FileUtils +import "root:/modules/common/functions/string_utils.js" as StringUtils +import "root:/modules/common/functions/object_utils.js" as ObjectUtils +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland +import Qt.labs.platform + +/** + * Loads and manages the shell configuration file. + * The config file is by default at XDG_CONFIG_HOME/quickshell/config.json. + * Automatically reloaded when the file changes, but does not provide a way to save changes. + */ +Singleton { + id: root + property string filePath: Directories.shellConfigPath + property bool firstLoad: true + + function loadConfig() { + configFileView.reload() + } + + function applyConfig(fileContent) { + try { + const json = JSON.parse(fileContent); + + // Extract font configuration if it exists + let fontConfig = null; + let configForOptions = {}; + + // Copy all properties except font to configForOptions + for (let key in json) { + if (key !== "font") { + configForOptions[key] = json[key]; + } else { + fontConfig = json[key]; + } + } + + // Apply the non-font configuration to ConfigOptions + ObjectUtils.applyToQtObject(ConfigOptions, configForOptions); + + // Apply font configuration to Appearance if it exists + if (fontConfig && typeof Appearance !== 'undefined') { + if (fontConfig.family && Appearance.font && Appearance.font.family) { + ObjectUtils.applyToQtObject(Appearance.font.family, fontConfig.family); + } + if (fontConfig.pixelSize && Appearance.font && Appearance.font.pixelSize) { + ObjectUtils.applyToQtObject(Appearance.font.pixelSize, fontConfig.pixelSize); + } + } + + if (root.firstLoad) { + root.firstLoad = false; + } else { + Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration reloaded")}" "${root.filePath}"`) + } + } catch (e) { + console.error("[ConfigLoader] Error reading file:", e); + Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration failed to load")}" "${root.filePath}"`) + return; + } + } + + function setLiveConfigValue(nestedKey, value) { + let keys = nestedKey.split("."); + let targetObject = ConfigOptions; + + // Check if this is a font-related configuration + if (keys[0] === "font") { + targetObject = Appearance; + } + + let obj = targetObject; + let parents = [obj]; + + // Traverse and collect parent objects + for (let i = 0; i < keys.length - 1; ++i) { + if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") { + obj[keys[i]] = {}; + } + obj = obj[keys[i]]; + parents.push(obj); + } + + // Convert value to correct type using JSON.parse when safe + let convertedValue = value; + if (typeof value === "string") { + let trimmed = value.trim(); + if (trimmed === "true" || trimmed === "false" || !isNaN(Number(trimmed))) { + try { + convertedValue = JSON.parse(trimmed); + } catch (e) { + convertedValue = value; + } + } + } + + console.log(`[ConfigLoader] Setting live config value: ${nestedKey} = ${convertedValue}`); + obj[keys[keys.length - 1]] = convertedValue; + } + + function saveConfig() { + const plainConfig = ObjectUtils.toPlainObject(ConfigOptions); + Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(JSON.stringify(plainConfig, null, 2))}' > '${root.filePath}'`) + } + + Timer { + id: delayedFileRead + interval: ConfigOptions.hacks.arbitraryRaceConditionDelay + repeat: false + running: false + onTriggered: { + root.applyConfig(configFileView.text()) + } + } + + FileView { + id: configFileView + path: Qt.resolvedUrl(root.filePath) + watchChanges: true + onFileChanged: { + console.log("[ConfigLoader] File changed, reloading...") + this.reload() + delayedFileRead.start() + } + onLoadedChanged: { + const fileContent = configFileView.text() + root.applyConfig(fileContent) + } + onLoadFailed: (error) => { + if(error == FileViewError.FileNotFound) { + console.log("[ConfigLoader] File not found, creating new file.") + root.saveConfig() + Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration created")}" "${root.filePath}"`) + } else { + Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration failed to load")}" "${root.filePath}"`) + } + } + } +}
\ No newline at end of file diff --git a/config/quickshell/services/HyprlandData.qml b/config/quickshell/services/HyprlandData.qml new file mode 100644 index 00000000..2b88ad9c --- /dev/null +++ b/config/quickshell/services/HyprlandData.qml @@ -0,0 +1,69 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +/** + * Provides access to some Hyprland data not available in Quickshell.Hyprland. + */ +Singleton { + id: root + property var windowList: [] + property var addresses: [] + property var windowByAddress: ({}) + property var monitors: [] + + function updateWindowList() { + getClients.running = true + getMonitors.running = true + } + + Component.onCompleted: { + updateWindowList() + } + + Connections { + target: Hyprland + + function onRawEvent(event) { + // Filter out redundant old v1 events for the same thing + if(event.name in [ + "activewindow", "focusedmon", "monitoradded", + "createworkspace", "destroyworkspace", "moveworkspace", + "activespecial", "movewindow", "windowtitle" + ]) return ; + updateWindowList() + } + } + + Process { + id: getClients + command: ["bash", "-c", "hyprctl clients -j | jq -c"] + stdout: SplitParser { + onRead: (data) => { + root.windowList = JSON.parse(data) + let tempWinByAddress = {} + for (var i = 0; i < root.windowList.length; ++i) { + var win = root.windowList[i] + tempWinByAddress[win.address] = win + } + root.windowByAddress = tempWinByAddress + root.addresses = root.windowList.map((win) => win.address) + } + } + } + Process { + id: getMonitors + command: ["bash", "-c", "hyprctl monitors -j | jq -c"] + stdout: SplitParser { + onRead: (data) => { + root.monitors = JSON.parse(data) + } + } + } +} + diff --git a/config/quickshell/services/HyprlandKeybinds.qml b/config/quickshell/services/HyprlandKeybinds.qml new file mode 100644 index 00000000..189ba76d --- /dev/null +++ b/config/quickshell/services/HyprlandKeybinds.qml @@ -0,0 +1,73 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import "root:/modules/common" +import "root:/modules/common/functions/file_utils.js" as FileUtils +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +/** + * A service that provides access to Hyprland keybinds. + * Uses the `get_keybinds.py` script to parse comments in config files in a certain format and convert to JSON. + */ +Singleton { + id: root + property string keybindParserPath: FileUtils.trimFileProtocol(`${Directories.config}/quickshell/scripts/hyprland/get_keybinds.py`) + property string defaultKeybindConfigPath: FileUtils.trimFileProtocol(`${Directories.config}/hypr/hyprland/keybinds.conf`) + property string userKeybindConfigPath: FileUtils.trimFileProtocol(`${Directories.config}/hypr/custom/keybinds.conf`) + property var defaultKeybinds: {"children": []} + property var userKeybinds: {"children": []} + property var keybinds: ({ + children: [ + ...(defaultKeybinds.children ?? []), + ...(userKeybinds.children ?? []), + ] + }) + + Connections { + target: Hyprland + + function onRawEvent(event) { + if (event.name == "configreloaded") { + getDefaultKeybinds.running = true + getUserKeybinds.running = true + } + } + } + + Process { + id: getDefaultKeybinds + running: true + command: [root.keybindParserPath, "--path", root.defaultKeybindConfigPath,] + + stdout: SplitParser { + onRead: data => { + try { + root.defaultKeybinds = JSON.parse(data) + } catch (e) { + console.error("[CheatsheetKeybinds] Error parsing keybinds:", e) + } + } + } + } + + Process { + id: getUserKeybinds + running: true + command: [root.keybindParserPath, "--path", root.userKeybindConfigPath] + + stdout: SplitParser { + onRead: data => { + try { + root.userKeybinds = JSON.parse(data) + } catch (e) { + console.error("[CheatsheetKeybinds] Error parsing keybinds:", e) + } + } + } + } +} + diff --git a/config/quickshell/services/MaterialThemeLoader.qml b/config/quickshell/services/MaterialThemeLoader.qml new file mode 100644 index 00000000..2d67ad5b --- /dev/null +++ b/config/quickshell/services/MaterialThemeLoader.qml @@ -0,0 +1,58 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import "root:/modules/common" +import QtQuick +import Quickshell +import Quickshell.Io + +/** + * Automatically reloads generated material colors. + * It is necessary to run reapplyTheme() on startup because Singletons are lazily loaded. + */ +Singleton { + id: root + property string filePath: Directories.generatedMaterialThemePath + + function reapplyTheme() { + themeFileView.reload() + } + + function applyColors(fileContent) { + const json = JSON.parse(fileContent) + for (const key in json) { + if (json.hasOwnProperty(key)) { + // Convert snake_case to CamelCase + const camelCaseKey = key.replace(/_([a-z])/g, (g) => g[1].toUpperCase()) + const m3Key = `m3${camelCaseKey}` + Appearance.m3colors[m3Key] = json[key] + } + } + + Appearance.m3colors.darkmode = (Appearance.m3colors.m3windowBackground.hslLightness < 0.5) + } + + Timer { + id: delayedFileRead + interval: ConfigOptions?.hacks?.arbitraryRaceConditionDelay ?? 100 + repeat: false + running: false + onTriggered: { + root.applyColors(themeFileView.text()) + } + } + + FileView { + id: themeFileView + path: Qt.resolvedUrl(root.filePath) + watchChanges: true + onFileChanged: { + this.reload() + delayedFileRead.start() + } + onLoadedChanged: { + const fileContent = themeFileView.text() + root.applyColors(fileContent) + } + } +} diff --git a/config/quickshell/shell.qml b/config/quickshell/shell.qml new file mode 100644 index 00000000..b14c8773 --- /dev/null +++ b/config/quickshell/shell.qml @@ -0,0 +1,26 @@ +//@ pragma UseQApplication +//@ pragma Env QS_NO_RELOAD_POPUP=1 +//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic + +import "./modules/common/" +import "./modules/overview/" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import "./services/" + +ShellRoot { + // Enable/disable modules here. False = not loaded at all, so rest assured + // no unnecessary stuff will take up memory if you decide to only use, say, the overview. + property bool enableOverview: true + + // Force initialization of some singletons + Component.onCompleted: { + MaterialThemeLoader.reapplyTheme() + ConfigLoader.loadConfig() + } + + Loader { active: enableOverview; sourceComponent: Overview {} } + +}
\ No newline at end of file diff --git a/config/wallust/templates/qml_color.json b/config/wallust/templates/qml_color.json new file mode 100644 index 00000000..03565181 --- /dev/null +++ b/config/wallust/templates/qml_color.json @@ -0,0 +1,17 @@ +{ + "windowBackground": "#0f0f15", + "primaryText": "#bac2de", + "layerBackground1": "{{color7}}", + "layerBackground2": "{{color6}}", + "layerBackground3": "#2D282E", + "surfaceText": "#EAE0E7", + "secondaryText": "#CFC3CD", + "borderPrimary": "{{color7}}", + "shadowColor": "#000000", + "accentPrimary": "{{color7}}", + "accentSecondary": "#{{color7}}", + "selectionBackground": "{{color7}}", + "accentPrimaryText": "#FFFFFF", + "selectionText": "#F2DCF3", + "borderSecondary": "#{{color5}}" +}
\ No newline at end of file diff --git a/config/wallust/wallust.toml b/config/wallust/wallust.toml index a7f66721..d1f40ab2 100644 --- a/config/wallust/wallust.toml +++ b/config/wallust/wallust.toml @@ -49,6 +49,9 @@ waybar.target = '~/.config/waybar/wallust/colors-waybar.css' kitty.template = 'colors-kitty.conf' kitty.target = '~/.config/kitty/kitty-themes/01-Wallust.conf' +quickshell.template = 'qml_color.json' +quickshell.target = '~/.config/quickshell/qml_color.json' + #swaync.template = 'colors-swaync.css' #swaync.target = '~/.config/swaync/wallust/colors-wallust.css' |
