Allow direct terminal typing and collapsible session panels
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#include "session_tab.h"
|
||||
|
||||
#include "session_backend_factory.h"
|
||||
#include "terminal_view.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QDateTime>
|
||||
@@ -17,6 +18,7 @@
|
||||
#include <QPushButton>
|
||||
#include <QTextCursor>
|
||||
#include <QThread>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <memory>
|
||||
@@ -30,14 +32,16 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
|
||||
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_sendInputButton(nullptr),
|
||||
m_clearTerminalButton(nullptr)
|
||||
m_clearTerminalButton(nullptr),
|
||||
m_toggleDetailsButton(nullptr),
|
||||
m_toggleEventsButton(nullptr),
|
||||
m_detailsPanel(nullptr),
|
||||
m_eventsPanel(nullptr)
|
||||
{
|
||||
qRegisterMetaType<SessionConnectOptions>("SessionConnectOptions");
|
||||
qRegisterMetaType<SessionState>("SessionState");
|
||||
@@ -164,17 +168,6 @@ 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();
|
||||
@@ -230,25 +223,35 @@ void SessionTab::setupUi()
|
||||
{
|
||||
auto* rootLayout = new QVBoxLayout(this);
|
||||
|
||||
auto* profileLabel = new QLabel(QStringLiteral("Profile: %1").arg(m_profile.name), 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("<none>") : 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_detailsPanel);
|
||||
auto* authLabel = new QLabel(QStringLiteral("Auth: %1").arg(m_profile.authMode), m_detailsPanel);
|
||||
|
||||
m_statusLabel = new QLabel(this);
|
||||
m_errorLabel = new QLabel(QStringLiteral("Last Error: None"), this);
|
||||
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"), this);
|
||||
m_disconnectButton = new QPushButton(QStringLiteral("Disconnect"), this);
|
||||
m_reconnectButton = new QPushButton(QStringLiteral("Reconnect"), this);
|
||||
m_copyErrorButton = new QPushButton(QStringLiteral("Copy Error"), this);
|
||||
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);
|
||||
@@ -256,6 +259,13 @@ void SessionTab::setupUi()
|
||||
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);
|
||||
@@ -263,49 +273,71 @@ void SessionTab::setupUi()
|
||||
terminalHeader->addStretch();
|
||||
terminalHeader->addWidget(m_clearTerminalButton);
|
||||
|
||||
m_terminalOutput = new QPlainTextEdit(this);
|
||||
m_terminalOutput->setReadOnly(true);
|
||||
m_terminalOutput = new TerminalView(this);
|
||||
m_terminalOutput->setMaximumBlockCount(4000);
|
||||
QFont terminalFont(QStringLiteral("Monospace"));
|
||||
terminalFont.setStyleHint(QFont::TypeWriter);
|
||||
m_terminalOutput->setFont(terminalFont);
|
||||
m_terminalOutput->setMinimumHeight(220);
|
||||
m_terminalOutput->setMinimumHeight(260);
|
||||
m_terminalOutput->setPlaceholderText(
|
||||
QStringLiteral("Connect, then type directly here to interact with the SSH session."));
|
||||
|
||||
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* eventsHeader = new QHBoxLayout();
|
||||
m_toggleEventsButton = new QToolButton(this);
|
||||
m_toggleEventsButton->setCheckable(true);
|
||||
eventsHeader->addWidget(m_toggleEventsButton);
|
||||
eventsHeader->addStretch();
|
||||
|
||||
auto* eventHeader = new QLabel(QStringLiteral("Session Events"), this);
|
||||
m_eventLog = new QPlainTextEdit(this);
|
||||
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);
|
||||
|
||||
rootLayout->addWidget(profileLabel);
|
||||
rootLayout->addWidget(endpointLabel);
|
||||
rootLayout->addWidget(authLabel);
|
||||
rootLayout->addWidget(m_statusLabel);
|
||||
rootLayout->addWidget(m_errorLabel);
|
||||
rootLayout->addLayout(actionRow);
|
||||
eventsLayout->addWidget(eventTitle);
|
||||
eventsLayout->addWidget(m_eventLog);
|
||||
|
||||
rootLayout->addLayout(detailsHeader);
|
||||
rootLayout->addWidget(m_detailsPanel);
|
||||
rootLayout->addLayout(terminalHeader);
|
||||
rootLayout->addWidget(m_terminalOutput, 1);
|
||||
rootLayout->addLayout(terminalInputRow);
|
||||
rootLayout->addWidget(eventHeader);
|
||||
rootLayout->addWidget(m_eventLog, 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_sendInputButton, &QPushButton::clicked, this, &SessionTab::onSendInputClicked);
|
||||
connect(m_terminalInput, &QLineEdit::returnPressed, this, &SessionTab::onSendInputClicked);
|
||||
connect(m_clearTerminalButton,
|
||||
&QPushButton::clicked,
|
||||
this,
|
||||
&SessionTab::onClearTerminalClicked);
|
||||
connect(m_terminalOutput,
|
||||
&TerminalView::inputGenerated,
|
||||
this,
|
||||
[this](const QString& input) { emit requestInput(input); });
|
||||
}
|
||||
|
||||
std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
|
||||
@@ -455,6 +487,26 @@ void SessionTab::refreshActionButtons()
|
||||
|| m_state == SessionState::Disconnected);
|
||||
m_copyErrorButton->setEnabled(!m_lastError.isEmpty());
|
||||
|
||||
m_terminalInput->setEnabled(isConnected);
|
||||
m_sendInputButton->setEnabled(isConnected);
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include <QPointer>
|
||||
#include <optional>
|
||||
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPlainTextEdit;
|
||||
class QPushButton;
|
||||
class QThread;
|
||||
class SessionBackend;
|
||||
class TerminalView;
|
||||
class QToolButton;
|
||||
|
||||
class SessionTab : public QWidget
|
||||
{
|
||||
@@ -38,7 +40,6 @@ private slots:
|
||||
void onDisconnectClicked();
|
||||
void onReconnectClicked();
|
||||
void onCopyErrorClicked();
|
||||
void onSendInputClicked();
|
||||
void onClearTerminalClicked();
|
||||
|
||||
void onBackendStateChanged(SessionState state, const QString& message);
|
||||
@@ -56,15 +57,17 @@ private:
|
||||
|
||||
QLabel* m_statusLabel;
|
||||
QLabel* m_errorLabel;
|
||||
QPlainTextEdit* m_terminalOutput;
|
||||
QLineEdit* m_terminalInput;
|
||||
TerminalView* m_terminalOutput;
|
||||
QPlainTextEdit* m_eventLog;
|
||||
QPushButton* m_connectButton;
|
||||
QPushButton* m_disconnectButton;
|
||||
QPushButton* m_reconnectButton;
|
||||
QPushButton* m_copyErrorButton;
|
||||
QPushButton* m_sendInputButton;
|
||||
QPushButton* m_clearTerminalButton;
|
||||
QToolButton* m_toggleDetailsButton;
|
||||
QToolButton* m_toggleEventsButton;
|
||||
QWidget* m_detailsPanel;
|
||||
QWidget* m_eventsPanel;
|
||||
|
||||
void setupUi();
|
||||
std::optional<SessionConnectOptions> buildConnectOptions();
|
||||
@@ -73,6 +76,7 @@ private:
|
||||
void setState(SessionState state, const QString& message);
|
||||
QString stateSuffix() const;
|
||||
void refreshActionButtons();
|
||||
void setPanelExpanded(QToolButton* button, QWidget* panel, const QString& name, bool expanded);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
78
src/terminal_view.cpp
Normal file
78
src/terminal_view.cpp
Normal file
@@ -0,0 +1,78 @@
|
||||
#include "terminal_view.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QKeyEvent>
|
||||
|
||||
TerminalView::TerminalView(QWidget* parent) : QPlainTextEdit(parent)
|
||||
{
|
||||
setReadOnly(true);
|
||||
setUndoRedoEnabled(false);
|
||||
}
|
||||
|
||||
void TerminalView::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Qt::KeyboardModifiers modifiers = event->modifiers();
|
||||
|
||||
if (modifiers == Qt::ControlModifier) {
|
||||
switch (event->key()) {
|
||||
case Qt::Key_C:
|
||||
emit inputGenerated(QStringLiteral("\x03"));
|
||||
return;
|
||||
case Qt::Key_D:
|
||||
emit inputGenerated(QStringLiteral("\x04"));
|
||||
return;
|
||||
case Qt::Key_L:
|
||||
emit inputGenerated(QStringLiteral("\x0c"));
|
||||
return;
|
||||
case Qt::Key_V: {
|
||||
const QString clipboardText = QApplication::clipboard()->text();
|
||||
if (!clipboardText.isEmpty()) {
|
||||
emit inputGenerated(clipboardText);
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (event->key()) {
|
||||
case Qt::Key_Return:
|
||||
case Qt::Key_Enter:
|
||||
emit inputGenerated(QStringLiteral("\n"));
|
||||
return;
|
||||
case Qt::Key_Backspace:
|
||||
emit inputGenerated(QStringLiteral("\x7f"));
|
||||
return;
|
||||
case Qt::Key_Tab:
|
||||
emit inputGenerated(QStringLiteral("\t"));
|
||||
return;
|
||||
case Qt::Key_Left:
|
||||
emit inputGenerated(QStringLiteral("\x1b[D"));
|
||||
return;
|
||||
case Qt::Key_Right:
|
||||
emit inputGenerated(QStringLiteral("\x1b[C"));
|
||||
return;
|
||||
case Qt::Key_Up:
|
||||
emit inputGenerated(QStringLiteral("\x1b[A"));
|
||||
return;
|
||||
case Qt::Key_Down:
|
||||
emit inputGenerated(QStringLiteral("\x1b[B"));
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const QString text = event->text();
|
||||
if (!text.isEmpty()) {
|
||||
emit inputGenerated(text);
|
||||
return;
|
||||
}
|
||||
|
||||
QPlainTextEdit::keyPressEvent(event);
|
||||
}
|
||||
20
src/terminal_view.h
Normal file
20
src/terminal_view.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef ORBITHUB_TERMINAL_VIEW_H
|
||||
#define ORBITHUB_TERMINAL_VIEW_H
|
||||
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
class TerminalView : public QPlainTextEdit
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TerminalView(QWidget* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void inputGenerated(const QString& input);
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user