diff options
| author | Tim Hämisch <tim@thaemisch.net> | 2025-06-15 13:40:47 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-15 13:40:47 +0200 |
| commit | 68874082b4cfee63feaecc0640646ad0ba753da7 (patch) | |
| tree | 815a1113a4bd83373283253a35f0220a08a8cfe6 | |
| parent | launcher: use standard logout command (diff) | |
| parent | dashboard: fix uptime (diff) | |
| download | caelestia-shell-68874082b4cfee63feaecc0640646ad0ba753da7.tar.gz caelestia-shell-68874082b4cfee63feaecc0640646ad0ba753da7.tar.bz2 caelestia-shell-68874082b4cfee63feaecc0640646ad0ba753da7.zip | |
Merge branch 'main' into betteractions
65 files changed, 976 insertions, 447 deletions
@@ -31,14 +31,7 @@ https://github.com/user-attachments/assets/0840f496-575c-4ca6-83a8-87bb01a85c5f ## Installation -### Automated installation (recommended) - -Install [`caelestia-scripts`](https://github.com/caelestia-dots/scripts) and run `caelestia install shell`. - -### Manual installation - -Install all [dependencies](https://github.com/caelestia-dots/scripts/blob/main/install/shell.fish#L10), then -clone this repo into `$XDG_CONFIG_HOME/quickshell/caelestia` and run `qs -c caelestia`. +This is not implemented as of now. ## Usage diff --git a/assets/beat_detector.cpp b/assets/beat_detector.cpp new file mode 100755 index 0000000..4eb9b48 --- /dev/null +++ b/assets/beat_detector.cpp @@ -0,0 +1,568 @@ +#include <algorithm> +#include <pipewire/pipewire.h> +#include <spa/param/audio/format-utils.h> +#include <spa/param/props.h> +#include <aubio/aubio.h> +#include <memory> +#include <iostream> +#include <fstream> +#include <csignal> +#include <atomic> +#include <vector> +#include <cstring> +#include <chrono> +#include <iomanip> +#include <sstream> +#include <thread> +#include <cmath> + +class EnhancedBeatDetector { +private: + static constexpr uint32_t SAMPLE_RATE = 44100; + static constexpr uint32_t CHANNELS = 1; + + // PipeWire objects + pw_main_loop* main_loop_; + pw_context* context_; + pw_core* core_; + pw_stream* stream_; + + // Aubio objects + std::unique_ptr<aubio_tempo_t, decltype(&del_aubio_tempo)> tempo_; + std::unique_ptr<fvec_t, decltype(&del_fvec)> input_buffer_; + std::unique_ptr<fvec_t, decltype(&del_fvec)> output_buffer_; + + // Additional aubio objects for enhanced features + std::unique_ptr<aubio_onset_t, decltype(&del_aubio_onset)> onset_; + std::unique_ptr<aubio_pitch_t, decltype(&del_aubio_pitch)> pitch_; + std::unique_ptr<fvec_t, decltype(&del_fvec)> pitch_buffer_; + + const uint32_t buf_size_; + const uint32_t fft_size_; + + static std::atomic<bool> should_quit_; + static EnhancedBeatDetector* instance_; + + // Enhanced features + std::ofstream log_file_; + bool enable_logging_; + bool enable_performance_stats_; + bool enable_pitch_detection_; + bool enable_visual_feedback_; + + // Performance tracking + std::chrono::high_resolution_clock::time_point last_process_time_; + std::vector<double> process_times_; + uint64_t total_beats_; + uint64_t total_onsets_; + std::chrono::steady_clock::time_point start_time_; + + // Beat analysis + std::vector<float> recent_bpms_; + static constexpr size_t BPM_HISTORY_SIZE = 10; + float last_bpm_; + std::chrono::steady_clock::time_point last_beat_time_; + + // Useless Visual feedback + std::string generate_beat_visual(float bpm, bool is_beat) { + if (!enable_visual_feedback_) return ""; + + std::stringstream ss; + if (is_beat) { + // Useless Animated beat indicator based on BPM intensity + int intensity = static_cast<int>(std::min(bpm / 20.0f, 10.0f)); + ss << "\r "; + for (int i = 0; i < intensity; ++i) ss << "█"; + for (int i = intensity; i < 10; ++i) ss << "░"; + ss << " BPM: " << std::fixed << std::setprecision(1) << bpm; + ss << " | Avg: " << get_average_bpm(); + } + return ss.str(); + } + +public: + explicit EnhancedBeatDetector(uint32_t buf_size = 512, + bool enable_logging = true, + bool enable_performance_stats = true, + bool enable_pitch_detection = false, + bool enable_visual_feedback = true) + : main_loop_(nullptr) + , context_(nullptr) + , core_(nullptr) + , stream_(nullptr) + , tempo_(nullptr, &del_aubio_tempo) + , input_buffer_(nullptr, &del_fvec) + , output_buffer_(nullptr, &del_fvec) + , onset_(nullptr, &del_aubio_onset) + , pitch_(nullptr, &del_aubio_pitch) + , pitch_buffer_(nullptr, &del_fvec) + , buf_size_(buf_size) + , fft_size_(buf_size * 2) + , enable_logging_(enable_logging) + , enable_performance_stats_(enable_performance_stats) + , enable_pitch_detection_(enable_pitch_detection) + , enable_visual_feedback_(enable_visual_feedback) + , total_beats_(0) + , total_onsets_(0) + , last_bpm_(0.0f) + { + instance_ = this; + recent_bpms_.reserve(BPM_HISTORY_SIZE); + if (enable_performance_stats_) { + process_times_.reserve(1000); // Reserve space for performance data + } + initialize(); + } + + ~EnhancedBeatDetector() { + print_final_stats(); + cleanup(); + instance_ = nullptr; + } + + // Delete copy constructor and assignment operator + EnhancedBeatDetector(const EnhancedBeatDetector&) = delete; + EnhancedBeatDetector& operator=(const EnhancedBeatDetector&) = delete; + + bool initialize() { + start_time_ = std::chrono::steady_clock::now(); + + // Useless Initialize logging (actually useful) + if (enable_logging_) { + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + std::stringstream filename; + filename << "beat_log_" << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S") << ".txt"; + log_file_.open(filename.str()); + if (log_file_.is_open()) { + log_file_ << "# Beat Detection Log - " << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S") << "\n"; + log_file_ << "# Timestamp,BPM,Onset,Pitch(Hz),ProcessTime(ms)\n"; + std::cout << " Logging to: " << filename.str() << std::endl; + } + } + + // Initialize PipeWire + pw_init(nullptr, nullptr); + + main_loop_ = pw_main_loop_new(nullptr); + if (!main_loop_) { + std::cerr << " Failed to create main loop" << std::endl; + return false; + } + + context_ = pw_context_new(pw_main_loop_get_loop(main_loop_), nullptr, 0); + if (!context_) { + std::cerr << " Failed to create context" << std::endl; + return false; + } + + core_ = pw_context_connect(context_, nullptr, 0); + if (!core_) { + std::cerr << " Failed to connect to PipeWire" << std::endl; + return false; + } + + // Initialize Aubio objects + tempo_.reset(new_aubio_tempo("default", fft_size_, buf_size_, SAMPLE_RATE)); + if (!tempo_) { + std::cerr << " Failed to create aubio tempo detector" << std::endl; + return false; + } + + input_buffer_.reset(new_fvec(buf_size_)); + output_buffer_.reset(new_fvec(1)); + + if (!input_buffer_ || !output_buffer_) { + std::cerr << " Failed to create aubio buffers" << std::endl; + return false; + } + + // Initialize onset detection + onset_.reset(new_aubio_onset("default", fft_size_, buf_size_, SAMPLE_RATE)); + if (!onset_) { + std::cerr << " Failed to create aubio onset detector" << std::endl; + return false; + } + + // Initialize pitch detection if enabled + if (enable_pitch_detection_) { + pitch_.reset(new_aubio_pitch("default", fft_size_, buf_size_, SAMPLE_RATE)); + pitch_buffer_.reset(new_fvec(1)); + if (!pitch_ || !pitch_buffer_) { + std::cerr << " Failed to create aubio pitch detector" << std::endl; + return false; + } + aubio_pitch_set_unit(pitch_.get(), "Hz"); + } + + return setup_stream(); + } + + void run() { + if (!main_loop_) return; + + print_startup_info(); + pw_main_loop_run(main_loop_); + } + + void stop() { + should_quit_ = true; + if (main_loop_) { + pw_main_loop_quit(main_loop_); + } + } + + static void signal_handler(int sig) { + if (instance_) { + std::cout << "\n Received signal " << sig << ", stopping gracefullllly..." << std::endl; + instance_->stop(); + } + } + +private: + void print_startup_info() { + std::cout << "\n Beat Detector Started!" << std::endl; + std::cout << " Buffer size: " << buf_size_ << " samples" << std::endl; + std::cout << " Sample rate: " << SAMPLE_RATE << " Hz" << std::endl; + std::cout << " Features enabled:" << std::endl; + std::cout << " Logging: " << (enable_logging_ ? " " : "") << std::endl; + std::cout << " Performance stats: " << (enable_performance_stats_ ? " " : "") << std::endl; + std::cout << " Pitch detection: " << (enable_pitch_detection_ ? " " : "") << std::endl; + std::cout << "\n Listening for beats... Press Ctrl+C to stop.\n" << std::endl; + } + + void print_final_stats() { + if (!enable_performance_stats_) return; + + auto end_time = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::seconds>(end_time - start_time_); + + std::cout << "\n Final Statistics:" << std::endl; + std::cout << " Total runtime: " << duration.count() << " seconds" << std::endl; + std::cout << " Total beats detected: " << total_beats_ << std::endl; + std::cout << " Total onsets detected: " << total_onsets_ << std::endl; + + if (!process_times_.empty()) { + double avg_time = 0; + for (double t : process_times_) avg_time += t; + avg_time /= process_times_.size(); + + auto max_time = *std::max_element(process_times_.begin(), process_times_.end()); + auto min_time = *std::min_element(process_times_.begin(), process_times_.end()); + + std::cout << " ⚡ Average processing time: " << std::fixed << std::setprecision(3) + << avg_time << " ms" << std::endl; + std::cout << " 📈 Max processing time: " << max_time << " ms" << std::endl; + std::cout << " 📉 Min processing time: " << min_time << " ms" << std::endl; + } + + if (!recent_bpms_.empty()) { + std::cout << " Final average BPM: " << get_average_bpm() << std::endl; + } + } + + float get_average_bpm() const { + if (recent_bpms_.empty()) return 0.0f; + float sum = 0; + for (float bpm : recent_bpms_) sum += bpm; + return sum / recent_bpms_.size(); + } + + bool setup_stream() { + // Stream events + static const pw_stream_events stream_events = { + .version = PW_VERSION_STREAM_EVENTS, + .destroy = nullptr, + .state_changed = on_state_changed, + .control_info = nullptr, + .io_changed = nullptr, + .param_changed = nullptr, + .add_buffer = nullptr, + .remove_buffer = nullptr, + .process = on_process, + .drained = nullptr, + .command = nullptr, + .trigger_done = nullptr, + }; + + stream_ = pw_stream_new_simple( + pw_main_loop_get_loop(main_loop_), + "enhanced-beat-detector", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Music", + nullptr + ), + &stream_events, + this + ); + + if (!stream_) { + std::cerr << " Failed to create stream" << std::endl; + return false; + } + + // Audio format parameters + uint8_t buffer[1024]; + spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + struct spa_audio_info_raw audio_info = {}; + audio_info.format = SPA_AUDIO_FORMAT_F32_LE; + audio_info.channels = CHANNELS; + audio_info.rate = SAMPLE_RATE; + audio_info.flags = 0; + + const spa_pod* params[1]; + params[0] = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_EnumFormat, &audio_info); + + if (pw_stream_connect(stream_, + PW_DIRECTION_INPUT, + PW_ID_ANY, + static_cast<pw_stream_flags>( + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS), + params, 1) < 0) { + std::cerr << " Failed to connect stream" << std::endl; + return false; + } + + return true; + } + + static void on_state_changed(void* userdata, enum pw_stream_state /* old_state */, + enum pw_stream_state state, const char* error) { + auto* detector = static_cast<EnhancedBeatDetector*>(userdata); + + const char* state_emoji = " "; + switch (state) { + case PW_STREAM_STATE_CONNECTING: state_emoji = " "; break; + case PW_STREAM_STATE_PAUSED: state_emoji = ""; break; + case PW_STREAM_STATE_STREAMING: state_emoji = " "; break; + case PW_STREAM_STATE_ERROR: state_emoji = ""; break; + case PW_STREAM_STATE_UNCONNECTED: state_emoji = " "; break; + } + + std::cout << state_emoji << " Stream state: " << pw_stream_state_as_string(state) << std::endl; + + if (state == PW_STREAM_STATE_ERROR) { + std::cerr << " Stream error: " << (error ? error : "unknown") << std::endl; + detector->stop(); + } + } + + static void on_process(void* userdata) { + auto* detector = static_cast<EnhancedBeatDetector*>(userdata); + detector->process_audio(); + } + + void process_audio() { + if (should_quit_) return; + + auto process_start = std::chrono::high_resolution_clock::now(); + + pw_buffer* buffer = pw_stream_dequeue_buffer(stream_); + if (!buffer) return; + + spa_buffer* spa_buf = buffer->buffer; + if (!spa_buf->datas[0].data) { + pw_stream_queue_buffer(stream_, buffer); + return; + } + + const float* audio_data = static_cast<const float*>(spa_buf->datas[0].data); + const uint32_t n_samples = spa_buf->datas[0].chunk->size / sizeof(float); + + // Process in chunks + for (uint32_t offset = 0; offset + buf_size_ <= n_samples; offset += buf_size_) { + std::memcpy(input_buffer_->data, audio_data + offset, buf_size_ * sizeof(float)); + + // Beat detection + aubio_tempo_do(tempo_.get(), input_buffer_.get(), output_buffer_.get()); + bool is_beat = output_buffer_->data[0] != 0.0f; + + // Onset detection + aubio_onset_do(onset_.get(), input_buffer_.get(), output_buffer_.get()); + bool is_onset = output_buffer_->data[0] != 0.0f; + + float pitch_hz = 0.0f; + if (enable_pitch_detection_) { + aubio_pitch_do(pitch_.get(), input_buffer_.get(), pitch_buffer_.get()); + pitch_hz = pitch_buffer_->data[0]; + } + + if (is_beat) { + total_beats_++; + last_bpm_ = aubio_tempo_get_bpm(tempo_.get()); + last_beat_time_ = std::chrono::steady_clock::now(); + + // Update BPM history + recent_bpms_.push_back(last_bpm_); + if (recent_bpms_.size() > BPM_HISTORY_SIZE) { + recent_bpms_.erase(recent_bpms_.begin()); + } + + // Visual feedback + if (enable_visual_feedback_) { + std::cout << generate_beat_visual(last_bpm_, true) << std::flush; + } else { + std::cout << " BPM: " << std::fixed << std::setprecision(1) << last_bpm_ << std::endl; + } + } + + if (is_onset) { + total_onsets_++; + } + + // Logging + if (enable_logging_ && log_file_.is_open() && (is_beat || is_onset)) { + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast<std::chrono::milliseconds>( + now.time_since_epoch()) % 1000; + + log_file_ << std::put_time(std::localtime(&time_t), "%H:%M:%S") + << "." << std::setfill('0') << std::setw(3) << ms.count() << "," + << (is_beat ? last_bpm_ : 0) << "," + << (is_onset ? 1 : 0) << "," + << pitch_hz << ","; + } + } + + pw_stream_queue_buffer(stream_, buffer); + + // Performance tracking + if (enable_performance_stats_) { + auto process_end = std::chrono::high_resolution_clock::now(); + auto process_time = std::chrono::duration<double, std::milli>(process_end - process_start).count(); + + if (log_file_.is_open() && (total_beats_ > 0 || total_onsets_ > 0)) { + log_file_ << std::fixed << std::setprecision(3) << process_time << "\n"; + } + + if (process_times_.size() < 1000) { + process_times_.push_back(process_time); + } + } + } + + void cleanup() { + if (log_file_.is_open()) { + log_file_.close(); + } + + if (stream_) { + pw_stream_destroy(stream_); + stream_ = nullptr; + } + + if (core_) { + pw_core_disconnect(core_); + core_ = nullptr; + } + + if (context_) { + pw_context_destroy(context_); + context_ = nullptr; + } + + if (main_loop_) { + pw_main_loop_destroy(main_loop_); + main_loop_ = nullptr; + } + + tempo_.reset(); + input_buffer_.reset(); + output_buffer_.reset(); + onset_.reset(); + pitch_.reset(); + pitch_buffer_.reset(); + + pw_deinit(); + + std::cout << "\n Cleanup complete - All resources freed!" << std::endl; + } +}; + +// Static member definitions +std::atomic<bool> EnhancedBeatDetector::should_quit_{false}; +EnhancedBeatDetector* EnhancedBeatDetector::instance_{nullptr}; + +void print_usage() { + std::cout << " Beat Detector Usage:" << std::endl; + std::cout << " ./beat_detector [buffer_size] [options]" << std::endl; + std::cout << "\nOptions:" << std::endl; + std::cout << " --no-log Disable logging to file" << std::endl; + std::cout << " --no-stats Disable performance statistics" << std::endl; + std::cout << " --pitch Enable pitch detection" << std::endl; + std::cout << " --no-visual Disable visual feedback" << std::endl; + std::cout << " --help Show this help" << std::endl; + std::cout << "\nExamples:" << std::endl; + std::cout << " ./beat_detector # Default settings" << std::endl; + std::cout << " ./beat_detector 256 --pitch # Smaller buffer with pitch detection" << std::endl; + std::cout << " ./beat_detector 1024 --no-visual # Larger buffer, no visual feedback" << std::endl; +} + +int main(int argc, char* argv[]) { + // Parse command line arguments + uint32_t buffer_size = 512; + bool enable_logging = true; + bool enable_performance_stats = true; + bool enable_pitch_detection = false; + bool enable_visual_feedback = true; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg == "--help" || arg == "-h") { + print_usage(); + return 0; + } else if (arg == "--no-log") { + enable_logging = false; + } else if (arg == "--no-stats") { + enable_performance_stats = false; + } else if (arg == "--pitch") { + enable_pitch_detection = true; + } else if (arg == "--no-visual") { + enable_visual_feedback = false; + } else if (arg[0] != '-') { + // Assume it's a buffer size + try { + buffer_size = std::stoul(arg); + if (buffer_size < 64 || buffer_size > 8192) { + std::cerr << " Buffer size must be between 64 and 8192" << std::endl; + return 1; + } + } catch (...) { + std::cerr << " Invalid buffer size: " << arg << std::endl; + return 1; + } + } else { + std::cerr << " Unknown option: " << arg << std::endl; + print_usage(); + return 1; + } + } + + // Set up signal handling + std::signal(SIGINT, EnhancedBeatDetector::signal_handler); + std::signal(SIGTERM, EnhancedBeatDetector::signal_handler); + + try { + EnhancedBeatDetector detector(buffer_size, enable_logging, enable_performance_stats, + enable_pitch_detection, enable_visual_feedback); + detector.run(); + } catch (const std::exception& e) { + std::cerr << " Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} + +/* + * Compilation command: + * g++ -std=c++17 -O3 -Wall -Wextra -I/usr/include/pipewire-0.3 -I/usr/include/spa-0.2 -I/usr/include/aubio \ + * -o beat_detector beat_detector.cpp -lpipewire-0.3 -laubio -pthread + */ diff --git a/assets/realtime-beat-detector.py b/assets/realtime-beat-detector.py deleted file mode 100755 index 0f4f54a..0000000 --- a/assets/realtime-beat-detector.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python - -import pyaudio -import numpy as np -import aubio -import signal -import sys - -from typing import List, Tuple - - -class BeatDetector: - def __init__(self, buf_size: int): - self.buf_size: int = buf_size - - # Set up pyaudio and aubio beat detector - self.audio: pyaudio.PyAudio = pyaudio.PyAudio() - samplerate: int = 44100 - - self.stream: pyaudio.Stream = self.audio.open( - format=pyaudio.paFloat32, - channels=1, - rate=samplerate, - input=True, - frames_per_buffer=self.buf_size, - stream_callback=self._pyaudio_callback - ) - - fft_size: int = self.buf_size * 2 - - # tempo detection - self.tempo: aubio.tempo = aubio.tempo("default", fft_size, self.buf_size, samplerate) - - # this one is called every time enough audio data (buf_size) has been read by the stream - def _pyaudio_callback(self, in_data, frame_count, time_info, status): - # Interpret a buffer as a 1-dimensional array (aubio do not work with raw data) - audio_data = np.frombuffer(in_data, dtype=np.float32) - # true if beat present - beat = self.tempo(audio_data) - - # if beat detected, calculate BPM and send to OSC - if beat[0]: - print(self.tempo.get_bpm(), flush=True) - - return None, pyaudio.paContinue # Tell pyAudio to continue - - def __del__(self): - self.stream.close() - self.audio.terminate() - print('--- Stopped ---') - - -# main -def main(): - bd = BeatDetector(512) - - # capture ctrl+c to stop gracefully process - def signal_handler(none, frame): - bd.stream.stop_stream() - bd.stream.close() - bd.audio.terminate() - print(' ===> Ctrl + C') - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - - # Audio processing happens in separate thread, so put this thread to sleep - signal.pause() - - -# main run -if __name__ == "__main__": - main() diff --git a/config/BarConfig.qml b/config/BarConfig.qml index 21f9314..31577c4 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -1,22 +1,14 @@ -pragma Singleton +import Quickshell.Io -import Quickshell -import QtQuick - -Singleton { - id: root - - readonly property Sizes sizes: Sizes {} - readonly property Workspaces workspaces: Workspaces {} - - component Sizes: QtObject { +JsonObject { + property JsonObject sizes: JsonObject { property int innerHeight: 30 property int windowPreviewSize: 400 property int trayMenuWidth: 300 property int batteryWidth: 250 } - component Workspaces: QtObject { + property JsonObject workspaces: JsonObject { property int shown: 5 property bool rounded: true property bool activeIndicator: true diff --git a/config/BorderConfig.qml b/config/BorderConfig.qml index a69cf59..7e95054 100644 --- a/config/BorderConfig.qml +++ b/config/BorderConfig.qml @@ -1,13 +1,9 @@ -pragma Singleton - import "root:/services" -import Quickshell +import Quickshell.Io import QtQuick -Singleton { - id: root - - readonly property color colour: Colours.palette.m3surface - readonly property int thickness: Appearance.padding.normal - readonly property int rounding: Appearance.rounding.large +JsonObject { + property color colour: Colours.palette.m3surface + property int thickness: Appearance.padding.normal + property int rounding: Appearance.rounding.large } diff --git a/config/Config.qml b/config/Config.qml new file mode 100644 index 0000000..2e1e48b --- /dev/null +++ b/config/Config.qml @@ -0,0 +1,36 @@ +pragma Singleton + +import "root:/utils" +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property alias bar: adapter.bar + property alias border: adapter.border + property alias dashboard: adapter.dashboard + property alias launcher: adapter.launcher + property alias notifs: adapter.notifs + property alias osd: adapter.osd + property alias session: adapter.session + + FileView { + path: `${Paths.config}/shell.json` + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + + JsonAdapter { + id: adapter + + property JsonObject bar: BarConfig {} + property JsonObject border: BorderConfig {} + property JsonObject dashboard: DashboardConfig {} + property JsonObject launcher: LauncherConfig {} + property JsonObject notifs: NotifsConfig {} + property JsonObject osd: OsdConfig {} + property JsonObject session: SessionConfig {} + } + } +} diff --git a/config/DashboardConfig.qml b/config/DashboardConfig.qml index 3c32e07..269d4b4 100644 --- a/config/DashboardConfig.qml +++ b/config/DashboardConfig.qml @@ -1,14 +1,10 @@ -pragma Singleton +import Quickshell.Io -import Quickshell -import QtQuick +JsonObject { + property int mediaUpdateInterval: 500 + property int visualiserBars: 45 -Singleton { - readonly property int mediaUpdateInterval: 500 - readonly property int visualiserBars: 45 - readonly property Sizes sizes: Sizes {} - - component Sizes: QtObject { + property JsonObject sizes: JsonObject { readonly property int tabIndicatorHeight: 3 readonly property int tabIndicatorSpacing: 5 readonly property int infoWidth: 200 diff --git a/config/LauncherConfig.qml b/config/LauncherConfig.qml index a3c154f..1757c7e 100644 --- a/config/LauncherConfig.qml +++ b/config/LauncherConfig.qml @@ -1,19 +1,15 @@ -pragma Singleton +import Quickshell.Io -import Quickshell -import QtQuick +JsonObject { + property int maxShown: 8 + property int maxWallpapers: 9 // Warning: even numbers look bad + property string actionPrefix: ">" + property bool allowDangerousActions: false // Allow actions that can change the system state, like shutdown, reboot and logout -Singleton { - readonly property int maxShown: 8 - readonly property int maxWallpapers: 9 // Warning: even numbers look bad - readonly property string actionPrefix: ">" - readonly property Sizes sizes: Sizes {} - readonly property bool allowDangerousActions: false // Allow actions that can change the system state, like shutdown, reboot and logout - - component Sizes: QtObject { - readonly property int itemWidth: 600 - readonly property int itemHeight: 57 - readonly property int wallpaperWidth: 280 - readonly property int wallpaperHeight: 200 + property JsonObject sizes: JsonObject { + property int itemWidth: 600 + property int itemHeight: 57 + property int wallpaperWidth: 280 + property int wallpaperHeight: 200 } } diff --git a/config/NotifsConfig.qml b/config/NotifsConfig.qml index 34b9226..f84def2 100644 --- a/config/NotifsConfig.qml +++ b/config/NotifsConfig.qml @@ -1,19 +1,15 @@ -pragma Singleton +import Quickshell.Io -import Quickshell -import QtQuick +JsonObject { + property bool expire: false + property int defaultExpireTimeout: 3000 + property real clearThreshold: 0.3 + property int expandThreshold: 20 + property bool actionOnClick: false -Singleton { - readonly property bool expire: false - readonly property int defaultExpireTimeout: 3000 - readonly property real clearThreshold: 0.3 - readonly property int expandThreshold: 20 - readonly property bool actionOnClick: false - readonly property Sizes sizes: Sizes {} - - component Sizes: QtObject { - readonly property int width: 400 - readonly property int image: 41 - readonly property int badge: 20 + property JsonObject sizes: JsonObject { + property int width: 400 + property int image: 41 + property int badge: 20 } } diff --git a/config/OsdConfig.qml b/config/OsdConfig.qml index 467d8e7..7275e22 100644 --- a/config/OsdConfig.qml +++ b/config/OsdConfig.qml @@ -1,14 +1,10 @@ -pragma Singleton +import Quickshell.Io -import Quickshell -import QtQuick +JsonObject { + property int hideDelay: 2000 -Singleton { - readonly property int hideDelay: 2000 - readonly property Sizes sizes: Sizes {} - - component Sizes: QtObject { - readonly property int sliderWidth: 30 - readonly property int sliderHeight: 150 + property JsonObject sizes: JsonObject { + property int sliderWidth: 30 + property int sliderHeight: 150 } } diff --git a/config/SessionConfig.qml b/config/SessionConfig.qml index c72d2f1..0915469 100644 --- a/config/SessionConfig.qml +++ b/config/SessionConfig.qml @@ -1,13 +1,9 @@ -pragma Singleton +import Quickshell.Io -import Quickshell -import QtQuick +JsonObject { + property int dragThreshold: 30 -Singleton { - readonly property int dragThreshold: 30 - readonly property Sizes sizes: Sizes {} - - component Sizes: QtObject { - readonly property int button: 80 + property JsonObject sizes: JsonObject { + property int button: 80 } } diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 2e8ab89..1d0db8d 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -63,7 +63,7 @@ Item { anchors.bottom: parent.bottom anchors.left: parent.left - implicitWidth: child.implicitWidth + BorderConfig.thickness * 2 + implicitWidth: child.implicitWidth + Config.border.thickness * 2 Item { id: child @@ -97,8 +97,8 @@ Item { MouseArea { anchors.fill: parent - anchors.leftMargin: -BorderConfig.thickness - anchors.rightMargin: -BorderConfig.thickness + anchors.leftMargin: -Config.border.thickness + anchors.rightMargin: -Config.border.thickness onWheel: event => { const activeWs = Hyprland.activeClient?.workspace?.name; diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index 3f3ca62..6807397 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -20,7 +20,7 @@ StyledRect { property real offset: Math.min(leading, trailing) property real size: { const s = Math.abs(leading - trailing) + currentSize; - if (BarConfig.workspaces.activeTrail && lastWs > currentWsIdx) + if (Config.bar.workspaces.activeTrail && lastWs > currentWsIdx) return Math.min(getWsY(lastWs) + (workspaces[lastWs]?.size ?? 0) - offset, s); return s; } @@ -43,9 +43,9 @@ StyledRect { clip: true x: 1 y: offset + 1 - implicitWidth: BarConfig.sizes.innerHeight - 2 + implicitWidth: Config.bar.sizes.innerHeight - 2 implicitHeight: size - 2 - radius: BarConfig.workspaces.rounded ? Appearance.rounding.full : 0 + radius: Config.bar.workspaces.rounded ? Appearance.rounding.full : 0 color: Colours.palette.m3primary StyledRect { @@ -72,13 +72,13 @@ StyledRect { } Behavior on leading { - enabled: BarConfig.workspaces.activeTrail + enabled: Config.bar.workspaces.activeTrail Anim {} } Behavior on trailing { - enabled: BarConfig.workspaces.activeTrail + enabled: Config.bar.workspaces.activeTrail Anim { duration: Appearance.anim.durations.normal * 2 @@ -86,19 +86,19 @@ StyledRect { } Behavior on currentSize { - enabled: BarConfig.workspaces.activeTrail + enabled: Config.bar.workspaces.activeTrail Anim {} } Behavior on offset { - enabled: !BarConfig.workspaces.activeTrail + enabled: !Config.bar.workspaces.activeTrail Anim {} } Behavior on size { - enabled: !BarConfig.workspaces.activeTrail + enabled: !Config.bar.workspaces.activeTrail Anim {} } diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index 4fba2f4..c5ed7cc 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -18,7 +18,7 @@ Item { onOccupiedChanged: { let count = 0; const start = groupOffset; - const end = start + BarConfig.workspaces.shown; + const end = start + Config.bar.workspaces.shown; for (const [ws, occ] of Object.entries(occupied)) { if (ws > start && ws <= end && occ) { if (!occupied[ws - 1]) { @@ -52,11 +52,11 @@ Item { readonly property Workspace end: root.workspaces[modelData.end - 1 - root.groupOffset] ?? null color: Colours.alpha(Colours.palette.m3surfaceContainerHigh, true) - radius: BarConfig.workspaces.rounded ? Appearance.rounding.full : 0 + radius: Config.bar.workspaces.rounded ? Appearance.rounding.full : 0 x: start?.x ?? 0 y: start?.y ?? 0 - implicitWidth: BarConfig.sizes.innerHeight + implicitWidth: Config.bar.sizes.innerHeight implicitHeight: end?.y + end?.height - start?.y anchors.horizontalCenter: parent.horizontalCenter diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index fa5fe62..6cf5c40 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -19,7 +19,7 @@ Item { readonly property int ws: groupOffset + index + 1 readonly property bool isOccupied: occupied[ws] ?? false - readonly property bool hasWindows: isOccupied && BarConfig.workspaces.showWindows + readonly property bool hasWindows: isOccupied && Config.bar.workspaces.showWindows Layout.preferredWidth: childrenRect.width Layout.preferredHeight: size @@ -27,24 +27,24 @@ Item { StyledText { id: indicator - readonly property string label: BarConfig.workspaces.label || root.ws - readonly property string occupiedLabel: BarConfig.workspaces.occupiedLabel || label - readonly property string activeLabel: BarConfig.workspaces.activeLabel || (root.isOccupied ? occupiedLabel : label) + readonly property string label: Config.bar.workspaces.label || root.ws + readonly property string occupiedLabel: Config.bar.workspaces.occupiedLabel || label + readonly property string activeLabel: Config.bar.workspaces.activeLabel || (root.isOccupied ? occupiedLabel : label) animate: true text: Hyprland.activeWsId === root.ws ? activeLabel : root.isOccupied ? occupiedLabel : label - color: BarConfig.workspaces.occupiedBg || root.isOccupied || Hyprland.activeWsId === root.ws ? Colours.palette.m3onSurface : Colours.palette.m3outlineVariant + color: Config.bar.workspaces.occupiedBg || root.isOccupied || Hyprland.activeWsId === root.ws ? Colours.palette.m3onSurface : Colours.palette.m3outlineVariant horizontalAlignment: StyledText.AlignHCenter verticalAlignment: StyledText.AlignVCenter - width: BarConfig.sizes.innerHeight - height: BarConfig.sizes.innerHeight + width: Config.bar.sizes.innerHeight + height: Config.bar.sizes.innerHeight } Loader { id: windows - active: BarConfig.workspaces.showWindows + active: Config.bar.workspaces.showWindows asynchronous: true anchors.horizontalCenter: indicator.horizontalCenter diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index 4f4c75b..9264762 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -14,7 +14,7 @@ Item { acc[curr.id] = curr.lastIpcObject.windows > 0; return acc; }, {}) - readonly property int groupOffset: Math.floor((Hyprland.activeWsId - 1) / BarConfig.workspaces.shown) * BarConfig.workspaces.shown + readonly property int groupOffset: Math.floor((Hyprland.activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown implicitWidth: layout.implicitWidth implicitHeight: layout.implicitHeight @@ -27,7 +27,7 @@ Item { layer.smooth: true Repeater { - model: BarConfig.workspaces.shown + model: Config.bar.workspaces.shown Workspace { occupied: root.occupied @@ -37,7 +37,7 @@ Item { } Loader { - active: BarConfig.workspaces.occupiedBg + active: Config.bar.workspaces.occupiedBg asynchronous: true z: -1 @@ -51,7 +51,7 @@ Item { } Loader { - active: BarConfig.workspaces.activeIndicator + active: Config.bar.workspaces.activeIndicator asynchronous: true sourceComponent: ActiveIndicator { diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index f3a5269..03130f1 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -61,8 +61,8 @@ Item { captureSource: ToplevelManager.toplevels.values.find(t => t.title === Hyprland.activeClient?.title) ?? null live: visible - constraintSize.width: BarConfig.sizes.windowPreviewSize - constraintSize.height: BarConfig.sizes.windowPreviewSize + constraintSize.width: Config.bar.sizes.windowPreviewSize + constraintSize.height: Config.bar.sizes.windowPreviewSize } } } diff --git a/modules/bar/popouts/Background.qml b/modules/bar/popouts/Background.qml index a56b0b7..c099118 100644 --- a/modules/bar/popouts/Background.qml +++ b/modules/bar/popouts/Background.qml @@ -8,13 +8,13 @@ ShapePath { required property Wrapper wrapper required property bool invertBottomRounding - readonly property real rounding: BorderConfig.rounding + readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.width < rounding * 2 readonly property real roundingX: flatten ? wrapper.width / 2 : rounding property real ibr: invertBottomRounding ? -1 : 1 strokeWidth: -1 - fillColor: BorderConfig.colour + fillColor: Config.border.colour PathArc { relativeX: root.roundingX diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 44e51ce..ebd0fc8 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -10,7 +10,7 @@ Column { id: root spacing: Appearance.spacing.normal - width: BarConfig.sizes.batteryWidth + width: Config.bar.sizes.batteryWidth StyledText { text: UPower.displayDevice.isLaptopBattery ? qsTr("Remaining: %1%").arg(Math.round(UPower.displayDevice.percentage * 100)) : qsTr("No battery detected") diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index d69bf38..433de7f 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -80,7 +80,7 @@ StackView { required property QsMenuEntry modelData - implicitWidth: BarConfig.sizes.trayMenuWidth + implicitWidth: Config.bar.sizes.trayMenuWidth implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight radius: Appearance.rounding.full @@ -153,7 +153,7 @@ StackView { font.family: label.font.family elide: Text.ElideRight - elideWidth: BarConfig.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0) + elideWidth: Config.bar.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0) } Loader { diff --git a/modules/dashboard/Background.qml b/modules/dashboard/Background.qml index ebc60c8..876311e 100644 --- a/modules/dashboard/Background.qml +++ b/modules/dashboard/Background.qml @@ -7,12 +7,12 @@ ShapePath { id: root required property Wrapper wrapper - readonly property real rounding: BorderConfig.rounding + readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding strokeWidth: -1 - fillColor: BorderConfig.colour + fillColor: Config.border.colour PathArc { relativeX: root.rounding diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index eaf4344..22132af 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -26,7 +26,7 @@ GridLayout { Rect { Layout.row: 0 Layout.columnSpan: 2 - Layout.preferredWidth: DashboardConfig.sizes.weatherWidth + Layout.preferredWidth: Config.dashboard.sizes.weatherWidth Layout.fillHeight: true Weather {} diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 43ef597..2d004d5 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -28,8 +28,8 @@ Item { return `${Math.floor(length / 60)}:${Math.floor(length % 60).toString().padStart(2, "0")}`; } - implicitWidth: cover.implicitWidth + DashboardConfig.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2 - implicitHeight: Math.max(cover.implicitHeight + DashboardConfig.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 + implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2 + implicitHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 Behavior on playerProgress { NumberAnimation { @@ -41,7 +41,7 @@ Item { Timer { running: root.shouldUpdate && (Players.active?.isPlaying ?? false) - interval: DashboardConfig.mediaUpdateInterval + interval: Config.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true onTriggered: Players.active?.positionChanged() @@ -66,7 +66,7 @@ Item { property color colour: Colours.palette.m3primary anchors.fill: cover - anchors.margins: -DashboardConfig.sizes.mediaVisualiserSize + anchors.margins: -Config.dashboard.sizes.mediaVisualiserSize onColourChanged: requestPaint() @@ -81,7 +81,7 @@ Item { ctx.lineWidth = 360 / len - Appearance.spacing.small / 4; ctx.lineCap = "round"; - const size = DashboardConfig.sizes.mediaVisualiserSize; + const size = Config.dashboard.sizes.mediaVisualiserSize; const cx = centerX; const cy = centerY; const rx = innerX + ctx.lineWidth / 2; @@ -116,10 +116,10 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: Appearance.padding.large + DashboardConfig.sizes.mediaVisualiserSize + anchors.leftMargin: Appearance.padding.large + Config.dashboard.sizes.mediaVisualiserSize - implicitWidth: DashboardConfig.sizes.mediaCoverArtSize - implicitHeight: DashboardConfig.sizes.mediaCoverArtSize + implicitWidth: Config.dashboard.sizes.mediaCoverArtSize + implicitHeight: Config.dashboard.sizes.mediaCoverArtSize color: Colours.palette.m3surfaceContainerHigh radius: Appearance.rounding.full diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 8513d19..b7bf8d0 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -65,7 +65,7 @@ Row { property bool primary readonly property real primaryMult: primary ? 1.2 : 1 - readonly property real thickness: DashboardConfig.sizes.resourceProgessThickness * primaryMult + readonly property real thickness: Config.dashboard.sizes.resourceProgessThickness * primaryMult property color fg1: Colours.palette.m3primary property color fg2: Colours.palette.m3secondary @@ -74,8 +74,8 @@ Row { anchors.verticalCenter: parent.verticalCenter - implicitWidth: DashboardConfig.sizes.resourceSize * primaryMult - implicitHeight: DashboardConfig.sizes.resourceSize * primaryMult + implicitWidth: Config.dashboard.sizes.resourceSize * primaryMult + implicitHeight: Config.dashboard.sizes.resourceSize * primaryMult onValue1Changed: canvas.requestPaint() onValue2Changed: canvas.requestPaint() diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 46cbd76..e678e6a 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -48,14 +48,14 @@ Item { id: indicator anchors.top: bar.bottom - anchors.topMargin: DashboardConfig.sizes.tabIndicatorSpacing + anchors.topMargin: Config.dashboard.sizes.tabIndicatorSpacing implicitWidth: bar.currentItem.implicitWidth - implicitHeight: DashboardConfig.sizes.tabIndicatorHeight + implicitHeight: Config.dashboard.sizes.tabIndicatorHeight x: { const tab = bar.currentItem; - const width = (root.nonAnimWidth - DashboardConfig.sizes.tabIndicatorSpacing * (bar.count - 1) * 2) / bar.count + const width = (root.nonAnimWidth - Config.dashboard.sizes.tabIndicatorSpacing * (bar.count - 1) * 2) / bar.count; return width * tab.TabBar.index + (width - tab.implicitWidth) / 2; } @@ -69,7 +69,6 @@ Item { color: Colours.palette.m3primary radius: Appearance.rounding.full - } Behavior on x { @@ -108,14 +107,17 @@ Item { cursorShape: Qt.PointingHandCursor - onPressed: ({x,y}) => { + onPressed: ({ + x, + y + }) => { tab.TabBar.tabBar.setCurrentIndex(tab.TabBar.index); const stateY = stateWrapper.y; rippleAnim.x = x; rippleAnim.y = y - stateY; - const dist = (ox,oy) => ox * ox + oy * oy; + const dist = (ox, oy) => ox * ox + oy * oy; const stateEndY = stateY + stateWrapper.height; rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY))); @@ -176,7 +178,7 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - implicitHeight: parent.height + DashboardConfig.sizes.tabIndicatorSpacing * 2 + implicitHeight: parent.height + Config.dashboard.sizes.tabIndicatorSpacing * 2 color: "transparent" radius: Appearance.rounding.small @@ -237,7 +239,6 @@ Item { text: tab.text color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant } - } } diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index 738fed1..25df7a5 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -8,7 +8,7 @@ Item { anchors.top: parent.top anchors.bottom: parent.bottom - implicitWidth: DashboardConfig.sizes.dateTimeWidth + implicitWidth: Config.dashboard.sizes.dateTimeWidth StyledText { id: hours diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 7122d69..f47079d 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -19,7 +19,7 @@ Item { anchors.top: parent.top anchors.bottom: parent.bottom - implicitWidth: DashboardConfig.sizes.mediaWidth + implicitWidth: Config.dashboard.sizes.mediaWidth Behavior on playerProgress { NumberAnimation { @@ -31,7 +31,7 @@ Item { Timer { running: root.shouldUpdate && (Players.active?.isPlaying ?? false) - interval: DashboardConfig.mediaUpdateInterval + interval: Config.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true onTriggered: Players.active?.positionChanged() @@ -43,16 +43,16 @@ Item { ShapePath { fillColor: "transparent" strokeColor: Colours.palette.m3surfaceContainerHigh - strokeWidth: DashboardConfig.sizes.mediaProgressThickness + strokeWidth: Config.dashboard.sizes.mediaProgressThickness capStyle: ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 - radiusX: (cover.width + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - radiusY: (cover.height + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - startAngle: -90 - DashboardConfig.sizes.mediaProgressSweep / 2 - sweepAngle: DashboardConfig.sizes.mediaProgressSweep + radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small + radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small + startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 + sweepAngle: Config.dashboard.sizes.mediaProgressSweep } Behavior on strokeColor { @@ -67,16 +67,16 @@ Item { ShapePath { fillColor: "transparent" strokeColor: Colours.palette.m3primary - strokeWidth: DashboardConfig.sizes.mediaProgressThickness + strokeWidth: Config.dashboard.sizes.mediaProgressThickness capStyle: ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 - radiusX: (cover.width + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - radiusY: (cover.height + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - startAngle: -90 - DashboardConfig.sizes.mediaProgressSweep / 2 - sweepAngle: DashboardConfig.sizes.mediaProgressSweep * root.playerProgress + radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small + radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small + startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 + sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress } Behavior on strokeColor { @@ -95,7 +95,7 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.large + DashboardConfig.sizes.mediaProgressThickness + Appearance.spacing.small + anchors.margins: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small implicitHeight: width color: Colours.palette.m3surfaceContainerHigh diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml index b22b2d1..9a1f07a 100644 --- a/modules/dashboard/dash/Resources.qml +++ b/modules/dashboard/dash/Resources.qml @@ -49,7 +49,7 @@ Row { anchors.bottom: icon.top anchors.bottomMargin: Appearance.spacing.small - implicitWidth: DashboardConfig.sizes.resourceProgessThickness + implicitWidth: Config.dashboard.sizes.resourceProgessThickness color: Colours.palette.m3surfaceContainerHigh radius: Appearance.rounding.full diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index f1b9b18..bf989c6 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -36,6 +36,7 @@ Row { Column { id: info + anchors.verticalCenter: parent.verticalCenter spacing: Appearance.spacing.normal InfoLine { @@ -69,8 +70,8 @@ Row { running: true command: ["uptime", "-p"] - stdout: SplitParser { - onRead: data => uptimeProc.uptime = data + stdout: StdioCollector { + onStreamFinished: uptimeProc.uptime = text.trim() } } } @@ -90,7 +91,7 @@ Row { id: icon anchors.left: parent.left - anchors.leftMargin: (DashboardConfig.sizes.infoIconSize - implicitWidth) / 2 + anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 text: line.icon color: line.colour @@ -109,7 +110,7 @@ Row { text: `: ${line.text}` font.pointSize: Appearance.font.size.normal - width: DashboardConfig.sizes.infoWidth + width: Config.dashboard.sizes.infoWidth elide: Text.ElideRight } } diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml index 1840da1..46ca477 100644 --- a/modules/drawers/Backgrounds.qml +++ b/modules/drawers/Backgrounds.qml @@ -16,7 +16,7 @@ Shape { required property Item bar anchors.fill: parent - anchors.margins: BorderConfig.thickness + anchors.margins: Config.border.thickness anchors.leftMargin: bar.implicitWidth preferredRendererType: Shape.CurveRenderer opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 diff --git a/modules/drawers/Border.qml b/modules/drawers/Border.qml index 9014d07..62dc277 100644 --- a/modules/drawers/Border.qml +++ b/modules/drawers/Border.qml @@ -16,7 +16,7 @@ Item { id: rect anchors.fill: parent - color: Colours.alpha(BorderConfig.colour, false) + color: Colours.alpha(Config.border.colour, false) visible: false } @@ -29,9 +29,9 @@ Item { Rectangle { anchors.fill: parent - anchors.margins: BorderConfig.thickness + anchors.margins: Config.border.thickness anchors.leftMargin: root.bar.implicitWidth - radius: BorderConfig.rounding + radius: Config.border.rounding } } diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 9930c6d..5cab75b 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -33,9 +33,9 @@ Variants { mask: Region { x: bar.implicitWidth - y: BorderConfig.thickness - width: win.width - bar.implicitWidth - BorderConfig.thickness - height: win.height - BorderConfig.thickness * 2 + y: Config.border.thickness + width: win.width - bar.implicitWidth - Config.border.thickness + height: win.height - Config.border.thickness * 2 intersection: Intersection.Xor regions: regions.instances @@ -55,7 +55,7 @@ Variants { required property Item modelData x: modelData.x + bar.implicitWidth - y: modelData.y + BorderConfig.thickness + y: modelData.y + Config.border.thickness width: modelData.width height: modelData.height intersection: Intersection.Subtract diff --git a/modules/drawers/Exclusions.qml b/modules/drawers/Exclusions.qml index 188aadb..5557cdb 100644 --- a/modules/drawers/Exclusions.qml +++ b/modules/drawers/Exclusions.qml @@ -31,6 +31,7 @@ Scope { component ExclusionZone: StyledWindow { screen: root.screen name: "border-exclusion" - exclusiveZone: BorderConfig.thickness + exclusiveZone: Config.border.thickness + mask: Region {} } } diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 10e37a8..d38cc8a 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -20,8 +20,8 @@ MouseArea { property bool osdShortcutActive function withinPanelHeight(panel: Item, x: real, y: real): bool { - const panelY = BorderConfig.thickness + panel.y; - return y >= panelY - BorderConfig.rounding && y <= panelY + panel.height + BorderConfig.rounding; + const panelY = Config.border.thickness + panel.y; + return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding; } function inRightPanel(panel: Item, x: real, y: real): bool { @@ -30,7 +30,7 @@ MouseArea { function inTopPanel(panel: Item, x: real, y: real): bool { const panelX = bar.implicitWidth + panel.x; - return y < BorderConfig.thickness + panel.y + panel.height && x >= panelX - BorderConfig.rounding && x <= panelX + panel.width + BorderConfig.rounding; + return y < Config.border.thickness + panel.y + panel.height && x >= panelX - Config.border.rounding && x <= panelX + panel.width + Config.border.rounding; } anchors.fill: parent @@ -51,7 +51,10 @@ MouseArea { } } - onPositionChanged: ({x, y}) => { + onPositionChanged: ({ + x, + y + }) => { // Show osd on hover const showOsd = inRightPanel(panels.osd, x, y); @@ -68,9 +71,9 @@ MouseArea { // Show/hide session on drag if (pressed && withinPanelHeight(panels.session, x, y)) { const dragX = x - dragStart.x; - if (dragX < -SessionConfig.dragThreshold) + if (dragX < -Config.session.dragThreshold) visibilities.session = true; - else if (dragX > SessionConfig.dragThreshold) + else if (dragX > Config.session.dragThreshold) visibilities.session = false; } diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 6c6d892..a422fcc 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -24,7 +24,7 @@ Item { readonly property BarPopouts.Wrapper popouts: popouts anchors.fill: parent - anchors.margins: BorderConfig.thickness + anchors.margins: Config.border.thickness anchors.leftMargin: bar.implicitWidth Component.onCompleted: Visibilities.panels[screen] = this @@ -83,7 +83,7 @@ Item { anchors.left: parent.left anchors.verticalCenter: parent.top anchors.verticalCenterOffset: { - const off = root.popouts.currentCenter - BorderConfig.thickness; + const off = root.popouts.currentCenter - Config.border.thickness; const diff = root.height - Math.floor(off + implicitHeight / 2); if (diff < 0) return off + diff; diff --git a/modules/launcher/ActionItem.qml b/modules/launcher/ActionItem.qml index 20638e3..720b272 100644 --- a/modules/launcher/ActionItem.qml +++ b/modules/launcher/ActionItem.qml @@ -9,7 +9,7 @@ Item { required property Actions.Action modelData required property var list - implicitHeight: LauncherConfig.sizes.itemHeight + implicitHeight: Config.launcher.sizes.itemHeight anchors.left: parent?.left anchors.right: parent?.right diff --git a/modules/launcher/Actions.qml b/modules/launcher/Actions.qml index d612cb0..1f6e72b 100644 --- a/modules/launcher/Actions.qml +++ b/modules/launcher/Actions.qml @@ -129,7 +129,7 @@ Singleton { })) function fuzzyQuery(search: string): var { - return Fuzzy.go(search.slice(LauncherConfig.actionPrefix.length), preppedActions, { + return Fuzzy.go(search.slice(Config.launcher.actionPrefix.length), preppedActions, { all: true, keys: ["name", "desc"], scoreFn: r => r[0].score > 0 ? r[0].score * 0.9 + r[1].score * 0.1 : 0 @@ -137,7 +137,7 @@ Singleton { } function autocomplete(list: AppList, text: string): void { - list.search.text = `${LauncherConfig.actionPrefix}${text} `; + list.search.text = `${Config.launcher.actionPrefix}${text} `; } function handleDangerousAction(list: AppList, process: QtObject): void { diff --git a/modules/launcher/AppItem.qml b/modules/launcher/AppItem.qml index b6a0bf5..e3f6720 100644 --- a/modules/launcher/AppItem.qml +++ b/modules/launcher/AppItem.qml @@ -11,7 +11,7 @@ Item { required property DesktopEntry modelData required property PersistentProperties visibilities - implicitHeight: LauncherConfig.sizes.itemHeight + implicitHeight: Config.launcher.sizes.itemHeight anchors.left: parent?.left anchors.right: parent?.right diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index a431395..b2b9f57 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -14,14 +14,14 @@ ListView { required property TextField search required property PersistentProperties visibilities - property bool isAction: search.text.startsWith(LauncherConfig.actionPrefix) + property bool isAction: search.text.startsWith(Config.launcher.actionPrefix) function getModelValues() { let text = search.text; if (isAction) return Actions.fuzzyQuery(text); - if (text.startsWith(LauncherConfig.actionPrefix)) - text = search.text.slice(LauncherConfig.actionPrefix.length); + if (text.startsWith(Config.launcher.actionPrefix)) + text = search.text.slice(Config.launcher.actionPrefix.length); return Apps.fuzzyQuery(text); } @@ -32,7 +32,7 @@ ListView { spacing: Appearance.spacing.small orientation: Qt.Vertical - implicitHeight: (LauncherConfig.sizes.itemHeight + spacing) * Math.min(LauncherConfig.maxShown, count) - spacing + implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxShown, count) - spacing highlightMoveDuration: Appearance.anim.durations.normal highlightResizeDuration: 0 diff --git a/modules/launcher/Background.qml b/modules/launcher/Background.qml index 5a3d5d4..82c5f4a 100644 --- a/modules/launcher/Background.qml +++ b/modules/launcher/Background.qml @@ -7,12 +7,12 @@ ShapePath { id: root required property Wrapper wrapper - readonly property real rounding: BorderConfig.rounding + readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding strokeWidth: -1 - fillColor: BorderConfig.colour + fillColor: Config.border.colour PathArc { relativeX: root.rounding diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 54cc872..b6d1056 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -74,7 +74,7 @@ Item { topPadding: Appearance.padding.larger bottomPadding: Appearance.padding.larger - placeholderText: qsTr("Type \"%1\" for commands").arg(LauncherConfig.actionPrefix) + placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix) background: null onAccepted: { @@ -83,7 +83,7 @@ Item { if (list.showWallpapers) { Wallpapers.setWallpaper(currentItem.modelData.path); root.visibilities.launcher = false; - } else if (text.startsWith(LauncherConfig.actionPrefix)) { + } else if (text.startsWith(Config.launcher.actionPrefix)) { currentItem.modelData.onClicked(list.currentList); } else { Apps.launch(currentItem.modelData); diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index 9818118..ad116f7 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -15,7 +15,7 @@ Item { required property int padding required property int rounding - property bool showWallpapers: search.text.startsWith(`${LauncherConfig.actionPrefix}wallpaper `) + property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `) property var currentList: (showWallpapers ? wallpaperList : appList).item anchors.horizontalCenter: parent.horizontalCenter @@ -29,7 +29,7 @@ Item { name: "apps" PropertyChanges { - root.implicitWidth: LauncherConfig.sizes.itemWidth + root.implicitWidth: Config.launcher.sizes.itemWidth root.implicitHeight: Math.max(empty.height, appList.height) appList.active: true } @@ -43,8 +43,8 @@ Item { name: "wallpapers" PropertyChanges { - root.implicitWidth: Math.max(LauncherConfig.sizes.itemWidth, wallpaperList.width) - root.implicitHeight: LauncherConfig.sizes.wallpaperHeight + root.implicitWidth: Math.max(Config.launcher.sizes.itemWidth, wallpaperList.width) + root.implicitHeight: Config.launcher.sizes.wallpaperHeight wallpaperList.active: true } } diff --git a/modules/launcher/WallpaperItem.qml b/modules/launcher/WallpaperItem.qml index dbd482e..862e910 100644 --- a/modules/launcher/WallpaperItem.qml +++ b/modules/launcher/WallpaperItem.qml @@ -42,7 +42,7 @@ StyledRect { path: root.modelData.path smooth: !root.PathView.view.moving - width: LauncherConfig.sizes.wallpaperWidth + width: Config.launcher.sizes.wallpaperWidth height: width / 16 * 9 } diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml index fef1726..fd82a2b 100644 --- a/modules/launcher/WallpaperList.qml +++ b/modules/launcher/WallpaperList.qml @@ -14,8 +14,8 @@ PathView { const screenWidth = QsWindow.window?.screen.width * 0.8; if (!screenWidth) return 0; - const itemWidth = LauncherConfig.sizes.wallpaperWidth * 0.8; - const max = LauncherConfig.maxWallpapers; + const itemWidth = Config.launcher.sizes.wallpaperWidth * 0.8; + const max = Config.launcher.maxWallpapers; if (max * itemWidth > screenWidth) { const items = Math.floor(screenWidth / itemWidth); return items > 1 && items % 2 === 0 ? items - 1 : items; @@ -43,7 +43,7 @@ PathView { Wallpapers.preview(currentItem.modelData.path); } - implicitWidth: Math.min(numItems, count) * (LauncherConfig.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2) + implicitWidth: Math.min(numItems, count) * (Config.launcher.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2) pathItemCount: numItems cacheItemCount: 4 diff --git a/modules/notifications/Background.qml b/modules/notifications/Background.qml index 2fd05f8..4a38510 100644 --- a/modules/notifications/Background.qml +++ b/modules/notifications/Background.qml @@ -8,13 +8,13 @@ ShapePath { id: root required property Wrapper wrapper - readonly property real rounding: BorderConfig.rounding + readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding - property real fullHeightRounding: wrapper.height >= QsWindow.window?.height - BorderConfig.thickness * 2 ? -rounding : rounding + property real fullHeightRounding: wrapper.height >= QsWindow.window?.height - Config.border.thickness * 2 ? -rounding : rounding strokeWidth: -1 - fillColor: BorderConfig.colour + fillColor: Config.border.colour PathLine { relativeX: -(root.wrapper.width + root.rounding) diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index a64ccb9..6983b95 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -14,7 +14,7 @@ Item { anchors.bottom: parent.bottom anchors.right: parent.right - implicitWidth: NotifsConfig.sizes.width + padding * 2 + implicitWidth: Config.notifs.sizes.width + padding * 2 implicitHeight: { const count = list.count; if (count === 0) @@ -29,19 +29,19 @@ Item { const panel = Visibilities.panels[screen]; if (visibilities && panel) { if (visibilities.osd) { - const h = panel.osd.y - BorderConfig.rounding * 2; + const h = panel.osd.y - Config.border.rounding * 2; if (height > h) height = h; } if (visibilities.session) { - const h = panel.session.y - BorderConfig.rounding * 2; + const h = panel.session.y - Config.border.rounding * 2; if (height > h) height = h; } } - return Math.min((screen?.height ?? 0) - BorderConfig.thickness * 2, height + padding * 2); + return Math.min((screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); } ClippingWrapperRectangle { @@ -108,7 +108,7 @@ Item { Anim { target: notif property: "x" - to: (notif.x >= 0 ? NotifsConfig.sizes.width : -NotifsConfig.sizes.width) * 2 + to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 duration: Appearance.anim.durations.normal easing.bezierCurve: Appearance.anim.curves.emphasized } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 51ed5f5..ebc5ce6 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -20,10 +20,10 @@ StyledRect { color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.palette.m3surfaceContainer radius: Appearance.rounding.normal - implicitWidth: NotifsConfig.sizes.width + implicitWidth: Config.notifs.sizes.width implicitHeight: inner.implicitHeight - x: NotifsConfig.sizes.width + x: Config.notifs.sizes.width Component.onCompleted: x = 0 RetainableLock { @@ -52,7 +52,7 @@ StyledRect { root.modelData.notification.dismiss(); } onReleased: event => { - if (Math.abs(root.x) < NotifsConfig.sizes.width * NotifsConfig.clearThreshold) + if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold) root.x = 0; else root.modelData.popup = false; @@ -60,12 +60,12 @@ StyledRect { onPositionChanged: event => { if (pressed) { const diffY = event.y - startY; - if (Math.abs(diffY) > NotifsConfig.expandThreshold) + if (Math.abs(diffY) > Config.notifs.expandThreshold) root.expanded = diffY > 0; } } onClicked: event => { - if (!NotifsConfig.actionOnClick || event.button !== Qt.LeftButton) + if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton) return; const actions = root.modelData.actions; @@ -93,7 +93,10 @@ StyledRect { implicitHeight: root.nonAnimHeight Behavior on implicitHeight { - Anim {} + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } Loader { @@ -104,14 +107,14 @@ StyledRect { anchors.left: parent.left anchors.top: parent.top - width: NotifsConfig.sizes.image - height: NotifsConfig.sizes.image + width: Config.notifs.sizes.image + height: Config.notifs.sizes.image visible: root.hasImage || root.hasAppIcon sourceComponent: ClippingRectangle { radius: Appearance.rounding.full - implicitWidth: NotifsConfig.sizes.image - implicitHeight: NotifsConfig.sizes.image + implicitWidth: Config.notifs.sizes.image + implicitHeight: Config.notifs.sizes.image Image { anchors.fill: parent @@ -137,8 +140,8 @@ StyledRect { sourceComponent: StyledRect { radius: Appearance.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3tertiaryContainer - implicitWidth: root.hasImage ? NotifsConfig.sizes.badge : NotifsConfig.sizes.image - implicitHeight: root.hasImage ? NotifsConfig.sizes.badge : NotifsConfig.sizes.image + implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image Loader { id: icon diff --git a/modules/osd/Background.qml b/modules/osd/Background.qml index 551637b..12cce6a 100644 --- a/modules/osd/Background.qml +++ b/modules/osd/Background.qml @@ -7,12 +7,12 @@ ShapePath { id: root required property Wrapper wrapper - readonly property real rounding: BorderConfig.rounding + readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.width < rounding * 2 readonly property real roundingX: flatten ? wrapper.width / 2 : rounding strokeWidth: -1 - fillColor: BorderConfig.colour + fillColor: Config.border.colour PathArc { relativeX: -root.roundingX diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 6814966..4bd43af 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -28,8 +28,8 @@ Column { value: Audio.volume onMoved: Audio.setVolume(value) - implicitWidth: OsdConfig.sizes.sliderWidth - implicitHeight: OsdConfig.sizes.sliderHeight + implicitWidth: Config.osd.sizes.sliderWidth + implicitHeight: Config.osd.sizes.sliderHeight } VerticalSlider { @@ -37,7 +37,7 @@ Column { value: root.monitor?.brightness ?? 0 onMoved: root.monitor?.setBrightness(value) - implicitWidth: OsdConfig.sizes.sliderWidth - implicitHeight: OsdConfig.sizes.sliderHeight + implicitWidth: Config.osd.sizes.sliderWidth + implicitHeight: Config.osd.sizes.sliderHeight } } diff --git a/modules/osd/Interactions.qml b/modules/osd/Interactions.qml index eecf0b6..fa75aa0 100644 --- a/modules/osd/Interactions.qml +++ b/modules/osd/Interactions.qml @@ -39,7 +39,7 @@ Scope { Timer { id: timer - interval: OsdConfig.hideDelay + interval: Config.osd.hideDelay onTriggered: { if (!root.hovered) root.visibilities.osd = false; diff --git a/modules/session/Background.qml b/modules/session/Background.qml index 551637b..12cce6a 100644 --- a/modules/session/Background.qml +++ b/modules/session/Background.qml @@ -7,12 +7,12 @@ ShapePath { id: root required property Wrapper wrapper - readonly property real rounding: BorderConfig.rounding + readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.width < rounding * 2 readonly property real roundingX: flatten ? wrapper.width / 2 : rounding strokeWidth: -1 - fillColor: BorderConfig.colour + fillColor: Config.border.colour PathArc { relativeX: -root.roundingX diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 7c69bbd..f6b1258 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -48,8 +48,8 @@ Column { } AnimatedImage { - width: SessionConfig.sizes.button - height: SessionConfig.sizes.button + width: Config.session.sizes.button + height: Config.session.sizes.button sourceSize.width: width sourceSize.height: height @@ -84,8 +84,8 @@ Column { required property string icon required property list<string> command - implicitWidth: SessionConfig.sizes.button - implicitHeight: SessionConfig.sizes.button + implicitWidth: Config.session.sizes.button + implicitHeight: Config.session.sizes.button radius: Appearance.rounding.large color: button.activeFocus ? Colours.palette.m3secondaryContainer : Colours.palette.m3surfaceContainer diff --git a/services/BeatDetector.qml b/services/BeatDetector.qml index 66bf2d4..65dd88e 100644 --- a/services/BeatDetector.qml +++ b/services/BeatDetector.qml @@ -10,9 +10,13 @@ Singleton { Process { running: true - command: [`${Quickshell.shellRoot}/assets/realtime-beat-detector.py`] + command: ["/usr/lib/caelestia/beat_detector", "--no-log", "--no-stats", "--no-visual"] stdout: SplitParser { - onRead: data => root.bpm = parseFloat(data) + onRead: data => { + const match = data.match(/BPM: ([0-9]+\.[0-9])/); + if (match) + root.bpm = parseFloat(match[1]); + } } } } diff --git a/services/Bluetooth.qml b/services/Bluetooth.qml index 43f3636..f5e9de1 100644 --- a/services/Bluetooth.qml +++ b/services/Bluetooth.qml @@ -24,31 +24,47 @@ Singleton { Process { id: getInfo + running: true - command: ["sh", "-c", "bluetoothctl show | paste -s"] - stdout: SplitParser { - onRead: data => { - root.powered = data.includes("Powered: yes"); - root.discovering = data.includes("Discovering: yes"); + command: ["bluetoothctl", "show"] + stdout: StdioCollector { + onStreamFinished: { + root.powered = text.includes("Powered: yes"); + root.discovering = text.includes("Discovering: yes"); } } } Process { id: getDevices + running: true - command: ["fish", "-c", `for a in (bluetoothctl devices | cut -d ' ' -f 2); bluetoothctl info $a | jq -R 'reduce (inputs / ":") as [$key, $value] ({}; .[$key | ltrimstr("\t")] = ($value | ltrimstr(" ")))' | jq -c --arg addr $a '.Address = $addr'; end | jq -sc`] - stdout: SplitParser { - onRead: data => { - const devices = JSON.parse(data).filter(d => d.Name); + command: ["fish", "-c", ` + for a in (bluetoothctl devices) + if string match -q 'Device *' $a + bluetoothctl info $addr (string split ' ' $a)[2] + echo + end + end`] + stdout: StdioCollector { + onStreamFinished: { + const devices = text.trim().split("\n\n").map(d => ({ + name: d.match(/Name: (.*)/)[1], + alias: d.match(/Alias: (.*)/)[1], + address: d.match(/Device ([0-9A-Z:]{17})/)[1], + icon: d.match(/Icon: (.*)/)[1], + connected: d.includes("Connected: yes"), + paired: d.includes("Paired: yes"), + trusted: d.includes("Trusted: yes") + })); const rDevices = root.devices; - const destroyed = rDevices.filter(rd => !devices.find(d => d.Address === rd.address)); + const destroyed = rDevices.filter(rd => !devices.find(d => d.address === rd.address)); for (const device of destroyed) rDevices.splice(rDevices.indexOf(device), 1).forEach(d => d.destroy()); for (const device of devices) { - const match = rDevices.find(d => d.address === device.Address); + const match = rDevices.find(d => d.address === device.address); if (match) { match.lastIpcObject = device; } else { @@ -63,13 +79,13 @@ Singleton { component Device: QtObject { required property var lastIpcObject - readonly property string name: lastIpcObject.Name - readonly property string alias: lastIpcObject.Alias - readonly property string address: lastIpcObject.Address - readonly property string icon: lastIpcObject.Icon - readonly property bool connected: lastIpcObject.Connected === "yes" - readonly property bool paired: lastIpcObject.Paired === "yes" - readonly property bool trusted: lastIpcObject.Trusted === "yes" + readonly property string name: lastIpcObject.name + readonly property string alias: lastIpcObject.alias + readonly property string address: lastIpcObject.address + readonly property string icon: lastIpcObject.icon + readonly property bool connected: lastIpcObject.connected + readonly property bool paired: lastIpcObject.paired + readonly property bool trusted: lastIpcObject.trusted } Component { diff --git a/services/Brightness.qml b/services/Brightness.qml index 1687878..3715853 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -9,7 +9,7 @@ import QtQuick Singleton { id: root - property var ddcMonitors: [] + property list<var> ddcMonitors: [] readonly property list<Monitor> monitors: variants.instances function getMonitorForScreen(screen: ShellScreen): var { @@ -49,19 +49,12 @@ Singleton { id: ddcProc command: ["ddcutil", "detect", "--brief"] - stdout: SplitParser { - splitMarker: "\n\n" - onRead: data => { - if (data.startsWith("Display ")) { - const lines = data.split("\n").map(l => l.trim()); - root.ddcMonitors.push({ - model: lines.find(l => l.startsWith("Monitor:")).split(":")[2], - busNum: lines.find(l => l.startsWith("I2C bus:")).split("/dev/i2c-")[1] - }); - } - } + stdout: StdioCollector { + onStreamFinished: root.ddcMonitors = text.trim().split("\n\n").filter(d => d.startsWith("Display ")).map(d => ({ + model: d.match(/Monitor:.*:(.*):.*/)[1], + busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)[1] + })) } - onExited: root.ddcMonitorsChanged() } Process { @@ -87,9 +80,9 @@ Singleton { property real brightness readonly property Process initProc: Process { - stdout: SplitParser { - onRead: data => { - const [, , , current, max] = data.split(" "); + stdout: StdioCollector { + onStreamFinished: { + const [, , , current, max] = text.split(" "); monitor.brightness = parseInt(current) / parseInt(max); } } diff --git a/services/Cava.qml b/services/Cava.qml index eaa6e20..3fa5083 100644 --- a/services/Cava.qml +++ b/services/Cava.qml @@ -11,7 +11,7 @@ Singleton { Process { running: true - command: ["sh", "-c", `printf '[general]\nframerate=60\nbars=${DashboardConfig.visualiserBars}\n[output]\nchannels=mono\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nascii_max_range=100' | cava -p /dev/stdin`] + command: ["sh", "-c", `printf '[general]\nframerate=60\nbars=${Config.dashboard.visualiserBars}\n[output]\nchannels=mono\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nascii_max_range=100' | cava -p /dev/stdin`] stdout: SplitParser { onRead: data => root.values = data.slice(0, -1).split(";").map(v => parseInt(v, 10)) } diff --git a/services/Colours.qml b/services/Colours.qml index 4383972..ea3b02a 100644 --- a/services/Colours.qml +++ b/services/Colours.qml @@ -36,12 +36,14 @@ Singleton { function load(data: string, isPreview: bool): void { const colours = isPreview ? preview : current; - for (const line of data.trim().split("\n")) { - let [name, colour] = line.split(" "); - name = name.trim(); - name = colourNames.includes(name) ? name : `m3${name}`; - if (colours.hasOwnProperty(name)) - colours[name] = `#${colour.trim()}`; + const scheme = JSON.parse(data); + + light = scheme.mode === "light"; + + for (const [name, colour] of Object.entries(scheme.colours)) { + const propName = colourNames.includes(name) ? name : `m3${name}`; + if (colours.hasOwnProperty(propName)) + colours[propName] = `#${colour}`; } if (!isPreview || (isPreview && endPreviewOnNextChange)) { @@ -51,7 +53,7 @@ Singleton { } function setMode(mode: string): void { - setModeProc.command = ["caelestia", "scheme", "dynamic", "default", mode]; + setModeProc.command = ["caelestia", "scheme", "-m", mode]; setModeProc.startDetached(); } @@ -60,14 +62,7 @@ Singleton { } FileView { - path: `${Paths.state}/scheme/current-mode.txt` - watchChanges: true - onFileChanged: reload() - onLoaded: root.light = text() === "light" - } - - FileView { - path: `${Paths.state}/scheme/current.txt` + path: `${Paths.state}/scheme.json` watchChanges: true onFileChanged: reload() onLoaded: root.load(text(), false) diff --git a/services/Hyprland.qml b/services/Hyprland.qml index 610fba2..b8fa7c5 100644 --- a/services/Hyprland.qml +++ b/services/Hyprland.qml @@ -41,10 +41,10 @@ Singleton { Process { id: getClients - command: ["sh", "-c", "hyprctl -j clients | jq -c"] - stdout: SplitParser { - onRead: data => { - const clients = JSON.parse(data); + command: ["hyprctl", "-j", "clients"] + stdout: StdioCollector { + onStreamFinished: { + const clients = JSON.parse(text); const rClients = root.clients; const destroyed = rClients.filter(rc => !clients.find(c => c.address === rc.address)); @@ -68,10 +68,9 @@ Singleton { Process { id: getActiveClient command: ["hyprctl", "-j", "activewindow"] - stdout: SplitParser { - splitMarker: "" - onRead: data => { - const client = JSON.parse(data); + stdout: StdioCollector { + onStreamFinished: { + const client = JSON.parse(text); const rClient = root.activeClient; if (client.address) { if (rClient) diff --git a/services/Network.qml b/services/Network.qml index b4827c8..6a7f50e 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -23,31 +23,36 @@ Singleton { Process { id: getNetworks running: true - command: ["sh", "-c", `nmcli -g ACTIVE,SIGNAL,FREQ,SSID,BSSID d w | jq -ncR '[(inputs | split("(?<!\\\\\\\\):"; "g")) | select(.[3] | length >= 4)]'`] - stdout: SplitParser { - onRead: data => { - const networks = JSON.parse(data).map(n => [n[0] === "yes", parseInt(n[1]), parseInt(n[2]), n[3], n[4].replace(/\\/g, "")]); + command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID", "d", "w"] + stdout: StdioCollector { + onStreamFinished: { + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; + const rep = new RegExp("\\\\:", "g"); + const rep2 = new RegExp(PLACEHOLDER, "g"); + + const networks = text.trim().split("\n").map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1]), + frequency: parseInt(net[2]), + ssid: net[3], + bssid: net[4].replace(rep2, ":") + }; + }); const rNetworks = root.networks; - const destroyed = rNetworks.filter(rn => !networks.find(n => n[2] === rn.frequency && n[3] === rn.ssid && n[4] === rn.bssid)); + const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid)); for (const network of destroyed) rNetworks.splice(rNetworks.indexOf(network), 1).forEach(n => n.destroy()); for (const network of networks) { - const match = rNetworks.find(n => n.frequency === network[2] && n.ssid === network[3] && n.bssid === network[4]); + const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid); if (match) { - match.active = network[0]; - match.strength = network[1]; - match.frequency = network[2]; - match.ssid = network[3]; - match.bssid = network[4]; + match.lastIpcObject = network; } else { rNetworks.push(apComp.createObject(root, { - active: network[0], - strength: network[1], - frequency: network[2], - ssid: network[3], - bssid: network[4] + lastIpcObject: network })); } } @@ -56,11 +61,12 @@ Singleton { } component AccessPoint: QtObject { - required property string ssid - required property string bssid - required property int strength - required property int frequency - required property bool active + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active } Component { diff --git a/services/Notifs.qml b/services/Notifs.qml index 4acd56f..73d98a2 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -80,9 +80,9 @@ Singleton { readonly property Timer timer: Timer { running: true - interval: notif.notification.expireTimeout > 0 ? notif.notification.expireTimeout : NotifsConfig.defaultExpireTimeout + interval: notif.notification.expireTimeout > 0 ? notif.notification.expireTimeout : Config.notifs.defaultExpireTimeout onTriggered: { - if (NotifsConfig.expire) + if (Config.notifs.expire) notif.popup = false; } } diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index ef953f1..9bf665a 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -98,38 +98,40 @@ Singleton { running: true command: ["sh", "-c", "df | grep '^/dev/' | awk '{print $1, $3, $4}'"] - stdout: SplitParser { - splitMarker: "" - onRead: data => { - const deviceMap = new Map(); + stdout: StdioCollector { + onStreamFinished: { + const deviceMap = new Map(); - for (const line of data.trim().split("\n")) { - if (line.trim() === "") continue; + for (const line of text.trim().split("\n")) { + if (line.trim() === "") + continue; - const parts = line.trim().split(/\s+/); - if (parts.length >= 3) { - const device = parts[0]; - const used = parseInt(parts[1], 10) || 0; - const avail = parseInt(parts[2], 10) || 0; - - // only keep the entry with the largest total space for each device - if (!deviceMap.has(device) || - (used + avail) > (deviceMap.get(device).used + deviceMap.get(device).avail)) { - deviceMap.set(device, { used: used, avail: avail }); - } - } - } + const parts = line.trim().split(/\s+/); + if (parts.length >= 3) { + const device = parts[0]; + const used = parseInt(parts[1], 10) || 0; + const avail = parseInt(parts[2], 10) || 0; - let totalUsed = 0; - let totalAvail = 0; - - for (const [device, stats] of deviceMap) { - totalUsed += stats.used; - totalAvail += stats.avail; - } + // Only keep the entry with the largest total space for each device + if (!deviceMap.has(device) || (used + avail) > (deviceMap.get(device).used + deviceMap.get(device).avail)) { + deviceMap.set(device, { + used: used, + avail: avail + }); + } + } + } + + let totalUsed = 0; + let totalAvail = 0; - root.storageUsed = totalUsed; - root.storageTotal = totalUsed + totalAvail; + for (const [device, stats] of deviceMap) { + totalUsed += stats.used; + totalAvail += stats.avail; + } + + root.storageUsed = totalUsed; + root.storageTotal = totalUsed + totalAvail; } } } @@ -138,10 +140,10 @@ Singleton { id: cpuTemp running: true - command: ["fish", "-c", "cat /sys/class/thermal/thermal_zone*/temp | string join ' '"] - stdout: SplitParser { - onRead: data => { - const temps = data.trim().split(" "); + command: ["sh", "-c", "cat /sys/class/thermal/thermal_zone*/temp"] + stdout: StdioCollector { + onStreamFinished: { + const temps = text.trim().split(" "); const sum = temps.reduce((acc, d) => acc + parseInt(d, 10), 0); root.cpuTemp = sum / temps.length / 1000; } @@ -153,10 +155,9 @@ Singleton { running: true command: ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] - stdout: SplitParser { - splitMarker: "" - onRead: data => { - const percs = data.trim().split("\n"); + stdout: StdioCollector { + onStreamFinished: { + const percs = text.trim().split("\n"); const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0); root.gpuPerc = sum / percs.length / 100; } @@ -167,13 +168,14 @@ Singleton { id: gpuTemp running: true - command: ["sh", "-c", "sensors | jq -nRc '[inputs]'"] - stdout: SplitParser { - onRead: data => { + command: ["sensors"] + stdout: StdioCollector { + onStreamFinished: { let eligible = false; let sum = 0; let count = 0; - for (const line of JSON.parse(data)) { + + for (const line of text.trim().split("\n")) { if (line === "Adapter: PCI adapter") eligible = true; else if (line === "") @@ -186,6 +188,7 @@ Singleton { } } } + root.gpuTemp = count > 0 ? sum / count : 0; } } diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml index 3f6bf15..a667017 100644 --- a/services/Wallpapers.qml +++ b/services/Wallpapers.qml @@ -9,8 +9,9 @@ import QtQuick Singleton { id: root - readonly property string currentNamePath: `${Paths.state}/wallpaper/last.txt`.slice(7) + readonly property string currentNamePath: `${Paths.state}/wallpaper/path.txt`.slice(7) readonly property string path: `${Paths.pictures}/Wallpapers`.slice(7) + readonly property list<string> extensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff"] readonly property list<Wallpaper> list: wallpapers.instances property bool showPreview: false @@ -77,11 +78,10 @@ Singleton { Process { id: getPreviewColoursProc - command: ["caelestia", "scheme", "print", root.previewPath] - stdout: SplitParser { - splitMarker: "" - onRead: data => { - Colours.load(data, true); + command: ["caelestia", "wallpaper", "-p", root.previewPath] + stdout: StdioCollector { + onStreamFinished: { + Colours.load(text, true); Colours.showPreview = true; } } @@ -97,10 +97,9 @@ Singleton { Process { running: true - command: ["fd", ".", root.path, "-t", "f", "-e", "jpg", "-e", "jpeg", "-e", "png", "-e", "svg"] - stdout: SplitParser { - splitMarker: "" - onRead: data => wallpapers.model = data.trim().split("\n") + command: ["find", root.path, "-type", "d", "-path", '*/.*', "-prune", "-o", "-not", "-name", '.*', "-type", "f", "-print"] + stdout: StdioCollector { + onStreamFinished: wallpapers.model = text.trim().split("\n").filter(w => root.extensions.includes(w.slice(w.lastIndexOf(".") + 1))).sort() } } diff --git a/services/Weather.qml b/services/Weather.qml index 13503f9..4f53d0b 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -7,6 +7,7 @@ import Quickshell.Io Singleton { id: root + property string loc property string icon property string description property real temperature @@ -15,17 +16,28 @@ Singleton { wttrProc.running = true; } + onLocChanged: wttrProc.running = true + Process { - id: wttrProc + id: ipProc running: true - command: ["fish", "-c", `curl "https://wttr.in/$(curl ipinfo.io | jq -r '.city' | string replace -a ' ' '%20')?format=j1" | jq -c '.current_condition[0] | {code: .weatherCode, desc: .weatherDesc[0].value, temp: .temp_C}'`] - stdout: SplitParser { - onRead: data => { - const json = JSON.parse(data); - root.icon = Icons.getWeatherIcon(json.code); - root.description = json.desc; - root.temperature = parseFloat(json.temp); + command: ["curl", "ipinfo.io"] + stdout: StdioCollector { + onStreamFinished: root.loc = JSON.parse(text).loc + } + } + + Process { + id: wttrProc + + command: ["curl", `https://wttr.in/${root.loc}?format=j1`] + stdout: StdioCollector { + onStreamFinished: { + const json = JSON.parse(text).current_condition[0]; + root.icon = Icons.getWeatherIcon(json.weatherCode); + root.description = json.weatherDesc[0].value; + root.temperature = parseFloat(json.temp_C); } } } diff --git a/utils/Paths.qml b/utils/Paths.qml index 88267ea..bd57608 100644 --- a/utils/Paths.qml +++ b/utils/Paths.qml @@ -13,6 +13,7 @@ Singleton { readonly property url data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/caelestia` readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/caelestia` readonly property url cache: `${StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]}/caelestia` + readonly property url config: `${StandardPaths.standardLocations(StandardPaths.GenericConfigLocation)[0]}/caelestia` readonly property url imagecache: `${cache}/imagecache` diff --git a/widgets/CachingImage.qml b/widgets/CachingImage.qml index 3382615..8f7f711 100644 --- a/widgets/CachingImage.qml +++ b/widgets/CachingImage.qml @@ -43,8 +43,8 @@ Image { property string path command: ["sha256sum", path] - stdout: SplitParser { - onRead: data => root.hash = data.split(" ")[0] + stdout: StdioCollector { + onStreamFinished: root.hash = text.split(" ")[0] } } } |