Internal
Public Access
Compare commits
3
Commits
v2026.9.16.3
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1506d87719 | ||
|
|
b3cedcfa48 | ||
|
|
0cdf930303 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.21)
|
cmake_minimum_required(VERSION 3.21)
|
||||||
|
|
||||||
project(OrbitHub VERSION 2026.9.16.3 LANGUAGES CXX)
|
project(OrbitHub VERSION 2026.9.16.6 LANGUAGES CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|||||||
@@ -184,6 +184,48 @@ Delivered:
|
|||||||
through the dialog), so importing any SSH/RDP entry without a
|
through the dialog), so importing any SSH/RDP entry without a
|
||||||
username still failed outright until this second check was found and
|
username still failed outright until this second check was found and
|
||||||
removed too
|
removed too
|
||||||
|
- A third, independent username check was still live even after the two
|
||||||
|
above were removed: `SessionTab::validateProfileForConnect()` (run at
|
||||||
|
the very top of `connectSession()`/`reconnectSession()`, before
|
||||||
|
`requestConnectOptions()` ever gets a chance to run its async prompt)
|
||||||
|
had its own hard-fail "SSH/RDP username is required" `QMessageBox`,
|
||||||
|
so a blank-username profile still couldn't connect at all -- it just
|
||||||
|
told the user to go edit the profile instead of ever prompting inline.
|
||||||
|
Removed; connect-time prompting is now the only username gate for
|
||||||
|
SSH/RDP
|
||||||
|
- Even with the three checks above gone, a username entered at the
|
||||||
|
connect-time prompt still never actually reached SSH or RDP
|
||||||
|
authentication: `SshSessionBackend`/`RdpSessionBackend` are constructed
|
||||||
|
with their own `Profile` copy up front (moved to a worker thread) and
|
||||||
|
read `profile().username` directly, which never sees `SessionTab`'s
|
||||||
|
later edit to its own in-memory profile once the user answers the
|
||||||
|
prompt. `SessionConnectOptions` (which already carries `password` the
|
||||||
|
same way) gained a `username` field, populated by `SessionTab` from
|
||||||
|
its profile copy on every connect attempt; both backends now prefer
|
||||||
|
`options.username` over `profile().username` when building the actual
|
||||||
|
connect target/auth call. Covered by a new SSH regression test
|
||||||
|
(`tests/fixtures/fake_ssh.sh`'s `requireuser` host only accepts an
|
||||||
|
exact `prompted-user@requireuser` target, so the test fails unless the
|
||||||
|
option, not the stale profile copy, is actually used) -- RDP has no
|
||||||
|
equivalent fake-server test harness, so that side relies on mirroring
|
||||||
|
the already-tested `m_activeOptions.password` pattern exactly
|
||||||
|
- Issue #22: the inline username/password prompt bar used to just be a
|
||||||
|
plain `QWidget` with `setAutoFillBackground(true)` and no explicit
|
||||||
|
color, which meant it rendered in the same color as everything else
|
||||||
|
around it and was easy to miss -- especially on a tab that wasn't the
|
||||||
|
active one, where there was previously no indication anything needed
|
||||||
|
attention at all. Now uses a solid `QPalette::Highlight` fill with
|
||||||
|
`QPalette::HighlightedText` for the label (the OS theme's own
|
||||||
|
guaranteed-contrasting pair, so it stays correct under both light and
|
||||||
|
dark themes without a hardcoded color) plus a hand-drawn "?" badge
|
||||||
|
(not a themed `QStyle` icon, whose own colors are outside our control
|
||||||
|
and could land close in hue to the bar's background); a background
|
||||||
|
tab showing the prompt gets its title suffixed "(Needs input)" and its
|
||||||
|
tab-bar text colored distinctly from the four connection-state colors.
|
||||||
|
A first pass at the tab color (`#6a1b9a`) was reported unreadable in
|
||||||
|
dark mode -- its perceived luminance was well below the four existing
|
||||||
|
state colors -- and was replaced with `#ab47bc`, tuned to roughly
|
||||||
|
match their visibility
|
||||||
- Robustness fix: an unrecognized `FramebufferUpdate` rectangle encoding
|
- Robustness fix: an unrecognized `FramebufferUpdate` rectangle encoding
|
||||||
used to abort the connection generically; `kAnnouncedEncodings` is now
|
used to abort the connection generically; `kAnnouncedEncodings` is now
|
||||||
the single source of truth for what `SetEncodings` announces and what
|
the single source of truth for what `SetEncodings` announces and what
|
||||||
|
|||||||
@@ -1640,7 +1640,13 @@ void RdpSessionBackend::workerMain()
|
|||||||
const Profile& p = profile();
|
const Profile& p = profile();
|
||||||
|
|
||||||
const QString host = p.host.trimmed();
|
const QString host = p.host.trimmed();
|
||||||
QString username = p.username.trimmed();
|
// m_activeOptions.username carries a value prompted for at connect time
|
||||||
|
// (see SessionTab::requestConnectOptions()) when the saved profile's
|
||||||
|
// own username was blank; profile().username never sees that edit
|
||||||
|
// since this backend's Profile copy was captured at construction time.
|
||||||
|
QString username = m_activeOptions.username.trimmed().isEmpty()
|
||||||
|
? p.username.trimmed()
|
||||||
|
: m_activeOptions.username.trimmed();
|
||||||
QString domain = p.domain.trimmed();
|
QString domain = p.domain.trimmed();
|
||||||
if (domain.isEmpty()) {
|
if (domain.isEmpty()) {
|
||||||
const int domainSeparator = username.indexOf(QLatin1Char('\\'));
|
const int domainSeparator = username.indexOf(QLatin1Char('\\'));
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
class SessionConnectOptions
|
class SessionConnectOptions
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
// Only set when the profile's own username was blank and SessionTab
|
||||||
|
// prompted for one inline at connect time (see issue #21); empty means
|
||||||
|
// "use the backend's own profile().username" as before. SSH/RDP need
|
||||||
|
// this up front, unlike VNC's Apple auth which discovers the need for
|
||||||
|
// one mid-connection via usernameRequested()/provideUsername() instead.
|
||||||
|
QString username;
|
||||||
QString password;
|
QString password;
|
||||||
QString privateKeyPath;
|
QString privateKeyPath;
|
||||||
QString knownHostsPolicy;
|
QString knownHostsPolicy;
|
||||||
|
|||||||
+83
-8
@@ -17,10 +17,14 @@
|
|||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QPixmap>
|
||||||
#include <QPlainTextEdit>
|
#include <QPlainTextEdit>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
|
#include <QColor>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QProcessEnvironment>
|
#include <QProcessEnvironment>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
@@ -59,6 +63,34 @@ TerminalTheme themeForName(const QString& themeName)
|
|||||||
return TerminalTheme::loadKonsoleTheme(
|
return TerminalTheme::loadKonsoleTheme(
|
||||||
QStringLiteral(":/KodoTermThemes/konsole/Breeze.colorscheme"));
|
QStringLiteral(":/KodoTermThemes/konsole/Breeze.colorscheme"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A filled circle with a bold "?", used on the username/password prompt
|
||||||
|
// bar (issue #22). Drawn by hand rather than pulled from a QStyle standard
|
||||||
|
// icon because a themed icon's own internal colors are outside our
|
||||||
|
// control and could end up close in hue to the bar's own background,
|
||||||
|
// undermining the contrast the bar is trying to achieve; painting it
|
||||||
|
// ourselves guarantees fillColor/textColor are exactly the same
|
||||||
|
// guaranteed-contrasting pair used for the rest of the bar.
|
||||||
|
QPixmap questionMarkBadgePixmap(const QColor& fillColor, const QColor& textColor, int diameter)
|
||||||
|
{
|
||||||
|
QPixmap pixmap(diameter, diameter);
|
||||||
|
pixmap.fill(Qt::transparent);
|
||||||
|
|
||||||
|
QPainter painter(&pixmap);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
painter.setPen(Qt::NoPen);
|
||||||
|
painter.setBrush(fillColor);
|
||||||
|
painter.drawEllipse(0, 0, diameter, diameter);
|
||||||
|
|
||||||
|
QFont font = painter.font();
|
||||||
|
font.setBold(true);
|
||||||
|
font.setPixelSize(static_cast<int>(diameter * 0.65));
|
||||||
|
painter.setFont(font);
|
||||||
|
painter.setPen(textColor);
|
||||||
|
painter.drawText(QRect(0, 0, diameter, diameter), Qt::AlignCenter, QStringLiteral("?"));
|
||||||
|
|
||||||
|
return pixmap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionTab::SessionTab(const Profile& profile,
|
SessionTab::SessionTab(const Profile& profile,
|
||||||
@@ -93,10 +125,12 @@ SessionTab::SessionTab(const Profile& profile,
|
|||||||
m_exportEventsButton(nullptr),
|
m_exportEventsButton(nullptr),
|
||||||
m_eventsPanel(nullptr),
|
m_eventsPanel(nullptr),
|
||||||
m_passwordPromptBar(nullptr),
|
m_passwordPromptBar(nullptr),
|
||||||
|
m_passwordPromptIcon(nullptr),
|
||||||
m_passwordPromptLabel(nullptr),
|
m_passwordPromptLabel(nullptr),
|
||||||
m_passwordPromptInput(nullptr),
|
m_passwordPromptInput(nullptr),
|
||||||
m_passwordPromptConnectButton(nullptr),
|
m_passwordPromptConnectButton(nullptr),
|
||||||
m_passwordPromptCancelButton(nullptr),
|
m_passwordPromptCancelButton(nullptr),
|
||||||
|
m_awaitingUserInput(false),
|
||||||
m_eventSeverityFilter(EventSeverity::Info),
|
m_eventSeverityFilter(EventSeverity::Info),
|
||||||
m_eventsPanelExpanded(preferences.eventsPanelExpanded)
|
m_eventsPanelExpanded(preferences.eventsPanelExpanded)
|
||||||
{
|
{
|
||||||
@@ -341,9 +375,17 @@ SessionTab::~SessionTab()
|
|||||||
|
|
||||||
QString SessionTab::tabTitle() const
|
QString SessionTab::tabTitle() const
|
||||||
{
|
{
|
||||||
|
if (m_awaitingUserInput) {
|
||||||
|
return QStringLiteral("%1 (Needs input)").arg(m_profile.name);
|
||||||
|
}
|
||||||
return QStringLiteral("%1 (%2)").arg(m_profile.name, stateSuffix());
|
return QStringLiteral("%1 (%2)").arg(m_profile.name, stateSuffix());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SessionTab::awaitingUserInput() const
|
||||||
|
{
|
||||||
|
return m_awaitingUserInput;
|
||||||
|
}
|
||||||
|
|
||||||
void SessionTab::connectSession()
|
void SessionTab::connectSession()
|
||||||
{
|
{
|
||||||
if (m_state == SessionState::Connecting || m_state == SessionState::Connected) {
|
if (m_state == SessionState::Connecting || m_state == SessionState::Connected) {
|
||||||
@@ -806,21 +848,42 @@ void SessionTab::setupUi()
|
|||||||
|
|
||||||
applyTerminalTheme(m_terminalThemeName);
|
applyTerminalTheme(m_terminalThemeName);
|
||||||
|
|
||||||
|
// Styled distinctly (issue #22: this bar used to blend straight into
|
||||||
|
// the plain window background and was easy to miss, especially on a
|
||||||
|
// tab you weren't actively looking at). A solid QPalette::Highlight
|
||||||
|
// fill with QPalette::HighlightedText for the label is used rather
|
||||||
|
// than a subtle tint -- a faint tint proved to still be easy to miss,
|
||||||
|
// and Highlight/HighlightedText are the OS theme's own guaranteed-
|
||||||
|
// contrasting pair, so this stays readable under both light and dark
|
||||||
|
// themes without hardcoding a color.
|
||||||
auto* passwordPromptLayout = new QHBoxLayout();
|
auto* passwordPromptLayout = new QHBoxLayout();
|
||||||
|
m_passwordPromptIcon = new QLabel(this);
|
||||||
|
const QColor highlight = palette().color(QPalette::Highlight);
|
||||||
|
const QColor highlightedText = palette().color(QPalette::HighlightedText);
|
||||||
|
m_passwordPromptIcon->setPixmap(questionMarkBadgePixmap(highlightedText, highlight, 22));
|
||||||
m_passwordPromptLabel = new QLabel(this);
|
m_passwordPromptLabel = new QLabel(this);
|
||||||
|
m_passwordPromptLabel->setObjectName(QStringLiteral("passwordPromptLabel"));
|
||||||
m_passwordPromptInput = new QLineEdit(this);
|
m_passwordPromptInput = new QLineEdit(this);
|
||||||
m_passwordPromptInput->setEchoMode(QLineEdit::Password);
|
m_passwordPromptInput->setEchoMode(QLineEdit::Password);
|
||||||
m_passwordPromptConnectButton = new QPushButton(QStringLiteral("Connect"), this);
|
m_passwordPromptConnectButton = new QPushButton(QStringLiteral("Connect"), this);
|
||||||
m_passwordPromptCancelButton = new QPushButton(QStringLiteral("Cancel"), this);
|
m_passwordPromptCancelButton = new QPushButton(QStringLiteral("Cancel"), this);
|
||||||
|
passwordPromptLayout->addWidget(m_passwordPromptIcon);
|
||||||
passwordPromptLayout->addWidget(m_passwordPromptLabel);
|
passwordPromptLayout->addWidget(m_passwordPromptLabel);
|
||||||
passwordPromptLayout->addWidget(m_passwordPromptInput, 1);
|
passwordPromptLayout->addWidget(m_passwordPromptInput, 1);
|
||||||
passwordPromptLayout->addWidget(m_passwordPromptConnectButton);
|
passwordPromptLayout->addWidget(m_passwordPromptConnectButton);
|
||||||
passwordPromptLayout->addWidget(m_passwordPromptCancelButton);
|
passwordPromptLayout->addWidget(m_passwordPromptCancelButton);
|
||||||
|
passwordPromptLayout->setContentsMargins(10, 8, 10, 8);
|
||||||
|
|
||||||
m_passwordPromptBar = new QWidget(this);
|
m_passwordPromptBar = new QWidget(this);
|
||||||
|
m_passwordPromptBar->setObjectName(QStringLiteral("passwordPromptBar"));
|
||||||
m_passwordPromptBar->setLayout(passwordPromptLayout);
|
m_passwordPromptBar->setLayout(passwordPromptLayout);
|
||||||
m_passwordPromptBar->setAutoFillBackground(true);
|
m_passwordPromptBar->setAutoFillBackground(true);
|
||||||
m_passwordPromptBar->setVisible(false);
|
m_passwordPromptBar->setVisible(false);
|
||||||
|
m_passwordPromptBar->setStyleSheet(
|
||||||
|
QStringLiteral("QWidget#passwordPromptBar { background-color: %1; }"
|
||||||
|
"QWidget#passwordPromptBar QLabel#passwordPromptLabel "
|
||||||
|
"{ color: %2; font-weight: bold; font-size: 11pt; }")
|
||||||
|
.arg(highlight.name(), highlightedText.name()));
|
||||||
rootLayout->addWidget(m_passwordPromptBar);
|
rootLayout->addWidget(m_passwordPromptBar);
|
||||||
|
|
||||||
connect(m_passwordPromptConnectButton, &QPushButton::clicked, this, [this]() {
|
connect(m_passwordPromptConnectButton, &QPushButton::clicked, this, [this]() {
|
||||||
@@ -998,6 +1061,10 @@ void SessionTab::requestConnectOptions(
|
|||||||
{
|
{
|
||||||
SessionConnectOptions baseOptions;
|
SessionConnectOptions baseOptions;
|
||||||
baseOptions.knownHostsPolicy = m_profile.knownHostsPolicy;
|
baseOptions.knownHostsPolicy = m_profile.knownHostsPolicy;
|
||||||
|
// The backend's own Profile copy was captured when it was constructed
|
||||||
|
// and never sees later edits to m_profile (e.g. the username prompt
|
||||||
|
// below) -- it has to travel through here instead.
|
||||||
|
baseOptions.username = m_profile.username.trimmed();
|
||||||
|
|
||||||
const bool isSsh = m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0;
|
const bool isSsh = m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0;
|
||||||
const bool isRdp = m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0;
|
const bool isRdp = m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0;
|
||||||
@@ -1157,12 +1224,24 @@ void SessionTab::showPasswordPrompt(const QString& labelText,
|
|||||||
m_passwordPromptInput->setEchoMode(maskInput ? QLineEdit::Password : QLineEdit::Normal);
|
m_passwordPromptInput->setEchoMode(maskInput ? QLineEdit::Password : QLineEdit::Normal);
|
||||||
m_passwordPromptBar->setVisible(true);
|
m_passwordPromptBar->setVisible(true);
|
||||||
m_passwordPromptInput->setFocus();
|
m_passwordPromptInput->setFocus();
|
||||||
|
|
||||||
|
if (!m_awaitingUserInput) {
|
||||||
|
m_awaitingUserInput = true;
|
||||||
|
emit awaitingUserInputChanged(true);
|
||||||
|
emit tabTitleChanged(tabTitle());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionTab::hidePasswordPrompt()
|
void SessionTab::hidePasswordPrompt()
|
||||||
{
|
{
|
||||||
m_passwordPromptBar->setVisible(false);
|
m_passwordPromptBar->setVisible(false);
|
||||||
m_passwordPromptCallback = nullptr;
|
m_passwordPromptCallback = nullptr;
|
||||||
|
|
||||||
|
if (m_awaitingUserInput) {
|
||||||
|
m_awaitingUserInput = false;
|
||||||
|
emit awaitingUserInputChanged(false);
|
||||||
|
emit tabTitleChanged(tabTitle());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SessionTab::validateProfileForConnect()
|
bool SessionTab::validateProfileForConnect()
|
||||||
@@ -1181,14 +1260,10 @@ bool SessionTab::validateProfileForConnect()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0
|
// SSH/RDP no longer hard-require a username here -- a blank one is
|
||||||
|| m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0)
|
// handled by requestConnectOptions() prompting for it inline at connect
|
||||||
&& m_profile.username.trimmed().isEmpty()) {
|
// time (see issue #21). Do not re-add a check here without also
|
||||||
QMessageBox::warning(this,
|
// updating that flow.
|
||||||
QStringLiteral("Connect"),
|
|
||||||
QStringLiteral("%1 username is required.").arg(m_profile.protocol));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,10 +67,16 @@ public:
|
|||||||
bool supportsVncScaleToggle() const;
|
bool supportsVncScaleToggle() const;
|
||||||
void setVncScaleToFit(bool scaleToFit);
|
void setVncScaleToFit(bool scaleToFit);
|
||||||
bool vncScaleToFit() const;
|
bool vncScaleToFit() const;
|
||||||
|
bool awaitingUserInput() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void tabTitleChanged(const QString& title);
|
void tabTitleChanged(const QString& title);
|
||||||
void tabStateChanged(SessionState state);
|
void tabStateChanged(SessionState state);
|
||||||
|
// Fires whenever the inline username/password prompt bar is shown or
|
||||||
|
// hidden -- independent of tabStateChanged(), since the backend is
|
||||||
|
// still just "Connecting" while it's up. Lets SessionWindow mark a
|
||||||
|
// background tab that needs the user's attention (issue #22).
|
||||||
|
void awaitingUserInputChanged(bool waiting);
|
||||||
void terminalThemeChanged(const QString& themeName);
|
void terminalThemeChanged(const QString& themeName);
|
||||||
void terminalFontSizeChanged(int pointSize);
|
void terminalFontSizeChanged(int pointSize);
|
||||||
void eventsPanelVisibilityChanged(bool expanded);
|
void eventsPanelVisibilityChanged(bool expanded);
|
||||||
@@ -129,11 +135,13 @@ private:
|
|||||||
QToolButton* m_exportEventsButton;
|
QToolButton* m_exportEventsButton;
|
||||||
QWidget* m_eventsPanel;
|
QWidget* m_eventsPanel;
|
||||||
QWidget* m_passwordPromptBar;
|
QWidget* m_passwordPromptBar;
|
||||||
|
QLabel* m_passwordPromptIcon;
|
||||||
QLabel* m_passwordPromptLabel;
|
QLabel* m_passwordPromptLabel;
|
||||||
QLineEdit* m_passwordPromptInput;
|
QLineEdit* m_passwordPromptInput;
|
||||||
QPushButton* m_passwordPromptConnectButton;
|
QPushButton* m_passwordPromptConnectButton;
|
||||||
QPushButton* m_passwordPromptCancelButton;
|
QPushButton* m_passwordPromptCancelButton;
|
||||||
std::function<void(std::optional<QString>)> m_passwordPromptCallback;
|
std::function<void(std::optional<QString>)> m_passwordPromptCallback;
|
||||||
|
bool m_awaitingUserInput;
|
||||||
enum class EventSeverity {
|
enum class EventSeverity {
|
||||||
Info,
|
Info,
|
||||||
Warning,
|
Warning,
|
||||||
|
|||||||
@@ -34,6 +34,19 @@ QColor tabColorForState(SessionState state, const QPalette& palette)
|
|||||||
return palette.color(QPalette::WindowText);
|
return palette.color(QPalette::WindowText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Distinct from all four tabColorForState() colors -- a tab awaiting a
|
||||||
|
// username/password prompt response needs to stand out even from a
|
||||||
|
// tab that's merely "Connecting" (issue #22), including when it isn't
|
||||||
|
// the one currently in view. #6a1b9a (a much darker violet) was tried
|
||||||
|
// first and reported unreadable against a dark-theme tab bar -- its
|
||||||
|
// perceived luminance is well below the other three colors above, which
|
||||||
|
// this one is tuned to roughly match so it reads about as well as they
|
||||||
|
// do in both light and dark themes.
|
||||||
|
QColor awaitingInputTabColor()
|
||||||
|
{
|
||||||
|
return QColor(QStringLiteral("#ab47bc"));
|
||||||
|
}
|
||||||
|
|
||||||
QStringList terminalThemeNames()
|
QStringList terminalThemeNames()
|
||||||
{
|
{
|
||||||
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
|
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
|
||||||
@@ -57,6 +70,7 @@ SessionWindow::SessionWindow(QWidget* parent)
|
|||||||
QWidget* tab = m_tabs->widget(index);
|
QWidget* tab = m_tabs->widget(index);
|
||||||
if (auto* sessionTab = qobject_cast<SessionTab*>(tab)) {
|
if (auto* sessionTab = qobject_cast<SessionTab*>(tab)) {
|
||||||
sessionTab->disconnectSession();
|
sessionTab->disconnectSession();
|
||||||
|
m_tabStates.remove(sessionTab);
|
||||||
}
|
}
|
||||||
m_tabs->removeTab(index);
|
m_tabs->removeTab(index);
|
||||||
delete tab;
|
delete tab;
|
||||||
@@ -261,6 +275,7 @@ void SessionWindow::addSessionTab(const Profile& profile)
|
|||||||
} else {
|
} else {
|
||||||
setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name));
|
setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name));
|
||||||
}
|
}
|
||||||
|
m_tabStates.insert(tab, SessionState::Disconnected);
|
||||||
m_tabs->tabBar()->setTabTextColor(
|
m_tabs->tabBar()->setTabTextColor(
|
||||||
index, tabColorForState(SessionState::Disconnected, m_tabs->palette()));
|
index, tabColorForState(SessionState::Disconnected, m_tabs->palette()));
|
||||||
|
|
||||||
@@ -272,6 +287,13 @@ void SessionWindow::addSessionTab(const Profile& profile)
|
|||||||
&SessionTab::tabStateChanged,
|
&SessionTab::tabStateChanged,
|
||||||
this,
|
this,
|
||||||
[this, tab](SessionState state) {
|
[this, tab](SessionState state) {
|
||||||
|
m_tabStates.insert(tab, state);
|
||||||
|
if (tab->awaitingUserInput()) {
|
||||||
|
// Keep the "needs input" color on top -- it'll be
|
||||||
|
// restored to reflect this state once the prompt
|
||||||
|
// resolves (see awaitingUserInputChanged below).
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (int i = 0; i < m_tabs->count(); ++i) {
|
for (int i = 0; i < m_tabs->count(); ++i) {
|
||||||
if (m_tabs->widget(i) == tab) {
|
if (m_tabs->widget(i) == tab) {
|
||||||
m_tabs->tabBar()->setTabTextColor(
|
m_tabs->tabBar()->setTabTextColor(
|
||||||
@@ -280,6 +302,22 @@ void SessionWindow::addSessionTab(const Profile& profile)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
connect(tab,
|
||||||
|
&SessionTab::awaitingUserInputChanged,
|
||||||
|
this,
|
||||||
|
[this, tab](bool waiting) {
|
||||||
|
for (int i = 0; i < m_tabs->count(); ++i) {
|
||||||
|
if (m_tabs->widget(i) != tab) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QColor color = waiting
|
||||||
|
? awaitingInputTabColor()
|
||||||
|
: tabColorForState(m_tabStates.value(tab, SessionState::Disconnected),
|
||||||
|
m_tabs->palette());
|
||||||
|
m_tabs->tabBar()->setTabTextColor(i, color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
connect(tab,
|
connect(tab,
|
||||||
&SessionTab::terminalThemeChanged,
|
&SessionTab::terminalThemeChanged,
|
||||||
this,
|
this,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "profile_repository.h"
|
#include "profile_repository.h"
|
||||||
#include "session_tab.h"
|
#include "session_tab.h"
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
|
|
||||||
class QTabWidget;
|
class QTabWidget;
|
||||||
@@ -21,6 +22,10 @@ private:
|
|||||||
QTabWidget* m_tabs;
|
QTabWidget* m_tabs;
|
||||||
ProfilesWindow* m_profilesWidget;
|
ProfilesWindow* m_profilesWidget;
|
||||||
SessionUiPreferences m_preferences;
|
SessionUiPreferences m_preferences;
|
||||||
|
// Last known connection state per tab, so the tab color can be
|
||||||
|
// restored correctly once an awaitingUserInputChanged(false) fires
|
||||||
|
// (that signal is orthogonal to SessionState -- see session_tab.h).
|
||||||
|
QHash<SessionTab*, SessionState> m_tabStates;
|
||||||
|
|
||||||
void addSessionTab(const Profile& profile);
|
void addSessionTab(const Profile& profile);
|
||||||
void updateTabTitle(SessionTab* tab, const QString& title);
|
void updateTabTitle(SessionTab* tab, const QString& title);
|
||||||
|
|||||||
@@ -388,9 +388,16 @@ bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
|
|||||||
<< QStringLiteral("PasswordAuthentication=no");
|
<< QStringLiteral("PasswordAuthentication=no");
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString target = p.username.trimmed().isEmpty()
|
// options.username carries a value prompted for at connect time (see
|
||||||
|
// SessionTab::requestConnectOptions()) when the saved profile's own
|
||||||
|
// username was blank; profile().username never sees that edit since
|
||||||
|
// the backend's Profile copy was captured at construction time.
|
||||||
|
const QString username = options.username.trimmed().isEmpty()
|
||||||
|
? p.username.trimmed()
|
||||||
|
: options.username.trimmed();
|
||||||
|
const QString target = username.isEmpty()
|
||||||
? p.host.trimmed()
|
? p.host.trimmed()
|
||||||
: QStringLiteral("%1@%2").arg(p.username.trimmed(), p.host.trimmed());
|
: QStringLiteral("%1@%2").arg(username, p.host.trimmed());
|
||||||
args << target;
|
args << target;
|
||||||
|
|
||||||
m_process->setProcessEnvironment(environment);
|
m_process->setProcessEnvironment(environment);
|
||||||
|
|||||||
Vendored
+16
@@ -6,6 +6,22 @@
|
|||||||
# host, optionally as user@host, as the final argument).
|
# host, optionally as user@host, as the final argument).
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
|
prompted-user@requireuser)
|
||||||
|
# Only the exact user@host below is accepted -- used to prove a
|
||||||
|
# username supplied via SessionConnectOptions (prompted for at
|
||||||
|
# connect time because the saved profile's own username was
|
||||||
|
# blank) actually reaches the ssh command line, not just that
|
||||||
|
# *some* connection to this host succeeds.
|
||||||
|
echo "Welcome to the fake host."
|
||||||
|
while IFS= read -r line; do
|
||||||
|
echo "$line"
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*@requireuser|requireuser)
|
||||||
|
echo "Permission denied (publickey,password)." >&2
|
||||||
|
exit 255
|
||||||
|
;;
|
||||||
*@succeed|succeed)
|
*@succeed|succeed)
|
||||||
echo "Welcome to the fake host."
|
echo "Welcome to the fake host."
|
||||||
# Stay alive echoing stdin back (simulates an interactive
|
# Stay alive echoing stdin back (simulates an interactive
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ Profile makeProfile(const QString& fixtureHost)
|
|||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Profile makeBlankUsernameProfile(const QString& fixtureHost)
|
||||||
|
{
|
||||||
|
Profile profile = makeProfile(fixtureHost);
|
||||||
|
profile.username.clear();
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
SessionConnectOptions makeOptions()
|
SessionConnectOptions makeOptions()
|
||||||
{
|
{
|
||||||
SessionConnectOptions options;
|
SessionConnectOptions options;
|
||||||
@@ -50,10 +57,12 @@ private slots:
|
|||||||
void connectionRefusedReachesFailedState();
|
void connectionRefusedReachesFailedState();
|
||||||
void sendInputEchoesThroughOutputReceived();
|
void sendInputEchoesThroughOutputReceived();
|
||||||
void reconnectRestartsAndReachesConnectedAgain();
|
void reconnectRestartsAndReachesConnectedAgain();
|
||||||
|
void connectOptionsUsernameReachesProcessWhenProfileUsernameIsBlank();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString fixturePath() const;
|
QString fixturePath() const;
|
||||||
void createBackend(const QString& fixtureHost);
|
void createBackend(const QString& fixtureHost);
|
||||||
|
void createBackend(const Profile& profile);
|
||||||
|
|
||||||
std::unique_ptr<SshSessionBackend> m_backend;
|
std::unique_ptr<SshSessionBackend> m_backend;
|
||||||
SessionState m_lastState = SessionState::Disconnected;
|
SessionState m_lastState = SessionState::Disconnected;
|
||||||
@@ -69,8 +78,12 @@ QString TestSshSessionBackend::fixturePath() const
|
|||||||
|
|
||||||
void TestSshSessionBackend::createBackend(const QString& fixtureHost)
|
void TestSshSessionBackend::createBackend(const QString& fixtureHost)
|
||||||
{
|
{
|
||||||
m_backend =
|
createBackend(makeProfile(fixtureHost));
|
||||||
std::make_unique<SshSessionBackend>(makeProfile(fixtureHost), fixturePath(), nullptr);
|
}
|
||||||
|
|
||||||
|
void TestSshSessionBackend::createBackend(const Profile& profile)
|
||||||
|
{
|
||||||
|
m_backend = std::make_unique<SshSessionBackend>(profile, fixturePath(), nullptr);
|
||||||
connect(m_backend.get(),
|
connect(m_backend.get(),
|
||||||
&SessionBackend::stateChanged,
|
&SessionBackend::stateChanged,
|
||||||
this,
|
this,
|
||||||
@@ -232,5 +245,24 @@ void TestSshSessionBackend::reconnectRestartsAndReachesConnectedAgain()
|
|||||||
QTRY_COMPARE(m_lastState, SessionState::Connected);
|
QTRY_COMPARE(m_lastState, SessionState::Connected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestSshSessionBackend::connectOptionsUsernameReachesProcessWhenProfileUsernameIsBlank()
|
||||||
|
{
|
||||||
|
// Regression test for a bug where a username entered at the
|
||||||
|
// connect-time prompt (SessionTab::requestConnectOptions(), for a
|
||||||
|
// profile with no saved username -- issue #21) never actually reached
|
||||||
|
// the ssh process: SshSessionBackend built its target purely from
|
||||||
|
// profile().username, which is a separate copy captured when the
|
||||||
|
// backend was constructed and never sees SessionTab's later edit.
|
||||||
|
// fixtures/fake_ssh.sh's "requireuser" host only accepts the exact
|
||||||
|
// target "prompted-user@requireuser", so this fails unless
|
||||||
|
// SessionConnectOptions::username is actually used.
|
||||||
|
createBackend(makeBlankUsernameProfile(QStringLiteral("requireuser")));
|
||||||
|
|
||||||
|
SessionConnectOptions options = makeOptions();
|
||||||
|
options.username = QStringLiteral("prompted-user");
|
||||||
|
m_backend->connectSession(options);
|
||||||
|
QTRY_COMPARE(m_lastState, SessionState::Connected);
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestSshSessionBackend)
|
QTEST_GUILESS_MAIN(TestSshSessionBackend)
|
||||||
#include "test_ssh_session_backend.moc"
|
#include "test_ssh_session_backend.moc"
|
||||||
|
|||||||
Reference in New Issue
Block a user