├── utils ├── Paths.qml ├── Config.qml └── Appearance.qml ├── widgets └── dock │ ├── configs │ ├── default.json │ └── power-menu.json │ ├── DockItem.qml │ └── Dock.qml ├── shell.qml ├── components └── Tooltip.qml ├── appearance.json ├── LICENSE └── README.md /utils/Paths.qml: -------------------------------------------------------------------------------- 1 | pragma Singleton 2 | 3 | import Quickshell 4 | import Qt.labs.platform 5 | 6 | Singleton { 7 | id: root 8 | 9 | readonly property url root: Quickshell.shellRoot 10 | } 11 | -------------------------------------------------------------------------------- /utils/Config.qml: -------------------------------------------------------------------------------- 1 | import Quickshell.Io 2 | 3 | FileView { 4 | id: config 5 | blockLoading: true 6 | watchChanges: true 7 | onFileChanged: reload() 8 | 9 | readonly property var data: JSON.parse(config.text()) 10 | } 11 | -------------------------------------------------------------------------------- /widgets/dock/configs/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": "bottom", 3 | "orientation": "horizontal", 4 | "iconSize": 48, 5 | "alwaysVisible": false, 6 | "showIconsBackground": true, 7 | "showTooltips": true, 8 | "falloff": 3, 9 | "scaleFactor": 0.3, 10 | "damp": 1 11 | } 12 | -------------------------------------------------------------------------------- /shell.qml: -------------------------------------------------------------------------------- 1 | import Quickshell 2 | 3 | import "widgets/dock" 4 | import "utils" 5 | 6 | ShellRoot { 7 | Dock {} 8 | Dock { 9 | name: "power-menu" 10 | screens: [Quickshell.screens.reduce((acc, screen) => screen.x > acc.x ? screen : acc, { x: -Infinity })] // Only use the rightmost screen for the power menu 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /widgets/dock/configs/power-menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": "right", 3 | "orientation": "vertical", 4 | "margins": { 5 | "bottom": 100 6 | }, 7 | "items": [ 8 | "logout", 9 | "sleep", 10 | "power-off" 11 | ], 12 | "iconSize": 56, 13 | "alwaysVisible": false, 14 | "showIconsBackground": true, 15 | "showTooltips": false, 16 | "falloff": 2, 17 | "scaleFactor": 0.4, 18 | "damp": 1.5 19 | } 20 | -------------------------------------------------------------------------------- /components/Tooltip.qml: -------------------------------------------------------------------------------- 1 | import Quickshell 2 | import Quickshell.Io 3 | import QtQuick 4 | import QtQuick.Controls 5 | 6 | import "root:/utils" 7 | 8 | ToolTip { 9 | id: tooltip 10 | text: modelData.name 11 | font.family: Appearance.data.font.family.regular 12 | font.pointSize: Appearance.data.font.size.xm 13 | delay: 1000 14 | 15 | contentItem: Text { 16 | text: tooltip.text 17 | font: tooltip.font 18 | color: "white" 19 | } 20 | 21 | background: Rectangle { 22 | color: "#50000000" 23 | radius: Appearance.data.rounding.small 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /utils/Appearance.qml: -------------------------------------------------------------------------------- 1 | pragma Singleton 2 | 3 | import Quickshell 4 | import Quickshell.Io 5 | 6 | Singleton { 7 | id: root 8 | 9 | readonly property var data: JSON.parse(fileView.text()) 10 | 11 | function getColor(color, alpha = 1) { 12 | if (alpha === true) { 13 | alpha = data.opacity 14 | } 15 | 16 | if (alpha === 1) { 17 | return data.colors[color] 18 | } 19 | 20 | return `#${alpha * 100}${data.colors[color].slice(1)}` 21 | } 22 | 23 | FileView { 24 | id: fileView 25 | path: `${Paths.root}/appearance.json` 26 | blockLoading: true 27 | watchChanges: true 28 | onFileChanged: reload() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /appearance.json: -------------------------------------------------------------------------------- 1 | { 2 | "opacity": 0.5, 3 | "colors": { 4 | "background": "#1B191B", 5 | "foreground": "#F8E2D2", 6 | "primary": "#D37D58" 7 | }, 8 | "rounding": { 9 | "small": 8, 10 | "medium": 12, 11 | "large": 16 12 | }, 13 | "spacing": { 14 | "small": 8, 15 | "medium": 16, 16 | "large": 24 17 | }, 18 | "padding": { 19 | "small": 6, 20 | "medium": 12, 21 | "large": 24 22 | }, 23 | "font": { 24 | "family": { 25 | "regular": "Kode Mono", 26 | "heading": "Comfortaa" 27 | }, 28 | "size": { 29 | "xm": 8, 30 | "s": 12, 31 | "m": 14, 32 | "l": 16, 33 | "xl": 18, 34 | "xxl": 28 35 | } 36 | }, 37 | "animation": { 38 | "duration": { 39 | "quick": 100, 40 | "normal": 200, 41 | "slow": 400, 42 | "sequential": 25 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 careem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /widgets/dock/DockItem.qml: -------------------------------------------------------------------------------- 1 | import Quickshell 2 | import Quickshell.Widgets 3 | import QtQuick 4 | 5 | import "root:/utils" 6 | import "root:/components" 7 | 8 | Rectangle { 9 | id: item 10 | 11 | property int length: config.data.iconSize * pseudoScale + Appearance.data.spacing.small + (config.data.alwaysVisible ? (config.data.iconSize + Appearance.data.spacing.small) : additionalHeight) 12 | property int breadth: config.data.iconSize + Appearance.data.spacing.small 13 | 14 | width: config.data.orientation == "vertical" ? length : breadth 15 | height: config.data.orientation == "vertical" ? breadth : length 16 | 17 | color: "transparent" 18 | 19 | Timer { 20 | id: timer 21 | } 22 | 23 | function delay(height, latestIndex = row.current) { 24 | timer.interval = Math.abs(index - latestIndex) * Appearance.data.animation.duration.sequential 25 | timer.repeat = false 26 | timer.triggered.connect(() => additionalHeight = height) 27 | timer.start() 28 | } 29 | 30 | property int additionalHeight: 0 31 | property real pseudoScale: { 32 | if (row.current == -1) return 0 33 | else { 34 | const falloff = config.data.falloff || 3 35 | let diff = Math.abs(index - row.current) 36 | diff = Math.max(0, falloff - diff) 37 | let damp = falloff - Math.max(1, diff) 38 | let sc = config.data.scaleFactor 39 | if (damp) sc /= damp * (config.data.damp || 1) 40 | diff = diff / falloff * sc 41 | return diff 42 | } 43 | } 44 | 45 | MouseArea { 46 | id: itemMouseArea 47 | anchors.fill: parent 48 | hoverEnabled: true 49 | onEntered: { row.current = index; if (!config.data.alwaysVisible) { window.expand() } } 50 | onExited: { if (row.current == index) { row.current = -1; if (!config.data.alwaysVisible) { window.collapse(index) } } } 51 | onClicked: modelData.execute() 52 | cursorShape: Qt.PointingHandCursor 53 | propagateComposedEvents: true 54 | 55 | Tooltip { 56 | visible: config.data.showTooltips ? parent.containsMouse : false 57 | } 58 | 59 | Column { 60 | anchors.top: config.data.position == "bottom" ? parent.top : undefined 61 | anchors.right: config.data.position == "left" ? parent.right : undefined 62 | anchors.bottom: config.data.position == "top" ? parent.bottom : undefined 63 | anchors.left: config.data.position == "right" ? parent.left : undefined 64 | 65 | anchors.horizontalCenter: config.data.orientation == "horizontal" ? parent.horizontalCenter : undefined 66 | anchors.verticalCenter: config.data.orientation == "vertical" ? parent.verticalCenter : undefined 67 | 68 | topPadding: config.data.position == "bottom" ? Appearance.data.spacing.small : undefined 69 | rightPadding: config.data.position == "left" ? Appearance.data.spacing.small : undefined 70 | bottomPadding: config.data.position == "top" ? Appearance.data.spacing.small : undefined 71 | leftPadding: config.data.position == "right" ? Appearance.data.spacing.small : undefined 72 | 73 | width: config.data.orientation == "vertical" ? item.length : config.data.iconSize 74 | height: config.data.orientation == "vertical" ? config.data.iconSize : item.length 75 | 76 | Rectangle { 77 | width: config.data.iconSize 78 | height: width 79 | 80 | color: config.data.showIconsBackground ? Appearance.getColor("background", true) : "transparent" 81 | radius: Appearance.data.rounding.medium 82 | border.color: Appearance.getColor("primary") 83 | border.width: itemMouseArea.containsPress ? 1 : 0 84 | 85 | Image { 86 | width: parent.width 87 | height: width 88 | source: Quickshell.iconPath(modelData.icon) 89 | 90 | transform: Scale { 91 | origin.x: config.data.iconSize / 2 92 | origin.y: config.data.iconSize / 2 93 | xScale: itemMouseArea.containsPress ? 0.9 : 1 94 | yScale: itemMouseArea.containsPress ? 0.9 : 1 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | Behavior on length { 102 | NumberAnimation { 103 | duration: Appearance.data.animation.duration.normal 104 | easing.type: Easing.OutBack 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /widgets/dock/Dock.qml: -------------------------------------------------------------------------------- 1 | import Quickshell 2 | import Quickshell.Widgets 3 | import QtQuick 4 | import Quickshell.Io 5 | import QtQuick.Controls 6 | import QtQuick.Layouts 7 | 8 | import "root:/utils" 9 | import "root:/components" 10 | 11 | Scope { 12 | property string name: "default" 13 | 14 | Config { 15 | id: config 16 | path: Qt.resolvedUrl(`./configs/${name}.json`) 17 | } 18 | 19 | property list screenIds: Quickshell.screens.map((_, i) => i) 20 | property list screens: Quickshell.screens.filter((_, i) => screenIds.includes(i)) 21 | 22 | IpcHandler { 23 | target: `dock_${name}` 24 | function expand(monitor: int): void { 25 | const instance = variants.instances[monitor] 26 | instance.expand(instance.apps.length / 2) 27 | } 28 | function collapse(monitor: int): void { 29 | const instance = variants.instances[monitor] 30 | instance.collapse(instance.apps.length / 2) 31 | } 32 | } 33 | 34 | Variants { 35 | id: variants 36 | model: screens 37 | 38 | PanelWindow { 39 | id: window 40 | property var modelData 41 | screen: modelData 42 | 43 | function getAnchor(pos) { return Boolean(config.data.position == pos || config.data.margins?.[pos]) } 44 | anchors { 45 | left: getAnchor("left") 46 | right: getAnchor("right") 47 | top: getAnchor("top") 48 | bottom: getAnchor("bottom") 49 | } 50 | 51 | function getMargin(pos) { return config.data.margins?.[pos] || 0 } 52 | margins { 53 | left: getMargin("left") 54 | right: getMargin("right") 55 | top: getMargin("top") 56 | bottom: getMargin("bottom") 57 | } 58 | 59 | property int length: (config.data.iconSize + Appearance.data.spacing.small) * apps.length 60 | property int breadth: config.data.iconSize * ((config.data.scaleFactor ?? .3) + 1) * 1.1 + Appearance.data.spacing.small 61 | 62 | implicitWidth: config.data.orientation == "vertical" ? breadth : length 63 | implicitHeight: config.data.orientation == "vertical" ? length : breadth 64 | color: "transparent" 65 | 66 | mask: Region { item: row } 67 | 68 | readonly property var apps: { 69 | let apps = config.data.items || []; 70 | if (apps.length == 0) apps = DesktopEntries.applications.values 71 | else apps = config.data.items.map(name => DesktopEntries.applications.values.find(app => app.name == name)) 72 | return apps.filter(app => app?.name && app?.icon) 73 | } 74 | 75 | function expand(startIndex) { 76 | apps.forEach((_, ind) => { 77 | repeater.itemAt(ind).delay(config.data.iconSize + Appearance.data.spacing.small, startIndex) 78 | }) 79 | } 80 | function collapse(startIndex) { 81 | apps.forEach((_, ind) => { 82 | repeater.itemAt(ind).delay(0, startIndex) 83 | }) 84 | } 85 | 86 | Rectangle { 87 | id: dock 88 | height: parent.height + 2 89 | width: parent.width + 2 90 | anchors.top: config.data.position == "top" ? parent.top : undefined 91 | anchors.right: config.data.position == "right" ? parent.right : undefined 92 | anchors.bottom: config.data.position == "bottom" ? parent.bottom : undefined 93 | anchors.left: config.data.position == "left" ? parent.left : undefined 94 | color: "transparent" 95 | 96 | Grid { 97 | id: row 98 | 99 | columns: config.data.orientation == "vertical" ? 1 : apps.length 100 | rows: config.data.orientation == "vertical" ? apps.length : 1 101 | 102 | horizontalItemAlignment: config.data.position == "left" ? Grid.AlignLeft : 103 | config.data.position == "right" ? Grid.AlignRight : Grid.AlignHCenter 104 | 105 | verticalItemAlignment: config.data.position == "top" ? Grid.AlignTop : 106 | config.data.position == "bottom" ? Grid.AlignBottom : Grid.AlignVCenter 107 | 108 | anchors.top: config.data.position == "top" ? parent.top : undefined 109 | anchors.right: config.data.position == "right" ? parent.right : undefined 110 | anchors.bottom: config.data.position == "bottom" ? parent.bottom : undefined 111 | anchors.margins: -2 112 | anchors.left: config.data.position == "left" ? parent.left : undefined 113 | 114 | anchors.horizontalCenter: config.data.orientation == "horizontal" ? parent.horizontalCenter : undefined 115 | anchors.verticalCenter: config.data.orientation == "vertical" ? parent.verticalCenter : undefined 116 | 117 | spacing: 0 118 | 119 | property int current: -1 120 | 121 | Repeater { 122 | id: repeater 123 | model: apps 124 | 125 | DockItem {} 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

Crawraps' Widgets Collection

3 | 4 |
5 | 6 | A customizable collection of widgets built with the [quickshell](https://quickshell.outfoxxed.me/) environment. 7 | 8 |
9 | 10 | https://github.com/user-attachments/assets/8cd1d3f1-74a7-4dd4-baf0-7fa908daabce 11 | 12 | ## Table of Contents 13 | 14 | - [Installation](#installation) 15 | - [Manual Installation](#manual-installation) 16 | - [Usage](#usage) 17 | - [Configuration](#configuration) 18 | - [Appearance Configuration](#appearance-configuration) 19 | - [Widgets Configuration](#widgets-configuration) 20 | - [Widgets](#widgets) 21 | - [Dock](#dock) 22 | - [Contributing](#contributing) 23 | - [Future Plans](#future-plans) 24 | 25 | ## Installation 26 | 27 | ### Manual Installation 28 | 29 | 1. **Dependencies:** 30 | Install the core dependency - [quickshell](https://quickshell.outfoxxed.me/docs/guide/install-setup/) 31 | 32 | > Some widgets may require additional dependencies for extra features. Read the widget overview before using them. 33 | 34 | 2. **Clone the Repository:** 35 | ```sh 36 | $ git clone https://github.com/crawraps/widgets-collection $HOME/.config/quickshell/cwc 37 | ``` 38 | 39 | > If $XDG_CONFIG_HOME is not defined, use $HOME/.config instead. 40 | 41 | ## Usage 42 | 43 | 1. Select the widgets you want to use by editing the `shell.qml` file. Example: 44 | ```qml 45 | // shell.qml 46 | 47 | ... 48 | 49 | ShellRoot { 50 | Dock {} // dock widget with the default configuration 51 | } 52 | ``` 53 | 54 | > Each widget creates an instance for each monitor by default. You can configure this behavior—read the configuration section for details. 55 | 56 | 2. Start the shell by typing `$ quickshell -c cwc` or `$ quickshell -dc cwc` to run as a daemon. 57 | 3. **\*Optional\*** Add the command above to system startup. For example, in Hyprland, add `exec-once=quickshell -dc cwc` to your hyprland.conf. 58 | 4. **\*Optional\*** Create an alias for better usability. For example, in zsh, add `alias -- cwc='quickshell -c cwc'` to your .zshrc. 59 | 60 | ## Configuration 61 | 62 | You can configure widgets and their appearance by modifying `.json` files. Every config file supports hot-reload. 63 | 64 | ### Appearance Configuration 65 | 66 | Global appearance (colors, fonts, spacing, etc.) is managed via `appearance.json` in your config directory (e.g., `$HOME/.config/quickshell/cwc/appearance.json`): 67 | 68 | ```json 69 | { 70 | "colors": { 71 | "primary": "#007acc", 72 | "background": "#20242b" 73 | }, 74 | "font": { 75 | "family": { "regular": "Fira Sans" }, 76 | "size": { "xm": 10 } 77 | }, 78 | "spacing": { "small": 8 }, 79 | "rounding": { "small": 4, "medium": 8 }, 80 | "animation": { "duration": { "normal": 200, "sequential": 50 } }, 81 | "opacity": 0.85 82 | } 83 | ``` 84 | 85 | ### Widgets Configuration 86 | 87 | Each widget can have several widget-specific `.json` configuration files, each located in the `widgets/[widget-name]/configs/` directory. 88 | 89 | When selecting a widget to use, you can provide several common options: 90 | 91 | ```qml 92 | // shell.qml 93 | 94 | ... 95 | 96 | Dock { 97 | name: "power-menu" 98 | screenIds: [0] 99 | screens: [Quickshell.screens.reduce((acc, screen) => screen.x > acc.x ? screen : acc, { x: -Infinity })] 100 | } 101 | ``` 102 | 103 | - **name:** Name of a `.json` configuration file. Default value is `"default"`. In this example, it would be `widgets/dock/configs/power-menu.json`. 104 | - **screenIds:** List of screen IDs where the widget should be displayed. Default: every screen. 105 | - **screens:** List of screens where the widget should be displayed. When this option is present, the **screenIds** option will be ignored. Use this for more precise configuration: in the example above, the widget will be displayed on the rightmost screen. 106 | 107 | ## Widgets 108 | 109 | ### Dock 110 | 111 |
112 | Showcase 113 | 114 | https://github.com/user-attachments/assets/9c57d09c-0931-4146-995b-eb85c1595df5 115 | 116 | https://github.com/user-attachments/assets/5544e0da-eee4-4849-af37-8fa03f177b61 117 |
118 | 119 | Each dock can be configured with a JSON file (e.g., `configs/applications.json`): 120 | 121 | ```json 122 | { 123 | "position": "bottom", 124 | "orientation": "horizontal", 125 | "margins": { 126 | "right": 200 127 | }, 128 | "items": [ 129 | "Zen", 130 | "Obsidian", 131 | "Thunderbird" 132 | ], 133 | "iconSize": 48, 134 | "alwaysVisible": false, 135 | "showIconsBackground": true, 136 | "showTooltips": true, 137 | "falloff": 3, 138 | "scaleFactor": 0.3, 139 | "damp": 1 140 | } 141 | ``` 142 | 143 | - **position:** `"top"`, `"bottom"`, `"left"`, `"right"` - where to position the dock 144 | - **orientation:** `"horizontal"` or `"vertical"` - how to orient the dock 145 | - **margins:** `{ "bottom": number, "top": number, "right": number, "left": number }` - spacing from screen edges. By default, the dock will be centered on the specified **position** edge 146 | - **items:** List of `.desktop` file app names to show; if not defined, all desktop applications will be listed 147 | - **iconSize:** Size of an icon in pixels 148 | - **alwaysVisible:** If false, the dock expands/collapses on hover 149 | - **showIconsBackground:** Whether to show background for icons 150 | - **showTooltips:** Whether to display tooltips 151 | - **falloff:** Number of items to be affected by hover animation 152 | - **scaleFactor:** Controls item's hover scaling animation strength 153 | - **damp:** Controls nearby items' hover scaling animation strength 154 | 155 | ## Contributing 156 | 157 | If you have new ideas, concepts or implementations, consider contributing to this collection. 158 | 159 | There are no guidelines yet, so I would appreciate following the existing architecture with examples of existing widgets. 160 | 161 | ## Future Plans 162 | 163 | Here's a list of widgets I want to create: 164 | 165 | - [x] dock 166 | - [ ] launcher 167 | - [ ] translator/dictionary 168 | - [ ] weather display 169 | - [ ] emoji picker 170 | - [ ] mpris 171 | - [ ] system info display 172 | - [ ] feature-rich calendar 173 | --------------------------------------------------------------------------------