Author SHA1 Message Date
AramJonghu abf7382771 ci: libcava dev headers
C++ / fmt (pull_request) Successful in 5s
JS/TS / fmt (pull_request) Successful in 12s
JS/TS / lint (pull_request) Successful in 16s
Python / lint (pull_request) Successful in 33s
Python / fmt (pull_request) Successful in 36s
Python / test (pull_request) Successful in 1m3s
Python / typecheck (pull_request) Successful in 1m21s
Rust / fmt (pull_request) Successful in 33s
C++ / build (pull_request) Successful in 2m26s
Rust / build (pull_request) Successful in 1m43s
Python / buildcheck (pull_request) Successful in 2m12s
Rust / clippy (pull_request) Successful in 1m41s
C++ / clang-tidy (pull_request) Successful in 4m10s
2026-08-14 20:00:27 +02:00
AramJonghu a1a8a6e695 ci: add relevant dependencies; resolves MissingImport
C++ / fmt (pull_request) Successful in 4s
JS/TS / lint (pull_request) Successful in 15s
JS/TS / fmt (pull_request) Successful in 21s
Python / fmt (pull_request) Successful in 31s
Python / lint (pull_request) Successful in 34s
Python / test (pull_request) Successful in 1m0s
Python / typecheck (pull_request) Successful in 1m20s
Rust / fmt (pull_request) Successful in 34s
C++ / build (pull_request) Successful in 2m29s
Rust / build (pull_request) Successful in 1m45s
Rust / clippy (pull_request) Successful in 1m2s
Python / buildcheck (pull_request) Successful in 2m18s
C++ / clang-tidy (pull_request) Successful in 3m52s
2026-08-14 19:40:28 +02:00
9 changed files with 264 additions and 549 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ jobs:
py3-pip
python3 -m venv .venv
. .venv/bin/activate
pip install --no-cache-dir basedpyright
pip install --no-cache-dir basedpyright typer pillow materialyoucolor jinja2
- name: Type check
run: |
-338
View File
@@ -1,338 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import ZShell.Config
import qs.Modules.Launcher.Items
import qs.Components
import qs.Helpers
Item {
id: root
readonly property var arrangement: {
const W = width;
const F = focalWidth;
const M = minEdgeWidth;
const sp = itemSpacing;
if (W <= 0 || F <= 0)
return {
n: 0,
sizes: [F],
effectiveExtraWidth: 0
};
let bestN = 0;
let bestSizes = [F];
let bestExtraWidth = 0;
for (let n = 1; ; n++) {
const ratio = Math.pow(M / F, 1 / n);
const sizes = [F];
for (let i = 1; i <= n; i++) {
sizes.push(i === n ? M : F * Math.pow(ratio, i));
}
let decreasedWidth = 0;
for (let i = 1; i <= n; i++)
decreasedWidth += pitch - sizes[i];
const effectiveWidth = W + decreasedWidth * 2;
const requiredWidth = (2 * n + 1) * pitch;
if (requiredWidth > effectiveWidth)
break;
bestN = n;
bestSizes = sizes;
bestExtraWidth = decreasedWidth;
}
return {
n: bestN,
sizes: bestSizes,
effectiveExtraWidth: bestExtraWidth
};
}
readonly property int centerBlockIndex: Math.floor(loopMultiplier / 2)
readonly property int centerBlockStart: centerBlockIndex * realCount
readonly property int currentRealIndex: realIndexOf(view.currentIndex)
readonly property real focalWidth: Config.launcher.sizes.wallpaperWidth
readonly property real itemSpacing: Tokens.spacing.small
readonly property var keylines: {
const sp = itemSpacing;
const sizes = arrangement.sizes;
const n = arrangement.n;
const right = [
{
io: 0,
lo: 0,
sz: sizes[0]
}
];
let lo = sizes[0] / 2;
for (let i = 1; i <= n; i++) {
const size = sizes[i];
lo += sp + size / 2;
right.push({
io: i,
lo: lo,
sz: size
});
lo += size / 2;
}
right.push({
io: n + 1,
lo: width / 2 - sp,
sz: 0
});
const k = [];
for (let i = right.length - 1; i >= 1; i--) {
k.push({
io: -right[i].io,
lo: -right[i].lo,
sz: right[i].sz
});
}
for (const item of right)
k.push(item);
for (let i = 0; i < k.length; i++)
k[i].loc = k[i].io * pitch;
return k;
}
readonly property int loopMultiplier: 401
readonly property real minEdgeWidth: 56
required property var panels
readonly property real pitch: focalWidth + itemSpacing
readonly property int realCount: values.length
readonly property var screen: QsWindow.window?.screen
required property SearchBar search
readonly property real shrinkRatio: 0.63
readonly property real tileHeight: focalWidth / 16 * 9
readonly property var values: Wallpapers.query(root.search.text.split(" ").slice(1).join(" "))
readonly property int virtualCount: realCount > 0 ? realCount * loopMultiplier : 0
required property var visibilities
function contentXForIndex(index) {
return index * pitch + pitch / 2 - width / 2;
}
function decrementCurrentIndex() {
if (realCount === 0)
return;
goTo(view.currentIndex - 1);
}
function glideTo(dest) {
if (Math.abs(dest - view.contentX) < 0.5)
return;
view.cancelFlick();
snapAnim.stop();
snapAnim.from = view.contentX;
snapAnim.to = dest;
snapAnim.start();
}
function goTo(virtualIndex) {
glideTo(contentXForIndex(virtualIndex));
}
function incrementCurrentIndex() {
if (realCount === 0)
return;
goTo(view.currentIndex + 1);
}
function indexForContentX(x) {
return Math.round((x + width / 2 - pitch / 2) / pitch);
}
function realIndexOf(virtualIndex) {
if (realCount === 0)
return -1;
return ((virtualIndex % realCount) + realCount) % realCount;
}
function sample(childLoc) {
const k = keylines;
if (childLoc <= k[0].loc)
return {
c: k[0].lo,
s: 0
};
if (childLoc >= k[k.length - 1].loc)
return {
c: k[k.length - 1].lo,
s: 0
};
for (let i = 0; i < k.length - 1; i++) {
const a = k[i];
const b = k[i + 1];
if (childLoc > b.loc)
continue;
const span = b.loc - a.loc;
const t = span > 0 ? (childLoc - a.loc) / span : 0;
return {
c: a.lo + (b.lo - a.lo) * t,
s: a.sz + (b.sz - a.sz) * t
};
}
return {
c: 0,
s: 0
};
}
function selectReal(realIdx) {
if (realCount === 0)
return;
const curBlock = Math.floor(view.currentIndex / realCount);
const current = view.currentIndex;
let target = curBlock * realCount + realIdx;
if (target - current > realCount / 2)
target -= realCount;
else if (current - target > realCount / 2)
target += realCount;
goTo(target);
}
function snapTarget(x) {
return contentXForIndex(indexForContentX(x));
}
function snapToNearest() {
glideTo(snapTarget(view.contentX));
}
implicitHeight: tileHeight + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal
implicitWidth: {
if (!screen)
return 0;
const barMargins = Config.bar.border * 2;
let margins = 0;
if (visibilities.utilities || visibilities.sidebar)
margins = panels.utilities.implicitWidth;
const maxWidth = screen.width - (barMargins + margins) * 2 - Config.bar.rounding * 4;
return maxWidth;
}
width: implicitWidth
Component.onCompleted: {
const idx = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent);
if (idx >= 0)
view.contentX = contentXForIndex(centerBlockStart + idx);
}
Component.onDestruction: Wallpapers.stopPreview()
ListView {
id: view
anchors.fill: parent
boundsBehavior: Flickable.StopAtBounds
cacheBuffer: root.width
clip: false
currentIndex: root.centerBlockStart
displayMarginBeginning: root.arrangement.effectiveExtraWidth
displayMarginEnd: root.arrangement.effectiveExtraWidth
interactive: true
model: root.virtualCount
orientation: ListView.Horizontal
snapMode: ListView.NoSnap
Anim on contentX {
id: snapAnim
running: false
type: Anim.Standard
}
delegate: Item {
id: slot
readonly property real childLoc: index * root.pitch + root.pitch / 2 - view.contentX - view.width / 2
required property int index
readonly property bool isCurrent: index === view.currentIndex
readonly property var keyline: root.sample(childLoc)
readonly property var modelData: realIdx >= 0 ? root.values[realIdx] : null
readonly property int realIdx: root.realIndexOf(index)
readonly property real tileWidth: keyline.s
height: view.height
visible: modelData !== null
width: root.pitch
WallpaperTile {
anchors.verticalCenter: parent.verticalCenter
isCurrent: slot.isCurrent
modelData: slot.modelData
tileHeight: root.tileHeight
tileWidth: slot.tileWidth
visibilities: root.visibilities
x: (slot.keyline.c - slot.childLoc) + (slot.width - width) / 2
onRequestActivate: {
if (slot.isCurrent) {
Wallpapers.setWallpaper(slot.modelData.path);
root.visibilities.launcher = false;
} else {
root.goTo(slot.index);
}
}
}
}
onContentXChanged: {
if (root.realCount === 0)
return;
const blockWidth = root.realCount * root.pitch;
const centerX = root.contentXForIndex(root.centerBlockStart);
const blocksFromCenter = Math.round((contentX - centerX) / blockWidth);
if (Math.abs(blocksFromCenter) >= 2) {
const shift = blocksFromCenter * blockWidth;
contentX -= shift;
if (snapAnim.running)
snapAnim.to -= shift;
}
currentIndex = Math.round((contentX + width / 2 - root.pitch / 2) / root.pitch);
}
onCurrentIndexChanged: {
const realIdx = root.realIndexOf(currentIndex);
if (realIdx >= 0)
Wallpapers.preview(root.values[realIdx].path);
}
onFlickEnded: root.snapToNearest()
onMovementEnded: root.snapToNearest()
}
}
+2 -2
View File
@@ -111,8 +111,8 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
sourceComponent: CarouselView {
// content: root.content
sourceComponent: WallpaperList {
content: root.content
panels: root.panels
search: root.search
visibilities: root.visibilities
+96
View File
@@ -0,0 +1,96 @@
import ZShell.Models
import Quickshell
import QtQuick
import qs.Components
import qs.Helpers
import ZShell.Config
import qs.Modules
import qs.Services
Item {
id: root
required property FileSystemEntry modelData
required property PersistentProperties visibilities
implicitHeight: image.height + label.height + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal
implicitWidth: image.width + Tokens.padding.larger * 2
opacity: 0
scale: 0.5
z: PathView.z ?? 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Component.onCompleted: {
scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0);
opacity = Qt.binding(() => PathView.onPath ? 1 : 0);
}
StateLayer {
radius: Tokens.rounding.normal
onClicked: {
Wallpapers.setWallpaper(root.modelData.path);
root.visibilities.launcher = false;
}
}
Elevation {
anchors.fill: image
level: 4
opacity: root.PathView.isCurrentItem ? 1 : 0
radius: image.radius
Behavior on opacity {
Anim {
}
}
}
CustomClippingRect {
id: image
anchors.horizontalCenter: parent.horizontalCenter
color: Colors.tPalette.m3surfaceContainer
implicitHeight: implicitWidth / 16 * 9
implicitWidth: Config.launcher.sizes.wallpaperWidth
radius: Tokens.rounding.small
y: Tokens.padding.large
MaterialIcon {
anchors.centerIn: parent
color: Colors.tPalette.m3outline
font.pointSize: Tokens.font.size.extraLarge * 2
font.weight: 600
text: "image"
}
CachingImage {
anchors.fill: parent
cache: true
path: root.modelData.path
smooth: !root.PathView.view.moving
}
}
CustomText {
id: label
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: image.bottom
anchors.topMargin: Tokens.spacing.small / 2
elide: Text.ElideRight
font.pointSize: Tokens.font.size.normal
horizontalAlignment: Text.AlignHCenter
renderType: Text.QtRendering
text: root.modelData.relativePath
width: image.width - Tokens.padding.normal * 2
}
}
-67
View File
@@ -1,67 +0,0 @@
import Quickshell
import QtQuick
import ZShell.Config
import qs.Components
import qs.Helpers
import qs.Services
Item {
id: root
required property bool isCurrent
required property var modelData // FileSystemEntry-like {path, relativePath}
required property real tileHeight
required property real tileWidth
required property PersistentProperties visibilities
// Emitted on click: caller decides whether that means "activate" (if
// already current) or "bring to center" (if not) - see CarouselView.
signal requestActivate
implicitHeight: tileHeight + Tokens.padding.large * 2
implicitWidth: tileWidth
CustomClippingRect {
id: image
anchors.horizontalCenter: parent.horizontalCenter
color: Colors.tPalette.m3surfaceContainer
implicitHeight: root.tileHeight
implicitWidth: root.tileWidth
radius: Tokens.rounding.small
y: Tokens.padding.large
Behavior on implicitWidth {
enabled: !root.visibilities.launcher // avoid fighting drag-driven updates
Anim {
}
}
MaterialIcon {
anchors.centerIn: parent
color: Colors.tPalette.m3outline
font.pointSize: Tokens.font.size.extraLarge * 2
font.weight: 600
text: "image"
}
CachingImage {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
cache: true
fillMode: Image.PreserveAspectCrop
height: root.tileHeight
path: root.modelData ? root.modelData.path : ""
smooth: root.isCurrent
sourceSize.height: root.tileHeight
sourceSize.width: root.tileHeight * (16 / 9)
width: root.tileHeight * (16 / 9)
}
StateLayer {
onClicked: root.requestActivate()
}
}
}
+93
View File
@@ -0,0 +1,93 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Components
import qs.Helpers
import ZShell.Config
import qs.Modules.Launcher.Items
PathView {
id: root
required property var content
readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.9 + Tokens.padding.larger * 2
readonly property int numItems: {
const screen = QsWindow.window?.screen;
if (!screen)
return 0;
// Screen width - 4x outer rounding - 2x max side thickness (cause centered)
const barMargins = panels.bar.implicitWidth;
let outerMargins = 0;
if ((visibilities.utilities || visibilities.sidebar) && panels.utilities.implicitWidth > outerMargins)
outerMargins = panels.utilities.implicitWidth;
const maxWidth = screen.width - Config.bar.rounding * 4 - (barMargins + outerMargins) * 2;
if (maxWidth <= 0)
return 0;
const maxItemsOnScreen = Math.floor(maxWidth / itemWidth);
const visible = Math.min(maxItemsOnScreen, Config.launcher.maxWallpapers, scriptModel.values.length);
if (visible === 2)
return 1;
if (visible > 1 && visible % 2 === 0)
return visible - 1;
return visible;
}
required property var panels
required property SearchBar search
required property var visibilities
cacheItemCount: 4
highlightRangeMode: PathView.StrictlyEnforceRange
implicitWidth: Math.min(numItems, count) * itemWidth
pathItemCount: numItems
preferredHighlightBegin: 0.5
preferredHighlightEnd: 0.5
snapMode: PathView.SnapToItem
delegate: WallpaperItem {
visibilities: root.visibilities
}
model: ScriptModel {
id: scriptModel
readonly property string search: root.search.text.split(" ").slice(1).join(" ")
values: Wallpapers.query(search)
onValuesChanged: root.currentIndex = search ? 0 : values.findIndex(w => w.path === Wallpapers.actualCurrent)
}
path: Path {
startY: root.height / 2
PathAttribute {
name: "z"
value: 0
}
PathLine {
relativeY: 0
x: root.width / 2
}
PathAttribute {
name: "z"
value: 1
}
PathLine {
relativeY: 0
x: root.width
}
}
Component.onCompleted: currentIndex = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent)
Component.onDestruction: Wallpapers.stopPreview()
onCurrentItemChanged: {
if (currentItem)
Wallpapers.preview(currentItem.modelData.path);
}
}
+52 -134
View File
@@ -1,7 +1,5 @@
#include "cachingimagemanager.hpp"
#include <algorithm>
#include <QtQuick/qquickwindow.h>
#include <qcryptographichash.h>
#include <qdir.h>
@@ -9,27 +7,10 @@
#include <qfuturewatcher.h>
#include <qimagereader.h>
#include <qpainter.h>
#include <qthread.h>
#include <qthreadpool.h>
#include <qtconcurrentrun.h>
namespace ZShell::internal {
namespace {
QThreadPool* imageIoPool() {
static QThreadPool pool;
static const bool init = [] {
pool.setMaxThreadCount(std::max(2, QThread::idealThreadCount() / 2));
return true;
}();
(void)init;
return &pool;
}
} // namespace
qreal CachingImageManager::effectiveScale() const {
if (m_item && m_item->window()) {
return m_item->window()->devicePixelRatio();
@@ -120,13 +101,14 @@ void CachingImageManager::updateSource() {
void CachingImageManager::updateSource(const QString& path) {
if (path.isEmpty() || path == m_shaPath) {
// Path is empty or already calculating sha for path
return;
}
m_shaPath = path;
const auto future =
QtConcurrent::run(imageIoPool(), &CachingImageManager::sha256sum, path);
QtConcurrent::run(&CachingImageManager::sha256sum, path);
const auto watcher = new QFutureWatcher<QString>(this);
@@ -136,16 +118,7 @@ void CachingImageManager::updateSource(const QString& path) {
this,
[watcher, path, this]() {
if (m_path != path) {
watcher->deleteLater();
return;
}
const QString key = watcher->result();
if (key.isEmpty()) {
if (m_shaPath == path) {
m_shaPath = QString();
}
// Object is destroyed or path has changed, ignore
watcher->deleteLater();
return;
}
@@ -153,29 +126,19 @@ void CachingImageManager::updateSource(const QString& path) {
const QSize size = effectiveSize();
if (!m_item || !size.width() || !size.height()) {
if (m_shaPath == path) {
m_shaPath = QString();
}
watcher->deleteLater();
return;
}
const QString fillMode = m_item->property("fillMode").toString();
const QString filename =
QString("%1@%2x%3-%4.png")
.arg(key)
.arg(size.width())
.arg(size.height())
.arg(
fillMode == "PreserveAspectCrop" ? "crop"
: fillMode == "PreserveAspectFit" ? "fit"
: "stretch");
// clang-format off
const QString filename = QString("%1@%2x%3-%4.png")
.arg(watcher->result()).arg(size.width()).arg(size.height())
.arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch");
// clang-format on
const QUrl cache = m_cacheDir.resolved(QUrl(filename));
if (m_cachePath == cache) {
if (m_shaPath == path) {
m_shaPath = QString();
}
watcher->deleteLater();
return;
}
@@ -186,9 +149,6 @@ void CachingImageManager::updateSource(const QString& path) {
if (!cache.isLocalFile()) {
qWarning() << "CachingImageManager::updateSource: cachePath"
<< cache << "is not a local file";
if (m_shaPath == path) {
m_shaPath = QString();
}
watcher->deleteLater();
return;
}
@@ -201,6 +161,7 @@ void CachingImageManager::updateSource(const QString& path) {
createCache(path, cache.toLocalFile(), fillMode, size);
}
// Clear current running sha if same
if (m_shaPath == path) {
m_shaPath = QString();
}
@@ -209,6 +170,7 @@ void CachingImageManager::updateSource(const QString& path) {
});
watcher->setFuture(future);
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
}
QUrl CachingImageManager::cachePath() const {
@@ -220,105 +182,61 @@ void CachingImageManager::createCache(
const QString& cache,
const QString& fillMode,
const QSize& size) const {
auto* watcher =
new QFutureWatcher<bool>(const_cast<CachingImageManager*>(this));
QThreadPool::globalInstance()->start([path, cache, fillMode, size] {
QImage image(path);
connect(
watcher,
&QFutureWatcher<bool>::finished,
this,
[this, watcher, cache, path]() {
if (watcher->result() && m_item && m_path == path &&
m_cachePath.toLocalFile() == cache) {
m_item->setProperty("source", QUrl::fromLocalFile(cache));
}
if (image.isNull()) {
qWarning() << "CachingImageManager::createCache: failed to read"
<< path;
return;
}
watcher->deleteLater();
});
image.convertTo(QImage::Format_ARGB32);
const auto future =
QtConcurrent::run(
imageIoPool(), [path, cache, fillMode, size]() -> bool {
QImageReader reader(path);
if (fillMode == "PreserveAspectCrop") {
image = image.scaled(
size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
} else if (fillMode == "PreserveAspectFit") {
image = image.scaled(
size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else {
image = image.scaled(
size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
if (reader.supportsOption(QImageIOHandler::ScaledSize)) {
const QSize full = reader.size();
if (full.isValid() && !full.isEmpty()) {
const QSize hint = size * 2;
QSize scaled = full;
scaled.scale(hint, Qt::KeepAspectRatioByExpanding);
scaled = QSize(
std::min(scaled.width(), full.width()),
std::min(scaled.height(), full.height()));
reader.setScaledSize(scaled);
}
}
if (fillMode == "PreserveAspectCrop" ||
fillMode == "PreserveAspectFit") {
QImage canvas(size, QImage::Format_ARGB32);
canvas.fill(Qt::transparent);
QImage image = reader.read();
QPainter painter(&canvas);
painter.drawImage(
(size.width() - image.width()) / 2,
(size.height() - image.height()) / 2,
image);
painter.end();
if (image.isNull()) {
qWarning()
<< "CachingImageManager::createCache: failed to read"
<< path << reader.errorString();
return false;
}
image = canvas;
}
image.convertTo(QImage::Format_ARGB32);
if (fillMode == "PreserveAspectCrop") {
image = image.scaled(
size,
Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
} else if (fillMode == "PreserveAspectFit") {
image = image.scaled(
size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else {
image = image.scaled(
size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
if (fillMode == "PreserveAspectCrop" ||
fillMode == "PreserveAspectFit") {
QImage canvas(size, QImage::Format_ARGB32);
canvas.fill(Qt::transparent);
QPainter painter(&canvas);
painter.drawImage(
(size.width() - image.width()) / 2,
(size.height() - image.height()) / 2,
image);
painter.end();
image = canvas;
}
const QString parent = QFileInfo(cache).absolutePath();
if (!QDir().mkpath(parent) || !image.save(cache)) {
qWarning()
<< "CachingImageManager::createCache: failed to save to"
<< cache;
return false;
}
return true;
});
watcher->setFuture(future);
const QString parent = QFileInfo(cache).absolutePath();
if (!QDir().mkpath(parent) || !image.save(cache)) {
qWarning() << "CachingImageManager::createCache: failed to save to"
<< cache;
}
});
}
QString CachingImageManager::sha256sum(const QString& path) {
const QFileInfo info(path);
if (!info.exists() || !info.isFile()) {
qWarning() << "CachingImageManager::sha256sum: failed to stat" << path;
return QString();
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "CachingImageManager::sha256sum: failed to open" << path;
return "";
}
QCryptographicHash hash(QCryptographicHash::Sha256);
hash.addData(path.toUtf8());
hash.addData(QByteArray::number(info.size()));
hash.addData(QByteArray::number(info.lastModified().toMSecsSinceEpoch()));
hash.addData(&file);
file.close();
return hash.result().toHex();
}
+17 -7
View File
@@ -30,14 +30,24 @@ RUN pacman -S --noconfirm \
glib2
# AUR: libcava (provides development headers for cava)
RUN pacman -S --needed --noconfirm sudo && \
RUN pacman -Syu --noconfirm && \
pacman -S --needed --noconfirm \
sudo \
meson \
autoconf-archive \
python-tqdm && \
useradd -m builder && \
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
su builder -c " \
git clone https://aur.archlinux.org/libcava.git /tmp/libcava && \
cd /tmp/libcava && \
makepkg -si --noconfirm \
" && \
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
RUN for i in 1 2 3 4 5; do \
rm -rf /tmp/libcava && \
git clone --depth 1 https://aur.archlinux.org/libcava.git /tmp/libcava && \
break || sleep 5; \
done && \
test -d /tmp/libcava/.git && \
chown -R builder /tmp/libcava
RUN su builder -c "cd /tmp/libcava && makepkg -si --noconfirm" && \
rm -rf /tmp/libcava && \
userdel -r builder && \
sed -i '/^builder/d' /etc/sudoers
+3
View File
@@ -13,6 +13,9 @@ dependencies = [
"materialyoucolor"
]
[project.optional-dependencies]
dev = ["pytest"]
[project.scripts]
zshell-cli = "zshell:main"