From 7d08ca2e4512536aad11e50f57afef9af4c81df0 Mon Sep 17 00:00:00 2001 From: reionwong Date: Tue, 20 Oct 2026 09:37:04 +0800 Subject: [PATCH] feat(privacy): list and remove apps allowed to control the computer --- po/cutefish-settings.pot | 32 +++++- src/pages/privacy/computercontrol.cpp | 77 +++++++++++++ src/pages/privacy/computercontrol.h | 47 ++++++++ src/pages/privacy/privacypage.cpp | 150 ++++++++++++++++++++++++++ src/window/pagelist.cpp | 3 + 5 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 src/pages/privacy/computercontrol.cpp create mode 100644 src/pages/privacy/computercontrol.h create mode 100644 src/pages/privacy/privacypage.cpp diff --git a/po/cutefish-settings.pot b/po/cutefish-settings.pot index bdea59c..9d79dd0 100644 --- a/po/cutefish-settings.pot +++ b/po/cutefish-settings.pot @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: cutefish-settings\n" "Report-Msgid-Bugs-To: https://github.com/cutefishos\n" -"POT-Creation-Date: 2026-10-19 23:02+0800\n" +"POT-Creation-Date: 2026-10-20 09:33+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: \n" "Language-Team: \n" @@ -1457,7 +1457,7 @@ msgstr "" msgid "Peer" msgstr "" -#: src/pages/network/vpn/vpneditor.cpp +#: src/pages/network/vpn/vpneditor.cpp src/pages/privacy/privacypage.cpp #: src/pages/shortcuts/shortcuteditdialog.cpp msgid "Remove" msgstr "" @@ -1470,7 +1470,7 @@ msgstr "" msgid "Preshared key" msgstr "" -#. TRANSLATORS: a placeholder: the stored preshared key stays unless a new one is typed. +#. TRANSLATORS: a placeholder: the stored key stays unless a new one is typed. #: src/pages/network/vpn/vpneditor.cpp msgid "Kept" msgstr "" @@ -1687,6 +1687,26 @@ msgstr "" msgid "Prevent automatic sleeping when the display is off" msgstr "" +#: src/pages/privacy/privacypage.cpp src/window/pagelist.cpp +msgid "Privacy" +msgstr "" + +#: src/pages/privacy/privacypage.cpp +msgid "Computer control" +msgstr "" + +#: src/pages/privacy/privacypage.cpp +msgid "Computer control is managed by the Cutefish compositor, which is not running." +msgstr "" + +#: src/pages/privacy/privacypage.cpp +msgid "No apps are allowed to control this computer without asking." +msgstr "" + +#: src/pages/privacy/privacypage.cpp +msgid "An app you always allow can let its agent see the screen and use the pointer and keyboard without asking first. The Stop button in the status bar still shows while it is in control." +msgstr "" + #: src/pages/shortcuts/shortcuteditdialog.cpp msgid "Press the new shortcut…" msgstr "" @@ -2259,7 +2279,7 @@ msgstr "" msgid "Add Wallpaper Folder" msgstr "" -#: src/widgets/searchfield.cpp +#: src/widgets/searchfield.cpp src/window/sidebar.cpp msgid "Search" msgstr "" @@ -2347,6 +2367,10 @@ msgstr "" msgid "web browser, email client, terminal" msgstr "" +#: src/window/pagelist.cpp +msgid "computer control, agents, allowed apps, permissions" +msgstr "" + #: src/window/pagelist.cpp msgid "charge, charging limit, battery health, capacity, history, percentage" msgstr "" diff --git a/src/pages/privacy/computercontrol.cpp b/src/pages/privacy/computercontrol.cpp new file mode 100644 index 0000000..f94a21d --- /dev/null +++ b/src/pages/privacy/computercontrol.cpp @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2026 Reion Wong +// SPDX-License-Identifier: BSD-3-Clause + +#include "pages/privacy/computercontrol.h" + +#include + +using namespace cutefish::base; + +namespace settings { + +static constexpr const char* kService = "org.cutefish.ComputerControl1"; +static constexpr const char* kPath = "/org/cutefish/ComputerControl1"; + +static std::vector appsOf(const dbus::Reply& reply) +{ + std::vector apps; + for (const dbus::VariantMap& entry : reply.value>().value_or( + std::vector())) { + ComputerControl::App app; + if (const auto it = entry.find("path"); it != entry.end()) + app.path = it->second.value(); + if (const auto it = entry.find("name"); it != entry.end()) + app.name = it->second.value(); + if (const auto it = entry.find("app_id"); it != entry.end()) + app.appId = it->second.value(); + if (!app.path.empty()) + apps.push_back(std::move(app)); + } + return apps; +} + +ComputerControl::ComputerControl() + : m_control(dbus::Connection::session(), kService, kPath, kService) +{ + dbus::Connection* bus = m_control.connection(); + if (!bus) + return; + // Read at once, so the page has the list for its first frame. + if (const dbus::Reply reply = m_control.call("allowedApps"); !reply.isError()) { + m_apps = appsOf(reply); + m_available = true; + } + m_changedMatch = m_control.onSignal("changed", [this](const dbus::Message&) { reload(); }); + m_watch = bus->watchName(kService, [this](const std::string& owner) { + if (!owner.empty()) { + reload(); + } else if (m_available) { + m_available = false; + m_apps.clear(); + changed.emit(); + } + }); +} + +void ComputerControl::reload() +{ + m_reload = m_control.callAsync("allowedApps", [this](const dbus::Reply& reply) { + if (reply.isError()) + return; + m_apps = appsOf(reply); + m_available = true; + changed.emit(); + }); +} + +void ComputerControl::forget(const std::string& path) +{ + if (!m_available) + return; + m_forget = m_control.callAsync("forget", [](const dbus::Reply& reply) { + if (reply.isError()) + fprintf(stderr, "Cannot forget the app: %s\n", reply.error().message.c_str()); + }, path); +} + +} // namespace settings diff --git a/src/pages/privacy/computercontrol.h b/src/pages/privacy/computercontrol.h new file mode 100644 index 0000000..0daace0 --- /dev/null +++ b/src/pages/privacy/computercontrol.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 Reion Wong +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once + +#include +#include + +#include "base/dbus.h" +#include "base/signal.h" + +namespace settings { + +// The compositor's org.cutefish.ComputerControl1: the apps whose agents may +// control the computer without asking. +class ComputerControl +{ +public: + struct App + { + std::string path; + // The agent's name for itself, and its application's desktop entry. + std::string name; + std::string appId; + }; + + ComputerControl(); + + bool isAvailable() const { return m_available; } + const std::vector& allowedApps() const { return m_apps; } + void forget(const std::string& path); + + cutefish::base::Signal<> changed; + +private: + void reload(); + + cutefish::base::dbus::Proxy m_control; + cutefish::base::dbus::NameWatch m_watch; + cutefish::base::dbus::Match m_changedMatch; + cutefish::base::dbus::PendingCall m_reload; + cutefish::base::dbus::PendingCall m_forget; + std::vector m_apps; + bool m_available = false; +}; + +} // namespace settings diff --git a/src/pages/privacy/privacypage.cpp b/src/pages/privacy/privacypage.cpp new file mode 100644 index 0000000..1224af8 --- /dev/null +++ b/src/pages/privacy/privacypage.cpp @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2026 Reion Wong +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include + +#include "base/application_registry.h" +#include "base/desktop_entry.h" +#include "base/i18n.h" +#include "base/signal.h" +#include "pages/privacy/computercontrol.h" +#include "ui/button.h" +#include "ui/cell_layout.h" +#include "ui/icon.h" +#include "ui/label.h" +#include "ui/theme.h" +#include "widgets/card.h" +#include "widgets/listrow.h" +#include "widgets/metrics.h" +#include "widgets/sections.h" +#include "window/page.h" +#include "window/pagelist.h" + +using namespace cutefish::ui; +using cutefish::base::ApplicationRegistry; +using cutefish::i18n::tr; + +namespace settings { + +namespace { + +// A label in the theme's secondary text colour. +class SecondaryLabel : public Label +{ +public: + using Label::Label; + +protected: + void layout() override + { + setColor(Theme::current().disabledText); + Label::layout(); + } +}; + +// An app allowed to control the computer: its name and icon over the program +// trusted, and Remove. +class AllowedAppRow : public ListRow +{ +public: + AllowedAppRow(const ComputerControl::App& app, std::function remove) + { + setInteractive(false); + const auto entry = app.appId.empty() ? nullptr : ApplicationRegistry::instance().byId(app.appId); + std::string title = entry && !entry->name.empty() ? entry->name : app.name; + if (title.empty()) + title = app.path; + setLeading(entry && !entry->icon.empty() ? entry->icon : std::string("computer"), 24); + ColumnLayout* texts = addContent(); + texts->setSpacing(metrics::textSpacing); + texts->setLayoutHints({.fillWidth = true, .verticalAlignment = Alignment::Center}); + Label* name = texts->addChild