Improve terminal theming, cursor UX, and size negotiation
This commit is contained in:
@@ -43,6 +43,7 @@ public slots:
|
|||||||
virtual void reconnectSession(const SessionConnectOptions& options) = 0;
|
virtual void reconnectSession(const SessionConnectOptions& options) = 0;
|
||||||
virtual void sendInput(const QString& input) = 0;
|
virtual void sendInput(const QString& input) = 0;
|
||||||
virtual void confirmHostKey(bool trustHost) = 0;
|
virtual void confirmHostKey(bool trustHost) = 0;
|
||||||
|
virtual void updateTerminalSize(int columns, int rows) = 0;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void stateChanged(SessionState state, const QString& message);
|
void stateChanged(SessionState state, const QString& message);
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
|
|||||||
m_backend,
|
m_backend,
|
||||||
&SessionBackend::confirmHostKey,
|
&SessionBackend::confirmHostKey,
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
connect(this,
|
||||||
|
&SessionTab::requestTerminalSize,
|
||||||
|
m_backend,
|
||||||
|
&SessionBackend::updateTerminalSize,
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
connect(m_backend,
|
connect(m_backend,
|
||||||
&SessionBackend::stateChanged,
|
&SessionBackend::stateChanged,
|
||||||
@@ -172,6 +177,11 @@ void SessionTab::onCopyErrorClicked()
|
|||||||
void SessionTab::onClearTerminalClicked()
|
void SessionTab::onClearTerminalClicked()
|
||||||
{
|
{
|
||||||
m_terminalOutput->clear();
|
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)
|
void SessionTab::onBackendStateChanged(SessionState state, const QString& message)
|
||||||
@@ -339,6 +349,10 @@ void SessionTab::setupUi()
|
|||||||
&TerminalView::inputGenerated,
|
&TerminalView::inputGenerated,
|
||||||
this,
|
this,
|
||||||
[this](const QString& input) { emit requestInput(input); });
|
[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,
|
connect(m_themeSelector,
|
||||||
&QComboBox::currentTextChanged,
|
&QComboBox::currentTextChanged,
|
||||||
m_terminalOutput,
|
m_terminalOutput,
|
||||||
@@ -493,9 +507,7 @@ void SessionTab::refreshActionButtons()
|
|||||||
m_copyErrorButton->setEnabled(!m_lastError.isEmpty());
|
m_copyErrorButton->setEnabled(!m_lastError.isEmpty());
|
||||||
|
|
||||||
m_terminalOutput->setEnabled(isConnected);
|
m_terminalOutput->setEnabled(isConnected);
|
||||||
if (isConnected) {
|
|
||||||
m_terminalOutput->setFocus();
|
m_terminalOutput->setFocus();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionTab::setPanelExpanded(QToolButton* button,
|
void SessionTab::setPanelExpanded(QToolButton* button,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ signals:
|
|||||||
void requestReconnect(const SessionConnectOptions& options);
|
void requestReconnect(const SessionConnectOptions& options);
|
||||||
void requestInput(const QString& input);
|
void requestInput(const QString& input);
|
||||||
void requestHostKeyConfirmation(bool trustHost);
|
void requestHostKeyConfirmation(bool trustHost);
|
||||||
|
void requestTerminalSize(int columns, int rows);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onConnectClicked();
|
void onConnectClicked();
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent)
|
|||||||
m_reconnectPending(false),
|
m_reconnectPending(false),
|
||||||
m_waitingForPasswordPrompt(false),
|
m_waitingForPasswordPrompt(false),
|
||||||
m_waitingForHostKeyConfirmation(false),
|
m_waitingForHostKeyConfirmation(false),
|
||||||
m_passwordSubmitted(false)
|
m_passwordSubmitted(false),
|
||||||
|
m_terminalColumns(0),
|
||||||
|
m_terminalRows(0)
|
||||||
{
|
{
|
||||||
m_connectedProbeTimer->setSingleShot(true);
|
m_connectedProbeTimer->setSingleShot(true);
|
||||||
|
|
||||||
@@ -152,6 +154,16 @@ void SshSessionBackend::confirmHostKey(bool trustHost)
|
|||||||
: QStringLiteral("Host key rejected by user."));
|
: 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()
|
void SshSessionBackend::onProcessStarted()
|
||||||
{
|
{
|
||||||
emit eventLogged(QStringLiteral("ssh process started."));
|
emit eventLogged(QStringLiteral("ssh process started."));
|
||||||
@@ -284,6 +296,10 @@ void SshSessionBackend::setState(SessionState state, const QString& message)
|
|||||||
m_state = state;
|
m_state = state;
|
||||||
emit stateChanged(state, message);
|
emit stateChanged(state, message);
|
||||||
emit eventLogged(message);
|
emit eventLogged(message);
|
||||||
|
|
||||||
|
if (m_state == SessionState::Connected) {
|
||||||
|
applyTerminalSizeIfAvailable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
|
bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
|
||||||
@@ -502,3 +518,21 @@ QString SshSessionBackend::knownHostsFileForNullDevice() const
|
|||||||
return QStringLiteral("/dev/null");
|
return QStringLiteral("/dev/null");
|
||||||
#endif
|
#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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public slots:
|
|||||||
void reconnectSession(const SessionConnectOptions& options) override;
|
void reconnectSession(const SessionConnectOptions& options) override;
|
||||||
void sendInput(const QString& input) override;
|
void sendInput(const QString& input) override;
|
||||||
void confirmHostKey(bool trustHost) override;
|
void confirmHostKey(bool trustHost) override;
|
||||||
|
void updateTerminalSize(int columns, int rows) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onProcessStarted();
|
void onProcessStarted();
|
||||||
@@ -43,6 +44,8 @@ private:
|
|||||||
bool m_waitingForPasswordPrompt;
|
bool m_waitingForPasswordPrompt;
|
||||||
bool m_waitingForHostKeyConfirmation;
|
bool m_waitingForHostKeyConfirmation;
|
||||||
bool m_passwordSubmitted;
|
bool m_passwordSubmitted;
|
||||||
|
int m_terminalColumns;
|
||||||
|
int m_terminalRows;
|
||||||
|
|
||||||
void setState(SessionState state, const QString& message);
|
void setState(SessionState state, const QString& message);
|
||||||
bool startSshProcess(const SessionConnectOptions& options);
|
bool startSshProcess(const SessionConnectOptions& options);
|
||||||
@@ -52,6 +55,7 @@ private:
|
|||||||
void cleanupAskPassScript();
|
void cleanupAskPassScript();
|
||||||
QString mapSshError(const QString& rawError) const;
|
QString mapSshError(const QString& rawError) const;
|
||||||
QString knownHostsFileForNullDevice() const;
|
QString knownHostsFileForNullDevice() const;
|
||||||
|
void applyTerminalSizeIfAvailable();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
|
#include <QFocusEvent>
|
||||||
|
#include <QFontMetrics>
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
|
#include <QResizeEvent>
|
||||||
|
#include <QTimer>
|
||||||
#include <QTextCursor>
|
#include <QTextCursor>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -21,13 +25,21 @@ TerminalView::TerminalView(QWidget* parent)
|
|||||||
m_hasFgColor(false),
|
m_hasFgColor(false),
|
||||||
m_hasBgColor(false)
|
m_hasBgColor(false)
|
||||||
{
|
{
|
||||||
setReadOnly(true);
|
setReadOnly(false);
|
||||||
setUndoRedoEnabled(false);
|
setUndoRedoEnabled(false);
|
||||||
document()->setMaximumBlockCount(4000);
|
|
||||||
setAcceptRichText(false);
|
setAcceptRichText(false);
|
||||||
|
setLineWrapMode(QTextEdit::NoWrap);
|
||||||
|
setContextMenuPolicy(Qt::NoContextMenu);
|
||||||
|
setCursorWidth(2);
|
||||||
|
document()->setMaximumBlockCount(4000);
|
||||||
|
|
||||||
applyThemePalette(paletteByName(QStringLiteral("Dark")));
|
applyThemePalette(paletteByName(QStringLiteral("Dark")));
|
||||||
resetSgrState();
|
resetSgrState();
|
||||||
|
|
||||||
|
QTimer::singleShot(0, this, [this]() {
|
||||||
|
moveCursor(QTextCursor::End);
|
||||||
|
emitTerminalSize();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList TerminalView::themeNames()
|
QStringList TerminalView::themeNames()
|
||||||
@@ -123,8 +135,19 @@ void TerminalView::keyPressEvent(QKeyEvent* event)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveCursor(QTextCursor::End);
|
||||||
|
|
||||||
const Qt::KeyboardModifiers modifiers = event->modifiers();
|
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) {
|
if (modifiers == Qt::ControlModifier) {
|
||||||
switch (event->key()) {
|
switch (event->key()) {
|
||||||
case Qt::Key_C:
|
case Qt::Key_C:
|
||||||
@@ -180,8 +203,18 @@ void TerminalView::keyPressEvent(QKeyEvent* event)
|
|||||||
emit inputGenerated(text);
|
emit inputGenerated(text);
|
||||||
return;
|
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)
|
TerminalView::ThemePalette TerminalView::paletteByName(const QString& themeName)
|
||||||
@@ -190,23 +223,23 @@ TerminalView::ThemePalette TerminalView::paletteByName(const QString& themeName)
|
|||||||
|
|
||||||
if (theme == QStringLiteral("light")) {
|
if (theme == QStringLiteral("light")) {
|
||||||
return ThemePalette{QStringLiteral("Light"),
|
return ThemePalette{QStringLiteral("Light"),
|
||||||
QColor(QStringLiteral("#fafafa")),
|
QColor(QStringLiteral("#ececec")),
|
||||||
QColor(QStringLiteral("#202124")),
|
QColor(QStringLiteral("#000000")),
|
||||||
{QColor(QStringLiteral("#000000")),
|
{QColor(QStringLiteral("#000000")),
|
||||||
QColor(QStringLiteral("#a31515")),
|
QColor(QStringLiteral("#aa0000")),
|
||||||
QColor(QStringLiteral("#008000")),
|
QColor(QStringLiteral("#008000")),
|
||||||
QColor(QStringLiteral("#795e26")),
|
QColor(QStringLiteral("#7a5f00")),
|
||||||
QColor(QStringLiteral("#0000ff")),
|
QColor(QStringLiteral("#0033cc")),
|
||||||
QColor(QStringLiteral("#af00db")),
|
QColor(QStringLiteral("#8a00a8")),
|
||||||
QColor(QStringLiteral("#0451a5")),
|
QColor(QStringLiteral("#005f87")),
|
||||||
QColor(QStringLiteral("#666666"))},
|
QColor(QStringLiteral("#333333"))},
|
||||||
{QColor(QStringLiteral("#7f7f7f")),
|
{QColor(QStringLiteral("#5c5c5c")),
|
||||||
QColor(QStringLiteral("#cd3131")),
|
QColor(QStringLiteral("#d30000")),
|
||||||
QColor(QStringLiteral("#14a10e")),
|
QColor(QStringLiteral("#00a000")),
|
||||||
QColor(QStringLiteral("#b5ba00")),
|
QColor(QStringLiteral("#9a7700")),
|
||||||
QColor(QStringLiteral("#0451a5")),
|
QColor(QStringLiteral("#0055ff")),
|
||||||
QColor(QStringLiteral("#bc05bc")),
|
QColor(QStringLiteral("#b300db")),
|
||||||
QColor(QStringLiteral("#0598bc")),
|
QColor(QStringLiteral("#007ea7")),
|
||||||
QColor(QStringLiteral("#111111"))}};
|
QColor(QStringLiteral("#111111"))}};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +371,7 @@ void TerminalView::handleSgrSequence(const QString& params)
|
|||||||
for (int i = 0; i < parts.size(); ++i) {
|
for (int i = 0; i < parts.size(); ++i) {
|
||||||
const QString part = parts.at(i).trimmed();
|
const QString part = parts.at(i).trimmed();
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
int code = part.isEmpty() ? 0 : part.toInt(&ok);
|
const int code = part.isEmpty() ? 0 : part.toInt(&ok);
|
||||||
if (!ok && !part.isEmpty()) {
|
if (!ok && !part.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -399,7 +432,7 @@ void TerminalView::handleSgrSequence(const QString& params)
|
|||||||
if (mode == 5 && i + 2 < parts.size()) {
|
if (mode == 5 && i + 2 < parts.size()) {
|
||||||
const int index = parts.at(i + 2).toInt(&ok);
|
const int index = parts.at(i + 2).toInt(&ok);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
QColor color = colorFrom256Index(index);
|
const QColor color = colorFrom256Index(index);
|
||||||
if (background) {
|
if (background) {
|
||||||
m_bgColor = color;
|
m_bgColor = color;
|
||||||
m_hasBgColor = true;
|
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))
|
return bright ? m_palette.bright.at(static_cast<size_t>(safeIndex))
|
||||||
: m_palette.normal.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());
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
|
|
||||||
class QKeyEvent;
|
class QKeyEvent;
|
||||||
|
class QFocusEvent;
|
||||||
|
class QResizeEvent;
|
||||||
|
|
||||||
class TerminalView : public QTextEdit
|
class TerminalView : public QTextEdit
|
||||||
{
|
{
|
||||||
@@ -20,9 +22,12 @@ public:
|
|||||||
|
|
||||||
signals:
|
signals:
|
||||||
void inputGenerated(const QString& input);
|
void inputGenerated(const QString& input);
|
||||||
|
void terminalSizeChanged(int columns, int rows);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void keyPressEvent(QKeyEvent* event) override;
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
|
void focusInEvent(QFocusEvent* event) override;
|
||||||
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct ThemePalette {
|
struct ThemePalette {
|
||||||
@@ -51,6 +56,9 @@ private:
|
|||||||
void handleSgrSequence(const QString& params);
|
void handleSgrSequence(const QString& params);
|
||||||
void appendTextChunk(const QString& text);
|
void appendTextChunk(const QString& text);
|
||||||
QColor paletteColor(bool background, int index, bool bright) const;
|
QColor paletteColor(bool background, int index, bool bright) const;
|
||||||
|
int terminalColumns() const;
|
||||||
|
int terminalRows() const;
|
||||||
|
void emitTerminalSize();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -33,3 +33,7 @@ void UnsupportedSessionBackend::sendInput(const QString&)
|
|||||||
void UnsupportedSessionBackend::confirmHostKey(bool)
|
void UnsupportedSessionBackend::confirmHostKey(bool)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UnsupportedSessionBackend::updateTerminalSize(int, int)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public slots:
|
|||||||
void reconnectSession(const SessionConnectOptions& options) override;
|
void reconnectSession(const SessionConnectOptions& options) override;
|
||||||
void sendInput(const QString& input) override;
|
void sendInput(const QString& input) override;
|
||||||
void confirmHostKey(bool trustHost) override;
|
void confirmHostKey(bool trustHost) override;
|
||||||
|
void updateTerminalSize(int columns, int rows) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user