Merge pull request #1243 from alebastr/config-unittest
Unit-tests for configuration includes
This commit is contained in:
commit
b028a47d57
|
@ -15,9 +15,10 @@ jobs:
|
||||||
export CPPFLAGS=-isystem/usr/local/include LDFLAGS=-L/usr/local/lib # sndio
|
export CPPFLAGS=-isystem/usr/local/include LDFLAGS=-L/usr/local/lib # sndio
|
||||||
sed -i '' 's/quarterly/latest/' /etc/pkg/FreeBSD.conf
|
sed -i '' 's/quarterly/latest/' /etc/pkg/FreeBSD.conf
|
||||||
pkg install -y git # subprojects/date
|
pkg install -y git # subprojects/date
|
||||||
pkg install -y evdev-proto gtk-layer-shell gtkmm30 jsoncpp libdbusmenu \
|
pkg install -y catch evdev-proto gtk-layer-shell gtkmm30 jsoncpp \
|
||||||
libevdev libfmt libmpdclient libudev-devd meson pkgconf pulseaudio \
|
libdbusmenu libevdev libfmt libmpdclient libudev-devd meson \
|
||||||
scdoc sndio spdlog
|
pkgconf pulseaudio scdoc sndio spdlog
|
||||||
run: |
|
run: |
|
||||||
meson build -Dman-pages=enabled
|
meson build -Dman-pages=enabled
|
||||||
ninja -C build
|
ninja -C build
|
||||||
|
meson test -C build --no-rebuild --print-errorlogs --suite waybar
|
||||||
|
|
|
@ -23,3 +23,5 @@ jobs:
|
||||||
run: meson -Dman-pages=enabled build
|
run: meson -Dman-pages=enabled build
|
||||||
- name: build
|
- name: build
|
||||||
run: ninja -C build
|
run: ninja -C build
|
||||||
|
- name: test
|
||||||
|
run: meson test -C build --no-rebuild --print-errorlogs --suite waybar
|
||||||
|
|
|
@ -3,11 +3,10 @@
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
#include <gdk/gdk.h>
|
#include <gdk/gdk.h>
|
||||||
#include <gdk/gdkwayland.h>
|
#include <gdk/gdkwayland.h>
|
||||||
#include <unistd.h>
|
|
||||||
#include <wayland-client.h>
|
#include <wayland-client.h>
|
||||||
#include <wordexp.h>
|
|
||||||
|
|
||||||
#include "bar.hpp"
|
#include "bar.hpp"
|
||||||
|
#include "config.hpp"
|
||||||
|
|
||||||
struct zwlr_layer_shell_v1;
|
struct zwlr_layer_shell_v1;
|
||||||
struct zwp_idle_inhibitor_v1;
|
struct zwp_idle_inhibitor_v1;
|
||||||
|
@ -29,18 +28,13 @@ class Client {
|
||||||
struct zxdg_output_manager_v1 * xdg_output_manager = nullptr;
|
struct zxdg_output_manager_v1 * xdg_output_manager = nullptr;
|
||||||
struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr;
|
struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr;
|
||||||
std::vector<std::unique_ptr<Bar>> bars;
|
std::vector<std::unique_ptr<Bar>> bars;
|
||||||
|
Config config;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Client() = default;
|
Client() = default;
|
||||||
std::tuple<const std::string, const std::string> getConfigs(const std::string &config,
|
const std::string getStyle(const std::string &style);
|
||||||
const std::string &style) const;
|
|
||||||
void bindInterfaces();
|
void bindInterfaces();
|
||||||
const std::string getValidPath(const std::vector<std::string> &paths) const;
|
|
||||||
void handleOutput(struct waybar_output &output);
|
void handleOutput(struct waybar_output &output);
|
||||||
bool isValidOutput(const Json::Value &config, struct waybar_output &output);
|
|
||||||
auto setupConfig(const std::string &config_file, int depth) -> void;
|
|
||||||
auto resolveConfigIncludes(Json::Value &config, int depth) -> void;
|
|
||||||
auto mergeConfig(Json::Value &a_config_, Json::Value &b_config_) -> void;
|
|
||||||
auto setupCss(const std::string &css_file) -> void;
|
auto setupCss(const std::string &css_file) -> void;
|
||||||
struct waybar_output & getOutput(void *);
|
struct waybar_output & getOutput(void *);
|
||||||
std::vector<Json::Value> getOutputConfigs(struct waybar_output &output);
|
std::vector<Json::Value> getOutputConfigs(struct waybar_output &output);
|
||||||
|
@ -55,7 +49,6 @@ class Client {
|
||||||
void handleMonitorRemoved(Glib::RefPtr<Gdk::Monitor> monitor);
|
void handleMonitorRemoved(Glib::RefPtr<Gdk::Monitor> monitor);
|
||||||
void handleDeferredMonitorRemoval(Glib::RefPtr<Gdk::Monitor> monitor);
|
void handleDeferredMonitorRemoval(Glib::RefPtr<Gdk::Monitor> monitor);
|
||||||
|
|
||||||
Json::Value config_;
|
|
||||||
Glib::RefPtr<Gtk::StyleContext> style_context_;
|
Glib::RefPtr<Gtk::StyleContext> style_context_;
|
||||||
Glib::RefPtr<Gtk::CssProvider> css_provider_;
|
Glib::RefPtr<Gtk::CssProvider> css_provider_;
|
||||||
std::list<struct waybar_output> outputs_;
|
std::list<struct waybar_output> outputs_;
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <json/json.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#ifndef SYSCONFDIR
|
||||||
|
#define SYSCONFDIR "/etc"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace waybar {
|
||||||
|
|
||||||
|
class Config {
|
||||||
|
public:
|
||||||
|
static const std::vector<std::string> CONFIG_DIRS;
|
||||||
|
|
||||||
|
/* Try to find any of provided names in the supported set of config directories */
|
||||||
|
static std::optional<std::string> findConfigPath(
|
||||||
|
const std::vector<std::string> &names, const std::vector<std::string> &dirs = CONFIG_DIRS);
|
||||||
|
|
||||||
|
Config() = default;
|
||||||
|
|
||||||
|
void load(const std::string &config);
|
||||||
|
|
||||||
|
Json::Value &getConfig() { return config_; }
|
||||||
|
|
||||||
|
std::vector<Json::Value> getOutputConfigs(const std::string &name, const std::string &identifier);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupConfig(Json::Value &dst, const std::string &config_file, int depth);
|
||||||
|
void resolveConfigIncludes(Json::Value &config, int depth);
|
||||||
|
void mergeConfig(Json::Value &a_config_, Json::Value &b_config_);
|
||||||
|
|
||||||
|
std::string config_file_;
|
||||||
|
|
||||||
|
Json::Value config_;
|
||||||
|
};
|
||||||
|
} // namespace waybar
|
|
@ -87,8 +87,9 @@ Also a minimal example configuration can be found on the at the bottom of this m
|
||||||
|
|
||||||
*include* ++
|
*include* ++
|
||||||
typeof: string|array ++
|
typeof: string|array ++
|
||||||
Paths to additional configuration files. In case of duplicate options, the including file's value takes precedence. Make sure to avoid circular imports.
|
Paths to additional configuration files.
|
||||||
For a multi-bar config, specify at least an empty object for each bar also in every file being included.
|
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.
|
||||||
|
|
||||||
# MODULE FORMAT
|
# MODULE FORMAT
|
||||||
|
|
||||||
|
|
10
meson.build
10
meson.build
|
@ -149,6 +149,7 @@ src_files = files(
|
||||||
'src/main.cpp',
|
'src/main.cpp',
|
||||||
'src/bar.cpp',
|
'src/bar.cpp',
|
||||||
'src/client.cpp',
|
'src/client.cpp',
|
||||||
|
'src/config.cpp',
|
||||||
'src/util/ustring_clen.cpp'
|
'src/util/ustring_clen.cpp'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -359,6 +360,15 @@ if scdoc.found()
|
||||||
endforeach
|
endforeach
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
catch2 = dependency(
|
||||||
|
'catch2',
|
||||||
|
fallback: ['catch2', 'catch2_dep'],
|
||||||
|
required: get_option('tests'),
|
||||||
|
)
|
||||||
|
if catch2.found()
|
||||||
|
subdir('test')
|
||||||
|
endif
|
||||||
|
|
||||||
clangtidy = find_program('clang-tidy', required: false)
|
clangtidy = find_program('clang-tidy', required: false)
|
||||||
|
|
||||||
if clangtidy.found()
|
if clangtidy.found()
|
||||||
|
|
|
@ -10,3 +10,4 @@ option('mpd', type: 'feature', value: 'auto', description: 'Enable support for t
|
||||||
option('gtk-layer-shell', type: 'feature', value: 'auto', description: 'Use gtk-layer-shell library for popups support')
|
option('gtk-layer-shell', type: 'feature', value: 'auto', description: 'Use gtk-layer-shell library for popups support')
|
||||||
option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL')
|
option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL')
|
||||||
option('sndio', type: 'feature', value: 'auto', description: 'Enable support for sndio')
|
option('sndio', type: 'feature', value: 'auto', description: 'Enable support for sndio')
|
||||||
|
option('tests', type: 'feature', value: 'auto', description: 'Enable tests')
|
||||||
|
|
160
src/client.cpp
160
src/client.cpp
|
@ -3,12 +3,10 @@
|
||||||
#include <fmt/ostream.h>
|
#include <fmt/ostream.h>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
#include "idle-inhibit-unstable-v1-client-protocol.h"
|
#include "idle-inhibit-unstable-v1-client-protocol.h"
|
||||||
#include "util/clara.hpp"
|
#include "util/clara.hpp"
|
||||||
#include "util/json.hpp"
|
|
||||||
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
|
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
|
||||||
|
|
||||||
waybar::Client *waybar::Client::inst() {
|
waybar::Client *waybar::Client::inst() {
|
||||||
|
@ -16,23 +14,6 @@ waybar::Client *waybar::Client::inst() {
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string waybar::Client::getValidPath(const std::vector<std::string> &paths) const {
|
|
||||||
wordexp_t p;
|
|
||||||
|
|
||||||
for (const std::string &path : paths) {
|
|
||||||
if (wordexp(path.c_str(), &p, 0) == 0) {
|
|
||||||
if (access(*p.we_wordv, F_OK) == 0) {
|
|
||||||
std::string result = *p.we_wordv;
|
|
||||||
wordfree(&p);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
wordfree(&p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::string();
|
|
||||||
}
|
|
||||||
|
|
||||||
void waybar::Client::handleGlobal(void *data, struct wl_registry *registry, uint32_t name,
|
void waybar::Client::handleGlobal(void *data, struct wl_registry *registry, uint32_t name,
|
||||||
const char *interface, uint32_t version) {
|
const char *interface, uint32_t version) {
|
||||||
auto client = static_cast<Client *>(data);
|
auto client = static_cast<Client *>(data);
|
||||||
|
@ -70,29 +51,6 @@ void waybar::Client::handleOutput(struct waybar_output &output) {
|
||||||
zxdg_output_v1_add_listener(output.xdg_output.get(), &xdgOutputListener, &output);
|
zxdg_output_v1_add_listener(output.xdg_output.get(), &xdgOutputListener, &output);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool waybar::Client::isValidOutput(const Json::Value &config, struct waybar_output &output) {
|
|
||||||
if (config["output"].isArray()) {
|
|
||||||
for (auto const &output_conf : config["output"]) {
|
|
||||||
if (output_conf.isString() &&
|
|
||||||
(output_conf.asString() == output.name || output_conf.asString() == output.identifier)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} else if (config["output"].isString()) {
|
|
||||||
auto config_output = config["output"].asString();
|
|
||||||
if (!config_output.empty()) {
|
|
||||||
if (config_output.substr(0, 1) == "!") {
|
|
||||||
return config_output.substr(1) != output.name &&
|
|
||||||
config_output.substr(1) != output.identifier;
|
|
||||||
}
|
|
||||||
return config_output == output.name || config_output == output.identifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct waybar::waybar_output &waybar::Client::getOutput(void *addr) {
|
struct waybar::waybar_output &waybar::Client::getOutput(void *addr) {
|
||||||
auto it = std::find_if(
|
auto it = std::find_if(
|
||||||
outputs_.begin(), outputs_.end(), [&addr](const auto &output) { return &output == addr; });
|
outputs_.begin(), outputs_.end(), [&addr](const auto &output) { return &output == addr; });
|
||||||
|
@ -103,17 +61,7 @@ struct waybar::waybar_output &waybar::Client::getOutput(void *addr) {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Json::Value> waybar::Client::getOutputConfigs(struct waybar_output &output) {
|
std::vector<Json::Value> waybar::Client::getOutputConfigs(struct waybar_output &output) {
|
||||||
std::vector<Json::Value> configs;
|
return config.getOutputConfigs(output.name, output.identifier);
|
||||||
if (config_.isArray()) {
|
|
||||||
for (auto const &config : config_) {
|
|
||||||
if (config.isObject() && isValidOutput(config, output)) {
|
|
||||||
configs.push_back(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (isValidOutput(config_, output)) {
|
|
||||||
configs.push_back(config_);
|
|
||||||
}
|
|
||||||
return configs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_output*/) {
|
void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_output*/) {
|
||||||
|
@ -203,94 +151,14 @@ void waybar::Client::handleDeferredMonitorRemoval(Glib::RefPtr<Gdk::Monitor> mon
|
||||||
outputs_.remove_if([&monitor](const auto &output) { return output.monitor == monitor; });
|
outputs_.remove_if([&monitor](const auto &output) { return output.monitor == monitor; });
|
||||||
}
|
}
|
||||||
|
|
||||||
std::tuple<const std::string, const std::string> waybar::Client::getConfigs(
|
const std::string waybar::Client::getStyle(const std::string &style) {
|
||||||
const std::string &config, const std::string &style) const {
|
auto css_file = style.empty() ? Config::findConfigPath({"style.css"}) : style;
|
||||||
auto config_file = config.empty() ? getValidPath({
|
if (!css_file) {
|
||||||
"$XDG_CONFIG_HOME/waybar/config",
|
throw std::runtime_error("Missing required resource files");
|
||||||
"$XDG_CONFIG_HOME/waybar/config.jsonc",
|
|
||||||
"$HOME/.config/waybar/config",
|
|
||||||
"$HOME/.config/waybar/config.jsonc",
|
|
||||||
"$HOME/waybar/config",
|
|
||||||
"$HOME/waybar/config.jsonc",
|
|
||||||
"/etc/xdg/waybar/config",
|
|
||||||
"/etc/xdg/waybar/config.jsonc",
|
|
||||||
SYSCONFDIR "/xdg/waybar/config",
|
|
||||||
"./resources/config",
|
|
||||||
})
|
|
||||||
: config;
|
|
||||||
auto css_file = style.empty() ? getValidPath({
|
|
||||||
"$XDG_CONFIG_HOME/waybar/style.css",
|
|
||||||
"$HOME/.config/waybar/style.css",
|
|
||||||
"$HOME/waybar/style.css",
|
|
||||||
"/etc/xdg/waybar/style.css",
|
|
||||||
SYSCONFDIR "/xdg/waybar/style.css",
|
|
||||||
"./resources/style.css",
|
|
||||||
})
|
|
||||||
: style;
|
|
||||||
if (css_file.empty() || config_file.empty()) {
|
|
||||||
throw std::runtime_error("Missing required resources files");
|
|
||||||
}
|
}
|
||||||
spdlog::info("Resources files: {}, {}", config_file, css_file);
|
spdlog::info("Using CSS file {}", css_file.value());
|
||||||
return {config_file, css_file};
|
return css_file.value();
|
||||||
}
|
};
|
||||||
|
|
||||||
auto waybar::Client::setupConfig(const std::string &config_file, int depth) -> void {
|
|
||||||
if (depth > 100) {
|
|
||||||
throw std::runtime_error("Aborting due to likely recursive include in config files");
|
|
||||||
}
|
|
||||||
std::ifstream file(config_file);
|
|
||||||
if (!file.is_open()) {
|
|
||||||
throw std::runtime_error("Can't open config file");
|
|
||||||
}
|
|
||||||
std::string str((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
||||||
util::JsonParser parser;
|
|
||||||
Json::Value tmp_config_ = parser.parse(str);
|
|
||||||
if (tmp_config_.isArray()) {
|
|
||||||
for (auto &config_part : tmp_config_) {
|
|
||||||
resolveConfigIncludes(config_part, depth);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolveConfigIncludes(tmp_config_, depth);
|
|
||||||
}
|
|
||||||
mergeConfig(config_, tmp_config_);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto waybar::Client::resolveConfigIncludes(Json::Value &config, int depth) -> void {
|
|
||||||
Json::Value includes = config["include"];
|
|
||||||
if (includes.isArray()) {
|
|
||||||
for (const auto &include : includes) {
|
|
||||||
spdlog::info("Including resource file: {}", include.asString());
|
|
||||||
setupConfig(getValidPath({include.asString()}), ++depth);
|
|
||||||
}
|
|
||||||
} else if (includes.isString()) {
|
|
||||||
spdlog::info("Including resource file: {}", includes.asString());
|
|
||||||
setupConfig(getValidPath({includes.asString()}), ++depth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto waybar::Client::mergeConfig(Json::Value &a_config_, Json::Value &b_config_) -> void {
|
|
||||||
if (!a_config_) {
|
|
||||||
// For the first config
|
|
||||||
a_config_ = b_config_;
|
|
||||||
} else if (a_config_.isObject() && b_config_.isObject()) {
|
|
||||||
for (const auto &key : b_config_.getMemberNames()) {
|
|
||||||
if (a_config_[key].isObject() && b_config_[key].isObject()) {
|
|
||||||
mergeConfig(a_config_[key], b_config_[key]);
|
|
||||||
} else {
|
|
||||||
a_config_[key] = b_config_[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (a_config_.isArray() && b_config_.isArray()) {
|
|
||||||
// This can happen only on the top-level array of a multi-bar config
|
|
||||||
for (Json::Value::ArrayIndex i = 0; i < b_config_.size(); i++) {
|
|
||||||
if (a_config_[i].isObject() && b_config_[i].isObject()) {
|
|
||||||
mergeConfig(a_config_[i], b_config_[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
spdlog::error("Cannot merge config, conflicting or invalid JSON types");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto waybar::Client::setupCss(const std::string &css_file) -> void {
|
auto waybar::Client::setupCss(const std::string &css_file) -> void {
|
||||||
css_provider_ = Gtk::CssProvider::create();
|
css_provider_ = Gtk::CssProvider::create();
|
||||||
|
@ -329,14 +197,14 @@ void waybar::Client::bindInterfaces() {
|
||||||
int waybar::Client::main(int argc, char *argv[]) {
|
int waybar::Client::main(int argc, char *argv[]) {
|
||||||
bool show_help = false;
|
bool show_help = false;
|
||||||
bool show_version = false;
|
bool show_version = false;
|
||||||
std::string config;
|
std::string config_opt;
|
||||||
std::string style;
|
std::string style_opt;
|
||||||
std::string bar_id;
|
std::string bar_id;
|
||||||
std::string log_level;
|
std::string log_level;
|
||||||
auto cli = clara::detail::Help(show_help) |
|
auto cli = clara::detail::Help(show_help) |
|
||||||
clara::detail::Opt(show_version)["-v"]["--version"]("Show version") |
|
clara::detail::Opt(show_version)["-v"]["--version"]("Show version") |
|
||||||
clara::detail::Opt(config, "config")["-c"]["--config"]("Config path") |
|
clara::detail::Opt(config_opt, "config")["-c"]["--config"]("Config path") |
|
||||||
clara::detail::Opt(style, "style")["-s"]["--style"]("Style path") |
|
clara::detail::Opt(style_opt, "style")["-s"]["--style"]("Style path") |
|
||||||
clara::detail::Opt(
|
clara::detail::Opt(
|
||||||
log_level,
|
log_level,
|
||||||
"trace|debug|info|warning|error|critical|off")["-l"]["--log-level"]("Log level") |
|
"trace|debug|info|warning|error|critical|off")["-l"]["--log-level"]("Log level") |
|
||||||
|
@ -367,8 +235,8 @@ int waybar::Client::main(int argc, char *argv[]) {
|
||||||
throw std::runtime_error("Bar need to run under Wayland");
|
throw std::runtime_error("Bar need to run under Wayland");
|
||||||
}
|
}
|
||||||
wl_display = gdk_wayland_display_get_wl_display(gdk_display->gobj());
|
wl_display = gdk_wayland_display_get_wl_display(gdk_display->gobj());
|
||||||
auto [config_file, css_file] = getConfigs(config, style);
|
config.load(config_opt);
|
||||||
setupConfig(config_file, 0);
|
auto css_file = getStyle(style_opt);
|
||||||
setupCss(css_file);
|
setupCss(css_file);
|
||||||
bindInterfaces();
|
bindInterfaces();
|
||||||
gtk_app->hold();
|
gtk_app->hold();
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
#include "config.hpp"
|
||||||
|
|
||||||
|
#include <fmt/ostream.h>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <wordexp.h>
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "util/json.hpp"
|
||||||
|
|
||||||
|
namespace waybar {
|
||||||
|
|
||||||
|
const std::vector<std::string> Config::CONFIG_DIRS = {
|
||||||
|
"$XDG_CONFIG_HOME/waybar/",
|
||||||
|
"$HOME/.config/waybar/",
|
||||||
|
"$HOME/waybar/",
|
||||||
|
"/etc/xdg/waybar/",
|
||||||
|
SYSCONFDIR "/xdg/waybar/",
|
||||||
|
"./resources/",
|
||||||
|
};
|
||||||
|
|
||||||
|
std::optional<std::string> tryExpandPath(const std::string &path) {
|
||||||
|
wordexp_t p;
|
||||||
|
if (wordexp(path.c_str(), &p, 0) == 0) {
|
||||||
|
if (access(*p.we_wordv, F_OK) == 0) {
|
||||||
|
std::string result = *p.we_wordv;
|
||||||
|
wordfree(&p);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
wordfree(&p);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> Config::findConfigPath(const std::vector<std::string> &names,
|
||||||
|
const std::vector<std::string> &dirs) {
|
||||||
|
std::vector<std::string> paths;
|
||||||
|
for (const auto &dir : dirs) {
|
||||||
|
for (const auto &name : names) {
|
||||||
|
if (auto res = tryExpandPath(dir + name); res) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::setupConfig(Json::Value &dst, const std::string &config_file, int depth) {
|
||||||
|
if (depth > 100) {
|
||||||
|
throw std::runtime_error("Aborting due to likely recursive include in config files");
|
||||||
|
}
|
||||||
|
std::ifstream file(config_file);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
throw std::runtime_error("Can't open config file");
|
||||||
|
}
|
||||||
|
std::string str((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||||
|
util::JsonParser parser;
|
||||||
|
Json::Value tmp_config = parser.parse(str);
|
||||||
|
if (tmp_config.isArray()) {
|
||||||
|
for (auto &config_part : tmp_config) {
|
||||||
|
resolveConfigIncludes(config_part, depth);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolveConfigIncludes(tmp_config, depth);
|
||||||
|
}
|
||||||
|
mergeConfig(dst, tmp_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::resolveConfigIncludes(Json::Value &config, int depth) {
|
||||||
|
Json::Value includes = config["include"];
|
||||||
|
if (includes.isArray()) {
|
||||||
|
for (const auto &include : includes) {
|
||||||
|
spdlog::info("Including resource file: {}", include.asString());
|
||||||
|
setupConfig(config, tryExpandPath(include.asString()).value_or(""), ++depth);
|
||||||
|
}
|
||||||
|
} else if (includes.isString()) {
|
||||||
|
spdlog::info("Including resource file: {}", includes.asString());
|
||||||
|
setupConfig(config, tryExpandPath(includes.asString()).value_or(""), ++depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::mergeConfig(Json::Value &a_config_, Json::Value &b_config_) {
|
||||||
|
if (!a_config_) {
|
||||||
|
// For the first config
|
||||||
|
a_config_ = b_config_;
|
||||||
|
} else if (a_config_.isObject() && b_config_.isObject()) {
|
||||||
|
for (const auto &key : b_config_.getMemberNames()) {
|
||||||
|
// [] creates key with default value. Use `get` to avoid that.
|
||||||
|
if (a_config_.get(key, Json::Value::nullSingleton()).isObject() &&
|
||||||
|
b_config_[key].isObject()) {
|
||||||
|
mergeConfig(a_config_[key], b_config_[key]);
|
||||||
|
} else if (!a_config_.isMember(key)) {
|
||||||
|
// do not allow overriding value set by top or previously included config
|
||||||
|
a_config_[key] = b_config_[key];
|
||||||
|
} else {
|
||||||
|
spdlog::trace("Option {} is already set; ignoring value {}", key, b_config_[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spdlog::error("Cannot merge config, conflicting or invalid JSON types");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool isValidOutput(const Json::Value &config, const std::string &name,
|
||||||
|
const std::string &identifier) {
|
||||||
|
if (config["output"].isArray()) {
|
||||||
|
for (auto const &output_conf : config["output"]) {
|
||||||
|
if (output_conf.isString() &&
|
||||||
|
(output_conf.asString() == name || output_conf.asString() == identifier)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else if (config["output"].isString()) {
|
||||||
|
auto config_output = config["output"].asString();
|
||||||
|
if (!config_output.empty()) {
|
||||||
|
if (config_output.substr(0, 1) == "!") {
|
||||||
|
return config_output.substr(1) != name && config_output.substr(1) != identifier;
|
||||||
|
}
|
||||||
|
return config_output == name || config_output == identifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::load(const std::string &config) {
|
||||||
|
auto file = config.empty() ? findConfigPath({"config", "config.jsonc"}) : config;
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Missing required resource files");
|
||||||
|
}
|
||||||
|
config_file_ = file.value();
|
||||||
|
spdlog::info("Using configuration file {}", config_file_);
|
||||||
|
setupConfig(config_, config_file_, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Json::Value> Config::getOutputConfigs(const std::string &name,
|
||||||
|
const std::string &identifier) {
|
||||||
|
std::vector<Json::Value> configs;
|
||||||
|
if (config_.isArray()) {
|
||||||
|
for (auto const &config : config_) {
|
||||||
|
if (config.isObject() && isValidOutput(config, name, identifier)) {
|
||||||
|
configs.push_back(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isValidOutput(config_, name, identifier)) {
|
||||||
|
configs.push_back(config_);
|
||||||
|
}
|
||||||
|
return configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace waybar
|
|
@ -0,0 +1,12 @@
|
||||||
|
[wrap-file]
|
||||||
|
directory = Catch2-2.13.3
|
||||||
|
source_url = https://github.com/catchorg/Catch2/archive/v2.13.3.zip
|
||||||
|
source_filename = Catch2-2.13.3.zip
|
||||||
|
source_hash = 1804feb72bc15c0856b4a43aa586c661af9c3685a75973b6a8fc0b950c7cfd13
|
||||||
|
patch_url = https://github.com/mesonbuild/catch2/releases/download/2.13.3-2/catch2.zip
|
||||||
|
patch_filename = catch2-2.13.3-2-wrap.zip
|
||||||
|
patch_hash = 21b590ab8c65b593ad5ee8f8e5b822bf9877b2c2672f97fbb52459751053eadf
|
||||||
|
|
||||||
|
[provide]
|
||||||
|
catch2 = catch2_dep
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
#define CATCH_CONFIG_MAIN
|
||||||
|
#include "config.hpp"
|
||||||
|
|
||||||
|
#include <catch2/catch.hpp>
|
||||||
|
|
||||||
|
TEST_CASE("Load simple config", "[config]") {
|
||||||
|
waybar::Config conf;
|
||||||
|
conf.load("test/config/simple.json");
|
||||||
|
|
||||||
|
SECTION("validate the config data") {
|
||||||
|
auto& data = conf.getConfig();
|
||||||
|
REQUIRE(data["layer"].asString() == "top");
|
||||||
|
REQUIRE(data["height"].asInt() == 30);
|
||||||
|
}
|
||||||
|
SECTION("select configs for configured output") {
|
||||||
|
auto configs = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0");
|
||||||
|
REQUIRE(configs.size() == 1);
|
||||||
|
}
|
||||||
|
SECTION("select configs for missing output") {
|
||||||
|
auto configs = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1");
|
||||||
|
REQUIRE(configs.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Load config with multiple bars", "[config]") {
|
||||||
|
waybar::Config conf;
|
||||||
|
conf.load("test/config/multi.json");
|
||||||
|
|
||||||
|
SECTION("select multiple configs #1") {
|
||||||
|
auto data = conf.getOutputConfigs("DP-0", "Fake DisplayPort output #0");
|
||||||
|
REQUIRE(data.size() == 3);
|
||||||
|
REQUIRE(data[0]["layer"].asString() == "bottom");
|
||||||
|
REQUIRE(data[0]["height"].asInt() == 20);
|
||||||
|
REQUIRE(data[1]["layer"].asString() == "top");
|
||||||
|
REQUIRE(data[1]["position"].asString() == "bottom");
|
||||||
|
REQUIRE(data[1]["height"].asInt() == 21);
|
||||||
|
REQUIRE(data[2]["layer"].asString() == "overlay");
|
||||||
|
REQUIRE(data[2]["position"].asString() == "right");
|
||||||
|
REQUIRE(data[2]["height"].asInt() == 23);
|
||||||
|
}
|
||||||
|
SECTION("select multiple configs #2") {
|
||||||
|
auto data = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0");
|
||||||
|
REQUIRE(data.size() == 2);
|
||||||
|
REQUIRE(data[0]["layer"].asString() == "bottom");
|
||||||
|
REQUIRE(data[0]["height"].asInt() == 20);
|
||||||
|
REQUIRE(data[1]["layer"].asString() == "overlay");
|
||||||
|
REQUIRE(data[1]["position"].asString() == "right");
|
||||||
|
REQUIRE(data[1]["height"].asInt() == 23);
|
||||||
|
}
|
||||||
|
SECTION("select single config by output description") {
|
||||||
|
auto data = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1");
|
||||||
|
REQUIRE(data.size() == 1);
|
||||||
|
REQUIRE(data[0]["layer"].asString() == "overlay");
|
||||||
|
REQUIRE(data[0]["position"].asString() == "left");
|
||||||
|
REQUIRE(data[0]["height"].asInt() == 22);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Load simple config with include", "[config]") {
|
||||||
|
waybar::Config conf;
|
||||||
|
conf.load("test/config/include.json");
|
||||||
|
|
||||||
|
SECTION("validate the config data") {
|
||||||
|
auto& data = conf.getConfig();
|
||||||
|
// config override behavior: preserve first included value
|
||||||
|
REQUIRE(data["layer"].asString() == "top");
|
||||||
|
REQUIRE(data["height"].asInt() == 30);
|
||||||
|
// config override behavior: preserve value from the top config
|
||||||
|
REQUIRE(data["position"].asString() == "top");
|
||||||
|
// config override behavior: explicit null is still a value and should be preserved
|
||||||
|
REQUIRE((data.isMember("nullOption") && data["nullOption"].isNull()));
|
||||||
|
}
|
||||||
|
SECTION("select configs for configured output") {
|
||||||
|
auto configs = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0");
|
||||||
|
REQUIRE(configs.size() == 1);
|
||||||
|
}
|
||||||
|
SECTION("select configs for missing output") {
|
||||||
|
auto configs = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1");
|
||||||
|
REQUIRE(configs.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Load multiple bar config with include", "[config]") {
|
||||||
|
waybar::Config conf;
|
||||||
|
conf.load("test/config/include-multi.json");
|
||||||
|
|
||||||
|
SECTION("bar config with sole include") {
|
||||||
|
auto data = conf.getOutputConfigs("OUT-0", "Fake ouptut #0");
|
||||||
|
REQUIRE(data.size() == 1);
|
||||||
|
REQUIRE(data[0]["height"].asInt() == 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("bar config with output and include") {
|
||||||
|
auto data = conf.getOutputConfigs("OUT-1", "Fake output #1");
|
||||||
|
REQUIRE(data.size() == 1);
|
||||||
|
REQUIRE(data[0]["height"].asInt() == 21);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("bar config with output override") {
|
||||||
|
auto data = conf.getOutputConfigs("OUT-2", "Fake output #2");
|
||||||
|
REQUIRE(data.size() == 1);
|
||||||
|
REQUIRE(data[0]["height"].asInt() == 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("multiple levels of include") {
|
||||||
|
auto data = conf.getOutputConfigs("OUT-3", "Fake output #3");
|
||||||
|
REQUIRE(data.size() == 1);
|
||||||
|
REQUIRE(data[0]["height"].asInt() == 23);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& data = conf.getConfig();
|
||||||
|
REQUIRE(data.isArray());
|
||||||
|
REQUIRE(data.size() == 4);
|
||||||
|
REQUIRE(data[0]["output"].asString() == "OUT-0");
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"layer": "top",
|
||||||
|
"position": "bottom",
|
||||||
|
"height": 30,
|
||||||
|
"output": ["HDMI-0", "DP-0"],
|
||||||
|
"nullOption": "not null"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"layer": "bottom"
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"output": "OUT-0",
|
||||||
|
"height": 20
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"height": 21
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"output": "OUT-1",
|
||||||
|
"height": 22
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"height": 23
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"output": "OUT-3",
|
||||||
|
"include": "test/config/include-multi-3-0.json"
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"include": "test/config/include-multi-0.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output": "OUT-1",
|
||||||
|
"include": "test/config/include-multi-1.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output": "OUT-2",
|
||||||
|
"include": "test/config/include-multi-2.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "test/config/include-multi-3.json"
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"include": ["test/config/include-1.json", "test/config/include-2.json"],
|
||||||
|
"position": "top",
|
||||||
|
"nullOption": null
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"layer": "bottom",
|
||||||
|
"height": 20,
|
||||||
|
"output": ["HDMI-0", "DP-0"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "bottom",
|
||||||
|
"layer": "top",
|
||||||
|
"height": 21,
|
||||||
|
"output": ["DP-0"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "left",
|
||||||
|
"layer": "overlay",
|
||||||
|
"height": 22,
|
||||||
|
"output": "Fake HDMI output #1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "right",
|
||||||
|
"layer": "overlay",
|
||||||
|
"height": 23,
|
||||||
|
"output": "!HDMI-1"
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"layer": "top",
|
||||||
|
"height": 30,
|
||||||
|
"output": ["HDMI-0", "DP-0"]
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
test_inc = include_directories('../include')
|
||||||
|
test_dep = [
|
||||||
|
catch2,
|
||||||
|
fmt,
|
||||||
|
jsoncpp,
|
||||||
|
spdlog,
|
||||||
|
]
|
||||||
|
|
||||||
|
config_test = executable(
|
||||||
|
'config_test',
|
||||||
|
'config.cpp',
|
||||||
|
'../src/config.cpp',
|
||||||
|
dependencies: test_dep,
|
||||||
|
include_directories: test_inc,
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Configuration test',
|
||||||
|
config_test,
|
||||||
|
workdir: meson.source_root(),
|
||||||
|
)
|
Loading…
Reference in New Issue