Lock the bar clock to the center
This commit is contained in:
@@ -23,7 +23,7 @@ My current setup is built around these core components:
|
|||||||
### Directory Structure
|
### Directory Structure
|
||||||
|
|
||||||
- **`hypr/`**: Hyprland configurations (keybindings, window rules, animations, monitor layout, lid-close display handling, and autostart). See [`hypr/keybinds.md`](hypr/keybinds.md) for custom keybindings.
|
- **`hypr/`**: Hyprland configurations (keybindings, window rules, animations, monitor layout, lid-close display handling, and autostart). See [`hypr/keybinds.md`](hypr/keybinds.md) for custom keybindings.
|
||||||
- **`omarchy/`**: Omarchy 4 configuration — `shell.json` (bar layout, custom bar modules, and idle/lock timings), `extensions/` (entries added to the Omarchy menu), `hooks/` (event hooks such as retinting AGS on theme change), `plugins/` (cloned shell plugins for the bar), and `themed/` (extra theme templates). See [`omarchy/README.md`](omarchy/README.md) for the bar layout and the plugin clones.
|
- **`omarchy/`**: Omarchy 4 configuration — `shell.json` (bar layout, custom bar modules, and idle/lock timings), `extensions/` (entries added to the Omarchy menu), `hooks/` (event hooks such as retinting AGS on theme change), `plugins/` (forked shell plugins: the bar itself, the menu, and the workspace switcher), and `themed/` (extra theme templates). See [`omarchy/README.md`](omarchy/README.md) for the bar layout and the plugin clones.
|
||||||
- **`ags/`**: Custom desktop widgets built with TypeScript and GTK — media player, notification hub, quick settings, system monitor, wallpaper picker, theme picker, and Claude Code usage.
|
- **`ags/`**: Custom desktop widgets built with TypeScript and GTK — media player, notification hub, quick settings, system monitor, wallpaper picker, theme picker, and Claude Code usage.
|
||||||
- **`scripts/`**: Global utility scripts seamlessly exposed as commands by the installer. See [`commands.md`](commands.md).
|
- **`scripts/`**: Global utility scripts seamlessly exposed as commands by the installer. See [`commands.md`](commands.md).
|
||||||
- **`wallpapers/`**: A collection of local custom wallpapers for dynamic theming. See [`wallpaper-gallery/`](wallpaper-gallery/index.md) for the full gallery (split alphabetically across multiple pages).
|
- **`wallpapers/`**: A collection of local custom wallpapers for dynamic theming. See [`wallpaper-gallery/`](wallpaper-gallery/index.md) for the full gallery (split alphabetically across multiple pages).
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ check_file "$SCRIPT_DIR/omarchy/shell.json" "$HOME_DIR/.config/omarchy/shell.jso
|
|||||||
check_file "$SCRIPT_DIR/omarchy/extensions/omarchy-menu.jsonc" "$HOME_DIR/.config/omarchy/extensions/omarchy-menu.jsonc" "omarchy/extensions/omarchy-menu.jsonc" || check_status=1
|
check_file "$SCRIPT_DIR/omarchy/extensions/omarchy-menu.jsonc" "$HOME_DIR/.config/omarchy/extensions/omarchy-menu.jsonc" "omarchy/extensions/omarchy-menu.jsonc" || check_status=1
|
||||||
check_file "$SCRIPT_DIR/omarchy/plugins/blob.workspaces/Workspaces.qml" "$HOME_DIR/.config/omarchy/plugins/blob.workspaces/Workspaces.qml" "omarchy/plugins/blob.workspaces" || check_status=1
|
check_file "$SCRIPT_DIR/omarchy/plugins/blob.workspaces/Workspaces.qml" "$HOME_DIR/.config/omarchy/plugins/blob.workspaces/Workspaces.qml" "omarchy/plugins/blob.workspaces" || check_status=1
|
||||||
check_file "$SCRIPT_DIR/omarchy/plugins/blob.menu/Menu.qml" "$HOME_DIR/.config/omarchy/plugins/blob.menu/Menu.qml" "omarchy/plugins/blob.menu" || check_status=1
|
check_file "$SCRIPT_DIR/omarchy/plugins/blob.menu/Menu.qml" "$HOME_DIR/.config/omarchy/plugins/blob.menu/Menu.qml" "omarchy/plugins/blob.menu" || check_status=1
|
||||||
|
check_file "$SCRIPT_DIR/omarchy/plugins/blob.bar/Bar.qml" "$HOME_DIR/.config/omarchy/plugins/blob.bar/Bar.qml" "omarchy/plugins/blob.bar" || check_status=1
|
||||||
check_file "$SCRIPT_DIR/omarchy/hooks/theme-set" "$HOME_DIR/.config/omarchy/hooks/theme-set" "omarchy/hooks/theme-set" || check_status=1
|
check_file "$SCRIPT_DIR/omarchy/hooks/theme-set" "$HOME_DIR/.config/omarchy/hooks/theme-set" "omarchy/hooks/theme-set" || check_status=1
|
||||||
check_file "$SCRIPT_DIR/omarchy/themed/zen.css.tpl" "$HOME_DIR/.config/omarchy/themed/zen.css.tpl" "omarchy/themed/zen.css.tpl" || check_status=1
|
check_file "$SCRIPT_DIR/omarchy/themed/zen.css.tpl" "$HOME_DIR/.config/omarchy/themed/zen.css.tpl" "omarchy/themed/zen.css.tpl" || check_status=1
|
||||||
check_file "$SCRIPT_DIR/ags/app.ts" "$HOME_DIR/.config/ags/app.ts" "ags/app.ts" || check_status=1
|
check_file "$SCRIPT_DIR/ags/app.ts" "$HOME_DIR/.config/ags/app.ts" "ags/app.ts" || check_status=1
|
||||||
|
|||||||
+25
-6
@@ -53,9 +53,10 @@ A command module with no `exec` key is a static icon; add `exec` and
|
|||||||
|
|
||||||
## Cloned plugins
|
## Cloned plugins
|
||||||
|
|
||||||
Both were made with `omarchy plugin clone`, which copies a built-in plugin,
|
`blob.workspaces` and `blob.menu` were made with `omarchy plugin clone`, which
|
||||||
disables the original, and points the bar at the copy - hence the `blob.` ids
|
copies a built-in plugin, disables the original, and points the bar at the copy
|
||||||
in `shell.json`.
|
- hence the `blob.` ids in `shell.json`. `blob.bar` was copied by hand; see
|
||||||
|
below for why.
|
||||||
|
|
||||||
`plugins/blob.workspaces/` clones `omarchy.workspaces`. The stock widget
|
`plugins/blob.workspaces/` clones `omarchy.workspaces`. The stock widget
|
||||||
hardcodes workspaces 1-5 as always visible and has no setting for it, so the
|
hardcodes workspaces 1-5 as always visible and has no setting for it, so the
|
||||||
@@ -68,9 +69,27 @@ wider. The two oversized menus (screen recording, font picker) keep their own
|
|||||||
520 and are untouched. `omarchy-menu` still targets `omarchy.menu` on the CLI -
|
520 and are untouched. `omarchy-menu` still targets `omarchy.menu` on the CLI -
|
||||||
the manifest records `clonedFrom`, and the shell routes those calls here.
|
the manifest records `clonedFrom`, and the shell routes those calls here.
|
||||||
|
|
||||||
Re-clone after an Omarchy update if the upstream widget gains something worth
|
`plugins/blob.bar/` replaces the whole bar so the clock cannot be dragged out
|
||||||
picking up: `omarchy plugin clone omarchy.workspaces`, then re-apply the
|
of the center. Omarchy 4 puts a drag-to-reorder handler on every bar module and
|
||||||
one-line `workspaceIds()` change.
|
persists the drop into `bar.layout`; once `blob.clock` leaves the center list,
|
||||||
|
`centerAnchor` matches nothing and the center renders as a plain group. The bar
|
||||||
|
config has no setting for this, so the only lever is `canReorder` in `Bar.qml`.
|
||||||
|
The copy is two lines away from stock: a `anchored` property on `ModuleSlot`,
|
||||||
|
and `canReorder` gated on it, which locks only the module named by
|
||||||
|
`centerAnchor`. Every other widget still drags.
|
||||||
|
|
||||||
|
Do not run `omarchy plugin clone omarchy.bar` to refresh it. That command copies
|
||||||
|
the whole directory, including `widgets/`, whose manifests re-declare
|
||||||
|
`omarchy.workspaces`, `omarchy.tray`, and four more ids that already exist.
|
||||||
|
`Bar.qml` needs only `BarModel.js` - its widgets come from the host registry -
|
||||||
|
so the copy is just `manifest.json`, `Bar.qml`, and `BarModel.js`. A bar is
|
||||||
|
selected by `bar.id` in `shell.json` rather than by the enabled-plugin list, and
|
||||||
|
it has no disabled state: you leave one bar by naming another.
|
||||||
|
|
||||||
|
Re-copy after an Omarchy update if the upstream widget or bar gains something
|
||||||
|
worth picking up: `omarchy plugin clone omarchy.workspaces` then re-apply the
|
||||||
|
one-line `workspaceIds()` change, or copy `Bar.qml` and `BarModel.js` from
|
||||||
|
`/usr/share/omarchy/shell/plugins/bar/` and re-apply the two `canReorder` lines.
|
||||||
|
|
||||||
## Editing
|
## Editing
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,231 @@
|
|||||||
|
function isPlainObject(value) {
|
||||||
|
return !!value && typeof value === "object" && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePosition(value) {
|
||||||
|
var next = String(value || "").trim()
|
||||||
|
return /^(top|bottom|left|right)$/.test(next) ? next : "top"
|
||||||
|
}
|
||||||
|
|
||||||
|
function entrySettings(entry) {
|
||||||
|
if (!isPlainObject(entry)) return {}
|
||||||
|
var copy = {}
|
||||||
|
for (var key in entry) {
|
||||||
|
if (key === "id") continue
|
||||||
|
copy[key] = entry[key]
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
|
||||||
|
function entryId(entry) {
|
||||||
|
if (typeof entry === "string") return entry
|
||||||
|
if (isPlainObject(entry)) {
|
||||||
|
var id = entry["id"]
|
||||||
|
if (id !== undefined && id !== null && String(id) !== "") return String(id)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function pinTrayToInner(entries, section) {
|
||||||
|
var trayEntry = null
|
||||||
|
var result = []
|
||||||
|
var values = Array.isArray(entries) ? entries : []
|
||||||
|
for (var i = 0; i < values.length; i++) {
|
||||||
|
if (entryId(values[i]) === "omarchy.tray") trayEntry = values[i]
|
||||||
|
else result.push(values[i])
|
||||||
|
}
|
||||||
|
if (trayEntry) {
|
||||||
|
if (section === "right") result.unshift(trayEntry)
|
||||||
|
else result.push(trayEntry)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function moduleString(entry, key, fallback) {
|
||||||
|
var settings = entrySettings(entry)
|
||||||
|
var value = settings[key]
|
||||||
|
return value === undefined || value === null ? fallback : String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function entryIndex(entries, name) {
|
||||||
|
if (!Array.isArray(entries)) return -1
|
||||||
|
for (var i = 0; i < entries.length; i++) {
|
||||||
|
if (entryId(entries[i]) === name) return i
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function entriesBefore(entries, name) {
|
||||||
|
var index = entryIndex(entries, name)
|
||||||
|
return index <= 0 ? [] : entries.slice(0, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function entriesAfter(entries, name) {
|
||||||
|
var index = entryIndex(entries, name)
|
||||||
|
return index === -1 ? [] : entries.slice(index + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A shell.json write that only changes inline widget settings (the battery
|
||||||
|
// percentage toggle, a clock format change) must not rebuild the bar.
|
||||||
|
// Compare two normalized layouts: when the structure is unchanged — same
|
||||||
|
// entry ids in the same order per region — return the settings-only changes
|
||||||
|
// as {region, index, entry}. Return null when the change is structural, or
|
||||||
|
// touches an entry a live settings push cannot safely reach: custom modules
|
||||||
|
// read their entry directly rather than an injected settings property, and
|
||||||
|
// a duplicated id makes the push ambiguous.
|
||||||
|
function inlineSettingsDelta(current, next) {
|
||||||
|
if (!isPlainObject(current) || !isPlainObject(next)) return null
|
||||||
|
var regions = ["left", "center", "right"]
|
||||||
|
var counts = {}
|
||||||
|
for (var r = 0; r < regions.length; r++) {
|
||||||
|
var entries = Array.isArray(next[regions[r]]) ? next[regions[r]] : []
|
||||||
|
for (var i = 0; i < entries.length; i++) {
|
||||||
|
var id = entryId(entries[i])
|
||||||
|
counts[id] = (counts[id] || 0) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var changes = []
|
||||||
|
for (var s = 0; s < regions.length; s++) {
|
||||||
|
var region = regions[s]
|
||||||
|
var a = Array.isArray(current[region]) ? current[region] : []
|
||||||
|
var b = Array.isArray(next[region]) ? next[region] : []
|
||||||
|
if (a.length !== b.length) return null
|
||||||
|
for (var j = 0; j < a.length; j++) {
|
||||||
|
if (entryId(a[j]) !== entryId(b[j])) return null
|
||||||
|
if (JSON.stringify(a[j]) === JSON.stringify(b[j])) continue
|
||||||
|
if (customModuleType(a[j]) || customModuleType(b[j])) return null
|
||||||
|
if (counts[entryId(b[j])] > 1) return null
|
||||||
|
changes.push({ region: region, index: j, entry: b[j] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandPath(value, home) {
|
||||||
|
var path = String(value || "")
|
||||||
|
if (path === "") return ""
|
||||||
|
if (path.indexOf("~/") === 0) return home + path.substring(1)
|
||||||
|
if (path.indexOf("$HOME/") === 0) return home + path.substring(5)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
function customModuleSafeName(name) {
|
||||||
|
var value = String(name || "")
|
||||||
|
return value !== "" && value.indexOf("..") === -1 && value[0] !== "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
function customModuleType(entry) {
|
||||||
|
var settings = entrySettings(entry)
|
||||||
|
var type = String(settings.type || "")
|
||||||
|
if (type) return type
|
||||||
|
if (settings.exec) return "command"
|
||||||
|
if (settings.source) return "qml"
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function customModulePath(entry, home, configDir) {
|
||||||
|
var settings = entrySettings(entry)
|
||||||
|
var name = entryId(entry)
|
||||||
|
var source = settings.source ? expandPath(settings.source, home) : ""
|
||||||
|
if (!source && customModuleSafeName(name))
|
||||||
|
source = String(configDir || "") + "/bar/modules/" + String(name) + ".qml"
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
// A center module is mounted twice once an anchor is set: the copy that is
|
||||||
|
// actually drawn, and a zero-size placeholder holding its place in the flow
|
||||||
|
// beside the anchor. Panel routing has to pick the drawn one — it is the only
|
||||||
|
// one that can anchor a popup, carry the open-panel mark, or be found again
|
||||||
|
// by switchPanelFrom — and fall back to the placeholder only when nothing is
|
||||||
|
// on screen. The order the two are registered in is not stable across a live
|
||||||
|
// bar reconfiguration, so picking the first match is not good enough.
|
||||||
|
function isDrawnSlot(slot) {
|
||||||
|
return !!slot && slot.visible === true && slot.width > 0 && slot.height > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickDrawnSlot(slots) {
|
||||||
|
var placeholder = null
|
||||||
|
var list = slots || []
|
||||||
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
if (!list[i]) continue
|
||||||
|
if (isDrawnSlot(list[i])) return list[i]
|
||||||
|
if (!placeholder) placeholder = list[i]
|
||||||
|
}
|
||||||
|
return placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
// A bar surface is built per monitor, so a panel hotkey has several live
|
||||||
|
// copies of the same widget to route to, and the panel opens on whichever
|
||||||
|
// monitor's copy answers. Candidates are `{ slot, screenName, opened }`.
|
||||||
|
//
|
||||||
|
// An open copy wins first: hide and toggle have to reach the panel the user
|
||||||
|
// can actually see, wherever it was opened from. Otherwise the focused
|
||||||
|
// monitor's copy wins, so a summon lands where the user is working instead of
|
||||||
|
// on whichever output registered its slot first. Neither narrowing applies on
|
||||||
|
// a single monitor, or when the focused output has no bar of its own.
|
||||||
|
function pickPanelSlot(candidates, focusedScreen) {
|
||||||
|
var rows = Array.isArray(candidates) ? candidates : []
|
||||||
|
var pool = rows.filter(function(row) { return row && row.opened === true })
|
||||||
|
if (pool.length === 0) pool = rows.filter(function(row) { return !!row })
|
||||||
|
|
||||||
|
var focused = String(focusedScreen || "")
|
||||||
|
if (focused) {
|
||||||
|
var onFocused = pool.filter(function(row) { return row.screenName === focused })
|
||||||
|
if (onFocused.length > 0) pool = onFocused
|
||||||
|
}
|
||||||
|
|
||||||
|
return pickDrawnSlot(pool.map(function(row) { return row.slot }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a pointer anywhere along the bar to the closest insertion edge.
|
||||||
|
// Requiring the pointer to sit inside another widget makes the empty space
|
||||||
|
// around a centered group a dead zone, even though it visually reads as the
|
||||||
|
// most natural place to drop.
|
||||||
|
function nearestDropTarget(candidates, point, vertical) {
|
||||||
|
var rows = Array.isArray(candidates) ? candidates : []
|
||||||
|
var axis = vertical ? Number(point && point.y) : Number(point && point.x)
|
||||||
|
if (!isFinite(axis)) return null
|
||||||
|
|
||||||
|
var best = null
|
||||||
|
var bestDistance = Infinity
|
||||||
|
for (var i = 0; i < rows.length; i++) {
|
||||||
|
var row = rows[i]
|
||||||
|
if (!row || !row.slot) continue
|
||||||
|
|
||||||
|
var start = Number(vertical ? row.y : row.x)
|
||||||
|
var size = Number(vertical ? row.height : row.width)
|
||||||
|
if (!isFinite(start) || !isFinite(size) || size <= 0) continue
|
||||||
|
|
||||||
|
var beforeDistance = Math.abs(axis - start)
|
||||||
|
var afterDistance = Math.abs(axis - (start + size))
|
||||||
|
var after = afterDistance < beforeDistance
|
||||||
|
var distance = after ? afterDistance : beforeDistance
|
||||||
|
if (distance < bestDistance) {
|
||||||
|
best = { slot: row.slot, after: after }
|
||||||
|
bestDistance = distance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module !== "undefined") {
|
||||||
|
module.exports = {
|
||||||
|
isDrawnSlot: isDrawnSlot,
|
||||||
|
pickDrawnSlot: pickDrawnSlot,
|
||||||
|
pickPanelSlot: pickPanelSlot,
|
||||||
|
nearestDropTarget: nearestDropTarget,
|
||||||
|
normalizePosition: normalizePosition,
|
||||||
|
entrySettings: entrySettings,
|
||||||
|
entryId: entryId,
|
||||||
|
pinTrayToInner: pinTrayToInner,
|
||||||
|
moduleString: moduleString,
|
||||||
|
entryIndex: entryIndex,
|
||||||
|
entriesBefore: entriesBefore,
|
||||||
|
entriesAfter: entriesAfter,
|
||||||
|
inlineSettingsDelta: inlineSettingsDelta,
|
||||||
|
expandPath: expandPath,
|
||||||
|
customModuleSafeName: customModuleSafeName,
|
||||||
|
customModuleType: customModuleType,
|
||||||
|
customModulePath: customModulePath
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "blob.bar",
|
||||||
|
"name": "My Bar",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Omarchy",
|
||||||
|
"description": "Status bar with widgets",
|
||||||
|
"kinds": [
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
|
"entryPoints": {
|
||||||
|
"bar": "Bar.qml"
|
||||||
|
},
|
||||||
|
"omarchy": {
|
||||||
|
"clonedFrom": "omarchy.bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
"lock": 301
|
"lock": 301
|
||||||
},
|
},
|
||||||
"bar": {
|
"bar": {
|
||||||
|
"id": "blob.bar",
|
||||||
"position": "top",
|
"position": "top",
|
||||||
"transparent": false,
|
"transparent": false,
|
||||||
"centerAnchor": "blob.clock",
|
"centerAnchor": "blob.clock",
|
||||||
|
|||||||
Reference in New Issue
Block a user