Session UX: inline password prompt and terminal font size controls

Replace the modal RDP/SSH password dialog with an inline prompt bar
embedded in the session tab instead of a separate popup window. Add
per-tab terminal font size controls (increase/decrease/reset/set
exact point size) via the tab context menu, with the chosen size
persisted across sessions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 12:02:03 -06:00
co-authored by Claude Sonnet 5
parent c1c23d115a
commit 27cc3a3bb2
5 changed files with 325 additions and 82 deletions
+213 -48
View File
@@ -13,7 +13,6 @@
#include <QFont>
#include <QFontDatabase>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
@@ -22,6 +21,7 @@
#include <QClipboard>
#include <QComboBox>
#include <QProcessEnvironment>
#include <QPushButton>
#include <QThread>
#include <QTimer>
#include <QToolButton>
@@ -71,6 +71,9 @@ SessionTab::SessionTab(const Profile& profile,
m_terminalThemeName(preferences.terminalThemeName.trimmed().isEmpty()
? QStringLiteral("Dark")
: preferences.terminalThemeName.trimmed()),
m_terminalFontPointSize(preferences.terminalFontPointSize > 0
? preferences.terminalFontPointSize
: 0),
m_sshTerminal(nullptr),
m_rdpDisplay(nullptr),
m_terminalOutput(nullptr),
@@ -81,6 +84,11 @@ SessionTab::SessionTab(const Profile& profile,
m_clearEventsButton(nullptr),
m_exportEventsButton(nullptr),
m_eventsPanel(nullptr),
m_passwordPromptBar(nullptr),
m_passwordPromptLabel(nullptr),
m_passwordPromptInput(nullptr),
m_passwordPromptConnectButton(nullptr),
m_passwordPromptCancelButton(nullptr),
m_eventSeverityFilter(EventSeverity::Info),
m_eventsPanelExpanded(preferences.eventsPanelExpanded)
{
@@ -265,7 +273,7 @@ void SessionTab::connectSession()
return;
}
const std::optional<SessionConnectOptions> options = buildConnectOptions();
requestConnectOptions([this](std::optional<SessionConnectOptions> options) {
if (!options.has_value()) {
return;
}
@@ -273,13 +281,12 @@ void SessionTab::connectSession()
m_lastConnectOptions = options.value();
if (m_useKodoTermForSsh) {
if (!startSshTerminal(options.value())) {
return;
}
startSshTerminal(options.value());
return;
}
emit requestConnect(options.value());
});
}
void SessionTab::disconnectSession()
@@ -305,7 +312,7 @@ void SessionTab::reconnectSession()
return;
}
const std::optional<SessionConnectOptions> options = buildConnectOptions();
requestConnectOptions([this](std::optional<SessionConnectOptions> options) {
if (!options.has_value()) {
return;
}
@@ -323,6 +330,7 @@ void SessionTab::reconnectSession()
}
emit requestReconnect(options.value());
});
}
void SessionTab::clearTerminal()
@@ -380,6 +388,84 @@ bool SessionTab::supportsClearAction() const
return m_useKodoTermForSsh || m_terminalOutput != nullptr;
}
bool SessionTab::supportsZoom() const
{
return m_useKodoTermForSsh || m_terminalOutput != nullptr;
}
void SessionTab::zoomIn()
{
if (m_useKodoTermForSsh && m_sshTerminal != nullptr) {
m_sshTerminal->zoomIn();
m_terminalFontPointSize = m_sshTerminal->getConfig().font.pointSize();
} else if (m_terminalOutput != nullptr) {
m_terminalFontPointSize = m_terminalOutput->font().pointSize() + 1;
m_terminalOutput->setFontPointSize(m_terminalFontPointSize);
} else {
return;
}
emit terminalFontSizeChanged(m_terminalFontPointSize);
}
void SessionTab::zoomOut()
{
if (m_useKodoTermForSsh && m_sshTerminal != nullptr) {
m_sshTerminal->zoomOut();
m_terminalFontPointSize = m_sshTerminal->getConfig().font.pointSize();
} else if (m_terminalOutput != nullptr) {
const int newSize = m_terminalOutput->font().pointSize() - 1;
if (newSize < 6) {
return;
}
m_terminalFontPointSize = newSize;
m_terminalOutput->setFontPointSize(m_terminalFontPointSize);
} else {
return;
}
emit terminalFontSizeChanged(m_terminalFontPointSize);
}
void SessionTab::resetZoom()
{
if (m_useKodoTermForSsh && m_sshTerminal != nullptr) {
m_sshTerminal->resetZoom();
m_terminalFontPointSize = m_sshTerminal->getConfig().font.pointSize();
} else if (m_terminalOutput != nullptr) {
m_terminalFontPointSize = defaultTerminalFont().pointSize();
m_terminalOutput->setFontPointSize(m_terminalFontPointSize);
} else {
return;
}
emit terminalFontSizeChanged(m_terminalFontPointSize);
}
void SessionTab::setTerminalFontPointSize(int pointSize)
{
const int clamped = qBound(6, pointSize, 72);
if (m_useKodoTermForSsh && m_sshTerminal != nullptr) {
KodoTermConfig config = m_sshTerminal->getConfig();
config.font.setPointSize(clamped);
m_sshTerminal->setConfig(config);
m_terminalFontPointSize = clamped;
} else if (m_terminalOutput != nullptr) {
m_terminalFontPointSize = clamped;
m_terminalOutput->setFontPointSize(clamped);
} else {
return;
}
emit terminalFontSizeChanged(m_terminalFontPointSize);
}
int SessionTab::terminalFontPointSize() const
{
return m_terminalFontPointSize;
}
bool SessionTab::isEventsPanelExpanded() const
{
return m_eventsPanelExpanded;
@@ -535,7 +621,10 @@ void SessionTab::setupUi()
if (m_useKodoTermForSsh) {
m_sshTerminal = new KodoTerm(this);
const QFont terminalFont = defaultTerminalFont();
QFont terminalFont = defaultTerminalFont();
if (m_terminalFontPointSize > 0) {
terminalFont.setPointSize(m_terminalFontPointSize);
}
KodoTermConfig config = m_sshTerminal->getConfig();
config.font = terminalFont;
@@ -548,7 +637,11 @@ void SessionTab::setupUi()
rootLayout->addWidget(m_rdpDisplay, 1);
} else {
m_terminalOutput = new TerminalView(this);
m_terminalOutput->setFont(defaultTerminalFont());
QFont fallbackFont = defaultTerminalFont();
if (m_terminalFontPointSize > 0) {
fallbackFont.setPointSize(m_terminalFontPointSize);
}
m_terminalOutput->setFont(fallbackFont);
m_terminalOutput->setMinimumHeight(260);
m_terminalOutput->setReadOnly(true);
if (m_profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
@@ -563,6 +656,45 @@ void SessionTab::setupUi()
applyTerminalTheme(m_terminalThemeName);
auto* passwordPromptLayout = new QHBoxLayout();
m_passwordPromptLabel = new QLabel(this);
m_passwordPromptInput = new QLineEdit(this);
m_passwordPromptInput->setEchoMode(QLineEdit::Password);
m_passwordPromptConnectButton = new QPushButton(QStringLiteral("Connect"), this);
m_passwordPromptCancelButton = new QPushButton(QStringLiteral("Cancel"), this);
passwordPromptLayout->addWidget(m_passwordPromptLabel);
passwordPromptLayout->addWidget(m_passwordPromptInput, 1);
passwordPromptLayout->addWidget(m_passwordPromptConnectButton);
passwordPromptLayout->addWidget(m_passwordPromptCancelButton);
m_passwordPromptBar = new QWidget(this);
m_passwordPromptBar->setLayout(passwordPromptLayout);
m_passwordPromptBar->setAutoFillBackground(true);
m_passwordPromptBar->setVisible(false);
rootLayout->addWidget(m_passwordPromptBar);
connect(m_passwordPromptConnectButton, &QPushButton::clicked, this, [this]() {
if (!m_passwordPromptCallback) {
return;
}
const QString password = m_passwordPromptInput->text();
const auto callback = m_passwordPromptCallback;
hidePasswordPrompt();
callback(password);
});
connect(m_passwordPromptCancelButton, &QPushButton::clicked, this, [this]() {
if (!m_passwordPromptCallback) {
return;
}
const auto callback = m_passwordPromptCallback;
hidePasswordPrompt();
callback(std::nullopt);
});
connect(m_passwordPromptInput,
&QLineEdit::returnPressed,
m_passwordPromptConnectButton,
&QPushButton::click);
auto* eventsHeader = new QHBoxLayout();
m_toggleEventsButton = new QToolButton(this);
m_toggleEventsButton->setCheckable(true);
@@ -676,77 +808,85 @@ void SessionTab::setupUi()
}
}
std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
void SessionTab::requestConnectOptions(
std::function<void(std::optional<SessionConnectOptions>)> callback)
{
SessionConnectOptions options;
options.knownHostsPolicy = m_profile.knownHostsPolicy;
SessionConnectOptions baseOptions;
baseOptions.knownHostsPolicy = m_profile.knownHostsPolicy;
const bool isSsh = m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0;
const bool isRdp = m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0;
if (!isSsh && !isRdp) {
return options;
callback(baseOptions);
return;
}
if (isRdp) {
if (m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) != 0) {
return options;
callback(baseOptions);
return;
}
bool accepted = false;
const QString password = QInputDialog::getText(
this,
QStringLiteral("RDP Password"),
QStringLiteral("Password for %1:")
const QString label = QStringLiteral("RDP password for %1:")
.arg(m_profile.username.trimmed().isEmpty()
? m_profile.host
: QStringLiteral("%1@%2").arg(m_profile.username, m_profile.host)),
QLineEdit::Password,
QString(),
&accepted);
if (!accepted) {
return std::nullopt;
: QStringLiteral("%1@%2").arg(m_profile.username, m_profile.host));
showPasswordPrompt(
label,
[this, baseOptions, callback](std::optional<QString> password) {
if (!password.has_value()) {
callback(std::nullopt);
return;
}
if (password.isEmpty()) {
QMessageBox::warning(this,
if (password->isEmpty()) {
QMessageBox::warning(
this,
QStringLiteral("Connect"),
QStringLiteral("Password is required for password authentication."));
return std::nullopt;
callback(std::nullopt);
return;
}
options.password = password;
return options;
SessionConnectOptions options = baseOptions;
options.password = password.value();
callback(options);
});
return;
}
if (m_useKodoTermForSsh
&& m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) {
// Password is entered directly in terminal prompt.
return options;
callback(baseOptions);
return;
}
if (m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) {
bool accepted = false;
const QString password = QInputDialog::getText(this,
QStringLiteral("SSH Password"),
QStringLiteral("Password for %1@%2:")
.arg(m_profile.username, m_profile.host),
QLineEdit::Password,
QString(),
&accepted);
if (!accepted) {
return std::nullopt;
showPasswordPrompt(
QStringLiteral("SSH password for %1@%2:").arg(m_profile.username, m_profile.host),
[this, baseOptions, callback](std::optional<QString> password) {
if (!password.has_value()) {
callback(std::nullopt);
return;
}
if (password.isEmpty()) {
QMessageBox::warning(this,
if (password->isEmpty()) {
QMessageBox::warning(
this,
QStringLiteral("Connect"),
QStringLiteral("Password is required for password authentication."));
return std::nullopt;
callback(std::nullopt);
return;
}
options.password = password;
return options;
SessionConnectOptions options = baseOptions;
options.password = password.value();
callback(options);
});
return;
}
QString keyPath = m_profile.privateKeyPath.trimmed();
@@ -756,7 +896,8 @@ std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
QString(),
QStringLiteral("All Files (*)"));
if (keyPath.isEmpty()) {
return std::nullopt;
callback(std::nullopt);
return;
}
}
@@ -764,11 +905,35 @@ std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("Private key file not found: %1").arg(keyPath));
return std::nullopt;
callback(std::nullopt);
return;
}
SessionConnectOptions options = baseOptions;
options.privateKeyPath = keyPath;
return options;
callback(options);
}
void SessionTab::showPasswordPrompt(const QString& labelText,
std::function<void(std::optional<QString>)> callback)
{
if (m_passwordPromptCallback) {
const auto previousCallback = m_passwordPromptCallback;
m_passwordPromptCallback = nullptr;
previousCallback(std::nullopt);
}
m_passwordPromptCallback = std::move(callback);
m_passwordPromptLabel->setText(labelText);
m_passwordPromptInput->clear();
m_passwordPromptBar->setVisible(true);
m_passwordPromptInput->setFocus();
}
void SessionTab::hidePasswordPrompt()
{
m_passwordPromptBar->setVisible(false);
m_passwordPromptCallback = nullptr;
}
bool SessionTab::validateProfileForConnect()
+22 -1
View File
@@ -8,6 +8,7 @@
#include <QStringList>
#include <QtGlobal>
#include <functional>
#include <optional>
#include <vector>
@@ -19,12 +20,15 @@ class RdpDisplayWidget;
class QToolButton;
class QLineEdit;
class QComboBox;
class QLabel;
class QPushButton;
class KodoTerm;
struct SessionUiPreferences
{
QString terminalThemeName = QStringLiteral("Dark");
bool eventsPanelExpanded = false;
int terminalFontPointSize = 0;
};
class SessionTab : public QWidget
@@ -46,6 +50,12 @@ public:
QString terminalThemeName() const;
bool supportsThemeSelection() const;
bool supportsClearAction() const;
bool supportsZoom() const;
void zoomIn();
void zoomOut();
void resetZoom();
void setTerminalFontPointSize(int pointSize);
int terminalFontPointSize() const;
bool isEventsPanelExpanded() const;
void setEventsPanelExpanded(bool expanded);
void clearEvents();
@@ -56,6 +66,7 @@ signals:
void tabTitleChanged(const QString& title);
void tabStateChanged(SessionState state);
void terminalThemeChanged(const QString& themeName);
void terminalFontSizeChanged(int pointSize);
void eventsPanelVisibilityChanged(bool expanded);
void requestConnect(const SessionConnectOptions& options);
void requestDisconnect();
@@ -88,6 +99,7 @@ private:
QString m_lastError;
SessionConnectOptions m_lastConnectOptions;
QString m_terminalThemeName;
int m_terminalFontPointSize;
KodoTerm* m_sshTerminal;
RdpDisplayWidget* m_rdpDisplay;
@@ -99,6 +111,12 @@ private:
QToolButton* m_clearEventsButton;
QToolButton* m_exportEventsButton;
QWidget* m_eventsPanel;
QWidget* m_passwordPromptBar;
QLabel* m_passwordPromptLabel;
QLineEdit* m_passwordPromptInput;
QPushButton* m_passwordPromptConnectButton;
QPushButton* m_passwordPromptCancelButton;
std::function<void(std::optional<QString>)> m_passwordPromptCallback;
enum class EventSeverity {
Info,
Warning,
@@ -114,7 +132,10 @@ private:
bool m_eventsPanelExpanded;
void setupUi();
std::optional<SessionConnectOptions> buildConnectOptions();
void requestConnectOptions(std::function<void(std::optional<SessionConnectOptions>)> callback);
void showPasswordPrompt(const QString& labelText,
std::function<void(std::optional<QString>)> callback);
void hidePasswordPrompt();
bool validateProfileForConnect();
void appendEvent(const QString& message);
void setState(SessionState state, const QString& message);
+48
View File
@@ -6,6 +6,7 @@
#include <QAction>
#include <QColor>
#include <QInputDialog>
#include <QMenu>
#include <QMenuBar>
#include <QPalette>
@@ -105,6 +106,18 @@ SessionWindow::SessionWindow(const Profile& profile, QWidget* parent)
clearAction = menu.addAction(QStringLiteral("Clear"));
}
QAction* zoomInAction = nullptr;
QAction* zoomOutAction = nullptr;
QAction* resetZoomAction = nullptr;
QAction* setFontSizeAction = nullptr;
if (tab->supportsZoom()) {
menu.addSeparator();
zoomInAction = menu.addAction(QStringLiteral("Increase Font Size"));
zoomOutAction = menu.addAction(QStringLiteral("Decrease Font Size"));
resetZoomAction = menu.addAction(QStringLiteral("Reset Font Size"));
setFontSizeAction = menu.addAction(QStringLiteral("Set Font Size..."));
}
QAction* chosen = menu.exec(m_tabs->tabBar()->mapToGlobal(pos));
if (chosen == disconnectAction) {
tab->disconnectSession();
@@ -120,6 +133,27 @@ SessionWindow::SessionWindow(const Profile& profile, QWidget* parent)
tab->clearEvents();
} else if (clearAction != nullptr && chosen == clearAction) {
tab->clearTerminal();
} else if (zoomInAction != nullptr && chosen == zoomInAction) {
tab->zoomIn();
} else if (zoomOutAction != nullptr && chosen == zoomOutAction) {
tab->zoomOut();
} else if (resetZoomAction != nullptr && chosen == resetZoomAction) {
tab->resetZoom();
} else if (setFontSizeAction != nullptr && chosen == setFontSizeAction) {
bool accepted = false;
const int currentSize =
tab->terminalFontPointSize() > 0 ? tab->terminalFontPointSize() : 10;
const int newSize = QInputDialog::getInt(this,
QStringLiteral("Set Font Size"),
QStringLiteral("Font size (points):"),
currentSize,
6,
72,
1,
&accepted);
if (accepted) {
tab->setTerminalFontPointSize(newSize);
}
} else {
for (QAction* themeAction : themeActions) {
if (chosen == themeAction) {
@@ -185,6 +219,16 @@ void SessionWindow::addSessionTab(const Profile& profile)
: themeName.trimmed();
saveUiPreferences();
});
connect(tab,
&SessionTab::terminalFontSizeChanged,
this,
[this](int pointSize) {
if (pointSize <= 0) {
return;
}
m_preferences.terminalFontPointSize = pointSize;
saveUiPreferences();
});
connect(tab,
&SessionTab::eventsPanelVisibilityChanged,
this,
@@ -216,6 +260,8 @@ void SessionWindow::loadUiPreferences()
}
m_preferences.eventsPanelExpanded =
settings.value(QStringLiteral("session/eventsPanelExpanded"), false).toBool();
m_preferences.terminalFontPointSize =
settings.value(QStringLiteral("session/terminalFontPointSize"), 0).toInt();
}
void SessionWindow::saveUiPreferences() const
@@ -225,4 +271,6 @@ void SessionWindow::saveUiPreferences() const
m_preferences.terminalThemeName);
settings.setValue(QStringLiteral("session/eventsPanelExpanded"),
m_preferences.eventsPanelExpanded);
settings.setValue(QStringLiteral("session/terminalFontPointSize"),
m_preferences.terminalFontPointSize);
}
+8
View File
@@ -52,6 +52,14 @@ void TerminalView::setThemeName(const QString& themeName)
applyThemePalette(paletteByName(themeName));
}
void TerminalView::setFontPointSize(int pointSize)
{
QFont updatedFont = font();
updatedFont.setPointSize(pointSize);
setFont(updatedFont);
emitTerminalSize();
}
void TerminalView::appendTerminalData(const QString& data)
{
if (data.isEmpty()) {
+1
View File
@@ -19,6 +19,7 @@ public:
static QStringList themeNames();
void setThemeName(const QString& themeName);
void appendTerminalData(const QString& data);
void setFontPointSize(int pointSize);
signals:
void inputGenerated(const QString& input);