feat(nosignal): interface redesign — slim glass bar + floating popouts
Convert the caelestia shell to the NoSignal "design_handoff_quickshell_shell" spec: a slim translucent top bar with floating glass popouts, fixed dark-glass chrome + Material You dynamic accent, square corners, JetBrains Mono. - Theme singleton (services/Theme.qml): design tokens; accent = m3primary (dynamic), fixed glass/text, square radii. - New slim bar as its own PanelWindow (modules/nsbar/NsBar.qml), additive in shell.qml; reuses caelestia services + components. Helpers: BarPill, NsWorkspaces (numbered, active=accent pill), NsOverlay (popout host, one-at-a- time via services/NsShell.qml, outside-click + Esc), components/NsPanel + components/NsSwitch. - Old caelestia frame removed: BarWrapper collapsed; ContentWindow border 0 + borderTop 0; Exclusions zones 0. Drawer engine kept for launcher/OSD/lock. - 9 popouts (modules/nsbar/panels/): Calendar, QuickSettings, Network, Bluetooth, Notifications, Audio, SystemTray, Overview, PowerMenu. - Clock restyled (date muted + time DemiBold). - nexus (Settings) reskinned: dark-glass window + cards, square corners (Nexus.qml, common/ConnectedRect.qml) — keeps all pages/functionality. Launcher kept as-is. Font (JetBrainsMono) + rounding.scale=0 + Super+Alt+Space nexus bind are caelestia/hypr CONFIG and ship in the OS, not this fork. Next: full Material Symbols -> Nerd Font icon remap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8bada1d61a
commit
36f6f5a157
24 changed files with 2341 additions and 115 deletions
43
components/NsPanel.qml
Normal file
43
components/NsPanel.qml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal floating popout container: dark-glass card, 1px border, 4px radius.
|
||||
// Content goes in a ColumnLayout (default children). Sizes to its content height;
|
||||
// set `width` on the instance. A background MouseArea swallows clicks so clicking
|
||||
// inside the panel doesn't fall through to the overlay's close-catcher.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property int pad: Theme.size.panelPad
|
||||
property alias spacing: holder.spacing
|
||||
default property alias content: holder.data
|
||||
|
||||
// anchoring hint read by NsOverlay: "center" | "right" | "trigger"
|
||||
property string anchorMode: "right"
|
||||
|
||||
implicitHeight: holder.implicitHeight + root.pad * 2
|
||||
color: Theme.panelBg
|
||||
radius: Theme.radius.panel
|
||||
border.width: 1
|
||||
border.color: Theme.panelBorder
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
// swallow clicks/hover so they don't reach the overlay close-catcher
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: holder
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: root.pad
|
||||
spacing: Theme.size.gap
|
||||
}
|
||||
}
|
||||
46
components/NsSwitch.qml
Normal file
46
components/NsSwitch.qml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal toggle switch (animated 16px knob) used in panel headers.
|
||||
|
||||
import QtQuick
|
||||
import qs.services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool on: false
|
||||
signal toggled
|
||||
|
||||
implicitWidth: 38
|
||||
implicitHeight: 22
|
||||
radius: 11
|
||||
color: on ? Theme.accent : Theme.fillSubtle
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.hoverMs
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: root.on ? parent.width - width - 3 : 3
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitWidth: 16
|
||||
implicitHeight: 16
|
||||
radius: 8
|
||||
color: root.on ? Theme.onAccent : Theme.text
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 120
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.toggled()
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,9 @@ Item {
|
|||
readonly property int padding: Math.max(Tokens.padding.small, Config.border.thickness)
|
||||
readonly property int contentHeight: Tokens.sizes.bar.innerWidth + padding * 2
|
||||
readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentHeight : Config.border.thickness
|
||||
readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered)
|
||||
// NoSignal redesign: the old caelestia bar is replaced by the slim NsBar
|
||||
// PanelWindow. Collapse it (the drawer engine is kept for launcher/OSD/lock).
|
||||
readonly property bool shouldBeVisible: false
|
||||
property bool isHovered
|
||||
|
||||
function closeTray(): void {
|
||||
|
|
|
|||
|
|
@ -1,113 +1,34 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal redesign: center clock module — date (muted) + time (DemiBold),
|
||||
// JetBrains Mono. The hover/active pill is applied by the bar module wrapper;
|
||||
// this is just the content. Opens the Calendar popout.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Caelestia.Config
|
||||
import qs.components
|
||||
import qs.services
|
||||
|
||||
StyledRect {
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
readonly property color colour: Colours.palette.m3tertiary
|
||||
readonly property int padding: Config.bar.clock.background ? Tokens.padding.medium : Tokens.padding.extraSmall
|
||||
readonly property var font: Tokens.font.body.builders.small.scale(1.1)
|
||||
spacing: 8
|
||||
|
||||
implicitHeight: Tokens.sizes.bar.innerWidth
|
||||
implicitWidth: layout.implicitWidth + root.padding * 2
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
Loader {
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
asynchronous: true
|
||||
active: Config.bar.clock.showIcon
|
||||
visible: active
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "calendar_month"
|
||||
color: root.colour
|
||||
}
|
||||
text: Time.format("ddd d MMM")
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bar
|
||||
font.weight: Font.Normal
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: Config.bar.clock.showDate
|
||||
|
||||
horizontalAlignment: StyledText.AlignHCenter
|
||||
text: Time.format("ddd d")
|
||||
font: Tokens.font.body.small
|
||||
color: root.colour
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: root.padding
|
||||
Layout.bottomMargin: root.padding
|
||||
visible: Config.bar.clock.showDate
|
||||
implicitWidth: 1
|
||||
color: Colours.palette.m3outlineVariant
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: Time.hourStr
|
||||
font: {
|
||||
const scale = text === "11" ? 1.15 : Math.min(1.05, Math.max(hourMetrics.width, minMetrics.width) / hourMetrics.width);
|
||||
return root.font.width(scale * 100).letterSpacing(scale).build();
|
||||
}
|
||||
color: root.colour
|
||||
|
||||
TextMetrics {
|
||||
id: hourMetrics
|
||||
|
||||
font: root.font.build()
|
||||
text: Time.hourStr
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: ":"
|
||||
font: root.font.build()
|
||||
color: root.colour
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: Time.minuteStr
|
||||
font: {
|
||||
const scale = text === "11" ? 1.15 : Math.min(1.05, Math.max(hourMetrics.width, minMetrics.width) / minMetrics.width);
|
||||
return root.font.width(scale * 100).letterSpacing(scale).build();
|
||||
}
|
||||
color: root.colour
|
||||
|
||||
TextMetrics {
|
||||
id: minMetrics
|
||||
|
||||
font: root.font.build()
|
||||
text: Time.minuteStr
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
asynchronous: true
|
||||
active: GlobalConfig.services.useTwelveHourClock
|
||||
visible: active
|
||||
|
||||
sourceComponent: StyledText {
|
||||
text: Time.amPmStr.toLowerCase()
|
||||
font: Tokens.font.body.builders.small.scale(0.9).build()
|
||||
color: root.colour
|
||||
}
|
||||
}
|
||||
text: Time.format("hh:mm")
|
||||
color: Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bar
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ StyledWindow {
|
|||
|
||||
property real fsTransitionProg: hasFullscreen ? 1 : 0
|
||||
readonly property real sdfBorderOffset: 2 * fsTransitionProg // SDFs joins are not exact, so offset by 2px to ensure nothing shows
|
||||
readonly property real borderThickness: contentItem.Config.border.thickness * (1 - fsTransitionProg)
|
||||
readonly property real borderRounding: contentItem.Config.border.rounding * (1 - fsTransitionProg)
|
||||
readonly property real borderThickness: 0 // NoSignal: no screen-edge frame (NsBar owns the top)
|
||||
readonly property real borderRounding: 0 // NoSignal: no rounded screen frame
|
||||
readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg)
|
||||
readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ StyledWindow {
|
|||
radius: root.borderRounding
|
||||
borderLeft: root.borderThickness - anchors.margins - root.sdfBorderOffset
|
||||
borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset
|
||||
borderTop: bar.implicitHeight - anchors.margins - root.sdfBorderOffset
|
||||
borderTop: -anchors.margins - root.sdfBorderOffset // NoSignal: no top frame
|
||||
borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Scope {
|
|||
|
||||
ExclusionZone {
|
||||
anchors.top: true
|
||||
exclusiveZone: root.bar.exclusiveZone
|
||||
exclusiveZone: 0 // NoSignal: NsBar owns the top exclusive zone
|
||||
}
|
||||
|
||||
ExclusionZone {
|
||||
|
|
@ -32,7 +32,7 @@ Scope {
|
|||
component ExclusionZone: StyledWindow {
|
||||
screen: root.screen
|
||||
name: "border-exclusion"
|
||||
exclusiveZone: contentItem.Config.border.thickness
|
||||
exclusiveZone: 0 // NoSignal: no screen-edge frame
|
||||
mask: Region {}
|
||||
implicitWidth: 1
|
||||
implicitHeight: 1
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Item {
|
|||
|
||||
onClose: root.close()
|
||||
}
|
||||
property color blobColour: Colours.tPalette.m3surfaceContainerLow
|
||||
property color blobColour: Theme.panelBg // NoSignal: fixed dark-glass window surface
|
||||
|
||||
signal close
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ Item {
|
|||
BlobGroup {
|
||||
id: blobGroup
|
||||
|
||||
smoothing: root.Tokens.rounding.medium
|
||||
smoothing: 0 // NoSignal: crisp corners (no organic blob smoothing)
|
||||
color: root.blobColour
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ Item {
|
|||
anchors.fill: parent
|
||||
group: blobGroup
|
||||
opacity: root.blobColour.a
|
||||
radius: Tokens.rounding.large
|
||||
radius: Theme.radius.panel // NoSignal: sharp 4px corners
|
||||
|
||||
borderLeft: navPane.width + navPane.anchors.margins * 2
|
||||
borderRight: Tokens.padding.medium
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ StyledRect {
|
|||
property bool first
|
||||
property bool last
|
||||
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
topLeftRadius: first ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall
|
||||
topRightRadius: first ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall
|
||||
bottomLeftRadius: last ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall
|
||||
bottomRightRadius: last ? Tokens.rounding.extraLarge : Tokens.rounding.extraSmall
|
||||
// NoSignal: dark-glass cards, 4px outer corners (connected-list look)
|
||||
color: Theme.fillSubtle
|
||||
topLeftRadius: first ? Theme.radius.panel : 0
|
||||
topRightRadius: first ? Theme.radius.panel : 0
|
||||
bottomLeftRadius: last ? Theme.radius.panel : 0
|
||||
bottomRightRadius: last ? Theme.radius.panel : 0
|
||||
}
|
||||
|
|
|
|||
46
modules/nsbar/BarPill.qml
Normal file
46
modules/nsbar/BarPill.qml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal bar module pill: hover-highlightable (radius 5, hover bg); when its
|
||||
// panel is open it uses accent.soft bg + accent foreground. Children go in a
|
||||
// centered Row. `active` drives the open/highlight state; `triggered()` fires on
|
||||
// click (the bar wires this to toggle its openPanel string).
|
||||
|
||||
import QtQuick
|
||||
import qs.services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool active: false
|
||||
property int hPad: 9
|
||||
signal triggered
|
||||
|
||||
default property alias content: inner.data
|
||||
|
||||
implicitWidth: inner.implicitWidth + hPad * 2
|
||||
implicitHeight: 26
|
||||
radius: Theme.radius.barModule
|
||||
color: active ? Theme.accentSoft : mouse.containsMouse ? Theme.hover : "transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.hoverMs
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: inner
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.triggered()
|
||||
}
|
||||
}
|
||||
257
modules/nsbar/NsBar.qml
Normal file
257
modules/nsbar/NsBar.qml
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal slim top bar (interface redesign 2026-06-14).
|
||||
// Flat 38px translucent-glass PanelWindow at the top edge. Three zones:
|
||||
// left : logo · | · workspaces · | · window-title
|
||||
// center: clock
|
||||
// right : media · bell · wifi · bt · volume · battery · | · power
|
||||
// Each interactive module is a BarPill (hover bg; active = accent.soft + accent
|
||||
// fg). `openPanel` is the single shell-level "one panel open at a time" string;
|
||||
// clicking a pill toggles it. The popup panels themselves are built next — for
|
||||
// now a click just highlights the pill. Reuses the existing service singletons.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Services.UPower
|
||||
import qs.services
|
||||
import qs.utils
|
||||
import qs.components
|
||||
import qs.modules.bar.components as BC
|
||||
import qs.modules.bar.components.workspaces as WS
|
||||
|
||||
Variants {
|
||||
model: Screens.screens
|
||||
|
||||
PanelWindow {
|
||||
id: win
|
||||
|
||||
required property ShellScreen modelData
|
||||
|
||||
screen: modelData
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "nsbar" // for Hyprland blur layerrule
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
|
||||
implicitHeight: Theme.size.barHeight
|
||||
exclusiveZone: Theme.size.barHeight
|
||||
|
||||
readonly property int hPad: 12
|
||||
readonly property int gap: 6
|
||||
|
||||
// ── Glass surface + 1px bottom border ──────────────────────────────
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.barBg
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 1
|
||||
color: Theme.barBorder
|
||||
}
|
||||
}
|
||||
|
||||
// ── Left zone ──────────────────────────────────────────────────────
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: win.hPad
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: win.gap
|
||||
|
||||
BC.OsIcon {}
|
||||
|
||||
Divider {}
|
||||
|
||||
NsWorkspaces {}
|
||||
|
||||
Divider {}
|
||||
|
||||
BarPill {
|
||||
id: titlePill
|
||||
|
||||
active: NsShell.open === "overview"
|
||||
onTriggered: NsShell.toggle("overview", mapToItem(null, width / 2, 0).x)
|
||||
|
||||
MaterialIcon {
|
||||
text: "desktop_windows"
|
||||
color: titlePill.active ? Theme.accent : Theme.textMuted
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Hypr.activeToplevel?.title ?? "Desktop"
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, 220)
|
||||
color: titlePill.active ? Theme.accent : Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Center zone: clock ─────────────────────────────────────────────
|
||||
BarPill {
|
||||
id: clockPill
|
||||
|
||||
anchors.centerIn: parent
|
||||
active: NsShell.open === "calendar"
|
||||
onTriggered: NsShell.toggle("calendar", mapToItem(null, width / 2, 0).x)
|
||||
|
||||
BC.Clock {}
|
||||
}
|
||||
|
||||
// ── Right zone ─────────────────────────────────────────────────────
|
||||
RowLayout {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: win.hPad
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: win.gap
|
||||
|
||||
// Media / now-playing
|
||||
BarPill {
|
||||
id: mediaPill
|
||||
|
||||
visible: Players.active !== null
|
||||
active: NsShell.open === "quicksettings"
|
||||
onTriggered: NsShell.toggle("quicksettings", mapToItem(null, width / 2, 0).x)
|
||||
|
||||
MaterialIcon {
|
||||
text: Players.active?.isPlaying ? "pause" : "music_note"
|
||||
color: mediaPill.active ? Theme.accent : Theme.green
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Players.active?.trackTitle || "Awake"
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, 90)
|
||||
color: mediaPill.active ? Theme.accent : Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bar
|
||||
}
|
||||
}
|
||||
|
||||
// System tray
|
||||
IconPill {
|
||||
icon: "widgets"
|
||||
panel: "tray"
|
||||
}
|
||||
|
||||
// Notifications bell (with unread dot overlaid on the icon)
|
||||
BarPill {
|
||||
id: bellPill
|
||||
|
||||
active: NsShell.open === "notifications"
|
||||
onTriggered: NsShell.toggle("notifications", mapToItem(null, width / 2, 0).x)
|
||||
|
||||
MaterialIcon {
|
||||
text: Notifs.notClosed.length > 0 ? "notifications" : "notifications_none"
|
||||
color: bellPill.active ? Theme.accent : Theme.text
|
||||
|
||||
Rectangle {
|
||||
visible: Notifs.notClosed.length > 0
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.rightMargin: -1
|
||||
anchors.topMargin: 1
|
||||
implicitWidth: 6
|
||||
implicitHeight: 6
|
||||
radius: 3
|
||||
color: Theme.accent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wi-Fi
|
||||
IconPill {
|
||||
icon: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off"
|
||||
panel: "network"
|
||||
}
|
||||
|
||||
// Bluetooth
|
||||
IconPill {
|
||||
icon: {
|
||||
if (!Bluetooth.defaultAdapter?.enabled)
|
||||
return "bluetooth_disabled";
|
||||
if (Bluetooth.devices.values.some(d => d.connected))
|
||||
return "bluetooth_connected";
|
||||
return "bluetooth";
|
||||
}
|
||||
panel: "bluetooth"
|
||||
}
|
||||
|
||||
// Volume
|
||||
IconPill {
|
||||
icon: Icons.getVolumeIcon(Audio.volume, Audio.muted)
|
||||
panel: "audio"
|
||||
}
|
||||
|
||||
// Battery / Quick Settings — always present (control-centre trigger):
|
||||
// battery + % on laptops, a "tune" icon on desktops.
|
||||
BarPill {
|
||||
id: batteryPill
|
||||
|
||||
readonly property bool laptop: UPower.displayDevice.isLaptopBattery
|
||||
active: NsShell.open === "quicksettings"
|
||||
onTriggered: NsShell.toggle("quicksettings", mapToItem(null, width / 2, 0).x)
|
||||
|
||||
MaterialIcon {
|
||||
text: batteryPill.laptop ? Icons.getBatteryIcon(UPower.displayDevice.percentage, [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state)) : "tune"
|
||||
color: batteryPill.active ? Theme.accent : !batteryPill.laptop || !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? Theme.text : Theme.danger
|
||||
fill: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: batteryPill.laptop
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Math.round(UPower.displayDevice.percentage * 100) + "%"
|
||||
color: batteryPill.active ? Theme.accent : Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bar
|
||||
}
|
||||
}
|
||||
|
||||
Divider {}
|
||||
|
||||
// Power
|
||||
IconPill {
|
||||
icon: "power_settings_new"
|
||||
panel: "power"
|
||||
iconColour: Theme.danger
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reusable bits ──────────────────────────────────────────────────
|
||||
component Divider: Rectangle {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
implicitWidth: 1
|
||||
implicitHeight: 16
|
||||
color: Theme.barBorder
|
||||
}
|
||||
|
||||
// Icon-only pill bound to the shell openPanel state
|
||||
component IconPill: BarPill {
|
||||
id: p
|
||||
|
||||
property string icon
|
||||
property string panel
|
||||
property color iconColour: Theme.text
|
||||
|
||||
active: NsShell.open === panel
|
||||
onTriggered: NsShell.toggle(panel, mapToItem(null, width / 2, 0).x)
|
||||
|
||||
MaterialIcon {
|
||||
text: p.icon
|
||||
color: p.active ? Theme.accent : p.iconColour
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
161
modules/nsbar/NsOverlay.qml
Normal file
161
modules/nsbar/NsOverlay.qml
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal popup overlay: one full-screen layer per screen that hosts the active
|
||||
// floating panel (driven by NsShell.open). A transparent click-catcher behind the
|
||||
// panel closes on outside-click; Escape also closes. Panels are positioned below
|
||||
// the bar, anchored center / right / under-trigger per their `anchorMode`.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.services
|
||||
import "panels" as Panels
|
||||
|
||||
Variants {
|
||||
model: Screens.screens
|
||||
|
||||
PanelWindow {
|
||||
id: ov
|
||||
|
||||
required property ShellScreen modelData
|
||||
|
||||
screen: modelData
|
||||
visible: NsShell.open !== ""
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "nspanels"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
|
||||
anchors.top: true
|
||||
anchors.bottom: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
exclusiveZone: 0
|
||||
|
||||
// outside-click closes
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: NsShell.close()
|
||||
}
|
||||
|
||||
// Escape closes
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: ov.visible
|
||||
Keys.onEscapePressed: NsShell.close()
|
||||
}
|
||||
|
||||
// full-screen modal: Power Menu
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: NsShell.open === "power"
|
||||
sourceComponent: powerC
|
||||
|
||||
Component {
|
||||
id: powerC
|
||||
|
||||
Panels.NsPowerMenu {}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
|
||||
active: NsShell.open !== "" && NsShell.open !== "power"
|
||||
y: Theme.size.barHeight + 6
|
||||
x: {
|
||||
if (!item)
|
||||
return 0;
|
||||
const w = item.implicitWidth;
|
||||
const mode = item.anchorMode ?? "right";
|
||||
if (mode === "center")
|
||||
return Math.round((ov.width - w) / 2);
|
||||
if (mode === "trigger")
|
||||
return Math.max(8, Math.min(ov.width - w - 8, NsShell.anchorX - w / 2));
|
||||
return ov.width - w - 12; // right
|
||||
}
|
||||
|
||||
// small enter slide
|
||||
opacity: active ? 1 : 0
|
||||
transform: Translate {
|
||||
y: loader.active ? 0 : -5
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 120
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: {
|
||||
switch (NsShell.open) {
|
||||
case "calendar":
|
||||
return calendarC;
|
||||
case "quicksettings":
|
||||
return quickSettingsC;
|
||||
case "network":
|
||||
return networkC;
|
||||
case "bluetooth":
|
||||
return bluetoothC;
|
||||
case "notifications":
|
||||
return notificationsC;
|
||||
case "audio":
|
||||
return audioC;
|
||||
case "tray":
|
||||
return trayC;
|
||||
case "overview":
|
||||
return overviewC;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: calendarC
|
||||
|
||||
Panels.NsCalendar {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: quickSettingsC
|
||||
|
||||
Panels.NsQuickSettings {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: networkC
|
||||
|
||||
Panels.NsNetwork {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: bluetoothC
|
||||
|
||||
Panels.NsBluetooth {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notificationsC
|
||||
|
||||
Panels.NsNotifications {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: audioC
|
||||
|
||||
Panels.NsAudio {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: trayC
|
||||
|
||||
Panels.NsSystemTray {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: overviewC
|
||||
|
||||
Panels.NsOverview {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
modules/nsbar/NsWorkspaces.qml
Normal file
85
modules/nsbar/NsWorkspaces.qml
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal workspaces module: numbered 1..N, no capsule. Active = filled accent
|
||||
// pill with dark (onAccent) text + workspace name (e.g. "2 code"); occupied =
|
||||
// bright number; empty = faint number. Click switches workspace.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Caelestia.Config
|
||||
import qs.services
|
||||
import qs.components
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: Config.bar.workspaces.shown
|
||||
|
||||
delegate: Rectangle {
|
||||
id: ws
|
||||
|
||||
required property int index
|
||||
readonly property int wsId: index + 1
|
||||
readonly property var hw: Hypr.workspaces.values.find(w => w.id === wsId) ?? null
|
||||
readonly property bool active: Hypr.activeWsId === wsId
|
||||
readonly property bool occupied: (hw?.lastIpcObject.windows ?? 0) > 0
|
||||
readonly property string wsName: {
|
||||
const n = hw?.name ?? "";
|
||||
return n && n !== String(wsId) && !n.startsWith("special") ? n : "";
|
||||
}
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
implicitWidth: row.implicitWidth + (active ? 16 : 10)
|
||||
implicitHeight: 22
|
||||
radius: Theme.radius.barModule
|
||||
color: active ? Theme.accent : mouse.containsMouse ? Theme.hover : "transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.hoverMs
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: 120
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
StyledText {
|
||||
text: ws.wsId
|
||||
color: ws.active ? Theme.onAccent : ws.occupied ? Theme.text : Theme.textFaint
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bar
|
||||
font.weight: ws.active ? Font.DemiBold : Font.Normal
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: ws.active && ws.wsName !== ""
|
||||
text: ws.wsName
|
||||
color: Theme.onAccent
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bar
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Hypr.dispatch(`workspace ${ws.wsId}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
162
modules/nsbar/panels/NsAudio.qml
Normal file
162
modules/nsbar/panels/NsAudio.qml
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal Audio Mixer popout (off the volume pill). Output device row, master
|
||||
// slider, per-app application sliders, input (mic) slider. Wired to Pipewire
|
||||
// via the Audio service.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.components
|
||||
|
||||
NsPanel {
|
||||
id: root
|
||||
|
||||
implicitWidth: 340
|
||||
anchorMode: "right"
|
||||
|
||||
// output device
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
MaterialIcon {
|
||||
text: "speaker"
|
||||
color: Theme.textMuted
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: Audio.sink?.description || Audio.sink?.name || "Output"
|
||||
color: Theme.text
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
text: "expand_more"
|
||||
color: Theme.textFaint
|
||||
}
|
||||
}
|
||||
|
||||
AudioSlider {
|
||||
icon: Audio.muted ? "volume_off" : "volume_up"
|
||||
value: Audio.volume
|
||||
onMoved: v => Audio.setVolume(v)
|
||||
}
|
||||
|
||||
// applications
|
||||
StyledText {
|
||||
visible: Audio.streams.length > 0
|
||||
text: "APPLICATIONS"
|
||||
color: Theme.textFaint
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.label
|
||||
font.letterSpacing: Theme.font.trackingLabel
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Audio.streams
|
||||
|
||||
delegate: AudioSlider {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
icon: "graphic_eq"
|
||||
label: modelData?.properties?.["application.name"] || modelData?.description || modelData?.name || "App"
|
||||
value: modelData?.audio?.volume ?? 0
|
||||
onMoved: v => {
|
||||
if (modelData?.audio)
|
||||
modelData.audio.volume = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// input
|
||||
StyledText {
|
||||
text: "INPUT"
|
||||
Layout.topMargin: 2
|
||||
color: Theme.textFaint
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.label
|
||||
font.letterSpacing: Theme.font.trackingLabel
|
||||
}
|
||||
|
||||
AudioSlider {
|
||||
icon: Audio.sourceMuted ? "mic_off" : "mic"
|
||||
value: Audio.sourceVolume
|
||||
onMoved: v => Audio.setSourceVolume(v)
|
||||
}
|
||||
|
||||
component AudioSlider: ColumnLayout {
|
||||
id: s
|
||||
|
||||
property string icon
|
||||
property string label: ""
|
||||
property real value: 0
|
||||
signal moved(real v)
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 3
|
||||
|
||||
StyledText {
|
||||
visible: s.label !== ""
|
||||
text: s.label
|
||||
color: Theme.textMuted
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.meta
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
MaterialIcon {
|
||||
text: s.icon
|
||||
color: Theme.textMuted
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 6
|
||||
radius: 3
|
||||
color: Theme.fillSubtle
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * Math.max(0, Math.min(1, s.value))
|
||||
height: parent.height
|
||||
radius: 3
|
||||
color: Theme.accent
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: parent.width * Math.max(0, Math.min(1, s.value)) - width / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitWidth: 14
|
||||
implicitHeight: 14
|
||||
radius: 7
|
||||
color: Theme.accent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: e => s.moved(Math.max(0, Math.min(1, e.x / width)))
|
||||
onPositionChanged: e => {
|
||||
if (pressed)
|
||||
s.moved(Math.max(0, Math.min(1, e.x / width)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Math.round(s.value * 100) + "%"
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.meta
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
150
modules/nsbar/panels/NsBluetooth.qml
Normal file
150
modules/nsbar/panels/NsBluetooth.qml
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal Bluetooth popout (off the BT pill). Title + switch; device rows (icon
|
||||
// tile peach-tinted if connected, name, status, battery %). Footer "Bluetooth
|
||||
// settings". Wired to Quickshell.Bluetooth (BlueZ).
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Bluetooth
|
||||
import qs.services
|
||||
import qs.utils
|
||||
import qs.components
|
||||
|
||||
NsPanel {
|
||||
id: root
|
||||
|
||||
implicitWidth: 340
|
||||
anchorMode: "right"
|
||||
|
||||
readonly property var adapter: Bluetooth.defaultAdapter
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: "Bluetooth"
|
||||
color: Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.panelTitle
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NsSwitch {
|
||||
on: root.adapter?.enabled ?? false
|
||||
onToggled: if (root.adapter)
|
||||
root.adapter.enabled = !root.adapter.enabled
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
visible: root.adapter?.enabled ?? false
|
||||
|
||||
Repeater {
|
||||
model: Bluetooth.devices
|
||||
|
||||
delegate: Rectangle {
|
||||
id: row
|
||||
|
||||
required property BluetoothDevice modelData
|
||||
readonly property bool connected: modelData?.connected ?? false
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 44
|
||||
radius: Theme.radius.button
|
||||
color: ma.containsMouse ? Theme.hover : "transparent"
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: 30
|
||||
implicitHeight: 30
|
||||
radius: 0
|
||||
color: row.connected ? Theme.accentSoft : Theme.fillSubtle
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
text: Icons.getBluetoothIcon(row.modelData?.icon ?? "")
|
||||
color: row.connected ? Theme.accent : Theme.text
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
text: row.modelData?.name ?? "?"
|
||||
color: Theme.text
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
StyledText {
|
||||
text: row.connected ? "Connected" : (row.modelData?.paired ?? false) ? "Paired" : "Not connected"
|
||||
color: row.connected ? Theme.accent : Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.meta
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: row.connected && (row.modelData?.battery ?? 0) > 0
|
||||
text: Math.round((row.modelData?.battery ?? 0) * 100) + "%"
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.meta
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: Bluetooth.devices.values.length === 0
|
||||
text: "No devices"
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: !(root.adapter?.enabled ?? false)
|
||||
text: "Bluetooth is off"
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 4
|
||||
implicitHeight: 1
|
||||
color: Theme.barBorder
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Bluetooth settings"
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
175
modules/nsbar/panels/NsCalendar.qml
Normal file
175
modules/nsbar/panels/NsCalendar.qml
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal Calendar popout (off the center clock). Month grid (Mon-first),
|
||||
// today = filled accent circle. Below a divider, a TODAY events section
|
||||
// (empty-state for now; real events can be wired from ICS/khal later).
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.components
|
||||
|
||||
NsPanel {
|
||||
id: root
|
||||
|
||||
implicitWidth: 304
|
||||
anchorMode: "center"
|
||||
|
||||
property int monthOffset: 0
|
||||
readonly property date base: new Date(Time.date.getFullYear(), Time.date.getMonth() + monthOffset, 1)
|
||||
readonly property int year: base.getFullYear()
|
||||
readonly property int month: base.getMonth()
|
||||
readonly property bool isCurrentMonth: monthOffset === 0
|
||||
readonly property int todayDate: Time.date.getDate()
|
||||
|
||||
readonly property var cells: {
|
||||
const first = new Date(year, month, 1);
|
||||
const lead = (first.getDay() + 6) % 7; // Monday-first
|
||||
const dim = new Date(year, month + 1, 0).getDate();
|
||||
const arr = [];
|
||||
for (let i = 0; i < lead; i++)
|
||||
arr.push(0);
|
||||
for (let d = 1; d <= dim; d++)
|
||||
arr.push(d);
|
||||
return arr;
|
||||
}
|
||||
|
||||
// ── Header: month + year, prev/next ──────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
text: root.base.toLocaleString(Qt.locale(), "MMMM")
|
||||
color: Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.panelTitle
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.year
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NavBtn {
|
||||
icon: "chevron_left"
|
||||
onActivated: root.monthOffset--
|
||||
}
|
||||
|
||||
NavBtn {
|
||||
icon: "chevron_right"
|
||||
onActivated: root.monthOffset++
|
||||
}
|
||||
}
|
||||
|
||||
// ── Grid: weekday header + days ──────────────────────────────────────
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
columns: 7
|
||||
rowSpacing: 2
|
||||
columnSpacing: 0
|
||||
|
||||
Repeater {
|
||||
model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
|
||||
|
||||
StyledText {
|
||||
required property string modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: modelData
|
||||
color: Theme.textFaint
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.label
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.cells
|
||||
|
||||
Item {
|
||||
required property int modelData
|
||||
readonly property bool today: root.isCurrentMonth && modelData === root.todayDate
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 30
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
visible: parent.modelData > 0
|
||||
implicitWidth: 26
|
||||
implicitHeight: 26
|
||||
radius: 13
|
||||
color: parent.today ? Theme.accent : "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: parent.parent.modelData > 0 ? parent.parent.modelData : ""
|
||||
color: parent.parent.today ? Theme.onAccent : Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Divider ──────────────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 4
|
||||
implicitHeight: 1
|
||||
color: Theme.barBorder
|
||||
}
|
||||
|
||||
// ── TODAY events ─────────────────────────────────────────────────────
|
||||
StyledText {
|
||||
text: "TODAY"
|
||||
color: Theme.textFaint
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.label
|
||||
font.letterSpacing: Theme.font.trackingLabel
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "No events"
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
|
||||
component NavBtn: Rectangle {
|
||||
property string icon
|
||||
signal activated
|
||||
|
||||
implicitWidth: 24
|
||||
implicitHeight: 24
|
||||
radius: Theme.radius.button
|
||||
color: ma.containsMouse ? Theme.hover : "transparent"
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
text: parent.icon
|
||||
color: Theme.textMuted
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: parent.activated()
|
||||
}
|
||||
}
|
||||
}
|
||||
139
modules/nsbar/panels/NsNetwork.qml
Normal file
139
modules/nsbar/panels/NsNetwork.qml
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal Network popout (off the Wi-Fi pill). Title + switch; list of networks
|
||||
// (signal glyph accent if connected, SSID, Connected label, lock if secured);
|
||||
// connected row = accent.soft. Footer "Network settings". Wired to Network/Nmcli.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.utils
|
||||
import qs.components
|
||||
|
||||
NsPanel {
|
||||
id: root
|
||||
|
||||
implicitWidth: 340
|
||||
anchorMode: "right"
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: "Wi-Fi"
|
||||
color: Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.panelTitle
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NsSwitch {
|
||||
on: Network.wifiEnabled
|
||||
onToggled: Network.toggleWifi()
|
||||
}
|
||||
}
|
||||
|
||||
// network list
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
visible: Network.wifiEnabled
|
||||
|
||||
Repeater {
|
||||
model: Network.networks
|
||||
|
||||
delegate: Rectangle {
|
||||
id: row
|
||||
|
||||
required property var modelData
|
||||
readonly property bool connected: modelData?.active ?? false
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
radius: Theme.radius.button
|
||||
color: connected ? Theme.accentSoft : ma.containsMouse ? Theme.hover : "transparent"
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 10
|
||||
|
||||
MaterialIcon {
|
||||
text: Icons.getNetworkIcon(row.modelData?.strength ?? 0)
|
||||
color: row.connected ? Theme.accent : Theme.text
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
text: row.modelData?.ssid ?? "?"
|
||||
color: Theme.text
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
StyledText {
|
||||
visible: row.connected
|
||||
text: "Connected"
|
||||
color: Theme.accent
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.meta
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
visible: row.modelData?.isSecure ?? false
|
||||
text: "lock"
|
||||
color: Theme.textFaint
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: Network.networks.length === 0
|
||||
text: "No networks found"
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: !Network.wifiEnabled
|
||||
text: "Wi-Fi is off"
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
|
||||
// footer
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 4
|
||||
implicitHeight: 1
|
||||
color: Theme.barBorder
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Network settings"
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
171
modules/nsbar/panels/NsNotifications.qml
Normal file
171
modules/nsbar/panels/NsNotifications.qml
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal Notifications popout (off the bell). Header (count + DND + Clear),
|
||||
// scrollable cards (tinted icon, title, body, time), empty state. Uses Notifs.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.utils
|
||||
import qs.components
|
||||
|
||||
NsPanel {
|
||||
id: root
|
||||
|
||||
implicitWidth: 380
|
||||
anchorMode: "right"
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: "Notifications"
|
||||
color: Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.panelTitle
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "· " + Notifs.notClosed.length + " new"
|
||||
color: Theme.textFaint
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: 26
|
||||
implicitHeight: 26
|
||||
radius: 13
|
||||
color: Notifs.dnd ? Theme.accentSoft : dndMa.containsMouse ? Theme.hover : "transparent"
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
text: Notifs.dnd ? "do_not_disturb_on" : "dark_mode"
|
||||
color: Notifs.dnd ? Theme.accent : Theme.textMuted
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dndMa
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Notifs.dnd = !Notifs.dnd
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Clear"
|
||||
color: clearMa.containsMouse ? Theme.accent : Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
|
||||
MouseArea {
|
||||
id: clearMa
|
||||
anchors.fill: parent
|
||||
anchors.margins: -6
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
for (const n of Notifs.notClosed)
|
||||
n.notification?.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cards
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: Notifs.notClosed
|
||||
|
||||
delegate: Rectangle {
|
||||
id: card
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: cardRow.implicitHeight + 20
|
||||
radius: Theme.radius.button
|
||||
color: Theme.fillSubtle
|
||||
|
||||
RowLayout {
|
||||
id: cardRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
implicitWidth: 30
|
||||
implicitHeight: 30
|
||||
radius: 0
|
||||
color: Theme.accentSoft
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
text: Icons.getNotifIcon(card.modelData?.summary ?? "", card.modelData?.urgency ?? 1)
|
||||
color: Theme.accent
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: card.modelData?.summary ?? ""
|
||||
color: Theme.text
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
StyledText {
|
||||
text: card.modelData?.timeStr ?? ""
|
||||
color: Theme.textFaint
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.meta
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
visible: text !== ""
|
||||
text: card.modelData?.body ?? ""
|
||||
color: Theme.textMuted
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.meta
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: Notifs.notClosed.length === 0
|
||||
Layout.topMargin: 6
|
||||
text: "No notifications"
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
}
|
||||
113
modules/nsbar/panels/NsOverview.qml
Normal file
113
modules/nsbar/panels/NsOverview.qml
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal Workspaces overview (off the window-title module). Row of workspace
|
||||
// thumbnails (active = accent border + glow), number + name below, click to
|
||||
// switch. Wired to Hypr.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Caelestia.Config
|
||||
import qs.services
|
||||
import qs.components
|
||||
|
||||
NsPanel {
|
||||
id: root
|
||||
|
||||
implicitWidth: 5 * 124 + 16 * 2 + Theme.size.panelPad * 2 - 16
|
||||
anchorMode: "center"
|
||||
|
||||
readonly property int shown: Config.bar.workspaces.shown
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
text: "Workspaces"
|
||||
color: Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.panelTitle
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.shown + " desktops"
|
||||
color: Theme.textFaint
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 16
|
||||
|
||||
Repeater {
|
||||
model: root.shown
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: cell
|
||||
|
||||
required property int index
|
||||
readonly property int wsId: index + 1
|
||||
readonly property var hw: Hypr.workspaces.values.find(w => w.id === wsId) ?? null
|
||||
readonly property bool active: Hypr.activeWsId === wsId
|
||||
readonly property bool occupied: (hw?.lastIpcObject.windows ?? 0) > 0
|
||||
readonly property string wsName: {
|
||||
const n = hw?.name ?? "";
|
||||
return n && n !== String(wsId) && !n.startsWith("special") ? n : "";
|
||||
}
|
||||
|
||||
spacing: 6
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: 108
|
||||
implicitHeight: 68 // 16:10-ish
|
||||
radius: Theme.radius.tile
|
||||
color: Qt.rgba(0, 0, 0, cell.occupied ? 0.35 : 0.5)
|
||||
border.width: cell.active ? 2 : 1
|
||||
border.color: cell.active ? Theme.accent : Theme.panelBorder
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: cell.occupied
|
||||
text: "web_asset"
|
||||
color: Theme.textFaint
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
Hypr.dispatch(`workspace ${cell.wsId}`);
|
||||
NsShell.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 5
|
||||
|
||||
StyledText {
|
||||
text: cell.wsId
|
||||
color: cell.active ? Theme.accent : Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
font.weight: cell.active ? Font.DemiBold : Font.Normal
|
||||
}
|
||||
StyledText {
|
||||
visible: cell.wsName !== ""
|
||||
text: cell.wsName
|
||||
color: cell.active ? Theme.accent : Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
143
modules/nsbar/panels/NsPowerMenu.qml
Normal file
143
modules/nsbar/panels/NsPowerMenu.qml
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal Power Menu — full-screen modal overlay. Big clock + date, then 5
|
||||
// action tiles (Lock, Suspend, Log Out, Reboot, Shut Down=danger). Esc / click
|
||||
// scrim cancels. Rendered full-screen by NsOverlay (open === "power").
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.services
|
||||
import qs.components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
function run(cmd: string): void {
|
||||
Quickshell.execDetached(["sh", "-c", cmd]);
|
||||
NsShell.close();
|
||||
}
|
||||
|
||||
// scrim
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.overlayBg
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: NsShell.close()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 28
|
||||
|
||||
// big clock + date
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: Time.format("hh:mm")
|
||||
color: Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.clockBig
|
||||
font.weight: Font.Light
|
||||
font.letterSpacing: Theme.font.trackingClockBig
|
||||
}
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: Time.format("dddd, d MMMM")
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.body
|
||||
}
|
||||
}
|
||||
|
||||
// action tiles
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 14
|
||||
|
||||
Tile {
|
||||
icon: "lock"
|
||||
label: "Lock"
|
||||
onActivated: root.run("loginctl lock-session")
|
||||
}
|
||||
Tile {
|
||||
icon: "bedtime"
|
||||
label: "Suspend"
|
||||
onActivated: root.run("systemctl suspend")
|
||||
}
|
||||
Tile {
|
||||
icon: "logout"
|
||||
label: "Log Out"
|
||||
onActivated: root.run("hyprctl dispatch exit")
|
||||
}
|
||||
Tile {
|
||||
icon: "restart_alt"
|
||||
label: "Reboot"
|
||||
onActivated: root.run("systemctl reboot")
|
||||
}
|
||||
Tile {
|
||||
icon: "power_settings_new"
|
||||
label: "Shut Down"
|
||||
danger: true
|
||||
onActivated: root.run("systemctl poweroff")
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "Press Esc to cancel"
|
||||
color: Theme.textFaint
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
|
||||
component Tile: Rectangle {
|
||||
id: tile
|
||||
|
||||
property string icon
|
||||
property string label
|
||||
property bool danger: false
|
||||
signal activated
|
||||
|
||||
implicitWidth: 96
|
||||
implicitHeight: 96
|
||||
radius: Theme.radius.panel
|
||||
color: ma.containsMouse ? Theme.accentSoft : Theme.panelBg
|
||||
border.width: 1
|
||||
border.color: ma.containsMouse ? (tile.danger ? Theme.danger : Theme.accent) : Theme.panelBorder
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: tile.icon
|
||||
color: tile.danger ? Theme.danger : Theme.text
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: tile.label
|
||||
color: tile.danger ? Theme.danger : Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: tile.activated()
|
||||
}
|
||||
}
|
||||
}
|
||||
382
modules/nsbar/panels/NsQuickSettings.qml
Normal file
382
modules/nsbar/panels/NsQuickSettings.qml
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal Quick Settings popout (off the media / battery pill). Avatar + user +
|
||||
// uptime + power button; 2-col toggle grid; brightness + volume sliders; media
|
||||
// card. Wired to the real services (Network, Bluetooth, Notifs, Audio,
|
||||
// Brightness, Players); Night Light / Airplane / VPN are stateful toggles for now.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Bluetooth
|
||||
import qs.services
|
||||
import qs.utils
|
||||
import qs.components
|
||||
|
||||
NsPanel {
|
||||
id: root
|
||||
|
||||
implicitWidth: 380
|
||||
anchorMode: "right"
|
||||
|
||||
readonly property var brightMon: Brightness.monitors[0] ?? null
|
||||
|
||||
// ── Header ───────────────────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
radius: 20
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Vertical
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: Theme.accent
|
||||
}
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: Theme.blue
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: (SysInfo.user || "?").charAt(0).toUpperCase()
|
||||
color: Theme.onAccent
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.body
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
StyledText {
|
||||
text: SysInfo.user || "user"
|
||||
color: Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.panelTitle
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
StyledText {
|
||||
text: "uptime " + SysInfo.uptime
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: 34
|
||||
implicitHeight: 34
|
||||
radius: 17
|
||||
color: pwrMa.containsMouse ? Theme.accentSoft : Theme.fillSubtle
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
text: "power_settings_new"
|
||||
color: Theme.danger
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pwrMa
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: NsShell.show("power", 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Toggle grid ──────────────────────────────────────────────────────
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
columns: 2
|
||||
rowSpacing: 8
|
||||
columnSpacing: 8
|
||||
|
||||
QSToggle {
|
||||
icon: "wifi"
|
||||
label: "Wi-Fi"
|
||||
sub: Network.wifiEnabled ? Network.active?.ssid ?? "On" : "Off"
|
||||
on: Network.wifiEnabled
|
||||
onToggled: Network.toggleWifi()
|
||||
}
|
||||
QSToggle {
|
||||
icon: "bluetooth"
|
||||
label: "Bluetooth"
|
||||
sub: Bluetooth.defaultAdapter?.enabled ? "On" : "Off"
|
||||
on: Bluetooth.defaultAdapter?.enabled ?? false
|
||||
onToggled: if (Bluetooth.defaultAdapter)
|
||||
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled
|
||||
}
|
||||
QSToggle {
|
||||
icon: "do_not_disturb_on"
|
||||
label: "Do Not Disturb"
|
||||
sub: Notifs.dnd ? "On" : "Off"
|
||||
on: Notifs.dnd
|
||||
onToggled: Notifs.dnd = !Notifs.dnd
|
||||
}
|
||||
QSToggle {
|
||||
id: nightTog
|
||||
icon: "nightlight"
|
||||
label: "Night Light"
|
||||
sub: on ? "Warm 3500K" : "Off"
|
||||
onToggled: on = !on
|
||||
}
|
||||
QSToggle {
|
||||
id: airTog
|
||||
icon: "airplanemode_active"
|
||||
label: "Airplane"
|
||||
sub: on ? "On" : "Off"
|
||||
onToggled: on = !on
|
||||
}
|
||||
QSToggle {
|
||||
id: vpnTog
|
||||
icon: "vpn_key"
|
||||
label: "VPN"
|
||||
sub: on ? "On" : "Off"
|
||||
onToggled: on = !on
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sliders ──────────────────────────────────────────────────────────
|
||||
QSSlider {
|
||||
icon: "brightness_6"
|
||||
value: root.brightMon?.brightness ?? 0
|
||||
visible: root.brightMon !== null
|
||||
onMoved: v => root.brightMon?.setBrightness(v)
|
||||
}
|
||||
|
||||
QSSlider {
|
||||
icon: Audio.muted ? "volume_off" : "volume_up"
|
||||
value: Audio.volume
|
||||
onMoved: v => Audio.setVolume(v)
|
||||
}
|
||||
|
||||
// ── Media card ───────────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 4
|
||||
implicitHeight: 80
|
||||
radius: Theme.radius.tile
|
||||
color: Theme.fillSubtle
|
||||
visible: Players.active !== null
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: 56
|
||||
implicitHeight: 56
|
||||
radius: 0
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Vertical
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: Theme.accent
|
||||
}
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: Theme.blue
|
||||
}
|
||||
}
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
text: "music_note"
|
||||
color: Theme.onAccent
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: Players.active?.trackTitle || "Awake"
|
||||
color: Theme.text
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.body
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: Players.active?.trackArtist || ""
|
||||
color: Theme.textMuted
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
Layout.topMargin: 2
|
||||
|
||||
MediaBtn {
|
||||
icon: "skip_previous"
|
||||
onActivated: Players.previous()
|
||||
}
|
||||
MediaBtn {
|
||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||
onActivated: Players.togglePlaying()
|
||||
}
|
||||
MediaBtn {
|
||||
icon: "skip_next"
|
||||
onActivated: Players.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reusable bits ────────────────────────────────────────────────────
|
||||
component QSToggle: Rectangle {
|
||||
id: tile
|
||||
|
||||
property string icon
|
||||
property string label
|
||||
property string sub
|
||||
property bool on: false
|
||||
signal toggled
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 56
|
||||
radius: Theme.radius.button
|
||||
color: tile.on ? Theme.accentSoft : Theme.fillSubtle
|
||||
border.width: tile.on ? 1 : 0
|
||||
border.color: Theme.accent
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 11
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
implicitWidth: 30
|
||||
implicitHeight: 30
|
||||
radius: 15
|
||||
color: tile.on ? Theme.accent : Qt.rgba(1, 1, 1, 0.06)
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
text: tile.icon
|
||||
color: tile.on ? Theme.onAccent : Theme.text
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
StyledText {
|
||||
text: tile.label
|
||||
color: Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
StyledText {
|
||||
text: tile.sub
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.meta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: tile.toggled()
|
||||
}
|
||||
}
|
||||
|
||||
component QSSlider: RowLayout {
|
||||
id: s
|
||||
|
||||
property string icon
|
||||
property real value: 0
|
||||
signal moved(real v)
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
MaterialIcon {
|
||||
text: s.icon
|
||||
color: Theme.textMuted
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: track
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 6
|
||||
radius: 3
|
||||
color: Theme.fillSubtle
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * Math.max(0, Math.min(1, s.value))
|
||||
height: parent.height
|
||||
radius: 3
|
||||
color: Theme.accent
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: parent.width * Math.max(0, Math.min(1, s.value)) - width / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitWidth: 14
|
||||
implicitHeight: 14
|
||||
radius: 7
|
||||
color: Theme.accent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: e => s.moved(Math.max(0, Math.min(1, e.x / width)))
|
||||
onPositionChanged: e => {
|
||||
if (pressed)
|
||||
s.moved(Math.max(0, Math.min(1, e.x / width)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Math.round(s.value * 100) + "%"
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.meta
|
||||
}
|
||||
}
|
||||
|
||||
component MediaBtn: Rectangle {
|
||||
property string icon
|
||||
signal activated
|
||||
|
||||
implicitWidth: 28
|
||||
implicitHeight: 28
|
||||
radius: 14
|
||||
color: mbm.containsMouse ? Theme.hover : "transparent"
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
text: parent.icon
|
||||
color: Theme.text
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mbm
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: parent.activated()
|
||||
}
|
||||
}
|
||||
}
|
||||
100
modules/nsbar/panels/NsSystemTray.qml
Normal file
100
modules/nsbar/panels/NsSystemTray.qml
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal System Tray popout (off the tray pill). Grid of StatusNotifierItems;
|
||||
// left-click activates, right-click opens the item's menu. Uses SystemTray.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import qs.services
|
||||
import qs.components
|
||||
|
||||
NsPanel {
|
||||
id: root
|
||||
|
||||
implicitWidth: 260
|
||||
anchorMode: "right"
|
||||
|
||||
StyledText {
|
||||
text: "System Tray"
|
||||
color: Theme.text
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.panelTitle
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
columns: 4
|
||||
rowSpacing: 8
|
||||
columnSpacing: 8
|
||||
visible: SystemTray.items.values.length > 0
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: cell
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 4
|
||||
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitWidth: 44
|
||||
implicitHeight: 44
|
||||
radius: Theme.radius.tile
|
||||
color: cellMa.containsMouse ? Theme.hover : Theme.fillSubtle
|
||||
|
||||
Image {
|
||||
anchors.centerIn: parent
|
||||
width: 22
|
||||
height: 22
|
||||
source: cell.modelData?.icon ?? ""
|
||||
sourceSize.width: 44
|
||||
sourceSize.height: 44
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cellMa
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton)
|
||||
cell.modelData?.display(cell, 0, height);
|
||||
else if (cell.modelData?.onlyMenu)
|
||||
cell.modelData?.display(cell, 0, height);
|
||||
else
|
||||
cell.modelData?.activate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.maximumWidth: 50
|
||||
text: cell.modelData?.title || cell.modelData?.id || ""
|
||||
color: Theme.textMuted
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.meta
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: SystemTray.items.values.length === 0
|
||||
text: "No tray items"
|
||||
color: Theme.textMuted
|
||||
font.family: Theme.font.family
|
||||
font.pixelSize: Theme.font.bodySmall
|
||||
}
|
||||
}
|
||||
35
services/NsShell.qml
Normal file
35
services/NsShell.qml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal shell-level popup state. "One panel open at a time" — `open` is the
|
||||
// id of the active panel ("" = none). `anchorX` is the scene x of the trigger
|
||||
// (a bar pill) center, used to position under-trigger / right-anchored popups.
|
||||
// The NsBar pills call toggle(); the NsOverlay reads `open` and renders the panel.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string open: ""
|
||||
property real anchorX: 0
|
||||
|
||||
function toggle(id: string, x: real): void {
|
||||
if (root.open === id) {
|
||||
root.open = "";
|
||||
} else {
|
||||
root.open = id;
|
||||
root.anchorX = x;
|
||||
}
|
||||
}
|
||||
|
||||
function show(id: string, x: real): void {
|
||||
root.open = id;
|
||||
root.anchorX = x;
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
root.open = "";
|
||||
}
|
||||
}
|
||||
91
services/Theme.qml
Normal file
91
services/Theme.qml
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
// NoSignal design-system tokens (interface redesign 2026-06-14).
|
||||
// Fixed dark-glass chrome + text, but the ACCENT is Material You (wallpaper-
|
||||
// derived) per the user's choice. Components in the redesigned bar/panels read
|
||||
// from here instead of hard-coding hex values. See the design handoff README.
|
||||
//
|
||||
// NB: the "backdrop blur" in the spec is done at the compositor level (Hyprland
|
||||
// layerrules blur on the shell namespaces), not in QML — these fills are the
|
||||
// translucent surface colours that sit over that blur.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// ── Accent: Material You, dynamic from wallpaper ──────────────────────
|
||||
readonly property color accent: Colours.palette.m3primary
|
||||
readonly property color onAccent: Colours.palette.m3onPrimary // contrasting text/icon on accent fills
|
||||
// accent.soft — translucent accent for active-module bg / selected rows
|
||||
readonly property color accentSoft: Qt.alpha(accent, 0.15)
|
||||
readonly property color accentSofter: Qt.alpha(accent, 0.13)
|
||||
|
||||
// ── Secondary semantic accents (fixed) ───────────────────────────────
|
||||
readonly property color blue: "#8FB5D6" // avatar gradient, event tint
|
||||
readonly property color green: "#86C09A" // now-playing tint
|
||||
readonly property color danger: "#E0746A" // power button + Shut Down
|
||||
|
||||
// ── Surfaces: fixed dark glass ───────────────────────────────────────
|
||||
readonly property color barBg: Qt.rgba(15 / 255, 18 / 255, 24 / 255, 0.60)
|
||||
readonly property color barBorder: Qt.rgba(1, 1, 1, 0.06)
|
||||
readonly property color panelBg: Qt.rgba(17 / 255, 20 / 255, 26 / 255, 0.90)
|
||||
readonly property color panelBorder: Qt.rgba(1, 1, 1, 0.09)
|
||||
readonly property color overlayBg: Qt.rgba(8 / 255, 10 / 255, 14 / 255, 0.82)
|
||||
|
||||
// panel drop shadow + inset highlight
|
||||
readonly property color shadow: Qt.rgba(0, 0, 0, 0.55)
|
||||
readonly property color insetHighlight: Qt.rgba(1, 1, 1, 0.05)
|
||||
|
||||
// ── Text (fixed) ─────────────────────────────────────────────────────
|
||||
readonly property color text: Qt.rgba(233 / 255, 238 / 255, 243 / 255, 0.92)
|
||||
readonly property color textMuted: Qt.rgba(233 / 255, 238 / 255, 243 / 255, 0.50)
|
||||
readonly property color textFaint: Qt.rgba(233 / 255, 238 / 255, 243 / 255, 0.34)
|
||||
|
||||
// ── Fills / states (fixed) ───────────────────────────────────────────
|
||||
readonly property color hover: Qt.rgba(1, 1, 1, 0.07)
|
||||
readonly property color fillSubtle: Qt.rgba(1, 1, 1, 0.05)
|
||||
|
||||
// ── Typography ───────────────────────────────────────────────────────
|
||||
readonly property string fontFamily: "JetBrainsMono Nerd Font"
|
||||
readonly property QtObject font: QtObject {
|
||||
readonly property string family: root.fontFamily
|
||||
readonly property int bar: 12
|
||||
readonly property int panelTitle: 13
|
||||
readonly property int body: 12
|
||||
readonly property int bodySmall: 11
|
||||
readonly property int label: 10 // section labels (uppercase, tracked)
|
||||
readonly property int meta: 10 // times, %, device sub
|
||||
readonly property int clockBig: 52 // power-menu clock
|
||||
readonly property real trackingLabel: 1.6
|
||||
readonly property real trackingBar: -0.1
|
||||
readonly property real trackingClockBig: 2
|
||||
}
|
||||
|
||||
// ── Shape ────────────────────────────────────────────────────────────
|
||||
// NoSignal: no curved corners anywhere — all rectangular elements are square.
|
||||
readonly property QtObject radius: QtObject {
|
||||
readonly property int panel: 0
|
||||
readonly property int button: 0
|
||||
readonly property int barModule: 0
|
||||
readonly property int tile: 0
|
||||
readonly property int full: 1000 // genuine circles only (toggle icons, avatar, switch knob)
|
||||
}
|
||||
|
||||
// ── Spacing / sizing ─────────────────────────────────────────────────
|
||||
readonly property QtObject size: QtObject {
|
||||
readonly property int barHeight: 38 // exclusive zone
|
||||
readonly property int popoutTopRight: 44 // right-anchored panels
|
||||
readonly property int popoutTopCenter: 48 // centered panels
|
||||
readonly property int panelPad: 16 // 14-18
|
||||
readonly property int gap: 12 // 8-16
|
||||
readonly property int rowMin: 30 // panel row hit target
|
||||
readonly property int barIcon: 16 // 15-17
|
||||
}
|
||||
|
||||
// ── Animation ────────────────────────────────────────────────────────
|
||||
readonly property int hoverMs: 120
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import "modules"
|
||||
import "modules/drawers"
|
||||
import "modules/nsbar"
|
||||
import "modules/background"
|
||||
import "modules/areapicker"
|
||||
import "modules/lock"
|
||||
|
|
@ -17,7 +18,9 @@ ShellRoot {
|
|||
GSFLoader {}
|
||||
|
||||
Background {}
|
||||
Drawers {}
|
||||
Drawers {} // NoSignal: kept for launcher/OSD/lock; its bar is collapsed (see BarWrapper)
|
||||
NsBar {} // NoSignal redesign: slim glass top bar
|
||||
NsOverlay {} // NoSignal redesign: floating popout panels
|
||||
AreaPicker {}
|
||||
Lock {
|
||||
id: lock
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue