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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions BarWidget.qml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,33 @@ BarWidget {
readonly property int maxIcons: Math.max(1, Math.min(10, Math.floor(Number(setting("maxIcons", service ? service.barMaxIcons : 5)) || 5)))
readonly property bool hideWhenIdle: setting("hideWhenIdle", service ? service.hideBarWhenIdle : false) !== false
readonly property bool showInBar: setting("showInBar", service ? service.showInBar : true) !== false
// brand keeps the Greyforge cyan and amber. theme matches the bar: wings in
// the bar foreground, halo and state markers in the theme accent. accent
// paints the whole mark in the accent. Alerts always use the urgent colour.
readonly property string accentMode: Model.normalizeBarAccent(setting("barAccent", service ? service.barAccent : Model.DEFAULT_BAR_ACCENT))
readonly property bool themed: accentMode === "theme"
readonly property bool monoAccent: accentMode === "accent"
readonly property bool recolored: themed || monoAccent
readonly property var trayEntries: tray ? parkedEntries.slice(0, maxIcons) : []
readonly property int overflow: tray ? Math.max(0, parked - maxIcons) : parked
readonly property real iconSize: Style.bar.iconCanvas
readonly property color foreground: bar ? bar.barForeground : Color.bar.text
readonly property color brandAmber: "#fda52b"
readonly property color brandCyan: "#38c8e8"
// Wings (theme: bar text, accent: the accent itself).
readonly property color markColor: themed ? foreground : (monoAccent ? Color.accent : brandCyan)
// Halo and state accents.
readonly property color detailColor: recolored ? Color.accent : brandCyan
readonly property color parkedColor: recolored ? Color.accent : brandAmber
// Chrome around the per-window icons: subtle bar text in two-tone mode.
readonly property color chromeColor: themed ? foreground : (monoAccent ? Color.accent : brandCyan)
property bool pulse: false

// Black or white, whichever stays readable on a filled badge.
function readableOn(c) {
return (0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b) > 0.55 ? "#1a1206" : "#f7f3ea"
}

visible: showInBar && (service ? (parked > 0 || attention || !hideWhenIdle) : !hideWhenIdle)
implicitWidth: vertical ? barSize : layout.implicitWidth
implicitHeight: vertical ? layout.implicitHeight : barSize
Expand Down Expand Up @@ -133,14 +152,17 @@ BarWidget {
anchors.horizontalCenterOffset: root.vertical || root.overflow === 0 ? 0 : -Style.space(3)
size: root.iconSize + Style.space(2)
surface: bar ? bar.background : Color.bar.background
dot: root.attention ? (bar ? bar.urgent : Color.urgent) : root.brandAmber
tint: root.recolored ? root.markColor : "transparent"
halo: root.recolored ? root.detailColor : "transparent"
dot: root.attention ? (bar ? bar.urgent : Color.urgent) : root.parkedColor
showDot: root.parked > 0 || root.attention
scale: root.pulse ? 1.15 : 1
Behavior on scale { NumberAnimation { duration: 180; easing.type: Easing.OutBack } }
}

// Count badge
Rectangle {
id: badge
visible: root.overflow > 0
anchors.right: parent.right
anchors.bottom: parent.bottom
Expand All @@ -149,13 +171,13 @@ BarWidget {
width: Math.max(height, badgeText.implicitWidth + Style.space(5))
height: Style.font.caption + Style.space(3)
radius: height / 2
color: root.attention ? (bar ? bar.urgent : Color.urgent) : root.brandAmber
color: root.attention ? (bar ? bar.urgent : Color.urgent) : root.parkedColor
Text {
id: badgeText
anchors.centerIn: parent
text: (root.tray ? "+" : "") + root.overflow
textFormat: Text.PlainText
color: "#1a1206"
color: root.readableOn(badge.color)
font.family: bar ? bar.fontFamily : Style.font.family
font.pixelSize: Style.font.caption
font.bold: true
Expand Down Expand Up @@ -194,7 +216,7 @@ BarWidget {
radius: Math.max(Style.space(4), Math.min(Style.cornerRadius, Style.space(6)))
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, slot.hovered ? 0.14 : 0.07)
border.width: 1
border.color: Qt.rgba(root.brandCyan.r, root.brandCyan.g, root.brandCyan.b, slot.hovered ? 0.7 : 0.3)
border.color: Qt.rgba(root.chromeColor.r, root.chromeColor.g, root.chromeColor.b, slot.hovered ? 0.7 : 0.3)
Behavior on color { ColorAnimation { duration: 120 } }
}

Expand Down Expand Up @@ -225,7 +247,7 @@ BarWidget {
width: Style.space(5)
height: width
radius: width / 2
color: modelData.recovered ? root.brandCyan : root.brandAmber
color: modelData.recovered ? root.detailColor : root.parkedColor
border.width: 1
border.color: bar ? bar.background : Color.bar.background
}
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

All notable changes to Reprieve. Versions follow SemVer.

## [Unreleased]

### Added
- Bar widget colour option (`barAccent`): `brand` keeps the cyan angel and
amber markers, `theme` makes the mark two-tone like other bar icons (wings
in the bar text colour, halo and parked markers in the theme accent), and
`accent` paints the whole mark in the accent. Alerts keep the urgent colour.
`reprieve bar theme on|off` and `reprieve bar accent on|off` switch it;
`reprieve bar status` reports it.

## [1.5.5] — 2026-10-25

### Fixed
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,19 @@ has three parts:
it home, right-click pulls it to the current workspace;
- a `+N` overflow once more windows are hidden than icons shown.

The cyan angel and amber markers are Greyforge's brand colours, so they stand
apart from the rest of the bar. Most bar icons draw in the bar's text colour
and reserve the theme accent for state and detail; `reprieve bar theme on`
does the same with the Reprieve mark — wings in the bar text colour, halo and
parked markers in the active Omarchy accent. The attention colour still comes
from the theme's urgent colour. Prefer a single colour? `reprieve bar accent
on` paints the whole angel in the accent. Switch back with `reprieve bar theme
off` (or `reprieve bar accent off`).

<p align="center">
<img src="docs/screenshots/bar-colours.png" alt="The Reprieve bar widget in brand, two-tone and accent colours" width="680">
</p>

Left-click the mark for the timeline, right-click to bring back the latest
window, middle-click to bring back all of them. The mark swells when a
window is parked. The widget stays in the bar at all times — the safety
Expand All @@ -182,6 +195,8 @@ reprieve bar status # placement and toggles
reprieve bar hide-idle on # fade out when there is nothing to show
reprieve bar tray off # mark and count only
reprieve bar icons 8 # icons before collapsing to +N (1–10)
reprieve bar theme on # two-tone: bar text wings, accent halo/markers
reprieve bar accent on # solid theme accent angel
reprieve bar show off # remove from the bar; the service keeps running
reprieve bar install center # place it, or move it to another section
```
Expand Down Expand Up @@ -280,6 +295,7 @@ placed, otherwise under `plugins[]`).
| `showInBar` | `true` | Show the bar widget |
| `barTray` | `true` | Per-window icons in the bar (off = glyph and count) |
| `barMaxIcons` | `5` | Icons before collapsing into `+N` (1–10) |
| `barAccent` | `brand` | Widget colours: `brand` keeps the cyan angel and amber markers, `theme` is two-tone (bar text wings, accent halo and markers), `accent` is solid accent (alerts stay urgent) |
| `hideBarWhenIdle` | `false` | Fade the widget out when nothing is hidden and nothing needs attention (`true` restores hide-when-idle) |

```json
Expand Down
13 changes: 13 additions & 0 deletions ReprieveModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ function normalizeFlight(value) {
return FLIGHT_MODES.indexOf(v) === -1 ? DEFAULT_FLIGHT : v
}

// Bar widget accent: the fixed Greyforge cyan and amber, a two-tone theme
// mark (bar text + theme accent), or a solid theme accent. Anything
// unrecognised keeps the brand colours.
var BAR_ACCENTS = ["brand", "theme", "accent"]
var DEFAULT_BAR_ACCENT = "brand"
function normalizeBarAccent(value) {
var v = String(value === undefined || value === null ? "" : value).trim().toLowerCase()
return BAR_ACCENTS.indexOf(v) === -1 ? DEFAULT_BAR_ACCENT : v
}

// Parked-at stamps, floored to whole milliseconds. Entries without a
// positive stamp predate the timeout feature and are exempt from expiry.
function stampMs(value) {
Expand Down Expand Up @@ -935,6 +945,9 @@ if (typeof module !== "undefined") {
FLIGHT_MODES: FLIGHT_MODES,
DEFAULT_FLIGHT: DEFAULT_FLIGHT,
normalizeFlight: normalizeFlight,
BAR_ACCENTS: BAR_ACCENTS,
DEFAULT_BAR_ACCENT: DEFAULT_BAR_ACCENT,
normalizeBarAccent: normalizeBarAccent,
createState: createState,
cloneState: cloneState,
normalizeAddress: normalizeAddress,
Expand Down
10 changes: 9 additions & 1 deletion Service.qml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ Item {
// Bar widget preferences (also overridable per layout entry in shell.json).
readonly property bool barTray: setting("barTray", true) !== false
readonly property int barMaxIcons: Math.max(1, Math.min(10, Math.floor(Number(setting("barMaxIcons", 5)) || 5)))
// brand keeps Greyforge cyan and amber; theme makes the mark two-tone
// (bar text + accent); accent paints it in the accent (see
// Model.normalizeBarAccent).
readonly property string barAccent: Model.normalizeBarAccent(setting("barAccent", Model.DEFAULT_BAR_ACCENT))
// The mark stays in the bar at all times so the safety net is visible
// even when idle; users can still fade it with hideBarWhenIdle.
readonly property bool hideBarWhenIdle: setting("hideBarWhenIdle", false) !== false
Expand Down Expand Up @@ -312,6 +316,7 @@ Item {
showToast: "bool", pauseMediaOnPark: "bool", trackAppClose: "bool",
showInBar: "bool", barTray: "bool", hideBarWhenIdle: "bool",
maxStack: "int", barMaxIcons: "int", setupDismissed: "bool",
barAccent: "barAccent",
parkTimeout: "int", flight: "flight", parkFlight: "flight", restoreFlight: "flight"
}
var kind = allowed[String(name)]
Expand All @@ -321,6 +326,9 @@ Item {
if (kind === "flight") {
if (Model.FLIGHT_MODES.indexOf(String(rawValue).trim().toLowerCase()) === -1) return "expected off|subtle|angel"
value = Model.normalizeFlight(rawValue)
} else if (kind === "barAccent") {
if (Model.BAR_ACCENTS.indexOf(String(rawValue).trim().toLowerCase()) === -1) return "expected brand|theme|accent"
value = Model.normalizeBarAccent(rawValue)
} else if (kind === "bool") {
var s = String(rawValue).toLowerCase()
if (s === "true" || s === "on" || s === "1" || s === "yes") value = true
Expand Down Expand Up @@ -1410,7 +1418,7 @@ Item {
summary.stranded = root.strandedCount
summary.attention = root.attentionReason
summary.entry = root.entryLocation
summary.bar = { placed: root.entryLocation === "bar", show: root.showInBar, tray: root.barTray, hideWhenIdle: root.hideBarWhenIdle, maxIcons: root.barMaxIcons }
summary.bar = { placed: root.entryLocation === "bar", show: root.showInBar, tray: root.barTray, hideWhenIdle: root.hideBarWhenIdle, maxIcons: root.barMaxIcons, accent: root.barAccent }
summary.journal = root.journalStatus
summary.flight = { parkMode: root.parkFlight, restoreMode: root.restoreFlight, frames: root.flightHandler ? Object.keys(root.flightHandler.memory).length : 0, lastSkip: root.lastFlightSkip, lastRead: root.lastParkRead, screens: root.flightHandler ? Object.keys(root.flightHandler.containers) : [], active: root.flightHandler ? root.flightHandler.activeCount : 0, mode: root.flight, handler: !!root.flightHandler, pending: Object.keys(root.pendingFlights).length, flown: root.flightHandler ? Number(root.flightHandler.flown || 0) : 0, anchors: root.barAnchors }
summary.session = root.session ? root.session.slice(0, 12) : ""
Expand Down
22 changes: 19 additions & 3 deletions bin/reprieve
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@ Usage: reprieve <command>
remove-binds Remove Reprieve's keybinding block
uninstall Restore parked windows and remove keybindings
bar Bar widget: status | install [left|center|right] |
show on|off | tray on|off | hide-idle on|off | icons N
show on|off | tray on|off | hide-idle on|off | icons N |
theme on|off | accent on|off
set KEY VALUE Change a setting (maxStack, parkTimeout, flight, parkFlight,
restoreFlight, pauseMediaOnPark,
trackAppClose, showToast, showInBar, barTray, hideBarWhenIdle,
barMaxIcons)
barMaxIcons, barAccent)
doctor Check the installation without changing anything
USAGE
}
Expand All @@ -86,6 +87,7 @@ print("placed in bar " + ("yes" if b.get("placed") else "no (run: reprieve bar
print("show " + ("on" if b.get("show", True) else "off"))
print("tray icons " + ("on" if b.get("tray", True) else "off") + " (max " + str(b.get("maxIcons", 5)) + ")")
print("hide when idle " + ("on" if b.get("hideWhenIdle", False) else "off"))
print("colours " + {"theme": "two-tone (bar text + accent)", "accent": "theme accent (solid)"}.get(b.get("accent"), "brand (cyan and amber)"))
t = d.get("parkTimeout") or 0
print("park timeout " + ("off (reprieve set parkTimeout N)" if not t else str(t) + " s"))
print("parked " + str(d.get("parked", 0)))'
Expand Down Expand Up @@ -134,7 +136,21 @@ for k, v in (json.load(sys.stdin) or {}).items():
tray) need_plugin setSetting barTray "${1:-on}" ;;
hide-idle) need_plugin setSetting hideBarWhenIdle "${1:-on}" ;;
icons) need_plugin setSetting barMaxIcons "${1:-5}" ;;
*) echo "usage: reprieve bar status|install [section]|show on|off|tray on|off|hide-idle on|off|icons N" >&2; exit 2 ;;
theme)
case "${1:-on}" in
on|true|yes|theme) need_plugin setSetting barAccent theme ;;
off|false|no|brand) need_plugin setSetting barAccent brand ;;
*) echo "reprieve: expected theme on|off" >&2; exit 2 ;;
esac
;;
accent)
case "${1:-on}" in
on|true|yes|accent) need_plugin setSetting barAccent accent ;;
off|false|no|brand) need_plugin setSetting barAccent brand ;;
*) echo "reprieve: expected accent on|off" >&2; exit 2 ;;
esac
;;
*) echo "usage: reprieve bar status|install [section]|show on|off|tray on|off|hide-idle on|off|icons N|theme on|off|accent on|off" >&2; exit 2 ;;
esac
}

Expand Down
64 changes: 61 additions & 3 deletions brand/ReprieveAngel.qml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import QtQuick
import QtQuick.Effects

// Reprieve product mark: a halo over swept angel wings in cyan and electric
// blue. Fixed brand artwork — unlike GreyforgeMark it does not recolor with
// the theme, so state (attention, parked count, pulse) is carried by the
// blue. The default artwork is fixed brand art; `tint` and `halo` let a caller
// repaint the wings and halo in theme colours (the bar widget uses the bar
// foreground plus the accent for its two-tone mode). State is carried by the
// caller's overlays, and legibility on light surfaces comes from the
// light-surface variant, picked automatically from `surface` unless
// `variant` is set explicitly.
Expand All @@ -14,6 +16,11 @@ import QtQuick
// dot — optional status dot colour ("" = none), drawn bottom-left so
// it never collides with the count badge callers put bottom-right
// showDot — dot visibility switch
// tint — optional flat colour for the wings (transparent = brand art);
// callers use it to follow the bar foreground like every other
// widget glyph
// halo — optional flat colour for the halo, so the mark can stay
// two-tone; defaults to `tint` for a single-colour mark
// artOpacity — for dimmed states (empty timeline, idle)
Item {
id: root
Expand All @@ -24,11 +31,16 @@ Item {
property bool compact: size <= 24
property color dot: "#fda52b"
property bool showDot: false
property color tint: "transparent"
property color halo: tint
property real artOpacity: 1.0

width: size
height: size

readonly property bool tinted: tint.a > 0
readonly property bool haloShown: tinted && halo.a > 0

function luminance(c) {
return 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b
}
Expand All @@ -44,14 +56,60 @@ Item {
return compact ? "icons/reprieve-angel-compact.svg" : "icons/reprieve-angel.svg"
}

// Brand artwork (multi-colour): shown as-is unless a flat tint is asked for.
Image {
id: art
id: brandArt
anchors.fill: parent
source: root.art
fillMode: Image.PreserveAspectFit
smooth: true
asynchronous: true
opacity: root.artOpacity
visible: !root.tinted
}

// Flat masks, painted by MultiEffect. Their layers stay enabled so a live
// colour-mode switch has a texture ready; toggling layer.enabled on a newly
// created widget can otherwise leave the mark blank until a reload.
Image {
id: bodyMask
anchors.fill: parent
source: "icons/reprieve-angel-mask.svg"
fillMode: Image.PreserveAspectFit
smooth: true
asynchronous: false
visible: false
layer.enabled: true
}

MultiEffect {
anchors.fill: bodyMask
source: bodyMask
visible: root.tinted
opacity: root.artOpacity
colorization: 1.0
colorizationColor: root.tint
}

// The halo rides on top of the wings so it can carry the second colour.
Image {
id: haloMask
anchors.fill: parent
source: "icons/reprieve-angel-halo-mask.svg"
fillMode: Image.PreserveAspectFit
smooth: true
asynchronous: false
visible: false
layer.enabled: true
}

MultiEffect {
anchors.fill: haloMask
source: haloMask
visible: root.haloShown
opacity: root.artOpacity
colorization: 1.0
colorizationColor: root.halo
}

Rectangle {
Expand Down
7 changes: 7 additions & 0 deletions brand/icons/reprieve-angel-halo-mask.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions brand/icons/reprieve-angel-mask.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/bar-colours.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading