Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Ambxst is a highly customizable Wayland shell built with Quickshell. It provides
| `AxctlService` | Singleton | `modules/services/AxctlService.qml` | Compositor abstraction (focus, dispatch) |
| `StateService` | Singleton | `modules/services/StateService.qml` | JSON persistence for session state |
| `FocusGrabManager` | Singleton | `modules/services/FocusGrabManager.qml` | Input focus coordination |
| `SafeLoader` | Component | `modules/components/SafeLoader.qml` | Loader with error handling and fallback UI |

## CONVENTIONS
- **Singletons**: `pragma Singleton` + `Singleton { id: root }` for all services and global state.
Expand All @@ -114,6 +115,7 @@ Ambxst is a highly customizable Wayland shell built with Quickshell. It provides
```bash
# Run shell (requires Quickshell + Hyprland)
qs -p shell.qml

# Or via CLI wrapper:
./cli.sh

Expand Down
21 changes: 21 additions & 0 deletions cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,27 @@ if [ -n "${QML2_IMPORT_PATH:-}" ] && [ -z "${QML_IMPORT_PATH:-}" ]; then
export QML_IMPORT_PATH="$QML2_IMPORT_PATH"
fi

# Qt Quick GPU acceleration settings (user overrides preserved)
# Vulkan preferred for both AMD and NVIDIA (requires mesa/26.0+ or NVIDIA 525+)
# Falls back gracefully on older drivers.
# We probe with 'vulkaninfo --summary' rather than 'glxinfo' because glxinfo
# only indicates OpenGL/GLX availability — it does NOT confirm a Vulkan ICD
# is present. Setting QT_QUICK_BACKEND=vulkan without a working ICD causes Qt
# to silently fall back to software rendering.
if [[ -z "${QT_QUICK_BACKEND:-}" ]]; then
if vulkaninfo --summary >/dev/null 2>&1; then
export QT_QUICK_BACKEND=vulkan
else
export QT_QUICK_BACKEND=opengl
fi
fi

# Faster QML array operations
[[ -z "${QML_USE_TYPED_ARRAYS:-}" ]] && export QML_USE_TYPED_ARRAYS=1

# Multi-threaded rendering for smoother UI
[[ -z "${QT_THREADED_RENDERER:-}" ]] && export QT_THREADED_RENDERER=1

# Ensure config files exist - copy from preset if missing
ensure_config_files() {
local config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/ambxst/config"
Expand Down
2 changes: 2 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ install_dependencies() {
google-roboto-fonts google-roboto-mono-fonts dejavu-sans-fonts liberation-fonts
google-noto-fonts-common google-noto-cjk-fonts google-noto-emoji-fonts
mpvpaper matugen R-CRAN-phosphoricons adw-gtk3-theme quickshell unzip curl
vulkan-loader
)

log_info "Installing dependencies..."
Expand Down Expand Up @@ -177,6 +178,7 @@ install_dependencies() {
ttf-nerd-fonts-symbols
matugen gpu-screen-recorder wl-clip-persist mpvpaper gradia
quickshell ttf-phosphor-icons ttf-league-gothic adw-gtk-theme
vulkan-icd-loader
)

log_info "Installing dependencies with $AUR_HELPER..."
Expand Down
4 changes: 2 additions & 2 deletions modules/bar/BarContent.qml
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ Item {
readonly property int leftOuterMargin: (orientation === "horizontal" || barPosition === "left") ? barBg.outerMargin : 0
readonly property int rightOuterMargin: (orientation === "horizontal" || barPosition === "right") ? barBg.outerMargin : 0

readonly property int contentImplicitWidth: orientation === "horizontal" ? (horizontalLoader.item && horizontalLoader.item.implicitWidth !== undefined ? horizontalLoader.item.implicitWidth : 0) : (verticalLoader.item && verticalLoader.item.implicitWidth !== undefined ? verticalLoader.item.implicitWidth : 0)
readonly property int contentImplicitHeight: orientation === "horizontal" ? (horizontalLoader.item && horizontalLoader.item.implicitHeight !== undefined ? horizontalLoader.item.implicitHeight : 0) : (verticalLoader.item && verticalLoader.item.implicitHeight !== undefined ? verticalLoader.item.implicitHeight : 0)
readonly property int contentImplicitWidth: orientation === "horizontal" ? (horizontalLoader.item ? horizontalLoader.item.implicitWidth : 0) : (verticalLoader.item ? verticalLoader.item.implicitWidth : 0)
readonly property int contentImplicitHeight: orientation === "horizontal" ? (horizontalLoader.item ? horizontalLoader.item.implicitHeight : 0) : (verticalLoader.item ? verticalLoader.item.implicitHeight : 0)

readonly property int barTargetWidth: orientation === "vertical" ? (contentImplicitWidth + 2 * barPadding) : 0
readonly property int barTargetHeight: orientation === "horizontal" ? (contentImplicitHeight + 2 * barPadding) : 0
Expand Down
2 changes: 1 addition & 1 deletion modules/bar/systray/SysTrayItem.qml
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ MouseArea {
StyledToolTip {
show: root.isHovered
tooltipText: root.item.tooltipTitle || root.item.title
desciription: root.item.tooltipDescription || ""
description: root.item.tooltipDescription || ""
}

HoverHandler {
Expand Down
80 changes: 80 additions & 0 deletions modules/components/SafeLoader.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import QtQuick

/**
* SafeLoader - A Loader with built-in error handling and fallback UI
* Use this instead of plain Loader for components that may fail to load
*/
Item {
id: root

property var sourceComponent
property string placeholderText: "Loading..."
property bool showPlaceholder: true
property color placeholderColor: "#808080"
property var fallbackItem

// Internal loader — not exposed as public API to prevent callers from
// bypassing root.sourceComponent and writing to it directly.
Loader {
id: internalLoader
anchors.fill: parent
sourceComponent: root.sourceComponent
asynchronous: true

onStatusChanged: {
if (internalLoader.status === Loader.Error) {
console.error("[SafeLoader] Failed to load component:", internalLoader.errorString);
root.handleError(internalLoader.errorString);
}
}
}

signal loadError(string errorString)

function handleError(errorString) {
root.loadError(errorString);
}

// Read-only state properties
readonly property bool isLoading: internalLoader.status === Loader.Loading
readonly property bool hasError: internalLoader.status === Loader.Error
readonly property bool isReady: internalLoader.status === Loader.Ready

// Fallback content when loading or error
StyledRect {
id: placeholder
anchors.fill: parent
visible: root.showPlaceholder && (root.isLoading || root.hasError) && !root.fallbackItem
Comment on lines +44 to +47
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Provide a variant for SafeLoader's StyledRect placeholder

StyledRect requires its variant property, but this placeholder instance never sets one. As soon as SafeLoader is instantiated, QML will raise a required-property initialization error for this child and the fallback/loading UI cannot be created reliably. Assign a valid variant (for example "common" or "popup") on this StyledRect instance.

Useful? React with 👍 / 👎.

color: root.hasError ? "#20000000" : "transparent"

Text {
anchors.centerIn: parent
text: root.hasError ? "Failed to load" : root.placeholderText
color: root.placeholderColor
font.pixelSize: 14
}
}

// Container for a caller-supplied fallback item; shown on error only.
Item {
id: fallbackContainer
anchors.fill: parent
visible: root.fallbackItem !== null && root.hasError
z: 10
}

// Reparent a caller-supplied item into the fallback container so it
// becomes visible when the load fails.
function setFallback(item) {
root.fallbackItem = item;
item.parent = fallbackContainer;
}

// Retry loading by null-cycling sourceComponent (standard QML pattern).
function retry() {
var current = root.sourceComponent;
internalLoader.sourceComponent = null;
internalLoader.sourceComponent = current;
Comment on lines +76 to +77
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep sourceComponent binding intact in retry()

internalLoader.sourceComponent starts as a binding to root.sourceComponent, but retry() overwrites it imperatively (null then current). In QML, that assignment removes the original binding, so after the first retry later updates to root.sourceComponent no longer propagate to the loader. This can leave SafeLoader showing stale content when callers swap components after a retry.

Useful? React with 👍 / 👎.

}
}

14 changes: 7 additions & 7 deletions modules/components/StyledToolTip.qml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import qs.modules.components
ToolTip {
id: root
property string tooltipText: ""
property string desciription: ""
property string description: ""
property bool show: false

text: tooltipText
delay: 1000
delay: 700
timeout: -1
visible: show && tooltipText.length > 0

Expand All @@ -28,16 +28,16 @@ ToolTip {
Text {
text: root.tooltipText
color: Colors.overBackground
font.pixelSize: Config.theme.fontSize
font.pixelSize: Styling.fontSize(0)
font.weight: Font.Bold
font.family: Config.theme.font
}

Text {
text: root.desciription
visible: root.desciription.length > 0
color: Colors.overBackground
font.pixelSize: Config.theme.fontSize - 2
text: root.description
visible: root.description.length > 0
color: Qt.rgba(Colors.overBackground.r, Colors.overBackground.g, Colors.overBackground.b, 0.7)
font.pixelSize: Styling.fontSize(-2)
font.family: Config.theme.font
}
}
Expand Down
6 changes: 4 additions & 2 deletions modules/lockscreen/LockScreen.qml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ WlSessionLockSurface {
id: clockContainer
anchors.centerIn: parent
width: clockRow.width
height: hoursText.height + (hoursText.height * 0.5)
height: hoursText.height + (hoursText.height * 0.5) + 48
z: 10

property date currentTime: new Date()
Expand Down Expand Up @@ -275,7 +275,8 @@ WlSessionLockSurface {
repeat: true
onTriggered: clockContainer.currentTime = new Date()
}
}

}

// Music player (slides from left)
Item {
Expand Down Expand Up @@ -412,6 +413,7 @@ WlSessionLockSurface {
smooth: true
asynchronous: true
visible: status === Image.Ready
sourceSize: Qt.size(128, 128)

layer.enabled: true
layer.effect: MultiEffect {
Expand Down
20 changes: 16 additions & 4 deletions modules/notch/NotchAnimationBehavior.qml
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import QtQuick
import qs.config

// Comportamiento estándar para animaciones de elementos que aparecen en el notch
// Standard animation behavior for elements appearing inside the notch
Item {
id: root

// Propiedad para controlar la visibilidad con animaciones
// Controls visibility with animated entrance/exit
property bool isVisible: false

// Aplicar las animaciones estándar del notch
scale: isVisible ? 1.0 : 0.8
// Scale + opacity entrance — scale pops from 0.85 with OutBack overshoot
scale: isVisible ? 1.0 : 0.85
opacity: isVisible ? 1.0 : 0.0
visible: opacity > 0

// Subtle vertical translate: drop in from slightly above when appearing
property real slideOffset: isVisible ? 0 : -5
transform: Translate { y: root.slideOffset }

Behavior on slideOffset {
enabled: Config.animDuration > 0
NumberAnimation {
duration: Config.animDuration
easing.type: Easing.OutQuart
}
}

Behavior on scale {
enabled: Config.animDuration > 0
NumberAnimation {
Expand Down
68 changes: 47 additions & 21 deletions modules/shell/osd/OSD.qml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ PanelWindow {

color: "transparent"

visible: GlobalStates.osdVisible
// Keep the PanelWindow alive whenever visible OR while the exit animation
// is still playing (opacity > 0). Binding only to GlobalStates.osdVisible
// would hide the window immediately on dismiss, cutting the fade-out dead.
visible: GlobalStates.osdVisible || osdRect.opacity > 0

// Internal state for responsiveness
property real osdValue: 0
Expand All @@ -43,17 +46,38 @@ PanelWindow {
variant: "popup"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
implicitWidth: 220
implicitHeight: 52
implicitWidth: 240
implicitHeight: 56
radius: Styling.radius(16)

// Slide-in from below when visible, slide out down when hiding
property real slideY: GlobalStates.osdVisible ? 0 : 16
transform: Translate { y: osdRect.slideY }

Behavior on slideY {
enabled: Config.animDuration > 0
NumberAnimation {
duration: Config.animDuration
easing.type: Easing.OutQuart
}
}

opacity: GlobalStates.osdVisible ? 1.0 : 0.0
Behavior on opacity {
enabled: Config.animDuration > 0
NumberAnimation {
duration: Config.animDuration
easing.type: Easing.OutQuart
}
}

RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 24
anchors.leftMargin: 14
anchors.rightMargin: 16
anchors.topMargin: 8
anchors.bottomMargin: 8
spacing: 14
spacing: 12

Text {
id: iconText
Expand All @@ -67,7 +91,7 @@ PanelWindow {
}
}
font.family: Icons.font
font.pixelSize: 22
font.pixelSize: 20
color: Colors.overBackground
Layout.alignment: Qt.AlignVCenter

Expand Down Expand Up @@ -111,23 +135,26 @@ PanelWindow {
return "";
}
font.family: Config.theme.font
font.pixelSize: 15
font.pixelSize: Styling.fontSize(0)
font.bold: false
color: Colors.overBackground
Layout.alignment: Qt.AlignBottom
}

Item {
Layout.fillWidth: true
}
Item { Layout.fillWidth: true }

Text {
text: Math.round(root.osdValue * 100)
// Show percentage value; clamp display to avoid "-0%"
text: Math.max(0, Math.round(root.osdValue * 100)) + "%"
font.family: Config.theme.font
font.pixelSize: 15
font.pixelSize: Styling.fontSize(0)
font.bold: false
color: Colors.overBackground
Layout.alignment: Qt.AlignBottom

Behavior on text {
enabled: false // text changes should be instant
}
}
}

Expand All @@ -147,19 +174,18 @@ PanelWindow {
}
}

// Close on click or hover
// Hovering pauses the auto-hide timer; clicking dismisses immediately
MouseArea {
anchors.fill: parent
onEntered: {
hideTimer.stop();
hideTimer.triggered();
}
hoverEnabled: true
onEntered: hideTimer.stop()
onExited: hideTimer.restart()
onClicked: GlobalStates.osdVisible = false
}

Timer {
id: hideTimer
interval: 2500
interval: 3000
onTriggered: GlobalStates.osdVisible = false
}

Expand All @@ -172,7 +198,7 @@ PanelWindow {
}
}

// Services connections - Direct and responsive
// Service connections — direct and responsive
Connections {
target: Audio
function onVolumeChanged(volume, muted, node) {
Expand All @@ -194,7 +220,7 @@ PanelWindow {
Connections {
target: Brightness
function onBrightnessChanged(value, screen) {
// Check if the change happened on THIS screen or if it's a sync change
// Only react if the change is for this screen, or synced across all screens
if (!screen || !root.targetScreen || screen.name === root.targetScreen.name || Brightness.syncBrightness) {
root.osdValue = value;
root.osdMuted = false;
Expand Down
Loading