melawy-welcome/src/hello.cpp

457 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "hello.hpp"
#include "helper.hpp"
#include <glib/gi18n.h>
#include <libintl.h>
#include <filesystem>
#include <iostream>
#include <iterator>
#include <stdexcept>
#include <unordered_map>
#include <fmt/core.h>
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<std::string, 2> get_lsb_infos() {
std::unordered_map<std::string, std::string> 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<GtkDialogFlags>(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 weve added
gtk_container_add(GTK_CONTAINER(content_area), label);
gtk_widget_show_all(dialog);
int result = gtk_dialog_run(GTK_DIALOG(dialog));
std::vector<std::string> 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<GObject*>(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<Glib::Object> 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<std::string>().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<Gtk::Container*>(child)->get_children();
const auto& second_child = reinterpret_cast<Gtk::Container*>(first_child[0])->get_children();
const auto& third_child = reinterpret_cast<Gtk::Container*>(second_child[0])->get_children();
const auto& label = reinterpret_cast<Gtk::Label*>(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<std::string>(), g_refHello->m_save);
const auto& application = g_refHello->get_application();
application->quit();
}