1 Commits

20 changed files with 590 additions and 664 deletions
+12 -5
View File
@@ -22,6 +22,7 @@ Singleton {
property alias notifs: adapter.notifs property alias notifs: adapter.notifs
property alias osd: adapter.osd property alias osd: adapter.osd
property alias overview: adapter.overview property alias overview: adapter.overview
property alias plugins: adapter.plugins
property bool recentlySaved: false property bool recentlySaved: false
property alias screenshot: adapter.screenshot property alias screenshot: adapter.screenshot
property alias services: adapter.services property alias services: adapter.services
@@ -140,7 +141,8 @@ Singleton {
launcher: serializeLauncher(), launcher: serializeLauncher(),
colors: serializeColors(), colors: serializeColors(),
dock: serializeDock(), dock: serializeDock(),
screenshot: serializeScreenshot() screenshot: serializeScreenshot(),
plugins: serializePlugins()
}; };
} }
@@ -214,10 +216,6 @@ Singleton {
}, },
idle: { idle: {
timeouts: general.idle.timeouts timeouts: general.idle.timeouts
},
battery: {
popupThresholds: general.battery.popupThresholds,
critPerc: general.battery.critPerc
} }
}; };
} }
@@ -293,6 +291,13 @@ Singleton {
}; };
} }
function serializePlugins(): var {
return {
enabled: plugins.enabled,
entries: plugins.entries
};
}
function serializeScreenshot(): var { function serializeScreenshot(): var {
return { return {
enable_pp: screenshot.enable_pp, enable_pp: screenshot.enable_pp,
@@ -462,6 +467,8 @@ Singleton {
} }
property Overview overview: Overview { property Overview overview: Overview {
} }
property PluginConfig plugins: PluginConfig {
}
property Screenshot screenshot: Screenshot { property Screenshot screenshot: Screenshot {
} }
property Services services: Services { property Services services: Services {
-13
View File
@@ -4,8 +4,6 @@ import Quickshell
JsonObject { JsonObject {
property Apps apps: Apps { property Apps apps: Apps {
} }
property Battery battery: Battery {
}
property Color color: Color { property Color color: Color {
} }
property string dateFormat: "ddd d MMM - hh:mm:ss" property string dateFormat: "ddd d MMM - hh:mm:ss"
@@ -21,17 +19,6 @@ JsonObject {
property list<string> playback: ["mpv"] property list<string> playback: ["mpv"]
property list<string> terminal: ["kitty"] property list<string> terminal: ["kitty"]
} }
component Battery: JsonObject {
property int critPerc: 5
property list<var> popupThresholds: [
{
perc: 20,
name: qsTr("Low battery"),
message: qsTr("Battery at %1%").arg(Battery.currentPerc * 100),
icon: "battery_android_frame_2"
},
]
}
component Color: JsonObject { component Color: JsonObject {
property int hyprsunsetTemp: 5000 property int hyprsunsetTemp: 5000
property string mode: "dark" property string mode: "dark"
+11
View File
@@ -0,0 +1,11 @@
import Quickshell.Io
JsonObject {
property bool enabled: false
property list<var> entries: [
{
id: "Plugin",
enabled: false
},
]
}
-46
View File
@@ -1,46 +0,0 @@
import Quickshell
import Quickshell.Services.UPower
import QtQuick
import ZShell
import qs.Config
import qs.Components.Toast
Scope {
id: root
readonly property real currentPerc: UPower.displayDevice.percentage
readonly property list<var> popupThresholds: [...Config.general.battery.popupThresholds].sort((a, b) => b.perc - a.perc)
Connections {
function onOnBatteryChanged(): void {
if (UPower.onBattery) {
if (Config.utilities.toasts.chargingChanged)
Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off");
} else {
if (Config.utilities.toasts.chargingChanged)
Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power");
for (const level of root.popupThresholds)
level.warned = false;
}
}
target: UPower
}
Connections {
function onPercentageChanged(): void {
if (!UPower.onBattery)
return;
const p = UPower.displayDevice.percentage * 100;
for (const perc of root.popupThresholds) {
if (p <= perc.perc && !perc.warned) {
perc.warned = true;
Toaster.toast(perc.title ?? qsTr("Battery warning"), perc.message ?? qsTr("Battery perc is low"), perc.icon ?? "battery_android_alert", perc.critical ? Toast.Error : Toast.Warning);
}
}
}
target: UPower.displayDevice
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ Variants {
property var root: Quickshell.shellDir property var root: Quickshell.shellDir
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
// WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
color: "transparent" color: "transparent"
contentItem.focus: true contentItem.focus: true
mask: visibilities.isDrawing ? null : region mask: visibilities.isDrawing ? null : region
+18
View File
@@ -0,0 +1,18 @@
pragma Singleton
import Quickshell
import ZShell.Models
Singleton {
id: root
property alias plugins: plugins.entries
FileSystemModel {
id: plugins
nameFilters: ["*.qml"]
path: Quickshell.env("HOME") + "/.config/zshell"
recursive: false
}
}
+17
View File
@@ -0,0 +1,17 @@
import Quickshell
import QtQuick
import ZShell.Models
import qs.Config
Repeater {
model: FetchPlugins.plugins
LazyLoader {
required property FileSystemEntry modelData
activeAsync: Config.plugins.entries.some(p => {
return p.id === modelData.baseName && p.enabled;
})
source: modelData.path
}
}
+55 -12
View File
@@ -4,7 +4,6 @@ import Quickshell
import QtQuick import QtQuick
import qs.Components import qs.Components
import qs.Config import qs.Config
import qs.Modules.Launcher.Services
Item { Item {
id: root id: root
@@ -20,17 +19,26 @@ Item {
max -= panels.popouts.nonAnimHeight; max -= panels.popouts.nonAnimHeight;
return max; return max;
} }
property real offsetScale: shouldBeActive ? 0 : 1
required property var panels required property var panels
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.launcher
required property PersistentProperties visibilities required property PersistentProperties visibilities
readonly property bool shouldBeActive: visibilities.launcher
property real offsetScale: shouldBeActive ? 0 : 1
onShouldBeActiveChanged: {
if (shouldBeActive) {
implicitHeight = Qt.binding(() => content.implicitHeight);
timer.stop();
} else {
implicitHeight = implicitHeight;
}
}
visible: offsetScale < 1
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth || 400 implicitWidth: content.implicitWidth || 400
opacity: 1 - offsetScale opacity: 1 - offsetScale
visible: offsetScale < 1
Behavior on offsetScale { Behavior on offsetScale {
Anim { Anim {
@@ -39,26 +47,61 @@ Item {
} }
} }
Component.onCompleted: Qt.callLater(() => Apps) onMaxHeightChanged: timer.start()
onShouldBeActiveChanged: {
if (shouldBeActive) Connections {
implicitHeight = Qt.binding(() => content.implicitHeight); function onEnabledChanged(): void {
else timer.start();
implicitHeight = implicitHeight; }
function onMaxShownChanged(): void {
timer.start();
}
target: Config.launcher
}
Connections {
function onValuesChanged(): void {
if (DesktopEntries.applications.values.length < Config.launcher.maxAppsShown)
timer.start();
}
target: DesktopEntries.applications
}
Timer {
id: timer
interval: Appearance.anim.durations.small
onRunningChanged: {
if (running && !root.shouldBeActive) {
content.visible = false;
content.active = true;
} else {
root.contentHeight = Math.min(root.maxHeight, content.implicitHeight);
content.active = Qt.binding(() => root.shouldBeActive || root.visible);
content.visible = true;
}
}
} }
Loader { Loader {
id: content id: content
active: root.shouldBeActive || root.visible active: false
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
asynchronous: true
sourceComponent: Content { sourceComponent: Content {
maxHeight: root.maxHeight maxHeight: root.maxHeight
panels: root.panels panels: root.panels
visibilities: root.visibilities visibilities: root.visibilities
Component.onCompleted: root.contentHeight = implicitHeight
} }
Component.onCompleted: timer.start()
} }
} }
+6
View File
@@ -116,6 +116,12 @@ Item {
key: "updates" key: "updates"
name: "Updates" name: "Updates"
} }
ListElement {
icon: "extension"
key: "plugins"
name: "Extensions"
}
} }
CustomClippingRect { CustomClippingRect {
+18
View File
@@ -0,0 +1,18 @@
import qs.Modules.Settings.Controls
import qs.Config
SettingsPage {
SettingsSection {
sectionId: "Plugins"
SettingsHeader {
name: "Plugins"
}
SettingBarEntryList {
name: "Enable or disable plugins"
object: Config.plugins
setting: "entries"
}
}
}
+9
View File
@@ -79,6 +79,8 @@ Item {
stack.push(screenshot); stack.push(screenshot);
else if (currentCategory === "updates") else if (currentCategory === "updates")
stack.push(updates); stack.push(updates);
else if (currentCategory === "plugins")
stack.push(plugins);
} }
target: root target: root
@@ -245,4 +247,11 @@ Item {
Cat.SystemUpdates { Cat.SystemUpdates {
} }
} }
Component {
id: plugins
Cat.Plugins {
}
}
} }
+1 -1
View File
@@ -6,7 +6,7 @@ import qs.Modules.DesktopIcons
Loader { Loader {
active: Config.background.enabled active: Config.background.enabled
asynchronous: false asynchronous: true
sourceComponent: Variants { sourceComponent: Variants {
model: Quickshell.screens model: Quickshell.screens
+16 -25
View File
@@ -12,7 +12,8 @@ FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePat
, m_path(path) , m_path(path)
, m_relativePath(relativePath) , m_relativePath(relativePath)
, m_isImageInitialised(false) , m_isImageInitialised(false)
, m_mimeTypeInitialised(false) {} , m_mimeTypeInitialised(false) {
}
QString FileSystemEntry::path() const { QString FileSystemEntry::path() const {
return m_path; return m_path;
@@ -57,8 +58,8 @@ bool FileSystemEntry::isImage() const {
QString FileSystemEntry::mimeType() const { QString FileSystemEntry::mimeType() const {
if (!m_mimeTypeInitialised) { if (!m_mimeTypeInitialised) {
const QMimeDatabase db; static const QMimeDatabase s_db;
m_mimeType = db.mimeTypeForFile(m_path).name(); m_mimeType = s_db.mimeTypeForFile(m_path).name();
m_mimeTypeInitialised = true; m_mimeTypeInitialised = true;
} }
return m_mimeType; return m_mimeType;
@@ -219,7 +220,7 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) {
if (m_recursive && m_watchChanges) { if (m_recursive && m_watchChanges) {
const auto currentDir = m_dir; const auto currentDir = m_dir;
const bool showHidden = m_showHidden; const bool showHidden = m_showHidden;
const auto future = QtConcurrent::run([showHidden, path]() { auto future = QtConcurrent::run([showHidden, path]() {
QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot;
if (showHidden) { if (showHidden) {
filters |= QDir::Hidden; filters |= QDir::Hidden;
@@ -232,16 +233,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) {
} }
return dirs; return dirs;
}); });
const auto watcher = new QFutureWatcher<QStringList>(this); future.then(this, [currentDir, showHidden, this](const QStringList& paths) {
connect(watcher, &QFutureWatcher<QStringList>::finished, this, [currentDir, showHidden, watcher, this]() {
const auto paths = watcher->result();
if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) {
// Ignore if dir or showHidden has changed // Ignore if dir or showHidden has changed
m_watcher.addPaths(paths); m_watcher.addPaths(paths);
} }
watcher->deleteLater();
}); });
watcher->setFuture(future);
} }
} }
@@ -295,7 +292,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
oldPaths << entry->path(); oldPaths << entry->path();
} }
const auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) { auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString> > >& promise) {
const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
std::optional<QDirIterator> iter; std::optional<QDirIterator> iter;
@@ -353,7 +350,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
newPaths.insert(path); newPaths.insert(path);
} }
if (promise.isCanceled() || newPaths == oldPaths) { if (promise.isCanceled()) {
return; return;
} }
@@ -365,23 +362,17 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
} }
m_futures.insert(dir, future); m_futures.insert(dir, future);
const auto watcher = new QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>(this); future
.then(this,
connect(watcher, &QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>::finished, this, [dir, watcher, this]() { [dir, this](QPair<QSet<QString>, QSet<QString> > result) {
m_futures.remove(dir); m_futures.remove(dir);
if (!result.first.isEmpty() || !result.second.isEmpty()) {
if (!watcher->future().isResultReadyAt(0)) {
watcher->deleteLater();
return;
}
const auto result = watcher->result();
applyChanges(result.first, result.second); applyChanges(result.first, result.second);
}
watcher->deleteLater(); })
.onCanceled(this, [dir, this]() {
m_futures.remove(dir);
}); });
watcher->setFuture(future);
} }
void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) { void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) {
+90 -90
View File
@@ -13,136 +13,136 @@
namespace ZShell::models { namespace ZShell::models {
class FileSystemEntry : public QObject { class FileSystemEntry : public QObject {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel") QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel")
Q_PROPERTY(QString path READ path CONSTANT) Q_PROPERTY(QString path READ path CONSTANT)
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged) Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
Q_PROPERTY(QString name READ name CONSTANT) Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString baseName READ baseName CONSTANT) Q_PROPERTY(QString baseName READ baseName CONSTANT)
Q_PROPERTY(QString parentDir READ parentDir CONSTANT) Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
Q_PROPERTY(QString suffix READ suffix CONSTANT) Q_PROPERTY(QString suffix READ suffix CONSTANT)
Q_PROPERTY(qint64 size READ size CONSTANT) Q_PROPERTY(qint64 size READ size CONSTANT)
Q_PROPERTY(bool isDir READ isDir CONSTANT) Q_PROPERTY(bool isDir READ isDir CONSTANT)
Q_PROPERTY(bool isImage READ isImage CONSTANT) Q_PROPERTY(bool isImage READ isImage CONSTANT)
Q_PROPERTY(QString mimeType READ mimeType CONSTANT) Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
public: public:
explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr); explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr);
[[nodiscard]] QString path() const; [[nodiscard]] QString path() const;
[[nodiscard]] QString relativePath() const; [[nodiscard]] QString relativePath() const;
[[nodiscard]] QString name() const; [[nodiscard]] QString name() const;
[[nodiscard]] QString baseName() const; [[nodiscard]] QString baseName() const;
[[nodiscard]] QString parentDir() const; [[nodiscard]] QString parentDir() const;
[[nodiscard]] QString suffix() const; [[nodiscard]] QString suffix() const;
[[nodiscard]] qint64 size() const; [[nodiscard]] qint64 size() const;
[[nodiscard]] bool isDir() const; [[nodiscard]] bool isDir() const;
[[nodiscard]] bool isImage() const; [[nodiscard]] bool isImage() const;
[[nodiscard]] QString mimeType() const; [[nodiscard]] QString mimeType() const;
void updateRelativePath(const QDir& dir); void updateRelativePath(const QDir& dir);
signals: signals:
void relativePathChanged(); void relativePathChanged();
private: private:
const QFileInfo m_fileInfo; const QFileInfo m_fileInfo;
const QString m_path; const QString m_path;
QString m_relativePath; QString m_relativePath;
mutable bool m_isImage; mutable bool m_isImage;
mutable bool m_isImageInitialised; mutable bool m_isImageInitialised;
mutable QString m_mimeType; mutable QString m_mimeType;
mutable bool m_mimeTypeInitialised; mutable bool m_mimeTypeInitialised;
}; };
class FileSystemModel : public QAbstractListModel { class FileSystemModel : public QAbstractListModel {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged) Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged)
Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged) Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged)
Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged) Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged)
Q_PROPERTY(bool sortReverse READ sortReverse WRITE setSortReverse NOTIFY sortReverseChanged) Q_PROPERTY(bool sortReverse READ sortReverse WRITE setSortReverse NOTIFY sortReverseChanged)
Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged) Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged)
Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged) Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged)
Q_PROPERTY(QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries NOTIFY entriesChanged) Q_PROPERTY(QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries NOTIFY entriesChanged)
public: public:
enum Filter { enum Filter {
NoFilter, NoFilter,
Images, Images,
Files, Files,
Dirs Dirs
}; };
Q_ENUM(Filter) Q_ENUM(Filter)
explicit FileSystemModel(QObject* parent = nullptr); explicit FileSystemModel(QObject* parent = nullptr);
int rowCount(const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QString path() const; [[nodiscard]] QString path() const;
void setPath(const QString& path); void setPath(const QString& path);
[[nodiscard]] bool recursive() const; [[nodiscard]] bool recursive() const;
void setRecursive(bool recursive); void setRecursive(bool recursive);
[[nodiscard]] bool watchChanges() const; [[nodiscard]] bool watchChanges() const;
void setWatchChanges(bool watchChanges); void setWatchChanges(bool watchChanges);
[[nodiscard]] bool showHidden() const; [[nodiscard]] bool showHidden() const;
void setShowHidden(bool showHidden); void setShowHidden(bool showHidden);
[[nodiscard]] bool sortReverse() const; [[nodiscard]] bool sortReverse() const;
void setSortReverse(bool sortReverse); void setSortReverse(bool sortReverse);
[[nodiscard]] Filter filter() const; [[nodiscard]] Filter filter() const;
void setFilter(Filter filter); void setFilter(Filter filter);
[[nodiscard]] QStringList nameFilters() const; [[nodiscard]] QStringList nameFilters() const;
void setNameFilters(const QStringList& nameFilters); void setNameFilters(const QStringList& nameFilters);
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries(); [[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
signals: signals:
void pathChanged(); void pathChanged();
void recursiveChanged(); void recursiveChanged();
void watchChangesChanged(); void watchChangesChanged();
void showHiddenChanged(); void showHiddenChanged();
void sortReverseChanged(); void sortReverseChanged();
void filterChanged(); void filterChanged();
void nameFiltersChanged(); void nameFiltersChanged();
void entriesChanged(); void entriesChanged();
private: private:
QDir m_dir; QDir m_dir;
QFileSystemWatcher m_watcher; QFileSystemWatcher m_watcher;
QList<FileSystemEntry*> m_entries; QList<FileSystemEntry*> m_entries;
QHash<QString, QFuture<QPair<QSet<QString>, QSet<QString>>>> m_futures; QHash<QString, QFuture<QPair<QSet<QString>, QSet<QString> > > > m_futures;
QString m_path; QString m_path;
bool m_recursive; bool m_recursive;
bool m_watchChanges; bool m_watchChanges;
bool m_showHidden; bool m_showHidden;
bool m_sortReverse; bool m_sortReverse = false;
Filter m_filter; Filter m_filter;
QStringList m_nameFilters; QStringList m_nameFilters;
void watchDirIfRecursive(const QString& path); void watchDirIfRecursive(const QString& path);
void update(); void update();
void updateWatcher(); void updateWatcher();
void updateEntries(); void updateEntries();
void updateEntriesForDir(const QString& dir); void updateEntriesForDir(const QString& dir);
void applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths); void applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
[[nodiscard]] bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const; [[nodiscard]] bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const;
}; };
} // namespace ZShell::models } // namespace ZShell::models
+2 -39
View File
@@ -1,53 +1,16 @@
from __future__ import annotations from __future__ import annotations
import sys
from pathlib import Path
import click
import typer import typer
from typer._completion_shared import install, _get_shell_name
from zshell.subcommands import shell, scheme, screenshot, wallpaper, record from zshell.subcommands import shell, scheme, screenshot, wallpaper, record
app = typer.Typer(name="zshell-cli", add_completion=False) app = typer.Typer()
app.add_typer(shell.app, name="shell") app.add_typer(shell.app, name="shell")
app.add_typer(scheme.app, name="scheme") app.add_typer(scheme.app, name="scheme")
app.add_typer(screenshot.app, name="screenshot") app.add_typer(screenshot.app, name="screenshot")
app.add_typer(wallpaper.app, name="wallpaper") app.add_typer(wallpaper.app, name="wallpaper")
app.add_typer(record.app, name="record") app.add_typer(record.app, name="record")
# app.add_typer(preset.app, name="preset")
def _completion_installed() -> bool:
shell = _get_shell_name()
match shell:
case "zsh":
return (Path.home() / ".zfunc" / "_zshell-cli").exists()
case "bash":
return (Path.home() / ".bash_completions" / "zshell-cli.sh").exists()
case "fish":
return (Path.home() / ".config" / "fish" / "completions" / "zshell-cli.fish").exists()
return False
def _install_completion() -> None:
if _completion_installed():
click.echo("zshell-cli: Shell completion already installed.")
raise typer.Exit()
shell = _get_shell_name()
if shell is None:
click.echo("zshell-cli: Unable to detect shell type.", err=True)
raise typer.Exit(code=1)
try:
_, path = install(prog_name="zshell-cli")
click.secho(f"zshell-cli: Shell completion installed ({shell}: {path})", fg="green")
click.echo("zshell-cli: Restart your shell or source the file to enable tab-completion.")
except Exception:
pass
def main() -> None: def main() -> None:
if "--install-autocomplete" in sys.argv:
_install_completion()
return
if sys.stdout.isatty() and not _completion_installed():
click.echo("zshell-cli: Tip: run with --install-autocomplete for tab completion.", err=True)
app() app()
+24 -20
View File
@@ -18,7 +18,8 @@ TEMP_RECORDING = STATE_DIR / "recording.mp4"
REPLAY_RECORDING = STATE_DIR / "replay.mp4" REPLAY_RECORDING = STATE_DIR / "replay.mp4"
NOTIF_ID_FILE = STATE_DIR / "notifid.txt" NOTIF_ID_FILE = STATE_DIR / "notifid.txt"
RECORDINGS_DIR = os.getenv("ZSHELL_RECORDINGS_DIR", str(Path(HOME) / "Videos/Recordings")) RECORDINGS_DIR = os.getenv("ZSHELL_RECORDINGS_DIR",
str(Path(HOME) / "Videos/Recordings"))
def _read_extra_args() -> list[str]: def _read_extra_args() -> list[str]:
@@ -35,7 +36,7 @@ def _is_recording() -> bool:
return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
def _notify(summary: str, body: str = "", actions: list | None = None, timeout: int = 5000) -> Optional[int]: def _notify(summary: str, body: str = "", actions: list = None, timeout: int = 5000) -> Optional[int]:
args = ["notify-send", summary, body, "-t", str(timeout), "-p"] args = ["notify-send", summary, body, "-t", str(timeout), "-p"]
if actions: if actions:
for action in actions: for action in actions:
@@ -48,12 +49,14 @@ def _notify(summary: str, body: str = "", actions: list | None = None, timeout:
def _close_notification(notif_id: int): def _close_notification(notif_id: int):
subprocess.run(["notify-send", "--close", str(notif_id)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(["notify-send", "--close", str(notif_id)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def _get_monitors() -> list[dict]: def _get_monitors() -> list[dict]:
try: try:
res = subprocess.run(["hyprctl", "monitors", "-j"], capture_output=True, text=True) res = subprocess.run(["hyprctl", "monitors", "-j"],
capture_output=True, text=True)
return json.loads(res.stdout) return json.loads(res.stdout)
except Exception: except Exception:
return [] return []
@@ -89,7 +92,6 @@ def _slurp_region() -> Optional[str]:
def _parse_geometry(geometry: str) -> Optional[tuple[int, int, int, int]]: def _parse_geometry(geometry: str) -> Optional[tuple[int, int, int, int]]:
import re import re
match = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", geometry) match = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", geometry)
if match: if match:
return int(match.group(3)), int(match.group(4)), int(match.group(1)), int(match.group(2)) return int(match.group(3)), int(match.group(4)), int(match.group(1)), int(match.group(2))
@@ -137,7 +139,8 @@ def start_recording(region: Optional[str], sound: bool):
cmd.extend(extra_args) cmd.extend(extra_args)
cmd.extend(["-o", str(TEMP_RECORDING)]) cmd.extend(["-o", str(TEMP_RECORDING)])
subprocess.Popen(cmd, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.Popen(cmd, start_new_session=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
notif_id = _notify("Recording started", f"Saving to {TEMP_RECORDING}") notif_id = _notify("Recording started", f"Saving to {TEMP_RECORDING}")
if notif_id is not None: if notif_id is not None:
@@ -145,12 +148,14 @@ def start_recording(region: Optional[str], sound: bool):
time.sleep(1) time.sleep(1)
if not _is_recording(): if not _is_recording():
_notify("Recording failed", "Check gpu-screen-recorder output.", timeout=5000) _notify("Recording failed",
"Check gpu-screen-recorder output.", timeout=5000)
raise typer.Exit(code=1) raise typer.Exit(code=1)
def stop_recording(clipboard: bool): def stop_recording(clipboard: bool):
subprocess.run(["pkill", "-f", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(["pkill", "-f", RECORDER],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(50): for _ in range(50):
if not _is_recording(): if not _is_recording():
@@ -173,31 +178,30 @@ def stop_recording(clipboard: bool):
NOTIF_ID_FILE.unlink() NOTIF_ID_FILE.unlink()
if clipboard: if clipboard:
subprocess.run( subprocess.run(["wl-copy", "--type", "text/uri-list", f"file://{final_path}"],
["wl-copy", "--type", "text/uri-list", f"file://{final_path}"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
_notify("Recording stopped", f"Saved to {final_path}", timeout=5000) _notify("Recording stopped", f"Saved to {final_path}", timeout=5000)
def toggle_pause(): def toggle_pause():
subprocess.run(["pkill", "-USR2", "-f", RECORDER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(["pkill", "-USR2", "-f", RECORDER],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
typer.echo("Toggled pause.") typer.echo("Toggled pause.")
@app.command() @app.command()
def record( def record(
region: Optional[str] = typer.Option( region: Optional[str] = typer.Option(
None, None, "--region", "-r",
"--region",
"-r",
help="Record a region. Use 'slurp' (or omit value) to select interactively, or give 'WxH+X+Y'.", help="Record a region. Use 'slurp' (or omit value) to select interactively, or give 'WxH+X+Y'.",
), ),
sound: bool = typer.Option(False, "--sound", "-s", help="Record audio from default output."), sound: bool = typer.Option(
pause: bool = typer.Option(False, "--pause", "-p", help="Toggle pause/resume."), False, "--sound", "-s", help="Record audio from default output."),
clipboard: bool = typer.Option(False, "--clipboard", "-c", help="Copy the final recording path to clipboard."), pause: bool = typer.Option(
False, "--pause", "-p", help="Toggle pause/resume."),
clipboard: bool = typer.Option(
False, "--clipboard", "-c", help="Copy the final recording path to clipboard."),
): ):
"""Start or stop a screen recording with gpu-screen-recorder.""" """Start or stop a screen recording with gpu-screen-recorder."""
if pause: if pause:
+15 -107
View File
@@ -2,7 +2,6 @@ import typer
import json import json
import shutil import shutil
import os import os
import sys
import re import re
import subprocess import subprocess
@@ -16,61 +15,11 @@ from materialyoucolor.score.score import Score
from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
from materialyoucolor.hct.hct import Hct from materialyoucolor.hct.hct import Hct
from materialyoucolor.utils.color_utils import argb_from_rgb from materialyoucolor.utils.color_utils import argb_from_rgb
from materialyoucolor.utils.math_utils import ( from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double
difference_degrees,
rotation_direction,
sanitize_degrees_double,
)
app = typer.Typer() app = typer.Typer()
def _complete_scheme_name(incomplete):
schemes = [
"fruit-salad",
"expressive",
"monochrome",
"rainbow",
"tonal-spot",
"neutral",
"fidelity",
"content",
"vibrant",
]
return [s for s in schemes if incomplete in s]
def _complete_preset(incomplete):
results = []
for sid, meta in list_schemes().items():
for v in meta.variants:
preset = f"{sid}:{v.id}"
if incomplete in preset:
results.append((preset, f"{meta.name} - {v.name}"))
return results
def _complete_mode(incomplete):
return [m for m in ("dark", "light") if incomplete in m]
def _complete_accent(ctx, incomplete):
preset_val = ctx.params.get("preset")
if preset_val:
try:
p_scheme, p_variant = resolve_preset(preset_val)
for v in list_schemes()[p_scheme].variants:
if v.id == p_variant:
return [a for a in v.accents if incomplete in a]
except (ValueError, KeyError):
pass
all_accents = set()
for meta in list_schemes().values():
for v in meta.variants:
all_accents.update(v.accents)
return [a for a in sorted(all_accents) if incomplete in a]
@app.command() @app.command()
def list_presets( def list_presets(
json_format: bool = typer.Option(False, "--json", help="Output in JSON format"), json_format: bool = typer.Option(False, "--json", help="Output in JSON format"),
@@ -81,7 +30,7 @@ def list_presets(
for sid, meta in sorted(schemes.items()): for sid, meta in sorted(schemes.items()):
variants = {} variants = {}
for v in meta.variants: for v in meta.variants:
entry: dict[str, Any] = {"modes": sorted(v.modes)} entry = {"modes": sorted(v.modes)}
if v.accents: if v.accents:
entry["accents"] = sorted(v.accents) entry["accents"] = sorted(v.accents)
entry["default_accent"] = sorted(v.accents)[0] entry["default_accent"] = sorted(v.accents)[0]
@@ -106,35 +55,14 @@ def list_presets(
@app.command() @app.command()
def generate( def generate(
image_path: Optional[Path] = typer.Option( image_path: Optional[Path] = typer.Option(None, help="Path to source image. Required for image mode."),
None, help="Path to source image. Required for image mode."
),
scheme: Optional[str] = typer.Option( scheme: Optional[str] = typer.Option(
None, None, help="Color scheme algorithm to use for image mode. Ignored in preset mode."
help="Color scheme algorithm to use for image mode. Ignored in preset mode.",
autocompletion=_complete_scheme_name,
),
preset: Optional[str] = typer.Option(
None,
help="Name of a premade scheme in this format: <scheme>:<variant>",
autocompletion=_complete_preset,
),
mode: Optional[str] = typer.Option(
None,
help="Mode of the preset scheme (dark or light).",
autocompletion=_complete_mode,
),
accent: Optional[str] = typer.Option(
None,
help="Accent for schemes that support it (e.g. mauve).",
autocompletion=_complete_accent,
), ),
preset: Optional[str] = typer.Option(None, help="Name of a premade scheme in this format: <scheme>:<variant>"),
mode: Optional[str] = typer.Option(None, help="Mode of the preset scheme (dark or light)."),
accent: Optional[str] = typer.Option(None, help="Accent for schemes that support it (e.g. mauve)."),
): ):
if not any([image_path, scheme, preset, mode, accent]):
print(
"Hint: use --preset <scheme>:<variant> or --image-path <path>",
file=sys.stderr,
)
HOME = str(os.getenv("HOME")) HOME = str(os.getenv("HOME"))
OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json") OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json")
@@ -272,15 +200,11 @@ def generate(
def harmonize(from_hct: Hct, to_hct: Hct, tone_boost: float) -> Hct: def harmonize(from_hct: Hct, to_hct: Hct, tone_boost: float) -> Hct:
diff = difference_degrees(from_hct.hue, to_hct.hue) diff = difference_degrees(from_hct.hue, to_hct.hue)
rotation = min(diff * 0.8, 100) rotation = min(diff * 0.8, 100)
output_hue = sanitize_degrees_double( output_hue = sanitize_degrees_double(from_hct.hue + rotation * rotation_direction(from_hct.hue, to_hct.hue))
from_hct.hue + rotation * rotation_direction(from_hct.hue, to_hct.hue)
)
tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost))) tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost)))
return Hct.from_hct(output_hue, from_hct.chroma, tone) return Hct.from_hct(output_hue, from_hct.chroma, tone)
def terminal_palette( def terminal_palette(colors: dict[str, str], mode: str, variant: str) -> dict[str, str]:
colors: dict[str, str], mode: str, variant: str
) -> dict[str, str]:
light = mode.lower() == "light" light = mode.lower() == "light"
key_hex = ( key_hex = (
@@ -312,7 +236,7 @@ def generate(
image = Image.open(image_path) image = Image.open(image_path)
image = image.convert("RGB") image = image.convert("RGB")
image.thumbnail(size, Image.Resampling.NEAREST) image.thumbnail(size, Image.NEAREST)
thumbnail_file.parent.mkdir(parents=True, exist_ok=True) thumbnail_file.parent.mkdir(parents=True, exist_ok=True)
image.save(thumbnail_path, "JPEG") image.save(thumbnail_path, "JPEG")
@@ -344,15 +268,8 @@ def generate(
is_dark = "" is_dark = ""
with Image.open(image_path) as img: with Image.open(image_path) as img:
img.thumbnail((1, 1), Image.Resampling.LANCZOS) img.thumbnail((1, 1), Image.LANCZOS)
px = img.getpixel((0, 0)) hct = Hct.from_int(argb_from_rgb(*img.getpixel((0, 0))))
if isinstance(px, (int, float)):
r = g = b = int(px)
elif px is not None:
r, g, b = int(px[0]), int(px[1]), int(px[2])
else:
r = g = b = 0
hct = Hct.from_int(argb_from_rgb(r, g, b))
is_dark = "light" if hct.tone > 50 else "dark" is_dark = "light" if hct.tone > 50 else "dark"
return is_dark return is_dark
@@ -514,8 +431,6 @@ def generate(
raw = tpl_path.read_text(encoding="utf-8") raw = tpl_path.read_text(encoding="utf-8")
out_path, body = split_directive_and_body(raw) out_path, body = split_directive_and_body(raw)
if out_path is None:
continue
out_path.parent.mkdir(parents=True, exist_ok=True) out_path.parent.mkdir(parents=True, exist_ok=True)
@@ -569,30 +484,23 @@ def generate(
with CONFIG.open() as f: with CONFIG.open() as f:
config = json.load(f) config = json.load(f)
scheme_type = config["colors"].get("schemeType", "fruit-salad") scheme = scheme or config["colors"]["schemeType"]
scheme = scheme or scheme_type
assert isinstance(scheme, str)
config_mode = config["general"]["color"]["mode"] config_mode = config["general"]["color"]["mode"]
smart = bool(config["general"]["color"].get("smart", False)) smart = bool(config["general"]["color"].get("smart", False))
scheme_class = get_scheme_class(scheme) scheme_class = get_scheme_class(scheme)
p_variant = "default"
if preset: if preset:
p_scheme, p_variant = resolve_preset(preset) p_scheme, p_variant = resolve_preset(preset)
schemes = list_schemes() schemes = list_schemes()
if accent and p_scheme in schemes: if accent and p_scheme in schemes:
meta = schemes[p_scheme] meta = schemes[p_scheme]
var_accents = next( var_accents = next((v.accents for v in meta.variants if v.id == p_variant), ())
(v.accents for v in meta.variants if v.id == p_variant), ()
)
if accent not in var_accents: if accent not in var_accents:
available = ", ".join(var_accents) if var_accents else "none" available = ", ".join(var_accents) if var_accents else "none"
raise typer.BadParameter( raise typer.BadParameter(
f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}" f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}"
) )
palette_obj = get_palette( palette_obj = get_palette(p_scheme, p_variant, mode or config_mode, accent=accent)
p_scheme, p_variant, mode or config_mode, accent=accent
)
colors = palette_obj.colors colors = palette_obj.colors
effective_mode = palette_obj.mode effective_mode = palette_obj.mode
name = palette_obj.scheme name = palette_obj.scheme
-1
View File
@@ -51,7 +51,6 @@ def show():
result = subprocess.run(args + ["ipc"] + ["show"], capture_output=True) result = subprocess.run(args + ["ipc"] + ["show"], capture_output=True)
if result.returncode != 0: if result.returncode != 0:
raise click.ClickException(result.stderr.decode().strip()) raise click.ClickException(result.stderr.decode().strip())
sys.stdout.write(result.stdout.decode())
sys.stderr.write(result.stderr.decode()) sys.stderr.write(result.stderr.decode())
+2 -2
View File
@@ -34,9 +34,9 @@ def lockscreen(
return return
if size[0] < 3840 or size[1] < 2160: if size[0] < 3840 or size[1] < 2160:
img = img.resize((size[0] // 2, size[1] // 2), Image.Resampling.NEAREST) img = img.resize((size[0] // 2, size[1] // 2), Image.NEAREST)
else: else:
img = img.resize((size[0] // 4, size[1] // 4), Image.Resampling.NEAREST) img = img.resize((size[0] // 4, size[1] // 4), Image.NEAREST)
img = img.filter(ImageFilter.GaussianBlur(blur_amount)) img = img.filter(ImageFilter.GaussianBlur(blur_amount))
+2 -11
View File
@@ -6,20 +6,15 @@
//@ pragma Env QT_SCALE_FACTOR_ROUNDING_POLICY=Round //@ pragma Env QT_SCALE_FACTOR_ROUNDING_POLICY=Round
//@ pragma DropExpensiveFonts //@ pragma DropExpensiveFonts
import Quickshell import Quickshell
import Quickshell.Services.UPower import qs.Extensions
import qs.Modules import qs.Modules
import qs.Modules.Wallpaper import qs.Modules.Wallpaper
import qs.Modules.Lock import qs.Modules.Lock
import qs.Drawers import qs.Drawers
import qs.Helpers import qs.Helpers
import qs.Modules.Polkit import qs.Modules.Polkit
import qs.Daemons
ShellRoot { ShellRoot {
id: root
readonly property bool laptop: UPower.displayDevice.isLaptopBattery
settings.watchFiles: true settings.watchFiles: true
Windows { Windows {
@@ -45,10 +40,6 @@ ShellRoot {
Polkit { Polkit {
} }
LazyLoader { LoadExtensions {
activeAsync: root.laptop
component: Battery {
}
} }
} }