diff --git a/src/session_backend.h b/src/session_backend.h index 0a02ff1..2e61ad7 100644 --- a/src/session_backend.h +++ b/src/session_backend.h @@ -43,6 +43,7 @@ public slots: virtual void reconnectSession(const SessionConnectOptions& options) = 0; virtual void sendInput(const QString& input) = 0; virtual void confirmHostKey(bool trustHost) = 0; + virtual void updateTerminalSize(int columns, int rows) = 0; signals: void stateChanged(SessionState state, const QString& message); diff --git a/src/session_tab.cpp b/src/session_tab.cpp index 5083ad1..ea2bbd2 100644 --- a/src/session_tab.cpp +++ b/src/session_tab.cpp @@ -79,6 +79,11 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent) m_backend, &SessionBackend::confirmHostKey, Qt::QueuedConnection); + connect(this, + &SessionTab::requestTerminalSize, + m_backend, + &SessionBackend::updateTerminalSize, + Qt::QueuedConnection); connect(m_backend, &SessionBackend::stateChanged, @@ -172,6 +177,11 @@ void SessionTab::onCopyErrorClicked() void SessionTab::onClearTerminalClicked() { m_terminalOutput->clear(); + if (m_state == SessionState::Connected) { + // Ask the remote shell to repaint a prompt after local clear. + emit requestInput(QStringLiteral("\x0c")); + } + m_terminalOutput->setFocus(); } void SessionTab::onBackendStateChanged(SessionState state, const QString& message) @@ -339,6 +349,10 @@ void SessionTab::setupUi() &TerminalView::inputGenerated, this, [this](const QString& input) { emit requestInput(input); }); + connect(m_terminalOutput, + &TerminalView::terminalSizeChanged, + this, + [this](int columns, int rows) { emit requestTerminalSize(columns, rows); }); connect(m_themeSelector, &QComboBox::currentTextChanged, m_terminalOutput, @@ -493,9 +507,7 @@ void SessionTab::refreshActionButtons() m_copyErrorButton->setEnabled(!m_lastError.isEmpty()); m_terminalOutput->setEnabled(isConnected); - if (isConnected) { - m_terminalOutput->setFocus(); - } + m_terminalOutput->setFocus(); } void SessionTab::setPanelExpanded(QToolButton* button, diff --git a/src/session_tab.h b/src/session_tab.h index 0beed74..ec7b790 100644 --- a/src/session_tab.h +++ b/src/session_tab.h @@ -35,6 +35,7 @@ signals: void requestReconnect(const SessionConnectOptions& options); void requestInput(const QString& input); void requestHostKeyConfirmation(bool trustHost); + void requestTerminalSize(int columns, int rows); private slots: void onConnectClicked(); diff --git a/src/ssh_session_backend.cpp b/src/ssh_session_backend.cpp index a2f87a8..9e0e3be 100644 --- a/src/ssh_session_backend.cpp +++ b/src/ssh_session_backend.cpp @@ -25,7 +25,9 @@ SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent) m_reconnectPending(false), m_waitingForPasswordPrompt(false), m_waitingForHostKeyConfirmation(false), - m_passwordSubmitted(false) + m_passwordSubmitted(false), + m_terminalColumns(0), + m_terminalRows(0) { m_connectedProbeTimer->setSingleShot(true); @@ -152,6 +154,16 @@ void SshSessionBackend::confirmHostKey(bool trustHost) : QStringLiteral("Host key rejected by user.")); } +void SshSessionBackend::updateTerminalSize(int columns, int rows) +{ + m_terminalColumns = columns; + m_terminalRows = rows; + + if (m_state == SessionState::Connected) { + applyTerminalSizeIfAvailable(); + } +} + void SshSessionBackend::onProcessStarted() { emit eventLogged(QStringLiteral("ssh process started.")); @@ -284,6 +296,10 @@ void SshSessionBackend::setState(SessionState state, const QString& message) m_state = state; emit stateChanged(state, message); emit eventLogged(message); + + if (m_state == SessionState::Connected) { + applyTerminalSizeIfAvailable(); + } } bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options) @@ -502,3 +518,21 @@ QString SshSessionBackend::knownHostsFileForNullDevice() const return QStringLiteral("/dev/null"); #endif } + +void SshSessionBackend::applyTerminalSizeIfAvailable() +{ + if (m_process->state() != QProcess::Running) { + return; + } + + if (m_terminalColumns <= 0 || m_terminalRows <= 0) { + return; + } + + const QString command = QStringLiteral("stty cols %1 rows %2\\n") + .arg(m_terminalColumns) + .arg(m_terminalRows); + m_process->write(command.toUtf8()); + emit eventLogged( + QStringLiteral("Applied terminal size: %1x%2").arg(m_terminalColumns).arg(m_terminalRows)); +} diff --git a/src/ssh_session_backend.h b/src/ssh_session_backend.h index c651c7e..cb522e5 100644 --- a/src/ssh_session_backend.h +++ b/src/ssh_session_backend.h @@ -21,6 +21,7 @@ public slots: void reconnectSession(const SessionConnectOptions& options) override; void sendInput(const QString& input) override; void confirmHostKey(bool trustHost) override; + void updateTerminalSize(int columns, int rows) override; private slots: void onProcessStarted(); @@ -43,6 +44,8 @@ private: bool m_waitingForPasswordPrompt; bool m_waitingForHostKeyConfirmation; bool m_passwordSubmitted; + int m_terminalColumns; + int m_terminalRows; void setState(SessionState state, const QString& message); bool startSshProcess(const SessionConnectOptions& options); @@ -52,6 +55,7 @@ private: void cleanupAskPassScript(); QString mapSshError(const QString& rawError) const; QString knownHostsFileForNullDevice() const; + void applyTerminalSizeIfAvailable(); }; #endif diff --git a/src/terminal_view.cpp b/src/terminal_view.cpp index 6b1ed7c..4ed5512 100644 --- a/src/terminal_view.cpp +++ b/src/terminal_view.cpp @@ -3,7 +3,11 @@ #include #include #include +#include +#include #include +#include +#include #include #include @@ -21,13 +25,21 @@ TerminalView::TerminalView(QWidget* parent) m_hasFgColor(false), m_hasBgColor(false) { - setReadOnly(true); + setReadOnly(false); setUndoRedoEnabled(false); - document()->setMaximumBlockCount(4000); setAcceptRichText(false); + setLineWrapMode(QTextEdit::NoWrap); + setContextMenuPolicy(Qt::NoContextMenu); + setCursorWidth(2); + document()->setMaximumBlockCount(4000); applyThemePalette(paletteByName(QStringLiteral("Dark"))); resetSgrState(); + + QTimer::singleShot(0, this, [this]() { + moveCursor(QTextCursor::End); + emitTerminalSize(); + }); } QStringList TerminalView::themeNames() @@ -123,8 +135,19 @@ void TerminalView::keyPressEvent(QKeyEvent* event) return; } + moveCursor(QTextCursor::End); + const Qt::KeyboardModifiers modifiers = event->modifiers(); + if (modifiers == (Qt::ControlModifier | Qt::ShiftModifier) + && event->key() == Qt::Key_C) { + const QString selected = textCursor().selectedText(); + if (!selected.isEmpty()) { + QApplication::clipboard()->setText(selected); + } + return; + } + if (modifiers == Qt::ControlModifier) { switch (event->key()) { case Qt::Key_C: @@ -180,8 +203,18 @@ void TerminalView::keyPressEvent(QKeyEvent* event) emit inputGenerated(text); return; } +} - QTextEdit::keyPressEvent(event); +void TerminalView::focusInEvent(QFocusEvent* event) +{ + QTextEdit::focusInEvent(event); + moveCursor(QTextCursor::End); +} + +void TerminalView::resizeEvent(QResizeEvent* event) +{ + QTextEdit::resizeEvent(event); + emitTerminalSize(); } TerminalView::ThemePalette TerminalView::paletteByName(const QString& themeName) @@ -190,23 +223,23 @@ TerminalView::ThemePalette TerminalView::paletteByName(const QString& themeName) if (theme == QStringLiteral("light")) { return ThemePalette{QStringLiteral("Light"), - QColor(QStringLiteral("#fafafa")), - QColor(QStringLiteral("#202124")), + QColor(QStringLiteral("#ececec")), + QColor(QStringLiteral("#000000")), {QColor(QStringLiteral("#000000")), - QColor(QStringLiteral("#a31515")), + QColor(QStringLiteral("#aa0000")), QColor(QStringLiteral("#008000")), - QColor(QStringLiteral("#795e26")), - QColor(QStringLiteral("#0000ff")), - QColor(QStringLiteral("#af00db")), - QColor(QStringLiteral("#0451a5")), - QColor(QStringLiteral("#666666"))}, - {QColor(QStringLiteral("#7f7f7f")), - QColor(QStringLiteral("#cd3131")), - QColor(QStringLiteral("#14a10e")), - QColor(QStringLiteral("#b5ba00")), - QColor(QStringLiteral("#0451a5")), - QColor(QStringLiteral("#bc05bc")), - QColor(QStringLiteral("#0598bc")), + QColor(QStringLiteral("#7a5f00")), + QColor(QStringLiteral("#0033cc")), + QColor(QStringLiteral("#8a00a8")), + QColor(QStringLiteral("#005f87")), + QColor(QStringLiteral("#333333"))}, + {QColor(QStringLiteral("#5c5c5c")), + QColor(QStringLiteral("#d30000")), + QColor(QStringLiteral("#00a000")), + QColor(QStringLiteral("#9a7700")), + QColor(QStringLiteral("#0055ff")), + QColor(QStringLiteral("#b300db")), + QColor(QStringLiteral("#007ea7")), QColor(QStringLiteral("#111111"))}}; } @@ -338,7 +371,7 @@ void TerminalView::handleSgrSequence(const QString& params) for (int i = 0; i < parts.size(); ++i) { const QString part = parts.at(i).trimmed(); bool ok = false; - int code = part.isEmpty() ? 0 : part.toInt(&ok); + const int code = part.isEmpty() ? 0 : part.toInt(&ok); if (!ok && !part.isEmpty()) { continue; } @@ -399,7 +432,7 @@ void TerminalView::handleSgrSequence(const QString& params) if (mode == 5 && i + 2 < parts.size()) { const int index = parts.at(i + 2).toInt(&ok); if (ok) { - QColor color = colorFrom256Index(index); + const QColor color = colorFrom256Index(index); if (background) { m_bgColor = color; m_hasBgColor = true; @@ -466,3 +499,22 @@ QColor TerminalView::paletteColor(bool, int index, bool bright) const return bright ? m_palette.bright.at(static_cast(safeIndex)) : m_palette.normal.at(static_cast(safeIndex)); } + +int TerminalView::terminalColumns() const +{ + const QFontMetrics metrics(font()); + const int cellWidth = std::max(1, metrics.horizontalAdvance(QChar::fromLatin1('M'))); + return std::max(1, viewport()->width() / cellWidth); +} + +int TerminalView::terminalRows() const +{ + const QFontMetrics metrics(font()); + const int cellHeight = std::max(1, metrics.lineSpacing()); + return std::max(1, viewport()->height() / cellHeight); +} + +void TerminalView::emitTerminalSize() +{ + emit terminalSizeChanged(terminalColumns(), terminalRows()); +} diff --git a/src/terminal_view.h b/src/terminal_view.h index 70ff7da..18c7e94 100644 --- a/src/terminal_view.h +++ b/src/terminal_view.h @@ -6,6 +6,8 @@ #include class QKeyEvent; +class QFocusEvent; +class QResizeEvent; class TerminalView : public QTextEdit { @@ -20,9 +22,12 @@ public: signals: void inputGenerated(const QString& input); + void terminalSizeChanged(int columns, int rows); protected: void keyPressEvent(QKeyEvent* event) override; + void focusInEvent(QFocusEvent* event) override; + void resizeEvent(QResizeEvent* event) override; private: struct ThemePalette { @@ -51,6 +56,9 @@ private: void handleSgrSequence(const QString& params); void appendTextChunk(const QString& text); QColor paletteColor(bool background, int index, bool bright) const; + int terminalColumns() const; + int terminalRows() const; + void emitTerminalSize(); }; #endif diff --git a/src/unsupported_session_backend.cpp b/src/unsupported_session_backend.cpp index 32436f3..7ecca42 100644 --- a/src/unsupported_session_backend.cpp +++ b/src/unsupported_session_backend.cpp @@ -33,3 +33,7 @@ void UnsupportedSessionBackend::sendInput(const QString&) void UnsupportedSessionBackend::confirmHostKey(bool) { } + +void UnsupportedSessionBackend::updateTerminalSize(int, int) +{ +} diff --git a/src/unsupported_session_backend.h b/src/unsupported_session_backend.h index d50051d..ccbd83b 100644 --- a/src/unsupported_session_backend.h +++ b/src/unsupported_session_backend.h @@ -16,6 +16,7 @@ public slots: void reconnectSession(const SessionConnectOptions& options) override; void sendInput(const QString& input) override; void confirmHostKey(bool trustHost) override; + void updateTerminalSize(int columns, int rows) override; }; #endif