Start Milestone 4 interactive SSH terminal and host-key flow

This commit is contained in:
Keith Smith
2026-03-01 09:50:03 -07:00
parent 3c158269bf
commit 2ea712db36
10 changed files with 245 additions and 132 deletions

View File

@@ -15,6 +15,7 @@
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QTextCursor>
#include <QThread>
#include <QVBoxLayout>
@@ -28,11 +29,15 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
m_state(SessionState::Disconnected),
m_statusLabel(nullptr),
m_errorLabel(nullptr),
m_terminalOutput(nullptr),
m_terminalInput(nullptr),
m_eventLog(nullptr),
m_connectButton(nullptr),
m_disconnectButton(nullptr),
m_reconnectButton(nullptr),
m_copyErrorButton(nullptr)
m_copyErrorButton(nullptr),
m_sendInputButton(nullptr),
m_clearTerminalButton(nullptr)
{
qRegisterMetaType<SessionConnectOptions>("SessionConnectOptions");
qRegisterMetaType<SessionState>("SessionState");
@@ -59,6 +64,16 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
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,
@@ -75,6 +90,16 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
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();
@@ -139,6 +164,22 @@ void SessionTab::onCopyErrorClicked()
appendEvent(QStringLiteral("Copied last error to clipboard."));
}
void SessionTab::onSendInputClicked()
{
const QString input = m_terminalInput->text();
if (input.isEmpty()) {
return;
}
emit requestInput(input + QStringLiteral("\n"));
m_terminalInput->clear();
}
void SessionTab::onClearTerminalClicked()
{
m_terminalOutput->clear();
}
void SessionTab::onBackendStateChanged(SessionState state, const QString& message)
{
setState(state, message);
@@ -156,6 +197,35 @@ void SessionTab::onBackendConnectionError(const QString& displayMessage, const Q
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);
@@ -186,20 +256,33 @@ void SessionTab::setupUi()
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;"));
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 QPlainTextEdit(this);
m_terminalOutput->setReadOnly(true);
m_terminalOutput->setMaximumBlockCount(4000);
QFont terminalFont(QStringLiteral("Monospace"));
terminalFont.setStyleHint(QFont::TypeWriter);
m_terminalOutput->setFont(terminalFont);
m_terminalOutput->setMinimumHeight(220);
auto* terminalInputRow = new QHBoxLayout();
m_terminalInput = new QLineEdit(this);
m_terminalInput->setPlaceholderText(QStringLiteral("Type command and press Enter..."));
m_sendInputButton = new QPushButton(QStringLiteral("Send"), this);
terminalInputRow->addWidget(m_terminalInput, 1);
terminalInputRow->addWidget(m_sendInputButton);
auto* eventHeader = new QLabel(QStringLiteral("Session Events"), this);
m_eventLog = new QPlainTextEdit(this);
m_eventLog->setReadOnly(true);
m_eventLog->setPlaceholderText(QStringLiteral("Session event log..."));
m_eventLog->setMinimumHeight(180);
m_eventLog->setMinimumHeight(140);
rootLayout->addWidget(profileLabel);
rootLayout->addWidget(endpointLabel);
@@ -207,13 +290,22 @@ void SessionTab::setupUi()
rootLayout->addWidget(m_statusLabel);
rootLayout->addWidget(m_errorLabel);
rootLayout->addLayout(actionRow);
rootLayout->addWidget(surfaceLabel);
rootLayout->addLayout(terminalHeader);
rootLayout->addWidget(m_terminalOutput, 1);
rootLayout->addLayout(terminalInputRow);
rootLayout->addWidget(eventHeader);
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);
connect(m_sendInputButton, &QPushButton::clicked, this, &SessionTab::onSendInputClicked);
connect(m_terminalInput, &QLineEdit::returnPressed, this, &SessionTab::onSendInputClicked);
connect(m_clearTerminalButton,
&QPushButton::clicked,
this,
&SessionTab::onClearTerminalClicked);
}
std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
@@ -352,12 +444,17 @@ QString SessionTab::stateSuffix() const
void SessionTab::refreshActionButtons()
{
m_connectButton->setEnabled(m_state == SessionState::Disconnected
|| m_state == SessionState::Failed);
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_terminalInput->setEnabled(isConnected);
m_sendInputButton->setEnabled(isConnected);
}