Compare commits

..

10 commits

Author SHA1 Message Date
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
65 changed files with 766 additions and 407 deletions

BIN
assets/nosignal-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

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()
}

View file

@ -13,7 +13,7 @@ Rectangle {
implicitWidth: 38
implicitHeight: 22
radius: 11
radius: 0
color: on ? Theme.accent : Theme.fillSubtle
Behavior on color {
@ -27,7 +27,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 16
implicitHeight: 16
radius: 8
radius: 0
color: root.on ? Theme.onAccent : Theme.text
Behavior on x {

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

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

@ -198,7 +198,9 @@ StyledWindow {
id: osdBg
panel: panels.osdWrapper
deformAmount: 0.25
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 {

View file

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

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

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

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

@ -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
NsIcon {
Layout.alignment: Qt.AlignHCenter
icon: "map"
color: Colours.palette.m3outlineVariant
fontStyle: 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

@ -216,6 +216,11 @@ PageBase {
Layout.fillWidth: true
}
// NoSignal: CachyOS repos enable/disable (sudoless, auto-tier)
CachyRepoToggleRow {
Layout.fillWidth: true
}
SelectRow {
Layout.fillWidth: true
last: true

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

View file

@ -16,6 +16,7 @@ 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
@ -139,10 +140,12 @@ Variants {
}
}
// System tray
// 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)
@ -164,7 +167,7 @@ Variants {
anchors.topMargin: 1
implicitWidth: 6
implicitHeight: 6
radius: 3
radius: 0
color: Theme.accent
}
}

View file

@ -122,13 +122,13 @@ NsPanel {
Rectangle {
Layout.fillWidth: true
implicitHeight: 6
radius: 3
radius: 0
color: Theme.fillSubtle
Rectangle {
width: parent.width * Math.max(0, Math.min(1, s.value))
height: parent.height
radius: 3
radius: 0
color: Theme.accent
}
@ -137,7 +137,7 @@ NsPanel {
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 14
implicitHeight: 14
radius: 7
radius: 0
color: Theme.accent
}

View file

@ -109,7 +109,7 @@ NsPanel {
visible: parent.modelData > 0
implicitWidth: 26
implicitHeight: 26
radius: 13
radius: 0
color: parent.today ? Theme.accent : "transparent"
StyledText {

View file

@ -40,7 +40,7 @@ NsPanel {
Rectangle {
implicitWidth: 26
implicitHeight: 26
radius: 13
radius: 0
color: Notifs.dnd ? Theme.accentSoft : dndMa.containsMouse ? Theme.hover : "transparent"
NsIcon {

View file

@ -3,10 +3,13 @@ pragma ComponentBehavior: Bound
// NoSignal Quick Settings popout (off the media / battery pill). Avatar + user +
// uptime + power button; 2-col toggle grid; brightness + volume sliders; media
// card. Wired to the real services (Network, Bluetooth, Notifs, Audio,
// Brightness, Players); Night Light / Airplane / VPN are stateful toggles for now.
// 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
@ -20,6 +23,35 @@ NsPanel {
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
@ -28,7 +60,7 @@ NsPanel {
Rectangle {
implicitWidth: 40
implicitHeight: 40
radius: 20
radius: 0
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
@ -75,7 +107,7 @@ NsPanel {
Rectangle {
implicitWidth: 34
implicitHeight: 34
radius: 17
radius: 0
color: pwrMa.containsMouse ? Theme.accentSoft : Theme.fillSubtle
NsIcon {
@ -128,21 +160,31 @@ NsPanel {
icon: "nightlight"
label: "Night Light"
sub: on ? "Warm 3500K" : "Off"
onToggled: on = !on
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"
sub: on ? "On" : "Off"
onToggled: on = !on
}
QSToggle {
id: vpnTog
icon: "vpn_key"
label: "VPN"
sub: on ? "On" : "Off"
onToggled: on = !on
// 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"]);
}
}
}
@ -264,7 +306,7 @@ NsPanel {
Rectangle {
implicitWidth: 30
implicitHeight: 30
radius: 15
radius: 0
color: tile.on ? Theme.accent : Qt.rgba(1, 1, 1, 0.06)
NsIcon {
@ -319,13 +361,13 @@ NsPanel {
Layout.fillWidth: true
implicitHeight: 6
radius: 3
radius: 0
color: Theme.fillSubtle
Rectangle {
width: parent.width * Math.max(0, Math.min(1, s.value))
height: parent.height
radius: 3
radius: 0
color: Theme.accent
}
@ -334,7 +376,7 @@ NsPanel {
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 14
implicitHeight: 14
radius: 7
radius: 0
color: Theme.accent
}
@ -362,7 +404,7 @@ NsPanel {
implicitWidth: 28
implicitHeight: 28
radius: 14
radius: 0
color: mbm.containsMouse ? Theme.hover : "transparent"
NsIcon {

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

View file

@ -160,9 +160,9 @@ Item {
Component {
id: iconBtn
MaterialIcon {
NsIcon {
animate: action.modelData.isCopy ?? false
text: action.modelData.isCopy ? "content_copy" : "close"
icon: action.modelData.isCopy ? "content_copy" : "close"
color: Colours.palette.m3onSurfaceVariant
}
}

View file

@ -110,8 +110,8 @@ StyledRect {
Component {
id: materialIconComp
MaterialIcon {
text: Icons.getNotifIcon(root.activeNotifs[0]?.summary, root.urgency)
NsIcon {
icon: Icons.getNotifIcon(root.activeNotifs[0]?.summary, root.urgency)
color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
fontStyle: Tokens.font.icon.medium
}
@ -214,9 +214,9 @@ StyledRect {
font: Tokens.font.body.small
}
MaterialIcon {
NsIcon {
Layout.rightMargin: -Tokens.padding.extraSmall / 2
text: "expand_more"
icon: "expand_more"
color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurfaceVariant
rotation: root.expanded ? 180 : 0
Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.extraSmall) : 0

View file

@ -33,11 +33,11 @@ StyledRect {
radius: Tokens.rounding.full
color: IdleInhibitor.enabled ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
MaterialIcon {
NsIcon {
id: icon
anchors.centerIn: parent
text: "coffee"
icon: "coffee"
color: IdleInhibitor.enabled ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
fontStyle: Tokens.font.icon.large
}

View file

@ -43,12 +43,12 @@ StyledRect {
radius: Tokens.rounding.full
color: Recorder.running ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
MaterialIcon {
NsIcon {
id: icon
anchors.centerIn: parent
anchors.verticalCenterOffset: 1
text: "screen_record"
icon: "screen_record"
color: Recorder.running ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
fontStyle: Tokens.font.icon.large
}

View file

@ -29,9 +29,9 @@ ColumnLayout {
RowLayout {
spacing: Tokens.spacing.medium
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignVCenter
text: "list"
icon: "list"
fontStyle: Tokens.font.icon.large
}
@ -165,9 +165,9 @@ ColumnLayout {
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: "scan_delete"
icon: "scan_delete"
color: Colours.palette.m3outline
fontStyle: Tokens.font.icon.extraLarge
@ -193,9 +193,9 @@ ColumnLayout {
RowLayout {
spacing: Tokens.spacing.medium
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: "scan_delete"
icon: "scan_delete"
color: Colours.palette.m3outline
opacity: !root.props.recordingListExpanded ? 1 : 0

View file

@ -70,11 +70,11 @@ StyledRect {
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Tokens.padding.large
MaterialIcon {
NsIcon {
id: icon
anchors.centerIn: parent
text: root.modelData.icon
icon: root.modelData.icon
color: {
if (root.modelData.type === Toast.Success)
return Colours.palette.m3onSuccess;

View file

@ -41,13 +41,13 @@ ColumnLayout {
onClicked: root.moveToWsExpanded = !root.moveToWsExpanded
}
MaterialIcon {
NsIcon {
id: moveToWsIcon
anchors.centerIn: parent
animate: true
text: root.moveToWsExpanded ? "expand_more" : "keyboard_arrow_right"
icon: root.moveToWsExpanded ? "expand_more" : "keyboard_arrow_right"
color: Colours.palette.m3onPrimary
fontStyle: Tokens.font.icon.large
}

View file

@ -135,11 +135,11 @@ ColumnLayout {
spacing: Tokens.spacing.medium
MaterialIcon {
NsIcon {
id: icon
Layout.alignment: Qt.AlignVCenter
text: detail.icon
icon: detail.icon
}
StyledText {

View file

@ -40,9 +40,9 @@ Item {
sourceComponent: ColumnLayout {
spacing: 0
MaterialIcon {
NsIcon {
Layout.alignment: Qt.AlignHCenter
text: "web_asset_off"
icon: "web_asset_off"
color: Colours.palette.m3outline
fontStyle: Tokens.font.icon.builders.extraLarge.scale(3).build()
}

View file

@ -2,8 +2,8 @@ pragma Singleton
pragma ComponentBehavior: Bound
// NoSignal icon remap: Material Symbol name -> Nerd Font (JetBrainsMono NF)
// codepoint (Font Awesome range), via String.fromCharCode so the source stays
// pure ASCII. Used by components/NsIcon. Covers bar/panel icons + the dynamic
// codepoint (Font Awesome + MDI ranges), via String.fromCodePoint so the source
// stays pure ASCII. Used by components/NsIcon. Covers bar/panel icons + the dynamic
// Icons.getX() returns. get() falls back to a question glyph so anything
// unmapped is visible (easy to spot + add a mapping).
@ -14,7 +14,9 @@ Singleton {
id: root
function cp(code: int): string {
return String.fromCharCode(code);
// fromCodePoint (not fromCharCode) so astral-plane glyphs (Nerd Font
// MDI range, > 0xFFFF) emit correct surrogate pairs, not truncated.
return String.fromCodePoint(code);
}
readonly property var map: {
@ -143,16 +145,21 @@ Singleton {
"warning": root.cp(0xf071),
"web_asset_off": root.cp(0xf2d0),
"wifi_find": root.cp(0xf1eb),
// weather (Icons.getWeatherIcon)
"clear_day": root.cp(0xf185),
"clear_night": root.cp(0xf186),
"partly_cloudy_day": root.cp(0xf6c4),
"cloudy": root.cp(0xf0c2),
"cloud": root.cp(0xf0c2),
"rainy": root.cp(0xf73d),
"cloudy_snowing": root.cp(0xf2dc),
"foggy": root.cp(0xf75f),
"air": root.cp(0xf72e),
// weather (Icons.getWeatherIcon) Material-weather family, every
// codepoint verified present in JetBrainsMono Nerd Font Mono. The old
// Font-Awesome mapping had several glyphs absent from the font (rendered
// as "?" boxes) and was missing thunderstorm/snowing_heavy (F-D1).
"clear_day": root.cp(0xf0599),
"clear_night": root.cp(0xf0594),
"partly_cloudy_day": root.cp(0xf0595),
"cloudy": root.cp(0xf0590),
"cloud": root.cp(0xf0590),
"rainy": root.cp(0xf0597),
"cloudy_snowing": root.cp(0xf0598),
"snowing_heavy": root.cp(0xf0f37),
"thunderstorm": root.cp(0xf0593),
"foggy": root.cp(0xf0591),
"air": root.cp(0xf059d),
// apps / devices (Icons.get*)
"code": root.cp(0xf121),
"forum": root.cp(0xf086),
@ -197,7 +204,55 @@ Singleton {
"update": root.cp(0xf021),
"wallpaper": root.cp(0xf03e),
"workspaces": root.cp(0xf009),
"dock": root.cp(0xf2d1)
"dock": root.cp(0xf2d1),
// lock screen
"login": root.cp(0xf090),
"key": root.cp(0xf084),
// dashboard / utilities / launcher leftovers
"account_tree": root.cp(0xf0e8),
"android": root.cp(0xf17b),
"category": root.cp(0xf02c),
"clear_all": root.cp(0xf2ed),
"compare": root.cp(0xf362),
"compare_arrows": root.cp(0xf362),
"contrast": root.cp(0xf042),
"delete_forever": root.cp(0xf1f8),
"filter_b_and_w": root.cp(0xf0b0),
"fullscreen": root.cp(0xf065),
"gamepad": root.cp(0xf11b),
"gradient": root.cp(0xf043),
"hard_disk": root.cp(0xf0a0),
"keep": root.cp(0xf08d),
"location_on": root.cp(0xf041),
"location_searching": root.cp(0xf05b),
"looks": root.cp(0xf53f),
"memory": root.cp(0xf2db),
"notifications_off": root.cp(0xf1f6),
"nutrition": root.cp(0xf787),
"page_header": root.cp(0xf036),
"picture_in_picture_center": root.cp(0xf2d0),
"resize": root.cp(0xf065),
"screenshot_region": root.cp(0xf247),
"select_to_speak": root.cp(0xf028),
"sentiment_calm": root.cp(0xf118),
"sentiment_very_dissatisfied": root.cp(0xf119),
"stop": root.cp(0xf04d),
"thermostat": root.cp(0xf2c9),
"water_drop": root.cp(0xf043),
"wb_twilight": root.cp(0xf185),
// additions-page icons (nexus Additional Software)
"brush": root.cp(0xf1fc), // fa-paintbrush
"cloud_sync": root.cp(0xf063f), // md-cloud_sync
"data_object": root.cp(0xf0169), // md-code_braces
"deployed_code": root.cp(0xf1b2), // fa-cube
"movie_edit": root.cp(0xf008), // fa-film
"neurology": root.cp(0xee9c), // fa-brain
"psychology": root.cp(0xf133c), // md-head_cog
"robot_2": root.cp(0xee0d), // fa-robot
"smart_toy": root.cp(0xf06a9), // md-robot
"sports_esports": root.cp(0xf11b), // fa-gamepad
"videocam": root.cp(0xf03d), // fa-video_camera
"vpn_lock": root.cp(0xf099d) // md-shield_lock
}
function get(name: string): string {

View file

@ -41,15 +41,43 @@ Singleton {
fetchCoordsFromCity(configLocation);
}
} else if (!loc || timer.elapsed() > 900) {
Requests.get("https://ipinfo.io/json", text => {
fetchLocationFromIp();
}
}
// Auto-detect location from the public IP (used when weatherLocation is
// empty). Stock caelestia only queried ipinfo.io with no error handling;
// on the 2026-06-15 hardware test that call never populated `loc` (the
// city path works via the same Requests.get, so it is provider-specific
// finding F-W2). Repair: wrap parsing defensively and fall back to a second
// provider (geojs.io, which returns separate latitude/longitude fields) on
// any error or empty response.
function fetchLocationFromIp(): void {
const useGeoJs = () => {
Requests.get("https://get.geojs.io/v1/ip/geo.json", text => {
try {
const g = JSON.parse(text);
if (g.latitude && g.longitude) {
loc = g.latitude + "," + g.longitude;
city = g.city ?? "";
timer.restart();
}
} catch (e) {}
});
};
Requests.get("https://ipinfo.io/json", text => {
try {
const response = JSON.parse(text);
if (response.loc) {
loc = response.loc;
city = response.city ?? "";
timer.restart();
return;
}
});
}
} catch (e) {}
useGeoJs();
}, useGeoJs);
}
function fetchCityFromCoords(coords: string): void {
@ -131,8 +159,10 @@ Singleton {
for (let i = 0; i < json.daily.time.length; i++)
forecastList.push({
date: json.daily.time[i].replace(/-/g, "/"),
maxTempC: json.daily.temperature_2m_max[i],
minTempC: json.daily.temperature_2m_min[i],
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
minTempC: Math.round(json.daily.temperature_2m_min[i]),
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),
weatherCode: json.daily.weather_code[i],
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
});
@ -210,6 +240,11 @@ Singleton {
onLocChanged: fetchWeatherData()
// Stock caelestia only called reload() on a config CHANGE, never at startup,
// so a saved location blanked after a reboot and an empty (auto/IP) location
// never populated on a fresh boot (finding F-W1). Fetch once on load.
Component.onCompleted: reload()
Connections {
function onWeatherLocationChanged(): void {
root.reload();
@ -222,7 +257,7 @@ Singleton {
interval: 3600000 // 1 hour
running: true
repeat: true
onTriggered: fetchWeatherData()
onTriggered: reload() // re-detect an auto/IP location too, not just refetch a known one
}
ElapsedTimer {