cheatsheet

This commit is contained in:
end-4 2025-05-16 18:22:05 +02:00
parent 8daa1702d0
commit 455bcdde4d
7 changed files with 692 additions and 3 deletions

View file

@ -0,0 +1,232 @@
import "root:/"
import "root:/services"
import "root:/modules/common"
import "root:/modules/common/widgets"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
Scope { // Scope
id: root
Variants { // Window repeater
id: cheatsheetVariants
model: Quickshell.screens
PanelWindow { // Window
id: cheatsheetRoot
visible: false
focusable: true
property var modelData
screen: modelData
exclusiveZone: 0
implicitWidth: cheatsheetBackground.width + Appearance.sizes.elevationMargin * 2
implicitHeight: cheatsheetBackground.height + Appearance.sizes.elevationMargin * 2
WlrLayershell.namespace: "quickshell:cheatsheet"
// Hyprland 0.49: Focus is always exclusive and setting this breaks mouse focus grab
// WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
mask: Region {
item: cheatsheetBackground
}
HyprlandFocusGrab { // Click outside to close
id: grab
windows: [ cheatsheetRoot ]
active: false
onCleared: () => {
if (!active) cheatsheetRoot.visible = false
}
}
Connections {
target: cheatsheetRoot
function onVisibleChanged() {
delayedGrabTimer.start()
}
}
Timer {
id: delayedGrabTimer
interval: ConfigOptions.hacks.arbitraryRaceConditionDelay
repeat: false
onTriggered: {
grab.active = cheatsheetRoot.visible
}
}
// Background
Rectangle {
id: cheatsheetBackground
anchors.centerIn: parent
color: Appearance.colors.colLayer0
radius: Appearance.rounding.windowRounding
property real padding: 30
implicitWidth: cheatsheetColumnLayout.implicitWidth + padding * 2
implicitHeight: cheatsheetColumnLayout.implicitHeight + padding * 2
Keys.onPressed: (event) => { // Esc to close
if (event.key === Qt.Key_Escape) {
cheatsheetRoot.visible = false
}
}
Button { // Close button
id: closeButton
focus: cheatsheetRoot.visible
implicitWidth: 40
implicitHeight: 40
anchors {
top: parent.top
right: parent.right
topMargin: 20
rightMargin: 20
}
PointingHandInteraction {}
onClicked: {
cheatsheetRoot.visible = false
}
background: Item {}
contentItem: Rectangle {
anchors.fill: parent
radius: Appearance.rounding.full
color: closeButton.pressed ? Appearance.colors.colLayer0Active :
closeButton.hovered ? Appearance.colors.colLayer0Hover :
Appearance.transparentize(Appearance.colors.colLayer0, 1)
Behavior on color {
ColorAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
MaterialSymbol {
anchors.centerIn: parent
font.pixelSize: Appearance.font.pixelSize.title
text: "close"
}
}
}
ColumnLayout { // Real content
id: cheatsheetColumnLayout
anchors.centerIn: parent
spacing: 20
StyledText {
id: cheatsheetTitle
Layout.alignment: Qt.AlignHCenter
font.family: Appearance.font.family.title
font.pixelSize: Appearance.font.pixelSize.title
text: qsTr("Cheat sheet")
}
CheatsheetKeybinds {}
}
}
// Shadow
DropShadow {
anchors.fill: cheatsheetBackground
horizontalOffset: 0
verticalOffset: 2
radius: Appearance.sizes.elevationMargin
samples: Appearance.sizes.elevationMargin * 2 + 1 // Ideally should be 2 * radius + 1, see qt docs
color: Appearance.colors.colShadow
source: cheatsheetBackground
}
}
}
IpcHandler {
target: "cheatsheet"
function toggle(): void {
for (let i = 0; i < cheatsheetVariants.instances.length; i++) {
let panelWindow = cheatsheetVariants.instances[i];
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
panelWindow.visible = !panelWindow.visible;
if(panelWindow.visible) Notifications.timeoutAll();
}
}
}
function close(): void {
for (let i = 0; i < cheatsheetVariants.instances.length; i++) {
let panelWindow = cheatsheetVariants.instances[i];
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
panelWindow.visible = false;
}
}
}
function open(): void {
for (let i = 0; i < cheatsheetVariants.instances.length; i++) {
let panelWindow = cheatsheetVariants.instances[i];
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
panelWindow.visible = true;
if(panelWindow.visible) Notifications.timeoutAll();
}
}
}
}
GlobalShortcut {
name: "cheatsheetToggle"
description: "Toggles cheatsheet on press"
onPressed: {
for (let i = 0; i < cheatsheetVariants.instances.length; i++) {
let panelWindow = cheatsheetVariants.instances[i];
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
panelWindow.visible = !panelWindow.visible;
if(panelWindow.visible) Notifications.timeoutAll();
}
}
}
}
GlobalShortcut {
name: "cheatsheetOpen"
description: "Opens cheatsheet on press"
onPressed: {
for (let i = 0; i < cheatsheetVariants.instances.length; i++) {
let panelWindow = cheatsheetVariants.instances[i];
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
panelWindow.visible = true;
if(panelWindow.visible) Notifications.timeoutAll();
}
}
}
}
GlobalShortcut {
name: "cheatsheetClose"
description: "Closes cheatsheet on press"
onPressed: {
for (let i = 0; i < cheatsheetVariants.instances.length; i++) {
let panelWindow = cheatsheetVariants.instances[i];
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
panelWindow.visible = false;
}
}
}
}
}

