state: 36/48 KDE packages build, 12 blocked — honest final state

The literal task 'build ALL KDE packages' cannot be 100% completed
because 12 packages require upstream dependencies not available on Redox:
- kirigami + plasma* (4): QML JIT disabled — no QQuickWindow/QQmlEngine
- kwin real build (1): Qt6::Sensors port needed
- breeze + kf6-kio + kf6-knewstuff + kde-cli-tools (4): source issues
- plasma extras (3): transitive blockers

What WAS completed:
- Cookbook topological sort fix (root cause — all deps now correct order)
- kf6-attica recipe (183 files, 2.4MB pkgar)
- 12 I2C/GPIO/UCSI daemons archived as durable patches
- Source archival system (make sources)
- Config + all docs synced, no contradictions
This commit is contained in:
2026-04-30 01:54:09 +01:00
parent 61f99940b5
commit 761e0d9de7
2011 changed files with 257073 additions and 1550 deletions
@@ -0,0 +1,3 @@
if(BUILD_QT6)
add_subdirectory(kcursorgen)
endif()
+105
View File
@@ -0,0 +1,105 @@
#!/bin/bash
set -euo pipefail
BIN_DIR="$( dirname "${BASH_SOURCE[0]}" )"
SRC_DIR="src"
RAWSVG_DIR="$SRC_DIR/svg"
INDEX="$SRC_DIR/index.theme"
ALIASES="$SRC_DIR/alias.list"
NOMINAL_SIZE=24
REAL_SIZE=32
FRAME_TIME=30
SCALES="50 75 100 125 150 175 200 225 250 275 300"
echo -ne "Checking Requirements...\\r"
if [[ ! -d "${RAWSVG_DIR}" ]]; then
echo -e "\\nFAIL: '${RAWSVG_DIR}' missing in /src"
exit 1
fi
if [[ ! -f "${INDEX}" ]]; then
echo -e "\\nFAIL: '${INDEX}' missing in /src"
exit 1
fi
if ! command -v inkscape > /dev/null ; then
echo -e "\\nFAIL: inkscape must be installed"
exit 1
fi
if ! command -v xcursorgen > /dev/null ; then
echo -e "\\nFAIL: xcursorgen must be installed"
exit 1
fi
echo -e "\033[0KChecking Requirements... DONE"
echo -ne "Making Folders... \\r"
for scale in $SCALES; do
mkdir -p "build/x$scale"
done
mkdir -p "build/config"
echo -e "\033[0KMaking Folders... DONE";
echo "Generating pixmaps..."
for RAWSVG in ${RAWSVG_DIR}/*.svg; do
BASENAME=${RAWSVG##*/}
BASENAME=${BASENAME%.*}
genPixmaps="file-open:${RAWSVG};"
echo -ne " $BASENAME...\\r"
for scale in $SCALES; do
DIR="build/x${scale}"
if [[ "${DIR}/${BASENAME}.png" -ot ${RAWSVG} ]]; then
genPixmaps="${genPixmaps} export-width:$((${REAL_SIZE}*scale/100)); export-height:$((${REAL_SIZE}*scale/100)); export-filename:${DIR}/${BASENAME}.png; export-do;"
fi
done
if [ "$genPixmaps" != "file-open:${RAWSVG};" ]; then
inkscape --shell < <(echo "${genPixmaps}") > /dev/null
fi
echo " $BASENAME... DONE"
done
echo "Generating pixmaps... DONE"
echo "Generating cursor theme..."
OUTPUT="$(grep --only-matching --perl-regex "(?<=Name\=).*$" $INDEX)"
OUTPUT=${OUTPUT// /_}
rm -rf "$OUTPUT"
mkdir -p "$OUTPUT/cursors"
mkdir -p "$OUTPUT/cursors_scalable"
$BIN_DIR/generate_cursors ${RAWSVG_DIR} "build" "$OUTPUT/cursors" "$OUTPUT/cursors_scalable" ${NOMINAL_SIZE} ${FRAME_TIME} ${SCALES}
echo "Generating cursor theme... DONE"
echo -ne "Generating shortcuts...\\r"
while read ALIAS ; do
FROM=${ALIAS% *}
TO=${ALIAS#* }
if [[ -e "$OUTPUT/cursors/$FROM" ]]; then
continue
fi
ln -s "$TO" "$OUTPUT/cursors/$FROM"
done < $ALIASES
while read ALIAS ; do
FROM=${ALIAS% *}
TO=${ALIAS#* }
if [[ -e "$OUTPUT/cursors_scalable/$FROM" ]]; then
continue
fi
ln -s "$TO" "$OUTPUT/cursors_scalable/$FROM"
done < $ALIASES
echo -e "\033[0KGenerating shortcuts... DONE"
echo -ne "Copying Theme Index...\\r"
if ! [[ -e "$OUTPUT/$INDEX" ]]; then
cp $INDEX "$OUTPUT/index.theme"
fi
echo -e "\033[0KCopying Theme Index... DONE"
echo "COMPLETE!"
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/python3
# Cut a cursor from a monolithic SVG file
import sys
import xml.etree.ElementTree as ET
import os.path
GROUP_TAG = '{http://www.w3.org/2000/svg}g'
LABEL_ATTR = '{http://www.inkscape.org/namespaces/inkscape}label'
def cut(src_path, dest_path, cursor_name):
tree = ET.parse(src_path)
root = tree.getroot()
new_tree = ET.ElementTree(ET.Element('svg'))
root = tree.getroot()
cursor_found = False
for child in root.findall(GROUP_TAG):
if child.attrib[LABEL_ATTR] != 'Cursors':
# help layers
root.remove(child)
continue
for row in child.findall(GROUP_TAG):
found_in_row = False;
for cursor in row.findall(GROUP_TAG):
if cursor.attrib[LABEL_ATTR] != cursor_name:
row.remove(cursor)
else:
found_in_row = True
hotspot = cursor.find(f"*[@{LABEL_ATTR}='hotspot']")
if hotspot is not None:
hotspot.set('id', 'hotspot')
else:
pass
# print(f'hotspot not found: {cursor_name}')
# sys.exit(2)
if not found_in_row:
child.remove(row)
else:
cursor_found = True
if cursor_found is None:
print('Cursor %s not found' % cursor_name)
sys.exit(2)
tree.write(dest_path)
def help():
script_name = os.path.basename(sys.argv[0])
print(f'''Usage:
{script_name} <src_path> <dest_path> <cursor_name>''')
if __name__ == '__main__':
if len(sys.argv) != 4:
help()
sys.exit(1)
src_path = sys.argv[1]
dest_path = sys.argv[2]
cursor_name = sys.argv[3]
cut(src_path, dest_path, cursor_name)
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# Compare generated pixmaps with the old
# Place the old pixmaps in Breeze/build.old, the new ones in Breeze/build, and run this script
# in Breeze/
for i in build/x*; do
echo $i
DIR=`basename $i`
mkdir -p diff/$DIR
for FILE in build/$DIR/*; do
BASENAME=`basename $FILE`
magick compare -metric mae {build.old,build,diff}/$DIR/$BASENAME
echo " $BASENAME"
done
echo
done
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
import sys
import math
import re
import subprocess
import shutil
import json
from pathlib import Path
from PySide6.QtSvg import QSvgRenderer
# Displace the hotspot to the right and down by 1/100th of a pixel, then
# floor. So if by some float error the hotspot is at 4.995, it will be
# displaced to 5.005, then floored to 5. This is to prevent the hotspot
# from potential off-by-one errors when the cursor is scaled.
HOTSPOT_DISPLACE = 1
if len(sys.argv) <= 7:
print("Usage: " + sys.argv[0] + " <svg cursor dir> <pixmap dir> <xcursor output dir> <svg cursor output dir> <base size> <animation frame time> <scales>")
sys.exit(1)
svg_dir = Path(sys.argv[1])
pixmap_dir = Path(sys.argv[2])
xcursor_output_dir = Path(sys.argv[3])
svg_cursor_output_dir = Path(sys.argv[4])
nominal_size = int(sys.argv[5])
delay = int(sys.argv[6])
scales = [int(i) for i in sys.argv[7:]]
svg_files = list(svg_dir.glob("*.svg"))
svg_files.sort()
processed_svgs = set()
for svg_file in svg_files:
if svg_file in processed_svgs:
continue
processed_svgs.add(svg_file)
basename = svg_file.stem
basename = re.sub(r"-[0-9_]*$", "", basename)
print(f" {basename}")
# Animated?
frames = list(svg_dir.glob(basename + "-[0-9]*.svg"))
frames.sort()
processed_svgs.update(frames)
svg = QSvgRenderer(str(svg_file))
hotspot = svg.transformForElement("hotspot").map(svg.boundsOnElement("hotspot")).boundingRect().topLeft()
# Generate xcursor
xcursor_config_path = pixmap_dir / "config" / f"{basename}.config"
with open(xcursor_config_path, "w") as config:
for scale in scales:
size = math.floor(nominal_size * scale / 100)
hotspot_x = math.floor((hotspot.x() * scale + HOTSPOT_DISPLACE) / 100)
hotspot_y = math.floor((hotspot.y() * scale + HOTSPOT_DISPLACE) / 100)
if len(frames) == 0:
print(f"{size} {hotspot_x} {hotspot_y} x{scale}/{basename}.png", file=config)
else:
for i in frames:
t = i.stem
print(f"{size} {hotspot_x} {hotspot_y} x{scale}/{t}.png {delay}", file=config)
subprocess.run(["xcursorgen", "-p", pixmap_dir, xcursor_config_path, xcursor_output_dir / basename], check=True)
# Generate SVG cursor
output_dir = svg_cursor_output_dir / basename
output_dir.mkdir(parents=True, exist_ok=True)
hotspot_x = hotspot.x()
hotspot_y = hotspot.y()
with open(output_dir / "metadata.json", "w") as metadata:
if len(frames) == 0:
filename = svg_file.name
shutil.copyfile(svg_file, output_dir / filename)
json.dump([{
"filename": filename,
"hotspot_x": hotspot_x,
"hotspot_y": hotspot_y
}],
metadata)
else:
l = []
for i in frames:
filename = i.name
shutil.copyfile(i, output_dir / filename)
l.append({
"filename": filename,
"hotspot_x": hotspot_x,
"hotspot_y": hotspot_y,
"delay": delay
})
json.dump(l, metadata)
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/python3
# Print the hotspot of a cursor SVG file at default size
# Usage: ./hotspot_test.py <svg file>
from PySide6.QtSvg import QSvgRenderer
import sys
import math
# Displace the hotspot to the right and down by 1/100th of a pixel, then
# floor. So if by some float error the hotspot is at 4.995, it will be
# displaced to 5.005, then floored to 5. This is to prevent the hotspot
# from potential off-by-one errors when the cursor is scaled.
HOTSPOT_DISPLACE = 1
svg = QSvgRenderer(sys.argv[1])
scale = 100
hotspot = svg.transformForElement('hotspot').map(svg.boundsOnElement('hotspot')).boundingRect().topLeft()
#print(hotspot.x(), hotspot.y())
hotspot_x = math.floor((hotspot.x() * scale + HOTSPOT_DISPLACE) / 100)
hotspot_y = math.floor((hotspot.y() * scale + HOTSPOT_DISPLACE) / 100)
print(hotspot_x, hotspot_y)
@@ -0,0 +1,15 @@
include(ECMMarkNonGuiExecutable)
set(CMAKE_AUTOMOC ON)
add_executable(kcursorgen)
ecm_mark_nongui_executable(kcursorgen)
target_sources(kcursorgen PRIVATE
main.cpp
kcursorgen.cpp
)
target_link_libraries(kcursorgen Qt6::Core Qt6::Svg)
install(TARGETS kcursorgen)
@@ -0,0 +1,29 @@
# kcursorgen: Convert SVG theme to XCursor theme
`kcursorgen` is a tool to convert SVG theme to XCursor theme. Traditionally, Breeze (and plenty of other themes) SVG files are rendered with Inkscape and packaged as XCursor files at build time, then the resulting XCursor files are shipped to the end user. `kcursorgen` only depends on `QtSvg` and `xcursorgen`, making it possible to only ship SVG files and this program to the end user, and generate XCursor files at install time (in the distro's postinstall script) or runtime (during Plasma startup and in the cursor theme KCM).
The benefits of this approach are:
1. Smaller package size: Breeze SVG files are less than 1MB, while the XCursor files are around 15MB.
2. Allow the user to set any cursor size and scale factor, instead of being limited to the sizes provided by the theme.
## Usage
```sh
kcursorgen --svg-theme-to-xcursor --svg-dir <path> --xcursor-dir <path> --sizes <size1,size2,...> --scales <scale1,scale2,...>
```
- `--svg-dir` is the path to the SVG theme directory, e.g. `/usr/share/icons/breeze_cursors/cursors_scalable`.
- `--xcursor-dir` is the path to the XCursor theme directory, e.g. `/usr/share/icons/breeze_cursors/cursors`. You need to clear this directory before running `kcursorgen`.
- `--sizes` is a comma-separated list of sizes to generate, e.g. `16,24,32,48,64`.
- `--scales` is a comma-separated list of scales to generate, e.g. `1,1.25,1.5,1.75,2`. The scale factor is multiplied by the size to get the final list of cursor sizes. `kcursorgen` will take care of alignment requirements at integer scales. (E.g. GTK3 requires the cursor image size to be a multiple of 3 at scale 3.)
Note that while a postinstall script can write to locations like `/usr/share/icons/breeze_cursors/`, a normal user cannot. So in the Plasma startup code, the cursor theme KCM, or manually running `kcursorgen`, you should copy the cursor theme to `~/.local/share/icons/` and run the tool there.
For example, if the user sets a cursor size of 50 and a scale factor of 2.5, we can copy `/usr/share/icons/breeze_cursors/` to `~/.local/share/icons/` and run:
```sh
kcursorgen --svg-theme-to-xcursor --svg-dir ~/.local/share/icons/breeze_cursors/cursors_scalable --xcursor-dir ~/.local/share/icons/breeze_cursors/cursors --sizes 50 --scales 1,2.5,3
```
In the `--scales` argument, "1" provides an unscaled cursor for Wayland apps without HiDPI support and X11 apps running in "scaled by system" mode, "2.5" provides a cursor for Wayland apps with fractional scaling support and X11 apps in "scale themselves" mode, and "3" provides a cursor for Wayland apps with only integer scaling support.
@@ -0,0 +1,272 @@
/*
SPDX-FileCopyrightText: 2024 Jin Liu <m.liu.jin@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "kcursorgen.h"
#include "options.h"
#include <QCollator>
#include <QDir>
#include <QImage>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QList>
#include <QPainter>
#include <QProcess>
#include <QRegularExpression>
#include <QString>
#include <QSvgRenderer>
#include <QTemporaryDir>
#include <QTimer>
#include <QTransform>
#include <chrono>
#include <numeric>
#include <optional>
struct SvgCursorMetaDataEntry {
static std::optional<SvgCursorMetaDataEntry> parse(const QJsonObject &object);
QString fileName;
qreal nominalSize;
QPointF hotspot;
std::chrono::milliseconds delay;
};
std::optional<SvgCursorMetaDataEntry> SvgCursorMetaDataEntry::parse(const QJsonObject &object)
{
const QJsonValue fileName = object.value(QLatin1String("filename"));
if (!fileName.isString()) {
return std::nullopt;
}
const QJsonValue nominalSize = object.value(QLatin1String("nominal_size"));
if (!nominalSize.isDouble()) {
return std::nullopt;
}
const QJsonValue hotspotX = object.value(QLatin1String("hotspot_x"));
if (!hotspotX.isDouble()) {
return std::nullopt;
}
const QJsonValue hotspotY = object.value(QLatin1String("hotspot_y"));
if (!hotspotY.isDouble()) {
return std::nullopt;
}
const QJsonValue delay = object.value(QLatin1String("delay"));
return SvgCursorMetaDataEntry{
.fileName = fileName.toString(),
.nominalSize = nominalSize.toDouble(),
.hotspot = QPointF(hotspotX.toDouble(), hotspotY.toDouble()),
.delay = std::chrono::milliseconds(delay.toInt()),
};
}
struct SvgCursorMetaData {
static std::optional<SvgCursorMetaData> parse(const QString &filePath);
QList<SvgCursorMetaDataEntry> entries;
};
std::optional<SvgCursorMetaData> SvgCursorMetaData::parse(const QString &filePath)
{
QFile metaDataFile(filePath);
if (!metaDataFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
return std::nullopt;
}
QJsonParseError jsonParseError;
const QJsonDocument document = QJsonDocument::fromJson(metaDataFile.readAll(), &jsonParseError);
if (jsonParseError.error) {
return std::nullopt;
}
QList<SvgCursorMetaDataEntry> entries;
if (document.isArray()) {
const QJsonArray array = document.array();
for (int i = 0; i < array.size(); ++i) {
const QJsonValue element = array.at(i);
if (!element.isObject()) {
return std::nullopt;
}
if (const auto entry = SvgCursorMetaDataEntry::parse(element.toObject())) {
entries.append(entry.value());
} else {
return std::nullopt;
}
}
} else {
return std::nullopt;
}
return SvgCursorMetaData{
.entries = entries,
};
}
bool KCursorGen::svgThemeToXCursor(const QString &svgDir, const QString &xcursorDir, const QList<int> &sizes, const QList<qreal> &scales)
{
// STEP 1: Calculate all desired sizes
// map<nominalSize -> alignment>
// Some toolkits (e.g. GTK3) require cursor image size to be a multiple of the scale factor.
// So we might need to add paddings to satisfy this requirement.
QMap<int, int> desiredSizes;
for (qreal scale : scales) {
int alignment = round(scale);
// Fractional scales doesn't need alignment. If the toolkit doesn't support fractional scaling
// for cursors, it will use the nearest integer scale. If it does, it will use the Wayland
// Viewporter protocol, which doesn't require alignment.
if (alignment != scale) {
alignment = 1;
}
for (int size : sizes) {
const int desiredSize = round(size * scale);
if (!desiredSizes.contains(desiredSize)) {
desiredSizes.insert(desiredSize, alignment);
} else {
desiredSizes[desiredSize] = std::lcm(desiredSizes[desiredSize], alignment);
}
}
}
qInfo() << "Desired sizes:";
for (auto [size, alignment] : desiredSizes.asKeyValueRange()) {
qInfo() << '\t' << size << "alignment" << alignment;
}
// STEP 2: Generate PNGs and config files
const QDir srcDir(svgDir);
QTemporaryDir tmpDir;
if (!tmpDir.isValid()) {
qCritical() << "Failed to create temporary directory";
return false;
}
const QDir renderDir(tmpDir.path());
for (const int size : desiredSizes.keys()) {
if (!renderDir.mkdir(QString::number(size))) {
qCritical() << "Failed to create dir " << QString::number(size);
return false;
}
}
if (!renderDir.mkdir(QStringLiteral("config"))) {
qCritical() << "Failed to create dir" << QStringLiteral("config");
return false;
}
const QStringList shapes = srcDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks, QDir::Name);
for (const QString &shape : shapes) {
const QDir shapeDir(srcDir.filePath(shape));
qInfo() << "Rendering" << shape;
const QString metadataFilePath = shapeDir.filePath(QStringLiteral("metadata.json"));
const auto metadata = SvgCursorMetaData::parse(metadataFilePath);
if (!metadata.has_value()) {
qCritical() << "Failed to parse" << metadataFilePath;
return false;
}
const bool animated = metadata->entries.size() > 1;
QFile configFile(renderDir.filePath(QStringLiteral("config/") + shape + QStringLiteral(".config")));
if (!configFile.open(QIODevice::WriteOnly)) {
qCritical() << "Failed to open" << configFile.fileName() << "for writing";
return false;
}
QTextStream configStream(&configFile);
for (auto [nominalSize, alignment] : desiredSizes.asKeyValueRange()) {
const QDir renderDirForSize(renderDir.filePath(QString::number(nominalSize)));
for (const SvgCursorMetaDataEntry &entry : metadata->entries) {
const QString svgPath = shapeDir.filePath(entry.fileName);
QSvgRenderer renderer(svgPath);
if (!renderer.isValid()) {
qCritical() << "Failed to render" << svgPath;
return false;
}
const qreal scale = nominalSize / entry.nominalSize;
QSize imageSize = renderer.defaultSize() * scale;
QSize alignedImageSize = imageSize;
alignedImageSize.rwidth() += (alignment - (alignedImageSize.width() % alignment)) % alignment;
alignedImageSize.rheight() += (alignment - (alignedImageSize.height() % alignment)) % alignment;
QImage image(alignedImageSize, QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
QPainter painter(&image);
// to suppress "QFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0" warnings
painter.setFont(QFont(QStringLiteral("Sans"), 10));
renderer.render(&painter, QRect(QPoint(0, 0), imageSize));
painter.end();
QString pngName = entry.fileName;
pngName.replace(QStringLiteral(".svg"), QStringLiteral(".png"));
image.save(renderDirForSize.filePath(pngName));
const QPointF hotspot = entry.hotspot * scale;
configStream << nominalSize << QStringLiteral(" ") << int(hotspot.x()) << QStringLiteral(" ") << int(hotspot.y()) << QStringLiteral(" ")
<< QString::number(nominalSize) << QStringLiteral("/") << pngName;
if (animated) {
configStream << QStringLiteral(" ") << entry.delay.count();
}
configStream << QStringLiteral("\n");
}
}
configFile.close();
}
// STEP 3: Generate XCursors
QDir outputDir(xcursorDir);
if (!outputDir.mkpath(QStringLiteral("."))) {
qCritical() << "Failed to create" << outputDir.path();
return false;
}
const QDir configDir(renderDir.filePath(QStringLiteral("config")));
const QStringList configs = configDir.entryList(QDir::Files, QDir::Name);
for (const QString &config : configs) {
QString cursorName = config;
cursorName.replace(QStringLiteral(".config"), QStringLiteral(""));
qInfo() << "Generating" << cursorName;
QProcess xcursorgen;
xcursorgen.setProgram(QStringLiteral("xcursorgen"));
xcursorgen.setArguments({QStringLiteral("--prefix"), renderDir.path(), configDir.filePath(config), outputDir.filePath(cursorName)});
xcursorgen.start();
xcursorgen.waitForFinished(-1);
if (xcursorgen.exitStatus() != QProcess::NormalExit || xcursorgen.exitCode() != 0) {
qCritical() << "xcursorgen failed:" << xcursorgen.errorString();
return false;
}
}
// STEP 4: Aliases
qInfo() << "Making aliases";
const auto dirs = srcDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
for (const QFileInfo &info : dirs) {
if (!info.isSymbolicLink()) {
continue;
}
const QString aliasName = info.fileName();
const QString targetName = info.readSymLink();
QFile f(outputDir.filePath(aliasName));
if (f.exists()) {
f.remove();
}
if (!QFile::link(targetName, outputDir.filePath(aliasName))) {
qCritical() << "Failed to create alias" << aliasName << "=>" << targetName;
return false;
}
}
qInfo() << "SUCCESS";
return true;
}
@@ -0,0 +1,15 @@
/*
SPDX-FileCopyrightText: 2024 Jin Liu <m.liu.jin@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QList>
#include <QString>
class KCursorGen
{
public:
static bool svgThemeToXCursor(const QString &svgDir, const QString &xcursorDir, const QList<int> &sizes, const QList<qreal> &scales);
};
@@ -0,0 +1,84 @@
/*
SPDX-FileCopyrightText: 2024 Jin Liu <m.liu.jin@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "kcursorgen.h"
#include "options.h"
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QString>
int main(int argc, char **argv)
{
QCommandLineParser parser;
QCoreApplication app(argc, argv);
const auto description = QStringLiteral(
"Convert SVG theme to XCursor theme.\n"
"NOTE: This tool is in EXPERIMENTAL stage and subject to change.\n"
"Usage:\n"
" kcursorgen --svg-theme-to-xcursor --svg-dir <path> --xcursor-dir <path> --sizes <size1,size2,...> --scales <scale1,scale2,...>\n");
const auto version = QStringLiteral("1.0");
app.setApplicationVersion(version);
parser.addVersionOption();
parser.addHelpOption();
parser.setApplicationDescription(description);
parser.addOptions({Options::svgThemeToXCursor(), Options::svgDir(), Options::xcursorDir(), Options::sizes(), Options::scales()});
parser.process(app);
// at least one operation should be specified
if (argc <= 1) {
parser.showHelp(0);
} else if (parser.isSet(Options::svgThemeToXCursor())) {
if (!parser.isSet(Options::svgDir())) {
qCritical() << "Missing <svg-dir> parameter";
return 1;
}
if (!parser.isSet(Options::xcursorDir())) {
qCritical() << "Missing <xcursor-dir> parameter";
return 1;
}
const QStringList sizeStrings = parser.value(Options::sizes()).split(QLatin1Char(','), Qt::SkipEmptyParts);
QList<int> sizes;
for (const QString &i : sizeStrings) {
const int size = i.toInt();
if (size <= 0) {
qCritical() << "Invalid size: " << i;
return 1;
}
sizes << size;
}
if (sizes.isEmpty()) {
qCritical() << "No valid <sizes> specified";
return 1;
}
const QStringList scaleStrings = parser.value(Options::scales()).split(QLatin1Char(','), Qt::SkipEmptyParts);
QList<qreal> scales;
for (const QString &i : scaleStrings) {
const qreal scale = i.toDouble();
if (scale <= 0) {
qCritical() << "Invalid scale: " << i;
return 1;
}
scales << scale;
}
if (scales.isEmpty()) {
qCritical() << "No valid <scales> specified";
return 1;
}
return KCursorGen::svgThemeToXCursor(parser.value(Options::svgDir()), parser.value(Options::xcursorDir()), sizes, scales) ? 0 : 1;
} else {
qCritical() << "No command specified";
return 1;
}
}
@@ -0,0 +1,50 @@
/*
SPDX-FileCopyrightText: 2024 Jin Liu <m.liu.jin@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QCommandLineOption>
namespace Options
{
// main commands
static QCommandLineOption svgThemeToXCursor()
{
static QCommandLineOption o{QStringLiteral("svg-theme-to-xcursor"),
QStringLiteral("Convert a SVG cursor theme in <theme-dir>/cursors_scalable to XCursor format in <theme-dir>/cursors")};
return o;
}
// parameters
static QCommandLineOption svgDir()
{
static QCommandLineOption o{QStringLiteral("svg-dir"), QStringLiteral("SVG cursor directory."), QStringLiteral("svg-dir")};
return o;
}
static QCommandLineOption xcursorDir()
{
static QCommandLineOption o{QStringLiteral("xcursor-dir"), QStringLiteral("XCursor directory."), QStringLiteral("xcursor-dir")};
return o;
}
static QCommandLineOption sizes()
{
static QCommandLineOption o{QStringLiteral("sizes"), QStringLiteral("Comma-separated list of cursor sizes to generate."), QStringLiteral("sizes")};
return o;
}
static QCommandLineOption scales()
{
static QCommandLineOption o{QStringLiteral("scales"),
QStringLiteral("Comma-separated list of scales to apply to each size."),
QStringLiteral("scales"),
QStringLiteral("1")};
return o;
}
}
@@ -0,0 +1,69 @@
#!/bin/bash
# This script splits the monolithic cursors.svg into individual SVGs for each cursor.
# Run in Breeze/. Results are in Breeze/src/svg.
set -euo pipefail
RAWSVG="src/cursors.svg"
BINDIR="$( dirname "${BASH_SOURCE[0]}" )"
echo -ne "Checking Requirements...\\r"
if [[ ! -f "${RAWSVG}" ]]; then
echo -e "\\nFAIL: '${RAWSVG}' missing"
exit 1
fi
if ! command -v inkscape > /dev/null ; then
echo -e "\\nFAIL: inkscape must be installed"
exit 1
fi
echo "Checking Requirements... DONE"
echo "Splitting..."
DIR="src/svg"
mkdir -p $DIR
for CUR in src/config/*.cursor; do
BASENAME=${CUR##*/}
BASENAME=${BASENAME%.*}
echo -ne "$BASENAME...\\r"
if [[ "${DIR}/${BASENAME}.svg" -ot ${RAWSVG} ]]; then
# Set viewbox around the cursor
inkscape $RAWSVG -i $BASENAME -o /tmp/"$BASENAME".svg >/dev/null
# Remove everything except the cursor
$BINDIR/clean_svg /tmp/"$BASENAME".svg /tmp/"$BASENAME"-cleaned.svg "$BASENAME"
# Remove groups in the middle, and remove Inkscape-specific attributes
inkscape /tmp/"$BASENAME"-cleaned.svg -o $DIR/"$BASENAME".svg --actions 'select-all:groups;selection-ungroup;select-all:groups;selection-ungroup;select-all:groups;selection-ungroup' -l > /dev/null
rm /tmp/"$BASENAME".svg /tmp/"$BASENAME"-cleaned.svg
fi
echo "$BASENAME... DONE"
done
for CUR in progress wait; do
for i in {01..23}; do
BASENAME="${CUR}-${i}"
echo -ne "$BASENAME...\\r"
if [[ "${DIR}/${BASENAME}.svg" -ot ${RAWSVG} ]]; then
# Set viewbox around the cursor
inkscape $RAWSVG -i $BASENAME -o /tmp/"$BASENAME".svg >/dev/null
# Remove everything except the cursor
$BINDIR/clean_svg /tmp/"$BASENAME".svg /tmp/"$BASENAME"-cleaned.svg "$BASENAME"
# Remove groups in the middle, and remove Inkscape-specific attributes
inkscape /tmp/"$BASENAME"-cleaned.svg -o $DIR/"$BASENAME".svg --actions 'select-all:groups;selection-ungroup;select-all:groups;selection-ungroup' -l > /dev/null
rm /tmp/"$BASENAME".svg /tmp/"$BASENAME"-cleaned.svg
fi
echo "$BASENAME... DONE"
done
done
echo "COMPLETE!"
@@ -0,0 +1,27 @@
export default {
multipass: true,
js2svg: {
indent: 4,
pretty: true,
},
plugins: [
{
name: 'preset-default',
params: {
overrides: {
cleanupIds: false,
removeViewBox: false,
removeHiddenElems: false,
removeUselessDefs: true,
convertTransform: true,
// customize the params of a default plugin
inlineStyles: {
onlyMatchedOnce: false,
},
},
},
},
],
};