// SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // lint-disable-file no-char-pointer /* fingerprintd-agent — run something in YOUR session when a finger matches. The daemon is root. It has no session bus, no display and no business starting your applications, so it does not try: it announces every matched finger on the system bus and stops there. net.catcrafts.Fingerprintd1.FingerMatched(s finger, u uid) on /net/reactivated/Fprint/Device/0 This is the other half. It runs as you, inside your session, with your environment, and maps fingers to commands from a file you own: ~/.config/fingerprintd/fingers.conf right-ring-finger kde-open camera left-index-finger plasma-settings Different trust domain from /etc/fingerprintd/actions.conf, deliberately. That file is a root shell and is guarded like one. This one runs your commands as you, so it is an ordinary dotfile: no ownership check, relative commands fine, re-read when it changes so editing it does not need a restart. */ // stdio.h before the module import, for the same reason main.cpp does it: // setvbuf and its constants come from the C header, and the std module does // not re-export the macro. #include #include #include import std; import Fingerprintd; namespace { constexpr const char* BusName = "net.reactivated.Fprint"; constexpr const char* DevicePath = "/net/reactivated/Fprint/Device/0"; constexpr const char* Iface = "net.catcrafts.Fingerprintd1"; constexpr const char* Signal = "FingerMatched"; std::string ConfigPath() { const char* xdg = std::getenv("XDG_CONFIG_HOME"); if (xdg && *xdg) return std::string(xdg) + "/fingerprintd/fingers.conf"; const char* home = std::getenv("HOME"); return std::string(home ? home : ".") + "/.config/fingerprintd/fingers.conf"; } struct Config { std::filesystem::file_time_type stamp{}; std::map byFinger; // Re-read when the file's mtime moves, so editing the map takes effect // without restarting the agent. Cheap: one stat per matched finger, // and a finger match is a human-scale event. void Refresh(const std::string& path) { std::error_code ec; auto now = std::filesystem::last_write_time(path, ec); if (ec) { byFinger.clear(); return; } if (now == stamp && !byFinger.empty()) return; stamp = now; byFinger.clear(); std::ifstream f(path); std::string line; while (std::getline(f, line)) { std::string_view v(line); while (!v.empty() && (v.front() == ' ' || v.front() == '\t')) v.remove_prefix(1); while (!v.empty() && (v.back() == ' ' || v.back() == '\t' || v.back() == '\r')) v.remove_suffix(1); if (v.empty() || v.front() == '#') continue; std::size_t sp = v.find_first_of(" \t"); if (sp == std::string_view::npos) continue; std::string name(v.substr(0, sp)); std::string_view cmd = v.substr(sp); while (!cmd.empty() && (cmd.front() == ' ' || cmd.front() == '\t')) cmd.remove_prefix(1); if (cmd.empty()) continue; // Validated against the same vocabulary the daemon uses, so a // typo is reported here rather than silently never firing. if (!fingerprintd::store::FingerFromName(name)) { std::println(std::cerr, "{}: not a finger name: {}", path, name); continue; } byFinger[name] = std::string(cmd); } } }; Config g_config; std::string g_configPath; std::uint32_t g_me = 0; void OnFingerMatched(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar*, GVariant* params, gpointer) { const gchar* finger = nullptr; guint32 uid = 0; g_variant_get(params, "(&su)", &finger, &uid); if (!finger) return; // The signal is visible to every local user. Someone else's finger is // not your trigger. if (uid != g_me) return; g_config.Refresh(g_configPath); auto it = g_config.byFinger.find(finger); if (it == g_config.byFinger.end()) return; std::println("{} -> {}", finger, it->second); GError* err = nullptr; // Async: a slow application must not wedge the agent, and the agent is // not the thing that decides whether the unlock succeeded -- that has // already happened by the time this signal arrives. if (!g_spawn_command_line_async(it->second.c_str(), &err)) { std::println(std::cerr, " failed: {}", err ? err->message : "unknown"); if (err) g_error_free(err); } } } int main(int argc, char** argv) { ::setvbuf(stdout, nullptr, _IOLBF, 0); std::span args(argv, static_cast(argc)); for (std::string_view a : args.subspan(1)) { if (a == "--version") { std::println("fingerprintd-agent 0.2.2"); return 0; } if (a.starts_with("--config=")) g_configPath = a.substr(9); } // Root has a session bus about as often as it has a display. Refusing is // clearer than starting and never firing. if (::geteuid() == 0) { std::println(std::cerr, "fingerprintd-agent: run as your user, not root"); return 1; } if (g_configPath.empty()) g_configPath = ConfigPath(); g_me = static_cast(::getuid()); GError* err = nullptr; GDBusConnection* conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, &err); if (!conn) { std::println(std::cerr, "system bus: {}", err ? err->message : "unavailable"); return 1; } g_dbus_connection_signal_subscribe( conn, BusName, Iface, Signal, DevicePath, nullptr, G_DBUS_SIGNAL_FLAGS_NONE, OnFingerMatched, nullptr, nullptr); g_config.Refresh(g_configPath); std::println("fingerprintd-agent: uid {}, {} finger(s) mapped from {}", g_me, g_config.byFinger.size(), g_configPath); GMainLoop* loop = g_main_loop_new(nullptr, FALSE); g_main_loop_run(loop); return 0; }