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:
@@ -2,15 +2,16 @@
|
||||
|
||||
A GTK3-based SSH client with a tabbed terminal interface, server sidebar, notifications, and SSH key storage.
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Tabbed terminal** - Multiple SSH sessions in tabs
|
||||
- **Server sidebar** - Saved servers displayed in a sidebar, double-click to connect
|
||||
- **Server sidebar** - Saved servers in a sidebar, double-click to connect, right-click for Edit/Delete
|
||||
- **Quick connect** - Connect directly via host/user/port fields in the toolbar
|
||||
- **Password storage** - Optionally save passwords with servers (stored in config file)
|
||||
- **SSH key management** - Generate, import, and manage SSH keys
|
||||
- **Notifications** - Animated notification bar for status messages (saved, connected, disconnected, errors)
|
||||
- **Notifications** - Animated notification bar for status messages
|
||||
- **VTE terminal** - Full terminal emulation with 256 color support
|
||||
- **Resizable pty** - Terminal auto-resizes the remote pty on window resize
|
||||
- **System integration** - `.desktop` file and icon for application menu entry
|
||||
@@ -63,11 +64,25 @@ SSH keys are stored in:
|
||||
## Usage
|
||||
|
||||
1. Start the application: `x3-ssh`
|
||||
2. **Server Sidebar**: Saved servers appear in the left sidebar. Double-click to connect.
|
||||
2. **Server Sidebar**: Saved servers appear in the left sidebar
|
||||
- **Double-click** to connect
|
||||
- **Right-click** for context menu (Connect / Edit / Delete)
|
||||
- Click `+` to add a new server
|
||||
3. **Quick Connect**: Enter host, user, and port in the toolbar, click "Connect"
|
||||
4. **Add Server**: Click the `+` button in the sidebar header, or use the "Add Server" dialog
|
||||
5. **SSH Keys**: Click "Keys" in the toolbar to generate or import SSH keys
|
||||
6. **Tabs**: Click "+ Tab" for a new empty tab, "Close Tab" to close the current one
|
||||
4. **SSH Keys**: Click "Keys" in the toolbar to generate or import SSH keys
|
||||
5. **Tabs**: Click "+ Tab" for a new empty tab, "Close Tab" to close the current one
|
||||
|
||||
## Server Dialog Fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| Name | Display name for the server |
|
||||
| Host | Hostname or IP address |
|
||||
| User | SSH username |
|
||||
| Port | SSH port (default: 22) |
|
||||
| Password | Optional, can be saved |
|
||||
| Save password | Checkbox to store password in config |
|
||||
| SSH Key | Optional key for public key authentication |
|
||||
|
||||
## Notifications
|
||||
|
||||
@@ -90,7 +105,7 @@ Notifications auto-hide after 4 seconds.
|
||||
│ [Server▼] [host___] [user___] [port] [Connect] ...│
|
||||
├───────────┬────────────────────────────────────────┤
|
||||
│ Servers │ [Tab: MyServer] [Tab: Prod] [+] │
|
||||
│ ─────── │ │
|
||||
│ +─────── │ │
|
||||
│ MyServer │ user@host:~$ │
|
||||
│ root@h:22 │ │
|
||||
│ │ │
|
||||
@@ -101,6 +116,15 @@ Notifications auto-hide after 4 seconds.
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Right-click on a server in the sidebar:
|
||||
```
|
||||
┌──────────┐
|
||||
│ Connect │
|
||||
│ Edit │
|
||||
│ Delete │
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 21 KiB |
@@ -55,7 +55,9 @@ void ServerStore::load() {
|
||||
else if (key == "port") { try { current.port = std::stoi(val); } catch (...) { current.port = 22; } }
|
||||
else if (key == "user") current.user = val;
|
||||
else if (key == "keyfile") current.keyfile = val;
|
||||
else if (key == "password") current.password = val;
|
||||
else if (key == "use_key") current.use_key = (val == "true");
|
||||
else if (key == "save_password") current.save_password = (val == "true");
|
||||
}
|
||||
if (in_server && !current.name.empty()) {
|
||||
servers_.push_back(current);
|
||||
@@ -65,7 +67,7 @@ void ServerStore::load() {
|
||||
void ServerStore::save() const {
|
||||
std::string dir = config_path_.substr(0, config_path_.rfind('/'));
|
||||
std::string cmd = "mkdir -p \"" + dir + "\"";
|
||||
system(cmd.c_str());
|
||||
(void)system(cmd.c_str());
|
||||
|
||||
std::ofstream f(config_path_);
|
||||
if (!f.is_open()) return;
|
||||
@@ -79,7 +81,9 @@ void ServerStore::save() const {
|
||||
f << "port=" << s.port << "\n";
|
||||
f << "user=" << s.user << "\n";
|
||||
f << "keyfile=" << s.keyfile << "\n";
|
||||
f << "use_key=" << (s.use_key ? "true" : "false") << "\n\n";
|
||||
f << "password=" << s.password << "\n";
|
||||
f << "use_key=" << (s.use_key ? "true" : "false") << "\n";
|
||||
f << "save_password=" << (s.save_password ? "true" : "false") << "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ struct ServerConfig {
|
||||
int port = 22;
|
||||
std::string user;
|
||||
std::string keyfile;
|
||||
std::string password;
|
||||
bool use_key = false;
|
||||
bool save_password = false;
|
||||
};
|
||||
|
||||
class ServerStore {
|
||||
|
||||
+110
-97
@@ -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;
|
||||
}
|
||||
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,9 +388,80 @@ 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 {
|
||||
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 ───────────────────────────────────── */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+1
-1
@@ -37,11 +37,11 @@ private:
|
||||
static void on_close_tab(GtkWidget* w, gpointer d);
|
||||
static void on_connect(GtkWidget* w, gpointer d);
|
||||
static void on_disconnect(GtkWidget* w, gpointer d);
|
||||
static void on_manage_servers(GtkWidget* w, gpointer d);
|
||||
static void on_manage_keys(GtkWidget* w, gpointer d);
|
||||
static void on_quick_connect(GtkWidget* w, gpointer d);
|
||||
static void on_add_server(GtkWidget* w, gpointer d);
|
||||
static void on_server_row_activated(GtkListBox* list, GtkListBoxRow* row, gpointer d);
|
||||
static gboolean on_server_row_button(GtkWidget* widget, GdkEventButton* event, gpointer d);
|
||||
|
||||
GtkWidget* window_;
|
||||
GtkWidget* notebook_;
|
||||
|
||||
+49
-14
@@ -22,7 +22,10 @@ SshTerminal::SshTerminal()
|
||||
|
||||
SshTerminal::~SshTerminal() {
|
||||
disconnect();
|
||||
if (vte_box_) gtk_widget_destroy(vte_box_);
|
||||
if (vte_box_) {
|
||||
gtk_widget_destroy(vte_box_);
|
||||
vte_box_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void SshTerminal::on_size_changed(VteTerminal* term, guint cols, guint rows, gpointer data) {
|
||||
@@ -130,6 +133,14 @@ bool SshTerminal::connect(const std::string& host, int port, const std::string&
|
||||
const std::string& keyfile, const std::string& password) {
|
||||
if (connected_) disconnect();
|
||||
|
||||
/* Reset state for a fresh connect attempt */
|
||||
cleanup_done_ = true;
|
||||
session_ = nullptr;
|
||||
channel_ = nullptr;
|
||||
pty_master_ = -1;
|
||||
pty_slave_ = -1;
|
||||
vte_pty_ = nullptr;
|
||||
|
||||
session_ = ssh_new();
|
||||
if (!session_) return false;
|
||||
|
||||
@@ -142,7 +153,9 @@ bool SshTerminal::connect(const std::string& host, int port, const std::string&
|
||||
|
||||
if (ssh_connect(session_) != SSH_OK) {
|
||||
std::string err = ssh_get_error(session_);
|
||||
cleanup();
|
||||
/* Manually free since cleanup_done_ is still true */
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "Connection failed: " + err);
|
||||
return false;
|
||||
}
|
||||
@@ -164,7 +177,9 @@ bool SshTerminal::connect(const std::string& host, int port, const std::string&
|
||||
int rc = ssh_userauth_password(session_, nullptr, password.c_str());
|
||||
if (rc != SSH_AUTH_SUCCESS) {
|
||||
std::string err = ssh_get_error(session_);
|
||||
cleanup();
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "Auth failed: " + err);
|
||||
return false;
|
||||
}
|
||||
@@ -173,13 +188,19 @@ bool SshTerminal::connect(const std::string& host, int port, const std::string&
|
||||
auth_done:
|
||||
channel_ = ssh_channel_new(session_);
|
||||
if (!channel_) {
|
||||
cleanup();
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "Channel creation failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ssh_channel_open_session(channel_) != SSH_OK) {
|
||||
cleanup();
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "Session open failed");
|
||||
return false;
|
||||
}
|
||||
@@ -187,32 +208,47 @@ auth_done:
|
||||
guint cols = vte_terminal_get_column_count(terminal_);
|
||||
guint rows = vte_terminal_get_row_count(terminal_);
|
||||
if (ssh_channel_request_pty_size(channel_, "xterm-256color", cols, rows) != SSH_OK) {
|
||||
cleanup();
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "PTY request failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ssh_channel_request_shell(channel_) != SSH_OK) {
|
||||
cleanup();
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "Shell request failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
pty_master_ = openpty(&pty_slave_, nullptr, nullptr, nullptr, nullptr);
|
||||
if (pty_master_ < 0) {
|
||||
cleanup();
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "PTY creation failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* VTE gets the master side of the pty. It reads terminal output from
|
||||
master and writes user input to master. Our IO thread uses the slave
|
||||
side to bridge between the pty and the SSH channel. */
|
||||
vte_pty_ = vte_pty_new_foreign_sync(pty_master_, nullptr, nullptr);
|
||||
if (vte_pty_) {
|
||||
vte_terminal_set_pty(terminal_, vte_pty_);
|
||||
} else {
|
||||
cleanup();
|
||||
close(pty_slave_); pty_slave_ = -1;
|
||||
close(pty_master_); pty_master_ = -1;
|
||||
ssh_channel_free(channel_);
|
||||
channel_ = nullptr;
|
||||
ssh_disconnect(session_);
|
||||
ssh_free(session_);
|
||||
session_ = nullptr;
|
||||
if (on_status_change) on_status_change(false, "VTE pty setup failed");
|
||||
return false;
|
||||
}
|
||||
@@ -234,11 +270,10 @@ void SshTerminal::disconnect() {
|
||||
thread_ = nullptr;
|
||||
}
|
||||
|
||||
/* Clean up VTE pty first (releases master fd ownership) */
|
||||
if (vte_pty_) {
|
||||
g_object_unref(vte_pty_);
|
||||
vte_pty_ = nullptr;
|
||||
vte_terminal_set_pty(terminal_, nullptr);
|
||||
if (terminal_) vte_terminal_set_pty(terminal_, nullptr);
|
||||
}
|
||||
|
||||
cleanup();
|
||||
|
||||
Reference in New Issue
Block a user