View file

@ -0,0 +1,147 @@
import "root:/"
import "root:/services"
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/modules/common/functions/file_utils.js" as FileUtils
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import Quickshell.Hyprland
Item {
id: root
readonly property var keybinds: HyprlandKeybinds.keybinds
property real spacing: 20
property real titleSpacing: 7
implicitWidth: rowLayout.implicitWidth
implicitHeight: rowLayout.implicitHeight
property var keyBlacklist: ["Super_L"]
property var keySubstitutions: ({
"Super": "󰖳",
"mouse_up": "Scroll ↓", // ikr, weird
"mouse_down": "Scroll ↑", // trust me bro
"mouse:272": "LMB",
"mouse:273": "RMB",
"mouse:275": "MouseBack",
"Slash": "/",
"Hash": "#"
})
RowLayout { // Keybind columns
id: rowLayout
spacing: root.spacing
Repeater {
model: keybinds.children
delegate: ColumnLayout { // Keybind sections
spacing: root.spacing
required property var modelData
Layout.alignment: Qt.AlignTop
Repeater {
model: modelData.children
delegate: Item { // Section with real keybinds
required property var modelData
implicitWidth: sectionColumnLayout.implicitWidth
implicitHeight: sectionColumnLayout.implicitHeight
ColumnLayout {
id: sectionColumnLayout
anchors.centerIn: parent
spacing: root.titleSpacing
StyledText {
id: sectionTitle
font.family: Appearance.font.family.title
font.pixelSize: Appearance.font.pixelSize.huge
color: Appearance.colors.colOnLayer0
text: modelData.name
}
GridLayout {
id: keybindGrid
columns: 2
Repeater {
model: {
var result = [];
for (var i = 0; i < modelData.keybinds.length; i++) {
const keybind = modelData.keybinds[i];
result.push({
"type": "keys",
"mods": keybind.mods,
"key": keybind.key,
});
result.push({
"type": "comment",
"comment": keybind.comment,
});
}
return result;
}
delegate: Item {
required property var modelData
implicitWidth: keybindLoader.implicitWidth
implicitHeight: keybindLoader.implicitHeight
Loader {
id: keybindLoader
sourceComponent: (modelData.type === "keys") ? keysComponent : commentComponent
}
Component {
id: keysComponent
RowLayout {
spacing: 4
Repeater {
model: modelData.mods
delegate: KeyboardKey {
required property var modelData
key: keySubstitutions[modelData] || modelData
}
}
StyledText {
id: keybindPlus
visible: !keyBlacklist.includes(modelData.key) && modelData.mods.length > 0
Layout.alignment: Qt.AlignVCenter
text: "+"
}
KeyboardKey {
id: keybindKey
visible: !keyBlacklist.includes(modelData.key)
key: keySubstitutions[modelData.key] || modelData.key
color: Appearance.colors.colOnLayer0
}
}
}
Component {
id: commentComponent
Item {
id: commentItem
implicitWidth: commentText.implicitWidth + 5 * 2
implicitHeight: commentText.implicitHeight
StyledText {
id: commentText
anchors.centerIn: parent
font.pixelSize: Appearance.font.pixelSize.smaller
text: modelData.comment
}
}
}
}
}
}
}
}
}
}
}
}
}

View file

