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
2 changes: 1 addition & 1 deletion manual/06-themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Omarchy comes with twenty-two beautiful themes. You can select between them via

Each theme styles the desktop, terminal, neovim, activity screen (btop), Chromium, and the entire Omarchy shell: top bar, menu, notifications, OSD, and the lock screen. (For Obsidian, you must manually select the Omarchy theme via _Appearance > Themes_ inside the app).

Themes have a set of background images that you can pick between using `Super + Ctrl + Space`.
Themes have a set of background images that you can pick between using `Super + Ctrl + Space`. In the theme picker, `Left`/`Right` choose a theme and `Up`/`Down` preview that theme's other wallpapers; `Enter` still applies the theme.

You can find even more themes on [the extra themes page](https://omarchy.org/themes/) or even [make your own theme](43-making-your-own-theme.md).

Expand Down
2 changes: 1 addition & 1 deletion manual/07-hotkeys.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ All capture options are also accessible under _Trigger > Capture_ in the Omarchy

| Hotkey | Function |
| ----------------------- | --------------------- |
| `Super + Ctrl + Shift + Space` | Pick a new theme |
| `Super + Ctrl + Shift + Space` | Pick a new theme (`Up`/`Down` preview that theme's wallpapers) |
| `Super + Ctrl + Space` | Pick theme background |
| `Super + Backspace` | Toggle transparency on a window |
| `Super + Ctrl + Backspace` | Toggle single-window square aspect |
Expand Down
188 changes: 185 additions & 3 deletions shell/plugins/image-picker/ImagePicker.qml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Item {
var path = currentPath()
if (!path) return filterText ? "No matches" : ""

return labelForPath(path)
return ImagePickerModel.themeWallpaperLabel(path, displayByIndex[selectedIndex] || "", filterText)
}

function itemMatches(index) {
Expand All @@ -110,6 +110,8 @@ Item {
if (index === selectedIndex && immediate !== true) return

selectedIndex = index
if (inThemeSwitcher())
ensureThemeWallpapers(themeSlugForIndex(index))
}

function selectAdjacent(direction) {
Expand All @@ -126,6 +128,133 @@ Item {
}
}

function inThemeSwitcher() {
if (showLabels && filterable) return true
var path = currentPath()
return String(path).indexOf("/theme-selector/previews/") !== -1
}

function resetWallpaperState() {
wallpaperLists = ({})
wallpaperOffsets = ({})
wallpaperPending = ({})
displayByIndex = []
wallpaperSerial += 1
themeBgsProc.queue = []
themeBgsProc.activeSlug = ""
themeBgsProc.activeSerial = 0
}

function themeSlugForIndex(index) {
if (index < 0 || index >= imageArray.length || !imageArray[index]) return ""
return nameForPath(imageArray[index].filePath)
}

function itemForSlug(slug) {
for (var i = 0; i < imageArray.length; i++) {
if (themeSlugForIndex(i) === slug) return imageArray[i]
}
return null
}

function previewIndexInList(list, item) {
if (!item || !list || !list.length) return -1
var filePath = item.filePath
var thumbnailPath = item.thumbnailPath
var fileName = item.fileName
for (var i = 0; i < list.length; i++) {
if (list[i] === filePath || list[i] === thumbnailPath) return i
if (fileName && list[i].split("/").pop() === fileName) return i
}
return -1
}

function computeDisplayPath(index) {
if (index < 0 || index >= imageArray.length || !imageArray[index]) return ""
var item = imageArray[index]
var slug = nameForPath(item.filePath)
var list = wallpaperLists[slug]
if (!list || !list.length) return item.thumbnailPath
var idx = wallpaperOffsets[slug] || 0
if (idx < 0 || idx >= list.length) return item.thumbnailPath
return list[idx]
}

function refreshDisplays() {
var next = []
for (var i = 0; i < imageArray.length; i++)
next.push(computeDisplayPath(i))
displayByIndex = next
}

function ensureThemeWallpapers(slug) {
if (!inThemeSwitcher() || !slug) return
if (Object.prototype.hasOwnProperty.call(wallpaperLists, slug)) return
if (themeBgsProc.activeSlug === slug || themeBgsProc.queue.indexOf(slug) >= 0) return
themeBgsProc.queue = themeBgsProc.queue.concat([slug])
startThemeBgsQueue()
}

function startThemeBgsQueue() {
if (themeBgsProc.running || themeBgsProc.queue.length === 0) return
var slug = themeBgsProc.queue[0]
themeBgsProc.queue = themeBgsProc.queue.slice(1)
themeBgsProc.activeSlug = slug
themeBgsProc.activeSerial = wallpaperSerial
themeBgsProc.command = ["bash", root.scriptPath("list-theme-bgs.sh"), slug]
themeBgsProc.running = true
}

function takeThemeWallpapers(slug, lines) {
var item = itemForSlug(slug)
var list = lines.slice()
var match = previewIndexInList(list, item)
if (match < 0 && item && item.thumbnailPath) {
list.unshift(item.thumbnailPath)
match = 0
}
if (match < 0) match = 0
var lists = Object.assign({}, wallpaperLists)
var offsets = Object.assign({}, wallpaperOffsets)
lists[slug] = list
if (offsets[slug] === undefined) offsets[slug] = match
wallpaperLists = lists
wallpaperOffsets = offsets
var pending = wallpaperPending[slug] || 0
if (pending) {
var pendingMap = Object.assign({}, wallpaperPending)
pendingMap[slug] = 0
wallpaperPending = pendingMap
applyWallpaperCycle(slug, pending)
return
}
refreshDisplays()
}

function applyWallpaperCycle(slug, direction) {
var list = wallpaperLists[slug]
if (!list || list.length < 2) return
var idx = wallpaperOffsets[slug] || 0
var offsets = Object.assign({}, wallpaperOffsets)
offsets[slug] = (idx + direction % list.length + list.length) % list.length
wallpaperOffsets = offsets
refreshDisplays()
}

function cycleThemeWallpaper(direction) {
if (!inThemeSwitcher()) return
var slug = themeSlugForIndex(selectedIndex)
if (!slug) return
if (Object.prototype.hasOwnProperty.call(wallpaperLists, slug)) {
applyWallpaperCycle(slug, direction)
return
}
var pending = Object.assign({}, wallpaperPending)
pending[slug] = (pending[slug] || 0) + direction
wallpaperPending = pending
ensureThemeWallpapers(slug)
}

function updateFilter(nextFilterText) {
filterText = nextFilterText

Expand Down Expand Up @@ -200,6 +329,9 @@ Item {
root.selectedIndex = root.indexForSelectedImage(newImages)
root.imageArray = newImages
root.imagesLoaded = true
root.refreshDisplays()
if (root.inThemeSwitcher())
root.ensureThemeWallpapers(root.themeSlugForIndex(root.selectedIndex))

if (reveal !== false) {
root.opened = true
Expand All @@ -223,6 +355,7 @@ Item {
filterable = nextFilterable === true || nextFilterable === "true"
filterText = ""
layoutSettled = false
resetWallpaperState()

if (imageRows && imageRows === loadedImageRows && imageArray.length > 0) {
root.select(root.selectedImageIndex(), true)
Expand Down Expand Up @@ -254,6 +387,11 @@ Item {
}

property var imageArray: []
property var wallpaperLists: ({})
property var wallpaperOffsets: ({})
property var wallpaperPending: ({})
property var displayByIndex: []
property int wallpaperSerial: 0

function startImageScan(serial, dirs) {
if (loadImagesProc.running) {
Expand Down Expand Up @@ -300,6 +438,26 @@ Item {
}
}

Process {
id: themeBgsProc
property string activeSlug: ""
property int activeSerial: 0
property var queue: []
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
if (themeBgsProc.activeSerial !== root.wallpaperSerial) return
var lines = String(text || "").split("\n").filter(function(line) { return line.length > 0 })
root.takeThemeWallpapers(themeBgsProc.activeSlug, lines)
}
}
onExited: {
activeSlug = ""
activeSerial = 0
root.startThemeBgsQueue()
}
}

// Lifecycle hooks invoked by omarchy-shell summon/hide. shell.summon(id,
// payloadJson) hands the JSON to open() here; shell.hide(id) calls close().
// The shell host owns the stable `image-selector` IPC target and forwards
Expand Down Expand Up @@ -426,12 +584,28 @@ Item {
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_Tab) {
root.selectAdjacent(1)
event.accepted = true
} else if (event.key === Qt.Key_Up) {
root.cycleThemeWallpaper(-1)
event.accepted = true
} else if (event.key === Qt.Key_Down) {
root.cycleThemeWallpaper(1)
event.accepted = true
} else if (root.filterable && event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127 && (event.modifiers === Qt.NoModifier || event.modifiers === Qt.ShiftModifier)) {
root.updateFilter(root.filterText + event.text)
event.accepted = true
}
}

WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: function(event) {
if (!root.inThemeSwitcher()) return
if (event.angleDelta.y > 0) root.cycleThemeWallpaper(-1)
else if (event.angleDelta.y < 0) root.cycleThemeWallpaper(1)
event.accepted = true
}
}

Component.onCompleted: forceActiveFocus()

Repeater {
Expand All @@ -445,6 +619,7 @@ Item {
readonly property string filePath: imageData ? imageData.filePath : ""
readonly property string fileName: imageData ? imageData.fileName : ""
readonly property string thumbnailPath: imageData ? imageData.thumbnailPath : ""
readonly property string displayPath: (root.displayByIndex[index] || thumbnailPath)

readonly property bool matched: root.itemMatches(index)
readonly property int relativeIndex: root.filteredPosition(index) - root.selectedFilteredPosition()
Expand Down Expand Up @@ -505,11 +680,13 @@ Item {
// Load only the initial/visited nearby images, but keep the
// source once activated so Qt does not tear textures down as
// selection moves through the carousel.
source: item.sourceActivated && item.thumbnailPath ? Util.fileUrl(item.thumbnailPath) : ""
source: item.sourceActivated && item.displayPath ? Util.fileUrl(item.displayPath) : ""
fillMode: Image.PreserveAspectCrop
asynchronous: false
cache: true
smooth: true
sourceSize.width: item.selected ? root.expandedWidth : root.sliceWidth
sourceSize.height: item.selected ? root.expandedHeight : root.sliceHeight
}

Rectangle {
Expand Down Expand Up @@ -551,7 +728,12 @@ Item {
anchors.topMargin: Style.space(16)
anchors.horizontalCenter: carousel.horizontalCenter
width: root.expandedWidth
text: root.currentLabel()
text: {
root.displayByIndex
root.selectedIndex
root.filterText
return root.currentLabel()
}
color: root.foreground
style: Text.Outline
styleColor: Util.alpha(root.dimColor, 0.7)
Expand Down
17 changes: 17 additions & 0 deletions shell/plugins/image-picker/ImagePickerModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ function labelForPath(path) {
return nameForPath(path).replace(/[-_]+/g, " ").replace(/\b\w/g, function(match) { return match.toUpperCase() })
}

function wallpaperLabelForPath(path) {
return labelForPath(path).replace(/^\d+\s+/, "")
}

function themeWallpaperLabel(themePath, displayPath, filterText) {
if (!themePath) return filterText ? "No matches" : ""

var themeLabel = labelForPath(themePath)
if (!displayPath) return themeLabel

var wallpaperLabel = wallpaperLabelForPath(displayPath)
if (!wallpaperLabel || wallpaperLabel === themeLabel) return themeLabel
return themeLabel + " · " + wallpaperLabel
}

function loadRows(rows) {
var images = []
var seen = {}
Expand Down Expand Up @@ -86,6 +101,8 @@ if (typeof module !== "undefined") {
module.exports = {
nameForPath: nameForPath,
labelForPath: labelForPath,
wallpaperLabelForPath: wallpaperLabelForPath,
themeWallpaperLabel: themeWallpaperLabel,
loadRows: loadRows,
itemMatches: itemMatches,
firstMatchingIndex: firstMatchingIndex,
Expand Down
36 changes: 36 additions & 0 deletions shell/plugins/image-picker/list-theme-bgs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/bash

theme_name="${1:-}"
if [[ -z $theme_name ]]; then
exit 0
fi

declare -A seen=()

add_dir() {
local dir="$1"
local image real

if [[ ! -d $dir ]]; then
return 0
fi

while IFS= read -r -d '' image; do
real=$(readlink -f "$image" 2>/dev/null || printf '%s' "$image")
if [[ -z $real ]]; then
continue
fi
if [[ -n ${seen[$real]+x} ]]; then
continue
fi

seen[$real]=1
printf '%s\n' "$image"
done < <(find -L "$dir" -maxdepth 1 -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.bmp' -o -iname '*.webp' \) \
-print0 2>/dev/null | sort -z)
}

add_dir "$HOME/.config/omarchy/backgrounds/$theme_name"
add_dir "$HOME/.config/omarchy/themes/$theme_name/backgrounds"
add_dir "$OMARCHY_PATH/themes/$theme_name/backgrounds"
Loading