#include "session_tab.h" #include "session_backend_factory.h" #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_eventLog(nullptr), m_connectButton(nullptr), m_disconnectButton(nullptr), m_reconnectButton(nullptr), m_copyErrorButton(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(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); 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::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::setupUi() { auto* rootLayout = new QVBoxLayout(this); auto* profileLabel = new QLabel(QStringLiteral("Profile: %1").arg(m_profile.name), this); 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)), this); auto* authLabel = new QLabel(QStringLiteral("Auth: %1").arg(m_profile.authMode), this); m_statusLabel = new QLabel(this); m_errorLabel = new QLabel(QStringLiteral("Last Error: None"), this); m_errorLabel->setWordWrap(true); auto* actionRow = new QHBoxLayout(); m_connectButton = new QPushButton(QStringLiteral("Connect"), this); m_disconnectButton = new QPushButton(QStringLiteral("Disconnect"), this); m_reconnectButton = new QPushButton(QStringLiteral("Reconnect"), this); m_copyErrorButton = new QPushButton(QStringLiteral("Copy Error"), this); actionRow->addWidget(m_connectButton); actionRow->addWidget(m_disconnectButton); actionRow->addWidget(m_reconnectButton); actionRow->addWidget(m_copyErrorButton); actionRow->addStretch(); auto* surfaceLabel = new QLabel(QStringLiteral("OrbitHub Native Surface"), this); QFont surfaceFont = surfaceLabel->font(); surfaceFont.setPointSize(surfaceFont.pointSize() + 6); surfaceFont.setBold(true); surfaceLabel->setFont(surfaceFont); surfaceLabel->setAlignment(Qt::AlignCenter); surfaceLabel->setMinimumHeight(180); surfaceLabel->setStyleSheet( QStringLiteral("border: 1px solid #8a8a8a; background-color: #f5f5f5;")); m_eventLog = new QPlainTextEdit(this); m_eventLog->setReadOnly(true); m_eventLog->setPlaceholderText(QStringLiteral("Session event log...")); m_eventLog->setMinimumHeight(180); rootLayout->addWidget(profileLabel); rootLayout->addWidget(endpointLabel); rootLayout->addWidget(authLabel); rootLayout->addWidget(m_statusLabel); rootLayout->addWidget(m_errorLabel); rootLayout->addLayout(actionRow); rootLayout->addWidget(surfaceLabel); rootLayout->addWidget(m_eventLog, 1); 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); } 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() { m_connectButton->setEnabled(m_state == SessionState::Disconnected || m_state == SessionState::Failed); 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()); }