Adding css reloader
parent
6e12f81223
commit
d7ed4f1fa8
|
@ -7,6 +7,7 @@
|
|||
|
||||
#include "bar.hpp"
|
||||
#include "config.hpp"
|
||||
#include "util/css_reload_helper.hpp"
|
||||
#include "util/portal.hpp"
|
||||
|
||||
struct zwlr_layer_shell_v1;
|
||||
|
@ -55,6 +56,8 @@ class Client {
|
|||
Glib::RefPtr<Gtk::CssProvider> css_provider_;
|
||||
std::unique_ptr<Portal> portal;
|
||||
std::list<struct waybar_output> outputs_;
|
||||
std::unique_ptr<CssReloadHelper> m_cssReloadHelper;
|
||||
std::string m_cssFile;
|
||||
};
|
||||
|
||||
} // namespace waybar
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
struct pollfd;
|
||||
|
||||
namespace waybar {
|
||||
class CssReloadHelper {
|
||||
public:
|
||||
CssReloadHelper(std::string cssFile, std::function<void()> callback);
|
||||
|
||||
~CssReloadHelper();
|
||||
|
||||
virtual void monitorChanges();
|
||||
|
||||
void stop();
|
||||
|
||||
protected:
|
||||
std::vector<std::string> parseImports(const std::string& cssFile);
|
||||
|
||||
void parseImports(const std::string& cssFile,
|
||||
std::unordered_map<std::string, bool>& imports);
|
||||
|
||||
|
||||
void watchFiles(const std::vector<std::string>& files);
|
||||
|
||||
bool handleInotifyEvents(int fd);
|
||||
|
||||
bool watch(int inotifyFd, pollfd * pollFd);
|
||||
|
||||
virtual std::string getFileContents(const std::string& filename);
|
||||
|
||||
virtual std::string findPath(const std::string& filename);
|
||||
|
||||
private:
|
||||
std::string m_cssFile;
|
||||
std::function<void()> m_callback;
|
||||
std::atomic<bool> m_running = false;
|
||||
std::thread m_thread;
|
||||
std::mutex m_mutex;
|
||||
std::condition_variable m_cv;
|
||||
};
|
||||
} // namespace waybar
|
|
@ -196,7 +196,8 @@ src_files = files(
|
|||
'src/util/sanitize_str.cpp',
|
||||
'src/util/rewrite_string.cpp',
|
||||
'src/util/gtk_icon.cpp',
|
||||
'src/util/regex_collection.cpp'
|
||||
'src/util/regex_collection.cpp',
|
||||
'src/util/css_reload_helper.cpp'
|
||||
)
|
||||
|
||||
inc_dirs = ['include']
|
||||
|
|
|
@ -262,15 +262,18 @@ int waybar::Client::main(int argc, char *argv[]) {
|
|||
if (!portal) {
|
||||
portal = std::make_unique<waybar::Portal>();
|
||||
}
|
||||
auto css_file = getStyle(style_opt);
|
||||
setupCss(css_file);
|
||||
m_cssFile = getStyle(style_opt);
|
||||
setupCss(m_cssFile);
|
||||
m_cssReloadHelper = std::make_unique<CssReloadHelper>(m_cssFile, [&]() { setupCss(m_cssFile); });
|
||||
portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) {
|
||||
auto css_file = getStyle(style_opt, appearance);
|
||||
setupCss(css_file);
|
||||
});
|
||||
m_cssReloadHelper->monitorChanges();
|
||||
bindInterfaces();
|
||||
gtk_app->hold();
|
||||
gtk_app->run();
|
||||
m_cssReloadHelper.reset(); // stop watching css file
|
||||
bars.clear();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
#include "util/css_reload_helper.hpp"
|
||||
|
||||
#include <poll.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <sys/inotify.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <regex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "config.hpp"
|
||||
namespace {
|
||||
const std::regex IMPORT_REGEX(R"(@import\s+(?:url\()?(?:"|')([^"')]+)(?:"|')\)?;)");
|
||||
}
|
||||
|
||||
waybar::CssReloadHelper::CssReloadHelper(std::string cssFile, std::function<void()> callback)
|
||||
: m_cssFile(std::move(cssFile)), m_callback(std::move(callback)) {}
|
||||
|
||||
waybar::CssReloadHelper::~CssReloadHelper() { stop(); }
|
||||
|
||||
std::string waybar::CssReloadHelper::getFileContents(const std::string& filename) {
|
||||
if (filename.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ifstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
std::string waybar::CssReloadHelper::findPath(const std::string& filename) {
|
||||
// try path and fallback to looking relative to the config
|
||||
if (std::filesystem::exists(filename)) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
return Config::findConfigPath({filename}).value_or("");
|
||||
}
|
||||
|
||||
void waybar::CssReloadHelper::monitorChanges() {
|
||||
m_thread = std::thread([this] {
|
||||
m_running = true;
|
||||
while (m_running) {
|
||||
auto files = parseImports(m_cssFile);
|
||||
watchFiles(files);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<std::string> waybar::CssReloadHelper::parseImports(const std::string& cssFile) {
|
||||
std::unordered_map<std::string, bool> imports;
|
||||
|
||||
auto cssFullPath = findPath(cssFile);
|
||||
if (cssFullPath.empty()) {
|
||||
spdlog::error("Failed to find css file: {}", cssFile);
|
||||
return {};
|
||||
}
|
||||
|
||||
spdlog::debug("Parsing imports for file: {}", cssFullPath);
|
||||
imports[cssFullPath] = false;
|
||||
|
||||
auto previousSize = 0UL;
|
||||
auto maxIterations = 100U;
|
||||
do {
|
||||
previousSize = imports.size();
|
||||
for (const auto& [file, parsed] : imports) {
|
||||
if (!parsed) {
|
||||
parseImports(file, imports);
|
||||
}
|
||||
}
|
||||
|
||||
} while (imports.size() > previousSize && maxIterations-- > 0);
|
||||
|
||||
std::vector<std::string> result;
|
||||
for (const auto& [file, parsed] : imports) {
|
||||
if (parsed) {
|
||||
spdlog::debug("Adding file to watch list: {}", file);
|
||||
result.push_back(file);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void waybar::CssReloadHelper::parseImports(const std::string& cssFile,
|
||||
std::unordered_map<std::string, bool>& imports) {
|
||||
// if the file has already been parsed, skip
|
||||
if (imports.find(cssFile) != imports.end() && imports[cssFile]) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto contents = getFileContents(cssFile);
|
||||
std::smatch matches;
|
||||
while (std::regex_search(contents, matches, IMPORT_REGEX)) {
|
||||
auto importFile = findPath({matches[1].str()});
|
||||
if (!importFile.empty() && imports.find(importFile) == imports.end()) {
|
||||
imports[importFile] = false;
|
||||
}
|
||||
|
||||
contents = matches.suffix().str();
|
||||
}
|
||||
|
||||
imports[cssFile] = true;
|
||||
}
|
||||
|
||||
void waybar::CssReloadHelper::stop() {
|
||||
if (!m_running) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_running = false;
|
||||
m_cv.notify_all();
|
||||
if (m_thread.joinable()) {
|
||||
m_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void waybar::CssReloadHelper::watchFiles(const std::vector<std::string>& files) {
|
||||
auto inotifyFd = inotify_init1(IN_NONBLOCK);
|
||||
if (inotifyFd < 0) {
|
||||
spdlog::error("Failed to initialize inotify: {}", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<int> watchFds;
|
||||
for (const auto& file : files) {
|
||||
auto watchFd = inotify_add_watch(inotifyFd, file.c_str(), IN_MODIFY | IN_MOVED_TO);
|
||||
if (watchFd < 0) {
|
||||
spdlog::error("Failed to add watch for file: {}", file);
|
||||
} else {
|
||||
spdlog::debug("Added watch for file: {}", file);
|
||||
}
|
||||
watchFds.push_back(watchFd);
|
||||
}
|
||||
|
||||
auto pollFd = pollfd{inotifyFd, POLLIN, 0};
|
||||
|
||||
while (true) {
|
||||
if (watch(inotifyFd, &pollFd)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& watchFd : watchFds) {
|
||||
inotify_rm_watch(inotifyFd, watchFd);
|
||||
}
|
||||
|
||||
close(inotifyFd);
|
||||
}
|
||||
|
||||
bool waybar::CssReloadHelper::watch(int inotifyFd, pollfd* pollFd) {
|
||||
auto pollResult = poll(pollFd, 1, 10);
|
||||
if (pollResult < 0) {
|
||||
spdlog::error("Failed to poll inotify: {}", strerror(errno));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pollResult == 0) {
|
||||
// check if we should stop
|
||||
if (!m_running) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(m_mutex);
|
||||
// a condition variable is used to allow the thread to be stopped immediately while still not
|
||||
// spamming poll
|
||||
m_cv.wait_for(lock, std::chrono::milliseconds(250), [this] { return !m_running; });
|
||||
|
||||
// timeout
|
||||
return false;
|
||||
}
|
||||
|
||||
if (static_cast<bool>(pollFd->revents & POLLIN)) {
|
||||
if (handleInotifyEvents(inotifyFd)) {
|
||||
// after the callback is fired we need to re-parse the imports and setup the watches
|
||||
// again in case the import list has changed
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool waybar::CssReloadHelper::handleInotifyEvents(int inotify_fd) {
|
||||
// inotify event
|
||||
auto buffer = std::array<char, 4096>{};
|
||||
auto readResult = read(inotify_fd, buffer.data(), buffer.size());
|
||||
if (readResult < 0) {
|
||||
spdlog::error("Failed to read inotify event: {}", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto offset = 0;
|
||||
auto shouldFireCallback = false;
|
||||
|
||||
// read all events on the fd
|
||||
while (offset < readResult) {
|
||||
auto* event = reinterpret_cast<inotify_event*>(buffer.data() + offset);
|
||||
offset += sizeof(inotify_event) + event->len;
|
||||
shouldFireCallback = true;
|
||||
}
|
||||
|
||||
// we only need to fire the callback once
|
||||
if (shouldFireCallback) {
|
||||
m_callback();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
#include "util/css_reload_helper.hpp"
|
||||
#include <map>
|
||||
#include <fstream>
|
||||
|
||||
#if __has_include(<catch2/catch_test_macros.hpp>)
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_all.hpp>
|
||||
#else
|
||||
#include <catch2/catch.hpp>
|
||||
#endif
|
||||
|
||||
|
||||
class CssReloadHelperTest : public waybar::CssReloadHelper
|
||||
{
|
||||
public:
|
||||
CssReloadHelperTest()
|
||||
: CssReloadHelper("/tmp/waybar_test.css", [this]() {callback();})
|
||||
{
|
||||
}
|
||||
|
||||
void callback()
|
||||
{
|
||||
m_callbackCounter++;
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
std::string getFileContents(const std::string& filename) override
|
||||
{
|
||||
return m_fileContents[filename];
|
||||
}
|
||||
|
||||
std::string findPath(const std::string& filename) override
|
||||
{
|
||||
return filename;
|
||||
}
|
||||
|
||||
void setFileContents(const std::string& filename, const std::string& contents)
|
||||
{
|
||||
m_fileContents[filename] = contents;
|
||||
}
|
||||
|
||||
int getCallbackCounter() const
|
||||
{
|
||||
return m_callbackCounter;
|
||||
}
|
||||
|
||||
private:
|
||||
int m_callbackCounter{};
|
||||
std::map<std::string, std::string> m_fileContents;
|
||||
};
|
||||
|
||||
TEST_CASE_METHOD(CssReloadHelperTest, "parse_imports", "[util][css_reload_helper]")
|
||||
{
|
||||
SECTION("no imports")
|
||||
{
|
||||
setFileContents("/tmp/waybar_test.css", "body { color: red; }");
|
||||
auto files = parseImports("/tmp/waybar_test.css");
|
||||
REQUIRE(files.size() == 1);
|
||||
CHECK(files[0] == "/tmp/waybar_test.css");
|
||||
}
|
||||
|
||||
SECTION("single import")
|
||||
{
|
||||
setFileContents("/tmp/waybar_test.css", "@import 'test.css';");
|
||||
setFileContents("test.css", "body { color: red; }");
|
||||
auto files = parseImports("/tmp/waybar_test.css");
|
||||
std::sort(files.begin(), files.end());
|
||||
REQUIRE(files.size() == 2);
|
||||
CHECK(files[0] == "/tmp/waybar_test.css");
|
||||
CHECK(files[1] == "test.css");
|
||||
}
|
||||
|
||||
SECTION("multiple imports")
|
||||
{
|
||||
setFileContents("/tmp/waybar_test.css", "@import 'test.css'; @import 'test2.css';");
|
||||
setFileContents("test.css", "body { color: red; }");
|
||||
setFileContents("test2.css", "body { color: blue; }");
|
||||
auto files = parseImports("/tmp/waybar_test.css");
|
||||
std::sort(files.begin(), files.end());
|
||||
REQUIRE(files.size() == 3);
|
||||
CHECK(files[0] == "/tmp/waybar_test.css");
|
||||
CHECK(files[1] == "test.css");
|
||||
CHECK(files[2] == "test2.css");
|
||||
}
|
||||
|
||||
SECTION("nested imports")
|
||||
{
|
||||
setFileContents("/tmp/waybar_test.css", "@import 'test.css';");
|
||||
setFileContents("test.css", "@import 'test2.css';");
|
||||
setFileContents("test2.css", "body { color: red; }");
|
||||
auto files = parseImports("/tmp/waybar_test.css");
|
||||
std::sort(files.begin(), files.end());
|
||||
REQUIRE(files.size() == 3);
|
||||
CHECK(files[0] == "/tmp/waybar_test.css");
|
||||
CHECK(files[1] == "test.css");
|
||||
CHECK(files[2] == "test2.css");
|
||||
}
|
||||
|
||||
SECTION("circular imports")
|
||||
{
|
||||
setFileContents("/tmp/waybar_test.css", "@import 'test.css';");
|
||||
setFileContents("test.css", "@import 'test2.css';");
|
||||
setFileContents("test2.css", "@import 'test.css';");
|
||||
auto files = parseImports("/tmp/waybar_test.css");
|
||||
std::sort(files.begin(), files.end());
|
||||
REQUIRE(files.size() == 3);
|
||||
CHECK(files[0] == "/tmp/waybar_test.css");
|
||||
CHECK(files[1] == "test.css");
|
||||
CHECK(files[2] == "test2.css");
|
||||
}
|
||||
|
||||
SECTION("empty")
|
||||
{
|
||||
setFileContents("/tmp/waybar_test.css", "");
|
||||
auto files = parseImports("/tmp/waybar_test.css");
|
||||
REQUIRE(files.size() == 1);
|
||||
CHECK(files[0] == "/tmp/waybar_test.css");
|
||||
}
|
||||
|
||||
SECTION("empty name")
|
||||
{
|
||||
auto files = parseImports("");
|
||||
REQUIRE(files.empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("file_watcher", "[util][css_reload_helper]")
|
||||
{
|
||||
SECTION("file does not exist")
|
||||
{
|
||||
std::atomic<int> count;
|
||||
std::string f1 = std::tmpnam(nullptr);
|
||||
waybar::CssReloadHelper helper(f1, [&count](){++count;});
|
||||
helper.monitorChanges();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
CHECK(count == 0);
|
||||
helper.stop();
|
||||
std::remove(f1.c_str());
|
||||
}
|
||||
|
||||
SECTION("file exists")
|
||||
{
|
||||
std::atomic<int> count;
|
||||
std::string f1 = std::tmpnam(nullptr);
|
||||
std::ofstream(f1) << "body { color: red; }";
|
||||
waybar::CssReloadHelper helper(f1, [&count](){++count;});
|
||||
helper.monitorChanges();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
CHECK(count == 0);
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
std::ofstream(f1) << "body { color: blue; }";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
CHECK(count == 1);
|
||||
helper.stop();
|
||||
std::remove(f1.c_str());
|
||||
}
|
||||
|
||||
SECTION("multiple files")
|
||||
{
|
||||
std::atomic<int> count;
|
||||
std::string f1 = std::tmpnam(nullptr);
|
||||
std::string f2 = std::tmpnam(nullptr);
|
||||
std::ofstream(f1) << ("@import '" + f2 + " ';");
|
||||
std::ofstream(f2) << "body { color: red; }";
|
||||
waybar::CssReloadHelper helper(f1, [&count](){++count;});
|
||||
helper.monitorChanges();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
CHECK(count == 0);
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
std::ofstream(f2) << "body { color: blue; }";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
CHECK(count == 1);
|
||||
helper.stop();
|
||||
std::remove(f1.c_str());
|
||||
std::remove(f2.c_str());
|
||||
}
|
||||
}
|
|
@ -10,7 +10,9 @@ test_src = files(
|
|||
'main.cpp',
|
||||
'SafeSignal.cpp',
|
||||
'config.cpp',
|
||||
'css_reload_helper.cpp',
|
||||
'../src/config.cpp',
|
||||
'../src/util/css_reload_helper.cpp',
|
||||
)
|
||||
|
||||
if tz_dep.found()
|
||||
|
|
Loading…
Reference in New Issue