fix: critical bugs + .desktop file + system install

Bug fixes:
- Fixed PTY bridge: IO thread now uses pty_slave_ instead of pty_master_
  (remote output was never reaching the terminal)
- Fixed double-delete crash on tab close by removing page-removed callback
- Fixed port being ignored in password dialog (always used port 22)
- Added EINTR handling in select() for IO thread
- Added mutex for thread-safe cleanup
- Added try/catch for std::stoi in server config parsing
- VtePty lifecycle now managed properly (no double-close)
- Redundant ssh_channel_poll_timeout call removed

New:
- Added .desktop file for system-wide application menu entry
- Makefile.am installs desktop file to $(datadir)/applications
- Makefile.am installs icon to $(datadir)/icons/hicolor/256x256/apps
This commit is contained in:
x3
2026-09-07 15:00:08 +00:00
parent 77f9568cb1
commit 4a2b2eac33
7 changed files with 109 additions and 72 deletions
+32 -34
View File
@@ -52,7 +52,10 @@ SshClientWindow::SshClientWindow()
}
SshClientWindow::~SshClientWindow() {
for (auto* t : terminals_) delete t;
for (auto* t : terminals_) {
t->disconnect();
delete t;
}
terminals_.clear();
if (window_) gtk_widget_destroy(window_);
}
@@ -67,7 +70,7 @@ void SshClientWindow::build_ui() {
const char* icon_paths[] = {
"icons/x3-ssh.png",
"/usr/share/icons/x3-ssh.png",
"/usr/share/icons/hicolor/256x256/apps/x3-ssh.png",
"/usr/local/share/icons/x3-ssh.png",
nullptr
};
@@ -133,7 +136,6 @@ void SshClientWindow::build_toolbar() {
void SshClientWindow::build_notebook() {
notebook_ = gtk_notebook_new();
gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook_), TRUE);
g_signal_connect(notebook_, "page-removed", G_CALLBACK(on_tab_close), this);
}
void SshClientWindow::build_statusbar() {
@@ -183,31 +185,40 @@ void SshClientWindow::close_current_tab() {
void SshClientWindow::close_tab_at(int idx) {
GtkWidget* page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook_), idx);
if (!page) return;
/* Find and disconnect the terminal first, before removing from notebook */
SshTerminal* found = nullptr;
for (auto it = terminals_.begin(); it != terminals_.end(); ++it) {
if ((*it)->widget() == page) {
(*it)->disconnect();
delete *it;
found = *it;
terminals_.erase(it);
break;
}
}
if (found) {
found->disconnect();
delete found;
}
/* Now safe to remove page — no page-removed callback will cause issues */
gtk_notebook_remove_page(GTK_NOTEBOOK(notebook_), idx);
}
void SshClientWindow::on_tab_close(GtkNotebook* nb, GtkWidget* page, guint idx, gpointer d) {
(void)nb; (void)idx;
auto* self = static_cast<SshClientWindow*>(d);
for (auto it = self->terminals_.begin(); it != self->terminals_.end(); ++it) {
if ((*it)->widget() == page) {
(*it)->disconnect();
delete *it;
self->terminals_.erase(it);
void SshClientWindow::disconnect() {
int idx = gtk_notebook_get_current_page(GTK_NOTEBOOK(notebook_));
if (idx < 0) return;
GtkWidget* page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook_), idx);
for (auto* t : terminals_) {
if (t->widget() == page) {
t->disconnect();
update_status("Disconnected");
return;
}
}
}
/* ─── connect / disconnect ─────────────────────────────────── */
/* ─── connect ─────────────────────────────────────────────── */
void SshClientWindow::connect_to_server(const std::string& host, int port,
const std::string& user, const std::string& keyfile,
@@ -239,19 +250,6 @@ void SshClientWindow::connect_to_server(const std::string& host, int port,
}
}
void SshClientWindow::disconnect() {
int idx = gtk_notebook_get_current_page(GTK_NOTEBOOK(notebook_));
if (idx < 0) return;
GtkWidget* page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook_), idx);
for (auto* t : terminals_) {
if (t->widget() == page) {
t->disconnect();
update_status("Disconnected");
return;
}
}
}
/* ─── quick connect ───────────────────────────────────────── */
void SshClientWindow::on_quick_connect(GtkWidget*, gpointer d) {
@@ -273,17 +271,17 @@ 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.user, s.keyfile, s.name);
self->show_password_dialog(s.host, s.port, s.user, s.keyfile, s.name);
return;
}
self->show_password_dialog(host, u, "", host);
self->show_password_dialog(host, port, u, "", host);
}
/* ─── password dialog ─────────────────────────────────────── */
void SshClientWindow::show_password_dialog(const std::string& host, const std::string& user,
const std::string& keyfile, const std::string& name) {
void SshClientWindow::show_password_dialog(const std::string& host, int port,
const std::string& user, const std::string& keyfile, const std::string& name) {
GtkWidget* dialog = gtk_dialog_new_with_buttons(
"SSH Authentication", GTK_WINDOW(window_),
@@ -303,7 +301,7 @@ void SshClientWindow::show_password_dialog(const std::string& host, const std::s
GtkWidget* info_label = gtk_label_new(nullptr);
char markup[512];
snprintf(markup, sizeof(markup), "<b>Connect to %s@%s</b>", user.c_str(), host.c_str());
snprintf(markup, sizeof(markup), "<b>Connect to %s@%s:%d</b>", user.c_str(), host.c_str(), port);
gtk_label_set_markup(GTK_LABEL(info_label), markup);
gtk_grid_attach(GTK_GRID(grid), info_label, 0, 0, 2, 1);
@@ -322,7 +320,7 @@ void SshClientWindow::show_password_dialog(const std::string& host, const std::s
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
const char* password = gtk_entry_get_text(GTK_ENTRY(pass_entry));
connect_to_server(host, 22, user, keyfile, password ? password : "", name);
connect_to_server(host, port, user, keyfile, password ? password : "", name);
}
gtk_widget_destroy(dialog);
}
@@ -662,7 +660,7 @@ void SshClientWindow::on_close_tab(GtkWidget*, gpointer d) {
void SshClientWindow::on_connect(GtkWidget*, gpointer d) {
auto* self = static_cast<SshClientWindow*>(d);
self->show_password_dialog("localhost", "root", "", "Local");
self->show_password_dialog("localhost", 22, "root", "", "Local");
}
void SshClientWindow::on_disconnect(GtkWidget*, gpointer d) {