From 7dba9bb01a90030742dbe485367415ada4b2d80e Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Tue, 24 Jan 2023 15:26:11 -0700 Subject: [PATCH 01/15] Cleanup based on various issue checking tools (#281) * Use getpwnam_r and getgrnam_r and other cleanup * Cleanup from codeclimate recommendations * Avoid duplicate runs * Remove unused --- .github/workflows/build.yml | 2 ++ README.md | 4 ++- applications/launcher/appitem.h | 1 + applications/launcher/notificationlist.h | 2 ++ applications/launcher/wifinetworklist.h | 2 ++ applications/lockscreen/controller.h | 3 +- applications/process-manager/taskitem.h | 1 + applications/screenshot-viewer/controller.h | 1 + .../screenshot-viewer/screenshotlist.h | 5 +++ applications/system-service/application.h | 31 ++++++++++++++----- applications/system-service/appsapi.h | 1 + applications/system-service/bss.h | 1 + applications/system-service/dbusservice.h | 1 + .../system-service/digitizerhandler.h | 1 + applications/system-service/fifohandler.h | 5 ++- applications/system-service/network.h | 1 + applications/system-service/notification.h | 1 + applications/system-service/notificationapi.h | 1 + applications/system-service/powerapi.h | 3 +- applications/system-service/screenapi.h | 1 + applications/system-service/screenshot.h | 1 + applications/system-service/systemapi.h | 3 +- applications/system-service/wifiapi.h | 4 +-- applications/system-service/wlan.h | 1 + applications/task-switcher/controller.h | 1 + shared/liboxide/liboxide.cpp | 24 -------------- shared/liboxide/liboxide.h | 4 +++ shared/liboxide/settingsfile.cpp | 9 ++---- web/src/_themes/oxide/static/oxide.css | 4 +-- 29 files changed, 70 insertions(+), 49 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0fc23086..5360e4b9e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,6 +2,8 @@ name: Build oxide on: push: + branches: + - master paths: - 'applications/**' - 'shared/**' diff --git a/README.md b/README.md index 7a5394bbc..a55cf0b11 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ [![Discord](https://img.shields.io/discord/385916768696139794.svg?label=reMarkable&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/ATqQGfu) # Oxide + A launcher application for the [reMarkable tablet](https://remarkable.com/). Head over to the [releases](https://github.com/Eeems/oxide/releases) page for more information on the latest release. You can also see some (likely outdated) [screenshots here](https://github.com/Eeems/oxide/wiki/Screenshots). -Here is an outdated video of it in action: +Here is an outdated video of it in action: [![Oxide v2.0-beta](https://i.imgur.com/1Q9A4NF.png)](https://youtu.be/rIRKgqy21L0 "Oxide v2.0-beta") ## Building @@ -19,6 +20,7 @@ Here is an outdated video of it in action: Install the reMarkable toolchain and then run `make release`. It will produce a folder named `release` containing all the output. ### Nix + Works on x86_64-linux or macOS with [linuxkit-nix](https://github.com/nix-community/linuxkit-nix). diff --git a/applications/launcher/appitem.h b/applications/launcher/appitem.h index 67f3380e1..5f123b87a 100644 --- a/applications/launcher/appitem.h +++ b/applications/launcher/appitem.h @@ -13,6 +13,7 @@ using namespace codes::eeems::oxide1; class AppItem : public QObject { Q_OBJECT + public: AppItem(QObject* parent) : QObject(parent){} diff --git a/applications/launcher/notificationlist.h b/applications/launcher/notificationlist.h index 558c2cdb7..7a6ace23b 100644 --- a/applications/launcher/notificationlist.h +++ b/applications/launcher/notificationlist.h @@ -13,6 +13,7 @@ class NotificationItem : public QObject { Q_PROPERTY(QString text READ text NOTIFY textChanged) Q_PROPERTY(QString icon READ icon NOTIFY iconChanged) Q_PROPERTY(int created READ created NOTIFY createdChanged) + public: NotificationItem(Notification* notification, QObject* parent) : QObject(parent) { m_identifier = notification->identifier(); @@ -71,6 +72,7 @@ public slots: class NotificationList : public QAbstractListModel { Q_OBJECT + public: NotificationList() : QAbstractListModel(nullptr) {} diff --git a/applications/launcher/wifinetworklist.h b/applications/launcher/wifinetworklist.h index 039c78d9f..a4a9c813d 100644 --- a/applications/launcher/wifinetworklist.h +++ b/applications/launcher/wifinetworklist.h @@ -20,6 +20,7 @@ class WifiNetwork : public QObject { Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) Q_PROPERTY(bool available READ available NOTIFY availableChanged) Q_PROPERTY(bool known READ known NOTIFY knownChanged) + public: WifiNetwork(Network* network, Wifi* api, QObject* parent) : QObject(parent), @@ -137,6 +138,7 @@ class WifiNetworkList : public QAbstractListModel { Q_OBJECT Q_PROPERTY(bool scanning READ scanning NOTIFY scanningChanged); + public: explicit WifiNetworkList() : QAbstractListModel(nullptr), networks() {} diff --git a/applications/lockscreen/controller.h b/applications/lockscreen/controller.h index cbea2b000..ad041dc9e 100644 --- a/applications/lockscreen/controller.h +++ b/applications/lockscreen/controller.h @@ -30,6 +30,7 @@ class Controller : public QObject { Q_PROPERTY(bool telemetry READ telemetry WRITE setTelemetry NOTIFY telemetryChanged) Q_PROPERTY(bool applicationUsage READ applicationUsage WRITE setApplicationUsage NOTIFY applicationUsageChanged) Q_PROPERTY(bool crashReport READ crashReport WRITE setCrashReport NOTIFY crashReportChanged) + public: Controller(QObject* parent) : QObject(parent), confirmPin(), settings(this) { @@ -173,7 +174,6 @@ class Controller : public QObject { setState("loading"); }); }); - } void launchStartupApp(){ QDBusObjectPath path = appsApi->startupApplication(); @@ -230,7 +230,6 @@ class Controller : public QObject { }); }); return true; - }else if(state == "loaded"){ qDebug() << "PIN doesn't match!"; onFailedLogin(); diff --git a/applications/process-manager/taskitem.h b/applications/process-manager/taskitem.h index 0d54887c1..b3d8cfeb6 100755 --- a/applications/process-manager/taskitem.h +++ b/applications/process-manager/taskitem.h @@ -21,6 +21,7 @@ class TaskItem : public QObject { Q_PROPERTY(bool killable MEMBER _killable READ killable WRITE setKillable NOTIFY killableChanged); Q_PROPERTY(uint cpu MEMBER _cpu READ cpu WRITE setCpu NOTIFY cpuChanged); Q_PROPERTY(QString mem MEMBER _mem READ mem WRITE setMem NOTIFY memChanged); + public: TaskItem(int pid, QObject* parent) : QObject(parent), diff --git a/applications/screenshot-viewer/controller.h b/applications/screenshot-viewer/controller.h index ebae93f2e..ad319b62d 100644 --- a/applications/screenshot-viewer/controller.h +++ b/applications/screenshot-viewer/controller.h @@ -28,6 +28,7 @@ class Controller : public QObject { Q_OBJECT Q_PROPERTY(ScreenshotList* screenshots MEMBER screenshots READ getScreenshots NOTIFY screenshotsChanged) Q_PROPERTY(int columns READ columns WRITE setColumns NOTIFY columnsChanged) + public: Controller(QObject* parent) : QObject(parent), settings(this), applications() { diff --git a/applications/screenshot-viewer/screenshotlist.h b/applications/screenshot-viewer/screenshotlist.h index 03e896a2f..767496624 100644 --- a/applications/screenshot-viewer/screenshotlist.h +++ b/applications/screenshot-viewer/screenshotlist.h @@ -11,6 +11,7 @@ class ScreenshotItem : public QObject { Q_OBJECT Q_PROPERTY(QString path READ path NOTIFY pathChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged) + public: ScreenshotItem(Screenshot* screenshot, QObject* parent) : QObject(parent) { m_screenshot = screenshot; @@ -40,6 +41,7 @@ class ScreenshotItem : public QObject { return true; } QString qPath() { return m_screenshot->QDBusAbstractInterface::path(); } + signals: void pathChanged(QString); void nameChanged(QString); @@ -58,6 +60,7 @@ public slots: class ScreenshotList : public QAbstractListModel { Q_OBJECT + public: ScreenshotList() : QAbstractListModel(nullptr) {} @@ -151,8 +154,10 @@ class ScreenshotList : public QAbstractListModel } int length() { return screenshots.length(); } bool empty() { return screenshots.empty(); } + signals: void updated(); + private: QList screenshots; }; diff --git a/applications/system-service/application.h b/applications/system-service/application.h index 6788e19a6..746c8ffff 100644 --- a/applications/system-service/application.h +++ b/applications/system-service/application.h @@ -43,6 +43,7 @@ class SandBoxProcess : public QProcess{ Q_OBJECT + public: SandBoxProcess(QObject* parent = nullptr) : QProcess(parent), m_gid(0), m_uid(0), m_chroot(""), m_mask(0) {} @@ -80,6 +81,7 @@ class SandBoxProcess : public QProcess{ void setMask(mode_t mask){ m_mask = mask; } + protected: void setupChildProcess() override { // Drop all privileges in the child process @@ -95,6 +97,7 @@ class SandBoxProcess : public QProcess{ setsid(); prctl(PR_SET_PDEATHSIG, SIGTERM); } + private: gid_t m_gid; uid_t m_uid; @@ -102,18 +105,30 @@ class SandBoxProcess : public QProcess{ mode_t m_mask; uid_t getUID(const QString& name){ - auto user = getpwnam(name.toStdString().c_str()); - if(user == NULL){ + char buffer[1024]; + struct passwd user; + struct passwd* result; + auto status = getpwnam_r(name.toStdString().c_str(), &user, buffer, sizeof(buffer), &result); + if(status != 0){ + throw std::runtime_error("Failed to get user" + status); + } + if(result == NULL){ throw std::runtime_error("Invalid user name: " + name.toStdString()); } - return user->pw_uid; + return result->pw_uid; } gid_t getGID(const QString& name){ - auto group = getgrnam(name.toStdString().c_str()); - if(group == NULL){ + char buffer[1024]; + struct group grp; + struct group* result; + auto status = getgrnam_r(name.toStdString().c_str(), &grp, buffer, sizeof(buffer), &result); + if(status != 0){ + throw std::runtime_error("Failed to get group" + status); + } + if(result == NULL){ throw std::runtime_error("Invalid group name: " + name.toStdString()); } - return group->gr_gid; + return result->gr_gid; } }; @@ -144,6 +159,7 @@ class Application : public QObject{ Q_PROPERTY(QString group READ group) Q_PROPERTY(QStringList directories READ directories WRITE setDirectories NOTIFY directoriesChanged) Q_PROPERTY(QByteArray screenCapture READ screenCapture) + public: Application(QDBusObjectPath path, QObject* parent) : Application(path.path(), parent) {} Application(QString path, QObject* parent) : QObject(parent), m_path(path), m_backgrounded(false), fifos() { @@ -396,6 +412,7 @@ class Application : public QObject{ void uninterruptApplication(); void waitForPause(); void waitForResume(); + signals: void launched(); void paused(); @@ -473,6 +490,7 @@ private slots: } void errorOccurred(QProcess::ProcessError error); void powerStateDataRecieved(FifoHandler* handler, const QString& data); + private: QVariantMap m_config; QString m_path; @@ -733,7 +751,6 @@ private slots: if(mount.startsWith("/")){ activeMounts.append(mount); } - } mounts.close(); activeMounts.sort(Qt::CaseSensitive); diff --git a/applications/system-service/appsapi.h b/applications/system-service/appsapi.h index a04bd09de..2c2f7c523 100644 --- a/applications/system-service/appsapi.h +++ b/applications/system-service/appsapi.h @@ -36,6 +36,7 @@ class AppsAPI : public APIBase { Q_PROPERTY(QDBusObjectPath currentApplication READ currentApplication) Q_PROPERTY(QVariantMap runningApplications READ runningApplications) Q_PROPERTY(QVariantMap pausedApplications READ pausedApplications) + public: static AppsAPI* singleton(AppsAPI* self = nullptr){ static AppsAPI* instance; diff --git a/applications/system-service/bss.h b/applications/system-service/bss.h index 072c12aee..e1e88ce37 100644 --- a/applications/system-service/bss.h +++ b/applications/system-service/bss.h @@ -19,6 +19,7 @@ class BSS : public QObject{ Q_PROPERTY(ushort signal READ signal) Q_PROPERTY(QDBusObjectPath network READ network) Q_PROPERTY(QStringList key_mgmt READ key_mgmt) + public: BSS(QString path, QString bssid, QString ssid, QObject* parent); BSS(QString path, IBSS* bss, QObject* parent) : BSS(path, bss->bSSID(), bss->sSID(), parent) {} diff --git a/applications/system-service/dbusservice.h b/applications/system-service/dbusservice.h index 4c711f290..dab57847e 100644 --- a/applications/system-service/dbusservice.h +++ b/applications/system-service/dbusservice.h @@ -36,6 +36,7 @@ class DBusService : public APIBase { Q_OBJECT Q_CLASSINFO("D-Bus Interface", OXIDE_GENERAL_INTERFACE) Q_PROPERTY(int tarnishPid READ tarnishPid) + public: static DBusService* singleton(){ static DBusService* instance; diff --git a/applications/system-service/digitizerhandler.h b/applications/system-service/digitizerhandler.h index 6377e9664..be4c93f0e 100644 --- a/applications/system-service/digitizerhandler.h +++ b/applications/system-service/digitizerhandler.h @@ -21,6 +21,7 @@ using namespace std; class DigitizerHandler : public QThread { Q_OBJECT + public: static DigitizerHandler* singleton_touchScreen(){ static DigitizerHandler* instance; diff --git a/applications/system-service/fifohandler.h b/applications/system-service/fifohandler.h index 176b08ca6..c524cfa61 100644 --- a/applications/system-service/fifohandler.h +++ b/applications/system-service/fifohandler.h @@ -10,6 +10,7 @@ class FifoHandler : public QObject { Q_OBJECT + public: FifoHandler(QString name, QString path, QObject* host) : QObject(), @@ -59,10 +60,12 @@ class FifoHandler : public QObject { } } const QString& name() { return _name; } + signals: void started(); void finished(); void dataRecieved(FifoHandler* handler, const QString& data); + protected: void run() { if(!in.is_open()){ @@ -81,6 +84,7 @@ class FifoHandler : public QObject { } thread()->yieldCurrentThread(); } + private: QObject* host; QThread _thread; @@ -90,7 +94,6 @@ class FifoHandler : public QObject { std::ifstream in; std::ofstream out; bool getline_async(std::istream& is, std::string& str, char delim = '\n') { - static std::string lineSoFar; char inChar; int charsRead = 0; diff --git a/applications/system-service/network.h b/applications/system-service/network.h index 4bbb6bad8..7d0075bbc 100644 --- a/applications/system-service/network.h +++ b/applications/system-service/network.h @@ -18,6 +18,7 @@ class Network : public QObject { Q_PROPERTY(QString password READ getNull WRITE setPassword) Q_PROPERTY(QString protocol READ protocol WRITE setProtocol) Q_PROPERTY(QVariantMap properties READ properties WRITE setProperties NOTIFY propertiesChanged) + public: Network(QString path, QString ssid, QVariantMap properties, QObject* parent); Network(QString path, QVariantMap properties, QObject* parent) diff --git a/applications/system-service/notification.h b/applications/system-service/notification.h index 01b468d2d..7f6c797a6 100644 --- a/applications/system-service/notification.h +++ b/applications/system-service/notification.h @@ -17,6 +17,7 @@ class Notification : public QObject{ Q_PROPERTY(QString application READ application WRITE setApplication) Q_PROPERTY(QString text READ text WRITE setText) Q_PROPERTY(QString icon READ icon WRITE setIcon) + public: Notification(const QString& path, const QString& identifier, const QString& owner, const QString& application, const QString& text, const QString& icon, QObject* parent); ~Notification(){ diff --git a/applications/system-service/notificationapi.h b/applications/system-service/notificationapi.h index 0ea578d12..1af8b791d 100644 --- a/applications/system-service/notificationapi.h +++ b/applications/system-service/notificationapi.h @@ -18,6 +18,7 @@ class NotificationAPI : public APIBase { Q_PROPERTY(bool enabled READ enabled) Q_PROPERTY(QList allNotifications READ getAllNotifications) Q_PROPERTY(QList unownedNotifications READ getUnownedNotifications) + public: static NotificationAPI* singleton(NotificationAPI* self = nullptr){ static NotificationAPI* instance; diff --git a/applications/system-service/powerapi.h b/applications/system-service/powerapi.h index e267a1f3d..9e3650783 100644 --- a/applications/system-service/powerapi.h +++ b/applications/system-service/powerapi.h @@ -24,6 +24,7 @@ class PowerAPI : public APIBase { Q_PROPERTY(int batteryLevel READ batteryLevel NOTIFY batteryLevelChanged) Q_PROPERTY(int batteryTemperature READ batteryTemperature NOTIFY batteryTemperatureChanged) Q_PROPERTY(int chargerState READ chargerState NOTIFY chargerStateChanged) + public: static PowerAPI* singleton(PowerAPI* self = nullptr){ static PowerAPI* instance; @@ -38,7 +39,7 @@ class PowerAPI : public APIBase { Oxide::Sentry::sentry_span(t, "singleton", "Setup singleton", [this]{ singleton(this); }); - Oxide::Sentry::sentry_span(t, "sysfs", "Determine power devices from sysfs", [this](Oxide::Sentry::Span* s){ + Oxide::Sentry::sentry_span(t, "sysfs", "Determine power devices from sysfs", [this](){ Oxide::Power::batteries(); Oxide::Power::chargers(); }); diff --git a/applications/system-service/screenapi.h b/applications/system-service/screenapi.h index fad816fe4..6a9f3edd1 100644 --- a/applications/system-service/screenapi.h +++ b/applications/system-service/screenapi.h @@ -32,6 +32,7 @@ class ScreenAPI : public APIBase { Q_CLASSINFO("D-Bus Interface", OXIDE_SCREEN_INTERFACE) Q_PROPERTY(bool enabled READ enabled) Q_PROPERTY(QList screenshots READ screenshots) + public: static ScreenAPI* singleton(ScreenAPI* self = nullptr){ static ScreenAPI* instance; diff --git a/applications/system-service/screenshot.h b/applications/system-service/screenshot.h index 30934a2cf..4e47b3aac 100644 --- a/applications/system-service/screenshot.h +++ b/applications/system-service/screenshot.h @@ -15,6 +15,7 @@ class Screenshot : public QObject{ Q_CLASSINFO("D-Bus Interface", OXIDE_SCREENSHOT_INTERFACE) Q_PROPERTY(QByteArray blob READ blob WRITE setBlob) Q_PROPERTY(QString path READ getPath) + public: Screenshot(QString path, QString filePath, QObject* parent) : QObject(parent), m_path(path), mutex() { m_file = new QFile(filePath); diff --git a/applications/system-service/systemapi.h b/applications/system-service/systemapi.h index f5936288d..4ae35e0a0 100644 --- a/applications/system-service/systemapi.h +++ b/applications/system-service/systemapi.h @@ -67,6 +67,7 @@ class SystemAPI : public APIBase { Q_PROPERTY(int autoSleep READ autoSleep WRITE setAutoSleep NOTIFY autoSleepChanged) Q_PROPERTY(bool sleepInhibited READ sleepInhibited NOTIFY sleepInhibitedChanged) Q_PROPERTY(bool powerOffInhibited READ powerOffInhibited NOTIFY powerOffInhibitedChanged) + public: enum SwipeDirection { None, Right, Left, Up, Down }; Q_ENUM(SwipeDirection) @@ -117,7 +118,6 @@ class SystemAPI : public APIBase { m_autoSleep = autoSleep; if(autoSleep < 0) { m_autoSleep = 0; - }else if(autoSleep > 10){ m_autoSleep = 10; } @@ -687,7 +687,6 @@ private slots: } if(swipeDirection == Left){ emit rightAction(); - }else{ emit leftAction(); } diff --git a/applications/system-service/wifiapi.h b/applications/system-service/wifiapi.h index 227df0f82..03b3e89b9 100644 --- a/applications/system-service/wifiapi.h +++ b/applications/system-service/wifiapi.h @@ -27,6 +27,7 @@ class WifiAPI : public APIBase { Q_PROPERTY(QDBusObjectPath network READ network) Q_PROPERTY(QList networks READ getNetworkPaths) Q_PROPERTY(bool scanning READ scanning NOTIFY scanningChanged) + public: static WifiAPI* singleton(WifiAPI* self = nullptr){ static WifiAPI* instance; @@ -656,7 +657,6 @@ class WifiAPI : public APIBase { void bssRemoved(QDBusObjectPath); void scanningChanged(bool); - private slots: // wpa_supplicant signals void InterfaceAdded(const QDBusObjectPath &path, const QVariantMap &properties){ @@ -686,6 +686,7 @@ private slots: void PropertiesChanged(const QVariantMap &properties){ Q_UNUSED(properties); } + private: bool m_enabled; QTimer* timer; @@ -806,7 +807,6 @@ private slots: m_rssi = crssi; emit rssiChanged(crssi); } - } State getCurrentState(){ State state = Off; diff --git a/applications/system-service/wlan.h b/applications/system-service/wlan.h index af99170ca..557b0dd9d 100644 --- a/applications/system-service/wlan.h +++ b/applications/system-service/wlan.h @@ -13,6 +13,7 @@ using Oxide::SysObject; class Wlan : public QObject, public SysObject { Q_OBJECT + public: Wlan(QString path, QObject* parent) : QObject(parent), SysObject(path), m_blobs(), m_iface(){ m_iface = QFileInfo(path).fileName(); diff --git a/applications/task-switcher/controller.h b/applications/task-switcher/controller.h index cbc5f85ea..69a1ac929 100644 --- a/applications/task-switcher/controller.h +++ b/applications/task-switcher/controller.h @@ -32,6 +32,7 @@ enum WifiState { WifiUnknown, WifiOff, WifiDisconnected, WifiOffline, WifiOnline class Controller : public QObject { Q_OBJECT + public: Controller(QObject* parent, ScreenProvider* screenProvider) : QObject(parent), settings(this), applications() { diff --git a/shared/liboxide/liboxide.cpp b/shared/liboxide/liboxide.cpp index 032510beb..d3b871fa4 100644 --- a/shared/liboxide/liboxide.cpp +++ b/shared/liboxide/liboxide.cpp @@ -31,30 +31,6 @@ std::string readFile(const std::string& path){ return buffer.str(); } #endif -void sentry_setup_user(){ -#ifdef SENTRY - if(!sharedSettings.telemetry()){ - return; - } - sentry_value_t user = sentry_value_new_object(); - sentry_value_set_by_key(user, "id", sentry_value_new_string(readFile("/etc/machine-id").c_str())); - sentry_set_user(user); -#endif -} -void sentry_setup_context(){ -#ifdef SENTRY - if(!sharedSettings.telemetry()){ - return; - } - std::string version = readFile("/etc/version"); - sentry_set_tag("os.version", version.c_str()); - sentry_value_t device = sentry_value_new_object(); - sentry_value_set_by_key(device, "machine-id", sentry_value_new_string(readFile("/etc/machine-id").c_str())); - sentry_value_set_by_key(device, "version", sentry_value_new_string(readFile("/etc/version").c_str())); - sentry_set_context("device", device); -#endif -} - #define BITS_PER_LONG (sizeof(long) * 8) #define NBITS(x) ((((x)-1)/BITS_PER_LONG)+1) #define OFF(x) ((x)%BITS_PER_LONG) diff --git a/shared/liboxide/liboxide.h b/shared/liboxide/liboxide.h index 6f679e0e6..56a4907d1 100644 --- a/shared/liboxide/liboxide.h +++ b/shared/liboxide/liboxide.h @@ -241,6 +241,7 @@ namespace Oxide { * \sa setWifinetworks, wifinetworksChanged */ Q_PROPERTY(WifiNetworks wifinetworks MEMBER m_wifinetworks READ wifinetworks WRITE setWifinetworks RESET resetWifinetworks NOTIFY wifinetworksChanged) + public: WifiNetworks wifinetworks(); /*! @@ -261,11 +262,13 @@ namespace Oxide { */ void setWifiNetwork(const QString& name, QVariantMap properties); void resetWifinetworks(); + signals: /*! * \brief The contents of the wifi network list has changed */ void wifinetworksChanged(WifiNetworks); + private: ~XochitlSettings(); WifiNetworks m_wifinetworks; @@ -330,6 +333,7 @@ namespace Oxide { * \brief If crash reporting has been enabled or disabled */ O_SETTINGS_PROPERTY(bool, General, crashReport, true) + private: ~SharedSettings(); }; diff --git a/shared/liboxide/settingsfile.cpp b/shared/liboxide/settingsfile.cpp index eb6753be0..d22991835 100644 --- a/shared/liboxide/settingsfile.cpp +++ b/shared/liboxide/settingsfile.cpp @@ -13,13 +13,8 @@ namespace Oxide { } SettingsFile::SettingsFile(QString path) : QSettings(path, QSettings::IniFormat), - fileWatcher(QStringList() << path) - { - - } - SettingsFile::~SettingsFile(){ - - } + fileWatcher(QStringList() << path) { } + SettingsFile::~SettingsFile(){ } void SettingsFile::fileChanged(){ if(!fileWatcher.files().contains(fileName()) && !fileWatcher.addPath(fileName())){ qWarning() << "Unable to watch " << fileName(); diff --git a/web/src/_themes/oxide/static/oxide.css b/web/src/_themes/oxide/static/oxide.css index e94751d64..c738504b0 100644 --- a/web/src/_themes/oxide/static/oxide.css +++ b/web/src/_themes/oxide/static/oxide.css @@ -91,12 +91,12 @@ pre { } div.warning { - padding: var(--text-height); - overflow: auto; border-radius: var(--border-radius); border-color: var(--bg-color-inv); border-style: dashed; font-size: 0.888rem; + overflow: auto; + padding: var(--text-height); } a { From 1643535724d51bf6fe070c26d00559dabfd5cb5a Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Tue, 24 Jan 2023 22:54:05 -0700 Subject: [PATCH 02/15] Add notify-send application (#282) * Move slothandler and json to liboxide * Fix API docs * Add notify-send application * Update actions to latest * Give better build errors if sentry missing --- .github/actions/sync-repository/action.yml | 2 +- .github/actions/web/action.yaml | 6 +- .github/workflows/build.yml | 8 +- .github/workflows/publish.yml | 2 +- .github/workflows/web.yaml | 4 +- Makefile | 11 +- applications/launcher/oxide.pro | 20 +- applications/lockscreen/decay.pro | 20 +- applications/notify-send/.gitignore | 73 ++++++ applications/notify-send/main.cpp | 163 ++++++++++++++ applications/notify-send/notify-send.pro | 45 ++++ applications/process-manager/erode.pro | 21 +- applications/screenshot-tool/fret.pro | 20 +- applications/screenshot-viewer/anxiety.pro | 20 +- applications/settings-manager/json.h | 130 ----------- applications/settings-manager/main.cpp | 47 ++-- applications/settings-manager/rot.pro | 24 +- applications/settings-manager/slothandler.h | 80 ------- applications/system-service/tarnish.pro | 20 +- applications/task-switcher/corrupt.pro | 20 +- package | 12 +- shared/liboxide/json.cpp | 207 ++++++++++++++++++ shared/liboxide/json.h | 39 ++++ shared/liboxide/liboxide.h | 2 + shared/liboxide/liboxide.pro | 25 ++- shared/liboxide/slothandler.cpp | 87 ++++++++ shared/liboxide/slothandler.h | 63 ++++++ shared/liboxide/sysobject.h | 3 +- ...3_notification.rst => 02_notification.rst} | 108 --------- web/src/documentation/api/02_power.rst | 108 +++++++++ web/src/documentation/api/02_screenshot.rst | 153 +++++++++++++ .../api/{04_screenshot.rst => 02_system.rst} | 154 +------------ .../api/{05_wifi.rst => 02_wifi.rst} | 0 33 files changed, 1109 insertions(+), 588 deletions(-) create mode 100644 applications/notify-send/.gitignore create mode 100644 applications/notify-send/main.cpp create mode 100644 applications/notify-send/notify-send.pro delete mode 100644 applications/settings-manager/json.h delete mode 100644 applications/settings-manager/slothandler.h create mode 100644 shared/liboxide/json.cpp create mode 100644 shared/liboxide/json.h create mode 100644 shared/liboxide/slothandler.cpp create mode 100644 shared/liboxide/slothandler.h rename web/src/documentation/api/{03_notification.rst => 02_notification.rst} (65%) create mode 100644 web/src/documentation/api/02_power.rst create mode 100644 web/src/documentation/api/02_screenshot.rst rename web/src/documentation/api/{04_screenshot.rst => 02_system.rst} (57%) rename web/src/documentation/api/{05_wifi.rst => 02_wifi.rst} (100%) diff --git a/.github/actions/sync-repository/action.yml b/.github/actions/sync-repository/action.yml index bb10a3983..8fc4f4b4d 100644 --- a/.github/actions/sync-repository/action.yml +++ b/.github/actions/sync-repository/action.yml @@ -22,7 +22,7 @@ runs: sudo apt-get update -yq echo "syncAptVersion=sshfs-$(apt-cache policy sshfs | grep -oP '(?<=Candidate:\s)(.+)')" >> $GITHUB_ENV - name: Cache Apt packages - uses: actions/cache@v2 + uses: actions/cache@v3 id: cache-apt with: path: ~/.aptcache diff --git a/.github/actions/web/action.yaml b/.github/actions/web/action.yaml index bec596222..40f7e8da7 100644 --- a/.github/actions/web/action.yaml +++ b/.github/actions/web/action.yaml @@ -4,11 +4,11 @@ runs: using: composite steps: - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.8' - name: Cache Python environment - uses: actions/cache@v2 + uses: actions/cache@v3 id: cache-python with: path: web/.venv @@ -19,7 +19,7 @@ runs: sudo apt-get update -yq echo "webAptVersion=doxygen-$(apt-cache policy doxygen | grep -oP '(?<=Candidate:\s)(.+)')-graphviz-$(apt-cache policy graphviz | grep -oP '(?<=Candidate:\s)(.+)')-libgraphviz-dev-$(apt-cache policy libgraphviz-dev | grep -oP '(?<=Candidate:\s)(.+)')" >> $GITHUB_ENV - name: Cache Apt packages - uses: actions/cache@v2 + uses: actions/cache@v3 id: cache-apt with: path: ~/.aptcache diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5360e4b9e..f1da6e02c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout toltec Git repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: toltec-dev/toltec.git ref: testing @@ -33,11 +33,11 @@ jobs: run: | rm -rf package/oxide/* mkdir package/oxide/src - - uses: actions/checkout@v2.3.1 + - uses: actions/checkout@v3 with: path: package/oxide/src - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.8' - name: Setup Toltec dependencies @@ -58,7 +58,7 @@ jobs: done timeout-minutes: 15 - name: Save packages - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: packages path: output diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 298f10bd5..fcdb384b1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout the Git repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: submodules: true - name: Build web diff --git a/.github/workflows/web.yaml b/.github/workflows/web.yaml index 5b48702fb..9f7d37ef4 100644 --- a/.github/workflows/web.yaml +++ b/.github/workflows/web.yaml @@ -22,13 +22,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the Git repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: submodules: true - name: Build web uses: ./.github/actions/web - name: Save web - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: web path: web/dist diff --git a/Makefile b/Makefile index c1ee56421..d06ed7c1a 100644 --- a/Makefile +++ b/Makefile @@ -27,8 +27,9 @@ release: clean build $(RELOBJ) INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/launcher install INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/lockscreen install INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/task-switcher install + INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/notify-send install -build: liboxide tarnish erode rot oxide decay corrupt fret anxiety +build: liboxide tarnish erode rot oxide decay corrupt fret anxiety notify-send liboxide: $(OBJ) .build/liboxide/libliboxide.so @@ -102,6 +103,14 @@ anxiety: tarnish liboxide .build/screenshot-viewer/anxiety cd .build/screenshot-viewer && qmake $(DEFINES) anxiety.pro $(MAKE) -j`nproc` -C .build/screenshot-viewer all +notify-send: tarnish liboxide .build/notify-send/notify-send + +.build/notify-send/notify-send: + mkdir -p .build/notify-send + cp -r applications/notify-send/* .build/notify-send + cd .build/notify-send && qmake $(DEFINES) notify-send.pro + $(MAKE) -j`nproc` -C .build/notify-send all + sentry: .build/sentry/libsentry.so .build/sentry/libsentry.so: diff --git a/applications/launcher/oxide.pro b/applications/launcher/oxide.pro index 84238ac64..59b7d0d7a 100644 --- a/applications/launcher/oxide.pro +++ b/applications/launcher/oxide.pro @@ -56,14 +56,18 @@ LIBS += -L$$PWD/../../shared/ -lqsgepaper INCLUDEPATH += $$PWD/../../shared DEPENDPATH += $$PWD/../../shared -exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library +contains(DEFINES, SENTRY){ + exists($$PWD/../../.build/sentry) { + LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client + INCLUDEPATH += $$PWD/../../.build/sentry/include + DEPENDPATH += $$PWD/../../.build/sentry/lib + + library.files = ../../.build/sentry/libsentry.so + library.path = /opt/lib + INSTALLS += library + }else{ + error(You need to build sentry first) + } } LIBS += -L$$PWD/../../.build/liboxide -lliboxide diff --git a/applications/lockscreen/decay.pro b/applications/lockscreen/decay.pro index b65fd6990..a83495738 100644 --- a/applications/lockscreen/decay.pro +++ b/applications/lockscreen/decay.pro @@ -33,14 +33,18 @@ LIBS += -L$$PWD/../../shared/ -lqsgepaper INCLUDEPATH += $$PWD/../../shared DEPENDPATH += $$PWD/../../shared -exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library +contains(DEFINES, SENTRY){ + exists($$PWD/../../.build/sentry) { + LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client + INCLUDEPATH += $$PWD/../../.build/sentry/include + DEPENDPATH += $$PWD/../../.build/sentry/lib + + library.files = ../../.build/sentry/libsentry.so + library.path = /opt/lib + INSTALLS += library + }else{ + error(You need to build sentry first) + } } LIBS += -L$$PWD/../../.build/liboxide -lliboxide diff --git a/applications/notify-send/.gitignore b/applications/notify-send/.gitignore new file mode 100644 index 000000000..fab7372d7 --- /dev/null +++ b/applications/notify-send/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/applications/notify-send/main.cpp b/applications/notify-send/main.cpp new file mode 100644 index 000000000..1081d314a --- /dev/null +++ b/applications/notify-send/main.cpp @@ -0,0 +1,163 @@ +#include +#include +#include +#include +#include + +#include + +#include "dbusservice_interface.h" +#include "notificationapi_interface.h" +#include "notification_interface.h" + +using namespace codes::eeems::oxide1; +using namespace Oxide::Sentry; +using namespace Oxide::JSON; + +int qExit(int ret){ + QTimer::singleShot(0, [ret](){ + qApp->exit(ret); + }); + return qApp->exec(); +} + +int main(int argc, char *argv[]){ + QCoreApplication app(argc, argv); + sentry_init("notify-send", argv); + app.setOrganizationName("Eeems"); + app.setOrganizationDomain(OXIDE_SERVICE); + app.setApplicationName("notify-send"); + app.setApplicationVersion(OXIDE_INTERFACE_VERSION); + QCommandLineParser parser; + parser.setApplicationDescription("a program to send desktop notifications"); + parser.addHelpOption(); + parser.applicationDescription(); + parser.addVersionOption(); + parser.addPositionalArgument("summary", "Summary of the notification", "{SUMMARY}"); + parser.addPositionalArgument("body", "Body of the notification", "[BODY]"); + QCommandLineOption appNameOption( + {"a", "app-name"}, + "Specifies the app name for the notification.", + "APP_NAME" + ); + parser.addOption(appNameOption); + QCommandLineOption iconOption( + {"i", "icon"}, + "Specifies an icon filename or stock icon to display.", + "ICON" + ); + parser.addOption(iconOption); + QCommandLineOption expireOption( + {"t", "expire-time"}, + "The duration, in milliseconds to wait for the notification. Does nothing without --wait", + "TIME" + ); + parser.addOption(expireOption); + QCommandLineOption printOption( + {"p", "print-id"}, + "Print the notification identifier." + ); + parser.addOption(printOption); + QCommandLineOption replaceOption( + {"r", "replace-id"}, + "The identifier of the notification to replace.", + "REPLACE_ID" + ); + parser.addOption(replaceOption); + QCommandLineOption waitOption( + {"w", "wait"}, + "Wait for the notification to be closed before exiting. If the expire-time is set, it will be used as the maximum waiting time." + ); + parser.addOption(waitOption); + QCommandLineOption transientOption( + {"e", "transient"}, + "Show a transient notification. This notification will be removed immediatly after being shown on the screen" + ); + parser.addOption(transientOption); + // TODO add action, urgency, category, and hint + parser.process(app); + QStringList args = parser.positionalArguments(); + if(args.isEmpty()){ +#ifdef SENTRY + sentry_breadcrumb("error", "No arguments"); +#endif + parser.showHelp(EXIT_FAILURE); + } + if(args.count() > 2){ +#ifdef SENTRY + sentry_breadcrumb("error", "Too many arguments"); +#endif + parser.showHelp(EXIT_FAILURE); + } + auto bus = QDBusConnection::systemBus(); + General api(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); + qDebug() << "Requesting notification API..."; + QDBusObjectPath path = api.requestAPI("notification"); + if(path.path() == "/"){ + qDebug() << "Unable to get notification API"; + return qExit(EXIT_FAILURE); + } + Notifications notifications(OXIDE_SERVICE, path.path(), bus); + QString guid; + auto appName = parser.isSet(appNameOption) ? parser.value(appNameOption) : "codes.eeems.notify-send"; + auto iconPath = parser.isSet(iconOption) ? parser.value(iconOption) : ""; + auto text = args.join("\n"); + if(parser.isSet(replaceOption)){ + guid = parser.value(replaceOption); + path = notifications.get(guid); + if(path.path() == "/" || !notifications.take(guid)){ + guid = QUuid::createUuid().toString(); + path = notifications.add(guid, appName, text, iconPath); + } + if(path.path() == "/"){ + qDebug() << "Failed to add notification"; + return qExit(EXIT_FAILURE); + } + Notification notification(OXIDE_SERVICE, path.path(), bus); + notification.setApplication(appName); + notification.setIcon(iconPath); + notification.setText(text); + }else{ + guid = QUuid::createUuid().toString(); + path = notifications.add(guid, appName, text, iconPath); + if(path.path() == "/"){ + qDebug() << "Failed to add notification"; + return qExit(EXIT_FAILURE); + } + } + Notification notification(OXIDE_SERVICE, path.path(), bus); + if(parser.isSet(printOption)){ + QTextStream qStdOut(stdout, QIODevice::WriteOnly); + qStdOut << guid << Qt::endl; + } + qDebug() << "Displaying notification" << guid; + notification.display().waitForFinished(); + if(parser.isSet(waitOption)){ + qDebug() << "Waiting for notification to be closed"; + if(!Oxide::DBusConnect( + ¬ification, + "removed", + [](QVariantList args){ + Q_UNUSED(args); + qApp->exit(EXIT_SUCCESS); + }, + true + )){ + qDebug() << "Failed to wait for notification to exit"; + return qExit(EXIT_FAILURE); + } + if(parser.isSet(expireOption)){ + auto timeout = parser.value(expireOption); + qDebug() << ("Timeout set to " + timeout + "ms").toStdString().c_str(); + QTimer::singleShot(timeout.toInt(), [¬ification]{ + qDebug() << "Notification wait timed out"; + notification.remove().waitForFinished(); + qApp->exit(EXIT_FAILURE); + }); + } + return app.exec(); + }else if(parser.isSet(transientOption)){ + notification.remove(); + } + return qExit(EXIT_SUCCESS); +} diff --git a/applications/notify-send/notify-send.pro b/applications/notify-send/notify-send.pro new file mode 100644 index 000000000..b56b568b3 --- /dev/null +++ b/applications/notify-send/notify-send.pro @@ -0,0 +1,45 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +DEFINES += QT_DEPRECATED_WARNINGS +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp + +INCLUDEPATH += ../../shared + +DBUS_INTERFACES += ../../interfaces/dbusservice.xml +DBUS_INTERFACES += ../../interfaces/notificationapi.xml +DBUS_INTERFACES += ../../interfaces/notification.xml + +# Default rules for deployment. +target.path = /opt/bin +!isEmpty(target.path): INSTALLS += target + +contains(DEFINES, SENTRY){ + exists($$PWD/../../.build/sentry) { + LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client + INCLUDEPATH += $$PWD/../../.build/sentry/include + DEPENDPATH += $$PWD/../../.build/sentry/lib + + library.files = ../../.build/sentry/libsentry.so + library.path = /opt/lib + INSTALLS += library + }else{ + error(You need to build sentry first) + } +} + +LIBS += -L$$PWD/../../.build/liboxide -lliboxide +INCLUDEPATH += $$PWD/../../shared/liboxide +DEPENDPATH += $$PWD/../../shared/liboxide + +QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib + +HEADERS += + +VERSION = 2.5 diff --git a/applications/process-manager/erode.pro b/applications/process-manager/erode.pro index a48d1175d..e2f984b79 100755 --- a/applications/process-manager/erode.pro +++ b/applications/process-manager/erode.pro @@ -1,4 +1,5 @@ QT += quick +QT += dbus CONFIG += c++11 DEFINES += QT_DEPRECATED_WARNINGS @@ -33,14 +34,18 @@ LIBS += -L$$PWD/../../shared/ -lqsgepaper INCLUDEPATH += $$PWD/../../shared DEPENDPATH += $$PWD/../../shared -exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library +contains(DEFINES, SENTRY){ + exists($$PWD/../../.build/sentry) { + LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client + INCLUDEPATH += $$PWD/../../.build/sentry/include + DEPENDPATH += $$PWD/../../.build/sentry/lib + + library.files = ../../.build/sentry/libsentry.so + library.path = /opt/lib + INSTALLS += library + }else{ + error(You need to build sentry first) + } } LIBS += -L$$PWD/../../.build/liboxide -lliboxide diff --git a/applications/screenshot-tool/fret.pro b/applications/screenshot-tool/fret.pro index 4c9a25dbc..b064b9355 100644 --- a/applications/screenshot-tool/fret.pro +++ b/applications/screenshot-tool/fret.pro @@ -27,14 +27,18 @@ LIBS += -L$$PWD/../../shared/ -lqsgepaper INCLUDEPATH += $$PWD/../../shared DEPENDPATH += $$PWD/../../shared -exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library +contains(DEFINES, SENTRY){ + exists($$PWD/../../.build/sentry) { + LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client + INCLUDEPATH += $$PWD/../../.build/sentry/include + DEPENDPATH += $$PWD/../../.build/sentry/lib + + library.files = ../../.build/sentry/libsentry.so + library.path = /opt/lib + INSTALLS += library + }else{ + error(You need to build sentry first) + } } LIBS += -L$$PWD/../../.build/liboxide -lliboxide diff --git a/applications/screenshot-viewer/anxiety.pro b/applications/screenshot-viewer/anxiety.pro index 1e3fce3f0..ae658a54b 100644 --- a/applications/screenshot-viewer/anxiety.pro +++ b/applications/screenshot-viewer/anxiety.pro @@ -39,14 +39,18 @@ LIBS += -L$$PWD/../../shared/ -lqsgepaper INCLUDEPATH += $$PWD/../../shared DEPENDPATH += $$PWD/../../shared -exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library +contains(DEFINES, SENTRY){ + exists($$PWD/../../.build/sentry) { + LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client + INCLUDEPATH += $$PWD/../../.build/sentry/include + DEPENDPATH += $$PWD/../../.build/sentry/lib + + library.files = ../../.build/sentry/libsentry.so + library.path = /opt/lib + INSTALLS += library + }else{ + error(You need to build sentry first) + } } LIBS += -L$$PWD/../../.build/liboxide -lliboxide diff --git a/applications/settings-manager/json.h b/applications/settings-manager/json.h deleted file mode 100644 index 6e74ca809..000000000 --- a/applications/settings-manager/json.h +++ /dev/null @@ -1,130 +0,0 @@ -#ifndef JSON_H -#define JSON_H -#include -#include -#include -#include -#include -#include - -QVariant sanitizeForJson(QVariant value); -QVariant decodeDBusArgument(const QDBusArgument& arg){ - auto type = arg.currentType(); - if(type == QDBusArgument::BasicType || type == QDBusArgument::VariantType){ - return sanitizeForJson(arg.asVariant()); - } - if(type == QDBusArgument::ArrayType){ - QVariantList list; - arg.beginArray(); - while(!arg.atEnd()){ - list.append(decodeDBusArgument(arg)); - } - arg.endArray(); - return sanitizeForJson(list); - } - if(type == QDBusArgument::MapType){ - qDebug() << "Map Type"; - QMap map; - arg.beginMap(); - while(!arg.atEnd()){ - arg.beginMapEntry(); - auto key = decodeDBusArgument(arg); - auto value = decodeDBusArgument(arg); - arg.endMapEntry(); - map.insert(sanitizeForJson(key), sanitizeForJson(value)); - } - arg.endMap(); - return sanitizeForJson(QVariant::fromValue(map)); - } - qDebug() << "Unable to sanitize QDBusArgument as it is an unknown type"; - return QVariant(); -} -QVariant sanitizeForJson(QVariant value){ - auto userType = value.userType(); - if(userType == QMetaType::type("QDBusObjectPath")){ - return value.value().path(); - } - if(userType == QMetaType::type("QDBusSignature")){ - return value.value().signature(); - } - if(userType == QMetaType::type("QDBusVariant")){ - return value.value().variant(); - } - if(userType == QMetaType::type("QDBusArgument")){ - return decodeDBusArgument(value.value()); - } - if(userType == QMetaType::type("QList")){ - QVariantList list; - for(auto value : value.value>()){ - list.append(sanitizeForJson(value.variant())); - } - return list; - } - if(userType == QMetaType::type("QList")){ - QStringList list; - for(auto value : value.value>()){ - list.append(value.signature()); - } - return list; - } - if(userType == QMetaType::type("QList")){ - QStringList list; - for(auto value : value.value>()){ - list.append(value.path()); - } - return list; - } - if(userType == QMetaType::QByteArray){ - auto byteArray = value.toByteArray(); - QVariantList list; - for(auto byte : byteArray){ - list.append(byte); - } - return list; - } - if(userType == QMetaType::QVariantMap){ - QVariantMap map; - auto input = value.toMap(); - for(auto key : input.keys()){ - map.insert(key, sanitizeForJson(input[key])); - } - return map; - } - if(userType == QMetaType::QVariantList){ - QVariantList list = value.toList(); - QMutableListIterator i(list); - while(i.hasNext()){ - i.setValue(sanitizeForJson(i.next())); - } - return list; - } - return value; -} -QString toJson(QVariant value){ - if(value.isNull()){ - return "null"; - } - auto jsonVariant = QJsonValue::fromVariant(sanitizeForJson(value)); - if(jsonVariant.isNull()){ - return "null"; - } - if(jsonVariant.isUndefined()){ - return "undefined"; - } - QJsonArray jsonArray; - jsonArray.append(jsonVariant); - QJsonDocument doc(jsonArray); - auto json = doc.toJson(QJsonDocument::Compact); - return json.mid(1, json.length() - 2); -} -QVariant fromJson(QByteArray json){ - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson("[" + json + "]", &error); - if(error.error != QJsonParseError::NoError){ - qDebug() << "Unable to read json value" << error.errorString(); - qDebug() << "Value to parse" << json; - } - return doc.array().first().toVariant(); -} - -#endif // JSON_H diff --git a/applications/settings-manager/main.cpp b/applications/settings-manager/main.cpp index c64f50ca4..5a1af4f29 100644 --- a/applications/settings-manager/main.cpp +++ b/applications/settings-manager/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -19,11 +20,9 @@ #include "notificationapi_interface.h" #include "notification_interface.h" -#include "json.h" -#include "slothandler.h" - using namespace codes::eeems::oxide1; using namespace Oxide::Sentry; +using namespace Oxide::JSON; int qExit(int ret){ QTimer::singleShot(0, [ret](){ @@ -318,6 +317,7 @@ int main(int argc, char *argv[]){ return qExit(EXIT_FAILURE); } QDBusAbstractInterface* iapi = qobject_cast(api); + QTextStream qStdOut(stdout, QIODevice::WriteOnly); if(action == "get"){ auto value = api->property(args.at(2).toStdString().c_str()); if(iapi != nullptr){ @@ -330,7 +330,7 @@ int main(int argc, char *argv[]){ return qExit(EXIT_FAILURE); } } - QTextStream(stdout, QIODevice::WriteOnly) << toJson(value).toStdString().c_str() << Qt::endl; + qStdOut << toJson(value).toStdString().c_str() << Qt::endl; }else if(action == "set"){ auto property = args.at(2).toStdString(); if(!api->setProperty(property.c_str(), args.at(3).toStdString().c_str())){ @@ -344,29 +344,23 @@ int main(int argc, char *argv[]){ return qExit(EXIT_FAILURE); } }else if(action == "listen"){ - auto metaObject = api->metaObject(); - auto name = QString(args.at(2).toStdString().c_str()); - for(int methodId = 0; methodId < metaObject->methodCount(); methodId++){ - auto method = metaObject->method(methodId); - if(method.methodType() == QMetaMethod::Signal && method.name() == name){ - QByteArray slotName = method.name().prepend("on").append("("); - QStringList parameters; - for(int i = 0, j = method.parameterCount(); i < j; ++i){ - parameters << QMetaType::typeName(method.parameterType(i)); - } - slotName.append(parameters.join(",").toUtf8()).append(")"); - QByteArray theSignal = QMetaObject::normalizedSignature(method.methodSignature().constData()); - QByteArray theSlot = QMetaObject::normalizedSignature(slotName); - if(!QMetaObject::checkConnectArgs(theSignal, theSlot)){ - continue; - } - auto slotHandler = new SlotHandler(OXIDE_SERVICE, parameters, parser.isSet("once"), [=](){ - qApp->exit(EXIT_SUCCESS); - }); - if(slotHandler->connect(api, methodId)){ - return app.exec(); + if(Oxide::DBusConnect( + (QDBusAbstractInterface*)api, + QString(args.at(2).toStdString().c_str()), + [](QVariantList args){ + QTextStream qStdOut(stdout, QIODevice::WriteOnly); + if(args.size() > 1){ + qStdOut << toJson(args).toStdString().c_str() << Qt::endl; + }else if(args.size() == 1 && !args.first().isNull()){ + qStdOut << toJson(args.first()).toStdString().c_str() << Qt::endl; + }else{ + qStdOut << "undefined" << Qt::endl; } - } + }, + [](){ qApp->exit(EXIT_SUCCESS); }, + parser.isSet("once") + )){ + return app.exec(); } qDebug() << "Unable to listen to signal"; if(iapi != nullptr){ @@ -431,7 +425,6 @@ int main(int argc, char *argv[]){ } QDBusMessage reply = iapi->callWithArgumentList(QDBus::Block, method, arguments); auto result = reply.arguments(); - QTextStream qStdOut(stdout, QIODevice::WriteOnly); if(result.size() > 1){ qStdOut << toJson(result).toStdString().c_str() << Qt::endl; }else if(!result.first().isNull()){ diff --git a/applications/settings-manager/rot.pro b/applications/settings-manager/rot.pro index f3cf50dbe..7419a6a49 100644 --- a/applications/settings-manager/rot.pro +++ b/applications/settings-manager/rot.pro @@ -29,14 +29,18 @@ DBUS_INTERFACES += ../../interfaces/notification.xml target.path = /opt/bin !isEmpty(target.path): INSTALLS += target -exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library +contains(DEFINES, SENTRY){ + exists($$PWD/../../.build/sentry) { + LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client + INCLUDEPATH += $$PWD/../../.build/sentry/include + DEPENDPATH += $$PWD/../../.build/sentry/lib + + library.files = ../../.build/sentry/libsentry.so + library.path = /opt/lib + INSTALLS += library + }else{ + error(You need to build sentry first) + } } LIBS += -L$$PWD/../../.build/liboxide -lliboxide @@ -45,8 +49,6 @@ DEPENDPATH += $$PWD/../../shared/liboxide QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib -HEADERS += \ - json.h \ - slothandler.h +HEADERS += VERSION = 2.5 diff --git a/applications/settings-manager/slothandler.h b/applications/settings-manager/slothandler.h deleted file mode 100644 index 9dafff1ae..000000000 --- a/applications/settings-manager/slothandler.h +++ /dev/null @@ -1,80 +0,0 @@ -#ifndef SLOTHANDLER_H -#define SLOTHANDLER_H -#include -#include -#include -#include -#include - -#include "json.h" - -class SlotHandler : public QObject { -public: - SlotHandler(const QString& serviceName, QStringList parameters, bool once, std::function callback) - : QObject(0), - serviceName(serviceName), - parameters(parameters), - once(once), - m_disconnected(false), - qStdOut(stdout, QIODevice::WriteOnly), - callback(callback) - { - watcher = new QDBusServiceWatcher(serviceName, QDBusConnection::systemBus(), QDBusServiceWatcher::WatchForUnregistration, this); - QObject::connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [=](const QString& name){ - Q_UNUSED(name); - if(!m_disconnected){ - qDebug() << QDBusError(QDBusError::ServiceUnknown, "The name " + serviceName + " is no longer registered"); - m_disconnected = true; - callback(); - } - }); - } - ~SlotHandler() {}; - int qt_metacall(QMetaObject::Call call, int id, void **arguments){ - id = QObject::qt_metacall(call, id, arguments); - if (id < 0 || call != QMetaObject::InvokeMetaMethod){ - return id; - } - Q_ASSERT(id < 1); - if(!m_disconnected){ - handleSlot(sender(), arguments); - } - return -1; - } - bool connect(QObject* sender, int methodId){ - return QMetaObject::connect(sender, methodId, this, this->metaObject()->methodCount()); - } - -private: - QString serviceName; - QStringList parameters; - bool once; - bool m_disconnected; - QDBusServiceWatcher* watcher; - QTextStream qStdOut; - std::function callback; - - void handleSlot(QObject* api, void** arguments){ - Q_UNUSED(api); - QVariantList args; - for(int i = 0; i < parameters.length(); i++){ - auto typeId = QMetaType::type(parameters[i].toStdString().c_str()); - QMetaType type(typeId); - void* ptr = reinterpret_cast(arguments[i + 1]); - args << QVariant(typeId, ptr); - } - if(args.size() > 1){ - qStdOut << toJson(args).toStdString().c_str() << Qt::endl; - }else if(args.size() == 1 && !args.first().isNull()){ - qStdOut << toJson(args.first()).toStdString().c_str() << Qt::endl; - }else{ - qStdOut << "undefined" << Qt::endl; - } - if(once){ - m_disconnected = true; - callback(); - } - } -}; - -#endif // SLOTHANDLER_H diff --git a/applications/system-service/tarnish.pro b/applications/system-service/tarnish.pro index 233205486..dfbc183f9 100644 --- a/applications/system-service/tarnish.pro +++ b/applications/system-service/tarnish.pro @@ -86,14 +86,18 @@ DISTFILES += \ generate_xml.sh \ org.freedesktop.login1.xml -exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library +contains(DEFINES, SENTRY){ + exists($$PWD/../../.build/sentry) { + LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client + INCLUDEPATH += $$PWD/../../.build/sentry/include + DEPENDPATH += $$PWD/../../.build/sentry/lib + + library.files = ../../.build/sentry/libsentry.so + library.path = /opt/lib + INSTALLS += library + }else{ + error(You need to build sentry first) + } } LIBS += -L$$PWD/../../.build/liboxide -lliboxide diff --git a/applications/task-switcher/corrupt.pro b/applications/task-switcher/corrupt.pro index 31764e482..498fa025a 100644 --- a/applications/task-switcher/corrupt.pro +++ b/applications/task-switcher/corrupt.pro @@ -34,14 +34,18 @@ LIBS += -L$$PWD/../../shared/ -lqsgepaper INCLUDEPATH += $$PWD/../../shared DEPENDPATH += $$PWD/../../shared -exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library +contains(DEFINES, SENTRY){ + exists($$PWD/../../.build/sentry) { + LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client + INCLUDEPATH += $$PWD/../../.build/sentry/include + DEPENDPATH += $$PWD/../../.build/sentry/lib + + library.files = ../../.build/sentry/libsentry.so + library.path = /opt/lib + INSTALLS += library + }else{ + error(You need to build sentry first) + } } LIBS += -L$$PWD/../../.build/liboxide -lliboxide diff --git a/package b/package index e687ce9d1..9cb9ee04a 100644 --- a/package +++ b/package @@ -2,7 +2,7 @@ # Copyright (c) 2020 The Toltec Contributors # SPDX-License-Identifier: MIT -pkgnames=(erode fret oxide rot tarnish decay corrupt anxiety liboxide libsentry) +pkgnames=(erode fret oxide rot tarnish decay corrupt anxiety liboxide libsentry notify-send) pkgver="2.5~~VERSION~" timestamp="$(date -u +%Y-%m-%dT%H:%MZ)" maintainer="Eeems " @@ -137,6 +137,16 @@ anxiety() { } } +notify-send() { + pkgdesc="A program to send desktop notifications for Oxide" + section=utils + installdepends=("tarnish=$pkgver" "liboxide=$pkgver" "libsentry=$_sentry") + + package() { + install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/notify-send + } +} + liboxide() { pkgdesc="Shared library for oxide applications" section=devel diff --git a/shared/liboxide/json.cpp b/shared/liboxide/json.cpp new file mode 100644 index 000000000..cc40ecbe8 --- /dev/null +++ b/shared/liboxide/json.cpp @@ -0,0 +1,207 @@ +#include "json.h" + +#include +#include +#include + +static bool qIsNumericType(uint tp){ + static const qulonglong numericTypeBits = + Q_UINT64_C(1) << QMetaType::Bool | + Q_UINT64_C(1) << QMetaType::Double | + Q_UINT64_C(1) << QMetaType::Float | + Q_UINT64_C(1) << QMetaType::Char | + Q_UINT64_C(1) << QMetaType::SChar | + Q_UINT64_C(1) << QMetaType::UChar | + Q_UINT64_C(1) << QMetaType::Short | + Q_UINT64_C(1) << QMetaType::UShort | + Q_UINT64_C(1) << QMetaType::Int | + Q_UINT64_C(1) << QMetaType::UInt | + Q_UINT64_C(1) << QMetaType::Long | + Q_UINT64_C(1) << QMetaType::ULong | + Q_UINT64_C(1) << QMetaType::LongLong | + Q_UINT64_C(1) << QMetaType::ULongLong; + return tp < (CHAR_BIT * sizeof numericTypeBits) ? numericTypeBits & (Q_UINT64_C(1) << tp) : false; +} + +int compareAsString(const QVariant* v1, const QVariant* v2){ + int r = v1->toString().compare(v2->toString(), Qt::CaseInsensitive); + if (r == 0) { + return (v1->type() < v2->type()) ? -1 : 1; + } + return r; +} +int compare(const QVariant* v1, const QVariant* v2){ + if (qIsNumericType(v1->type()) && qIsNumericType(v2->type())){ + if(v1 == v2){ + return 0; + } + if(v1 < v2){ + return -1; + } + return 1; + } + if ((int)v1->type() >= (int)QMetaType::User) { + int result; + const void* v1d = v1->constData(); + const void* v2d = v2->constData(); + if(QMetaType::compare(v1d, v2d, v1->type(), &result)){ + return result; + } + } + switch (v1->type()){ + case QVariant::Date: + return v1->toDate() < v2->toDate() ? -1 : 1; + case QVariant::Time: + return v1->toTime() < v2->toTime() ? -1 : 1; + case QVariant::DateTime: + return v1->toDateTime() < v2->toDateTime() ? -1 : 1; + case QVariant::StringList: + return v1->toStringList() < v2->toStringList() ? -1 : 1; + default: + return compareAsString(v1, v2); + } +} + +bool operator<(const QVariant& lhs, const QVariant& rhs){ + const QVariant* v1 = &lhs; + const QVariant* v2 = &rhs; + if(lhs.type() != rhs.type()){ + if (v2->canConvert(v1->type())) { + QVariant converted2 = *v2; + if (converted2.convert(v1->type())){ + v2 = &converted2; + } + } + if (v1->type() != v2->type() && v1->canConvert(v2->type())) { + QVariant converted1 = *v1; + if (converted1.convert(v2->type())){ + v1 = &converted1; + } + } + if (v1->type() != v2->type()) { + return compareAsString(v1, v2) < 0; + } + } + return compare(v1, v2) < 0; +} + +namespace Oxide::JSON { + QVariant decodeDBusArgument(const QDBusArgument& arg){ + auto type = arg.currentType(); + if(type == QDBusArgument::BasicType || type == QDBusArgument::VariantType){ + return sanitizeForJson(arg.asVariant()); + } + if(type == QDBusArgument::ArrayType){ + QVariantList list; + arg.beginArray(); + while(!arg.atEnd()){ + list.append(decodeDBusArgument(arg)); + } + arg.endArray(); + return sanitizeForJson(list); + } + if(type == QDBusArgument::MapType){ + qDebug() << "Map Type"; + QMap map; + arg.beginMap(); + while(!arg.atEnd()){ + arg.beginMapEntry(); + auto key = decodeDBusArgument(arg); + auto value = decodeDBusArgument(arg); + arg.endMapEntry(); + map.insert(sanitizeForJson(key), sanitizeForJson(value)); + } + arg.endMap(); + return sanitizeForJson(QVariant::fromValue(map)); + } + qDebug() << "Unable to sanitize QDBusArgument as it is an unknown type"; + return QVariant(); + } + QVariant sanitizeForJson(QVariant value){ + auto userType = value.userType(); + if(userType == QMetaType::type("QDBusObjectPath")){ + return value.value().path(); + } + if(userType == QMetaType::type("QDBusSignature")){ + return value.value().signature(); + } + if(userType == QMetaType::type("QDBusVariant")){ + return value.value().variant(); + } + if(userType == QMetaType::type("QDBusArgument")){ + return decodeDBusArgument(value.value()); + } + if(userType == QMetaType::type("QList")){ + QVariantList list; + for(auto value : value.value>()){ + list.append(sanitizeForJson(value.variant())); + } + return list; + } + if(userType == QMetaType::type("QList")){ + QStringList list; + for(auto value : value.value>()){ + list.append(value.signature()); + } + return list; + } + if(userType == QMetaType::type("QList")){ + QStringList list; + for(auto value : value.value>()){ + list.append(value.path()); + } + return list; + } + if(userType == QMetaType::QByteArray){ + auto byteArray = value.toByteArray(); + QVariantList list; + for(auto byte : byteArray){ + list.append(byte); + } + return list; + } + if(userType == QMetaType::QVariantMap){ + QVariantMap map; + auto input = value.toMap(); + for(auto key : input.keys()){ + map.insert(key, sanitizeForJson(input[key])); + } + return map; + } + if(userType == QMetaType::QVariantList){ + QVariantList list = value.toList(); + QMutableListIterator i(list); + while(i.hasNext()){ + i.setValue(sanitizeForJson(i.next())); + } + return list; + } + return value; + } + QString toJson(QVariant value){ + if(value.isNull()){ + return "null"; + } + auto jsonVariant = QJsonValue::fromVariant(sanitizeForJson(value)); + if(jsonVariant.isNull()){ + return "null"; + } + if(jsonVariant.isUndefined()){ + return "undefined"; + } + QJsonArray jsonArray; + jsonArray.append(jsonVariant); + QJsonDocument doc(jsonArray); + auto json = doc.toJson(QJsonDocument::Compact); + return json.mid(1, json.length() - 2); + } + QVariant fromJson(QByteArray json){ + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson("[" + json + "]", &error); + if(error.error != QJsonParseError::NoError){ + qDebug() << "Unable to read json value" << error.errorString(); + qDebug() << "Value to parse" << json; + } + return doc.array().first().toVariant(); + } +} diff --git a/shared/liboxide/json.h b/shared/liboxide/json.h new file mode 100644 index 000000000..72d464d14 --- /dev/null +++ b/shared/liboxide/json.h @@ -0,0 +1,39 @@ +/*! + * \file json.h + */ +#ifndef JSON_H +#define JSON_H + +#include "liboxide_global.h" + +#include +#include +#include + +namespace Oxide::JSON { + /*! + * \brief Decode a DBus Argument into a QVariant + * \param DBus Argument to decode + * \return QVariant + */ + LIBOXIDE_EXPORT QVariant decodeDBusArgument(const QDBusArgument& arg); + /*! + * \brief Sanitize a QVariant into a value that can be converted to JSON + * \param QVariant to sanitize + * \return Sanitized value + */ + LIBOXIDE_EXPORT QVariant sanitizeForJson(QVariant value); + /*! + * \brief Convert a QVariant to a JSON string + * \param QVariant to convert + * \return JSON string + */ + LIBOXIDE_EXPORT QString toJson(QVariant value); + /*! + * \brief Convert a JSON string into a QVariant + * \param JSON string to convert + * \return The converted QVaraint + */ + LIBOXIDE_EXPORT QVariant fromJson(QByteArray json); +} +#endif // JSON_H diff --git a/shared/liboxide/liboxide.h b/shared/liboxide/liboxide.h index 56a4907d1..aafd28de8 100644 --- a/shared/liboxide/liboxide.h +++ b/shared/liboxide/liboxide.h @@ -8,7 +8,9 @@ #include "settingsfile.h" #include "power.h" +#include "json.h" #include "signalhandler.h" +#include "slothandler.h" #include #include diff --git a/shared/liboxide/liboxide.pro b/shared/liboxide/liboxide.pro index 8d73bad01..48dd9d63a 100644 --- a/shared/liboxide/liboxide.pro +++ b/shared/liboxide/liboxide.pro @@ -1,5 +1,6 @@ QT -= gui QT += quick +QT += dbus TEMPLATE = lib DEFINES += LIBOXIDE_LIBRARY @@ -10,9 +11,11 @@ DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs depr SOURCES += \ eventfilter.cpp \ + json.cpp \ liboxide.cpp \ power.cpp \ settingsfile.cpp \ + slothandler.cpp \ sysobject.cpp \ signalhandler.cpp @@ -21,7 +24,9 @@ HEADERS += \ liboxide_global.h \ liboxide.h \ power.h \ + json.h \ settingsfile.h \ + slothandler.h \ sysobject.h \ signalhandler.h @@ -32,14 +37,18 @@ LIBS += -L$$PWD/../../shared/ -lqsgepaper INCLUDEPATH += $$PWD/../../shared DEPENDPATH += $$PWD/../../shared -exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library +contains(DEFINES, SENTRY){ + exists($$PWD/../../.build/sentry) { + LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client + INCLUDEPATH += $$PWD/../../.build/sentry/include + DEPENDPATH += $$PWD/../../.build/sentry/lib + + library.files = ../../.build/sentry/libsentry.so + library.path = /opt/lib + INSTALLS += library + }else{ + error(You need to build sentry first) + } } target.path = /opt/usr/lib diff --git a/shared/liboxide/slothandler.cpp b/shared/liboxide/slothandler.cpp new file mode 100644 index 000000000..31a46eb7b --- /dev/null +++ b/shared/liboxide/slothandler.cpp @@ -0,0 +1,87 @@ +#include "slothandler.h" +#include "json.h" +#include "liboxide.h" + +#include +#include +#include +#include + +using namespace Oxide::JSON; + +namespace Oxide{ + bool DBusConnect(QDBusAbstractInterface* interface, const QString& slotName, std::function onMessage, const bool& once=false){ + return DBusConnect(interface, slotName, onMessage, []{}, once); + } + bool DBusConnect(QDBusAbstractInterface* interface, const QString& slotName, std::function onMessage, std::function callback, const bool& once){ + auto metaObject = interface->metaObject(); + for(int methodId = 0; methodId < metaObject->methodCount(); methodId++){ + auto method = metaObject->method(methodId); + if(method.methodType() == QMetaMethod::Signal && method.name() == slotName){ + QByteArray slotName = method.name().prepend("on").append("("); + QStringList parameters; + for(int i = 0, j = method.parameterCount(); i < j; ++i){ + parameters << QMetaType::typeName(method.parameterType(i)); + } + slotName.append(parameters.join(",").toUtf8()).append(")"); + QByteArray theSignal = QMetaObject::normalizedSignature(method.methodSignature().constData()); + QByteArray theSlot = QMetaObject::normalizedSignature(slotName); + if(!QMetaObject::checkConnectArgs(theSignal, theSlot)){ + continue; + } + auto slotHandler = new Oxide::SlotHandler(OXIDE_SERVICE, parameters, once, onMessage, callback); + return slotHandler->connect(interface, methodId); + } + } + return false; + } + SlotHandler::SlotHandler(const QString& serviceName, QStringList parameters, bool once, std::function onMessage, std::function callback) + : QObject(0), + serviceName(serviceName), + parameters(parameters), + once(once), + m_disconnected(false), + onMessage(onMessage), + callback(callback) + { + watcher = new QDBusServiceWatcher(serviceName, QDBusConnection::systemBus(), QDBusServiceWatcher::WatchForUnregistration, this); + QObject::connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [=](const QString& name){ + Q_UNUSED(name); + if(!m_disconnected){ + qDebug() << QDBusError(QDBusError::ServiceUnknown, "The name " + serviceName + " is no longer registered"); + m_disconnected = true; + callback(); + } + }); + } + SlotHandler::~SlotHandler() {} + int SlotHandler::qt_metacall(QMetaObject::Call call, int id, void **arguments){ + id = QObject::qt_metacall(call, id, arguments); + if (id < 0 || call != QMetaObject::InvokeMetaMethod){ + return id; + } + Q_ASSERT(id < 1); + if(!m_disconnected){ + handleSlot(sender(), arguments); + } + return -1; + } + bool SlotHandler::connect(QObject* sender, int methodId){ + return QMetaObject::connect(sender, methodId, this, this->metaObject()->methodCount()); + } + void SlotHandler::handleSlot(QObject* api, void** arguments){ + Q_UNUSED(api); + QVariantList args; + for(int i = 0; i < parameters.length(); i++){ + auto typeId = QMetaType::type(parameters[i].toStdString().c_str()); + QMetaType type(typeId); + void* ptr = reinterpret_cast(arguments[i + 1]); + args << QVariant(typeId, ptr); + } + onMessage(args); + if(once){ + m_disconnected = true; + callback(); + } + } +} diff --git a/shared/liboxide/slothandler.h b/shared/liboxide/slothandler.h new file mode 100644 index 000000000..3597b912d --- /dev/null +++ b/shared/liboxide/slothandler.h @@ -0,0 +1,63 @@ +/*! + * \file slothandler.h + */ +#ifndef SLOTHANDLER_H +#define SLOTHANDLER_H + +#include "liboxide_global.h" + +#include +#include +#include + +namespace Oxide{ + /*! + * \brief Connect to a slot on a DBus interface + * \param Interface to connect to + * \param Slot to connect to + * \param Method to run when an event is recieved on the slot + * \return If the connect succeeded + */ + LIBOXIDE_EXPORT bool DBusConnect(QDBusAbstractInterface* interface, const QString& slotName, std::function onMessage); + /*! + * \brief Connect to a slot on a DBus interface + * \param Interface to connect to + * \param Slot to connect to + * \param Method to run when an event is recieved on the slot + * \param If this should disconnect after the first event + * \return If the connect succeeded + */ + LIBOXIDE_EXPORT bool DBusConnect(QDBusAbstractInterface* interface, const QString& slotName, std::function onMessage, const bool& once); + /*! + * \brief Connect to a slot on a DBus interface + * \param Interface to connect to + * \param Slot to connect to + * \param Method to run when an event is recieved on the slot + * \param Method to run when disconnecting + * \param If this should disconnect after the first event + * \return If the connect succeeded + */ + LIBOXIDE_EXPORT bool DBusConnect(QDBusAbstractInterface* interface, const QString& slotName, std::function onMessage, std::function callback, const bool& once=false); + /*! + * \brief A class for handling DBus slots + */ + class LIBOXIDE_EXPORT SlotHandler : public QObject { + public: + SlotHandler(const QString& serviceName, QStringList parameters, bool once, std::function onMessage, std::function callback); + ~SlotHandler(); + int qt_metacall(QMetaObject::Call call, int id, void **arguments); + bool connect(QObject* sender, int methodId); + + private: + QString serviceName; + QStringList parameters; + bool once; + bool m_disconnected; + QDBusServiceWatcher* watcher; + std::function onMessage; + std::function callback; + void handleSlot(QObject* api, void** arguments); + }; +} + +#endif // SLOTHANDLER_H diff --git a/shared/liboxide/sysobject.h b/shared/liboxide/sysobject.h index 6839450ec..4d3a9c710 100644 --- a/shared/liboxide/sysobject.h +++ b/shared/liboxide/sysobject.h @@ -13,8 +13,7 @@ namespace Oxide { /*! * \brief A class to make interacting with sysfs easier */ - class LIBOXIDE_EXPORT SysObject - { + class LIBOXIDE_EXPORT SysObject { public: explicit SysObject(QString path) : m_path(path.toStdString()){} /*! diff --git a/web/src/documentation/api/03_notification.rst b/web/src/documentation/api/02_notification.rst similarity index 65% rename from web/src/documentation/api/03_notification.rst rename to web/src/documentation/api/02_notification.rst index 45acc0c12..fbe0d7fd9 100644 --- a/web/src/documentation/api/03_notification.rst +++ b/web/src/documentation/api/02_notification.rst @@ -212,111 +212,3 @@ Example Usage rot --object Notification:$path notification call display rot --object Notification:$path notification call remove -Power API ---------- - -+----------------------+----------------------+----------------------+ -| Name | Specification | Description | -+======================+======================+======================+ -| state | ``INT32`` property | Currently requested | -| | (read/write) | power state. | -| | | Possible values: | -| | | - ``0`` Normal | -| | | - ``1`` Power Saving | -+----------------------+----------------------+----------------------+ -| batteryState | ``INT32`` property | Current battery | -| | (read) | state. | -| | | - ``0`` Unknown | -| | | - ``1`` Charging | -| | | - ``2`` Discharging | -| | | - ``3`` Not Present | -+----------------------+----------------------+----------------------+ -| batteryLevel | ``INT32`` property | Current battery | -| | (read) | percentage. | -+----------------------+----------------------+----------------------+ -| batteryTemperature | ``INT32`` property | Current battery | -| | (read) | temperature in | -| | | Celsius. | -+----------------------+----------------------+----------------------+ -| chargerState | ``INT32`` property | Current charger | -| | (read) | state. | -| | | - ``0`` Unknown | -| | | - ``1`` Connected | -| | | - ``2`` Not | -| | | Connected | -+----------------------+----------------------+----------------------+ -| stateChanged | signal | Signal sent when the | -| | - (out) ``INT32`` | requested power | -| | | state has changed. | -+----------------------+----------------------+----------------------+ -| batteryStateChanged | signal | Signal sent when the | -| | - (out) ``INT32`` | battery state has | -| | | changed. | -+----------------------+----------------------+----------------------+ -| batteryLevelChanged | signal | Signal sent when the | -| | - (out) ``INT32`` | battery level has | -| | | changed. | -+----------------------+----------------------+----------------------+ -| batte | signal | Signal sent when the | -| ryTemperatureChanged | - (out) ``INT32`` | battery temperature | -| | | has changed. | -+----------------------+----------------------+----------------------+ -| chargerStateChanged | signal | Signal sent when the | -| | - (out) ``INT32`` | charger state has | -| | | changed. | -+----------------------+----------------------+----------------------+ -| batteryWarning | signal | Signal sent when a | -| | | battery warning has | -| | | been detected. | -+----------------------+----------------------+----------------------+ -| batteryAlert | signal | Signal sent when a | -| | | battery alert has | -| | | been detected. | -+----------------------+----------------------+----------------------+ -| chargerWarning | signal | Signal sent when a | -| | | charger warning has | -| | | been detected. | -+----------------------+----------------------+----------------------+ - -.. _example-usage-6: - -Example Usage -~~~~~~~~~~~~~ - -.. code:: cpp - - #include - #include "dbusservice_interface.h" - #include "powerapi_interface.h" - - using namespace codes::eeems::oxide1; - - int main(int argc, char* argv[]){ - QCoreApplication app(argc, argv); - - auto bus = QDBusConnection::systemBus(); - General api(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); - qDebug() << "Requesting power API..."; - QDBusObjectPath path = api.requestAPI("power"); - if(path.path() == "/"){ - qDebug() << "Unable to get power API"; - return EXIT_FAILURE; - } - qDebug() << "Got the power API!"; - - Power power(OXIDE_SERVICE, path.path(), bus); - qDebug() << "Logging battery level:"; - qDebug() << power.batteryLevel(); - QObject::connect(&power, &Power::batteryLevelChanged, [](int batteryLevel){ - qDebug() << batteryLevel; - }); - return app.exec(); - } - -.. code:: shell - - #!/bin/bash - echo "Logging battery level:" - rot power get batteryLevel - rot power listen batteryLevelChanged - diff --git a/web/src/documentation/api/02_power.rst b/web/src/documentation/api/02_power.rst new file mode 100644 index 000000000..5ce80023e --- /dev/null +++ b/web/src/documentation/api/02_power.rst @@ -0,0 +1,108 @@ +========= +Power API +========= + ++----------------------+----------------------+----------------------+ +| Name | Specification | Description | ++======================+======================+======================+ +| state | ``INT32`` property | Currently requested | +| | (read/write) | power state. | +| | | Possible values: | +| | | - ``0`` Normal | +| | | - ``1`` Power Saving | ++----------------------+----------------------+----------------------+ +| batteryState | ``INT32`` property | Current battery | +| | (read) | state. | +| | | - ``0`` Unknown | +| | | - ``1`` Charging | +| | | - ``2`` Discharging | +| | | - ``3`` Not Present | ++----------------------+----------------------+----------------------+ +| batteryLevel | ``INT32`` property | Current battery | +| | (read) | percentage. | ++----------------------+----------------------+----------------------+ +| batteryTemperature | ``INT32`` property | Current battery | +| | (read) | temperature in | +| | | Celsius. | ++----------------------+----------------------+----------------------+ +| chargerState | ``INT32`` property | Current charger | +| | (read) | state. | +| | | - ``0`` Unknown | +| | | - ``1`` Connected | +| | | - ``2`` Not | +| | | Connected | ++----------------------+----------------------+----------------------+ +| stateChanged | signal | Signal sent when the | +| | - (out) ``INT32`` | requested power | +| | | state has changed. | ++----------------------+----------------------+----------------------+ +| batteryStateChanged | signal | Signal sent when the | +| | - (out) ``INT32`` | battery state has | +| | | changed. | ++----------------------+----------------------+----------------------+ +| batteryLevelChanged | signal | Signal sent when the | +| | - (out) ``INT32`` | battery level has | +| | | changed. | ++----------------------+----------------------+----------------------+ +| batte | signal | Signal sent when the | +| ryTemperatureChanged | - (out) ``INT32`` | battery temperature | +| | | has changed. | ++----------------------+----------------------+----------------------+ +| chargerStateChanged | signal | Signal sent when the | +| | - (out) ``INT32`` | charger state has | +| | | changed. | ++----------------------+----------------------+----------------------+ +| batteryWarning | signal | Signal sent when a | +| | | battery warning has | +| | | been detected. | ++----------------------+----------------------+----------------------+ +| batteryAlert | signal | Signal sent when a | +| | | battery alert has | +| | | been detected. | ++----------------------+----------------------+----------------------+ +| chargerWarning | signal | Signal sent when a | +| | | charger warning has | +| | | been detected. | ++----------------------+----------------------+----------------------+ + +.. _example-usage-6: + +Example Usage +~~~~~~~~~~~~~ + +.. code:: cpp + + #include + #include "dbusservice_interface.h" + #include "powerapi_interface.h" + + using namespace codes::eeems::oxide1; + + int main(int argc, char* argv[]){ + QCoreApplication app(argc, argv); + + auto bus = QDBusConnection::systemBus(); + General api(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); + qDebug() << "Requesting power API..."; + QDBusObjectPath path = api.requestAPI("power"); + if(path.path() == "/"){ + qDebug() << "Unable to get power API"; + return EXIT_FAILURE; + } + qDebug() << "Got the power API!"; + + Power power(OXIDE_SERVICE, path.path(), bus); + qDebug() << "Logging battery level:"; + qDebug() << power.batteryLevel(); + QObject::connect(&power, &Power::batteryLevelChanged, [](int batteryLevel){ + qDebug() << batteryLevel; + }); + return app.exec(); + } + +.. code:: shell + + #!/bin/bash + echo "Logging battery level:" + rot power get batteryLevel + rot power listen batteryLevelChanged diff --git a/web/src/documentation/api/02_screenshot.rst b/web/src/documentation/api/02_screenshot.rst new file mode 100644 index 000000000..c231f914b --- /dev/null +++ b/web/src/documentation/api/02_screenshot.rst @@ -0,0 +1,153 @@ +========== +Screen API +========== + ++---------------------+----------------------+----------------------+ +| Name | Specification | Description | ++=====================+======================+======================+ +| screenshots | ` | Get the list of | +| | `ARRAY OBJECT_PATH`` | screenshots on the | +| | property (read) | device. | ++---------------------+----------------------+----------------------+ +| screenshotAdded | signal | Signal sent when a | +| | - (out) | screenshot is added. | +| | ``OBJECT_PATH`` | | ++---------------------+----------------------+----------------------+ +| screenshotRemoved | signal | Signal sent when a | +| | - (out) | screenshot is | +| | ``OBJECT_PATH`` | removed. | ++---------------------+----------------------+----------------------+ +| screenshotModified | signal | Signal sent when a | +| | - (out) | screenshot is | +| | ``OBJECT_PATH`` | modified. | ++---------------------+----------------------+----------------------+ +| addScreenshot | method | Add a screenshot | +| | - (in) blob | taken by an | +| | ``ARRAY BYTE`` | application. | +| | - (out) | | +| | ``OBJECT_PATH`` | | ++---------------------+----------------------+----------------------+ +| drawFullScreenImage | method | Draw an image to the | +| | - (in) path | screen. | +| | ``STRING`` | | +| | - (out) ``BOOLEAN`` | | ++---------------------+----------------------+----------------------+ +| screenshot | method | Take a screenshot. | +| | - (out) | | +| | ``OBJECT_PATH`` | | ++---------------------+----------------------+----------------------+ + +.. _example-usage-7: + +Example Usage +~~~~~~~~~~~~~ + +.. code:: cpp + + #include + #include "dbusservice_interface.h" + #include "screenapi_interface.h" + + using namespace codes::eeems::oxide1; + + int main(int argc, char* argv[]){ + Q_UNUSED(argc); + Q_UNUSED(argv); + + auto bus = QDBusConnection::systemBus(); + General api(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); + qDebug() << "Requesting screen API..."; + QDBusObjectPath path = api.requestAPI("screen"); + if(path.path() == "/"){ + qDebug() << "Unable to get screen API"; + return EXIT_FAILURE; + } + qDebug() << "Got the screen API!"; + + Screen screen(OXIDE_SERVICE, path.path(), bus); + path = screen.screenshot(); + if(path.path() == "/"){ + qDebug() << "Screenshot failed"; + }else{ + qDebug() << "Screenshot taken"; + } + return EXIT_SUCCESS; + } + +.. code:: shell + + #!/bin/bash + echo -n "Screenshot " + if [ $(rot screen call screenshot | jq -cr) = "/" ]; then + echo "failed" + else + echo "taken" + fi + +Screenshot +~~~~~~~~~~ + ++----------+----------------------------+----------------------------+ +| Name | Specification | Description | ++==========+============================+============================+ +| blob | ``ARRAY BYTE`` property | The blob data of the | +| | (read/write) | screenshot. | ++----------+----------------------------+----------------------------+ +| path | ``STRING`` property (read) | The path to the screenshot | +| | | on disk. | ++----------+----------------------------+----------------------------+ +| modified | signal | Signal sent when the | +| | | screenshot is modified. | ++----------+----------------------------+----------------------------+ +| removed | signal | Signal sent when the | +| | | screenshot is removed. | ++----------+----------------------------+----------------------------+ +| remove | method | Remove the screenshot from | +| | | the device. | ++----------+----------------------------+----------------------------+ + +.. _example-usage-8: + +Example Usage +^^^^^^^^^^^^^ + +.. code:: cpp + + #include + #include "dbusservice_interface.h" + #include "screenapi_interface.h" + #include "screenshot_interface.h" + + using namespace codes::eeems::oxide1; + + int main(int argc, char* argv[]){ + Q_UNUSED(argc); + Q_UNUSED(argv); + + auto bus = QDBusConnection::systemBus(); + General api(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); + qDebug() << "Requesting screen API..."; + QDBusObjectPath path = api.requestAPI("screen"); + if(path.path() == "/"){ + qDebug() << "Unable to get screen API"; + return EXIT_FAILURE; + } + qDebug() << "Got the screen API!"; + + Screen screen(OXIDE_SERVICE, path.path(), bus); + for(auto path : screen.screenshots()){ + Screenshot(OXIDE_SERVICE, path.path(), bus).remove().waitForFinished(); + } + qDebug() << "Screenshots removed"; + return EXIT_SUCCESS; + } + +.. code:: shell + + #!/bin/bash + rot screen get screenshots \ + | jq -cr 'values | join("\n")' \ + | sed 's|/codes/eeems/oxide1/||' \ + | xargs -rI {} rot --object Screenshot:{} screen call remove + echo "Screenshots removed" + diff --git a/web/src/documentation/api/04_screenshot.rst b/web/src/documentation/api/02_system.rst similarity index 57% rename from web/src/documentation/api/04_screenshot.rst rename to web/src/documentation/api/02_system.rst index b993c00f5..0acbc0e60 100644 --- a/web/src/documentation/api/04_screenshot.rst +++ b/web/src/documentation/api/02_system.rst @@ -1,158 +1,6 @@ ========== -Screen API -========== - -+---------------------+----------------------+----------------------+ -| Name | Specification | Description | -+=====================+======================+======================+ -| screenshots | ` | Get the list of | -| | `ARRAY OBJECT_PATH`` | screenshots on the | -| | property (read) | device. | -+---------------------+----------------------+----------------------+ -| screenshotAdded | signal | Signal sent when a | -| | - (out) | screenshot is added. | -| | ``OBJECT_PATH`` | | -+---------------------+----------------------+----------------------+ -| screenshotRemoved | signal | Signal sent when a | -| | - (out) | screenshot is | -| | ``OBJECT_PATH`` | removed. | -+---------------------+----------------------+----------------------+ -| screenshotModified | signal | Signal sent when a | -| | - (out) | screenshot is | -| | ``OBJECT_PATH`` | modified. | -+---------------------+----------------------+----------------------+ -| addScreenshot | method | Add a screenshot | -| | - (in) blob | taken by an | -| | ``ARRAY BYTE`` | application. | -| | - (out) | | -| | ``OBJECT_PATH`` | | -+---------------------+----------------------+----------------------+ -| drawFullScreenImage | method | Draw an image to the | -| | - (in) path | screen. | -| | ``STRING`` | | -| | - (out) ``BOOLEAN`` | | -+---------------------+----------------------+----------------------+ -| screenshot | method | Take a screenshot. | -| | - (out) | | -| | ``OBJECT_PATH`` | | -+---------------------+----------------------+----------------------+ - -.. _example-usage-7: - -Example Usage -~~~~~~~~~~~~~ - -.. code:: cpp - - #include - #include "dbusservice_interface.h" - #include "screenapi_interface.h" - - using namespace codes::eeems::oxide1; - - int main(int argc, char* argv[]){ - Q_UNUSED(argc); - Q_UNUSED(argv); - - auto bus = QDBusConnection::systemBus(); - General api(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); - qDebug() << "Requesting screen API..."; - QDBusObjectPath path = api.requestAPI("screen"); - if(path.path() == "/"){ - qDebug() << "Unable to get screen API"; - return EXIT_FAILURE; - } - qDebug() << "Got the screen API!"; - - Screen screen(OXIDE_SERVICE, path.path(), bus); - path = screen.screenshot(); - if(path.path() == "/"){ - qDebug() << "Screenshot failed"; - }else{ - qDebug() << "Screenshot taken"; - } - return EXIT_SUCCESS; - } - -.. code:: shell - - #!/bin/bash - echo -n "Screenshot " - if [ $(rot screen call screenshot | jq -cr) = "/" ]; then - echo "failed" - else - echo "taken" - fi - -Screenshot -~~~~~~~~~~ - -+----------+----------------------------+----------------------------+ -| Name | Specification | Description | -+==========+============================+============================+ -| blob | ``ARRAY BYTE`` property | The blob data of the | -| | (read/write) | screenshot. | -+----------+----------------------------+----------------------------+ -| path | ``STRING`` property (read) | The path to the screenshot | -| | | on disk. | -+----------+----------------------------+----------------------------+ -| modified | signal | Signal sent when the | -| | | screenshot is modified. | -+----------+----------------------------+----------------------------+ -| removed | signal | Signal sent when the | -| | | screenshot is removed. | -+----------+----------------------------+----------------------------+ -| remove | method | Remove the screenshot from | -| | | the device. | -+----------+----------------------------+----------------------------+ - -.. _example-usage-8: - -Example Usage -^^^^^^^^^^^^^ - -.. code:: cpp - - #include - #include "dbusservice_interface.h" - #include "screenapi_interface.h" - #include "screenshot_interface.h" - - using namespace codes::eeems::oxide1; - - int main(int argc, char* argv[]){ - Q_UNUSED(argc); - Q_UNUSED(argv); - - auto bus = QDBusConnection::systemBus(); - General api(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); - qDebug() << "Requesting screen API..."; - QDBusObjectPath path = api.requestAPI("screen"); - if(path.path() == "/"){ - qDebug() << "Unable to get screen API"; - return EXIT_FAILURE; - } - qDebug() << "Got the screen API!"; - - Screen screen(OXIDE_SERVICE, path.path(), bus); - for(auto path : screen.screenshots()){ - Screenshot(OXIDE_SERVICE, path.path(), bus).remove().waitForFinished(); - } - qDebug() << "Screenshots removed"; - return EXIT_SUCCESS; - } - -.. code:: shell - - #!/bin/bash - rot screen get screenshots \ - | jq -cr 'values | join("\n")' \ - | sed 's|/codes/eeems/oxide1/||' \ - | xargs -rI {} rot --object Screenshot:{} screen call remove - echo "Screenshots removed" - System API ----------- +========== +----------------------+----------------------+----------------------+ | Name | Specification | Description | diff --git a/web/src/documentation/api/05_wifi.rst b/web/src/documentation/api/02_wifi.rst similarity index 100% rename from web/src/documentation/api/05_wifi.rst rename to web/src/documentation/api/02_wifi.rst From d1077636fd3c304641d5a74635c9623c6cd49d83 Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Wed, 25 Jan 2023 11:26:45 -0700 Subject: [PATCH 03/15] Unify logging, add missing arguments to notify-send (#283) * Unify logging, add missing arguments to notify-send * Make titles consistent --- applications/launcher/appitem.cpp | 6 +- applications/launcher/controller.cpp | 6 +- applications/launcher/oxide.pro | 1 + applications/lockscreen/controller.h | 10 +-- applications/lockscreen/decay.pro | 1 + applications/lockscreen/main.cpp | 2 +- applications/notify-send/main.cpp | 36 +++++++++- applications/notify-send/notify-send.pro | 1 + applications/process-manager/erode.pro | 1 + applications/process-manager/main.cpp | 2 +- applications/screenshot-tool/fret.pro | 1 + applications/screenshot-tool/main.cpp | 2 +- applications/screenshot-viewer/anxiety.pro | 1 + applications/screenshot-viewer/main.cpp | 2 +- applications/settings-manager/main.cpp | 2 +- applications/settings-manager/rot.pro | 1 + applications/system-service/apibase.h | 2 +- applications/system-service/application.cpp | 4 +- applications/system-service/application.h | 19 +++--- applications/system-service/appsapi.h | 4 +- applications/system-service/fifohandler.h | 4 +- applications/system-service/main.cpp | 4 +- applications/system-service/network.cpp | 2 - applications/system-service/powerapi.h | 10 +-- applications/system-service/tarnish.pro | 1 + applications/system-service/wifiapi.h | 3 +- applications/system-service/wlan.h | 2 +- applications/task-switcher/appitem.cpp | 6 +- applications/task-switcher/controller.h | 4 +- applications/task-switcher/corrupt.pro | 1 + applications/task-switcher/main.cpp | 2 +- shared/liboxide/debug.cpp | 11 +++ shared/liboxide/debug.h | 37 +++++++++++ shared/liboxide/eventfilter.cpp | 30 ++++----- shared/liboxide/json.cpp | 9 ++- shared/liboxide/liboxide.cpp | 52 +++++++-------- shared/liboxide/liboxide.h | 24 +------ shared/liboxide/liboxide.pro | 5 ++ shared/liboxide/meta.h | 74 +++++++++++++++++++++ shared/liboxide/power.cpp | 16 ++--- shared/liboxide/settingsfile.cpp | 25 ++----- shared/liboxide/settingsfile.h | 13 +--- shared/liboxide/slothandler.cpp | 7 +- shared/liboxide/sysobject.cpp | 7 +- web/src/documentation/api/01_general.rst | 4 +- 45 files changed, 286 insertions(+), 171 deletions(-) create mode 100644 shared/liboxide/debug.cpp create mode 100644 shared/liboxide/debug.h create mode 100644 shared/liboxide/meta.h diff --git a/applications/launcher/appitem.cpp b/applications/launcher/appitem.cpp index f0a95011b..a5b25a2ac 100755 --- a/applications/launcher/appitem.cpp +++ b/applications/launcher/appitem.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include "appitem.h" #include "dbusservice_interface.h" #include "appsapi_interface.h" @@ -15,7 +17,7 @@ bool AppItem::ok(){ return getApp() != nullptr; } void AppItem::execute(){ if(!getApp() || !app->isValid()){ - qWarning() << "Application instance is not valid"; + O_WARNING("Application instance is not valid"); return; } qDebug() << "Running application " << app->path(); @@ -28,7 +30,7 @@ void AppItem::execute(){ } void AppItem::stop(){ if(!getApp() || !app->isValid()){ - qWarning() << "Application instance is not valid"; + O_WARNING("Application instance is not valid"); return; } app->stop(); diff --git a/applications/launcher/controller.cpp b/applications/launcher/controller.cpp index 9d3156103..7e634d9b3 100644 --- a/applications/launcher/controller.cpp +++ b/applications/launcher/controller.cpp @@ -10,7 +10,7 @@ #include #include #include -#include "sysobject.h" +#include #include "controller.h" #include "dbusservice_interface.h" @@ -52,7 +52,7 @@ void Controller::loadSettings(){ if(!line.startsWith("#") && !line.isEmpty()){ QStringList parts = line.split("="); if(parts.length() != 2){ - qWarning() << " Wrong format on " << line; + O_WARNING(" Wrong format on " << line); continue; } QString lhs = parts.at(0).trimmed(); @@ -281,7 +281,7 @@ void Controller::importDraftApps(){ } QStringList parts = line.split("="); if(parts.length() != 2){ - qWarning() << "wrong format on " << line; + O_WARNING("wrong format on " << line); continue; } QString lhs = parts.at(0); diff --git a/applications/launcher/oxide.pro b/applications/launcher/oxide.pro index 59b7d0d7a..bcd2aa354 100644 --- a/applications/launcher/oxide.pro +++ b/applications/launcher/oxide.pro @@ -77,3 +77,4 @@ DEPENDPATH += $$PWD/../../shared/liboxide QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib VERSION = 2.5 +DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/lockscreen/controller.h b/applications/lockscreen/controller.h index ad041dc9e..19d443733 100644 --- a/applications/lockscreen/controller.h +++ b/applications/lockscreen/controller.h @@ -181,7 +181,7 @@ class Controller : public QObject { path = appsApi->getApplicationPath("codes.eeems.oxide"); } if(path.path() == "/"){ - qWarning() << "Unable to find startup application to launch."; + O_WARNING("Unable to find startup application to launch."); return; } Application app(OXIDE_SERVICE, path.path(), QDBusConnection::systemBus()); @@ -447,11 +447,11 @@ private slots: } auto path = settings.value("onLogin").toString(); if(!QFile::exists(path)){ - qWarning() << "onLogin script does not exist" << path; + O_WARNING("onLogin script does not exist" << path); return; } if(!QFileInfo(path).isExecutable()){ - qWarning() << "onLogin script is not executable" << path; + O_WARNING("onLogin script is not executable" << path); return; } QProcess::execute(path, QStringList()); @@ -462,11 +462,11 @@ private slots: } auto path = settings.value("onFailedLogin").toString(); if(!QFile::exists(path)){ - qWarning() << "onFailedLogin script does not exist" << path; + O_WARNING("onFailedLogin script does not exist" << path); return; } if(!QFileInfo(path).isExecutable()){ - qWarning() << "onFailedLogin script is not executable" << path; + O_WARNING("onFailedLogin script is not executable" << path); return; } QProcess::execute(path, QStringList()); diff --git a/applications/lockscreen/decay.pro b/applications/lockscreen/decay.pro index a83495738..b8cb9f5ec 100644 --- a/applications/lockscreen/decay.pro +++ b/applications/lockscreen/decay.pro @@ -54,3 +54,4 @@ DEPENDPATH += $$PWD/../../shared/liboxide QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib VERSION = 2.5 +DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/lockscreen/main.cpp b/applications/lockscreen/main.cpp index 0135c5767..7ae3f6a46 100644 --- a/applications/lockscreen/main.cpp +++ b/applications/lockscreen/main.cpp @@ -39,7 +39,7 @@ int main(int argc, char *argv[]){ app.setOrganizationName("Eeems"); app.setOrganizationDomain(OXIDE_SERVICE); app.setApplicationName("decay"); - app.setApplicationVersion(OXIDE_INTERFACE_VERSION); + app.setApplicationVersion(APP_VERSION); Controller controller(&app); QQmlApplicationEngine engine; QQmlContext* context = engine.rootContext(); diff --git a/applications/notify-send/main.cpp b/applications/notify-send/main.cpp index 1081d314a..93b0024eb 100644 --- a/applications/notify-send/main.cpp +++ b/applications/notify-send/main.cpp @@ -27,10 +27,9 @@ int main(int argc, char *argv[]){ app.setOrganizationName("Eeems"); app.setOrganizationDomain(OXIDE_SERVICE); app.setApplicationName("notify-send"); - app.setApplicationVersion(OXIDE_INTERFACE_VERSION); + app.setApplicationVersion(APP_VERSION); QCommandLineParser parser; parser.setApplicationDescription("a program to send desktop notifications"); - parser.addHelpOption(); parser.applicationDescription(); parser.addVersionOption(); parser.addPositionalArgument("summary", "Summary of the notification", "{SUMMARY}"); @@ -74,8 +73,39 @@ int main(int argc, char *argv[]){ "Show a transient notification. This notification will be removed immediatly after being shown on the screen" ); parser.addOption(transientOption); - // TODO add action, urgency, category, and hint + QCommandLineOption urgencyOption( + {"u", "urgency"}, + "NOT IMPLEMENTED", + "LEVEL" + ); + parser.addOption(urgencyOption); + QCommandLineOption appOption( + {"A", "action"}, + "NOT IMPLEMENTED", + "[NAME=]TEXT" + ); + parser.addOption(appOption); + QCommandLineOption categoryOption( + {"c", "category"}, + "NOT IMPLEMENTED", + "TYPE[,TYPE]" + ); + parser.addOption(categoryOption); + QCommandLineOption hintOption( + {"h", "hint"}, + "NOT IMPLEMENTED", + "TYPE:NAME:VALUE" + ); + parser.addOption(hintOption); + QCommandLineOption helpOption( + {"?", "help"}, + "Show help and exit" + ); + parser.addOption(helpOption); parser.process(app); + if(parser.isSet(helpOption)){ + parser.showHelp(); + } QStringList args = parser.positionalArguments(); if(args.isEmpty()){ #ifdef SENTRY diff --git a/applications/notify-send/notify-send.pro b/applications/notify-send/notify-send.pro index b56b568b3..65e961be2 100644 --- a/applications/notify-send/notify-send.pro +++ b/applications/notify-send/notify-send.pro @@ -43,3 +43,4 @@ QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib HEADERS += VERSION = 2.5 +DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/process-manager/erode.pro b/applications/process-manager/erode.pro index e2f984b79..ead79d1d1 100755 --- a/applications/process-manager/erode.pro +++ b/applications/process-manager/erode.pro @@ -55,3 +55,4 @@ DEPENDPATH += $$PWD/../../shared/liboxide QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib VERSION = 2.5 +DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/process-manager/main.cpp b/applications/process-manager/main.cpp index 3da8741ab..ad47fb2a5 100755 --- a/applications/process-manager/main.cpp +++ b/applications/process-manager/main.cpp @@ -41,7 +41,7 @@ int main(int argc, char *argv[]){ app.setOrganizationDomain(OXIDE_SERVICE); app.setApplicationName("tarnish"); app.setApplicationDisplayName("Process Monitor"); - app.setApplicationVersion(OXIDE_INTERFACE_VERSION); + app.setApplicationVersion(APP_VERSION); EventFilter filter; app.installEventFilter(&filter); QQmlApplicationEngine engine; diff --git a/applications/screenshot-tool/fret.pro b/applications/screenshot-tool/fret.pro index b064b9355..af1f94973 100644 --- a/applications/screenshot-tool/fret.pro +++ b/applications/screenshot-tool/fret.pro @@ -48,3 +48,4 @@ DEPENDPATH += $$PWD/../../shared/liboxide QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib VERSION = 2.5 +DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/screenshot-tool/main.cpp b/applications/screenshot-tool/main.cpp index c0c093500..9ce640419 100644 --- a/applications/screenshot-tool/main.cpp +++ b/applications/screenshot-tool/main.cpp @@ -53,7 +53,7 @@ int main(int argc, char *argv[]){ app.setOrganizationName("Eeems"); app.setOrganizationDomain(OXIDE_SERVICE); app.setApplicationName("fret"); - app.setApplicationVersion(OXIDE_INTERFACE_VERSION); + app.setApplicationVersion(APP_VERSION); auto bus = QDBusConnection::systemBus(); qDebug() << "Waiting for tarnish to start up..."; while(!bus.interface()->registeredServiceNames().value().contains(OXIDE_SERVICE)){ diff --git a/applications/screenshot-viewer/anxiety.pro b/applications/screenshot-viewer/anxiety.pro index ae658a54b..9565b0e1d 100644 --- a/applications/screenshot-viewer/anxiety.pro +++ b/applications/screenshot-viewer/anxiety.pro @@ -60,3 +60,4 @@ DEPENDPATH += $$PWD/../../shared/liboxide QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib VERSION = 2.5 +DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/screenshot-viewer/main.cpp b/applications/screenshot-viewer/main.cpp index d1e77af14..bb8d824af 100644 --- a/applications/screenshot-viewer/main.cpp +++ b/applications/screenshot-viewer/main.cpp @@ -47,7 +47,7 @@ int main(int argc, char *argv[]){ app.setOrganizationDomain(OXIDE_SERVICE); app.setApplicationName("anxiety"); app.setApplicationDisplayName("Screenshots"); - app.setApplicationVersion(OXIDE_INTERFACE_VERSION); + app.setApplicationVersion(APP_VERSION); Controller controller(&app); QQmlApplicationEngine engine; QQmlContext* context = engine.rootContext(); diff --git a/applications/settings-manager/main.cpp b/applications/settings-manager/main.cpp index 5a1af4f29..736b52a39 100644 --- a/applications/settings-manager/main.cpp +++ b/applications/settings-manager/main.cpp @@ -37,7 +37,7 @@ int main(int argc, char *argv[]){ app.setOrganizationName("Eeems"); app.setOrganizationDomain(OXIDE_SERVICE); app.setApplicationName("rot"); - app.setApplicationVersion(OXIDE_INTERFACE_VERSION); + app.setApplicationVersion(APP_VERSION); QCommandLineParser parser; parser.setApplicationDescription("Oxide settings tool"); parser.addHelpOption(); diff --git a/applications/settings-manager/rot.pro b/applications/settings-manager/rot.pro index 7419a6a49..6c6097b4d 100644 --- a/applications/settings-manager/rot.pro +++ b/applications/settings-manager/rot.pro @@ -52,3 +52,4 @@ QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib HEADERS += VERSION = 2.5 +DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/system-service/apibase.h b/applications/system-service/apibase.h index 1f74e0e51..03df40b54 100644 --- a/applications/system-service/apibase.h +++ b/applications/system-service/apibase.h @@ -7,9 +7,9 @@ #include #include +#include "../../shared/liboxide/liboxide.h" #include -#include "../../shared/liboxide/liboxide.h" class APIBase : public QObject, protected QDBusContext { Q_OBJECT diff --git a/applications/system-service/application.cpp b/applications/system-service/application.cpp index d34d49199..67248db64 100644 --- a/applications/system-service/application.cpp +++ b/applications/system-service/application.cpp @@ -477,13 +477,13 @@ void Application::showSplashScreen(){ void Application::powerStateDataRecieved(FifoHandler* handler, const QString& data){ Q_UNUSED(handler); if(!permissions().contains("power")){ - qWarning() << "Denied powerState request"; + O_WARNING("Denied powerState request"); return; } if((QStringList() << "mem" << "freeze" << "standby").contains(data)){ systemAPI->suspend(); }else{ - qWarning() << "Unknown power state call: " << data; + O_WARNING("Unknown power state call: " << data); } } void Application::startSpan(std::string operation, std::string description){ diff --git a/applications/system-service/application.h b/applications/system-service/application.h index 746c8ffff..35fa63ee6 100644 --- a/applications/system-service/application.h +++ b/applications/system-service/application.h @@ -33,7 +33,6 @@ #include #include -#include "../../shared/liboxide/liboxide.h" #include "mxcfb.h" #include "screenapi.h" #include "fifohandler.h" @@ -358,7 +357,7 @@ class Application : public QObject{ QBuffer buffer(&bytes); buffer.open(QIODevice::WriteOnly); if(!EPFrameBuffer::framebuffer()->save(&buffer, "JPG", 100)){ - qWarning() << "Failed to save buffer"; + O_WARNING("Failed to save buffer"); } }); qDebug() << "Compressing data..."; @@ -549,14 +548,14 @@ private slots: auto csource = source.toStdString(); qDebug() << "mount" << source << target; if(mount(csource.c_str(), ctarget.c_str(), NULL, MS_BIND, NULL)){ - qWarning() << "Failed to create bindmount: " << ::strerror(errno); + O_WARNING("Failed to create bindmount: " << ::strerror(errno)); return; } if(!readOnly){ return; } if(mount(csource.c_str(), ctarget.c_str(), NULL, MS_REMOUNT | MS_BIND | MS_RDONLY, NULL)){ - qWarning() << "Failed to remount bindmount read only: " << ::strerror(errno); + O_WARNING("Failed to remount bindmount read only: " << ::strerror(errno)); } qDebug() << "mount ro" << source << target; } @@ -565,7 +564,7 @@ private slots: umount(path); qDebug() << "sysfs" << path; if(mount("none", path.toStdString().c_str(), "sysfs", 0, "")){ - qWarning() << "Failed to mount sysfs: " << ::strerror(errno); + O_WARNING("Failed to mount sysfs: " << ::strerror(errno)); } } void ramdisk(const QString& path){ @@ -573,7 +572,7 @@ private slots: umount(path); qDebug() << "ramdisk" << path; if(mount("tmpfs", path.toStdString().c_str(), "tmpfs", 0, "size=249m,mode=755")){ - qWarning() << "Failed to create ramdisk: " << ::strerror(errno); + O_WARNING("Failed to create ramdisk: " << ::strerror(errno)); } } void umount(const QString& path){ @@ -594,17 +593,17 @@ private slots: } FifoHandler* mkfifo(const QString& name, const QString& target){ if(isMounted(target)){ - qWarning() << target << "Already mounted"; + O_WARNING(target << "Already mounted"); return fifos.contains(name) ? fifos[name] : nullptr; } auto source = resourcePath() + "/" + name; if(!QFile::exists(source)){ if(::mkfifo(source.toStdString().c_str(), 0644)){ - qWarning() << "Failed to create " << name << " fifo: " << ::strerror(errno); + O_WARNING("Failed to create " << name << " fifo: " << ::strerror(errno)); } } if(!QFile::exists(source)){ - qWarning() << "No fifo for " << name; + O_WARNING("No fifo for " << name); return fifos.contains(name) ? fifos[name] : nullptr; } bind(source, target); @@ -630,7 +629,7 @@ private slots: } qDebug() << "symlink" << source << target; if(::symlink(target.toStdString().c_str(), source.toStdString().c_str())){ - qWarning() << "Failed to create symlink: " << ::strerror(errno); + O_WARNING("Failed to create symlink: " << ::strerror(errno)); return; } } diff --git a/applications/system-service/appsapi.h b/applications/system-service/appsapi.h index 2c2f7c523..15025bcb0 100644 --- a/applications/system-service/appsapi.h +++ b/applications/system-service/appsapi.h @@ -416,7 +416,7 @@ class AppsAPI : public APIBase { void forceRecordPreviousApplication(){ auto currentApplication = getApplication(this->currentApplicationNoSecurityCheck()); if(currentApplication == nullptr){ - qWarning() << "Unable to find current application"; + O_WARNING("Unable to find current application"); return; } auto name = currentApplication->name(); @@ -427,7 +427,7 @@ class AppsAPI : public APIBase { void recordPreviousApplication(){ auto currentApplication = getApplication(this->currentApplicationNoSecurityCheck()); if(currentApplication == nullptr){ - qWarning() << "Unable to find current application"; + O_WARNING("Unable to find current application"); return; } if(currentApplication->qPath() == lockscreenApplication()){ diff --git a/applications/system-service/fifohandler.h b/applications/system-service/fifohandler.h index c524cfa61..799856aed 100644 --- a/applications/system-service/fifohandler.h +++ b/applications/system-service/fifohandler.h @@ -26,7 +26,7 @@ class FifoHandler : public QObject { emit started(); in.open(this->path.toStdString().c_str(), std::ifstream::in); if(!in.good()){ - qWarning() << "Unable to open fifi (in)" << ::strerror(errno); + O_WARNING("Unable to open fifi (in)" << ::strerror(errno)); } timer.start(10); }); @@ -38,7 +38,7 @@ class FifoHandler : public QObject { QThread::create([this]{ out.open(this->path.toStdString().c_str(), std::ifstream::out); if(!out.good()){ - qWarning() << "Unable to open fifi (out)" << ::strerror(errno); + O_WARNING("Unable to open fifi (out)" << ::strerror(errno)); } })->start(); moveToThread(&_thread); diff --git a/applications/system-service/main.cpp b/applications/system-service/main.cpp index 54f703d2b..5c43063f9 100755 --- a/applications/system-service/main.cpp +++ b/applications/system-service/main.cpp @@ -18,11 +18,11 @@ void sigHandler(int signal){ int main(int argc, char *argv[]){ if(deviceSettings.getDeviceType() == Oxide::DeviceSettings::RM2 && getenv("RM2FB_ACTIVE") == nullptr){ - qWarning() << "rm2fb not detected. Running xochitl instead!"; + O_WARNING("rm2fb not detected. Running xochitl instead!"); return QProcess::execute("/usr/bin/xochitl", QStringList()); } if (strcmp(qt_version, QT_VERSION_STR) != 0){ - qDebug() << "Version mismatch, Runtime: " << qt_version << ", Build: " << QT_VERSION_STR; + O_WARNING("Version mismatch, Runtime: " << qt_version << ", Build: " << QT_VERSION_STR); } #ifdef __arm__ // Setup epaper diff --git a/applications/system-service/network.cpp b/applications/system-service/network.cpp index ba15c95d9..86dc0da4c 100644 --- a/applications/system-service/network.cpp +++ b/applications/system-service/network.cpp @@ -3,8 +3,6 @@ #include "network.h" #include "wifiapi.h" -//#define DEBUG - QSet none{ "NONE", "WPA-NONE" diff --git a/applications/system-service/powerapi.h b/applications/system-service/powerapi.h index 9e3650783..c3eabdf67 100644 --- a/applications/system-service/powerapi.h +++ b/applications/system-service/powerapi.h @@ -1,7 +1,7 @@ #ifndef BATTERYAPI_H #define BATTERYAPI_H -#include +#include #include #include @@ -168,7 +168,7 @@ class PowerAPI : public APIBase { setBatteryState(BatteryUnknown); } if(!m_batteryWarning){ - qWarning() << "Can't find battery information"; + O_WARNING("Can't find battery information"); m_batteryWarning = true; emit batteryWarning(); } @@ -176,11 +176,11 @@ class PowerAPI : public APIBase { } if(!Oxide::Power::batteryPresent()){ if(m_batteryState != BatteryNotPresent){ - qWarning() << "Battery is somehow not in the device?"; + O_WARNING("Battery is somehow not in the device?"); setBatteryState(BatteryNotPresent); } if(!m_batteryWarning){ - qWarning() << "Battery is somehow not in the device?"; + O_WARNING("Battery is somehow not in the device?"); m_batteryWarning = true; emit batteryWarning(); } @@ -221,7 +221,7 @@ class PowerAPI : public APIBase { setChargerState(ChargerUnknown); } if(!m_chargerWarning){ - qWarning() << "Can't find charger information"; + O_WARNING("Can't find charger information"); m_chargerWarning = true; emit chargerWarning(); } diff --git a/applications/system-service/tarnish.pro b/applications/system-service/tarnish.pro index dfbc183f9..1f57e0657 100644 --- a/applications/system-service/tarnish.pro +++ b/applications/system-service/tarnish.pro @@ -107,3 +107,4 @@ DEPENDPATH += $$PWD/../../shared/liboxide QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib VERSION = 2.5 +DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/system-service/wifiapi.h b/applications/system-service/wifiapi.h index 03b3e89b9..f74d8cf43 100644 --- a/applications/system-service/wifiapi.h +++ b/applications/system-service/wifiapi.h @@ -8,6 +8,7 @@ #include #include +#include #include "apibase.h" #include "wlan.h" @@ -79,7 +80,7 @@ class WifiAPI : public APIBase { Oxide::Sentry::Span* span = Oxide::Sentry::start_span(s, "connect", "Connect to DBus interface"); QDBusConnection bus = QDBusConnection::systemBus(); if(!bus.isConnected()){ - qWarning("Failed to connect to system bus."); + O_WARNING("Failed to connect to system bus."); throw QException(); } validateSupplicant(); diff --git a/applications/system-service/wlan.h b/applications/system-service/wlan.h index 557b0dd9d..fcc9bb206 100644 --- a/applications/system-service/wlan.h +++ b/applications/system-service/wlan.h @@ -70,7 +70,7 @@ class Wlan : public QObject, public SysObject { signed int rssi(){ QDBusMessage message = m_interface->call("SignalPoll"); if (message.type() == QDBusMessage::ErrorMessage) { - qWarning() << "SignalPoll error: " << message.errorMessage(); + O_WARNING("SignalPoll error: " << message.errorMessage()); return -100; } auto props = qdbus_cast(message.arguments().at(0).value().variant().value()); diff --git a/applications/task-switcher/appitem.cpp b/applications/task-switcher/appitem.cpp index df8f68e56..21a44199f 100755 --- a/applications/task-switcher/appitem.cpp +++ b/applications/task-switcher/appitem.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include "appitem.h" #include "dbusservice_interface.h" #include "appsapi_interface.h" @@ -14,7 +16,7 @@ bool AppItem::ok(){ return getApp() != nullptr; } void AppItem::execute(){ if(!getApp() || !app->isValid()){ - qWarning() << "Application instance is not valid"; + O_WARNING("Application instance is not valid"); return; } qDebug() << "Running application " << property("name").toString(); @@ -28,7 +30,7 @@ void AppItem::execute(){ } void AppItem::stop(){ if(!getApp() || !app->isValid()){ - qWarning() << "Application instance is not valid"; + O_WARNING("Application instance is not valid"); return; } QDBusPendingReply reply = app->stop(); diff --git a/applications/task-switcher/controller.h b/applications/task-switcher/controller.h index 69a1ac929..b9d4d11e2 100644 --- a/applications/task-switcher/controller.h +++ b/applications/task-switcher/controller.h @@ -213,7 +213,7 @@ class Controller : public QObject { Oxide::Sentry::sentry_span(s, name.toStdString(), "Load image from application", [this, &img, previousApplications, name]{ auto path = ((QDBusObjectPath)appsApi->getApplicationPath(name)).path(); if(path == "/"){ - qWarning() << "Unable to get save screen for" << name; + O_WARNING("Unable to get save screen for" << name); return; } auto bus = QDBusConnection::systemBus(); @@ -221,7 +221,7 @@ class Controller : public QObject { auto data = app.screenCapture(); auto image = QImage::fromData(data, "JPG"); if(image.isNull()){ - qWarning() << "Image for " << name << " is corrupt, trying next application"; + O_WARNING("Image for " << name << " is corrupt, trying next application"); return; } img = new QImage(image); diff --git a/applications/task-switcher/corrupt.pro b/applications/task-switcher/corrupt.pro index 498fa025a..e53552e23 100644 --- a/applications/task-switcher/corrupt.pro +++ b/applications/task-switcher/corrupt.pro @@ -55,3 +55,4 @@ DEPENDPATH += $$PWD/../../shared/liboxide QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib VERSION = 2.5 +DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/task-switcher/main.cpp b/applications/task-switcher/main.cpp index a01d65c12..1c18b3855 100644 --- a/applications/task-switcher/main.cpp +++ b/applications/task-switcher/main.cpp @@ -48,7 +48,7 @@ int main(int argc, char *argv[]){ app.setOrganizationName("Eeems"); app.setOrganizationDomain(OXIDE_SERVICE); app.setApplicationName("corrupt"); - app.setApplicationVersion(OXIDE_INTERFACE_VERSION); + app.setApplicationVersion(APP_VERSION); auto screenProvider = new ScreenProvider(&app); Controller controller(&app, screenProvider); QQmlApplicationEngine engine; diff --git a/shared/liboxide/debug.cpp b/shared/liboxide/debug.cpp new file mode 100644 index 000000000..f885ced16 --- /dev/null +++ b/shared/liboxide/debug.cpp @@ -0,0 +1,11 @@ +#include "debug.h" + +namespace Oxide { + bool debugEnabled(){ + if(getenv("DEBUG") == NULL){ + return false; + } + QString env = qgetenv("DEBUG"); + return !(QStringList() << "0" << "n" << "no" << "false").contains(env.toLower()); + } +} diff --git a/shared/liboxide/debug.h b/shared/liboxide/debug.h new file mode 100644 index 000000000..2434ca599 --- /dev/null +++ b/shared/liboxide/debug.h @@ -0,0 +1,37 @@ +/*! + * \file debug.h + */ +#ifndef DEBUG_H +#define DEBUG_H + +#include "liboxide_global.h" + +#include + +/*! + * \def O_DEBUG(msg) + * \brief Log a debug message if compiled with DEBUG mode, and debugging is enabled + * \param Debug message to log + */ +#ifdef DEBUG +#define O_DEBUG(msg) if(Oxide::debugEnabled()){ qDebug() << msg; } +#else +#define O_DEBUG(msg) +#endif +/*! + * \def O_WARNING(msg) + * \brief Log a warning message if debugging is enabled + * \param Warning message to log + */ +#define O_WARNING(msg) if(Oxide::debugEnabled()){ qWarning() << msg; } + +namespace Oxide { + /*! + * \brief Return the state of debugging + * \return Debugging state + * \snippet examples/oxide.cpp debugEnabled + */ + LIBOXIDE_EXPORT bool debugEnabled(); +} + +#endif // DEBUG_H diff --git a/shared/liboxide/eventfilter.cpp b/shared/liboxide/eventfilter.cpp index 5912c1b08..dd9b523bf 100644 --- a/shared/liboxide/eventfilter.cpp +++ b/shared/liboxide/eventfilter.cpp @@ -1,6 +1,7 @@ #include "eventfilter.h" +#include "debug.h" + #include -#include #include #include #include @@ -11,6 +12,11 @@ #define WACOM_X_SCALAR (float(DISPLAYWIDTH) / float(DISPLAYHEIGHT)) #define WACOM_Y_SCALAR (float(DISPLAYHEIGHT) / float(DISPLAYWIDTH)) //#define DEBUG_EVENTS +#ifdef DEBUG_EVENTS +#define O_DEBUG_EVENT(msg) O_DEBUG(msg) +#else +#define O_DEBUG_EVENT(msg) +#endif namespace Oxide{ EventFilter::EventFilter(QObject *parent) : QObject(parent), root(nullptr){} @@ -86,9 +92,7 @@ namespace Oxide{ auto pos = mouseEvent->globalPos(); for(auto postWidget : widgetsAt(root, pos)){ if(parentCount((QQuickItem*)postWidget)){ -#ifdef DEBUG_EVENTS - qDebug() << "postWidget: " << postWidget; -#endif + O_DEBUG_EVENT("postWidget: " << postWidget); auto event = new QMouseEvent( mouseEvent->type(), mouseEvent->localPos(), mouseEvent->windowPos(), mouseEvent->screenPos(), mouseEvent->button(), mouseEvent->buttons(), @@ -110,19 +114,13 @@ namespace Oxide{ bool filtered = QObject::eventFilter(obj, ev); if(!filtered){ if(type == QEvent::TabletPress){ -#ifdef DEBUG_EVENTS - qDebug() << ev; -#endif + O_DEBUG_EVENT(ev); postEvent(QMouseEvent::MouseButtonPress, ev, root); }else if(type == QEvent::TabletRelease){ -#ifdef DEBUG_EVENTS - qDebug() << ev; -#endif + O_DEBUG_EVENT(ev); postEvent(QMouseEvent::MouseButtonRelease, ev, root); }else if(type == QEvent::TabletMove){ -#ifdef DEBUG_EVENTS - qDebug() << ev; -#endif + O_DEBUG_EVENT(ev); postEvent(QMouseEvent::MouseMove, ev, root); } #ifdef DEBUG_EVENTS @@ -133,11 +131,11 @@ namespace Oxide{ ){ for(auto widget : widgetsAt(root, ((QMouseEvent*)ev)->globalPos())){ if(parentCount((QQuickItem*)widget)){ - qDebug() << "postWidget: " << widget; + O_DEBUG("postWidget: " << widget); } } - qDebug() << obj; - qDebug() << ev; + O_DEBUG(obj); + O_DEBUG(ev); } #endif } diff --git a/shared/liboxide/json.cpp b/shared/liboxide/json.cpp index cc40ecbe8..712e052fa 100644 --- a/shared/liboxide/json.cpp +++ b/shared/liboxide/json.cpp @@ -1,6 +1,6 @@ #include "json.h" +#include "debug.h" -#include #include #include @@ -101,7 +101,6 @@ namespace Oxide::JSON { return sanitizeForJson(list); } if(type == QDBusArgument::MapType){ - qDebug() << "Map Type"; QMap map; arg.beginMap(); while(!arg.atEnd()){ @@ -114,7 +113,7 @@ namespace Oxide::JSON { arg.endMap(); return sanitizeForJson(QVariant::fromValue(map)); } - qDebug() << "Unable to sanitize QDBusArgument as it is an unknown type"; + O_WARNING("Unable to sanitize QDBusArgument as it is an unknown type"); return QVariant(); } QVariant sanitizeForJson(QVariant value){ @@ -199,8 +198,8 @@ namespace Oxide::JSON { QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson("[" + json + "]", &error); if(error.error != QJsonParseError::NoError){ - qDebug() << "Unable to read json value" << error.errorString(); - qDebug() << "Value to parse" << json; + O_WARNING("Unable to read json value" << error.errorString()); + O_WARNING("Value to parse" << json); } return doc.array().first().toVariant(); } diff --git a/shared/liboxide/liboxide.cpp b/shared/liboxide/liboxide.cpp index d3b871fa4..9568f6ca5 100644 --- a/shared/liboxide/liboxide.cpp +++ b/shared/liboxide/liboxide.cpp @@ -1,6 +1,5 @@ #include "liboxide.h" -#include #include #include #include @@ -41,21 +40,21 @@ static void *invalid_mem = (void *)1; void logMachineIdError(int error, QString name, QString path){ if(error == -ENOENT){ - qWarning() << "/etc/machine-id is missing"; + O_WARNING("/etc/machine-id is missing"); }else if(error == -ENOMEDIUM){ - qWarning() << path + " is empty or all zeros"; + O_WARNING(path + " is empty or all zeros"); }else if(error == -EIO){ - qWarning() << path + " has the incorrect format"; + O_WARNING(path + " has the incorrect format"); }else if(error == -EPERM){ - qWarning() << path + " access denied"; + O_WARNING(path + " access denied"); } if(error == -EINVAL){ - qWarning() << "Error while reading " + name + ": Buffer invalid"; + O_WARNING("Error while reading " + name + ": Buffer invalid"); }else if(error == -ENXIO){ - qWarning() << "Error while reading " + name + ": No invocation ID is set"; + O_WARNING("Error while reading " + name + ": No invocation ID is set"); }else if(error == -EOPNOTSUPP){ - qWarning() << "Error while reading " + name + ": Operation not supported"; + O_WARNING("Error while reading " + name + ": Operation not supported"); }else{ - qWarning() << "Unexpected error code reading " + name + ":" << strerror(error); + O_WARNING("Unexpected error code reading " + name + ":" << strerror(error)); } } std::string getAppSpecific(sd_id128_t base){ @@ -212,16 +211,12 @@ namespace Oxide { // Handle settings changing QObject::connect(&sharedSettings, &SharedSettings::telemetryChanged, [name, argv, autoSessionTracking](bool telemetry){ Q_UNUSED(telemetry) - if(debugEnabled()){ - qDebug() << "Telemetry changed to" << telemetry; - } + O_DEBUG("Telemetry changed to" << telemetry); sentry_init(name, argv, autoSessionTracking); }); QObject::connect(&sharedSettings, &SharedSettings::crashReportChanged, [name, argv, autoSessionTracking](bool crashReport){ Q_UNUSED(crashReport) - if(debugEnabled()){ - qDebug() << "CrashReport changed to" << crashReport; - } + O_DEBUG("CrashReport changed to" << crashReport); sentry_init(name, argv, autoSessionTracking); }); #else @@ -392,50 +387,51 @@ namespace Oxide { DeviceSettings::DeviceSettings(): _deviceType(DeviceType::RM1) { readDeviceType(); + O_DEBUG("Looking for input devices..."); QDir dir("/dev/input"); - qDebug() << "Looking for input devices..."; for(auto path : dir.entryList(QDir::Files | QDir::NoSymLinks | QDir::System)){ - qDebug() << (" Checking " + path + "...").toStdString().c_str(); + O_DEBUG((" Checking " + path + "...").toStdString().c_str()); QString fullPath(dir.path() + "/" + path); QFile device(fullPath); device.open(QIODevice::ReadOnly); int fd = device.handle(); int version; if (ioctl(fd, EVIOCGVERSION, &version)){ - qDebug() << " Invalid"; + O_DEBUG(" Invalid"); continue; } unsigned long bit[EV_MAX]; ioctl(fd, EVIOCGBIT(0, EV_MAX), bit); if (test_bit(EV_KEY, bit)) { if (checkBitSet(fd, EV_KEY, BTN_STYLUS) && test_bit(EV_ABS, bit)) { - qDebug() << " Wacom input device detected"; + O_DEBUG(" Wacom input device detected"); wacomPath = fullPath.toStdString(); continue; } if (checkBitSet(fd, EV_KEY, KEY_POWER)) { - qDebug() << " Buttons input device detected"; + O_DEBUG(" Buttons input device detected"); buttonsPath = fullPath.toStdString(); continue; } } if (checkBitSet(fd, EV_ABS, ABS_MT_SLOT)) { - qDebug() << " Touch input device detected"; + O_DEBUG(" Touch input device detected"); touchPath = fullPath.toStdString(); continue; } - qDebug() << " Invalid"; + O_DEBUG(" Invalid"); } if (wacomPath.empty()) { - qWarning() << "Wacom input device not found"; + O_WARNING("Wacom input device not found"); } if (touchPath.empty()) { - qWarning() << "Touch input device not found"; + O_WARNING("Touch input device not found"); } if (buttonsPath.empty()){ - qWarning() << "Buttons input device not found"; + O_WARNING("Buttons input device not found"); } } + DeviceSettings::~DeviceSettings(){} bool DeviceSettings::checkBitSet(int fd, int type, int i) { unsigned long bit[NBITS(KEY_MAX)]; ioctl(fd, EVIOCGBIT(type, KEY_MAX), bit); @@ -445,18 +441,18 @@ namespace Oxide { void DeviceSettings::readDeviceType() { QFile file("/sys/devices/soc0/machine"); if(!file.exists() || !file.open(QIODevice::ReadOnly | QIODevice::Text)){ - qDebug() << "Couldn't open " << file.fileName(); + O_DEBUG("Couldn't open " << file.fileName()); _deviceType = DeviceType::Unknown; return; } QTextStream in(&file); QString modelName = in.readLine(); if (modelName.startsWith("reMarkable 2")) { - qDebug() << "RM2 detected..."; + O_DEBUG("RM2 detected..."); _deviceType = DeviceType::RM2; return; } - qDebug() << "RM1 detected..."; + O_DEBUG("RM1 detected..."); _deviceType = DeviceType::RM1; } diff --git a/shared/liboxide/liboxide.h b/shared/liboxide/liboxide.h index aafd28de8..3143ad1c0 100644 --- a/shared/liboxide/liboxide.h +++ b/shared/liboxide/liboxide.h @@ -6,11 +6,13 @@ #include "liboxide_global.h" +#include "meta.h" #include "settingsfile.h" #include "power.h" #include "json.h" #include "signalhandler.h" #include "slothandler.h" +#include "debug.h" #include #include @@ -18,26 +20,6 @@ #include #include -#define WPA_SUPPLICANT_SERVICE "fi.w1.wpa_supplicant1" -#define WPA_SUPPLICANT_SERVICE_PATH "/fi/w1/wpa_supplicant1" - -#define OXIDE_SERVICE "codes.eeems.oxide1" -#define OXIDE_SERVICE_PATH "/codes/eeems/oxide1" -#define OXIDE_INTERFACE_VERSION "1.0.0" - -#define OXIDE_GENERAL_INTERFACE OXIDE_SERVICE ".General" -#define OXIDE_POWER_INTERFACE OXIDE_SERVICE ".Power" -#define OXIDE_WIFI_INTERFACE OXIDE_SERVICE ".Wifi" -#define OXIDE_NETWORK_INTERFACE OXIDE_SERVICE ".Network" -#define OXIDE_BSS_INTERFACE OXIDE_SERVICE ".BSS" -#define OXIDE_APPS_INTERFACE OXIDE_SERVICE ".Apps" -#define OXIDE_APPLICATION_INTERFACE OXIDE_SERVICE ".Application" -#define OXIDE_SYSTEM_INTERFACE OXIDE_SERVICE ".System" -#define OXIDE_SCREEN_INTERFACE OXIDE_SERVICE ".Screen" -#define OXIDE_NOTIFICATIONS_INTERFACE OXIDE_SERVICE ".Notifications" -#define OXIDE_NOTIFICATION_INTERFACE OXIDE_SERVICE ".Notification" -#define OXIDE_SCREENSHOT_INTERFACE OXIDE_SERVICE ".Screenshot" - /*! * \def deviceSettings() * \brief Get the Oxide::DeviceSettings instance @@ -186,7 +168,7 @@ namespace Oxide { DeviceType _deviceType; DeviceSettings(); - ~DeviceSettings() {}; + ~DeviceSettings(); void readDeviceType(); bool checkBitSet(int fd, int type, int i); std::string buttonsPath = ""; diff --git a/shared/liboxide/liboxide.pro b/shared/liboxide/liboxide.pro index 48dd9d63a..b3bcb36a0 100644 --- a/shared/liboxide/liboxide.pro +++ b/shared/liboxide/liboxide.pro @@ -6,10 +6,12 @@ TEMPLATE = lib DEFINES += LIBOXIDE_LIBRARY CONFIG += c++11 +CONFIG += warn_on DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 SOURCES += \ + debug.cpp \ eventfilter.cpp \ json.cpp \ liboxide.cpp \ @@ -20,9 +22,11 @@ SOURCES += \ signalhandler.cpp HEADERS += \ + debug.h \ eventfilter.h \ liboxide_global.h \ liboxide.h \ + meta.h \ power.h \ json.h \ settingsfile.h \ @@ -55,3 +59,4 @@ target.path = /opt/usr/lib !isEmpty(target.path): INSTALLS += target VERSION = 2.5 +DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/shared/liboxide/meta.h b/shared/liboxide/meta.h new file mode 100644 index 000000000..47b9607f9 --- /dev/null +++ b/shared/liboxide/meta.h @@ -0,0 +1,74 @@ +#ifndef META_H +#define META_H + +/*! + * \brief wpa_supplicant DBus service name + */ +#define WPA_SUPPLICANT_SERVICE "fi.w1.wpa_supplicant1" +/*! + * \brief wpa_supplicant DBus service path + */ +#define WPA_SUPPLICANT_SERVICE_PATH "/fi/w1/wpa_supplicant1" + +/*! + * \brief DBus service for tarnish + */ +#define OXIDE_SERVICE "codes.eeems.oxide1" +/*! + * \brief DBus path for tarnish + */ +#define OXIDE_SERVICE_PATH "/codes/eeems/oxide1" +/*! + * \brief Version of Tarnish and liboxide + */ +#define OXIDE_INTERFACE_VERSION "2.5.0" +/*! + * \brief DBus service for the general API + */ +#define OXIDE_GENERAL_INTERFACE OXIDE_SERVICE ".General" +/*! + * \brief DBus service for the apps API + */ +#define OXIDE_APPS_INTERFACE OXIDE_SERVICE ".Apps" +/*! + * \brief DBus service for the notifications API + */ +#define OXIDE_NOTIFICATIONS_INTERFACE OXIDE_SERVICE ".Notifications" +/*! + * \brief DBus service for the power API + */ +#define OXIDE_POWER_INTERFACE OXIDE_SERVICE ".Power" +/*! + * \brief DBus service for the screen API + */ +#define OXIDE_SCREEN_INTERFACE OXIDE_SERVICE ".Screen" +/*! + * \brief DBus service for the system API + */ +#define OXIDE_SYSTEM_INTERFACE OXIDE_SERVICE ".System" +/*! + * \brief DBus service for the wifi API + */ +#define OXIDE_WIFI_INTERFACE OXIDE_SERVICE ".Wifi" +/*! + * \brief DBus service for an application object + */ +#define OXIDE_APPLICATION_INTERFACE OXIDE_SERVICE ".Application" +/*! + * \brief DBus service for a bss object + */ +#define OXIDE_BSS_INTERFACE OXIDE_SERVICE ".BSS" +/*! + * \brief DBus service for a network object + */ +#define OXIDE_NETWORK_INTERFACE OXIDE_SERVICE ".Network" +/*! + * \brief DBus service for a notification object + */ +#define OXIDE_NOTIFICATION_INTERFACE OXIDE_SERVICE ".Notification" +/*! + * \brief DBus service for a screenshot object + */ +#define OXIDE_SCREENSHOT_INTERFACE OXIDE_SERVICE ".Screenshot" + +#endif // META_H diff --git a/shared/liboxide/power.cpp b/shared/liboxide/power.cpp index be7f30cb0..c88600a1c 100644 --- a/shared/liboxide/power.cpp +++ b/shared/liboxide/power.cpp @@ -1,8 +1,8 @@ #include "power.h" +#include "debug.h" #include #include -#include using Oxide::SysObject; @@ -25,27 +25,27 @@ void _setup(){ _chargers = new QList(); } QDir dir("/sys/class/power_supply"); - qDebug() << "Looking for batteries and chargers..."; + O_DEBUG("Looking for batteries and chargers..."); for(auto& path : dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable)){ - qDebug() << (" Checking " + path + "...").toStdString().c_str(); + O_DEBUG((" Checking " + path + "...").toStdString().c_str()); SysObject item(dir.path() + "/" + path); if(!item.hasProperty("type")){ - qDebug() << " Missing type property"; + O_DEBUG(" Missing type property"); continue; } if(item.hasProperty("present") && !item.intProperty("present")){ - qDebug() << " Either missing present property, or battery is not present"; + O_DEBUG(" Either missing present property, or battery is not present"); continue; } auto type = item.strProperty("type"); if(type == "Battery"){ - qDebug() << " Found Battery!"; + O_DEBUG(" Found Battery!"); _batteries->append(item); }else if(type == "USB" || type == "USB_CDP"){ - qDebug() << " Found Charger!"; + O_DEBUG(" Found Charger!"); _chargers->append(item); }else{ - qDebug() << " Unknown type"; + O_DEBUG(" Unknown type"); } } if(_chargers->empty()){ diff --git a/shared/liboxide/settingsfile.cpp b/shared/liboxide/settingsfile.cpp index d22991835..b7280798f 100644 --- a/shared/liboxide/settingsfile.cpp +++ b/shared/liboxide/settingsfile.cpp @@ -1,27 +1,18 @@ #include -#include #include "settingsfile.h" +#include "debug.h" namespace Oxide { - bool debugEnabled(){ - if(getenv("DEBUG") == NULL){ - return false; - } - QString env = qgetenv("DEBUG"); - return !(QStringList() << "0" << "n" << "no" << "false").contains(env.toLower()); - } SettingsFile::SettingsFile(QString path) : QSettings(path, QSettings::IniFormat), fileWatcher(QStringList() << path) { } SettingsFile::~SettingsFile(){ } void SettingsFile::fileChanged(){ if(!fileWatcher.files().contains(fileName()) && !fileWatcher.addPath(fileName())){ - qWarning() << "Unable to watch " << fileName(); - } - if(debugEnabled()){ - qDebug() << "Settings file" << fileName() << "changed!"; + O_WARNING("Unable to watch " << fileName()); } + O_DEBUG("Settings file" << fileName() << "changed!"); // Load new values sync(); auto metaObj = metaObject(); @@ -31,17 +22,13 @@ namespace Oxide { auto value = property.read(this); auto value2 = this->value(property.name()); if(value != value2){ - if(debugEnabled()){ - qDebug() << "Property" << property.name() << "changed"; - } + O_DEBUG("Property" << property.name() << "changed") property.write(this, value2); property.notifySignal().invoke(this, Qt::QueuedConnection, QGenericArgument(value2.typeName(), value2.data())); } } } - if(debugEnabled()){ - qDebug() << "Settings file" << fileName() << "changes loaded"; - } + O_DEBUG("Settings file" << fileName() << "changes loaded"); } void SettingsFile::reloadProperty(const QString& name){ auto metaObj = metaObject(); @@ -99,7 +86,7 @@ namespace Oxide { sync(); reloadProperties(); if(!fileWatcher.files().contains(fileName()) && !fileWatcher.addPath(fileName())){ - qWarning() << "Unable to watch " << fileName(); + O_WARNING("Unable to watch " << fileName()); } connect(&fileWatcher, &QFileSystemWatcher::fileChanged, this, &SettingsFile::fileChanged); } diff --git a/shared/liboxide/settingsfile.h b/shared/liboxide/settingsfile.h index 515f41cdc..fff830847 100644 --- a/shared/liboxide/settingsfile.h +++ b/shared/liboxide/settingsfile.h @@ -8,18 +8,13 @@ #include #include -#include #include #include #include #include -#ifdef DEBUG -#define O_SETTINGS_DEBUG(msg) if(debugEnabled()){ qDebug() << msg; } -#else -#define O_SETTINGS_DEBUG(msg) -#endif +#define O_SETTINGS_DEBUG(msg) O_DEBUG(msg) #define O_SETTINGS_PROPERTY_0(_type, member, _group) \ Q_PROPERTY(QString __META_GROUP_##member READ __META_GROUP_##member CONSTANT FINAL) \ @@ -103,12 +98,6 @@ namespace Oxide { - /*! - * \brief Return the state of debugging - * \return Debugging state - * \snippet examples/oxide.cpp debugEnabled - */ - LIBOXIDE_EXPORT bool debugEnabled(); /*! * \brief A better version of [QSettings](https://doc.qt.io/qt-5/qsettings.html) * diff --git a/shared/liboxide/slothandler.cpp b/shared/liboxide/slothandler.cpp index 31a46eb7b..cc47280fc 100644 --- a/shared/liboxide/slothandler.cpp +++ b/shared/liboxide/slothandler.cpp @@ -1,13 +1,12 @@ #include "slothandler.h" -#include "json.h" -#include "liboxide.h" +#include "meta.h" +#include "debug.h" #include #include #include #include -using namespace Oxide::JSON; namespace Oxide{ bool DBusConnect(QDBusAbstractInterface* interface, const QString& slotName, std::function onMessage, const bool& once=false){ @@ -48,7 +47,7 @@ namespace Oxide{ QObject::connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [=](const QString& name){ Q_UNUSED(name); if(!m_disconnected){ - qDebug() << QDBusError(QDBusError::ServiceUnknown, "The name " + serviceName + " is no longer registered"); + O_DEBUG(QDBusError(QDBusError::ServiceUnknown, "The name " + serviceName + " is no longer registered")); m_disconnected = true; callback(); } diff --git a/shared/liboxide/sysobject.cpp b/shared/liboxide/sysobject.cpp index 09db213e3..2a21067ee 100644 --- a/shared/liboxide/sysobject.cpp +++ b/shared/liboxide/sysobject.cpp @@ -2,11 +2,10 @@ #include #include #include -#include #include #include -#include +#include namespace Oxide{ std::string SysObject::propertyPath(const std::string& name){ @@ -32,9 +31,7 @@ namespace Oxide{ auto path = propertyPath(name); QFile file(path.c_str()); if(!file.open(QIODevice::ReadOnly | QIODevice::Text)){ - if(Oxide::debugEnabled()){ - qDebug() << "Couldn't find the file " << path.c_str(); - } + O_DEBUG("Couldn't find the file " << path.c_str()); return "0"; } QTextStream in(&file); diff --git a/web/src/documentation/api/01_general.rst b/web/src/documentation/api/01_general.rst index 9a004d0d7..4a17ef417 100644 --- a/web/src/documentation/api/01_general.rst +++ b/web/src/documentation/api/01_general.rst @@ -64,8 +64,8 @@ usage of the API away. .. _example-usage-1: -Example Usage: -~~~~~~~~~~~~~~ +Example Usage +~~~~~~~~~~~~~ .. code:: cpp From 6c3b34217c072548e0a895a8fd4215ba089b745e Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Wed, 25 Jan 2023 12:29:05 -0700 Subject: [PATCH 04/15] Split out application logging with identifiers (#284) * Split out application logging with identifiers --- applications/system-service/application.cpp | 40 +++++++++++++++++++++ applications/system-service/application.h | 35 +++++++++++++----- web/src/faq.rst | 7 ++++ 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/applications/system-service/application.cpp b/applications/system-service/application.cpp index 67248db64..567e73acb 100644 --- a/applications/system-service/application.cpp +++ b/applications/system-service/application.cpp @@ -60,6 +60,38 @@ void Application::launchNoSecurityCheck(){ } m_process->setUser(user()); m_process->setGroup(group()); + if(p_stdout == nullptr){ + int fd = sd_journal_stream_fd(name().toStdString().c_str(), LOG_INFO, 1); + if (fd < 0) { + errno = -fd; + qDebug() << "Failed to create stdout fd:" << -fd; + }else{ + FILE* log = fdopen(fd, "w"); + if(!log){ + qDebug() << "Failed to create stdout FILE:" << errno; + close(fd); + }else{ + p_stdout = new QTextStream(log); + qDebug() << "Opened stdout for " << name(); + } + } + } + if(p_stderr == nullptr){ + int fd = sd_journal_stream_fd(name().toStdString().c_str(), LOG_ERR, 1); + if (fd < 0) { + errno = -fd; + qDebug() << "Failed to create sterr fd:" << -fd; + }else{ + FILE* log = fdopen(fd, "w"); + if(!log){ + qDebug() << "Failed to create stderr FILE:" << errno; + close(fd); + }else{ + p_stderr = new QTextStream(log); + qDebug() << "Opened stderr for " << name(); + } + } + } m_process->start(); m_process->waitForStarted(); if(type() == AppsAPI::Background){ @@ -321,6 +353,14 @@ void Application::stopNoSecurityCheck(){ } }); appsAPI->removeFromPreviousApplications(name()); + if(p_stdout != nullptr){ + delete p_stdout; + p_stdout = nullptr; + } + if(p_stderr != nullptr){ + delete p_stderr; + p_stderr = nullptr; + } }); } void Application::signal(int signal){ diff --git a/applications/system-service/application.h b/applications/system-service/application.h index 35fa63ee6..8f2a79b42 100644 --- a/applications/system-service/application.h +++ b/applications/system-service/application.h @@ -174,11 +174,18 @@ class Application : public QObject{ connect(m_process, &SandBoxProcess::errorOccurred, this, &Application::errorOccurred); } ~Application() { + stopNoSecurityCheck(); unregisterPath(); if(m_screenCapture != nullptr){ delete m_screenCapture; } umountAll(); + if(p_stdout != nullptr){ + delete p_stdout; + } + if(p_stderr != nullptr){ + delete p_stderr; + } } QString path() { return m_path; } @@ -443,20 +450,30 @@ private slots: void started(); void finished(int exitCode); void readyReadStandardError(){ - const char* prefix = ("[" + name() + " " + QString::number(m_process->processId()) + "]").toUtf8(); QString error = m_process->readAllStandardError(); - for(QString line : error.split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts)){ - if(!line.isEmpty()){ - sd_journal_print(LOG_ERR, "%s %s", prefix, (const char*)line.toUtf8()); + if(p_stderr != nullptr){ + *p_stderr << error.toStdString().c_str(); + p_stderr->flush(); + }else{ + const char* prefix = ("[" + name() + " " + QString::number(m_process->processId()) + "]").toUtf8(); + for(QString line : error.split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts)){ + if(!line.isEmpty()){ + sd_journal_print(LOG_ERR, "%s %s", prefix, (const char*)line.toUtf8()); + } } } } void readyReadStandardOutput(){ - const char* prefix = ("[" + name() + " " + QString::number(m_process->processId()) + "]").toUtf8(); QString output = m_process->readAllStandardOutput(); - for(QString line : output.split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts)){ - if(!line.isEmpty()){ - sd_journal_print(LOG_INFO, "%s %s", prefix, (const char*)line.toUtf8()); + if(p_stdout != nullptr){ + *p_stdout << output.toStdString().c_str(); + p_stdout->flush(); + }else{ + const char* prefix = ("[" + name() + " " + QString::number(m_process->processId()) + "]").toUtf8(); + for(QString line : output.split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts)){ + if(!line.isEmpty()){ + sd_journal_print(LOG_INFO, "%s %s", prefix, (const char*)line.toUtf8()); + } } } } @@ -500,6 +517,8 @@ private slots: QMap fifos; Oxide::Sentry::Transaction* transaction = nullptr; Oxide::Sentry::Span* span = nullptr; + QTextStream* p_stdout = nullptr; + QTextStream* p_stderr = nullptr; bool hasPermission(QString permission, const char* sender = __builtin_FUNCTION()); void delayUpTo(int milliseconds){ diff --git a/web/src/faq.rst b/web/src/faq.rst index b46c52e5b..b04469240 100644 --- a/web/src/faq.rst +++ b/web/src/faq.rst @@ -92,6 +92,13 @@ for Oxide's programs, and any application you run through Oxide, you can run the journalctl -eau tarnish +As of Oxide 2.5, you can now get logs for specific applications with the following, where +``codes.eeems.oxide`` is the name of the application as it's been registered. + +.. code:: bash + + journalctl -eat codes.eeems.oxide + Where are the configuration files? ================================== From f08b8c817321d2060b685be236b9b74df98f2501 Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Wed, 25 Jan 2023 12:41:27 -0700 Subject: [PATCH 05/15] Formatting --- applications/task-switcher/appitem.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/applications/task-switcher/appitem.h b/applications/task-switcher/appitem.h index 52bdc5a03..5f123b87a 100644 --- a/applications/task-switcher/appitem.h +++ b/applications/task-switcher/appitem.h @@ -13,6 +13,7 @@ using namespace codes::eeems::oxide1; class AppItem : public QObject { Q_OBJECT + public: AppItem(QObject* parent) : QObject(parent){} @@ -34,6 +35,7 @@ class AppItem : public QObject { Q_INVOKABLE void execute(); Q_INVOKABLE void stop(); + signals: void nameChanged(QString); void displayNameChanged(QString); From aaa46caf06d85aa144b3bd4659ad3940a8999c43 Mon Sep 17 00:00:00 2001 From: Ben Siraphob Date: Wed, 25 Jan 2023 13:49:27 -0600 Subject: [PATCH 06/15] Nix maintenance (#201) * Nix maintenance - stdenv.lib has been deprecated (https://github.com/NixOS/nixpkgs/issues/108938) - replace linuxkit-nix (unmaintained) with nix-docker - updated niv sources * Add Nix workflow * Adjust Nix build to use oxide target * Use auth token for cachix Co-authored-by: Nathaniel van Diepen --- .github/workflows/nix.yml | 44 +++++++++++++++++++++++++++++++++++++++ README.md | 6 ++---- nix/sources.json | 12 +++++------ oxide.nix | 8 +++---- 4 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/nix.yml diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 000000000..edaead1e1 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,44 @@ +name: Build with Nix + +on: + push: + branches: + - master + paths: + - 'applications/**' + - 'shared/**' + - 'assets/**' + - 'interfaces/**' + - 'Makefile' + - '*.nix' + pull_request: + paths: + - 'applications/**' + - 'shared/**' + - 'assets/**' + - 'interfaces/**' + - 'Makefile' + - '*.nix' +jobs: + nix-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: cachix/install-nix-action@v18 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@v12 + with: + name: nix-remarkable + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + - name: Build + run: nix-build --argstr system 'x86_64-linux' + timeout-minutes: 15 + - run: | + mkdir output + cp -a result/. output/ + - name: Save Artifact + uses: actions/upload-artifact@v3 + with: + name: output + path: output diff --git a/README.md b/README.md index a55cf0b11..76cc70554 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,8 @@ Here is an outdated video of it in action: Install the reMarkable toolchain and then run `make release`. It will produce a folder named `release` containing all the output. ### Nix - -Works on x86_64-linux or macOS with -[linuxkit-nix](https://github.com/nix-community/linuxkit-nix). +Works on x86_64-linux or macOS via [nix-docker](https://github.com/LnL7/nix-docker). ```ShellSession -$ nix build +$ nix-build --argstr system 'x86_64-linux' ``` diff --git a/nix/sources.json b/nix/sources.json index 084bdeb6c..be881ad27 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -5,10 +5,10 @@ "homepage": "https://github.com/nmattia/niv", "owner": "nmattia", "repo": "niv", - "rev": "ba57d5a29b4e0f2085917010380ef3ddc3cf380f", - "sha256": "1kpsvc53x821cmjg1khvp1nz7906gczq8mp83664cr15h94sh8i4", + "rev": "5830a4dd348d77e39a0f3c4c762ff2663b602d4c", + "sha256": "1d3lsrqvci4qz2hwjrcnd8h5vfkg8aypq3sjd4g3izbc8frwz5sm", "type": "tarball", - "url": "https://github.com/nmattia/niv/archive/ba57d5a29b4e0f2085917010380ef3ddc3cf380f.tar.gz", + "url": "https://github.com/nmattia/niv/archive/5830a4dd348d77e39a0f3c4c762ff2663b602d4c.tar.gz", "url_template": "https://github.com///archive/.tar.gz" }, "nix-inclusive": { @@ -29,10 +29,10 @@ "homepage": "", "owner": "NixOS", "repo": "nixpkgs", - "rev": "db103e0f98d461f3e66cd68702492afca0810db5", - "sha256": "19gz926nqv7ggq281mv2qi1ah6a5slg2vvhsvc5jdnkp28m8f55k", + "rev": "77fda7f672726e1a95c8cd200f27bccfc86c870b", + "sha256": "07qj1d45pkqsmkahbhh7hilwwbvg8vlz1wg497hzjrlx1a57v4y5", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/db103e0f98d461f3e66cd68702492afca0810db5.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/77fda7f672726e1a95c8cd200f27bccfc86c870b.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/oxide.nix b/oxide.nix index 3c952ebc3..8d0614453 100644 --- a/oxide.nix +++ b/oxide.nix @@ -1,4 +1,4 @@ -{ stdenv, fetchFromGitHub, inclusive, qt512, remarkable-toolchain }: +{ stdenv, lib, fetchFromGitHub, inclusive, qt512, remarkable-toolchain }: stdenv.mkDerivation { name = "oxide"; @@ -14,17 +14,17 @@ stdenv.mkDerivation { ]; preBuild = '' - source ${remarkable-toolchain}/environment-setup-cortexa9hf-neon-oe-linux-gnueabi + source ${remarkable-toolchain}/environment-setup-cortexa9hf-neon-remarkable-linux-gnueabi ''; enableParallelBuilding = true; makeFlags = [ "release" ]; installPhase = '' - cp -r release/. $out + cp -a release/. $out ''; - meta = with stdenv.lib; { + meta = with lib; { description = "A launcher application for the reMarkable tablet"; platform = [ "x86_64-linux" ]; maintainers = [ maintainers.siraben ]; From a42a6b912a5848e3270400eab0a09c56c0ec3641 Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Thu, 26 Jan 2023 14:57:12 -0700 Subject: [PATCH 07/15] Fix icon change handling (#285) --- applications/launcher/appitem.h | 2 +- applications/task-switcher/appitem.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/launcher/appitem.h b/applications/launcher/appitem.h index 5f123b87a..d195126d3 100644 --- a/applications/launcher/appitem.h +++ b/applications/launcher/appitem.h @@ -60,7 +60,7 @@ private slots: } void onIconChanged(QString path){ _imgFile = path; - emit onIconChanged(path); + emit imgFileChanged(path); } private: diff --git a/applications/task-switcher/appitem.h b/applications/task-switcher/appitem.h index 5f123b87a..d195126d3 100644 --- a/applications/task-switcher/appitem.h +++ b/applications/task-switcher/appitem.h @@ -60,7 +60,7 @@ private slots: } void onIconChanged(QString path){ _imgFile = path; - emit onIconChanged(path); + emit imgFileChanged(path); } private: From 3c9da61bc4c1124825b004b94b4486cd6f221837 Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Thu, 26 Jan 2023 15:08:20 -0700 Subject: [PATCH 08/15] Feature/notify when screenshot being taken (#286) * Notify when screenshot is being taken --- applications/system-service/notification.cpp | 59 ++----------------- applications/system-service/notification.h | 1 - applications/system-service/notificationapi.h | 35 +++++++++++ applications/system-service/screenapi.cpp | 27 +++++++++ applications/system-service/screenapi.h | 23 +++----- applications/system-service/tarnish.pro | 1 + 6 files changed, 75 insertions(+), 71 deletions(-) create mode 100644 applications/system-service/screenapi.cpp diff --git a/applications/system-service/notification.cpp b/applications/system-service/notification.cpp index d2e794627..08d091956 100644 --- a/applications/system-service/notification.cpp +++ b/applications/system-service/notification.cpp @@ -32,7 +32,7 @@ void Notification::display(){ return; } notificationAPI->lock(); - dispatchToMainThread([=]{ + Oxide::dispatchToMainThread([=]{ qDebug() << "Displaying notification" << identifier(); auto path = appsAPI->currentApplicationNoSecurityCheck(); Application* resumeApp = nullptr; @@ -52,61 +52,10 @@ void Notification::remove(){ emit removed(); } -void Notification::dispatchToMainThread(std::function callback){ - if(this->thread() == qApp->thread()){ - callback(); - return; - } - // any thread - QTimer* timer = new QTimer(); - timer->moveToThread(qApp->thread()); - timer->setSingleShot(true); - QObject::connect(timer, &QTimer::timeout, [=](){ - // main thread - callback(); - timer->deleteLater(); - }); - QMetaObject::invokeMethod(timer, "start", Qt::BlockingQueuedConnection, Q_ARG(int, 0)); -} void Notification::paintNotification(Application* resumeApp){ - auto frameBuffer = EPFrameBuffer::framebuffer(); - qDebug() << "Waiting for other painting to finish..."; - while(frameBuffer->paintingActive()){ - EPFrameBuffer::waitForLastUpdate(); - } qDebug() << "Painting notification" << identifier(); - screenBackup = frameBuffer->copy(); - qDebug() << "Painting to framebuffer..."; - QPainter painter(frameBuffer); - auto size = frameBuffer->size(); - auto fm = painter.fontMetrics(); - auto padding = 10; - auto radius = 10; - QImage icon(m_icon); - auto iconSize = icon.isNull() ? 0 : 50; - auto width = fm.horizontalAdvance(text()) + iconSize + (padding * 3); - auto height = max(fm.height(), iconSize) + (padding * 2); - auto left = size.width() - width; - auto top = size.height() - height; - updateRect = QRect(left, top, width, height); - painter.fillRect(updateRect, Qt::black); - painter.setPen(Qt::black); - painter.drawRoundedRect(updateRect, radius, radius); - painter.setPen(Qt::white); - QRect textRect(left + padding, top + padding, width - iconSize - (padding * 2), height - padding); - painter.drawText(textRect, Qt::AlignCenter, text()); - painter.end(); - qDebug() << "Updating screen " << updateRect << "..."; - EPFrameBuffer::sendUpdate(updateRect, EPFrameBuffer::Mono, EPFrameBuffer::PartialUpdate, true); - if(!icon.isNull()){ - QPainter painter2(frameBuffer); - QRect iconRect(size.width() - iconSize - padding, top + padding, iconSize, iconSize); - painter2.fillRect(iconRect, Qt::white); - painter2.drawImage(iconRect, icon); - painter2.end(); - EPFrameBuffer::sendUpdate(iconRect, EPFrameBuffer::Mono, EPFrameBuffer::PartialUpdate, true); - } - EPFrameBuffer::waitForLastUpdate(); + screenBackup = screenAPI->copy(); + updateRect = notificationAPI->paintNotification(text(), m_icon); qDebug() << "Painted notification" << identifier(); emit displayed(); QTimer::singleShot(2000, [this, resumeApp]{ @@ -117,7 +66,7 @@ void Notification::paintNotification(Application* resumeApp){ qDebug() << "Finished displaying notification" << identifier(); EPFrameBuffer::waitForLastUpdate(); if(!notificationAPI->notificationDisplayQueue.isEmpty()){ - dispatchToMainThread([resumeApp] { + Oxide::dispatchToMainThread([resumeApp] { notificationAPI->notificationDisplayQueue.takeFirst()->paintNotification(resumeApp); }); return; diff --git a/applications/system-service/notification.h b/applications/system-service/notification.h index 7f6c797a6..48b6dd00c 100644 --- a/applications/system-service/notification.h +++ b/applications/system-service/notification.h @@ -130,7 +130,6 @@ class Notification : public QObject{ QImage screenBackup; QRect updateRect; - void dispatchToMainThread(std::function callback); bool hasPermission(QString permission, const char* sender = __builtin_FUNCTION()); }; diff --git a/applications/system-service/notificationapi.h b/applications/system-service/notificationapi.h index 1af8b791d..f1ec8002d 100644 --- a/applications/system-service/notificationapi.h +++ b/applications/system-service/notificationapi.h @@ -106,6 +106,41 @@ class NotificationAPI : public APIBase { } return m_notifications.value(identifier); } + QRect paintNotification(const QString& text, const QString& iconPath){ + qDebug() << "Painting to framebuffer..."; + auto frameBuffer = EPFrameBuffer::framebuffer(); + QPainter painter(frameBuffer); + auto size = frameBuffer->size(); + auto fm = painter.fontMetrics(); + auto padding = 10; + auto radius = 10; + QImage icon(iconPath); + auto iconSize = icon.isNull() ? 0 : 50; + auto width = fm.horizontalAdvance(text) + iconSize + (padding * 3); + auto height = max(fm.height(), iconSize) + (padding * 2); + auto left = size.width() - width; + auto top = size.height() - height; + QRect updateRect(left, top, width, height); + painter.fillRect(updateRect, Qt::black); + painter.setPen(Qt::black); + painter.drawRoundedRect(updateRect, radius, radius); + painter.setPen(Qt::white); + QRect textRect(left + padding, top + padding, width - iconSize - (padding * 2), height - padding); + painter.drawText(textRect, Qt::AlignCenter, text); + painter.end(); + qDebug() << "Updating screen " << updateRect << "..."; + EPFrameBuffer::sendUpdate(updateRect, EPFrameBuffer::Mono, EPFrameBuffer::PartialUpdate, true); + if(!icon.isNull()){ + QPainter painter2(frameBuffer); + QRect iconRect(size.width() - iconSize - padding, top + padding, iconSize, iconSize); + painter2.fillRect(iconRect, Qt::white); + painter2.drawImage(iconRect, icon); + painter2.end(); + EPFrameBuffer::sendUpdate(iconRect, EPFrameBuffer::Mono, EPFrameBuffer::PartialUpdate, true); + } + EPFrameBuffer::waitForLastUpdate(); + return updateRect; + } public slots: QDBusObjectPath add(const QString& identifier, const QString& application, const QString& text, const QString& icon, QDBusMessage message){ diff --git a/applications/system-service/screenapi.cpp b/applications/system-service/screenapi.cpp new file mode 100644 index 000000000..6d3d7de39 --- /dev/null +++ b/applications/system-service/screenapi.cpp @@ -0,0 +1,27 @@ +#include "screenapi.h" +#include "notificationapi.h" + +QDBusObjectPath ScreenAPI::screenshot(){ + if(!hasPermission("screen")){ + return QDBusObjectPath("/"); + } + qDebug() << "Taking screenshot"; + auto filePath = getNextPath(); +#ifdef DEBUG + qDebug() << "Using path" << filePath; +#endif + QImage screen = copy(); + QRect rect = notificationAPI->paintNotification("Taking Screenshot...", ""); + EPFrameBuffer::sendUpdate(rect, EPFrameBuffer::Mono, EPFrameBuffer::PartialUpdate, true); + QDBusObjectPath path("/"); + if(!screen.save(filePath)){ + qDebug() << "Failed to take screenshot"; + }else{ + path = addScreenshot(filePath)->qPath(); + } + QPainter painter(EPFrameBuffer::framebuffer()); + painter.drawImage(rect, screen, rect); + painter.end(); + EPFrameBuffer::sendUpdate(rect, EPFrameBuffer::HighQualityGrayscale, EPFrameBuffer::PartialUpdate, true); + return path; +} diff --git a/applications/system-service/screenapi.h b/applications/system-service/screenapi.h index 6a9f3edd1..430a3e7ff 100644 --- a/applications/system-service/screenapi.h +++ b/applications/system-service/screenapi.h @@ -103,8 +103,7 @@ class ScreenAPI : public APIBase { } Oxide::Sentry::sentry_transaction("screen", "drawFullscrenImage", [img, path](Oxide::Sentry::Transaction* t){ Q_UNUSED(t); - auto size = EPFrameBuffer::framebuffer()->size(); - QRect rect(0, 0, size.width(), size.height()); + QRect rect = EPFrameBuffer::framebuffer()->rect(); QPainter painter(EPFrameBuffer::framebuffer()); painter.drawImage(rect, img); painter.end(); @@ -114,20 +113,14 @@ class ScreenAPI : public APIBase { return true; } - Q_INVOKABLE QDBusObjectPath screenshot(){ - if(!hasPermission("screen")){ - return QDBusObjectPath("/"); - } - qDebug() << "Taking screenshot"; - auto filePath = getNextPath(); -#ifdef DEBUG - qDebug() << "Using path" << filePath; -#endif - if(!EPFrameBuffer::framebuffer()->save(filePath)){ - qDebug() << "Failed to take screenshot"; - return QDBusObjectPath("/"); + Q_INVOKABLE QDBusObjectPath screenshot(); + QImage copy(){ + auto frameBuffer = EPFrameBuffer::framebuffer(); + qDebug() << "Waiting for other painting to finish..."; + while(frameBuffer->paintingActive()){ + EPFrameBuffer::waitForLastUpdate(); } - return addScreenshot(filePath)->qPath(); + return frameBuffer->copy(); } public slots: diff --git a/applications/system-service/tarnish.pro b/applications/system-service/tarnish.pro index 1f57e0657..c49db7e42 100644 --- a/applications/system-service/tarnish.pro +++ b/applications/system-service/tarnish.pro @@ -19,6 +19,7 @@ SOURCES += \ event_device.cpp \ network.cpp \ notification.cpp \ + screenapi.cpp \ screenshot.cpp \ systemapi.cpp \ wlan.cpp \ From 037fb56efa0162668ae10cebc6e6249952649360 Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Thu, 26 Jan 2023 16:50:05 -0700 Subject: [PATCH 09/15] Add more doc (#287) * Add documentation for sentry * Add more missing doc --- shared/liboxide/json.h | 3 + shared/liboxide/liboxide.h | 104 +++++++++++++++++++++++++++++- shared/liboxide/liboxide_global.h | 1 + shared/liboxide/meta.h | 20 ++++++ shared/liboxide/settingsfile.h | 25 ++++--- 5 files changed, 143 insertions(+), 10 deletions(-) diff --git a/shared/liboxide/json.h b/shared/liboxide/json.h index 72d464d14..9dc27fc3a 100644 --- a/shared/liboxide/json.h +++ b/shared/liboxide/json.h @@ -10,6 +10,9 @@ #include #include +/*! + * The JSON namespace + */ namespace Oxide::JSON { /*! * \brief Decode a DBus Argument into a QVariant diff --git a/shared/liboxide/liboxide.h b/shared/liboxide/liboxide.h index 3143ad1c0..8d7520683 100644 --- a/shared/liboxide/liboxide.h +++ b/shared/liboxide/liboxide.h @@ -62,46 +62,148 @@ namespace Oxide { * \snippet examples/oxide.cpp dispatchToMainThread */ LIBOXIDE_EXPORT void dispatchToMainThread(std::function callback); + /*! + *\brief The Sentry namespace + */ namespace Sentry{ + /*! + * \brief A sentry_transaction_t wrapper + */ struct Transaction { #ifdef SENTRY /*! * \brief The sentry_transaction_t instance */ sentry_transaction_t* inner; + /*! + * \brief Create a sentry_transaction_t wrapper + * \param sentry_transaction_t instance to wrap + */ explicit Transaction(sentry_transaction_t* t); #else void* inner; explicit Transaction(void* t); #endif }; + /*! + * \brief A sentry_span_t wrapper + */ struct Span { #ifdef SENTRY /*! * \brief The sentry_span_t instance */ sentry_span_t* inner; + /*! + * \brief Create a sentry_span_t wrapper + * \param The sentry_span_t instance to wrap + */ explicit Span(sentry_span_t* s); #else void* inner; explicit Span(void* s); #endif }; - + /*! + * \brief Get the boot identifier of the device using sd_id128_get_boot + * \return The boot identifier + */ LIBOXIDE_EXPORT const char* bootId(); + /*! + * \brief Get the machine identifier of the device using sd_id128_get_machine + * \return The machine identifier + */ LIBOXIDE_EXPORT const char* machineId(); + /*! + * \brief Initialize sentry tracking + * \param Name of the application + * \param Arguments passed to the application + * \param If automatic session tracking should be enabled + */ LIBOXIDE_EXPORT void sentry_init(const char* name, char* argv[], bool autoSessionTracking = true); + /*! + * \brief Create a breadcrumb in the current sentry transaction + * \param Category of the breadcrumb + * \param Message of the breadcrumb + * \param Type of breadcrumb + * \param Logging level of the breadcrumb + */ LIBOXIDE_EXPORT void sentry_breadcrumb(const char* category, const char* message, const char* type = "default", const char* level = "info"); + /*! + * \brief Start a transaction + * \param Name of the transaction + * \param Action being performed + * \return The transaction wrapper + */ LIBOXIDE_EXPORT Transaction* start_transaction(const std::string& name, const std::string& action); + /*! + * \brief Stop a sentry transaction + * \param The transaction wrapper to stop + */ LIBOXIDE_EXPORT void stop_transaction(Transaction* transaction); + /*! + * \brief Record a sentry trancation + * \param Name of the transaction + * \param Action being performed + * \param Code to run inside the transaction + */ LIBOXIDE_EXPORT void sentry_transaction(const std::string& name, const std::string& action, std::function callback); + /*! + * \brief Start a span inside a sentry transaction + * \param Transaction wrapper to attach the span to + * \param Operation being performed + * \param Description of the span + * \return The span wrapper + */ LIBOXIDE_EXPORT Span* start_span(Transaction* transaction, const std::string& operation, const std::string& description); + /*! + * \brief Start a span inside another sentry span + * \param The parent sentry span wrapper + * \param Operation being performed + * \param Description of the span + * \return The span wrapper + */ LIBOXIDE_EXPORT Span* start_span(Span* parent, const std::string& operation, const std::string& description); + /*! + * \brief Stop a sentry span + * \param The span wrapper to stop + */ LIBOXIDE_EXPORT void stop_span(Span* span); + /*! + * \brief Record a sentry span + * \param The transaction wrapper to attach this span to + * \param Operation being performed + * \param Description of the span + * \param Code to run inside the transaction + */ LIBOXIDE_EXPORT void sentry_span(Transaction* transaction, const std::string& operation, const std::string& description, std::function callback); + /*! + * \brief Record a sentry span + * \param The transaction wrapper to attach this span to + * \param Operation being performed + * \param Description of the span + * \param Code to run inside the transaction + */ LIBOXIDE_EXPORT void sentry_span(Transaction* transaction, const std::string& operation, const std::string& description, std::function callback); + /*! + * \brief Record a sentry span + * \param The span wrapper to attach this span to + * \param Operation being performed + * \param Description of the span + * \param Code to run inside the transaction + */ LIBOXIDE_EXPORT void sentry_span(Span* parent, const std::string& operation, const std::string& description, std::function callback); + /*! + * \brief Record a sentry span + * \param The span wrapper to attach this span to + * \param Operation being performed + * \param Description of the span + * \param Code to run inside the transaction + */ LIBOXIDE_EXPORT void sentry_span(Span* parent, const std::string& operation, const std::string& description, std::function callback); + /*! + * \brief Trigger a crash. Useful to test that sentry integration is working + */ LIBOXIDE_EXPORT void trigger_crash(); } /*! diff --git a/shared/liboxide/liboxide_global.h b/shared/liboxide/liboxide_global.h index 45e6be73f..8845ffb1c 100644 --- a/shared/liboxide/liboxide_global.h +++ b/shared/liboxide/liboxide_global.h @@ -15,6 +15,7 @@ # endif #else # define SENTRY +# define DEBUG # define LIBOXIDE_EXPORT #endif diff --git a/shared/liboxide/meta.h b/shared/liboxide/meta.h index 47b9607f9..887a100cb 100644 --- a/shared/liboxide/meta.h +++ b/shared/liboxide/meta.h @@ -1,72 +1,92 @@ +/*! + * \file meta.h + */ #ifndef META_H #define META_H /*! + * \def WPA_SUPPLICANT_SERVICE * \brief wpa_supplicant DBus service name */ #define WPA_SUPPLICANT_SERVICE "fi.w1.wpa_supplicant1" /*! + * \def WPA_SUPPLICANT_SERVICE_PATH * \brief wpa_supplicant DBus service path */ #define WPA_SUPPLICANT_SERVICE_PATH "/fi/w1/wpa_supplicant1" /*! + * \def OXIDE_SERVICE * \brief DBus service for tarnish */ #define OXIDE_SERVICE "codes.eeems.oxide1" /*! + * \def OXIDE_SERVICE_PATH * \brief DBus path for tarnish */ #define OXIDE_SERVICE_PATH "/codes/eeems/oxide1" /*! + * \def OXIDE_INTERFACE_VERSION * \brief Version of Tarnish and liboxide */ #define OXIDE_INTERFACE_VERSION "2.5.0" /*! + * \def OXIDE_GENERAL_INTERFACE * \brief DBus service for the general API */ #define OXIDE_GENERAL_INTERFACE OXIDE_SERVICE ".General" /*! + * \def OXIDE_APPS_INTERFACE * \brief DBus service for the apps API */ #define OXIDE_APPS_INTERFACE OXIDE_SERVICE ".Apps" /*! + * \def OXIDE_NOTIFICATIONS_INTERFACE * \brief DBus service for the notifications API */ #define OXIDE_NOTIFICATIONS_INTERFACE OXIDE_SERVICE ".Notifications" /*! + * \def OXIDE_POWER_INTERFACE * \brief DBus service for the power API */ #define OXIDE_POWER_INTERFACE OXIDE_SERVICE ".Power" /*! + * \def OXIDE_SCREEN_INTERFACE * \brief DBus service for the screen API */ #define OXIDE_SCREEN_INTERFACE OXIDE_SERVICE ".Screen" /*! + * \def OXIDE_SYSTEM_INTERFACE * \brief DBus service for the system API */ #define OXIDE_SYSTEM_INTERFACE OXIDE_SERVICE ".System" /*! + * \def OXIDE_WIFI_INTERFACE * \brief DBus service for the wifi API */ #define OXIDE_WIFI_INTERFACE OXIDE_SERVICE ".Wifi" /*! + * \def OXIDE_APPLICATION_INTERFACE * \brief DBus service for an application object */ #define OXIDE_APPLICATION_INTERFACE OXIDE_SERVICE ".Application" /*! + * \def OXIDE_BSS_INTERFACE * \brief DBus service for a bss object */ #define OXIDE_BSS_INTERFACE OXIDE_SERVICE ".BSS" /*! + * \def OXIDE_NETWORK_INTERFACE * \brief DBus service for a network object */ #define OXIDE_NETWORK_INTERFACE OXIDE_SERVICE ".Network" /*! + * \def OXIDE_NOTIFICATION_INTERFACE * \brief DBus service for a notification object */ #define OXIDE_NOTIFICATION_INTERFACE OXIDE_SERVICE ".Notification" /*! + * \def OXIDE_SCREENSHOT_INTERFACE * \brief DBus service for a screenshot object */ #define OXIDE_SCREENSHOT_INTERFACE OXIDE_SERVICE ".Screenshot" diff --git a/shared/liboxide/settingsfile.h b/shared/liboxide/settingsfile.h index fff830847..5ccbd6feb 100644 --- a/shared/liboxide/settingsfile.h +++ b/shared/liboxide/settingsfile.h @@ -32,13 +32,11 @@ #define O_SETTINGS_PROPERTY_1(_type, group, member) \ Q_PROPERTY(_type member MEMBER m_##member READ member WRITE set_##member NOTIFY member##Changed FINAL) \ O_SETTINGS_PROPERTY_0(_type, member, group) - #define O_SETTINGS_PROPERTY_2(_type, group, member, _default) \ Q_PROPERTY(_type member MEMBER m_##member READ member WRITE set_##member NOTIFY member##Changed RESET reset_##member) \ O_SETTINGS_PROPERTY_0(_type, member, group) \ public: \ void reset_##member(); - #define O_SETTINGS_PROPERTY_BODY_0(_class, _type, member, _group) \ void _class::set_##member(_type _arg_##member) { \ O_SETTINGS_DEBUG(fileName() + " Setting " + #_group + "." + #member) \ @@ -55,10 +53,8 @@ _type _class::member() const { return m_##member; } \ void _class::reload_##member() { reloadProperty(#member); } \ QString _class::__META_GROUP_##member() const { return #_group; } - #define O_SETTINGS_PROPERTY_BODY_1(_class, _type, group, member) \ O_SETTINGS_PROPERTY_BODY_0(_class, _type, member, group) - #define O_SETTINGS_PROPERTY_BODY_2(_class, _type, group, member, _default) \ O_SETTINGS_PROPERTY_BODY_0(_class, _type, member, group) \ void _class::reset_##member() { \ @@ -67,25 +63,36 @@ setProperty(#member, _default); \ O_SETTINGS_DEBUG(" Done") \ } - #define O_SETTINGS_PROPERTY_X_get_func(arg1, arg2, arg3, arg4, arg5, ...) arg5 #define O_SETTINGS_PROPERTY_X(...) \ O_SETTINGS_PROPERTY_X_get_func(__VA_ARGS__, \ O_SETTINGS_PROPERTY_2, \ O_SETTINGS_PROPERTY_1, \ ) - #define O_SETTINGS_PROPERTY_BODY_X_get_func(arg1, arg2, arg3, arg4, arg5, arg6, ...) arg6 #define O_SETTINGS_PROPERTY_BODY_X(...) \ O_SETTINGS_PROPERTY_BODY_X_get_func(__VA_ARGS__, \ O_SETTINGS_PROPERTY_BODY_2, \ O_SETTINGS_PROPERTY_BODY_1, \ ) - +/*! + * \def O_SETTINGS_PROPERTY + * \brief Add a property to a SettingsFile derived class + * \sa O_SETTINGS, O_SETTINGS_PROPERTY_BODY, Oxide::SettingsFile + */ #define O_SETTINGS_PROPERTY(...) O_SETTINGS_PROPERTY_X(__VA_ARGS__)(__VA_ARGS__) +/*! + * \def O_SETTINGS_PROPERTY_BODY + * \brief Add the body for a property on a SettingsFile derived class + * \sa O_SETTINGS, O_SETTINGS_PROPERTY, Oxide::SettingsFile + */ #define O_SETTINGS_PROPERTY_BODY(...) O_SETTINGS_PROPERTY_BODY_X(__VA_ARGS__)(__VA_ARGS__) - +/*! + * \def O_SETTINGS + * \brief Define the instance() and constructor methods for a SettingsFile derived class + * \sa O_SETTINGS_PROPERTY, O_SETTINGS_PROPERTY_BODY, Oxide::SettingsFile + */ #define O_SETTINGS(_type, path) \ public: \ static _type& instance(){ \ @@ -104,7 +111,7 @@ namespace Oxide { * This base class adds dynamic updates of changes to a settings file from disk. * It also implements a static instance method that will return the singleton for this class. * - * \sa sharedSettings, xochitlSettings + * \sa sharedSettings, xochitlSettings, O_SETTINGS, O_SETTINGS_PROPERTY, O_SETTINGS_PROPERTY_BODY */ class LIBOXIDE_EXPORT SettingsFile : public QSettings { Q_OBJECT From 10391dd4571f9b1177dd64d47312e885367fba19 Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Thu, 26 Jan 2023 18:50:18 -0700 Subject: [PATCH 10/15] Move sentry to it's own file (#288) * Move sentry to it's own file --- shared/liboxide/liboxide.cpp | 349 ------------------------------ shared/liboxide/liboxide.h | 149 +------------ shared/liboxide/liboxide.pro | 2 + shared/liboxide/oxide_sentry.cpp | 354 +++++++++++++++++++++++++++++++ shared/liboxide/oxide_sentry.h | 164 ++++++++++++++ 5 files changed, 521 insertions(+), 497 deletions(-) create mode 100644 shared/liboxide/oxide_sentry.cpp create mode 100644 shared/liboxide/oxide_sentry.h diff --git a/shared/liboxide/liboxide.cpp b/shared/liboxide/liboxide.cpp index 9568f6ca5..102d1d3f2 100644 --- a/shared/liboxide/liboxide.cpp +++ b/shared/liboxide/liboxide.cpp @@ -4,71 +4,16 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -// String: 5aa5ca39ee0b4f48927529ca17519524 -// UUID: 5aa5ca39-ee0b-4f48-9275-29ca17519524 -#define OXIDE_UID SD_ID128_MAKE(5a,a5,ca,39,ee,0b,4f,48,92,75,29,ca,17,51,95,24) -#ifdef SENTRY -#define SAMPLE_RATE 1.0 -std::string readFile(const std::string& path){ - std::ifstream t(path); - std::stringstream buffer; - buffer << t.rdbuf(); - return buffer.str(); -} -#endif #define BITS_PER_LONG (sizeof(long) * 8) #define NBITS(x) ((((x)-1)/BITS_PER_LONG)+1) #define OFF(x) ((x)%BITS_PER_LONG) #define LONG(x) ((x)/BITS_PER_LONG) #define test_bit(bit, array) ((array[LONG(bit)] >> OFF(bit)) & 1) -static void *invalid_mem = (void *)1; - -void logMachineIdError(int error, QString name, QString path){ - if(error == -ENOENT){ - O_WARNING("/etc/machine-id is missing"); - }else if(error == -ENOMEDIUM){ - O_WARNING(path + " is empty or all zeros"); - }else if(error == -EIO){ - O_WARNING(path + " has the incorrect format"); - }else if(error == -EPERM){ - O_WARNING(path + " access denied"); - } if(error == -EINVAL){ - O_WARNING("Error while reading " + name + ": Buffer invalid"); - }else if(error == -ENXIO){ - O_WARNING("Error while reading " + name + ": No invocation ID is set"); - }else if(error == -EOPNOTSUPP){ - O_WARNING("Error while reading " + name + ": Operation not supported"); - }else{ - O_WARNING("Unexpected error code reading " + name + ":" << strerror(error)); - } -} -std::string getAppSpecific(sd_id128_t base){ - QCryptographicHash hash(QCryptographicHash::Sha256); - char buf[SD_ID128_STRING_MAX]; - hash.addData(sd_id128_to_string(base, buf)); - hash.addData(sd_id128_to_string(OXIDE_UID, buf)); - auto r = hash.result(); - r[6] = (r.at(6) & 0x0F) | 0x40; - r[8] = (r.at(8) & 0x3F) | 0x80; - QUuid uid(r.at(0), r.at(1), r.at(2), r.at(3), r.at(4), r.at(5), r.at(6), r.at(7), r.at(8), r.at(9), r.at(10)); - return uid.toString((QUuid::Id128)).toStdString(); -} - namespace Oxide { void dispatchToMainThread(std::function callback){ if(QThread::currentThread() == qApp->thread()){ @@ -86,300 +31,6 @@ namespace Oxide { }); QMetaObject::invokeMethod(timer, "start", Qt::BlockingQueuedConnection, Q_ARG(int, 0)); } - namespace Sentry{ -#ifdef SENTRY - static bool initialized = false; - Transaction::Transaction(sentry_transaction_t* t){ - inner = t; - } - Span::Span(sentry_span_t* s){ - inner = s; - } -#else - Transaction::Transaction(void* t){ - Q_UNUSED(t); - inner = nullptr; - } - Span::Span(void* s){ - Q_UNUSED(s); - inner = nullptr; - } -#endif - const char* bootId(){ - static std::string bootId(""); - if(!bootId.empty()){ - return bootId.c_str(); - } - sd_id128_t id; - int ret = sd_id128_get_boot(&id); - // TODO - eventually replace with the following when supported by the - // reMarkable kernel - // int ret = sd_id128_get_boot_app_specific(OXIDE_UID, &id); - if(ret == EXIT_SUCCESS){ - bootId = getAppSpecific(id); - // TODO - eventually replace with the following when supported by the - // reMarkable kernel - //char buf[SD_ID128_STRING_MAX]; - //bootId = sd_id128_to_string(id, buf); - return bootId.c_str(); - } - logMachineIdError(ret, "boot_id", "/proc/sys/kernel/random/boot_id"); - return ""; - } - const char* machineId(){ - static std::string machineId(""); - if(!machineId.empty()){ - return machineId.c_str(); - } - sd_id128_t id; - int ret = sd_id128_get_machine(&id); - // TODO - eventually replace with the following when supported by the - // reMarkable kernel - // int ret = sd_id128_get_machine_app_specific(OXIDE_UID, &id); - if(ret == EXIT_SUCCESS){ - machineId = getAppSpecific(id); - // TODO - eventually replace with the following when supported by the - // reMarkable kernel - //char buf[SD_ID128_STRING_MAX]; - //machineId = sd_id128_to_string(id, buf); - return machineId.c_str(); - } - logMachineIdError(ret, "machine-id", "/etc/machine-id"); - return ""; - } - bool enabled(){ - return sharedSettings.crashReport() || sharedSettings.telemetry(); - } -#ifdef SENTRY - sentry_options_t* options = sentry_options_new(); -#endif - void sentry_init(const char* name, char* argv[], bool autoSessionTracking){ -#ifdef SENTRY - if(sharedSettings.crashReport()){ - sentry_options_set_sample_rate(options, SAMPLE_RATE); - }else{ - sentry_options_set_sample_rate(options, 0.0); - } - if(sharedSettings.telemetry()){ - sentry_options_set_traces_sample_rate(options, SAMPLE_RATE); - sentry_options_set_max_spans(options, 1000); - }else{ - sentry_options_set_traces_sample_rate(options, 0.0); - } - if(!sharedSettings.telemetry() && !sharedSettings.crashReport()){ - sentry_user_consent_revoke(); - }else{ - sentry_user_consent_give(); - } - sentry_options_set_auto_session_tracking(options, autoSessionTracking && sharedSettings.telemetry()); - if(initialized){ - return; - } - initialized = true; - // Setup options - sentry_options_set_dsn(options, "https://a0136a12d63048c5a353c4a1c2d38914@sentry.eeems.codes/2"); - sentry_options_set_symbolize_stacktraces(options, true); - if(QLibraryInfo::isDebugBuild()){ - sentry_options_set_environment(options, "debug"); - }else{ - sentry_options_set_environment(options, "release"); - } - sentry_options_set_debug(options, debugEnabled()); - sentry_options_set_database_path(options, "/home/root/.cache/Eeems/sentry"); - sentry_options_set_release(options, (std::string(name) + "@2.4").c_str()); - sentry_init(options); - - // Setup user - sentry_value_t user = sentry_value_new_object(); - sentry_value_set_by_key(user, "id", sentry_value_new_string(machineId())); - sentry_set_user(user); - // Setup context - std::string version = readFile("/etc/version"); - sentry_set_tag("os.version", version.c_str()); - sentry_set_tag("name", name); - sentry_value_t device = sentry_value_new_object(); - sentry_value_set_by_key(device, "machine-id", sentry_value_new_string(machineId())); - sentry_value_set_by_key(device, "version", sentry_value_new_string(readFile("/etc/version").c_str())); - sentry_value_set_by_key(device, "model", sentry_value_new_string(deviceSettings.getDeviceName())); - sentry_set_context("device", device); - // Setup transaction - sentry_set_transaction(name); - // Add close guard - QObject::connect(qApp, &QCoreApplication::aboutToQuit, []() { - sentry_close(); - }); - // Handle settings changing - QObject::connect(&sharedSettings, &SharedSettings::telemetryChanged, [name, argv, autoSessionTracking](bool telemetry){ - Q_UNUSED(telemetry) - O_DEBUG("Telemetry changed to" << telemetry); - sentry_init(name, argv, autoSessionTracking); - }); - QObject::connect(&sharedSettings, &SharedSettings::crashReportChanged, [name, argv, autoSessionTracking](bool crashReport){ - Q_UNUSED(crashReport) - O_DEBUG("CrashReport changed to" << crashReport); - sentry_init(name, argv, autoSessionTracking); - }); -#else - Q_UNUSED(name); - Q_UNUSED(argv); - Q_UNUSED(autoSessionTracking); -#endif - } - void sentry_breadcrumb(const char* category, const char* message, const char* type, const char* level){ -#ifdef SENTRY - if(!sharedSettings.telemetry()){ - return; - } - sentry_value_t crumb = sentry_value_new_breadcrumb(type, message); - sentry_value_set_by_key(crumb, "category", sentry_value_new_string(category)); - sentry_value_set_by_key(crumb, "level", sentry_value_new_string(level)); - sentry_add_breadcrumb(crumb); -#else - Q_UNUSED(category); - Q_UNUSED(message); - Q_UNUSED(type); - Q_UNUSED(level); -#endif - } - Transaction* start_transaction(const std::string& name, const std::string& action){ -#ifdef SENTRY - sentry_transaction_context_t* context = sentry_transaction_context_new(name.c_str(), action.c_str()); - // Hack to force transactions to be reported even though SAMPLE_RATE is 100% - sentry_transaction_context_set_sampled(context, 1); - sentry_transaction_t* transaction = sentry_transaction_start(context, sentry_value_new_null()); - return new Transaction(transaction); -#else - Q_UNUSED(name); - Q_UNUSED(action); - return nullptr; -#endif - } - void stop_transaction(Transaction* transaction){ -#ifdef SENTRY - if(transaction != nullptr && transaction->inner != nullptr){ - sentry_transaction_finish(transaction->inner); - } -#else - Q_UNUSED(transaction); -#endif - } - void sentry_transaction(const std::string& name, const std::string& action, std::function callback){ -#ifdef SENTRY - if(!sharedSettings.telemetry()){ - callback(nullptr); - return; - } - Transaction* transaction = start_transaction(name, action); - auto scopeGuard = qScopeGuard([transaction] { - stop_transaction(transaction); - }); - callback(transaction); -#else - Q_UNUSED(name); - Q_UNUSED(action); - callback(nullptr); -#endif - } - Span* start_span(Transaction* transaction, const std::string& operation, const std::string& description){ -#ifdef SENTRY - if(transaction == nullptr){ - return nullptr; - } - return new Span(sentry_transaction_start_child(transaction->inner, (char*)operation.c_str(), (char*)description.c_str())); -#else - Q_UNUSED(transaction); - Q_UNUSED(operation); - Q_UNUSED(description); - return nullptr; -#endif - } - Span* start_span(Span* parent, const std::string& operation, const std::string& description){ -#ifdef SENTRY - if(parent == nullptr){ - return nullptr; - } - return new Span(sentry_span_start_child(parent->inner, (char*)operation.c_str(), (char*)description.c_str())); -#else - Q_UNUSED(parent); - Q_UNUSED(operation); - Q_UNUSED(description); - return nullptr; -#endif - } - void stop_span(Span* span){ -#ifdef SENTRY - if(span != nullptr && span->inner != nullptr){ - sentry_span_finish(span->inner); - } -#else - Q_UNUSED(span); -#endif - } - void sentry_span(Transaction* transaction, const std::string& operation, const std::string& description, std::function callback){ -#ifdef SENTRY - sentry_span(transaction, operation, description, [callback](Span* s){ - Q_UNUSED(s); - callback(); - }); -#else - Q_UNUSED(transaction); - Q_UNUSED(operation); - Q_UNUSED(description); - callback(); -#endif - } - void sentry_span(Transaction* transaction, const std::string& operation, const std::string& description, std::function callback){ -#ifdef SENTRY - if(!sharedSettings.telemetry() || transaction == nullptr || transaction->inner == nullptr){ - callback(nullptr); - return; - } - Span* span = start_span(transaction, operation, description); - auto scopeGuard = qScopeGuard([span] { - stop_span(span); - }); - callback(span); -#else - Q_UNUSED(transaction); - Q_UNUSED(operation); - Q_UNUSED(description); - callback(nullptr); -#endif - } - void sentry_span(Span* parent, const std::string& operation, const std::string& description, std::function callback){ -#ifdef SENTRY - sentry_span(parent, operation, description, [callback](Span* s){ - Q_UNUSED(s); - callback(); - }); -#else - Q_UNUSED(parent); - Q_UNUSED(operation); - Q_UNUSED(description); - callback(); -#endif - } - - void sentry_span(Span* parent, const std::string& operation, const std::string& description, std::function callback){ -#ifdef SENTRY - if(!sharedSettings.telemetry() || parent == nullptr || parent->inner == nullptr){ - callback(nullptr); - return; - } - Span* span = start_span(parent, operation, description); - auto scopeGuard = qScopeGuard([span] { - stop_span(span); - }); - callback(span); -#else - Q_UNUSED(parent); - Q_UNUSED(operation); - Q_UNUSED(description); - callback(nullptr); -#endif - } - void trigger_crash(){ memset((char *)invalid_mem, 1, 100); } - } DeviceSettings& DeviceSettings::instance() { static DeviceSettings INSTANCE; return INSTANCE; diff --git a/shared/liboxide/liboxide.h b/shared/liboxide/liboxide.h index 8d7520683..8439ab6da 100644 --- a/shared/liboxide/liboxide.h +++ b/shared/liboxide/liboxide.h @@ -13,6 +13,7 @@ #include "signalhandler.h" #include "slothandler.h" #include "debug.h" +#include "oxide_sentry.h" #include #include @@ -36,10 +37,6 @@ */ #define sharedSettings Oxide::SharedSettings::instance() -#ifdef SENTRY -#include -#include -#endif /*! * \brief Wifi Network definition @@ -62,150 +59,6 @@ namespace Oxide { * \snippet examples/oxide.cpp dispatchToMainThread */ LIBOXIDE_EXPORT void dispatchToMainThread(std::function callback); - /*! - *\brief The Sentry namespace - */ - namespace Sentry{ - /*! - * \brief A sentry_transaction_t wrapper - */ - struct Transaction { -#ifdef SENTRY - /*! - * \brief The sentry_transaction_t instance - */ - sentry_transaction_t* inner; - /*! - * \brief Create a sentry_transaction_t wrapper - * \param sentry_transaction_t instance to wrap - */ - explicit Transaction(sentry_transaction_t* t); -#else - void* inner; - explicit Transaction(void* t); -#endif - }; - /*! - * \brief A sentry_span_t wrapper - */ - struct Span { -#ifdef SENTRY - /*! - * \brief The sentry_span_t instance - */ - sentry_span_t* inner; - /*! - * \brief Create a sentry_span_t wrapper - * \param The sentry_span_t instance to wrap - */ - explicit Span(sentry_span_t* s); -#else - void* inner; - explicit Span(void* s); -#endif - }; - /*! - * \brief Get the boot identifier of the device using sd_id128_get_boot - * \return The boot identifier - */ - LIBOXIDE_EXPORT const char* bootId(); - /*! - * \brief Get the machine identifier of the device using sd_id128_get_machine - * \return The machine identifier - */ - LIBOXIDE_EXPORT const char* machineId(); - /*! - * \brief Initialize sentry tracking - * \param Name of the application - * \param Arguments passed to the application - * \param If automatic session tracking should be enabled - */ - LIBOXIDE_EXPORT void sentry_init(const char* name, char* argv[], bool autoSessionTracking = true); - /*! - * \brief Create a breadcrumb in the current sentry transaction - * \param Category of the breadcrumb - * \param Message of the breadcrumb - * \param Type of breadcrumb - * \param Logging level of the breadcrumb - */ - LIBOXIDE_EXPORT void sentry_breadcrumb(const char* category, const char* message, const char* type = "default", const char* level = "info"); - /*! - * \brief Start a transaction - * \param Name of the transaction - * \param Action being performed - * \return The transaction wrapper - */ - LIBOXIDE_EXPORT Transaction* start_transaction(const std::string& name, const std::string& action); - /*! - * \brief Stop a sentry transaction - * \param The transaction wrapper to stop - */ - LIBOXIDE_EXPORT void stop_transaction(Transaction* transaction); - /*! - * \brief Record a sentry trancation - * \param Name of the transaction - * \param Action being performed - * \param Code to run inside the transaction - */ - LIBOXIDE_EXPORT void sentry_transaction(const std::string& name, const std::string& action, std::function callback); - /*! - * \brief Start a span inside a sentry transaction - * \param Transaction wrapper to attach the span to - * \param Operation being performed - * \param Description of the span - * \return The span wrapper - */ - LIBOXIDE_EXPORT Span* start_span(Transaction* transaction, const std::string& operation, const std::string& description); - /*! - * \brief Start a span inside another sentry span - * \param The parent sentry span wrapper - * \param Operation being performed - * \param Description of the span - * \return The span wrapper - */ - LIBOXIDE_EXPORT Span* start_span(Span* parent, const std::string& operation, const std::string& description); - /*! - * \brief Stop a sentry span - * \param The span wrapper to stop - */ - LIBOXIDE_EXPORT void stop_span(Span* span); - /*! - * \brief Record a sentry span - * \param The transaction wrapper to attach this span to - * \param Operation being performed - * \param Description of the span - * \param Code to run inside the transaction - */ - LIBOXIDE_EXPORT void sentry_span(Transaction* transaction, const std::string& operation, const std::string& description, std::function callback); - /*! - * \brief Record a sentry span - * \param The transaction wrapper to attach this span to - * \param Operation being performed - * \param Description of the span - * \param Code to run inside the transaction - */ - LIBOXIDE_EXPORT void sentry_span(Transaction* transaction, const std::string& operation, const std::string& description, std::function callback); - /*! - * \brief Record a sentry span - * \param The span wrapper to attach this span to - * \param Operation being performed - * \param Description of the span - * \param Code to run inside the transaction - */ - LIBOXIDE_EXPORT void sentry_span(Span* parent, const std::string& operation, const std::string& description, std::function callback); - /*! - * \brief Record a sentry span - * \param The span wrapper to attach this span to - * \param Operation being performed - * \param Description of the span - * \param Code to run inside the transaction - */ - LIBOXIDE_EXPORT void sentry_span(Span* parent, const std::string& operation, const std::string& description, std::function callback); - /*! - * \brief Trigger a crash. Useful to test that sentry integration is working - */ - LIBOXIDE_EXPORT void trigger_crash(); - } /*! * \brief Device specific values */ diff --git a/shared/liboxide/liboxide.pro b/shared/liboxide/liboxide.pro index b3bcb36a0..f7e073178 100644 --- a/shared/liboxide/liboxide.pro +++ b/shared/liboxide/liboxide.pro @@ -15,6 +15,7 @@ SOURCES += \ eventfilter.cpp \ json.cpp \ liboxide.cpp \ + oxide_sentry.cpp \ power.cpp \ settingsfile.cpp \ slothandler.cpp \ @@ -27,6 +28,7 @@ HEADERS += \ liboxide_global.h \ liboxide.h \ meta.h \ + oxide_sentry.h \ power.h \ json.h \ settingsfile.h \ diff --git a/shared/liboxide/oxide_sentry.cpp b/shared/liboxide/oxide_sentry.cpp new file mode 100644 index 000000000..0fd2b4268 --- /dev/null +++ b/shared/liboxide/oxide_sentry.cpp @@ -0,0 +1,354 @@ +#include "oxide_sentry.h" +#include "liboxide.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +// String: 5aa5ca39ee0b4f48927529ca17519524 +// UUID: 5aa5ca39-ee0b-4f48-9275-29ca17519524 +#define OXIDE_UID SD_ID128_MAKE(5a,a5,ca,39,ee,0b,4f,48,92,75,29,ca,17,51,95,24) + +#ifdef SENTRY +#define SAMPLE_RATE 1.0 +std::string readFile(const std::string& path){ + std::ifstream t(path); + std::stringstream buffer; + buffer << t.rdbuf(); + return buffer.str(); +} +#endif + +static void* invalid_mem = (void *)1; + +void logMachineIdError(int error, QString name, QString path){ + if(error == -ENOENT){ + O_WARNING("/etc/machine-id is missing"); + }else if(error == -ENOMEDIUM){ + O_WARNING(path + " is empty or all zeros"); + }else if(error == -EIO){ + O_WARNING(path + " has the incorrect format"); + }else if(error == -EPERM){ + O_WARNING(path + " access denied"); + } if(error == -EINVAL){ + O_WARNING("Error while reading " + name + ": Buffer invalid"); + }else if(error == -ENXIO){ + O_WARNING("Error while reading " + name + ": No invocation ID is set"); + }else if(error == -EOPNOTSUPP){ + O_WARNING("Error while reading " + name + ": Operation not supported"); + }else{ + O_WARNING("Unexpected error code reading " + name + ":" << strerror(error)); + } +} +std::string getAppSpecific(sd_id128_t base){ + QCryptographicHash hash(QCryptographicHash::Sha256); + char buf[SD_ID128_STRING_MAX]; + hash.addData(sd_id128_to_string(base, buf)); + hash.addData(sd_id128_to_string(OXIDE_UID, buf)); + auto r = hash.result(); + r[6] = (r.at(6) & 0x0F) | 0x40; + r[8] = (r.at(8) & 0x3F) | 0x80; + QUuid uid(r.at(0), r.at(1), r.at(2), r.at(3), r.at(4), r.at(5), r.at(6), r.at(7), r.at(8), r.at(9), r.at(10)); + return uid.toString((QUuid::Id128)).toStdString(); +} + +namespace Oxide::Sentry{ +#ifdef SENTRY + static bool initialized = false; + Transaction::Transaction(sentry_transaction_t* t){ + inner = t; + } + Span::Span(sentry_span_t* s){ + inner = s; + } +#else + Transaction::Transaction(void* t){ + Q_UNUSED(t); + inner = nullptr; + } + Span::Span(void* s){ + Q_UNUSED(s); + inner = nullptr; + } +#endif + const char* bootId(){ + static std::string bootId(""); + if(!bootId.empty()){ + return bootId.c_str(); + } + sd_id128_t id; + int ret = sd_id128_get_boot(&id); + // TODO - eventually replace with the following when supported by the + // reMarkable kernel + // int ret = sd_id128_get_boot_app_specific(OXIDE_UID, &id); + if(ret == EXIT_SUCCESS){ + bootId = getAppSpecific(id); + // TODO - eventually replace with the following when supported by the + // reMarkable kernel + //char buf[SD_ID128_STRING_MAX]; + //bootId = sd_id128_to_string(id, buf); + return bootId.c_str(); + } + logMachineIdError(ret, "boot_id", "/proc/sys/kernel/random/boot_id"); + return ""; + } + const char* machineId(){ + static std::string machineId(""); + if(!machineId.empty()){ + return machineId.c_str(); + } + sd_id128_t id; + int ret = sd_id128_get_machine(&id); + // TODO - eventually replace with the following when supported by the + // reMarkable kernel + // int ret = sd_id128_get_machine_app_specific(OXIDE_UID, &id); + if(ret == EXIT_SUCCESS){ + machineId = getAppSpecific(id); + // TODO - eventually replace with the following when supported by the + // reMarkable kernel + //char buf[SD_ID128_STRING_MAX]; + //machineId = sd_id128_to_string(id, buf); + return machineId.c_str(); + } + logMachineIdError(ret, "machine-id", "/etc/machine-id"); + return ""; + } + bool enabled(){ + return sharedSettings.crashReport() || sharedSettings.telemetry(); + } +#ifdef SENTRY + sentry_options_t* options = sentry_options_new(); +#endif + void sentry_init(const char* name, char* argv[], bool autoSessionTracking){ +#ifdef SENTRY + if(sharedSettings.crashReport()){ + sentry_options_set_sample_rate(options, SAMPLE_RATE); + }else{ + sentry_options_set_sample_rate(options, 0.0); + } + if(sharedSettings.telemetry()){ + sentry_options_set_traces_sample_rate(options, SAMPLE_RATE); + sentry_options_set_max_spans(options, 1000); + }else{ + sentry_options_set_traces_sample_rate(options, 0.0); + } + if(!sharedSettings.telemetry() && !sharedSettings.crashReport()){ + sentry_user_consent_revoke(); + }else{ + sentry_user_consent_give(); + } + sentry_options_set_auto_session_tracking(options, autoSessionTracking && sharedSettings.telemetry()); + if(initialized){ + return; + } + initialized = true; + // Setup options + sentry_options_set_dsn(options, "https://a0136a12d63048c5a353c4a1c2d38914@sentry.eeems.codes/2"); + sentry_options_set_symbolize_stacktraces(options, true); + if(QLibraryInfo::isDebugBuild()){ + sentry_options_set_environment(options, "debug"); + }else{ + sentry_options_set_environment(options, "release"); + } + sentry_options_set_debug(options, debugEnabled()); + sentry_options_set_database_path(options, "/home/root/.cache/Eeems/sentry"); + sentry_options_set_release(options, (std::string(name) + "@2.4").c_str()); + sentry_init(options); + + // Setup user + sentry_value_t user = sentry_value_new_object(); + sentry_value_set_by_key(user, "id", sentry_value_new_string(machineId())); + sentry_set_user(user); + // Setup context + std::string version = readFile("/etc/version"); + sentry_set_tag("os.version", version.c_str()); + sentry_set_tag("name", name); + sentry_value_t device = sentry_value_new_object(); + sentry_value_set_by_key(device, "machine-id", sentry_value_new_string(machineId())); + sentry_value_set_by_key(device, "version", sentry_value_new_string(readFile("/etc/version").c_str())); + sentry_value_set_by_key(device, "model", sentry_value_new_string(deviceSettings.getDeviceName())); + sentry_set_context("device", device); + // Setup transaction + sentry_set_transaction(name); + // Add close guard + QObject::connect(qApp, &QCoreApplication::aboutToQuit, []() { + sentry_close(); + }); + // Handle settings changing + QObject::connect(&sharedSettings, &SharedSettings::telemetryChanged, [name, argv, autoSessionTracking](bool telemetry){ + Q_UNUSED(telemetry) + O_DEBUG("Telemetry changed to" << telemetry); + sentry_init(name, argv, autoSessionTracking); + }); + QObject::connect(&sharedSettings, &SharedSettings::crashReportChanged, [name, argv, autoSessionTracking](bool crashReport){ + Q_UNUSED(crashReport) + O_DEBUG("CrashReport changed to" << crashReport); + sentry_init(name, argv, autoSessionTracking); + }); +#else + Q_UNUSED(name); + Q_UNUSED(argv); + Q_UNUSED(autoSessionTracking); +#endif + } + void sentry_breadcrumb(const char* category, const char* message, const char* type, const char* level){ +#ifdef SENTRY + if(!sharedSettings.telemetry()){ + return; + } + sentry_value_t crumb = sentry_value_new_breadcrumb(type, message); + sentry_value_set_by_key(crumb, "category", sentry_value_new_string(category)); + sentry_value_set_by_key(crumb, "level", sentry_value_new_string(level)); + sentry_add_breadcrumb(crumb); +#else + Q_UNUSED(category); + Q_UNUSED(message); + Q_UNUSED(type); + Q_UNUSED(level); +#endif + } + Transaction* start_transaction(const std::string& name, const std::string& action){ +#ifdef SENTRY + sentry_transaction_context_t* context = sentry_transaction_context_new(name.c_str(), action.c_str()); + // Hack to force transactions to be reported even though SAMPLE_RATE is 100% + sentry_transaction_context_set_sampled(context, 1); + sentry_transaction_t* transaction = sentry_transaction_start(context, sentry_value_new_null()); + return new Transaction(transaction); +#else + Q_UNUSED(name); + Q_UNUSED(action); + return nullptr; +#endif + } + void stop_transaction(Transaction* transaction){ +#ifdef SENTRY + if(transaction != nullptr && transaction->inner != nullptr){ + sentry_transaction_finish(transaction->inner); + } +#else + Q_UNUSED(transaction); +#endif + } + void sentry_transaction(const std::string& name, const std::string& action, std::function callback){ +#ifdef SENTRY + if(!sharedSettings.telemetry()){ + callback(nullptr); + return; + } + Transaction* transaction = start_transaction(name, action); + auto scopeGuard = qScopeGuard([transaction] { + stop_transaction(transaction); + }); + callback(transaction); +#else + Q_UNUSED(name); + Q_UNUSED(action); + callback(nullptr); +#endif + } + Span* start_span(Transaction* transaction, const std::string& operation, const std::string& description){ +#ifdef SENTRY + if(transaction == nullptr){ + return nullptr; + } + return new Span(sentry_transaction_start_child(transaction->inner, (char*)operation.c_str(), (char*)description.c_str())); +#else + Q_UNUSED(transaction); + Q_UNUSED(operation); + Q_UNUSED(description); + return nullptr; +#endif + } + Span* start_span(Span* parent, const std::string& operation, const std::string& description){ +#ifdef SENTRY + if(parent == nullptr){ + return nullptr; + } + return new Span(sentry_span_start_child(parent->inner, (char*)operation.c_str(), (char*)description.c_str())); +#else + Q_UNUSED(parent); + Q_UNUSED(operation); + Q_UNUSED(description); + return nullptr; +#endif + } + void stop_span(Span* span){ +#ifdef SENTRY + if(span != nullptr && span->inner != nullptr){ + sentry_span_finish(span->inner); + } +#else + Q_UNUSED(span); +#endif + } + void sentry_span(Transaction* transaction, const std::string& operation, const std::string& description, std::function callback){ +#ifdef SENTRY + sentry_span(transaction, operation, description, [callback](Span* s){ + Q_UNUSED(s); + callback(); + }); +#else + Q_UNUSED(transaction); + Q_UNUSED(operation); + Q_UNUSED(description); + callback(); +#endif + } + void sentry_span(Transaction* transaction, const std::string& operation, const std::string& description, std::function callback){ +#ifdef SENTRY + if(!sharedSettings.telemetry() || transaction == nullptr || transaction->inner == nullptr){ + callback(nullptr); + return; + } + Span* span = start_span(transaction, operation, description); + auto scopeGuard = qScopeGuard([span] { + stop_span(span); + }); + callback(span); +#else + Q_UNUSED(transaction); + Q_UNUSED(operation); + Q_UNUSED(description); + callback(nullptr); +#endif + } + void sentry_span(Span* parent, const std::string& operation, const std::string& description, std::function callback){ +#ifdef SENTRY + sentry_span(parent, operation, description, [callback](Span* s){ + Q_UNUSED(s); + callback(); + }); +#else + Q_UNUSED(parent); + Q_UNUSED(operation); + Q_UNUSED(description); + callback(); +#endif + } + + void sentry_span(Span* parent, const std::string& operation, const std::string& description, std::function callback){ +#ifdef SENTRY + if(!sharedSettings.telemetry() || parent == nullptr || parent->inner == nullptr){ + callback(nullptr); + return; + } + Span* span = start_span(parent, operation, description); + auto scopeGuard = qScopeGuard([span] { + stop_span(span); + }); + callback(span); +#else + Q_UNUSED(parent); + Q_UNUSED(operation); + Q_UNUSED(description); + callback(nullptr); +#endif + } + void trigger_crash(){ memset((char *)invalid_mem, 1, 100); } +} diff --git a/shared/liboxide/oxide_sentry.h b/shared/liboxide/oxide_sentry.h new file mode 100644 index 000000000..3af7e010b --- /dev/null +++ b/shared/liboxide/oxide_sentry.h @@ -0,0 +1,164 @@ +/*! + * \file sentry.h + */ +#ifndef OXIDE_SENTRY_H +#define OXIDE_SENTRY_H + +#include "liboxide_global.h" + +#include +#include +#include +#include + +#ifdef SENTRY +#include +#include +#endif + +/*! + *\brief The Sentry namespace + */ +namespace Oxide::Sentry{ + /*! + * \brief A sentry_transaction_t wrapper + */ + struct Transaction { +#ifdef SENTRY + /*! + * \brief The sentry_transaction_t instance + */ + sentry_transaction_t* inner; + /*! + * \brief Create a sentry_transaction_t wrapper + * \param sentry_transaction_t instance to wrap + */ + explicit Transaction(sentry_transaction_t* t); +#else + void* inner; + explicit Transaction(void* t); +#endif + }; + /*! + * \brief A sentry_span_t wrapper + */ + struct Span { +#ifdef SENTRY + /*! + * \brief The sentry_span_t instance + */ + sentry_span_t* inner; + /*! + * \brief Create a sentry_span_t wrapper + * \param The sentry_span_t instance to wrap + */ + explicit Span(sentry_span_t* s); +#else + void* inner; + explicit Span(void* s); +#endif + }; + /*! + * \brief Get the boot identifier of the device using sd_id128_get_boot + * \return The boot identifier + */ + LIBOXIDE_EXPORT const char* bootId(); + /*! + * \brief Get the machine identifier of the device using sd_id128_get_machine + * \return The machine identifier + */ + LIBOXIDE_EXPORT const char* machineId(); + /*! + * \brief Initialize sentry tracking + * \param Name of the application + * \param Arguments passed to the application + * \param If automatic session tracking should be enabled + */ + LIBOXIDE_EXPORT void sentry_init(const char* name, char* argv[], bool autoSessionTracking = true); + /*! + * \brief Create a breadcrumb in the current sentry transaction + * \param Category of the breadcrumb + * \param Message of the breadcrumb + * \param Type of breadcrumb + * \param Logging level of the breadcrumb + */ + LIBOXIDE_EXPORT void sentry_breadcrumb(const char* category, const char* message, const char* type = "default", const char* level = "info"); + /*! + * \brief Start a transaction + * \param Name of the transaction + * \param Action being performed + * \return The transaction wrapper + */ + LIBOXIDE_EXPORT Transaction* start_transaction(const std::string& name, const std::string& action); + /*! + * \brief Stop a sentry transaction + * \param The transaction wrapper to stop + */ + LIBOXIDE_EXPORT void stop_transaction(Transaction* transaction); + /*! + * \brief Record a sentry trancation + * \param Name of the transaction + * \param Action being performed + * \param Code to run inside the transaction + */ + LIBOXIDE_EXPORT void sentry_transaction(const std::string& name, const std::string& action, std::function callback); + /*! + * \brief Start a span inside a sentry transaction + * \param Transaction wrapper to attach the span to + * \param Operation being performed + * \param Description of the span + * \return The span wrapper + */ + LIBOXIDE_EXPORT Span* start_span(Transaction* transaction, const std::string& operation, const std::string& description); + /*! + * \brief Start a span inside another sentry span + * \param The parent sentry span wrapper + * \param Operation being performed + * \param Description of the span + * \return The span wrapper + */ + LIBOXIDE_EXPORT Span* start_span(Span* parent, const std::string& operation, const std::string& description); + /*! + * \brief Stop a sentry span + * \param The span wrapper to stop + */ + LIBOXIDE_EXPORT void stop_span(Span* span); + /*! + * \brief Record a sentry span + * \param The transaction wrapper to attach this span to + * \param Operation being performed + * \param Description of the span + * \param Code to run inside the transaction + */ + LIBOXIDE_EXPORT void sentry_span(Transaction* transaction, const std::string& operation, const std::string& description, std::function callback); + /*! + * \brief Record a sentry span + * \param The transaction wrapper to attach this span to + * \param Operation being performed + * \param Description of the span + * \param Code to run inside the transaction + */ + LIBOXIDE_EXPORT void sentry_span(Transaction* transaction, const std::string& operation, const std::string& description, std::function callback); + /*! + * \brief Record a sentry span + * \param The span wrapper to attach this span to + * \param Operation being performed + * \param Description of the span + * \param Code to run inside the transaction + */ + LIBOXIDE_EXPORT void sentry_span(Span* parent, const std::string& operation, const std::string& description, std::function callback); + /*! + * \brief Record a sentry span + * \param The span wrapper to attach this span to + * \param Operation being performed + * \param Description of the span + * \param Code to run inside the transaction + */ + LIBOXIDE_EXPORT void sentry_span(Span* parent, const std::string& operation, const std::string& description, std::function callback); + /*! + * \brief Trigger a crash. Useful to test that sentry integration is working + */ + LIBOXIDE_EXPORT void trigger_crash(); +} + +#endif // OXIDE_SENTRY_H From a1ffa1442971b9aea41abd74fa7dd496674e7c4c Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Fri, 27 Jan 2023 22:03:45 -0700 Subject: [PATCH 11/15] Precompiled headers (#289) * Use precompiled headers and start building liboxide headers * Rework build system to use qmake * Use toltecmk for building instead of the toltec Makefile --- .github/workflows/build.yml | 39 ++--- .github/workflows/nix.yml | 4 + .gitignore | 4 + Makefile | 151 ++++++------------ README.md | 12 +- applications/applications.pro | 22 +++ .../launcher/{oxide.pro => launcher.pro} | 42 ++--- applications/launcher/main.cpp | 1 - applications/launcher/oxide_stable.h | 50 ++++++ applications/lockscreen/decay.pro | 57 ------- applications/lockscreen/lockscreen.pro | 35 ++++ applications/notify-send/notify-send.pro | 33 +--- applications/process-manager/erode.pro | 58 ------- applications/process-manager/erode_stable.h | 25 +++ .../process-manager/process-manager.pro | 39 +++++ applications/screenshot-tool/fret.pro | 51 ------ .../screenshot-tool/screenshot-tool.pro | 27 ++++ applications/screenshot-viewer/anxiety.pro | 63 -------- .../screenshot-viewer/anxiety_stable.h | 13 ++ applications/screenshot-viewer/main.cpp | 1 - .../screenshot-viewer/screenshot-viewer.pro | 44 +++++ .../{rot.pro => settings-manager.pro} | 33 +--- applications/system-service/apibase.h | 2 +- applications/system-service/application.h | 3 + applications/system-service/bss.h | 6 +- applications/system-service/buttonhandler.cpp | 4 +- applications/system-service/dbusservice.h | 5 +- applications/system-service/generate_xml.sh | 19 ++- applications/system-service/network.h | 6 +- applications/system-service/notification.h | 6 +- applications/system-service/screenshot.h | 4 +- .../{tarnish.pro => system-service.pro} | 41 ++--- applications/system-service/tarnish_stable.h | 76 +++++++++ applications/system-service/wlan.h | 5 +- applications/task-switcher/corrupt.pro | 58 ------- applications/task-switcher/corrupt_stable.h | 30 ++++ applications/task-switcher/task-switcher.pro | 41 +++++ oxide.nix | 2 + oxide.pro | 7 + package | 4 +- qmake/common.pri | 3 + qmake/epaper.pri | 2 + qmake/liboxide.pri | 2 + qmake/sentry.pri | 16 ++ shared/dbussettings.h | 1 - shared/{ => epaper}/epframebuffer.h | 0 shared/{ => epaper}/libepaper.so | Bin shared/{ => epaper}/libqsgepaper.a | Bin shared/liboxide/liboxide.h | 1 + shared/liboxide/liboxide.pro | 41 ++--- shared/liboxide/liboxide_stable.h | 53 ++++++ shared/liboxide/oxide_sentry.h | 1 - shared/liboxide/power.h | 1 - shared/liboxide/sysobject.cpp | 4 +- shared/shared.pro | 4 + 55 files changed, 680 insertions(+), 572 deletions(-) create mode 100644 applications/applications.pro rename applications/launcher/{oxide.pro => launcher.pro} (58%) create mode 100644 applications/launcher/oxide_stable.h delete mode 100644 applications/lockscreen/decay.pro create mode 100644 applications/lockscreen/lockscreen.pro delete mode 100755 applications/process-manager/erode.pro create mode 100644 applications/process-manager/erode_stable.h create mode 100755 applications/process-manager/process-manager.pro delete mode 100644 applications/screenshot-tool/fret.pro create mode 100644 applications/screenshot-tool/screenshot-tool.pro delete mode 100644 applications/screenshot-viewer/anxiety.pro create mode 100644 applications/screenshot-viewer/anxiety_stable.h create mode 100644 applications/screenshot-viewer/screenshot-viewer.pro rename applications/settings-manager/{rot.pro => settings-manager.pro} (52%) rename applications/system-service/{tarnish.pro => system-service.pro} (67%) create mode 100644 applications/system-service/tarnish_stable.h delete mode 100644 applications/task-switcher/corrupt.pro create mode 100644 applications/task-switcher/corrupt_stable.h create mode 100644 applications/task-switcher/task-switcher.pro create mode 100644 oxide.pro create mode 100644 qmake/common.pri create mode 100644 qmake/epaper.pri create mode 100644 qmake/liboxide.pri create mode 100644 qmake/sentry.pri delete mode 120000 shared/dbussettings.h rename shared/{ => epaper}/epframebuffer.h (100%) rename shared/{ => epaper}/libepaper.so (100%) rename shared/{ => epaper}/libqsgepaper.a (100%) create mode 100644 shared/liboxide/liboxide_stable.h create mode 100644 shared/shared.pro diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1da6e02c..cadb6e022 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,6 +9,8 @@ on: - 'shared/**' - 'assets/**' - 'interfaces/**' + - 'qmake/**' + - 'oxide.pro' - 'Makefile' - 'package' pull_request: @@ -17,6 +19,8 @@ on: - 'shared/**' - 'assets/**' - 'interfaces/**' + - 'qmake/**' + - 'oxide.pro' - 'Makefile' - 'package' jobs: @@ -24,41 +28,18 @@ jobs: name: Build and package runs-on: ubuntu-latest steps: - - name: Checkout toltec Git repository - uses: actions/checkout@v3 - with: - repository: toltec-dev/toltec.git - ref: testing - - name: Cleanup - run: | - rm -rf package/oxide/* - mkdir package/oxide/src - uses: actions/checkout@v3 - with: - path: package/oxide/src - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: - python-version: '3.8' - - name: Setup Toltec dependencies - uses: ./.github/actions/setup - - name: Prepare build files - run: | - cd package/oxide/src - echo "r$(git rev-list --count HEAD).$(git rev-parse --short HEAD)" > ../version.txt - sed -i "s/~VERSION~/$(cat ../version.txt)/" ./package - mv ./package .. - tar --exclude='./.git' -czvf ../oxide.tar.gz . + python-version: 3.9 + - name: Install toltecmk + run: pip install toltecmk - name: Build packages - run: | - make FLAGS=--verbose oxide - mkdir output - find . -name '*.ipk' | while read -r file; do - cp "$file" output/"$(basename $file)" - done + run: make package timeout-minutes: 15 - name: Save packages uses: actions/upload-artifact@v3 with: name: packages - path: output + path: dist diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index edaead1e1..69344f6df 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -9,6 +9,8 @@ on: - 'shared/**' - 'assets/**' - 'interfaces/**' + - 'qmake/**' + - 'oxide.pro' - 'Makefile' - '*.nix' pull_request: @@ -17,6 +19,8 @@ on: - 'shared/**' - 'assets/**' - 'interfaces/**' + - 'qmake/**' + - 'oxide.pro' - 'Makefile' - '*.nix' jobs: diff --git a/.gitignore b/.gitignore index 447d2adb8..65e6f2195 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ release/ applications/*/Makefile result *.orig +oxide.tar.gz +build/ +dist/ +version.txt diff --git a/Makefile b/Makefile index d06ed7c1a..94d729684 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean release build liboxide erode tarnish rot fret oxide decay corrupt anxiety sentry +.PHONY: all clean release build sentry package all: release @@ -12,109 +12,64 @@ RELOBJ += release/opt/lib/libsentry.so DEFINES += 'DEFINES+="SENTRY"' endif +OBJ += _make +RELOBJ += _install + clean: rm -rf .build rm -rf release release: clean build $(RELOBJ) - mkdir -p release - INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/liboxide install - INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/process-manager install - INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/system-service install - INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/settings-manager install - INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/screenshot-tool install - INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/screenshot-viewer install - INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/launcher install - INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/lockscreen install - INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/task-switcher install - INSTALL_ROOT=../../release $(MAKE) -j`nproc` -C .build/notify-send install - -build: liboxide tarnish erode rot oxide decay corrupt fret anxiety notify-send - -liboxide: $(OBJ) .build/liboxide/libliboxide.so - -.build/liboxide/libliboxide.so: - mkdir -p .build/liboxide - cp -r shared/liboxide/* .build/liboxide - cd .build/liboxide && qmake $(DEFINES) liboxide.pro - $(MAKE) -j`nproc` -C .build/liboxide all - -erode: liboxide .build/process-manager/erode - -.build/process-manager/erode: - mkdir -p .build/process-manager - cp -r applications/process-manager/* .build/process-manager - cd .build/process-manager && qmake $(DEFINES) erode.pro - $(MAKE) -j`nproc` -C .build/process-manager all - -tarnish: liboxide .build/system-service/tarnish - -.build/system-service/tarnish: - mkdir -p .build/system-service - cp -r applications/system-service/* .build/system-service - cd .build/system-service && qmake $(DEFINES) tarnish.pro - $(MAKE) -j`nproc` -C .build/system-service all - -rot: tarnish liboxide .build/settings-manager/rot - -.build/settings-manager/rot: - mkdir -p .build/settings-manager - cp -r applications/settings-manager/* .build/settings-manager - cd .build/settings-manager && qmake $(DEFINES) rot.pro - $(MAKE) -j`nproc` -C .build/settings-manager all - -fret: tarnish liboxide .build/screenshot-tool/fret - -.build/screenshot-tool/fret: - mkdir -p .build/screenshot-tool - cp -r applications/screenshot-tool/* .build/screenshot-tool - cd .build/screenshot-tool && qmake $(DEFINES) fret.pro - $(MAKE) -j`nproc` -C .build/screenshot-tool all - -oxide: tarnish liboxide .build/launcher/oxide - -.build/launcher/oxide: - mkdir -p .build/launcher - cp -r applications/launcher/* .build/launcher - cd .build/launcher && qmake $(DEFINES) oxide.pro - $(MAKE) -j`nproc` -C .build/launcher all - -decay: tarnish liboxide .build/lockscreen/decay - -.build/lockscreen/decay: - mkdir -p .build/lockscreen - cp -r applications/lockscreen/* .build/lockscreen - cd .build/lockscreen && qmake $(DEFINES) decay.pro - $(MAKE) -j`nproc` -C .build/lockscreen all - -corrupt: tarnish liboxide .build/task-switcher/corrupt - -.build/task-switcher/corrupt: - mkdir -p .build/task-switcher - cp -r applications/task-switcher/* .build/task-switcher - cd .build/task-switcher && qmake $(DEFINES) corrupt.pro - $(MAKE) -j`nproc` -C .build/task-switcher all - -anxiety: tarnish liboxide .build/screenshot-viewer/anxiety - -.build/screenshot-viewer/anxiety: - mkdir -p .build/screenshot-viewer - cp -r applications/screenshot-viewer/* .build/screenshot-viewer - cd .build/screenshot-viewer && qmake $(DEFINES) anxiety.pro - $(MAKE) -j`nproc` -C .build/screenshot-viewer all - -notify-send: tarnish liboxide .build/notify-send/notify-send - -.build/notify-send/notify-send: - mkdir -p .build/notify-send - cp -r applications/notify-send/* .build/notify-send - cd .build/notify-send && qmake $(DEFINES) notify-send.pro - $(MAKE) -j`nproc` -C .build/notify-send all + +build: $(OBJ) + +package: REV="~r$(shell git rev-list --count HEAD).$(shell git rev-parse --short HEAD)" +package: + rm -rf .build/package/ dist/ + if [ -d .git ];then \ + echo $(REV) > version.txt; \ + else \ + echo "~manual" > version.txt; \ + fi; + mkdir -p .build/package + sed "s/~VERSION~/`cat version.txt`/" ./package > .build/package/package + tar \ + --exclude='./.git' \ + --exclude='./.build' \ + --exclude='./dist' \ + --exclude='./release' \ + -czvf .build/package/oxide.tar.gz \ + applications \ + assets \ + interfaces \ + qmake \ + shared \ + oxide.pro \ + Makefile + toltecmk \ + -w .build/package/build \ + -d .build/package/dist \ + .build/package + mkdir dist/ + cp -a .build/package/dist/rmall/*.ipk dist/ sentry: .build/sentry/libsentry.so +_make: .build/oxide/Makefile + $(MAKE) -j`nproc` -C .build/oxide all + +_install: _make + mkdir -p $(CURDIR)/release + INSTALL_ROOT=$(CURDIR)/release $(MAKE) -j`nproc` -C .build/oxide install + +.build/oxide: + mkdir -p .build/oxide + +.build/oxide/Makefile: .build/oxide + cd .build/oxide && qmake -r $(DEFINES) ../../oxide.pro + .build/sentry/libsentry.so: - cd shared/sentry && cmake -B ../../.build/sentry \ + cd shared/sentry && cmake -B ../../.build/sentry/src \ -DBUILD_SHARED_LIBS=ON \ -DSENTRY_INTEGRATION_QT=ON \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ @@ -123,9 +78,9 @@ sentry: .build/sentry/libsentry.so -DSENTRY_BREAKPAD_SYSTEM=OFF \ -DSENTRY_EXPORT_SYMBOLS=ON \ -DSENTRY_PTHREAD=ON - cd shared/sentry && cmake --build ../../.build/sentry --parallel - cd shared/sentry && cmake --install ../../.build/sentry --prefix ../../.build/sentry --config RelWithDebInfo + cd shared/sentry && cmake --build ../../.build/sentry/src --parallel + cd shared/sentry && cmake --install ../../.build/sentry/src --prefix ../../.build/sentry --config RelWithDebInfo release/opt/lib/libsentry.so: sentry mkdir -p release/opt/lib - cp .build/sentry/libsentry.so release/opt/lib/ + cp -a .build/sentry/lib/libsentry.so release/opt/lib/ diff --git a/README.md b/README.md index 76cc70554..c4c428e05 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,17 @@ Here is an outdated video of it in action: ## Building -Install the reMarkable toolchain and then run `make release`. It will produce a folder named `release` containing all the output. +### Binaries + + 1. Install the [reMarkable toolchain](https://remarkablewiki.com/devel/toolchain) + 2. Run `make release` or `make FEATURES=sentry release` + 3. The built files can be found in the `release/` folder + +### Package files + + 1. Install [toltecmk](https://pypi.org/project/toltecmk/) + 2. Run `make package` + 3. The ipk files can be found in the `dist/` folder ### Nix Works on x86_64-linux or macOS via [nix-docker](https://github.com/LnL7/nix-docker). diff --git a/applications/applications.pro b/applications/applications.pro new file mode 100644 index 000000000..1a6bb2729 --- /dev/null +++ b/applications/applications.pro @@ -0,0 +1,22 @@ +TEMPLATE = subdirs + +SUBDIRS = \ + launcher \ + lockscreen \ + notify-send \ + process-manager \ + screenshot-tool \ + screenshot-viewer \ + settings-manager \ + system-service \ + task-switcher + +launcher.depends = system-service +lockscreen.depends = system-service +notify-send.depends = system-service +process-manager.depends = +screenshot-tool.depends = system-service +screenshot-viewer.depends = system-service +settings-manager.depends = system-service +system-service.depends = +task-switcher.depends = system-service diff --git a/applications/launcher/oxide.pro b/applications/launcher/launcher.pro similarity index 58% rename from applications/launcher/oxide.pro rename to applications/launcher/launcher.pro index bcd2aa354..bc375142a 100644 --- a/applications/launcher/oxide.pro +++ b/applications/launcher/launcher.pro @@ -1,8 +1,11 @@ QT += gui QT += quick QT += dbus + CONFIG += c++11 CONFIG += qml_debug +CONFIG += qtquickcompiler +CONFIG += precompile_header DEFINES += QT_DEPRECATED_WARNINGS DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 @@ -15,11 +18,10 @@ SOURCES += \ RESOURCES += qml.qrc -# Default rules for deployment. +TARGET = oxide +include(../../qmake/common.pri) target.path = /opt/bin -!isEmpty(target.path): INSTALLS += target - -INCLUDEPATH += ../../shared +INSTALLS += target DBUS_INTERFACES += ../../interfaces/dbusservice.xml DBUS_INTERFACES += ../../interfaces/powerapi.xml @@ -49,32 +51,12 @@ HEADERS += \ appitem.h \ mxcfb.h \ notificationlist.h \ + oxide_stable.h \ wifinetworklist.h +PRECOMPILED_HEADER = \ + oxide_stable.h -LIBS += -L$$PWD/../../shared/ -lqsgepaper -INCLUDEPATH += $$PWD/../../shared -DEPENDPATH += $$PWD/../../shared - -contains(DEFINES, SENTRY){ - exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library - }else{ - error(You need to build sentry first) - } -} - -LIBS += -L$$PWD/../../.build/liboxide -lliboxide -INCLUDEPATH += $$PWD/../../shared/liboxide -DEPENDPATH += $$PWD/../../shared/liboxide - -QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib - -VERSION = 2.5 -DEFINES += APP_VERSION=\\\"$$VERSION\\\" +include(../../qmake/epaper.pri) +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/launcher/main.cpp b/applications/launcher/main.cpp index c60679912..8fb9de2ba 100644 --- a/applications/launcher/main.cpp +++ b/applications/launcher/main.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include "controller.h" diff --git a/applications/launcher/oxide_stable.h b/applications/launcher/oxide_stable.h new file mode 100644 index 000000000..01f9cf1b4 --- /dev/null +++ b/applications/launcher/oxide_stable.h @@ -0,0 +1,50 @@ +#if defined __cplusplus +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "application_interface.h" +#include "appsapi_interface.h" +#include "bss_interface.h" +#include "dbusservice_interface.h" +#include "network_interface.h" +#include "notification_interface.h" +#include "notificationapi_interface.h" +#include "powerapi_interface.h" +#include "systemapi_interface.h" +#include "wifiapi_interface.h" + +#include "controller.h" +#include "mxcfb.h" +#endif diff --git a/applications/lockscreen/decay.pro b/applications/lockscreen/decay.pro deleted file mode 100644 index b8cb9f5ec..000000000 --- a/applications/lockscreen/decay.pro +++ /dev/null @@ -1,57 +0,0 @@ -QT += gui -QT += quick -QT += dbus - -CONFIG += c++11 -CONFIG += qml_debug - -DEFINES += QT_DEPRECATED_WARNINGS -DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 - -SOURCES += \ - main.cpp - -# Default rules for deployment. -target.path = /opt/bin -!isEmpty(target.path): INSTALLS += target - -DBUS_INTERFACES += ../../interfaces/dbusservice.xml -DBUS_INTERFACES += ../../interfaces/systemapi.xml -DBUS_INTERFACES += ../../interfaces/powerapi.xml -DBUS_INTERFACES += ../../interfaces/wifiapi.xml -DBUS_INTERFACES += ../../interfaces/appsapi.xml -DBUS_INTERFACES += ../../interfaces/application.xml - -INCLUDEPATH += ../../shared -HEADERS += \ - controller.h - -RESOURCES += \ - qml.qrc - -LIBS += -L$$PWD/../../shared/ -lqsgepaper -INCLUDEPATH += $$PWD/../../shared -DEPENDPATH += $$PWD/../../shared - -contains(DEFINES, SENTRY){ - exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library - }else{ - error(You need to build sentry first) - } -} - -LIBS += -L$$PWD/../../.build/liboxide -lliboxide -INCLUDEPATH += $$PWD/../../shared/liboxide -DEPENDPATH += $$PWD/../../shared/liboxide - -QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib - -VERSION = 2.5 -DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/lockscreen/lockscreen.pro b/applications/lockscreen/lockscreen.pro new file mode 100644 index 000000000..9b0b949ac --- /dev/null +++ b/applications/lockscreen/lockscreen.pro @@ -0,0 +1,35 @@ +QT += gui +QT += quick +QT += dbus + +CONFIG += c++11 +CONFIG += qml_debug +CONFIG += qtquickcompiler + +DEFINES += QT_DEPRECATED_WARNINGS +DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp + +TARGET = decay +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +DBUS_INTERFACES += ../../interfaces/dbusservice.xml +DBUS_INTERFACES += ../../interfaces/systemapi.xml +DBUS_INTERFACES += ../../interfaces/powerapi.xml +DBUS_INTERFACES += ../../interfaces/wifiapi.xml +DBUS_INTERFACES += ../../interfaces/appsapi.xml +DBUS_INTERFACES += ../../interfaces/application.xml + +HEADERS += \ + controller.h + +RESOURCES += \ + qml.qrc + +include(../../qmake/epaper.pri) +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/notify-send/notify-send.pro b/applications/notify-send/notify-send.pro index 65e961be2..c847059c1 100644 --- a/applications/notify-send/notify-send.pro +++ b/applications/notify-send/notify-send.pro @@ -10,37 +10,16 @@ DEFINES += QT_DEPRECATED_WARNINGS SOURCES += \ main.cpp -INCLUDEPATH += ../../shared +HEADERS += DBUS_INTERFACES += ../../interfaces/dbusservice.xml DBUS_INTERFACES += ../../interfaces/notificationapi.xml DBUS_INTERFACES += ../../interfaces/notification.xml -# Default rules for deployment. +TARGET = notify-send +include(../../qmake/common.pri) target.path = /opt/bin -!isEmpty(target.path): INSTALLS += target - -contains(DEFINES, SENTRY){ - exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library - }else{ - error(You need to build sentry first) - } -} - -LIBS += -L$$PWD/../../.build/liboxide -lliboxide -INCLUDEPATH += $$PWD/../../shared/liboxide -DEPENDPATH += $$PWD/../../shared/liboxide - -QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib - -HEADERS += +INSTALLS += target -VERSION = 2.5 -DEFINES += APP_VERSION=\\\"$$VERSION\\\" +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/process-manager/erode.pro b/applications/process-manager/erode.pro deleted file mode 100755 index ead79d1d1..000000000 --- a/applications/process-manager/erode.pro +++ /dev/null @@ -1,58 +0,0 @@ -QT += quick -QT += dbus -CONFIG += c++11 - -DEFINES += QT_DEPRECATED_WARNINGS -DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 - -SOURCES += main.cpp - -RESOURCES += qml.qrc - -# Default rules for deployment. -target.path = /opt/bin -!isEmpty(target.path): INSTALLS += target - -icons.files += \ - ../../assets/etc/draft/icons/erode.svg \ - ../../assets/etc/draft/icons/erode-splash.png -icons.path = /opt/etc/draft/icons -INSTALLS += icons - -HEADERS += \ - controller.h \ - taskitem.h \ - tasklist.h - -INCLUDEPATH += $$PWD/../../shared -INCLUDEPATH += ../../shared -DEPENDPATH += $$PWD/../../shared - -LIBS += -lsystemd - -LIBS += -L$$PWD/../../shared/ -lqsgepaper -INCLUDEPATH += $$PWD/../../shared -DEPENDPATH += $$PWD/../../shared - -contains(DEFINES, SENTRY){ - exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library - }else{ - error(You need to build sentry first) - } -} - -LIBS += -L$$PWD/../../.build/liboxide -lliboxide -INCLUDEPATH += $$PWD/../../shared/liboxide -DEPENDPATH += $$PWD/../../shared/liboxide - -QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib - -VERSION = 2.5 -DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/process-manager/erode_stable.h b/applications/process-manager/erode_stable.h new file mode 100644 index 000000000..1d011d70c --- /dev/null +++ b/applications/process-manager/erode_stable.h @@ -0,0 +1,25 @@ +#if defined __cplusplus +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "controller.h" +#endif diff --git a/applications/process-manager/process-manager.pro b/applications/process-manager/process-manager.pro new file mode 100755 index 000000000..76cf47d60 --- /dev/null +++ b/applications/process-manager/process-manager.pro @@ -0,0 +1,39 @@ +QT += quick +QT += dbus + +CONFIG += c++11 +CONFIG += qtquickcompiler +CONFIG += precompile_header + +DEFINES += QT_DEPRECATED_WARNINGS +DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += main.cpp + +RESOURCES += qml.qrc + +TARGET = erode +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +icons.files += \ + ../../assets/etc/draft/icons/erode.svg \ + ../../assets/etc/draft/icons/erode-splash.png +icons.path = /opt/etc/draft/icons + +INSTALLS += icons + +HEADERS += \ + controller.h \ + taskitem.h \ + tasklist.h + +PRECOMPILED_HEADER = \ + erode_stable.h + +LIBS += -lsystemd + +include(../../qmake/epaper.pri) +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/screenshot-tool/fret.pro b/applications/screenshot-tool/fret.pro deleted file mode 100644 index af1f94973..000000000 --- a/applications/screenshot-tool/fret.pro +++ /dev/null @@ -1,51 +0,0 @@ -QT -= gui -QT += dbus - -CONFIG += c++11 console -CONFIG -= app_bundle - -DEFINES += QT_DEPRECATED_WARNINGS -DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 - -SOURCES += \ - main.cpp - -# Default rules for deployment. -target.path = /opt/bin -!isEmpty(target.path): INSTALLS += target - -DBUS_INTERFACES += ../../interfaces/dbusservice.xml -DBUS_INTERFACES += ../../interfaces/systemapi.xml -DBUS_INTERFACES += ../../interfaces/screenapi.xml -DBUS_INTERFACES += ../../interfaces/screenshot.xml -DBUS_INTERFACES += ../../interfaces/notificationapi.xml -DBUS_INTERFACES += ../../interfaces/notification.xml - -INCLUDEPATH += ../../shared - -LIBS += -L$$PWD/../../shared/ -lqsgepaper -INCLUDEPATH += $$PWD/../../shared -DEPENDPATH += $$PWD/../../shared - -contains(DEFINES, SENTRY){ - exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library - }else{ - error(You need to build sentry first) - } -} - -LIBS += -L$$PWD/../../.build/liboxide -lliboxide -INCLUDEPATH += $$PWD/../../shared/liboxide -DEPENDPATH += $$PWD/../../shared/liboxide - -QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib - -VERSION = 2.5 -DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/screenshot-tool/screenshot-tool.pro b/applications/screenshot-tool/screenshot-tool.pro new file mode 100644 index 000000000..b9d7b5a3f --- /dev/null +++ b/applications/screenshot-tool/screenshot-tool.pro @@ -0,0 +1,27 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +DEFINES += QT_DEPRECATED_WARNINGS +DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp + +TARGET = fret +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +DBUS_INTERFACES += ../../interfaces/dbusservice.xml +DBUS_INTERFACES += ../../interfaces/systemapi.xml +DBUS_INTERFACES += ../../interfaces/screenapi.xml +DBUS_INTERFACES += ../../interfaces/screenshot.xml +DBUS_INTERFACES += ../../interfaces/notificationapi.xml +DBUS_INTERFACES += ../../interfaces/notification.xml + +include(../../qmake/epaper.pri) +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/screenshot-viewer/anxiety.pro b/applications/screenshot-viewer/anxiety.pro deleted file mode 100644 index 9565b0e1d..000000000 --- a/applications/screenshot-viewer/anxiety.pro +++ /dev/null @@ -1,63 +0,0 @@ -QT += gui -QT += quick -QT += dbus - -CONFIG += c++11 -CONFIG += qml_debug - -DEFINES += QT_DEPRECATED_WARNINGS -DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 - -SOURCES += \ - main.cpp - -# Default rules for deployment. -target.path = /opt/bin -!isEmpty(target.path): INSTALLS += target - -icons.files += \ - ../../assets/etc/draft/icons/image.svg \ - ../../assets/etc/draft/icons/anxiety-splash.png - -icons.path = /opt/etc/draft/icons -INSTALLS += icons - -DBUS_INTERFACES += ../../interfaces/dbusservice.xml -DBUS_INTERFACES += ../../interfaces/screenapi.xml -DBUS_INTERFACES += ../../interfaces/screenshot.xml - -INCLUDEPATH += ../../shared -HEADERS += \ - ../../shared/epframebuffer.h \ - controller.h \ - screenshotlist.h - -RESOURCES += \ - qml.qrc - -LIBS += -L$$PWD/../../shared/ -lqsgepaper -INCLUDEPATH += $$PWD/../../shared -DEPENDPATH += $$PWD/../../shared - -contains(DEFINES, SENTRY){ - exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library - }else{ - error(You need to build sentry first) - } -} - -LIBS += -L$$PWD/../../.build/liboxide -lliboxide -INCLUDEPATH += $$PWD/../../shared/liboxide -DEPENDPATH += $$PWD/../../shared/liboxide - -QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib - -VERSION = 2.5 -DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/screenshot-viewer/anxiety_stable.h b/applications/screenshot-viewer/anxiety_stable.h new file mode 100644 index 000000000..50019ece3 --- /dev/null +++ b/applications/screenshot-viewer/anxiety_stable.h @@ -0,0 +1,13 @@ +#if defined __cplusplus +#include +#include +#include +#include +#include +#include +#include +#include + +#include "controller.h" +#include "screenshotlist.h" +#endif diff --git a/applications/screenshot-viewer/main.cpp b/applications/screenshot-viewer/main.cpp index bb8d824af..74abd5934 100644 --- a/applications/screenshot-viewer/main.cpp +++ b/applications/screenshot-viewer/main.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include "controller.h" diff --git a/applications/screenshot-viewer/screenshot-viewer.pro b/applications/screenshot-viewer/screenshot-viewer.pro new file mode 100644 index 000000000..d2e156ee7 --- /dev/null +++ b/applications/screenshot-viewer/screenshot-viewer.pro @@ -0,0 +1,44 @@ +QT += gui +QT += quick +QT += dbus + +CONFIG += c++11 +CONFIG += qml_debug +CONFIG += qtquickcompiler +CONFIG += precompile_header + +DEFINES += QT_DEPRECATED_WARNINGS +DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp + +TARGET = anxiety +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +icons.files += \ + ../../assets/etc/draft/icons/image.svg \ + ../../assets/etc/draft/icons/anxiety-splash.png + +icons.path = /opt/etc/draft/icons +INSTALLS += icons + +DBUS_INTERFACES += ../../interfaces/dbusservice.xml +DBUS_INTERFACES += ../../interfaces/screenapi.xml +DBUS_INTERFACES += ../../interfaces/screenshot.xml + +HEADERS += \ + controller.h \ + screenshotlist.h + +RESOURCES += \ + qml.qrc + +PRECOMPILED_HEADER = \ + anxiety_stable.h + +include(../../qmake/epaper.pri) +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/settings-manager/rot.pro b/applications/settings-manager/settings-manager.pro similarity index 52% rename from applications/settings-manager/rot.pro rename to applications/settings-manager/settings-manager.pro index 6c6097b4d..75a80afe7 100644 --- a/applications/settings-manager/rot.pro +++ b/applications/settings-manager/settings-manager.pro @@ -10,7 +10,7 @@ DEFINES += QT_DEPRECATED_WARNINGS SOURCES += \ main.cpp -INCLUDEPATH += ../../shared +HEADERS += DBUS_INTERFACES += ../../interfaces/dbusservice.xml DBUS_INTERFACES += ../../interfaces/powerapi.xml @@ -25,31 +25,10 @@ DBUS_INTERFACES += ../../interfaces/screenshot.xml DBUS_INTERFACES += ../../interfaces/notificationapi.xml DBUS_INTERFACES += ../../interfaces/notification.xml -# Default rules for deployment. +TARGET = rot +include(../../qmake/common.pri) target.path = /opt/bin -!isEmpty(target.path): INSTALLS += target - -contains(DEFINES, SENTRY){ - exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library - }else{ - error(You need to build sentry first) - } -} - -LIBS += -L$$PWD/../../.build/liboxide -lliboxide -INCLUDEPATH += $$PWD/../../shared/liboxide -DEPENDPATH += $$PWD/../../shared/liboxide - -QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib - -HEADERS += +INSTALLS += target -VERSION = 2.5 -DEFINES += APP_VERSION=\\\"$$VERSION\\\" +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/system-service/apibase.h b/applications/system-service/apibase.h index 03df40b54..b29b8a385 100644 --- a/applications/system-service/apibase.h +++ b/applications/system-service/apibase.h @@ -7,7 +7,7 @@ #include #include -#include "../../shared/liboxide/liboxide.h" +#include #include diff --git a/applications/system-service/application.h b/applications/system-service/application.h index 8f2a79b42..188270542 100644 --- a/applications/system-service/application.h +++ b/applications/system-service/application.h @@ -38,6 +38,9 @@ #include "fifohandler.h" #include "buttonhandler.h" +// Must be included so that generate_xml.sh will work +#include "../../shared/liboxide/meta.h" + #define DEFAULT_PATH "/opt/bin:/opt/sbin:/opt/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin" class SandBoxProcess : public QProcess{ diff --git a/applications/system-service/bss.h b/applications/system-service/bss.h index e1e88ce37..9888c082e 100644 --- a/applications/system-service/bss.h +++ b/applications/system-service/bss.h @@ -4,10 +4,14 @@ #include #include -#include "../../shared/liboxide/liboxide.h" +#include + #include "supplicant.h" #include "network.h" +// Must be included so that generate_xml.sh will work +#include "../../shared/liboxide/meta.h" + class BSS : public QObject{ Q_OBJECT Q_CLASSINFO("Version", OXIDE_INTERFACE_VERSION) diff --git a/applications/system-service/buttonhandler.cpp b/applications/system-service/buttonhandler.cpp index ee58e4d50..da174b856 100644 --- a/applications/system-service/buttonhandler.cpp +++ b/applications/system-service/buttonhandler.cpp @@ -1,6 +1,8 @@ #include "buttonhandler.h" + +#include + #include "dbusservice.h" -#include "liboxide.h" void button_exit_handler(){ // Release lock diff --git a/applications/system-service/dbusservice.h b/applications/system-service/dbusservice.h index dab57847e..4ae7a5254 100644 --- a/applications/system-service/dbusservice.h +++ b/applications/system-service/dbusservice.h @@ -10,8 +10,8 @@ #include #include #include +#include -#include "../../shared/liboxide/liboxide.h" #include "powerapi.h" #include "wifiapi.h" #include "appsapi.h" @@ -21,6 +21,9 @@ #include "buttonhandler.h" #include "digitizerhandler.h" +// Must be included so that generate_xml.sh will work +#include "../../shared/liboxide/meta.h" + #define dbusService DBusService::singleton() using namespace std; diff --git a/applications/system-service/generate_xml.sh b/applications/system-service/generate_xml.sh index be7e63609..deb516166 100644 --- a/applications/system-service/generate_xml.sh +++ b/applications/system-service/generate_xml.sh @@ -1,12 +1,17 @@ #!/bin/sh cd "$(dirname "$0")" mkdir -p ../../interfaces -qdbuscpp2xml -A dbusservice.h -o ../../interfaces/dbusservice.xml -qdbuscpp2xml -A network.h -o ../../interfaces/network.xml -qdbuscpp2xml -A bss.h -o ../../interfaces/bss.xml -qdbuscpp2xml -A application.h -o ../../interfaces/application.xml -qdbuscpp2xml -A screenshot.h -o ../../interfaces/screenshot.xml -qdbuscpp2xml -A notification.h -o ../../interfaces/notification.xml +p(){ + echo "qdbuscpp2xml $1" + qdbuscpp2xml -A "$1" -o ../../interfaces/"$(basename "$1" .h)".xml +} + +p dbusservice.h +p network.h +p bss.h +p application.h +p screenshot.h +p notification.h \ls ./*api.h | while read file; do - qdbuscpp2xml -A "$file" -o ../../interfaces/"$(basename "$file" .h)".xml + p "$file" done diff --git a/applications/system-service/network.h b/applications/system-service/network.h index 7d0075bbc..55c442b5c 100644 --- a/applications/system-service/network.h +++ b/applications/system-service/network.h @@ -5,9 +5,13 @@ #include #include -#include "../../shared/liboxide/liboxide.h" +#include + #include "supplicant.h" +// Must be included so that generate_xml.sh will work +#include "../../shared/liboxide/meta.h" + class Network : public QObject { Q_OBJECT Q_CLASSINFO("Version", OXIDE_INTERFACE_VERSION) diff --git a/applications/system-service/notification.h b/applications/system-service/notification.h index 48b6dd00c..a4aa9d5e3 100644 --- a/applications/system-service/notification.h +++ b/applications/system-service/notification.h @@ -5,9 +5,13 @@ #include #include -#include "../../shared/liboxide/liboxide.h" +#include + #include "application.h" +// Must be included so that generate_xml.sh will work +#include "../../shared/liboxide/meta.h" + class Notification : public QObject{ Q_OBJECT Q_CLASSINFO("Version", OXIDE_INTERFACE_VERSION) diff --git a/applications/system-service/screenshot.h b/applications/system-service/screenshot.h index 4e47b3aac..a855911ab 100644 --- a/applications/system-service/screenshot.h +++ b/applications/system-service/screenshot.h @@ -6,8 +6,10 @@ #include #include +#include -#include "../../shared/liboxide/liboxide.h" +// Must be included so that generate_xml.sh will work +#include "../../shared/liboxide/meta.h" class Screenshot : public QObject{ Q_OBJECT diff --git a/applications/system-service/tarnish.pro b/applications/system-service/system-service.pro similarity index 67% rename from applications/system-service/tarnish.pro rename to applications/system-service/system-service.pro index c49db7e42..5c4c6722c 100644 --- a/applications/system-service/tarnish.pro +++ b/applications/system-service/system-service.pro @@ -3,7 +3,7 @@ QT += dbus CONFIG += c++17 CONFIG += console CONFIG -= app_bundle -CONFIG += precompile_header_c +CONFIG += precompile_header DEFINES += QT_DEPRECATED_WARNINGS DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 @@ -26,8 +26,8 @@ SOURCES += \ wpa_supplicant.cpp \ main.cpp -TARGET=tarnish - +TARGET = tarnish +include(../../qmake/common.pri) target.path = /opt/bin INSTALLS += target @@ -48,7 +48,6 @@ system(qdbusxml2cpp -N -p wpa_supplicant.h:wpa_supplicant.cpp fi.w1.wpa_supplica DBUS_INTERFACES += org.freedesktop.login1.xml HEADERS += \ - ../../shared/liboxide/liboxide.h \ apibase.h \ application.h \ appsapi.h \ @@ -67,19 +66,18 @@ HEADERS += \ screenshot.h \ supplicant.h \ systemapi.h \ + tarnish_stable.h \ wifiapi.h \ wlan.h \ wpa_supplicant.h +PRECOMPILED_HEADER = \ + tarnish_stable.h + LIBS += -lpng16 LIBS += -lsystemd LIBS += -lz -LIBS += -L$$PWD/../../shared/ -lqsgepaper -INCLUDEPATH += $$PWD/../../shared -DEPENDPATH += $$PWD/../../shared -QMAKE_POST_LINK += sh $$_PRO_FILE_PWD_/generate_xml.sh - DISTFILES += \ ../../assets/opt/usr/share/applications/codes.eeems.anxiety.oxide \ ../../assets/opt/usr/share/applications/codes.eeems.corrupt.oxide \ @@ -87,25 +85,8 @@ DISTFILES += \ generate_xml.sh \ org.freedesktop.login1.xml -contains(DEFINES, SENTRY){ - exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib +include(../../qmake/epaper.pri) +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library - }else{ - error(You need to build sentry first) - } -} - -LIBS += -L$$PWD/../../.build/liboxide -lliboxide -INCLUDEPATH += $$PWD/../../shared/liboxide -DEPENDPATH += $$PWD/../../shared/liboxide - -QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib - -VERSION = 2.5 -DEFINES += APP_VERSION=\\\"$$VERSION\\\" +QMAKE_POST_LINK += sh $$_PRO_FILE_PWD_/generate_xml.sh diff --git a/applications/system-service/tarnish_stable.h b/applications/system-service/tarnish_stable.h new file mode 100644 index 000000000..872a6bc80 --- /dev/null +++ b/applications/system-service/tarnish_stable.h @@ -0,0 +1,76 @@ +#if defined __cplusplus +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "apibase.h" +#include "mxcfb.h" +#include "supplicant.h" +#endif diff --git a/applications/system-service/wlan.h b/applications/system-service/wlan.h index fcc9bb206..049480b4a 100644 --- a/applications/system-service/wlan.h +++ b/applications/system-service/wlan.h @@ -4,11 +4,12 @@ #include #include -#include -#include "sysobject.h" #include "supplicant.h" +// Must be included so that generate_xml.sh will work +#include "../../shared/liboxide/sysobject.h" + using Oxide::SysObject; class Wlan : public QObject, public SysObject { diff --git a/applications/task-switcher/corrupt.pro b/applications/task-switcher/corrupt.pro deleted file mode 100644 index e53552e23..000000000 --- a/applications/task-switcher/corrupt.pro +++ /dev/null @@ -1,58 +0,0 @@ -QT += gui -QT += quick -QT += dbus - -CONFIG += c++11 -CONFIG += qml_debug - -DEFINES += QT_DEPRECATED_WARNINGS -DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 - -SOURCES += \ - appitem.cpp \ - main.cpp - -# Default rules for deployment. -target.path = /opt/bin -!isEmpty(target.path): INSTALLS += target - -DBUS_INTERFACES += ../../interfaces/dbusservice.xml -DBUS_INTERFACES += ../../interfaces/screenapi.xml -DBUS_INTERFACES += ../../interfaces/appsapi.xml -DBUS_INTERFACES += ../../interfaces/application.xml - -INCLUDEPATH += ../../shared -HEADERS += \ - appitem.h \ - controller.h \ - screenprovider.h - -RESOURCES += \ - qml.qrc - -LIBS += -L$$PWD/../../shared/ -lqsgepaper -INCLUDEPATH += $$PWD/../../shared -DEPENDPATH += $$PWD/../../shared - -contains(DEFINES, SENTRY){ - exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library - }else{ - error(You need to build sentry first) - } -} - -LIBS += -L$$PWD/../../.build/liboxide -lliboxide -INCLUDEPATH += $$PWD/../../shared/liboxide -DEPENDPATH += $$PWD/../../shared/liboxide - -QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib - -VERSION = 2.5 -DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/applications/task-switcher/corrupt_stable.h b/applications/task-switcher/corrupt_stable.h new file mode 100644 index 000000000..db7eb5285 --- /dev/null +++ b/applications/task-switcher/corrupt_stable.h @@ -0,0 +1,30 @@ +#if defined __cplusplus +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "application_interface.h" +#include "appsapi_interface.h" +#include "dbusservice_interface.h" +#include "screenapi_interface.h" + +#include "controller.h" +#include "screenprovider.h" +#endif diff --git a/applications/task-switcher/task-switcher.pro b/applications/task-switcher/task-switcher.pro new file mode 100644 index 000000000..5eb76cd26 --- /dev/null +++ b/applications/task-switcher/task-switcher.pro @@ -0,0 +1,41 @@ +QT += gui +QT += quick +QT += dbus + +CONFIG += c++11 +CONFIG += qml_debug +CONFIG += qtquickcompiler +CONFIG += precompile_header + +DEFINES += QT_DEPRECATED_WARNINGS +DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + appitem.cpp \ + main.cpp + +TARGET = corrupt +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +DBUS_INTERFACES += ../../interfaces/dbusservice.xml +DBUS_INTERFACES += ../../interfaces/screenapi.xml +DBUS_INTERFACES += ../../interfaces/appsapi.xml +DBUS_INTERFACES += ../../interfaces/application.xml + +INCLUDEPATH += ../../shared +HEADERS += \ + appitem.h \ + controller.h \ + screenprovider.h + +RESOURCES += \ + qml.qrc + +PRECOMPILED_HEADER = \ + corrupt_stable.h + +include(../../qmake/epaper.pri) +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/oxide.nix b/oxide.nix index 8d0614453..7e92756b5 100644 --- a/oxide.nix +++ b/oxide.nix @@ -11,6 +11,8 @@ stdenv.mkDerivation { ./assets ./interfaces ./shared + ./oxide.pro + ./qmake ]; preBuild = '' diff --git a/oxide.pro b/oxide.pro new file mode 100644 index 000000000..808b827b3 --- /dev/null +++ b/oxide.pro @@ -0,0 +1,7 @@ +TEMPLATE = subdirs + +SUBDIRS = \ + shared \ + applications + +applications.depends = shared diff --git a/package b/package index 9cb9ee04a..92fb28da8 100644 --- a/package +++ b/package @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT pkgnames=(erode fret oxide rot tarnish decay corrupt anxiety liboxide libsentry notify-send) -pkgver="2.5~~VERSION~" +pkgver="2.6~VERSION~" timestamp="$(date -u +%Y-%m-%dT%H:%MZ)" maintainer="Eeems " url=https://oxide.eeems.codes @@ -17,6 +17,8 @@ build() { find . -name "*.pro" -type f -print0 \ | xargs -r -0 sed -i 's/linux-oe-g++/linux-arm-remarkable-g++/g' CMAKE_TOOLCHAIN_FILE="/usr/share/cmake/$CHOST.cmake" make FEATURES=sentry release + # Cleanup build files + rm -r .build } erode() { diff --git a/qmake/common.pri b/qmake/common.pri new file mode 100644 index 000000000..c8b3ac4fa --- /dev/null +++ b/qmake/common.pri @@ -0,0 +1,3 @@ +QMAKE_RPATHDIR += /lib /usr/lib /opt/lib /opt/usr/lib +VERSION = 2.6 +DEFINES += APP_VERSION=\\\"$$VERSION\\\" diff --git a/qmake/epaper.pri b/qmake/epaper.pri new file mode 100644 index 000000000..f74112c42 --- /dev/null +++ b/qmake/epaper.pri @@ -0,0 +1,2 @@ +LIBS += -L$$PWD/../shared/epaper -lqsgepaper +INCLUDEPATH += $$PWD/../shared/epaper diff --git a/qmake/liboxide.pri b/qmake/liboxide.pri new file mode 100644 index 000000000..fbd942875 --- /dev/null +++ b/qmake/liboxide.pri @@ -0,0 +1,2 @@ +LIBS += -L$$OUT_PWD/../../shared/liboxide -lliboxide +INCLUDEPATH += $$OUT_PWD/../../shared/liboxide/include diff --git a/qmake/sentry.pri b/qmake/sentry.pri new file mode 100644 index 000000000..e107a4de0 --- /dev/null +++ b/qmake/sentry.pri @@ -0,0 +1,16 @@ +contains(DEFINES, SENTRY){ + LIBSENTRY_ROOT = $$PWD/../.build/sentry + LIBSENTRY_LIB = $$LIBSENTRY_ROOT/lib + LIBSENTRY_SO = $$LIBSENTRY_LIB/libsentry.so + !exists($$LIBSENTRY_SO){ + error(Missing $$LIBSENTRY_SO) + } + LIBSENTRY_INC = $$LIBSENTRY_ROOT/include + LIBSENTRY_H = $$LIBSENTRY_INC/sentry.h + !exists($$LIBSENTRY_H){ + error(Missing $$LIBSENTRY_H) + } + LIBS_PRIVATE += -L$$LIBSENTRY_LIB -lsentry -ldl -lcurl -lbreakpad_client + INCLUDEPATH += $$LIBSENTRY_INC + DEPENDPATH += $$LIBSENTRY_LIB +} diff --git a/shared/dbussettings.h b/shared/dbussettings.h deleted file mode 120000 index bb714c76b..000000000 --- a/shared/dbussettings.h +++ /dev/null @@ -1 +0,0 @@ -../applications/system-service/dbussettings.h \ No newline at end of file diff --git a/shared/epframebuffer.h b/shared/epaper/epframebuffer.h similarity index 100% rename from shared/epframebuffer.h rename to shared/epaper/epframebuffer.h diff --git a/shared/libepaper.so b/shared/epaper/libepaper.so similarity index 100% rename from shared/libepaper.so rename to shared/epaper/libepaper.so diff --git a/shared/libqsgepaper.a b/shared/epaper/libqsgepaper.a similarity index 100% rename from shared/libqsgepaper.a rename to shared/epaper/libqsgepaper.a diff --git a/shared/liboxide/liboxide.h b/shared/liboxide/liboxide.h index 8439ab6da..b586b34b6 100644 --- a/shared/liboxide/liboxide.h +++ b/shared/liboxide/liboxide.h @@ -12,6 +12,7 @@ #include "json.h" #include "signalhandler.h" #include "slothandler.h" +#include "sysobject.h" #include "debug.h" #include "oxide_sentry.h" diff --git a/shared/liboxide/liboxide.pro b/shared/liboxide/liboxide.pro index f7e073178..2c2c96185 100644 --- a/shared/liboxide/liboxide.pro +++ b/shared/liboxide/liboxide.pro @@ -7,6 +7,7 @@ DEFINES += LIBOXIDE_LIBRARY CONFIG += c++11 CONFIG += warn_on +CONFIG += precompile_header DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 @@ -36,29 +37,31 @@ HEADERS += \ sysobject.h \ signalhandler.h +PRECOMPILED_HEADER = \ + liboxide_stable.h + LIBS += -lsystemd -INCLUDEPATH += ../../shared -LIBS += -L$$PWD/../../shared/ -lqsgepaper -INCLUDEPATH += $$PWD/../../shared -DEPENDPATH += $$PWD/../../shared +include.target = include/liboxide +include.commands = \ + mkdir -p include/liboxide && \ + echo $$HEADERS | xargs -rn1 | xargs -rI {} cp $$PWD/{} include/liboxide/ + +liboxide_h.target = include/liboxide.h +liboxide_h.depends += include +liboxide_h.commands = \ + echo \\$$LITERAL_HASH"ifndef LIBOXIDE" > include/liboxide.h && \ + echo \\$$LITERAL_HASH"define LIBOXIDE" >> include/liboxide.h && \ + echo \"$$LITERAL_HASH"include \\\"liboxide/liboxide.h\\\"\"" >> include/liboxide.h && \ + echo \\$$LITERAL_HASH"endif // LIBOXIDE" >> include/liboxide.h -contains(DEFINES, SENTRY){ - exists($$PWD/../../.build/sentry) { - LIBS += -L$$PWD/../../.build/sentry/lib -lsentry -ldl -lcurl -lbreakpad_client - INCLUDEPATH += $$PWD/../../.build/sentry/include - DEPENDPATH += $$PWD/../../.build/sentry/lib - library.files = ../../.build/sentry/libsentry.so - library.path = /opt/lib - INSTALLS += library - }else{ - error(You need to build sentry first) - } -} +QMAKE_EXTRA_TARGETS += include liboxide_h +POST_TARGETDEPS += include/liboxide.h +include(../../qmake/common.pri) target.path = /opt/usr/lib -!isEmpty(target.path): INSTALLS += target +INSTALLS += target -VERSION = 2.5 -DEFINES += APP_VERSION=\\\"$$VERSION\\\" +include(../../qmake/epaper.pri) +include(../../qmake/sentry.pri) diff --git a/shared/liboxide/liboxide_stable.h b/shared/liboxide/liboxide_stable.h new file mode 100644 index 000000000..551a2873b --- /dev/null +++ b/shared/liboxide/liboxide_stable.h @@ -0,0 +1,53 @@ +#if defined __cplusplus +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "liboxide_global.h" +#include "meta.h" +#endif diff --git a/shared/liboxide/oxide_sentry.h b/shared/liboxide/oxide_sentry.h index 3af7e010b..fbb4a01e7 100644 --- a/shared/liboxide/oxide_sentry.h +++ b/shared/liboxide/oxide_sentry.h @@ -13,7 +13,6 @@ #ifdef SENTRY #include -#include #endif /*! diff --git a/shared/liboxide/power.h b/shared/liboxide/power.h index 80ece4189..40ace4767 100644 --- a/shared/liboxide/power.h +++ b/shared/liboxide/power.h @@ -5,7 +5,6 @@ #define POWER_H #include "liboxide_global.h" -#include #include #include diff --git a/shared/liboxide/sysobject.cpp b/shared/liboxide/sysobject.cpp index 2a21067ee..8b52c59ec 100644 --- a/shared/liboxide/sysobject.cpp +++ b/shared/liboxide/sysobject.cpp @@ -1,12 +1,12 @@ #include "sysobject.h" +#include "debug.h" + #include #include #include #include #include -#include - namespace Oxide{ std::string SysObject::propertyPath(const std::string& name){ return m_path + "/" + name; diff --git a/shared/shared.pro b/shared/shared.pro new file mode 100644 index 000000000..86665818b --- /dev/null +++ b/shared/shared.pro @@ -0,0 +1,4 @@ +TEMPLATE = subdirs + +SUBDIRS = \ + liboxide From 237cfc59b643a856d57eb1086fb40373ab0e3880 Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Fri, 27 Jan 2023 22:30:48 -0700 Subject: [PATCH 12/15] Fix deploy in qt creator --- applications/applications.pro | 11 +++++++++++ oxide.pro | 4 ++++ shared/shared.pro | 2 ++ 3 files changed, 17 insertions(+) diff --git a/applications/applications.pro b/applications/applications.pro index 1a6bb2729..84f03aac6 100644 --- a/applications/applications.pro +++ b/applications/applications.pro @@ -20,3 +20,14 @@ screenshot-viewer.depends = system-service settings-manager.depends = system-service system-service.depends = task-switcher.depends = system-service + +INSTALLS += \ + launcher \ + lockscreen \ + notify-send \ + process-manager \ + screenshot-tool \ + screenshot-viewer \ + settings-manager \ + system-service \ + task-switcher diff --git a/oxide.pro b/oxide.pro index 808b827b3..491bd934d 100644 --- a/oxide.pro +++ b/oxide.pro @@ -5,3 +5,7 @@ SUBDIRS = \ applications applications.depends = shared + +INSTALLS += \ + shared \ + applications diff --git a/shared/shared.pro b/shared/shared.pro index 86665818b..66b8326b4 100644 --- a/shared/shared.pro +++ b/shared/shared.pro @@ -2,3 +2,5 @@ TEMPLATE = subdirs SUBDIRS = \ liboxide + +INSTALLS += liboxide From c52abd5c50582b4c1a42d65d8f76450f7def04f3 Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Thu, 9 Feb 2023 21:51:40 -0700 Subject: [PATCH 13/15] Add helper executables for interacting with the API (#290) * Add upate-desktop-database * Update package install * Fix description * Move dbus interfaces to liboxide, start adding desktop-file-validate * Get desktop-file-validate working * Log exception, add logging, update package * Lots of documentation changes * Fix generate_xml.sh being picky * Implment desktop-file-validate flags * Move validation to liboxide * Add xdg-desktop-menu * Add xdg-desktop-icon * Add xdg-open * Clean up xdg-open parsing * Implement gio cat * Implement gio help and gio version * Add gio open * Padd commands help * Implement gio launch * Implement gio copy and stub out other items * Make stubs show error * Implement gio mkdir * Implement gio remove * Implement gio rename * Formatting * Lazy umount from chroot and add locking to tarnish * Implment xdg-settings * Initial documentation * Implement xdg-icon-resource * Allow named icons * Use -j from initial command/environment * Implement desktop-file-edit * Update faq and desktop-file-validate * Move application registration files to the correct project * Add logo to main page * Add missing dep and make cache explicit * Updating caching mechanism * Update to new apt cache action * Add gestures image * Add image to usage * Implement desktop-file-install --- .github/actions/web/action.yaml | 30 +- .github/workflows/build.yml | 2 +- Makefile | 77 +-- applications/applications.pro | 34 +- applications/desktop-file-edit/.gitignore | 73 +++ .../desktop-file-edit/desktop-file-edit.pro | 23 + applications/desktop-file-edit/edit.cpp | 165 ++++++ applications/desktop-file-edit/edit.h | 12 + applications/desktop-file-edit/main.cpp | 78 +++ applications/desktop-file-install/.gitignore | 73 +++ .../desktop-file-install.pro | 23 + applications/desktop-file-install/main.cpp | 121 ++++ applications/desktop-file-validate/.gitignore | 73 +++ .../desktop-file-validate.pro | 21 + applications/desktop-file-validate/main.cpp | 65 +++ applications/gio/.gitignore | 73 +++ applications/gio/cat.h | 37 ++ applications/gio/common.cpp | 112 ++++ applications/gio/common.h | 82 +++ applications/gio/copy.h | 130 +++++ applications/gio/gio.pro | 32 ++ applications/gio/help.h | 30 + applications/gio/launch.h | 90 +++ applications/gio/main.cpp | 60 ++ applications/gio/mkdir.h | 59 ++ applications/gio/open.h | 28 + applications/gio/remove.h | 61 ++ applications/gio/rename.h | 48 ++ applications/gio/version.h | 18 + applications/launcher/appitem.cpp | 2 - applications/launcher/appitem.h | 8 +- applications/launcher/controller.cpp | 78 +-- applications/launcher/controller.h | 9 - applications/launcher/launcher.pro | 20 +- applications/launcher/notificationlist.h | 2 - applications/launcher/oxide_stable.h | 11 - applications/launcher/wifinetworklist.h | 9 +- applications/lockscreen/controller.h | 7 - applications/lockscreen/lockscreen.pro | 9 +- applications/lockscreen/main.cpp | 3 +- applications/notify-send/main.cpp | 4 - applications/notify-send/notify-send.pro | 4 - .../process-manager/process-manager.pro | 14 +- applications/process-manager/tasklist.h | 3 +- applications/screenshot-tool/main.cpp | 7 - .../screenshot-tool/screenshot-tool.pro | 9 +- applications/screenshot-viewer/controller.h | 4 - applications/screenshot-viewer/main.cpp | 2 - .../screenshot-viewer/screenshot-viewer.pro | 15 +- .../screenshot-viewer/screenshotlist.h | 3 +- applications/settings-manager/main.cpp | 16 - .../settings-manager/settings-manager.pro | 13 - applications/system-service/application.cpp | 60 +- applications/system-service/application.h | 77 +-- applications/system-service/appsapi.h | 103 +--- applications/system-service/main.cpp | 93 +++- .../system-service/system-service.pro | 10 +- applications/task-switcher/appitem.cpp | 2 - applications/task-switcher/appitem.h | 8 +- applications/task-switcher/controller.h | 6 - applications/task-switcher/corrupt_stable.h | 5 - applications/task-switcher/main.cpp | 2 - applications/task-switcher/task-switcher.pro | 7 +- .../update-desktop-database/.gitignore | 73 +++ applications/update-desktop-database/main.cpp | 157 ++++++ .../update-desktop-database.pro | 25 + applications/xdg-desktop-icon/.gitignore | 73 +++ applications/xdg-desktop-icon/main.cpp | 132 +++++ .../xdg-desktop-icon/xdg-desktop-icon.pro | 21 + applications/xdg-desktop-menu/.gitignore | 73 +++ applications/xdg-desktop-menu/main.cpp | 203 +++++++ .../xdg-desktop-menu/xdg-desktop-menu.pro | 21 + applications/xdg-icon-resource/.gitignore | 73 +++ applications/xdg-icon-resource/main.cpp | 203 +++++++ .../xdg-icon-resource/xdg-icon-resource.pro | 21 + applications/xdg-open/.gitignore | 73 +++ applications/xdg-open/main.cpp | 76 +++ applications/xdg-open/xdg-open.pro | 21 + applications/xdg-settings/.gitignore | 73 +++ applications/xdg-settings/main.cpp | 285 ++++++++++ applications/xdg-settings/xdg-settings.pro | 21 + assets/etc/draft/icons/erode.svg | 1 - assets/etc/draft/icons/image.svg | 1 - assets/etc/systemd/system/tarnish.service | 2 +- .../applications/codes.eeems.anxiety.oxide | 4 +- .../applications/codes.eeems.decay.oxide | 2 +- .../applications/codes.eeems.erode.oxide | 4 +- .../share/applications/codes.eeems.fret.oxide | 2 +- .../applications/codes.eeems.oxide.oxide | 2 +- .../opt/usr/share/applications/xochitl.oxide | 2 +- .../share/icons/oxide/48x48/apps/erode.png | Bin 0 -> 377 bytes .../share/icons/oxide/48x48/apps/image.png | Bin 0 -> 595 bytes .../share/icons/oxide/48x48/apps}/xochitl.png | Bin .../icons/oxide/702x702/splash/anxiety.png} | Bin .../icons/oxide/702x702/splash/erode.png} | Bin .../icons/oxide/702x702/splash/oxide.png} | Bin interfaces/application.xml | 1 + oxide.pro | 4 +- package | 81 ++- shared/liboxide/applications.cpp | 523 ++++++++++++++++++ shared/liboxide/applications.h | 212 +++++++ shared/liboxide/dbus.h | 26 + shared/liboxide/debug.h | 11 +- shared/liboxide/eventfilter.h | 11 +- shared/liboxide/examples/oxide.cpp | 20 +- shared/liboxide/json.cpp | 15 +- shared/liboxide/json.h | 22 +- shared/liboxide/liboxide.cpp | 107 ++++ shared/liboxide/liboxide.h | 86 ++- shared/liboxide/liboxide.pro | 42 +- shared/liboxide/liboxide_global.h | 11 +- shared/liboxide/liboxide_stable.h | 14 + shared/liboxide/meta.h | 10 +- shared/liboxide/oxide_sentry.h | 12 +- shared/liboxide/power.h | 11 +- shared/liboxide/settingsfile.h | 9 +- shared/liboxide/signalhandler.h | 10 +- shared/liboxide/slothandler.h | 10 +- shared/liboxide/sysobject.h | 13 +- shared/shared.pro | 2 +- web/Makefile | 64 ++- web/images/favicon.svg.tex | 23 + web/images/gestures.svg.tex | 99 ++++ web/images/logo.png.tex | 22 + web/images/remarkable.tex | 172 ++++++ web/src/_static/images/.gitignore | 2 + web/src/_themes/oxide/static/oxide.css | 2 +- web/src/documentation/01_usage.rst | 5 + web/src/documentation/02_oxide-utils.rst | 41 ++ ...=> 03_application_registration_format.rst} | 23 +- .../documentation/{03_api.rst => 04_api.rst} | 0 web/src/faq.rst | 12 +- web/src/index.rst | 5 + 133 files changed, 5127 insertions(+), 605 deletions(-) create mode 100644 applications/desktop-file-edit/.gitignore create mode 100644 applications/desktop-file-edit/desktop-file-edit.pro create mode 100644 applications/desktop-file-edit/edit.cpp create mode 100644 applications/desktop-file-edit/edit.h create mode 100644 applications/desktop-file-edit/main.cpp create mode 100644 applications/desktop-file-install/.gitignore create mode 100644 applications/desktop-file-install/desktop-file-install.pro create mode 100644 applications/desktop-file-install/main.cpp create mode 100644 applications/desktop-file-validate/.gitignore create mode 100644 applications/desktop-file-validate/desktop-file-validate.pro create mode 100644 applications/desktop-file-validate/main.cpp create mode 100644 applications/gio/.gitignore create mode 100644 applications/gio/cat.h create mode 100644 applications/gio/common.cpp create mode 100644 applications/gio/common.h create mode 100644 applications/gio/copy.h create mode 100644 applications/gio/gio.pro create mode 100644 applications/gio/help.h create mode 100644 applications/gio/launch.h create mode 100644 applications/gio/main.cpp create mode 100644 applications/gio/mkdir.h create mode 100644 applications/gio/open.h create mode 100644 applications/gio/remove.h create mode 100644 applications/gio/rename.h create mode 100644 applications/gio/version.h create mode 100644 applications/update-desktop-database/.gitignore create mode 100644 applications/update-desktop-database/main.cpp create mode 100644 applications/update-desktop-database/update-desktop-database.pro create mode 100644 applications/xdg-desktop-icon/.gitignore create mode 100644 applications/xdg-desktop-icon/main.cpp create mode 100644 applications/xdg-desktop-icon/xdg-desktop-icon.pro create mode 100644 applications/xdg-desktop-menu/.gitignore create mode 100644 applications/xdg-desktop-menu/main.cpp create mode 100644 applications/xdg-desktop-menu/xdg-desktop-menu.pro create mode 100644 applications/xdg-icon-resource/.gitignore create mode 100644 applications/xdg-icon-resource/main.cpp create mode 100644 applications/xdg-icon-resource/xdg-icon-resource.pro create mode 100644 applications/xdg-open/.gitignore create mode 100644 applications/xdg-open/main.cpp create mode 100644 applications/xdg-open/xdg-open.pro create mode 100644 applications/xdg-settings/.gitignore create mode 100644 applications/xdg-settings/main.cpp create mode 100644 applications/xdg-settings/xdg-settings.pro delete mode 100644 assets/etc/draft/icons/erode.svg delete mode 100644 assets/etc/draft/icons/image.svg create mode 100644 assets/opt/usr/share/icons/oxide/48x48/apps/erode.png create mode 100644 assets/opt/usr/share/icons/oxide/48x48/apps/image.png rename assets/{etc/draft/icons => opt/usr/share/icons/oxide/48x48/apps}/xochitl.png (100%) rename assets/{etc/draft/icons/anxiety-splash.png => opt/usr/share/icons/oxide/702x702/splash/anxiety.png} (100%) rename assets/{etc/draft/icons/erode-splash.png => opt/usr/share/icons/oxide/702x702/splash/erode.png} (100%) rename assets/{etc/draft/icons/oxide-splash.png => opt/usr/share/icons/oxide/702x702/splash/oxide.png} (100%) create mode 100644 shared/liboxide/applications.cpp create mode 100644 shared/liboxide/applications.h create mode 100644 shared/liboxide/dbus.h create mode 100644 web/images/favicon.svg.tex create mode 100644 web/images/gestures.svg.tex create mode 100644 web/images/logo.png.tex create mode 100644 web/images/remarkable.tex create mode 100644 web/src/_static/images/.gitignore create mode 100644 web/src/documentation/02_oxide-utils.rst rename web/src/documentation/{02_application_registration_format.rst => 03_application_registration_format.rst} (94%) rename web/src/documentation/{03_api.rst => 04_api.rst} (100%) diff --git a/.github/actions/web/action.yaml b/.github/actions/web/action.yaml index 40f7e8da7..03f226ea9 100644 --- a/.github/actions/web/action.yaml +++ b/.github/actions/web/action.yaml @@ -13,29 +13,17 @@ runs: with: path: web/.venv key: web/.venv-${{ hashFiles('web/requirements.txt') }} - - name: Check for Apt updates - shell: bash - run: | - sudo apt-get update -yq - echo "webAptVersion=doxygen-$(apt-cache policy doxygen | grep -oP '(?<=Candidate:\s)(.+)')-graphviz-$(apt-cache policy graphviz | grep -oP '(?<=Candidate:\s)(.+)')-libgraphviz-dev-$(apt-cache policy libgraphviz-dev | grep -oP '(?<=Candidate:\s)(.+)')" >> $GITHUB_ENV - - name: Cache Apt packages - uses: actions/cache@v3 + - name: Install base Apt packages id: cache-apt + uses: awalsh128/cache-apt-pkgs-action@latest with: - path: ~/.aptcache - key: ${{ env.webAptVersion }} - - name: Install or restore Apt packages - shell: bash - env: - CACHE_HIT: ${{ steps.cache-apt.outputs.cache-hit }} - run: | - if [[ "$CACHE_HIT" != 'true' ]]; then - sudo apt-get install -yq doxygen graphviz libgraphviz-dev - mkdir -p ~/.aptcache - sudo dpkg -L doxygen graphviz libgraphviz-dev | while IFS= read -r f; do if test -f $f; then echo $f; fi; done | xargs cp --parents --target-directory ~/.aptcache/ - else - sudo cp --verbose --force --recursive ~/.aptcache/* / - fi + execute_install_scripts: true + packages: doxygen graphviz libgraphviz-dev librsvg2-bin pdf2svg + version: 1.0 + - name: Apt-Cache-Action + uses: Eeems-Org/apt-cache-action@v1 + with: + packages: texlive-base texlive-latex-extra - name: Build website shell: bash run: cd web && make prod diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cadb6e022..196d7cdda 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,4 +42,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: packages - path: dist + path: release diff --git a/Makefile b/Makefile index 94d729684..65eb2c40b 100644 --- a/Makefile +++ b/Makefile @@ -6,39 +6,44 @@ all: release # FEATURES += sentry +DIST=$(CURDIR)/release +BUILD=$(CURDIR)/.build + + ifneq ($(filter sentry,$(FEATURES)),) OBJ += sentry -RELOBJ += release/opt/lib/libsentry.so +RELOBJ += $(DIST)/opt/lib/libsentry.so DEFINES += 'DEFINES+="SENTRY"' endif -OBJ += _make -RELOBJ += _install +OBJ += $(BUILD)/oxide/Makefile clean: - rm -rf .build - rm -rf release + rm -rf $(DIST) $(BUILD) release: clean build $(RELOBJ) + mkdir -p $(DIST) + INSTALL_ROOT=$(DIST) $(MAKE) -C $(BUILD)/oxide install -build: $(OBJ) +build: $(BUILD) $(OBJ) + $(MAKE) -C $(BUILD)/oxide all package: REV="~r$(shell git rev-list --count HEAD).$(shell git rev-parse --short HEAD)" package: - rm -rf .build/package/ dist/ if [ -d .git ];then \ echo $(REV) > version.txt; \ else \ echo "~manual" > version.txt; \ fi; - mkdir -p .build/package - sed "s/~VERSION~/`cat version.txt`/" ./package > .build/package/package + mkdir -p $(BUILD)/package + rm -rf $(BUILD)/package/build + sed "s/~VERSION~/`cat version.txt`/" ./package > $(BUILD)/package/package tar \ - --exclude='./.git' \ - --exclude='./.build' \ - --exclude='./dist' \ - --exclude='./release' \ - -czvf .build/package/oxide.tar.gz \ + --exclude='$(CURDIR)/.git' \ + --exclude='$(BUILD)' \ + --exclude='$(CURDIR)/dist' \ + --exclude='$(DIST)' \ + -czvf $(BUILD)/package/oxide.tar.gz \ applications \ assets \ interfaces \ @@ -47,29 +52,29 @@ package: oxide.pro \ Makefile toltecmk \ - -w .build/package/build \ - -d .build/package/dist \ - .build/package - mkdir dist/ - cp -a .build/package/dist/rmall/*.ipk dist/ + --verbose \ + -w $(BUILD)/package/build \ + -d $(BUILD)/package/dist \ + $(BUILD)/package + mkdir -p $(DIST) + cp -a $(BUILD)/package/dist/rmall/*.ipk $(DIST) -sentry: .build/sentry/libsentry.so +sentry: $(BUILD)/sentry/libsentry.so -_make: .build/oxide/Makefile - $(MAKE) -j`nproc` -C .build/oxide all +$(BUILD): + mkdir -p $(BUILD) -_install: _make - mkdir -p $(CURDIR)/release - INSTALL_ROOT=$(CURDIR)/release $(MAKE) -j`nproc` -C .build/oxide install +$(BUILD)/.nobackup: $(BUILD) + touch $(BUILD)/.nobackup -.build/oxide: - mkdir -p .build/oxide +$(BUILD)/oxide: $(BUILD)/.nobackup + mkdir -p $(BUILD)/oxide -.build/oxide/Makefile: .build/oxide - cd .build/oxide && qmake -r $(DEFINES) ../../oxide.pro +$(BUILD)/oxide/Makefile: $(BUILD)/oxide + cd $(BUILD)/oxide && qmake -r $(DEFINES) $(CURDIR)/oxide.pro -.build/sentry/libsentry.so: - cd shared/sentry && cmake -B ../../.build/sentry/src \ +$(BUILD)/sentry/libsentry.so: $(BUILD)/.nobackup + cd shared/sentry && cmake -B $(BUILD)/sentry/src \ -DBUILD_SHARED_LIBS=ON \ -DSENTRY_INTEGRATION_QT=ON \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ @@ -78,9 +83,9 @@ _install: _make -DSENTRY_BREAKPAD_SYSTEM=OFF \ -DSENTRY_EXPORT_SYMBOLS=ON \ -DSENTRY_PTHREAD=ON - cd shared/sentry && cmake --build ../../.build/sentry/src --parallel - cd shared/sentry && cmake --install ../../.build/sentry/src --prefix ../../.build/sentry --config RelWithDebInfo + cd shared/sentry && cmake --build $(BUILD)/sentry/src --parallel + cd shared/sentry && cmake --install $(BUILD)/sentry/src --prefix $(BUILD)/sentry --config RelWithDebInfo -release/opt/lib/libsentry.so: sentry - mkdir -p release/opt/lib - cp -a .build/sentry/lib/libsentry.so release/opt/lib/ +$(DIST)/opt/lib/libsentry.so: sentry + mkdir -p $(DIST)/opt/lib + cp -a $(BUILD)/sentry/lib/libsentry.so $(DIST)/opt/lib/ diff --git a/applications/applications.pro b/applications/applications.pro index 84f03aac6..30aa36b31 100644 --- a/applications/applications.pro +++ b/applications/applications.pro @@ -1,6 +1,10 @@ TEMPLATE = subdirs SUBDIRS = \ + desktop-file-edit \ + desktop-file-install \ + desktop-file-validate \ + gio \ launcher \ lockscreen \ notify-send \ @@ -9,9 +13,15 @@ SUBDIRS = \ screenshot-viewer \ settings-manager \ system-service \ - task-switcher + task-switcher \ + update-desktop-database \ + xdg-desktop-icon \ + xdg-desktop-menu \ + xdg-icon-resource \ + xdg-open \ + xdg-settings -launcher.depends = system-service +launcher.depends = system-service update-desktop-database lockscreen.depends = system-service notify-send.depends = system-service process-manager.depends = @@ -20,14 +30,14 @@ screenshot-viewer.depends = system-service settings-manager.depends = system-service system-service.depends = task-switcher.depends = system-service +update-desktop-database.depends = system-service +xdg-desktop-icon.depends = system-service +xdg-desktop-menu.depends = system-service +xdg-open.depends = system-service +gio.depends = system-service +xdg-settings.depends = system-service +xdg-icon-resource.depends = system-service +desktop-file-edit.depends = desktop-file-edit +desktop-file-install.depends = -INSTALLS += \ - launcher \ - lockscreen \ - notify-send \ - process-manager \ - screenshot-tool \ - screenshot-viewer \ - settings-manager \ - system-service \ - task-switcher +INSTALLS += $$SUBDIRS diff --git a/applications/desktop-file-edit/.gitignore b/applications/desktop-file-edit/.gitignore new file mode 100644 index 000000000..fab7372d7 --- /dev/null +++ b/applications/desktop-file-edit/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/applications/desktop-file-edit/desktop-file-edit.pro b/applications/desktop-file-edit/desktop-file-edit.pro new file mode 100644 index 000000000..e7b171de0 --- /dev/null +++ b/applications/desktop-file-edit/desktop-file-edit.pro @@ -0,0 +1,23 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +DEFINES += QT_DEPRECATED_WARNINGS +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + edit.cpp \ + main.cpp + +HEADERS += \ + edit.h + +TARGET = desktop-file-edit +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/desktop-file-edit/edit.cpp b/applications/desktop-file-edit/edit.cpp new file mode 100644 index 000000000..0c309b5c7 --- /dev/null +++ b/applications/desktop-file-edit/edit.cpp @@ -0,0 +1,165 @@ +#include "edit.h" + +QCommandLineOption setKeyOption( + "set-key", + "Set the KEY key to the value passed to the next --set-value option. A matching --set-value option is mandatory.", + "KEY" +); +QCommandLineOption setValueOption( + "set-value", + "Set the key specified with the previous --set-key option to VALUE. A matching --set-key option is mandatory.", + "VALUE" +); +QCommandLineOption setNameOption( + "set-name", + "NOT IMPLEMENTED", + "NAME" +); +QCommandLineOption copyNameToGenericNameOption( + "copy-name-to-generic-name", + "Copy the value of the Name key to the GenericName key. Note that a desktop file requires a Name key to be valid, so this option will always have an effect." +); +QCommandLineOption setGenericNameOption( + "set-generic-name", + "Set the generic name (key DisplayName) to GENERIC-NAME. If a generic name was already set, it will be overridden. Localizations of the old generic name will be removed.", + "GENERIC-NAME" +); +QCommandLineOption copyGenericNameToNameOption( + "copy-generic-name-to-name", + "NOT IMPLEMENTED" +); +QCommandLineOption setCommentOption( + "set-comment", + "NOT IMPLEMENTED", + "COMMENT" +); +QCommandLineOption setIconOption( + "set-icon", + "Set the icon (key Icon) to ICON. If an icon was already set, it will be overridden. Localizations of the old icon will be removed.", + "ICON" +); +QCommandLineOption addCategoryOption( + "add-category", + "NOT IMPLEMENTED", + "CATEGORY" +); +QCommandLineOption removeCategoryOption( + "remove-category", + "NOT IMPLEMENTED", + "CATEGORY" +); +QCommandLineOption addMimeTypeOption( + "add-mime-type", + "NOT IMPLEMENTED", + "MIME-TYPE" +); +QCommandLineOption removeMimeTypeOption( + "remove-mime-type", + "NOT IMPLEMENTED", + "MIME-TYPE" +); +QCommandLineOption addOnlyShowInOption( + "add-only-show-in", + "NOT IMPLEMENTED", + "ENVIRONMENT" +); +QCommandLineOption removeOnlyShowInOption( + "remove-only-show-in", + "NOT IMPLEMENTED", + "ENVIRONMENT" +); +QCommandLineOption addNotShowInOption( + "add-not-show-in", + "NOT IMPLEMENTED", + "ENVIRONMENT" +); +QCommandLineOption removeNotShowInOption( + "remove-not-show-in", + "NOT IMPLEMENTED", + "ENVIRONMENT" +); +QCommandLineOption removeKeyOption( + "remove-key", + "Remove the KEY key from the desktop files, if present.", + "KEY" +); + +void addEditOptions(QCommandLineParser& parser){ + parser.addOption(setKeyOption); + parser.addOption(setValueOption); + parser.addOption(setNameOption); + parser.addOption(copyNameToGenericNameOption); + parser.addOption(setGenericNameOption); + parser.addOption(copyGenericNameToNameOption); + parser.addOption(setCommentOption); + parser.addOption(setIconOption); + parser.addOption(addCategoryOption); + parser.addOption(removeCategoryOption); + parser.addOption(addMimeTypeOption); + parser.addOption(removeMimeTypeOption); + parser.addOption(addOnlyShowInOption); + parser.addOption(removeOnlyShowInOption); + parser.addOption(addNotShowInOption); + parser.addOption(removeNotShowInOption); + parser.addOption(removeKeyOption); +} + +bool validateSetKeyValueOptions(QCommandLineParser& parser){ + auto options = parser.optionNames(); + if(parser.isSet(setKeyOption) || parser.isSet(setValueOption)){ + for(int i=0; i< options.length(); i++){ + auto option = options[i]; + if(setValueOption.names().contains(option)){ + qDebug() << "Option \"--set-value\" used without a prior \"--set-key\" option"; + qDebug() << "Run 'desktop-file-edit --help' to see a full list of available command line options."; + return false; + } + if(setKeyOption.names().contains(option)){ + option = options[++i]; + if(!setValueOption.names().contains(option)){ + qDebug() << "Option \"--set-key\" used without a following \"--set-value\" option"; + qDebug() << "Run 'desktop-file-edit --help' to see a full list of available command line options."; + return false; + } + } + } + } + return true; +} + +void applyChanges(QCommandLineParser& parser, QJsonObject& reg, const QString& name){ + int removeIndex = 0; + int keyIndex = 0; + int genericNameIndex = 0; + int iconIndex = 0; + QString key; + auto options = parser.optionNames(); + for(int i=0; i< options.length(); i++){ + auto option = options[i]; + if(copyNameToGenericNameOption.names().contains(option)){ + reg["displayName"] = name; + continue; + } + if(removeKeyOption.names().contains(option)){ + reg.remove(parser.values(removeKeyOption)[removeIndex]); + removeIndex++; + continue; + } + if(setGenericNameOption.names().contains(option)){ + reg["displayname"] = parser.values(setGenericNameOption)[genericNameIndex]; + genericNameIndex++; + continue; + } + if(setIconOption.names().contains(option)){ + reg["icon"] = parser.values(setIconOption)[iconIndex]; + iconIndex++; + continue; + } + if(setKeyOption.names().contains(option)){ + key = parser.values(setKeyOption)[keyIndex]; + reg[key] = parser.values(setValueOption)[keyIndex]; + i++; + keyIndex++; + } + } +} diff --git a/applications/desktop-file-edit/edit.h b/applications/desktop-file-edit/edit.h new file mode 100644 index 000000000..b1623e6e3 --- /dev/null +++ b/applications/desktop-file-edit/edit.h @@ -0,0 +1,12 @@ +#pragma once +#ifndef EDIT_H +#define EDIT_H + +#include +#include + +void addEditOptions(QCommandLineParser& parser); +bool validateSetKeyValueOptions(QCommandLineParser& parser); +void applyChanges(QCommandLineParser& parser, QJsonObject& reg, const QString& name); + +#endif // EDIT_H diff --git a/applications/desktop-file-edit/main.cpp b/applications/desktop-file-edit/main.cpp new file mode 100644 index 000000000..5f62980e8 --- /dev/null +++ b/applications/desktop-file-edit/main.cpp @@ -0,0 +1,78 @@ +#include +#include +#include + +#include +#include +#include + +#include "edit.h" + +using namespace Oxide::Sentry; +using namespace Oxide::JSON; +using namespace Oxide::Applications; + +QTextStream& qStdOut(){ + static QTextStream ts( stdout ); + return ts; +} + +int main(int argc, char *argv[]){ + QCoreApplication app(argc, argv); + sentry_init("desktop-file-edit", argv); + app.setOrganizationName("Eeems"); + app.setOrganizationDomain(OXIDE_SERVICE); + app.setApplicationName("desktop-file-edit"); + app.setApplicationVersion(APP_VERSION); + QCommandLineParser parser; + parser.setApplicationDescription("Edit application registration files"); + parser.applicationDescription(); + parser.addHelpOption(); + parser.addVersionOption(); + addEditOptions(parser); + parser.addPositionalArgument("FILE", "Application registration to edit"); + parser.process(app); + auto args = parser.positionalArguments(); + if(args.empty() || args.length() > 1){ + parser.showHelp(EXIT_FAILURE); + } + if(!validateSetKeyValueOptions(parser)){ + return EXIT_FAILURE; + } + auto path = args.first(); + QFile file(path); + if(!file.exists()){ + qDebug() << "Error on file" << path << ": No such file or directory"; + return EXIT_FAILURE; + } + auto reg = getRegistration(&file); + file.close(); + if(!file.open(QFile::WriteOnly | QFile::Truncate)){ + qDebug() << "Error on file" << path << ": Cannot write to file"; + return EXIT_FAILURE; + } + if(reg.isEmpty()){ + qDebug() << "Error on file" << path << ": is not valid"; + return EXIT_FAILURE; + } + + QFileInfo info(file); + auto name = info.baseName(); + applyChanges(parser, reg, name); + auto json = toJson(reg, QJsonDocument::Indented); + file.write(json.toUtf8()); + file.close(); + bool success = true; + for(auto error : validateRegistration(name, reg)){ + qStdOut() << path << ": " << error << Qt::endl; + auto level = error.level; + if(level == ErrorLevel::Error || level == ErrorLevel::Critical){ + success = false; + } + } + if(info.suffix() != "oxide"){ + qDebug() << path.toStdString().c_str() << ": error: filename does not have a .oxide extension"; + success = false; + } + return success ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/applications/desktop-file-install/.gitignore b/applications/desktop-file-install/.gitignore new file mode 100644 index 000000000..fab7372d7 --- /dev/null +++ b/applications/desktop-file-install/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/applications/desktop-file-install/desktop-file-install.pro b/applications/desktop-file-install/desktop-file-install.pro new file mode 100644 index 000000000..d41b248a1 --- /dev/null +++ b/applications/desktop-file-install/desktop-file-install.pro @@ -0,0 +1,23 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +DEFINES += QT_DEPRECATED_WARNINGS +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + ../desktop-file-edit/edit.cpp \ + main.cpp + +HEADERS += \ + ../desktop-file-edit/edit.h + +TARGET = desktop-file-install +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/desktop-file-install/main.cpp b/applications/desktop-file-install/main.cpp new file mode 100644 index 000000000..1f36a9c19 --- /dev/null +++ b/applications/desktop-file-install/main.cpp @@ -0,0 +1,121 @@ +#include +#include +#include + +#include +#include +#include + +#include "../desktop-file-edit/edit.h" + +using namespace Oxide::Sentry; +using namespace Oxide::JSON; +using namespace Oxide::Applications; + +QTextStream& qStdOut(){ + static QTextStream ts( stdout ); + return ts; +} + +int main(int argc, char *argv[]){ + QCoreApplication app(argc, argv); + sentry_init("desktop-file-install", argv); + app.setOrganizationName("Eeems"); + app.setOrganizationDomain(OXIDE_SERVICE); + app.setApplicationName("desktop-file-install"); + app.setApplicationVersion(APP_VERSION); + QCommandLineParser parser; + parser.setApplicationDescription("Install application registration files"); + parser.applicationDescription(); + parser.addHelpOption(); + parser.addVersionOption(); + QCommandLineOption dirOption( + "dir", + "Install desktop files to the DIR directory.", + "DIR", + OXIDE_APPLICATION_REGISTRATIONS_DIRECTORY + ); + parser.addOption(dirOption); + QCommandLineOption modeOption( + {"m", "mode"}, + "NOT IMPLEMENTED", + "MODE" + ); + parser.addOption(modeOption); + QCommandLineOption vendorOption( + "vendor", + "NOT IMPLEMENTED", + "VENDOR" + ); + parser.addOption(vendorOption); + QCommandLineOption deleteOriginalOption( + "delete-original", + "Delete the source registration files, leaving only the target files. Effectively \"renames\" the registration files." + ); + parser.addOption(deleteOriginalOption); + QCommandLineOption rebuildMimeInfoCacheOption( + "rebuild-mime-info-cache", + "NOT IMPLEMENTED" + ); + parser.addOption(rebuildMimeInfoCacheOption); + addEditOptions(parser); + parser.addPositionalArgument("FILE", "Application registration(s) to install", "FILE..."); + parser.process(app); + auto args = parser.positionalArguments(); + if(args.empty()){ + parser.showHelp(EXIT_FAILURE); + } + if(!validateSetKeyValueOptions(parser)){ + return EXIT_FAILURE; + } + bool success = true; + for(auto path : args){ + QFile file(path); + if(!file.exists()){ + qDebug() << "Error on file" << path << ": No such file or directory"; + success = false; + continue; + } + auto reg = getRegistration(&file); + file.close(); + QFileInfo info(file); + QFile toFile(QDir::cleanPath(parser.value(dirOption) + QDir::separator() + info.baseName() + ".oxide")); + if(!toFile.open(QFile::WriteOnly | QFile::Truncate)){ + qDebug() << "Error on file" << path << ": Cannot write to file"; + success = false; + continue; + } + if(info.suffix() != "oxide"){ + qDebug() << path.toStdString().c_str() << ": error: filename does not have a .oxide extension"; + success = false; + continue; + } + if(reg.isEmpty()){ + qDebug() << "Error on file" << path << ": is not valid"; + success = false; + continue; + } + + auto name = info.baseName(); + applyChanges(parser, reg, name); + bool hadError = false; + for(auto error : validateRegistration(name, reg)){ + qStdOut() << path << ": " << error << Qt::endl; + auto level = error.level; + if(level == ErrorLevel::Error || level == ErrorLevel::Critical){ + hadError = true; + } + } + if(hadError){ + success = false; + continue; + } + auto json = toJson(reg, QJsonDocument::Indented); + toFile.write(json.toUtf8()); + toFile.close(); + if(parser.isSet(deleteOriginalOption)){ + file.remove(); + } + } + return success ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/applications/desktop-file-validate/.gitignore b/applications/desktop-file-validate/.gitignore new file mode 100644 index 000000000..fab7372d7 --- /dev/null +++ b/applications/desktop-file-validate/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/applications/desktop-file-validate/desktop-file-validate.pro b/applications/desktop-file-validate/desktop-file-validate.pro new file mode 100644 index 000000000..bae7481a5 --- /dev/null +++ b/applications/desktop-file-validate/desktop-file-validate.pro @@ -0,0 +1,21 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +DEFINES += QT_DEPRECATED_WARNINGS +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp + +HEADERS += + +TARGET = desktop-file-validate +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/desktop-file-validate/main.cpp b/applications/desktop-file-validate/main.cpp new file mode 100644 index 000000000..92fa172ea --- /dev/null +++ b/applications/desktop-file-validate/main.cpp @@ -0,0 +1,65 @@ +#include + +#include +#include +#include + +using namespace Oxide::Sentry; +using namespace Oxide::Applications; + +int qExit(int ret){ + QTimer::singleShot(0, [ret](){ + qApp->exit(ret); + }); + return qApp->exec(); +} +QTextStream& qStdOut(){ + static QTextStream ts( stdout ); + return ts; +} + +int main(int argc, char *argv[]){ + QCoreApplication app(argc, argv); + sentry_init("desktop-file-validate", argv); + app.setOrganizationName("Eeems"); + app.setOrganizationDomain(OXIDE_SERVICE); + app.setApplicationName("desktop-file-validate"); + app.setApplicationVersion(APP_VERSION); + QCommandLineParser parser; + parser.setApplicationDescription("Validates application registration files"); + parser.applicationDescription(); + parser.addHelpOption(); + parser.addVersionOption(); + QCommandLineOption noHintsOption( + "no-hints", + "Do not output hints about things that might be improved in the application registration." + ); + parser.addOption(noHintsOption); + QCommandLineOption noWarnDeprecatedOption( + "no-warn-deprecated", + "Do not warn about usage of deprecated items that were defined in the previous version of the specification." + ); + parser.addOption(noWarnDeprecatedOption); + QCommandLineOption warnKDEOption("warn-kde", "NOT IMPLEMENTED"); + parser.addOption(warnKDEOption); + parser.addPositionalArgument("file", "Application registration(s) to validate", "FILE..."); + parser.process(app); + auto args = parser.positionalArguments(); + if(args.empty()){ + parser.showHelp(EXIT_FAILURE); + } + bool skipHint = parser.isSet(noHintsOption); + bool skipDeprecations = parser.isSet(noWarnDeprecatedOption); + for(QString path : args){ + for(auto error : validateRegistration(path)){ + if(skipHint && error.level == ErrorLevel::Hint){ + continue; + } + if(skipDeprecations && error.level == ErrorLevel::Deprecation){ + continue; + } + qStdOut() << path << ": " << error << Qt::endl; + } + } + return qExit(EXIT_SUCCESS); +} diff --git a/applications/gio/.gitignore b/applications/gio/.gitignore new file mode 100644 index 000000000..fab7372d7 --- /dev/null +++ b/applications/gio/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/applications/gio/cat.h b/applications/gio/cat.h new file mode 100644 index 000000000..e88d28886 --- /dev/null +++ b/applications/gio/cat.h @@ -0,0 +1,37 @@ +#pragma once + +#include "common.h" + +#include +#include + +class CatCommand : ICommand{ + O_COMMAND(CatCommand, "cat", "Concatenates the given files and prints them to the standard output.") + int arguments() override{ + parser->addPositionalArgument("location", "Locations to concatenate", "LOCATION..."); + return EXIT_SUCCESS; + } + int command(const QStringList& args) override{ + for(const auto& path : args){ + auto url = urlFromPath(path); + if(!url.isLocalFile()){ + GIO_ERROR(url, path, "Error while parsing path", "Path is not a local file"); + continue; + } + QFile file(path); + if(!file.exists()){ + GIO_ERROR(url, path, "Error when getting information for file", "No such file or directory"); + continue; + } + if(!file.open(QFile::ReadOnly)){ + GIO_ERROR(url, path, "Error opening file", "Permission denied"); + continue; + } + while(!file.atEnd()){ + qStdOut() << file.read(1024); + } + file.close(); + } + return EXIT_SUCCESS; + } +}; diff --git a/applications/gio/common.cpp b/applications/gio/common.cpp new file mode 100644 index 000000000..3f7d169d9 --- /dev/null +++ b/applications/gio/common.cpp @@ -0,0 +1,112 @@ +#include "common.h" + +#include +#include + +QTextStream& qStdOut(){ + static QTextStream ts( stdout ); + return ts; +} + +QMap* ICommand::commands = new QMap; +QCommandLineParser* ICommand::parser = nullptr; + +QCommandLineOption ICommand::versionOption(){ + static QCommandLineOption value( + {"v", "version"}, + "Displays version information." + ); + return value; +} + +#define FULL_SIZE 66 + +QString ICommand::commandsHelp(){ + QString value; + const QList& keys = commands->keys(); + int leftSize = (*std::max_element(keys.constBegin(), keys.constEnd(), [](const QString& v1, const QString& v2){ + return v1.length() < v2.length(); + })).length() + 2; + int rightSize = FULL_SIZE - leftSize - 1; + for(const auto& name : keys){ + const auto& item = commands->value(name); + value += "\n" + name.leftJustified(leftSize, ' '); + if(item.help > rightSize){ + QStringList words = item.help.split(' '); + QString padding = QString().leftJustified(leftSize + 1, ' '); + bool first = true; + while(!words.isEmpty()){ + QString strPart; + while(!words.isEmpty()){ + auto word = words.first(); + // If the first word is larger than a single line + if(!strPart.length() && word.length() > rightSize){ + // Fill the line with what you can + strPart = word.left(rightSize); + words.replace(0, word.mid(rightSize)); + // Exit since we have what we need to display + break; + } + // Exit if the next word would be too long + if(strPart.length() + 1 + word.length() > rightSize){ + break; + } + // Add the word to the results + strPart += word + " "; + words.removeFirst(); + } + // Don't padd the first line, it doesn't need it + if(first){ + first = false; + }else{ + value += "\n"; + value += padding; + } + value += strPart; + } + }else{ + value += item.help; + } + } + return value; +} +int ICommand::exec(QCommandLineParser& _parser){ + parser = &_parser; + QStringList args = _parser.positionalArguments(); + if (args.isEmpty()) { + _parser.showHelp(EXIT_FAILURE); + } + if(_parser.isSet(ICommand::versionOption())){ + _parser.showVersion(); + } + auto name = args.first(); + if(!commands->contains(name)){ + _parser.showHelp(EXIT_FAILURE); + } + auto command = commands->value(name).command; + parser->clearPositionalArguments(); + parser->addPositionalArgument("", "", name); + auto res = command->arguments(); + if(res){ + return res; + } + _parser.process(*qApp); + if(_parser.isSet(versionOption())){ + _parser.showHelp(EXIT_FAILURE); + } + args = _parser.positionalArguments(); + args.removeFirst(); + if(!command->allowEmpty && args.isEmpty()){ + _parser.showHelp(EXIT_FAILURE); + } + return command->command(args); +} + +ICommand::ICommand(bool allowEmpty) : allowEmpty(allowEmpty){} +QUrl ICommand::urlFromPath(const QString& path){ + auto url = QUrl::fromUserInput(path, QDir::currentPath(), QUrl::AssumeLocalFile); + if(url.scheme().isEmpty()){ + url.setScheme("file"); + } + return url; +} diff --git a/applications/gio/common.h b/applications/gio/common.h new file mode 100644 index 000000000..ecf06c61e --- /dev/null +++ b/applications/gio/common.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +QTextStream& qStdOut(); + +#define GIO_ERROR(url, path, message, error) \ + qDebug() \ + << "gio:" \ + << url.toString().toStdString().c_str() \ + << ": " \ + << message \ + << QFileInfo(path).absoluteFilePath().toStdString().c_str() \ + << ": " \ + << error; + +#define O_COMMAND_BODY(_class, _name, _help, _allowEmpty) \ +public: \ + _class() : ICommand(_allowEmpty) { \ + if(commands->contains(_name)){ \ + throw "ICommand " _name " already exists"; \ + } \ + commands->insert(_name, Command{ \ + .help = _help, \ + .command = this, \ + }); \ + } +#define O_COMMAND_X_1(_class, _name, _help) O_COMMAND_BODY(_class, _name, _help, false) +#define O_COMMAND_X_2(_class, _name, _help, _allowEmpty) O_COMMAND_BODY(_class, _name, _help, _allowEmpty) +#define O_COMMAND_X_get_func(arg1, arg2, arg3, arg4, arg5, ...) arg5 +#define O_COMMAND_X(...) \ + O_COMMAND_X_get_func(__VA_ARGS__, \ + O_COMMAND_X_2, \ + O_COMMAND_X_1, \ + ) + +#define O_COMMAND(...) O_COMMAND_X(__VA_ARGS__)(__VA_ARGS__) +#define STATIC_INSTANCE(_class) Q_UNUSED(new _class()); +#define O_COMMAND_STUB(_name) \ + class _name ## Command : ICommand{ \ + O_COMMAND(_name ## Command, #_name, "NOT IMPLEMENTED") \ + int arguments() override{ \ + qDebug() << "This has not been implemented."; \ + return EXIT_FAILURE; \ + } \ + int command(const QStringList& args) override{ \ + Q_UNUSED(args) \ + qDebug() << "This has not been implemented."; \ + return EXIT_FAILURE; \ + } \ + }; \ + STATIC_INSTANCE(_name ## Command); + +class ICommand; + +struct Command { + QString help; + ICommand* command; +}; + +class ICommand { +public: + static QMap* commands; + static QCommandLineParser* parser; + + static QCommandLineOption versionOption(); + static QString commandsHelp(); + static int exec(QCommandLineParser& _parser); + + ICommand(bool allowEmpty); + virtual int arguments(){ return EXIT_FAILURE; } + virtual int command(const QStringList& args){ return EXIT_FAILURE; } + +protected: + bool allowEmpty = false; + QUrl urlFromPath(const QString& path); +}; diff --git a/applications/gio/copy.h b/applications/gio/copy.h new file mode 100644 index 000000000..b3b00e860 --- /dev/null +++ b/applications/gio/copy.h @@ -0,0 +1,130 @@ +#pragma once + +#include "common.h" + +#include +#include +#include + +// [OPTION...] SOURCE... DESTINATION +class CopyCommand : ICommand{ + O_COMMAND( + CopyCommand, "copy", + "Copies one or more files from SOURCE to DESTINATION. If more than one source is specified, the destination must be a directory. " + ) + int arguments() override{ + parser->addOption(noTargetDirectoryOption); + parser->addOption(progressOption); + parser->addOption(interactiveOption); + parser->addOption(preserveOption); + parser->addOption(backupOption); + parser->addOption(noDereferenceOption); + parser->addOption(defaultPermissionsOption); + parser->addPositionalArgument("SOURCE", "The source file(s) to copy to the destination", "SOURCE..."); + parser->addPositionalArgument("DESTINATION", "The destination to copy to"); + return EXIT_SUCCESS; + } + int command(const QStringList& args) override{ + if(args.length() < 2){ + parser->showHelp(EXIT_FAILURE); + } + auto toPath = args.last(); + auto toUrl = urlFromPath(toPath); + if(!toUrl.isLocalFile()){ + GIO_ERROR(toUrl, toPath, "Error while parsing path", "Path is not a local file"); + return EXIT_FAILURE; + } + if(!QFile::exists(toPath)){ + GIO_ERROR(toUrl, toPath, "Error when getting information for file", "No such file or directory"); + return EXIT_FAILURE; + } + QFileInfo toInfo(toPath); + if(toInfo.isFile() && args.length() > 2){ + qDebug() << "gio: Destination" << toPath << "is not a directory"; + parser->showHelp(EXIT_FAILURE); + } + bool failed = false; + for(const auto& path : args.mid(0, args.length() - 1)){ + auto url = urlFromPath(path); + if(!url.isLocalFile()){ + GIO_ERROR(url, path, "Error while parsing path", "Path is not a local file"); + failed = true; + continue; + } + QFileInfo info(path); + if(!info.exists()){ + GIO_ERROR(url, path, "Error when getting information for file", "No such file or directory"); + failed = true; + continue; + } + if(info.isDir()){ + qDebug() << "gio:" << url << ": Can’t recursively copy directory"; + failed = true; + continue; + } + if(!info.isReadable()){ + GIO_ERROR(url, path, "Error opening file", "Permission denied"); + failed = true; + continue; + } + } + if(failed){ + return EXIT_FAILURE; + } + auto cpArgs = QStringList() << "cp"; + if(parser->isSet(noTargetDirectoryOption)){ + cpArgs.append("-T"); + } + if(parser->isSet(interactiveOption)){ + cpArgs.append("-i"); + } + if(parser->isSet(preserveOption) && !parser->isSet(defaultPermissionsOption)){ + cpArgs.append("-p"); + } + if(parser->isSet(noDereferenceOption)){ + cpArgs.append("-P"); + }else{ + cpArgs.append("-L"); + } + auto* p = new QProcess(); + p->setInputChannelMode(QProcess::ForwardedInputChannel); + p->setProcessChannelMode(QProcess::ForwardedChannels); + + p->start("busybox", cpArgs + args); + qApp->processEvents(); + p->waitForFinished(); + auto res = p->exitCode(); + p->deleteLater(); + return res; + } + +private: + QCommandLineOption noTargetDirectoryOption = QCommandLineOption( + {"T", "no-target-directory"}, + "Don’t copy into DESTINATION even if it is a directory." + ); + QCommandLineOption progressOption = QCommandLineOption( + {"p", "progress"}, + "NOT IMPLEMENTED" + ); + QCommandLineOption interactiveOption = QCommandLineOption( + {"i", "interactive"}, + "Prompt for confirmation before overwriting files." + ); + QCommandLineOption preserveOption = QCommandLineOption( + "preserve", + "Preserve all attributes of copied files." + ); + QCommandLineOption backupOption = QCommandLineOption( + {"b", "backup"}, + "NOT IMPLEMENTED" + ); + QCommandLineOption noDereferenceOption = QCommandLineOption( + {"P", "no-dereference"}, + "Never follow symbolic links." + ); + QCommandLineOption defaultPermissionsOption = QCommandLineOption( + "default-permissions", + "Use the default permissions of the current process for the destination file, rather than copying the permissions of the source file." + ); +}; diff --git a/applications/gio/gio.pro b/applications/gio/gio.pro new file mode 100644 index 000000000..e86bcaf03 --- /dev/null +++ b/applications/gio/gio.pro @@ -0,0 +1,32 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +DEFINES += QT_DEPRECATED_WARNINGS +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + common.cpp \ + main.cpp + +HEADERS += \ + cat.h \ + common.h \ + copy.h \ + help.h \ + launch.h \ + mkdir.h \ + open.h \ + remove.h \ + rename.h \ + version.h + +TARGET = gio +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/gio/help.h b/applications/gio/help.h new file mode 100644 index 000000000..d9099445e --- /dev/null +++ b/applications/gio/help.h @@ -0,0 +1,30 @@ +#pragma once + +#include "common.h" + +#include +#include + +class HelpCommand : ICommand{ + O_COMMAND(HelpCommand, "help", "Print help", true) + int arguments() override{ + parser->addPositionalArgument("Commands:", commandsHelp(), "[COMMAND]"); + return EXIT_SUCCESS; + } + int command(const QStringList& args) override{ + if(args.isEmpty()){ + parser->showHelp(EXIT_SUCCESS); + } + auto command = args.first(); + auto tempArgs = QStringList() << command; + if(!commands->contains(command)){ + parser->showHelp(EXIT_FAILURE); + } + parser->clearPositionalArguments(); + parser->addPositionalArgument("", "", command); + auto res = commands->value(command).command->arguments(); + parser->process(tempArgs); + parser->showHelp(res); + return res; + } +}; diff --git a/applications/gio/launch.h b/applications/gio/launch.h new file mode 100644 index 000000000..2ac176ecb --- /dev/null +++ b/applications/gio/launch.h @@ -0,0 +1,90 @@ +#pragma once + +#include "common.h" + +#include +#include + +#include +#include +#include +#include + +using namespace codes::eeems::oxide1; +using namespace Oxide::Applications; + +class LaunchCommand : ICommand{ + O_COMMAND(LaunchCommand, "launch", "Launch an application from an application registration file") + int arguments() override{ + parser->addPositionalArgument("DESKTOP-FILE", "Application registration to launch"); + parser->addPositionalArgument("FILE-ARG", "NOT IMPLEMENTED", "[FILE-ARG...]"); + return EXIT_SUCCESS; + } + int command(const QStringList& args) override{ + const auto& path = args.first(); + auto url = urlFromPath(path); + if(!url.isLocalFile()){ + GIO_ERROR(url, path, "Error while parsing path", "Path is not a local file"); + return EXIT_FAILURE; + } + auto suffix = QFileInfo(url.path()).suffix(); + if(suffix != "oxide"){ + GIO_ERROR(url, path, "Unhandled file extension", suffix); + return EXIT_FAILURE; + } + auto reg = getRegistration(path); + if(reg.isEmpty()){ + GIO_ERROR(url, path, "Invalid application registration", "Invalid JSON"); + return EXIT_FAILURE; + } + if(!reg.contains("flags")){ + reg["flags"] = QJsonArray(); + } + if(!reg["flags"].isArray()){ + GIO_ERROR(url, path, "Invalid application registration", "Key \"flags\" must be an array"); + return EXIT_FAILURE; + } + auto flags = reg["flags"].toArray(); + flags.prepend("transient"); + reg["flags"] = flags; + if(!reg.contains("displayName")){ + QFileInfo info(path); + // Workaround because basename will sometimes return the suffix instead + reg["displayName"] = info.fileName().left(info.fileName().length() - 6); + } + auto name = QUuid::createUuid().toString(); + auto errors = validateRegistration(name, reg); + if(std::any_of(errors.constBegin(), errors.constEnd(), [](const ValidationError& error){ + auto level = error.level; + return level == ErrorLevel::Error || level == ErrorLevel::Critical; + })){ + for(const auto& error : errors){ + if(error.level == ErrorLevel::Error || error.level == ErrorLevel::Critical){ + GIO_ERROR(url, path, "Invalid application registration", error) + } + } + return EXIT_FAILURE; + } + auto bus = QDBusConnection::systemBus(); + General api(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); + QDBusObjectPath qpath = api.requestAPI("apps"); + if(qpath.path() == "/"){ + GIO_ERROR(url, path, "Error registering transient application", "Unable to get apps API"); + return EXIT_FAILURE; + } + Apps apps(OXIDE_SERVICE, qpath.path(), bus); + auto properties = registrationToMap(reg, name); + if(properties.isEmpty()){ + GIO_ERROR(url, path, "Error registering transient application", "Unable to convert application registration to QVariantMap"); + return EXIT_FAILURE; + } + qpath = apps.registerApplication(properties); + if(qpath.path() == "/"){ + GIO_ERROR(url, path, "Error registering transient application", "Failed to register" << name.toStdString().c_str()); + return EXIT_FAILURE; + } + Application app(OXIDE_SERVICE, qpath.path(), bus); + app.launch().waitForFinished(); + return EXIT_SUCCESS; + } +}; diff --git a/applications/gio/main.cpp b/applications/gio/main.cpp new file mode 100644 index 000000000..0aceb17f2 --- /dev/null +++ b/applications/gio/main.cpp @@ -0,0 +1,60 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Oxide::Sentry; +using namespace Oxide::Applications; + +int main(int argc, char *argv[]){ + QCoreApplication app(argc, argv); + sentry_init("gio", argv); + app.setOrganizationName("Eeems"); + app.setOrganizationDomain(OXIDE_SERVICE); + app.setApplicationName("gio"); + app.setApplicationVersion(APP_VERSION); + + STATIC_INSTANCE(HelpCommand); + STATIC_INSTANCE(VersionCommand); + STATIC_INSTANCE(CatCommand); + STATIC_INSTANCE(CopyCommand); + O_COMMAND_STUB(info); + STATIC_INSTANCE(LaunchCommand); + O_COMMAND_STUB(list); + O_COMMAND_STUB(mime); + STATIC_INSTANCE(MkdirCommand); + O_COMMAND_STUB(monitor); + O_COMMAND_STUB(mount); + O_COMMAND_STUB(move); + STATIC_INSTANCE(OpenCommand); + STATIC_INSTANCE(RenameCommand); + STATIC_INSTANCE(RemoveCommand); + O_COMMAND_STUB(save); + O_COMMAND_STUB(set); + O_COMMAND_STUB(trash); + O_COMMAND_STUB(tree); + + QCommandLineParser parser; + parser.setApplicationDescription("GIO command line tool"); + parser.applicationDescription(); + parser.addHelpOption(); + parser.addOption(ICommand::versionOption()); + parser.addPositionalArgument("Commands:", ICommand::commandsHelp(), "COMMAND"); + parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments); + parser.parse(app.arguments()); + parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsOptions); + return ICommand::exec(parser); +} diff --git a/applications/gio/mkdir.h b/applications/gio/mkdir.h new file mode 100644 index 000000000..3353fae91 --- /dev/null +++ b/applications/gio/mkdir.h @@ -0,0 +1,59 @@ +#pragma once + +#include "common.h" + +#include +#include +#include + +// [OPTION...] SOURCE... DESTINATION +class MkdirCommand : ICommand{ + O_COMMAND( + MkdirCommand, "mkdir", + "Creates directories." + ) + int arguments() override{ + parser->addOption(parentOption); + parser->addPositionalArgument("LOCATION", "The directories to create", "LOCATION..."); + return EXIT_SUCCESS; + } + int command(const QStringList& args) override{ + auto mkdirArgs = QStringList(); + if(parser->isSet(parentOption)){ + mkdirArgs.append("-p"); + } + auto* p = new QProcess(); + p->setInputChannelMode(QProcess::ForwardedInputChannel); + p->setProcessChannelMode(QProcess::ForwardedChannels); + bool failed = false; + for(const auto& path : args){ + auto url = urlFromPath(path); + if(!url.isLocalFile()){ + GIO_ERROR(url, path, "Error while parsing path", "Path is not a local file"); + failed = true; + continue; + } + QFileInfo info(path); + if(info.exists()){ + GIO_ERROR(url, path, "Error creating directory", "File exists"); + failed = true; + continue; + } + + p->start("mkdir", QStringList() << mkdirArgs << path); + qApp->processEvents(); + p->waitForFinished(); + if(p->exitCode()){ + failed = true; + } + } + p->deleteLater(); + return failed ? EXIT_FAILURE : EXIT_SUCCESS; + } + +private: + QCommandLineOption parentOption = QCommandLineOption( + {"p", "parent"}, + "Create parent directories when necessary." + ); +}; diff --git a/applications/gio/open.h b/applications/gio/open.h new file mode 100644 index 000000000..f6f1db2dc --- /dev/null +++ b/applications/gio/open.h @@ -0,0 +1,28 @@ +#pragma once + +#include "common.h" + +#include +#include + +class OpenCommand : ICommand{ + O_COMMAND(OpenCommand, "open", "Open file(s) with xdg-open") + int arguments() override{ + parser->addPositionalArgument("location", "Locations to open", "LOCATION..."); + return EXIT_SUCCESS; + } + int command(const QStringList& args) override{ + for(const auto& path : args){ + auto url = QUrl::fromUserInput(path, QDir::currentPath(), QUrl::AssumeLocalFile); + if(url.scheme().isEmpty()){ + url.setScheme("file"); + } + if(!url.isLocalFile()){ + GIO_ERROR(url, path, "Error while parsing path", "Path is not a local file"); + continue; + } + QProcess::execute("xdg-open", QStringList() << url.toString()); + } + return EXIT_SUCCESS; + } +}; diff --git a/applications/gio/remove.h b/applications/gio/remove.h new file mode 100644 index 000000000..4c26fe3e4 --- /dev/null +++ b/applications/gio/remove.h @@ -0,0 +1,61 @@ +#pragma once + +#include "common.h" + +#include +#include +#include + +// [OPTION...] SOURCE... DESTINATION +class RemoveCommand : ICommand{ + O_COMMAND( + RemoveCommand, "remove", + "Deletes each given file." + ) + int arguments() override{ + parser->addOption(forceOption); + parser->addPositionalArgument("LOCATION", "The locations to remove", "LOCATION..."); + return EXIT_SUCCESS; + } + int command(const QStringList& args) override{ + auto* p = new QProcess(); + p->setInputChannelMode(QProcess::ForwardedInputChannel); + p->setProcessChannelMode(QProcess::ForwardedChannels); + bool failed = false; + for(const auto& path : args){ + auto url = urlFromPath(path); + if(!url.isLocalFile()){ + GIO_ERROR(url, path, "Error while parsing path", "Path is not a local file"); + failed = true; + continue; + } + QFileInfo info(path); + if(!info.exists()){ + if(!parser->isSet(forceOption)){ + GIO_ERROR(url, path, "Error removing file", "No such file or directory"); + failed = true; + } + continue; + } + QStringList rmArgs; + if(info.isDir()){ + rmArgs.append("rmdir"); + }else{ + rmArgs.append("rm"); + rmArgs.append("-f"); + } + p->start("busybox", QStringList() << rmArgs << path); + qApp->processEvents(); + p->waitForFinished(); + failed = failed || p->exitCode(); + } + p->deleteLater(); + return failed ? EXIT_FAILURE : EXIT_SUCCESS; + } + +private: + QCommandLineOption forceOption = QCommandLineOption( + {"f", "force"}, + "Ignore non-existent and non-deletable files." + ); +}; diff --git a/applications/gio/rename.h b/applications/gio/rename.h new file mode 100644 index 000000000..8fd796fb0 --- /dev/null +++ b/applications/gio/rename.h @@ -0,0 +1,48 @@ +#pragma once + +#include "common.h" + +#include +#include +#include + +// [OPTION...] SOURCE... DESTINATION +class RenameCommand : ICommand{ + O_COMMAND( + RenameCommand, "rename", + "Renames a file." + ) + int arguments() override{ + parser->addPositionalArgument("LOCATION", "The location to rename."); + parser->addPositionalArgument("NAME", "The new name."); + return EXIT_SUCCESS; + } + int command(const QStringList& args) override{ + if(args.length() < 2){ + parser->showHelp(EXIT_FAILURE); + } + auto name = args.last(); + if(name.contains("/")){ + qDebug() << "gio: File names cannot contain \"/\""; + return EXIT_FAILURE; + } + auto path = args.first(); + auto url = urlFromPath(path); + if(!url.isLocalFile()){ + GIO_ERROR(url, path, "Error while parsing path", "Path is not a local file"); + return EXIT_FAILURE; + } + QFileInfo info(path); + if(!info.exists()){ + GIO_ERROR(url, path, "Error renaming file", "No such file or directory"); + return EXIT_FAILURE; + } + auto toPath = info.absoluteDir().path() + "/" + name; + if(!QFile::rename(path, toPath)){ + GIO_ERROR(url, path, "Error renaming file", "Rename failed."); + return EXIT_FAILURE; + } + qDebug() << "Rename successful. New uri:" << urlFromPath(toPath).toString().toStdString().c_str(); + return EXIT_SUCCESS; + } +}; diff --git a/applications/gio/version.h b/applications/gio/version.h new file mode 100644 index 000000000..5352cdbfc --- /dev/null +++ b/applications/gio/version.h @@ -0,0 +1,18 @@ +#pragma once + +#include "common.h" + +#include +#include + +class VersionCommand : ICommand{ + O_COMMAND(VersionCommand, "version", "Print the version of Oxide that gio belongs to", true) + int arguments() override{ return EXIT_SUCCESS; } + int command(const QStringList& args) override{ + if(!args.isEmpty()){ + parser->showHelp(EXIT_FAILURE); + } + parser->showVersion(); + return EXIT_SUCCESS; + } +}; diff --git a/applications/launcher/appitem.cpp b/applications/launcher/appitem.cpp index a5b25a2ac..3f238f93f 100755 --- a/applications/launcher/appitem.cpp +++ b/applications/launcher/appitem.cpp @@ -8,8 +8,6 @@ #include #include "appitem.h" -#include "dbusservice_interface.h" -#include "appsapi_interface.h" #include "mxcfb.h" #include "controller.h" diff --git a/applications/launcher/appitem.h b/applications/launcher/appitem.h index d195126d3..b2b2c8927 100644 --- a/applications/launcher/appitem.h +++ b/applications/launcher/appitem.h @@ -1,13 +1,7 @@ #ifndef APP_H #define APP_H #include - -#include "application_interface.h" - -#ifndef OXIDE_SERVICE -#define OXIDE_SERVICE "codes.eeems.oxide1" -#define OXIDE_SERVICE_PATH "/codes/eeems/oxide1" -#endif +#include using namespace codes::eeems::oxide1; diff --git a/applications/launcher/controller.cpp b/applications/launcher/controller.cpp index 7e634d9b3..583b7b0f4 100644 --- a/applications/launcher/controller.cpp +++ b/applications/launcher/controller.cpp @@ -13,7 +13,6 @@ #include #include "controller.h" -#include "dbusservice_interface.h" QSet settings = { "columns", "autoStartApplication" }; QSet booleanSettings {"showWifiDb", "showBatteryPercent", "showBatteryTemperature", "showDate" }; @@ -207,7 +206,7 @@ QList Controller::getApps(){ for(auto item : appsApi->applications()){ auto path = item.value().path(); Application app(OXIDE_SERVICE, path, bus, this); - if(app.hidden()){ + if(app.hidden() || app.transient()){ continue; } auto name = app.name(); @@ -256,80 +255,7 @@ AppItem* Controller::getApplication(QString name){ return nullptr; } -void Controller::importDraftApps(){ - qDebug() << "Importing Draft Applications"; - auto bus = QDBusConnection::systemBus(); - for(auto configDirectoryPath : configDirectoryPaths){ - QDir configDirectory(configDirectoryPath); - configDirectory.setFilter( QDir::Files | QDir::NoSymLinks | QDir::NoDot | QDir::NoDotDot); - auto images = configDirectory.entryInfoList(QDir::NoFilter,QDir::SortFlag::Name); - for(QFileInfo fi : images){ - if(fi.fileName() != "conf"){ - auto f = fi.absoluteFilePath(); - qDebug() << "parsing file " << f; - QFile file(fi.absoluteFilePath()); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qCritical() << "Couldn't find the file " << f; - continue; - } - QTextStream in(&file); - QVariantMap properties; - while (!in.atEnd()) { - QString line = in.readLine(); - if(line.startsWith("#") || line.trimmed().isEmpty()){ - continue; - } - QStringList parts = line.split("="); - if(parts.length() != 2){ - O_WARNING("wrong format on " << line); - continue; - } - QString lhs = parts.at(0); - QString rhs = parts.at(1); - if(rhs != ":" && rhs != ""){ - if(lhs == "name"){ - properties.insert("name", rhs); - }else if(lhs == "desc"){ - properties.insert("description", rhs); - }else if(lhs == "imgFile"){ - auto icon = configDirectoryPath + "/icons/" + rhs + ".png"; - if(icon.startsWith("qrc:")){ - icon = ""; - } - properties.insert("icon", icon); - }else if(lhs == "call"){ - properties.insert("bin", rhs); - }else if(lhs == "term"){ - properties.insert("onStop", rhs.trimmed()); - } - } - } - file.close(); - auto name = properties["name"].toString(); - QDBusObjectPath path = appsApi->getApplicationPath(name); - if(path.path() != "/"){ - qDebug() << "Already exists" << name; - auto icon = properties["icon"].toString(); - if(icon.isEmpty()){ - continue; - } - Application app(OXIDE_SERVICE, path.path(), bus, this); - if(app.icon().isEmpty()){ - app.setIcon(icon); - } - continue; - } - qDebug() << "Not found, creating..."; - properties.insert("displayName", name); - path = appsApi->registerApplication(properties); - if(path.path() == "/"){ - qDebug() << "Failed to import" << name; - } - } - } - } - qDebug() << "Finished Importing Draft Applications"; -} +void Controller::importDraftApps(){ QProcess::execute("update-desktop-database", QStringList() << "--verbose"); } void Controller::powerOff(){ qDebug() << "Powering off..."; systemApi->powerOff(); diff --git a/applications/launcher/controller.h b/applications/launcher/controller.h index 2c4e129a5..c5cbe6ec5 100644 --- a/applications/launcher/controller.h +++ b/applications/launcher/controller.h @@ -12,15 +12,6 @@ #include "appitem.h" #include "wifinetworklist.h" #include "notificationlist.h" -#include "dbusservice_interface.h" -#include "powerapi_interface.h" -#include "wifiapi_interface.h" -#include "network_interface.h" -#include "bss_interface.h" -#include "appsapi_interface.h" -#include "systemapi_interface.h" -#include "notification_interface.h" -#include "notificationapi_interface.h" #define OXIDE_SERVICE "codes.eeems.oxide1" #define OXIDE_SERVICE_PATH "/codes/eeems/oxide1" diff --git a/applications/launcher/launcher.pro b/applications/launcher/launcher.pro index bc375142a..18a06de04 100644 --- a/applications/launcher/launcher.pro +++ b/applications/launcher/launcher.pro @@ -15,7 +15,6 @@ SOURCES += \ controller.cpp \ appitem.cpp - RESOURCES += qml.qrc TARGET = oxide @@ -23,19 +22,12 @@ include(../../qmake/common.pri) target.path = /opt/bin INSTALLS += target -DBUS_INTERFACES += ../../interfaces/dbusservice.xml -DBUS_INTERFACES += ../../interfaces/powerapi.xml -DBUS_INTERFACES += ../../interfaces/wifiapi.xml -DBUS_INTERFACES += ../../interfaces/network.xml -DBUS_INTERFACES += ../../interfaces/bss.xml -DBUS_INTERFACES += ../../interfaces/appsapi.xml -DBUS_INTERFACES += ../../interfaces/application.xml -DBUS_INTERFACES += ../../interfaces/systemapi.xml -DBUS_INTERFACES += ../../interfaces/notificationapi.xml -DBUS_INTERFACES += ../../interfaces/notification.xml - -icons.files = ../../assets/etc/draft/icons/oxide-splash.png -icons.path = /opt/etc/draft/icons +applications.files = ../../assets/opt/usr/share/applications/codes.eeems.oxide.oxide +applications.path = /opt/usr/share/applications/ +INSTALLS += applications + +icons.files = ../../assets/opt/usr/share/icons/oxide/702x702/splash/oxide.png +icons.path = /opt/usr/share/icons/oxide/702x702/splash/ INSTALLS += icons configFile.files = ../../assets/etc/oxide.conf diff --git a/applications/launcher/notificationlist.h b/applications/launcher/notificationlist.h index 7a6ace23b..015eaf9fd 100644 --- a/applications/launcher/notificationlist.h +++ b/applications/launcher/notificationlist.h @@ -3,8 +3,6 @@ #include -#include "notification_interface.h" - using namespace codes::eeems::oxide1; class NotificationItem : public QObject { diff --git a/applications/launcher/oxide_stable.h b/applications/launcher/oxide_stable.h index 01f9cf1b4..77ee343f7 100644 --- a/applications/launcher/oxide_stable.h +++ b/applications/launcher/oxide_stable.h @@ -34,17 +34,6 @@ #include #include -#include "application_interface.h" -#include "appsapi_interface.h" -#include "bss_interface.h" -#include "dbusservice_interface.h" -#include "network_interface.h" -#include "notification_interface.h" -#include "notificationapi_interface.h" -#include "powerapi_interface.h" -#include "systemapi_interface.h" -#include "wifiapi_interface.h" - #include "controller.h" #include "mxcfb.h" #endif diff --git a/applications/launcher/wifinetworklist.h b/applications/launcher/wifinetworklist.h index a4a9c813d..5eb3fbb5d 100644 --- a/applications/launcher/wifinetworklist.h +++ b/applications/launcher/wifinetworklist.h @@ -2,14 +2,7 @@ #define WIFINETWORK_H #include - -#include "wifiapi_interface.h" -#include "network_interface.h" -#include "bss_interface.h" - -#ifndef OXIDE_SERVICE -#define OXIDE_SERVICE "codes.eeems.oxide1" -#endif +#include using namespace codes::eeems::oxide1; diff --git a/applications/lockscreen/controller.h b/applications/lockscreen/controller.h index 19d443733..bd9c64ee6 100644 --- a/applications/lockscreen/controller.h +++ b/applications/lockscreen/controller.h @@ -5,13 +5,6 @@ #include #include -#include "dbusservice_interface.h" -#include "systemapi_interface.h" -#include "powerapi_interface.h" -#include "wifiapi_interface.h" -#include "appsapi_interface.h" -#include "application_interface.h" - using namespace codes::eeems::oxide1; using namespace Oxide::Sentry; diff --git a/applications/lockscreen/lockscreen.pro b/applications/lockscreen/lockscreen.pro index 9b0b949ac..4018bfef9 100644 --- a/applications/lockscreen/lockscreen.pro +++ b/applications/lockscreen/lockscreen.pro @@ -17,12 +17,9 @@ include(../../qmake/common.pri) target.path = /opt/bin INSTALLS += target -DBUS_INTERFACES += ../../interfaces/dbusservice.xml -DBUS_INTERFACES += ../../interfaces/systemapi.xml -DBUS_INTERFACES += ../../interfaces/powerapi.xml -DBUS_INTERFACES += ../../interfaces/wifiapi.xml -DBUS_INTERFACES += ../../interfaces/appsapi.xml -DBUS_INTERFACES += ../../interfaces/application.xml +applications.files = ../../assets/opt/usr/share/applications/codes.eeems.decay.oxide +applications.path = /opt/usr/share/applications/ +INSTALLS += applications HEADERS += \ controller.h diff --git a/applications/lockscreen/main.cpp b/applications/lockscreen/main.cpp index 7ae3f6a46..946ad7d06 100644 --- a/applications/lockscreen/main.cpp +++ b/applications/lockscreen/main.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "controller.h" @@ -12,8 +13,6 @@ Q_IMPORT_PLUGIN(QsgEpaperPlugin) #endif -#include "dbusservice_interface.h" - using namespace codes::eeems::oxide1; using namespace Oxide; using namespace Oxide::Sentry; diff --git a/applications/notify-send/main.cpp b/applications/notify-send/main.cpp index 93b0024eb..428edd7ff 100644 --- a/applications/notify-send/main.cpp +++ b/applications/notify-send/main.cpp @@ -6,10 +6,6 @@ #include -#include "dbusservice_interface.h" -#include "notificationapi_interface.h" -#include "notification_interface.h" - using namespace codes::eeems::oxide1; using namespace Oxide::Sentry; using namespace Oxide::JSON; diff --git a/applications/notify-send/notify-send.pro b/applications/notify-send/notify-send.pro index c847059c1..49c8d89c5 100644 --- a/applications/notify-send/notify-send.pro +++ b/applications/notify-send/notify-send.pro @@ -12,10 +12,6 @@ SOURCES += \ HEADERS += -DBUS_INTERFACES += ../../interfaces/dbusservice.xml -DBUS_INTERFACES += ../../interfaces/notificationapi.xml -DBUS_INTERFACES += ../../interfaces/notification.xml - TARGET = notify-send include(../../qmake/common.pri) target.path = /opt/bin diff --git a/applications/process-manager/process-manager.pro b/applications/process-manager/process-manager.pro index 76cf47d60..2d3552b4b 100755 --- a/applications/process-manager/process-manager.pro +++ b/applications/process-manager/process-manager.pro @@ -17,13 +17,19 @@ include(../../qmake/common.pri) target.path = /opt/bin INSTALLS += target -icons.files += \ - ../../assets/etc/draft/icons/erode.svg \ - ../../assets/etc/draft/icons/erode-splash.png -icons.path = /opt/etc/draft/icons +applications.files = ../../assets/opt/usr/share/applications/codes.eeems.erode.oxide +applications.path = /opt/usr/share/applications/ +INSTALLS += applications +icons.path = /opt/usr/share/icons/oxide/48x48/apps/ +icons.files += ../../assets/opt/usr/share/icons/oxide/48x48/apps/erode.png INSTALLS += icons +splash.path = /opt/usr/share/icons/oxide/702x702/splash/ +splash.files += ../../assets/opt/usr/share/icons/oxide/702x702/splash/erode.png +INSTALLS += splash + + HEADERS += \ controller.h \ taskitem.h \ diff --git a/applications/process-manager/tasklist.h b/applications/process-manager/tasklist.h index 3c2e7de7c..9b2e184c1 100644 --- a/applications/process-manager/tasklist.h +++ b/applications/process-manager/tasklist.h @@ -179,8 +179,7 @@ class TaskList : public QAbstractListModel qCritical() << "Unable to access /proc"; return; } - directory.setFilter( QDir::Dirs | QDir::NoDot | QDir::NoDotDot); - auto processes = directory.entryInfoList(QDir::NoFilter, QDir::SortFlag::Name); + auto processes = directory.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable); // Get all pids we care about for(QFileInfo fi : processes){ std::string pid = fi.baseName().toStdString(); diff --git a/applications/screenshot-tool/main.cpp b/applications/screenshot-tool/main.cpp index 9ce640419..afc9b9fdb 100644 --- a/applications/screenshot-tool/main.cpp +++ b/applications/screenshot-tool/main.cpp @@ -6,13 +6,6 @@ #include #include -#include "dbusservice_interface.h" -#include "systemapi_interface.h" -#include "screenapi_interface.h" -#include "screenshot_interface.h" -#include "notificationapi_interface.h" -#include "notification_interface.h" - using namespace codes::eeems::oxide1; using namespace Oxide::Sentry; diff --git a/applications/screenshot-tool/screenshot-tool.pro b/applications/screenshot-tool/screenshot-tool.pro index b9d7b5a3f..4342a9b2f 100644 --- a/applications/screenshot-tool/screenshot-tool.pro +++ b/applications/screenshot-tool/screenshot-tool.pro @@ -15,12 +15,9 @@ include(../../qmake/common.pri) target.path = /opt/bin INSTALLS += target -DBUS_INTERFACES += ../../interfaces/dbusservice.xml -DBUS_INTERFACES += ../../interfaces/systemapi.xml -DBUS_INTERFACES += ../../interfaces/screenapi.xml -DBUS_INTERFACES += ../../interfaces/screenshot.xml -DBUS_INTERFACES += ../../interfaces/notificationapi.xml -DBUS_INTERFACES += ../../interfaces/notification.xml +applications.files = ../../assets/opt/usr/share/applications/codes.eeems.fret.oxide +applications.path = /opt/usr/share/applications/ +INSTALLS += applications include(../../qmake/epaper.pri) include(../../qmake/liboxide.pri) diff --git a/applications/screenshot-viewer/controller.h b/applications/screenshot-viewer/controller.h index ad319b62d..01eeafec6 100644 --- a/applications/screenshot-viewer/controller.h +++ b/applications/screenshot-viewer/controller.h @@ -8,10 +8,6 @@ #include "epframebuffer.h" -#include "dbusservice_interface.h" -#include "screenapi_interface.h" -#include "screenshot_interface.h" - #include "screenshotlist.h" using namespace codes::eeems::oxide1; diff --git a/applications/screenshot-viewer/main.cpp b/applications/screenshot-viewer/main.cpp index 74abd5934..aedc4c3ef 100644 --- a/applications/screenshot-viewer/main.cpp +++ b/applications/screenshot-viewer/main.cpp @@ -13,8 +13,6 @@ Q_IMPORT_PLUGIN(QsgEpaperPlugin) #endif -#include "dbusservice_interface.h" - using namespace std; using namespace Oxide; using namespace Oxide::Sentry; diff --git a/applications/screenshot-viewer/screenshot-viewer.pro b/applications/screenshot-viewer/screenshot-viewer.pro index d2e156ee7..789474b8a 100644 --- a/applications/screenshot-viewer/screenshot-viewer.pro +++ b/applications/screenshot-viewer/screenshot-viewer.pro @@ -18,16 +18,17 @@ include(../../qmake/common.pri) target.path = /opt/bin INSTALLS += target -icons.files += \ - ../../assets/etc/draft/icons/image.svg \ - ../../assets/etc/draft/icons/anxiety-splash.png +applications.files = ../../assets/opt/usr/share/applications/codes.eeems.anxiety.oxide +applications.path = /opt/usr/share/applications/ +INSTALLS += applications -icons.path = /opt/etc/draft/icons +icons.files += ../../assets/opt/usr/share/icons/oxide/48x48/apps/image.png +icons.path = /opt/usr/share/icons/oxide/48x48/apps INSTALLS += icons -DBUS_INTERFACES += ../../interfaces/dbusservice.xml -DBUS_INTERFACES += ../../interfaces/screenapi.xml -DBUS_INTERFACES += ../../interfaces/screenshot.xml +splash.files += ../../assets/opt/usr/share/icons/oxide/702x702/splash/anxiety.png +splash.path = /opt/usr/share/icons/oxide/702x702/splash +INSTALLS += splash HEADERS += \ controller.h \ diff --git a/applications/screenshot-viewer/screenshotlist.h b/applications/screenshot-viewer/screenshotlist.h index 767496624..af9643ccb 100644 --- a/applications/screenshot-viewer/screenshotlist.h +++ b/applications/screenshot-viewer/screenshotlist.h @@ -2,8 +2,7 @@ #define SCREENSHOTLIST_H #include - -#include "screenshot_interface.h" +#include using namespace codes::eeems::oxide1; diff --git a/applications/settings-manager/main.cpp b/applications/settings-manager/main.cpp index 736b52a39..c77461d45 100644 --- a/applications/settings-manager/main.cpp +++ b/applications/settings-manager/main.cpp @@ -1,25 +1,9 @@ #include -#include -#include #include -#include #include #include -#include "dbusservice_interface.h" -#include "powerapi_interface.h" -#include "wifiapi_interface.h" -#include "network_interface.h" -#include "bss_interface.h" -#include "appsapi_interface.h" -#include "application_interface.h" -#include "systemapi_interface.h" -#include "screenapi_interface.h" -#include "screenshot_interface.h" -#include "notificationapi_interface.h" -#include "notification_interface.h" - using namespace codes::eeems::oxide1; using namespace Oxide::Sentry; using namespace Oxide::JSON; diff --git a/applications/settings-manager/settings-manager.pro b/applications/settings-manager/settings-manager.pro index 75a80afe7..e7046ca41 100644 --- a/applications/settings-manager/settings-manager.pro +++ b/applications/settings-manager/settings-manager.pro @@ -12,19 +12,6 @@ SOURCES += \ HEADERS += -DBUS_INTERFACES += ../../interfaces/dbusservice.xml -DBUS_INTERFACES += ../../interfaces/powerapi.xml -DBUS_INTERFACES += ../../interfaces/wifiapi.xml -DBUS_INTERFACES += ../../interfaces/network.xml -DBUS_INTERFACES += ../../interfaces/bss.xml -DBUS_INTERFACES += ../../interfaces/appsapi.xml -DBUS_INTERFACES += ../../interfaces/application.xml -DBUS_INTERFACES += ../../interfaces/systemapi.xml -DBUS_INTERFACES += ../../interfaces/screenapi.xml -DBUS_INTERFACES += ../../interfaces/screenshot.xml -DBUS_INTERFACES += ../../interfaces/notificationapi.xml -DBUS_INTERFACES += ../../interfaces/notification.xml - TARGET = rot include(../../qmake/common.pri) target.path = /opt/bin diff --git a/applications/system-service/application.cpp b/applications/system-service/application.cpp index 567e73acb..c80707f1f 100644 --- a/applications/system-service/application.cpp +++ b/applications/system-service/application.cpp @@ -10,6 +10,8 @@ #include "buttonhandler.h" #include "digitizerhandler.h" +using namespace Oxide::Applications; + const event_device touchScreen(deviceSettings.getTouchDevicePath(), O_WRONLY); void Application::launch(){ @@ -61,15 +63,15 @@ void Application::launchNoSecurityCheck(){ m_process->setUser(user()); m_process->setGroup(group()); if(p_stdout == nullptr){ - int fd = sd_journal_stream_fd(name().toStdString().c_str(), LOG_INFO, 1); - if (fd < 0) { - errno = -fd; - qDebug() << "Failed to create stdout fd:" << -fd; + p_stdout_fd = sd_journal_stream_fd(name().toStdString().c_str(), LOG_INFO, 1); + if (p_stdout_fd < 0) { + errno = -p_stdout_fd; + qDebug() << "Failed to create stdout fd:" << -p_stdout_fd; }else{ - FILE* log = fdopen(fd, "w"); + FILE* log = fdopen(p_stdout_fd, "w"); if(!log){ qDebug() << "Failed to create stdout FILE:" << errno; - close(fd); + close(p_stdout_fd); }else{ p_stdout = new QTextStream(log); qDebug() << "Opened stdout for " << name(); @@ -77,15 +79,15 @@ void Application::launchNoSecurityCheck(){ } } if(p_stderr == nullptr){ - int fd = sd_journal_stream_fd(name().toStdString().c_str(), LOG_ERR, 1); - if (fd < 0) { - errno = -fd; - qDebug() << "Failed to create sterr fd:" << -fd; + p_stderr_fd = sd_journal_stream_fd(name().toStdString().c_str(), LOG_ERR, 1); + if (p_stderr_fd < 0) { + errno = -p_stderr_fd; + qDebug() << "Failed to create sterr fd:" << -p_stderr_fd; }else{ - FILE* log = fdopen(fd, "w"); + FILE* log = fdopen(p_stderr_fd, "w"); if(!log){ qDebug() << "Failed to create stderr FILE:" << errno; - close(fd); + close(p_stderr_fd); }else{ p_stderr = new QTextStream(log); qDebug() << "Opened stderr for " << name(); @@ -94,7 +96,7 @@ void Application::launchNoSecurityCheck(){ } m_process->start(); m_process->waitForStarted(); - if(type() == AppsAPI::Background){ + if(type() == Background){ startSpan("background", "Application is in the background"); }else{ startSpan("foreground", "Application is in the foreground"); @@ -112,7 +114,7 @@ void Application::pauseNoSecurityCheck(bool startIfNone){ if( !m_process->processId() || stateNoSecurityCheck() == Paused - || type() == AppsAPI::Background + || type() == Background ){ return; } @@ -141,7 +143,7 @@ void Application::interruptApplication(){ if( !m_process->processId() || stateNoSecurityCheck() == Paused - || type() == AppsAPI::Background + || type() == Background ){ return; } @@ -160,11 +162,11 @@ void Application::interruptApplication(){ } Oxide::Sentry::sentry_span(t, "background", "Background application", [this](){ switch(type()){ - case AppsAPI::Background: + case Background: // Already in the background. How did we get here? startSpan("background", "Application is in the background"); return; - case AppsAPI::Backgroundable: + case Backgroundable: qDebug() << "Waiting for SIGUSR2 ack"; appsAPI->connectSignals(this, 2); kill(-m_process->processId(), SIGUSR2); @@ -184,7 +186,7 @@ void Application::interruptApplication(){ startSpan("background", "Application is in the background"); } break; - case AppsAPI::Foreground: + case Foreground: default: kill(-m_process->processId(), SIGSTOP); waitForPause(); @@ -217,7 +219,7 @@ void Application::resumeNoSecurityCheck(){ if( !m_process->processId() || stateNoSecurityCheck() == InForeground - || (type() == AppsAPI::Background && stateNoSecurityCheck() == InBackground) + || (type() == Background && stateNoSecurityCheck() == InBackground) ){ qDebug() << "Can't Resume" << path() << "Already running!"; return; @@ -233,7 +235,7 @@ void Application::resumeNoSecurityCheck(){ appsAPI->recordPreviousApplication(); qDebug() << "Resuming " << path(); appsAPI->pauseAll(); - if(!flags().contains("nosavescreen") && (type() != AppsAPI::Backgroundable || stateNoSecurityCheck() == Paused)){ + if(!flags().contains("nosavescreen") && (type() != Backgroundable || stateNoSecurityCheck() == Paused)){ recallScreen(); } uninterruptApplication(); @@ -247,7 +249,7 @@ void Application::uninterruptApplication(){ if( !m_process->processId() || stateNoSecurityCheck() == InForeground - || (type() == AppsAPI::Background && stateNoSecurityCheck() == InBackground) + || (type() == Background && stateNoSecurityCheck() == InBackground) ){ return; } @@ -266,8 +268,8 @@ void Application::uninterruptApplication(){ } Oxide::Sentry::sentry_span(t, "foreground", "Foreground application", [this](){ switch(type()){ - case AppsAPI::Background: - case AppsAPI::Backgroundable: + case Background: + case Backgroundable: if(stateNoSecurityCheck() == Paused){ touchHandler->clear_buffer(); kill(-m_process->processId(), SIGCONT); @@ -286,7 +288,7 @@ void Application::uninterruptApplication(){ m_backgrounded = false; startSpan("background", "Application is in the background"); break; - case AppsAPI::Foreground: + case Foreground: default: touchHandler->clear_buffer(); kill(-m_process->processId(), SIGCONT); @@ -391,7 +393,7 @@ int Application::stateNoSecurityCheck(){ return Paused; } } - if(type() == AppsAPI::Background || (type() == AppsAPI::Backgroundable && m_backgrounded)){ + if(type() == Background || (type() == Backgroundable && m_backgrounded)){ return InBackground; } return InForeground; @@ -404,7 +406,7 @@ int Application::stateNoSecurityCheck(){ void Application::setConfig(const QVariantMap& config){ auto oldBin = bin(); m_config = config; - if(type() == AppsAPI::Foreground){ + if(type() == Foreground){ setAutoStart(false); } if(oldBin == bin()){ @@ -424,6 +426,9 @@ void Application::finished(int exitCode){ appsAPI->resumeIfNone(); emit appsAPI->applicationExited(qPath(), exitCode); umountAll(); + if(transient()){ + unregister(); + } } void Application::errorOccurred(QProcess::ProcessError error){ switch(error){ @@ -431,6 +436,9 @@ void Application::errorOccurred(QProcess::ProcessError error){ qDebug() << "Application" << name() << "failed to start."; emit exited(-1); emit appsAPI->applicationExited(qPath(), -1); + if(transient()){ + unregister(); + } break; case QProcess::Crashed: qDebug() << "Application" << name() << "crashed."; diff --git a/applications/system-service/application.h b/applications/system-service/application.h index 188270542..243645166 100644 --- a/applications/system-service/application.h +++ b/applications/system-service/application.h @@ -23,8 +23,6 @@ #include #include #include -#include -#include #include #include #include @@ -52,7 +50,7 @@ class SandBoxProcess : public QProcess{ bool setUser(const QString& name){ try{ - m_uid = getUID(name); + m_uid = Oxide::getUID(name); return true; } catch(const std::runtime_error&){ @@ -61,7 +59,7 @@ class SandBoxProcess : public QProcess{ } bool setGroup(const QString& name){ try{ - m_gid = getGID(name); + m_gid = Oxide::getGID(name); return true; } catch(const std::runtime_error&){ @@ -105,33 +103,6 @@ class SandBoxProcess : public QProcess{ uid_t m_uid; QString m_chroot; mode_t m_mask; - - uid_t getUID(const QString& name){ - char buffer[1024]; - struct passwd user; - struct passwd* result; - auto status = getpwnam_r(name.toStdString().c_str(), &user, buffer, sizeof(buffer), &result); - if(status != 0){ - throw std::runtime_error("Failed to get user" + status); - } - if(result == NULL){ - throw std::runtime_error("Invalid user name: " + name.toStdString()); - } - return result->pw_uid; - } - gid_t getGID(const QString& name){ - char buffer[1024]; - struct group grp; - struct group* result; - auto status = getgrnam_r(name.toStdString().c_str(), &grp, buffer, sizeof(buffer), &result); - if(status != 0){ - throw std::runtime_error("Failed to get group" + status); - } - if(result == NULL){ - throw std::runtime_error("Invalid group name: " + name.toStdString()); - } - return result->gr_gid; - } }; class Application : public QObject{ @@ -152,6 +123,7 @@ class Application : public QObject{ Q_PROPERTY(int state READ state) Q_PROPERTY(bool systemApp READ systemApp) Q_PROPERTY(bool hidden READ hidden) + Q_PROPERTY(bool transient READ transient) Q_PROPERTY(QString icon READ icon WRITE setIcon NOTIFY iconChanged) Q_PROPERTY(QString splash READ splash WRITE setSplash NOTIFY splashChanged) Q_PROPERTY(QVariantMap environment READ environment NOTIFY environmentChanged) @@ -184,11 +156,21 @@ class Application : public QObject{ } umountAll(); if(p_stdout != nullptr){ + p_stdout->flush(); delete p_stdout; } + if(p_stdout_fd > 0){ + close(p_stdout_fd); + p_stdout_fd = -1; + } if(p_stderr != nullptr){ + p_stderr->flush(); delete p_stderr; } + if(p_stderr_fd > 0){ + close(p_stderr_fd); + p_stderr_fd = -1; + } } QString path() { return m_path; } @@ -282,6 +264,7 @@ class Application : public QObject{ } } bool systemApp() { return flags().contains("system"); } + bool transient() { return flags().contains("transient"); } bool hidden() { return flags().contains("hidden"); } int type() { return (int)value("type", 0).toInt(); } int state(){ @@ -291,15 +274,35 @@ class Application : public QObject{ return stateNoSecurityCheck(); } int stateNoSecurityCheck(); - QString icon() { return value("icon", "").toString(); } + QString icon(){ + auto _icon = value("icon", "").toString(); + if(_icon.isEmpty() || !_icon.contains("-") || QFile::exists(_icon)){ + return _icon; + } + auto path = Oxide::Applications::iconPath(_icon); + if(path.isEmpty()){ + return _icon; + } + return path; + } void setIcon(QString icon){ if(!hasPermission("permissions")){ return; } setValue("icon", icon); - emit iconChanged(icon); + emit iconChanged(this->icon()); + } + QString splash(){ + auto _splash = value("splash", "").toString(); + if(_splash.isEmpty() || !_splash.contains("-") || QFile::exists(_splash)){ + return _splash; + } + auto path = Oxide::Applications::iconPath(_splash); + if(path.isEmpty()){ + return _splash; + } + return path; } - QString splash() { return value("splash", "").toString(); } void setSplash(QString splash){ if(!hasPermission("permissions")){ return; @@ -520,7 +523,9 @@ private slots: QMap fifos; Oxide::Sentry::Transaction* transaction = nullptr; Oxide::Sentry::Span* span = nullptr; + int p_stdout_fd = -1; QTextStream* p_stdout = nullptr; + int p_stderr_fd = -1; QTextStream* p_stderr = nullptr; bool hasPermission(QString permission, const char* sender = __builtin_FUNCTION()); @@ -602,8 +607,8 @@ private slots: return; } auto cpath = path.toStdString(); - ::umount(cpath.c_str()); - if(isMounted(path)){ + auto ret = ::umount2(cpath.c_str(), MNT_DETACH); + if((ret && ret != EINVAL && ret != ENOENT) || isMounted(path)){ qDebug() << "umount failed" << path; return; } diff --git a/applications/system-service/appsapi.h b/applications/system-service/appsapi.h index 15025bcb0..80c8b3b93 100644 --- a/applications/system-service/appsapi.h +++ b/applications/system-service/appsapi.h @@ -22,6 +22,7 @@ #define appsAPI AppsAPI::singleton() using namespace Oxide; +using namespace Oxide::Applications; class AppsAPI : public APIBase { Q_OBJECT @@ -103,9 +104,6 @@ class AppsAPI : public APIBase { void startup(); int state() { return 0; } // Ignore this, it's a kludge to get the xml to generate - enum ApplicationType { Foreground, Background, Backgroundable}; - Q_ENUM(ApplicationType) - void setEnabled(bool enabled){ qDebug() << "Apps API" << enabled; for(auto app : applications){ @@ -126,8 +124,8 @@ class AppsAPI : public APIBase { QDBusObjectPath registerApplicationNoSecurityCheck(QVariantMap properties){ QString name = properties.value("name", "").toString(); QString bin = properties.value("bin", "").toString(); - int type = properties.value("type", Foreground).toInt(); - if(type < Foreground || type > Backgroundable){ + int type = properties.value("type", ApplicationType::Foreground).toInt(); + if(type < ApplicationType::Foreground || type > ApplicationType::Backgroundable){ qDebug() << "Invalid configuration: Invalid type" << type; return QDBusObjectPath("/"); } @@ -613,7 +611,7 @@ public slots: settings.endArray(); for(auto name : applications.keys()){ auto app = applications[name]; - if(!names.contains(name) && !app->systemApp()){ + if(!names.contains(name) && !app->systemApp() && !app->transient()){ app->unregisterNoSecurityCheck(); } } @@ -671,16 +669,11 @@ public slots: } settings.endArray(); // Load system applications from disk - QDir dir("/opt/usr/share/applications/"); + QDir dir(OXIDE_APPLICATION_REGISTRATIONS_DIRECTORY); dir.setNameFilters(QStringList() << "*.oxide"); QMap apps; for(auto entry : dir.entryInfoList()){ - QFile file(entry.filePath()); - if(!file.open(QIODevice::ReadOnly)){ - continue; - } - auto data = file.readAll(); - auto app = QJsonDocument::fromJson(data).object(); + auto app = getRegistration(entry.filePath()); if(app.isEmpty()){ qDebug() << "Invalid file " << entry.filePath(); continue; @@ -700,15 +693,6 @@ public slots: // Register/Update any system application. for(auto app : apps){ auto name = app["name"].toString(); - int type = Foreground; - QString typeString = app.contains("type") ? app["type"].toString().toLower() : ""; - if(typeString == "background"){ - type = Background; - }else if(typeString == "backgroundable"){ - type = Backgroundable; - }else if(!typeString.isEmpty() && typeString != "foreground"){ - qDebug() << "Invalid type string:" << typeString; - } auto bin = app["bin"].toString(); if(bin.isEmpty() || !QFile::exists(bin)){ qDebug() << name << "Can't find application binary:" << bin; @@ -717,76 +701,13 @@ public slots: #endif continue; } - auto flags = QStringList() << "system"; - if(app.contains("flags")){ - for(auto flag : app["flags"].toArray()){ - auto value = flag.toString(); - if(!value.isEmpty() && value != "system"){ - flags << value; - } - } - } - QVariantMap properties { - {"name", name}, - {"bin", bin}, - {"type", type}, - {"flags", flags}, - }; - if(app.contains("displayName")){ - properties.insert("displayName", app["displayName"].toString()); - } - if(app.contains("description")){ - properties.insert("description", app["description"].toString()); - } - if(app.contains("icon")){ - properties.insert("icon", app["icon"].toString()); - } - if(app.contains("user")){ - properties.insert("user", app["user"].toString()); - } - if(app.contains("group")){ - properties.insert("group", app["group"].toString()); - } - if(app.contains("workingDirectory")){ - properties.insert("workingDirectory", app["workingDirectory"].toString()); - } - if(app.contains("directories")){ - QStringList directories; - for(auto directory : app["directories"].toArray()){ - directories.append(directory.toString()); - } - properties.insert("directories", directories); - } - if(app.contains("permissions")){ - QStringList permissions; - for(auto permission : app["permissions"].toArray()){ - permissions.append(permission.toString()); - } - properties.insert("permissions", permissions); - } - if(app.contains("events")){ - auto events = app["events"].toObject(); - for(auto event : events.keys()){ - if(event == "stop"){ - properties.insert("onStop", events[event].toString()); - }else if(event == "pause"){ - properties.insert("onPause", events[event].toString()); - }else if(event == "resume"){ - properties.insert("onResume", events[event].toString()); - } - } - } - if(app.contains("environment")){ - QVariantMap envMap; - auto environment = app["environment"].toObject(); - for(auto key : environment.keys()){ - envMap.insert(key, environment[key].toString()); - } - properties.insert("environment", envMap); - } - if(app.contains("splash")){ - properties.insert("splash", app["splash"].toString()); + if(!app.contains("flags") || !app["flags"].isArray()){ + app["flags"] = QJsonArray(); } + auto flags = app["flags"].toArray(); + flags.prepend("system"); + app["flags"] = flags; + auto properties = registrationToMap(app); if(applications.contains(name)){ #ifdef DEBUG qDebug() << "Updating " << name; diff --git a/applications/system-service/main.cpp b/applications/system-service/main.cpp index 5c43063f9..066b65cf4 100755 --- a/applications/system-service/main.cpp +++ b/applications/system-service/main.cpp @@ -1,7 +1,9 @@ #include #include +#include #include +#include #include #include "dbusservice.h" @@ -9,14 +11,36 @@ using namespace std; using namespace Oxide::Sentry; -const char *qt_version = qVersion(); +const char* qt_version = qVersion(); +const std::string runPath = "/run/oxide"; +const char* pidPath = "/run/oxide/oxide.pid"; +const char* lockPath = "/run/oxide/oxide.lock"; void sigHandler(int signal){ ::signal(signal, SIG_DFL); qApp->quit(); } +bool stopProcess(pid_t pid){ + if(pid <= 1){ + return false; + } + qDebug() << "Waiting for other instance to stop..."; + kill(pid, SIGTERM); + int tries = 0; + while(0 == kill(pid, 0)){ + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if(++tries == 50){ + qDebug() << "Instance is taking too long, killing..."; + kill(pid, SIGKILL); + }else if(tries == 60){ + qDebug() << "Unable to kill process"; + return false; + } + } + return true; +} -int main(int argc, char *argv[]){ +int main(int argc, char* argv[]){ if(deviceSettings.getDeviceType() == Oxide::DeviceSettings::RM2 && getenv("RM2FB_ACTIVE") == nullptr){ O_WARNING("rm2fb not detected. Running xochitl instead!"); return QProcess::execute("/usr/bin/xochitl", QStringList()); @@ -40,9 +64,60 @@ int main(int argc, char *argv[]){ parser.addHelpOption(); parser.applicationDescription(); parser.addVersionOption(); + QCommandLineOption breakLockOption( + {"f", "break-lock"}, + "Break existing locks and force startup if another version of tarnish is already running" + ); + parser.addOption(breakLockOption); parser.process(app); - const QStringList args = parser.positionalArguments(); + if(!args.isEmpty()){ + parser.showHelp(EXIT_FAILURE); + } + auto actualPid = QString::number(app.applicationPid()); + QString pid = Oxide::execute( + "systemctl", + QStringList() << "--no-pager" << "show" << "--property" << "MainPID" << "--value" << "tarnish" + ).trimmed(); + if(pid != "0" && pid != actualPid){ + if(!parser.isSet(breakLockOption)){ + qDebug() << "tarnish.service is already running"; + return EXIT_FAILURE; + } + if(QProcess::execute("systemctl", QStringList() << "stop" << "tarnish")){ + qDebug() << "tarnish.service is already running"; + qDebug() << "Unable to stop service"; + return EXIT_FAILURE; + } + } + qDebug() << "Creating lock file" << lockPath; + if(!QFile::exists(QString::fromStdString(runPath)) && !std::filesystem::create_directories(runPath)){ + qDebug() << "Failed to create" << runPath.c_str(); + return EXIT_FAILURE; + } + int lock = Oxide::tryGetLock(lockPath); + if(lock < 0){ + qDebug() << "Unable to establish lock on" << lockPath << strerror(errno); + if(!parser.isSet(breakLockOption)){ + return EXIT_FAILURE; + } + qDebug() << "Attempting to stop all other instances of tarnish" << lockPath; + for(auto lockingPid : Oxide::lsof(lockPath)){ + if(Oxide::processExists(lockingPid)){ + stopProcess(lockingPid); + } + } + lock = Oxide::tryGetLock(lockPath); + if(lock < 0){ + qDebug() << "Unable to establish lock on" << lockPath << strerror(errno); + return EXIT_FAILURE; + } + } + + QObject::connect(&app, &QGuiApplication::aboutToQuit, [lock]{ + qDebug() << "Releasing lock " << lockPath; + Oxide::releaseLock(lock, lockPath); + }); signal(SIGINT, sigHandler); signal(SIGSEGV, sigHandler); @@ -52,10 +127,16 @@ int main(int argc, char *argv[]){ QTimer::singleShot(0, []{ dbusService->startup(); }); - system("mkdir -p /run/oxide"); - system(("echo " + to_string(app.applicationPid()) + " > /run/oxide/oxide.pid").c_str()); + QFile pidFile(pidPath); + if(!pidFile.open(QFile::ReadWrite)){ + qWarning() << "Unable to create " << pidPath; + return app.exec(); + } + pidFile.seek(0); + pidFile.write(actualPid.toUtf8()); + pidFile.close(); QObject::connect(&app, &QGuiApplication::aboutToQuit, []{ - remove("/run/oxide/oxide.pid"); + remove(pidPath); }); return app.exec(); } diff --git a/applications/system-service/system-service.pro b/applications/system-service/system-service.pro index 5c4c6722c..ed26c2a78 100644 --- a/applications/system-service/system-service.pro +++ b/applications/system-service/system-service.pro @@ -31,7 +31,7 @@ include(../../qmake/common.pri) target.path = /opt/bin INSTALLS += target -configFile.files = ../../assets/etc/dbus-1/system.d/* +configFile.files = ../../assets/etc/dbus-1/system.d/codes.eeems.oxide.conf configFile.path = /etc/dbus-1/system.d/ INSTALLS += configFile @@ -39,10 +39,14 @@ service.files = ../../assets/etc/systemd/system/tarnish.service service.path = /etc/systemd/system/ INSTALLS += service -applications.files = ../../assets/opt/usr/share/applications/* +applications.files = ../../assets/opt/usr/share/applications/xochitl.oxide applications.path = /opt/usr/share/applications/ INSTALLS += applications +icons.files += ../../assets/opt/usr/share/icons/oxide/48x48/apps/xochitl.png +icons.path = /opt/usr/share/icons/oxide/48x48/apps +INSTALLS += icons + system(qdbusxml2cpp -N -p wpa_supplicant.h:wpa_supplicant.cpp fi.w1.wpa_supplicant1.xml) DBUS_INTERFACES += org.freedesktop.login1.xml @@ -79,8 +83,6 @@ LIBS += -lsystemd LIBS += -lz DISTFILES += \ - ../../assets/opt/usr/share/applications/codes.eeems.anxiety.oxide \ - ../../assets/opt/usr/share/applications/codes.eeems.corrupt.oxide \ fi.w1.wpa_supplicant1.xml \ generate_xml.sh \ org.freedesktop.login1.xml diff --git a/applications/task-switcher/appitem.cpp b/applications/task-switcher/appitem.cpp index 21a44199f..9f860158f 100755 --- a/applications/task-switcher/appitem.cpp +++ b/applications/task-switcher/appitem.cpp @@ -8,8 +8,6 @@ #include #include "appitem.h" -#include "dbusservice_interface.h" -#include "appsapi_interface.h" #include "controller.h" bool AppItem::ok(){ return getApp() != nullptr; } diff --git a/applications/task-switcher/appitem.h b/applications/task-switcher/appitem.h index d195126d3..b2b2c8927 100644 --- a/applications/task-switcher/appitem.h +++ b/applications/task-switcher/appitem.h @@ -1,13 +1,7 @@ #ifndef APP_H #define APP_H #include - -#include "application_interface.h" - -#ifndef OXIDE_SERVICE -#define OXIDE_SERVICE "codes.eeems.oxide1" -#define OXIDE_SERVICE_PATH "/codes/eeems/oxide1" -#endif +#include using namespace codes::eeems::oxide1; diff --git a/applications/task-switcher/controller.h b/applications/task-switcher/controller.h index b9d4d11e2..3400bd259 100644 --- a/applications/task-switcher/controller.h +++ b/applications/task-switcher/controller.h @@ -11,11 +11,6 @@ #include #include -#include "dbusservice_interface.h" -#include "screenapi_interface.h" -#include "appsapi_interface.h" -#include "application_interface.h" - #include "screenprovider.h" #include "appitem.h" @@ -125,7 +120,6 @@ class Controller : public QObject { for(auto item : runningApplications){ auto path = item.value().path(); Application app(OXIDE_SERVICE, path, bus, this); - qDebug() << app.name() << app.hidden(); if(app.hidden()){ continue; } diff --git a/applications/task-switcher/corrupt_stable.h b/applications/task-switcher/corrupt_stable.h index db7eb5285..a22fb4ffd 100644 --- a/applications/task-switcher/corrupt_stable.h +++ b/applications/task-switcher/corrupt_stable.h @@ -20,11 +20,6 @@ #include #include -#include "application_interface.h" -#include "appsapi_interface.h" -#include "dbusservice_interface.h" -#include "screenapi_interface.h" - #include "controller.h" #include "screenprovider.h" #endif diff --git a/applications/task-switcher/main.cpp b/applications/task-switcher/main.cpp index 1c18b3855..c56934d0f 100644 --- a/applications/task-switcher/main.cpp +++ b/applications/task-switcher/main.cpp @@ -16,8 +16,6 @@ Q_IMPORT_PLUGIN(QsgEpaperPlugin) #endif -#include "dbusservice_interface.h" - using namespace std; using namespace Oxide; using namespace Oxide::Sentry; diff --git a/applications/task-switcher/task-switcher.pro b/applications/task-switcher/task-switcher.pro index 5eb76cd26..5f7faa0f2 100644 --- a/applications/task-switcher/task-switcher.pro +++ b/applications/task-switcher/task-switcher.pro @@ -19,10 +19,9 @@ include(../../qmake/common.pri) target.path = /opt/bin INSTALLS += target -DBUS_INTERFACES += ../../interfaces/dbusservice.xml -DBUS_INTERFACES += ../../interfaces/screenapi.xml -DBUS_INTERFACES += ../../interfaces/appsapi.xml -DBUS_INTERFACES += ../../interfaces/application.xml +applications.files = ../../assets/opt/usr/share/applications/codes.eeems.corrupt.oxide +applications.path = /opt/usr/share/applications/ +INSTALLS += applications INCLUDEPATH += ../../shared HEADERS += \ diff --git a/applications/update-desktop-database/.gitignore b/applications/update-desktop-database/.gitignore new file mode 100644 index 000000000..fab7372d7 --- /dev/null +++ b/applications/update-desktop-database/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/applications/update-desktop-database/main.cpp b/applications/update-desktop-database/main.cpp new file mode 100644 index 000000000..45ad8b893 --- /dev/null +++ b/applications/update-desktop-database/main.cpp @@ -0,0 +1,157 @@ +#include + +#include + +using namespace codes::eeems::oxide1; +using namespace Oxide::Sentry; +using namespace Oxide::JSON; +using namespace Oxide::Applications; + +#define LOG_VERBOSE(msg) if(!parser.isSet(quietOption) && parser.isSet(verboseOption)){qDebug() << msg;} +#define LOG(msg) if(!parser.isSet(quietOption)){qDebug() << msg;} + +int qExit(int ret){ + QTimer::singleShot(0, [ret](){ + qApp->exit(ret); + }); + return qApp->exec(); +} + +QList configDirectoryPaths = { "/opt/etc/draft", "/etc/draft", "/home/root/.config/draft" }; + +int main(int argc, char *argv[]){ + QCoreApplication app(argc, argv); + sentry_init("update-desktop-database", argv); + app.setOrganizationName("Eeems"); + app.setOrganizationDomain(OXIDE_SERVICE); + app.setApplicationName("update-desktop-database"); + app.setApplicationVersion(APP_VERSION); + QCommandLineParser parser; + parser.setApplicationDescription("Reload the application registration cache for Oxide"); + parser.applicationDescription(); + parser.addHelpOption(); + parser.addPositionalArgument("directory", "NOT IMPLEMENTED", "[DIRECTORY...]"); + // TODO handle using $XDG_DATA_DIRS/applications if directory is not set + QCommandLineOption versionOption("version", "Display the version and exit"); + parser.addOption(versionOption); + QCommandLineOption quietOption( + {"q", "quiet"}, + "Do not display any information about processing and updating progress." + ); + parser.addOption(quietOption); + QCommandLineOption verboseOption( + {"v", "verbose"}, + "Display more information about processing and upating progress" + ); + parser.addOption(verboseOption); + parser.process(app); + if(parser.isSet(versionOption)){ + parser.showVersion(); + } + auto bus = QDBusConnection::systemBus(); + General api(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); + LOG_VERBOSE("Requesting apps API"); + QDBusObjectPath path = api.requestAPI("apps"); + if(path.path() == "/"){ + LOG("Unable to get apps API"); + return qExit(EXIT_FAILURE); + } + Apps apps(OXIDE_SERVICE, path.path(), bus); + LOG("Loading applications from disk"); + QDir dir(OXIDE_APPLICATION_REGISTRATIONS_DIRECTORY); + dir.setNameFilters(QStringList() << "*.oxide"); + for(auto entry : dir.entryInfoList()){ + auto path = entry.filePath(); + auto reg = getRegistration(path); + auto name = entry.completeBaseName(); + auto errors = validateRegistration(name, reg); + bool cache = true; + for(auto error : errors){ + if(error.level == ErrorLevel::Error || error.level == ErrorLevel::Critical){ + LOG_VERBOSE(" " << path.toStdString().c_str() << ": " << error); + cache = false; + } + } + if(cache && !addToTarnishCache(name, reg)){ + LOG(" " << path << ": Failed to cache") + } + } + LOG_VERBOSE("Finished reloading applications"); + LOG("Importing Draft Applications"); + for(auto configDirectoryPath : configDirectoryPaths){ + QDir configDirectory(configDirectoryPath); + configDirectory.setFilter( QDir::Files | QDir::NoSymLinks | QDir::NoDot | QDir::NoDotDot); + auto images = configDirectory.entryInfoList(QDir::NoFilter,QDir::SortFlag::Name); + for(QFileInfo fi : images){ + if(fi.fileName() != "conf"){ + auto f = fi.absoluteFilePath(); + LOG_VERBOSE("parsing file " << f); + QFile file(fi.absoluteFilePath()); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + if(!parser.isSet(quietOption)){ + qCritical() << "Couldn't find the file " << f; + } + continue; + } + QTextStream in(&file); + QVariantMap properties; + while (!in.atEnd()) { + QString line = in.readLine(); + if(line.startsWith("#") || line.trimmed().isEmpty()){ + continue; + } + QStringList parts = line.split("="); + if(parts.length() != 2){ + if(!parser.isSet(quietOption)){ + O_WARNING("wrong format on " << line); + } + continue; + } + QString lhs = parts.at(0); + QString rhs = parts.at(1); + if(rhs != ":" && rhs != ""){ + if(lhs == "name"){ + properties.insert("name", rhs); + }else if(lhs == "desc"){ + properties.insert("description", rhs); + }else if(lhs == "imgFile"){ + auto icon = configDirectoryPath + "/icons/" + rhs + ".png"; + if(icon.startsWith("qrc:")){ + icon = ""; + } + properties.insert("icon", icon); + }else if(lhs == "call"){ + properties.insert("bin", rhs); + }else if(lhs == "term"){ + properties.insert("onStop", rhs.trimmed()); + } + } + } + file.close(); + auto name = properties["name"].toString(); + path = apps.getApplicationPath(name); + if(path.path() != "/"){ + LOG_VERBOSE("Already exists" << name); + auto icon = properties["icon"].toString(); + if(icon.isEmpty()){ + continue; + } + Application application(OXIDE_SERVICE, path.path(), bus); + if(application.icon().isEmpty()){ + application.setIcon(icon); + } + continue; + } + LOG_VERBOSE("Not found, creating..."); + properties.insert("displayName", name); + path = apps.registerApplication(properties); + if(path.path() == "/"){ + LOG("Failed to import" << name); + } + } + } + } + LOG_VERBOSE("Finished Importing Draft Applications"); + apps.reload().waitForFinished(); + return qExit(EXIT_SUCCESS); +} diff --git a/applications/update-desktop-database/update-desktop-database.pro b/applications/update-desktop-database/update-desktop-database.pro new file mode 100644 index 000000000..9ea92d5b4 --- /dev/null +++ b/applications/update-desktop-database/update-desktop-database.pro @@ -0,0 +1,25 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +DEFINES += QT_DEPRECATED_WARNINGS +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp + +HEADERS += + +DBUS_INTERFACES += ../../interfaces/dbusservice.xml +DBUS_INTERFACES += ../../interfaces/appsapi.xml +DBUS_INTERFACES += ../../interfaces/application.xml + +TARGET = update-desktop-database +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/xdg-desktop-icon/.gitignore b/applications/xdg-desktop-icon/.gitignore new file mode 100644 index 000000000..fab7372d7 --- /dev/null +++ b/applications/xdg-desktop-icon/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/applications/xdg-desktop-icon/main.cpp b/applications/xdg-desktop-icon/main.cpp new file mode 100644 index 000000000..0b3057932 --- /dev/null +++ b/applications/xdg-desktop-icon/main.cpp @@ -0,0 +1,132 @@ +#include + +#include +#include +#include +#include + +using namespace Oxide::Sentry; +using namespace Oxide::Applications; + +#define APPS_DIR OXIDE_APPLICATION_REGISTRATIONS_DIRECTORY + +QCommandLineOption versionOption( + {"v", "version"}, + "Displays version information." +); + +QStringList positionArguments(QCommandLineParser& parser){ + parser.process(*qApp); + if(parser.isSet(versionOption)){ + parser.showHelp(EXIT_FAILURE); + } + auto args = parser.positionalArguments(); + args.removeFirst(); + if(args.isEmpty() || args.count() > 1){ + parser.showHelp(EXIT_FAILURE); + } + return args; +} + +int install(QCommandLineParser& parser){ + parser.addPositionalArgument("install", "Install one or more application registration.", "install [options]"); + QCommandLineOption novendorOption( + "novendor", + "NOT IMPLEMENETED" + ); + parser.addOption(novendorOption); + parser.addPositionalArgument("file", "Application registration to install.", "FILE"); + + QString path = positionArguments(parser).first(); + if(!QFile::exists(path)){ + qDebug() << "error:" << path.toStdString().c_str() << "does not exist"; + return EXIT_FAILURE; + } + if(!QFile::exists(APPS_DIR) && mkdir(APPS_DIR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)){ + qDebug() << "error: failed to created " APPS_DIR; + return EXIT_FAILURE; + } + QFileInfo info(path); + if(info.absoluteDir().path() == APPS_DIR){ + qDebug() << "error: " << path.toStdString().c_str() << "is already installed"; + return EXIT_FAILURE; + } + auto errors = validateRegistration(path); + if(std::any_of(errors.constBegin(), errors.constEnd(), [](const ValidationError& e){ + return e.level == ErrorLevel::Error || e.level == ErrorLevel::Critical; + })){ + qDebug() << "error: " << path.toStdString().c_str() << "is not valid"; + return EXIT_FAILURE; + } + auto toPath = APPS_DIR "/" + info.fileName(); + if(QFile::exists(toPath) && !QFile::remove(toPath)){ + qDebug() << "error: " << toPath.toStdString().c_str() << " already exists and can't be removed"; + return EXIT_FAILURE; + } + if(!QFile::copy(path, toPath)){ + qDebug() << "error: Failed to created" << toPath.toStdString().c_str(); + return EXIT_FAILURE; + } + qDebug() << "success: Installed" << path.toStdString().c_str(); + return QProcess::execute("update-desktop-database", QStringList("--quiet")); +} +int uninstall(QCommandLineParser& parser){ + parser.addPositionalArgument("uninstall", "Uninstall one or more application registration.", "uninstall"); + parser.addPositionalArgument("file", "Application registration to uninstall.", "FILE"); + + QString path = positionArguments(parser).first(); + if(!QFile::exists(path)){ + qDebug() << "error:" << path.toStdString().c_str() << "does not exist"; + return EXIT_FAILURE; + } + auto toPath = APPS_DIR "/" + QFileInfo(path).fileName(); + if(!QFile::exists(toPath)){ + qDebug() << "success:" << toPath.toStdString().c_str() << "doesn't exist"; + }else if(!QFile::remove(toPath)){ + qDebug() << "error: Could not remove" << toPath.toStdString().c_str(); + return EXIT_FAILURE; + }else{ + qDebug() << "success: Uninstalled" << toPath.toStdString().c_str(); + } + return QProcess::execute("update-desktop-database", QStringList("--quiet")); +} + +int main(int argc, char *argv[]){ + QCoreApplication app(argc, argv); + sentry_init("xdg-desktop-icon", argv); + app.setOrganizationName("Eeems"); + app.setOrganizationDomain(OXIDE_SERVICE); + app.setApplicationName("xdg-desktop-icon"); + app.setApplicationVersion(APP_VERSION); + QCommandLineParser parser; + parser.setApplicationDescription("A program to (un)install application registrations"); + parser.applicationDescription(); + parser.addHelpOption(); + parser.addOption(versionOption); + parser.addPositionalArgument("command", + "install Install one or more application registration.\n" + "uninstall Uninstall one or more application registration.\n" + ); + + parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments); + parser.parse(app.arguments()); + parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsOptions); + QStringList args = parser.positionalArguments(); + if (args.isEmpty()) { + parser.showHelp(EXIT_FAILURE); + } + if(parser.isSet(versionOption)){ + parser.showVersion(); + } + + auto command = args.first(); + if(command == "install"){ + parser.clearPositionalArguments(); + return install(parser); + } + if(command == "uninstall"){ + parser.clearPositionalArguments(); + return uninstall(parser); + } + parser.showHelp(EXIT_FAILURE); +} diff --git a/applications/xdg-desktop-icon/xdg-desktop-icon.pro b/applications/xdg-desktop-icon/xdg-desktop-icon.pro new file mode 100644 index 000000000..c3ef13f85 --- /dev/null +++ b/applications/xdg-desktop-icon/xdg-desktop-icon.pro @@ -0,0 +1,21 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +DEFINES += QT_DEPRECATED_WARNINGS +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp + +HEADERS += + +TARGET = xdg-desktop-icon +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/xdg-desktop-menu/.gitignore b/applications/xdg-desktop-menu/.gitignore new file mode 100644 index 000000000..fab7372d7 --- /dev/null +++ b/applications/xdg-desktop-menu/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/applications/xdg-desktop-menu/main.cpp b/applications/xdg-desktop-menu/main.cpp new file mode 100644 index 000000000..aa39268be --- /dev/null +++ b/applications/xdg-desktop-menu/main.cpp @@ -0,0 +1,203 @@ +#include + +#include +#include +#include +#include + +using namespace Oxide::Sentry; +using namespace Oxide::Applications; + +#define APPS_DIR OXIDE_APPLICATION_REGISTRATIONS_DIRECTORY + +QCommandLineOption versionOption( + {"v", "version"}, + "Displays version information." +); + +QStringList positionArguments(QCommandLineParser& parser, bool allowEmpty = false){ + parser.process(*qApp); + if(parser.isSet(versionOption)){ + parser.showHelp(EXIT_FAILURE); + } + auto args = parser.positionalArguments(); + args.removeFirst(); + if(!allowEmpty && args.isEmpty()){ + parser.showHelp(EXIT_FAILURE); + } + return args; +} + +int install(QCommandLineParser& parser){ + parser.addPositionalArgument("install", "Install one or more application registration.", "install [options]"); + QCommandLineOption noupdateOption( + "noupdate", + "Do not update the application cache." + ); + parser.addOption(noupdateOption); + QCommandLineOption novendorOption( + "novendor", + "NOT IMPLEMENETED" + ); + parser.addOption(novendorOption); + QCommandLineOption modeOption( + "mode", + "NOT IMPLEMENETED", + "mode" + ); + parser.addOption(modeOption); + parser.addPositionalArgument("files", "Application registration(s) to install.", "directory-file(s) desktop-file(s)"); + + auto args = positionArguments(parser); + if(!QFile::exists(APPS_DIR) && mkdir(APPS_DIR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)){ + qDebug() << "error: failed to created " APPS_DIR; + return EXIT_FAILURE; + } + bool failure = false; + bool updated = false; + for(QString path : args){ + if(!QFile::exists(path)){ + qDebug() << "error:" << path.toStdString().c_str() << "does not exist"; + failure = true; + continue; + } + QFileInfo info(path); + if(info.absoluteDir().path() == APPS_DIR){ + qDebug() << "error: " << path.toStdString().c_str() << "is already installed"; + failure = true; + continue; + } + auto errors = validateRegistration(path); + if(std::any_of(errors.constBegin(), errors.constEnd(), [](const ValidationError& e){ + return e.level == ErrorLevel::Error || e.level == ErrorLevel::Critical; + })){ + qDebug() << "error: " << path.toStdString().c_str() << "is not valid"; + failure = true; + continue; + } + auto toPath = APPS_DIR "/" + info.fileName(); + if(QFile::exists(toPath) && !QFile::remove(toPath)){ + failure = true; + updated = true; + qDebug() << "error: " << toPath.toStdString().c_str() << " already exists and can't be removed"; + continue; + } + if(!QFile::copy(path, toPath)){ + qDebug() << "error: Failed to created" << toPath.toStdString().c_str(); + failure = true; + continue; + } + qDebug() << "success: Installed" << path.toStdString().c_str(); + updated = true; + } + + if(!updated || parser.isSet(noupdateOption)){ + return failure ? EXIT_FAILURE : EXIT_SUCCESS; + } + int res = QProcess::execute("update-desktop-database", QStringList("--quiet")); + return failure ? EXIT_FAILURE : res; +} +int uninstall(QCommandLineParser& parser){ + parser.addPositionalArgument("uninstall", "Uninstall one or more application registration.", "uninstall [options]"); + QCommandLineOption noupdateOption( + "noupdate", + "Do not update the application cache." + ); + parser.addOption(noupdateOption); + QCommandLineOption modeOption( + "mode", + "NOT IMPLEMENETED", + "mode" + ); + parser.addOption(modeOption); + parser.addPositionalArgument("files", "Application registration(s) to install.", "directory-file(s) desktop-file(s)"); + + auto args = positionArguments(parser); + bool failure = false; + bool updated = false; + for(QString path : args){ + if(!QFile::exists(path)){ + qDebug() << "error:" << path.toStdString().c_str() << "does not exist"; + failure = true; + continue; + } + auto toPath = APPS_DIR "/" + QFileInfo(path).fileName(); + if(!QFile::exists(toPath)){ + qDebug() << "success:" << toPath.toStdString().c_str() << "doesn't exist"; + continue; + } + if(!QFile::remove(toPath)){ + failure = true; + qDebug() << "error: Could not remove" << toPath.toStdString().c_str(); + continue; + } + qDebug() << "success: Uninstalled" << toPath.toStdString().c_str(); + updated = true; + } + if(!updated || parser.isSet(noupdateOption)){ + return failure ? EXIT_FAILURE : EXIT_SUCCESS; + } + int res = QProcess::execute("update-desktop-database", QStringList("--quiet")); + return failure ? EXIT_FAILURE : res; +} + +int forceupdate(QCommandLineParser& parser){ + parser.addPositionalArgument("forceupdate", "Force an update of the application registration cache.", "forceupdate [options]"); + QCommandLineOption modeOption( + "mode", + "NOT IMPLEMENETED", + "mode" + ); + parser.addOption(modeOption); + + if(!positionArguments(parser, true).isEmpty()){ + parser.showHelp(); + } + return QProcess::execute("update-desktop-database", QStringList()); +} + +int main(int argc, char *argv[]){ + QCoreApplication app(argc, argv); + sentry_init("xdg-desktop-menu", argv); + app.setOrganizationName("Eeems"); + app.setOrganizationDomain(OXIDE_SERVICE); + app.setApplicationName("xdg-desktop-menu"); + app.setApplicationVersion(APP_VERSION); + QCommandLineParser parser; + parser.setApplicationDescription("A program to (un)install application registrations"); + parser.applicationDescription(); + parser.addHelpOption(); + parser.addOption(versionOption); + parser.addPositionalArgument("command", + "install Install one or more application registration.\n" + "uninstall Uninstall one or more application registration.\n" + "forceupdate Force an update of the application registration \n" + " cache.\n" + ); + + parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments); + parser.parse(app.arguments()); + parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsOptions); + QStringList args = parser.positionalArguments(); + if (args.isEmpty()) { + parser.showHelp(EXIT_FAILURE); + } + if(parser.isSet(versionOption)){ + parser.showVersion(); + } + + auto command = args.first(); + if(command == "install"){ + parser.clearPositionalArguments(); + return install(parser); + } + if(command == "uninstall"){ + parser.clearPositionalArguments(); + return uninstall(parser); + } + if(command == "forceupdate"){ + parser.clearPositionalArguments(); + return forceupdate(parser); + } + parser.showHelp(EXIT_FAILURE); +} diff --git a/applications/xdg-desktop-menu/xdg-desktop-menu.pro b/applications/xdg-desktop-menu/xdg-desktop-menu.pro new file mode 100644 index 000000000..34aecfd97 --- /dev/null +++ b/applications/xdg-desktop-menu/xdg-desktop-menu.pro @@ -0,0 +1,21 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +DEFINES += QT_DEPRECATED_WARNINGS +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp + +HEADERS += + +TARGET = xdg-desktop-menu +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/xdg-icon-resource/.gitignore b/applications/xdg-icon-resource/.gitignore new file mode 100644 index 000000000..fab7372d7 --- /dev/null +++ b/applications/xdg-icon-resource/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/applications/xdg-icon-resource/main.cpp b/applications/xdg-icon-resource/main.cpp new file mode 100644 index 000000000..f4bb59728 --- /dev/null +++ b/applications/xdg-icon-resource/main.cpp @@ -0,0 +1,203 @@ +#include + +#include +#include +#include +#include + +using namespace Oxide::Sentry; +using namespace Oxide::Applications; + +#define ICON_DIR OXIDE_ICONS_DIRECTORY + +QCommandLineOption versionOption( + {"v", "version"}, + "Displays version information." +); + +QCommandLineOption themeOption( + "theme", + "Installs or removes the icon file as part of theme. If no theme is specified the icons will be installed as part of the default hicolor theme. Applications may install icons under multiple themes but should at least install icons for the default hicolor theme.", + "theme", + "hicolor" +); + +QCommandLineOption modeOption( + "mode", + "NOT IMPLEMENETED", + "mode" +); +QCommandLineOption noupdateOption( + "noupdate", + "Do not update the application cache." +); +QCommandLineOption sizeOption( + "size", + "Specifies the size of the icon. All icons must be square. Common sizes for icons in the apps context are: 16, 22, 32, 48, 64 and 128. Common sizes for icons in the mimetypes context are: 16, 22, 32, 48, 64 and 128", + "size" +); +QCommandLineOption contextOption( + "context", + "Specifies the context for the icon. Icons to be used in the application menu and as desktop icon should use apps as context which is the default context. Icons to be used as file icons should use mimetypes as context. Other common contexts are actions, devices, emblems, filesystems and stock.", + "context", + "apps" +); +QCommandLineOption novendorOption( + "novendor", + "NOT IMPLEMENETED" +); + +QStringList positionArguments(QCommandLineParser& parser, bool allowEmpty = false){ + parser.process(*qApp); + if(parser.isSet(versionOption)){ + parser.showHelp(EXIT_FAILURE); + } + auto args = parser.positionalArguments(); + args.removeFirst(); + if(!allowEmpty && args.isEmpty()){ + parser.showHelp(EXIT_FAILURE); + } + return args; +} +QString iconDir(QCommandLineParser& parser){ return Oxide::Applications::iconDirPath( + parser.value(sizeOption).toUInt(), + parser.value(themeOption), + parser.value(contextOption) +); } +QString iconFile(QCommandLineParser& parser, const QString& name){ return Oxide::Applications::iconPath( + name, + parser.value(sizeOption).toUInt(), + parser.value(themeOption), + parser.value(contextOption) +); } + +int install(QCommandLineParser& parser){ + parser.addPositionalArgument("", "", "install [options]"); + parser.addOption(noupdateOption); + parser.addOption(novendorOption); + parser.addOption(themeOption); + parser.addOption(modeOption); + parser.addOption(contextOption); + parser.addOption(sizeOption); + parser.addPositionalArgument("icon-file", "Icon file to install."); + parser.addPositionalArgument("icon-name", "Name to use when installing.", "[icon-name]"); + + auto args = positionArguments(parser); + if(args.length() > 2 || !parser.isSet(sizeOption)){ + parser.showHelp(EXIT_FAILURE); + } + + auto fromPath = args.first(); + if(!QFile::exists(fromPath)){ + qDebug() << "error: file does not exist" << fromPath; + return EXIT_FAILURE; + } + + auto dirPath = iconDir(parser); + QDir dir(dirPath); + if(!dir.exists() && !dir.mkpath(".")){ + qDebug() << "error: failed to created directory" << dirPath; + return EXIT_FAILURE; + } + + QString name = args.length() == 2 ? args.last() : QFileInfo(fromPath).baseName(); + auto toPath = iconFile(parser, name); + if(!QFile::copy(fromPath, toPath)){ + qDebug() << "error: failed to copy file to" << toPath; + return EXIT_FAILURE; + } + + if(parser.isSet(noupdateOption)){ + return EXIT_SUCCESS; + } + return QProcess::execute("update-desktop-database", QStringList("--quiet")); +} +int uninstall(QCommandLineParser& parser){ + parser.addPositionalArgument("", "", "uninstall [options]"); + parser.addOption(noupdateOption); + parser.addOption(themeOption); + parser.addOption(modeOption); + parser.addOption(contextOption); + parser.addOption(sizeOption); + parser.addPositionalArgument("icon-name", "Name to use when installing.", "[icon-name]"); + + auto args = positionArguments(parser); + if(args.length() > 1 || !parser.isSet(sizeOption)){ + parser.showHelp(EXIT_FAILURE); + } + + auto path = iconFile(parser, args.last()); + if(QFile::exists(path) && !QFile::remove(path)){ + qDebug() << "error: failed to delete" << path; + return EXIT_FAILURE; + } + + if(parser.isSet(noupdateOption)){ + return EXIT_SUCCESS; + } + return QProcess::execute("update-desktop-database", QStringList("--quiet")); +} + +int forceupdate(QCommandLineParser& parser){ + parser.addPositionalArgument("", "", "forceupdate [options]"); + parser.addOption(themeOption); + parser.addOption(modeOption); + + if(!positionArguments(parser, true).isEmpty()){ + parser.showHelp(); + } + return QProcess::execute("update-desktop-database", QStringList()); +} + +int main(int argc, char *argv[]){ + QCoreApplication app(argc, argv); + sentry_init("xdg-icon-resource", argv); + app.setOrganizationName("Eeems"); + app.setOrganizationDomain(OXIDE_SERVICE); + app.setApplicationName("xdg-icon-resource"); + app.setApplicationVersion(APP_VERSION); + QCommandLineParser parser; + parser.setApplicationDescription("Command line tool for (un)installing icon resources."); + parser.applicationDescription(); + parser.addHelpOption(); + parser.addOption(versionOption); + parser.addPositionalArgument("command", + "install Installs the icon file indicated by icon-file to\n" + " the desktop icon system under the name icon-name.\n" + " Icon names do not have an extension. If icon-name\n" + " is not provided the name is derived from\n" + " icon-file.\n" + "uninstall Removes the icon indicated by icon-name from the\n" + " desktop icon system. Note that icon names do not\n" + " have an extension.\n" + "forceupdate Force an update of the desktop icon system. This\n" + " is only useful if the last call to\n" + " xdg-icon-resource included the --noupdate option.\n" + ); + + parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments); + parser.parse(app.arguments()); + parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsOptions); + QStringList args = parser.positionalArguments(); + if (args.isEmpty()) { + parser.showHelp(EXIT_FAILURE); + } + if(parser.isSet(versionOption)){ + parser.showVersion(); + } + + auto command = args.first(); + if(command == "install"){ + parser.clearPositionalArguments(); + return install(parser); + } + if(command == "uninstall"){ + parser.clearPositionalArguments(); + return uninstall(parser); + } + if(command == "forceupdate"){ + parser.clearPositionalArguments(); + return forceupdate(parser); + } + parser.showHelp(EXIT_FAILURE); +} diff --git a/applications/xdg-icon-resource/xdg-icon-resource.pro b/applications/xdg-icon-resource/xdg-icon-resource.pro new file mode 100644 index 000000000..b8eb63f8e --- /dev/null +++ b/applications/xdg-icon-resource/xdg-icon-resource.pro @@ -0,0 +1,21 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +DEFINES += QT_DEPRECATED_WARNINGS +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp + +HEADERS += + +TARGET = xdg-icon-resource +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/xdg-open/.gitignore b/applications/xdg-open/.gitignore new file mode 100644 index 000000000..fab7372d7 --- /dev/null +++ b/applications/xdg-open/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/applications/xdg-open/main.cpp b/applications/xdg-open/main.cpp new file mode 100644 index 000000000..4da520f29 --- /dev/null +++ b/applications/xdg-open/main.cpp @@ -0,0 +1,76 @@ +#include + +#include +#include +#include +#include + +using namespace Oxide::Sentry; +using namespace Oxide::Applications; +using namespace codes::eeems::oxide1; + +int launchOxideApp(const QString& name){ + auto bus = QDBusConnection::systemBus(); + General api(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); + QDBusObjectPath path = api.requestAPI("apps"); + if(path.path() == "/"){ + qDebug() << "Unable to get apps API"; + return EXIT_FAILURE; + } + Apps apps(OXIDE_SERVICE, path.path(), bus); + path = apps.getApplicationPath(name); + if(path.path() == "/"){ + qDebug() << "Application does not exist:" << name.toStdString().c_str(); + return EXIT_FAILURE; + } + Application app(OXIDE_SERVICE, path.path(), bus); + app.launch().waitForFinished(); + return EXIT_SUCCESS; +} + +int main(int argc, char *argv[]){ + QCoreApplication app(argc, argv); + sentry_init("xdg-open", argv); + app.setOrganizationName("Eeems"); + app.setOrganizationDomain(OXIDE_SERVICE); + app.setApplicationName("xdg-open"); + app.setApplicationVersion(APP_VERSION); + QCommandLineParser parser; + parser.setApplicationDescription("Open a file or URL."); + parser.applicationDescription(); + parser.addHelpOption(); + parser.addVersionOption(); + parser.process(app); + QStringList args = parser.positionalArguments(); + if (args.isEmpty() || args.length() > 1) { + parser.showHelp(EXIT_FAILURE); + } + auto path = args.first(); + auto url = QUrl::fromUserInput(path, QDir::currentPath(), QUrl::AssumeLocalFile); + if(url.scheme().isEmpty()){ + url.setScheme("file"); + } + if(url.isLocalFile()){ + QFileInfo info(url.path()); + if(info.suffix() != "oxide"){ + qDebug() << "The extension is not supported:" << path.toStdString().c_str(); + return EXIT_FAILURE; + } + if(info.absoluteDir().path() != OXIDE_APPLICATION_REGISTRATIONS_DIRECTORY){ + qDebug() << "The registration file must be in " OXIDE_APPLICATION_REGISTRATIONS_DIRECTORY ":" << path.toStdString().c_str(); + return EXIT_FAILURE; + } + // Workaround because basename will sometimes return the suffix instead + auto name = info.fileName().left(info.fileName().length() - 6); + return launchOxideApp(name); + }else if(url.scheme() == "oxide"){ + if(url.hasFragment() || url.hasQuery() || !url.userInfo().isEmpty() || !url.authority().isEmpty()){ + qDebug() << "Url must bein the format oxide://{appname} :" << path.toStdString().c_str(); + return EXIT_FAILURE; + } + auto name = url.path(); + return launchOxideApp(name); + } + qDebug() << "Operation not supported:" << path.toStdString().c_str(); + return EXIT_FAILURE; +} diff --git a/applications/xdg-open/xdg-open.pro b/applications/xdg-open/xdg-open.pro new file mode 100644 index 000000000..fdcccb524 --- /dev/null +++ b/applications/xdg-open/xdg-open.pro @@ -0,0 +1,21 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +DEFINES += QT_DEPRECATED_WARNINGS +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp + +HEADERS += + +TARGET = xdg-open +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/applications/xdg-settings/.gitignore b/applications/xdg-settings/.gitignore new file mode 100644 index 000000000..fab7372d7 --- /dev/null +++ b/applications/xdg-settings/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/applications/xdg-settings/main.cpp b/applications/xdg-settings/main.cpp new file mode 100644 index 000000000..7755558f6 --- /dev/null +++ b/applications/xdg-settings/main.cpp @@ -0,0 +1,285 @@ +#include + +#include +#include +#include +#include + +using namespace codes::eeems::oxide1; +using namespace Oxide::Sentry; +using namespace Oxide::JSON; + +const QSet ApiNames = QSet{ + "general", + "power", + "wifi", + "apps", + "system", + "screen", + "notification", +}; + +QTextStream& qStdOut(){ + static QTextStream ts( stdout ); + return ts; +} + +QObject* getApi(const QString& name){ + if(!ApiNames.contains(name)){ + return nullptr; + } + auto bus = QDBusConnection::systemBus(); + if(!bus.isConnected()){ + qDebug() << "xdg-settings: Failed to connect to DBus"; + return nullptr; + } + if(name == "general"){ + return new General(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); + } + General generalApi(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); + auto reply = generalApi.requestAPI(name); + reply.waitForFinished(); + if(reply.isError()){ + qDebug() << "xdg-settings: Failed to connect to general API"; + return nullptr; + } + auto path = ((QDBusObjectPath)reply).path(); + if(path == "/"){ + qDebug() << "xdg-settings: Failed to get API path from tarnish"; + return nullptr; + } + if(name == "power"){ + return new Power(OXIDE_SERVICE, path, bus); + } + if(name == "wifi"){ + return new Wifi(OXIDE_SERVICE, path, bus); + } + if(name == "apps"){ + return new Apps(OXIDE_SERVICE, path, bus); + } + if(name == "system"){ + return new System(OXIDE_SERVICE, path, bus); + } + if(name == "screen"){ + return new Screen(OXIDE_SERVICE, path, bus); + } + if(name == "notification"){ + return new Notifications(OXIDE_SERVICE, path, bus); + } + qDebug() << "xdg-settings: Unknown API" << name; + return nullptr; +} + +bool isValidProperty(QObject* obj, const QString& name){ + auto meta = obj->metaObject(); + for(int i = meta->propertyOffset(); i < meta->propertyCount(); ++i){ + auto property = meta->property(i); + if(property.name() == name){ + return property.isReadable() && property.isWritable(); + } + } + return false; +} + +QList getProperties(QObject* obj){ + QList res; + auto meta = obj->metaObject(); + for(int i = meta->propertyOffset(); i < meta->propertyCount(); ++i){ + auto property = meta->property(i); + auto name = property.name(); + if(!QString(name).startsWith("__META_GROUP_") && property.isReadable() && property.isWritable()){ + res.append(name); + } + } + return res; +} + +QStringList positionArguments(QCommandLineParser& parser, int mandatoryCount, int maxCount){ + parser.process(*qApp); + auto args = parser.positionalArguments(); + args.removeFirst(); + auto len = args.length(); + parser.parse(args); + if(len < mandatoryCount || len > maxCount){ + parser.showHelp(EXIT_FAILURE); + } + return args; +} + +QObject* getObj(QStringList* args, int isGet = false){ + switch(args->length()){ + case 2: + if(!isGet){ + return &sharedSettings; + } + break; + case 1: + return &sharedSettings; + case 3: + default: + break; + } + auto name = args->first(); + QObject* obj = getApi(name); + args->removeFirst(); + return obj; +} + +int get(QCommandLineParser& parser){ + parser.clearPositionalArguments(); + parser.addPositionalArgument("", "", "get"); + parser.addPositionalArgument("property", "Property to get", "{property}"); + parser.addPositionalArgument("subproperty", "Subproperty to get", "[subproperty]"); + auto args = positionArguments(parser, 1, 2); + + QObject* obj = getObj(&args, true); + if(obj == nullptr){ + parser.showHelp(EXIT_FAILURE); + } + auto name = args.first(); + if(!isValidProperty(obj, name)){ + qDebug() << "xdg-settings: Unknown property" << name; + qDebug() << args; + parser.showHelp(EXIT_FAILURE); + } + QVariant value = obj->property(name.toStdString().c_str()); + if(!value.isValid()){ + qDebug() << "xdg-settings: Failed to get property"; + parser.showHelp(EXIT_FAILURE); + } + qStdOut() << toJson(value).toStdString().c_str() << Qt::endl; + return EXIT_SUCCESS; +} + +int check(QCommandLineParser& parser){ + parser.clearPositionalArguments(); + parser.addPositionalArgument("", "", "check"); + parser.addPositionalArgument("property", "Property to check", "{property}"); + parser.addPositionalArgument("subproperty", "Subproperty to check", "[subproperty]"); + parser.addPositionalArgument("value", "Value to check if the property/subproperty is", "value"); + auto args = positionArguments(parser, 2, 3); + + QObject* obj = getObj(&args); + if(obj == nullptr){ + parser.showHelp(EXIT_FAILURE); + } + auto name = args.first(); + if(!isValidProperty(obj, name)){ + qDebug() << "xdg-settings: Unknown property" << name; + parser.showHelp(EXIT_FAILURE); + } + QVariant value = obj->property(name.toStdString().c_str()); + if(!value.isValid()){ + qDebug() << "xdg-settings: Failed to get property"; + parser.showHelp(EXIT_FAILURE); + } + qStdOut() << (args.last() == value ? "yes" : "no") << Qt::endl; + return EXIT_SUCCESS; +} + +int set(QCommandLineParser& parser){ + parser.clearPositionalArguments(); + parser.addPositionalArgument("", "", "set"); + parser.addPositionalArgument("property", "Property to set", "{property}"); + parser.addPositionalArgument("subproperty", "Subproperty to set", "[subproperty]"); + parser.addPositionalArgument("value", "Value to set the property/subproperty to", "value"); + auto args = positionArguments(parser, 2, 3); + + QObject* obj = getObj(&args); + if(obj == nullptr){ + parser.showHelp(EXIT_FAILURE); + } + auto name = args.first(); + if(!isValidProperty(obj, name)){ + qDebug() << "xdg-settings: Unknown property" << name; + parser.showHelp(EXIT_FAILURE); + } + QVariant value = obj->property(name.toStdString().c_str()); + if(!value.isValid()){ + qDebug() << "xdg-settings: Failed to get property"; + parser.showHelp(EXIT_FAILURE); + } + if(obj->setProperty(name.toStdString().c_str(), args.last())){ + return EXIT_SUCCESS; + } + QDBusAbstractInterface* api = qobject_cast(obj); + if(api == nullptr){ + qDebug() << "xdg-settings: failed to set property"; + }else{ + qDebug() << "xdg-settings:" << api->lastError(); + } + return EXIT_FAILURE; +} + +int list(QCommandLineParser& parser){ + if(!parser.positionalArguments().isEmpty()){ + parser.showHelp(EXIT_FAILURE); + } + qStdOut() << "Known properties:" << Qt::endl; + for(auto name : getProperties(&sharedSettings)){ + qStdOut() << " " << name << Qt::endl; + } + for(auto name : ApiNames){ + auto api = getApi(name); + if(api == nullptr){ + return EXIT_FAILURE; + } + auto props = getProperties(api); + if(!props.isEmpty()){ + qStdOut() << " " << name << Qt::endl; + for(auto prop : props){ + qStdOut() << " " << prop << Qt::endl; + } + } + delete api; + } + return EXIT_SUCCESS; +} + +int main(int argc, char *argv[]){ + QCoreApplication app(argc, argv); + sentry_init("xdg-settings", argv); + app.setOrganizationName("Eeems"); + app.setOrganizationDomain(OXIDE_SERVICE); + app.setApplicationName("xdg-settings"); + app.setApplicationVersion(APP_VERSION); + QCommandLineParser parser; + parser.setApplicationDescription("Get various settings from the desktop environment"); + parser.applicationDescription(); + parser.addHelpOption(); + parser.addVersionOption(); + QCommandLineOption listOption("list", "List all properties xdg-settings knows about."); + parser.addOption(listOption); + parser.addPositionalArgument("Commands:", + "get Get the value of a setting.\n" + "check Check to see if a setting is a specific value.\n" + "set Set the value of a setting.\n" + ); + + parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments); + parser.parse(app.arguments()); + parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsOptions); + QStringList args = parser.positionalArguments(); + if(parser.isSet(listOption)){ + return list(parser); + } + if (args.isEmpty()) { + parser.showHelp(EXIT_FAILURE); + } + + + auto command = args.first(); + if(command == "get"){ + parser.clearPositionalArguments(); + return get(parser); + } + if(command == "check"){ + parser.clearPositionalArguments(); + return check(parser); + } + if(command == "set"){ + parser.clearPositionalArguments(); + return set(parser); + } + parser.showHelp(EXIT_FAILURE); +} diff --git a/applications/xdg-settings/xdg-settings.pro b/applications/xdg-settings/xdg-settings.pro new file mode 100644 index 000000000..9d0f2c1bc --- /dev/null +++ b/applications/xdg-settings/xdg-settings.pro @@ -0,0 +1,21 @@ +QT -= gui +QT += dbus + +CONFIG += c++11 console +CONFIG -= app_bundle + +DEFINES += QT_DEPRECATED_WARNINGS +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp + +HEADERS += + +TARGET = xdg-settings +include(../../qmake/common.pri) +target.path = /opt/bin +INSTALLS += target + +include(../../qmake/liboxide.pri) +include(../../qmake/sentry.pri) diff --git a/assets/etc/draft/icons/erode.svg b/assets/etc/draft/icons/erode.svg deleted file mode 100644 index e0f0ffc11..000000000 --- a/assets/etc/draft/icons/erode.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/etc/draft/icons/image.svg b/assets/etc/draft/icons/image.svg deleted file mode 100644 index 410aa8d43..000000000 --- a/assets/etc/draft/icons/image.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/etc/systemd/system/tarnish.service b/assets/etc/systemd/system/tarnish.service index 78d72cb85..2889afeb2 100644 --- a/assets/etc/systemd/system/tarnish.service +++ b/assets/etc/systemd/system/tarnish.service @@ -11,7 +11,7 @@ Conflicts=sync.service [Service] Type=dbus BusName=codes.eeems.oxide1 -ExecStart=/opt/bin/tarnish +ExecStart=/opt/bin/tarnish --break-lock Restart=on-failure RestartSec=5 Environment="HOME=/home/root" diff --git a/assets/opt/usr/share/applications/codes.eeems.anxiety.oxide b/assets/opt/usr/share/applications/codes.eeems.anxiety.oxide index 4a253abdd..050aad4d8 100644 --- a/assets/opt/usr/share/applications/codes.eeems.anxiety.oxide +++ b/assets/opt/usr/share/applications/codes.eeems.anxiety.oxide @@ -2,8 +2,8 @@ "displayName": "Screenshots", "description": "View and manage screenshots", "bin": "/opt/bin/anxiety", - "icon": "/opt/etc/draft/icons/image.svg", - "splash": "/opt/etc/draft/icons/anxiety-splash.png", + "icon": "oxide:image-48", + "splash": "oxide:splash:anxiety-702", "flags": [], "type": "foreground", "permissions": ["screen"] diff --git a/assets/opt/usr/share/applications/codes.eeems.decay.oxide b/assets/opt/usr/share/applications/codes.eeems.decay.oxide index 5eecd80df..38ccdffb1 100644 --- a/assets/opt/usr/share/applications/codes.eeems.decay.oxide +++ b/assets/opt/usr/share/applications/codes.eeems.decay.oxide @@ -4,6 +4,6 @@ "bin": "/opt/bin/decay", "flags": ["hidden"], "type": "foreground", - "splash": "/opt/etc/draft/icons/oxide-splash.png", + "splash": "oxide:splash:oxide-702", "permissions": ["apps", "power", "system", "wifi"] } diff --git a/assets/opt/usr/share/applications/codes.eeems.erode.oxide b/assets/opt/usr/share/applications/codes.eeems.erode.oxide index 408b250c3..25b459b2a 100644 --- a/assets/opt/usr/share/applications/codes.eeems.erode.oxide +++ b/assets/opt/usr/share/applications/codes.eeems.erode.oxide @@ -1,7 +1,7 @@ { "displayName": "Process Manager", "description": "List and kill running processes", - "icon": "/opt/etc/draft/icons/erode.svg", - "splash": "/opt/etc/draft/icons/erode-splash.png", + "icon": "oxide:erode-48", + "splash": "oxide:splash:erode-702", "bin": "/opt/bin/erode" } diff --git a/assets/opt/usr/share/applications/codes.eeems.fret.oxide b/assets/opt/usr/share/applications/codes.eeems.fret.oxide index 66df10e10..0d8710f2c 100644 --- a/assets/opt/usr/share/applications/codes.eeems.fret.oxide +++ b/assets/opt/usr/share/applications/codes.eeems.fret.oxide @@ -4,6 +4,6 @@ "bin": "/opt/bin/fret", "flags": ["autoStart", "hidden"], "type": "background", - "splash": "/opt/etc/draft/icons/oxide-splash.png", + "splash": "oxide:splash:oxide-702", "permissions": ["notification", "screen", "system"] } diff --git a/assets/opt/usr/share/applications/codes.eeems.oxide.oxide b/assets/opt/usr/share/applications/codes.eeems.oxide.oxide index c2ea43023..46b761778 100644 --- a/assets/opt/usr/share/applications/codes.eeems.oxide.oxide +++ b/assets/opt/usr/share/applications/codes.eeems.oxide.oxide @@ -4,6 +4,6 @@ "bin": "/opt/bin/oxide", "type": "foreground", "flags": ["hidden"], - "splash": "/opt/etc/draft/icons/oxide-splash.png", + "splash": "oxide:splash:oxide-702", "permissions": ["apps", "wifi", "power", "system", "notification"] } diff --git a/assets/opt/usr/share/applications/xochitl.oxide b/assets/opt/usr/share/applications/xochitl.oxide index a397f1c8e..046354577 100644 --- a/assets/opt/usr/share/applications/xochitl.oxide +++ b/assets/opt/usr/share/applications/xochitl.oxide @@ -2,7 +2,7 @@ "displayName": "Xochitl", "description": "Read documents and take notes", "bin": "/opt/bin/xochitl", - "icon": "/opt/etc/draft/icons/xochitl.png", + "icon": "oxide:xochitl-48", "flags": ["nosplash", "chroot"], "permissions": ["power"], "directories": [ diff --git a/assets/opt/usr/share/icons/oxide/48x48/apps/erode.png b/assets/opt/usr/share/icons/oxide/48x48/apps/erode.png new file mode 100644 index 0000000000000000000000000000000000000000..dfcf3b22ca6667f60ce05b26a99f8f6b648c72f4 GIT binary patch literal 377 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tg=CK)Uj~LMH3o);76yi2K%s^g z3=E|}g|8AA7_4S6Fo+k-*%fF5lweBoc6VW5Sknydjsn^C;oi5 zOFV~`*1urCxPo=DSWlEe)cps|0B<)HN{H zH8Kw|w6HR;urjoOXgK{OqZz0{18ze}W^QV6Nn&mRiY3NY2Bub~CJ;TBk8ZF8>S6G7 L^>bP0l+XkK;QMJw literal 0 HcmV?d00001 diff --git a/assets/opt/usr/share/icons/oxide/48x48/apps/image.png b/assets/opt/usr/share/icons/oxide/48x48/apps/image.png new file mode 100644 index 0000000000000000000000000000000000000000..d614d6798ff242e93f4e3161d489bd1fa49ce0b1 GIT binary patch literal 595 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tg=CK)Uj~LMH3o);76yi2K%s^g z3=E|}g|8AA7_4S6Fo+k-*%fF5lweBoc6VW5Sk^BH#}?U<5Q4{4f3)r5?MKwy`q7= z;=q448>4mktNsN>UVb)1{OlDefkM6mtnYt z)up$7_dR3Zqm#=!yBI1(ZIxJ>%>9hly;0R-%9|QFUw+@tQ(s%v8Y-Xl^6b25QmKK`Dr!5VYDkNv06B)+%j^IoeMUt$yll+XSy1 z898IkzVDxmF3KOvn6&8$uSGX7098v|BT7;dOH!?pi&B9UgOP!ev95uUu7O#IfsvJ| zk(G&=wt<0_fx&~NVZkUGa`RI%(<(t440R2Rb&bqJ3@xk-EUXMIAR10T$!G>@(16=e ml9`)YT#}eufMSWUm4TU+5me9D=^5cbJq(_%elF{r5}E)g)6Jv+ literal 0 HcmV?d00001 diff --git a/assets/etc/draft/icons/xochitl.png b/assets/opt/usr/share/icons/oxide/48x48/apps/xochitl.png similarity index 100% rename from assets/etc/draft/icons/xochitl.png rename to assets/opt/usr/share/icons/oxide/48x48/apps/xochitl.png diff --git a/assets/etc/draft/icons/anxiety-splash.png b/assets/opt/usr/share/icons/oxide/702x702/splash/anxiety.png similarity index 100% rename from assets/etc/draft/icons/anxiety-splash.png rename to assets/opt/usr/share/icons/oxide/702x702/splash/anxiety.png diff --git a/assets/etc/draft/icons/erode-splash.png b/assets/opt/usr/share/icons/oxide/702x702/splash/erode.png similarity index 100% rename from assets/etc/draft/icons/erode-splash.png rename to assets/opt/usr/share/icons/oxide/702x702/splash/erode.png diff --git a/assets/etc/draft/icons/oxide-splash.png b/assets/opt/usr/share/icons/oxide/702x702/splash/oxide.png similarity index 100% rename from assets/etc/draft/icons/oxide-splash.png rename to assets/opt/usr/share/icons/oxide/702x702/splash/oxide.png diff --git a/interfaces/application.xml b/interfaces/application.xml index bebe68885..1192d0145 100644 --- a/interfaces/application.xml +++ b/interfaces/application.xml @@ -15,6 +15,7 @@ + diff --git a/oxide.pro b/oxide.pro index 491bd934d..d471787cb 100644 --- a/oxide.pro +++ b/oxide.pro @@ -6,6 +6,4 @@ SUBDIRS = \ applications.depends = shared -INSTALLS += \ - shared \ - applications +INSTALLS += $$SUBDIRS diff --git a/package b/package index 92fb28da8..d0c23801b 100644 --- a/package +++ b/package @@ -2,7 +2,7 @@ # Copyright (c) 2020 The Toltec Contributors # SPDX-License-Identifier: MIT -pkgnames=(erode fret oxide rot tarnish decay corrupt anxiety liboxide libsentry notify-send) +pkgnames=(erode fret oxide rot tarnish decay corrupt anxiety liboxide libsentry oxide-utils) pkgver="2.6~VERSION~" timestamp="$(date -u +%Y-%m-%dT%H:%MZ)" maintainer="Eeems " @@ -24,45 +24,54 @@ build() { erode() { pkgdesc="Task manager" section=utils - installdepends=("tarnish=$pkgver" "liboxide=$pkgver" "libsentry=$_sentry") + installdepends=("tarnish=$pkgver" "oxide-utils=$pkgver" "liboxide=$pkgver" "libsentry=$_sentry") package() { install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/erode - install -D -m 644 -t "$pkgdir"/opt/etc/draft/icons "$srcdir"/release/opt/etc/draft/icons/erode.svg - install -D -m 644 -t "$pkgdir"/opt/etc/draft/icons "$srcdir"/release/opt/etc/draft/icons/erode-splash.png + install -D -m 644 -t "$pkgdir"/opt/usr/share/icons/oxide/48x48/apps "$srcdir"/release/opt/usr/share/icons/oxide/48x48/apps/erode.png + install -D -m 644 -t "$pkgdir"/opt/usr/share/icons/oxide/702x702/splash "$srcdir"/release/opt/usr/share/icons/oxide/702x702/splash/erode.png install -D -m 644 -t "$pkgdir"/opt/usr/share/applications "$srcdir"/release/opt/usr/share/applications/codes.eeems.erode.oxide } + + configure() { + if is-active "tarnish.service"; then + update-desktop-database + fi + } } fret() { pkgdesc="Take screenshots" section=utils - installdepends=("tarnish=$pkgver" "liboxide=$pkgver" "libsentry=$_sentry") + installdepends=("tarnish=$pkgver" "oxide-utils=$pkgver" "liboxide=$pkgver" "libsentry=$_sentry") package() { install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/fret install -D -m 644 -t "$pkgdir"/opt/usr/share/applications "$srcdir"/release/opt/usr/share/applications/codes.eeems.fret.oxide } + + configure() { + if is-active "tarnish.service"; then + update-desktop-database + fi + } } oxide() { pkgdesc="Launcher application" section=launchers - installdepends=("erode=$pkgver" "fret=$pkgver" "tarnish=$pkgver" "rot=$pkgver" "decay=$pkgver" "corrupt=$pkgver" "liboxide=$pkgver" display "libsentry=$_sentry") + installdepends=("erode=$pkgver" "fret=$pkgver" "tarnish=$pkgver" "rot=$pkgver" "decay=$pkgver" "corrupt=$pkgver" "oxide-utils=$pkgver" "liboxide=$pkgver" display "libsentry=$_sentry") package() { install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/oxide install -D -m 644 -t "$pkgdir"/opt/etc "$srcdir"/release/opt/etc/oxide.conf install -D -m 644 -t "$pkgdir"/opt/usr/share/applications "$srcdir"/release/opt/usr/share/applications/codes.eeems.oxide.oxide - install -D -m 644 -t "$pkgdir"/opt/etc/draft/icons "$srcdir"/release/opt/etc/draft/icons/oxide-splash.png + install -D -m 644 -t "$pkgdir"/opt/usr/share/icons/oxide/702x702/splash "$srcdir"/release/opt/usr/share/icons/oxide/702x702/splash/oxide.png } configure() { - if ! is-enabled "tarnish.service"; then - echo "" - echo "Run the following command(s) to use $pkgname as your launcher" - how-to-enable "tarnish.service" - echo "" + if is-active "tarnish.service"; then + update-desktop-database fi } } @@ -90,6 +99,12 @@ tarnish() { configure() { systemctl daemon-reload + if ! is-enabled "tarnish.service"; then + echo "" + echo "Run the following command(s) to use $pkgname as your launcher" + how-to-enable "tarnish.service" + echo "" + fi } preremove() { @@ -107,45 +122,71 @@ tarnish() { decay() { pkgdesc="Lockscreen application" section=utils - installdepends=("tarnish=$pkgver" "liboxide=$pkgver" "libsentry=$_sentry") + installdepends=("tarnish=$pkgver" "oxide-utils=$pkgver" "liboxide=$pkgver" "libsentry=$_sentry") package() { install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/decay install -D -m 644 -t "$pkgdir"/opt/usr/share/applications "$srcdir"/release/opt/usr/share/applications/codes.eeems.decay.oxide } + + configure() { + if is-active "tarnish.service"; then + update-desktop-database + fi + } } corrupt() { pkgdesc="Task Switcher for Oxide" section=utils - installdepends=("tarnish=$pkgver" "liboxide=$pkgver" "libsentry=$_sentry") + installdepends=("tarnish=$pkgver" "oxide-utils=$pkgver" "liboxide=$pkgver" "libsentry=$_sentry") package() { install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/corrupt install -D -m 644 -t "$pkgdir"/opt/usr/share/applications "$srcdir"/release/opt/usr/share/applications/codes.eeems.corrupt.oxide } + + configure() { + if is-active "tarnish.service"; then + update-desktop-database + fi + } } anxiety() { pkgdesc="Screenshot viewer for Oxide" section=utils - installdepends=("tarnish=$pkgver" "liboxide=$pkgver" "libsentry=$_sentry") + installdepends=("tarnish=$pkgver" "oxide-utils=$pkgver" "liboxide=$pkgver" "libsentry=$_sentry") package() { install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/anxiety install -D -m 644 -t "$pkgdir"/opt/usr/share/applications "$srcdir"/release/opt/usr/share/applications/codes.eeems.anxiety.oxide - install -D -m 644 -t "$pkgdir"/opt/etc/draft/icons "$srcdir"/release/opt/etc/draft/icons/image.svg - install -D -m 644 -t "$pkgdir"/opt/etc/draft/icons "$srcdir"/release/opt/etc/draft/icons/anxiety-splash.png + install -D -m 644 -t "$pkgdir"/opt/usr/share/icons/oxide/48x48/apps "$srcdir"/release/opt/usr/share/icons/oxide/48x48/apps/image.png + install -D -m 644 -t "$pkgdir"/opt/usr/share/icons/oxide/702x702/splash "$srcdir"/release/opt/usr/share/icons/oxide/702x702/splash/anxiety.png + } + + configure() { + if is-active "tarnish.service"; then + update-desktop-database + fi } } -notify-send() { - pkgdesc="A program to send desktop notifications for Oxide" +oxide-utils() { + pkgdesc="Command line tools for Oxide" section=utils installdepends=("tarnish=$pkgver" "liboxide=$pkgver" "libsentry=$_sentry") + replaces=(notify-send update-desktop-database desktop-file-validate) + conflicts=(notify-send update-desktop-database desktop-file-validate) package() { install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/notify-send + install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/update-desktop-database + install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/desktop-file-validate + install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/xdg-desktop-menu + install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/xdg-desktop-icon + install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/xdg-open + install -D -m 755 -t "$pkgdir"/opt/bin "$srcdir"/release/opt/bin/gio } } @@ -154,7 +195,7 @@ liboxide() { section=devel package() { - install -D -m 755 -t "$pkgdir"/opt/usr/lib "$srcdir"/release/opt/usr/lib/libliboxide.so* + install -D -m 755 -t "$pkgdir"/opt/lib "$srcdir"/release/opt/lib/libliboxide.so* } } diff --git a/shared/liboxide/applications.cpp b/shared/liboxide/applications.cpp new file mode 100644 index 000000000..a8ff8a5cc --- /dev/null +++ b/shared/liboxide/applications.cpp @@ -0,0 +1,523 @@ +#include "applications.h" +#include "json.h" +#include "liboxide.h" + +#include +#include +#include +#include + +using namespace codes::eeems::oxide1; + +const QList SystemFlags { + "system", + "transient", +}; +const QList Flags { + "autoStart", + "chroot", + "hidden", + "nosplash", + "nosavescreen", + "system", +}; + +namespace Oxide::Applications{ + QList _validateRegistration(const QString& name, const QJsonObject& app, bool exitEarly){ + QList errors; +#define addError(_level, _msg) errors.append(ValidationError { .level = _level, .msg = _msg }); + auto contains = [app, &errors](const QString& name) -> bool{ + if(app.contains(name)){ + return true; + } + addError(ErrorLevel::Critical, QString( + "Key \"%1\" is missing" + ).arg(name)); + return false; + }; + auto isString = [app, &errors, contains](const QString& name, bool required) -> bool{ + if(required && !contains(name)){ + return false; + }else if(!required && !app.contains(name)){ + return false; + } + if(app[name].isString()){ + return true; + } + addError(required ? ErrorLevel::Error : ErrorLevel::Warning, QString( + "Key \"%1\" must contain a string" + ).arg(name)); + return false; + }; + auto isArray = [app, &errors, contains](const QString& name, const ErrorLevel& level, bool required) -> bool{ + if(required && !contains(name)){ + return false; + }else if(!required && !app.contains(name)){ + return false; + } + if(app[name].isArray()){ + return true; + } + addError(level, QString( + "Key \"%1\" must contain an array" + ).arg(name)); + return false; + }; + auto isFile = [app, &errors, isString](const QString& name, const ErrorLevel& level, bool required) -> bool{ + if(!isString(name, required)){ + return false; + } + if(!app.contains(name)){ + return false; + } + auto value = app[name]; + auto str = value.toString(); + if(str.isEmpty()){ + return false; + } + if(QFile::exists(str)){ + return true; + } + addError(level, QString( + "Value \"%1\" for key \"%2\" is a path that does not exist" + ).arg(str, name)); + return false; + }; + auto isIcon = [app, &errors, isString](const QString& name, const ErrorLevel& level, bool required) -> bool{ + if(!isString(name, required)){ + return false; + } + if(!app.contains(name)){ + return false; + } + auto value = app[name].toString(); + auto path = value; + if(QFile::exists(path)){ + QImage image; + if(image.load(path) && !image.isNull()){ + return true; + } + addError(level, QString( + "Value \"%1\" for key \"%2\" is a path to a file that is not a valid image" + ).arg(value, name)); + return false; + } + path = iconPath(value); + if(path.isEmpty()){ + addError(level, QString( + "Value \"%1\" for key \"%2\" is not a valid icon spec" + ).arg(value, name)); + return false; + } + if(!QFile::exists(path)){ + addError(level, QString( + "Value \"%1\" for key \"%2\" is a path that does not exist" + ).arg(value, name)); + return false; + } + QImage image; + if(image.load(path) && !image.isNull()){ + return true; + } + addError(level, QString( + "Value \"%1\" for key \"%2\" is an icon spec for an icon that does not exist" + ).arg(value, name)); + return false; + }; +#define isError(level) (level == ErrorLevel::Error || level == ErrorLevel::Critical) +#define addError(_level, _msg) errors.append(ValidationError { .level = _level, .msg = _msg }); if(exitEarly && isError(_level)){ return errors; } +#define shouldExit if(exitEarly && std::any_of(errors.constBegin(), errors.constEnd(), [](const ValidationError& error){ return isError(error.level); })){ return errors; } + QString type = app.contains("type") ? app["type"].toString().toLower() : ""; + if(type == "background"){ + // TODO validate any extra settings required by background apps + }else if(type == "backgroundable"){ + // TODO validate any extra settings required by backgroundable apps + }else if(type == "foreground" || type.isEmpty()){ + // TODO validate any extra settings required by foreground apps + }else{ + addError(ErrorLevel::Warning, QString( + "Value \"%1\" for key \"type\" is not valid and will default to foreground" + ).arg(type)); + } + if(isFile("bin", ErrorLevel::Error, true)){ + auto bin = app["bin"].toString(); + QFileInfo info(bin); + if(!info.isFile()){ + addError(ErrorLevel::Error, QString( + "Value \"%1\" for key \"bin\" is not a path to a file" + ).arg(bin)); + }else if(!info.isExecutable()){ + addError(ErrorLevel::Error, QString( + "Value \"%1\" for key \"bin\" is a path to a file that is not executable" + ).arg(bin)); + } + } else shouldExit + QStringList flags; + if(isArray("flags", ErrorLevel::Critical, false)){ + auto flagsArray = app["flags"].toArray(); + auto flagsJson = Oxide::JSON::toJson(flagsArray); + for(auto flag : flagsArray){ + if(!flag.isString()){ + addError(ErrorLevel::Error, QString( + "Value \"%1\" for key \"flags\" contains an entry that is not a string \"%2\"" + ).arg(flagsJson, Oxide::JSON::toJson(flag.toVariant()))); + continue; + } + auto value = flag.toString().trimmed(); + if(value.isEmpty()){ + addError(ErrorLevel::Error, QString( + "Value \"%1\" for key \"flags\" contains an entry that is empty" + ).arg(flagsJson)); + continue; + } + if(SystemFlags.contains(value)){ + addError(ErrorLevel::Warning, QString( + "Value \"%1\" for key \"flags\" contains an entry that should only be used by the system \"%2\"" + ).arg(flagsJson, value)); + }else if(!Flags.contains(value)){ + addError(ErrorLevel::Warning, QString( + "Value \"%1\" for key \"flags\" contains an entry that is not known \"%2\"" + ).arg(flagsJson, value)); + } + flags << value; + } + if(type == "background" && flags.contains("nosavescreen")){ + addError(ErrorLevel::Hint, "Key \"flags\" contains \"nosavescreen\" while \"type\" has value \"background\""); + } + } else shouldExit + if(isArray("directories", ErrorLevel::Critical, false)){ + auto directories = app["directories"].toArray(); + for(int i = 0; i < directories.count(); i++){ + QJsonValue entry = directories[i]; + if(!entry.isString()){ + addError(ErrorLevel::Error, QString( + "Value \"%1\" for key \"directories[%2]\" contains an entry that is not a string \"%3\"" + ).arg(Oxide::JSON::toJson(directories), QString::number(i), Oxide::JSON::toJson(entry))); + continue; + } + auto directory = entry.toString(); + if(!QFile::exists(directory)){ + addError(ErrorLevel::Error, QString( + "Value \"%1\" for key \"directories[%2]\" contains a path that does not exist \"%3\"" + ).arg(Oxide::JSON::toJson(directories), QString::number(i), directory)); + } + } + } else shouldExit else if(flags.contains("chroot")){ + addError(ErrorLevel::Hint, "Key \"flags\" contains \"chroot\" while \"directories\" is missing"); + } + if(isArray("permissions", ErrorLevel::Critical, false)){ + auto permissions = app["permissions"].toArray(); + for(int i = 0; i < permissions.count(); i++){ + QJsonValue entry = permissions[i]; + if(!entry.isString()){ + addError(ErrorLevel::Error, QString( + "Value \"%1\" for key \"permissions[%2]\" contains a value that is not a string \"%3\"" + ).arg(Oxide::JSON::toJson(permissions), QString::number(i), Oxide::JSON::toJson(entry))); + } + } + } else shouldExit + isFile("workingDirectory", ErrorLevel::Error, false); shouldExit + isString("displayName", false); shouldExit + isString("description", false); shouldExit + if(isString("user", false)){ + auto user = app["user"].toString(); + try{ + Oxide::getUID(user); + }catch(const std::exception& e){ + addError(ErrorLevel::Error, QString( + "Value \"%1\" for key \"user\" is not a valid user: \"%2\"" + ).arg(user, e.what())); + } + } else shouldExit + if(isString("group", false)){ + auto group = app["group"].toString(); + try{ + Oxide::getGID(group); + }catch(const std::exception& e){ + addError(ErrorLevel::Error, QString( + "Value \"%1\" for key \"group\" is not a valid group: \"%2\"" + ).arg(group, e.what())); + } + } else shouldExit + isIcon("icon", ErrorLevel::Warning, false); + if(isIcon("splash", ErrorLevel::Warning, false) && flags.contains("nosplash")){ + addError(ErrorLevel::Hint, "Key \"splash\" provided while \"flags\" contains \"nosplash\" value"); + } else shouldExit + if(registrationToMap(app, name).isEmpty()){ + addError(ErrorLevel::Critical, "Unable to convert registration to QVariantMap"); + } + return errors; +#undef isError +#undef addError +#undef shouldExit +} + QTextStream& operator<<(QTextStream& s, const ErrorLevel& l){ + switch (l) { + case ErrorLevel::Hint: + s << "hint"; + break; + case ErrorLevel::Deprecation: + s << "deprecation"; + break; + case ErrorLevel::Warning: + s << "warning"; + break; + case ErrorLevel::Error: + s << "error"; + break; + case ErrorLevel::Critical: + s << "critical"; + break; + default: + s << "unknown"; + } + return s; + } + QTextStream& operator<<(QTextStream& s, const ValidationError& e){ + s << e.level << ": " << e.msg; + return s; + } + QDebug& operator<<(QDebug& s, const ErrorLevel& l){ + switch (l) { + case ErrorLevel::Hint: + s << "hint"; + break; + case ErrorLevel::Deprecation: + s << "deprecation"; + break; + case ErrorLevel::Warning: + s << "warning"; + break; + case ErrorLevel::Error: + s << "error"; + break; + case ErrorLevel::Critical: + s << "critical"; + break; + default: + s << "unknown"; + } + return s; + } + QDebug& operator<<(QDebug& s, const ValidationError& e){ + s << e.level << ": " << e.msg; + return s; + } + bool operator==(const ValidationError& v1, const ValidationError& v2){ return v1.level == v2.level && v1.msg == v2.msg; } + QVariantMap registrationToMap(const QJsonObject& app, const QString& name){ + auto _name = name; + if(name.isEmpty()){ + if(!app.contains("name") || !app["name"].isString() || app["name"].toString().isEmpty()){ + return QVariantMap(); + } + _name = app["name"].toString(); + } + int type = Foreground; + QString typeString = app.contains("type") ? app["type"].toString().toLower() : ""; + if(typeString == "background"){ + type = Background; + }else if(typeString == "backgroundable"){ + type = Backgroundable; + } + // Anything else defaults to Foreground + auto flags = QStringList(); + if(app.contains("flags")){ + for(auto flag : app["flags"].toArray()){ + if(!flag.isString()){ + continue; + } + auto value = flag.toString(); + if(!value.isEmpty()){ + flags << value; + } + } + } + QVariantMap properties { + {"name", _name}, + {"bin", app["bin"].toString()}, + {"type", type}, + {"flags", flags}, + }; + if(app.contains("displayName")){ + properties.insert("displayName", app["displayName"].toString()); + } + if(app.contains("description")){ + properties.insert("description", app["description"].toString()); + } + if(app.contains("icon")){ + properties.insert("icon", app["icon"].toString()); + } + if(app.contains("user")){ + properties.insert("user", app["user"].toString()); + } + if(app.contains("group")){ + properties.insert("group", app["group"].toString()); + } + if(app.contains("workingDirectory")){ + properties.insert("workingDirectory", app["workingDirectory"].toString()); + } + if(app.contains("directories")){ + QStringList directories; + for(auto directory : app["directories"].toArray()){ + directories.append(directory.toString()); + } + properties.insert("directories", directories); + } + if(app.contains("permissions")){ + QStringList permissions; + for(auto permission : app["permissions"].toArray()){ + permissions.append(permission.toString()); + } + properties.insert("permissions", permissions); + } + if(app.contains("events")){ + auto events = app["events"].toObject(); + for(auto event : events.keys()){ + if(event == "stop"){ + properties.insert("onStop", events[event].toString()); + }else if(event == "pause"){ + properties.insert("onPause", events[event].toString()); + }else if(event == "resume"){ + properties.insert("onResume", events[event].toString()); + } + } + } + if(app.contains("environment")){ + QVariantMap envMap; + auto environment = app["environment"].toObject(); + for(auto key : environment.keys()){ + envMap.insert(key, environment[key].toString()); + } + properties.insert("environment", envMap); + } + if(app.contains("splash")){ + properties.insert("splash", app["splash"].toString()); + } + return properties; + } + QJsonObject getRegistration(const char* path){ return getRegistration(QString(path)); } + QJsonObject getRegistration(const std::string& path){ return getRegistration(QString(path.c_str())); } + QJsonObject getRegistration(const QString& path){ + QFile file(path); + auto res = getRegistration(&file); + if(file.isOpen()){ + file.close(); + } + return res; + } + QJsonObject getRegistration(QFile* file){ + if(!file->isOpen() && !file->open(QFile::ReadOnly)){ + return QJsonObject(); + } + auto data = file->readAll(); + auto app = QJsonDocument::fromJson(data).object(); + return app; + } + QList validateRegistration(const char* path){ return validateRegistration(QString(path)); } + QList validateRegistration(const std::string& path){ return validateRegistration(QString(path.c_str())); } + QList validateRegistration(const QString& path){ + QFile file(path); + auto res = validateRegistration(&file); + if(file.isOpen()){ + file.close(); + } + return res; + } + QList validateRegistration(QFile* file){ + if(!file->isOpen() && !file->open(QFile::ReadOnly)){ + QList errors; + errors.append(ValidationError{ + .level = ErrorLevel::Critical, + .msg = "Could not open file" + }); + return errors; + } + auto data = file->readAll(); + auto app = QJsonDocument::fromJson(data).object(); + if(!app.isEmpty()){ + return validateRegistration(QFileInfo(file->fileName()).completeBaseName(), app); + } + QList errors; + errors.append(ValidationError{ + .level = ErrorLevel::Critical, + .msg = "File is not valid JSON or is empty" + }); + return errors; + } + QList validateRegistration(const QString& name, const QJsonObject& app){ return _validateRegistration(name, app, false); } + bool addToTarnishCache(const char* path){ return addToTarnishCache(QString(path)); } + bool addToTarnishCache(const std::string& path){ return addToTarnishCache(QString(path.c_str())); } + bool addToTarnishCache(const QString& path){ + QFile file(path); + auto res = addToTarnishCache(&file); + if(file.isOpen()){ + file.close(); + } + return res; + } + bool addToTarnishCache(QFile* file){ + auto app = getRegistration(file); + auto name = QFileInfo(file->fileName()).completeBaseName(); + return addToTarnishCache(name, app); + } + bool addToTarnishCache(const QString& name, const QJsonObject& app){ + if(app.isEmpty()){ + return false; + } + if(!_validateRegistration(name, app, true).isEmpty()){ + return false; + } + auto bus = QDBusConnection::systemBus(); + General api(OXIDE_SERVICE, OXIDE_SERVICE_PATH, bus); + QDBusObjectPath path = api.requestAPI("apps"); + if(path.path() == "/"){ + return false; + } + Apps apps(OXIDE_SERVICE, path.path(), bus); + auto properties = registrationToMap(app, name); + if(properties.isEmpty()){ + return false; + } + path = apps.registerApplication(properties); + return path.path() != "/"; + } + QString iconDirPath(int size, const QString& theme, const QString& context){ + return QString(OXIDE_ICONS_DIRECTORY "/%1/%2x%2/%3").arg( + theme, + QString::number(size), + context + ); + } + QString iconPath(const QString& name, int size, const QString& theme, const QString& context){ + return QString(OXIDE_ICONS_DIRECTORY "/%1/%2x%2/%3/%4.png").arg( + theme, + QString::number(size), + context, + name + ); + } + QString iconPath(const QString& spec){ + if(spec.isEmpty() || !spec.contains("-")){ + return ""; + } + auto parts = spec.split('-'); + auto size = parts.last().toUInt(); + if(size == 0){ + return ""; + } + parts.removeLast(); + auto name = parts.join('-'); + if(!name.contains(":")){ + return iconPath(name, size); + } + parts = name.split(':'); + auto len = parts.length(); + if(len == 1 || len > 3){ + return ""; + } + if(len == 2){ + return iconPath(parts.last(), size, parts.first()); + } + return iconPath(parts.last(), size, parts.first(), parts.at(1)); + } +} diff --git a/shared/liboxide/applications.h b/shared/liboxide/applications.h new file mode 100644 index 000000000..007b81a9d --- /dev/null +++ b/shared/liboxide/applications.h @@ -0,0 +1,212 @@ +/*! + * \addtogroup Applications + * \brief The Applications module + * @{ + * \file + */ +#pragma once + +#include "liboxide_global.h" + +#include +#include +#include + +#include + +/*! + * \def OXIDE_APPLICATION_REGISTRATIONS_DIRECTORY + * \brief Application directory location + */ +#define OXIDE_APPLICATION_REGISTRATIONS_DIRECTORY "/opt/usr/share/applications" +#define OXIDE_ICONS_DIRECTORY "/opt/usr/share/icons" + +/*! + * \brief The applications namespace + */ +namespace Oxide::Applications{ + /*! + * \typedef ApplicationType + * \brief Application registration type + */ + typedef enum { + Foreground, /*!< Only runs in the foreground */ + Background, /*!< Only runs in the background */ + Backgroundable /*!< Runs in either the foreground or background */ + } ApplicationType; + /*! + * \typedef ErrorLevel + * \brief ValidationError error levels + */ + typedef enum{ + Hint, /*!< A hint */ + Deprecation, /*!< A deprecation warning */ + Warning, /*!< A warning */ + Error, /*!< An error */ + Critical /*!< A critical error*/ + } ErrorLevel; + /*! + * \struct ValidationError + * \brief Errors returned by validateRegistration + */ + typedef struct{ + ErrorLevel level; /*!< Error level */ + QString msg; /*!< Error message */ + } ValidationError; + /*! + * \brief Convert an application registration to a QVariantMap + * \param Application registration to convert + * \param Name of the application + * \return QVariantMap of the cached application + */ + LIBOXIDE_EXPORT QVariantMap registrationToMap(const QJsonObject& app, const QString& name = ""); + /*! + * \brief Convert an ErrorLevel to a human readable string when piping to a text stream + * \param Text stream to write to + * \param Error level + * \return Resulting text stream + * \sa ErrorLevel + */ + LIBOXIDE_EXPORT QTextStream& operator<<(QTextStream& s, const ErrorLevel& l); + /*! + * \brief Convert an ValidationError to a human readable string when piping to a text stream + * \param Text stream to write to + * \param Validation error to convert + * \return Resulting text stream + * \sa ValidationError + */ + LIBOXIDE_EXPORT QTextStream& operator<<(QTextStream& s, const ValidationError& t); + /*! + * \brief Convert an ErrorLevel to a human readable string when piping to a text stream + * \param Text stream to write to + * \param Error level + * \return Resulting text stream + * \sa ErrorLevel + */ + LIBOXIDE_EXPORT QDebug& operator<<(QDebug& s, const ErrorLevel& l); + /*! + * \brief Convert an ValidationError to a human readable string when piping to a text stream + * \param Text stream to write to + * \param Validation error to convert + * \return Resulting text stream + * \sa ValidationError + */ + LIBOXIDE_EXPORT QDebug& operator<<(QDebug& s, const ValidationError& t); + /*! + * \brief Compare two ValidationError instances + * \param First instance to comprae + * \param Second instance to compare + * \return If they are the same + * \sa ValidationError + */ + LIBOXIDE_EXPORT bool operator==(const ValidationError& v1, const ValidationError& v2); + /*! + * \brief Get an application registration. + * \param Path to application registration + * \return Application registration + */ + LIBOXIDE_EXPORT QJsonObject getRegistration(const char* path); + /*! + * \brief Get an application registration. + * \param Path to application registration + * \return Application registration + */ + LIBOXIDE_EXPORT QJsonObject getRegistration(const std::string& path); + /*! + * \brief Get an application registration. + * \param Path to application registration + * \return Application registration + */ + LIBOXIDE_EXPORT QJsonObject getRegistration(const QString& path); + /*! + * \brief Get an application registration. + * \param Application registration file + * \return Application registration + */ + LIBOXIDE_EXPORT QJsonObject getRegistration(QFile* file); + /*! + * \brief Validate a application registration file and return any errors found + * \param Path to the file to validate + * \return List of validation errors + */ + LIBOXIDE_EXPORT QList validateRegistration(const char* path); + /*! + * \brief Validate a application registration file and return any errors found + * \param Path to the file to validate + * \return List of validation errors + */ + LIBOXIDE_EXPORT QList validateRegistration(const std::string& path); + /*! + * \brief Validate a application registration file and return any errors found + * \param Path to the file to validate + * \return List of validation errors + */ + LIBOXIDE_EXPORT QList validateRegistration(const QString& path); + /*! + * \brief Validate a application registration file and return any errors found + * \param The file to validate + * \return List of validation errors + */ + LIBOXIDE_EXPORT QList validateRegistration(QFile* file); + /*! + * \brief Validate a application registration file and return any errors found + * \param The file to validate + * \return List of validation errors + */ + LIBOXIDE_EXPORT QList validateRegistration(const QString& name, const QJsonObject& app); + /*! + * \brief Add an application to the tarnish application cache + * \param Path to the application registration file + * \return If the application was successfully added + */ + LIBOXIDE_EXPORT bool addToTarnishCache(const char* path); + /*! + * \brief Add an application to the tarnish application cache + * \param Path to the application registration file + * \return If the application was successfully added + */ + LIBOXIDE_EXPORT bool addToTarnishCache(const std::string& path); + /*! + * \brief Add an application to the tarnish application cache + * \param Path to the application registration file + * \return If the application was successfully added + */ + LIBOXIDE_EXPORT bool addToTarnishCache(const QString& path); + /*! + * \brief Add an application to the tarnish application cache + * \param The application registration file + * \return If the application was successfully added + */ + LIBOXIDE_EXPORT bool addToTarnishCache(QFile* file); + /*! + * \brief Add an application to the tarnish application cache + * \param The name of the application + * \param The application registration file + * \return If the application was successfully added + */ + LIBOXIDE_EXPORT bool addToTarnishCache(const QString& name, const QJsonObject& app); + /*! + * \brief Get the path to the directory that an icon would be stored in. + * \param Size of the icon. + * \param Icon theme. Default is hicolor. + * \param Icon context. Default is apps. + * \return Path to the directory that would contain the icon. + */ + LIBOXIDE_EXPORT QString iconDirPath(int size, const QString& theme = "hicolor", const QString& context = "apps"); + /*! + * \brief Get the path to an icon. + * \param Name of the icon. + * \param Size of the icon. + * \param Icon theme. Default is hicolor. + * \param Icon context. Default is apps. + * \return Path to the icon. + */ + LIBOXIDE_EXPORT QString iconPath(const QString& name, int size, const QString& theme = "hicolor", const QString& context = "apps"); + /*! + * \brief Get the path to an icon from an icon name spec. + * \param Icon name spec using the following format: [theme:][context:]{name}-{size}. e.g. oxide:splash:xochitl-702 + * \return Path to the icon. An empty string if it failed to parse the spec. + */ + LIBOXIDE_EXPORT QString iconPath(const QString& spec); +} +/*! @} */ diff --git a/shared/liboxide/dbus.h b/shared/liboxide/dbus.h new file mode 100644 index 000000000..4a504b2e6 --- /dev/null +++ b/shared/liboxide/dbus.h @@ -0,0 +1,26 @@ +/*! + * \addtogroup DBus + * \brief The DBus module + * @{ + * \file + */ +#pragma once +// This must be here to make precompiled headers happy +#ifndef LIBOXIDE_DBUS_H +#define LIBOXIDE_DBUS_H + +#include "dbusservice_interface.h" +#include "powerapi_interface.h" +#include "wifiapi_interface.h" +#include "network_interface.h" +#include "bss_interface.h" +#include "appsapi_interface.h" +#include "application_interface.h" +#include "systemapi_interface.h" +#include "screenapi_interface.h" +#include "screenshot_interface.h" +#include "notificationapi_interface.h" +#include "notification_interface.h" + +#endif // LIBOXIDE_DBUS_H +/*! @} */ diff --git a/shared/liboxide/debug.h b/shared/liboxide/debug.h index 2434ca599..a99080145 100644 --- a/shared/liboxide/debug.h +++ b/shared/liboxide/debug.h @@ -1,13 +1,13 @@ /*! - * \file debug.h + * \addtogroup Oxide + * @{ + * \file */ -#ifndef DEBUG_H -#define DEBUG_H +#pragma once #include "liboxide_global.h" #include - /*! * \def O_DEBUG(msg) * \brief Log a debug message if compiled with DEBUG mode, and debugging is enabled @@ -33,5 +33,4 @@ namespace Oxide { */ LIBOXIDE_EXPORT bool debugEnabled(); } - -#endif // DEBUG_H +/*! @} */ diff --git a/shared/liboxide/eventfilter.h b/shared/liboxide/eventfilter.h index 07fa721a0..8faa1c62a 100644 --- a/shared/liboxide/eventfilter.h +++ b/shared/liboxide/eventfilter.h @@ -1,13 +1,13 @@ /*! - * \file eventfilter.h + * \addtogroup Oxide + * @{ + * \file */ -#ifndef EVENTFILTER_H -#define EVENTFILTER_H +#pragma once #include #include #include - namespace Oxide{ /*! * \brief An event filter that maps pen events to Qt touch events @@ -31,5 +31,4 @@ namespace Oxide{ bool eventFilter(QObject* obj, QEvent* ev); }; } - -#endif // EVENTFILTER_H +/*! @} */ diff --git a/shared/liboxide/examples/oxide.cpp b/shared/liboxide/examples/oxide.cpp index 191fc7946..4fa0c3b94 100644 --- a/shared/liboxide/examples/oxide.cpp +++ b/shared/liboxide/examples/oxide.cpp @@ -1,11 +1,11 @@ /*! - * \file examples/oxide.cpp + * \file */ //! [debugEnabled] if(Oxide::debugEnabled()){ qDebug() << "Debugging is enabled"; } -//! [debugEnabled] +//! [debugEnabled] //! [dispatchToMainThread] Oxide::dispatchToMainThread([=]{ qDebug() << "This is running on the main thread"; @@ -18,3 +18,19 @@ connect(signalHandler, &SignalHandler::sigUsr1, [=]{ qDebug() << "SIGUSR1 recieved"; }); //! [SignalHandler] +//! [getGID] +try{ + auto gid = Oxide::getGID("admin"); + qDebug() << "admin GID is " << gid; +}catch(const std::exception& e){ + qDebug() << "Failed to get group: " << e.what(); +} +//! [getGID] +//! [getUID] +try{ + auto gid = Oxide::getUID("root"); + qDebug() << "root UID is " << gid; +}catch(const std::exception& e){ + qDebug() << "Failed to get user: " << e.what(); +} +//! [getUID] diff --git a/shared/liboxide/json.cpp b/shared/liboxide/json.cpp index 712e052fa..f71340be4 100644 --- a/shared/liboxide/json.cpp +++ b/shared/liboxide/json.cpp @@ -177,7 +177,7 @@ namespace Oxide::JSON { } return value; } - QString toJson(QVariant value){ + QString toJson(QVariant value, QJsonDocument::JsonFormat format){ if(value.isNull()){ return "null"; } @@ -188,6 +188,18 @@ namespace Oxide::JSON { if(jsonVariant.isUndefined()){ return "undefined"; } + if(jsonVariant.isArray()){ + QJsonDocument doc(jsonVariant.toArray()); + return doc.toJson(format); + } + if(jsonVariant.isObject()){ + QJsonDocument doc(jsonVariant.toObject()); + return doc.toJson(format); + } + if(jsonVariant.isBool()){ + return jsonVariant.toBool() ? "true" : "false"; + } + // Number, string or other unknown type QJsonArray jsonArray; jsonArray.append(jsonVariant); QJsonDocument doc(jsonArray); @@ -203,4 +215,5 @@ namespace Oxide::JSON { } return doc.array().first().toVariant(); } + QVariant fromJson(QFile* file){ return fromJson(file->readAll()); } } diff --git a/shared/liboxide/json.h b/shared/liboxide/json.h index 9dc27fc3a..da9ecf8cd 100644 --- a/shared/liboxide/json.h +++ b/shared/liboxide/json.h @@ -1,17 +1,18 @@ /*! - * \file json.h + * \addtogroup JSON + * \brief The JSON module + * @{ + * \file */ -#ifndef JSON_H -#define JSON_H +#pragma once #include "liboxide_global.h" #include #include #include - /*! - * The JSON namespace + * \brief The JSON namespace */ namespace Oxide::JSON { /*! @@ -29,14 +30,21 @@ namespace Oxide::JSON { /*! * \brief Convert a QVariant to a JSON string * \param QVariant to convert + * \param Format to use * \return JSON string */ - LIBOXIDE_EXPORT QString toJson(QVariant value); + LIBOXIDE_EXPORT QString toJson(QVariant value, QJsonDocument::JsonFormat format = QJsonDocument::Compact); /*! * \brief Convert a JSON string into a QVariant * \param JSON string to convert * \return The converted QVaraint */ LIBOXIDE_EXPORT QVariant fromJson(QByteArray json); + /*! + * \brief Convert a JSON file into a QVariant + * \param JSON fle to convert + * \return The converted QVaraint + */ + LIBOXIDE_EXPORT QVariant fromJson(QFile* file); } -#endif // JSON_H +/*! @} */ diff --git a/shared/liboxide/liboxide.cpp b/shared/liboxide/liboxide.cpp index 102d1d3f2..26699f066 100644 --- a/shared/liboxide/liboxide.cpp +++ b/shared/liboxide/liboxide.cpp @@ -5,7 +5,12 @@ #include #include +#include +#include #include +#include +#include +#include #define BITS_PER_LONG (sizeof(long) * 8) @@ -15,6 +20,82 @@ #define test_bit(bit, array) ((array[LONG(bit)] >> OFF(bit)) & 1) namespace Oxide { + QString execute(const QString& program, const QStringList& args){ + QString output; + QProcess p; + p.setProgram(program); + p.setArguments(args); + p.setProcessChannelMode(QProcess::MergedChannels); + p.connect(&p, &QProcess::readyReadStandardOutput, [&p, &output]{ + output += (QString)p.readAllStandardOutput(); + }); + p.start(); + p.waitForFinished(); + return output; + } + // https://stackoverflow.com/a/1643134 + int tryGetLock(char const* lockName){ + mode_t m = umask(0); + int fd = open(lockName, O_RDWR | O_CREAT, 0666); + umask(m); + if(fd < 0){ + return -1; + } + if(!flock(fd, LOCK_EX | LOCK_NB)){ + return fd; + } + close(fd); + return -1; + } + void releaseLock(int fd, char const* lockName){ + if(fd < 0){ + return; + } + if(!flock(fd, F_ULOCK | LOCK_NB)){ + remove(lockName); + } + close(fd); + } + bool processExists(pid_t pid){ return QFile::exists(QString("/proc/%1").arg(pid)); } + QList lsof(const QString& path){ + QList pids; + QDir directory("/proc"); + if (!directory.exists() || directory.isEmpty()){ + qCritical() << "Unable to access /proc"; + return pids; + } + QString qpath(QFileInfo(path).canonicalFilePath()); + auto processes = directory.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable, QDir::Name); + // Get all pids we care about + for(QFileInfo fi : processes){ + auto pid = fi.baseName().toUInt(); + if(!pid || !processExists(pid)){ + continue; + } + QFile statm(QString("/proc/%1/statm").arg(pid)); + QTextStream stream(&statm); + if(!statm.open(QIODevice::ReadOnly | QIODevice::Text)){ + continue; + } + QString content = stream.readAll().trimmed(); + statm.close(); + // Ignore kernel processes + if(content == "0 0 0 0 0 0 0"){ + continue; + } + QDir fd_directory(QString("/proc/%1/fd").arg(pid)); + if(!fd_directory.exists() || fd_directory.isEmpty()){ + continue; + } + auto fds = fd_directory.entryInfoList(QDir::Files | QDir::NoDot | QDir::NoDotDot); + for(QFileInfo fd : fds){ + if(fd.canonicalFilePath() == qpath){ + pids.append(pid); + } + } + } + return pids; + } void dispatchToMainThread(std::function callback){ if(QThread::currentThread() == qApp->thread()){ callback(); @@ -31,6 +112,32 @@ namespace Oxide { }); QMetaObject::invokeMethod(timer, "start", Qt::BlockingQueuedConnection, Q_ARG(int, 0)); } + uid_t getUID(const QString& name){ + char buffer[1024]; + struct passwd user; + struct passwd* result; + auto status = getpwnam_r(name.toStdString().c_str(), &user, buffer, sizeof(buffer), &result); + if(status != 0){ + throw std::runtime_error("Failed to get user" + status); + } + if(result == NULL){ + throw std::runtime_error("Invalid user name: " + name.toStdString()); + } + return result->pw_uid; + } + gid_t getGID(const QString& name){ + char buffer[1024]; + struct group grp; + struct group* result; + auto status = getgrnam_r(name.toStdString().c_str(), &grp, buffer, sizeof(buffer), &result); + if(status != 0){ + throw std::runtime_error("Failed to get group" + status); + } + if(result == NULL){ + throw std::runtime_error("Invalid group name: " + name.toStdString()); + } + return result->gr_gid; + } DeviceSettings& DeviceSettings::instance() { static DeviceSettings INSTANCE; return INSTANCE; diff --git a/shared/liboxide/liboxide.h b/shared/liboxide/liboxide.h index b586b34b6..ed34c1078 100644 --- a/shared/liboxide/liboxide.h +++ b/shared/liboxide/liboxide.h @@ -1,12 +1,16 @@ /*! - * \file liboxide.h + * \addtogroup Oxide + * \brief The main Oxide module + * @{ + * \file */ -#ifndef LIBOXIDE_H -#define LIBOXIDE_H +#pragma once #include "liboxide_global.h" #include "meta.h" +#include "dbus.h" +#include "applications.h" #include "settingsfile.h" #include "power.h" #include "json.h" @@ -22,6 +26,7 @@ #include #include +#include /*! * \def deviceSettings() * \brief Get the Oxide::DeviceSettings instance @@ -44,15 +49,40 @@ */ typedef QMap WifiNetworks; Q_DECLARE_METATYPE(WifiNetworks); -/*! - * \addtogroup Oxide - * \brief The main Oxide namespace - * @{ - */ /*! * \brief The main Oxide namespace */ namespace Oxide { + /*! + * \brief Execute a program and return it's output + * \param Program to run + * \param Arguments to pass to the program + * \return Output if it ran. Otherwise NULL. + */ + LIBOXIDE_EXPORT QString execute(const QString& program, const QStringList& args); + /*! + * \brief Try to get a lock + * \param Path to the lock file + * \return File descriptor of the lock file if a positive number or -1 if it errored + */ + LIBOXIDE_EXPORT int tryGetLock(char const *lockName); + /*! + * \brief Release a lock file + * \param File descriptor of the lock file + * \param Path to the lock file + */ + LIBOXIDE_EXPORT void releaseLock(int fd, char const* lockName); + /*! + * \brief Checks to see if a process exists + * \return If the process exists + */ + LIBOXIDE_EXPORT bool processExists(pid_t pid); + /*! + * \brief Get list of pids that have a file open + * \param File to check + * \return list of pids that have the file open + */ + LIBOXIDE_EXPORT QList lsof(const QString& path); /*! * \brief Run code on the main Qt thread * \param callback The code to run on the main thread @@ -60,6 +90,22 @@ namespace Oxide { * \snippet examples/oxide.cpp dispatchToMainThread */ LIBOXIDE_EXPORT void dispatchToMainThread(std::function callback); + /*! + * \brief Get the UID for a username + * \param Username to search for + * \throws std::runtime_error Failed to get the UID for the username + * \return The UID for the username + * \snippet examples/oxide.cpp getUID + */ + LIBOXIDE_EXPORT uid_t getUID(const QString& name); + /*! + * \brief Get the GID for a groupname + * \param Groupname to search for + * \throws std::runtime_error Failed to get the GID for the groupname + * \return The GID for the groupname + * \snippet examples/oxide.cpp getGID + */ + LIBOXIDE_EXPORT gid_t getGID(const QString& name); /*! * \brief Device specific values */ @@ -226,7 +272,30 @@ namespace Oxide { */ // cppcheck-suppress uninitMemberVarPrivate O_SETTINGS(SharedSettings, "/home/root/.config/Eeems/shared.conf") + /*! + * \property version + * \brief Current version of the settings file + * \sa set_version, versionChanged + */ + /*! + * \fn versionChanged + * \brief If the version number has changed + */ O_SETTINGS_PROPERTY(int, General, version) + /*! + * \property firstLaunch + * \brief If this is the first time that things have been run + * \sa set_firstLaunch, firstLaunchChanged + */ + /*! + * \fn set_firstLaunch + * \param _arg_firstLaunch + * \brief Change the state of firstLaunch + */ + /*! + * \fn firstLaunchChanged + * \brief If firstLaunch has changed + */ O_SETTINGS_PROPERTY(bool, General, firstLaunch, true) /*! * \property telemetry @@ -279,4 +348,3 @@ namespace Oxide { }; } /*! @} */ -#endif // LIBOXIDE_H diff --git a/shared/liboxide/liboxide.pro b/shared/liboxide/liboxide.pro index 2c2c96185..e262925b0 100644 --- a/shared/liboxide/liboxide.pro +++ b/shared/liboxide/liboxide.pro @@ -12,6 +12,7 @@ CONFIG += precompile_header DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 SOURCES += \ + applications.cpp \ debug.cpp \ eventfilter.cpp \ json.cpp \ @@ -24,6 +25,8 @@ SOURCES += \ signalhandler.cpp HEADERS += \ + applications.h \ + dbus.h \ debug.h \ eventfilter.h \ liboxide_global.h \ @@ -40,27 +43,44 @@ HEADERS += \ PRECOMPILED_HEADER = \ liboxide_stable.h +DBUS_INTERFACES += \ + ../../interfaces/dbusservice.xml \ + ../../interfaces/powerapi.xml \ + ../../interfaces/wifiapi.xml \ + ../../interfaces/network.xml \ + ../../interfaces/bss.xml \ + ../../interfaces/appsapi.xml \ + ../../interfaces/application.xml \ + ../../interfaces/systemapi.xml \ + ../../interfaces/screenapi.xml \ + ../../interfaces/screenshot.xml \ + ../../interfaces/notificationapi.xml \ + ../../interfaces/notification.xml + LIBS += -lsystemd -include.target = include/liboxide -include.commands = \ +liboxide_liboxide_h.target = include/liboxide/liboxide.h +liboxide_liboxide_h.commands = \ mkdir -p include/liboxide && \ - echo $$HEADERS | xargs -rn1 | xargs -rI {} cp $$PWD/{} include/liboxide/ + echo $$HEADERS | xargs -rn1 | xargs -rI {} cp $$PWD/{} include/liboxide/ && \ + echo $$DBUS_INTERFACES | xargs -rn1 | xargs -rI {} basename \"{}\" .xml | xargs -rI {} cp $$OUT_PWD/\"{}\"_interface.h include/liboxide/ liboxide_h.target = include/liboxide.h -liboxide_h.depends += include +liboxide_h.depends += liboxide_liboxide_h liboxide_h.commands = \ - echo \\$$LITERAL_HASH"ifndef LIBOXIDE" > include/liboxide.h && \ - echo \\$$LITERAL_HASH"define LIBOXIDE" >> include/liboxide.h && \ - echo \"$$LITERAL_HASH"include \\\"liboxide/liboxide.h\\\"\"" >> include/liboxide.h && \ - echo \\$$LITERAL_HASH"endif // LIBOXIDE" >> include/liboxide.h + echo \\$$LITERAL_HASH"pragma once" > include/liboxide.h && \ + echo \"$$LITERAL_HASH"include \\\"liboxide/liboxide.h\\\"\"" >> include/liboxide.h +clean_headers.target = include/.clean-target +clean_headers.commands = rm -rf include -QMAKE_EXTRA_TARGETS += include liboxide_h -POST_TARGETDEPS += include/liboxide.h +QMAKE_EXTRA_TARGETS += liboxide_liboxide_h liboxide_h clean_headers +PRE_TARGETDEPS += $$clean_headers.target +POST_TARGETDEPS += $$liboxide_liboxide_h.target $$liboxide_h.target +QMAKE_CLEAN += $$liboxide_h.target include/liboxide/*.h include(../../qmake/common.pri) -target.path = /opt/usr/lib +target.path = /opt/lib INSTALLS += target include(../../qmake/epaper.pri) diff --git a/shared/liboxide/liboxide_global.h b/shared/liboxide/liboxide_global.h index 8845ffb1c..719e84775 100644 --- a/shared/liboxide/liboxide_global.h +++ b/shared/liboxide/liboxide_global.h @@ -1,9 +1,9 @@ /*! - * \file liboxide_global.h + * \addtogroup Oxide + * @{ + * \file */ -#ifndef LIBOXIDE_GLOBAL_H -#define LIBOXIDE_GLOBAL_H - +#pragma once #include @@ -18,5 +18,4 @@ # define DEBUG # define LIBOXIDE_EXPORT #endif - -#endif // LIBOXIDE_GLOBAL_H +/*! @} */ diff --git a/shared/liboxide/liboxide_stable.h b/shared/liboxide/liboxide_stable.h index 551a2873b..aff608cf5 100644 --- a/shared/liboxide/liboxide_stable.h +++ b/shared/liboxide/liboxide_stable.h @@ -2,7 +2,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -44,10 +46,22 @@ #include #include #include +#include #include #include #include +#include "application_interface.h" +#include "appsapi_interface.h" +#include "bss_interface.h" +#include "dbusservice_interface.h" +#include "network_interface.h" +#include "notification_interface.h" +#include "notificationapi_interface.h" +#include "powerapi_interface.h" +#include "systemapi_interface.h" +#include "wifiapi_interface.h" + #include "liboxide_global.h" #include "meta.h" #endif diff --git a/shared/liboxide/meta.h b/shared/liboxide/meta.h index 887a100cb..2a7a8150a 100644 --- a/shared/liboxide/meta.h +++ b/shared/liboxide/meta.h @@ -1,9 +1,10 @@ /*! - * \file meta.h + * \addtogroup Oxide + * @{ + * \file */ -#ifndef META_H -#define META_H +#pragma once /*! * \def WPA_SUPPLICANT_SERVICE * \brief wpa_supplicant DBus service name @@ -90,5 +91,4 @@ * \brief DBus service for a screenshot object */ #define OXIDE_SCREENSHOT_INTERFACE OXIDE_SERVICE ".Screenshot" - -#endif // META_H +/*! @} */ diff --git a/shared/liboxide/oxide_sentry.h b/shared/liboxide/oxide_sentry.h index fbb4a01e7..cd30a415d 100644 --- a/shared/liboxide/oxide_sentry.h +++ b/shared/liboxide/oxide_sentry.h @@ -1,8 +1,10 @@ /*! - * \file sentry.h + * \addtogroup Sentry + * \brief The Sentry module + * @{ + * \file */ -#ifndef OXIDE_SENTRY_H -#define OXIDE_SENTRY_H +#pragma once #include "liboxide_global.h" @@ -14,7 +16,6 @@ #ifdef SENTRY #include #endif - /*! *\brief The Sentry namespace */ @@ -159,5 +160,4 @@ namespace Oxide::Sentry{ */ LIBOXIDE_EXPORT void trigger_crash(); } - -#endif // OXIDE_SENTRY_H +/*! @} */ diff --git a/shared/liboxide/power.h b/shared/liboxide/power.h index 40ace4767..c4b849b71 100644 --- a/shared/liboxide/power.h +++ b/shared/liboxide/power.h @@ -1,8 +1,10 @@ /*! - * \file power.h + * \addtogroup Power + * \brief The Power module + * @{ + * \file */ -#ifndef POWER_H -#define POWER_H +#pragma once #include "liboxide_global.h" @@ -71,5 +73,4 @@ namespace Oxide::Power { */ LIBOXIDE_EXPORT bool chargerConnected(); } - -#endif // POWER_H +/*! @} */ diff --git a/shared/liboxide/settingsfile.h b/shared/liboxide/settingsfile.h index 5ccbd6feb..847b23daa 100644 --- a/shared/liboxide/settingsfile.h +++ b/shared/liboxide/settingsfile.h @@ -1,8 +1,9 @@ /*! - * \file settingsfile.h + * \addtogroup Oxide + * @{ + * \file */ -#ifndef SETTINGSFILE_H -#define SETTINGSFILE_H +#pragma once #include "liboxide_global.h" @@ -130,4 +131,4 @@ namespace Oxide { bool initalized = false; }; } -#endif // SETTINGSFILE_H +/*! @} */ diff --git a/shared/liboxide/signalhandler.h b/shared/liboxide/signalhandler.h index b9f899c57..ac45f3d6a 100644 --- a/shared/liboxide/signalhandler.h +++ b/shared/liboxide/signalhandler.h @@ -1,14 +1,14 @@ /*! - * \file signalhandler.h + * \addtogroup Oxide + * @{ + * \file */ -#ifndef SIGNALHANDLER_H -#define SIGNALHANDLER_H +#pragma once #include "liboxide_global.h" #include #include - /*! * \brief signalHandler() */ @@ -68,4 +68,4 @@ namespace Oxide { QSocketNotifier* snUsr2; }; } -#endif // SIGNALHANDLER_H +/*! @} */ diff --git a/shared/liboxide/slothandler.h b/shared/liboxide/slothandler.h index 3597b912d..a42e06557 100644 --- a/shared/liboxide/slothandler.h +++ b/shared/liboxide/slothandler.h @@ -1,8 +1,9 @@ /*! - * \file slothandler.h + * \addtogroup Oxide + * @{ + * \file */ -#ifndef SLOTHANDLER_H -#define SLOTHANDLER_H +#pragma once #include "liboxide_global.h" @@ -59,5 +60,4 @@ namespace Oxide{ void handleSlot(QObject* api, void** arguments); }; } - -#endif // SLOTHANDLER_H +/*! @} */ diff --git a/shared/liboxide/sysobject.h b/shared/liboxide/sysobject.h index 4d3a9c710..04efaeecd 100644 --- a/shared/liboxide/sysobject.h +++ b/shared/liboxide/sysobject.h @@ -1,8 +1,12 @@ /*! - * \file sysobject.h + * \addtogroup Oxide + * @{ + * \file */ -#ifndef SYSOBJECT_H -#define SYSOBJECT_H +#pragma once +// This is required to allow generate_xml.sh to work +#ifndef LIBOXIDE_SYSOBJECT_H +#define LIBOXIDE_SYSOBJECT_H #include "liboxide_global.h" @@ -61,4 +65,5 @@ namespace Oxide { std::string m_path; }; } -#endif // SYSOBJECT_H +#endif // LIBOXIDE_SYSOBJECT_H +/*! @} */ diff --git a/shared/shared.pro b/shared/shared.pro index 66b8326b4..183ce12ea 100644 --- a/shared/shared.pro +++ b/shared/shared.pro @@ -3,4 +3,4 @@ TEMPLATE = subdirs SUBDIRS = \ liboxide -INSTALLS += liboxide +INSTALLS += $$SUBDIRS diff --git a/web/Makefile b/web/Makefile index 07294830b..cffcb2485 100644 --- a/web/Makefile +++ b/web/Makefile @@ -1,6 +1,15 @@ -DIST=dist -SRC=src -VENV=.venv +DIST=$(CURDIR)/dist +SRC=$(CURDIR)/src +IMAGES=$(CURDIR)/images +VENV=$(CURDIR)/.venv +BUILD=$(CURDIR)/.build + +texSvgFiles := $(wildcard $(IMAGES)/*.svg.tex) +svgFiles := $(texSvgFiles:$(IMAGES)/%.svg.tex=$(SRC)/_static/images/%.svg) +texPngFiles := $(wildcard $(IMAGES)/*.png.tex) +pngFiles := $(texPngFiles:$(IMAGES)/%.png.tex=$(SRC)/_static/images/%.png) + +.PHONY: all prod images dev dev-images clean all: prod @@ -10,7 +19,38 @@ $(VENV)/bin/activate: . .venv/bin/activate; \ python -m pip install -r requirements.txt -prod: $(VENV)/bin/activate +$(SRC)/_static/images/%.svg: $(IMAGES)/%.svg.tex + mkdir -p $(SRC)/_static/images + mkdir -p $(BUILD)/images + cd $(IMAGES) && pdflatex \ + -shell-escape \ + -halt-on-error \ + -file-line-error \ + -interaction nonstopmode \ + -output-directory=$(BUILD)/images \ + $*.svg.tex + pdf2svg $(BUILD)/images/$*.svg.pdf $(SRC)/_static/images/$*.svg + +$(SRC)/_static/images/%.png: $(IMAGES)/%.png.tex + mkdir -p $(SRC)/_static/images + mkdir -p $(BUILD)/images + cd $(IMAGES) && pdflatex \ + -shell-escape \ + -halt-on-error \ + -file-line-error \ + -interaction nonstopmode \ + -output-directory=$(BUILD)/images \ + $*.png.tex + cd $(BUILD)/images && \ + pdf2svg $*.png.pdf $*.svg && \ + rsvg-convert $*.svg -o $(SRC)/_static/images/$*.png + +$(SRC)/_static/images/favicon.png: $(SRC)/_static/images/favicon.svg + rsvg-convert -h 180 $(SRC)/_static/images/favicon.svg -o $(SRC)/_static/images/favicon.png + +images: $(svgFiles) $(pngFiles) $(SRC)/_static/images/favicon.png + +$(DIST): $(VENV)/bin/activate images . $(VENV)/bin/activate; \ sphinx-build -a -n -E -b html $(SRC) $(DIST) # Clean unused files inherited from default theme @@ -35,12 +75,18 @@ prod: $(VENV)/bin/activate $(DIST)/_static/underscore-1.13.1.js \ $(DIST)/_static/underscore.js \ +prod: $(DIST) + dev: $(VENV)/bin/activate . $(VENV)/bin/activate; \ - sphinx-autobuild -a $(SRC) $(DIST) + sphinx-autobuild -a $(SRC) $(DIST) --port=0 --open-browser -clean: - rm -r $(DIST) - rm -r $(VENV) +dev-images: + while inotifywait -e close_write,create $(IMAGES) $(IMAGES)/*.tex;do \ + rm -f $(svgFiles) $(pngFiles); \ + $(MAKE) images; \ + done -.PHONY: all prod dev clean +clean: + rm -rf $(DIST) $(VENV) $(BUILD) + rm -f $(SRC)/_static/images/*.png $(SRC)/_static/images/*.svg diff --git a/web/images/favicon.svg.tex b/web/images/favicon.svg.tex new file mode 100644 index 000000000..ee98e3859 --- /dev/null +++ b/web/images/favicon.svg.tex @@ -0,0 +1,23 @@ +% Copyright (c) 2021 The Toltec Contributors +% SPDX-License-Identifier: MIT +% +% Example document for the Toltec shapes library +\documentclass[crop, tikz, border=.1cm]{standalone} + +\usepackage{tikz} +\usetikzlibrary{arrows.meta} +\input{remarkable} +\usepackage[T1]{fontenc} +\usepackage{lmodern} +\usepackage{graphicx} + +\begin{document} + \begin{tikzpicture} + \pic (rM-pen) at (6.75, -3) {rM1}; + \node[font=\fontsize{1}{3.5}\selectfont] at ($(rM-pen-screen-top-right)+(-0.21,-1.9)$) {Starting launcher...}; + \node[inner sep=0pt] (icon) at ($(rM-pen-screen-center)+(0,0)$){ + \includegraphics[scale=0.3]{../../assets/opt/usr/share/icons/oxide/702x702/splash/oxide.png} + }; + \pic[rotate=-25] at ($(rM-pen-screen-center)+(-.45,-.5)$) {rM pen}; + \end{tikzpicture} +\end{document} diff --git a/web/images/gestures.svg.tex b/web/images/gestures.svg.tex new file mode 100644 index 000000000..c15471984 --- /dev/null +++ b/web/images/gestures.svg.tex @@ -0,0 +1,99 @@ +% Copyright (c) 2021 The Toltec Contributors +% SPDX-License-Identifier: MIT +% +% Example document for the Toltec shapes library +\documentclass[crop, tikz, border=.1cm]{standalone} + +\usepackage{tikz} +\usetikzlibrary{arrows.meta} +\input{remarkable} +\usepackage[T1]{fontenc} +\usepackage{lmodern} +\usepackage{graphicx} + +\begin{document} + \begin{tikzpicture} + \pic (rM-scroll-left) at (0, 0) {rM1}; + \pic (swipe-left) at ($(rM-scroll-left-screen-center)+(.2, 0)$) {scroll left}; + \node[font=\fontsize{2}{3.5}\selectfont] + at ($(rM-scroll-left-screen-center)+(.2, -.22)$) + {Swipe left from edge}; + \node[rectangle,draw,fill=white,font=\fontsize{3}{3.5}\selectfont] + at ($(rM-scroll-left-screen-center)+(0, -1.6)$) + {Take a screenshot}; + + \pic (rM-scroll-right) at (2.25, 0) {rM1}; + \pic at ($(rM-scroll-right-screen-center)+(-.2, 0)$) {scroll right}; + \node[font=\fontsize{2}{3.5}\selectfont] + at ($(rM-scroll-right-screen-center)+(-.2, -.22)$) + {Swipe right from edge}; + \node[rectangle,draw,fill=white,font=\fontsize{3}{3.5}\selectfont] + at ($(rM-scroll-right-screen-center)+(0, -1.6)$) + {Open previous application}; + + \pic (rM-scroll-up) at (4.5, 0) {rM1}; + \pic at ($(rM-scroll-up-screen-center)+(0, -.35)$) {scroll up}; + \node[font=\fontsize{2}{3.5}\selectfont] + at ($(rM-scroll-up-screen-center)+(0, .25)$) + {Swipe up from edge}; + \node[rectangle,draw,fill=white,font=\fontsize{3}{3.5}\selectfont] + at ($(rM-scroll-up-screen-center)+(0, -1.6)$) + {Open task switcher}; + + \pic (rM-scroll-down) at (6.75, 0) {rM1}; + \pic at ($(rM-scroll-down-screen-center)+(0, .35)$) {scroll down}; + \node[font=\fontsize{2}{3.5}\selectfont] + at ($(rM-scroll-down-screen-center)+(0, -.25)$) + {Swipe down from edge}; + \node[rectangle,draw,fill=white,font=\fontsize{3}{3.5}\selectfont] + at ($(rM-scroll-down-screen-center)+(0, -1.6)$) + {Toggle touch gestures}; + + \pic (rM-power) at (0, -3) {rM1}; + \draw[rM annotation, ultra thick, {Latex[length=2.2mm]}-] + ($(rM-power-key-power.south)+(0, -.05)$) + to ++(0, -.6); + \node[font=\fontsize{2}{3.5}\selectfont] + at ($(rM-power-key-power.south)+(0, -.7)$) + {Press button}; + \node[rectangle,draw,fill=white,font=\fontsize{3}{3.5}\selectfont] + at ($(rM-power-screen-center)+(0, -1.6)$) + {Suspend device}; + + \pic[rM1 left fill=rM border] + (rM-left) at (2.25, -3) {rM1}; + \draw[rM annotation, ultra thick, {Latex[length=2.2mm]}-] + ($(rM-left-key-left.north)+(0, .05)$) + to [out=85, in=-130] ++(.2, .6); + \node[font=\fontsize{2}{3.5}\selectfont] + at ($(rM-left-key-left.north)+(.5, .7)$) + {Hold button}; + \node[rectangle,draw,fill=white,font=\fontsize{3}{3.5}\selectfont] + at ($(rM-left-screen-center)+(0, -1.6)$) + {Open launcher}; + + \pic[rM1 home fill=rM border] + (rM-home) at (4.5, -3) {rM1}; + \draw[rM annotation, ultra thick, {Latex[length=2.2mm]}-] + ($(rM-home-key-home.north)+(0, .05)$) + to ++(0, .6); + \node[font=\fontsize{2}{3.5}\selectfont] + at ($(rM-home-key-home.north)+(0, .7)$) + {Hold button}; + \node[rectangle,draw,fill=white,font=\fontsize{3}{3.5}\selectfont] + at ($(rM-home-screen-center)+(0, -1.6)$) + {Open process manager}; + + \pic[rM1 right fill=rM border] + (rM-right) at (6.75, -3) {rM1}; + \draw[rM annotation, ultra thick, {Latex[length=2.2mm]}-] + ($(rM-right-key-right.north)+(0, .05)$) + to [out=95, in=-50] ++(-.2, .6); + \node[font=\fontsize{2}{3.5}\selectfont] + at ($(rM-right-key-right.north)+(-.5, .7)$) + {Hold button}; + \node[rectangle,draw,fill=white,font=\fontsize{3}{3.5}\selectfont] + at ($(rM-right-screen-center)+(0, -1.6)$) + {Take a screenshot}; + \end{tikzpicture} +\end{document} diff --git a/web/images/logo.png.tex b/web/images/logo.png.tex new file mode 100644 index 000000000..b82bfc95f --- /dev/null +++ b/web/images/logo.png.tex @@ -0,0 +1,22 @@ +% Copyright (c) 2021 The Toltec Contributors +% SPDX-License-Identifier: MIT +% +% Example document for the Toltec shapes library +\documentclass[crop, tikz, border=.1cm]{standalone} + +\usepackage{tikz} +\usetikzlibrary{arrows.meta} +\input{remarkable} +\usepackage[T1]{fontenc} +\usepackage{lmodern} +\usepackage{graphicx} + +\begin{document} + \begin{tikzpicture} + \pic[scale=10, every node/.style={scale=10}] (tablet) at (6.75, -3) {rM1}; + \node[scale=10, every node/.style={scale=10}, font=\fontsize{1}{3.5}\selectfont] at ($(tablet-screen-top-right)+(-2.2,-19)$) {Starting launcher...}; + \node[scale=10, every node/.style={scale=10}, inner sep=0pt] (icon) at ($(tablet-screen-center)+(0,0)$){ + \includegraphics[scale=0.3]{../../assets/opt/usr/share/icons/oxide/702x702/splash/oxide.png} + }; + \end{tikzpicture} +\end{document} diff --git a/web/images/remarkable.tex b/web/images/remarkable.tex new file mode 100644 index 000000000..4b1384675 --- /dev/null +++ b/web/images/remarkable.tex @@ -0,0 +1,172 @@ +% Copyright (c) 2021 The Toltec Contributors +% SPDX-License-Identifier: MIT +% +% Library of reMarkable-related TikZ shapes for illustrating the documentation +% of community projects +\usetikzlibrary{calc} + +\colorlet{rM border}{black!60} +\colorlet{rM screen}{black!6} +\colorlet{rM overscreen}{black!10} +\colorlet{rM annotation}{black!70} + +\tikzset{ + % Shape of a reMarkable 1 tablet + % Arguments: by border + rM1 left fill/.initial={white}, + rM1 home fill/.initial={white}, + rM1 right fill/.initial={white}, + pics/rM1/.style args={#1 by #2 border #3}{ + code={ + \node[ + fill=rM border, + rounded corners=0.25, + minimum width={2 * #3 cm}, + minimum height=2pt, + yshift=-.25pt, + inner sep=0pt, + ] at (.5 * #1, #2) (-key-power) {}; + + \draw[ + draw=rM border, fill=white, + rounded corners=1, + ] (0, 0) rectangle (#1, #2); + + \fill[rM screen] + (#3, 4 * #3) + coordinate (-screen-bottom-left) + rectangle (#1 - #3, #2 - 2 * #3) + coordinate (-screen-top-right); + + \coordinate (-screen-center) + at ($(-screen-bottom-left)!.5!(-screen-top-right)$); + + \tikzset{ + lower key/.style={ + draw=rM border, rectangle, + line width=.3, + rounded corners=0.25, + minimum width={2 * #3 cm}, + minimum height={2 * #3 cm}, + inner sep=0pt, + }, + } + + \node[ + lower key, + fill=\pgfkeysvalueof{/tikz/rM1 left fill}, + ] at (2 * #3, 2 * #3) (-key-left) {}; + \node[ + lower key, + fill=\pgfkeysvalueof{/tikz/rM1 home fill}, + ] at (.5 * #1, 2 * #3) (-key-home) {}; + \node[ + lower key, + fill=\pgfkeysvalueof{/tikz/rM1 right fill}, + ] at (#1 - 2 * #3, 2 * #3) (-key-right) {}; + } + }, + % + % Shape of a reMarkable pen + % Arguments: by + pics/rM pen/.style args={#1 by #2}{ + code={ + \draw[rM annotation, fill=white] (0, 0) + to [out=135, in=-90] ++(-.5 * #1, #1) + to ++(0, #2) to ++(#1, 0) + to ++(0, -#2) + to [out=-90, in=45] ++(-.5 * #1, -#1); + } + }, + pics/rM pen/.default={.12 by .8}, + pics/rM1/.default={1.75 by 2.55 border .1}, + % + % Symbol representing a tap on a touch screen + % Arguments: // + pics/tap/.style args={#1/#2/#3}{ + code={ + \draw[rM annotation, semithick] (0, 0) circle (#1); + + \draw[rM annotation] (#2, 0) + arc[radius=#2, start angle=0, delta angle=#3] + (#2, 0) + arc[radius=#2, start angle=0, delta angle=-#3]; + + \draw[rM annotation] (-#2, 0) + arc[radius=#2, start angle=180, delta angle=#3] + (-#2, 0) + arc[radius=#2, start angle=180, delta angle=-#3]; + } + }, + pics/tap/.default={.15/.22/50}, + % + % Symbol representing a horizontal swipe in any direction + % Arguments: none + pics/scroll x/.style={ + code={ + \draw[rM annotation, semithick] (0, 0) circle (#1); + \draw[rM annotation, ultra thick, -{Latex[length=2.2mm]}] + (-#1, 0) -- ++(-.4, 0); + \draw[rM annotation, ultra thick, -{Latex[length=2.2mm]}] + (#1, 0) -- ++(.4, 0); + } + }, + pics/scroll x/.default={.15}, + % + % Symbol representing a horizontal swipe to the left + % Arguments: none + pics/scroll left/.style={ + code={ + \draw[rM annotation, semithick] (.4, 0) circle (#1); + \draw[rM annotation, ultra thick, -{Latex[length=2.2mm]}] + (#1+.1, 0) -- ++(-.8, 0); + } + }, + pics/scroll left/.default={.15}, + % + % Symbol representing a horizontal swipe to the right + % Arguments: none + pics/scroll right/.style={ + code={ + \draw[rM annotation, semithick] (-.4, 0) circle (#1); + \draw[rM annotation, ultra thick, -{Latex[length=2.2mm]}] + (-#1-.1, 0) -- ++(.8, 0); + } + }, + pics/scroll right/.default={.15}, + % + % Symbol representing a vertical swipe in any direction + % Arguments: none + pics/scroll y/.style={ + code={ + \draw[rM annotation, semithick] (0, 0) circle (#1); + \draw[rM annotation, ultra thick, -{Latex[length=2.2mm]}] + (#1, -#1) -- ++(0, -.4); + \draw[rM annotation, ultra thick, -{Latex[length=2.2mm]}] + (#1, #1) -- ++(0, .4); + } + }, + pics/scroll y/.default={.15}, + % + % Symbol representing a vertical swipe up + % Arguments: none + pics/scroll up/.style={ + code={ + \draw[rM annotation, semithick] (0, -.4) circle (#1); + \draw[rM annotation, ultra thick, -{Latex[length=2.2mm]}] + (0, -#1-.1) -- ++(0, .8); + } + }, + pics/scroll up/.default={.15}, + % + % Symbol representing a vertical swipe down + % Arguments: none + pics/scroll down/.style={ + code={ + \draw[rM annotation, semithick] (0, .4) circle (#1); + \draw[rM annotation, ultra thick, -{Latex[length=2.2mm]}] + (0, #1+.1) -- ++(0, -.8); + } + }, + pics/scroll down/.default={.15}, +} diff --git a/web/src/_static/images/.gitignore b/web/src/_static/images/.gitignore new file mode 100644 index 000000000..664db108e --- /dev/null +++ b/web/src/_static/images/.gitignore @@ -0,0 +1,2 @@ +*.png +*.svg diff --git a/web/src/_themes/oxide/static/oxide.css b/web/src/_themes/oxide/static/oxide.css index c738504b0..c2ce3ccac 100644 --- a/web/src/_themes/oxide/static/oxide.css +++ b/web/src/_themes/oxide/static/oxide.css @@ -294,7 +294,7 @@ main > section:first-of-type > h1:first-of-type { .logo { display: block; - width: calc(var(--scaling) * 256px); + max-width: calc(var(--scaling) * 256px); margin: calc(var(--scaling) * -32px) auto; } diff --git a/web/src/documentation/01_usage.rst b/web/src/documentation/01_usage.rst index b74a26107..40b4df13f 100644 --- a/web/src/documentation/01_usage.rst +++ b/web/src/documentation/01_usage.rst @@ -2,6 +2,11 @@ Usage ===== +.. raw:: html + + gestures +
+ Lockscreen (decay) ================== diff --git a/web/src/documentation/02_oxide-utils.rst b/web/src/documentation/02_oxide-utils.rst new file mode 100644 index 000000000..aecea5a97 --- /dev/null +++ b/web/src/documentation/02_oxide-utils.rst @@ -0,0 +1,41 @@ +=========== +Oxide-Utils +=========== + +As of version 2.6, Oxide ships with several new command line utilities meant to mimic common +existing linux command line tools meant for dealing with desktop environments. + +desktop-file-validate +===================== + +https://man.archlinux.org/man/desktop-file-validate.1.en + +update-desktop-database +======================= + +https://man.archlinux.org/man/update-desktop-database.1.en + +xdg-desktop-menu +================ + +https://man.archlinux.org/man/xdg-desktop-menu.1 + +xdg-desktop-icon +================ + +https://man.archlinux.org/man/xdg-desktop-icon.1 + +xdg-open +======== + +https://man.archlinux.org/man/xdg-open.1 + +xdg-settings +============ + +https://man.archlinux.org/man/xdg-settings.1.en + +gio +=== + +https://man.archlinux.org/man/gio.1 diff --git a/web/src/documentation/02_application_registration_format.rst b/web/src/documentation/03_application_registration_format.rst similarity index 94% rename from web/src/documentation/02_application_registration_format.rst rename to web/src/documentation/03_application_registration_format.rst index 4131531ac..cbefeabfa 100644 --- a/web/src/documentation/02_application_registration_format.rst +++ b/web/src/documentation/03_application_registration_format.rst @@ -133,7 +133,14 @@ Properties | icon | string | No | Path to an image | | | | | file to use as the | | | | | icon for this | -| | | | application. | +| | | | application. Or an | +| | | | icon spec. | ++------------------+--------------+----------+-----------------------+ +| splash | string | No | Path to an image | +| | | | file to use as the | +| | | | splash screen for | +| | | | this application. Or | +| | | | an icon spec. | +------------------+--------------+----------+-----------------------+ | user | string | No | User to run this | | | | | application as. | @@ -226,3 +233,17 @@ Properties | | | | stopped | +------------------+--------------+----------+-----------------------+ +Icon Spec +========= + +Icon specifications can be in the following format: ``[theme:][context:]{name}-{size}`` + +Some examples: + +- ``oxide:splash:xochitl-702`` +- ``oxide:apps:xochitl-48`` +- ``oxide:xochitl-48`` +- ``xochitl-48`` + +You can find available icons in ``/opt/usr/share/icons``. The default theme is +hicolor, and the default context is apps. diff --git a/web/src/documentation/03_api.rst b/web/src/documentation/04_api.rst similarity index 100% rename from web/src/documentation/03_api.rst rename to web/src/documentation/04_api.rst diff --git a/web/src/faq.rst b/web/src/faq.rst index b04469240..338bd8e91 100644 --- a/web/src/faq.rst +++ b/web/src/faq.rst @@ -34,9 +34,9 @@ How can I disable the telemetry? .. code:: bash - rot settings set telemetry false - rot settings set crashReport false - rot settings set applicationUsage false + xdg-settings set telemetry false + xdg-settings set crashReport false + xdg-settings set applicationUsage false Or you can compile the applications manually without the ``sentry`` feature enabled. @@ -81,6 +81,12 @@ top left of the launcher. If your application is still not listed, you may need logs to determine why it's failing to load. If an application is configured in draft to pass arguments in the ``call=`` line, it will fail to import as this is not supported by Oxide. +You can check for errors with your application registration files with the following command: + +.. code:: bash + + desktop-file-validate /opt/usr/share/applications/*.oxide + How do I review my device logs? =============================== diff --git a/web/src/index.rst b/web/src/index.rst index d837f5f8e..14df8f7d8 100644 --- a/web/src/index.rst +++ b/web/src/index.rst @@ -2,6 +2,11 @@ Home ==== +.. raw:: html + + +
+ Oxide is a `desktop environment `_ for the `reMarkable tablet `_. Features From 7cfc2437e1c087803acd94606cf937e581d6a560 Mon Sep 17 00:00:00 2001 From: Nathaniel van Diepen Date: Fri, 10 Feb 2023 14:30:51 -0700 Subject: [PATCH 14/15] Move some settings to shared settings (#293) * Move lockscreen settings to shared settings * Add pin information to FAQs * Move autoSleep and swipes to shared settings --- applications/launcher/main.cpp | 1 - applications/launcher/oxide_stable.h | 1 - applications/lockscreen/controller.h | 63 +++++++------ applications/system-service/systemapi.cpp | 41 +++++---- applications/system-service/systemapi.h | 104 ++++++++++++++-------- applications/task-switcher/controller.h | 17 +--- shared/liboxide/liboxide.cpp | 4 + shared/liboxide/liboxide.h | 77 +++++++++++++++- shared/liboxide/power.cpp | 2 +- shared/liboxide/settingsfile.cpp | 76 +++++++++++----- shared/liboxide/settingsfile.h | 34 +++++-- web/src/faq.rst | 13 +-- 12 files changed, 297 insertions(+), 136 deletions(-) diff --git a/applications/launcher/main.cpp b/applications/launcher/main.cpp index 8fb9de2ba..f9a9d6836 100644 --- a/applications/launcher/main.cpp +++ b/applications/launcher/main.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include diff --git a/applications/launcher/oxide_stable.h b/applications/launcher/oxide_stable.h index 77ee343f7..1c024ce1f 100644 --- a/applications/launcher/oxide_stable.h +++ b/applications/launcher/oxide_stable.h @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include diff --git a/applications/lockscreen/controller.h b/applications/lockscreen/controller.h index bd9c64ee6..17c6ff94d 100644 --- a/applications/lockscreen/controller.h +++ b/applications/lockscreen/controller.h @@ -8,8 +8,6 @@ using namespace codes::eeems::oxide1; using namespace Oxide::Sentry; -#define DECAY_SETTINGS_VERSION 1 - enum State { Normal, PowerSaving }; enum BatteryState { BatteryUnknown, BatteryCharging, BatteryDischarging, BatteryNotPresent }; enum ChargerState { ChargerUnknown, ChargerConnected, ChargerNotConnected, ChargerNotPresent }; @@ -26,7 +24,7 @@ class Controller : public QObject { public: Controller(QObject* parent) - : QObject(parent), confirmPin(), settings(this) { + : QObject(parent), confirmPin() { clockTimer = new QTimer(root); auto bus = QDBusConnection::systemBus(); qDebug() << "Waiting for tarnish to start up..."; @@ -88,10 +86,26 @@ class Controller : public QObject { } appsApi = new Apps(OXIDE_SERVICE, path.path(), bus, this); - settings.sync(); - auto version = settings.value("version", 0).toInt(); - if(version < DECAY_SETTINGS_VERSION){ - migrate(&settings, version); + QSettings settings; + if(QFile::exists(settings.fileName())){ + qDebug() << "Importing old settings"; + settings.sync(); + if(settings.contains("pin")){ + qDebug() << "Importing old pin"; + sharedSettings.set_pin(settings.value("pin").toString()); + } + if(settings.contains("onLogin")){ + qDebug() << "Importing old onLogin"; + sharedSettings.set_onLogin(settings.value("onLogin").toString()); + } + if(settings.contains("onFailedLogin")){ + qDebug() << "Importing old onFailedLogin"; + sharedSettings.set_onFailedLogin(settings.value("onFailedLogin").toString()); + } + settings.clear(); + settings.sync(); + QFile::remove(settings.fileName()); + sharedSettings.sync(); } connect(&sharedSettings, &Oxide::SharedSettings::firstLaunchChanged, this, &Controller::firstLaunchChanged); @@ -142,7 +156,7 @@ class Controller : public QObject { return; } // There is no PIN configuration - if(!settings.contains("pin")){ + if(!sharedSettings.has_pin()){ qDebug() << "No Pin"; QTimer::singleShot(100, [this]{ stateControllerUI->setProperty("state", xochitlPin().isEmpty() ? "pinPrompt" : "import"); @@ -180,7 +194,7 @@ class Controller : public QObject { Application app(OXIDE_SERVICE, path.path(), QDBusConnection::systemBus()); app.launch(); } - bool hasPin(){ return settings.contains("pin") && storedPin().length(); } + bool hasPin(){ return sharedSettings.has_pin() && storedPin().length(); } void previousApplication(){ if(!appsApi->previousApplication()){ launchStartupApp(); @@ -225,6 +239,7 @@ class Controller : public QObject { return true; }else if(state == "loaded"){ qDebug() << "PIN doesn't match!"; + O_DEBUG(pin << "!=" << storedPin()); onFailedLogin(); return false; } @@ -289,10 +304,16 @@ class Controller : public QObject { } stateControllerUI->setProperty("state", state); } - QString storedPin() { return settings.value("pin", "").toString(); } + QString storedPin() { + if(!sharedSettings.has_pin()){ + O_DEBUG("Does not have pin and storedPin was called"); + return ""; + } + return sharedSettings.pin(); + } void setStoredPin(QString pin) { - settings.setValue("pin", pin); - settings.sync(); + sharedSettings.set_pin(pin); + sharedSettings.sync(); } void setRoot(QObject* root){ this->root = root; } @@ -435,10 +456,10 @@ private slots: // TODO handle charger } void onLogin(){ - if(!settings.contains("onLogin")){ + if(!sharedSettings.has_onLogin()){ return; } - auto path = settings.value("onLogin").toString(); + auto path = sharedSettings.onLogin(); if(!QFile::exists(path)){ O_WARNING("onLogin script does not exist" << path); return; @@ -450,10 +471,10 @@ private slots: QProcess::execute(path, QStringList()); } void onFailedLogin(){ - if(!settings.contains("onFailedLogin")){ + if(!sharedSettings.has_onFailedLogin()){ return; } - auto path = settings.value("onFailedLogin").toString(); + auto path = sharedSettings.onFailedLogin(); if(!QFile::exists(path)){ O_WARNING("onFailedLogin script does not exist" << path); return; @@ -467,7 +488,6 @@ private slots: private: QString confirmPin; - QSettings settings; General* api; System* systemApi; codes::eeems::oxide1::Power* powerApi; @@ -503,15 +523,6 @@ private slots: return pinEntryUI; } - static void migrate(QSettings* settings, int fromVersion){ - if(fromVersion != 0){ - throw "Unknown settings version"; - } - // In the future migrate changes to settings between versions - settings->setValue("version", DECAY_SETTINGS_VERSION); - settings->sync(); - } - static QString xochitlPin(){ return xochitlSettings.passcode(); } static void removeXochitlPin(){ xochitlSettings.remove("Passcode"); diff --git a/applications/system-service/systemapi.cpp b/applications/system-service/systemapi.cpp index 5588f169a..cac8229f3 100644 --- a/applications/system-service/systemapi.cpp +++ b/applications/system-service/systemapi.cpp @@ -83,9 +83,9 @@ void SystemAPI::PrepareForSleep(bool suspending){ Oxide::Sentry::sentry_span(t, "enable", "Enable various services", [this, device]{ buttonHandler->setEnabled(true); emit deviceResuming(); - if(m_autoSleep && powerAPI->chargerState() != PowerAPI::ChargerConnected){ + if(autoSleep() && powerAPI->chargerState() != PowerAPI::ChargerConnected){ qDebug() << "Suspend timer re-enabled due to resume"; - suspendTimer.start(m_autoSleep * 60 * 1000); + suspendTimer.start(autoSleep() * 60 * 1000); } if(device == Oxide::DeviceSettings::DeviceType::RM2){ system("modprobe brcmfmac"); @@ -98,20 +98,19 @@ void SystemAPI::PrepareForSleep(bool suspending){ }); } } -void SystemAPI::setAutoSleep(int autoSleep){ - if(autoSleep < 0 || autoSleep > 360){ +void SystemAPI::setAutoSleep(int _autoSleep){ + if(_autoSleep < 0 || _autoSleep > 360){ return; } - qDebug() << "Auto Sleep" << autoSleep; - m_autoSleep = autoSleep; - if(m_autoSleep && powerAPI->chargerState() != PowerAPI::ChargerConnected){ - suspendTimer.setInterval(m_autoSleep * 60 * 1000); - }else if(!m_autoSleep){ + qDebug() << "Auto Sleep" << _autoSleep; + sharedSettings.set_autoSleep(_autoSleep); + if(_autoSleep && powerAPI->chargerState() != PowerAPI::ChargerConnected){ + suspendTimer.setInterval(_autoSleep * 60 * 1000); + }else if(!_autoSleep){ suspendTimer.stop(); } - settings.setValue("autoSleep", autoSleep); - settings.sync(); - emit autoSleepChanged(autoSleep); + sharedSettings.sync(); + emit autoSleepChanged(_autoSleep); } void SystemAPI::uninhibitAll(QString name){ if(powerOffInhibited()){ @@ -126,25 +125,25 @@ void SystemAPI::uninhibitAll(QString name){ emit sleepInhibitedChanged(false); } } - if(!sleepInhibited() && m_autoSleep && powerAPI->chargerState() != PowerAPI::ChargerConnected && !suspendTimer.isActive()){ + if(!sleepInhibited() && autoSleep() && powerAPI->chargerState() != PowerAPI::ChargerConnected && !suspendTimer.isActive()){ qDebug() << "Suspend timer re-enabled due to uninhibit" << name; - suspendTimer.start(m_autoSleep * 60 * 1000); + suspendTimer.start(autoSleep() * 60 * 1000); } } void SystemAPI::startSuspendTimer(){ - if(m_autoSleep && powerAPI->chargerState() != PowerAPI::ChargerConnected && !suspendTimer.isActive()){ + if(autoSleep() && powerAPI->chargerState() != PowerAPI::ChargerConnected && !suspendTimer.isActive()){ qDebug() << "Suspend timer re-enabled due to start Suspend timer"; - suspendTimer.start(m_autoSleep * 60 * 1000); + suspendTimer.start(autoSleep() * 60 * 1000); } } void SystemAPI::activity(){ auto active = suspendTimer.isActive(); suspendTimer.stop(); - if(m_autoSleep && powerAPI->chargerState() != PowerAPI::ChargerConnected){ + if(autoSleep() && powerAPI->chargerState() != PowerAPI::ChargerConnected){ if(!active){ qDebug() << "Suspend timer re-enabled due to activity"; } - suspendTimer.start(m_autoSleep * 60 * 1000); + suspendTimer.start(autoSleep() * 60 * 1000); }else if(active){ qDebug() << "Suspend timer disabled"; } @@ -155,10 +154,10 @@ void SystemAPI::uninhibitSleep(QDBusMessage message){ return; } sleepInhibitors.removeAll(message.service()); - if(!sleepInhibited() && m_autoSleep && powerAPI->chargerState() != PowerAPI::ChargerConnected){ + if(!sleepInhibited() && autoSleep() && powerAPI->chargerState() != PowerAPI::ChargerConnected){ if(!suspendTimer.isActive()){ qDebug() << "Suspend timer re-enabled due to uninhibit sleep" << message.service(); - suspendTimer.start(m_autoSleep * 60 * 1000); + suspendTimer.start(autoSleep() * 60 * 1000); } releaseSleepInhibitors(true); } @@ -167,7 +166,7 @@ void SystemAPI::uninhibitSleep(QDBusMessage message){ } } void SystemAPI::timeout(){ - if(m_autoSleep && powerAPI->chargerState() != PowerAPI::ChargerConnected){ + if(autoSleep() && powerAPI->chargerState() != PowerAPI::ChargerConnected){ qDebug() << "Automatic suspend due to inactivity..."; suspend(); } diff --git a/applications/system-service/systemapi.h b/applications/system-service/systemapi.h index 4ae35e0a0..ef6f71e6c 100644 --- a/applications/system-service/systemapi.h +++ b/applications/system-service/systemapi.h @@ -114,38 +114,67 @@ class SystemAPI : public APIBase { }); }); Oxide::Sentry::sentry_span(t, "autoSleep", "Setup automatic sleep", [this](Oxide::Sentry::Span* s){ - auto autoSleep = settings.value("autoSleep", 1).toInt(); - m_autoSleep = autoSleep; - if(autoSleep < 0) { - m_autoSleep = 0; - }else if(autoSleep > 10){ - m_autoSleep = 10; + QSettings settings; + if(QFile::exists(settings.fileName())){ + qDebug() << "Importing old settings"; + settings.sync(); + if(settings.contains("autoSleep")){ + qDebug() << "Importing old autoSleep"; + sharedSettings.set_autoSleep(settings.value("autoSleep").toInt()); + } + int size = settings.beginReadArray("swipes"); + if(size){ + sharedSettings.beginWriteArray("swipes"); + for(short i = Right; i <= Down && i < size; i++){ + settings.setArrayIndex(i); + sharedSettings.setArrayIndex(i); + qDebug() << QString("Importing old swipe[%1]").arg(i); + sharedSettings.setValue("enabled", settings.value("enabled", true)); + sharedSettings.setValue("length", settings.value("length", 30)); + } + sharedSettings.endArray(); + } + settings.endArray(); + settings.remove("swipes"); + settings.sync(); + sharedSettings.sync(); } - if(autoSleep != m_autoSleep){ - Oxide::Sentry::sentry_span(s, "update", "Update value", [this, autoSleep]{ - m_autoSleep = autoSleep; - settings.setValue("autoSleep", autoSleep); - settings.sync(); - emit autoSleepChanged(autoSleep); - }); + if(autoSleep() < 0) { + sharedSettings.set_autoSleep(0); + }else if(autoSleep() > 10){ + sharedSettings.set_autoSleep(10); } - qDebug() << "Auto Sleep" << autoSleep; + qDebug() << "Auto Sleep" << autoSleep(); Oxide::Sentry::sentry_span(s, "timer", "Setup timer", [this]{ - if(m_autoSleep){ - suspendTimer.start(m_autoSleep * 60 * 1000); - }else if(!m_autoSleep){ + if(autoSleep()){ + suspendTimer.start(autoSleep() * 60 * 1000); + }else if(!autoSleep()){ suspendTimer.stop(); } }); + connect(&sharedSettings, &Oxide::SharedSettings::autoSleepChanged, [=](int _autoSleep){ + emit autoSleepChanged(_autoSleep); + }); + connect(&sharedSettings, &Oxide::SharedSettings::changed, [=](){ + sharedSettings.beginReadArray("swipes"); + for(short i = Right; i <= Down; i++){ + sharedSettings.setArrayIndex(i); + swipeStates[(SwipeDirection)i] = sharedSettings.value("enabled", true).toBool(); + int length = sharedSettings.value("length", 30).toInt(); + swipeLengths[(SwipeDirection)i] = length; + emit swipeLengthChanged(i, length); + } + sharedSettings.endArray(); + }); }); - Oxide::Sentry::sentry_span(t, "swipes", "Load swipe settings", [this]{ - settings.beginReadArray("swipes"); + Oxide::Sentry::sentry_span(t, "swipes", "Load swipe settings", [=](){ + sharedSettings.beginReadArray("swipes"); for(short i = Right; i <= Down; i++){ - settings.setArrayIndex(i); - swipeStates[(SwipeDirection)i] = settings.value("enabled", true).toBool(); - swipeLengths[(SwipeDirection)i] = settings.value("length", 30).toInt(); + sharedSettings.setArrayIndex(i); + swipeStates[(SwipeDirection)i] = sharedSettings.value("enabled", true).toBool(); + swipeLengths[(SwipeDirection)i] = sharedSettings.value("length", 30).toInt(); } - settings.endArray(); + sharedSettings.endArray(); }); // Ask Systemd to tell us nicely when we are about to suspend or resume Oxide::Sentry::sentry_span(t, "inhibit", "Inhibit sleep and power off", [this](Oxide::Sentry::Span* s){ @@ -180,8 +209,8 @@ class SystemAPI : public APIBase { void setEnabled(bool enabled){ qDebug() << "System API" << enabled; } - int autoSleep(){return m_autoSleep; } - void setAutoSleep(int autoSleep); + int autoSleep(){return sharedSettings.autoSleep(); } + void setAutoSleep(int _autoSleep); bool sleepInhibited(){ return sleepInhibitors.length(); } bool powerOffInhibited(){ return powerOffInhibitors.length(); } void uninhibitAll(QString name); @@ -223,14 +252,14 @@ class SystemAPI : public APIBase { return; } swipeStates[direction] = enabled; - settings.beginWriteArray("swipes"); + sharedSettings.beginWriteArray("swipes"); for(short i = Right; i <= Down; i++){ - settings.setArrayIndex(i); - settings.setValue("enabled", swipeStates[(SwipeDirection)i]); - settings.setValue("length", swipeLengths[(SwipeDirection)i]); + sharedSettings.setArrayIndex(i); + sharedSettings.setValue("enabled", swipeStates[(SwipeDirection)i]); + sharedSettings.setValue("length", swipeLengths[(SwipeDirection)i]); } - settings.endArray(); - settings.sync(); + sharedSettings.endArray(); + sharedSettings.sync(); } Q_INVOKABLE bool getSwipeEnabled(int direction){ if(!hasPermission("system")){ @@ -295,14 +324,14 @@ class SystemAPI : public APIBase { return; } swipeLengths[direction] = length; - settings.beginWriteArray("swipes"); + sharedSettings.beginWriteArray("swipes"); for(short i = Right; i <= Down; i++){ - settings.setArrayIndex(i); - settings.setValue("enabled", swipeStates[(SwipeDirection)i]); - settings.setValue("length", swipeLengths[(SwipeDirection)i]); + sharedSettings.setArrayIndex(i); + sharedSettings.setValue("enabled", swipeStates[(SwipeDirection)i]); + sharedSettings.setValue("length", swipeLengths[(SwipeDirection)i]); } - settings.endArray(); - settings.sync(); + sharedSettings.endArray(); + sharedSettings.sync(); emit swipeLengthChanged(direction, length); } Q_INVOKABLE int getSwipeLength(int direction){ @@ -515,7 +544,6 @@ private slots: QMutex mutex; QMap touches; int currentSlot = 0; - int m_autoSleep; bool wifiWasOn = false; bool penActive = false; int swipeDirection = None; diff --git a/applications/task-switcher/controller.h b/applications/task-switcher/controller.h index 3400bd259..41c995a4f 100644 --- a/applications/task-switcher/controller.h +++ b/applications/task-switcher/controller.h @@ -30,7 +30,7 @@ class Controller : public QObject { public: Controller(QObject* parent, ScreenProvider* screenProvider) - : QObject(parent), settings(this), applications() { + : QObject(parent),applications() { blankImage = new QImage(qApp->primaryScreen()->geometry().size(), QImage::Format_Mono); this->screenProvider = screenProvider; auto bus = QDBusConnection::systemBus(); @@ -68,11 +68,6 @@ class Controller : public QObject { connect(appsApi, &Apps::applicationLaunched, this, &Controller::reload); connect(appsApi, &Apps::applicationExited, this, &Controller::reload); - settings.sync(); - auto version = settings.value("version", 0).toInt(); - if(version < CORRUPT_SETTINGS_VERSION){ - migrate(&settings, version); - } updateImage(); } ~Controller(){} @@ -273,7 +268,6 @@ private slots: } private: - QSettings settings; General* api; Screen* screenApi; Apps* appsApi; @@ -288,15 +282,6 @@ private slots: stateControllerUI = root->findChild("stateController"); return stateControllerUI; } - - static void migrate(QSettings* settings, int fromVersion){ - if(fromVersion != 0){ - throw "Unknown settings version"; - } - // In the future migrate changes to settings between versions - settings->setValue("version", CORRUPT_SETTINGS_VERSION); - settings->sync(); - } }; #endif // CONTROLLER_H diff --git a/shared/liboxide/liboxide.cpp b/shared/liboxide/liboxide.cpp index 26699f066..fa839d2a5 100644 --- a/shared/liboxide/liboxide.cpp +++ b/shared/liboxide/liboxide.cpp @@ -303,4 +303,8 @@ namespace Oxide { O_SETTINGS_PROPERTY_BODY(SharedSettings, bool, General, telemetry, false) O_SETTINGS_PROPERTY_BODY(SharedSettings, bool, General, applicationUsage, false) O_SETTINGS_PROPERTY_BODY(SharedSettings, bool, General, crashReport, true) + O_SETTINGS_PROPERTY_BODY(SharedSettings, int, General, autoSleep, 1) + O_SETTINGS_PROPERTY_BODY(SharedSettings, QString, Lockscreen, pin) + O_SETTINGS_PROPERTY_BODY(SharedSettings, QString, Lockscreen, onLogin) + O_SETTINGS_PROPERTY_BODY(SharedSettings, QString, Lockscreen, onFailedLogin) } diff --git a/shared/liboxide/liboxide.h b/shared/liboxide/liboxide.h index ed34c1078..a29823742 100644 --- a/shared/liboxide/liboxide.h +++ b/shared/liboxide/liboxide.h @@ -335,13 +335,88 @@ namespace Oxide { /*! * \fn set_crashReport * \param _arg_crashReport - * \brief Enable or disable crash reporting + * \brief Enable or disable crash reporting */ /*! * \fn crashReportChanged * \brief If crash reporting has been enabled or disabled */ O_SETTINGS_PROPERTY(bool, General, crashReport, true) + /*! + * \property autoSleep + * \brief How long without activity before the device should suspend + * \sa set_autoSleep, autoSleepChanged + */ + /*! + * \fn set_autoSleep + * \param _arg_autoSleep + * \brief Change autoSleep + */ + /*! + * \fn autoSleepChanged + * \brief If autoSleep has been changed + */ + O_SETTINGS_PROPERTY(int, General, autoSleep, 1) + /*! + * \property pin + * \brief The lockscreen pin + * \sa set_pin, pinChanged + */ + /*! + * \fn set_pin + * \param _arg_pin + * \brief Change lockscreen pin + */ + /*! + * \fn has_pin + * \brief Change lockscreen pin + * \return If the lockscreen pin is set + */ + /*! + * \fn pinChanged + * \brief If the lockscreen pin has been changed + */ + O_SETTINGS_PROPERTY(QString, Lockscreen, pin) + /*! + * \property onLogin + * \brief The lockscreen onLogin + * \sa set_onLogin, onLoginChanged + */ + /*! + * \fn set_onLogin + * \param _arg_onLogin + * \brief Change lockscreen onLogin + */ + /*! + * \fn has_onLogin + * \brief If lockscreen onLogin has been set + * \return If the lockscreen onLogin is set + */ + /*! + * \fn onLoginChanged + * \brief If the lockscreen onLogin has been changed + */ + O_SETTINGS_PROPERTY(QString, Lockscreen, onLogin) + /*! + * \property onFailedLogin + * \brief The lockscreen onFailedLogin + * \sa set_onFailedLogin, onFailedLoginChanged + */ + /*! + * \fn set_onFailedLogin + * \param _arg_onFailedLogin + * \brief Change lockscreen onFailedLogin + */ + /*! + * \fn has_onFailedLogin + * \brief If lockscreen onFailedLogin has been set + * \return If the lockscreen onFailedLogin is set + */ + /*! + * \fn onFailedLoginChanged + * \brief If the lockscreen onFailedLogin has been changed + */ + O_SETTINGS_PROPERTY(QString, Lockscreen, onFailedLogin) private: ~SharedSettings(); diff --git a/shared/liboxide/power.cpp b/shared/liboxide/power.cpp index c88600a1c..64dfa6697 100644 --- a/shared/liboxide/power.cpp +++ b/shared/liboxide/power.cpp @@ -156,5 +156,5 @@ namespace Oxide::Power { } bool batteryHasWarning(){ return batteryWarning().length(); } bool batteryHasAlert(){ return batteryAlert().length(); } - bool chargerConnected(){ return _chargerInt("online") || batteryCharging(); } + bool chargerConnected(){ return batteryCharging() || _chargerInt("online"); } } diff --git a/shared/liboxide/settingsfile.cpp b/shared/liboxide/settingsfile.cpp index b7280798f..e8223351e 100644 --- a/shared/liboxide/settingsfile.cpp +++ b/shared/liboxide/settingsfile.cpp @@ -6,6 +6,7 @@ namespace Oxide { SettingsFile::SettingsFile(QString path) : QSettings(path, QSettings::IniFormat), + reloadSemaphore(1), fileWatcher(QStringList() << path) { } SettingsFile::~SettingsFile(){ } void SettingsFile::fileChanged(){ @@ -17,35 +18,52 @@ namespace Oxide { sync(); auto metaObj = metaObject(); for (int i = metaObj->propertyOffset(); i < metaObj->propertyCount(); ++i) { - auto property = metaObj->property(i); - if(property.hasNotifySignal()){ - auto value = property.read(this); - auto value2 = this->value(property.name()); - if(value != value2){ - O_DEBUG("Property" << property.name() << "changed") - property.write(this, value2); - property.notifySignal().invoke(this, Qt::QueuedConnection, QGenericArgument(value2.typeName(), value2.data())); + auto prop = metaObj->property(i); + if(QString(prop.name()).startsWith("__META_GROUP_") || !prop.isWritable() || !prop.hasNotifySignal()){ + continue; + } + auto value = prop.read(this); + auto groupName = this->groupName(prop.name()); + if(groupName.isNull()){ + continue; + } + beginGroup(groupName != "General" ? groupName : ""); + bool exists = contains(prop.name()); + auto value2 = this->value(prop.name()); + endGroup(); + if(!exists){ + reloadSemaphore.acquire(); + if(prop.isResettable()){ + prop.reset(this); + }else if(!prop.read(this).isNull()){ + prop.write(this, QVariant()); } + reloadSemaphore.release(); + continue; } + if(!value2.isValid()){ + O_DEBUG("Property" << prop.name() << "new value invalid") + continue; + } + if(value == value2){ + continue; + } + O_DEBUG("Property" << prop.name() << "changed" << value2) + reloadSemaphore.acquire(); + prop.write(this, value2); + reloadSemaphore.release(); } O_DEBUG("Settings file" << fileName() << "changes loaded"); + emit changed(); } void SettingsFile::reloadProperty(const QString& name){ - auto metaObj = metaObject(); - - auto propertyName = "__META_GROUP_" + name; - auto idx = metaObj->indexOfProperty(propertyName.toStdString().c_str()); - if(idx == -1){ - O_SETTINGS_DEBUG("Group for " + name + " not found") + auto groupName = this->groupName(name); + if(groupName.isNull()){ return; } - auto groupName = property(propertyName.toStdString().c_str()).toString(); O_SETTINGS_DEBUG((fileName() + " Reloading " + groupName + "." + name).toStdString().c_str()) - if(groupName != "General"){ - beginGroup(groupName); - }else{ - beginGroup(""); - } + reloadSemaphore.acquire(); + beginGroup(groupName != "General" ? groupName : ""); if(contains(name)){ O_SETTINGS_DEBUG(" Value exists") auto value = this->value(name); @@ -57,8 +75,16 @@ namespace Oxide { } }else{ O_SETTINGS_DEBUG(" No Value") + auto metaObj = metaObject(); + auto prop = metaObj->property(metaObj->indexOfProperty(name.toStdString().c_str())); + if(prop.isResettable()){ + resetProperty(name); + }else if(!prop.read(this).isNull()){ + prop.write(this, QVariant()); + } } endGroup(); + reloadSemaphore.release(); O_SETTINGS_DEBUG(" Done") } void SettingsFile::resetProperty(const QString& name){ @@ -110,4 +136,14 @@ namespace Oxide { resetProperty(property.name()); } } + QString SettingsFile::groupName(const QString& name){ + auto metaObj = metaObject(); + auto propertyName = "__META_GROUP_" + name; + auto idx = metaObj->indexOfProperty(propertyName.toStdString().c_str()); + if(idx == -1){ + O_SETTINGS_DEBUG("Group for " + name + " not found") + return QString(); + } + return property(propertyName.toStdString().c_str()).toString(); + } } diff --git a/shared/liboxide/settingsfile.h b/shared/liboxide/settingsfile.h index 847b23daa..530e05699 100644 --- a/shared/liboxide/settingsfile.h +++ b/shared/liboxide/settingsfile.h @@ -12,6 +12,7 @@ #include #include #include +#include #include @@ -22,6 +23,7 @@ public: \ void set_##member(_type _arg_##member); \ _type member() const; \ + bool has_##member(); \ void reload_##member(); \ Q_SIGNALS: \ void member##Changed(const _type&); \ @@ -40,18 +42,31 @@ void reset_##member(); #define O_SETTINGS_PROPERTY_BODY_0(_class, _type, member, _group) \ void _class::set_##member(_type _arg_##member) { \ + if(m_##member == _arg_##member){ \ + O_SETTINGS_DEBUG(fileName() + " No Change " + #_group + "." + #member) \ + return; \ + } \ O_SETTINGS_DEBUG(fileName() + " Setting " + #_group + "." + #member) \ m_##member = _arg_##member; \ - if(std::strcmp("General", #_group) != 0){ \ - beginGroup(#_group); \ + if(reloadSemaphore.tryAcquire()){ \ + beginGroup(std::strcmp("General", #_group) != 0 ? #_group : ""); \ + setValue(#member, QVariant::fromValue<_type>(_arg_##member)); \ + endGroup(); \ + O_SETTINGS_DEBUG(fileName() + " Saving " + #_group + "." + #member) \ + sync(); \ + reloadSemaphore.release(); \ }else{ \ - beginGroup(""); \ + O_SETTINGS_DEBUG(fileName() + " Not Saving " + #_group + "." + #member) \ } \ - setValue(#member, QVariant::fromValue<_type>(_arg_##member)); \ - endGroup(); \ - sync(); \ + emit member##Changed(m_##member);\ } \ _type _class::member() const { return m_##member; } \ + bool _class::has_##member() { \ + beginGroup(std::strcmp("General", #_group) != 0 ? #_group : ""); \ + bool res = contains(#member); \ + endGroup(); \ + return res; \ + } \ void _class::reload_##member() { reloadProperty(#member); } \ QString _class::__META_GROUP_##member() const { return #_group; } #define O_SETTINGS_PROPERTY_BODY_1(_class, _type, group, member) \ @@ -116,8 +131,12 @@ namespace Oxide { */ class LIBOXIDE_EXPORT SettingsFile : public QSettings { Q_OBJECT + signals: + void changed(); + private slots: void fileChanged(); + protected: SettingsFile(QString path); ~SettingsFile(); @@ -126,6 +145,9 @@ namespace Oxide { void init(); void reloadProperties(); void resetProperties(); + QString groupName(const QString& name); + QSemaphore reloadSemaphore; + private: QFileSystemWatcher fileWatcher; bool initalized = false; diff --git a/web/src/faq.rst b/web/src/faq.rst index 338bd8e91..3241a3055 100644 --- a/web/src/faq.rst +++ b/web/src/faq.rst @@ -63,14 +63,17 @@ Oxide (and most other applications) on the reMarkable 2 requires How do I change my pin after I've set it? ========================================= -There is no way to currently trigger a pin change, but you can wipe your current pin, and trigger -the pin setting dialog by doing the following: +As of 2.6 you can change your pin to any 4 numbers with the following command: .. code:: bash - systemctl stop tarnish - rm /home/root/.config/Eeems/decay.conf - systemctl start tarnish + xdg-settings set pin + +As of 2.6 you can clear your pin to skip the lock screen with the following command: + +.. code:: bash + + xdg-settings set pin '' Not all of my applications are listed? From 6656c646880fa67558ce96c6cce2d789ab6fdb68 Mon Sep 17 00:00:00 2001 From: "imgbot[bot]" <31301654+imgbot[bot]@users.noreply.github.com> Date: Fri, 10 Feb 2023 14:32:50 -0700 Subject: [PATCH 15/15] [ImgBot] Optimize images (#294) *Total -- 156.39kb -> 130.62kb (16.48%) /shared/sentry/external/crashpad/build/ios/Default.png -- 1.67kb -> 0.61kb (63.44%) /assets/opt/usr/share/icons/oxide/702x702/splash/oxide.png -- 12.10kb -> 8.23kb (31.99%) /assets/opt/usr/share/icons/oxide/702x702/splash/erode.png -- 3.08kb -> 2.38kb (22.67%) /assets/opt/usr/share/icons/oxide/702x702/splash/anxiety.png -- 7.61kb -> 6.13kb (19.48%) /shared/sentry/external/breakpad/docs/breakpad.png -- 82.18kb -> 67.94kb (17.33%) /assets/opt/usr/share/icons/oxide/48x48/apps/erode.png -- 0.37kb -> 0.32kb (13.53%) /shared/sentry/external/crashpad/doc/layering.png -- 22.91kb -> 19.91kb (13.08%) /assets/opt/usr/share/icons/oxide/48x48/apps/image.png -- 0.58kb -> 0.52kb (9.75%) /shared/sentry/external/crashpad/doc/overview.png -- 25.90kb -> 24.58kb (5.08%) Signed-off-by: ImgBotApp Co-authored-by: ImgBotApp --- .../share/icons/oxide/48x48/apps/erode.png | Bin 377 -> 326 bytes .../share/icons/oxide/48x48/apps/image.png | Bin 595 -> 537 bytes .../icons/oxide/702x702/splash/anxiety.png | Bin 7794 -> 6276 bytes .../icons/oxide/702x702/splash/erode.png | Bin 3149 -> 2435 bytes .../icons/oxide/702x702/splash/oxide.png | Bin 12390 -> 8426 bytes .../external/breakpad/docs/breakpad.png | Bin 84153 -> 69566 bytes .../external/crashpad/build/ios/Default.png | Bin 1707 -> 624 bytes .../sentry/external/crashpad/doc/layering.png | Bin 23460 -> 20391 bytes .../sentry/external/crashpad/doc/overview.png | Bin 26517 -> 25170 bytes 9 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/opt/usr/share/icons/oxide/48x48/apps/erode.png b/assets/opt/usr/share/icons/oxide/48x48/apps/erode.png index dfcf3b22ca6667f60ce05b26a99f8f6b648c72f4..f1b4bd60dc1c10148f21e55229fba99a6aa6edbb 100644 GIT binary patch delta 212 zcmey#bc|_&3NK5#qpu?a!^VE@KZ&dp6{G68*i^0i=a#=`U|_KHba4!cIQ;h7MqUO5 z4p+x-@loOiN}a83)%(@13n^yaw~%?jx4~)^gC-YHH3P!}&Icc>geQD)7iP)bQl@$- zMI9)pTH+c}l9E`GYL#4+3Zxi}42+C*4UBXR4MGgetc=a949v6*46F-K}|sb0I`n?{9y%=AzFV51||agwX7rn009h1L_t(o!|j)` zY63wJhCeioSP7P~v9Yk!S4iPo9E6lULwpGnpE&PE@?#==hfsJ)+X?2z8KT^pnw9aGkrok=PGmYybcc+Ty+r!D30 zoZi447q?E-K-p0u?2z!@M?d%W3(=R)xW^V%g<(rpS>a_P{?z4pc~c|j%kSHH>T9c7L*=tx zo}Cx1+)Ei}&j}Ur-0PzGp!;-`#mC>%HCSV=_p$#Jn#A|^eBNsn<7>=eehl1yHEP$J zeLWW`Xq(`bBO_<5+4udE(M9=V8Iv|W;kD?VyqZy_-oPxxz{twf$jZb_+rYrez~I5s zuwVuT2GtVRh?11Vl2ohQ{FKbJN(LhXBST#SV_hTj5JL+q0}Crd3y6l(PcoW;8Z_V< vN-}d(i%Sx73qX2|bq$PkL6#U>8JJlaLG^r{o)Hez!{Ev8>gTe~DWM4fUiY+G diff --git a/assets/opt/usr/share/icons/oxide/702x702/splash/anxiety.png b/assets/opt/usr/share/icons/oxide/702x702/splash/anxiety.png index 2b729896aa3cef7d5a9d44532252634fbb9875e2..fb2ba07efaddb7c34d5a19b7cee2a629bf0cb991 100644 GIT binary patch literal 6276 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 literal 7794 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 T4GcQMk@dd4hxU~0_Kf>4`twny diff --git a/assets/opt/usr/share/icons/oxide/702x702/splash/erode.png b/assets/opt/usr/share/icons/oxide/702x702/splash/erode.png index 302cfe10ecb833556b93ac2a0ecb8ada87b5c49e..966906e2c0579637f4b5ecc5ee44b0203ba5eb4e 100644 GIT binary patch literal 2435 zcmeAS@N?(olHy`uVBq!ia0y~yVA==594tVQcYRm?0V$>=Z+904hE=`FMnE2Cfk$L9 zQ1x{XW^~e+T>%tiFY)wsWq;1Z#ik}ak+tU<0|Vocs*s41pu}>8f};Gi%$!t(lFEWq zh0Nq+1_q1XrNPmCPZW6mJQt~Ed7N(HC)Kp&`~Cw`Jb!1+o0@h#Xv&hDQ>~hp-bYt{>CAIh*^rrHjJU14~8ci)8fAy?C+t!u#jTt)5(R z4zz!kd1uQfcPAaQ&t+%I?Pf90x@-15mv516Wk~4Oa)8E&&^fRo?~0j9SShzC&%XS`k~RsLxT_lGb>{=D+4oa0|P4qgNgRP7NTg#%}>cpt3=me zYGrH;(Qy6I#VDW#X^;)T=7&{senDkXW_m^mLqKU#PG+)#OMY%*X5O}mhS5M}N-$+9 zsl~}fnFS@8`FRZPp1uJJNu`-NCAyh;3dKS0PCn_5!a&ucFx9zX2Z*%#2QO8c%2y^(cp_k~PDj^^WuY2QtpVCwUL}0GWS?%~^{>7CnUwS%ka0e{Er*^?J_PpOCx$`E|qdi=M=`+0|}zcV^5K4h{wo99BwZ69^v zB(v>WZE=DwER1Io9MYy4s(WhwxOUyPH0bWp%4zlY=U-j^OqY$t#{^&BjwE*mUlv z)-}hqp6i~AT6!$%`xAYh=U=C$uDdMdB-9ENUAg(*iUY-lu=@wOsID>_zP7XVLL$h`$5b8Le2h&9HJ{VBqrfba4!+xb^ndY2Ic7 z5!Zv>PY!vcX14kzS$T45DoTfOXl^k6*7Jm^qUK)Y1fUV4fb0;su=XU!gGrVrZ%Z_| z*-Q>eZYY*>V_<0bz{e$xhZ#2CgMtUIr(nSYqj83(z`hZa_-GrDMCDOjPC&`^ z*ite^WRslra9NFEGB$+-lMzwA2W3Y(<~Bk_2=y`%9ewYo$sGH7(vn?IV}D98Hq>_2 zYSr}TuL@vd_&vq(=T*iLP;lM}He}lIWOj-v+k%DpLS$FNqZ+9n0te3iW$->GF8(^> Q{11@np00i_>zopr0PzNsz5oCK diff --git a/assets/opt/usr/share/icons/oxide/702x702/splash/oxide.png b/assets/opt/usr/share/icons/oxide/702x702/splash/oxide.png index 931bc7045c0d5e1196bcfd538654842ad011cd32..990321f198ee8a8e44cccbdd5d601ce2e3030d9c 100644 GIT binary patch literal 8426 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%0AqyKxe@5kmCcMKq!Ckt-h$*|rP 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 literal 84153 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

Dn+UmXymPW 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 diff --git a/shared/sentry/external/crashpad/build/ios/Default.png b/shared/sentry/external/crashpad/build/ios/Default.png index 8c9089d5c6789aae633902ca93ad552f14bfadd7..ca857795bf313dfc8005de2a0cc4050356ade305 100644 GIT binary patch literal 624 zcmeAS@N?(olHy`uVBq!ia0y~yU}|7sU@2e(3NQqHxv>^Vv7|ftIx;Y9?C1WI$O_~u zBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZkpafHrx4R1iF!Z_gM4t>j@+_&xoa&~0``kA7DT5w2FJ zg#n`!X+cnbi1Fm%jT=&e^JIV_u3F+6QIe8al4_M)lnSI6j0}v7bq$Pk4GlsJ%&d&f ztc=XG4GgRd3@%Iky@R45H$NpatrAm%0aU|-Bqv#*21$?&!TD(=<%vb942~)JNvR5+ cxryniL8*x;m4zo$LCJu@)78&qol`;+0EH~AumAu6 literal 1707 zcmdT^J!sQ$5PkW8epw_Ss70g%1t+PTIEWyT1Py4dZ6iWKT5V1oN>mC_5D67aT-a0POUYLJLVmvcLvhefUF=P{-`%}?@7CMPasftvXzuo4HXPug=WgyZx?u3dmsW z{A4M5gcvPGjZtERXCcI3F=z}D1H5PnqvtSxbg27p^s1{U%N>Df{_KF43AQA-R$k6}` z&;SYGRV}%L(Rktu8(V(shWx@TrOF#C~6`mpn8&GuR#rB zpgj6Pnw+K`)K3yrNY(@+X|iN1d2lhY2~8}xb6#f75LLL5)W@~VL{vauDVHo*REUB; z(rFq%UTmk)R7)%A2WfJec90VVQz2OsjHEe9TI9jS#3nTSMBdFZa|U=Z8cBUz%S`wg zC0~QfO%BY9`aP2)IjO{VKW}2Cb4=XvYbS!8a-zYHpU`pz$tFS*i&vs~)YJt!KpSWQ zO`r}`fFh6wa-^}1a<|*)vfuz8z{{eKplb zkj7Gc{xNsN|7&(qtXxXJ{Bw^J4^RL59y>|Wf6n#u!0=b3O5d-JIMH%xwD|1I^uljm C#i;ZE diff --git a/shared/sentry/external/crashpad/doc/layering.png b/shared/sentry/external/crashpad/doc/layering.png index 791bea5383d404e3bbfe08b0d1884e0e031e425d..8de5ac441cf59fa668df5511d81c0869efc4fa45 100644 GIT binary patch literal 20391 zcma*P2SAg{^DgYM3j%f&6tGYQsnW}_(3?n;E-myXy(_3Fh$uC5=^{-DMY@Re1f(0P zfP@+l0)bHP1U$cT|KGj$J4GRRci-LF*_mf%=h>I{RAi|r=qUE>+ealYclZ9jef!_; z+xM5;(f#mA&>KD+{B_Xcmh!EA`#yzHZW9CMV@ao)F&x{mz+UyTm$ z&Gzk6$(Fx+OT(idKV+j3;a)A|)7zUjq|0IYVen~Px^?Z#17{Cfu{lQk^=Pfh#KBt+ z1p~+*@X-H{Urr!H|NYIMUyh<>pv`~(V%C7LFuU|x;!S1v>{@g9MAc7qn!IIcP7&9` z3;qXhMF(Pek2U*VJ$=3Gsgr!GcP*t!{dJ#{jn}S?Z-p@(ML);#&?BxM4ftY{o5?&$t}^q)^xDWQ8MJ>Q1=cE{8H#85UxMA8KrR?o)?%}WaI+{(aX zq?n)mY0Uiw>vt>Vm=%`8&S5~p_u=p1sRP;ShWHu$k>Se6)o)*JG+5ZO#=sBub&rcw zs8J`$fV~*-)2#(P(K9gb)5yx1 z!3!4NXezMrN^Z^1hOf-}G=`o(5tp|@_^Fi|Keiu!Hz(K5m7A-rU$ymj*G8p-S*mVk zx>)y0*jL-#w>`(AZrsZCh^H6Yi7d(>ytVR3xB-tlmXr&!iCvqRgcrGPr4_^tb?0Zs z3a#|KG_zBJL)vo+5_uBYt>*h6mw!8@b+c3TNLiYB8LmwQ>zBV)t zeDEAgFe5_ey)52aKhJMj(=k}s6)tp24B;BgSz2HPZVYwl#Nwyx(v*iluLcG`!)KXp zH>))4B?S7bOGm30FYL~}y?-v#tr+i3Zz(!aQddW;qKYL+^Y09%w@+!$x@bx`TYeYN z)Vi9M6YD=;H+P7?!olo{5>r2}K|)7OPwOh>@nJ>!_zc%a{Y+eW5d(H+uJnid=cn

493sklDkRpU#yS_L;%ZmX$U?NLz98 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 YS7@(6aE-hWG}tFEt#UX2_QQbx1=Hf5>;M1& literal 23460 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 diff --git a/shared/sentry/external/crashpad/doc/overview.png b/shared/sentry/external/crashpad/doc/overview.png index 78f849efffcf48b3887df080361592be7b4e0a5e..5e04d38a11fcf6d34575245924ddb33f3ecfb6f9 100644 GIT binary patch literal 25170 zcmaI72UJr{*9MxT zB94*Uhk5qJqb~F5URBZub$$UP7iCCT*+ig8c;Igkj3bCfh6Jt(zS0=PgRd!QQt(wK z?9zYl+PLDrc4Kf2|MzaG7_Q;}-u<6$SpWC#|L-7dZM^kX?a{LnA6~yEt5*g?{U}H4 z<{P+I8Gf}u|GmHAxypBk;mHjgL8fK*kIe* z+C(RjH>9P{5W|PZXidS*?)if<%Jv3a6=t^eSIyhkI~V) zgNDxO>FJK3%fg}XE4MM-DY8DhD;JysfF^8hZ9}OT>49?O<(olo7cX8U_(uNb%59l^ zo%Qi5O(f(Jhbk`@*Q4BlRQb1e{SKe``=1UMKPD`69xYwmoNWhB85E{EjhK zCl@G9Lqj8IthKGJdWHuNXC^i$xQVW=XDIq;ca_Ho-2H0Xmk}QyKT=|?tD}=B9dHWz z5%*lXOkX1#LUDobfubTOAKzq^+kA#f9GIRvP`{vcXS|rJ#ZZB9ozEVOmru;6=ZhQ| z`=UQ`Iln@N`tfj)x&2^Xk6oF}mN)p`RF{^P22?&fKkqzNz6u7I?@VA2w(Fm5kLl~} zEj4h|(HZ7RASvl<(v%kASf-}P*C7B zRquE8>eUxSly^N=^eWM^ri7LPu`XU#Rno?az8-kngcFTr4i9nkW&Ql*GKmmL%y@u9Hh^RBzMs1w7pSe!eS-dfgxy z=lVaY(1HR2I{60Ly1IL<97$>E>9n-8#`V5eQQgl}A3X4yc`FKo8$8?m)7l=xKmX?s zNjZ#)oJ;WLO}U~xgZ$36Hg0b2w$|1Q@-;r&3s^*067WARuIvKb$EiE{9?YGp&If(?(~icVg+ZE+v?Bv6!YSkVyhII^15=%y_5|oC3s<8klHA#6~lNGp}9|OaBKTcC3I^Tb_Q& zw)Y7bSj_Km8~6eE@Eew|{E8JE85wyVCF(fJ=F3WQ!lG_uB;kqyUe7@$Id50y4Llae za+#^OGeKN-YYv3WhYue*d!;BBKoa6#z zf`dFmbU9??+QIhP_9Y+nxq`Vya#pl+v>Qya^x__UdX&Y@s~Od*_4Sf4$F_Ifh(oy^ zZ&SzS;!PYD!<_l0lDeLhT%{m-tD~96*{S)bE)B^g7b~D5?D)mW;)@hEyE>D|kmx(K z5(T00-QS1Ob-(0*t5Ib$2&hUs#XF4)4U}5SZQ|hWbyOT}+@AgJ(BPm!zD{_{ce-GJ z@qL(5WV={`YHClVa#b!uzeulmpKynKMrDRrgd!+YCFGq}?UimZGp&iN`LeFeHced~ zoptrKEZ`M3HXBDLz60lqne=n5neeXj53^_oV=W5zDG0qmH7F-cR8u^qP!hPAp4}_L zDUvl{mpedDLa=Yk0g_26vZ8SYd#t)wlm0cmwfavcqnbiP`Ff;(y+8R99j`VYJSt{z zVBpdGxhld9>FMdqxhpz{eyxx7i)PIFd3qE2r0e&JaEsar(N~OhP)oC>G3W!VoDHjf z2Vlc}z_`$~p+2sb7B8^Tesn0K|Do;7!_wha%k@7UZ5{Hd?~?*#6g`WPLmGN|KVx!a zu}3E(n=87d<##Od#PZg)AHV4)o$Ix@qk-*=Jbt1RQ)K3JX!PIK-i51_T)&r9$(-@he!_pv z=BnY+7A&72ttx!OG}dD)3T3xSIuoQ{0;BuEG#jL9+*dj&&Kvc~P@LLAV8j5mYE zq(~~}K<=oA@9T@N!Ol39OWJYy5GQO5I+y?=m<+j$fd780;xz^lPKK1Ca)a@4$r{%n z75%-BrCOczis7Jn=@0TxP)Zi!KPW6zbN4?jYS7;q7AO%>!}7X#WhhGH@;ljdHV_Jw zysx4`<1vjS@jp3g-lErEGTRV5M8VWwGigKU5!C(oB8>y}gbq@TgnEJA2x=R&Gj)o% z0N!r7=RPif1J_LnvNpizXrMm=Pq-oR&KVfYF8@Pkp# z9>vCjuZUwzf5+;>knUA>6YWFGhS!Y=vrq;I_O?k1J(ML21Q~c6LmRvfeVR;m3rfU7 zB7~QXB32~uMNwb*@2s6O(V>&t#}Y=Xd3FCm47x^`Z;7#7Ts)oM=VOGcIgwp-S8%NRqtPe#8vwGmO;IpAL z@;Va9gdnjPY}L7C!<~Gkra~wg_BG_n(2k8)5IpbnmOqPTM?xgkiP@mR2*wD!{xCyS zg$-6FnMo0k9?Hdn{|g~?i;@Iy5k-{+Jv=wHMD_bt%Zsmq__ehRB}gg*xe=EJIYmQG zVyL?Dosy|Y@N^LPpHPtBM0p*BA0R>ygGR2kfE?myC_{-xVlDhKRC--ad=JhTwT}VF zXk>rn*Df`bc>cg_fDYg`T^R9;Zy>24SQ>S6YxKqyfr}_!)AwEK&nAcQ{>~jR=ItcQ z1Ib7YW?JDi@s3e^-%hR&&4?l_f(*{hni@#oc?Uu*0$GM+p}BR1!lR~6d2Mv@*=g%0 zhf4mFTo3yU2|?wks4t2Sw9_+kGz7I{M{*okb7^zKXwji%p_D`~&=w#8+!iA1HVLD{ z<3g}TP*XH`#vtW~sDsWmP)qJoGeQ40!)ywY%hW*$rmBx)kpi(R@RLHxo z>a#d#csmXYj4@nD`%Ua@o>k*LzsfJKy2QO)3J3|SIu@!MqB2t?yds7`%3Yk z33EV_{-Az)Y5OMaflriAO`1ZACY*^t09|HzfMWUyv4im3BFQ3th`5_fyb0x^A@GLE zA!u(AZQyAm3XpP?-6U-I-Yg7Rc(sV@$@u+yPUC;AJQ~qHdtp7be&AVdR))=U-=@c( zTSlu|tn4>S*B7l0t%ASGB&g0~k(fbDFbHjYsYcSD&5RroIWDkvsJN0UZvs)ay# z>}&dDnxfYOQxYiREIc*~Xr*Q2JzUsh^%`x2Mjgr(Cq`+HqAtrRIjgLjDu+FD`e5A3 zBg|vCK390@)CFf5HYv2*Tn}wg}eb%W$UAR;?@sP5g(z(Y0zV62veC z_*w{&*TG^?8;-OTF(Ybz33o`SoO)0;{8EkR4T6t`01r`*l>284vfwY0 zx@S4q+{NBSNjsm?039s?!4dH)Ma{In#cCH-A0Id$@x>p1UTHtlMhHYuEB;{Gxy(YB zMfd?Ffgnl?GodS|)`i?dkh4LnP}evKY7uuME|xb`5&%?#2)CS1U?o|Ldo1H_!<1i+EzO0 z4z{$rJX?7i3Of3tGnbc^kT4dIvl3cl^Xa9nB}D7&ybV>cgL8%^^ibwxXQpN2nBh3n z;coewG@d?j{Xe?3x-5>G*#2RaOE5*cjmrpt+v2prGShoUhHii6=ejbNGRpmW%hJ7G zsTWq3azP`Y+>LA|y{5H4@Gj#8V`d*9R2UdbjLK^x={}ydZe>pW^uP4&sc28U!rSmC zSpt=hn81|R9RT&?g|*{T+uPYGMRQcxFMgb53a-~&H%N`A@p!OL+ksKa300GJZjjM8 z=a8;Y3Czf@&JNFFdNn_}bC*JsLj1|yW%(fX!7q$VWrLF+XL)f2QZVHlUWLRzc`RCMq zJSn-;`mujffcu5@pO`90H)$NWf+H6xWmjP!Jpi0|R+ z!>CuUU%iNW;d8~Ov}D1yrJ4NGZG@oN8{daH5<6MgtgiW)`E$&;u(PwXca`4rZUX;6 z?PX?ID6z&{A@9Tk1dB`A^in~NF+Z2d=j=}hd7C5O%3he;PbB6u{#hZCuth#id-6vM z-H(2aP9N2XyfeBNcrz%%$q{=*{dW0T^;H$Y(OhE6&yKFqbh{-h1^$~GiCf|Y!)s}r z8eevU0f~J3_APtlqVTFcVcYW-BUXuBvz3jz_3IZads=(I(}Q!G(93ijzZ8*9 zyKW37W9nJlM!9-%H(R+iXi15k-#df#t#{p|rGP#!EVFM!o7N7j5@6wMO6i*>GYN)& z3^BG={d%**mA>CROIC3yu7@=nQi=he`9q#{~aJVSiPq8y@N=f*eXv*6`A-w9jzCi~#WAx_6nDf}^ zHZqS>FbX~5(`i@6eff1wZW8@l`8t!XE;iPnXXIg^XaNgt?s(#6Zfwo~FCfub-DnO$My z${0bJhR3{_j<&))Md0_p+H@k&fKr3( zgzwK%tO6VQ-Blpn(qRas$|*134Gbm3gJx@u_V@pw<^YT267-8bCi=ajlV!H$L*gTa zMvM-Ea3O9log6(Xa}-6&@Zk_b8$oXQ@{r#_A-tNgue1*{=a(^H_g4Nf5B2VWFR)Ht zyb?|_pk4REFUBHq`FYhVDR3veuW{i4J`h^}N%KGiB3sKKe=xK`9E}WS1S8{K%>AOB?7E$E<*^P{Ejpw~p_yUfJv@T4w|_llz>9B~~_ z>^n?0adpBLZtA>)bd7Aboev(`I@HW`>)m1-Ha)3HmC~LcFye=$hP^cp1JI+3_T@)spdc1z59(zAnJ6{V#2dE>VDG+NVRN}yg>Ffx2I>atIYCO<>kU^ z{ZORgKbamBDAmu^!PM5-QhSDb;yx79Qvjb!K|6}XeKV^Z3h3znkx0kib= zXET2P??(rzuX272 z`0OmoSRF%paEZZz!RTJhfMXPWx@MH-5!c~8%1?nA<#uCW9mmM@URpr!4l=}BJ}R}1 zdAnuSXCWmgw>+u2x%u641*ppy)RI^F*o@C_Syb+nhp0JfjPGD#= zeDeFmTp;`3%*z9<@Fff*c&g^jR$64 zy)L}plxS;oAGe%(%#@(I7anl@*uVMMq|HRFHEGm-j3I!$y82q^ch>icU+mE3MQ-$T z?{S26xY?-!RI3Ve4d|=~w7!G_11uQju6oqmyh!@5g*pI?TSb8~VmX2(KI3g?yOadKS@ z{r)WyX_=jq^Ew~H4UqInNi6?0oEjbsXEWW?;?@QU=a-~Iy2i-x@Xwg=^Ag3xM+8f< zI;Tacz~`cuwb|LP3UA<^xGg3$>3{4W5TmQ>$T9v3M-E!ps6tag_!ioWpU@Fc1xYYb z|C_;!+~%=2>QXnI$UfgqP_4g;3)MHQG3^J{<Jfm{)w#G}ArTSos5YllRlPc=7boT6xHfIV%eGh#^IKO|JjBcV6at>+ z(J6*seo~^J+1G=i_}-1_8FL-&Q0w!QW{n@CRRx~y;FzEqCP)FFe9O!9$Gc>XY(B4` z2Gpb{Jp3W>&Rd{fz{DVSjq25Xnc#|-MttjuvJ|CaxQ?hon_qlO9OI15!*WNh^zkhz-pU7zHgbN4HTlv0(A@#DWxdWqq zA!dJ{f)ozr_SjTMz5b|VRbebi`@Ya`B8~dR-^UiDCelE5V~-RT5>jto1(s-Y5b67? zhhC9YD%qL50{W4?hvPAyGtpN=VBwSx*)_+h-}=-|e))1;NB6B+VF*ZHc6MLD-4t}q zqzhJ3h)+v*2PskNY|G{r%lQ_-8l5l+$~o><1OpE^<74ozv2nSQ{Z>?cGbBVyeweD>v5D z-{1e~Av@^h)0zdgsi`T;Tju82o{6V7=66Dhn9F@bV+Q{R=H{&MG19DoC8WVuMVtpP zDZ`|_=9;qIVk}M^i_;y3YMA{zT7pZFnXHkm@v0nProc@`s+RWCOD#-S>RIa#duoa; zTaK6mT=xQcFbVQ3G!cqy*hj{p=jxB3CE}3A7-IG4W=)Gm#wjW76i7pJVI~f3D*81pdGce%wL5BAtw`At)Ki18d=>$#e}{^CP7H zTweDPum)?sD*^npdnBOr?s2TjXUTO;SzLa@A<)%Voq`(V>XY6TF9v0g(kChggKh4a zoyK?F*EDR=^{s+WJYF>c8UE110(MgO0u_2O#=$0q;C+Pg)1i^v&5=R#PcJ#_XYU%+ zM)LX(V3nOt=h8;DV0^8YGyI}-RtzhugluAVk3Kc6f{Ij>asiGL)vPUi(})ogyOt24z%R&)BgUShQl77?MM<>K<1zj@~0+U;Y97)_Ae!UWck|q zhgBch*L5}UdMCHV_`%S^+0Vq?%7^k9w?4PZl~lrfOxqHi9O_INW=5rM;(4 z7D}vmHo2)8ffs$lT{znh1A1I443PGlb2xIGBF<<~0@gp9;h$^xWZtkUo+8Rkb**Y> z%16f8H%rd76ghM~rk#|yC6{l3ZCTgfKS4wNO13Q^i}bA%_rqT!O+x!O^{YmgM(<>Y@##6ONE=9+ zx4j{cW|-kntum{^p0=0*bxrqIDFb!-gNS}C9HP%fzFryD``T|>6ANB!G_86lvwcuC z{3oCwOw_2J-UQDN7>H-;i|M~d zt^30q{Pq*5O?+*=z*Y0reSiQIF@=-2Gf&oDFQ+gx0B{5mr#pB#sGQAfFSW}=UsK(n zSWWMu2XHgsn$cg4o1RvBN+0ry25g03o|@l{E3e*-5+?NIH9J~TSP#K$!g6zaF}H)h zPlHP4Ij)r=iRY^OO41#4w`EUcQtAYK&j{7Joev{OjnGtOt$TL**Se!puOhoV9&^ z{&J-gFh5qM^smLm@Baq*dPK@X0V6$KPOiM&3tFDUDqaVjmO}sjH%34E1BhHM$gT13Qg(SIJ1{5sZ;I7k!JLhS$qI@5OWqFE{fp}_u zf!{DrxV)KXcdZ1j{uPdCIt5FKo=xxy2cWgac~K$E^OA!|oI!pDE?sA$AG_Q5hHjc= z4KlzdDR0L<6c!f#H{%}~Dc-qmA;pf440yDt*gRLz}^p&9mP& z6I}zhD;s@M0yvT^c!P7hI~!zqN43Cb1`?+ct(K~pk^kmt7iL(0x(Ljb(Dzs^tc3=! zi|g+zC!BD33cMJ?lbzWaY6#!&`0PCH4boX=lpPQ(VnYL@C|;rxLjA%zD7F zT#Un4K!-X1L+x;4JF3m5%m5&DI9s*JwQt{DS^pp%2n#m`?{j->(*6oCCiQ0GGzEL2 zkjq2OxSoT%Z(C}RKVX;s_kK-=W}VEAPK!3M;>?@;o9WAgbmQ+|MY1>4N*t}VuSaGJ zn*21VdT0Jg{Q5|J46p(7b&$am?s{h1GfTsnWb=$l>e2K8AV=3&<{E{VKmH60_X?|U zv@sf{&6g!l>%&a^z@#VzBvc{ZpX*GO*ZnZ<-o=lKg2 z=8JB^nXF-S(fsuw*GSxu?iU-v(T{|hXA_IXQ@h5V-XP*fosXn$os>~lJgIkG2B-s4 z{$b;`ZUJL57!j%GybA!yfp_FWf?&7}cE6-@Bjk81X2?s!Nu_k43}6-@A2$k%#LTxV z)e3#xu%jIyS%F-v{Pn5foeWsLx)ngB3#6$*J1tG{tp^#P(<|ls#q_d5)Y`omS6eKA zfjL@O3G05N(ZSk0E)gYi0+6ciNM{WT9mQBLpB8}Eq*Q`al~zE&iEt^9@BDXChkR!S z_g`rWaqLXPs1rI0#U)RYt|14o?H~>l^N(s=UeNK1QZW^hzBuS1%zE|%px&U) zP)KwNSp!ZZGJ=i1GtqNt_3g_KaW95z^^Z+tXF#&xq`LNMP63!I9W9&7TKV$$^p4JB zMbpoIdskx3us}qhGq$lkzSg#UiD$k|AFs;v%;M4{AUWmN*Fj8y*Z{foYQ6f+am*Y; zMdHHPdMO zUiudBIY7`jYw>45p8O6s0SpNc2daS)|C5bY5aF_ubus95sW@_lkTVycnxLK;p#PBd ztdC_+CjhTXsVVV2=>zorJ@{QTeZy2=)9joW0TVH9_V=yk?RNkR?@Pi@2f`=c-W30m z0#u|LM*gBEo%RU%yncw_Ip|a7`!9e1bzd`nzhm0zZmcS=)-g05Qw<9T`M8f~ilm~P zjYrqem}lW2XC?*^;x%=qagB22E9zYaRcp8gg%f~V9ZBEm2D~JA32t8G3pBGen+lv> zQQl3%xzbnrllJAC05So@_G=#j2ar&h%B`^gK6EML*A;13rNiD8pun}FebPGDU;Cuw zF_x-HcgBFTPk^_rRnLHmb?c9+gkb@I|FJ)eeP1u+14u=Z$-x}(Ft8W^wBzySn^>8y zgJU{DKoD8qyc=^qiq8KT7xmQza4@)4O@X*%(;4WshP*-$7mxd;l?ToFo07yq{F}G8 zfmPPbT#p=0fp6Wu{i82fxjA^+4ws|EyD?j@qu=~wrY-ZbkZCZ#({+FTarJQFE-$`9v-d_C}?!ND@P%EVYX# z>8Bw5u1CXS@Gdsu@1w3Gc5ij|;pdbe3edVt%;?fZ97a^=kmx~ZlY!8G;9wodGgcz4 zlS7Cb%8_>s8WK|0;D>^e;9X?7qz@QY3N!b$tBZKqHpaDzq?yI|4qL zO~Fa<8f4^JV;s+hj`BscxD%!Ujti>6~B}a4Jav^CO)_9Wxa8tw9;o@&-Y(2bmLt#qev#E zM@=I{)bR0c26p_@mmp$V8aFefVsdf{tJk>8U95mV=@sN>QV%BDVmomnXCvJ389O? zy8(9T zDD1|2z~;dc(~J9jL^58!H}Be^SIsW)_XOwPjKsT}49z0OLkP1#a{?h|bcGn&%&40X zZv>Ih>uPO?QX{Lsvam~;3RSOBV;eO(@ z?dd!&*n&1ejs}agu97=A_VmaMOTr$gZK_yNRC!XaW@zk#BM1xUQ%9P(zLDO29C_F(?fe^sVUfcn`vBFza-!;G{C~Cq6rZj0f!k2@qX-RF(afJt4GfCIGm-zjoQS&&iWr5= zQmrKtIN4Xf?*CdL1eoz^R6=MuhhQm$tj5jnT+WsM13LRSg@E09IMfNv`DWzljqLqz(>MC# z{$siz4}lb^^8!cj{`CMZTUYwJE06Fc?)~5K5+vO2x&9_mJNa+Fk~mZZ9Hd8QXmh3O z|J5h33a**~))~+7K=tTv*Fo@l}jJMLot zy{9WOTKS)x=jKdAw^LT=HM43xgI6MVi=t2wETTvZR0A*xNEopvdWR2^(B#X z`3XfV9BHeBgl$j1=l`hE*RPrM`8|+s?Nslb!@9lPTyYj1Z>W=eq`dH98zal#p1bZw zZZDiPyCUwhH{0}mfgNU=d*E=``}Pv-Pnwek>DI*{=`$;@9q{1b=bJ>+h0p(NOGmo3 z>oDI6;nOd#v%?O4->V9K>wK1vj>(Rq1}iq%u=&~9{MIQNx2wY54}VEM$<|U5ZzfR9 zbQ{@>&-#iq=m|7`^&pq}%2t4q?Q{lkXYsV+7MG)T z8aS531=!^mR%Iy89qgD4 z5WYpp-xYoV;uxuljg{@-rjyC{;Au&5Kl7urW}Uh0AS{2ro~Nfs6qgOQvmE)R$pilC zW6|<5?xn^@b5KOJtp6(D&C4eVsw!#}5%H(iSx`}*op%vZ^9A{w-g^NLyUWM?7{zDHWtm)sr6@@Wld~Sx=3C-|N!bvDO)#)WQT{+Ki$cZjD7fDIxnmTipo5 z=2yhUxf}<82MyUtT_TY7TWo${oBJVJ`GZ@#$h8xRy8Q^3{Hw8YDWS!yE7T>r>m-sW zo09#LhU8oC+dh^W{q^ZgvN5}|>WMX*wMSh(TTR`?eQN_GOj729z``Q%RI{I1t#JWo zh0pDAdXJg()6Jm(raF?iqvbs}H>`h^KZ=?9Kp031=5cWzZkjFdk-R#*J4Gu8 z@n1dnW1KrLx|l1RRU#+j=t6F?PfTG>LZt+SAAgT{6q#A4b<48g95KHl^nR`q0@}X4-Jmqu057aF8HQfgVA+*|W9Y zkkq7bpo^ssJDz&J8lyQQxtaLnv{xwE*T}4KtR0Oil)9IV&FZO zVTL`PGNzB;7>~9y1d0XvC`lg`0W-b0`2ONa#SM-wl}`Aj>OJM{sSG;iC7|@~)ljC! zl>u?~^!)z{#I@2%=+kv6@3}8HC(C!e$R5*SY=~XT4gvk}=W#Mhnd|gEZB@2v zna`W_Z@E@lE6PvueKMujxd{rqAh=9}N1JH+18p0%e3y}slj;*-q@WQ;06Tsj@}tl3 z(uAdChx3IDK8eYn^f8)_UIU3PRixBgiM`ex=C7cVi%iNTk% z-FH@p`CYl0^i7@xV<0jBoUEQko{65FnlkHgpw4@^=-l*983%r(byKCtc|G5?(bp-D zGgVBhUH|;4nwhh75NEn(ugiRk%#W7(A*OcLQ{wlO#BG+e)tkLHAZw;W>FD62 z>Bl^OK~wCqN38)4eF5H04~*Dgskli5{hbE0=1qN_zL8DqnoC%$yU0egYN zG>1#~Wi3J00VZ7fMKz#TDi->y=1(wK=73z*lz}!B)&pn|wf#%wJg(2LzLPa?sXzxm zP}7XbTkp=vDiy=!@lvs1l`zlWsz$)T>Hl$0t`+3>`7Lk)0PKh;*4y^>)7FksN;jxP z+DhQ^5syz7vxaE)fAbNUxX1=l;{3Qk|G>cFj`v&nRE_LEPeBKC{v;>4L(AX-*ikBW zjW34hbQF^^pwwQj+w(U$^+`2{exc)z?v`j@fcZ4mMRiR+5KJOlbD*BZ;Dcx$s8ok z?=i0m2Yf|*oANf4ARuh`C%yMiwa>op9Z#>d8y-OZ&Z5XLcxv6V%7~JZa$0gM1?GnA ztIEkyDjYK8jSdMRifNzSIuPTVJpA>Omu!G86)irB0XgHL@mCWNg4@43TEhlafv?Ss z1u#gNN2?!|M^(7Ah=??JS)eu~5iZB6bxdlV@S1>=B$)7&MbzIl7}tu=uEWsK5O_$oekMa0l(C_l( z4fQ~6P0z1=!vG#Zq5O~@C%docdIvdx4aPfOYl52d?E_TU;AuYU_ZC`bV!$Us_? zo!vU^613;QbD1n%Ou()E!itL1!ZuTIC_Nm=z_p1Ts7hjLiD-uu$^Frfj! zAfQT1>;vRHO1SI=(!i@z!)b=by7G@I@fAi_u3iOc281V|4_`f!=bC%@2=CK$3705{IWQv5Ghigl1(RZJ6dwcub+}!WPiQgsR+YZ2c z<2nZKNsa-{3AZH0owdDu;0EHjY<~$~-kf9qNUydf_3U~-AEP+Wg%kQj_QZjDPLs^Js#mP;Hfd!QU(1X9Oe$_v(CswtQ&Wy~;=@cV{heb%Ur>DLp4U z8|Z+F0B>TI5;g_H5tc^>-+&7HFpPGti?MNjiPyOAoTvfhXx|m*Jq0O6#;OirD`9m* zX)gxk7Ges9lcv|bI9&BktSP@<`oU!-?+pOdpbTHY5CMdmxu#?re>oEw$As@O^KKuI zpNmtIjjqudPDyl(Y)uq11REDF*Q-*r+{ye*%!R$;8SN(FZ<;O zrpxk|0?k8)UU6p^AhY>B)rN8FYP-69r6}{=B}PDKXxP(Z78+;={YdTi0E?bZ-<$#D z5ikIfKZFjd1eMu#{lsg znH%EvdtBxXqel`D0HWbMVV)61vXWzct!nbs{cV4N(O06X{SKEnR9&Jn*q3;>7dMM_2n zyKkpQWoB^VD%gmaJJ6Qsc#hq;C5|(wz45Pfi(K*Ca|jhp*5=lBcXtQoom!t)Mx2BA z`I7A0{FdWz5$hF-)6I5%{@RV_UGtjUQPWM)t(G@$-dr9k^~$6TpdPh$<@ zKlrf-VHy%~d3>aS>x;=jMtPj*NYBRgy>;F7q)(ErO&>^8$S0ik!1>KJso_2tU;1Ivfwyluzz&>tVXubcJBS_7wES=nXiuC=eZAd0WB~Ezu%;xE^B}-zJqlU zv!U>d@VQW@<^_iP5iOw{DF=miESGEut|6pY$i*|5Hp(zWVG!kQ17FP7an;GuFc8u; z{9v!}NbgEdK~H!a^9lM)c|Snz`$b;!kCH(hPsq5362hoX zV2Sqxi{y$RQ~VL~l}Qn7Muo586G2U|y3)zyU~eRB4@w4VXHZZCT7Zy7K>MLoW)u-U z(Q|;-)Ot?-F6MutUJNWYSzI}>mcNv7i0vQoj=WOgfvTfnI#H%8h{Pp;Azn zT<~Pkg_j?V|0Q&_-@OXw>d!N z(lR}0SYcf#S`(c#fnrjJkfI2?@gGB~#I7+dJ*t2NqsV`fFzkxFY|_DRMUkmPg)vas z?`u6&Y|=k5q^OJPPysV~Gw9biX8ABdaQvp_@*<83zzVL`6q<->o2AWH^SLfT}!euTR+j2a^Q-MhaIagnC79YfVfC9>iroOoB7yqPEox_jYT>W^brb%a{A4pw|2t|&S zzhlz*LwStcG#buq+&OXE7Dm*K5_R=>!F&IoC6~Z5j-B5iuGHxI%H`O2!`-p8R>ycN z9g@pztw^A!f!NHBS zEv+Bg12G#=cOAd;5?SpFu}5we8Bd?6Id}Q^x1L-SRaV zTc>D9^}Hd&az%ZR>yjmDcW?)y-c#A>ov*5=&ubg3lM~jw{losnQuxU_R;v%=YJ(-; zp-YUBn)*JFD!l&Y!sZ2D_ep;zFXy^EpQ^&V^}#^0& zXlrUADu^(l2+30Kc1%j($Y!h|JiNz>rqE+%IEhvN_FD4OQjBe z9>v9#y&4B#-)e_b3C{uOeQ>cGgMI8; zG3E$Ps6N>mJFM`u^C-~H>(H^vmNA6aHcRHt!)UcFqMT<(wo?pVRq$v6Hb za~(ZxJ=AMIDFY0(cFsDn#zv!<&s(26^9)Vvr1ak($8T1#zGlS%a2ZV~Qx+jF3ZRS% zT=EW6BQ1=<3>=}PQind>{9wauoD>U za(+?$qTQ-}o@Xvx^9@5fgK(vX3$-$}7?x&t2TIlWhGR#KN|#bii>*`ZS3Gyz;iuK& zP(Q<8@0(jI^IDd@hK|?Tmtr`$*{L2;cs;~k$$<*~e%-A0m|wdvpz`$Izr3qZgJxi0 zpkAb!<=X81Xxo*Y#9%`|{ItE0XA|_N8KQZ6eAKB{M@KW7S4~x&O(MgeVy?WucK!IUB$(?z%h)M zp|;?(s_{6F2$YjRqefyBK&qdR-oWd{qfSJCg$P$vUWkBXTLhq(7O2_q zoeiVn*29qBh#j^%iQoga})e0-4 zH>kRyXT)6YW_%%nb&WKaAS7(8Idm_Je5^FLNeM%KrcJDfdT16=IpLF>&g5RVU&c}@ z{Lai(f0%Nv!j%JQP@BrjL_*L{?_SMbmsDZ;EAD%^LJm`G-Y4G`^*dM9>L0G^feZi0}_S9}5JsflrB|&cYADPmJa@fiDG! znGt6|-y$H2P@_g6S3X0(W!?g+<&CaJ#<&Z87-L&kuti*{hv(J1AC&^3%#y;AeeR2s z2;Fi#{#bsM_bOglIl|b_OoL272{T_G;cD9N@7l8xMu68fWDhU;w9CS}@Oe>}7YO$Q zZzJGlcE5@`flB<0pbof!xW`W|w?AI}l3CylVu_;Al26o7@vsYB&=#EGg@5 z@xzil>tt)+w(sxmeYmF+Q>Hu^?x^X^P{6Dj5K0&Bqq;dh^w#veAn4y^e(!~iQ-he; z>gBUsST}?!nQ7y-+tf3QeEC%54V^~HEC@WAvSyqj3rgP2(kWL+EQF#gdR_4wKOVDR zt!A{U!|$;!4uz|~Q0!PqL62Fv#fS=Kz>8Bz2%ahnBn?la5yBbxV4>S-o@tJ0D{4zzW%`cXow|v- z#TB2~2M3X&*FZ2VE2+>GKirL{F)iSfYULVoSESbYMCC3zZ+thSQ&5Od0oh5 z*r0JWt9|AhsslSpTZ^1uPMi-d5q|t9I7j9@7C}Ygn3WftFmVfe8oD3yY`2jDMZg06 zM8JuTdOufyoDz$;uC`d2=^y2wZTVW7q?O=uew@&6B1^cjlYfy)Kz{zGp^-u~qyv>S zF8$w{RQ^|RbXt!?f5?lhK{wo6Jx@28@@SynOJxhF&Pv#d5nC%NBJyOhrJ_ zqph21d-|ys&YHHf-S(Et&p+Q{h+@Fi-Ol4gN8c#5hu58V_49)do`&P3Y%ksMgREZe zs*YF7ZVe}`2E5zfDi~Vk5a&>6#!OE+3;aMc$q+uS8{UU;v*kim^bCy%)9|ihyt_^Q zpX$y$9Ln$SQb@42pV&2`P3`^>pN_c`bOoO8clZ(uHA%B(lSq|?%2 z59lE{pEWy?%vgA%_~=0h8Zh(_EplJ3GcTKF$l9@ZvuhE2_i0U_wIrl~kf`Svnf|^Q zAyDo2;kuiD#X$bPRqX3NuIc9khV=sVe0XfSRX)=HYKkn_3;uR~UjxLU7Aio`jdHKy zYGjWOYYtN@RJqJ5p_}_A_m~Tq&svYZ0cbFU-d6Buhn(1t#ye!`%P-1Hyia|HTEO{^ zGc-IRo%zYUrj59P-(9JKEJd}Ste@qLPac2d&?50%rumV1P^OEMy{J2#POWs|K3BR1 zfYqImIMxyF^MY6}$_*y-JE8g(_AEu^e7SrNSQ^<}&OJ0AVn-M2p^y=EzY-VM0m{D} z6L@xnAIpXXTti8@(uQp7VhVp5J=o%&m&cX2j1A@zzxsaVI?2z)Ixs%$P+V;tbmp1z zZ^q72Bi_rW&*A8GYXwl(&v?BgY>_1mYybR$8Al#wHzTT`5br!N2R^&(1FO1Gam*1M ziD4ovl{vGg*rE{VX^vw!!?b`>;4l1A-gOEtORxHiY>%Qf%MDH>ECjt>2hzHivi3SF zEM`@Pus$bg>_u$mj8eu&(|{_w0HLDmcZ;MI>PE?CywE?;$WXch`baqc4Z+#a=cgS| zsU+h5?nuiiwc*hi!c?e1?I8nbHH1B7p8y=|H`~UlE$#@3s1%)L4P0w|+fAHzTv=Kg z${4wdxjI5#DY$U0u9rQ%px1xmfbZhjqKuCO%U0ri(l5kdRjV==!v8bM1*%VjL98~d{=D#b@AIcKc&0o=5hn;iV&ZKh{Wb* z=Kfa@rW85@vNZg>^?qjv^ag`pefp~-i^H$#goTLpWz4x?ZE+|QdODP|1N2Ox5lfjA z79r{KI))V1t3yDcFq2Fdld0?d(W1ofr%wpKGaTgM1u@e}iI#tq#PFDT`AtVq zTBgc$aWX`jP7zi5gQJ#SAc{-sL#5~k=MnAX!64xX58Est)->C^1VdPx9oNE8LNA3f zc|3T*!3w*i1uQ&_Pd{fHBx~@h4^cid3P8F($M$K3GDu)VUWQjs3Bp6*{AGl@af=*=ucaN1rkMehl+>h22aRNu2VKzc;VeZCb^xd5Gfl zGZL}3L~hiK?Vs%7Iru0(FjNa}rws<(i|`2ON3ZXPzM78BtgDY zgU%qlln`D$d)aVAO{#zw-JOA>ocLZB`--1y{!|cYX{%VhUBhAB?{r#5z9SfqNhi3| zyt>E8_v(-G-a5_Rhmkss4Rz*a=U`k5JqoAK3>By)JtI7fk~0)EI-JddIoXXl!qW|R z1}VnAXOMkhkH^yjNHxrD=EEDpK~rHK^oMy7n`cL~T`@u!UAMd1JZ8!gr!@~`zh2FJ zs0VuzdZH2JCfSvuN!+rYaYxJRj4rdOWP0I&0AHJGbfEfZa7_qUDzdw7u;iVbJ(Xnm z91uJw9-n{fU0u#v@Zc`G$_D!t7=ZktbFXD$fN)x~eW`i%3ZWiTDCQdQk+T4z8)|%d z(Gw##amYMl_6R4li!(l08Dc_^1!PtSnW<#Www0{8lIa+QD13++-^bJeajN^Ju*y_i zZfX&7@mMCZiCj-7avbtp%=F@U*X*`Gzg}n!y3MP^sQ~wd%ZQ`56BkfTqs$3)tX*2! zde210nDgU7|J2y0VOUaAN$c_>j<|Dtij{eXI1bB0WMYnWLr;bRCigbsFu_#n9zlw( zwEW({zD&Y$>zR|$bVoxk<=*(QKLTUt3M)M5#Kp@@nc{PtKl$qkMG1iDO$);4p`-TA0<)p31Lti(^sNQ+o)nO?fS zAdw@_>qeJN&~kJ~`4?(inCLUDK~$|F*`Lk-(3g^PaUJiMfGGp)>2V{4BcpEah0p?F zSO3?N(>5jTm~+3M>%Bg=gK@ny%4LU(3*FhD&fb@$>dTOJYz1b%eUbQvb~An^opa%)M_n$#bzA zu^`V@Oic#N+3ub=$jV`jFz<#hO~6N_VuA>?-vn~oCn0A&?@two|_l% znSE}0RRY;CrRW;#A@~t?IO6@`{otPO=rsSd)K7w=+ezlSta8)!z~Vgl?v~0Re2LJ+ zz#Zax$;O?=uvc-{hSfthCmd(x3?+SD1oeDLj~?~hJ_YX8U{8(Rd}wrDpye6==8wY2 z;`RsSJou;P1!)_(i5FfteH84YTbwiV+7ICEd%J+@Z?c#B`lLM#Me9@~<{jrLn#_E+j%@XlC@O z{gpMl8l%$Q8T8c*wxLV7wk-2Z#M=8!t-t8@0nJ-QKz0FZbf1cfk%FvxQjx~TU(pS! z4PUJGwzCUu=B*`6GKHsqOfzd?NM{^y^6Nu?F+QPyJJor!AwlD+8ji7Q1Zh<4CS)DbQD-+Sd*VCuRmIdMPP;!!WtMS z9;`ya8X($ORppApPlYirujgKI=i;(lx`E@467C}{=>p2jesUPx`cmh;N3Chsj^k2b zfc?m# zH<(EK-DBMAo9R5^G4SV#A=#erb3JPPWA;$rQhtJH`kF`)Yz)SOE&7@8cwL9{g1874 zq>7c{1OQ_aXOgU>l`rtsyt_IKogf^$AHLyo%*N-L)D=u$lZWm!EF7F3aV#Y&@Cb9F!XrZS(d% zBmoe~1DGRxJRltlQ?&I3(_oa$VHnd4>coku(r~2qV1_^Hs?Ah*444pn&pmlS_;R2D z8RPnB@P~&Q=#^lH6{L}$tUIO_^&;BMI>0??=lPBhF1euN?~q?%@-)w3#uX}%(Xqa~En@GP%zT)*XeUzt6BsZqr>Um<3w@g=OlPOi0O%wCwBM9~P z>q*HC%?;bDR{UJ2EI+5e!kxuhViyJcQB|*Dkv9R9a~X5>E14e2LE;eRIn}dV$9-1$ zzH;&M0L|;j=F$xi4xmV#5#>I*e=C^kF#Id{S+JP#bxiEi;g#mhc9Zj#Bwg8VzCB#! zWeaqh@9s^c|J+M2f3S3WJos3wNbD>&(is?f0e$IkCz&1}ny-midc1jZ!q)m-Uhqh; zc21motot1&rxb&OJGhN6&hJr(^p;(M!vJ!uM*Z0L$RctReq>Q+(aNYQ_l7WFJbi$N z*$y-j_wjpq%v(KwwQYuy&^#aFW3*0^Yk((uc{={~c@e{F;8~ZT{exX92I(_3v>1Ki z*1%%(c!&s9%pf#N3jH;xYJUET-P?lK%QuH_mQ@|v#aBkGg6FB8p92JRv2iJy6FJjI z1XG|oq1AVtk!r~sDt4uIj?IqZ#-itqVBiUR<#5|=yS-nTYmBR)@br{9XIfNom{64Ez{zLNm3 zAcm9#05|}kkASs33`mwZSCV-}@r8@(-x4`-uE4~R#sE!T#0a$}8CMj0cJ(ein~BOB zTbQXtD6J_C&-*$%AL9uL7zmnQtO@d341BmqqGo_U9x8Rcow||lB%cGk;C7l!0jLOo zN9|^FY3{hY2pG+gj&|u#wD*6V4Y(!IxOvdoXhyX={Sx?O?Vim)r~SVbpgo=Zr^J5` zOvH{Q;Qu9Z04E28{SG(!Pw@Xew7Vr-HL%^aK6_TRWp^^TVhQ5& zk?ORZ{*B8VHz$srTOS?pI<|t`Cc#5_z&=U1buf9zKc*gHlU zSQuBh&TSiO-+su*bNivfj=N<7M9q%-(R)$ZdGem6dM?!)-g2(Uz+Qr@tEs3Di%qN=;x{N*d4Dks$0{R;p%CJ=v)DWSKfdR#WA!B7OR z$R}wdN}w92sWK@?Roiw2;ANRgNa$|_n6uGZ_EK$X{j>tjIJ-oil@mA=+@AaN4onaf z>^%MW8`j+_67*dJiM}nqbJgoe#$ZLDwFV9R+^}MMbtG?CbO8PKH&=4|m-c;oRhukY z7S46#2;L4^Z$Yf1c=sxo1Xsdz-_}{Jggv_eaH^Yw-8#M6ahjttB_4{hC=ei2pfttM!^s<*4-sTar zNlJuI$i4cc*0w5sea;aiid$BE#mVh9^X19S5mSFSs4p3(_MxCRh7v| zmW35x+;{2Nyk*TV3V>5H^W0jyMB zG_1a^SS^clQw=7&p_BG1)ljMMyY2EZDBc!%`7)H0K&#;5%c|0;?uS0{xzzVpDun^9 zwO%S09rt`je(Qkynl1=)<7FT?1U^jY86(XWUxP$NI|`1fUX0djUyodEcN;FNUMTGY z{66U+t-ccW*RAQDPWDP@qv?UwAK@Dj=2Uhc&H;dQuRc$8FSa+IEb}ZL_*>ATX-_KC>_A?(^B86@G_o)Ie%{F8YmtC(n8>Cy)l)&Fr$Psp6;Yh zc2Vc+&!6xTscgNTTxlYl({Eb%T5P9u1xF1DS!iM){Guc`XneN1XCqn#0G{Tg*yz?i zw(QI1EoCArM*1T=zt0~AuXV>68ye0GR#&PO8XFa6{F?q^jNgiOudH589i4m8EZl6b zME&zn)dLHXM^-i4L&@!qrDi^gmZl$FD}&;%ZS_t@w^3)3sZ#jXE%%u(V++dP7PydH z+gzAjW^e?HkYBYGhM*7DNLdDP?joc@m;8-_4=X`ir6yKQMgZDiolC~f7`c;5ao* zLW#KUa=!cFLK_ctsW)P_w@GU$Y!6F#rGn literal 26517 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 A%K!iX