Compare commits

...
Sign in to create a new pull request.

24 commits

Author SHA1 Message Date
0ae8593113 fix(nosignal): F-sudo focus via hyprctl (stayfocused invalid in Hypr 0.55)
VM-cert of round 4 surfaced a Hyprland config-error banner: the shipped
`windowrule = stayfocused on, match:class nosignal-sudo` is rejected at parse
time — `stayfocused` is not a valid field in Hyprland 0.55's match: windowrule
grammar (only float/center/pin/size are). The line was skipped, so the prompt
never got the focus rule and an error banner showed on every boot.

Replace it: keep the dedicated nosignal-sudo class (float/center/pin/size, all
valid — set in the builder) and pull keyboard focus to the prompt from here via
`hyprctl dispatch focuswindow class:^(nosignal-sudo)$` (the handover's "or
activate it" option), nudged a few times to cover terminal startup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:26:30 +01:00
e42538896f fix(nosignal): hardware-test round 4 — Wi-Fi join, multi-monitor popouts, sudo focus
From the 2026-06-20 Acer muxless-Optimus hardware test:

- F-wifi (NsNetwork.qml): the Wi-Fi panel was display-only — no onClicked,
  no password field — so a secured network could not be joined from the GUI.
  Wire the existing Network service: click to connect; open/saved networks
  connect directly; secured-without-profile expands an inline password field
  (StyledTextField, echoMode Password). Also strip a stray NUL byte that the
  on-box edit left at the `?? " "` sentinel. HW-verified on the test laptop.
- F-multimon (NsShell.qml + NsBar.qml + NsOverlay.qml): bar popouts mirrored
  on every monitor. Add a `screen` identity to NsShell; pills pass their
  output name; each NsOverlay only renders when it owns the open panel ("" =
  unscoped, for the global power modal). HW-verified dual-monitor.
- F-sudo (SudoToggleRow.qml): the passwordless-sudo prompt used the shared
  TUI.float class with no focus rule, so keystrokes missed it and three blank
  tries tripped pam_faillock with no feedback. Use a dedicated `nosignal-sudo`
  class (builder ships float/center/pin/stayfocused rules) and call the new
  `nosignal-sudo-toggle enable-tui` wrapper, which keeps the terminal open on
  failure so the reason is visible.

Add .gitattributes (*.qml/*.conf/*.json text) for clean diffs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:49:18 +01:00
2cc089b41f fix(nosignal): hardware-test round 3 — notif Clear + Game Mode → DeckShift
F2: NsNotifications.qml "Clear" mirrors the service (Notifs.list.slice() →
n.close()) instead of n.notification?.dismiss() — cards now actually leave the
list, and it works for disk-restored notifications (whose notification is null).

C2: Toggles.qml gameMode delegate launches the guarded switch-to-gaming via
Quickshell.execDetached (debounced + disabled + 5s timer so a double-click can't
fire two SDDM restarts), and the tile is hidden entirely when DeckShift isn't
installed (Process probe for the gamescope session file).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 06:34:08 +01:00
44b257ce85 feat(nosignal): CachyOS repo toggle + wire Quick Settings placebos
- CachyRepoToggleRow.qml (Settings -> Services): sudoless CachyOS optimized
  repos + linux-cachyos kernel switch, auto-tier (v4>v3); polls
  nosignal-cachy-repo status, runs enable/disable in a floating kitty. Inserted
  after SudoToggleRow in ServicesPage.qml.
- NsQuickSettings.qml: Night Light now drives hyprsunset (tile bound to the real
  pgrep'd process, not a flag); Airplane drives nmcli + bluetoothctl and binds to
  real radio state; VPN placebo tile removed (no backend).

Both built + live-tested on the reference box (06-17 PM). Helper script, sudoers
and hyprsunset dep ship in the nosignal builder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:06:20 +01:00
53c5833de0 fix(nosignal): hardware-test feedback round 2 — weather, dashboard, tray
From the 2026-06-15 on-box hardware test (all 6 prior fixes confirmed; these
are the new findings, baked into the fork source rather than shipped as
pacman-hook patches, per the Phase-2 "we own the shell" approach):

- F-W1 (Weather.qml): fetch at startup. Stock only called reload() on a config
  CHANGE, so a saved location blanked after reboot and an empty/auto location
  never populated on a fresh boot. Add Component.onCompleted: reload() and
  switch the hourly Timer to reload() (re-detects auto/IP too).
- F-W3 (Weather.qml): 7-day forecast showed "undefined°" in °F mode — only
  maxTempC/minTempC were stored. Store rounded maxTempF/minTempF (and round °C).
- F-W2 (Weather.qml): empty/auto-IP location never populated — the in-shell
  ipinfo.io fetch didn't set loc (provider-specific; the city path works via
  the same Requests.get). Repair attempt: defensive JSON parse + error callback
  + fallback to geojs.io. Compiled Requests module not inspectable, so this is
  best-effort; a saved city still works regardless.
- F-D1a (modules/dashboard/*.qml): square the 25 card radii bound to the
  compiled Tokens.rounding.* Material defaults (which ignore shell.json
  rounding.scale=0) so the dashboard matches the square redesign.
- F-D1b (Glyphs.qml): remap the weather glyphs to the Material-weather family
  (every codepoint verified present in JetBrainsMono Nerd Font Mono); the old
  Font-Awesome codepoints were absent (rendered "?") and thunderstorm /
  snowing_heavy were unmapped.
- F-T1 (NsBar.qml): gate the system-tray pill on SystemTray.items count so it
  only shows when an app registers a tray icon (was opening an empty panel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:04:28 +01:00
f21e5b5eaf fix(nosignal): hardware-test feedback — additions icons, power menu, weather picker
From the 2026-06-15 hardware-test feedback (redesign passed end-to-end):

- Glyphs: map the 12 unmapped additions-page icon names (brush, cloud_sync,
  data_object, deployed_code, movie_edit, neurology, psychology, robot_2,
  smart_toy, sports_esports, videocam, vpn_lock) so the nexus Additional
  Software page stops rendering "?" boxes. Verified Nerd Font codepoints.
- Glyphs: cp() now uses String.fromCodePoint, not fromCharCode — the 5 MDI
  glyphs above 0xFFFF were being truncated by the 16-bit fromCharCode.
- Shortcuts: Super+Escape (session) now drives the redesigned full-screen
  Power Menu via NsShell.toggle("power"); old caelestia session panel is
  now dead code.
- nexus LanguageAndRegion: replace the "coming soon" weather placeholder with
  a real location input bound to GlobalConfig.services.weatherLocation (the
  Weather backend already geocodes / IP-auto-detects).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:02:53 +01:00
83c6b62c13 feat(nosignal): finish Nerd Font icon remap — core surfaces
Convert remaining reachable surfaces to NsIcon (launcher, utilities, windowinfo,
sidebar, dashboard, notification toast, shared controls IconTextButton/Menu/
SplitButton/ToggleButton/CollapsibleSection/CustomSpinBox, CoverArt). Handles
the `sourceComponent: MaterialIcon {` form and icon-via-alias controls.

- IconTextButton: alias -> plain `icon` property + bound NsIcon (cross-file alias
  to a custom property was rejected).
- Reverted IconButton `font:` (the controls-wide font->fontStyle sweep wrongly
  hit ButtonBase, which uses font not fontStyle).
- Glyphs map: +31 names (dashboard/utilities/launcher leftovers) — no "?" glyphs.

Old collapsed bar + old lock sub-components left as-is (unused, never render).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:59:28 +01:00
c4738ae974 fix(nosignal): notification toasts — below the bar + no blob curve
- Position the toast wrapper at y:46 so toasts clear the 38px NsBar (they were
  overlapping it; caelestia's Content negates the wrapper top margin, so the
  offset must be on the wrapper's y, not its margin).
- Fully collapse the notifsBg blob (visible:false + implicitWidth/Height 0) —
  opacity/visible alone don't hide a BlobGroup member, which left a rounded
  corner peeking under the square card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:14:36 +01:00
f6a2584d31 fix(nosignal): square notification toasts + guard lock screen.name
- Notification toasts: square the cards + round urgency/icon badges
  (Tokens.rounding.full ignored rounding.scale) and hide/flatten the notifsBg
  blob (deformAmount 0, radius 0, opacity 0) so toasts are flat square cards.
- LockSurface: guard screen?.name to silence a transient init warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:00:16 +01:00
dcce2440f6 fix(nosignal): square the OSD (volume/brightness) to match the design
The volume/brightness OSD popped out as a rounded caelestia blob in the dynamic
surface colour. Square it + match the design glass:
- osd/Content: draw a flat square Theme.panelBg + 1px border background.
- ContentWindow: osdBg blob deformAmount 0 + radius 0 + opacity 0 (hidden;
  the OSD now owns its background).
- FilledSlider: track + handle radius -> 0 (Tokens.rounding.full ignored
  rounding.scale, so it stayed round).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:54:35 +01:00
16de3f16fe feat(nosignal): NoSignal logo + V1 on About; fully square the UI
- About page: replace caelestia AnimatedLogo with the NoSignal logo
  (assets/nosignal-logo.png), title "NoSignal", version reads "V1".
- Square every remaining curved element to match the no-curves design:
  avatars, toggle/icon circles, power/media buttons, slider tracks+handles,
  NsSwitch track+knob, notification dot, calendar today, lock avatar -> radius 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:42:08 +01:00
7c06b808ae feat(nosignal): redesign lock screen to match handoff
Rewrite the WlSessionLockSurface visuals to the NoSignal lock design while
keeping the secure session-lock + PAM machinery (Pam buffer/handleKey/state,
WlSessionLock unlock):

- Blurred screencopy wallpaper + dark scrim; top status row (lock+Locked /
  wifi/bt/battery); centred 108px ExtraLight clock + date; gradient avatar +
  username; password field (key glyph, masked dots, peach enter glyph, error
  border) fed to pam.handleKey; bottom now-playing strip + "Press Enter to
  unlock". JetBrains Mono / Nerd Font (NsIcon) / Theme tokens.
- Escape does not dismiss; correct password (PAM) unlocks; wrong password shows
  inline error. Verified in VM: renders, masks, unlocks.
- Glyphs: add login (enter arrow) + key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:30:20 +01:00
83d5de1ba3 feat(nosignal): Nerd Font icons in IconButton (controls-wide)
Convert the shared IconButton control to render via NsIcon, so every icon-button
across the shell (nexus, lock, utilities, launcher chrome, ...) uses Nerd Font.
Alias icon -> label.icon; internal MaterialIcon -> NsIcon (sizes from root.font
via fontStyle); dropped the Material-Symbols vertical-center fudge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:19:16 +01:00
cf3c0d823d feat(nosignal): Nerd Font icon remap — nexus (Settings)
Convert caelestia's nexus Settings to Nerd Font icons (drop-in NsIcon).

- NsIcon hardened into a MaterialIcon drop-in: accepts fontStyle (sizes from
  it, keeps the NF Mono family), plus no-op fill/grade/animate for API parity.
- Glyphs map extended with caelestia core names: nexus static icons, nav/page
  icons (palette, wallpaper, dashboard, devices_other, extension, globe, info,
  monitor, shuffle, workspaces, ...), weather + app/device icons.
- Converted all MaterialIcon in modules/nexus/ to NsIcon; fixed icon aliases
  (NavRow/SliderRow .text -> .icon); reverted IconButton font: (it has font,
  not fontStyle, and renders its own icon).

Remaining core surfaces still Material Symbols (no breakage): launcher, osd,
lock, sidebar, utilities — to convert next with any further Glyphs entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:16:01 +01:00
6a933aa260 feat(nosignal): Nerd Font icon remap — bar + popouts
Route the redesign's icons through JetBrains Mono Nerd Font instead of Material
Symbols, for visual consistency with the JetBrains Mono text.

- services/Glyphs.qml: Material-Symbol-name -> Nerd Font (Font Awesome range)
  codepoint map (via String.fromCharCode), covering bar/panel icons + the
  dynamic Icons.getX() returns; get() falls back to a "?" glyph.
- components/NsIcon.qml: reactive icon component (input `icon` = name -> mapped
  glyph), so dynamic icons (volume/battery/play-pause) stay live. Uses the
  "Mono" NF variant so glyphs center in a fixed cell.
- Converted all MaterialIcon usages in NsBar + the 9 panels to NsIcon.

caelestia core (nexus/launcher/osd/lock) still uses Material Symbols — no
breakage (mixed), to be converted next with an expanded Glyphs map.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:01:44 +01:00
36f6f5a157 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>
2026-06-14 10:36:33 +01:00
8bada1d61a feat(bar): popouts drop down from the top bar — Phase B
The bar hover fly-outs (wifi/audio/bluetooth/tray/active-window) slid out
sideways from the old left edge; now they drop DOWN from the top bar:

- ClipWrapper.qml: the collapsing/slide axis is now height (was width); the
  popout is positioned along the bar by x (from the hovered widget's x-centre,
  via currentCenter) and docks at the top edge; the dock<->detach animation
  guard moves to the x (along-bar) axis. The content slides in via a negative
  top margin instead of left margin.
- Wrapper.qml: mirror the size-transition animation guards (height is now the
  always-animated extent axis, width the gated along-bar axis).

Verified live in the dev VM: all popouts open downward under their widget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:34:44 +01:00
b1e2bb3bc6 feat(bar): horizontal top bar — Phase A geometry (NoSignal layout)
Convert the bar from a vertical left-edge strip to a horizontal top bar. The
shell had no orientation abstraction, so this flips the geometry across the bar
and the drawer/region/interaction layer:

- Bar.qml: ColumnLayout -> RowLayout; hit-testing (checkPopout/handleWheel),
  scroll halves and popout-center anchoring all keyed off x instead of y;
  first/last padding -> left/right margins.
- BarWrapper.qml: the bar's "thickness" is now its height (clampedHeight/
  contentHeight/implicitHeight); content anchors left/right/top.
- drawers: Exclusions (exclusive zone moved left->top), Panels (content inset
  topMargin = bar height), Regions + ContentWindow (panel->window coordinate
  mapping and the thick border flip left->top), Interactions (bar-region check
  y<barHeight, panel Y-origin uses bar height, scroll passes x, bar drag-reveal
  is vertical).
- widgets: Clock (Row, HH:MM with a colon), StatusIcons (Row), Workspaces +
  Workspace (Row), Tray (Row, expand chevron at the right) — each sizes to the
  bar height instead of width.

Verified live in the dev VM: top bar renders with centered-clock entries, the
exclusion zone keeps windows below the bar, and the dashboard drops down
correctly. Popout fly-outs still anchor sideways (Phase B).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:26:55 +01:00
152923f5d8 fold: bake NoSignal shell patches into the fork (Phase 2)
We now own the pinned shell, so the runtime patches that the pacman-hook +
patch-script machinery re-applied after every caelestia-shell upgrade are
baked directly into source. Folds in all five caelestia-shell-targeting
patches from the builder's `os updates/`:

- Lock PAM (F1, blocker): ship faillock-free assets/pam.d/caelestia and point
  modules/lock/Pam.qml's passwd PamContext at it (config "passwd" -> "caelestia").
  A desktop screen-lock must never lock the user out of their own session.
- Updates page: add modules/nexus/pages/UpdatesPage.qml + register it in the
  first System slot of PageCompRegistry.qml.
- Additions page: add modules/nexus/pages/AdditionsPage.qml + register it in the
  Plugins slot; relabel Plugins -> Additions in PageRegistry.qml.
- Sudo toggle: add modules/nexus/common/SudoToggleRow.qml + insert it into
  ServicesPage.qml after the Smart colour scheme toggle.
- Wi-Fi wrong-password recovery: NetworkConnection.qml saved-profile branch now
  passes a real callback that forgets the bad profile and reopens the dialog.

The builder will drop the corresponding patch-*.sh calls + pacman hooks and
bump NOSIGNAL_SHELL_COMMIT to this commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:08:59 +01:00
28allday
71288e2126 packaging: DEV.md — dev workflow + layout map for the fork
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:55:11 +01:00
28allday
f39dc4e6c7 packaging: dev-sync.sh background loop for seamless live VM preview
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:51:21 +01:00
28allday
f13126eb53 packaging: one-shot dev-mount.sh for the live VM preview loop
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:38:55 +01:00
28allday
50a38546cd packaging: add dev-setup.sh for live VM shell preview (9p fork mount)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:31:18 +01:00
28allday
70e5639560 packaging: add nosignal-shell PKGBUILD (builds the fork, provides/conflicts caelestia-shell)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:08:11 +01:00
121 changed files with 4261 additions and 662 deletions

3
.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
*.qml text
*.conf text
*.json text

BIN
assets/nosignal-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

20
assets/pam.d/caelestia Normal file
View file

@ -0,0 +1,20 @@
#%PAM-1.0
# Caelestia lock-screen authentication (NoSignal).
#
# Deliberately does NOT use pam_faillock: a desktop screen-lock must never lock
# the user out of their OWN running session. With faillock in the path (the old
# "passwd"->system-auth route), a few failed unlocks tripped a temporary account
# lock that then refused the *correct* password until /run/faillock cleared on
# reboot. See README.md / finding F1.
#
# pam_unix verifies the password supplied via the PAM conversation (the lock UI)
# using the setuid unix_chkpwd helper, so it works for the uid-1000 Quickshell
# process. Add pam_systemd_home below if/when systemd-homed users are supported.
#
# F8a: deliberately NO `nullok` — a screen lock must never accept an empty
# password. (Only changes behaviour for empty-password accounts, which are
# correctly rejected; normal password auth is unaffected.)
auth required pam_unix.so
account required pam_unix.so
password required pam_unix.so
session required pam_unix.so

View file

@ -12,10 +12,10 @@ ColumnLayout {
spacing: Tokens.spacing.medium
Layout.alignment: Qt.AlignHCenter
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
animate: true
text: root.icon
icon: root.icon
fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).weight(Font.Bold).build()
}

32
components/NsIcon.qml Normal file
View file

@ -0,0 +1,32 @@
pragma ComponentBehavior: Bound
// NoSignal icon: renders a Nerd Font (JetBrainsMono NF) glyph from a Material
// Symbol name via the Glyphs map. Drop-in for MaterialIcon but the input is
// `icon` (the name) instead of `text`, so dynamic bindings stay reactive.
import QtQuick
import qs.services
Text {
id: root
property string icon: ""
property int size: 16
// MaterialIcon API parity so NsIcon is a drop-in (these are no-ops here
// Nerd Font has no fill/grade variable axes; sizing comes from fontStyle).
property real fill: 0
property int grade: 0
property bool animate: false
property var fontStyle: null
renderType: Text.NativeRendering
textFormat: Text.PlainText
text: Glyphs.get(root.icon)
// "Mono" variant centers each icon glyph in a fixed cell (the plain NF
// variant uses a wide advance, which left/right-shifts centered icons).
font.family: "JetBrainsMono Nerd Font Mono"
font.pixelSize: root.fontStyle ? (root.fontStyle.pixelSize > 0 ? root.fontStyle.pixelSize : Math.round((root.fontStyle.pointSize || 12) * 1.33)) : root.size
color: Theme.text
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}

43
components/NsPanel.qml Normal file
View 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
View 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: 0
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: 0
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()
}
}

View file

@ -45,8 +45,8 @@ ColumnLayout {
Layout.fillWidth: true
}
MaterialIcon {
text: "expand_more"
NsIcon {
icon: "expand_more"
rotation: root.expanded ? 180 : 0
color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.medium

View file

@ -110,11 +110,11 @@ RowLayout {
}
}
MaterialIcon {
NsIcon {
id: upIcon
anchors.centerIn: parent
text: "keyboard_arrow_up"
icon: "keyboard_arrow_up"
color: Colours.palette.m3onPrimary
}
}
@ -145,11 +145,11 @@ RowLayout {
onReleased: timer.stop()
}
MaterialIcon {
NsIcon {
id: downIcon
anchors.centerIn: parent
text: "keyboard_arrow_down"
icon: "keyboard_arrow_down"
color: Colours.palette.m3onPrimary
}
}

View file

@ -16,7 +16,7 @@ Slider {
background: StyledRect {
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Tokens.rounding.full
radius: 0
StyledRect {
anchors.left: parent.left
@ -51,7 +51,7 @@ Slider {
anchors.fill: parent
color: Colours.palette.m3inverseSurface
radius: Tokens.rounding.full
radius: 0
MouseArea {
id: handleInteraction

View file

@ -6,7 +6,7 @@ import qs.services
ButtonBase {
id: root
property alias icon: label.text
property alias icon: label.icon
readonly property alias label: label
font: Tokens.font.icon.medium
@ -34,11 +34,10 @@ ButtonBase {
return h;
}
MaterialIcon {
NsIcon {
id: label
anchors.centerIn: parent
anchors.verticalCenterOffset: 1 // AHHHHHHH material symbols whyyyy
color: root.onColour
fontStyle: root.font
fill: !root.isToggle || root.internalChecked ? 1 : 0

View file

@ -7,7 +7,7 @@ import qs.services
ButtonBase {
id: root
property alias icon: iconLabel.text
property string icon
property alias text: label.text
readonly property alias iconLabel: iconLabel
@ -44,9 +44,10 @@ ButtonBase {
anchors.centerIn: parent
spacing: Tokens.spacing.small
MaterialIcon {
NsIcon {
id: iconLabel
icon: root.icon
Layout.alignment: Qt.AlignVCenter
Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575)
color: root.onColour

View file

@ -157,9 +157,9 @@ MouseArea {
anchors.margins: Tokens.padding.medium
spacing: Tokens.spacing.small
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignVCenter
text: item.modelData?.icon ?? ""
icon: item.modelData?.icon ?? ""
color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant
}
@ -176,8 +176,8 @@ MouseArea {
active: item.modelData?.trailingIcon.length > 0
visible: active
sourceComponent: MaterialIcon {
text: item.modelData.trailingIcon
sourceComponent: NsIcon {
icon: item.modelData.trailingIcon
color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant
}
}

View file

@ -64,12 +64,12 @@ Row {
anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4)
spacing: Tokens.spacing.small
MaterialIcon {
NsIcon {
id: iconLabel
Layout.alignment: Qt.AlignVCenter
animate: true
text: root.active?.activeIcon ?? root.fallbackIcon
icon: root.active?.activeIcon ?? root.fallbackIcon
color: root.disabled ? root.disabledTextColour : root.textColour
fill: 1
}
@ -116,13 +116,13 @@ Row {
onClicked: root.expanded = !root.expanded
}
MaterialIcon {
NsIcon {
id: expandIcon
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4)
text: "expand_more"
icon: "expand_more"
color: root.disabled ? root.disabledTextColour : root.textColour
rotation: root.expanded ? 180 : 0

View file

@ -54,12 +54,12 @@ StyledRect {
anchors.centerIn: parent
spacing: Tokens.spacing.medium
MaterialIcon {
NsIcon {
id: toggleBtnIcon
visible: !!text
fill: root.toggled ? 1 : 0
text: root.icon
icon: root.icon
color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`]
fontStyle: Tokens.font.icon.size(root.iconSize).build()

View file

@ -57,11 +57,11 @@ Item {
}
}
MaterialIcon {
NsIcon {
anchors.centerIn: parent
grade: 200
text: image.status === Image.Error ? "broken_image" : "art_track"
icon: image.status === Image.Error ? "broken_image" : "art_track"
color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.size((parent.width * 0.35) || 1).build()
opacity: image.status === Image.Null || image.status === Image.Error ? 1 : 0

View file

@ -54,8 +54,9 @@ Scope {
onPressed: {
if (root.hasFullscreen)
return;
const visibilities = Visibilities.getForActive();
visibilities.session = !visibilities.session;
// NoSignal: drive the redesigned full-screen Power Menu (NsOverlay),
// not the old caelestia session panel (now dead code).
NsShell.toggle("power", 0);
}
}

View file

@ -10,14 +10,18 @@ import Caelestia.Config
import qs.components
import qs.services
ColumnLayout {
// NoSignal: horizontal top bar. The widgets flow left->right in a RowLayout and
// the bar's "thickness" is its HEIGHT; hit-testing / scroll / popout anchoring all
// key off the x-axis (the position along the bar). Upstream caelestia is a
// vertical left-edge bar this is the NoSignal layout fork (Phase 3).
RowLayout {
id: root
required property ShellScreen screen
required property DrawerVisibilities visibilities
required property BarPopouts.Wrapper popouts
required property bool fullscreen
readonly property int vPadding: Tokens.padding.large
readonly property int hPadding: Tokens.padding.large
function closeTray(): void {
if (!Config.bar.tray.compact)
@ -31,8 +35,8 @@ ColumnLayout {
}
}
function checkPopout(y: real): void {
const ch = childAt(width / 2, y) as WrappedLoader;
function checkPopout(x: real): void {
const ch = childAt(x, height / 2) as WrappedLoader;
if (ch?.id !== "tray")
closeTray();
@ -43,24 +47,24 @@ ColumnLayout {
}
const id = ch.id;
const top = ch.y;
const left = ch.x;
if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
const items = (ch.item as StatusIcons).items;
const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y);
const icon = items.childAt(mapToItem(items, x, 0).x, items.height / 2);
if (icon) {
popouts.currentName = icon.name;
popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, 0, icon.implicitHeight / 2).y);
popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, icon.implicitWidth / 2, 0).x);
popouts.hasCurrent = true;
}
} else if (id === "tray" && Config.bar.popouts.tray) {
const tray = ch.item as Tray;
if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, tray.implicitWidth / 2, y)))) {
const index = Math.floor(((y - top - tray.padding * 2 + tray.spacing) / tray.layout.implicitHeight) * tray.items.count);
if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, x, tray.implicitHeight / 2)))) {
const index = Math.floor(((x - left - tray.padding * 2 + tray.spacing) / tray.layout.implicitWidth) * tray.items.count);
const trayItem = tray.items.itemAt(index);
if (trayItem) {
popouts.currentName = `traymenu${index}`;
popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y);
popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, trayItem.implicitWidth / 2, 0).x);
popouts.hasCurrent = true;
} else {
popouts.hasCurrent = false;
@ -71,13 +75,13 @@ ColumnLayout {
}
} else if (id === "activeWindow" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) {
popouts.currentName = id.toLowerCase();
popouts.currentCenter = (ch.item as Item).mapToItem(root, 0, (ch.item as Item).implicitHeight / 2).y ?? 0;
popouts.currentCenter = (ch.item as Item).mapToItem(root, (ch.item as Item).implicitWidth / 2, 0).x ?? 0;
popouts.hasCurrent = true;
}
}
function handleWheel(y: real, angleDelta: point): void {
const ch = childAt(width / 2, y) as WrappedLoader;
function handleWheel(x: real, angleDelta: point): void {
const ch = childAt(x, height / 2) as WrappedLoader;
if (ch?.id === "workspaces" && Config.bar.scrollActions.workspaces) {
// Workspace scroll
const mon = (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor);
@ -86,14 +90,14 @@ ColumnLayout {
Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`);
else if (angleDelta.y < 0 || (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1)
Hypr.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`);
} else if (y < screen.height / 2 && Config.bar.scrollActions.volume) {
// Volume scroll on top half
} else if (x < screen.width / 2 && Config.bar.scrollActions.volume) {
// Volume scroll on left half
if (angleDelta.y > 0)
Audio.incrementVolume();
else if (angleDelta.y < 0)
Audio.decrementVolume();
} else if (Config.bar.scrollActions.brightness) {
// Brightness scroll on bottom half
// Brightness scroll on right half
const monitor = Brightness.getMonitorForScreen(screen);
if (angleDelta.y > 0)
monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement);
@ -115,7 +119,7 @@ ColumnLayout {
DelegateChoice {
roleValue: "spacer"
delegate: WrappedLoader {
Layout.fillHeight: enabled
Layout.fillWidth: enabled
}
}
DelegateChoice {
@ -201,11 +205,11 @@ ColumnLayout {
}
asynchronous: true
Layout.alignment: Qt.AlignHCenter
Layout.alignment: Qt.AlignVCenter
// Cursed ahh thing to add padding to first and last enabled components
Layout.topMargin: findFirstEnabled() === this ? root.vPadding : 0
Layout.bottomMargin: findLastEnabled() === this ? root.vPadding : 0
Layout.leftMargin: findFirstEnabled() === this ? root.hPadding : 0
Layout.rightMargin: findLastEnabled() === this ? root.hPadding : 0
visible: enabled
active: enabled

View file

@ -17,35 +17,40 @@ Item {
readonly property bool disabled: Strings.testRegexList(Config.bar.excludedScreens, screen.name)
readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth)
// NoSignal horizontal top bar: the bar's "thickness" is its HEIGHT (was width
// for the upstream vertical left-edge bar). clampedHeight/contentHeight and
// implicitHeight carry the thickness; the wrapper fills the screen width.
readonly property int clampedHeight: Math.max(Config.border.minThickness, implicitHeight)
readonly property int padding: Math.max(Tokens.padding.small, Config.border.thickness)
readonly property int contentWidth: Tokens.sizes.bar.innerWidth + padding * 2
readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness
readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered)
readonly property int contentHeight: Tokens.sizes.bar.innerWidth + padding * 2
readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentHeight : Config.border.thickness
// 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 {
(content.item as Bar)?.closeTray();
}
function checkPopout(y: real): void {
(content.item as Bar)?.checkPopout(y);
function checkPopout(x: real): void {
(content.item as Bar)?.checkPopout(x);
}
function handleWheel(y: real, angleDelta: point): void {
(content.item as Bar)?.handleWheel(y, angleDelta);
function handleWheel(x: real, angleDelta: point): void {
(content.item as Bar)?.handleWheel(x, angleDelta);
}
clip: true
visible: width > Config.border.thickness
implicitWidth: fullscreen ? 0 : Config.border.thickness
visible: height > Config.border.thickness
implicitHeight: fullscreen ? 0 : Config.border.thickness
states: State {
name: "visible"
when: root.shouldBeVisible
PropertyChanges {
root.implicitWidth: root.contentWidth
root.implicitHeight: root.contentHeight
}
}
@ -56,7 +61,7 @@ Item {
Anim {
target: root
property: "implicitWidth"
property: "implicitHeight"
}
},
Transition {
@ -65,7 +70,7 @@ Item {
Anim {
target: root
property: "implicitWidth"
property: "implicitHeight"
type: Anim.Emphasized
}
}
@ -74,14 +79,14 @@ Item {
Loader {
id: content
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
active: root.shouldBeVisible
sourceComponent: Bar {
width: root.contentWidth
height: root.contentHeight
screen: root.screen
visibilities: root.visibilities
popouts: root.popouts // qmllint disable incompatible-type

View file

@ -1,106 +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
implicitWidth: Tokens.sizes.bar.innerWidth
implicitHeight: layout.implicitHeight + root.padding * 2
StyledText {
Layout.alignment: Qt.AlignVCenter
text: Time.format("ddd d MMM")
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
font.weight: Font.Normal
}
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Tokens.rounding.full
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: Tokens.spacing.small
Loader {
Layout.alignment: Qt.AlignHCenter
asynchronous: true
active: Config.bar.clock.showIcon
visible: active
sourceComponent: MaterialIcon {
text: "calendar_month"
color: root.colour
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
visible: Config.bar.clock.showDate
horizontalAlignment: StyledText.AlignHCenter
text: Time.format("ddd\nd")
font: Tokens.font.body.small
color: root.colour
}
Rectangle {
Layout.fillWidth: true
visible: Config.bar.clock.showDate
implicitHeight: 1
color: Colours.palette.m3outlineVariant
}
StyledText {
Layout.alignment: Qt.AlignHCenter
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.topMargin: -parent.spacing - 4
Layout.alignment: Qt.AlignHCenter
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.topMargin: -parent.spacing - 4
Layout.alignment: Qt.AlignHCenter
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
}
}
StyledText {
Layout.alignment: Qt.AlignVCenter
text: Time.format("hh:mm")
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
font.weight: Font.DemiBold
}
}

View file

@ -20,16 +20,13 @@ StyledRect {
radius: Tokens.rounding.full
clip: true
implicitWidth: Tokens.sizes.bar.innerWidth
implicitHeight: iconColumn.implicitHeight + Tokens.padding.medium * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0)
implicitHeight: Tokens.sizes.bar.innerWidth
implicitWidth: iconColumn.implicitWidth + Tokens.padding.medium * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0)
ColumnLayout {
RowLayout {
id: iconColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: Tokens.padding.medium
anchors.centerIn: parent
spacing: Tokens.spacing.medium / 2
@ -261,7 +258,7 @@ StyledRect {
required property string name
asynchronous: true
Layout.alignment: Qt.AlignHCenter
Layout.alignment: Qt.AlignVCenter
visible: active
}
}

View file

@ -19,27 +19,27 @@ StyledRect {
property bool expanded
readonly property real nonAnimHeight: {
readonly property real nonAnimWidth: {
if (!Config.bar.tray.compact)
return layout.implicitHeight + padding * 2;
return (expanded ? expandIcon.implicitHeight + layout.implicitHeight + spacing : expandIcon.implicitHeight) + padding * 2;
return layout.implicitWidth + padding * 2;
return (expanded ? expandIcon.implicitWidth + layout.implicitWidth + spacing : expandIcon.implicitWidth) + padding * 2;
}
clip: true
visible: height > 0
visible: width > 0
implicitWidth: Tokens.sizes.bar.innerWidth
implicitHeight: nonAnimHeight
implicitHeight: Tokens.sizes.bar.innerWidth
implicitWidth: nonAnimWidth
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Tokens.rounding.full
Column {
Row {
id: layout
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: root.padding
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: root.padding
spacing: Tokens.spacing.small
opacity: root.expanded || !Config.bar.tray.compact ? 1 : 0
@ -86,37 +86,37 @@ StyledRect {
asynchronous: true
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
active: Config.bar.tray.compact && items.count > 0
sourceComponent: Item {
implicitWidth: expandIconInner.implicitWidth
implicitHeight: expandIconInner.implicitHeight - Tokens.padding.small
implicitHeight: expandIconInner.implicitHeight
implicitWidth: expandIconInner.implicitWidth - Tokens.padding.small
MaterialIcon {
id: expandIconInner
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Config.bar.tray.background ? Tokens.padding.extraSmall : -Tokens.padding.extraSmall
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Config.bar.tray.background ? Tokens.padding.extraSmall : -Tokens.padding.extraSmall
text: "expand_less"
fontStyle: Tokens.font.icon.large
rotation: root.expanded ? 180 : 0
rotation: root.expanded ? 270 : 90
Behavior on rotation {
Anim {}
}
Behavior on anchors.bottomMargin {
Behavior on anchors.rightMargin {
Anim {}
}
}
}
}
Behavior on implicitHeight {
Behavior on implicitWidth {
Anim {}
}
}

View file

@ -24,7 +24,7 @@ ColumnLayout {
readonly property bool isOccupied: occupied[ws] ?? false
readonly property bool hasWindows: isOccupied && Config.bar.workspaces.showWindows
Layout.alignment: Qt.AlignHCenter
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: size
spacing: 0

View file

@ -27,8 +27,8 @@ StyledClippingRect {
property real blur: onSpecial ? 1 : 0
implicitWidth: Tokens.sizes.bar.innerWidth
implicitHeight: layout.implicitHeight + Tokens.padding.small
implicitHeight: Tokens.sizes.bar.innerWidth
implicitWidth: layout.implicitWidth + Tokens.padding.small
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.full
@ -60,7 +60,7 @@ StyledClippingRect {
}
}
ColumnLayout {
RowLayout {
id: layout
anchors.centerIn: parent
@ -81,7 +81,7 @@ StyledClippingRect {
Loader {
asynchronous: true
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
active: Config.bar.workspaces.activeIndicator
sourceComponent: ActiveIndicator {

View file

@ -12,21 +12,24 @@ Item {
required property real borderThickness
readonly property alias content: content
property real offsetScale: x > 0 || content.hasCurrent ? 0 : 1
property real offsetScale: y > 0 || content.hasCurrent ? 0 : 1
visible: width > 0 && height > 0
clip: true
implicitWidth: content.implicitWidth * (1 - offsetScale)
implicitHeight: content.implicitHeight
// NoSignal top bar: popouts drop DOWN from the bar. The collapsing axis is
// now height (was width for the vertical left bar); position along the bar
// is x (from the hovered widget's x-centre), the docked edge is the top.
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight * (1 - offsetScale)
x: content.isDetached ? (parent.width - content.nonAnimWidth) / 2 : 0
y: {
y: content.isDetached ? (parent.height - content.nonAnimHeight) / 2 : 0
x: {
if (content.isDetached)
return (parent.height - content.nonAnimHeight) / 2;
return (parent.width - content.nonAnimWidth) / 2;
const off = content.currentCenter - borderThickness - content.nonAnimHeight / 2;
const diff = parent.height - Math.floor(off + content.nonAnimHeight);
const off = content.currentCenter - borderThickness - content.nonAnimWidth / 2;
const diff = parent.width - Math.floor(off + content.nonAnimWidth);
if (diff < 0)
return off + diff;
return Math.max(off, 0);
@ -36,14 +39,14 @@ Item {
Anim {}
}
Behavior on x {
Behavior on y {
Anim {
duration: content.animLength
easing: content.animCurve
}
}
Behavior on y {
Behavior on x {
enabled: root.offsetScale < 1
Anim {
@ -58,8 +61,8 @@ Item {
screen: root.screen
offsetScale: root.offsetScale
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: (-implicitWidth - 5) * root.offsetScale
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: (-implicitHeight - 5) * root.offsetScale
}
}

View file

@ -149,14 +149,14 @@ Item {
}
}
Behavior on implicitWidth {
Behavior on implicitHeight {
Anim {
duration: root.animLength
easing: root.animCurve
}
}
Behavior on implicitHeight {
Behavior on implicitWidth {
enabled: root.offsetScale < 1
Anim {

View file

@ -75,7 +75,7 @@ Item {
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.large
radius: Tokens.rounding.large
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: "transparent"
Flickable {

View file

@ -21,7 +21,7 @@ GridLayout {
Layout.preferredWidth: Tokens.sizes.dashboard.userWidth
Layout.fillHeight: true
radius: Tokens.rounding.extraLarge
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
User {
id: user
@ -37,7 +37,7 @@ GridLayout {
Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth
Layout.preferredHeight: weather.implicitHeight
radius: Tokens.rounding.extraLarge * 1.5
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
SmallWeather {
id: weather
@ -49,7 +49,7 @@ GridLayout {
Layout.preferredWidth: dateTime.implicitWidth
Layout.fillHeight: true
radius: Tokens.rounding.large
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
DateTime {
id: dateTime
@ -63,7 +63,7 @@ GridLayout {
Layout.fillWidth: true
Layout.preferredHeight: calendar.implicitHeight
radius: Tokens.rounding.extraLarge
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
Calendar {
id: calendar
@ -78,7 +78,7 @@ GridLayout {
Layout.preferredWidth: resources.implicitWidth
Layout.fillHeight: true
radius: Tokens.rounding.large
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
Resources {
id: resources
@ -92,7 +92,7 @@ GridLayout {
Layout.preferredWidth: media.implicitWidth
Layout.fillHeight: true
radius: Tokens.rounding.extraLarge * 2
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
Media {
id: media

View file

@ -102,11 +102,11 @@ Item {
CAnim {}
}
MaterialIcon {
NsIcon {
id: icon
anchors.centerIn: parent
text: "queue_music"
icon: "queue_music"
fontStyle: Tokens.font.icon.builders.large.scale(2).build()
color: Colours.palette.m3onPrimaryContainer
}

View file

@ -23,9 +23,9 @@ Item {
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.medium
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: "tune"
icon: "tune"
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).build()
color: Colours.palette.m3onSurfaceVariant
}

View file

@ -86,7 +86,7 @@ Item {
implicitHeight: parent.implicitHeight * 2
color: Colours.palette.m3primary
radius: Tokens.rounding.full
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
}
Behavior on x {
@ -148,18 +148,18 @@ Item {
anchors.verticalCenter: parent.verticalCenter
implicitHeight: parent.height + Tokens.sizes.dashboard.tabIndicatorSpacing * 2
radius: Tokens.rounding.medium
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface
onClicked: root.dashState.currentTab = tab.TabBar.index
}
MaterialIcon {
NsIcon {
id: icon
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: label.top
text: tab.iconName
icon: tab.iconName
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
fill: tab.current ? 1 : 0
fontStyle: Tokens.font.icon.medium

View file

@ -67,7 +67,7 @@ Item {
Layout.fillWidth: true
implicitHeight: bigInfoRow.implicitHeight + Tokens.padding.small
radius: Tokens.rounding.extraLarge * 2
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.tPalette.m3surfaceContainer
RowLayout {
@ -76,9 +76,9 @@ Item {
anchors.centerIn: parent
spacing: Tokens.spacing.largeIncreased
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignVCenter
text: Weather.icon
icon: Weather.icon
fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).build()
color: Colours.palette.m3secondary
animate: true
@ -155,7 +155,7 @@ Item {
Layout.fillWidth: true
implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.medium * 2
radius: Tokens.rounding.large
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.tPalette.m3surfaceContainer
ColumnLayout {
@ -180,9 +180,9 @@ Item {
color: Colours.palette.m3onSurfaceVariant
}
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: forecastItem.modelData.icon
icon: forecastItem.modelData.icon
fontStyle: Tokens.font.icon.extraLarge
color: Colours.palette.m3secondary
}
@ -209,15 +209,15 @@ Item {
Layout.fillWidth: true
Layout.preferredHeight: 60
radius: Tokens.rounding.medium
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.tPalette.m3surfaceContainer
Row {
anchors.centerIn: parent
spacing: Tokens.spacing.medium
MaterialIcon {
text: detailRoot.icon
NsIcon {
icon: detailRoot.icon
color: detailRoot.colour
fontStyle: Tokens.font.icon.large
anchors.verticalCenter: parent.verticalCenter
@ -252,8 +252,8 @@ Item {
spacing: Tokens.spacing.small
MaterialIcon {
text: weatherStat.icon
NsIcon {
icon: weatherStat.icon
fontStyle: Tokens.font.icon.extraLarge
color: weatherStat.colour
}

View file

@ -62,7 +62,7 @@ CustomMouseArea {
StateLayer {
color: Colours.palette.m3primary
radius: pressed ? Tokens.rounding.small : Tokens.rounding.large
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
disabled: {
const now = new Date();
return root.currMonth === now.getMonth() && root.currYear === now.getFullYear();

View file

@ -65,10 +65,10 @@ Item {
Anim {}
}
MaterialIcon {
NsIcon {
anchors.centerIn: parent
text: res.icon
font: Tokens.font.icon.large
icon: res.icon
fontStyle: Tokens.font.icon.large
color: res.fgColour
}
}

View file

@ -13,14 +13,14 @@ Item {
Component.onCompleted: Weather.reload()
MaterialIcon {
NsIcon {
id: icon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
animate: true
text: Weather.icon
icon: Weather.icon
color: Colours.palette.m3secondary
fontStyle: Tokens.font.icon.builders.extraLarge.scale(1.6).build()
}

View file

@ -75,8 +75,8 @@ Item {
asynchronous: true
active: pfp.status !== Image.Ready
sourceComponent: MaterialIcon {
text: "person_add"
sourceComponent: NsIcon {
icon: "person_add"
color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.extraLarge
fill: 1
@ -120,9 +120,9 @@ Item {
}
}
MaterialIcon {
NsIcon {
anchors.centerIn: parent
text: "person_edit"
icon: "person_edit"
color: Colours.palette.m3onPrimary
fontStyle: Tokens.font.icon.large
}
@ -187,9 +187,9 @@ Item {
CAnim {}
}
MaterialIcon {
NsIcon {
anchors.centerIn: parent
text: "clock_arrow_up"
icon: "clock_arrow_up"
color: Colours.palette.m3onTertiaryContainer
fontStyle: Tokens.font.icon.medium
}
@ -216,7 +216,7 @@ Item {
implicitWidth: 10
implicitHeight: 10
radius: Tokens.rounding.full
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.palette.m3secondaryContainer
}
@ -229,7 +229,7 @@ Item {
implicitWidth: 15
implicitHeight: 15
radius: Tokens.rounding.full
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.palette.m3secondaryContainer
}
@ -240,7 +240,7 @@ Item {
anchors.leftMargin: -Tokens.padding.medium
y: Tokens.padding.extraSmall
radius: Tokens.rounding.largeIncreased
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
color: Colours.palette.m3secondaryContainer
implicitWidth: wmLabel.implicitWidth + Tokens.padding.medium * 2
implicitHeight: wmLabel.implicitHeight + Tokens.padding.small * 2
@ -251,11 +251,11 @@ Item {
anchors.centerIn: parent
spacing: Tokens.spacing.extraSmall
MaterialIcon {
NsIcon {
id: wmIcon
anchors.verticalCenter: parent.verticalCenter
text: "select_window"
icon: "select_window"
color: Colours.palette.m3onSecondaryContainer
fontStyle: wmText.font
}

View file

@ -176,7 +176,7 @@ Item {
implicitWidth: shape.implicitSize + Tokens.padding.medium * 2
implicitHeight: shape.implicitSize + Tokens.padding.medium * 2
color: Colours.palette.m3primaryContainer
radius: Tokens.rounding.full
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
LoadingIndicator {
id: shape
@ -212,9 +212,9 @@ Item {
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: "sentiment_sad"
icon: "sentiment_sad"
fontStyle: Tokens.font.icon.builders.large.scale(2).build()
color: Colours.palette.m3outline
}

View file

@ -20,9 +20,9 @@ Item {
spacing: Tokens.spacing.medium
z: 1
MaterialIcon {
NsIcon {
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
text: "lyrics"
icon: "lyrics"
fontStyle: Tokens.font.icon.medium
}

View file

@ -33,7 +33,7 @@ Item {
anchors.fill: parent
anchors.margins: !btn.pressed && btn.containsMouse ? -Tokens.padding.extraSmall : 0
group: blobGroup
radius: Tokens.rounding.medium
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
Behavior on anchors.margins {
Anim {}
@ -50,7 +50,7 @@ Item {
implicitHeight: parent.height
group: blobGroup
radius: Tokens.rounding.medium
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
deformScale: 0.00001
states: State {
@ -197,11 +197,11 @@ Item {
hoverEnabled: true
onClicked: root.open = !root.open
MaterialIcon {
NsIcon {
id: icon
anchors.centerIn: parent
text: "more_vert"
icon: "more_vert"
fontStyle: Tokens.font.icon.medium
}
}

View file

@ -12,7 +12,7 @@ StyledClippingRect {
property real animPerc: UPower.displayDevice.percentage
color: Colours.palette.m3secondaryContainer
radius: Tokens.rounding.large
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && Gpu.type !== Gpu.None) || Config.dashboard.performance.showStorage || Config.dashboard.performance.showMemory ? Tokens.sizes.dashboard.perfBattWidth : Tokens.sizes.dashboard.perfBattWidthSingle
implicitHeight: Tokens.sizes.dashboard.perfBattHeight
@ -39,7 +39,7 @@ StyledClippingRect {
implicitHeight: parent.height * root.animPerc
color: Colours.palette.m3secondary
radius: Tokens.rounding.extraSmall
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
clip: true
Contents {
@ -65,9 +65,9 @@ StyledClippingRect {
spacing: 0
MaterialIcon {
NsIcon {
Layout.leftMargin: -Tokens.padding.extraSmall
text: "battery_full"
icon: "battery_full"
color: contents.accentColour
fontStyle: Tokens.font.icon.large
}
@ -115,8 +115,8 @@ StyledClippingRect {
Layout.alignment: Qt.AlignRight
spacing: Tokens.spacing.extraSmall
MaterialIcon {
text: "bolt"
NsIcon {
icon: "bolt"
color: contents.accentColour
fontStyle: Tokens.font.icon.large
fill: 1

View file

@ -17,7 +17,7 @@ StyledRect {
required property real temperature
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.extraLarge
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: Tokens.sizes.dashboard.perfHeroCardWidth
implicitHeight: Math.max(tempProg.implicitHeight + detailsRow.implicitHeight + Tokens.spacing.large, usageShape.implicitHeight + usageLabel.implicitHeight) + Tokens.padding.large * 2
@ -40,11 +40,11 @@ StyledRect {
Anim {}
}
MaterialIcon {
NsIcon {
id: icon
anchors.centerIn: parent
text: root.icon
icon: root.icon
color: root.accent
fontStyle: Tokens.font.icon.medium
}
@ -84,9 +84,9 @@ StyledRect {
Layout.leftMargin: -Tokens.padding.extraSmall
spacing: Tokens.spacing.extraSmall
MaterialIcon {
NsIcon {
Layout.topMargin: Math.round(fontInfo.pointSize * 0.08)
text: root.temperature > 90 ? "thermometer_alert" : "thermometer"
icon: root.temperature > 90 ? "thermometer_alert" : "thermometer"
color: root.temperature > 90 ? Colours.palette.m3error : root.accent
fontStyle: Tokens.font.icon.medium
fill: 1

View file

@ -12,7 +12,7 @@ StyledRect {
readonly property color accent: Colours.palette.m3tertiary
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.medium
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: layout.implicitWidth + Tokens.padding.extraLargeIncreased * 2
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
@ -31,8 +31,8 @@ StyledRect {
Layout.leftMargin: -Tokens.padding.extraSmall
spacing: Tokens.spacing.small
MaterialIcon {
text: "memory_alt"
NsIcon {
icon: "memory_alt"
fill: 1
color: root.accent
fontStyle: Tokens.font.icon.builders.medium.weight(Font.DemiBold).build() // DemiBold to fix fill issues

View file

@ -10,7 +10,7 @@ StyledRect {
id: root
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.extraLarge
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: Tokens.sizes.dashboard.perfNetworkCardWidth
implicitHeight: Tokens.sizes.dashboard.perfNetworkCardHeight
@ -30,8 +30,8 @@ StyledRect {
RowLayout {
spacing: Tokens.spacing.small
MaterialIcon {
text: "swap_vert"
NsIcon {
icon: "swap_vert"
color: Colours.palette.m3primary
fontStyle: Tokens.font.icon.medium
}
@ -105,8 +105,8 @@ StyledRect {
Layout.fillWidth: true
spacing: Tokens.spacing.small
MaterialIcon {
text: "download"
NsIcon {
icon: "download"
color: Colours.palette.m3tertiary
fontStyle: Tokens.font.icon.medium
}
@ -136,8 +136,8 @@ StyledRect {
Layout.fillWidth: true
spacing: Tokens.spacing.small
MaterialIcon {
text: "upload"
NsIcon {
icon: "upload"
color: Colours.palette.m3secondary
fontStyle: Tokens.font.icon.medium
}
@ -167,8 +167,8 @@ StyledRect {
Layout.fillWidth: true
spacing: Tokens.spacing.small
MaterialIcon {
text: "history"
NsIcon {
icon: "history"
color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.medium
}

View file

@ -14,7 +14,7 @@ StyledRect {
readonly property real percentage: Storage.primaryDisk?.perc ?? 0
color: Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.extraExtraLarge
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
@ -55,9 +55,9 @@ StyledRect {
anchors.centerIn: parent
spacing: 0
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: "hard_drive"
icon: "hard_drive"
color: root.accent
fontStyle: Tokens.font.icon.medium
}

View file

@ -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
@ -85,14 +85,14 @@ StyledWindow {
Region {
id: emptyRegion
x: panels.notifications.x + bar.implicitWidth
y: panels.notifications.y + root.borderThickness
x: panels.notifications.x + root.borderThickness
y: panels.notifications.y + bar.implicitHeight
width: panels.notifications.width
height: panels.notifications.height
Region {
x: root.width - width
y: panels.osdWrapper.y + root.borderThickness
y: panels.osdWrapper.y + bar.implicitHeight
width: panels.osdWrapper.width * (1 - panels.osd.offsetScale) + root.borderThickness
height: panels.osd.height
}
@ -155,9 +155,9 @@ StyledWindow {
anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers
group: blobGroup
radius: root.borderRounding
borderLeft: bar.implicitWidth - anchors.margins - root.sdfBorderOffset
borderLeft: root.borderThickness - anchors.margins - root.sdfBorderOffset
borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset
borderTop: root.borderThickness - anchors.margins - root.sdfBorderOffset
borderTop: -anchors.margins - root.sdfBorderOffset // NoSignal: no top frame
borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset
}
@ -180,7 +180,7 @@ StyledWindow {
panel: panels.sessionWrapper
deformAmount: 0.2
x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth
x: panels.sessionWrapper.x + panels.session.x + root.borderThickness
implicitWidth: panels.session.width
}
@ -198,8 +198,10 @@ StyledWindow {
id: osdBg
panel: panels.osdWrapper
deformAmount: 0.25
x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth
deformAmount: 0 // NoSignal: square OSD (no organic blob deformation)
radius: 0
opacity: 0 // NoSignal: OSD draws its own flat square glass bg (osd/Content)
x: panels.osdWrapper.x + panels.osd.x + root.borderThickness
implicitWidth: panels.osd.width
}
@ -207,6 +209,12 @@ StyledWindow {
id: notifsBg
panel: panels.notifications
deformAmount: 0 // NoSignal: square notification toasts
radius: 0
opacity: 0
visible: false
implicitWidth: 0 // collapse the blob entirely (toasts draw their own square cards)
implicitHeight: 0
}
PanelBg {
@ -226,7 +234,7 @@ StyledWindow {
panel: panels.popoutsWrapper
deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1
x: panels.popoutsWrapper.x + panels.popouts.x + bar.implicitWidth - panels.popouts.width * extraWidth
x: panels.popoutsWrapper.x + panels.popouts.x + root.borderThickness - panels.popouts.width * extraWidth
implicitWidth: panels.popouts.width * (1 + extraWidth)
Behavior on extraWidth {
@ -292,8 +300,9 @@ StyledWindow {
BarWrapper {
id: bar
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
screen: root.screen
visibilities: visibilities
@ -310,8 +319,8 @@ StyledWindow {
property real deformAmount: 0.15
group: blobGroup
x: panel.x + bar.implicitWidth
y: panel.y + root.borderThickness
x: panel.x + root.borderThickness
y: panel.y + bar.implicitHeight
implicitWidth: panel.width
implicitHeight: panel.height
radius: Tokens.rounding.extraLarge

View file

@ -14,11 +14,11 @@ Scope {
ExclusionZone {
anchors.left: true
exclusiveZone: root.bar.exclusiveZone
}
ExclusionZone {
anchors.top: true
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

View file

@ -23,27 +23,29 @@ CustomMouseArea {
property bool osdShortcutActive
property bool utilitiesShortcutActive
// NoSignal top bar: the bar's thickness now insets the TOP, so panel Y-origins
// are offset by bar.implicitHeight; the left inset is just the border thickness.
function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = root.borderThickness + panel.y;
const panelY = root.bar.implicitHeight + panel.y;
return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding;
}
function withinPanelWidth(panel: Item, x: real, y: real): bool {
const panelX = bar.implicitWidth + panel.x;
const panelX = root.borderThickness + panel.x;
return x >= panelX - Config.border.rounding && x <= panelX + panel.width + Config.border.rounding;
}
function inLeftPanel(panel: Item, x: real, y: real): bool {
return x < bar.implicitWidth + panel.x + panel.width && withinPanelHeight(panel, x, y);
return x < root.borderThickness + panel.x + panel.width && withinPanelHeight(panel, x, y);
}
function inRightPanel(panel: Item, x: real, y: real): bool {
return x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panel.x) && withinPanelHeight(panel, x, y);
return x > Math.min(width - Config.border.minThickness, root.borderThickness + panel.x) && withinPanelHeight(panel, x, y);
}
function inTopPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property
return y < Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) && withinPanelWidth(panel, x, y);
return y < Math.max(Config.border.minThickness, root.bar.implicitHeight + panelHeight) && withinPanelWidth(panel, x, y);
}
function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool {
@ -54,8 +56,8 @@ CustomMouseArea {
function onWheel(event: WheelEvent): void {
if (fullscreen)
return;
if (event.x < bar.implicitWidth) {
bar.handleWheel(event.y, event.angleDelta);
if (event.y < bar.implicitHeight) {
bar.handleWheel(event.x, event.angleDelta);
}
}
@ -102,15 +104,15 @@ CustomMouseArea {
return;
}
// Show bar in non-exclusive mode on hover
if (!visibilities.bar && Config.bar.showOnHover && x < bar.clampedWidth)
// Show bar in non-exclusive mode on hover (top edge)
if (!visibilities.bar && Config.bar.showOnHover && y < bar.clampedHeight)
bar.isHovered = true;
// Show/hide bar on drag
if (pressed && dragStart.x < bar.clampedWidth) {
if (dragX > Config.bar.dragThreshold)
// Show/hide bar on drag (vertical, from the top edge)
if (pressed && dragStart.y < bar.clampedHeight) {
if (dragY > Config.bar.dragThreshold)
visibilities.bar = true;
else if (dragX < -Config.bar.dragThreshold)
else if (dragY < -Config.bar.dragThreshold)
visibilities.bar = false;
}
@ -128,7 +130,7 @@ CustomMouseArea {
root.panels.osd.hovered = true;
}
const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x);
const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, root.borderThickness + panels.sidebar.x);
// Show/hide session on drag
if (pressed && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) {
@ -213,9 +215,9 @@ CustomMouseArea {
utilitiesShortcutActive = false;
}
// Show popouts on hover
if (x < bar.implicitWidth) {
bar.checkPopout(y);
// Show popouts on hover (top bar)
if (y < bar.implicitHeight) {
bar.checkPopout(x);
} else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popoutsWrapper, x, y)) {
popouts.hasCurrent = false;
bar.closeTray();

View file

@ -36,7 +36,7 @@ Item {
anchors.fill: parent
anchors.margins: borderThickness
anchors.leftMargin: bar.implicitWidth
anchors.topMargin: bar.implicitHeight
Item {
id: osdWrapper
@ -70,7 +70,7 @@ Item {
sessionPanel: sessionWrapper
utilitiesPanel: utilities
anchors.top: parent.top
y: 46 // NoSignal: drop the toasts below the 38px NsBar (+ gap)
anchors.right: parent.right
}

View file

@ -15,10 +15,10 @@ Region {
readonly property real borderThickness: win.contentItem.Config.border.thickness
readonly property real clampedThickness: win.contentItem.Config.border.clampedThickness
x: bar.clampedWidth + win.dragMaskPadding
y: clampedThickness + win.dragMaskPadding
width: win.width - bar.clampedWidth - clampedThickness - win.dragMaskPadding * 2
height: win.height - clampedThickness * 2 - win.dragMaskPadding * 2
x: clampedThickness + win.dragMaskPadding
y: bar.clampedHeight + win.dragMaskPadding
width: win.width - clampedThickness * 2 - win.dragMaskPadding * 2
height: win.height - bar.clampedHeight - clampedThickness - win.dragMaskPadding * 2
intersection: Intersection.Xor
R {
@ -75,8 +75,8 @@ Region {
component R: Region {
required property Item panel
x: panel.x + root.bar.implicitWidth
y: panel.y + root.borderThickness
x: panel.x + root.borderThickness
y: panel.y + root.bar.implicitHeight
width: panel.width
height: panel.height
intersection: Intersection.Subtract

View file

@ -58,14 +58,14 @@ Item {
implicitHeight: Math.max(searchIcon.implicitHeight, search.implicitHeight, clearIcon.implicitHeight)
MaterialIcon {
NsIcon {
id: searchIcon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: root.padding
text: "search"
icon: "search"
color: Colours.palette.m3onSurfaceVariant
}
@ -145,7 +145,7 @@ Item {
}
}
MaterialIcon {
NsIcon {
id: clearIcon
anchors.verticalCenter: parent.verticalCenter
@ -163,7 +163,7 @@ Item {
return 1;
}
text: "close"
icon: "close"
color: Colours.palette.m3onSurfaceVariant
MouseArea {

View file

@ -116,8 +116,8 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
MaterialIcon {
text: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search"
NsIcon {
icon: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search"
color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.extraLarge

View file

@ -25,11 +25,11 @@ Item {
anchors.rightMargin: Tokens.padding.medium
anchors.margins: Tokens.padding.small
MaterialIcon {
NsIcon {
id: icon
anchors.verticalCenter: parent.verticalCenter
text: root.modelData?.icon ?? ""
icon: root.modelData?.icon ?? ""
color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.builders.large.scale(1.3).build()
}

View file

@ -79,8 +79,8 @@ Item {
anchors.right: parent.right
active: root.modelData && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, root.modelData.id)
sourceComponent: MaterialIcon {
text: "favorite"
sourceComponent: NsIcon {
icon: "favorite"
fill: 1
color: Colours.palette.m3primary
}

View file

@ -40,8 +40,8 @@ Item {
spacing: Tokens.spacing.medium
MaterialIcon {
text: "function"
NsIcon {
icon: "function"
fontStyle: Tokens.font.icon.extraLarge
Layout.alignment: Qt.AlignVCenter
}
@ -105,14 +105,14 @@ Item {
}
}
MaterialIcon {
NsIcon {
id: icon
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.medium
text: "open_in_new"
icon: "open_in_new"
color: Colours.palette.m3onTertiary
fontStyle: Tokens.font.icon.large
}

View file

@ -92,8 +92,8 @@ Item {
active: `${root.modelData?.name} ${root.modelData?.flavour}` === Schemes.currentScheme
sourceComponent: MaterialIcon {
text: "check"
sourceComponent: NsIcon {
icon: "check"
color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.large
}

View file

@ -26,10 +26,10 @@ Item {
anchors.rightMargin: Tokens.padding.medium
anchors.margins: Tokens.padding.small
MaterialIcon {
NsIcon {
id: icon
text: root.modelData?.icon ?? ""
icon: root.modelData?.icon ?? ""
fontStyle: Tokens.font.icon.extraLarge
anchors.verticalCenter: parent.verticalCenter
@ -68,8 +68,8 @@ Item {
active: root.modelData?.variant === Schemes.currentVariant
sourceComponent: MaterialIcon {
text: "check"
sourceComponent: NsIcon {
icon: "check"
color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.large
}

View file

@ -57,9 +57,9 @@ Item {
implicitWidth: Tokens.sizes.launcher.wallpaperWidth
implicitHeight: implicitWidth / 16 * 9
MaterialIcon {
NsIcon {
anchors.centerIn: parent
text: "image"
icon: "image"
color: Colours.tPalette.m3outline
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
}

View file

@ -1,11 +1,22 @@
pragma ComponentBehavior: Bound
// NoSignal lock screen surface (interface redesign 2026-06-14).
// Secure ext-session-lock content: blurred wallpaper + scrim, top status row,
// centred clock/date/avatar/username/password, bottom now-playing strip.
// Reuses caelestia's Pam (buffer + handleKey + state) for PAM auth and the
// WlSessionLock unlock signal input is fed to pam.handleKey, never compared
// in QML. JetBrains Mono / Nerd Font / NoSignal Theme tokens.
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell.Wayland
import Quickshell.Bluetooth
import Quickshell.Services.UPower
import Caelestia.Config
import qs.components
import qs.services
import qs.utils
WlSessionLockSurface {
id: root
@ -13,153 +24,26 @@ WlSessionLockSurface {
required property WlSessionLock lock
required property Pam pam
readonly property alias unlocking: unlockAnim.running
readonly property bool errored: root.pam.state === "error" || root.pam.state === "fail"
contentItem.Config.screen: screen.name
contentItem.Tokens.screen: screen.name
contentItem.Config.screen: screen?.name ?? ""
contentItem.Tokens.screen: screen?.name ?? ""
color: "transparent"
Connections {
function onUnlock(): void {
unlockAnim.start();
root.lock.locked = false;
}
target: root.lock
}
SequentialAnimation {
id: unlockAnim
ParallelAnimation {
Anim {
target: lockContent
properties: "implicitWidth,implicitHeight"
to: lockContent.size
}
Anim {
target: lockBg
property: "radius"
to: lockContent.radius
}
Anim {
target: content
property: "scale"
to: 0
}
Anim {
target: content
property: "opacity"
to: 0
type: Anim.StandardSmall
}
Anim {
target: lockIcon
property: "opacity"
to: 1
type: Anim.StandardLarge
}
Anim {
target: background
property: "opacity"
to: 0
type: Anim.StandardLarge
}
SequentialAnimation {
PauseAnimation {
duration: Tokens.anim.durations.small
}
Anim {
type: Anim.Standard
target: lockContent
property: "opacity"
to: 0
}
}
}
PropertyAction {
target: root.lock
property: "locked"
value: false
}
}
ParallelAnimation {
id: initAnim
running: true
Anim {
target: background
property: "opacity"
to: 1
type: Anim.StandardLarge
}
SequentialAnimation {
ParallelAnimation {
Anim {
target: lockContent
property: "scale"
to: 1
type: Anim.FastSpatial
}
Anim {
target: lockContent
property: "rotation"
to: 360
duration: Tokens.anim.durations.expressiveFastSpatial
easing: Tokens.anim.standardAccel
}
}
ParallelAnimation {
Anim {
target: lockIcon
property: "rotation"
to: 360
easing: Tokens.anim.standardDecel
}
Anim {
type: Anim.DefaultEffects
target: lockIcon
property: "opacity"
to: 0
}
Anim {
type: Anim.DefaultEffects
target: content
property: "opacity"
to: 1
}
Anim {
target: content
property: "scale"
to: 1
}
Anim {
target: lockBg
property: "radius"
to: lockContent.Tokens.rounding.extraLarge * 1.5
}
Anim {
target: lockContent
property: "implicitWidth"
to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult * lockContent.Tokens.sizes.lock.ratio
}
Anim {
target: lockContent
property: "implicitHeight"
to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult
}
}
}
}
// Blurred wallpaper + scrim
ScreencopyView {
id: background
anchors.fill: parent
captureSource: root.screen
opacity: 0
layer.enabled: true
layer.effect: MultiEffect {
@ -168,57 +52,265 @@ WlSessionLockSurface {
blur: 1
blurMax: 64
blurMultiplier: 1
brightness: -0.38
saturation: 0.1
}
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(8 / 255, 10 / 255, 14 / 255, 0.5)
}
// Keyboard capture (feed PAM; Escape does nothing)
Item {
id: lockContent
anchors.fill: parent
focus: true
Component.onCompleted: forceActiveFocus()
Keys.onPressed: event => root.pam.handleKey(event)
}
readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4
readonly property int radius: size / 4 * Tokens.rounding.scale
// Top status row
RowLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 22
anchors.leftMargin: 28
anchors.rightMargin: 28
anchors.centerIn: parent
implicitWidth: size
implicitHeight: size
rotation: 180
scale: 0
StyledRect {
id: lockBg
anchors.fill: parent
color: Colours.palette.m3surface
radius: parent.radius
opacity: Colours.transparency.enabled ? Colours.transparency.base : 1
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
blurMax: 15
shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7)
RowLayout {
spacing: 7
NsIcon {
icon: "lock"
color: Theme.textMuted
size: 15
}
StyledText {
text: "Locked"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
}
}
MaterialIcon {
id: lockIcon
anchors.centerIn: parent
text: "lock"
fontStyle: Tokens.font.icon.builders.extraLarge.scale(4).weight(Font.Bold).build()
rotation: 180
Item {
Layout.fillWidth: true
}
Content {
id: content
RowLayout {
spacing: 14
anchors.centerIn: parent
width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.extraLargeIncreased
height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.extraLargeIncreased
NsIcon {
icon: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off"
color: Theme.textMuted
size: 15
}
NsIcon {
icon: Bluetooth.defaultAdapter?.enabled ? "bluetooth_connected" : "bluetooth_disabled"
color: Theme.textMuted
size: 15
}
RowLayout {
spacing: 6
visible: UPower.displayDevice.isLaptopBattery
NsIcon {
icon: Icons.getBatteryIcon(UPower.displayDevice.percentage, [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged].includes(UPower.displayDevice.state))
color: Theme.textMuted
size: 15
}
StyledText {
text: Math.round(UPower.displayDevice.percentage * 100) + "%"
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: Theme.font.bar
}
}
}
}
lock: root
opacity: 0
scale: 0
// Centre block
ColumnLayout {
anchors.centerIn: parent
spacing: 0
StyledText {
Layout.alignment: Qt.AlignHCenter
text: Time.format("hh:mm")
color: Theme.text
font.family: Theme.font.family
font.pixelSize: 108
font.weight: Font.ExtraLight
font.letterSpacing: 3
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: Time.format("ddd, d MMMM yyyy")
color: Theme.textMuted
font.family: Theme.font.family
font.pixelSize: 15
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 64
implicitWidth: 74
implicitHeight: 74
radius: 0
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
position: 0
color: Theme.accent
}
GradientStop {
position: 1
color: Theme.blue
}
}
NsIcon {
anchors.centerIn: parent
icon: "person"
color: Theme.onAccent
size: 34
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 14
text: SysInfo.user || "user"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: 14
}
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 16
implicitWidth: 312
implicitHeight: 44
radius: Theme.radius.panel
color: Qt.rgba(17 / 255, 20 / 255, 26 / 255, 0.55)
border.width: 1
border.color: root.errored ? Qt.rgba(224 / 255, 116 / 255, 106 / 255, 0.6) : Qt.rgba(1, 1, 1, 0.1)
RowLayout {
anchors.fill: parent
anchors.leftMargin: 14
anchors.rightMargin: 12
spacing: 10
NsIcon {
icon: "key"
color: Theme.textMuted
size: 15
}
StyledText {
Layout.fillWidth: true
text: root.pam.buffer.length > 0 ? "•".repeat(root.pam.buffer.length) : "Enter password"
color: root.pam.buffer.length > 0 ? Theme.text : Theme.textFaint
elide: Text.ElideRight
font.family: Theme.font.family
font.pixelSize: Theme.font.body
font.letterSpacing: root.pam.buffer.length > 0 ? 2 : 0
}
NsIcon {
icon: "login"
color: root.pam.buffer.length > 0 ? Theme.accent : Theme.textFaint
size: 16
MouseArea {
anchors.fill: parent
anchors.margins: -6
cursorShape: Qt.PointingHandCursor
onClicked: if (root.pam.buffer.length > 0)
root.pam.passwd.start()
}
}
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 8
Layout.preferredHeight: 14
text: root.errored ? "Incorrect — try again" : (root.pam.lockMessage || "")
color: Theme.danger
font.family: Theme.font.family
font.pixelSize: 12
}
}
// Bottom strip
RowLayout {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottomMargin: 26
anchors.leftMargin: 28
anchors.rightMargin: 28
RowLayout {
spacing: 10
visible: Players.active !== null
Rectangle {
implicitWidth: 42
implicitHeight: 42
radius: Theme.radius.panel
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
position: 0
color: Theme.accent
}
GradientStop {
position: 1
color: Theme.blue
}
}
NsIcon {
anchors.centerIn: parent
icon: "music_note"
color: Theme.onAccent
size: 18
}
}
ColumnLayout {
spacing: 0
StyledText {
text: Players.active?.trackTitle || ""
color: Theme.text
elide: Text.ElideRight
Layout.maximumWidth: 280
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
StyledText {
text: Players.active?.trackArtist || ""
color: Theme.textMuted
elide: Text.ElideRight
Layout.maximumWidth: 280
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
}
Item {
Layout.fillWidth: true
}
StyledText {
text: "Press Enter to unlock"
color: Theme.textFaint
font.family: Theme.font.family
font.pixelSize: 11
}
}
}

View file

@ -40,7 +40,12 @@ Scope {
PamContext {
id: passwd
config: "passwd"
// NoSignal: faillock-free lock auth (finding F1). The stock "passwd"
// service routes through pam_faillock, which can lock the user out of
// their OWN session after a few failed unlocks refusing even the
// correct password until /run/faillock clears on reboot. The bundled
// "caelestia" service (assets/pam.d/caelestia) uses plain pam_unix.
config: "caelestia"
configDirectory: Quickshell.shellDir + "/assets/pam.d"
onMessageChanged: {

View file

@ -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

View file

@ -73,10 +73,20 @@ QtObject {
// System
Component {
PlaceholderComp {}
// Updates (NoSignal)
StackPage {
Component {
UpdatesPage {}
}
}
},
Component {
PlaceholderComp {}
// Additions (NoSignal)
StackPage {
Component {
AdditionsPage {}
}
}
},
// Shell
@ -155,11 +165,11 @@ QtObject {
anchors.centerIn: parent
spacing: Tokens.padding.extraSmall
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: "handyman"
icon: "handyman"
color: Colours.palette.m3outlineVariant
font: Tokens.font.icon.extraLarge
fontStyle: Tokens.font.icon.extraLarge
}
StyledText {

View file

@ -50,9 +50,9 @@ QtObject {
category: "system"
},
{
label: qsTr("Plugins"),
label: qsTr("Additions"),
icon: "extension",
description: qsTr("Manage plugins"),
description: qsTr("Install optional software"),
category: "system"
},

View file

@ -58,13 +58,13 @@ ItemList {
radius: Tokens.rounding.full
color: device.active ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
MaterialIcon {
NsIcon {
id: devIcon
anchors.centerIn: parent
text: root.iconName
icon: root.iconName
color: device.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
font: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.medium
fill: device.active ? 1 : 0
Behavior on fill {
@ -80,10 +80,10 @@ ItemList {
elide: Text.ElideRight
}
MaterialIcon {
text: "check"
NsIcon {
icon: "check"
color: Colours.palette.m3primary
font: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.medium
opacity: device.active ? 1 : 0
Behavior on opacity {

View file

@ -0,0 +1,73 @@
// CachyRepoToggleRow.qml (NoSignal) Settings -> Services toggle to enable /
// disable the CachyOS pacman repositories. Auto-detects the best x86-64 microarch
// tier the CPU supports (v4 > v3). Live state polls `nosignal-cachy-repo status`
// (no root). enable/disable run in a floating terminal so the pacman/download
// output stays visible; the grant is passwordless via sudo -n
// (/etc/sudoers.d/02-nosignal-cachy). enable adds repos, converts userspace to
// the optimized builds (pacman -Suu, pacman pinned stock), and installs the
// linux-cachyos kernel; disable reverts to stock and removes the cachy kernel
// (keeping stock `linux` bootable). A reboot switches the running kernel.
//
// Untracked file under modules/nexus/common auto-discovered as the type
// `CachyRepoToggleRow` via `import qs.modules.nexus.common`. Survives caelestia
// upgrades; pairs with the one-line insert in ServicesPage.qml.
import QtQuick
import Quickshell.Io
import qs.modules.nexus.common
ToggleRow {
id: root
property bool repoOn: false // NB: not `enabled` (reserved Item property)
text: qsTr("CachyOS repositories")
subtext: repoOn
? qsTr("On — CachyOS optimized builds + linux-cachyos kernel (reboot to run it)")
: qsTr("Switch to CachyOS optimized builds + linux-cachyos kernel (reboot after)")
onToggled: {
if (checked)
enableProc.running = true;
else
disableProc.running = true;
reconcile.restart();
}
// --- live state -----------------------------------------------------------
Process {
id: statusProc
command: ["/usr/local/bin/nosignal-cachy-repo", "status"]
stdout: StdioCollector {
onStreamFinished: {
root.repoOn = text.trim() === "enabled";
root.checked = root.repoOn; // drive switch from real state
}
}
}
// --- actions (passwordless; shown in a terminal for pacman output) ---------
Process {
id: enableProc
command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "sudo -n /usr/local/bin/nosignal-cachy-repo enable; printf '\\nPress Enter to close...'; read _"]
onExited: reconcile.restart()
}
Process {
id: disableProc
command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "sudo -n /usr/local/bin/nosignal-cachy-repo disable; printf '\\nPress Enter to close...'; read _"]
onExited: reconcile.restart()
}
// --- polling --------------------------------------------------------------
Timer {
interval: 5000
repeat: true
running: true
triggeredOnStart: true
onTriggered: statusProc.running = true
}
Timer {
id: reconcile
interval: 2000
onTriggered: statusProc.running = true
}
}

View file

@ -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
}

View file

@ -37,11 +37,11 @@ ConnectedRect {
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.extraSmall
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: root.placeholderIcon
icon: root.placeholderIcon
color: Colours.palette.m3outline
font: Tokens.font.icon.large
fontStyle: Tokens.font.icon.large
animate: true
}

View file

@ -10,7 +10,7 @@ import qs.modules.nexus.common
ConnectedRect {
id: root
property alias icon: icon.text
property alias icon: icon.icon
property alias label: label.text
property alias status: status.text
@ -32,11 +32,11 @@ ConnectedRect {
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
MaterialIcon {
NsIcon {
id: icon
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.medium
}
ColumnLayout {
@ -63,10 +63,10 @@ ConnectedRect {
}
}
MaterialIcon {
text: "chevron_right"
NsIcon {
icon: "chevron_right"
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.medium
}
}
}

View file

@ -11,7 +11,7 @@ import qs.modules.nexus.common
ConnectedRect {
id: root
property alias icon: icon.text
property alias icon: icon.icon
property alias label: label.text
property alias valueLabel: valueLabel.text
property real value
@ -28,11 +28,11 @@ ConnectedRect {
anchors.topMargin: Tokens.padding.large
spacing: Tokens.spacing.medium
MaterialIcon {
NsIcon {
id: icon
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.medium
}
ColumnLayout {

View file

@ -0,0 +1,102 @@
// SudoToggleRow.qml (NoSignal) Settings -> Services toggle for time-boxed
// passwordless sudo. Reflects live state by polling `nosignal-sudo-toggle
// status` (no root needed); enabling opens a floating terminal for the ONE
// password prompt, disabling runs passwordless inside the active window.
//
// Untracked file under modules/nexus/common auto-discovered as the type
// `SudoToggleRow` via `import qs.modules.nexus.common`. Survives caelestia
// upgrades; only the one-line insert in ServicesPage.qml is re-applied by hook.
import QtQuick
import Quickshell.Io
import qs.modules.nexus.common
ToggleRow {
id: root
property bool active: false
property int remaining: 0
text: qsTr("Passwordless sudo (15 min)")
subtext: active
? qsTr("On — %1 min left. Auto-reverts; a reboot also clears it.").arg(remaining)
: qsTr("Run sudo without a password for 15 minutes, then it reverts")
onToggled: {
if (checked) {
enableProc.running = true; // needs a password -> floating terminal
focusTimer.ticks = 0;
focusTimer.restart(); // then pull keyboard focus to the prompt
} else {
disableProc.running = true; // no password inside the active window
}
reconcile.restart();
}
// --- live state -----------------------------------------------------------
Process {
id: statusProc
command: ["nosignal-sudo-toggle", "status"]
stdout: StdioCollector {
onStreamFinished: {
const p = text.trim().split(/\s+/);
root.active = p[0] === "active";
root.remaining = parseInt(p[1] || "0") || 0;
root.checked = root.active; // drive switch from real state
}
}
}
// --- actions --------------------------------------------------------------
Process {
id: enableProc
// Dedicated window class (nosignal-sudo) so the shipped hypr windowrules
// (float/center/pin/stayfocused) force this prompt to GRAB keyboard focus.
// With the shared TUI.float class and no focus rule the prompt did not get
// keystrokes -> three blank tries tripped pam_faillock and the switch just
// snapped back with no feedback (hardware bug 2026-06-20). `enable-tui`
// runs the sudo prompt as the user and keeps the terminal open on failure
// so the reason is visible instead of vanishing.
command: ["kitty", "--class", "nosignal-sudo", "-e", "nosignal-sudo-toggle", "enable-tui"]
}
Process {
id: disableProc
command: ["sudo", "-n", "nosignal-sudo-toggle", "disable"]
}
// Force keyboard focus onto the password prompt once it has mapped. Hyprland
// 0.55's match: windowrule grammar has no working `stayfocused`, so we
// "activate it" from here instead (the prompt uses the dedicated nosignal-sudo
// class). Two nudges cover slow terminal startup.
Process {
id: focusProc
command: ["hyprctl", "dispatch", "focuswindow", "class:^(nosignal-sudo)$"]
}
Timer {
id: focusTimer
interval: 400
repeat: true
triggeredOnStart: false
property int ticks: 0
onTriggered: {
focusProc.running = true;
ticks += 1;
if (ticks >= 3) {
ticks = 0;
stop();
}
}
}
// --- polling --------------------------------------------------------------
Timer {
interval: 5000
repeat: true
running: true
triggeredOnStart: true
onTriggered: statusProc.running = true
}
Timer {
id: reconcile
interval: 2000
onTriggered: statusProc.running = true
}
}

View file

@ -86,11 +86,11 @@ VerticalFadeFlickable {
radius: Tokens.rounding.full
color: item.isCurrentPage ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
MaterialIcon {
NsIcon {
anchors.centerIn: parent
anchors.verticalCenterOffset: 1
text: item.modelData.icon
icon: item.modelData.icon
color: item.isCurrentPage ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
fontStyle: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
grade: 25

View file

@ -37,8 +37,8 @@ StyledRect {
spacing: Tokens.spacing.small
MaterialIcon {
text: "search"
NsIcon {
icon: "search"
color: Colours.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.medium
}

View file

@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Caelestia
import Caelestia.Config
@ -61,22 +62,27 @@ PageBase {
width: parent.width - Tokens.padding.largeIncreased * 2
spacing: Tokens.spacing.small
AnimatedLogo {
Image {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: implicitWidth
Layout.preferredHeight: implicitHeight
Layout.preferredWidth: 104
Layout.preferredHeight: 104
source: Quickshell.shellDir + "/assets/nosignal-logo.png"
sourceSize.width: 208
sourceSize.height: 208
fillMode: Image.PreserveAspectFit
smooth: true
}
StyledText {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Tokens.spacing.small
text: "Caelestia"
text: "NoSignal"
font: Tokens.font.headline.builders.large.width(110).build()
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: CUtils.version ? `v${CUtils.version}` : "…"
text: "V1"
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.body.medium
}

View file

@ -0,0 +1,120 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import Caelestia.Config
import qs.components
import qs.services
import qs.modules.nexus.common
// NoSignal: the Additions settings page (replaces the upstream Plugins
// placeholder). Optional software installed on demand from official sources
// (pacman repos / upstream installers no AUR, no Flatpak). Items come from
// the additions.json manifest via the status cache written by
// nosignal-additions; Install runs in a visible floating terminal so
// git/sudo/pacman output and prompts stay in front of the user.
PageBase {
id: root
title: qsTr("Additions")
property var status: ({})
readonly property var items: status.items || []
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// The Process objects live INSIDE the layout (its `data` accepts
// non-visual objects) PageBase's default property is a single
// `Item`, so declaring them at page level kills the whole shell.
// Same pattern as UpdatesPage / the upstream AboutPage.
// Read the cached status (instant).
Process {
id: readProc
running: true
command: ["sh", "-c", "cat \"${XDG_STATE_HOME:-$HOME/.local/state}/nosignal/additions-status.json\" 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.status = JSON.parse(text);
} catch (e) {
root.status = {};
}
}
}
}
// Refresh the cache on demand (re-runs every item's check).
Process {
id: checkProc
command: ["sh", "-c", "\"$HOME/.local/bin/nosignal-additions\" status >/dev/null 2>&1"]
onExited: readProc.running = true
}
// Run an installer in a visible floating terminal.
Process {
id: installProc
property string addId: ""
command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "\"$HOME/.local/bin/nosignal-additions\" install " + addId + "; printf '\\nPress Enter to close...'; read _"]
onExited: readProc.running = true
}
SectionHeader {
text: qsTr("Optional software")
}
Repeater {
model: root.items
NavRow {
required property var modelData
required property int index
first: index === 0
last: index === root.items.length - 1
icon: modelData.icon || "extension"
label: modelData.name
status: modelData.installed ? qsTr("Installed") : modelData.desc
onClicked: {
if (!modelData.installed && !installProc.running) {
installProc.addId = modelData.id;
installProc.running = true;
}
}
}
}
InfoRow {
visible: root.items.length === 0
first: true
last: true
label: qsTr("No additions manifest")
value: "—"
}
SectionHeader {
text: qsTr("Actions")
}
NavRow {
first: true
last: true
icon: "refresh"
label: qsTr("Re-check installed state")
status: installProc.running ? qsTr("Install running in terminal…") : (checkProc.running ? qsTr("Checking…") : qsTr("Refreshes the list above"))
onClicked: {
if (!checkProc.running)
checkProc.running = true;
}
}
}
}

View file

@ -97,9 +97,9 @@ PageBase {
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
MaterialIcon {
text: "tune"
font: Tokens.font.icon.medium
NsIcon {
icon: "tune"
fontStyle: Tokens.font.icon.medium
}
ColumnLayout {
@ -123,10 +123,10 @@ PageBase {
}
}
MaterialIcon {
text: "chevron_right"
NsIcon {
icon: "chevron_right"
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.medium
}
}
}

View file

@ -93,13 +93,13 @@ PageBase {
radius: Tokens.rounding.full
color: device.connected ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer
MaterialIcon {
NsIcon {
id: deviceIcon
anchors.centerIn: parent
text: Icons.getBluetoothIcon(device.modelData?.icon ?? "")
icon: Icons.getBluetoothIcon(device.modelData?.icon ?? "")
color: device.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer
font: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.medium
fill: device.connected ? 1 : 0
opacity: device.textOpacity
@ -193,9 +193,9 @@ PageBase {
Anim {}
}
MaterialIcon {
text: "add"
font: Tokens.font.icon.medium
NsIcon {
icon: "add"
fontStyle: Tokens.font.icon.medium
}
StyledText {

View file

@ -92,41 +92,71 @@ PageBase {
text: qsTr("Weather")
}
// Placeholder until the map-based location picker lands
// Location for the weather service. Weather.qml accepts a city name
// (geocoded) or a "lat,lon" string, and auto-detects via IP when empty.
ConnectedRect {
Layout.fillWidth: true
first: true
last: true
implicitHeight: comingSoon.implicitHeight + Tokens.padding.extraLarge * 2
implicitHeight: weatherLayout.implicitHeight + weatherLayout.anchors.margins * 2
ColumnLayout {
id: comingSoon
id: weatherLayout
anchors.centerIn: parent
width: parent.width - Tokens.padding.largeIncreased * 2
spacing: Tokens.padding.extraSmall
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "map"
color: Colours.palette.m3outlineVariant
font: Tokens.font.icon.extraLarge
}
anchors.fill: parent
anchors.margins: Tokens.padding.medium
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.small
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Location picker coming soon")
color: Colours.palette.m3outlineVariant
font: Tokens.font.title.small
Layout.fillWidth: true
text: qsTr("Location")
font: Tokens.font.body.small
elide: Text.ElideRight
}
StyledRect {
Layout.fillWidth: true
implicitHeight: locField.implicitHeight + Tokens.padding.medium * 2
radius: Tokens.rounding.small
color: Colours.tPalette.m3surfaceContainerLowest
border.color: Colours.palette.m3outlineVariant
Behavior on border.color {
CAnim {}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: locField.focus = true
}
StyledTextField {
id: locField
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Tokens.padding.large
anchors.rightMargin: Tokens.padding.large
text: GlobalConfig.services.weatherLocation
placeholderText: qsTr("City name or lat,long — empty = auto (IP)")
placeholderTextColor: Colours.palette.m3onSurfaceVariant
color: Colours.palette.m3onSurface
onEditingFinished: GlobalConfig.services.weatherLocation = text
}
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
text: qsTr("Choose your weather location on a map in a future update")
color: Colours.palette.m3outlineVariant
font: Tokens.font.body.small
text: qsTr("Leave empty to detect your location automatically.")
color: Colours.palette.m3outline
font: Tokens.font.label.small
}
}
}

View file

@ -131,10 +131,10 @@ PageBase {
anchors.rightMargin: Tokens.padding.extraLarge
spacing: Tokens.spacing.medium
MaterialIcon {
text: Icons.getNetworkIcon(network.modelData.strength)
NsIcon {
icon: Icons.getNetworkIcon(network.modelData.strength)
color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.medium
opacity: network.textOpacity
}
@ -165,10 +165,10 @@ PageBase {
Component {
id: iconComp
MaterialIcon {
text: network.modelData.active ? "settings" : "lock"
NsIcon {
icon: network.modelData.active ? "settings" : "lock"
color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.medium
opacity: network.textOpacity
}
}
@ -218,9 +218,9 @@ PageBase {
spacing: Tokens.spacing.medium
MaterialIcon {
text: "add"
font: Tokens.font.icon.medium
NsIcon {
icon: "add"
fontStyle: Tokens.font.icon.medium
}
StyledText {

View file

@ -211,6 +211,16 @@ PageBase {
onToggled: GlobalConfig.services.smartScheme = checked
}
// NoSignal: time-boxed passwordless sudo (15 min) toggle
SudoToggleRow {
Layout.fillWidth: true
}
// NoSignal: CachyOS repos enable/disable (sudoless, auto-tier)
CachyRepoToggleRow {
Layout.fillWidth: true
}
SelectRow {
Layout.fillWidth: true
last: true

View file

@ -0,0 +1,139 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import Caelestia.Config
import qs.components
import qs.services
import qs.modules.nexus.common
// NoSignal: the Updates settings page (replaces the upstream placeholder).
// Reads the JSON cache written by nosignal-update-check; "Update now" runs
// nosignal-update in a floating terminal (sudo + pacman prompts stay visible).
PageBase {
id: root
title: qsTr("Updates")
property var status: ({})
function count(v) {
return v === undefined ? "…" : String(v);
}
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
width: root.cappedWidth
spacing: Tokens.spacing.extraSmall / 2
// The Process objects live INSIDE the layout (its `data` accepts
// non-visual objects) PageBase's default property is a single
// `Item`, so declaring them at page level kills the whole shell
// ("Cannot assign Process to QQuickItem*"). Same pattern as the
// upstream AboutPage.
// Read the cached status (instant; the user timer keeps it fresh).
Process {
id: readProc
running: true
command: ["sh", "-c", "cat \"${XDG_STATE_HOME:-$HOME/.local/state}/nosignal/update-status.json\" 2>/dev/null"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.status = JSON.parse(text);
} catch (e) {
root.status = {};
}
}
}
}
// Refresh the cache on demand.
Process {
id: checkProc
command: ["sh", "-c", "\"$HOME/.local/bin/nosignal-update-check\" >/dev/null 2>&1"]
onExited: readProc.running = true
}
// Run the real update in a visible floating terminal.
Process {
id: updateProc
command: ["kitty", "--class", "TUI.float", "-e", "sh", "-c", "\"$HOME/.local/bin/nosignal-update\"; \"$HOME/.local/bin/nosignal-update-check\" >/dev/null 2>&1; printf '\\nPress Enter to close...'; read _"]
onExited: readProc.running = true
}
SectionHeader {
text: qsTr("Pending updates")
}
InfoRow {
first: true
label: qsTr("Official packages")
value: root.count(root.status.repo)
}
InfoRow {
label: qsTr("AUR packages")
value: root.count(root.status.aur)
}
InfoRow {
label: qsTr("Flatpak")
value: root.count(root.status.flatpak)
}
InfoRow {
last: true
label: qsTr("NoSignal layer")
subtext: qsTr("Idempotent migrations applied by nosignal-update")
value: root.status.migrations_pending === undefined ? "…" : (root.status.migrations_pending > 0 ? qsTr("%1 migration(s) pending").arg(root.status.migrations_pending) : qsTr("up to date"))
}
SectionHeader {
text: qsTr("History")
}
InfoRow {
first: true
label: qsTr("Last full upgrade")
value: root.status.last_upgrade || "—"
}
InfoRow {
last: true
label: qsTr("Last checked")
value: root.status.checked ? root.status.checked.replace("T", " ").substring(0, 16) : "—"
}
SectionHeader {
text: qsTr("Actions")
}
NavRow {
first: true
icon: "refresh"
label: qsTr("Check for updates now")
status: checkProc.running ? qsTr("Checking…") : qsTr("Refreshes the counts above")
onClicked: {
if (!checkProc.running)
checkProc.running = true;
}
}
NavRow {
last: true
icon: "system_update_alt"
label: qsTr("Update now")
status: updateProc.running ? qsTr("Running in terminal…") : qsTr("Opens a terminal: snapshot, packages, NoSignal layer")
onClicked: {
if (!updateProc.running)
updateProc.running = true;
}
}
}
}

View file

@ -46,11 +46,11 @@ PageBase {
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.extraSmall
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: "hide_image"
icon: "hide_image"
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.extraLarge
fontStyle: Tokens.font.icon.extraLarge
}
StyledText {

View file

@ -125,10 +125,10 @@ PageBase {
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium
MaterialIcon {
text: Icons.getBluetoothIcon(newDevice.modelData?.icon ?? "")
NsIcon {
icon: Icons.getBluetoothIcon(newDevice.modelData?.icon ?? "")
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.medium
opacity: newDevice.textOpacity
}

View file

@ -72,11 +72,11 @@ PageBase {
anchors.centerIn: parent
spacing: 0
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: "delete"
icon: "delete"
color: forgetBtn.onColour
font: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.medium
}
StyledText {
@ -126,11 +126,11 @@ PageBase {
ColumnLayout {
spacing: 0
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: root.connected ? "close" : "add"
icon: root.connected ? "close" : "add"
color: connectBtn.inactiveOnColour
font: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.medium
animate: true
}

View file

@ -22,11 +22,11 @@ PageBase {
anchors.centerIn: parent
spacing: Tokens.padding.extraSmall
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: "handyman"
icon: "handyman"
color: Colours.palette.m3outlineVariant
font: Tokens.font.icon.extraLarge
fontStyle: Tokens.font.icon.extraLarge
}
StyledText {

View file

@ -168,9 +168,9 @@ PageBase {
anchors.centerIn: parent
spacing: Tokens.spacing.extraSmall
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: "hide_image"
icon: "hide_image"
color: Colours.palette.m3outline
fontStyle: Tokens.font.icon.extraLarge
}

View file

@ -63,7 +63,7 @@ Item {
anchors.rightMargin: root.clampedPadding
color: "transparent"
radius: Tokens.rounding.large
radius: 0
StyledListView {
id: list

View file

@ -23,7 +23,7 @@ StyledRect {
property bool expanded: Config.notifs.openExpanded
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer
radius: Tokens.rounding.large
radius: 0
implicitHeight: inner.implicitHeight
@ -116,7 +116,7 @@ StyledRect {
visible: root.hasImage || root.hasAppIcon
sourceComponent: StyledClippingRect {
radius: Tokens.rounding.full
radius: 0
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer
implicitWidth: TokenConfig.sizes.notifs.image
implicitHeight: TokenConfig.sizes.notifs.image
@ -147,7 +147,7 @@ StyledRect {
anchors.bottom: root.hasImage ? image.bottom : undefined
sourceComponent: StyledRect {
radius: Tokens.rounding.full
radius: 0
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer
implicitWidth: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image
implicitHeight: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image
@ -177,8 +177,8 @@ StyledRect {
anchors.centerIn: parent
anchors.verticalCenterOffset: 1
sourceComponent: MaterialIcon {
text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)
sourceComponent: NsIcon {
icon: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
fontStyle: Tokens.font.icon.medium
}
@ -358,17 +358,17 @@ StyledRect {
implicitHeight: expandIcon.implicitHeight
StateLayer {
radius: Tokens.rounding.full
radius: 0
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
onClicked: root.expanded = !root.expanded
}
MaterialIcon {
NsIcon {
id: expandIcon
anchors.centerIn: parent
anchors.verticalCenterOffset: root.expanded ? -1 : 1
text: "expand_more"
icon: "expand_more"
fontStyle: Tokens.font.icon.medium
rotation: root.expanded ? 180 : 0

46
modules/nsbar/BarPill.qml Normal file
View 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()
}
}

260
modules/nsbar/NsBar.qml Normal file
View file

@ -0,0 +1,260 @@
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 Quickshell.Services.SystemTray
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, win.modelData.name)
NsIcon {
icon: "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, win.modelData.name)
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, win.modelData.name)
NsIcon {
icon: 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 only shown when an app actually registers a tray
// icon, otherwise the pill just opened an empty panel (F-T1).
IconPill {
icon: "widgets"
panel: "tray"
visible: SystemTray.items.values.length > 0
}
// 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, win.modelData.name)
NsIcon {
icon: 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: 0
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, win.modelData.name)
NsIcon {
icon: 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, win.modelData.name)
NsIcon {
icon: p.icon
color: p.active ? Theme.accent : p.iconColour
}
}
}
}

166
modules/nsbar/NsOverlay.qml Normal file
View file

@ -0,0 +1,166 @@
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
// This overlay owns the panel only when NsShell.screen names this output
// (or is "" = unscoped, e.g. the global power modal show on all screens).
// Without this, every screen's overlay rendered the panel mirrored popups.
readonly property bool onThisScreen: NsShell.screen === "" || NsShell.screen === modelData.name
screen: modelData
visible: NsShell.open !== "" && ov.onThisScreen
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" && ov.onThisScreen
sourceComponent: powerC
Component {
id: powerC
Panels.NsPowerMenu {}
}
}
Loader {
id: loader
active: NsShell.open !== "" && NsShell.open !== "power" && ov.onThisScreen
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 {}
}
}
}
}

View 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}`)
}
}
}
}

View 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
NsIcon {
icon: "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
}
NsIcon {
icon: "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
NsIcon {
icon: s.icon
color: Theme.textMuted
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 6
radius: 0
color: Theme.fillSubtle
Rectangle {
width: parent.width * Math.max(0, Math.min(1, s.value))
height: parent.height
radius: 0
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: 0
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
}
}
}
}

View 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
NsIcon {
anchors.centerIn: parent
icon: 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
}
}

View 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: 0
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"
NsIcon {
anchors.centerIn: parent
icon: parent.icon
color: Theme.textMuted
}
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: parent.activated()
}
}
}

View file

@ -0,0 +1,237 @@
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.
//
// FIX 2026-06-20 (hardware test): the shipped panel was display-only the row
// MouseArea had no onClicked and there was no password field, so a secured Wi-Fi
// network could not be joined from the GUI (user had to drop to nmcli/nmtui).
// This wires the existing Network service: click a row to connect; open or
// already-saved networks connect directly; a secured network with no saved
// profile expands an inline password field (StyledTextField, echoMode Password).
// NsOverlay grants OnDemand keyboard focus while visible, so the field is typable.
// NOTE: authored on the test machine but NOT yet built/cert'd review in builder.
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.utils
import qs.components
import qs.components.controls
NsPanel {
id: root
implicitWidth: 340
anchorMode: "right"
// ssid of the secured row currently showing its inline password field ("" = none)
property string expandedSsid: ""
// Decide what a click on a network row does.
function activate(m): void {
if (!m)
return;
if (m.active) {
// Already connected -> tapping disconnects.
Network.disconnectFromNetwork();
root.expandedSsid = "";
return;
}
if (!m.isSecure || Network.hasSavedProfile(m.ssid)) {
// Open network, or we already hold the PSK -> connect straight away.
Network.connectToNetworkWithPasswordCheck(m.ssid, m.isSecure, () => {}, m.bssid);
root.expandedSsid = "";
} else {
// Secured + no saved profile -> reveal the inline password field.
root.expandedSsid = (root.expandedSsid === m.ssid) ? "" : m.ssid;
}
}
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: ColumnLayout {
id: rowWrap
required property var modelData
readonly property bool connected: modelData?.active ?? false
readonly property bool expanded: root.expandedSsid === (modelData?.ssid ?? " ")
Layout.fillWidth: true
spacing: 2
function doConnect(): void {
const m = rowWrap.modelData;
if (!m || pwField.text.length === 0)
return;
Network.connectToNetwork(m.ssid, pwField.text, m.bssid, () => {});
pwField.text = "";
root.expandedSsid = "";
}
Rectangle {
id: row
Layout.fillWidth: true
implicitHeight: 40
radius: Theme.radius.button
color: rowWrap.connected ? Theme.accentSoft : ma.containsMouse ? Theme.hover : "transparent"
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 10
NsIcon {
icon: Icons.getNetworkIcon(rowWrap.modelData?.strength ?? 0)
color: rowWrap.connected ? Theme.accent : Theme.text
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
text: rowWrap.modelData?.ssid ?? "?"
color: Theme.text
elide: Text.ElideRight
Layout.fillWidth: true
font.family: Theme.font.family
font.pixelSize: Theme.font.bodySmall
}
StyledText {
visible: rowWrap.connected
text: "Connected"
color: Theme.accent
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
}
NsIcon {
visible: rowWrap.modelData?.isSecure ?? false
icon: "lock"
color: Theme.textFaint
}
}
MouseArea {
id: ma
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activate(rowWrap.modelData)
}
}
// inline password entry only for a secured network with no saved profile
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.bottomMargin: 4
spacing: 8
visible: rowWrap.expanded
onVisibleChanged: {
if (visible)
pwField.forceActiveFocus();
}
StyledTextField {
id: pwField
Layout.fillWidth: true
echoMode: TextField.Password
placeholderText: "Password"
onAccepted: rowWrap.doConnect()
}
Rectangle {
implicitWidth: 72
implicitHeight: 28
radius: Theme.radius.button
color: cma.containsMouse ? Theme.accent : Theme.accentSoft
StyledText {
anchors.centerIn: parent
text: "Connect"
color: Theme.text
font.family: Theme.font.family
font.pixelSize: Theme.font.meta
}
MouseArea {
id: cma
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: rowWrap.doConnect()
}
}
}
}
}
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
}
}

View file

@ -0,0 +1,179 @@
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: 0
color: Notifs.dnd ? Theme.accentSoft : dndMa.containsMouse ? Theme.hover : "transparent"
NsIcon {
anchors.centerIn: parent
icon: 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: {
// Mirror the service's own clear() / clearNotifs shortcut:
// NotifData.close() sets `closed` (drops it from notClosed so the
// card disappears), removes it from Notifs.list, dismisses the
// server notification and destroys it. Calling
// `notification?.dismiss()` directly cleared nothing it never
// touched the list, and is a no-op for notifications restored from
// disk (their `notification` is null). Iterate a slice() copy so
// mutating the list mid-loop is safe.
for (const n of Notifs.list.slice())
n.close();
}
}
}
}
// 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
NsIcon {
anchors.centerIn: parent
icon: 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
}
}
}

View 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
NsIcon {
anchors.centerIn: parent
visible: cell.occupied
icon: "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
}
}
}
}
}
}

View 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
NsIcon {
Layout.alignment: Qt.AlignHCenter
icon: 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()
}
}
}

View file

@ -0,0 +1,424 @@
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 drives hyprsunset; Airplane drives the
// NetworkManager + Bluetooth radios. (VPN tile removed pending a real backend.)
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
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
// Night Light state tracks the real hyprsunset process rather than a local
// flag, so the tile stays correct across shell reloads or changes made
// elsewhere. Seeded on load (probe runs immediately), reconciled on a timer,
// and re-checked right after a toggle.
property bool nightLightOn: false
Process {
id: nightProbe
command: ["pgrep", "-x", "hyprsunset"]
running: true
stdout: StdioCollector {
onStreamFinished: root.nightLightOn = text.trim().length > 0
}
}
Timer {
interval: 3000
running: true
repeat: true
onTriggered: nightProbe.running = true
}
Timer {
id: nightRefresh
interval: 500
repeat: false
onTriggered: nightProbe.running = true
}
// Header
RowLayout {
Layout.fillWidth: true
spacing: 12
Rectangle {
implicitWidth: 40
implicitHeight: 40
radius: 0
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: 0
color: pwrMa.containsMouse ? Theme.accentSoft : Theme.fillSubtle
NsIcon {
anchors.centerIn: parent
icon: "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"
on: root.nightLightOn
onToggled: {
if (root.nightLightOn)
Quickshell.execDetached(["pkill", "-x", "hyprsunset"]);
else
Quickshell.execDetached(["sh", "-c", "pkill -x hyprsunset; hyprsunset -t 3500"]);
nightRefresh.restart();
}
}
QSToggle {
id: airTog
icon: "airplanemode_active"
label: "Airplane"
// Reflects the real radio state: airplane is "On" when both Wi-Fi and
// Bluetooth are down. Network.wifiEnabled tracks nmcli; the adapter
// tracks BlueZ, so the tile self-corrects if radios change elsewhere.
readonly property bool radiosOff: !Network.wifiEnabled && !(Bluetooth.defaultAdapter?.enabled ?? false)
sub: radiosOff ? "On" : "Off"
on: radiosOff
onToggled: {
if (radiosOff)
Quickshell.execDetached(["sh", "-c", "nmcli radio all on; bluetoothctl power on"]);
else
Quickshell.execDetached(["sh", "-c", "nmcli radio all off; bluetoothctl power off"]);
}
}
}
// 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
}
}
NsIcon {
anchors.centerIn: parent
icon: "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: 0
color: tile.on ? Theme.accent : Qt.rgba(1, 1, 1, 0.06)
NsIcon {
anchors.centerIn: parent
icon: 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
NsIcon {
icon: s.icon
color: Theme.textMuted
}
Rectangle {
id: track
Layout.fillWidth: true
implicitHeight: 6
radius: 0
color: Theme.fillSubtle
Rectangle {
width: parent.width * Math.max(0, Math.min(1, s.value))
height: parent.height
radius: 0
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: 0
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: 0
color: mbm.containsMouse ? Theme.hover : "transparent"
NsIcon {
anchors.centerIn: parent
icon: parent.icon
color: Theme.text
}
MouseArea {
id: mbm
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: parent.activated()
}
}
}

View 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
}
}

View file

@ -24,6 +24,15 @@ Item {
implicitWidth: layout.implicitWidth + Tokens.padding.large + layout.anchors.horizontalCenterOffset * 2
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
// NoSignal: flat square glass background matching the design popouts
Rectangle {
anchors.fill: parent
color: Theme.panelBg
radius: 0
border.width: 1
border.color: Theme.panelBorder
}
ColumnLayout {
id: layout

Some files were not shown because too many files have changed in this diff Show more