#include "common.hh" #include #include #include #include #include #include #include #include #include "animation.hh" #include "animator.hh" #include "args.hh" #include "blissful_monitor.hh" #include "cairo.hh" #include "fake_monitor.hh" #include "io.hh" #include "monitor.hh" #include "monmon.hh" #include "pango.hh" #include "poll_looper.hh" #include "x.hh" namespace { class IceccMonMon : public MonMon, virtual Monitor::Observer { public: IceccMonMon(std::shared_ptr const& looper, unsigned columns) : MonMon(looper), connected_(false), max_jobs_(0), jobs_(0), requests_(0), force_columns_(columns) { } void connect(Args const* args) { std::unique_ptr monitor; #if FAKE_MONITOR monitor = FakeMonitor::create(looper_); #else monitor = Monitor::create(looper_); #endif monitor_ = BlissfulMonitor::create(looper_, std::move(monitor)); monitor_->add_observer(this); monitor_->connect(args->arg("network", ""), args->arg("scheduler", "")); } protected: void width_changed() override { job_pattern_.reset(); } #if FAKE_MONITOR void do_toggle_fakes() override { monitor_->toggle_fakes(); } #endif private: class MachineAnimation; struct Machine : virtual Animator::AnimationObserver { uint32_t id; Monitor::Machine data; unsigned jobs; unsigned requests; double x; std::shared_ptr animation; explicit Machine(uint32_t id) : id(id), jobs(0), requests(0), x(0.0) { } Machine(Machine const&) = delete; Machine& operator=(Machine const&) = delete; ~Machine() override { assert(!animation); } bool operator<(Machine const& machine) const { if (data.name < machine.data.name) return true; if (data.name > machine.data.name) return false; return id < machine.id; } void ticked(Animator*, Animation*, double value) override { x = value; } void stopped(Animator*, Animation*) override { x = static_cast(jobs) / data.max_jobs; animation.reset(); } }; class MachineAnimation : public Animation { public: explicit MachineAnimation(Machine* machine) : machine_(machine), last_(0.0) { } bool tick(double duration, double* value) override { auto target = static_cast(machine_->jobs) / machine_->data.max_jobs; if (duration == 0.0) { last_ = duration; *value = machine_->x; return machine_->x != target; } else { double diff = target - machine_->x; double max = (duration - last_) * .5; last_ = duration; if (fabs(diff) > max) { diff = diff < 0.0 ? -max : max; *value = machine_->x + diff; return true; } else { *value = target; return false; } } } private: Machine* machine_; double last_; }; void state(Monitor*, Monitor::State state) override { switch (state) { case Monitor::SEARCHING: connected_ = false; stop_all_animations(); machines_.clear(); max_jobs_ = 0; jobs_ = 0; requests_ = 0; if (!wnd_) return; draw(); break; case Monitor::CONNECTED: connected_ = true; draw(); break; } } void stop_all_animations() override { if (!animator_) return; for (auto& machine : machines_) { if (machine->animation) animator_->stop(machine->animation.get()); } } void internal_quit() override { monitor_->disconnect(); } void update(Machine* machine) { auto old = machine->data.max_jobs; machine->data = monitor_->machine(machine->id); if (old < machine->data.max_jobs) { max_jobs_ += machine->data.max_jobs - old; if (machine->jobs) animate(machine); } else if (old > machine->data.max_jobs) { max_jobs_ -= old - machine->data.max_jobs; if (machine->jobs) animate(machine); } } void draw_content(cairo_t* cairo, PangoLayout* layout, uint16_t w, uint16_t h) override { auto const pad_x = std::min(9.0, w / 20.0), pad_y = pad_x; auto const margin_x = std::min(7.5, w / 20.0), margin_y = margin_x; auto y = pad_y; auto const columns = force_columns_ ? force_columns_ : std::max(1, w / h); auto const box_width = (w - pad_x) / columns - pad_x; auto const box_height = box_height_ + margin_y * 2; if (!job_pattern_) { job_pattern_.reset(cairo_pattern_create_linear(0.0, 0.0, box_width, 0.0)); cairo_pattern_add_color_stop_rgb(job_pattern_.get(), 0.000000, 0.000000, 0.000000, 0.000000); cairo_pattern_add_color_stop_rgb(job_pattern_.get(), 0.594324, 0.729412, 0.000000, 0.000000); cairo_pattern_add_color_stop_rgb(job_pattern_.get(), 0.809683, 1.000000, 0.545098, 0.196078); cairo_pattern_add_color_stop_rgb(job_pattern_.get(), 0.899833, 0.972549, 0.937255, 0.074510); cairo_pattern_add_color_stop_rgb(job_pattern_.get(), 1.000000, 0.976471, 0.968627, 0.831373); } pango_layout_set_width(layout, (box_width - margin_x * 2) * PANGO_SCALE); unsigned col = 0; for (auto const& machine : machines_) { pango_layout_set_text(layout, machine->data.name.data(), machine->data.name.size()); auto x = pad_x + col * (box_width + pad_x); rounded_path(cairo, x, y, box_width, box_height); cairo_set_source_rgba(cairo, 0.1, 0.1, 0.1, 0.7); if (machine->x > 0.0) { cairo_fill_preserve(cairo); auto old = cairo::unique_path(cairo_copy_path(cairo)); cairo_new_path(cairo); cairo_rectangle(cairo, x, y, machine->x * box_width, box_height); cairo_clip(cairo); cairo_append_path(cairo, old.get()); cairo_matrix_t matrix; cairo_matrix_init_translate(&matrix, -x, 0); cairo_pattern_set_matrix(job_pattern_.get(), &matrix); cairo_set_source(cairo, job_pattern_.get()); cairo_fill(cairo); cairo_reset_clip(cairo); } else { cairo_fill(cairo); } if (machine->requests > 0) { auto radius = box_height / 10.0; cairo_set_line_width(cairo, 1.5); cairo_new_path(cairo); cairo_move_to(cairo, x + box_width - radius, y); cairo_rel_line_to(cairo, -(machine->requests * (box_width - radius * 2)) / requests_, 0); cairo_set_source_rgb(cairo, 0, 0.6, 0); cairo_stroke(cairo); } cairo_move_to(cairo, x + margin_x, y + margin_y); pango_cairo_layout_path(cairo, layout); cairo_set_source_rgb(cairo, 0.0, 0.0, 0.0); cairo_set_line_cap(cairo, CAIRO_LINE_CAP_ROUND); cairo_set_line_width(cairo, 2.0); cairo_stroke(cairo); cairo_set_source_rgb(cairo, 1.0, 1.0, 1.0); cairo_move_to(cairo, x + margin_x, y + margin_y); pango_cairo_show_layout(cairo, layout); if (++col == columns) { col = 0; y += box_height + pad_y * 2; if (y >= h) break; } } } void added_machine(Monitor*, uint32_t id) override { auto machine = std::make_unique(id); update(machine.get()); machines_.emplace( std::lower_bound(machines_.begin(), machines_.end(), machine, compare_machine), std::move(machine)); draw(); } static bool compare_machine(std::unique_ptr const& m1, std::unique_ptr const& m2) { return *m1 < *m2; } void updated_machine(Monitor*, uint32_t id) override { for (auto& machine : machines_) { if (machine->id == id) { auto old = machine->data.name; update(machine.get()); if (machine->data.name != old) { // TODO: Perhaps remove and insert instead? std::sort(machines_.begin(), machines_.end(), compare_machine); } draw(); return; } } assert(false); } void removed_machine(Monitor*, uint32_t id) override { for (auto it = machines_.begin(); it != machines_.end(); ++it) { if ((*it)->id == id) { max_jobs_ -= (*it)->data.max_jobs; requests_ -= (*it)->requests; jobs_ -= (*it)->jobs; if (animator_) animator_->stop((*it)->animation.get()); machines_.erase(it); draw(); return; } } assert(false); } void added_job(Monitor*, uint32_t source, uint32_t target) override { if (source == target) { for (auto& machine : machines_) { if (machine->id == source) { ++machine->jobs; animate(machine.get()); } } } else { for (auto& machine : machines_) { if (machine->id == source) { ++machine->requests; } if (machine->id == target) { ++machine->jobs; animate(machine.get()); } } ++requests_; } ++jobs_; draw(); } void removed_job(Monitor*, uint32_t source, uint32_t target) override { if (source == target) { for (auto& machine : machines_) { if (machine->id == source) { --machine->jobs; animate(machine.get()); } } } else { for (auto& machine : machines_) { if (machine->id == source) { --machine->requests; } if (machine->id == target) { --machine->jobs; animate(machine.get()); } } --requests_; } --jobs_; draw(); } void animate(Machine* machine) { if (!animator_) return; if (machine->animation) return; machine->animation = std::make_unique(machine); animator_->start(machine->animation, machine); } std::unique_ptr monitor_; bool connected_; std::vector> machines_; unsigned max_jobs_; unsigned jobs_; unsigned requests_; cairo::unique_pattern job_pattern_; unsigned force_columns_; }; struct XcbKeySymbolsDelete { void operator()(xcb_key_symbols_t* ptr) const { if (ptr) xcb_key_symbols_free(ptr); } }; struct XcbGenericEventDelete { void operator()(xcb_generic_event_t* ptr) const { free(ptr); } }; void xcb_event_loop(x::shared_connection const& conn, std::shared_ptr const& atoms, std::shared_ptr const& monmon) { std::unique_ptr syms; syms.reset(xcb_key_symbols_alloc(conn.get())); std::unique_ptr event; auto const quit = atoms->get("__MONMON_QUIT"); auto const wm_protocols = atoms->get("WM_PROTOCOLS"); auto const wm_delete_window = atoms->get("WM_DELETE_WINDOW"); while (true) { event.reset(xcb_wait_for_event(conn.get())); if (!event) { std::cerr << "fatal error: " << xcb_connection_has_error(conn.get()) << std::endl; monmon->quit_from_xcb(); return; } if (event->response_type == 0) { auto e = reinterpret_cast(event.get()); #ifndef NDEBUG std::cerr << "error: " << xcb_event_get_error_label(e->error_code) << std::endl; #endif continue; } switch (XCB_EVENT_RESPONSE_TYPE(event)) { case XCB_EXPOSE: { auto e = reinterpret_cast(event.get()); if (monmon->match(e->window)) { monmon->expose(e->x, e->y, e->width, e->height); } if (e->count != 0) continue; // Only flush for e->count == 0 break; } case XCB_KEY_PRESS: { auto e = reinterpret_cast(event.get()); auto sym = xcb_key_press_lookup_keysym(syms.get(), e, 0); switch (sym) { case 'Q': case 'q': case 0xff1b: { // escape if (monmon->match(e->event)) { monmon->quit_from_xcb(); return; } break; } #if FAKE_MONITOR case ' ': if (monmon->match(e->event)) { monmon->toggle_fakes(); } break; #endif } break; } case XCB_DESTROY_NOTIFY: { auto e = reinterpret_cast(event.get()); if (monmon->match(e->window)) { monmon->quit_from_xcb(); return; } break; } case XCB_CONFIGURE_NOTIFY: { auto e = reinterpret_cast(event.get()); if (XCB_EVENT_SENT(e) && monmon->match(e->window)) { monmon->configure(e->x, e->y, e->width, e->height); } break; } case XCB_CLIENT_MESSAGE: { auto e = reinterpret_cast(event.get()); if (e->format == 32 && e->type == wm_protocols && e->data.data32[0] == wm_delete_window) { if (monmon->match(e->window)) { monmon->quit_from_xcb(); return; } } else if (e->format == 32 && e->type == quit) { // Do not call quit_from_xcb() here, only reason to end up // here is if main already called quit_from_main() return; } break; } case XCB_MAPPING_NOTIFY: { auto e = reinterpret_cast(event.get()); xcb_refresh_keyboard_mapping(syms.get(), e); break; } case XCB_REPARENT_NOTIFY: { auto e = reinterpret_cast(event.get()); if (monmon->match(e->window)) { monmon->update_desktop_window(); } break; } case XCB_PROPERTY_NOTIFY: { auto e = reinterpret_cast(event.get()); monmon->update_desktop_window(e->window, e->atom); break; } } xcb_flush(conn.get()); } } } // namespace int main(int argc, char** argv) { std::unique_ptr args(Args::create()); args->add("network", "NETNAME", "monitor NETNAME instead of default ICECREAM"); args->add("scheduler", "HOST", "connect to scheduler at HOST instead of broadcasting"); args->add("display", "HOST:VS", "connect to X server at [HOST]:[VS]"); args->add("no-render", "do not use render extension even if available"); args->add("titlebar", "create a normal window instead of the default without" " titlebar (or other WM addons)"); args->add("black", "use black background instead of background pixmap"); args->add("columns", "COLUMNS", "force columns to be COLUMNS instead of" " calculated from window size"); args->add("help", "display this text and exit."); if (!args->run(argc, argv)) { std::cout << "Try `monmon --help` for usage." << std::endl; return EXIT_FAILURE; } if (args->is_set("help")) { std::cout << "Usage: `monmon [OPTIONS...]`\n" << "icecream (icecc) monitor\n" << "\n"; args->print_help(std::cout); return EXIT_FAILURE; } unsigned columns = 0; { auto tmp = args->arg("columns", nullptr); if (tmp) { char* end = nullptr; errno = 0; columns = strtoul(tmp, &end, 10); if (errno || columns == 0 || *end) { std::cerr << "Invalid value for columns: '" << tmp << "'" << std::endl; return EXIT_FAILURE; } } } int screen_index; x::shared_connection conn( xcb_connect(args->arg("display", nullptr), &screen_index)); if (xcb_connection_has_error(conn.get())) { std::cerr << "Unable to connect to X server." << std::endl; return EXIT_FAILURE; } x::prefetch_extensions(conn.get()); auto screen = x::get_screen(conn.get(), screen_index); auto format = x::get_best_format(conn.get(), screen, !args->is_set("no-render")); if (!format.good()) { std::cerr << "Unable to find a good X visual." << std::endl; return EXIT_FAILURE; } std::shared_ptr looper(PollLooper::create()); std::shared_ptr atoms(x::Atoms::create(conn)); atoms->preload("WM_DELETE_WINDOW"); atoms->preload("WM_PROTOCOLS"); atoms->preload("__MONMON_QUIT"); MonMon::preload(atoms.get()); std::shared_ptr ewmh(x::Ewmh::create(conn, screen_index)); auto monmon = std::make_shared(looper, columns); monmon->init(conn, screen, format, atoms, ewmh, 400, 400, args->is_set("titlebar"), args->is_set("black")); monmon->connect(args.get()); std::thread xcb_thread(xcb_event_loop, conn, atoms, monmon); conn.reset(); looper->run(); xcb_thread.join(); return EXIT_SUCCESS; }