From b2fc1110963fa583ad5348a9dc0101bd58ceac7a Mon Sep 17 00:00:00 2001 From: Oliver Enes Date: Sun, 5 Nov 2023 02:00:51 +0100 Subject: [PATCH] portal: Added back screenshot functionality (#127) --- hyprland.portal | 2 +- src/core/PortalManager.cpp | 15 +++ src/core/PortalManager.hpp | 2 + src/helpers/MiscFunctions.cpp | 45 +++++++- src/helpers/MiscFunctions.hpp | 5 +- src/portals/Screenshot.cpp | 189 ++++++++++++++++++++++++++++++++++ src/portals/Screenshot.hpp | 18 ++++ 7 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 src/portals/Screenshot.cpp create mode 100644 src/portals/Screenshot.hpp diff --git a/hyprland.portal b/hyprland.portal index 3c35316..d3e2cbe 100644 --- a/hyprland.portal +++ b/hyprland.portal @@ -1,4 +1,4 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.hyprland -Interfaces=org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.GlobalShortcuts; +Interfaces=org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.GlobalShortcuts; UseIn=wlroots;Hyprland;sway;Wayfire;river; diff --git a/src/core/PortalManager.cpp b/src/core/PortalManager.cpp index d9d3e87..1dd872f 100644 --- a/src/core/PortalManager.cpp +++ b/src/core/PortalManager.cpp @@ -1,5 +1,6 @@ #include "PortalManager.hpp" #include "../helpers/Log.hpp" +#include "../helpers/MiscFunctions.hpp" #include #include @@ -292,6 +293,20 @@ void CPortalManager::init() { else if (m_sWaylandConnection.hyprlandToplevelMgr) m_sPortals.screencopy->appendToplevelExport(m_sWaylandConnection.hyprlandToplevelMgr); + if (!inShellPath("grim")) + Debug::log(WARN, "grim not found. Screenshots will not work."); + else { + m_sPortals.screenshot = std::make_unique(); + + if (!inShellPath("slurp")) + Debug::log(WARN, "slurp not found. You won't be able to select a region when screenshotting."); + + if (!inShellPath("slurp") && !inShellPath("hyprpicker")) + Debug::log(WARN, "Neither slurp nor hyprpicker found. You won't be able to pick colors."); + else if (!inShellPath("hyprpicker")) + Debug::log(INFO, "hyprpicker not found. We suggest to use hyprpicker for color picking to be less meh."); + } + wl_display_roundtrip(m_sWaylandConnection.display); startEventLoop(); diff --git a/src/core/PortalManager.hpp b/src/core/PortalManager.hpp index 1813786..9b341e1 100644 --- a/src/core/PortalManager.hpp +++ b/src/core/PortalManager.hpp @@ -5,6 +5,7 @@ #include #include "../portals/Screencopy.hpp" +#include "../portals/Screenshot.hpp" #include "../portals/GlobalShortcuts.hpp" #include "../helpers/Timer.hpp" #include "../shared/ToplevelManager.hpp" @@ -43,6 +44,7 @@ class CPortalManager { struct { std::unique_ptr screencopy; + std::unique_ptr screenshot; std::unique_ptr globalShortcuts; } m_sPortals; diff --git a/src/helpers/MiscFunctions.cpp b/src/helpers/MiscFunctions.cpp index 353c8f0..98b70cc 100644 --- a/src/helpers/MiscFunctions.cpp +++ b/src/helpers/MiscFunctions.cpp @@ -1,8 +1,13 @@ #include "MiscFunctions.hpp" -#include -#include #include "../helpers/Log.hpp" +#include +#include +#include +#include +#include +#include + std::string execAndGet(const char* cmd) { Debug::log(LOG, "execAndGet: {}", cmd); @@ -24,4 +29,38 @@ void addHyprlandNotification(const std::string& icon, float timeMs, const std::s Debug::log(LOG, "addHyprlandNotification: {}", CMD); if (fork() == 0) execl("/bin/sh", "/bin/sh", "-c", CMD.c_str(), nullptr); -} \ No newline at end of file +} + +bool inShellPath(const std::string& exec) { + + if (exec.starts_with("/") || exec.starts_with("./") || exec.starts_with("../")) + return std::filesystem::exists(exec); + + // we are relative to our PATH + const char* path = std::getenv("PATH"); + + if (!path) + return false; + + // collect paths + std::string pathString = path; + std::vector paths; + uint32_t nextBegin = 0; + for (uint32_t i = 0; i < pathString.size(); i++) { + if (path[i] == ':') { + paths.push_back(pathString.substr(nextBegin, i - nextBegin)); + nextBegin = i + 1; + } + } + + if (nextBegin < pathString.size()) + paths.push_back(pathString.substr(nextBegin, pathString.size() - nextBegin)); + + return std::ranges::any_of(paths, [&exec](std::string& path) { return std::filesystem::exists(path + "/" + exec); }); +} + +void sendEmptyDbusMethodReply(sdbus::MethodCall& call, u_int32_t responseCode) { + auto reply = call.createReply(); + reply << (uint32_t)responseCode; + reply.send(); +} diff --git a/src/helpers/MiscFunctions.hpp b/src/helpers/MiscFunctions.hpp index fcb7cae..4b9764f 100644 --- a/src/helpers/MiscFunctions.hpp +++ b/src/helpers/MiscFunctions.hpp @@ -1,5 +1,8 @@ #pragma once #include +#include std::string execAndGet(const char* cmd); -void addHyprlandNotification(const std::string& icon, float timeMs, const std::string& color, const std::string& message); \ No newline at end of file +void addHyprlandNotification(const std::string& icon, float timeMs, const std::string& color, const std::string& message); +bool inShellPath(const std::string& exec); +void sendEmptyDbusMethodReply(sdbus::MethodCall& call, u_int32_t responseCode); \ No newline at end of file diff --git a/src/portals/Screenshot.cpp b/src/portals/Screenshot.cpp new file mode 100644 index 0000000..ba78dff --- /dev/null +++ b/src/portals/Screenshot.cpp @@ -0,0 +1,189 @@ +#include "Screenshot.hpp" +#include "../core/PortalManager.hpp" +#include "../helpers/Log.hpp" +#include "../helpers/MiscFunctions.hpp" + +#include +#include + +void pickHyprPicker(sdbus::MethodCall& call) { + const std::string HYPRPICKER_CMD = "hyprpicker --format=rgb --no-fancy"; + std::string rgbColor = execAndGet(HYPRPICKER_CMD.c_str()); + + if (rgbColor.size() > 12) { + Debug::log(ERR, "hyprpicker returned strange output: " + rgbColor); + sendEmptyDbusMethodReply(call, 1); + return; + } + + std::array colors{0, 0, 0}; + + try { + for (uint8_t i = 0; i < 2; i++) { + uint64_t next = rgbColor.find(' '); + + if (next == std::string::npos) { + Debug::log(ERR, "hyprpicker returned strange output: " + rgbColor); + sendEmptyDbusMethodReply(call, 1); + return; + } + + colors[i] = std::stoi(rgbColor.substr(0, next)); + rgbColor = rgbColor.substr(next + 1, rgbColor.size() - next); + } + colors[2] = std::stoi(rgbColor); + } catch (...) { + Debug::log(ERR, "Reading RGB values from hyprpicker failed. This is likely a string to integer error."); + sendEmptyDbusMethodReply(call, 1); + } + + auto [r, g, b] = colors; + std::unordered_map results; + results["color"] = sdbus::Struct(std::tuple{r / 255.0, g / 255.0, b / 255.0}); + + auto reply = call.createReply(); + + reply << (uint32_t)0; + reply << results; + reply.send(); +} + +void pickSlurp(sdbus::MethodCall& call) { + const std::string PICK_COLOR_CMD = "grim -g \"$(slurp -p)\" -t ppm -"; + std::string ppmColor = execAndGet(PICK_COLOR_CMD.c_str()); + + // unify whitespace + ppmColor = std::regex_replace(ppmColor, std::regex("\\s+"), std::string(" ")); + + // check if we got a 1x1 PPM Image + if (!ppmColor.starts_with("P6 1 1 ")) { + Debug::log(ERR, "grim did not return a PPM Image for us."); + sendEmptyDbusMethodReply(call, 1); + return; + } + + // convert it to a rgb value + try { + std::string maxValString = ppmColor.substr(7, ppmColor.size()); + maxValString = maxValString.substr(0, maxValString.find(' ')); + uint32_t maxVal = std::stoi(maxValString); + + double r, g, b; + + // 1 byte per triplet + if (maxVal < 256) { + std::string byteString = ppmColor.substr(11, 14); + + r = (uint8_t)byteString[0] / (maxVal * 1.0); + g = (uint8_t)byteString[1] / (maxVal * 1.0); + b = (uint8_t)byteString[2] / (maxVal * 1.0); + } else { + // 2 byte per triplet (MSB first) + std::string byteString = ppmColor.substr(11, 17); + + r = ((byteString[0] << 8) | byteString[1]) / (maxVal * 1.0); + g = ((byteString[2] << 8) | byteString[3]) / (maxVal * 1.0); + b = ((byteString[4] << 8) | byteString[5]) / (maxVal * 1.0); + } + + auto reply = call.createReply(); + + std::unordered_map results; + results["color"] = sdbus::Struct(std::tuple{r, g, b}); + + reply << (uint32_t)0; + reply << results; + reply.send(); + } catch (...) { + Debug::log(ERR, "Converting PPM to RGB failed. This is likely a string to integer error."); + sendEmptyDbusMethodReply(call, 1); + } +} + +CScreenshotPortal::CScreenshotPortal() { + m_pObject = sdbus::createObject(*g_pPortalManager->getConnection(), OBJECT_PATH); + + m_pObject->registerMethod(INTERFACE_NAME, "Screenshot", "ossa{sv}", "ua{sv}", [&](sdbus::MethodCall c) { onScreenshot(c); }); + m_pObject->registerMethod(INTERFACE_NAME, "PickColor", "ossa{sv}", "ua{sv}", [&](sdbus::MethodCall c) { onPickColor(c); }); + + m_pObject->registerProperty(INTERFACE_NAME, "version", "u", [](sdbus::PropertyGetReply& reply) -> void { reply << (uint32_t)(2); }); + + m_pObject->finishRegistration(); + + Debug::log(LOG, "[screenshot] init successful"); +} + +void CScreenshotPortal::onScreenshot(sdbus::MethodCall& call) { + sdbus::ObjectPath requestHandle; + call >> requestHandle; + + std::string appID; + call >> appID; + + std::string parentWindow; + call >> parentWindow; + + std::unordered_map options; + call >> options; + + Debug::log(LOG, "[screenshot] New screenshot request:"); + Debug::log(LOG, "[screenshot] | {}", requestHandle.c_str()); + Debug::log(LOG, "[screenshot] | appid: {}", appID); + + bool isInteractive = options.count("interactive") && options["interactive"].get() && inShellPath("slurp"); + + // make screenshot + const std::string HYPR_DIR = "/tmp/hypr/"; + const std::string SNAP_FILE = "xdph_screenshot.png"; + const std::string FILE_PATH = HYPR_DIR + SNAP_FILE; + const std::string SNAP_CMD = "grim " + FILE_PATH; + const std::string SNAP_INTERACTIVE_CMD = "grim -g \"$(slurp)\" " + FILE_PATH; + + std::unordered_map results; + results["uri"] = "file://" + FILE_PATH; + + std::filesystem::remove(FILE_PATH); + std::filesystem::create_directory(HYPR_DIR); + + if (isInteractive) + execAndGet(SNAP_INTERACTIVE_CMD.c_str()); + else + execAndGet(SNAP_CMD.c_str()); + + uint32_t responseCode = std::filesystem::exists(FILE_PATH) ? 0 : 1; + + auto reply = call.createReply(); + reply << responseCode; + reply << results; + reply.send(); +} + +void CScreenshotPortal::onPickColor(sdbus::MethodCall& call) { + sdbus::ObjectPath requestHandle; + call >> requestHandle; + + std::string appID; + call >> appID; + + std::string parentWindow; + call >> parentWindow; + + Debug::log(LOG, "[screenshot] New PickColor request:"); + Debug::log(LOG, "[screenshot] | {}", requestHandle.c_str()); + Debug::log(LOG, "[screenshot] | appid: {}", appID); + + bool hyprPickerInstalled = inShellPath("hyprpicker"); + bool slurpInstalled = inShellPath("slurp"); + + if (!slurpInstalled && !hyprPickerInstalled) { + Debug::log(ERR, "Neither slurp nor hyprpicker found. We can't pick colors."); + sendEmptyDbusMethodReply(call, 1); + return; + } + + // use hyprpicker if installed, slurp as fallback + if (hyprPickerInstalled) + pickHyprPicker(call); + else + pickSlurp(call); +} diff --git a/src/portals/Screenshot.hpp b/src/portals/Screenshot.hpp new file mode 100644 index 0000000..3593d5e --- /dev/null +++ b/src/portals/Screenshot.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +class CScreenshotPortal { + public: + CScreenshotPortal(); + + void onScreenshot(sdbus::MethodCall& call); + void onPickColor(sdbus::MethodCall& call); + + private: + std::unique_ptr m_pObject; + + const std::string INTERFACE_NAME = "org.freedesktop.impl.portal.Screenshot"; + const std::string OBJECT_PATH = "/org/freedesktop/portal/desktop"; +};