Fix: crash on failed connect, add sidebar edit/delete, password saving

- Fix crash when SSH connect fails: remove page BEFORE deleting terminal
- Sidebar right-click context menu: Connect / Edit / Delete
- Password field + Save password checkbox in server dialog
- ServerConfig stores password and save_password fields
- Auto-connect uses saved password when available
- Restore original flaticon icon
This commit is contained in:
x3
2026-09-07 15:34:23 +00:00
parent 16ae7ffde2
commit 6e719c3236
7 changed files with 202 additions and 124 deletions
+112 -99
View File
@@ -35,6 +35,7 @@ static void refresh_server_list_widget(GtkWidget* listbox, const std::vector<Ser
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);
@@ -101,7 +102,7 @@ void SshClientWindow::build_ui() {
GtkWidget* main_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add(GTK_CONTAINER(window_), main_vbox);
/* ── notification bar (top, hidden by default) ── */
/* ── 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);
@@ -121,7 +122,6 @@ void SshClientWindow::build_ui() {
}), notif_bar_);
gtk_box_pack_end(GTK_BOX(notif_box), notif_close, FALSE, FALSE, 0);
gtk_container_add(GTK_CONTAINER(notif_bar_), notif_box);
/* colored background */
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);
@@ -173,7 +173,7 @@ void SshClientWindow::build_ui() {
GtkWidget* paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
gtk_box_pack_start(GTK_BOX(main_vbox), paned, TRUE, TRUE, 0);
/* ── sidebar: server list ── */
/* ── 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);
@@ -200,12 +200,13 @@ void SshClientWindow::build_ui() {
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 for terminals ── */
/* ── notebook ── */
notebook_ = gtk_notebook_new();
gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook_), TRUE);
gtk_paned_pack2(GTK_PANED(paned), notebook_, TRUE, TRUE);
@@ -217,13 +218,11 @@ void SshClientWindow::build_ui() {
void SshClientWindow::show_notification(const std::string& msg, bool is_error) {
gtk_label_set_text(GTK_LABEL(notif_label_), msg.c_str());
GtkStyleContext* ctx = gtk_widget_get_style_context(notif_bar_);
gtk_style_context_remove_class(ctx, "notif-ok");
gtk_style_context_remove_class(ctx, "notif-err");
gtk_style_context_add_class(ctx, is_error ? "notif-err" : "notif-ok");
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);
/* auto-hide after 4 seconds */
g_timeout_add(4000, [](gpointer data) -> gboolean {
gtk_revealer_set_reveal_child(GTK_REVEALER(data), FALSE);
return G_SOURCE_REMOVE;
@@ -266,7 +265,6 @@ void SshClientWindow::add_new_tab() {
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() {
@@ -288,12 +286,13 @@ void SshClientWindow::close_tab_at(int idx) {
}
}
/* Remove page BEFORE deleting terminal to avoid crash */
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
if (found) {
found->disconnect();
delete found;
}
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
}
void SshClientWindow::disconnect() {
@@ -336,8 +335,9 @@ void SshClientWindow::connect_to_server(const std::string& host, int port,
for (auto it = terminals_.begin(); it != terminals_.end(); ++it) {
if (*it == term) { terminals_.erase(it); break; }
}
delete term;
/* 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));
@@ -365,7 +365,12 @@ void SshClientWindow::on_quick_connect(GtkWidget*, gpointer d) {
self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name);
return;
}
self->show_password_dialog(s.host, s.port, s.user, s.keyfile, s.name);
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;
}
@@ -383,10 +388,81 @@ void SshClientWindow::on_server_row_activated(GtkListBox*, GtkListBoxRow* row, g
if (s.use_key && !s.keyfile.empty()) {
self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name);
} else {
self->show_password_dialog(s.host, s.port, s.user, s.keyfile, s.name);
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<SshClientWindow*>(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<ConnectCtx*>(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<EditCtx*>(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<DelCtx*>(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) {
@@ -489,9 +565,25 @@ void SshClientWindow::show_server_dialog(int edit_index) {
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, 4, 1, 1);
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)");
@@ -511,7 +603,7 @@ void SshClientWindow::show_server_dialog(int edit_index) {
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_grid_attach(GTK_GRID(grid), key_combo, 1, 6, 1, 1);
gtk_container_add(GTK_CONTAINER(content), grid);
gtk_widget_show_all(dialog);
@@ -522,6 +614,8 @@ void SshClientWindow::show_server_dialog(int edit_index) {
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;
@@ -690,87 +784,6 @@ void SshClientWindow::show_key_dialog() {
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_widget(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_list();
refresh_server_list_widget(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_list();
refresh_server_list_widget(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()) {
std::string name = c->self->server_store_.servers()[idx].name;
c->self->server_store_.remove(idx);
c->self->update_server_list();
refresh_server_list_widget(c->listbox, c->self->server_store_.servers());
c->self->show_notification("Server removed: " + name);
}
}), 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_list();
}
/* ─── callbacks ───────────────────────────────────────────── */
void SshClientWindow::on_new_tab(GtkWidget*, gpointer d) {