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

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Tabbed terminal** - Multiple SSH sessions in tabs
|
- **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
|
- **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
|
- **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
|
- **VTE terminal** - Full terminal emulation with 256 color support
|
||||||
- **Resizable pty** - Terminal auto-resizes the remote pty on window resize
|
- **Resizable pty** - Terminal auto-resizes the remote pty on window resize
|
||||||
- **System integration** - `.desktop` file and icon for application menu entry
|
- **System integration** - `.desktop` file and icon for application menu entry
|
||||||
@@ -63,11 +64,25 @@ SSH keys are stored in:
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1. Start the application: `x3-ssh`
|
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"
|
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
|
4. **SSH Keys**: Click "Keys" in the toolbar to generate or import SSH keys
|
||||||
5. **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
|
||||||
6. **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
|
## Notifications
|
||||||
|
|
||||||
@@ -90,7 +105,7 @@ Notifications auto-hide after 4 seconds.
|
|||||||
│ [Server▼] [host___] [user___] [port] [Connect] ...│
|
│ [Server▼] [host___] [user___] [port] [Connect] ...│
|
||||||
├───────────┬────────────────────────────────────────┤
|
├───────────┬────────────────────────────────────────┤
|
||||||
│ Servers │ [Tab: MyServer] [Tab: Prod] [+] │
|
│ Servers │ [Tab: MyServer] [Tab: Prod] [+] │
|
||||||
│ ─────── │ │
|
│ +─────── │ │
|
||||||
│ MyServer │ user@host:~$ │
|
│ MyServer │ user@host:~$ │
|
||||||
│ root@h:22 │ │
|
│ 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
|
## License
|
||||||
|
|
||||||
MIT
|
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 == "port") { try { current.port = std::stoi(val); } catch (...) { current.port = 22; } }
|
||||||
else if (key == "user") current.user = val;
|
else if (key == "user") current.user = val;
|
||||||
else if (key == "keyfile") current.keyfile = 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 == "use_key") current.use_key = (val == "true");
|
||||||
|
else if (key == "save_password") current.save_password = (val == "true");
|
||||||
}
|
}
|
||||||
if (in_server && !current.name.empty()) {
|
if (in_server && !current.name.empty()) {
|
||||||
servers_.push_back(current);
|
servers_.push_back(current);
|
||||||
@@ -65,7 +67,7 @@ void ServerStore::load() {
|
|||||||
void ServerStore::save() const {
|
void ServerStore::save() const {
|
||||||
std::string dir = config_path_.substr(0, config_path_.rfind('/'));
|
std::string dir = config_path_.substr(0, config_path_.rfind('/'));
|
||||||
std::string cmd = "mkdir -p \"" + dir + "\"";
|
std::string cmd = "mkdir -p \"" + dir + "\"";
|
||||||
system(cmd.c_str());
|
(void)system(cmd.c_str());
|
||||||
|
|
||||||
std::ofstream f(config_path_);
|
std::ofstream f(config_path_);
|
||||||
if (!f.is_open()) return;
|
if (!f.is_open()) return;
|
||||||
@@ -79,7 +81,9 @@ void ServerStore::save() const {
|
|||||||
f << "port=" << s.port << "\n";
|
f << "port=" << s.port << "\n";
|
||||||
f << "user=" << s.user << "\n";
|
f << "user=" << s.user << "\n";
|
||||||
f << "keyfile=" << s.keyfile << "\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;
|
int port = 22;
|
||||||
std::string user;
|
std::string user;
|
||||||
std::string keyfile;
|
std::string keyfile;
|
||||||
|
std::string password;
|
||||||
bool use_key = false;
|
bool use_key = false;
|
||||||
|
bool save_password = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ServerStore {
|
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);
|
std::string detail = s.user + "@" + s.host + ":" + std::to_string(s.port);
|
||||||
if (s.use_key) detail += " [key]";
|
if (s.use_key) detail += " [key]";
|
||||||
|
else if (s.save_password) detail += " [saved]";
|
||||||
GtkWidget* detail_label = gtk_label_new(detail.c_str());
|
GtkWidget* detail_label = gtk_label_new(detail.c_str());
|
||||||
gtk_widget_set_halign(detail_label, GTK_ALIGN_START);
|
gtk_widget_set_halign(detail_label, GTK_ALIGN_START);
|
||||||
gtk_label_set_ellipsize(GTK_LABEL(detail_label), PANGO_ELLIPSIZE_END);
|
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);
|
GtkWidget* main_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
gtk_container_add(GTK_CONTAINER(window_), main_vbox);
|
gtk_container_add(GTK_CONTAINER(window_), main_vbox);
|
||||||
|
|
||||||
/* ── notification bar (top, hidden by default) ── */
|
/* ── notification bar ── */
|
||||||
notif_bar_ = gtk_revealer_new();
|
notif_bar_ = gtk_revealer_new();
|
||||||
gtk_revealer_set_transition_type(GTK_REVEALER(notif_bar_), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
|
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);
|
gtk_revealer_set_reveal_child(GTK_REVEALER(notif_bar_), FALSE);
|
||||||
@@ -121,7 +122,6 @@ void SshClientWindow::build_ui() {
|
|||||||
}), notif_bar_);
|
}), notif_bar_);
|
||||||
gtk_box_pack_end(GTK_BOX(notif_box), notif_close, FALSE, FALSE, 0);
|
gtk_box_pack_end(GTK_BOX(notif_box), notif_close, FALSE, FALSE, 0);
|
||||||
gtk_container_add(GTK_CONTAINER(notif_bar_), notif_box);
|
gtk_container_add(GTK_CONTAINER(notif_bar_), notif_box);
|
||||||
/* colored background */
|
|
||||||
GtkCssProvider* css = gtk_css_provider_new();
|
GtkCssProvider* css = gtk_css_provider_new();
|
||||||
gtk_css_provider_load_from_data(css, ".notif-ok { background: #2ecc71; color: #000; }"
|
gtk_css_provider_load_from_data(css, ".notif-ok { background: #2ecc71; color: #000; }"
|
||||||
".notif-err { background: #e74c3c; color: #fff; }", -1, nullptr);
|
".notif-err { background: #e74c3c; color: #fff; }", -1, nullptr);
|
||||||
@@ -173,7 +173,7 @@ void SshClientWindow::build_ui() {
|
|||||||
GtkWidget* paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
GtkWidget* paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||||
gtk_box_pack_start(GTK_BOX(main_vbox), paned, TRUE, TRUE, 0);
|
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);
|
GtkWidget* sidebar = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
gtk_paned_pack1(GTK_PANED(paned), sidebar, FALSE, FALSE);
|
gtk_paned_pack1(GTK_PANED(paned), sidebar, FALSE, FALSE);
|
||||||
gtk_widget_set_size_request(sidebar, 220, -1);
|
gtk_widget_set_size_request(sidebar, 220, -1);
|
||||||
@@ -200,12 +200,13 @@ void SshClientWindow::build_ui() {
|
|||||||
server_listbox_ = gtk_list_box_new();
|
server_listbox_ = gtk_list_box_new();
|
||||||
gtk_list_box_set_selection_mode(GTK_LIST_BOX(server_listbox_), GTK_SELECTION_SINGLE);
|
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_, "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_container_add(GTK_CONTAINER(scrolled), server_listbox_);
|
||||||
gtk_box_pack_start(GTK_BOX(sidebar), scrolled, TRUE, TRUE, 0);
|
gtk_box_pack_start(GTK_BOX(sidebar), scrolled, TRUE, TRUE, 0);
|
||||||
|
|
||||||
update_server_list();
|
update_server_list();
|
||||||
|
|
||||||
/* ── notebook for terminals ── */
|
/* ── notebook ── */
|
||||||
notebook_ = gtk_notebook_new();
|
notebook_ = gtk_notebook_new();
|
||||||
gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook_), TRUE);
|
gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook_), TRUE);
|
||||||
gtk_paned_pack2(GTK_PANED(paned), notebook_, TRUE, 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) {
|
void SshClientWindow::show_notification(const std::string& msg, bool is_error) {
|
||||||
gtk_label_set_text(GTK_LABEL(notif_label_), msg.c_str());
|
gtk_label_set_text(GTK_LABEL(notif_label_), msg.c_str());
|
||||||
GtkStyleContext* ctx = gtk_widget_get_style_context(notif_bar_);
|
GtkStyleContext* nctx = gtk_widget_get_style_context(notif_bar_);
|
||||||
gtk_style_context_remove_class(ctx, "notif-ok");
|
gtk_style_context_remove_class(nctx, "notif-ok");
|
||||||
gtk_style_context_remove_class(ctx, "notif-err");
|
gtk_style_context_remove_class(nctx, "notif-err");
|
||||||
gtk_style_context_add_class(ctx, is_error ? "notif-err" : "notif-ok");
|
gtk_style_context_add_class(nctx, is_error ? "notif-err" : "notif-ok");
|
||||||
gtk_revealer_set_reveal_child(GTK_REVEALER(notif_bar_), TRUE);
|
gtk_revealer_set_reveal_child(GTK_REVEALER(notif_bar_), TRUE);
|
||||||
|
|
||||||
/* auto-hide after 4 seconds */
|
|
||||||
g_timeout_add(4000, [](gpointer data) -> gboolean {
|
g_timeout_add(4000, [](gpointer data) -> gboolean {
|
||||||
gtk_revealer_set_reveal_child(GTK_REVEALER(data), FALSE);
|
gtk_revealer_set_reveal_child(GTK_REVEALER(data), FALSE);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
@@ -266,7 +265,6 @@ void SshClientWindow::add_new_tab() {
|
|||||||
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(notebook_), page, TRUE);
|
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(notebook_), page, TRUE);
|
||||||
gtk_widget_show_all(page);
|
gtk_widget_show_all(page);
|
||||||
gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook_), idx);
|
gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook_), idx);
|
||||||
update_status("New tab opened");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SshClientWindow::close_current_tab() {
|
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) {
|
if (found) {
|
||||||
found->disconnect();
|
found->disconnect();
|
||||||
delete found;
|
delete found;
|
||||||
}
|
}
|
||||||
|
|
||||||
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SshClientWindow::disconnect() {
|
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) {
|
for (auto it = terminals_.begin(); it != terminals_.end(); ++it) {
|
||||||
if (*it == term) { terminals_.erase(it); break; }
|
if (*it == term) { terminals_.erase(it); break; }
|
||||||
}
|
}
|
||||||
delete term;
|
/* Remove page FIRST, then delete terminal */
|
||||||
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
|
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
|
||||||
|
delete term;
|
||||||
} else {
|
} else {
|
||||||
update_status("Connected to " + user + "@" + host);
|
update_status("Connected to " + user + "@" + host);
|
||||||
show_notification("Server connected: " + user + "@" + host + ":" + std::to_string(port));
|
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);
|
self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name);
|
||||||
return;
|
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);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,8 +388,79 @@ void SshClientWindow::on_server_row_activated(GtkListBox*, GtkListBoxRow* row, g
|
|||||||
if (s.use_key && !s.keyfile.empty()) {
|
if (s.use_key && !s.keyfile.empty()) {
|
||||||
self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name);
|
self->connect_to_server(s.host, s.port, s.user, s.keyfile, "", s.name);
|
||||||
} else {
|
} 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);
|
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 ───────────────────────────────────── */
|
/* ─── 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);
|
else gtk_spin_button_set_value(GTK_SPIN_BUTTON(port_s), 22);
|
||||||
gtk_grid_attach(GTK_GRID(grid), port_s, 1, 3, 1, 1);
|
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:");
|
GtkWidget* key_l = gtk_label_new("SSH Key:");
|
||||||
gtk_widget_set_halign(key_l, GTK_ALIGN_START);
|
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();
|
GtkWidget* key_combo = gtk_combo_box_text_new();
|
||||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(key_combo), "(None / Password)");
|
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_combo_box_set_active(GTK_COMBO_BOX(key_combo), 0);
|
||||||
}
|
}
|
||||||
gtk_widget_set_hexpand(key_combo, TRUE);
|
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_container_add(GTK_CONTAINER(content), grid);
|
||||||
gtk_widget_show_all(dialog);
|
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.host = gtk_entry_get_text(GTK_ENTRY(host_e));
|
||||||
s.user = gtk_entry_get_text(GTK_ENTRY(user_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.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));
|
int key_idx = gtk_combo_box_get_active(GTK_COMBO_BOX(key_combo));
|
||||||
if (key_idx > 0 && key_idx <= (int)key_store_.keys().size()) {
|
if (key_idx > 0 && key_idx <= (int)key_store_.keys().size()) {
|
||||||
s.use_key = true;
|
s.use_key = true;
|
||||||
@@ -690,87 +784,6 @@ void SshClientWindow::show_key_dialog() {
|
|||||||
gtk_widget_destroy(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 ───────────────────────────────────────────── */
|
/* ─── callbacks ───────────────────────────────────────────── */
|
||||||
|
|
||||||
void SshClientWindow::on_new_tab(GtkWidget*, gpointer d) {
|
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_close_tab(GtkWidget* w, gpointer d);
|
||||||
static void on_connect(GtkWidget* w, gpointer d);
|
static void on_connect(GtkWidget* w, gpointer d);
|
||||||
static void on_disconnect(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_manage_keys(GtkWidget* w, gpointer d);
|
||||||
static void on_quick_connect(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_add_server(GtkWidget* w, gpointer d);
|
||||||
static void on_server_row_activated(GtkListBox* list, GtkListBoxRow* row, 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* window_;
|
||||||
GtkWidget* notebook_;
|
GtkWidget* notebook_;
|
||||||
|
|||||||
+49
-14
@@ -22,7 +22,10 @@ SshTerminal::SshTerminal()
|
|||||||
|
|
||||||
SshTerminal::~SshTerminal() {
|
SshTerminal::~SshTerminal() {
|
||||||
disconnect();
|
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) {
|
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) {
|
const std::string& keyfile, const std::string& password) {
|
||||||
if (connected_) disconnect();
|
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();
|
session_ = ssh_new();
|
||||||
if (!session_) return false;
|
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) {
|
if (ssh_connect(session_) != SSH_OK) {
|
||||||
std::string err = ssh_get_error(session_);
|
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);
|
if (on_status_change) on_status_change(false, "Connection failed: " + err);
|
||||||
return false;
|
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());
|
int rc = ssh_userauth_password(session_, nullptr, password.c_str());
|
||||||
if (rc != SSH_AUTH_SUCCESS) {
|
if (rc != SSH_AUTH_SUCCESS) {
|
||||||
std::string err = ssh_get_error(session_);
|
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);
|
if (on_status_change) on_status_change(false, "Auth failed: " + err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -173,13 +188,19 @@ bool SshTerminal::connect(const std::string& host, int port, const std::string&
|
|||||||
auth_done:
|
auth_done:
|
||||||
channel_ = ssh_channel_new(session_);
|
channel_ = ssh_channel_new(session_);
|
||||||
if (!channel_) {
|
if (!channel_) {
|
||||||
cleanup();
|
ssh_disconnect(session_);
|
||||||
|
ssh_free(session_);
|
||||||
|
session_ = nullptr;
|
||||||
if (on_status_change) on_status_change(false, "Channel creation failed");
|
if (on_status_change) on_status_change(false, "Channel creation failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ssh_channel_open_session(channel_) != SSH_OK) {
|
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");
|
if (on_status_change) on_status_change(false, "Session open failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -187,32 +208,47 @@ auth_done:
|
|||||||
guint cols = vte_terminal_get_column_count(terminal_);
|
guint cols = vte_terminal_get_column_count(terminal_);
|
||||||
guint rows = vte_terminal_get_row_count(terminal_);
|
guint rows = vte_terminal_get_row_count(terminal_);
|
||||||
if (ssh_channel_request_pty_size(channel_, "xterm-256color", cols, rows) != SSH_OK) {
|
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");
|
if (on_status_change) on_status_change(false, "PTY request failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ssh_channel_request_shell(channel_) != SSH_OK) {
|
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");
|
if (on_status_change) on_status_change(false, "Shell request failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pty_master_ = openpty(&pty_slave_, nullptr, nullptr, nullptr, nullptr);
|
pty_master_ = openpty(&pty_slave_, nullptr, nullptr, nullptr, nullptr);
|
||||||
if (pty_master_ < 0) {
|
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");
|
if (on_status_change) on_status_change(false, "PTY creation failed");
|
||||||
return false;
|
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);
|
vte_pty_ = vte_pty_new_foreign_sync(pty_master_, nullptr, nullptr);
|
||||||
if (vte_pty_) {
|
if (vte_pty_) {
|
||||||
vte_terminal_set_pty(terminal_, vte_pty_);
|
vte_terminal_set_pty(terminal_, vte_pty_);
|
||||||
} else {
|
} 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");
|
if (on_status_change) on_status_change(false, "VTE pty setup failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -234,11 +270,10 @@ void SshTerminal::disconnect() {
|
|||||||
thread_ = nullptr;
|
thread_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clean up VTE pty first (releases master fd ownership) */
|
|
||||||
if (vte_pty_) {
|
if (vte_pty_) {
|
||||||
g_object_unref(vte_pty_);
|
g_object_unref(vte_pty_);
|
||||||
vte_pty_ = nullptr;
|
vte_pty_ = nullptr;
|
||||||
vte_terminal_set_pty(terminal_, nullptr);
|
if (terminal_) vte_terminal_set_pty(terminal_, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|||||||
Reference in New Issue
Block a user