#include "session_tab.h" #include "session_backend_factory.h" #include "terminal_view.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include SessionTab::SessionTab(const Profile& profile, QWidget* parent) : QWidget(parent), m_profile(profile), m_backendThread(new QThread(this)), m_backend(nullptr), m_state(SessionState::Disconnected), m_statusLabel(nullptr), m_errorLabel(nullptr), m_terminalOutput(nullptr), m_eventLog(nullptr), m_connectButton(nullptr), m_disconnectButton(nullptr), m_reconnectButton(nullptr), m_copyErrorButton(nullptr), m_clearTerminalButton(nullptr), m_toggleDetailsButton(nullptr), m_toggleEventsButton(nullptr), m_detailsPanel(nullptr), m_eventsPanel(nullptr) { qRegisterMetaType("SessionConnectOptions"); qRegisterMetaType("SessionState"); setupUi(); std::unique_ptr backend = createSessionBackend(m_profile); m_backend = backend.release(); m_backend->moveToThread(m_backendThread); connect(m_backendThread, &QThread::finished, m_backend, &QObject::deleteLater); connect(this, &SessionTab::requestConnect, m_backend, &SessionBackend::connectSession, Qt::QueuedConnection); connect(this, &SessionTab::requestDisconnect, m_backend, &SessionBackend::disconnectSession, Qt::QueuedConnection); connect(this, &SessionTab::requestReconnect, m_backend, &SessionBackend::reconnectSession, Qt::QueuedConnection); connect(this, &SessionTab::requestInput, m_backend, &SessionBackend::sendInput, Qt::QueuedConnection); connect(this, &SessionTab::requestHostKeyConfirmation, m_backend, &SessionBackend::confirmHostKey, Qt::QueuedConnection); connect(m_backend, &SessionBackend::stateChanged, this, &SessionTab::onBackendStateChanged, Qt::QueuedConnection); connect(m_backend, &SessionBackend::eventLogged, this, &SessionTab::onBackendEventLogged, Qt::QueuedConnection); connect(m_backend, &SessionBackend::connectionError, this, &SessionTab::onBackendConnectionError, Qt::QueuedConnection); connect(m_backend, &SessionBackend::outputReceived, this, &SessionTab::onBackendOutputReceived, Qt::QueuedConnection); connect(m_backend, &SessionBackend::hostKeyConfirmationRequested, this, &SessionTab::onBackendHostKeyConfirmationRequested, Qt::QueuedConnection); m_backendThread->start(); setState(SessionState::Disconnected, QStringLiteral("Ready to connect.")); } SessionTab::~SessionTab() { if (m_backend != nullptr && m_backendThread->isRunning()) { QMetaObject::invokeMethod(m_backend, "disconnectSession", Qt::BlockingQueuedConnection); } m_backendThread->quit(); m_backendThread->wait(2000); } QString SessionTab::tabTitle() const { return QStringLiteral("%1 (%2)").arg(m_profile.name, stateSuffix()); } void SessionTab::onConnectClicked() { if (!validateProfileForConnect()) { return; } const std::optional options = buildConnectOptions(); if (!options.has_value()) { return; } emit requestConnect(options.value()); } void SessionTab::onDisconnectClicked() { emit requestDisconnect(); } void SessionTab::onReconnectClicked() { if (!validateProfileForConnect()) { return; } const std::optional options = buildConnectOptions(); if (!options.has_value()) { return; } emit requestReconnect(options.value()); } void SessionTab::onCopyErrorClicked() { if (m_lastError.isEmpty()) { return; } QGuiApplication::clipboard()->setText(m_lastError); appendEvent(QStringLiteral("Copied last error to clipboard.")); } void SessionTab::onClearTerminalClicked() { m_terminalOutput->clear(); } void SessionTab::onBackendStateChanged(SessionState state, const QString& message) { setState(state, message); } void SessionTab::onBackendEventLogged(const QString& message) { appendEvent(message); } void SessionTab::onBackendConnectionError(const QString& displayMessage, const QString& rawMessage) { m_lastError = rawMessage.isEmpty() ? displayMessage : rawMessage; m_errorLabel->setText(QStringLiteral("Last Error: %1").arg(displayMessage)); m_copyErrorButton->setEnabled(true); } void SessionTab::onBackendOutputReceived(const QString& text) { if (text.isEmpty()) { return; } QTextCursor cursor = m_terminalOutput->textCursor(); cursor.movePosition(QTextCursor::End); cursor.insertText(text); m_terminalOutput->setTextCursor(cursor); m_terminalOutput->ensureCursorVisible(); } void SessionTab::onBackendHostKeyConfirmationRequested(const QString& prompt) { const QString question = prompt.isEmpty() ? QStringLiteral("Unknown SSH host key. Do you trust this host?") : prompt; const QMessageBox::StandardButton reply = QMessageBox::question( this, QStringLiteral("SSH Host Key Confirmation"), QStringLiteral("%1\n\nTrust and continue?").arg(question), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); emit requestHostKeyConfirmation(reply == QMessageBox::Yes); } void SessionTab::setupUi() { auto* rootLayout = new QVBoxLayout(this); auto* detailsHeader = new QHBoxLayout(); m_toggleDetailsButton = new QToolButton(this); m_toggleDetailsButton->setCheckable(true); detailsHeader->addWidget(m_toggleDetailsButton); detailsHeader->addStretch(); m_detailsPanel = new QWidget(this); auto* detailsLayout = new QVBoxLayout(m_detailsPanel); detailsLayout->setContentsMargins(0, 0, 0, 0); auto* profileLabel = new QLabel(QStringLiteral("Profile: %1").arg(m_profile.name), m_detailsPanel); auto* endpointLabel = new QLabel( QStringLiteral("Endpoint: %1://%2@%3:%4") .arg(m_profile.protocol, m_profile.username.isEmpty() ? QStringLiteral("") : m_profile.username, m_profile.host, QString::number(m_profile.port)), m_detailsPanel); auto* authLabel = new QLabel(QStringLiteral("Auth: %1").arg(m_profile.authMode), m_detailsPanel); m_statusLabel = new QLabel(m_detailsPanel); m_errorLabel = new QLabel(QStringLiteral("Last Error: None"), m_detailsPanel); m_errorLabel->setWordWrap(true); auto* actionRow = new QHBoxLayout(); m_connectButton = new QPushButton(QStringLiteral("Connect"), m_detailsPanel); m_disconnectButton = new QPushButton(QStringLiteral("Disconnect"), m_detailsPanel); m_reconnectButton = new QPushButton(QStringLiteral("Reconnect"), m_detailsPanel); m_copyErrorButton = new QPushButton(QStringLiteral("Copy Error"), m_detailsPanel); actionRow->addWidget(m_connectButton); actionRow->addWidget(m_disconnectButton); actionRow->addWidget(m_reconnectButton); actionRow->addWidget(m_copyErrorButton); actionRow->addStretch(); detailsLayout->addWidget(profileLabel); detailsLayout->addWidget(endpointLabel); detailsLayout->addWidget(authLabel); detailsLayout->addWidget(m_statusLabel); detailsLayout->addWidget(m_errorLabel); detailsLayout->addLayout(actionRow); auto* terminalHeader = new QHBoxLayout(); auto* terminalLabel = new QLabel(QStringLiteral("SSH Terminal"), this); m_clearTerminalButton = new QPushButton(QStringLiteral("Clear"), this); terminalHeader->addWidget(terminalLabel); terminalHeader->addStretch(); terminalHeader->addWidget(m_clearTerminalButton); m_terminalOutput = new TerminalView(this); m_terminalOutput->setMaximumBlockCount(4000); QFont terminalFont(QStringLiteral("Monospace")); terminalFont.setStyleHint(QFont::TypeWriter); m_terminalOutput->setFont(terminalFont); m_terminalOutput->setMinimumHeight(260); m_terminalOutput->setPlaceholderText( QStringLiteral("Connect, then type directly here to interact with the SSH session.")); auto* eventsHeader = new QHBoxLayout(); m_toggleEventsButton = new QToolButton(this); m_toggleEventsButton->setCheckable(true); eventsHeader->addWidget(m_toggleEventsButton); eventsHeader->addStretch(); m_eventsPanel = new QWidget(this); auto* eventsLayout = new QVBoxLayout(m_eventsPanel); eventsLayout->setContentsMargins(0, 0, 0, 0); auto* eventTitle = new QLabel(QStringLiteral("Session Events"), m_eventsPanel); m_eventLog = new QPlainTextEdit(m_eventsPanel); m_eventLog->setReadOnly(true); m_eventLog->setPlaceholderText(QStringLiteral("Session event log...")); m_eventLog->setMinimumHeight(140); eventsLayout->addWidget(eventTitle); eventsLayout->addWidget(m_eventLog); rootLayout->addLayout(detailsHeader); rootLayout->addWidget(m_detailsPanel); rootLayout->addLayout(terminalHeader); rootLayout->addWidget(m_terminalOutput, 1); rootLayout->addLayout(eventsHeader); rootLayout->addWidget(m_eventsPanel); setPanelExpanded(m_toggleDetailsButton, m_detailsPanel, QStringLiteral("Details"), true); setPanelExpanded(m_toggleEventsButton, m_eventsPanel, QStringLiteral("Events"), false); connect(m_toggleDetailsButton, &QToolButton::toggled, this, [this](bool expanded) { setPanelExpanded( m_toggleDetailsButton, m_detailsPanel, QStringLiteral("Details"), expanded); }); connect(m_toggleEventsButton, &QToolButton::toggled, this, [this](bool expanded) { setPanelExpanded( m_toggleEventsButton, m_eventsPanel, QStringLiteral("Events"), expanded); }); connect(m_connectButton, &QPushButton::clicked, this, &SessionTab::onConnectClicked); connect(m_disconnectButton, &QPushButton::clicked, this, &SessionTab::onDisconnectClicked); connect(m_reconnectButton, &QPushButton::clicked, this, &SessionTab::onReconnectClicked); connect(m_copyErrorButton, &QPushButton::clicked, this, &SessionTab::onCopyErrorClicked); connect(m_clearTerminalButton, &QPushButton::clicked, this, &SessionTab::onClearTerminalClicked); connect(m_terminalOutput, &TerminalView::inputGenerated, this, [this](const QString& input) { emit requestInput(input); }); } std::optional SessionTab::buildConnectOptions() { SessionConnectOptions options; options.knownHostsPolicy = m_profile.knownHostsPolicy; if (m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) != 0) { return options; } if (m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) { bool accepted = false; const QString password = QInputDialog::getText(this, QStringLiteral("SSH Password"), QStringLiteral("Password for %1@%2:") .arg(m_profile.username, m_profile.host), QLineEdit::Password, QString(), &accepted); if (!accepted) { return std::nullopt; } if (password.isEmpty()) { QMessageBox::warning(this, QStringLiteral("Connect"), QStringLiteral("Password is required for password authentication.")); return std::nullopt; } options.password = password; return options; } QString keyPath = m_profile.privateKeyPath.trimmed(); if (keyPath.isEmpty()) { keyPath = QFileDialog::getOpenFileName(this, QStringLiteral("Select Private Key"), QString(), QStringLiteral("All Files (*)")); if (keyPath.isEmpty()) { return std::nullopt; } } if (!QFileInfo::exists(keyPath)) { QMessageBox::warning(this, QStringLiteral("Connect"), QStringLiteral("Private key file not found: %1").arg(keyPath)); return std::nullopt; } options.privateKeyPath = keyPath; return options; } bool SessionTab::validateProfileForConnect() { if (m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) != 0) { return true; } if (m_profile.host.trimmed().isEmpty()) { QMessageBox::warning(this, QStringLiteral("Connect"), QStringLiteral("SSH host is required.")); return false; } if (m_profile.username.trimmed().isEmpty()) { QMessageBox::warning(this, QStringLiteral("Connect"), QStringLiteral("SSH username is required.")); return false; } if (m_profile.port < 1 || m_profile.port > 65535) { QMessageBox::warning(this, QStringLiteral("Connect"), QStringLiteral("SSH port must be between 1 and 65535.")); return false; } return true; } void SessionTab::appendEvent(const QString& message) { const QString timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss")); m_eventLog->appendPlainText(QStringLiteral("[%1] %2").arg(timestamp, message)); } void SessionTab::setState(SessionState state, const QString& message) { m_state = state; QString style; switch (state) { case SessionState::Disconnected: style = QStringLiteral("border: 1px solid #8a8a8a; background-color: #efefef; padding: 6px;"); break; case SessionState::Connecting: style = QStringLiteral("border: 1px solid #a5a5a5; background-color: #fff3cd; padding: 6px;"); break; case SessionState::Connected: style = QStringLiteral("border: 1px solid #3c763d; background-color: #dff0d8; padding: 6px;"); break; case SessionState::Failed: style = QStringLiteral("border: 1px solid #a94442; background-color: #f2dede; padding: 6px;"); break; } m_statusLabel->setStyleSheet(style); m_statusLabel->setText(QStringLiteral("Connection State: %1").arg(message)); refreshActionButtons(); emit tabTitleChanged(tabTitle()); } QString SessionTab::stateSuffix() const { switch (m_state) { case SessionState::Disconnected: return QStringLiteral("Disconnected"); case SessionState::Connecting: return QStringLiteral("Connecting"); case SessionState::Connected: return QStringLiteral("Connected"); case SessionState::Failed: return QStringLiteral("Failed"); } return QStringLiteral("Unknown"); } void SessionTab::refreshActionButtons() { const bool isConnected = m_state == SessionState::Connected; const bool canConnect = m_state == SessionState::Disconnected || m_state == SessionState::Failed; m_connectButton->setEnabled(canConnect); m_disconnectButton->setEnabled(m_state == SessionState::Connected || m_state == SessionState::Connecting); m_reconnectButton->setEnabled(m_state == SessionState::Connected || m_state == SessionState::Failed || m_state == SessionState::Disconnected); m_copyErrorButton->setEnabled(!m_lastError.isEmpty()); m_terminalOutput->setEnabled(isConnected); if (isConnected) { m_terminalOutput->setFocus(); } } void SessionTab::setPanelExpanded(QToolButton* button, QWidget* panel, const QString& name, bool expanded) { if (button == nullptr || panel == nullptr) { return; } button->blockSignals(true); button->setChecked(expanded); button->blockSignals(false); panel->setVisible(expanded); button->setText(expanded ? QStringLiteral("Hide %1").arg(name) : QStringLiteral("Show %1").arg(name)); }