Improve terminal theming, cursor UX, and size negotiation

This commit is contained in:
Keith Smith
2026-03-01 10:14:43 -07:00
parent 20ee48db32
commit c3369b8e48
9 changed files with 141 additions and 24 deletions

View File

@@ -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);

View File

@@ -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();
}
}
void SessionTab::setPanelExpanded(QToolButton* button,

View File

@@ -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();

View File

@@ -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));
}

View File

@@ -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

View File

@@ -3,7 +3,11 @@
#include <QApplication>
#include <QClipboard>
#include <QColor>
#include <QFocusEvent>
#include <QFontMetrics>
#include <QKeyEvent>
#include <QResizeEvent>
#include <QTimer>
#include <QTextCursor>
#include <algorithm>
@@ -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<size_t>(safeIndex))
: m_palette.normal.at(static_cast<size_t>(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());
}

View File

@@ -6,6 +6,8 @@
#include <array>
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

View File

@@ -33,3 +33,7 @@ void UnsupportedSessionBackend::sendInput(const QString&)
void UnsupportedSessionBackend::confirmHostKey(bool)
{
}
void UnsupportedSessionBackend::updateTerminalSize(int, int)
{
}

View File

@@ -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