Merge pull request #2852 from dpayne/add_css_reload
Adding css reloader
This commit is contained in:
commit
1dce607c42
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
#include "bar.hpp"
|
#include "bar.hpp"
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
|
#include "util/css_reload_helper.hpp"
|
||||||
#include "util/portal.hpp"
|
#include "util/portal.hpp"
|
||||||
|
|
||||||
struct zwlr_layer_shell_v1;
|
struct zwlr_layer_shell_v1;
|
||||||
|
@ -55,6 +56,8 @@ class Client {
|
||||||
Glib::RefPtr<Gtk::CssProvider> css_provider_;
|
Glib::RefPtr<Gtk::CssProvider> css_provider_;
|
||||||
std::unique_ptr<Portal> portal;
|
std::unique_ptr<Portal> portal;
|
||||||
std::list<struct waybar_output> outputs_;
|
std::list<struct waybar_output> outputs_;
|
||||||
|
std::unique_ptr<CssReloadHelper> m_cssReloadHelper;
|
||||||
|
std::string m_cssFile;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace waybar
|
} // namespace waybar
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "giomm/file.h"
|
||||||
|
#include "giomm/filemonitor.h"
|
||||||
|
#include "glibmm/refptr.h"
|
||||||
|
|
||||||
|
struct pollfd;
|
||||||
|
|
||||||
|
namespace waybar {
|
||||||
|
class CssReloadHelper {
|
||||||
|
public:
|
||||||
|
CssReloadHelper(std::string cssFile, std::function<void()> callback);
|
||||||
|
|
||||||
|
virtual void monitorChanges();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
void handleFileChange(Glib::RefPtr<Gio::File> const& file,
|
||||||
|
Glib::RefPtr<Gio::File> const& other_type,
|
||||||
|
Gio::FileMonitorEvent event_type);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string m_cssFile;
|
||||||
|
|
||||||
|
std::function<void()> m_callback;
|
||||||
|
|
||||||
|
std::vector<std::tuple<Glib::RefPtr<Gio::FileMonitor>>> m_fileMonitors;
|
||||||
|
};
|
||||||
|
} // namespace waybar
|
|
@ -130,6 +130,11 @@ Also, a minimal example configuration can be found at the bottom of this man pag
|
||||||
Each file can contain a single object with any of the bar configuration options. In case of duplicate options, the first defined value takes precedence, i.e. including file -> first included file -> etc. Nested includes are permitted, but make sure to avoid circular imports.
|
Each file can contain a single object with any of the bar configuration options. In case of duplicate options, the first defined value takes precedence, i.e. including file -> first included file -> etc. Nested includes are permitted, but make sure to avoid circular imports.
|
||||||
For a multi-bar config, the include directive affects only current bar configuration object.
|
For a multi-bar config, the include directive affects only current bar configuration object.
|
||||||
|
|
||||||
|
*reload_style_on_change* ++
|
||||||
|
typeof: bool ++
|
||||||
|
default: *false* ++
|
||||||
|
Option to enable reloading the css style if a modification is detected on the style sheet file or any imported css files.
|
||||||
|
|
||||||
# MODULE FORMAT
|
# MODULE FORMAT
|
||||||
|
|
||||||
You can use PangoMarkupFormat (See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html#PangoMarkupFormat).
|
You can use PangoMarkupFormat (See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html#PangoMarkupFormat).
|
||||||
|
|
|
@ -196,7 +196,8 @@ src_files = files(
|
||||||
'src/util/sanitize_str.cpp',
|
'src/util/sanitize_str.cpp',
|
||||||
'src/util/rewrite_string.cpp',
|
'src/util/rewrite_string.cpp',
|
||||||
'src/util/gtk_icon.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']
|
inc_dirs = ['include']
|
||||||
|
|
|
@ -262,15 +262,21 @@ int waybar::Client::main(int argc, char *argv[]) {
|
||||||
if (!portal) {
|
if (!portal) {
|
||||||
portal = std::make_unique<waybar::Portal>();
|
portal = std::make_unique<waybar::Portal>();
|
||||||
}
|
}
|
||||||
auto css_file = getStyle(style_opt);
|
m_cssFile = getStyle(style_opt);
|
||||||
setupCss(css_file);
|
setupCss(m_cssFile);
|
||||||
|
m_cssReloadHelper = std::make_unique<CssReloadHelper>(m_cssFile, [&]() { setupCss(m_cssFile); });
|
||||||
portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) {
|
portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) {
|
||||||
auto css_file = getStyle(style_opt, appearance);
|
auto css_file = getStyle(style_opt, appearance);
|
||||||
setupCss(css_file);
|
setupCss(css_file);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (config.getConfig()["reload_style_on_change"].asBool()) {
|
||||||
|
m_cssReloadHelper->monitorChanges();
|
||||||
|
}
|
||||||
bindInterfaces();
|
bindInterfaces();
|
||||||
gtk_app->hold();
|
gtk_app->hold();
|
||||||
gtk_app->run();
|
gtk_app->run();
|
||||||
|
m_cssReloadHelper.reset(); // stop watching css file
|
||||||
bars.clear();
|
bars.clear();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
#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"
|
||||||
|
#include "giomm/file.h"
|
||||||
|
#include "glibmm/refptr.h"
|
||||||
|
|
||||||
|
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)) {}
|
||||||
|
|
||||||
|
std::string waybar::CssReloadHelper::getFileContents(const std::string& filename) {
|
||||||
|
if (filename.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream file(filename);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {(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
|
||||||
|
std::string result;
|
||||||
|
if (std::filesystem::exists(filename)) {
|
||||||
|
result = filename;
|
||||||
|
} else {
|
||||||
|
result = Config::findConfigPath({filename}).value_or("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// File monitor does not work with symlinks, so resolve them
|
||||||
|
if (std::filesystem::is_symlink(result)) {
|
||||||
|
result = std::filesystem::read_symlink(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void waybar::CssReloadHelper::monitorChanges() {
|
||||||
|
auto files = parseImports(m_cssFile);
|
||||||
|
for (const auto& file : files) {
|
||||||
|
auto gioFile = Gio::File::create_for_path(file);
|
||||||
|
if (!gioFile) {
|
||||||
|
spdlog::error("Failed to create file for path: {}", file);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fileMonitor = gioFile->monitor_file();
|
||||||
|
if (!fileMonitor) {
|
||||||
|
spdlog::error("Failed to create file monitor for path: {}", file);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto connection = fileMonitor->signal_changed().connect(
|
||||||
|
sigc::mem_fun(*this, &CssReloadHelper::handleFileChange));
|
||||||
|
|
||||||
|
if (!connection.connected()) {
|
||||||
|
spdlog::error("Failed to connect to file monitor for path: {}", file);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_fileMonitors.emplace_back(std::move(fileMonitor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr<Gio::File> const& file,
|
||||||
|
Glib::RefPtr<Gio::File> const& other_type,
|
||||||
|
Gio::FileMonitorEvent event_type) {
|
||||||
|
// Multiple events are fired on file changed (attributes, write, changes done hint, etc.), only
|
||||||
|
// fire for one
|
||||||
|
if (event_type == Gio::FileMonitorEvent::FILE_MONITOR_EVENT_CHANGES_DONE_HINT) {
|
||||||
|
spdlog::debug("Reloading style, file changed: {}", file->get_path());
|
||||||
|
m_callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
#include "util/css_reload_helper.hpp"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
#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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,9 @@ test_src = files(
|
||||||
'JsonParser.cpp',
|
'JsonParser.cpp',
|
||||||
'SafeSignal.cpp',
|
'SafeSignal.cpp',
|
||||||
'config.cpp',
|
'config.cpp',
|
||||||
|
'css_reload_helper.cpp',
|
||||||
'../src/config.cpp',
|
'../src/config.cpp',
|
||||||
|
'../src/util/css_reload_helper.cpp',
|
||||||
)
|
)
|
||||||
|
|
||||||
if tz_dep.found()
|
if tz_dep.found()
|
||||||
|
|
Loading…
Reference in New Issue