#include "hello.hpp" #include "helper.hpp" #include #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace { Hello* g_refHello; std::string fix_path(std::string&& path) noexcept { if (path[0] != '~') { return path; } replace_all(path, "~", Glib::get_home_dir().c_str()); return path; } nlohmann::json read_json(const std::string_view& path) { const auto& buf = fix_path(path.data()); if (!fs::exists(buf)) { throw std::runtime_error(fmt::format("File does not exist: \"{}\"", buf)); } // read a JSON file std::ifstream i(buf); nlohmann::json j; i >> j; return j; } void write_json(const std::string_view& path, const nlohmann::json& content) { // write data to JSON file std::ofstream o(fix_path(path.data())); o << content << '\n'; } // Read information from the lsb-release file. // // @Returns args from lsb-release file std::array get_lsb_infos() { std::unordered_map lsb{}; try { std::ifstream lsb_release("/etc/lsb-release"); std::string line; while (std::getline(lsb_release, line)) { if (line.find('=') != std::string::npos) { auto var = tokenize(line, "="); remove_all(var.first, "DISTRIB_"); remove_all(var.second, "\""); lsb[var.first] = var.second; } } } catch (const std::exception& e) { std::cerr << e.what() << '\n'; return {"not CachyOS", "0.0"}; } return {lsb["ID"], lsb["RELEASE"]}; } void child_watch_cb(GPid pid, [[maybe_unused]] gint status, gpointer /*user_data*/) { #if !defined(NDEBUG) g_message("Child %" G_PID_FORMAT " exited %s", pid, g_spawn_check_wait_status(status, nullptr) ? "normally" : "abnormally"); #endif // Free any resources associated with the child here, such as I/O channels // on its stdout and stderr FDs. If you have no code to put in the // child_watch_cb() callback, you can remove it and the g_child_watch_add() // call, but you must also remove the G_SPAWN_DO_NOT_REAP_CHILD flag, // otherwise the child process will stay around as a zombie until this // process exits. g_spawn_close_pid(pid); } void quick_message(Gtk::Window* parent, const std::string& message) { // Create the widgets const auto& flags = static_cast(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT); auto* dialog = gtk_dialog_new_with_buttons(message.c_str(), parent->gobj(), flags, _("_Offline"), GTK_RESPONSE_NO, _("_Online"), GTK_RESPONSE_YES, nullptr); auto* content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); auto* label = gtk_label_new(message.c_str()); // Add the label, and show everything we’ve added gtk_container_add(GTK_CONTAINER(content_area), label); gtk_widget_show_all(dialog); int result = gtk_dialog_run(GTK_DIALOG(dialog)); std::vector argv{}; if (result == GTK_RESPONSE_NO) { argv = {fix_path("/usr/local/bin/calamares-offline.sh")}; } else { argv = {fix_path("/usr/local/bin/calamares-online.sh")}; } int child_stdout{}; int child_stderr{}; Glib::Pid child_pid; // Spawn child process. try { Glib::spawn_async_with_pipes(".", argv, Glib::SpawnFlags::SPAWN_DO_NOT_REAP_CHILD, Glib::SlotSpawnChildSetup(), &child_pid, nullptr, &child_stdout, &child_stderr); } catch (Glib::Error& error) { g_critical("%s", error.what().c_str()); } // Add a child watch function which will be called when the child process // exits. g_child_watch_add(child_pid, child_watch_cb, nullptr); gtk_widget_destroy(dialog); } } // namespace Hello::Hello(int argc, char** argv) { set_title("CachyOS Hello"); set_border_width(6); if (argc > 1 && (strncmp(argv[1], "--dev", 5) == 0)) { m_dev = true; } g_refHello = this; auto screen = Gdk::Screen::get_default(); // Load preferences if (m_dev) { m_preferences = read_json("data/preferences.json"); m_preferences["data_path"] = "data/"; m_preferences["desktop_path"] = fmt::format("{}/{}.desktop", fs::current_path().string(), m_app); m_preferences["locale_path"] = "locale/"; m_preferences["ui_path"] = fmt::format("ui/{}.glade", m_app); m_preferences["style_path"] = "ui/style.css"; } else { m_preferences = read_json(fmt::format("/usr/share/{}/data/preferences.json", m_app)); } // Get saved infos const auto& save_path = fix_path(m_preferences["save_path"]); m_save = (!fs::exists(save_path)) ? nlohmann::json({{"locale", ""}}) : read_json(save_path); // Import Css auto provider = Gtk::CssProvider::create(); provider->load_from_path(m_preferences["style_path"]); Gtk::StyleContext::add_provider_for_screen(screen, provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); // Init window m_builder = Gtk::Builder::create_from_file(m_preferences["ui_path"]); gtk_builder_add_callback_symbol(m_builder->gobj(), "on_languages_changed", G_CALLBACK(on_languages_changed)); gtk_builder_add_callback_symbol(m_builder->gobj(), "on_action_clicked", G_CALLBACK(on_action_clicked)); gtk_builder_add_callback_symbol(m_builder->gobj(), "on_btn_clicked", G_CALLBACK(on_btn_clicked)); gtk_builder_add_callback_symbol(m_builder->gobj(), "on_link_clicked", G_CALLBACK(on_link_clicked)); gtk_builder_add_callback_symbol(m_builder->gobj(), "on_delete_window", G_CALLBACK(on_delete_window)); gtk_builder_connect_signals(m_builder->gobj(), nullptr); Gtk::Window* ref_window; m_builder->get_widget("window", ref_window); gobject_ = reinterpret_cast(ref_window->gobj()); // Subtitle of headerbar Gtk::HeaderBar* header; m_builder->get_widget("headerbar", header); const auto& lsb_info = get_lsb_infos(); header->set_subtitle(lsb_info[0] + " " + lsb_info[1]); // Load images if (fs::is_regular_file(m_preferences["logo_path"])) { const auto& logo = Gdk::Pixbuf::create_from_file(m_preferences["logo_path"]); set_icon(logo); Gtk::Image* image; m_builder->get_widget("distriblogo", image); image->set(logo); Gtk::AboutDialog* dialog; m_builder->get_widget("aboutdialog", dialog); dialog->set_logo(logo); } Gtk::Box* social_box; m_builder->get_widget("social", social_box); for (const auto& btn : social_box->get_children()) { const auto& name = btn->get_name(); const auto& icon_path = fmt::format("{}img/{}.png", m_preferences["data_path"], name.c_str()); Gtk::Image* image; m_builder->get_widget(name, image); image->set(icon_path); } Gtk::Grid* homepage_grid; m_builder->get_widget("homepage", homepage_grid); for (const auto& widget : homepage_grid->get_children()) { if (!G_TYPE_CHECK_INSTANCE_TYPE(widget->gobj(), GTK_TYPE_BUTTON)) { continue; } const auto& casted_widget = Glib::wrap(GTK_BUTTON(widget->gobj())); if (gtk_button_get_image_position(casted_widget->gobj()) != GtkPositionType::GTK_POS_RIGHT) { continue; } const auto& image_path = fmt::format("{}img/external-link.png", m_preferences["data_path"]); Gtk::Image image; image.set(image_path); image.set_margin_start(2); casted_widget->set_image(image); } // Create pages m_pages = fmt::format("{}pages/{}", m_preferences["data_path"], m_preferences["default_locale"]); for (const auto& page : fs::directory_iterator(m_pages)) { auto* scrolled_window = gtk_scrolled_window_new(nullptr, nullptr); auto* viewport = gtk_viewport_new(nullptr, nullptr); gtk_container_set_border_width(GTK_CONTAINER(viewport), 10); auto* label = gtk_label_new(nullptr); gtk_label_set_line_wrap(GTK_LABEL(label), true); auto* image = gtk_image_new_from_icon_name("go-previous", GTK_ICON_SIZE_BUTTON); auto* backBtn = gtk_button_new(); gtk_button_set_image(GTK_BUTTON(backBtn), image); gtk_widget_set_name(backBtn, "home"); g_signal_connect(backBtn, "clicked", G_CALLBACK(&on_btn_clicked), nullptr); auto* grid = GTK_GRID(gtk_grid_new()); gtk_grid_attach(grid, backBtn, 0, 1, 1, 1); gtk_grid_attach(grid, label, 1, 2, 1, 1); gtk_container_add(GTK_CONTAINER(viewport), GTK_WIDGET(grid)); gtk_container_add(GTK_CONTAINER(scrolled_window), GTK_WIDGET(viewport)); gtk_widget_show_all(scrolled_window); Glib::RefPtr stack = m_builder->get_object("stack"); const auto& child_name = page.path().filename().string() + "page"; gtk_stack_add_named(GTK_STACK(stack->gobj()), scrolled_window, child_name.c_str()); } // Init translation const std::string& locale_path = m_preferences["locale_path"]; bindtextdomain(m_app, locale_path.c_str()); bind_textdomain_codeset(m_app, "UTF-8"); textdomain(m_app); Gtk::ComboBoxText* languages; m_builder->get_widget("languages", languages); languages->set_active_id(get_best_locale()); // Set autostart switcher state m_autostart = fs::exists(fix_path(m_preferences["autostart_path"])); Gtk::Switch* autostart_switch; m_builder->get_widget("autostart", autostart_switch); autostart_switch->set_active(m_autostart); // Live systems if (fs::exists(m_preferences["live_path"]) && fs::is_regular_file(m_preferences["installer_path"])) { Gtk::Label* installlabel; m_builder->get_widget("installlabel", installlabel); installlabel->set_visible(true); Gtk::Button* install; m_builder->get_widget("install", install); install->set_visible(true); } } /// Returns the best locale, based on user's preferences. auto Hello::get_best_locale() const noexcept -> std::string { const auto& binary_path = fmt::format("{}{}{}.mo", m_preferences["locale_path"], "{}/LC_MESSAGES/", m_app); const auto& saved_locale = fmt::vformat(binary_path, fmt::make_format_args(m_save["locale"])); if (fs::is_regular_file(saved_locale)) { return m_save["locale"]; } else if (m_save["locale"] == m_preferences["default_locale"]) { return m_preferences["default_locale"]; } const auto& locale_name = std::locale("").name(); std::string sys_locale = locale_name.substr(0, locale_name.find('.')); const auto& user_locale = fmt::vformat(binary_path, fmt::make_format_args(sys_locale)); const auto& two_letters = sys_locale.substr(0, 2); // If user's locale is supported if (fs::is_regular_file(user_locale)) { if (sys_locale.find('_') != std::string::npos) { replace_all(sys_locale, "_", "-"); } return sys_locale; } // If two first letters of user's locale is supported (ex: en_US -> en) else if (fs::is_regular_file(fmt::vformat(binary_path, fmt::make_format_args(two_letters)))) { return two_letters; } return m_preferences["default_locale"]; } /// Sets locale of ui and pages. void Hello::set_locale(const std::string_view& use_locale) noexcept { #if !defined(NDEBUG) fmt::print( "┌{0:─^{2}}┐\n" "│{1: ^{2}}│\n" "└{0:─^{2}}┘\n", "", fmt::format("Locale changed to {}", use_locale), 40); #endif textdomain(m_app); Glib::setenv("LANGUAGE", use_locale.data()); m_save["locale"] = use_locale; // Real-time locale changing /* clang-format off */ nlohmann::json elts = { {"comments", {"aboutdialog"}}, {"label", { "autostartlabel", "development", "discover", "donate", "firstcategory", "forum", "install", "installlabel", "involved", "mailling", "readme", "release", "secondcategory", "thirdcategory", "welcomelabel", "welcometitle", "wiki"} }, {"tooltip_text", { "about", "development", "discover", "donate", "forum", "mailling", "wiki"} }}; /* clang-format on */ for (const auto& method : elts.items()) { if (!m_default_texts.contains(method.key())) { m_default_texts[method.key()] = {}; } for (const auto& elt : elts[method.key()].items()) { const std::string& elt_value = elt.value(); Gtk::Widget* item; m_builder->get_widget(elt_value, item); if (!m_default_texts[method.key()].contains(elt_value)) { gchar* item_buf; g_object_get(G_OBJECT(item->gobj()), method.key().c_str(), &item_buf, nullptr); m_default_texts[method.key()][elt_value] = item_buf; g_free(item_buf); } if (method.key() == "tooltip_text" || method.key() == "comments") { g_object_set(G_OBJECT(item->gobj()), method.key().c_str(), _(m_default_texts[method.key()][elt_value].get().c_str()), nullptr); } } } // Change content of pages for (const auto& page : fs::directory_iterator(m_pages)) { Gtk::Stack* stack; m_builder->get_widget("stack", stack); const auto& child = stack->get_child_by_name((page.path().filename().string() + "page").c_str()); if (child == nullptr) { fmt::print(stderr, "child not found\n"); continue; } const auto& first_child = reinterpret_cast(child)->get_children(); const auto& second_child = reinterpret_cast(first_child[0])->get_children(); const auto& third_child = reinterpret_cast(second_child[0])->get_children(); const auto& label = reinterpret_cast(third_child[0]); label->set_markup(get_page(page.path().filename().string())); } } void Hello::set_autostart(const bool& autostart) noexcept { fs::path autostart_path{fix_path(m_preferences["autostart_path"])}; const auto& config_dir = autostart_path.parent_path(); if (!fs::exists(config_dir)) { fs::create_directories(config_dir); } if (autostart && !fs::is_regular_file(autostart_path)) { fs::create_symlink(m_preferences["desktop_path"], autostart_path); } else if (!autostart && fs::is_regular_file(autostart_path)) { fs::remove(autostart_path); } m_autostart = autostart; } auto Hello::get_page(const std::string& name) const noexcept -> std::string { auto filename = fmt::format("{}pages/{}/{}", m_preferences["data_path"], m_save["locale"], name); if (!fs::is_regular_file(filename)) { filename = fmt::format("{}pages/{}/{}", m_preferences["data_path"], m_preferences["default_locale"], name); } return read_whole_file(filename); } // Handlers void Hello::on_languages_changed(GtkComboBox* combobox) noexcept { const auto& active_id = gtk_combo_box_get_active_id(combobox); g_refHello->set_locale(active_id); } void Hello::on_action_clicked(GtkWidget* widget) noexcept { const auto& name = gtk_widget_get_name(widget); if (strncmp(name, "install", 7) == 0) { quick_message(g_refHello, "Calamares install type"); return; } else if (strncmp(name, "autostart", 9) == 0) { const auto& action = Glib::wrap(GTK_SWITCH(widget)); g_refHello->set_autostart(action->get_active()); return; } Gtk::AboutDialog* dialog; g_refHello->m_builder->get_widget("aboutdialog", dialog); dialog->set_decorated(false); dialog->run(); dialog->hide(); } void Hello::on_btn_clicked(GtkWidget* widget) noexcept { const auto& name = gtk_widget_get_name(widget); Gtk::Stack* stack; g_refHello->m_builder->get_widget("stack", stack); stack->set_visible_child(fmt::format("{}page", name).c_str()); } void Hello::on_link_clicked(GtkWidget* widget) noexcept { const auto& name = gtk_widget_get_name(widget); const std::string uri = g_refHello->m_preferences["urls"][name]; gtk_show_uri_on_window(nullptr, uri.c_str(), GDK_CURRENT_TIME, nullptr); } void Hello::on_delete_window(GtkWidget* /*widget*/) noexcept { write_json(g_refHello->m_preferences["save_path"].get(), g_refHello->m_save); const auto& application = g_refHello->get_application(); application->quit(); }