diff --git a/CMakeLists.txt b/CMakeLists.txt index 90000c37..77e45ce4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ option(KTAILCTL_FLATPAK_BUILD "Build for Flatpak" OFF) set(QT_MAJOR_VERSION 6) set(QT6_MIN_VERSION 6.5.0) set(KF6_MIN_VERSION 6.5.0) -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -76,8 +76,9 @@ find_package( WindowSystem) find_package(nlohmann_json REQUIRED) -add_subdirectory(src) -add_subdirectory(tests) +# add_subdirectory(src) +add_subdirectory(src_new) +# add_subdirectory(tests) install(PROGRAMS org.fkoehler.KTailctl.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES org.fkoehler.KTailctl.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) diff --git a/scripts/fedora-deps.sh b/scripts/fedora-deps.sh index 481baf5f..d1e551eb 100755 --- a/scripts/fedora-deps.sh +++ b/scripts/fedora-deps.sh @@ -13,6 +13,10 @@ PACKAGES=( kf6-ki18n-devel kf6-kirigami2-devel kf6-knotifications-devel + kf6-kdbusaddons-devel + kf6-kwindowsystem-devel + kf6-breeze-icons-devel + json-devel ) -dnf install -y ${PACKAGES[@]} +dnf install -y "${PACKAGES[@]}" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 80e5fadb..c1af1bad 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -28,7 +28,9 @@ add_executable( taildrop_sender.cpp tailscale.cpp tray_icon.cpp - util.cpp) + util.cpp + property_list_model.hpp + peer.hpp) ecm_add_qml_module(ktailctl URI org.fkoehler.KTailctl GENERATE_PLUGIN_SOURCE) ecm_target_qml_sources( diff --git a/src/main.cpp b/src/main.cpp index 55d9400f..1d33c0f4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,6 +20,7 @@ #include "app.hpp" #include "ktailctlconfig.h" #include "logging.hpp" +#include "peer.hpp" #include "peer_model.hpp" #include "preferences.hpp" #include "speed_statistics.hpp" @@ -96,6 +97,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[]) qmlRegisterType("org.fkoehler.KTailctl", 1, 0, "SpeedStatistics"); qmlRegisterType("org.fkoehler.KTailctl", 1, 0, "Statistics"); qmlRegisterType("org.fkoehler.KTailctl", 1, 0, "KTailctlConfig"); // TODO(fk): remove, now handled via CMake + qmlRegisterType("org.fkoehler.KTailctl", 1, 0, "Peer"); engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); engine.rootContext()->setContextProperty(QStringLiteral("styleName"), QQuickStyle::name()); diff --git a/src/ui/components/PeerInfoPage.qml b/src/ui/components/PeerInfo.qml similarity index 100% rename from src/ui/components/PeerInfoPage.qml rename to src/ui/components/PeerInfo.qml diff --git a/src/wrapper/logging.cpp b/src/wrapper/logging.cpp deleted file mode 100644 index ef4364f0..00000000 --- a/src/wrapper/logging.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "logging_wrapper.hpp" -#include -#include - -extern "C" { - -void ktailctl_critical(const char *message) -{ - qCCritical(Logging::Wrapper, "%s", message); -} -void ktailctl_debug(const char *message) -{ - qCDebug(Logging::Wrapper, "%s", message); -} -void ktailctl_info(const char *message) -{ - qCInfo(Logging::Wrapper, "%s", message); -} -void ktailctl_warning(const char *message) -{ - qCWarning(Logging::Wrapper, "%s", message); -} -} diff --git a/src_new/CMakeLists.txt b/src_new/CMakeLists.txt new file mode 100644 index 00000000..93c350a3 --- /dev/null +++ b/src_new/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(new_ktailctl peer.hpp property_list_model.hpp main.cpp + util.hpp + tailscale/status/login_profile.hpp + tailscale/status/network_profile.hpp) +target_link_libraries(new_ktailctl PRIVATE Qt6::Core Qt6::Quick Qt6::Widgets KF6::CoreAddons KF6::I18n KF6::DBusAddons KF6::GuiAddons) +ecm_add_qml_module(new_ktailctl URI org.fkoehler.KTailctl GENERATE_PLUGIN_SOURCE) +ecm_target_qml_sources(new_ktailctl SOURCES ui/Main.qml) +install(TARGETS new_ktailctl ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + +add_subdirectory(tailscale) +add_subdirectory(ui) +add_subdirectory(icons) diff --git a/src_new/icons/CMakeLists.txt b/src_new/icons/CMakeLists.txt new file mode 100644 index 00000000..b3cfe318 --- /dev/null +++ b/src_new/icons/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(new_ktailctl PRIVATE icons.qrc) diff --git a/src_new/icons/icons.qrc b/src_new/icons/icons.qrc new file mode 100644 index 00000000..f36ff1f3 --- /dev/null +++ b/src_new/icons/icons.qrc @@ -0,0 +1,5 @@ + + + ./logo.svg + + diff --git a/src/icons/logo.svg b/src_new/icons/logo.svg similarity index 100% rename from src/icons/logo.svg rename to src_new/icons/logo.svg diff --git a/src_new/main.cpp b/src_new/main.cpp new file mode 100644 index 00000000..738a448d --- /dev/null +++ b/src_new/main.cpp @@ -0,0 +1,80 @@ +#include "util.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "tailscale/status/client_version.hpp" +#include "tailscale/status/exit_node_status.hpp" +#include "tailscale/status/peer_status.hpp" +#include "tailscale/status/status.hpp" +#include "tailscale/status/tailnet_status.hpp" +#include "tailscale/tailscale.hpp" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + QApplication::setWindowIcon(QIcon(QStringLiteral(":/icons/logo.svg"))); + + KLocalizedString::setApplicationDomain(QByteArrayLiteral("org.fkoehler.KTailctl")); + QCoreApplication::setOrganizationDomain(QStringLiteral("fkoehler.org")); + QCoreApplication::setApplicationName(QStringLiteral("KTailctl")); + QCoreApplication::setOrganizationName(QStringLiteral("fkoehler.org")); + + KAboutData aboutData(QStringLiteral("ktailctl"), + i18nc("@title", "KTailctl"), + // TODO: version string via CMake + QStringLiteral("1.0"), + i18n("GUI for tailscale on the Linux desktop"), + KAboutLicense::GPL, + i18n("(c) Fabian Koehler 2023")); + + aboutData.addAuthor(i18nc("@info:credit", "Fabian Koehler"), + i18nc("@info:credit", "Project Maintainer"), + QStringLiteral("fabian@fkoehler.me"), + QStringLiteral("https://fkoehler.org")); + + aboutData.setBugAddress("https://github.com/f-koehler/KTailctl/issues"); + aboutData.setDesktopFileName(QStringLiteral("org.fkoehler.KTailctl")); + aboutData.setHomepage(QStringLiteral("https://github.com/f-koehler/KTailctl")); + aboutData.setOrganizationDomain("fkoehler.org"); + aboutData.addAuthor(i18nc("@info:credit", "Fabian Köhler"), + i18nc("@info:credit", "Project Maintainer"), + QStringLiteral("me@fkoehler.org"), + QStringLiteral("https://fkoehler.org")); + // TODO(fk): about icon + aboutData.setProgramLogo(QIcon(QStringLiteral(":/icons/logo.svg"))); + KAboutData::setApplicationData(aboutData); + const KDBusService service(KDBusService::Unique); + + QQmlApplicationEngine engine; + qmlRegisterType("org.fkoehler.KTailctl", 1, 0, "ClientVersion"); + qmlRegisterType("org.fkoehler.KTailctl", 1, 0, "ExitNodeStatus"); + qmlRegisterType("org.fkoehler.KTailctl", 1, 0, "Location"); + qmlRegisterType("org.fkoehler.KTailctl", 1, 0, "PeerStatus"); + qmlRegisterType("org.fkoehler.KTailctl", 1, 0, "STatus"); + qmlRegisterType("org.fkoehler.KTailctl", 1, 0, "TailnetStatus"); + qmlRegisterType("org.fkoehler.KTailctl", 1, 0, "UserProfile"); + qmlRegisterType("org.fkoehler.KTailctl", 1, 0, "PeerModel"); + + TailscaleNew *tailscale = new TailscaleNew(); + Util* util = new Util(); + qmlRegisterSingletonInstance("org.fkoehler.KTailctl", 1, 0, "Tailscale", tailscale); + qmlRegisterSingletonInstance("org.fkoehler.KTailctl", 1, 0, "Util", util); + qmlRegisterSingletonType("org.fkoehler.KTailctl", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue { + return engine->toScriptValue(KAboutData::applicationData()); + }); + + engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); + engine.load(QStringLiteral("qrc:/Main.qml")); + if (engine.rootObjects().isEmpty()) { + return -1; + } + + return app.exec(); +} diff --git a/src_new/peer.hpp b/src_new/peer.hpp new file mode 100644 index 00000000..05021bcf --- /dev/null +++ b/src_new/peer.hpp @@ -0,0 +1,437 @@ +// +// Created by fkoehler on 12/6/25. +// + +#ifndef KTAILCTL_PEER_H +#define KTAILCTL_PEER_H + +#include +#include +#include +#include +#include +#include +#include + +class Peer : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString id READ id WRITE setId BINDABLE bindableId) + Q_PROPERTY(QString publicKey READ publicKey WRITE setPublicKey BINDABLE bindablePublicKey) + Q_PROPERTY(QString hostName READ hostName WRITE setHostName BINDABLE bindableHostName) + Q_PROPERTY(QString dnsName READ dnsName WRITE setDnsName BINDABLE bindableDnsName) + Q_PROPERTY(QString os READ os WRITE setOs BINDABLE bindableOs) + Q_PROPERTY(QStringList tailscaleIps READ tailscaleIps BINDABLE bindableTailscaleIps) + Q_PROPERTY(QString relay READ relay WRITE setRelay BINDABLE bindableRelay) + Q_PROPERTY(long receivedBytes READ receivedBytes WRITE setReceivedBytes BINDABLE bindableReceivedBytes) + Q_PROPERTY(long transmittedBytes WRITE transmittedBytes WRITE setTransmittedBytes BINDABLE bindableTransmittedBytes) + Q_PROPERTY(QDateTime created READ created WRITE setCreated BINDABLE bindableCreated) + Q_PROPERTY(QDateTime lastSeen READ lastSeen WRITE setLastSeen BINDABLE bindableLastSeen) + Q_PROPERTY(bool isOnline READ isOnline WRITE setIsOnline BINDABLE bindableIsOnline) + Q_PROPERTY(bool isActive READ isActive WRITE setIsActive BINDABLE bindableIsActive) + Q_PROPERTY(bool isCurrentExitNode READ isCurrentExitNode WRITE setIsCurrentExitNode BINDABLE bindableIsCurrentExitNode) + Q_PROPERTY(bool isExitNode READ isExitNode WRITE setIsExitNode BINDABLE bindableIsExitNode) + Q_PROPERTY(QStringList sshHostKeys READ sshHostKeys WRITE setSshHostKeys BINDABLE bindableSshHostKeys) + Q_PROPERTY(QStringList tags READ tags WRITE setTags BINDABLE bindableTags) + Q_PROPERTY(bool isMullvad READ isMullvad WRITE setIsMullvad BINDABLE bindableIsMullvad) + Q_PROPERTY(QString country READ country WRITE setCountry BINDABLE bindableCountry) + Q_PROPERTY(QString countryCode READ countryCode WRITE setCountryCode BINDABLE bindableCountryCode) + Q_PROPERTY(QString city READ city WRITE setCity BINDABLE bindableCity) + Q_PROPERTY(QString cityCode READ cityCode WRITE setCityCode BINDABLE bindableCityCode) + +private: + QProperty mId; + QProperty mPublickey; + QProperty mHostName; + QProperty mDnsName; + QProperty mOs; + QProperty mTailscaleIps; + QProperty mRelay; + QProperty mReceivedBytes; + QProperty mTransmittedBytes; + QProperty mCreated; + QProperty mLastSeen; + QProperty mIsOnline; + QProperty mIsActive; + QProperty mIsCurrentExitNode; + QProperty mIsExitNode; + QProperty mSshHostKeys; + QProperty mTags; + QProperty mIsMullvad; + QProperty mCountry; + QProperty mCountryCode; + QProperty mCity; + QProperty mCityCode; + QProperty mAdminPanelUrl; + +public: + explicit Peer(QObject *parent = nullptr) + : QObject(parent) + { + } + + void fromJson(const QJsonObject &object) + { + setId(object.value(QStringLiteral("ID")).toString()); + setPublicKey(object.value(QStringLiteral("PublicKey")).toString()); + setHostName(object.value("HostName").toString()); + setDnsName(object.value("DNSName").toString()); + setOs(object.value("OS").toString()); + + setTailscaleIps(object.value("TailscaleIPs").toVariant().toStringList()); + + setRelay(object.value("Relay").toString()); + setReceivedBytes(object.value("RxBytes").toInteger()); + setTransmittedBytes(object.value("TxBytes").toInteger()); + // TODO(fk): parse lastSeen and Created + setIsOnline(object.value("IsOnline").toBool()); + setIsActive(object.value("IsActive").toBool()); + // TODO(fk): setIsCurrentExitNode + // TODO(fk): setIsExitNode + } + + // getters + [[nodiscard]] const QString &id() const + { + return mId; + } + + [[nodiscard]] const QString &publicKey() const + { + return mPublickey; + }; + + [[nodiscard]] const QString &hostName() const + { + return mHostName; + } + + [[nodiscard]] const QString &dnsName() const + { + return mDnsName; + } + + [[nodiscard]] const QString &os() const + { + return mOs; + } + + [[nodiscard]] const QStringList &tailscaleIps() const + { + return mTailscaleIps; + } + + [[nodiscard]] const QString &relay() const + { + return mRelay; + } + + [[nodiscard]] long receivedBytes() const + { + return mReceivedBytes; + } + + [[nodiscard]] long transmittedBytes() const + { + return mTransmittedBytes; + } + + [[nodiscard]] const QDateTime &created() const + { + return mCreated; + } + + [[nodiscard]] const QDateTime &lastSeen() const + { + return mLastSeen; + } + + [[nodiscard]] bool isOnline() const + { + return mIsOnline; + } + + [[nodiscard]] bool isActive() const + { + return mIsActive; + } + + [[nodiscard]] bool isCurrentExitNode() const + { + return mIsCurrentExitNode; + } + + [[nodiscard]] bool isExitNode() const + { + return mIsExitNode; + } + + [[nodiscard]] const QStringList &sshHostKeys() const + { + return mSshHostKeys; + } + + [[nodiscard]] const QStringList &tags() const + { + return mTags; + } + + [[nodiscard]] bool isMullvad() const + { + return mIsMullvad; + } + + [[nodiscard]] const QString &country() const + { + return mCountry; + } + + [[nodiscard]] const QString &countryCode() const + { + return mCountryCode; + } + + [[nodiscard]] const QString &city() const + { + return mCity; + } + + [[nodiscard]] const QString &cityCode() const + { + return mCityCode; + } + + [[nodiscard]] const QUrl &adminPanelUrl() const + { + return mAdminPanelUrl; + } + + // bindables + QBindable bindableId() + { + return {&mId}; + } + + QBindable bindablePublicKey() + { + return {&mPublickey}; + } + + QBindable bindableHostName() + { + return {&mHostName}; + } + + QBindable bindableDnsName() + { + return {&mDnsName}; + } + + QBindable bindableOs() + { + return {&mOs}; + } + + QBindable bindableTailscaleIps() + { + return {&mTailscaleIps}; + } + + QBindable bindableRelay() + { + return {&mRelay}; + } + + QBindable bindableReceivedBytes() + { + return {&mReceivedBytes}; + } + + QBindable bindableTransmittedBytes() + { + return {&mTransmittedBytes}; + } + + QBindable bindableCreated() + { + return {&mCreated}; + } + + QBindable bindableLastSeen() + { + return {&mLastSeen}; + } + + QBindable bindableIsOnline() + { + return {&mIsOnline}; + } + + QBindable bindableIsActive() + { + return {&mIsActive}; + } + + QBindable bindableIsCurrentExitNode() + { + return {&mIsCurrentExitNode}; + } + + QBindable bindableIsExitNode() + { + return {&mIsExitNode}; + } + + QBindable bindableSshHostKeys() + { + return {&mSshHostKeys}; + } + + QBindable bindableTags() + { + return {&mTags}; + } + + QBindable bindableIsMullvad() + { + return {&mIsMullvad}; + } + + QBindable bindableCountry() + { + return {&mCountry}; + } + + QBindable bindableCountryCode() + { + return {&mCountryCode}; + } + + QBindable bindableCity() + { + return {&mCity}; + } + + QBindable bindableCityCode() + { + return {&mCityCode}; + } + + QBindable bindableAdminPanelUrl() + { + return {&mAdminPanelUrl}; + } + + // setters + void setId(const QString &id) + { + mId = id; + } + + void setPublicKey(const QString &publickey) + { + mPublickey = publickey; + } + + void setHostName(const QString &hostName) + { + mHostName = hostName; + } + + void setDnsName(const QString &dnsName) + { + mDnsName = dnsName; + } + + void setOs(const QString &os) + { + mOs = os; + } + + void setTailscaleIps(const QStringList &tailscaleIps) + { + mTailscaleIps = tailscaleIps; + } + + void setRelay(const QString &relay) + { + mRelay = relay; + } + + void setReceivedBytes(long receivedBytes) + { + mReceivedBytes = receivedBytes; + } + + void setTransmittedBytes(long transmittedBytes) + { + mTransmittedBytes = transmittedBytes; + } + + void setCreated(const QDateTime &created) + { + mCreated = created; + } + + void setLastSeen(const QDateTime &lastSeen) + { + mLastSeen = lastSeen; + } + + void setIsOnline(bool isOnline) + { + mIsOnline = isOnline; + } + + void setIsActive(bool isActive) + { + mIsActive = isActive; + } + + void setIsCurrentExitNode(bool isCurrentExitNode) + { + mIsCurrentExitNode = isCurrentExitNode; + } + + void setIsExitNode(bool isExitNode) + { + mIsExitNode = isExitNode; + } + + void setSshHostKeys(const QStringList &sshHostKeys) + { + mSshHostKeys = sshHostKeys; + } + + void setTags(const QStringList &tags) + { + mTags = tags; + } + + void setIsMullvad(bool isMullvad) + { + mIsMullvad = isMullvad; + } + + void setCountry(const QString &country) + { + mCountry = country; + } + + void setCountryCode(const QString &countryCode) + { + mCountryCode = countryCode; + } + + void setCity(const QString &city) + { + mCity = city; + } + + void setCityCode(const QString &cityCode) + { + mCityCode = cityCode; + } +}; + +#endif // KTAILCTL_PEER_H \ No newline at end of file diff --git a/src_new/property_list_model.hpp b/src_new/property_list_model.hpp new file mode 100644 index 00000000..fae84b33 --- /dev/null +++ b/src_new/property_list_model.hpp @@ -0,0 +1,184 @@ +#ifndef KTAILCTL_PROPERTY_LIST_MODEL_HPP +#define KTAILCTL_PROPERTY_LIST_MODEL_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +enum class PropertyListModelOwnership : bool { + Owning, + External, +}; + +template +class PropertyListModel : public QAbstractListModel +{ +public: + using Type = T; + static constexpr PropertyListModelOwnership Ownership = O; + static constexpr int SelfRole = Qt::UserRole; + + static_assert(std::is_base_of_v, "PropertyListModel requires T to derive from QObject"); + +private: + QVector> mItems; + QHash mRoleToPropertyIndex; + QHash mRoleNames; + +public: + explicit PropertyListModel(QObject *parent = nullptr) + : QAbstractListModel(parent) + { + setupMetaRoles(); + } + + [[nodiscard]] int rowCount([[maybe_unused]] const QModelIndex &parent) const noexcept override + { + return mItems.size(); + } + + [[nodiscard]] QVariant data(const QModelIndex &index, const int role) const override + { + if (!index.isValid()) { + return {}; + } + + const int row = index.row(); + if (row < 0 || row >= mItems.size()) { + return {}; + } + + const QPointer &item = mItems[row]; + if (!item) { + return {}; + } + + if (role == SelfRole) { + return QVariant::fromValue(static_cast(item.data())); + } + + const int propertyIndex = mRoleToPropertyIndex.value(role, -1); + if (propertyIndex < 0) { + return {}; + } + + const QMetaProperty property = item->metaObject()->property(propertyIndex); + + return property.read(item); + } + + [[nodiscard]] QHash roleNames() const noexcept override + { + return mRoleNames; + } + + // --- API ---------------------------------------------------------------- + + int addItem(Type *item) + { + if (item == nullptr) { + return -1; + } + + const int row = mItems.size(); + beginInsertRows({}, row, row); + + if constexpr (Ownership == PropertyListModelOwnership::Owning) { + if (!item->parent()) { + item->setParent(this); + } + } + + mItems.append(item); + + // Remove row automatically if item is destroyed externally + connect(item, &QObject::destroyed, this, [this, item]() { + if (const int idx = indexOf(item); idx >= 0) { + beginRemoveRows({}, idx, idx); + mItems.removeAt(idx); + endRemoveRows(); + } + }); + + endInsertRows(); + return row; + } + + bool removeItem(int row) + { + if (row < 0 || row >= mItems.size()) { + return false; + } + + beginRemoveRows({}, row, row); + + if constexpr (Ownership == PropertyListModelOwnership::Owning) { + if (mItems[row]) { + mItems[row]->deleteLater(); + } + } + + mItems.removeAt(row); + endRemoveRows(); + return true; + } + + void clear() + { + beginResetModel(); + + if constexpr (Ownership == PropertyListModelOwnership::Owning) { + for (const auto &item : mItems) { + if (item) { + item->deleteLater(); + } + } + } + + mItems.clear(); + endResetModel(); + } + + Type *at(int row) const + { + if (row < 0 || row >= mItems.size()) { + return nullptr; + } + return mItems[row]; + } + + int indexOf(Type *item) const + { + return mItems.indexOf(item); + } + +private: + void setupMetaRoles() + { + const QMetaObject &metaObject = T::staticMetaObject; + int role = Qt::UserRole; + + mRoleNames.insert(role, "self"); + mRoleToPropertyIndex.insert(role, -1); + ++role; + + for (int i = metaObject.propertyOffset(); i < metaObject.propertyCount(); ++i) { + const QMetaProperty property = metaObject.property(i); + + if (!property.isValid()) { + continue; + } + + mRoleToPropertyIndex.insert(role, i); + mRoleNames.insert(role, property.name()); + ++role; + } + } +}; + +#endif // KTAILCTL_PROPERTY_LIST_MODEL_HPP diff --git a/src_new/tailscale/CMakeLists.txt b/src_new/tailscale/CMakeLists.txt new file mode 100644 index 00000000..85c01fd1 --- /dev/null +++ b/src_new/tailscale/CMakeLists.txt @@ -0,0 +1,15 @@ +add_subdirectory(status) +add_subdirectory(wrapper) + +ecm_qt_declare_logging_category( + ktailctl + HEADER + "logging_tailscale.hpp" + IDENTIFIER + "Logging::Tailscale" + CATEGORY_NAME + "org.fkoehler.KTailctl.Tailscale" + DEFAULT_SEVERITY + Info) + +target_sources(new_ktailctl PRIVATE tailscale.hpp ${CMAKE_CURRENT_BINARY_DIR}/logging_tailscale.cpp) diff --git a/src_new/tailscale/status/CMakeLists.txt b/src_new/tailscale/status/CMakeLists.txt new file mode 100644 index 00000000..88021554 --- /dev/null +++ b/src_new/tailscale/status/CMakeLists.txt @@ -0,0 +1,22 @@ +ecm_qt_declare_logging_category( + ktailctl + HEADER + "logging_tailscale_status.hpp" + IDENTIFIER + "Logging::Tailscale::Status" + CATEGORY_NAME + "org.fkoehler.KTailctl.Tailscale.Status" + DEFAULT_SEVERITY + Info) + +target_sources( + new_ktailctl + PRIVATE client_version.hpp + exit_node_status.hpp + location.hpp + peer_status.hpp + status.hpp + tailnet_status.hpp + user_profile.hpp + ${CMAKE_CURRENT_BINARY_DIR}/logging_tailscale_status.cpp) +target_include_directories(new_ktailctl PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src_new/tailscale/status/client_version.hpp b/src_new/tailscale/status/client_version.hpp new file mode 100644 index 00000000..2073a102 --- /dev/null +++ b/src_new/tailscale/status/client_version.hpp @@ -0,0 +1,115 @@ +#ifndef KTAILCTL_CLIENT_VERSION_HPP +#define KTAILCTL_CLIENT_VERSION_HPP + +#include +#include +#include +#include +#include + +// https://pkg.go.dev/tailscale.com@v1.92.1/tailcfg#ClientVersion +class ClientVersion : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool runningLatest READ runningLatest BINDABLE bindableRunningLatest) + Q_PROPERTY(QString latestVersion READ latestVersion BINDABLE bindableLatestVersion) + Q_PROPERTY(bool urgentSecurityUpdate READ urgentSecurityUpdate BINDABLE bindableUrgentSecurityUpdate) + Q_PROPERTY(bool notify READ notify BINDABLE bindableNotify) + Q_PROPERTY(QString notifyUrl READ notifyUrl BINDABLE bindableNotifyUrl) + Q_PROPERTY(QString notifyText READ notifyText BINDABLE bindableNotifyText) + +private: + QProperty mRunningLatest; + QProperty mLatestVersion; + QProperty mUrgentSecurityUpdate; + QProperty mNotify; + QProperty mNotifyUrl; + QProperty mNotifyText; + +public: + explicit ClientVersion(QObject *parent = nullptr) + : QObject(parent) + { + } + + explicit ClientVersion(QJsonObject &json, QObject *parent = nullptr) + : QObject(parent) + { + updateFromJson(json); + } + + void updateFromJson(QJsonObject &json) + { + mRunningLatest = json.take(QStringLiteral("RunningLatest")).toBool(); + mLatestVersion = json.take(QStringLiteral("Version")).toString(); + mUrgentSecurityUpdate = json.take(QStringLiteral("UrgentSecurityUpdate")).toBool(); + mNotify = json.take(QStringLiteral("Notify")).toBool(); + mNotifyUrl = json.take(QStringLiteral("NotifyUrl")).toString(); + mNotifyText = json.take(QStringLiteral("NotifyText")).toString(); + } + + // Getters + [[nodiscard]] bool runningLatest() const noexcept + { + return mRunningLatest; + } + + [[nodiscard]] const QString &latestVersion() const noexcept + { + return mLatestVersion; + } + + [[nodiscard]] bool urgentSecurityUpdate() const noexcept + { + return mUrgentSecurityUpdate; + } + + [[nodiscard]] bool notify() const noexcept + { + return mNotify; + } + + [[nodiscard]] const QString ¬ifyUrl() const noexcept + { + return mNotifyUrl; + } + + [[nodiscard]] const QString ¬ifyText() const noexcept + { + return mNotifyText; + } + + // Bindables + [[nodiscard]] QBindable bindableRunningLatest() + { + return {&mRunningLatest}; + } + + [[nodiscard]] QBindable bindableLatestVersion() + { + return {&mLatestVersion}; + } + + [[nodiscard]] QBindable bindableUrgentSecurityUpdate() + { + return {&mUrgentSecurityUpdate}; + } + + [[nodiscard]] QBindable bindableNotify() + { + return {&mNotify}; + } + + [[nodiscard]] QBindable bindableNotifyUrl() + { + return {&mNotifyUrl}; + } + + [[nodiscard]] QBindable bindableNotifyText() + { + return {&mNotifyText}; + } +}; + +#endif // KTAILCTL_CLIENT_VERSION_HPP diff --git a/src_new/tailscale/status/exit_node_status.hpp b/src_new/tailscale/status/exit_node_status.hpp new file mode 100644 index 00000000..e47e19af --- /dev/null +++ b/src_new/tailscale/status/exit_node_status.hpp @@ -0,0 +1,70 @@ +#ifndef KTAILCTL_EXIT_NODE_STATUS_HPP +#define KTAILCTL_EXIT_NODE_STATUS_HPP + +#include +#include +#include + +// https://pkg.go.dev/tailscale.com/ipn/ipnstate#ExitNodeStatus +class ExitNodeStatus : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString id READ id BINDABLE bindableId) + Q_PROPERTY(bool isOnline READ isOnline BINDABLE bindableIsOnline) + Q_PROPERTY(QStringList tailscaleIps READ tailscaleIps BINDABLE bindableTailscaleIps) + +private: + QProperty mId; + QProperty mIsOnline; + QProperty mTailscaleIps; + +public: + explicit ExitNodeStatus(QObject *parent = nullptr) + : QObject(parent) + { + } + + explicit ExitNodeStatus(QJsonObject &json, QObject *parent = nullptr) + : QObject(parent) + { + updateFromJson(json); + } + + void updateFromJson(QJsonObject &json) + { + mId = json.take(QStringLiteral("ID")).toString(); + mIsOnline = json.take(QStringLiteral("Online")).toBool(); + mTailscaleIps = json.take(QStringLiteral("TailscaleIPs")).toVariant().toStringList(); + } + + // Getters + [[nodiscard]] const QString &id() const noexcept + { + return mId; + } + [[nodiscard]] bool isOnline() const noexcept + { + return mIsOnline; + } + [[nodiscard]] const QStringList &tailscaleIps() const noexcept + { + return mTailscaleIps; + } + + // Bindables + [[nodiscard]] QBindable bindableId() const + { + return {&mId}; + } + + [[nodiscard]] QBindable bindableIsOnline() const + { + return {&mIsOnline}; + } + [[nodiscard]] QBindable bindableTailscaleIps() const + { + return {&mTailscaleIps}; + } +}; + +#endif // KTAILCTL_EXIT_NODE_STATUS_HPP diff --git a/src_new/tailscale/status/location.hpp b/src_new/tailscale/status/location.hpp new file mode 100644 index 00000000..bb5cd073 --- /dev/null +++ b/src_new/tailscale/status/location.hpp @@ -0,0 +1,125 @@ +#ifndef KTAILCTL_LOCATION_HPP +#define KTAILCTL_LOCATION_HPP + +#include +#include +#include + +class Location : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString country READ country BINDABLE bindableCountry) + Q_PROPERTY(QString countryCode READ countryCode BINDABLE bindableCountryCode) + Q_PROPERTY(QString city READ city BINDABLE bindableCity) + Q_PROPERTY(QString cityCode READ cityCode BINDABLE bindableCityCode) + Q_PROPERTY(double latitude READ latitude BINDABLE bindableLatitude) + Q_PROPERTY(double longitude READ longitude BINDABLE bindableLongitude) + Q_PROPERTY(int priority READ priority BINDABLE bindablePriority) + +private: + QProperty mCountry; + QProperty mCountryCode; + QProperty mCity; + QProperty mCityCode; + QProperty mLatitude; + QProperty mLongitude; + QProperty mPriority; + +public: + explicit Location(QObject *parent = nullptr) + : QObject(parent) + { + } + + explicit Location(QJsonObject &json, QObject *parent = nullptr) + : QObject(parent) + { + updateFromJson(json); + } + + void updateFromJson(QJsonObject &json) + { + mCountry = json.take(QStringLiteral("Country")).toString(); + mCountryCode = json.take(QStringLiteral("CountryCode")).toString(); + mCity = json.take(QStringLiteral("City")).toString(); + mCityCode = json.take(QStringLiteral("CityCode")).toString(); + mLatitude = json.take(QStringLiteral("Latitude")).toDouble(); + mLongitude = json.take(QStringLiteral("Longitude")).toDouble(); + mPriority = json.take(QStringLiteral("Priority")).toInt(); + } + + // Getters + [[nodiscard]] const QString &country() const noexcept + { + return mCountry; + } + + [[nodiscard]] const QString &countryCode() const noexcept + { + return mCountryCode; + } + + [[nodiscard]] const QString &city() const noexcept + { + return mCity; + } + + [[nodiscard]] const QString &cityCode() const noexcept + { + return mCityCode; + } + + [[nodiscard]] double latitude() const noexcept + { + return mLatitude; + } + + [[nodiscard]] double longitude() const noexcept + { + return mLongitude; + } + + [[nodiscard]] int priority() const noexcept + { + return mPriority; + } + + // Bindables + [[nodiscard]] QBindable bindableCountry() + { + return {&mCountry}; + } + + [[nodiscard]] QBindable bindableCountryCode() + { + return {&mCountryCode}; + } + + [[nodiscard]] QBindable bindableCity() + { + return {&mCity}; + } + + [[nodiscard]] QBindable bindableCityCode() + { + return {&mCityCode}; + } + + [[nodiscard]] QBindable bindableLatitude() + { + return {&mLatitude}; + } + + [[nodiscard]] QBindable bindableLongitude() + { + return {&mLongitude}; + } + + [[nodiscard]] QBindable bindablePriority() + { + return {&mPriority}; + } +}; + +#endif // KTAILCTL_LOCATION_HPP diff --git a/src_new/tailscale/status/login_profile.hpp b/src_new/tailscale/status/login_profile.hpp new file mode 100644 index 00000000..dbf267c7 --- /dev/null +++ b/src_new/tailscale/status/login_profile.hpp @@ -0,0 +1,113 @@ +#ifndef KTAILCTL_LOGIN_PROFILE_HPP +#define KTAILCTL_LOGIN_PROFILE_HPP + +#include "network_profile.hpp" +#include "user_profile.hpp" + +#include + +class LoginProfile : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString id READ id BINDABLE bindableId) + Q_PROPERTY(QString name READ name BINDABLE bindableName) + Q_PROPERTY(NetworkProfile *networkProfile READ networkProfile CONSTANT) + Q_PROPERTY(QString key READ key BINDABLE bindableKey) + Q_PROPERTY(UserProfile *userProfile READ userProfile CONSTANT) + Q_PROPERTY(QString nodeId READ nodeId BINDABLE bindableNodeId) + Q_PROPERTY(QString localUserId READ localUserId BINDABLE bindableLocalUserId) + Q_PROPERTY(QString controlUrl READ controlUrl BINDABLE bindableControlUrl) + +private: + QProperty mId; + QProperty mName; + NetworkProfile *mNetworkProfile; + QProperty mKey; + UserProfile *mUserProfile; + QProperty mNodeId; + QProperty mLocalUserId; + QProperty mControlUrl; + +public: + LoginProfile(QObject *parent = nullptr) + : QObject(parent) + , mNetworkProfile(new NetworkProfile(this)) + , mUserProfile(new UserProfile(this)) + { + } + + void updateFromJson(QJsonObject& json) + { + mId = json.take(QStringLiteral("ID")).toString(); + mName = json.take(QStringLiteral("Name")).toString(); + mKey = json.take(QStringLiteral("Key")).toString(); + mNodeId = json.take(QStringLiteral("NodeId")).toString(); + mLocalUserId = json.take(QStringLiteral("LocalUserId")).toString(); + mControlUrl = json.take(QStringLiteral("ControlUrl")).toString(); + + auto networkJson = json.take(QStringLiteral("networkJson")).toObject(); + mNetworkProfile->updateFromJson(networkJson); + auto userProfileJson = json.take(QStringLiteral("userProfileJson")).toObject(); + mUserProfile->updateFromJson(userProfileJson); + } + + [[nodiscard]] const QString &id() const noexcept + { + return mId; + } + [[nodiscard]] const QString &name() const noexcept + { + return mName; + } + [[nodiscard]] NetworkProfile *networkProfile() const noexcept + { + return mNetworkProfile; + } + [[nodiscard]] const QString &key() const noexcept + { + return mKey; + } + [[nodiscard]] UserProfile *userProfile() const noexcept + { + return mUserProfile; + } + [[nodiscard]] const QString &nodeId() const noexcept + { + return mNodeId; + } + [[nodiscard]] const QString &localUserId() const noexcept + { + return mLocalUserId; + } + [[nodiscard]] const QString &controlUrl() const noexcept + { + return mControlUrl; + } + + [[nodiscard]] QBindable bindableId() + { + return {&mId}; + } + [[nodiscard]] QBindable bindableName() + { + return {&mName}; + } + [[nodiscard]] QBindable bindableKey() + { + return {&mKey}; + } + [[nodiscard]] QBindable bindableNodeId() + { + return {&mNodeId}; + } + [[nodiscard]] QBindable bindableLocalUserId() + { + return {&mLocalUserId}; + } + [[nodiscard]] QBindable bindableControlUrl() + { + return {&mControlUrl}; + } +}; + +#endif // KTAILCTL_LOGIN_PROFILE_HPP diff --git a/src_new/tailscale/status/network_profile.hpp b/src_new/tailscale/status/network_profile.hpp new file mode 100644 index 00000000..3f3f8da4 --- /dev/null +++ b/src_new/tailscale/status/network_profile.hpp @@ -0,0 +1,59 @@ +#ifndef KTAILCTL_NETWORK_PROFILE_HPP +#define KTAILCTL_NETWORK_PROFILE_HPP + +#include +#include + +class NetworkProfile : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString magicDnsName READ magicDnsName BINDABLE bindableMagicDnsName) + Q_PROPERTY(QString domainName READ domainName BINDABLE bindableDomainName) + Q_PROPERTY(QString displayName READ displayName BINDABLE bindableDisplayName) + +private: + QProperty mMagicDnsName; + QProperty mDomainName; + QProperty mDisplayName; + +public: + explicit NetworkProfile(QObject *parent = nullptr) + : QObject(parent) + { + } + + void updateFromJson(QJsonObject &json) + { + mMagicDnsName = json.take(QStringLiteral("MagicDNSName")).toString(); + mDomainName = json.take(QStringLiteral("DomainName")).toString(); + mDisplayName = json.take(QStringLiteral("DisplayName")).toString(); + } + + [[nodiscard]] const QString &magicDnsName() const noexcept + { + return mMagicDnsName; + } + [[nodiscard]] const QString &domainName() const noexcept + { + return mDomainName; + } + [[nodiscard]] const QString &displayName() const noexcept + { + return mDisplayName; + } + + [[nodiscard]] QBindable bindableMagicDnsName() + { + return {&mMagicDnsName}; + } + [[nodiscard]] QBindable bindableDomainName() + { + return {&mDomainName}; + } + [[nodiscard]] QBindable bindableDisplayName() + { + return {&mDisplayName}; + } +}; + +#endif // KTAILCTL_NETWORK_PROFILE_HPP diff --git a/src_new/tailscale/status/peer_status.hpp b/src_new/tailscale/status/peer_status.hpp new file mode 100644 index 00000000..26f2d39c --- /dev/null +++ b/src_new/tailscale/status/peer_status.hpp @@ -0,0 +1,536 @@ +#ifndef KTAILCTL_PEER_STATUS_HPP +#define KTAILCTL_PEER_STATUS_HPP + +#include "location.hpp" + +#include +#include +#include +#include +#include + +// https://pkg.go.dev/tailscale.com/ipn/ipnstate#PeerStatus +class PeerStatus : public QObject +{ + Q_OBJECT + + enum class TaildropTargetStatus : uint8_t { + Unknown, + Available, + NoNetmapAvailable, + IpnStateNotRunning, + MissingCapability, + Offline, + NoPeerInfo, + UnsupportedOs, + NoPeerApi, + OwnedByOtherUser + }; + + Q_ENUM(TaildropTargetStatus) + + Q_PROPERTY(QString id READ id BINDABLE bindableId) + Q_PROPERTY(QString publicKey READ publicKey BINDABLE bindablePublicKey) + Q_PROPERTY(QString hostName READ hostName BINDABLE bindableHostName) + Q_PROPERTY(QString dnsName READ dnsName BINDABLE bindableDnsName) + Q_PROPERTY(QString os READ os BINDABLE bindableOs) + Q_PROPERTY(qint64 userId READ userId BINDABLE bindableUserId) + Q_PROPERTY(qint64 sharerUserId READ sharerUserId BINDABLE bindableSharerUserId) + Q_PROPERTY(QStringList tailscaleIps READ tailscaleIps BINDABLE bindableTailscaleIps) + Q_PROPERTY(QStringList allowedIps READ allowedIps BINDABLE bindableAllowedIps) + Q_PROPERTY(QStringList tags READ tags BINDABLE bindableTags) + Q_PROPERTY(QStringList primaryRoutes READ primaryRoutes BINDABLE bindablePrimaryRoutes) + Q_PROPERTY(QStringList addresses READ addresses BINDABLE bindableAddresses) + Q_PROPERTY(QString currentAddress READ currentAddress BINDABLE bindableCurrentAddress) + Q_PROPERTY(QString relay READ relay BINDABLE bindableRelay) + Q_PROPERTY(QString peerRelay READ peerRelay BINDABLE bindablePeerRelay) + Q_PROPERTY(qint64 receivedBytes READ receivedBytes BINDABLE bindableReceivedBytes) + Q_PROPERTY(qint64 transmittedBytes READ transmittedBytes BINDABLE bindableTransmittedBytes) + Q_PROPERTY(QDateTime created READ created BINDABLE bindableCreated) + Q_PROPERTY(QDateTime lastWrite READ lastWrite BINDABLE bindableLastWrite) + Q_PROPERTY(QDateTime lastSeen READ lastSeen BINDABLE bindableLastSeen) + Q_PROPERTY(QDateTime lastHandshake READ lastHandshake BINDABLE bindableLastHandshake) + Q_PROPERTY(bool online READ online BINDABLE bindableOnline) + Q_PROPERTY(bool exitNode READ exitNode BINDABLE bindableExitNode) + Q_PROPERTY(bool exitNodeOption READ exitNodeOption BINDABLE bindableExitNodeOption) + Q_PROPERTY(bool active READ active BINDABLE bindableActive) + Q_PROPERTY(QStringList peerApiUrls READ peerApiUrls BINDABLE bindablePeerApiUrls) + Q_PROPERTY(TaildropTargetStatus taildropTargetStatus READ taildropTargetStatus BINDABLE bindableTaildropTargetStatus) + Q_PROPERTY(QString noFileSharingReason READ noFileSharingReason BINDABLE bindableNoFileSharingReason) + Q_PROPERTY(QStringList sshHostKeys READ sshHostKeys BINDABLE bindableSshHostKeys) + Q_PROPERTY(bool shareeNode READ shareeNode BINDABLE bindableShareeNode) + Q_PROPERTY(bool inNetworkMap READ inNetworkMap BINDABLE bindableInNetworkMap) + Q_PROPERTY(bool inMagicSock READ inMagicSock BINDABLE bindableInMagicSock) + Q_PROPERTY(bool inEngine READ inEngine BINDABLE bindableInEngine) + Q_PROPERTY(bool expired READ expired BINDABLE bindableExpired) + Q_PROPERTY(QDateTime keyExpiry READ keyExpiry BINDABLE bindableKeyExpiry) + Q_PROPERTY(Location *location READ location BINDABLE bindableLocation) + +public: +private: + QProperty mId; + QProperty mPublicKey; + QProperty mHostName; + QProperty mDnsName; + QProperty mOs; // TODO(fk): replace with enum? + QProperty mUserId; + QProperty mSharerUserId; + QProperty mTailscaleIps; + QProperty mAllowedIps; + QProperty mTags; + QProperty mPrimaryRoutes; + QProperty mAddresses; + QProperty mCurrentAddress; + QProperty mRelay; + QProperty mPeerRelay; + QProperty mReceivedBytes; + QProperty mTransmittedBytes; + QProperty mCreated; + QProperty mLastWrite; + QProperty mLastSeen; + QProperty mLastHandshake; + QProperty mOnline; + QProperty mExitNode; + QProperty mExitNodeOption; + QProperty mActive; + QProperty mPeerApiUrls; + QProperty mTaildropTargetStatus; + QProperty mNoFileSharingReason; + // TODO(fk): capmap + QProperty mSshHostKeys; + QProperty mShareeNode; + QProperty mInNetworkMap; + QProperty mInMagicSock; + QProperty mInEngine; + QProperty mExpired; + QProperty mKeyExpiry; + QProperty mLocation; + +public: + explicit PeerStatus(QObject *parent = nullptr) + : QObject(parent) + { + } + + explicit PeerStatus(QJsonObject &json, QObject *parent = nullptr) + : QObject(parent) + { + updateFromJson(json); + } + + void updateFromJson(QJsonObject &json) + { + mId = json.take(QStringLiteral("ID")).toString(); + mPublicKey = json.take(QStringLiteral("PublicKey")).toString(); + mHostName = json.take(QStringLiteral("HostName")).toString(); + mDnsName = json.take(QStringLiteral("DNSName")).toString(); + mOs = json.take(QStringLiteral("OS")).toString(); + mUserId = json.take(QStringLiteral("UserId")).toInteger(); + mSharerUserId = json.take(QStringLiteral("AltSharerUserId")).toInteger(); + mTailscaleIps = json.take(QStringLiteral("TailscaleIPs")).toVariant().toStringList(); + mAllowedIps = json.take(QStringLiteral("AllowedIPs")).toVariant().toStringList(); + mTags = json.take(QStringLiteral("Tags")).toVariant().toStringList(); + mPrimaryRoutes = json.take(QStringLiteral("PrimaryRoutes")).toVariant().toStringList(); + mAddresses = json.take(QStringLiteral("Addresses")).toVariant().toStringList(); + mCurrentAddress = json.take(QStringLiteral("CurrentAddress")).toString(); + mRelay = json.take(QStringLiteral("Relay")).toString(); + mPeerRelay = json.take(QStringLiteral("PeerRelay")).toString(); + mReceivedBytes = json.take(QStringLiteral("ReceivedBytes")).toInteger(); + mTransmittedBytes = json.take(QStringLiteral("TransmittedBytes")).toInteger(); + // mCreated = json.take(QStringLiteral("Created")).toString() + // mLastWrite = json.take(QStringLiteral("LastWrite")).toString() + // mLastSeen = json.take(QStringLiteral("LastSeen")).toString() + // mLastHandshake = json.take(QStringLiteral("LastHandshake")).toString() + mOnline = json.take(QStringLiteral("Online")).toBool(); + mExitNode = json.take(QStringLiteral("ExitNode")).toBool(); + mExitNodeOption = json.take(QStringLiteral("ExitNodeOption")).toBool(); + mActive = json.take(QStringLiteral("Active")).toBool(); + mPeerApiUrls = json.take(QStringLiteral("PeerApiUrls")).toVariant().toStringList(); + // mTaildropTargetStatus + mNoFileSharingReason = json.take(QStringLiteral("NoFileSharingReason")).toString(); + mSshHostKeys = json.take(QStringLiteral("sshHostKeys")).toVariant().toStringList(); + mShareeNode = json.take(QStringLiteral("ShareeNode")).toBool(); + mInNetworkMap = json.take(QStringLiteral("InNetworkMap")).toVariant().toBool(); + mInMagicSock = json.take(QStringLiteral("InMagicSock")).toBool(); + mInEngine = json.take(QStringLiteral("InEngine")).toBool(); + mExpired = json.take(QStringLiteral("Expired")).toBool(); + // mKeyExpiry = json.take(QStringLiteral("KeyExpiry")).toString() + + if (json.contains(QStringLiteral("Location"))) { + auto locationJson = json.take(QStringLiteral("Location")).toObject(); + if (mLocation.value() == nullptr) { + mLocation = new Location(this); + } + mLocation->updateFromJson(locationJson); + } else { + if (mLocation.value() != nullptr) { + mLocation->deleteLater(); + mLocation = nullptr; + } + } + } + + // Getters + [[nodiscard]] const QString &id() const noexcept + { + return mId; + } + + [[nodiscard]] const QString &publicKey() const noexcept + { + return mPublicKey; + } + + [[nodiscard]] const QString &hostName() const noexcept + { + return mHostName; + } + + [[nodiscard]] const QString &dnsName() const noexcept + { + return mDnsName; + } + + [[nodiscard]] const QString &os() const noexcept + { + return mOs; + } + + [[nodiscard]] qint64 userId() const noexcept + { + return mUserId; + } + + [[nodiscard]] qint64 sharerUserId() const noexcept + { + return mSharerUserId; + } + + [[nodiscard]] const QStringList &tailscaleIps() const noexcept + { + return mTailscaleIps; + } + + [[nodiscard]] const QStringList &allowedIps() const noexcept + { + return mAllowedIps; + } + + [[nodiscard]] const QStringList &tags() const noexcept + { + return mTags; + } + + [[nodiscard]] const QStringList &primaryRoutes() const noexcept + { + return mPrimaryRoutes; + } + + [[nodiscard]] const QStringList &addresses() const noexcept + { + return mAddresses; + } + + [[nodiscard]] const QString ¤tAddress() const noexcept + { + return mCurrentAddress; + } + + [[nodiscard]] const QString &relay() const noexcept + { + return mRelay; + } + + [[nodiscard]] const QString &peerRelay() const noexcept + { + return mPeerRelay; + } + + [[nodiscard]] qint64 receivedBytes() const noexcept + { + return mReceivedBytes; + } + + [[nodiscard]] qint64 transmittedBytes() const noexcept + { + return mTransmittedBytes; + } + + [[nodiscard]] const QDateTime &created() const noexcept + { + return mCreated; + } + + [[nodiscard]] const QDateTime &lastWrite() const noexcept + { + return mLastWrite; + } + + [[nodiscard]] const QDateTime &lastSeen() const noexcept + { + return mLastSeen; + } + + [[nodiscard]] const QDateTime &lastHandshake() const noexcept + { + return mLastHandshake; + } + + [[nodiscard]] bool online() const noexcept + { + return mOnline; + } + + [[nodiscard]] bool exitNode() const noexcept + { + return mExitNode; + } + + [[nodiscard]] bool exitNodeOption() const noexcept + { + return mExitNodeOption; + } + + [[nodiscard]] bool active() const noexcept + { + return mActive; + } + + [[nodiscard]] const QStringList &peerApiUrls() const noexcept + { + return mPeerApiUrls; + } + + [[nodiscard]] TaildropTargetStatus taildropTargetStatus() const noexcept + { + return mTaildropTargetStatus; + } + + [[nodiscard]] const QString &noFileSharingReason() const noexcept + { + return mNoFileSharingReason; + } + + [[nodiscard]] const QStringList &sshHostKeys() const noexcept + { + return mSshHostKeys; + } + + [[nodiscard]] bool shareeNode() const noexcept + { + return mShareeNode; + } + + [[nodiscard]] bool inNetworkMap() const noexcept + { + return mInNetworkMap; + } + + [[nodiscard]] bool inMagicSock() const noexcept + { + return mInMagicSock; + } + + [[nodiscard]] bool inEngine() const noexcept + { + return mInEngine; + } + + [[nodiscard]] bool expired() const noexcept + { + return mExpired; + } + + [[nodiscard]] const QDateTime &keyExpiry() const noexcept + { + return mKeyExpiry; + } + + [[nodiscard]] Location *location() const noexcept + { + return mLocation; + } + + // Bindables + [[nodiscard]] QBindable bindableId() + { + return {&mId}; + } + + [[nodiscard]] QBindable bindablePublicKey() + { + return {&mPublicKey}; + } + + [[nodiscard]] QBindable bindableHostName() + { + return {&mHostName}; + } + + [[nodiscard]] QBindable bindableDnsName() + { + return {&mDnsName}; + } + + [[nodiscard]] QBindable bindableOs() + { + return {&mOs}; + } + + [[nodiscard]] QBindable bindableUserId() + { + return {&mUserId}; + } + + [[nodiscard]] QBindable bindableSharerUserId() + { + return {&mSharerUserId}; + } + + [[nodiscard]] QBindable bindableTailscaleIps() + { + return {&mTailscaleIps}; + } + + [[nodiscard]] QBindable bindableAllowedIps() + { + return {&mAllowedIps}; + } + + [[nodiscard]] QBindable bindableTags() + { + return {&mTags}; + } + + [[nodiscard]] QBindable bindablePrimaryRoutes() + { + return {&mPrimaryRoutes}; + } + + [[nodiscard]] QBindable bindableAddresses() + { + return {&mAddresses}; + } + + [[nodiscard]] QBindable bindableCurrentAddress() + { + return {&mCurrentAddress}; + } + + [[nodiscard]] QBindable bindableRelay() + { + return {&mRelay}; + } + + [[nodiscard]] QBindable bindablePeerRelay() + { + return {&mPeerRelay}; + } + + [[nodiscard]] QBindable bindableReceivedBytes() + { + return {&mReceivedBytes}; + } + + [[nodiscard]] QBindable bindableTransmittedBytes() + { + return {&mTransmittedBytes}; + } + + [[nodiscard]] QBindable bindableCreated() + { + return {&mCreated}; + } + + [[nodiscard]] QBindable bindableLastWrite() + { + return {&mLastWrite}; + } + + [[nodiscard]] QBindable bindableLastSeen() + { + return {&mLastSeen}; + } + + [[nodiscard]] QBindable bindableLastHandshake() + { + return {&mLastHandshake}; + } + + [[nodiscard]] QBindable bindableOnline() + { + return {&mOnline}; + } + + [[nodiscard]] QBindable bindableExitNode() + { + return {&mExitNode}; + } + + [[nodiscard]] QBindable bindableExitNodeOption() + { + return {&mExitNodeOption}; + } + + [[nodiscard]] QBindable bindableActive() + { + return {&mActive}; + } + + [[nodiscard]] QBindable bindablePeerApiUrls() + { + return {&mPeerApiUrls}; + } + + [[nodiscard]] QBindable bindableTaildropTargetStatus() + { + return {&mTaildropTargetStatus}; + } + + [[nodiscard]] QBindable bindableNoFileSharingReason() + { + return {&mNoFileSharingReason}; + } + + [[nodiscard]] QBindable bindableSshHostKeys() + { + return {&mSshHostKeys}; + } + + [[nodiscard]] QBindable bindableShareeNode() + { + return {&mShareeNode}; + } + + [[nodiscard]] QBindable bindableInNetworkMap() + { + return {&mInNetworkMap}; + } + + [[nodiscard]] QBindable bindableInMagicSock() + { + return {&mInMagicSock}; + } + + [[nodiscard]] QBindable bindableInEngine() + { + return {&mInEngine}; + } + + [[nodiscard]] QBindable bindableExpired() + { + return {&mExpired}; + } + + [[nodiscard]] QBindable bindableKeyExpiry() + { + return {&mKeyExpiry}; + } + + [[nodiscard]] QBindable bindableLocation() + { + return {&mLocation}; + } +}; + +#endif // KTAILCTL_PEER_STATUS_HPP diff --git a/src_new/tailscale/status/status.hpp b/src_new/tailscale/status/status.hpp new file mode 100644 index 00000000..5c5b8992 --- /dev/null +++ b/src_new/tailscale/status/status.hpp @@ -0,0 +1,295 @@ +#ifndef KTAILCTL_STATUS_HPP +#define KTAILCTL_STATUS_HPP + +#include "libktailctl_wrapper.h" +#include "logging_tailscale_status.hpp" + +#include "client_version.hpp" +#include "exit_node_status.hpp" +#include "peer_status.hpp" +#include "tailnet_status.hpp" +#include "user_profile.hpp" + +#include +#include +#include + +#include "property_list_model.hpp" + +// https://pkg.go.dev/tailscale.com/ipn/ipnstate#Status +class Status : public QObject +{ + Q_OBJECT + +public: + using PeerModel = PropertyListModel; + + enum class BackendState : uint8_t { NoState, NeedsLogin, NeedsMachineAuth, Stopped, Starting, Running }; + + Q_ENUM(BackendState) + + Q_PROPERTY(QString version READ version BINDABLE bindableVersion) + Q_PROPERTY(bool isTun READ isTun BINDABLE bindableIsTun) + Q_PROPERTY(BackendState backendState READ backendState BINDABLE bindableBackendState) + Q_PROPERTY(bool haveNodeKey READ haveNodeKey BINDABLE bindableHaveNodeKey) + Q_PROPERTY(QString authUrl READ authUrl BINDABLE bindableAuthUrl) + Q_PROPERTY(QStringList tailscaleIps READ tailscaleIps BINDABLE bindableTailscaleIps) + Q_PROPERTY(PeerStatus *self READ self BINDABLE bindableSelf) + Q_PROPERTY(ExitNodeStatus *exitNodeStatus READ exitNodeStatus BINDABLE bindableExitNodeStatus) + Q_PROPERTY(QStringList health READ health BINDABLE bindableHealth) + Q_PROPERTY(TailnetStatus *currentTailnet READ currentTailnet BINDABLE bindableCurrentTailnet) + Q_PROPERTY(PeerModel *peers READ peerModel CONSTANT) + Q_PROPERTY(QMap users READ users BINDABLE bindableUsers) + Q_PROPERTY(ClientVersion *clientVersion READ clientVersion BINDABLE bindableClientVersion) + +private: + QProperty mVersion; + QProperty mIsTun; + QProperty mBackendState; + QProperty mHaveNodeKey; + QProperty mAuthUrl; + QProperty mTailscaleIps; + QProperty mSelf; + QProperty mExitNodeStatus; + QProperty mHealth; + QProperty mCurrentTailnet; + QMap mPeers; + QProperty mPeerModel; + QProperty> mUsers; + QProperty mClientVersion; + +public: + explicit Status(QObject *parent = nullptr) + : QObject(parent) + , mPeerModel(new PeerModel(this)) + { + } + + Q_INVOKABLE void refresh() + { + const std::unique_ptr json_str(tailscale_status(), &free); + const QByteArray json_buffer(json_str.get(), ::strlen(json_str.get())); + QJsonParseError error; + QJsonDocument json = QJsonDocument::fromJson(json_buffer, &error); + if (error.error != QJsonParseError::NoError) { + qCCritical(Logging::Tailscale::Status) << error.errorString(); + return; + } + qCInfo(Logging::Tailscale::Status) << "Status refreshed"; + QJsonObject json_obj = json.object(); + + updateFromJson(json_obj); + } + + void updateFromJson(QJsonObject &json) + { + mVersion = json.take(QStringLiteral("Version")).toString(); + mIsTun = json.take(QStringLiteral("TUN")).toBool(); + // mBackendState = + mHaveNodeKey = json.take(QStringLiteral("HaveNodeKey")).toBool(); + mAuthUrl = json.take(QStringLiteral("AuthUrl")).toString(); + mTailscaleIps = json.take(QStringLiteral("TailscaleIps")).toVariant().toStringList(); + + if (json.contains(QStringLiteral("Self"))) [[likely]] { + auto selfJson = json.take(QStringLiteral("Self")).toObject(); + if (mSelf.value() == nullptr) [[unlikely]] { + mSelf = new PeerStatus(this); + } + mSelf->updateFromJson(selfJson); + } else [[unlikely]] { + if (mSelf.value() != nullptr) { + mSelf->deleteLater(); + mSelf = nullptr; + } + } + + if (json.contains(QStringLiteral("ExitNodeStatus"))) [[likely]] { + auto exitNodeStatusJson = json.take(QStringLiteral("ExitNodeStatus")).toObject(); + if (mExitNodeStatus.value() == nullptr) [[unlikely]] { + mExitNodeStatus = new ExitNodeStatus(this); + } + mExitNodeStatus->updateFromJson(exitNodeStatusJson); + } else [[unlikely]] { + if (mExitNodeStatus.value() != nullptr) { + mExitNodeStatus->deleteLater(); + mExitNodeStatus = nullptr; + } + } + + mHealth = json.take(QStringLiteral("Health")).toVariant().toStringList(); + if (json.contains(QStringLiteral("CurrentTailnet"))) [[likely]] { + auto tailnetJson = json.take(QStringLiteral("CurrentTailnet")).toObject(); + if (mCurrentTailnet.value() == nullptr) [[unlikely]] { + mCurrentTailnet = new TailnetStatus(this); + } + mCurrentTailnet->updateFromJson(tailnetJson); + } else [[unlikely]] { + if (mCurrentTailnet.value() != nullptr) { + mCurrentTailnet->deleteLater(); + mCurrentTailnet = nullptr; + } + } + + if (!json.contains(QStringLiteral("Peer"))) [[unlikely]] { + mPeerModel->clear(); + mPeers.clear(); + } else [[likely]] { + auto peerJson = json.take(QStringLiteral("Peer")).toObject(); + QSet peersToRemove(mPeers.keyBegin(), mPeers.keyEnd()); + for (auto [id, data] : peerJson.asKeyValueRange()) { + auto pos = mPeers.find(id.toString()); + if (pos == mPeers.end()) [[unlikely]] { + pos = mPeers.insert(id.toString(), new PeerStatus(this)); + mPeerModel->addItem(pos.value()); + } + auto obj = data.toObject(); + pos.value()->updateFromJson(obj); + peersToRemove.remove(id.toString()); + } + for (const auto &id : peersToRemove) { + auto pos = mPeers.find(id); + if (pos == mPeers.end()) [[unlikely]] { + // This should not happen + continue; + } + mPeerModel->removeItem(mPeerModel->indexOf(pos.value())); + pos.value()->deleteLater(); + mPeers.erase(pos); + } + } + } + + // Getters + [[nodiscard]] const QString &version() const noexcept + { + return mVersion; + } + + [[nodiscard]] bool isTun() const noexcept + { + return mIsTun; + } + + [[nodiscard]] BackendState backendState() const noexcept + { + return mBackendState; + } + + [[nodiscard]] bool haveNodeKey() const noexcept + { + return mHaveNodeKey; + } + + [[nodiscard]] const QString &authUrl() const noexcept + { + return mAuthUrl; + } + + [[nodiscard]] const QStringList &tailscaleIps() const noexcept + { + return mTailscaleIps; + } + + [[nodiscard]] PeerStatus *self() const noexcept + { + return mSelf; + } + + [[nodiscard]] ExitNodeStatus *exitNodeStatus() const noexcept + { + return mExitNodeStatus; + } + + [[nodiscard]] const QStringList &health() const noexcept + { + return mHealth; + } + + [[nodiscard]] TailnetStatus *currentTailnet() const noexcept + { + return mCurrentTailnet; + } + + [[nodiscard]] PeerModel *peerModel() const noexcept + { + return mPeerModel; + } + + [[nodiscard]] const QMap &users() const noexcept + { + return mUsers; + } + + [[nodiscard]] ClientVersion *clientVersion() const noexcept + { + return mClientVersion; + } + + // Bindables + [[nodiscard]] QBindable bindableVersion() + { + return {&mVersion}; + } + + [[nodiscard]] QBindable bindableIsTun() + { + return {&mIsTun}; + } + + [[nodiscard]] QBindable bindableBackendState() + { + return {&mBackendState}; + } + + [[nodiscard]] QBindable bindableHaveNodeKey() + { + return {&mHaveNodeKey}; + } + + [[nodiscard]] QBindable bindableAuthUrl() + { + return {&mAuthUrl}; + } + + [[nodiscard]] QBindable bindableTailscaleIps() + { + return {&mTailscaleIps}; + } + + [[nodiscard]] QBindable bindableSelf() + { + return {&mSelf}; + } + + [[nodiscard]] QBindable bindableExitNodeStatus() + { + return {&mExitNodeStatus}; + } + + [[nodiscard]] QBindable bindableHealth() + { + return {&mHealth}; + } + + [[nodiscard]] QBindable bindableCurrentTailnet() + { + return {&mCurrentTailnet}; + } + + [[nodiscard]] QBindable bindablePeerModel() + { + return {&mPeerModel}; + } + + [[nodiscard]] QBindable> bindableUsers() + { + return {&mUsers}; + } + + [[nodiscard]] QBindable bindableClientVersion() + { + return {&mClientVersion}; + } +}; + +#endif // KTAILCTL_STATUS_HPP diff --git a/src_new/tailscale/status/tailnet_status.hpp b/src_new/tailscale/status/tailnet_status.hpp new file mode 100644 index 00000000..bfd160e2 --- /dev/null +++ b/src_new/tailscale/status/tailnet_status.hpp @@ -0,0 +1,73 @@ +#ifndef KTAILCTL_TAILNET_STATUS_HPP +#define KTAILCTL_TAILNET_STATUS_HPP + +#include +#include +#include +#include + +class TailnetStatus : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name BINDABLE bindableName) + Q_PROPERTY(QString magicDnsSuffix READ magicDnsSuffix BINDABLE bindableMagicDnsSuffix) + Q_PROPERTY(bool magicDnsEnabled READ magicDnsEnabled BINDABLE bindableMagicDnsEnabled) + +private: + QProperty mName; + QProperty mMagicDnsSuffix; + QProperty mMagicDnsEnabled; + +public: + explicit TailnetStatus(QObject *parent = nullptr) + : QObject(parent) + { + } + + explicit TailnetStatus(QJsonObject &json, QObject *parent = nullptr) + : QObject(parent) + { + updateFromJson(json); + } + + void updateFromJson(QJsonObject &json) + { + mName = json.take(QStringLiteral("Name")).toString(); + mMagicDnsSuffix = json.take(QStringLiteral("MagicDnsSuffix")).toString(); + mMagicDnsEnabled = json.take(QStringLiteral("MagicDnsEnabled")).toBool(); + } + + // Getters + [[nodiscard]] const QString &name() const noexcept + { + return mName; + } + + [[nodiscard]] const QString &magicDnsSuffix() const noexcept + { + return mMagicDnsSuffix; + } + + [[nodiscard]] bool magicDnsEnabled() const noexcept + { + return mMagicDnsEnabled; + } + + // Bindables + [[nodiscard]] QBindable bindableName() + { + return {&mName}; + } + + [[nodiscard]] QBindable bindableMagicDnsSuffix() + { + return {&mMagicDnsSuffix}; + } + + [[nodiscard]] QBindable bindableMagicDnsEnabled() + { + return {&mMagicDnsEnabled}; + } +}; + +#endif // KTAILCTL_TAILNET_STATUS_HPP diff --git a/src_new/tailscale/status/user_profile.hpp b/src_new/tailscale/status/user_profile.hpp new file mode 100644 index 00000000..2631dc5f --- /dev/null +++ b/src_new/tailscale/status/user_profile.hpp @@ -0,0 +1,85 @@ +#ifndef KTAILCTL_USER_PROFILE_HPP +#define KTAILCTL_USER_PROFILE_HPP + +#include +#include +#include + +class UserProfile : public QObject +{ + Q_OBJECT + Q_PROPERTY(qint64 userId READ userId BINDABLE bindableUserId) + Q_PROPERTY(QString loginName READ loginName BINDABLE bindableLoginName) + Q_PROPERTY(QString displayName READ displayName BINDABLE bindableDisplayName) + Q_PROPERTY(QString profilePicUrl READ profilePicUrl BINDABLE bindableProfilePicUrl) + +private: + QProperty mUserId; + QProperty mLoginName; + QProperty mDisplayName; + QProperty mProfilePicUrl; + +public: + explicit UserProfile(QObject *parent = nullptr) + : QObject(parent) + { + } + + explicit UserProfile(QJsonObject &json, QObject *parent = nullptr) + : QObject(parent) + { + updateFromJson(json); + } + + void updateFromJson(QJsonObject &json) + { + mUserId = json.take(QStringLiteral("UserID")).toInteger(); + mLoginName = json.take(QStringLiteral("LoginName")).toString(); + mDisplayName = json.take(QStringLiteral("DisplayName")).toString(); + mProfilePicUrl = json.take(QStringLiteral("ProfilePicUrl")).toString(); + } + + // Getters + [[nodiscard]] qint64 userId() const noexcept + { + return mUserId; + } + + [[nodiscard]] const QString &loginName() const noexcept + { + return mLoginName; + } + + [[nodiscard]] const QString &displayName() const noexcept + { + return mDisplayName; + } + + [[nodiscard]] const QString &profilePicUrl() const noexcept + { + return mProfilePicUrl; + } + + // Bindables + [[nodiscard]] QBindable bindableUserId() + { + return {&mUserId}; + } + + [[nodiscard]] QBindable bindableLoginName() + { + return {&mLoginName}; + } + + [[nodiscard]] QBindable bindableDisplayName() + { + return {&mDisplayName}; + } + + [[nodiscard]] QBindable bindableProfilePicUrl() + { + return {&mProfilePicUrl}; + } +}; + +#endif // KTAILCTL_USER_PROFILE_HPP diff --git a/src_new/tailscale/tailscale.hpp b/src_new/tailscale/tailscale.hpp new file mode 100644 index 00000000..e3c0b993 --- /dev/null +++ b/src_new/tailscale/tailscale.hpp @@ -0,0 +1,31 @@ +#ifndef KTAILCTL_TAILSCALE_NEW_HPP +#define KTAILCTL_TAILSCALE_NEW_HPP + +#include "property_list_model.hpp" +#include "status/status.hpp" +#include + +class TailscaleNew : public QObject +{ +public: + Q_OBJECT + Q_PROPERTY(Status *status READ status CONSTANT) + +private: + Status *mStatus; + +public: + explicit TailscaleNew(QObject *parent = nullptr) + : QObject(parent) + , mStatus(new Status(this)) + { + mStatus->refresh(); + } + + [[nodiscard]] Status *status() const noexcept + { + return mStatus; + } +}; + +#endif // KTAILCTL_TAILSCALE_HPP diff --git a/src/wrapper/.gitignore b/src_new/tailscale/wrapper/.gitignore similarity index 100% rename from src/wrapper/.gitignore rename to src_new/tailscale/wrapper/.gitignore diff --git a/src/wrapper/CMakeLists.txt b/src_new/tailscale/wrapper/CMakeLists.txt similarity index 63% rename from src/wrapper/CMakeLists.txt rename to src_new/tailscale/wrapper/CMakeLists.txt index 80c41203..71c84886 100644 --- a/src/wrapper/CMakeLists.txt +++ b/src_new/tailscale/wrapper/CMakeLists.txt @@ -1,26 +1,37 @@ -if(NOT DEFINED KTAILCTL_WRAPPER_GO_EXECUTABLE) - if(DEFINED $ENV{KTAILCTL_WRAPPER_GO_EXECUTABLE}) - set(KTAILCTL_WRAPPER_GO_EXECUTABLE $ENV{KTAILCTL_WRAPPER_GO_EXECUTABLE}) +if(NOT DEFINED GO_EXECUTABLE) + if(DEFINED $ENV{GO_EXECUTABLE}) + set(GO_EXECUTABLE $ENV{GO_EXECUTABLE}) else() - find_program(KTAILCTL_WRAPPER_GO_EXECUTABLE go REQUIRED) + find_program(GO_EXECUTABLE go REQUIRED) endif() endif() -message(STATUS "Go executable: ${KTAILCTL_WRAPPER_GO_EXECUTABLE}") -execute_process(COMMAND ${KTAILCTL_WRAPPER_GO_EXECUTABLE} version OUTPUT_VARIABLE KTAILCTL_WRAPPER_GO_VERSION) -message(STATUS "Go version: ${KTAILCTL_WRAPPER_GO_VERSION}") +message(STATUS "Go executable: ${GO_EXECUTABLE}") +execute_process(COMMAND ${GO_EXECUTABLE} version OUTPUT_VARIABLE GO_VERSION) +message(STATUS "Go version: ${GO_VERSION}") + +ecm_qt_declare_logging_category( + ktailctl + HEADER + "logging_tailscale_wrapper.hpp" + IDENTIFIER + "Logging::Tailscale::Wrapper" + CATEGORY_NAME + "org.fkoehler.KTailctl.Tailscale.Wrapper" + DEFAULT_SEVERITY + Info) add_library(ktailctl_wrapper_logging SHARED logging.cpp) target_link_libraries(ktailctl_wrapper_logging PUBLIC Qt6::Core) target_compile_definitions(ktailctl_wrapper_logging PRIVATE QT_MESSAGELOGCONTEXT) -target_include_directories(ktailctl_wrapper_logging PRIVATE ${PROJECT_BINARY_DIR}/src) +target_include_directories(ktailctl_wrapper_logging PRIVATE ${CMAKE_CURRENT_BINARY_DIR}}) -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/logging.go.in ${CMAKE_CURRENT_SOURCE_DIR}/logging.go @ONLY) +configure_file(logging.go.in ${CMAKE_CURRENT_SOURCE_DIR}/logging.go @ONLY) if(NOT KTAILCTL_FLATPAK_BUILD) add_custom_command( - OUTPUT go.sum - COMMAND ${KTAILCTL_WRAPPER_GO_EXECUTABLE} get . - MAIN_DEPENDENCY go.mod + OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/go.sum + COMMAND ${GO_EXECUTABLE} get . + MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/go.mod WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Downloading Go dependencies") endif() @@ -33,11 +44,11 @@ message(STATUS "KTailctl wrapper header: ${KTAILCTL_WRAPPER_HEADER}") if(CMAKE_BUILD_TYPE STREQUAL "Debug") add_custom_command( OUTPUT ${KTAILCTL_WRAPPER_LIBRARY} ${KTAILCTL_WRAPPER_HEADER} - COMMAND GOEXPERIMENT=cgocheck2 ${KTAILCTL_WRAPPER_GO_EXECUTABLE} build -v -trimpath -mod vendor -o - ${KTAILCTL_WRAPPER_LIBRARY} -buildmode=c-archive wrapper.go options.go taildrop.go callbacks.go logging.go + COMMAND GOEXPERIMENT=cgocheck2 ${GO_EXECUTABLE} build -v -trimpath -mod vendor -o ${KTAILCTL_WRAPPER_LIBRARY} + -buildmode=c-archive wrapper.go options.go taildrop.go callbacks.go logging.go MAIN_DEPENDENCY wrapper.go DEPENDS go.mod - go.sum + ${CMAKE_CURRENT_SOURCE_DIR}/go.sum ktailctl_wrapper_logging options.go taildrop.go @@ -48,11 +59,11 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") else() add_custom_command( OUTPUT ${KTAILCTL_WRAPPER_LIBRARY} ${KTAILCTL_WRAPPER_HEADER} - COMMAND ${KTAILCTL_WRAPPER_GO_EXECUTABLE} build -v -trimpath -mod vendor -o ${KTAILCTL_WRAPPER_LIBRARY} - -buildmode=c-archive wrapper.go options.go taildrop.go callbacks.go logging.go + COMMAND ${GO_EXECUTABLE} build -v -trimpath -mod vendor -o ${KTAILCTL_WRAPPER_LIBRARY} -buildmode=c-archive + wrapper.go options.go taildrop.go callbacks.go logging.go MAIN_DEPENDENCY wrapper.go DEPENDS go.mod - go.sum + ${CMAKE_CURRENT_SOURCE_DIR}/go.sum ktailctl_wrapper_logging options.go taildrop.go @@ -78,3 +89,6 @@ add_library(KTailctl::WrapperLogging ALIAS ktailctl_wrapper_logging) install(FILES ${KTAILCTL_WRAPPER_LIBRARY} TYPE LIB) install(TARGETS ktailctl_wrapper_logging) + +target_link_libraries(new_ktailctl PRIVATE KTailctl::Wrapper KTailctl::WrapperLogging) +target_sources(new_ktailctl PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/logging_tailscale_wrapper.cpp) diff --git a/src/wrapper/callbacks.go b/src_new/tailscale/wrapper/callbacks.go similarity index 100% rename from src/wrapper/callbacks.go rename to src_new/tailscale/wrapper/callbacks.go diff --git a/src/wrapper/go.mod b/src_new/tailscale/wrapper/go.mod similarity index 95% rename from src/wrapper/go.mod rename to src_new/tailscale/wrapper/go.mod index 43d0d0ed..cc56fb2e 100644 --- a/src/wrapper/go.mod +++ b/src_new/tailscale/wrapper/go.mod @@ -1,4 +1,4 @@ -module tailwrap +module tailscale_wrapper go 1.25.4 @@ -43,8 +43,8 @@ require ( github.com/x448/float16 v0.8.4 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/crypto v0.44.0 // indirect + golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/image v0.27.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.18.0 // indirect diff --git a/src/wrapper/go.sum b/src_new/tailscale/wrapper/go.sum similarity index 96% rename from src/wrapper/go.sum rename to src_new/tailscale/wrapper/go.sum index 3e944a90..18d07665 100644 --- a/src/wrapper/go.sum +++ b/src_new/tailscale/wrapper/go.sum @@ -86,10 +86,10 @@ go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4 go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= diff --git a/src_new/tailscale/wrapper/logging.cpp b/src_new/tailscale/wrapper/logging.cpp new file mode 100644 index 00000000..1722dd22 --- /dev/null +++ b/src_new/tailscale/wrapper/logging.cpp @@ -0,0 +1,23 @@ +#include "logging_tailscale_wrapper.hpp" +#include +#include + +extern "C" { + +void ktailctl_critical(const char *message) +{ + qCCritical(Logging::Tailscale::Wrapper, "%s", message); +} +void ktailctl_debug(const char *message) +{ + qCDebug(Logging::Tailscale::Wrapper, "%s", message); +} +void ktailctl_info(const char *message) +{ + qCInfo(Logging::Tailscale::Wrapper, "%s", message); +} +void ktailctl_warning(const char *message) +{ + qCWarning(Logging::Tailscale::Wrapper, "%s", message); +} +} diff --git a/src/wrapper/logging.go.in b/src_new/tailscale/wrapper/logging.go.in similarity index 100% rename from src/wrapper/logging.go.in rename to src_new/tailscale/wrapper/logging.go.in diff --git a/src/wrapper/logging.h b/src_new/tailscale/wrapper/logging.h similarity index 100% rename from src/wrapper/logging.h rename to src_new/tailscale/wrapper/logging.h diff --git a/src/wrapper/options.go b/src_new/tailscale/wrapper/options.go similarity index 100% rename from src/wrapper/options.go rename to src_new/tailscale/wrapper/options.go diff --git a/src/wrapper/taildrop.go b/src_new/tailscale/wrapper/taildrop.go similarity index 100% rename from src/wrapper/taildrop.go rename to src_new/tailscale/wrapper/taildrop.go diff --git a/src/wrapper/wrapper.go b/src_new/tailscale/wrapper/wrapper.go similarity index 100% rename from src/wrapper/wrapper.go rename to src_new/tailscale/wrapper/wrapper.go diff --git a/src_new/ui/CMakeLists.txt b/src_new/ui/CMakeLists.txt new file mode 100644 index 00000000..081c8743 --- /dev/null +++ b/src_new/ui/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(new_ktailctl PRIVATE ui.qrc) diff --git a/src_new/ui/Main.qml b/src_new/ui/Main.qml new file mode 100644 index 00000000..c514912b --- /dev/null +++ b/src_new/ui/Main.qml @@ -0,0 +1,66 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQml +import org.kde.kirigami as Kirigami +import org.fkoehler.KTailctl as KTailctl + +Kirigami.ApplicationWindow { + id: root + + globalDrawer: Kirigami.GlobalDrawer { + id: globalDrawer + + collapsed: true + collapsible: true + modal: false + title: "KTailctl" + titleIcon: ":/icons/logo.svg" + + actions: [ + Kirigami.PagePoolAction { + icon.name: "speedometer" + page: "qrc:/ui/pages/PeerList.qml" + pagePool: mainPagePool + text: i18n("Peers") + }, + Kirigami.PagePoolAction { + icon.name: "globe" + page: "qrc:/ui/pages/ExitNodeList.qml" + pagePool: mainPagePool + text: i18n("Exit Nodes") + }, + Kirigami.PagePoolAction { + icon.name: "help-about" + page: "qrc:/ui/pages/About.qml" + pagePool: mainPagePool + text: i18n("About") + }, + Kirigami.Action { + icon.name: "process-stop" + text: i18n("Stop tailscale") + }, + Kirigami.Action { + icon.name: "application-exit" + text: i18n("Quit") + + onTriggered: Qt.quit() + } + ] + } + + Kirigami.PagePool { + id: mainPagePool + } + + pageStack.initialPage: "qrc:/ui/pages/PeerList.qml" + + Timer { + interval: 5000 + repeat: true + running: true + triggeredOnStart: false + + onTriggered: KTailctl.Tailscale.status.refresh() + } +} diff --git a/src_new/ui/pages/About.qml b/src_new/ui/pages/About.qml new file mode 100644 index 00000000..b80e382b --- /dev/null +++ b/src_new/ui/pages/About.qml @@ -0,0 +1,7 @@ +import org.kde.kirigamiaddons.formcard as FormCard +import org.fkoehler.KTailctl as KTailctl + +FormCard.AboutPage { + aboutData: KTailctl.About + objectName: "About" +} diff --git a/src_new/ui/pages/ExitNodeList.qml b/src_new/ui/pages/ExitNodeList.qml new file mode 100644 index 00000000..72f2c94f --- /dev/null +++ b/src_new/ui/pages/ExitNodeList.qml @@ -0,0 +1,74 @@ +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick +import org.kde.kirigami as Kirigami +import org.fkoehler.KTailctl as KTailctl +import QtQml.Models as Models + +Kirigami.ScrollablePage { + Layout.fillWidth: true + + Models.SortFilterProxyModel { + id: exitNodeModel + model: KTailctl.Tailscale.status.peers + filters: [ + Models.ValueFilter { + roleName: "exitNodeOption" + value: true + } + ] + } + + ListView { + anchors.fill: parent + model: exitNodeModel + + delegate: ItemDelegate { + width: ListView.view.width + id: delegate + + // text: dnsName + contentItem: RowLayout { + Kirigami.Icon { + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.text: online ? "Online" : "Offline" + ToolTip.visible: hovered + source: online ? "online" : "offline" + } + + ToolButton { + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.text: "Copy DNS name to clipboard" + ToolTip.visible: hovered + icon.name: "edit-copy" + text: dnsName + onClicked: KTailctl.Util.setClipboardText(dnsName) + } + + ToolButton { + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.text: "Copy IP address to clipboard" + ToolTip.visible: hovered + icon.name: "edit-copy" + text: tailscaleIps[0] + onClicked: KTailctl.Util.setClipboardText(tailscaleIps[0]) + } + + Item { + Layout.fillWidth: true + } + + ToolButton { + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.text: "More actions" + ToolTip.visible: hovered + icon.name: "menu_new" + } + + Item { + width: Kirigami.Units.largeSpacing + } + } + } + } +} diff --git a/src_new/ui/pages/PeerInfo.qml b/src_new/ui/pages/PeerInfo.qml new file mode 100644 index 00000000..4fe84073 --- /dev/null +++ b/src_new/ui/pages/PeerInfo.qml @@ -0,0 +1,73 @@ +import org.kde.kirigamiaddons.formcard as FormCard +import org.fkoehler.KTailctl as KTailctl +import QtQuick + +FormCard.FormCardPage { + id: page + property KTailctl.PeerStatus peer: KTailctl.Tailscale.status.self + + FormCard.FormHeader { + title: "Identity" + } + + FormCard.FormCard { + FormCard.FormTextDelegate { + id: peerId + text: page.peer.id + } + + FormCard.FormDelegateSeparator { + below: peerId + above: publicKey + } + + FormCard.FormTextDelegate { + id: publicKey + text: page.peer.publicKey + } + + FormCard.FormDelegateSeparator { + below: publicKey + above: dnsName + } + + FormCard.FormTextDelegate { + id: dnsName + text: page.peer.dnsName + } + + FormCard.FormDelegateSeparator { + below: dnsName + above: hostName + } + + FormCard.FormTextDelegate { + id: hostName + text: page.peer.hostName + } + + FormCard.FormDelegateSeparator { + below: hostName + above: os + } + + FormCard.FormTextDelegate { + id: os + text: page.peer.os + } + } + + FormCard.FormHeader { + title: "Addresses" + } + + FormCard.FormCard { + Repeater { + model: page.peer.tailscaleIps + + FormCard.FormTextDelegate { + text: modelData + } + } + } +} \ No newline at end of file diff --git a/src_new/ui/pages/PeerList.qml b/src_new/ui/pages/PeerList.qml new file mode 100644 index 00000000..3e51f041 --- /dev/null +++ b/src_new/ui/pages/PeerList.qml @@ -0,0 +1,89 @@ +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick +import org.kde.kirigami as Kirigami +import org.fkoehler.KTailctl as KTailctl + +Kirigami.ScrollablePage { + id: page + + Layout.fillWidth: true + + actions: [ + Kirigami.Action { + id: customAction + + icon.name: "search" + text: "Custom Component" + + displayComponent: Kirigami.SearchField { + } + } + ] + + ListView { + anchors.fill: parent + model: KTailctl.Tailscale.status.peers + + delegate: ItemDelegate { + width: ListView.view.width + id: delegate + + contentItem: RowLayout { + Kirigami.Icon { + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.text: online ? "Online" : "Offline" + ToolTip.visible: hovered + source: online ? "online" : "offline" + } + + ToolButton { + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.text: "Copy DNS name to clipboard" + ToolTip.visible: hovered + icon.name: "edit-copy" + text: dnsName + onClicked: KTailctl.Util.setClipboardText(dnsName) + } + + ToolButton { + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.text: "Copy IP address to clipboard" + ToolTip.visible: hovered + icon.name: "edit-copy" + text: tailscaleIps[0] + onClicked: KTailctl.Util.setClipboardText(tailscaleIps[0]) + } + + Item { + Layout.fillWidth: true + } + + ToolButton { + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.text: "More actions" + ToolTip.visible: hovered + icon.name: "menu_new" + onClicked: menu.open() + + Menu { + id: menu + MenuItem { + icon.name: "icon_details" + text: "Node info" + // onClicked: root.pageStack.push("qrc:/ui/pages/PeerInfo.qml") + } + } + } + + Item { + width: Kirigami.Units.largeSpacing + } + } + } + } + Component { + id: pagePeerInfo + PeerInfo {} + } +} diff --git a/src_new/ui/ui.qrc b/src_new/ui/ui.qrc new file mode 100644 index 00000000..01167f3e --- /dev/null +++ b/src_new/ui/ui.qrc @@ -0,0 +1,9 @@ + + + ./Main.qml + ./pages/About.qml + ./pages/ExitNodeList.qml + ./pages/PeerList.qml + ./pages/PeerInfo.qml + + \ No newline at end of file diff --git a/src_new/util.hpp b/src_new/util.hpp new file mode 100644 index 00000000..395ba0b5 --- /dev/null +++ b/src_new/util.hpp @@ -0,0 +1,24 @@ +#ifndef KTAILCTL_UTIL_HPP +#define KTAILCTL_UTIL_HPP + +#include +#include +#include +#include + +class Util : public QObject +{ + Q_OBJECT + +public: + explicit Util(QObject *parent = nullptr) : QObject(parent) {} + + Q_INVOKABLE void setClipboardText(const QString &text) + { + auto *data = new QMimeData(); + data->setData(QStringLiteral("text/plain"), text.toUtf8()); + KSystemClipboard::instance()->setMimeData(data, QClipboard::Clipboard); + } +}; + +#endif // KTAILCTL_UTIL_HPP