feat: initial x3-ssh client

- GTK3 GUI with VTE terminal emulation
- Tabbed SSH sessions
- Server management (add/edit/remove)
- SSH key management (generate/import/delete)
- Quick connect bar
- libssh for SSH connections
- autotools build system (configure && make)
This commit is contained in:
x3
2026-09-07 14:50:20 +00:00
commit 77f9568cb1
15 changed files with 1476 additions and 0 deletions
+681
View File
@@ -0,0 +1,681 @@
#include "ssh_client.h"
#include <cstdlib>
#include <cstring>
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(GtkWidget* listbox, const std::vector<ServerConfig>& servers) {
clear_container(listbox);
for (const auto& s : servers) {
std::string text = s.name + " | " + s.user + "@" + s.host + ":" + std::to_string(s.port);
if (s.use_key) text += " [key]";
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);
}
}
static void refresh_key_list(GtkWidget* listbox, const std::vector<SshKey>& 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) {
build_ui();
}
SshClientWindow::~SshClientWindow() {
for (auto* t : terminals_) 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/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* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add(GTK_CONTAINER(window_), vbox);
build_toolbar();
GtkWidget* sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
gtk_box_pack_start(GTK_BOX(vbox), sep, FALSE, FALSE, 0);
build_notebook();
build_statusbar();
gtk_box_pack_start(GTK_BOX(vbox), notebook_, TRUE, TRUE, 0);
gtk_box_pack_end(GTK_BOX(vbox), statusbar_, FALSE, FALSE, 0);
}
void SshClientWindow::build_toolbar() {
GtkWidget* hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
gtk_widget_set_margin_start(hbox, 6);
gtk_widget_set_margin_end(hbox, 6);
gtk_widget_set_margin_top(hbox, 4);
gtk_widget_set_margin_bottom(hbox, 4);
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(hbox), 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(hbox), 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(hbox), 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(hbox), port_spin_, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hbox), make_button("Connect", G_CALLBACK(on_quick_connect), this), FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hbox), make_button("+ Tab", G_CALLBACK(on_new_tab), this), FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hbox), make_button("Close Tab", G_CALLBACK(on_close_tab), this), FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hbox), make_button("Disconnect", G_CALLBACK(on_disconnect), this), FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hbox), make_button("Servers", G_CALLBACK(on_manage_servers), this), FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hbox), make_button("Keys", G_CALLBACK(on_manage_keys), this), FALSE, FALSE, 0);
update_server_combo();
}
void SshClientWindow::build_notebook() {
notebook_ = gtk_notebook_new();
gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook_), TRUE);
g_signal_connect(notebook_, "page-removed", G_CALLBACK(on_tab_close), this);
}
void SshClientWindow::build_statusbar() {
statusbar_ = gtk_statusbar_new();
}
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_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);
update_status("New tab opened");
}
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;
for (auto it = terminals_.begin(); it != terminals_.end(); ++it) {
if ((*it)->widget() == page) {
(*it)->disconnect();
delete *it;
terminals_.erase(it);
break;
}
}
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
}
void SshClientWindow::on_tab_close(GtkNotebook* nb, GtkWidget* page, guint idx, gpointer d) {
(void)nb; (void)idx;
auto* self = static_cast<SshClientWindow*>(d);
for (auto it = self->terminals_.begin(); it != self->terminals_.end(); ++it) {
if ((*it)->widget() == page) {
(*it)->disconnect();
delete *it;
self->terminals_.erase(it);
return;
}
}
}
/* ─── connect / disconnect ─────────────────────────────────── */
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);
for (auto it = terminals_.begin(); it != terminals_.end(); ++it) {
if (*it == term) { terminals_.erase(it); break; }
}
delete term;
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
} else {
update_status("Connected to " + user + "@" + host);
}
}
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");
return;
}
}
}
/* ─── quick connect ───────────────────────────────────────── */
void SshClientWindow::on_quick_connect(GtkWidget*, gpointer d) {
auto* self = static_cast<SshClientWindow*>(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;
}
self->show_password_dialog(s.host, s.user, s.keyfile, s.name);
return;
}
self->show_password_dialog(host, u, "", host);
}
/* ─── password dialog ─────────────────────────────────────── */
void SshClientWindow::show_password_dialog(const std::string& host, 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), "<b>Connect to %s@%s</b>", user.c_str(), host.c_str());
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, 22, 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);
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, 4, 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, 4, 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));
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_combo();
}
}
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(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<KeyDlgCtx*>(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_combo();
refresh_key_list(c->listbox, c->self->key_store_.keys());
}
}
gtk_widget_destroy(dlg);
}), ctx);
g_signal_connect(imp_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* c = static_cast<KeyDlgCtx*>(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(c->listbox, c->self->key_store_.keys());
}
}
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<KeyDlgCtx*>(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()) {
c->self->key_store_.remove(idx);
refresh_key_list(c->listbox, c->self->key_store_.keys());
}
}), 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);
}
/* ─── server management dialog ────────────────────────────── */
struct SrvCtx { SshClientWindow* self; GtkWidget* listbox; };
void SshClientWindow::on_manage_servers(GtkWidget*, gpointer d) {
auto* self = static_cast<SshClientWindow*>(d);
GtkWidget* dialog = gtk_dialog_new_with_buttons(
"Manage Servers", GTK_WINDOW(self->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, 500, 250);
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_server_list(listbox, self->server_store_.servers());
GtkWidget* hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget* add_btn = gtk_button_new_with_label("Add");
GtkWidget* edit_btn = gtk_button_new_with_label("Edit");
GtkWidget* del_btn = gtk_button_new_with_label("Remove");
gtk_box_pack_start(GTK_BOX(hbox), add_btn, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(hbox), edit_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 SrvCtx{self, listbox};
g_signal_connect(add_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* c = static_cast<SrvCtx*>(d);
c->self->show_server_dialog(-1);
c->self->update_server_combo();
refresh_server_list(c->listbox, c->self->server_store_.servers());
}), ctx);
g_signal_connect(edit_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* c = static_cast<SrvCtx*>(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->server_store_.servers().size()) {
c->self->show_server_dialog(idx);
c->self->update_server_combo();
refresh_server_list(c->listbox, c->self->server_store_.servers());
}
}), ctx);
g_signal_connect(del_btn, "clicked", G_CALLBACK(+[](GtkWidget*, gpointer d) {
auto* c = static_cast<SrvCtx*>(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->server_store_.servers().size()) {
c->self->server_store_.remove(idx);
c->self->update_server_combo();
refresh_server_list(c->listbox, c->self->server_store_.servers());
}
}), ctx);
gtk_container_add(GTK_CONTAINER(content), vbox);
gtk_widget_show_all(dialog);
gtk_dialog_run(GTK_DIALOG(dialog));
delete ctx;
gtk_widget_destroy(dialog);
self->update_server_combo();
}
/* ─── callbacks ───────────────────────────────────────────── */
void SshClientWindow::on_new_tab(GtkWidget*, gpointer d) {
static_cast<SshClientWindow*>(d)->add_new_tab();
}
void SshClientWindow::on_close_tab(GtkWidget*, gpointer d) {
static_cast<SshClientWindow*>(d)->close_current_tab();
}
void SshClientWindow::on_connect(GtkWidget*, gpointer d) {
auto* self = static_cast<SshClientWindow*>(d);
self->show_password_dialog("localhost", "root", "", "Local");
}
void SshClientWindow::on_disconnect(GtkWidget*, gpointer d) {
static_cast<SshClientWindow*>(d)->disconnect();
}
void SshClientWindow::on_manage_keys(GtkWidget*, gpointer d) {
static_cast<SshClientWindow*>(d)->show_key_dialog();
}
/* ─── main ────────────────────────────────────────────────── */
void SshClientWindow::run() {
gtk_widget_show_all(window_);
gtk_main();
}