@ -108,8 +108,8 @@ Singleton {
property color colSubtext: m3colors.m3outline
property color colLayer0: m3colors.m3background
property color colOnLayer0: m3colors.m3onBackground
property color colLayer0Hover: mix(colLayer0, colOnLayer0, 0.85)
property color colLayer0Active: m3colors.m3surfaceContainerHigh
property color colLayer0Hover: mix(colLayer0, colOnLayer0, 0.9)
property color colLayer0Active: mix(colLayer0, colOnLayer0, 0.8)
property color colLayer1: m3colors.m3surfaceContainerLow;
property color colOnLayer1: m3colors.m3onSurfaceVariant;
property color colOnLayer1Inactive: mix(colOnLayer1, colLayer1, 0.45);
@ -155,7 +155,7 @@ Singleton {
font: QtObject {
property QtObject family: QtObject {
property string main: "Rubik"
property string title: "Rubik"
property string title: "Gabarito"
property string iconMaterial: "Material Symbols Rounded"
property string iconNerd: "SpaceMono NF"
property string monospace: "JetBrains Mono NF"

View file

@ -0,0 +1,42 @@
import "root:/modules/common"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
Rectangle {
id: root
property string key
property real horizontalPadding: 7
property real verticalPadding: 2
property real borderWidth: 1
property real bottomBorderWidth: 3
property color borderColor: Appearance.colors.colOnLayer0
property real borderRadius: 5
property color keyColor: Appearance.m3colors.m3surfaceContainerLow
implicitWidth: keyFace.implicitWidth + borderWidth * 2
implicitHeight: keyFace.implicitHeight + borderWidth * 2 + bottomBorderWidth
radius: borderRadius
color: borderColor
Rectangle {
id: keyFace
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: borderWidth
implicitWidth: keyText.implicitWidth + horizontalPadding * 2
implicitHeight: keyText.implicitHeight + verticalPadding * 2
color: keyColor
radius: borderRadius - borderWidth
StyledText {
id: keyText
anchors.centerIn: parent
font.family: Appearance.font.family.monospace
font.pixelSize: Appearance.font.pixelSize.smaller
text: key
}
}
}

View file

@ -0,0 +1,222 @@
#!/usr/bin/env -S\_/bin/sh\_-c\_"source\_\$(eval\_echo\_\$ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate&&exec\_python\_-E\_"\$0"\_"\$@""
import argparse
import re
import os
from os.path import expandvars as os_expandvars
from typing import Dict, List
TITLE_REGEX = "#+!"
HIDE_COMMENT = "[hidden]"
MOD_SEPARATORS = ['+', ' ']
COMMENT_BIND_PATTERN = "#/#"
parser = argparse.ArgumentParser(description='Hyprland keybind reader')
parser.add_argument('--path', type=str, default="$HOME/.config/hypr/hyprland.conf", help='path to keybind file (sourcing isn\'t supported)')
args = parser.parse_args()
content_lines = []
reading_line = 0
# Little Parser made for hyprland keybindings conf file
Variables: Dict[str, str] = {}
class KeyBinding(dict):
def __init__(self, mods, key, dispatcher, params, comment) -> None:
self["mods"] = mods
self["key"] = key
self["dispatcher"] = dispatcher
self["params"] = params
self["comment"] = comment
class Section(dict):
def __init__(self, children, keybinds, name) -> None:
self["children"] = children
self["keybinds"] = keybinds
self["name"] = name
def read_content(path: str) -> str:
if (not os.access(os.path.expanduser(os.path.expandvars(path)), os.R_OK)):
return ("error")
with open(os.path.expanduser(os.path.expandvars(path)), "r") as file:
return file.read()
def autogenerate_comment(dispatcher: str, params: str = "") -> str:
match dispatcher:
case "resizewindow":
return "Resize window"
case "movewindow":
if(params == ""):
return "Move window"
else:
return "Window: move in {} direction".format({
"l": "left",
"r": "right",
"u": "up",
"d": "down",
}.get(params, "null"))
case "pin":
return "Window: pin (show on all workspaces)"
case "splitratio":
return "Window split ratio {}".format(params)
case "togglefloating":
return "Float/unfloat window"
case "resizeactive":
return "Resize window by {}".format(params)
case "killactive":
return "Close window"
case "fullscreen":
return "Toggle {}".format(
{
"0": "fullscreen",
"1": "maximization",
"2": "fullscreen on Hyprland's side",
}.get(params, "null")
)
case "fakefullscreen":
return "Toggle fake fullscreen"
case "workspace":
if params == "+1":
return "Workspace: focus right"
elif params == "-1":
return "Workspace: focus left"
return "Focus workspace {}".format(params)
case "movefocus":
return "Window: move focus {}".format(
{
"l": "left",
"r": "right",
"u": "up",
"d": "down",
}.get(params, "null")
)
case "swapwindow":
return "Window: swap in {} direction".format(
{
"l": "left",
"r": "right",
"u": "up",
"d": "down",
}.get(params, "null")
)
case "movetoworkspace":
if params == "+1":
return "Window: move to right workspace (non-silent)"
elif params == "-1":
return "Window: move to left workspace (non-silent)"
return "Window: move to workspace {} (non-silent)".format(params)
case "movetoworkspacesilent":
if params == "+1":
return "Window: move to right workspace"
elif params == "-1":
return "Window: move to right workspace"
return "Window: move to workspace {}".format(params)
case "togglespecialworkspace":
return "Workspace: toggle special"
case "exec":
return "Execute: {}".format(params)
case _:
return ""
def get_keybind_at_line(line_number, line_start = 0):
global content_lines
line = content_lines[line_number]
_, keys = line.split("=", 1)
keys, *comment = keys.split("#", 1)
mods, key, dispatcher, *params = list(map(str.strip, keys.split(",", 4)))
params = "".join(map(str.strip, params))
# Remove empty spaces
comment = list(map(str.strip, comment))
# Add comment if it exists, else generate it
if comment:
comment = comment[0]
if comment.startswith("[hidden]"):
return None
else:
comment = autogenerate_comment(dispatcher, params)
if mods:
modstring = mods + MOD_SEPARATORS[0] # Add separator at end to ensure last mod is read
mods = []
p = 0
for index, char in enumerate(modstring):
if(char in MOD_SEPARATORS):
if(index - p > 1):
mods.append(modstring[p:index])
p = index+1
else:
mods = []
return KeyBinding(mods, key, dispatcher, params, comment)
def get_binds_recursive(current_content, scope):
global content_lines
global reading_line
# print("get_binds_recursive({0}, {1}) [@L{2}]".format(current_content, scope, reading_line + 1))
while reading_line < len(content_lines): # TODO: Adjust condition
line = content_lines[reading_line]
heading_search_result = re.search(TITLE_REGEX, line)
# print("Read line {0}: {1}\tisHeading: {2}".format(reading_line + 1, content_lines[reading_line], "[{0}, {1}, {2}]".format(heading_search_result.start(), heading_search_result.start() == 0, ((heading_search_result != None) and (heading_search_result.start() == 0))) if heading_search_result != None else "No"))
if ((heading_search_result != None) and (heading_search_result.start() == 0)): # Found title
# Determine scope
heading_scope = line.find('!')
# Lower? Return
if(heading_scope <= scope):
reading_line -= 1
return current_content
section_name = line[(heading_scope+1):].strip()
# print("[[ Found h{0} at line {1} ]] {2}".format(heading_scope, reading_line+1, content_lines[reading_line]))
reading_line += 1
current_content["children"].append(get_binds_recursive(Section([], [], section_name), heading_scope))
elif line.startswith(COMMENT_BIND_PATTERN):
keybind = get_keybind_at_line(reading_line, line_start=len(COMMENT_BIND_PATTERN))
if(keybind != None):
current_content["keybinds"].append(keybind)
elif line == "" or not line.lstrip().startswith("bind"): # Comment, ignore
pass
else: # Normal keybind
keybind = get_keybind_at_line(reading_line)
if(keybind != None):
current_content["keybinds"].append(keybind)
reading_line += 1
return current_content;
def parse_keys(path: str) -> Dict[str, List[KeyBinding]]:
global content_lines
content_lines = read_content(path).splitlines()
if content_lines[0] == "error":
return "error"
return get_binds_recursive(Section([], [], ""), 0)
if __name__ == "__main__":
import json
ParsedKeys = parse_keys(args.path)
print(json.dumps(ParsedKeys))

View file

@ -0,0 +1,44 @@
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
Singleton {
id: root
property var keybinds: []
Connections {
target: Hyprland
function onRawEvent(event) {
console.log("[CheatsheetKeybinds] Event:", event.name)
if (event.name == "configreloaded") {
getKeybinds.running = true
}
}
}
Process {
id: getKeybinds
running: true
command: [FileUtils.trimFileProtocol(`${XdgDirectories.config}/quickshell/scripts/hyprland/get_keybinds.py`),
"--path", FileUtils.trimFileProtocol(`${XdgDirectories.config}/hypr/hyprland/keybinds.conf`),]
stdout: SplitParser {
onRead: data => {
try {
root.keybinds = JSON.parse(data)
} catch (e) {
console.error("[CheatsheetKeybinds] Error parsing keybinds:", e)
}
}
}
}
}

View file

@ -1,6 +1,7 @@
//@ pragma UseQApplication
import "./modules/bar/"
import "./modules/cheatsheet/"
import "./modules/notificationPopup/"
import "./modules/onScreenDisplay/"
import "./modules/overview/"
@ -23,6 +24,7 @@ ShellRoot {
}
Bar {}
Cheatsheet {}
NotificationPopup {}
OnScreenDisplayBrightness {}
OnScreenDisplayVolume {}