#include "ssh_client.h" #include #include static GtkWidget* make_button(const char* label, GCallback cb, gpointer data) { GtkWidget* b = gtk_button_new_with_label(label); g_signal_connect(b, "clicked", cb, data); return b; } static void clear_container(GtkWidget* container) { GList* children = gtk_container_get_children(GTK_CONTAINER(container)); for (GList* l = children; l != nullptr; l = l->next) gtk_widget_destroy(GTK_WIDGET(l->data)); g_list_free(children); } static void refresh_server_list_widget(GtkWidget* listbox, const std::vector& servers) { clear_container(listbox); for (const auto& s : servers) { GtkWidget* row_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); gtk_widget_set_margin_start(row_box, 8); gtk_widget_set_margin_end(row_box, 8); gtk_widget_set_margin_top(row_box, 6); gtk_widget_set_margin_bottom(row_box, 6); GtkWidget* name_label = gtk_label_new(s.name.c_str()); gtk_widget_set_halign(name_label, GTK_ALIGN_START); gtk_label_set_ellipsize(GTK_LABEL(name_label), PANGO_ELLIPSIZE_END); PangoAttrList* attrs = pango_attr_list_new(); pango_attr_list_insert(attrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD)); gtk_label_set_attributes(GTK_LABEL(name_label), attrs); pango_attr_list_unref(attrs); gtk_box_pack_start(GTK_BOX(row_box), name_label, FALSE, FALSE, 0); std::string detail = s.user + "@" + s.host + ":" + std::to_string(s.port); if (s.use_key) detail += " [key]"; else if (s.save_password) detail += " [saved]"; GtkWidget* detail_label = gtk_label_new(detail.c_str()); gtk_widget_set_halign(detail_label, GTK_ALIGN_START); gtk_label_set_ellipsize(GTK_LABEL(detail_label), PANGO_ELLIPSIZE_END); gtk_box_pack_start(GTK_BOX(row_box), detail_label, FALSE, FALSE, 0); gtk_list_box_insert(GTK_LIST_BOX(listbox), row_box, -1); } } static void refresh_key_list_widget(GtkWidget* listbox, const std::vector& keys) { clear_container(listbox); for (const auto& k : keys) { std::string text = k.name + " | " + k.fingerprint; GtkWidget* row = gtk_label_new(text.c_str()); gtk_widget_set_halign(row, GTK_ALIGN_START); gtk_widget_set_margin_start(row, 8); gtk_widget_set_margin_top(row, 4); gtk_widget_set_margin_bottom(row, 4); gtk_list_box_insert(GTK_LIST_BOX(listbox), row, -1); } } /* ─── ctor / dtor ─────────────────────────────────────────── */ SshClientWindow::SshClientWindow() : window_(nullptr), notebook_(nullptr), statusbar_(nullptr), server_combo_(nullptr), quick_entry_(nullptr), user_entry_(nullptr), port_spin_(nullptr), server_listbox_(nullptr), notif_revealer_(nullptr), notif_label_(nullptr), notif_bar_(nullptr) { build_ui(); } SshClientWindow::~SshClientWindow() { for (auto* t : terminals_) { t->disconnect(); delete t; } terminals_.clear(); if (window_) gtk_widget_destroy(window_); } /* ─── build UI ────────────────────────────────────────────── */ void SshClientWindow::build_ui() { window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window_), "x3-ssh"); gtk_window_set_default_size(GTK_WINDOW(window_), 1100, 700); g_signal_connect(window_, "destroy", G_CALLBACK(gtk_main_quit), nullptr); const char* icon_paths[] = { "icons/x3-ssh.png", "/usr/share/icons/hicolor/256x256/apps/x3-ssh.png", "/usr/local/share/icons/x3-ssh.png", nullptr }; for (int i = 0; icon_paths[i]; ++i) { if (g_file_test(icon_paths[i], G_FILE_TEST_EXISTS)) { gtk_window_set_icon_from_file(GTK_WINDOW(window_), icon_paths[i], nullptr); break; } } GtkWidget* main_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add(GTK_CONTAINER(window_), main_vbox); /* ── notification bar ── */ notif_bar_ = gtk_revealer_new(); gtk_revealer_set_transition_type(GTK_REVEALER(notif_bar_), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); gtk_revealer_set_reveal_child(GTK_REVEALER(notif_bar_), FALSE); GtkWidget* notif_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); gtk_widget_set_margin_start(notif_box, 12); gtk_widget_set_margin_end(notif_box, 12); gtk_widget_set_margin_top(notif_box, 6); gtk_widget_set_margin_bottom(notif_box, 6); notif_label_ = gtk_label_new(""); gtk_widget_set_halign(notif_label_, GTK_ALIGN_START); gtk_widget_set_hexpand(notif_label_, TRUE); gtk_box_pack_start(GTK_BOX(notif_box), notif_label_, TRUE, TRUE, 0); GtkWidget* notif_close = gtk_button_new_from_icon_name("window-close", GTK_ICON_SIZE_MENU); gtk_widget_set_size_request(notif_close, 24, 24); g_signal_connect(notif_close, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) { gtk_revealer_set_reveal_child(GTK_REVEALER(d), FALSE); }), notif_bar_); gtk_box_pack_end(GTK_BOX(notif_box), notif_close, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(notif_bar_), notif_box); GtkCssProvider* css = gtk_css_provider_new(); gtk_css_provider_load_from_data(css, ".notif-ok { background: #2ecc71; color: #000; }" ".notif-err { background: #e74c3c; color: #fff; }", -1, nullptr); GtkStyleContext* ctx = gtk_widget_get_style_context(notif_bar_); gtk_style_context_add_provider(ctx, GTK_STYLE_PROVIDER(css), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_object_unref(css); gtk_box_pack_start(GTK_BOX(main_vbox), notif_bar_, FALSE, FALSE, 0); /* ── toolbar ── */ GtkWidget* toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); gtk_widget_set_margin_start(toolbar, 6); gtk_widget_set_margin_end(toolbar, 6); gtk_widget_set_margin_top(toolbar, 4); gtk_widget_set_margin_bottom(toolbar, 4); gtk_box_pack_start(GTK_BOX(main_vbox), toolbar, FALSE, FALSE, 0); server_combo_ = gtk_combo_box_text_new(); gtk_combo_box_set_wrap_width(GTK_COMBO_BOX(server_combo_), 0); gtk_widget_set_size_request(server_combo_, 180, -1); gtk_box_pack_start(GTK_BOX(toolbar), server_combo_, FALSE, FALSE, 0); quick_entry_ = gtk_entry_new(); gtk_entry_set_placeholder_text(GTK_ENTRY(quick_entry_), "host"); gtk_widget_set_size_request(quick_entry_, 150, -1); gtk_box_pack_start(GTK_BOX(toolbar), quick_entry_, FALSE, FALSE, 0); user_entry_ = gtk_entry_new(); gtk_entry_set_placeholder_text(GTK_ENTRY(user_entry_), "user"); gtk_widget_set_size_request(user_entry_, 100, -1); gtk_box_pack_start(GTK_BOX(toolbar), user_entry_, FALSE, FALSE, 0); port_spin_ = gtk_spin_button_new_with_range(1, 65535, 1); gtk_spin_button_set_value(GTK_SPIN_BUTTON(port_spin_), 22); gtk_widget_set_size_request(port_spin_, 80, -1); gtk_box_pack_start(GTK_BOX(toolbar), port_spin_, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(toolbar), make_button("Connect", G_CALLBACK(on_quick_connect), this), FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(toolbar), make_button("+ Tab", G_CALLBACK(on_new_tab), this), FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(toolbar), make_button("Close Tab", G_CALLBACK(on_close_tab), this), FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(toolbar), make_button("Disconnect", G_CALLBACK(on_disconnect), this), FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(toolbar), make_button("Keys", G_CALLBACK(on_manage_keys), this), FALSE, FALSE, 0); update_server_combo(); GtkWidget* sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); gtk_box_pack_start(GTK_BOX(main_vbox), sep, FALSE, FALSE, 0); /* ── paned: sidebar | notebook ── */ GtkWidget* paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); gtk_box_pack_start(GTK_BOX(main_vbox), paned, TRUE, TRUE, 0); /* ── sidebar ── */ GtkWidget* sidebar = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_paned_pack1(GTK_PANED(paned), sidebar, FALSE, FALSE); gtk_widget_set_size_request(sidebar, 220, -1); GtkWidget* sidebar_header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); gtk_widget_set_margin_start(sidebar_header, 8); gtk_widget_set_margin_end(sidebar_header, 4); gtk_widget_set_margin_top(sidebar_header, 6); gtk_widget_set_margin_bottom(sidebar_header, 6); GtkWidget* sidebar_title = gtk_label_new(nullptr); gtk_label_set_markup(GTK_LABEL(sidebar_title), "Servers"); gtk_box_pack_start(GTK_BOX(sidebar_header), sidebar_title, TRUE, TRUE, 0); GtkWidget* add_srv_btn = gtk_button_new_from_icon_name("list-add", GTK_ICON_SIZE_MENU); gtk_widget_set_size_request(add_srv_btn, 28, 28); g_signal_connect(add_srv_btn, "clicked", G_CALLBACK(on_add_server), this); gtk_box_pack_end(GTK_BOX(sidebar_header), add_srv_btn, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(sidebar), sidebar_header, FALSE, FALSE, 0); GtkWidget* sidebar_sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); gtk_box_pack_start(GTK_BOX(sidebar), sidebar_sep, FALSE, FALSE, 0); GtkWidget* scrolled = gtk_scrolled_window_new(nullptr, nullptr); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); server_listbox_ = gtk_list_box_new(); gtk_list_box_set_selection_mode(GTK_LIST_BOX(server_listbox_), GTK_SELECTION_SINGLE); g_signal_connect(server_listbox_, "row-activated", G_CALLBACK(on_server_row_activated), this); g_signal_connect(server_listbox_, "button-press-event", G_CALLBACK(on_server_row_button), this); gtk_container_add(GTK_CONTAINER(scrolled), server_listbox_); gtk_box_pack_start(GTK_BOX(sidebar), scrolled, TRUE, TRUE, 0); update_server_list(); /* ── notebook ── */ notebook_ = gtk_notebook_new(); gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook_), TRUE); gtk_paned_pack2(GTK_PANED(paned), notebook_, TRUE, TRUE); /* ── statusbar ── */ statusbar_ = gtk_statusbar_new(); gtk_box_pack_end(GTK_BOX(main_vbox), statusbar_, FALSE, FALSE, 0); } void SshClientWindow::show_notification(const std::string& msg, bool is_error) { gtk_label_set_text(GTK_LABEL(notif_label_), msg.c_str()); GtkStyleContext* nctx = gtk_widget_get_style_context(notif_bar_); gtk_style_context_remove_class(nctx, "notif-ok"); gtk_style_context_remove_class(nctx, "notif-err"); gtk_style_context_add_class(nctx, is_error ? "notif-err" : "notif-ok"); gtk_revealer_set_reveal_child(GTK_REVEALER(notif_bar_), TRUE); g_timeout_add(4000, [](gpointer data) -> gboolean { gtk_revealer_set_reveal_child(GTK_REVEALER(data), FALSE); return G_SOURCE_REMOVE; }, notif_bar_); } void SshClientWindow::update_server_combo() { gtk_combo_box_text_remove_all(GTK_COMBO_BOX_TEXT(server_combo_)); gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(server_combo_), "(Quick Connect)"); for (const auto& s : server_store_.servers()) { std::string label = s.name + " (" + s.user + "@" + s.host + ":" + std::to_string(s.port) + ")"; gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(server_combo_), label.c_str()); } gtk_combo_box_set_active(GTK_COMBO_BOX(server_combo_), 0); } void SshClientWindow::update_server_list() { refresh_server_list_widget(server_listbox_, server_store_.servers()); update_server_combo(); } void SshClientWindow::update_status(const std::string& msg) { if (!statusbar_) return; guint ctx_id = gtk_statusbar_get_context_id(GTK_STATUSBAR(statusbar_), "main"); gtk_statusbar_pop(GTK_STATUSBAR(statusbar_), ctx_id); gtk_statusbar_push(GTK_STATUSBAR(statusbar_), ctx_id, msg.c_str()); } /* ─── tab management ──────────────────────────────────────── */ void SshClientWindow::add_new_tab() { SshTerminal* term = new SshTerminal(); term->set_server_name("New Tab"); terminals_.push_back(term); GtkWidget* page = term->widget(); GtkWidget* label = gtk_label_new("New Tab"); int idx = gtk_notebook_append_page(GTK_NOTEBOOK(notebook_), page, label); gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(notebook_), page, TRUE); gtk_widget_show_all(page); gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook_), idx); } void SshClientWindow::close_current_tab() { int idx = gtk_notebook_get_current_page(GTK_NOTEBOOK(notebook_)); if (idx < 0) return; close_tab_at(idx); } void SshClientWindow::close_tab_at(int idx) { GtkWidget* page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook_), idx); if (!page) return; SshTerminal* found = nullptr; for (auto it = terminals_.begin(); it != terminals_.end(); ++it) { if ((*it)->widget() == page) { found = *it; terminals_.erase(it); break; } } /* Remove page BEFORE deleting terminal to avoid crash */ gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx); if (found) { found->disconnect(); delete found; } } void SshClientWindow::disconnect() { int idx = gtk_notebook_get_current_page(GTK_NOTEBOOK(notebook_)); if (idx < 0) return; GtkWidget* page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook_), idx); for (auto* t : terminals_) { if (t->widget() == page) { t->disconnect(); update_status("Disconnected"); show_notification("Server disconnected"); return; } } } /* ─── connect ─────────────────────────────────────────────── */ void SshClientWindow::connect_to_server(const std::string& host, int port, const std::string& user, const std::string& keyfile, const std::string& password, const std::string& name) { SshTerminal* term = new SshTerminal(); std::string tab_name = name.empty() ? (user + "@" + host) : name; term->set_server_name(tab_name); terminals_.push_back(term); GtkWidget* page = term->widget(); GtkWidget* label = gtk_label_new(tab_name.c_str()); int idx = gtk_notebook_append_page(GTK_NOTEBOOK(notebook_), page, label); gtk_widget_show_all(page); gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook_), idx); update_status("Connecting to " + host + ":" + std::to_string(port) + "..."); bool ok = term->connect(host, port, user, keyfile, password); if (!ok) { update_status("Failed to connect to " + host); show_notification("Failed to connect to " + host, true); for (auto it = terminals_.begin(); it != terminals_.end(); ++it) { if (*it == term) { terminals_.erase(it); break; } } /* Remove page FIRST, then delete terminal */ gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx); delete term; } else { update_status("Connected to " + user + "@" + host); show_notification("Server connected: " + user + "@" + host + ":" + std::to_string(port)); } } /* ─── quick connect ───────────────────────────────────────── */ void SshClientWindow::on_quick_connect(GtkWidget*, gpointer d) { auto* self = static_cast(d); const char* host = gtk_entry_get_text(GTK_ENTRY(self->quick_entry_)); const char* user = gtk_entry_get_text(GTK_ENTRY(self->user_entry_)); int port = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(self->port_spin_)); if (!host || !*host) { self->update_status("Please enter a hostname"); return; } std::string u = (user && *user) ? user : (getenv("USER") ? getenv("USER") : "root"); int sel = gtk_combo_box_get_active(GTK_COMBO_BOX(self->server_combo_)); if (sel > 0 && sel <= (int)self->server_store_.servers().size()) { const auto& s = self->server_store_.servers()[sel - 1]; if (s.use_key && !s.keyfile.empty()) { self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name); return; } std::string pw = s.save_password ? s.password : ""; if (pw.empty()) { self->show_password_dialog(s.host, s.port, s.user, s.keyfile, s.name); } else { self->connect_to_server(s.host, s.port, s.user, s.keyfile, pw, s.name); } return; } self->show_password_dialog(host, port, u, "", host); } /* ─── server row activated ────────────────────────────────── */ void SshClientWindow::on_server_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer d) { auto* self = static_cast(d); int idx = gtk_list_box_row_get_index(row); if (idx < 0 || idx >= (int)self->server_store_.servers().size()) return; const auto& s = self->server_store_.servers()[idx]; if (s.use_key && !s.keyfile.empty()) { self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name); } else { std::string pw = s.save_password ? s.password : ""; if (pw.empty()) { self->show_password_dialog(s.host, s.port, s.user, s.keyfile, s.name); } else { self->connect_to_server(s.host, s.port, s.user, s.keyfile, pw, s.name); } } } /* ─── server right-click context menu ─────────────────────── */ gboolean SshClientWindow::on_server_row_button(GtkWidget*, GdkEventButton* event, gpointer d) { if (event->type != GDK_BUTTON_PRESS || event->button != 3) return FALSE; auto* self = static_cast(d); GtkListBoxRow* row = gtk_list_box_get_row_at_y(GTK_LIST_BOX(self->server_listbox_), (int)event->y); if (!row) return FALSE; int idx = gtk_list_box_row_get_index(row); if (idx < 0 || idx >= (int)self->server_store_.servers().size()) return FALSE; const auto& s = self->server_store_.servers()[idx]; bool use_key = s.use_key && !s.keyfile.empty(); bool has_saved_pw = s.save_password && !s.password.empty(); GtkWidget* menu = gtk_menu_new(); /* Connect item */ GtkWidget* connect_item = gtk_menu_item_new_with_label("Connect"); struct ConnectCtx { SshClientWindow* w; int i; bool uk; bool sp; }; auto* connect_ctx = new ConnectCtx{self, idx, use_key, has_saved_pw}; g_signal_connect_data(connect_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* c = static_cast(d); const auto& sv = c->w->server_store_.servers()[c->i]; if (c->uk) { c->w->connect_to_server(sv.host, sv.port, sv.user, sv.keyfile, "", sv.name); } else if (c->sp) { c->w->connect_to_server(sv.host, sv.port, sv.user, sv.keyfile, sv.password, sv.name); } else { c->w->show_password_dialog(sv.host, sv.port, sv.user, sv.keyfile, sv.name); } delete c; }), connect_ctx, nullptr, (GConnectFlags)0); gtk_menu_shell_append(GTK_MENU_SHELL(menu), connect_item); /* Edit item */ GtkWidget* edit_item = gtk_menu_item_new_with_label("Edit"); struct EditCtx { SshClientWindow* w; int i; }; auto* edit_ctx = new EditCtx{self, idx}; g_signal_connect_data(edit_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* c = static_cast(d); c->w->show_server_dialog(c->i); c->w->update_server_list(); delete c; }), edit_ctx, nullptr, (GConnectFlags)0); gtk_menu_shell_append(GTK_MENU_SHELL(menu), edit_item); /* Delete item */ GtkWidget* del_item = gtk_menu_item_new_with_label("Delete"); struct DelCtx { SshClientWindow* w; int i; }; auto* del_ctx = new DelCtx{self, idx}; g_signal_connect_data(del_item, "activate", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* c = static_cast(d); std::string name = c->w->server_store_.servers()[c->i].name; c->w->server_store_.remove(c->i); c->w->update_server_list(); c->w->show_notification("Server removed: " + name); delete c; }), del_ctx, nullptr, (GConnectFlags)0); gtk_menu_shell_append(GTK_MENU_SHELL(menu), del_item); gtk_widget_show_all(menu); gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)event); return TRUE; } /* ─── add server button ───────────────────────────────────── */ void SshClientWindow::on_add_server(GtkWidget*, gpointer d) { auto* self = static_cast(d); self->show_server_dialog(-1); } /* ─── password dialog ─────────────────────────────────────── */ void SshClientWindow::show_password_dialog(const std::string& host, int port, const std::string& user, const std::string& keyfile, const std::string& name) { GtkWidget* dialog = gtk_dialog_new_with_buttons( "SSH Authentication", GTK_WINDOW(window_), (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), "_Cancel", GTK_RESPONSE_CANCEL, "_Connect", GTK_RESPONSE_ACCEPT, nullptr); GtkWidget* content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); GtkWidget* grid = gtk_grid_new(); gtk_grid_set_column_spacing(GTK_GRID(grid), 8); gtk_grid_set_row_spacing(GTK_GRID(grid), 6); gtk_widget_set_margin_start(grid, 12); gtk_widget_set_margin_end(grid, 12); gtk_widget_set_margin_top(grid, 12); gtk_widget_set_margin_bottom(grid, 12); GtkWidget* info_label = gtk_label_new(nullptr); char markup[512]; snprintf(markup, sizeof(markup), "Connect to %s@%s:%d", user.c_str(), host.c_str(), port); gtk_label_set_markup(GTK_LABEL(info_label), markup); gtk_grid_attach(GTK_GRID(grid), info_label, 0, 0, 2, 1); GtkWidget* pass_label = gtk_label_new("Password:"); gtk_widget_set_halign(pass_label, GTK_ALIGN_START); gtk_grid_attach(GTK_GRID(grid), pass_label, 0, 1, 1, 1); GtkWidget* pass_entry = gtk_entry_new(); gtk_entry_set_visibility(GTK_ENTRY(pass_entry), FALSE); gtk_entry_set_placeholder_text(GTK_ENTRY(pass_entry), "Enter password"); gtk_widget_set_hexpand(pass_entry, TRUE); gtk_grid_attach(GTK_GRID(grid), pass_entry, 1, 1, 1, 1); gtk_container_add(GTK_CONTAINER(content), grid); gtk_widget_show_all(dialog); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { const char* password = gtk_entry_get_text(GTK_ENTRY(pass_entry)); connect_to_server(host, port, user, keyfile, password ? password : "", name); } gtk_widget_destroy(dialog); } /* ─── server dialog ───────────────────────────────────────── */ void SshClientWindow::show_server_dialog(int edit_index) { bool editing = (edit_index >= 0); ServerConfig existing; if (editing && edit_index < (int)server_store_.servers().size()) { existing = server_store_.servers()[edit_index]; } GtkWidget* dialog = gtk_dialog_new_with_buttons( editing ? "Edit Server" : "Add Server", GTK_WINDOW(window_), (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), "_Cancel", GTK_RESPONSE_CANCEL, editing ? "_Save" : "_Add", GTK_RESPONSE_ACCEPT, nullptr); GtkWidget* content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); GtkWidget* grid = gtk_grid_new(); gtk_grid_set_column_spacing(GTK_GRID(grid), 8); gtk_grid_set_row_spacing(GTK_GRID(grid), 6); gtk_widget_set_margin_start(grid, 12); gtk_widget_set_margin_end(grid, 12); gtk_widget_set_margin_top(grid, 12); gtk_widget_set_margin_bottom(grid, 12); auto add_field = [&](int row, const char* lbl, const char* val) -> GtkWidget* { GtkWidget* l = gtk_label_new(lbl); gtk_widget_set_halign(l, GTK_ALIGN_START); gtk_grid_attach(GTK_GRID(grid), l, 0, row, 1, 1); GtkWidget* e = gtk_entry_new(); if (val && *val) gtk_entry_set_text(GTK_ENTRY(e), val); gtk_widget_set_hexpand(e, TRUE); gtk_grid_attach(GTK_GRID(grid), e, 1, row, 1, 1); return e; }; GtkWidget* name_e = add_field(0, "Name:", editing ? existing.name.c_str() : ""); GtkWidget* host_e = add_field(1, "Host:", editing ? existing.host.c_str() : ""); GtkWidget* user_e = add_field(2, "User:", editing ? existing.user.c_str() : ""); GtkWidget* port_l = gtk_label_new("Port:"); gtk_widget_set_halign(port_l, GTK_ALIGN_START); gtk_grid_attach(GTK_GRID(grid), port_l, 0, 3, 1, 1); GtkWidget* port_s = gtk_spin_button_new_with_range(1, 65535, 1); if (editing) gtk_spin_button_set_value(GTK_SPIN_BUTTON(port_s), existing.port); else gtk_spin_button_set_value(GTK_SPIN_BUTTON(port_s), 22); gtk_grid_attach(GTK_GRID(grid), port_s, 1, 3, 1, 1); /* Password field */ GtkWidget* pass_l = gtk_label_new("Password:"); gtk_widget_set_halign(pass_l, GTK_ALIGN_START); gtk_grid_attach(GTK_GRID(grid), pass_l, 0, 4, 1, 1); GtkWidget* pass_e = gtk_entry_new(); gtk_entry_set_visibility(GTK_ENTRY(pass_e), FALSE); gtk_entry_set_placeholder_text(GTK_ENTRY(pass_e), "optional"); if (editing) gtk_entry_set_text(GTK_ENTRY(pass_e), existing.password.c_str()); gtk_widget_set_hexpand(pass_e, TRUE); gtk_grid_attach(GTK_GRID(grid), pass_e, 1, 4, 1, 1); GtkWidget* save_pass_cb = gtk_check_button_new_with_label("Save password"); if (editing) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(save_pass_cb), existing.save_password); gtk_grid_attach(GTK_GRID(grid), save_pass_cb, 1, 5, 1, 1); /* SSH Key field */ GtkWidget* key_l = gtk_label_new("SSH Key:"); gtk_widget_set_halign(key_l, GTK_ALIGN_START); gtk_grid_attach(GTK_GRID(grid), key_l, 0, 6, 1, 1); GtkWidget* key_combo = gtk_combo_box_text_new(); gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(key_combo), "(None / Password)"); for (const auto& k : key_store_.keys()) { std::string lbl = k.name; if (!k.fingerprint.empty()) lbl += " [" + k.fingerprint.substr(0, 20) + "]"; gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(key_combo), lbl.c_str()); } if (editing && existing.use_key) { for (int i = 0; i < (int)key_store_.keys().size(); ++i) { if (key_store_.keys()[i].privkey_path == existing.keyfile) { gtk_combo_box_set_active(GTK_COMBO_BOX(key_combo), i + 1); break; } } } else { gtk_combo_box_set_active(GTK_COMBO_BOX(key_combo), 0); } gtk_widget_set_hexpand(key_combo, TRUE); gtk_grid_attach(GTK_GRID(grid), key_combo, 1, 6, 1, 1); gtk_container_add(GTK_CONTAINER(content), grid); gtk_widget_show_all(dialog); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { ServerConfig s; s.name = gtk_entry_get_text(GTK_ENTRY(name_e)); s.host = gtk_entry_get_text(GTK_ENTRY(host_e)); s.user = gtk_entry_get_text(GTK_ENTRY(user_e)); s.port = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(port_s)); s.password = gtk_entry_get_text(GTK_ENTRY(pass_e)); s.save_password = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(save_pass_cb)); int key_idx = gtk_combo_box_get_active(GTK_COMBO_BOX(key_combo)); if (key_idx > 0 && key_idx <= (int)key_store_.keys().size()) { s.use_key = true; s.keyfile = key_store_.keys()[key_idx - 1].privkey_path; } else { s.use_key = false; s.keyfile = ""; } if (!s.name.empty() && !s.host.empty() && !s.user.empty()) { if (editing) server_store_.update(edit_index, s); else server_store_.add(s); update_server_list(); show_notification(editing ? "Server updated: " + s.name : "Server saved: " + s.name); } } gtk_widget_destroy(dialog); } /* ─── key management dialog ───────────────────────────────── */ struct KeyDlgCtx { SshClientWindow* self; GtkWidget* listbox; }; void SshClientWindow::show_key_dialog() { GtkWidget* dialog = gtk_dialog_new_with_buttons( "Manage SSH Keys", GTK_WINDOW(window_), (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), "_Close", GTK_RESPONSE_CLOSE, nullptr); GtkWidget* content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); gtk_widget_set_margin_start(vbox, 12); gtk_widget_set_margin_end(vbox, 12); gtk_widget_set_margin_top(vbox, 12); GtkWidget* scrolled = gtk_scrolled_window_new(nullptr, nullptr); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_widget_set_size_request(scrolled, 450, 200); GtkWidget* listbox = gtk_list_box_new(); gtk_container_add(GTK_CONTAINER(scrolled), listbox); gtk_box_pack_start(GTK_BOX(vbox), scrolled, TRUE, TRUE, 0); refresh_key_list_widget(listbox, key_store_.keys()); GtkWidget* hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); GtkWidget* gen_btn = gtk_button_new_with_label("Generate New Key"); GtkWidget* imp_btn = gtk_button_new_with_label("Import Key"); GtkWidget* del_btn = gtk_button_new_with_label("Delete Selected"); gtk_box_pack_start(GTK_BOX(hbox), gen_btn, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), imp_btn, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), del_btn, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); auto* ctx = new KeyDlgCtx{this, listbox}; g_signal_connect(gen_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* c = static_cast(d); GtkWidget* dlg = gtk_dialog_new_with_buttons("Generate Key", GTK_WINDOW(c->self->window_), (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), "_Cancel", GTK_RESPONSE_CANCEL, "_Generate", GTK_RESPONSE_ACCEPT, nullptr); GtkWidget* ca = gtk_dialog_get_content_area(GTK_DIALOG(dlg)); GtkWidget* g = gtk_grid_new(); gtk_grid_set_column_spacing(GTK_GRID(g), 8); gtk_widget_set_margin_start(g, 12); gtk_widget_set_margin_end(g, 12); gtk_widget_set_margin_top(g, 12); GtkWidget* nl = gtk_label_new("Key Name:"); gtk_grid_attach(GTK_GRID(g), nl, 0, 0, 1, 1); GtkWidget* ne = gtk_entry_new(); gtk_entry_set_placeholder_text(GTK_ENTRY(ne), "my-key-name"); gtk_widget_set_hexpand(ne, TRUE); gtk_grid_attach(GTK_GRID(g), ne, 1, 0, 1, 1); GtkWidget* bl = gtk_label_new("Bits:"); gtk_grid_attach(GTK_GRID(g), bl, 0, 1, 1, 1); GtkWidget* bs = gtk_spin_button_new_with_range(2048, 16384, 1024); gtk_spin_button_set_value(GTK_SPIN_BUTTON(bs), 4096); gtk_grid_attach(GTK_GRID(g), bs, 1, 1, 1, 1); gtk_container_add(GTK_CONTAINER(ca), g); gtk_widget_show_all(dlg); if (gtk_dialog_run(GTK_DIALOG(dlg)) == GTK_RESPONSE_ACCEPT) { const char* name = gtk_entry_get_text(GTK_ENTRY(ne)); int bits = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(bs)); if (name && *name) { c->self->key_store_.generate_keypair(name, bits); c->self->update_server_list(); refresh_key_list_widget(c->listbox, c->self->key_store_.keys()); c->self->show_notification("Key generated: " + std::string(name)); } } gtk_widget_destroy(dlg); }), ctx); g_signal_connect(imp_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* c = static_cast(d); GtkWidget* fc = gtk_file_chooser_dialog_new("Import Private Key", GTK_WINDOW(c->self->window_), GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, nullptr); GtkFileFilter* filter = gtk_file_filter_new(); gtk_file_filter_set_name(filter, "SSH Keys"); gtk_file_filter_add_pattern(filter, "id_*"); gtk_file_filter_add_pattern(filter, "*.pem"); gtk_file_filter_add_pattern(filter, "*.key"); gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(fc), filter); if (gtk_dialog_run(GTK_DIALOG(fc)) == GTK_RESPONSE_ACCEPT) { char* path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(fc)); if (path) { GtkWidget* dlg = gtk_dialog_new_with_buttons("Key Name", GTK_WINDOW(c->self->window_), (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), "_Cancel", GTK_RESPONSE_CANCEL, "_OK", GTK_RESPONSE_ACCEPT, nullptr); GtkWidget* ca = gtk_dialog_get_content_area(GTK_DIALOG(dlg)); GtkWidget* g = gtk_grid_new(); gtk_grid_set_column_spacing(GTK_GRID(g), 8); gtk_widget_set_margin_start(g, 12); gtk_widget_set_margin_top(g, 12); GtkWidget* nl = gtk_label_new("Name:"); gtk_grid_attach(GTK_GRID(g), nl, 0, 0, 1, 1); GtkWidget* ne = gtk_entry_new(); gtk_widget_set_hexpand(ne, TRUE); gtk_grid_attach(GTK_GRID(g), ne, 1, 0, 1, 1); gtk_container_add(GTK_CONTAINER(ca), g); gtk_widget_show_all(dlg); if (gtk_dialog_run(GTK_DIALOG(dlg)) == GTK_RESPONSE_ACCEPT) { const char* name = gtk_entry_get_text(GTK_ENTRY(ne)); if (name && *name) { c->self->key_store_.import_key(name, path); refresh_key_list_widget(c->listbox, c->self->key_store_.keys()); c->self->show_notification("Key imported: " + std::string(name)); } } gtk_widget_destroy(dlg); g_free(path); } } gtk_widget_destroy(fc); }), ctx); g_signal_connect(del_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) { auto* c = static_cast(d); GtkListBoxRow* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(c->listbox)); if (!row) return; int idx = gtk_list_box_row_get_index(row); if (idx >= 0 && idx < (int)c->self->key_store_.keys().size()) { std::string name = c->self->key_store_.keys()[idx].name; c->self->key_store_.remove(idx); refresh_key_list_widget(c->listbox, c->self->key_store_.keys()); c->self->show_notification("Key deleted: " + name); } }), ctx); gtk_container_add(GTK_CONTAINER(content), vbox); gtk_widget_show_all(dialog); while (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_NONE) {} delete ctx; gtk_widget_destroy(dialog); } /* ─── callbacks ───────────────────────────────────────────── */ void SshClientWindow::on_new_tab(GtkWidget*, gpointer d) { static_cast(d)->add_new_tab(); } void SshClientWindow::on_close_tab(GtkWidget*, gpointer d) { static_cast(d)->close_current_tab(); } void SshClientWindow::on_connect(GtkWidget*, gpointer d) { auto* self = static_cast(d); self->show_password_dialog("localhost", 22, "root", "", "Local"); } void SshClientWindow::on_disconnect(GtkWidget*, gpointer d) { static_cast(d)->disconnect(); } void SshClientWindow::on_manage_keys(GtkWidget*, gpointer d) { static_cast(d)->show_key_dialog(); } /* ─── main ────────────────────────────────────────────────── */ void SshClientWindow::run() { gtk_widget_show_all(window_); gtk_revealer_set_reveal_child(GTK_REVEALER(notif_bar_), FALSE); gtk_main(); }