portal: Added back screenshot functionality (#127)

This commit is contained in:
Oliver Enes 2023-11-05 02:00:51 +01:00 committed by GitHub
parent 081b36add9
commit b2fc111096
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 271 additions and 5 deletions

View file

@ -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;

View file

@ -1,5 +1,6 @@
#include "PortalManager.hpp"
#include "../helpers/Log.hpp"
#include "../helpers/MiscFunctions.hpp"
#include <protocols/hyprland-global-shortcuts-v1-protocol.h>
#include <protocols/hyprland-toplevel-export-v1-protocol.h>
@ -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<CScreenshotPortal>();
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();

View file

@ -5,6 +5,7 @@
#include <wayland-client.h>
#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<CScreencopyPortal> screencopy;
std::unique_ptr<CScreenshotPortal> screenshot;
std::unique_ptr<CGlobalShortcutsPortal> globalShortcuts;
} m_sPortals;

View file

@ -1,8 +1,13 @@
#include "MiscFunctions.hpp"
#include <memory>
#include <unistd.h>
#include "../helpers/Log.hpp"
#include <unistd.h>
#include <filesystem>
#include <cstdlib>
#include <vector>
#include <string>
#include <algorithm>
std::string execAndGet(const char* cmd) {
Debug::log(LOG, "execAndGet: {}", cmd);
@ -25,3 +30,37 @@ void addHyprlandNotification(const std::string& icon, float timeMs, const std::s
if (fork() == 0)
execl("/bin/sh", "/bin/sh", "-c", CMD.c_str(), nullptr);
}
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<std::string> 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();
}

View file

@ -1,5 +1,8 @@
#pragma once
#include <string>
#include <sdbus-c++/Message.h>
std::string execAndGet(const char* cmd);
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);

189
src/portals/Screenshot.cpp Normal file
View file

@ -0,0 +1,189 @@
#include "Screenshot.hpp"
#include "../core/PortalManager.hpp"
#include "../helpers/Log.hpp"
#include "../helpers/MiscFunctions.hpp"
#include <regex>
#include <filesystem>
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<uint8_t, 3> 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<std::string, sdbus::Variant> 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<std::string, sdbus::Variant> 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<std::string, sdbus::Variant> 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<bool>() && 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<std::string, sdbus::Variant> 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);
}

View file

@ -0,0 +1,18 @@
#pragma once
#include <sdbus-c++/sdbus-c++.h>
#include <protocols/wlr-screencopy-unstable-v1-protocol.h>
class CScreenshotPortal {
public:
CScreenshotPortal();
void onScreenshot(sdbus::MethodCall& call);
void onPickColor(sdbus::MethodCall& call);
private:
std::unique_ptr<sdbus::IObject> m_pObject;
const std::string INTERFACE_NAME = "org.freedesktop.impl.portal.Screenshot";
const std::string OBJECT_PATH = "/org/freedesktop/portal/desktop";
};