Merge branch 'end-4:main' into parallax

This commit is contained in:
Ivan Rosinskii
2026-03-23 08:09:39 +01:00
committed by GitHub
21 changed files with 591 additions and 288 deletions
+4 -1
View File
@@ -140,7 +140,7 @@ misc {
disable_hyprland_logo = true
disable_splash_rendering = true
vfr = 1
vrr = 1
vrr = 0
mouse_move_enables_dpms = true
key_press_enables_dpms = true
animate_manual_resizes = false
@@ -166,3 +166,6 @@ cursor {
hotspot_padding = 1
}
xwayland {
force_zero_scaling = true
}
@@ -0,0 +1,6 @@
import QtQuick
// QtObject that allows stuff to be freely declared inside
QtObject {
default property list<QtObject> data
}
@@ -0,0 +1,63 @@
pragma ComponentBehavior: Bound
import QtQml
import QtQuick
import Quickshell.Io
import qs.services
import "../"
NestableObject {
id: root
required property string key
property alias fetching: fetchProc.running
property bool set
property var value
Component.onCompleted: fetch()
Connections {
target: HyprlandConfig
function onReloaded() {
root.fetch();
}
}
function fetch() {
fetchProc.command = fetchProc.baseCommand.concat([root.key]);
fetchProc.running = true;
}
function setValue(newValue) {
HyprlandConfig.set(root.key, newValue)
}
function reset() {
HyprlandConfig.reset(root.key)
}
Process {
id: fetchProc
property list<string> baseCommand: ["hyprctl", "getoption", "-j"]
stdout: StdioCollector {
onStreamFinished: {
if (text == "no such option")
return;
try {
const obj = JSON.parse(text);
// Note that the value is returned as "<data type>": <value>
// It's the only field that isn't always in the same key so we put it in an else
for (const key in obj) {
if (key == "option")
continue;
else if (key == "set")
root.set = obj[key];
else
root.value = obj[key];
}
} catch (e) {
console.log(`[HyprlandConfigOption] Failed to fetch option "${root.key}":\n - Output: ${text.trim()}\n - Error: ${e}`);
}
}
}
}
}
@@ -8,10 +8,10 @@ QuickToggleModel {
name: Translation.tr("Anti-flashbang")
tooltipText: Translation.tr("Anti-flashbang")
icon: "flash_off"
toggled: Config.options.light.antiFlashbang.enable
toggled: HyprlandAntiFlashbangShader.enabled
mainAction: () => {
Config.options.light.antiFlashbang.enable = !Config.options.light.antiFlashbang.enable;
HyprlandAntiFlashbangShader.toggle()
}
hasMenu: true
}
@@ -1,11 +1,12 @@
import QtQuick
import Quickshell.Io
import qs.modules.common.models.hyprland
import qs.services
QuickToggleModel {
id: root
name: Translation.tr("Game mode")
toggled: toggled
toggled: !confOpt.value
icon: "gamepad"
mainAction: () => {
@@ -34,13 +35,11 @@ QuickToggleModel {
]);
}
}
Process {
id: fetchActiveState
running: true
command: ["bash", "-c", `test "$(hyprctl getoption animations:enabled -j | jq ".int")" -ne 0`]
onExited: (exitCode, exitStatus) => {
root.toggled = exitCode !== 0; // Inverted because enabled = nonzero exit
}
HyprlandConfigOption {
id: confOpt
key: "animations:enabled"
}
tooltipText: Translation.tr("Game mode")
}
@@ -29,6 +29,11 @@ Item {
}
function setExtraWindowAndGrabFocus(window) {
if (root.activeMenu && root.activeMenu !== window) {
if (typeof root.activeMenu.close === "function")
root.activeMenu.close();
root.activeMenu = null;
}
root.activeMenu = window;
root.grabFocus();
}
@@ -1,3 +1,4 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.SystemTray
@@ -26,7 +27,11 @@ MouseArea {
item.activate();
break;
case Qt.RightButton:
if (item.hasMenu) menu.open();
if (item.hasMenu)
if (menu.active && menu.item && typeof menu.item.close === "function")
menu.item.close();
else
menu.open();
break;
}
event.accepted = true;
@@ -44,14 +49,16 @@ MouseArea {
sourceComponent: SysTrayMenu {
Component.onCompleted: this.open();
trayItemMenuHandle: root.item.menu
trayItemId: root.item.id
anchor {
window: root.QsWindow.window
rect.x: root.x + (Config.options.bar.vertical ? 0 : QsWindow.window?.width)
rect.y: root.y + (Config.options.bar.vertical ? QsWindow.window?.height : 0)
rect.height: root.height
rect.width: root.width
edges: Config.options.bar.bottom ? (Edges.Top | Edges.Left) : (Edges.Bottom | Edges.Right)
gravity: Config.options.bar.bottom ? (Edges.Top | Edges.Left) : (Edges.Bottom | Edges.Right)
item: root
gravity: Config.options.bar.vertical
? (Config.options.bar.bottom ? Edges.Left : Edges.Right)
: (Config.options.bar.bottom ? Edges.Top : Edges.Bottom)
edges: Config.options.bar.vertical
? (Config.options.bar.bottom ? Edges.Left : Edges.Right)
: (Config.options.bar.bottom ? Edges.Top : Edges.Bottom)
}
onMenuOpened: (window) => root.menuOpened(window);
onMenuClosed: {
@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import qs.modules.common.widgets
@@ -9,6 +11,7 @@ import Quickshell
PopupWindow {
id: root
required property QsMenuHandle trayItemMenuHandle
property string trayItemId: ""
property real popupBackgroundMargin: 0
signal menuClosed
@@ -173,6 +176,48 @@ PopupWindow {
}
}
}
RippleButton {
id: pinEntry
buttonRadius: popupBackground.radius - popupBackground.padding
horizontalPadding: 12
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
implicitHeight: 36
Layout.topMargin: 0
Layout.bottomMargin: 0
Layout.fillWidth: true
visible: root.trayItemId !== undefined && root.trayItemId.length > 0 && stackView.depth === 1
releaseAction: () => TrayService.togglePin(root.trayItemId);
contentItem: RowLayout {
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
right: parent.right
leftMargin: pinEntry.horizontalPadding
rightMargin: pinEntry.horizontalPadding
}
spacing: 8
MaterialSymbol {
iconSize: 18
text: "push_pin"
}
StyledText {
Layout.fillWidth: true
text: TrayService.isPinned(root.trayItemId) ? Translation.tr("Unpin") : Translation.tr("Pin")
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 1
color: Appearance.colors.colSubtext
Layout.topMargin: 4
Layout.bottomMargin: 4
}
Repeater {
id: menuEntriesRepeater
@@ -14,11 +14,14 @@ Item {
required property color overlayColor
property bool showAimLines: Config.options.regionSelector.rect.showAimLines
property bool breathingBorderOnly: false
// Overlay to darken screen
// Base dark overlay around region
Rectangle {
id: darkenOverlay
z: 1
visible: !root.breathingBorderOnly
anchors {
left: parent.left
top: parent.top
@@ -32,25 +35,6 @@ Item {
border.width: Math.max(root.width, root.height)
}
// Selection border
// Rectangle {
// id: selectionBorder
// z: 1
// anchors {
// left: parent.left
// top: parent.top
// leftMargin: root.regionX
// topMargin: root.regionY
// }
// width: root.regionWidth
// height: root.regionHeight
// color: "transparent"
// border.color: root.color
// border.width: 2
// // radius: root.standardRounding
// radius: 0 // TODO: figure out how to make the overlay thing work with rounding
// }
DashedBorder {
id: selectionBorder
z: 9
@@ -64,13 +48,23 @@ Item {
height: Math.round(root.regionHeight) + borderWidth * 2
color: root.color
dashLength: 6
gapLength: 3
dashLength: 8
gapLength: 4
borderWidth: 1
// Breathing
opacity: 0.9
SequentialAnimation on opacity {
running: root.breathingBorderOnly
loops: Animation.Infinite
NumberAnimation { from: 0.9; to: 0.3; duration: 1200; easing.type: Easing.InOutQuad }
NumberAnimation { from: 0.3; to: 0.9; duration: 1200; easing.type: Easing.InOutQuad }
}
}
StyledText {
z: 2
visible: !root.breathingBorderOnly
anchors {
top: selectionBorder.bottom
right: selectionBorder.right
@@ -82,7 +76,7 @@ Item {
// Coord lines
Rectangle { // Vertical
visible: root.showAimLines
visible: root.showAimLines && !root.breathingBorderOnly
opacity: 0.2
z: 2
x: root.mouseX
@@ -94,7 +88,7 @@ Item {
color: root.color
}
Rectangle { // Horizontal
visible: root.showAimLines
visible: root.showAimLines && !root.breathingBorderOnly
opacity: 0.2
z: 2
y: root.mouseY
@@ -27,13 +27,17 @@ PanelWindow {
bottom: true
}
// Modes
// TODO: Ask: sidebar AI
enum SnipAction { Copy, Edit, Search, CharRecognition, Record, RecordWithSound }
enum SelectionMode { RectCorners, Circle }
enum Phase { Select, Post }
property var action: RegionSelection.SnipAction.Copy
property var selectionMode: RegionSelection.SelectionMode.RectCorners
property var phase: RegionSelection.Phase.Select
signal dismiss()
// Styles
property string screenshotDir: Directories.screenshotTemp
property color overlayColor: ColorUtils.transparentize("#000000", 0.4)
property color brightText: Appearance.m3colors.darkmode ? Appearance.colors.colOnLayer0 : Appearance.colors.colLayer0
@@ -46,6 +50,10 @@ PanelWindow {
property color imageBorderColor: brightTertiary
property color imageFillColor: ColorUtils.transparentize(imageBorderColor, 0.85)
property color onBorderColor: "#ff000000"
property real targetRegionOpacity: Config.options.regionSelector.targetRegions.opacity
property bool contentRegionOpacity: Config.options.regionSelector.targetRegions.contentRegionOpacity
// Vars for indicators
readonly property var windows: [...HyprlandData.windowList].sort((a, b) => {
// Sort floating=true windows before others
if (a.floating === b.floating) return 0;
@@ -54,6 +62,7 @@ PanelWindow {
readonly property var layers: HyprlandData.layers
readonly property real falsePositivePreventionRatio: 0.5
// Screen & interaction vars
readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen)
readonly property real monitorScale: hyprlandMonitor.scale
readonly property real monitorOffsetX: hyprlandMonitor.x
@@ -105,13 +114,13 @@ PanelWindow {
return offsetAdjustedLayers;
}
// Config
property bool isCircleSelection: (root.selectionMode === RegionSelection.SelectionMode.Circle)
property bool enableWindowRegions: Config.options.regionSelector.targetRegions.windows && !isCircleSelection
property bool enableLayerRegions: Config.options.regionSelector.targetRegions.layers && !isCircleSelection
property bool enableContentRegions: Config.options.regionSelector.targetRegions.content
property real targetRegionOpacity: Config.options.regionSelector.targetRegions.opacity
property bool contentRegionOpacity: Config.options.regionSelector.targetRegions.contentRegionOpacity
// Target
property real targetedRegionX: -1
property real targetedRegionY: -1
property real targetedRegionWidth: 0
@@ -175,6 +184,7 @@ PanelWindow {
property real regionX: Math.min(dragStartX, draggingX)
property real regionY: Math.min(dragStartY, draggingY)
// Screenshot stuff
TempScreenshotProcess {
id: screenshotProc
running: true
@@ -247,6 +257,7 @@ PanelWindow {
}
}
// Execution after selection
function snip() {
// Validity check
if (root.regionWidth <= 0 || root.regionHeight <= 0) {
@@ -277,21 +288,27 @@ PanelWindow {
screenshotAction, //
screenshotDir
)
snipProc.command = command;
// Image post-processing
snipProc.startDetached();
Quickshell.execDetached(command);
if (root.action == RegionSelection.SnipAction.Record || root.action == RegionSelection.SnipAction.RecordWithSound) {
root.phase = RegionSelection.Phase.Post
} else {
root.dismiss();
}
Process {
id: snipProc
}
ScreencopyView {
// Only clickable in Selection phase
mask: Region {
item: switch(root.phase) {
case RegionSelection.Phase.Select: return mouseArea;
case RegionSelection.Phase.Post: return null;
}
}
ScreencopyView { // For freezing
anchors.fill: parent
live: false
captureSource: root.screen
visible: root.phase === RegionSelection.Phase.Select
focus: root.visible
Keys.onPressed: (event) => { // Esc to close
@@ -299,6 +316,7 @@ PanelWindow {
root.dismiss();
}
}
}
MouseArea {
id: mouseArea
@@ -361,6 +379,7 @@ PanelWindow {
mouseY: mouseArea.mouseY
color: root.selectionBorderColor
overlayColor: root.overlayColor
breathingBorderOnly: root.phase === RegionSelection.Phase.Post
}
}
@@ -375,8 +394,10 @@ PanelWindow {
}
}
// The thing to the bottom-right with an icon
CursorGuide {
z: 9999
visible: root.phase === RegionSelection.Phase.Select
x: root.dragging ? root.regionX + root.regionWidth : mouseArea.mouseX
y: root.dragging ? root.regionY + root.regionHeight : mouseArea.mouseY
action: root.action
@@ -386,17 +407,23 @@ PanelWindow {
// Window regions
Repeater {
model: ScriptModel {
values: root.enableWindowRegions ? root.windowRegions : []
values: {
if (root.phase === RegionSelection.Phase.Select && root.enableWindowRegions) {
return root.windowRegions
} else {
return []
}
}
}
delegate: TargetRegion {
z: 2
required property var modelData
clientDimensions: modelData
showIcon: true
targeted: !root.draggedAway &&
(root.targetedRegionX === modelData.at[0]
&& root.targetedRegionY === modelData.at[1]
&& root.targetedRegionWidth === modelData.size[0]
targeted: !root.draggedAway && //
(root.targetedRegionX === modelData.at[0] //
&& root.targetedRegionY === modelData.at[1] //
&& root.targetedRegionWidth === modelData.size[0] //
&& root.targetedRegionHeight === modelData.size[1])
opacity: root.draggedAway ? 0 : root.targetRegionOpacity
@@ -410,7 +437,13 @@ PanelWindow {
// Layer regions
Repeater {
model: ScriptModel {
values: root.enableLayerRegions ? root.layerRegions : []
values: {
if (root.phase === RegionSelection.Phase.Select && root.enableLayerRegions) {
return root.layerRegions
} else {
return []
}
}
}
delegate: TargetRegion {
z: 3
@@ -433,7 +466,13 @@ PanelWindow {
// Content regions
Repeater {
model: ScriptModel {
values: root.enableContentRegions ? root.imageRegions : []
values: {
if (root.phase === RegionSelection.Phase.Select && root.enableContentRegions) {
return root.imageRegions
} else {
return []
}
}
}
delegate: TargetRegion {
z: 4
@@ -456,6 +495,7 @@ PanelWindow {
Row {
id: regionSelectionControls
z: 10
visible: root.phase === RegionSelection.Phase.Select
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
@@ -514,5 +554,4 @@ PanelWindow {
}
}
}
}
@@ -65,12 +65,16 @@ Scope {
function record() {
root.action = RegionSelection.SnipAction.Record
root.selectionMode = RegionSelection.SelectionMode.RectCorners
// If already open then re-trigger to stop recording
if (GlobalStates.regionSelectorOpen) GlobalStates.regionSelectorOpen = false
GlobalStates.regionSelectorOpen = true
}
function recordWithSound() {
root.action = RegionSelection.SnipAction.RecordWithSound
root.selectionMode = RegionSelection.SelectionMode.RectCorners
// If already open then re-trigger to stop recording
if (GlobalStates.regionSelectorOpen) GlobalStates.regionSelectorOpen = false
GlobalStates.regionSelectorOpen = true
}
@@ -1,10 +1,9 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import qs
import qs.modules.common
import qs.modules.common.widgets
RippleButton {
id: root
@@ -42,7 +42,7 @@ WindowDialog {
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "lightbulb"
buttonIcon: "check"
text: Translation.tr("Enable now")
checked: Hyprsunset.active
onCheckedChanged: {
@@ -102,14 +102,32 @@ WindowDialog {
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "flash_off"
text: Translation.tr("Enable")
buttonIcon: "filter"
text: Translation.tr("Content adjustment")
checked: HyprlandAntiFlashbangShader.enabled
onCheckedChanged: {
if (checked) HyprlandAntiFlashbangShader.enable()
else HyprlandAntiFlashbangShader.disable()
}
StyledToolTip {
text: Translation.tr("<b>Dims screen content</b> as needed.<br><br>Pros: Immediately responsive<br>Cons: Expensive and can hurt color accuracy<br><br><i>Uses a Hyprland screen shader</i>")
}
}
ConfigSwitch {
anchors {
left: parent.left
right: parent.right
}
iconSize: Appearance.font.pixelSize.larger
buttonIcon: "light_mode"
text: Translation.tr("Brightness adjustment")
checked: Config.options.light.antiFlashbang.enable
onCheckedChanged: {
Config.options.light.antiFlashbang.enable = checked;
}
StyledToolTip {
text: Translation.tr("Example use case: eroge on one workspace, dark Discord window on another")
text: Translation.tr("Adapts the <b>display (physical screen) brightness</b><br><br>Pros: Less expensive, retains colors<br>Cons: Not immediately responsive<br><br><i>Adjusts display brightness after each Hyprland IPC event</i>")
}
}
}
@@ -2,6 +2,7 @@
import argparse
import re
import os
import tempfile
def edit_hyprland_config(file_path, set_args, reset_args):
try:
@@ -54,8 +55,19 @@ def edit_hyprland_config(file_path, set_args, reset_args):
new_lines[-1] += '\n'
new_lines.append(f"{key} = {value}\n")
with open(file_path, 'w') as file:
file.writelines(new_lines)
dir_name = os.path.dirname(os.path.abspath(file_path))
temp_path = None
try:
with tempfile.NamedTemporaryFile(mode='w', dir=dir_name, delete=False) as temp_file:
temp_file.writelines(new_lines)
temp_path = temp_file.name
os.chmod(temp_path, os.stat(file_path).st_mode)
os.replace(temp_path, file_path)
except Exception as e:
if temp_path and os.path.exists(temp_path):
os.remove(temp_path)
print(f"Error saving file: {e}")
return
for key in reset_set:
print(f"Removed '{key}' from '{file_path}'")
@@ -0,0 +1,38 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.modules.common.models.hyprland
Singleton {
id: root
readonly property string shaderPath: Quickshell.shellPath("services/hyprlandAntiFlashbangShader/anti-flashbang.glsl")
property bool enabled: confOpt.value == shaderPath
function enable() {
HyprlandConfig.setMany({
"decoration:screen_shader": root.shaderPath,
"debug:damage_tracking": 1, // Turn off dmg tracking to prevent weird flashes. 1 = monitor only
});
}
function disable() {
HyprlandConfig.resetMany([
"decoration:screen_shader",
"debug:damage_tracking"
]);
}
function toggle() {
if (root.enabled) disable()
else enable()
}
HyprlandConfigOption {
id: confOpt
key: "decoration:screen_shader"
}
}
@@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import qs.modules.common
import qs.modules.common.functions
@@ -14,6 +14,8 @@ import qs.modules.common.functions
Singleton {
id: root
signal reloaded()
readonly property string configuratorScriptPath: Quickshell.shellPath("scripts/hyprland/hyprconfigurator.py")
readonly property string shellOverridesPath: FileUtils.trimFileProtocol(`${Directories.config}/hypr/hyprland/shellOverrides/main.conf`)
@@ -39,7 +41,7 @@ Singleton {
])
}
function resetMany(keys: var) {
function resetMany(keys: list<string>) {
let args = ""
for (let i = 0; i < keys.length; i++) {
args += `--reset "${keys[i]}" `
@@ -48,4 +50,14 @@ Singleton {
`${root.configuratorScriptPath} --file ${root.shellOverridesPath} ${args}` //
])
}
Connections {
target: Hyprland
function onRawEvent(event) {
if (event.name == "configreloaded") {
root.reloaded()
}
}
}
}
@@ -33,6 +33,14 @@ Singleton {
function unpin(itemId) {
Config.options.tray.pinnedItems = Config.options.tray.pinnedItems.filter(id => id !== itemId);
}
function isPinned(itemId) {
for (var i = 0; i < root.pinnedItems.length; i++) {
if (root.pinnedItems[i].id === itemId)
return true;
}
return false;
}
function togglePin(itemId) {
var pins = Config.options.tray.pinnedItems;
if (pins.includes(itemId)) {
@@ -0,0 +1,47 @@
#version 300 es
precision highp float;
in vec2 v_texcoord;
uniform sampler2D tex;
out vec4 fragColor;
float overlayOpacityForBrightness(float x) {
// Note: range 0 to 1
// Will a fancy curve help?... I'll have to experiment more at night
// float y = pow(x, 2.0) * 0.75;
// float y = (1.0 - exp(-x))*1.15;
// float y = (1.0 - exp(-pow((x-0.15), 0.6)))*1.18;
float y = x*0.75;
return min(max(y, 0.001), 1.0);
}
void main() {
// 1. Get the current pixel color
vec4 pixColor = texture(tex, v_texcoord);
// 2. Calculate average screen brightness
vec3 totalRGB = vec3(0.0);
float samples = 0.0;
// We use a nested loop to create a 10x10 grid (100 samples)
// This is dense enough to catch small icons/text but light enough to run fast.
for(float x = 0.05; x < 1.0; x += 0.1) {
for(float y = 0.05; y < 1.0; y += 0.1) {
totalRGB += texture(tex, vec2(x, y)).rgb;
samples++;
}
}
vec3 avgColor = totalRGB / samples;
float globalBrightness = dot(avgColor, vec3(0.2126, 0.7152, 0.0722));
// 3. Get the specific opacity for this brightness level
float opacity = overlayOpacityForBrightness(globalBrightness);
// 4. Apply the "black overlay" effect
vec3 outColor = mix(pixColor.rgb, vec3(0.0), opacity);
fragColor = vec4(outColor, pixColor.a);
}
@@ -604,5 +604,7 @@
"Recognize music": "Recognize music",
"Stroke width": "Stroke width",
"Use varying shapes for password characters": "Use varying shapes for password characters",
"Battery full": "Battery full"
"Battery full": "Battery full",
"Pin": "Pin",
"Unpin": "Unpin"
}
@@ -718,5 +718,7 @@
"No applications": "没有应用",
"Creativity": "创意",
"Move left": "左移",
"Pin to Start": "固定到“开始”屏幕"
"Pin to Start": "固定到“开始”屏幕",
"Pin": "固定",
"Unpin": "取消固定"
}
@@ -1,4 +1,4 @@
_commit='6e17efab83d3a5ad5d6e59bc08d26095c6660502'
_commit='7511545ee20664e3b8b8d3322c0ffe7567c56f7a'
# Useful links:
# https://git.outfoxxed.me/quickshell/quickshell/commits/branch/master
# https://aur.archlinux.org/packages/quickshell-git
@@ -16,16 +16,16 @@ url='https://git.outfoxxed.me/quickshell/quickshell'
options=(!strip)
license=('LGPL-3.0-only')
depends=(
'cpptrace'
'jemalloc'
'mesa'
'qt6-declarative'
'qt6-base'
'jemalloc'
'qt6-svg'
'libdrm'
'libpipewire'
'libxcb'
'wayland'
'libdrm'
'mesa'
'google-breakpad'
# NOTE: Below are custom dependencies of illogical-impulse
qt6-5compat
qt6-avif-image-plugin
@@ -44,15 +44,15 @@ depends=(
syntax-highlighting
)
makedepends=(
'spirv-tools'
'qt6-shadertools'
'wayland'
'wayland-protocols'
'cli11'
'ninja'
'cmake'
'git'
'ninja'
'qt6-shadertools'
'spirv-tools'
'vulkan-headers'
'wayland'
'wayland-protocols'
)
provides=("$_pkgname")
conflicts=("$_pkgname")