diff --git a/src/cpp/project/blank-screens/.gitignore b/src/cpp/project/blank-screens/.gitignore new file mode 100644 index 00000000..5ef955b2 --- /dev/null +++ b/src/cpp/project/blank-screens/.gitignore @@ -0,0 +1 @@ +blank-screens diff --git a/src/cpp/project/blank-screens/Makefile b/src/cpp/project/blank-screens/Makefile new file mode 100644 index 00000000..ac6bd4a6 --- /dev/null +++ b/src/cpp/project/blank-screens/Makefile @@ -0,0 +1,34 @@ +VPATH = src + +CXX := g++ + +warnings = -Wall -Wextra -Werror \ + -Wswitch-default -Wfloat-equal \ + -Wdisabled-optimization -Wsign-promo +libs = -lX11 -lXrandr + +ifdef includeprefix + include = -I$(includeprefix) +else + include = +endif + +CXXFLAGS = $(warnings) -O3 -DNDEBUG -std=c++2c -pedantic -fpermissive $(include) +LDFLAGS = $(libs) + +EXE = blank-screens + +SRC = $(shell find src -name "*.cpp") +OBJ_ = $(SRC:src/%=%) +OBJ = $(OBJ_:.cpp=.o) + +$(EXE): $(OBJ) + $(CXX) $(LDFLAGS) $^ -o $(EXE) + +$(OBJ): %.o: %.cpp + $(CXX) -c $(CXXFLAGS) $< -o $@ + +clean: + rm -f -- $(OBJ) $(EXE) + +.PHONY: run clean diff --git a/src/cpp/project/blank-screens/src/blind.cpp b/src/cpp/project/blank-screens/src/blind.cpp new file mode 100644 index 00000000..34705c3e --- /dev/null +++ b/src/cpp/project/blank-screens/src/blind.cpp @@ -0,0 +1,108 @@ +#include "blind.hpp" + +#include + +#include + +#include + +bs::blind::blind(std::shared_ptr cli, + std::string_view monitor, + Display* display, + int default_screen, + Window root_window, + XRRCrtcInfo* crtc_info) + : m_monitor(monitor) + , m_cli(cli) + , m_display(display) +{ + m_window = XCreateSimpleWindow(display, + root_window, + crtc_info->x, + crtc_info->y, + crtc_info->width, + crtc_info->height, + 0, + 0, + 0); + + auto* class_hint = XAllocClassHint(); + static std::string class_name(xph::exec_name); + class_hint->res_name = class_name.data(); + class_hint->res_class = class_name.data(); + XSetClassHint(display, m_window, class_hint); + + XSetWindowAttributes window_attributes; + window_attributes.override_redirect = True; + + const auto wm_state = XInternAtom(display, "_NET_WM_STATE", False); + const auto wm_state_above = XInternAtom(display, "_NET_WM_STATE_ABOVE", False); + const auto wm_window_type = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False); + const auto wm_window_type_dock = XInternAtom(display, "_NET_WM_WINDOW_TYPE_DOCK", False); + XChangeProperty(display, + m_window, + wm_state, + XA_ATOM, + 32, + PropModeAppend, + reinterpret_cast(&wm_state_above), + 1); + XChangeProperty(display, + m_window, + wm_window_type, + XA_ATOM, + 32, + PropModeReplace, + reinterpret_cast(&wm_window_type), + 1); + XChangeProperty(display, + m_window, + wm_window_type_dock, + XA_ATOM, + 32, + PropModeReplace, + reinterpret_cast(&wm_window_type_dock), + 1); + + XChangeWindowAttributes(display, m_window, CWOverrideRedirect, &window_attributes); + + XMapWindow(display, m_window); + XRaiseWindow(display, m_window); + XFlush(display); + + set_alpha(cli->alpha()); + + XSetWindowBackground(display, m_window, BlackPixel(display, default_screen)); + XClearWindow(display, m_window); + XFlush(display); +} + +bs::blind::~blind(void) +{ + XDestroyWindow(m_display, m_window); + XFlush(m_display); +} + +void bs::blind::set_alpha(double alpha) +{ + alpha = std::clamp(alpha, m_cli->min_alpha(), m_cli->max_alpha()); + + unsigned long opacity; + if (alpha > 1 - m_cli->snap_threshold()) + opacity = 0xFFFFFFFFul; + else if (alpha < m_cli->snap_threshold()) + opacity = 0x00000000ul; + else + opacity = 0xFFFFFFFFul * alpha; + + const auto opacity_atom = XInternAtom(m_display, "_NET_WM_WINDOW_OPACITY", False); + XChangeProperty(m_display, + m_window, + opacity_atom, + XA_CARDINAL, + 32, + PropModeReplace, + reinterpret_cast(&opacity), + 1L); + XFlush(m_display); +} diff --git a/src/cpp/project/blank-screens/src/blind.hpp b/src/cpp/project/blank-screens/src/blind.hpp new file mode 100644 index 00000000..cf104dc6 --- /dev/null +++ b/src/cpp/project/blank-screens/src/blind.hpp @@ -0,0 +1,32 @@ +#ifndef HEADER_SCRIPTS_CXX_BS_BLIND_ +#define HEADER_SCRIPTS_CXX_BS_BLIND_ + +#include + +#include "cli.hpp" + +namespace bs { + class blind { + public: + std::string_view m_monitor; + + private: + std::shared_ptr m_cli; + Window m_window; + Display* m_display; + + public: + blind(void) = delete; + ~blind(void); + blind(std::shared_ptr cli, + std::string_view monitor, + Display* display, + int default_screen, + Window root_window, + XRRCrtcInfo* crtc_info); + + void set_alpha(double alpha); + }; +} // namespace bs + +#endif /* ifndef HEADER_SCRIPTS_CXX_BS_BLIND_ */ diff --git a/src/cpp/project/blank-screens/src/cli.cpp b/src/cpp/project/blank-screens/src/cli.cpp new file mode 100644 index 00000000..bfd687d6 --- /dev/null +++ b/src/cpp/project/blank-screens/src/cli.cpp @@ -0,0 +1,116 @@ +#include "cli.hpp" + +#include +#include +#include +#include + +#include + +#include + +#include + +bs::cli::cli(int argc, char** argv) +{ + bool show_help = false; + double fps = 240; + auto cli = + lyra::cli() | lyra::help(show_help).description("Blank monitors.") | + lyra::opt(m_alpha, "num")["-a"]["--alpha"]("set initial alpha of blinds") | + lyra::opt(m_fifo_path, "path")["-c"]["--command-fifo"]("listen for commands on fifo") | + lyra::opt(fps, "num")["-f"]["--fps"]("set frame rate of alpha interpolation") | + lyra::opt(m_max_alpha, "num")["-H"]["--high"]("set upper alpha bound") | + lyra::opt(m_min_alpha, "num")["-L"]["--low"]("set lower alpha bound") | + lyra::opt(m_lock_path, "path")["-l"]["--lock"]("set lock path") | + lyra::opt(m_ignore_primary)["-p"]["--ignore-primary"]("ignore primary monitor") | + lyra::opt(m_snap_threshold, "num")["-s"]["--snap"]("set alpha snap threshold") | + lyra::opt(m_lerp_factor, "num")["-t"]["--factor"]("set alpha linear interpolation factor") | + lyra::arg(m_monitors, "monitor")("initial monitor to blank") + .cardinality(0, std::numeric_limits::max()); + + auto args = cli.parse({ argc, argv }); + + if (!args) { + std::cerr << args.message() << '\n'; + std::cerr << "Try '" << xph::exec_path << " -h' for more information.\n"; + std::exit(EXIT_FAILURE); + } + + if (show_help) { + std::cout << cli << '\n'; + std::exit(EXIT_SUCCESS); + } + + m_frame_time = std::chrono::duration{ 1.0 / fps }; + + std::sort(m_monitors.begin(), m_monitors.end()); + m_monitors.erase(std::unique(m_monitors.begin(), m_monitors.end()), m_monitors.end()); +} + +double bs::cli::alpha(void) const noexcept +{ + return m_alpha; +} + +const std::string& bs::cli::fifo_path(void) const noexcept +{ + return m_fifo_path; +} + +const std::chrono::duration& bs::cli::frame_time(void) const noexcept +{ + return m_frame_time; +} + +double bs::cli::max_alpha(void) const noexcept +{ + return m_max_alpha; +} + +double bs::cli::min_alpha(void) const noexcept +{ + return m_min_alpha; +} + +const std::string& bs::cli::lock_path(void) const noexcept +{ + return m_lock_path; +} + +bool bs::cli::ignore_primary(void) const noexcept +{ + return m_ignore_primary; +} + +double bs::cli::snap_threshold(void) const noexcept +{ + return m_snap_threshold; +} + +double bs::cli::lerp_factor(void) const noexcept +{ + return m_lerp_factor; +} + +const std::vector& bs::cli::monitors(void) const noexcept +{ + return m_monitors; +} + +namespace bs { + std::ostream& operator<<(std::ostream& os, const bs::cli& cli) + { + os << "alpha: " << cli.alpha() << '\n'; + os << "fifo path: " << cli.fifo_path() << '\n'; + os << "frame time: " << cli.frame_time().count() << '\n'; + os << "max alpha: " << cli.max_alpha() << '\n'; + os << "min alpha: " << cli.min_alpha() << '\n'; + os << "lock path: " << cli.lock_path() << '\n'; + os << "ignore primary: " << std::boolalpha << cli.ignore_primary() << '\n'; + os << "snap threshold: " << cli.snap_threshold() << '\n'; + os << "lerp factor: " << cli.lerp_factor() << '\n'; + + return os; + } +} // namespace bs diff --git a/src/cpp/project/blank-screens/src/cli.hpp b/src/cpp/project/blank-screens/src/cli.hpp new file mode 100644 index 00000000..1a92a1e2 --- /dev/null +++ b/src/cpp/project/blank-screens/src/cli.hpp @@ -0,0 +1,51 @@ +#ifndef HEADER_SCRIPTS_CXX_PAF_CLI_ +#define HEADER_SCRIPTS_CXX_PAF_CLI_ + +#include +#include +#include + +namespace bs { + class cli { + public: + static const constexpr double k_default_alpha = 1.0; + static const constexpr double k_default_frame_rate = 100.0; + static const constexpr double k_default_max_alpha = 1.0; + static const constexpr double k_default_min_alpha = 0.0; + static const constexpr double k_default_snap_threshold = 0.01; + static const constexpr double k_default_lerp_factor = 0.10; + static const constexpr char* const k_default_fifo_path = "/tmp/blank-screens/fifo"; + static const constexpr char* const k_default_lock_path = "/tmp/blank-screens/"; + + private: + double m_alpha = k_default_alpha; + std::string m_fifo_path = k_default_fifo_path; + std::chrono::duration m_frame_time{ 1.0 / k_default_frame_rate }; + double m_max_alpha = k_default_max_alpha; + double m_min_alpha = k_default_min_alpha; + std::string m_lock_path = k_default_lock_path; + bool m_ignore_primary = false; + double m_snap_threshold = k_default_snap_threshold; + double m_lerp_factor = k_default_lerp_factor; + std::vector m_monitors; + + public: + cli(void) = delete; + cli(int argc, char** argv); + + double alpha(void) const noexcept; + const std::string& fifo_path(void) const noexcept; + const std::chrono::duration& frame_time(void) const noexcept; + double max_alpha(void) const noexcept; + double min_alpha(void) const noexcept; + const std::string& lock_path(void) const noexcept; + bool ignore_primary(void) const noexcept; + double snap_threshold(void) const noexcept; + double lerp_factor(void) const noexcept; + const std::vector& monitors(void) const noexcept; + + friend std::ostream& operator<<(std::ostream& os, const cli& cli); + }; +} // namespace bs + +#endif /* ifndef HEADER_SCRIPTS_CXX_PAF_CLI_ */ diff --git a/src/cpp/project/blank-screens/src/daemon.cpp b/src/cpp/project/blank-screens/src/daemon.cpp new file mode 100644 index 00000000..6dde5430 --- /dev/null +++ b/src/cpp/project/blank-screens/src/daemon.cpp @@ -0,0 +1,252 @@ +#include "daemon.hpp" + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include + +#include +#include + +namespace fs = std::filesystem; + +bs::daemon::daemon(const cli& cli) : m_cli(cli) +{ + m_display = XOpenDisplay(nullptr); + xph::die_if(!m_display, "unable to open display"); + + const auto& monitors = m_cli.monitors(); + for (const auto& monitor : monitors) + add_monitor(monitor); + + m_last_alpha = 0.0; + lerp_alpha(cli.alpha()); +} + +[[noreturn]] void bs::daemon::run() +{ + if (::mkfifo(m_cli.fifo_path().c_str(), 0600)) + xph::die("could not create fifo"); + + for (;;) { + std::ifstream fifo{ m_cli.fifo_path() }; + if (!fifo.is_open()) + xph::die("could not open fifo"); + + for (std::string line; std::getline(fifo, line); this->dispatch(line)) {} + } + + std::exit(EXIT_FAILURE); +} + +static std::string get_cursor_monitor() +{ + Display* display = XOpenDisplay(nullptr); + xph::die_if(!display, "unable to open display"); + + int screen = DefaultScreen(display); + Window root_window = RootWindow(display, screen); + + int root_x, root_y, win_x, win_y; + unsigned int mask; + Window root_return, child_return; + + XQueryPointer( + display, root_window, &root_return, &child_return, &root_x, &root_y, &win_x, &win_y, &mask); + + XRRScreenResources* screen_resources = XRRGetScreenResources(display, root_window); + xph::die_if(!screen_resources, "unable to get screen resources"); + + for (int i = 0; i < screen_resources->noutput; ++i) { + XRROutputInfo* output_info = + XRRGetOutputInfo(display, screen_resources, screen_resources->outputs[i]); + if (!output_info || output_info->connection != RR_Connected) { + if (output_info) + XRRFreeOutputInfo(output_info); + continue; + } + + XRRCrtcInfo* crtc_info = XRRGetCrtcInfo(display, screen_resources, output_info->crtc); + if (!crtc_info) { + XRRFreeOutputInfo(output_info); + continue; + } + + if (root_x >= crtc_info->x && root_x < static_cast(crtc_info->x + crtc_info->width) && + root_y >= crtc_info->y && root_y < static_cast(crtc_info->y + crtc_info->height)) { + std::string monitor_name = output_info->name; + XRRFreeCrtcInfo(crtc_info); + XRRFreeOutputInfo(output_info); + XRRFreeScreenResources(screen_resources); + XCloseDisplay(display); + return monitor_name; + } + + XRRFreeCrtcInfo(crtc_info); + XRRFreeOutputInfo(output_info); + } + + XRRFreeScreenResources(screen_resources); + XCloseDisplay(display); + + return {}; +} + +void bs::daemon::dispatch(const std::string& command_line) +{ + static std::vector argv{}; + static std::istringstream iss; + static std::string token; + + argv.clear(); + iss.clear(); + iss.str(command_line); + + for (iss.str(command_line); iss >> token; argv.push_back(token)) {} + + if (argv.empty()) + return; + + if (argv[0] == "alpha") { + std::cerr << xph::exec_name << ": setting alpha to " << argv[1] << '\n'; + const auto alpha = argv.size() < 2 ? m_cli.alpha() : std::stod(argv[1]); + lerp_alpha(alpha); + } else if (argv[0] == "add") { + for (const auto& monitor : argv | std::views::drop(1)) { + const auto& target_monitor = monitor == "@cursor" ? get_cursor_monitor() : monitor; + std::cerr << xph::exec_name << ": adding monitor " << target_monitor << '\n'; + add_monitor(target_monitor); + } + } else if (argv[0] == "remove") { + for (const auto& monitor : argv | std::views::drop(1)) { + const auto& target_monitor = monitor == "@cursor" ? get_cursor_monitor() : monitor; + std::cerr << xph::exec_name << ": removing monitor " << target_monitor << '\n'; + remove_monitor(target_monitor); + } + } else if (argv[0] == "toggle") { + for (const auto& monitor : argv | std::views::drop(1)) { + const auto& target_monitor = monitor == "@cursor" ? get_cursor_monitor() : monitor; + std::cerr << xph::exec_name << ": toggling monitor " << target_monitor << '\n'; + toggle_monitor(target_monitor); + } + } else if (argv[0] == "exit") { + std::exit(EXIT_SUCCESS); + } else { + std::cerr << xph::exec_name << ": unknown command [" << argv[0] << "]\n"; + } +} + +void bs::daemon::lerp_alpha(double alpha) +{ + auto current_alpha = m_last_alpha; + + while (!xph::approx_eq(current_alpha, alpha, km_epsilon)) { + current_alpha = + std::clamp(current_alpha + (alpha - current_alpha) * m_cli.lerp_factor(), 0.0, 1.0); + + for (auto& blind : m_blinds) + blind.set_alpha(current_alpha); + + std::this_thread::sleep_for(m_cli.frame_time()); + } + + m_last_alpha = alpha; +} + +void bs::daemon::add_monitor(const std::string& target_monitor) +{ + for (const auto& blind : m_blinds) { + if (blind.m_monitor == target_monitor) { + std::cerr << xph::exec_name << ": monitor " << target_monitor << " already exists\n"; + return; + } + } + + const auto default_screen = DefaultScreen(m_display); + const auto root_window = RootWindow(m_display, default_screen); + + const auto screen_resources = XRRGetScreenResources(m_display, root_window); + xph::die_if(!screen_resources, "unable to get screen resources"); + + auto primary_output = XRRGetOutputPrimary(m_display, root_window); + + for (decltype(screen_resources->noutput) i = 0; i < screen_resources->noutput; ++i) { + if (m_cli.ignore_primary() && screen_resources->outputs[i] == primary_output) + continue; + + const auto output_info = + XRRGetOutputInfo(m_display, screen_resources, screen_resources->outputs[i]); + if (!output_info) { + std::cerr << xph::exec_name << ": unable to get information for monitor " << i << '\n'; + continue; + } + + if (output_info->connection) + goto next; + if (output_info->connection || output_info->name != target_monitor) + goto next; + + XRRCrtcInfo* crtc_info; + crtc_info = XRRGetCrtcInfo(m_display, screen_resources, output_info->crtc); + if (!crtc_info) { + std::cerr << xph::exec_name << ": unable to get information for monitor " << i << '\n'; + goto next; + } + + m_blinds.emplace_back(std::make_shared(m_cli), + target_monitor, + m_display, + default_screen, + root_window, + crtc_info); + + XRRFreeCrtcInfo(crtc_info); +next: + XRRFreeOutputInfo(output_info); + } + XRRFreeScreenResources(screen_resources); +} + +void bs::daemon::remove_monitor(const std::string& target_monitor) +{ + for (auto it = m_blinds.begin(); it != m_blinds.end(); ++it) { + if (it->m_monitor == target_monitor) { + m_blinds.erase(it); + break; + } + } +} + +void bs::daemon::toggle_monitor(const std::string& target_monitor) +{ + for (auto it = m_blinds.begin(); it != m_blinds.end(); ++it) { + if (it->m_monitor == target_monitor) { + std::cerr << xph::exec_name << ": removing monitor " << target_monitor << '\n'; + m_blinds.erase(it); + return; + } + } + + std::cerr << xph::exec_name << ": adding monitor " << target_monitor << '\n'; + add_monitor(target_monitor); +} + +void bs::daemon::handle_signals([[maybe_unused]] int signal) +{ + fs::remove(m_cli.fifo_path()); + fs::remove_all(m_cli.lock_path()); + + lerp_alpha(0.0); + + m_blinds.clear(); + + XCloseDisplay(m_display); +} diff --git a/src/cpp/project/blank-screens/src/daemon.hpp b/src/cpp/project/blank-screens/src/daemon.hpp new file mode 100644 index 00000000..458aa6f2 --- /dev/null +++ b/src/cpp/project/blank-screens/src/daemon.hpp @@ -0,0 +1,32 @@ +#ifndef HEADER_SCRIPTS_CXX_BS_DAEMON_ +#define HEADER_SCRIPTS_CXX_BS_DAEMON_ + +#include "blind.hpp" +#include "cli.hpp" +#include + +namespace bs { + class daemon { + private: + const constexpr static double km_epsilon = 0.0001; + const cli& m_cli; + std::vector m_blinds; + Display* m_display; + double m_last_alpha; + + public: + daemon(void) = delete; + daemon(const cli& cli); + [[noreturn]] void run(void); + void handle_signals(int signal); + + private: + void dispatch(const std::string& command_line); + void lerp_alpha(double alpha); + void add_monitor(const std::string& target_monitor); + void remove_monitor(const std::string& target_monitor); + void toggle_monitor(const std::string& target_monitor); + }; +} // namespace bs + +#endif /* ifndef HEADER_SCRIPTS_CXX_BS_DAEMON_ */ diff --git a/src/cpp/project/blank-screens/src/main.cpp b/src/cpp/project/blank-screens/src/main.cpp new file mode 100644 index 00000000..a1dd76bb --- /dev/null +++ b/src/cpp/project/blank-screens/src/main.cpp @@ -0,0 +1,46 @@ +#include + +#include + +#include +#include +#include + +#include "cli.hpp" +#include "daemon.hpp" + +DEFINE_EXEC_INFO(); + +namespace fs = std::filesystem; + +const bs::daemon* g_daemon = nullptr; + +void handle_signal(int signal) +{ + if (g_daemon) + const_cast(g_daemon)->handle_signals(signal); +} + +int main(int argc, char* argv[]) +{ + xph::gather_exec_info(argc, argv); + bs::cli cli(argc, argv); + + std::cerr << cli; + + std::error_code ec; + if (fs::create_directory(cli.lock_path(), ec)) { + bs::daemon daemon{ cli }; + + g_daemon = &daemon; + xph::sys::signals<4>({ SIGINT, SIGTERM, SIGQUIT, SIGHUP }, handle_signal); + + daemon.run(); + return EXIT_SUCCESS; + } + + xph::die_if(ec == std::errc::file_exists, "lock is held by another daemon"); + xph::die("could not acquire daemon lock"); + + return EXIT_SUCCESS; +} diff --git a/src/cpp/util/core/blank-screens.cpp b/src/cpp/util/core/blank-screens.cpp deleted file mode 100644 index 99e7e5ee..00000000 --- a/src/cpp/util/core/blank-screens.cpp +++ /dev/null @@ -1,529 +0,0 @@ -// @LDFLAGS -lX11 -lXrandr - -// C++ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// C -#include -#include -#include -#include -#include -#include - -// C++ libraries -#include -#include -#include -#include -#include -#include -#include - -// C libraries -#include - -// third-party -#include -#include -#include - -std::string lock_file; -static Display* display; -static std::vector windows; -static double last_alpha = 0.0; -const static struct Options* options; -const constexpr double epsilon = 0.0001; - -DEFINE_EXEC_INFO(); - -struct Options { -private: - static const constexpr double k_default_alpha = 1.0; - static const constexpr double k_default_frame_rate = 100.0; - static const constexpr double k_default_lerp_factor = 0.10; - static const constexpr double k_default_low = 0.0; - static const constexpr double k_default_high = 1.0; - static const constexpr double k_default_snap_threshold = 0.01; - -public: - std::string_view alpha_path; - double alpha = k_default_alpha; - bool exit_if_none_selected = false; - std::chrono::duration frame_time{ 1.0 / k_default_frame_rate }; - double low = k_default_low; - double high = k_default_high; - std::string_view lock_path; - std::unordered_set ignored_monitors; - bool ignore_primary = false; - double snap_threshold = k_default_snap_threshold; - double lerp_factor = k_default_lerp_factor; - std::unordered_set selected_monitors; - -private: - std::string_view exec_name; - -public: - Options(void) = delete; - - Options(std::string_view exec_name, int& argc, char**& argv) : exec_name(exec_name) - { - parse_args(argc, argv); - } - - void help(void) - { - std::cout - << "Usage: " << exec_name << " [OPTION...] [NAME...]\n" - << "Blank monitors.\n" - "\n" - "If no NAME is provided, all monitors are blanked.\n" - "\n" - " -A PATH listen for alpha changes in PATH. listening is done via inotify.\n" - " -a ALPHA set alpha of blinds to ALPHA. default is " - << k_default_alpha - << ".\n" - " -e immediately exit if no monitors are blanked\n" - " -f FPS set frame rate of alpha interpolation to FPS. default is " - << k_default_frame_rate - << ".\n" - " -H HIGH set upper bound. default is " - << k_default_high - << ".\n" - " -h display this help and exit\n" - " -L LOW set lower bound. default is " - << k_default_low - << ".\n" - " -l PATH path to lock file. default is \"${TMPDIR:-/tmp}/" - << exec_name - << ".lock\"\n" - " -m NAME don't blank monitor with name NAME, can be given multiple times\n" - " -p don't blank primary monitor\n" - " -S SNAP set snap threshold. default is " - << k_default_snap_threshold - << ".\n" - " -t FACTOR set alpha interpolation factor to FACTOR. default is " - << k_default_lerp_factor << ".\n"; - std::exit(EXIT_SUCCESS); - } - - void error(void) - { - std::cerr << "Try '" << exec_name << " -h' for more information.\n"; - std::exit(EXIT_FAILURE); - } - - void parse_args(int& argc, char**& argv) - { - for (int i; (i = getopt(argc, argv, "A:a:ef:H:hL:l:m:pS:t:")) != -1;) { - switch (i) { - case 'A': - alpha_path = optarg; - break; - case 'a': - alpha = std::clamp( - xph::lexical_cast(optarg), 0.0, 1.0); - break; - case 'e': - exit_if_none_selected = true; - break; - case 'f': - frame_time = decltype(frame_time)( - 1.0 / - xph::lexical_cast(optarg)); - break; - case 'H': - high = std::clamp( - xph::lexical_cast(optarg), 0.0, 1.0); - break; - case 'h': - help(); - break; - case 'L': - low = std::clamp( - xph::lexical_cast(optarg), 0.0, 1.0); - break; - case 'l': - lock_path = optarg; - break; - case 'm': - ignored_monitors.emplace(optarg); - break; - case 'p': - ignore_primary = true; - break; - case 'S': - snap_threshold = - xph::lexical_cast(optarg); - break; - case 't': - lerp_factor = - xph::lexical_cast(optarg); - break; - default: - error(); - break; - } - } - - argc -= optind; - argv += optind; - - std::ranges::copy(std::views::counted(argv, argc), xph::inserter(selected_monitors)); - } -}; - -void set_window_alpha(Window window, double alpha) -{ - alpha = std::clamp(alpha, options->low, options->high); - - unsigned long opacity; - if (alpha > 1 - options->snap_threshold) - opacity = 0xFFFFFFFFul; - else if (alpha < options->snap_threshold) - opacity = 0x00000000ul; - else - opacity = 0xFFFFFFFFul * alpha; - - const auto opacity_atom = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False); - XChangeProperty(display, - window, - opacity_atom, - XA_CARDINAL, - 32, - PropModeReplace, - reinterpret_cast(&opacity), - 1L); - last_alpha = alpha; -} - -void lerp_alpha(double alpha) -{ - auto current_alpha = last_alpha; - while (!xph::approx_eq(current_alpha, alpha, epsilon)) { - current_alpha += (alpha - current_alpha) * options->lerp_factor; - current_alpha = std::clamp(current_alpha, 0.0, 1.0); - std::for_each(windows.begin(), windows.end(), [&](auto window) { - set_window_alpha(window, current_alpha); - XFlush(display); - }); - std::this_thread::sleep_for(options->frame_time); - } -} - -void cleanup(void) -{ - std::filesystem::remove(lock_file); - - lerp_alpha(0.0); - - for (const auto window : windows) - XDestroyWindow(display, window); - XCloseDisplay(display); -} - -[[noreturn]] void terminate(void) -{ - cleanup(); - std::exit(EXIT_SUCCESS); -} - -template -[[noreturn]] void die(const Ts&... args) -{ - cleanup(); - xph::die(args...); -} - -void handle_signals([[maybe_unused]] int sig) -{ - terminate(); -} - -std::optional force_single_instance() -{ - if (!options->lock_path.empty()) { - lock_file = options->lock_path; - } else { - const auto tmpdir = std::getenv("TMPDIR"); - lock_file = tmpdir ? tmpdir : "/tmp"; - - lock_file.reserve(lock_file.size() + 1 + xph::exec_name.size() + 5); - lock_file += "/"; - lock_file += xph::exec_name; - lock_file += ".lock"; - } - - if (std::ifstream ifl(lock_file); ifl.is_open()) { - ::pid_t pid; - ifl >> pid; - - errno = 0; - if (!kill(pid, 0) && !errno) { - kill(pid, SIGTERM); - ifl.close(); - return { EXIT_SUCCESS }; - } - } - - if (std::ofstream ofl(lock_file); ofl.is_open()) { - const auto pid = getpid(); - ofl << pid; - return std::nullopt; - } - - return { EXIT_FAILURE }; -} - -void create_windows() -{ - xph::die_if(!(display = XOpenDisplay(NULL)), "unable to open display"); - - const auto default_screen = DefaultScreen(display); - const auto root_window = RootWindow(display, default_screen); - - const auto screen_resources = XRRGetScreenResources(display, root_window); - xph::die_if(!screen_resources, "unable to get screen resources"); - - auto primary_output = XRRGetOutputPrimary(display, root_window); - - for (decltype(screen_resources->noutput) i = 0; i < screen_resources->noutput; ++i) { - if (options->ignore_primary && screen_resources->outputs[i] == primary_output) - continue; - - const auto output_info = - XRRGetOutputInfo(display, screen_resources, screen_resources->outputs[i]); - if (!output_info) { - std::cerr << xph::exec_name << ": unable to get information for monitor " << i << '\n'; - continue; - } - - if (output_info->connection || options->ignored_monitors.contains(output_info->name) || - (!options->selected_monitors.empty() && - !options->selected_monitors.contains(output_info->name))) - goto next; - - XRRCrtcInfo* crtc_info; - crtc_info = XRRGetCrtcInfo(display, screen_resources, output_info->crtc); - if (!crtc_info) { - std::cerr << xph::exec_name << ": unable to get information for monitor " << i << '\n'; - goto next; - } - - { - const auto window = XCreateSimpleWindow(display, - root_window, - crtc_info->x, - crtc_info->y, - crtc_info->width, - crtc_info->height, - 0, - 0, - 0); - - auto* class_hint = XAllocClassHint(); - static std::string class_name(xph::exec_name); - class_hint->res_name = class_name.data(); - class_hint->res_class = class_name.data(); - XSetClassHint(display, window, class_hint); - - windows.push_back(window); - } - - XRRFreeCrtcInfo(crtc_info); -next: - XRRFreeOutputInfo(output_info); - } - XRRFreeScreenResources(screen_resources); - - for (const auto window : windows) { - XSetWindowAttributes window_attributes; - window_attributes.override_redirect = True; - - const auto wm_state = XInternAtom(display, "_NET_WM_STATE", False); - const auto wm_state_above = XInternAtom(display, "_NET_WM_STATE_ABOVE", False); - const auto wm_window_type = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False); - const auto wm_window_type_dock = XInternAtom(display, "_NET_WM_WINDOW_TYPE_DOCK", False); - XChangeProperty(display, - window, - wm_state, - XA_ATOM, - 32, - PropModeAppend, - reinterpret_cast(&wm_state_above), - 1); - XChangeProperty(display, - window, - wm_window_type, - XA_ATOM, - 32, - PropModeReplace, - reinterpret_cast(&wm_window_type), - 1); - XChangeProperty(display, - window, - wm_window_type_dock, - XA_ATOM, - 32, - PropModeReplace, - reinterpret_cast(&wm_window_type_dock), - 1); - - XChangeWindowAttributes(display, window, CWOverrideRedirect, &window_attributes); - - XMapWindow(display, window); - XRaiseWindow(display, window); - XFlush(display); - - set_window_alpha(window, 0.0); - XSetWindowBackground(display, window, BlackPixel(display, default_screen)); - XClearWindow(display, window); - XFlush(display); - } - - lerp_alpha(options->alpha); -} - -[[nodiscard]] bool wait_for_alpha(const char* path) -{ - const constexpr auto eventsize = sizeof(struct inotify_event); - const constexpr auto buflen = (eventsize + 16) * 1024; - - const auto fd = inotify_init(); - if (fd < 0) { - perror("inotify_init"); - return false; - } - - const auto wd = inotify_add_watch(fd, path, IN_CREATE | IN_MODIFY | IN_MOVE_SELF); - - char buf[buflen]; - const auto length = read(fd, buf, buflen); - if (length < 0) { - perror("read"); - return false; - } - - std::remove_const::type i = 0; - while (i < length) { - const auto event = reinterpret_cast(&buf[i]); - if (event->len) - goto cleanup; - i += eventsize + event->len; - } - -cleanup: - inotify_rm_watch(fd, wd); - close(fd); - - return true; -} - -void handle_new_alpha(int fd, fd_set* read_fds) -{ - static auto alpha = options->alpha; - static std::stringstream ss; - static double new_alpha; - - const static auto read_alpha = [&](void) { - for (char buf; ::read(fd, &buf, sizeof(char)) == sizeof(char) && buf != '\n'; ss << buf) {} - ss >> new_alpha; - ss.str(""); - ss.clear(); - new_alpha = std::clamp(new_alpha, 0.0, 1.0); - }; - - read_alpha(); - - while (!xph::approx_eq(alpha, new_alpha, epsilon)) { - alpha += (new_alpha - alpha) * options->lerp_factor; - alpha = std::clamp(alpha, 0.0, 1.0); - std::for_each(windows.begin(), windows.end(), [&](auto window) { - set_window_alpha(window, alpha); - XFlush(display); - }); - - std::this_thread::sleep_for(options->frame_time); - - struct timeval timeout = { - .tv_sec = 0, - .tv_usec = 0, - }; - if (auto select = ::select(fd + 1, read_fds, nullptr, nullptr, &timeout); select == -1) { - std::perror("select"); - die("could not check for data in ", options->alpha_path); - } else if (select) { - read_alpha(); - } - } - - std::for_each(windows.begin(), windows.end(), [&](auto window) { - set_window_alpha(window, new_alpha); - XFlush(display); - }); - - alpha = new_alpha; -} - -void watch_alpha() -{ - if (!std::filesystem::exists(options->alpha_path) && - ::mkfifo(options->alpha_path.data(), 0666)) { - std::perror("mkfifo"); - die("could not create fifo at ", options->alpha_path); - } - - const auto fd = ::open(options->alpha_path.data(), O_RDONLY | O_NONBLOCK); - if (fd == -1) { - std::perror("open"); - die("could not open ", options->alpha_path); - } - - if (struct stat st; !::fstat(fd, &st) && !S_ISFIFO(st.st_mode)) - die(options->alpha_path, " exists and is not a fifo"); - - fd_set read_fds; - FD_ZERO(&read_fds); - FD_SET(fd, &read_fds); - - while (wait_for_alpha(options->alpha_path.data())) - handle_new_alpha(fd, &read_fds); - - die("could not watch ", options->alpha_path); -} - -int main(int argc, char* argv[]) -{ - xph::gather_exec_info(argc, argv); - - const Options options(xph::exec_name, argc, argv); - ::options = &options; - - if (std::optional optional_ret; (optional_ret = force_single_instance())) - return *optional_ret; - - create_windows(); - - if (options.exit_if_none_selected && windows.empty()) - terminate(); - - xph::sys::signals<4>({ SIGINT, SIGTERM, SIGQUIT, SIGHUP }, handle_signals); - - if (!options.alpha_path.empty()) - watch_alpha(); - - std::promise().get_future().wait(); - terminate(); -}