Initial implementation of KRDC SSH plugin
Adds an SSH terminal plugin for KRDC that embeds KonsolePart (Konsole's KPart terminal emulator) inside a RemoteView subclass. The plugin registers the ssh:// URL scheme and launches ssh via the TerminalInterface::startProgram() API. Components: - SshViewFactory: plugin factory handling ssh:// URLs - SshView: RemoteView subclass embedding KonsolePart - SshHostPreferences: configuration for username, identity file, X11/agent forwarding, compression, and port forwarding tunnels Key implementation details: - KonsolePart loaded via KPluginFactory from kf6/parts/konsolepart - ssh binary resolved via QStandardPaths::findExecutable() - scaleResize() unconditionally calls resize() (KRDC's scroll area does not use setWidgetResizable, relies on this slot) - deleteLater() for KonsolePart teardown to avoid use-after-free Build: standalone CMake project linking against krdccore and KF6::Parts.
This commit is contained in:
commit
ac239c1943
|
|
@ -0,0 +1 @@
|
||||||
|
build/
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
project(krdc-ssh VERSION 0.1.0)
|
||||||
|
|
||||||
|
set(QT_MIN_VERSION "6.6.0")
|
||||||
|
set(KF_MIN_VERSION "6.0.0")
|
||||||
|
|
||||||
|
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
|
||||||
|
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||||
|
|
||||||
|
include(KDEInstallDirs)
|
||||||
|
include(KDECMakeSettings)
|
||||||
|
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||||
|
include(ECMQtDeclareLoggingCategory)
|
||||||
|
include(FeatureSummary)
|
||||||
|
|
||||||
|
find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Gui Widgets)
|
||||||
|
|
||||||
|
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS
|
||||||
|
Config
|
||||||
|
CoreAddons
|
||||||
|
I18n
|
||||||
|
KIO
|
||||||
|
Parts
|
||||||
|
WidgetsAddons
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find krdccore headers and library
|
||||||
|
find_path(KRDCCORE_INCLUDE_DIR remoteviewfactory.h
|
||||||
|
PATH_SUFFIXES krdc
|
||||||
|
)
|
||||||
|
find_library(KRDCCORE_LIBRARY NAMES krdccore)
|
||||||
|
|
||||||
|
if(NOT KRDCCORE_INCLUDE_DIR OR NOT KRDCCORE_LIBRARY)
|
||||||
|
message(FATAL_ERROR "krdccore not found. Please install KRDC development files.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message(STATUS "Found krdccore: ${KRDCCORE_LIBRARY}")
|
||||||
|
message(STATUS "Found krdccore headers: ${KRDCCORE_INCLUDE_DIR}")
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
|
||||||
|
add_definitions(
|
||||||
|
-DQT_USE_QSTRINGBUILDER
|
||||||
|
-DQT_NO_CAST_TO_ASCII
|
||||||
|
-DQT_NO_CAST_FROM_ASCII
|
||||||
|
-DQT_STRICT_ITERATORS
|
||||||
|
-DQT_NO_URL_CAST_FROM_STRING
|
||||||
|
-DQT_NO_CAST_FROM_BYTEARRAY
|
||||||
|
-DQT_NO_KEYWORDS
|
||||||
|
-DQT_NO_FOREACH
|
||||||
|
)
|
||||||
|
|
||||||
|
add_subdirectory(src)
|
||||||
|
|
||||||
|
feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# KRDC SSH Plugin
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
KRDC plugin that provides SSH terminal sessions. Embeds KonsolePart (KDE's terminal
|
||||||
|
KPart) inside a RemoteView subclass. KonsolePart runs `ssh user@host -p port` via
|
||||||
|
startProgram() — handles terminal emulation, rendering, and input natively.
|
||||||
|
|
||||||
|
## Key Discovery
|
||||||
|
|
||||||
|
KRDC's RemoteViewScrollArea does NOT use setWidgetResizable(true). Instead it emits
|
||||||
|
`resized(w, h)` connected to `scaleResize()`. The view MUST call `resize(w, h)` on
|
||||||
|
itself in scaleResize to get a non-zero size. The `m_connected` guard in resizeEvent
|
||||||
|
prevents the re-entrant crash during tab insertion show sequence.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [x] Core plugin implementation
|
||||||
|
- [x] Configuration UI (username, identity, tunneling, options)
|
||||||
|
- [x] Fix crash: scaleResize during show (m_connected guard in resizeEvent)
|
||||||
|
- [x] Fix crash: disconnect (deleteLater instead of delete)
|
||||||
|
- [x] Fix blue screen: startProgram needs full path (QStandardPaths)
|
||||||
|
- [x] Fix blue screen: scaleResize isVisible() guard was blocking initial resize
|
||||||
|
- [x] Reverted to startProgram (not showShellInDir) for proper lifecycle
|
||||||
|
- [ ] Verify: no crash, no blue screen, ssh works on first connection
|
||||||
|
- [ ] Clean up dead code / unnecessary deferred resize in resizeEvent
|
||||||
|
- [ ] Squash/tidy commits if desired
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
add_library(krdc_sshplugin MODULE)
|
||||||
|
|
||||||
|
target_sources(krdc_sshplugin PRIVATE
|
||||||
|
sshview.cpp
|
||||||
|
sshviewfactory.cpp
|
||||||
|
sshhostpreferences.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
ki18n_wrap_ui(krdc_sshplugin
|
||||||
|
sshpreferences.ui
|
||||||
|
)
|
||||||
|
|
||||||
|
ecm_qt_declare_logging_category(krdc_sshplugin
|
||||||
|
HEADER krdc_ssh_debug.h
|
||||||
|
IDENTIFIER KRDC_SSH
|
||||||
|
CATEGORY_NAME KRDC.SSH
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(krdc_sshplugin PRIVATE
|
||||||
|
${KRDCCORE_INCLUDE_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(krdc_sshplugin
|
||||||
|
Qt::Core
|
||||||
|
Qt::Gui
|
||||||
|
Qt::Widgets
|
||||||
|
KF6::ConfigCore
|
||||||
|
KF6::CoreAddons
|
||||||
|
KF6::I18n
|
||||||
|
KF6::KIOWidgets
|
||||||
|
KF6::Parts
|
||||||
|
KF6::WidgetsAddons
|
||||||
|
${KRDCCORE_LIBRARY}
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS krdc_sshplugin DESTINATION ${KDE_INSTALL_PLUGINDIR}/krdc)
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Name": "KRDC SSH Plugin Authors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Category": "Service",
|
||||||
|
"Description": "Allows managing SSH terminal sessions through KRDC",
|
||||||
|
"EnabledByDefault": true,
|
||||||
|
"Icon": "utilities-terminal",
|
||||||
|
"License": "GPL",
|
||||||
|
"Name": "SSH"
|
||||||
|
},
|
||||||
|
"X-KDE-KRDC-Sorting": 30
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2024 KRDC SSH Plugin Authors
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "sshhostpreferences.h"
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
static const char username_config_key[] = "username";
|
||||||
|
static const char identity_file_config_key[] = "identityFile";
|
||||||
|
static const char x11_forwarding_config_key[] = "x11Forwarding";
|
||||||
|
static const char agent_forwarding_config_key[] = "agentForwarding";
|
||||||
|
static const char compression_config_key[] = "compression";
|
||||||
|
static const char tunnels_config_key[] = "tunnels";
|
||||||
|
|
||||||
|
SshHostPreferences::SshHostPreferences(KConfigGroup configGroup, QObject *parent)
|
||||||
|
: HostPreferences(configGroup, parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
SshHostPreferences::~SshHostPreferences()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget *SshHostPreferences::createProtocolSpecificConfigPage(QWidget *sshTunnelWidget)
|
||||||
|
{
|
||||||
|
Q_UNUSED(sshTunnelWidget);
|
||||||
|
|
||||||
|
auto *page = new QWidget();
|
||||||
|
sshUi.setupUi(page);
|
||||||
|
|
||||||
|
// Populate from config
|
||||||
|
sshUi.usernameEdit->setText(userName());
|
||||||
|
sshUi.identityFileEdit->setText(identityFile());
|
||||||
|
sshUi.x11ForwardingCheck->setChecked(x11Forwarding());
|
||||||
|
sshUi.agentForwardingCheck->setChecked(agentForwarding());
|
||||||
|
sshUi.compressionCheck->setChecked(compression());
|
||||||
|
|
||||||
|
// Populate tunnels list
|
||||||
|
const QStringList existingTunnels = tunnels();
|
||||||
|
for (const QString &tunnel : existingTunnels) {
|
||||||
|
sshUi.tunnelList->addItem(tunnel);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(sshUi.addTunnelButton, &QPushButton::clicked, this, &SshHostPreferences::addTunnel);
|
||||||
|
connect(sshUi.removeTunnelButton, &QPushButton::clicked, this, &SshHostPreferences::removeTunnel);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshHostPreferences::acceptConfig()
|
||||||
|
{
|
||||||
|
HostPreferences::acceptConfig();
|
||||||
|
|
||||||
|
setUserName(sshUi.usernameEdit->text().trimmed());
|
||||||
|
setIdentityFile(sshUi.identityFileEdit->text().trimmed());
|
||||||
|
setX11Forwarding(sshUi.x11ForwardingCheck->isChecked());
|
||||||
|
setAgentForwarding(sshUi.agentForwardingCheck->isChecked());
|
||||||
|
setCompression(sshUi.compressionCheck->isChecked());
|
||||||
|
|
||||||
|
// Collect tunnels from list widget
|
||||||
|
QStringList tunnelList;
|
||||||
|
for (int i = 0; i < sshUi.tunnelList->count(); ++i) {
|
||||||
|
tunnelList << sshUi.tunnelList->item(i)->text();
|
||||||
|
}
|
||||||
|
setTunnels(tunnelList);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshHostPreferences::addTunnel()
|
||||||
|
{
|
||||||
|
const QString spec = sshUi.tunnelSpecEdit->text().trimmed();
|
||||||
|
if (spec.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString flag;
|
||||||
|
switch (sshUi.tunnelTypeCombo->currentIndex()) {
|
||||||
|
case 0:
|
||||||
|
flag = QStringLiteral("-L");
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
flag = QStringLiteral("-R");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
flag = QStringLiteral("-D");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString entry = flag + QLatin1Char(' ') + spec;
|
||||||
|
sshUi.tunnelList->addItem(entry);
|
||||||
|
sshUi.tunnelSpecEdit->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshHostPreferences::removeTunnel()
|
||||||
|
{
|
||||||
|
const auto selected = sshUi.tunnelList->selectedItems();
|
||||||
|
for (auto *item : selected) {
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Config accessors ---
|
||||||
|
|
||||||
|
QString SshHostPreferences::userName() const
|
||||||
|
{
|
||||||
|
return m_configGroup.readEntry(username_config_key, QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshHostPreferences::setUserName(const QString &userName)
|
||||||
|
{
|
||||||
|
m_configGroup.writeEntry(username_config_key, userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SshHostPreferences::identityFile() const
|
||||||
|
{
|
||||||
|
return m_configGroup.readEntry(identity_file_config_key, QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshHostPreferences::setIdentityFile(const QString &path)
|
||||||
|
{
|
||||||
|
m_configGroup.writeEntry(identity_file_config_key, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SshHostPreferences::x11Forwarding() const
|
||||||
|
{
|
||||||
|
return m_configGroup.readEntry(x11_forwarding_config_key, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshHostPreferences::setX11Forwarding(bool enabled)
|
||||||
|
{
|
||||||
|
m_configGroup.writeEntry(x11_forwarding_config_key, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SshHostPreferences::agentForwarding() const
|
||||||
|
{
|
||||||
|
return m_configGroup.readEntry(agent_forwarding_config_key, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshHostPreferences::setAgentForwarding(bool enabled)
|
||||||
|
{
|
||||||
|
m_configGroup.writeEntry(agent_forwarding_config_key, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SshHostPreferences::compression() const
|
||||||
|
{
|
||||||
|
return m_configGroup.readEntry(compression_config_key, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshHostPreferences::setCompression(bool enabled)
|
||||||
|
{
|
||||||
|
m_configGroup.writeEntry(compression_config_key, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList SshHostPreferences::tunnels() const
|
||||||
|
{
|
||||||
|
return m_configGroup.readEntry(tunnels_config_key, QStringList());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshHostPreferences::setTunnels(const QStringList &tunnels)
|
||||||
|
{
|
||||||
|
m_configGroup.writeEntry(tunnels_config_key, tunnels);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2024 KRDC SSH Plugin Authors
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SSHHOSTPREFERENCES_H
|
||||||
|
#define SSHHOSTPREFERENCES_H
|
||||||
|
|
||||||
|
#include "hostpreferences.h"
|
||||||
|
#include "ui_sshpreferences.h"
|
||||||
|
|
||||||
|
class SshHostPreferences : public HostPreferences
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SshHostPreferences(KConfigGroup configGroup, QObject *parent = nullptr);
|
||||||
|
~SshHostPreferences() override;
|
||||||
|
|
||||||
|
QString userName() const;
|
||||||
|
void setUserName(const QString &userName);
|
||||||
|
|
||||||
|
QString identityFile() const;
|
||||||
|
void setIdentityFile(const QString &path);
|
||||||
|
|
||||||
|
bool x11Forwarding() const;
|
||||||
|
void setX11Forwarding(bool enabled);
|
||||||
|
|
||||||
|
bool agentForwarding() const;
|
||||||
|
void setAgentForwarding(bool enabled);
|
||||||
|
|
||||||
|
bool compression() const;
|
||||||
|
void setCompression(bool enabled);
|
||||||
|
|
||||||
|
QStringList tunnels() const;
|
||||||
|
void setTunnels(const QStringList &tunnels);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QWidget *createProtocolSpecificConfigPage(QWidget *sshTunnelWidget) override;
|
||||||
|
void acceptConfig() override;
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void addTunnel();
|
||||||
|
void removeTunnel();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ui::SshPreferences sshUi;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SSHHOSTPREFERENCES_H
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>SshPreferences</class>
|
||||||
|
<widget class="QWidget" name="SshPreferences">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>480</width>
|
||||||
|
<height>500</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="connectionGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Connection</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QFormLayout" name="connectionFormLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="usernameLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Username:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>usernameEdit</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QLineEdit" name="usernameEdit">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>Leave empty to use current user</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="identityFileLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Identity file:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>identityFileEdit</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="KUrlRequester" name="identityFileEdit">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>Default (~/.ssh/id_*)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="optionsGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Options</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="optionsLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="x11ForwardingCheck">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable &X11 forwarding (-X)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="agentForwardingCheck">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable &agent forwarding (-A)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="compressionCheck">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable &compression (-C)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="tunnelingGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Port Forwarding</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="tunnelingLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="tunnelTypeLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="tunnelTypeCombo">
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Local (-L)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Remote (-R)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Dynamic/SOCKS (-D)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="tunnelSpecEdit">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>e.g. 8080:localhost:80</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="addTunnelButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Add</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QListWidget" name="tunnelList">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>16777215</width>
|
||||||
|
<height>120</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="removeTunnelButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Remove Selected</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>10</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>KUrlRequester</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>kurlrequester.h</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
|
|
@ -0,0 +1,277 @@
|
||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2024 KRDC SSH Plugin Authors
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "sshview.h"
|
||||||
|
#include "krdc_ssh_debug.h"
|
||||||
|
#include "sshhostpreferences.h"
|
||||||
|
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QMimeData>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
#include <KPluginFactory>
|
||||||
|
#include <KPluginMetaData>
|
||||||
|
#include <kde_terminal_interface.h>
|
||||||
|
|
||||||
|
SshView::SshView(QWidget *parent, const QUrl &url, KConfigGroup configGroup)
|
||||||
|
: RemoteView(parent)
|
||||||
|
, m_framebufferSize(800, 600)
|
||||||
|
{
|
||||||
|
m_url = url;
|
||||||
|
m_host = url.host();
|
||||||
|
m_port = url.port();
|
||||||
|
|
||||||
|
if (m_port <= 0) {
|
||||||
|
m_port = 22;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hostPreferences = new SshHostPreferences(configGroup, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
SshView::~SshView()
|
||||||
|
{
|
||||||
|
if (!m_quitting) {
|
||||||
|
startQuitting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SshView::loadKonsolePart()
|
||||||
|
{
|
||||||
|
const auto result = KPluginFactory::instantiatePlugin<KParts::ReadOnlyPart>(
|
||||||
|
KPluginMetaData(QStringLiteral("kf6/parts/konsolepart")));
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
qCCritical(KRDC_SSH) << "Failed to load KonsolePart:" << result.errorString;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_terminalPart = result.plugin;
|
||||||
|
m_terminalPart->setParent(this);
|
||||||
|
|
||||||
|
// Embed the terminal widget into our layout
|
||||||
|
auto *layout = new QVBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(0);
|
||||||
|
layout->addWidget(m_terminalPart->widget());
|
||||||
|
m_terminalPart->widget()->setFocusPolicy(Qt::WheelFocus);
|
||||||
|
setFocusProxy(m_terminalPart->widget());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList SshView::buildSshCommand() const
|
||||||
|
{
|
||||||
|
QStringList args;
|
||||||
|
args << QStringLiteral("ssh");
|
||||||
|
|
||||||
|
// Add port if non-default
|
||||||
|
if (m_port != 22) {
|
||||||
|
args << QStringLiteral("-p") << QString::number(m_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identity file
|
||||||
|
const QString identityFile = m_hostPreferences->identityFile();
|
||||||
|
if (!identityFile.isEmpty()) {
|
||||||
|
args << QStringLiteral("-i") << identityFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options
|
||||||
|
if (m_hostPreferences->x11Forwarding()) {
|
||||||
|
args << QStringLiteral("-X");
|
||||||
|
}
|
||||||
|
if (m_hostPreferences->agentForwarding()) {
|
||||||
|
args << QStringLiteral("-A");
|
||||||
|
}
|
||||||
|
if (m_hostPreferences->compression()) {
|
||||||
|
args << QStringLiteral("-C");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port forwarding tunnels
|
||||||
|
const QStringList tunnelSpecs = m_hostPreferences->tunnels();
|
||||||
|
for (const QString &tunnel : tunnelSpecs) {
|
||||||
|
// Each tunnel is stored as "-L spec" or "-R spec" or "-D spec"
|
||||||
|
const QStringList parts = tunnel.split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||||
|
if (parts.size() == 2) {
|
||||||
|
args << parts.at(0) << parts.at(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build user@host
|
||||||
|
// Prefer username from preferences, fall back to URL username
|
||||||
|
QString user = m_hostPreferences->userName();
|
||||||
|
if (user.isEmpty()) {
|
||||||
|
user = m_url.userName();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString target;
|
||||||
|
if (!user.isEmpty()) {
|
||||||
|
target = user + QLatin1Char('@') + m_host;
|
||||||
|
} else {
|
||||||
|
target = m_host;
|
||||||
|
}
|
||||||
|
args << target;
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SshView::startConnection()
|
||||||
|
{
|
||||||
|
setStatus(Connecting);
|
||||||
|
|
||||||
|
if (!loadKonsolePart()) {
|
||||||
|
Q_EMIT errorMessage(i18n("SSH Error"),
|
||||||
|
i18n("Could not load the terminal emulator component (KonsolePart). "
|
||||||
|
"Please ensure Konsole is installed."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *terminalInterface = qobject_cast<TerminalInterface *>(m_terminalPart);
|
||||||
|
if (!terminalInterface) {
|
||||||
|
qCCritical(KRDC_SSH) << "KonsolePart does not implement TerminalInterface";
|
||||||
|
Q_EMIT errorMessage(i18n("SSH Error"),
|
||||||
|
i18n("Terminal component does not support the required interface."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the SSH command
|
||||||
|
const QStringList sshCommand = buildSshCommand();
|
||||||
|
qCDebug(KRDC_SSH) << "Starting SSH with command:" << sshCommand;
|
||||||
|
|
||||||
|
const QString sshPath = QStandardPaths::findExecutable(QStringLiteral("ssh"));
|
||||||
|
if (sshPath.isEmpty()) {
|
||||||
|
Q_EMIT errorMessage(i18n("SSH Error"),
|
||||||
|
i18n("Could not find the ssh executable in PATH."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to the terminal's finished signal to detect disconnection
|
||||||
|
connect(m_terminalPart, &KParts::ReadOnlyPart::destroyed, this, [this]() {
|
||||||
|
if (!m_quitting) {
|
||||||
|
setStatus(Disconnected);
|
||||||
|
Q_EMIT disconnected();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start SSH directly via startProgram for clean process lifecycle.
|
||||||
|
terminalInterface->startProgram(sshPath, sshCommand);
|
||||||
|
|
||||||
|
m_connected = true;
|
||||||
|
setStatus(Connected);
|
||||||
|
Q_EMIT connected();
|
||||||
|
Q_EMIT framebufferSizeChanged(m_framebufferSize.width(), m_framebufferSize.height());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshView::startQuittingConnection()
|
||||||
|
{
|
||||||
|
m_quitting = true;
|
||||||
|
|
||||||
|
if (m_terminalPart) {
|
||||||
|
// Disconnect signals to avoid callbacks during teardown
|
||||||
|
m_terminalPart->disconnect(this);
|
||||||
|
|
||||||
|
// Use deleteLater to let Qt process pending events for KonsolePart
|
||||||
|
// before destruction. Direct delete causes use-after-free in deferred
|
||||||
|
// child deletions.
|
||||||
|
m_terminalPart->deleteLater();
|
||||||
|
m_terminalPart = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(Disconnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize SshView::framebufferSize()
|
||||||
|
{
|
||||||
|
if (m_terminalPart && m_terminalPart->widget()) {
|
||||||
|
return m_terminalPart->widget()->size();
|
||||||
|
}
|
||||||
|
return m_framebufferSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize SshView::sizeHint() const
|
||||||
|
{
|
||||||
|
return m_framebufferSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize SshView::minimumSizeHint() const
|
||||||
|
{
|
||||||
|
return QSize(320, 240);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SshView::isQuitting()
|
||||||
|
{
|
||||||
|
return m_quitting;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SshView::supportsScaling() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SshView::supportsLocalCursor() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SshView::supportsViewOnly() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HostPreferences *SshView::hostPreferences()
|
||||||
|
{
|
||||||
|
return m_hostPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshView::scaleResize(int w, int h)
|
||||||
|
{
|
||||||
|
m_framebufferSize = QSize(w, h);
|
||||||
|
// Resize ourselves to fill the scroll area. The m_connected guard in
|
||||||
|
// resizeEvent prevents re-entrant crashes during the show sequence.
|
||||||
|
resize(w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshView::paintEvent(QPaintEvent *event)
|
||||||
|
{
|
||||||
|
// KonsolePart handles its own painting via its embedded widget.
|
||||||
|
// We just let the base class handle the background if needed.
|
||||||
|
RemoteView::paintEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshView::resizeEvent(QResizeEvent *event)
|
||||||
|
{
|
||||||
|
RemoteView::resizeEvent(event);
|
||||||
|
// Layout handles KonsolePart widget sizing.
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshView::handleKeyEvent(QKeyEvent *event)
|
||||||
|
{
|
||||||
|
// Input is handled directly by KonsolePart's widget via focus proxy.
|
||||||
|
Q_UNUSED(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshView::handleWheelEvent(QWheelEvent *event)
|
||||||
|
{
|
||||||
|
// Scrollback handled by KonsolePart directly.
|
||||||
|
Q_UNUSED(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshView::handleMouseEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
// Mouse events handled by KonsolePart directly.
|
||||||
|
Q_UNUSED(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SshView::handleLocalClipboardChanged(const QMimeData *data)
|
||||||
|
{
|
||||||
|
// KonsolePart handles clipboard internally.
|
||||||
|
Q_UNUSED(data);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2024 KRDC SSH Plugin Authors
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SSHVIEW_H
|
||||||
|
#define SSHVIEW_H
|
||||||
|
|
||||||
|
#include "remoteview.h"
|
||||||
|
|
||||||
|
#include <KConfigGroup>
|
||||||
|
#include <KParts/ReadOnlyPart>
|
||||||
|
|
||||||
|
class SshHostPreferences;
|
||||||
|
|
||||||
|
namespace KParts
|
||||||
|
{
|
||||||
|
class ReadOnlyPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSH terminal view. Embeds KonsolePart to provide a full terminal emulator
|
||||||
|
* connected to a remote host via SSH.
|
||||||
|
*/
|
||||||
|
class SshView : public RemoteView
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SshView(QWidget *parent = nullptr, const QUrl &url = QUrl(), KConfigGroup configGroup = KConfigGroup());
|
||||||
|
~SshView() override;
|
||||||
|
|
||||||
|
QSize framebufferSize() override;
|
||||||
|
QSize sizeHint() const override;
|
||||||
|
QSize minimumSizeHint() const override;
|
||||||
|
bool isQuitting() override;
|
||||||
|
bool supportsScaling() const override;
|
||||||
|
bool supportsLocalCursor() const override;
|
||||||
|
bool supportsViewOnly() const override;
|
||||||
|
|
||||||
|
HostPreferences *hostPreferences() override;
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
void scaleResize(int w, int h) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool startConnection() override;
|
||||||
|
void startQuittingConnection() override;
|
||||||
|
|
||||||
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
|
void handleKeyEvent(QKeyEvent *event) override;
|
||||||
|
void handleWheelEvent(QWheelEvent *event) override;
|
||||||
|
void handleMouseEvent(QMouseEvent *event) override;
|
||||||
|
void handleLocalClipboardChanged(const QMimeData *data) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool loadKonsolePart();
|
||||||
|
QStringList buildSshCommand() const;
|
||||||
|
|
||||||
|
KParts::ReadOnlyPart *m_terminalPart = nullptr;
|
||||||
|
SshHostPreferences *m_hostPreferences = nullptr;
|
||||||
|
bool m_quitting = false;
|
||||||
|
bool m_connected = false;
|
||||||
|
QSize m_framebufferSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SSHVIEW_H
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2024 KRDC SSH Plugin Authors
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "sshviewfactory.h"
|
||||||
|
#include "sshhostpreferences.h"
|
||||||
|
#include "sshview.h"
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
#include <KPluginFactory>
|
||||||
|
|
||||||
|
K_PLUGIN_CLASS_WITH_JSON(SshViewFactory, "krdc_ssh.json")
|
||||||
|
|
||||||
|
SshViewFactory::SshViewFactory(QObject *parent, const QVariantList &args)
|
||||||
|
: RemoteViewFactory(parent)
|
||||||
|
{
|
||||||
|
Q_UNUSED(args);
|
||||||
|
KLocalizedString::setApplicationDomain("krdc");
|
||||||
|
}
|
||||||
|
|
||||||
|
SshViewFactory::~SshViewFactory()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SshViewFactory::supportsUrl(const QUrl &url) const
|
||||||
|
{
|
||||||
|
return (url.scheme().compare(QLatin1String("ssh"), Qt::CaseInsensitive) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoteView *SshViewFactory::createView(QWidget *parent, const QUrl &url, KConfigGroup configGroup)
|
||||||
|
{
|
||||||
|
return new SshView(parent, url, configGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
HostPreferences *SshViewFactory::createHostPreferences(KConfigGroup configGroup, QWidget *parent)
|
||||||
|
{
|
||||||
|
return new SshHostPreferences(configGroup, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SshViewFactory::scheme() const
|
||||||
|
{
|
||||||
|
return QLatin1String("ssh");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SshViewFactory::connectActionText() const
|
||||||
|
{
|
||||||
|
return i18n("New SSH Connection\u2026");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SshViewFactory::connectButtonText() const
|
||||||
|
{
|
||||||
|
return i18n("Connect to an SSH Terminal");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SshViewFactory::connectToolTipText() const
|
||||||
|
{
|
||||||
|
return i18n(
|
||||||
|
"<html>Enter the address here.<br />"
|
||||||
|
"<i>Example: user@server (or server:port)</i></html>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "sshviewfactory.moc"
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2024 KRDC SSH Plugin Authors
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SSHVIEWFACTORY_H
|
||||||
|
#define SSHVIEWFACTORY_H
|
||||||
|
|
||||||
|
#include "remoteviewfactory.h"
|
||||||
|
|
||||||
|
class SshViewFactory : public RemoteViewFactory
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SshViewFactory(QObject *parent, const QVariantList &args);
|
||||||
|
~SshViewFactory() override;
|
||||||
|
|
||||||
|
bool supportsUrl(const QUrl &url) const override;
|
||||||
|
RemoteView *createView(QWidget *parent, const QUrl &url, KConfigGroup configGroup) override;
|
||||||
|
HostPreferences *createHostPreferences(KConfigGroup configGroup, QWidget *parent) override;
|
||||||
|
QString scheme() const override;
|
||||||
|
QString connectActionText() const override;
|
||||||
|
QString connectButtonText() const override;
|
||||||
|
QString connectToolTipText() const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SSHVIEWFACTORY_H
|
||||||
Loading…
Reference in New Issue