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:
+112
-99
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user