diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f932d5..afdb9f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,8 @@ add_executable(orbithub src/session_backend_factory.h src/session_tab.cpp src/session_tab.h + src/terminal_view.cpp + src/terminal_view.h src/session_window.cpp src/session_window.h src/ssh_session_backend.cpp diff --git a/src/session_tab.cpp b/src/session_tab.cpp index f748f1f..8ec6a3d 100644 --- a/src/session_tab.cpp +++ b/src/session_tab.cpp @@ -1,6 +1,7 @@ #include "session_tab.h" #include "session_backend_factory.h" +#include "terminal_view.h" #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include #include @@ -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"); qRegisterMetaType("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("") : 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 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)); } diff --git a/src/session_tab.h b/src/session_tab.h index 061d7de..dc5ed05 100644 --- a/src/session_tab.h +++ b/src/session_tab.h @@ -6,14 +6,16 @@ #include +#include #include 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 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 diff --git a/src/terminal_view.cpp b/src/terminal_view.cpp new file mode 100644 index 0000000..0bb6e17 --- /dev/null +++ b/src/terminal_view.cpp @@ -0,0 +1,78 @@ +#include "terminal_view.h" + +#include +#include +#include + +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); +} diff --git a/src/terminal_view.h b/src/terminal_view.h new file mode 100644 index 0000000..b3591b4 --- /dev/null +++ b/src/terminal_view.h @@ -0,0 +1,20 @@ +#ifndef ORBITHUB_TERMINAL_VIEW_H +#define ORBITHUB_TERMINAL_VIEW_H + +#include + +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