mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(idle): add protocol-based idle behavior management with IPC commands
This commit is contained in:
@@ -71,6 +71,8 @@ set(XDG_ACTIVATION_XML
|
||||
"${WAYLAND_PROTOCOLS_PKGDATADIR}/staging/xdg-activation/xdg-activation-v1.xml")
|
||||
set(EXT_SESSION_LOCK_XML
|
||||
"${WAYLAND_PROTOCOLS_PKGDATADIR}/staging/ext-session-lock/ext-session-lock-v1.xml")
|
||||
set(EXT_IDLE_NOTIFY_XML
|
||||
"${WAYLAND_PROTOCOLS_PKGDATADIR}/staging/ext-idle-notify/ext-idle-notify-v1.xml")
|
||||
set(IDLE_INHIBIT_XML
|
||||
"${WAYLAND_PROTOCOLS_PKGDATADIR}/unstable/idle-inhibit/idle-inhibit-unstable-v1.xml")
|
||||
set(WLR_FOREIGN_TOPLEVEL_XML
|
||||
@@ -112,6 +114,10 @@ set(EXT_SESSION_LOCK_PROTOCOL_C
|
||||
"${GENERATED_PROTOCOL_DIR}/ext-session-lock-v1-client-protocol.c")
|
||||
set(EXT_SESSION_LOCK_PROTOCOL_H
|
||||
"${GENERATED_PROTOCOL_DIR}/ext-session-lock-v1-client-protocol.h")
|
||||
set(EXT_IDLE_NOTIFY_PROTOCOL_C
|
||||
"${GENERATED_PROTOCOL_DIR}/ext-idle-notify-v1-client-protocol.c")
|
||||
set(EXT_IDLE_NOTIFY_PROTOCOL_H
|
||||
"${GENERATED_PROTOCOL_DIR}/ext-idle-notify-v1-client-protocol.h")
|
||||
set(IDLE_INHIBIT_PROTOCOL_C
|
||||
"${GENERATED_PROTOCOL_DIR}/idle-inhibit-unstable-v1-client-protocol.c")
|
||||
set(IDLE_INHIBIT_PROTOCOL_H
|
||||
@@ -268,6 +274,20 @@ add_custom_command(
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${EXT_IDLE_NOTIFY_PROTOCOL_C}"
|
||||
COMMAND "${WAYLAND_SCANNER_EXECUTABLE}" private-code "${EXT_IDLE_NOTIFY_XML}" "${EXT_IDLE_NOTIFY_PROTOCOL_C}"
|
||||
DEPENDS "${EXT_IDLE_NOTIFY_XML}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${EXT_IDLE_NOTIFY_PROTOCOL_H}"
|
||||
COMMAND "${WAYLAND_SCANNER_EXECUTABLE}" client-header "${EXT_IDLE_NOTIFY_XML}" "${EXT_IDLE_NOTIFY_PROTOCOL_H}"
|
||||
DEPENDS "${EXT_IDLE_NOTIFY_XML}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${IDLE_INHIBIT_PROTOCOL_C}"
|
||||
COMMAND "${WAYLAND_SCANNER_EXECUTABLE}" private-code "${IDLE_INHIBIT_XML}" "${IDLE_INHIBIT_PROTOCOL_C}"
|
||||
@@ -330,6 +350,8 @@ add_custom_target(noctalia_wayland_protocols
|
||||
"${XDG_ACTIVATION_PROTOCOL_H}"
|
||||
"${EXT_SESSION_LOCK_PROTOCOL_C}"
|
||||
"${EXT_SESSION_LOCK_PROTOCOL_H}"
|
||||
"${EXT_IDLE_NOTIFY_PROTOCOL_C}"
|
||||
"${EXT_IDLE_NOTIFY_PROTOCOL_H}"
|
||||
"${IDLE_INHIBIT_PROTOCOL_C}"
|
||||
"${IDLE_INHIBIT_PROTOCOL_H}"
|
||||
"${WLR_FOREIGN_TOPLEVEL_PROTOCOL_C}"
|
||||
@@ -359,6 +381,7 @@ add_executable(noctalia
|
||||
src/debug/debug_service.cpp
|
||||
src/font/font_service.cpp
|
||||
src/idle/idle_inhibitor.cpp
|
||||
src/idle/idle_manager.cpp
|
||||
src/ipc/ipc_client.cpp
|
||||
src/ipc/ipc_service.cpp
|
||||
src/launcher/app_provider.cpp
|
||||
@@ -479,6 +502,7 @@ add_executable(noctalia
|
||||
"${EXT_DATA_CONTROL_PROTOCOL_C}"
|
||||
"${XDG_ACTIVATION_PROTOCOL_C}"
|
||||
"${EXT_SESSION_LOCK_PROTOCOL_C}"
|
||||
"${EXT_IDLE_NOTIFY_PROTOCOL_C}"
|
||||
"${IDLE_INHIBIT_PROTOCOL_C}"
|
||||
"${WLR_FOREIGN_TOPLEVEL_PROTOCOL_C}"
|
||||
"${WLR_DATA_CONTROL_PROTOCOL_C}"
|
||||
|
||||
@@ -14,6 +14,7 @@ Changes are detected automatically via inotify — no restart required.
|
||||
- [Built-in widgets](#built-in-widgets)
|
||||
- [Weather](#weather)
|
||||
- [Audio](#audio)
|
||||
- [Idle](#idle)
|
||||
- [Shell](#shell)
|
||||
- [OSD](#osd)
|
||||
- [Wallpaper](#wallpaper)
|
||||
@@ -358,6 +359,77 @@ When it is `true`, those sliders allow values up to `150%`.
|
||||
|
||||
---
|
||||
|
||||
## Idle
|
||||
|
||||
Idle behavior is defined as named entries under `[idle.behavior.*]`.
|
||||
|
||||
When no `config.toml` exists, Noctalia uses this built-in default:
|
||||
|
||||
```toml
|
||||
[idle.behavior.lock]
|
||||
timeout = 660
|
||||
command = "noctalia:lock"
|
||||
enabled = false
|
||||
```
|
||||
|
||||
```toml
|
||||
[idle.behavior.lock]
|
||||
timeout = 16
|
||||
command = "noctalia:lock"
|
||||
|
||||
[idle.behavior.screen-off]
|
||||
timeout = 32
|
||||
command = "noctalia:dpms-off"
|
||||
|
||||
[idle.behavior.custom]
|
||||
timeout = 48
|
||||
command = "noctalia-msg lock"
|
||||
```
|
||||
|
||||
Available fields:
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|----------------|--------|---------|-------------|
|
||||
| `enabled` | bool | `true` | Enable or disable this behavior entry |
|
||||
| `timeout` | int | `0` | Timeout in seconds before the behavior triggers |
|
||||
| `command` | string | `""` | Command to execute when the idle timeout is reached |
|
||||
|
||||
`command` can be either:
|
||||
|
||||
- a regular shell command such as `notify-send 'Idle' 'Locking soon'`
|
||||
- a Noctalia IPC command using the `noctalia:` prefix
|
||||
|
||||
When you use the `noctalia:` prefix, the rest of the string is executed through the same IPC command registry as `noctalia-ipc`.
|
||||
That means all existing Noctalia IPC commands are available inside idle behaviors, not just a special idle-only subset.
|
||||
|
||||
Examples:
|
||||
|
||||
- `noctalia:lock`
|
||||
- `noctalia:dpms-off`
|
||||
- `noctalia:dpms-on`
|
||||
- `noctalia:enable-idle-inhibitor`
|
||||
- `noctalia:disable-idle-inhibitor`
|
||||
- `noctalia:toggle-idle-inhibitor`
|
||||
- `noctalia:toggle-launcher`
|
||||
- `noctalia:toggle-session-menu`
|
||||
|
||||
Idle behavior uses the Wayland `ext_idle_notifier_v1` protocol, so it reacts to compositor idle notifications instead of polling. The standard idle notification path respects active idle inhibitors.
|
||||
|
||||
Examples:
|
||||
|
||||
```toml
|
||||
[idle.behavior.notify]
|
||||
timeout = 300
|
||||
command = "notify-send 'Noctalia' 'You have been idle for 5 minutes'"
|
||||
|
||||
[idle.behavior.lock]
|
||||
timeout = 660
|
||||
command = "noctalia:lock"
|
||||
enabled = false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shell
|
||||
|
||||
Shell-wide UI settings for non-bar surfaces.
|
||||
|
||||
@@ -18,6 +18,7 @@ A lightweight Wayland shell and bar with no Qt or GTK dependency.
|
||||
| Multi-monitor | `zxdg-output-unstable-v1` |
|
||||
| Active window metadata | `zwlr-foreign-toplevel-management-unstable-v1` |
|
||||
| Lockscreen | `ext-session-lock-v1` |
|
||||
| Idle detection | `ext-idle-notify-v1` |
|
||||
| Cursor | `wp-cursor-shape-v1` |
|
||||
| Rendering | `EGL`, `OpenGL ES 3`, `wayland-egl` |
|
||||
| Text | `freetype`, `harfbuzz`, `msdfgen` (vendored), `fontconfig` |
|
||||
@@ -163,6 +164,22 @@ gdbus call --session --dest dev.noctalia.Debug --object-path /dev/noctalia/Debug
|
||||
gdbus call --session --dest dev.noctalia.Debug --object-path /dev/noctalia/Debug --method dev.noctalia.Debug.EmitInternalNotification "Noctalia" "Test" "Hello from debug" 5000 1
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Noctalia reads `$XDG_CONFIG_HOME/noctalia/config.toml` or `~/.config/noctalia/config.toml`.
|
||||
If no config file exists, it falls back to built-in defaults in code, including this disabled idle behavior:
|
||||
|
||||
```toml
|
||||
[idle.behavior.lock]
|
||||
timeout = 660
|
||||
command = "noctalia:lock"
|
||||
enabled = false
|
||||
```
|
||||
|
||||
Idle behaviors use a single `command` field.
|
||||
That can be either a normal shell command such as `notify-send 'Idle' 'Locking soon'`, or a Noctalia IPC command prefixed with `noctalia:` such as `noctalia:lock` or `noctalia:toggle-launcher`.
|
||||
See [CONFIG.md](/mnt/storage/GitHub/noctalia-dev/Nextalia/CONFIG.md) for the full configuration reference.
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Hardware and networking
|
||||
@@ -224,7 +241,7 @@ gdbus call --session --dest dev.noctalia.Debug --object-path /dev/noctalia/Debug
|
||||
- [ ] Lock keys (Caps/Num)
|
||||
- [ ] Dark mode button
|
||||
- [ ] Night light button
|
||||
- [ ] Keep awake (idle inhibitor)
|
||||
- [x] Keep awake (idle inhibitor)
|
||||
- [ ] Audio visualizer
|
||||
- [x] Launcher button
|
||||
- [x] Session menu button
|
||||
|
||||
@@ -95,6 +95,13 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool launchShellCommand(const std::string& command) {
|
||||
if (command.empty()) {
|
||||
return false;
|
||||
}
|
||||
return launchDetachedCommand({"/bin/sh", "-lc", command.c_str()});
|
||||
}
|
||||
|
||||
bool launchFirstAvailableCommand(std::initializer_list<std::initializer_list<const char*>> commandVariants) {
|
||||
for (const auto& variant : commandVariants) {
|
||||
if (variant.size() == 0) {
|
||||
@@ -223,6 +230,10 @@ void Application::initServices() {
|
||||
|
||||
m_idleInhibitor.initialize(m_wayland, &m_renderContext);
|
||||
m_idleInhibitor.setChangeCallback([this]() { m_bar.refresh(); });
|
||||
m_idleManager.initialize(m_wayland);
|
||||
m_idleManager.setCommandRunner([this](const std::string& command) { return runIdleCommand(command); });
|
||||
m_idleManager.reload(m_configService.config().idle);
|
||||
m_configService.addReloadCallback([this]() { m_idleManager.reload(m_configService.config().idle); });
|
||||
|
||||
m_wallpaper.initialize(m_wayland, &m_configService, &m_stateService);
|
||||
m_overview.initialize(m_wayland, &m_configService, &m_stateService, &m_wallpaper);
|
||||
@@ -609,6 +620,25 @@ void Application::initIpc() {
|
||||
"toggle-idle-inhibitor", "Toggle the compositor idle inhibitor");
|
||||
}
|
||||
|
||||
bool Application::runIdleCommand(const std::string& command) {
|
||||
constexpr std::string_view prefix = "noctalia:";
|
||||
|
||||
if (command.rfind(prefix, 0) == 0) {
|
||||
const std::string response = m_ipcService.execute(command.substr(prefix.size()));
|
||||
if (response.rfind("error:", 0) == 0) {
|
||||
kLog.warn("idle IPC command '{}' failed: {}", command, response.substr(0, response.find('\n')));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!launchShellCommand(command)) {
|
||||
kLog.warn("idle command failed to launch: {}", command);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<PollSource*> Application::buildPollSources() {
|
||||
std::vector<PollSource*> sources;
|
||||
if (m_bus != nullptr) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "dbus/upower/upower_service.h"
|
||||
#include "debug/debug_service.h"
|
||||
#include "idle/idle_inhibitor.h"
|
||||
#include "idle/idle_manager.h"
|
||||
#include "ipc/ipc_poll_source.h"
|
||||
#include "ipc/ipc_service.h"
|
||||
#include "net/http_client.h"
|
||||
@@ -67,6 +68,7 @@ private:
|
||||
void initServices();
|
||||
void initUi();
|
||||
void initIpc();
|
||||
bool runIdleCommand(const std::string& command);
|
||||
[[nodiscard]] std::vector<PollSource*> buildPollSources();
|
||||
|
||||
WaylandConnection m_wayland;
|
||||
@@ -80,6 +82,7 @@ private:
|
||||
std::unique_ptr<SystemMonitorService> m_systemMonitor;
|
||||
std::unique_ptr<DebugService> m_debugService;
|
||||
IdleInhibitor m_idleInhibitor;
|
||||
IdleManager m_idleManager;
|
||||
std::unique_ptr<MprisService> m_mprisService;
|
||||
std::unique_ptr<PowerProfilesService> m_powerProfilesService;
|
||||
std::unique_ptr<UPowerService> m_upowerService;
|
||||
|
||||
@@ -67,6 +67,12 @@ ConfigService::ConfigService() {
|
||||
} else {
|
||||
kLog.info("no config file found, using defaults");
|
||||
seedBuiltinWidgets(m_config);
|
||||
m_config.idle.behaviors.push_back(IdleBehaviorConfig{
|
||||
.name = "lock",
|
||||
.enabled = false,
|
||||
.timeoutSeconds = 660,
|
||||
.command = "noctalia:lock",
|
||||
});
|
||||
m_config.bars.push_back(BarConfig{});
|
||||
}
|
||||
setupWatch();
|
||||
@@ -100,6 +106,12 @@ void ConfigService::forceReload() {
|
||||
loadFromFile(m_configPath);
|
||||
} else {
|
||||
seedBuiltinWidgets(m_config);
|
||||
m_config.idle.behaviors.push_back(IdleBehaviorConfig{
|
||||
.name = "lock",
|
||||
.enabled = false,
|
||||
.timeoutSeconds = 660,
|
||||
.command = "noctalia:lock",
|
||||
});
|
||||
m_config.bars.push_back(BarConfig{});
|
||||
}
|
||||
for (const auto& cb : m_reloadCallbacks) {
|
||||
@@ -143,6 +155,12 @@ void ConfigService::checkReload() {
|
||||
loadFromFile(m_configPath);
|
||||
} else {
|
||||
seedBuiltinWidgets(m_config);
|
||||
m_config.idle.behaviors.push_back(IdleBehaviorConfig{
|
||||
.name = "lock",
|
||||
.enabled = false,
|
||||
.timeoutSeconds = 660,
|
||||
.command = "noctalia:lock",
|
||||
});
|
||||
m_config.bars.push_back(BarConfig{});
|
||||
}
|
||||
|
||||
@@ -527,12 +545,40 @@ void ConfigService::loadFromFile(const std::string& path) {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse [idle.behavior.*]
|
||||
if (auto* idleTbl = tbl["idle"].as_table()) {
|
||||
if (auto* behaviorTbl = (*idleTbl)["behavior"].as_table()) {
|
||||
for (const auto& [name, node] : *behaviorTbl) {
|
||||
auto* entryTbl = node.as_table();
|
||||
if (entryTbl == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IdleBehaviorConfig behavior;
|
||||
behavior.name = std::string(name.str());
|
||||
|
||||
if (auto v = (*entryTbl)["enabled"].value<bool>()) {
|
||||
behavior.enabled = *v;
|
||||
}
|
||||
if (auto v = (*entryTbl)["timeout"].value<int64_t>()) {
|
||||
behavior.timeoutSeconds = static_cast<std::int32_t>(*v);
|
||||
}
|
||||
if (auto v = (*entryTbl)["command"].value<std::string>()) {
|
||||
behavior.command = *v;
|
||||
}
|
||||
|
||||
m_config.idle.behaviors.push_back(std::move(behavior));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_config.bars.empty()) {
|
||||
kLog.info("no [bar.*] defined, using defaults");
|
||||
m_config.bars.push_back(BarConfig{});
|
||||
}
|
||||
|
||||
kLog.info("{} bar(s) defined", m_config.bars.size());
|
||||
kLog.info("idle behaviors={}", m_config.idle.behaviors.size());
|
||||
}
|
||||
|
||||
// ── WidgetConfig accessors ───────────────────────────────────────────────────
|
||||
|
||||
@@ -123,6 +123,17 @@ struct AudioConfig {
|
||||
bool enableOverdrive = false;
|
||||
};
|
||||
|
||||
struct IdleBehaviorConfig {
|
||||
std::string name;
|
||||
bool enabled = true;
|
||||
std::int32_t timeoutSeconds = 0;
|
||||
std::string command;
|
||||
};
|
||||
|
||||
struct IdleConfig {
|
||||
std::vector<IdleBehaviorConfig> behaviors;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
std::vector<BarConfig> bars;
|
||||
std::unordered_map<std::string, WidgetConfig> widgets;
|
||||
@@ -132,6 +143,7 @@ struct Config {
|
||||
OsdConfig osd;
|
||||
WeatherConfig weather;
|
||||
AudioConfig audio;
|
||||
IdleConfig idle;
|
||||
};
|
||||
|
||||
class ConfigService {
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#include "idle/idle_manager.h"
|
||||
|
||||
#include "core/log.h"
|
||||
#include "wayland/wayland_connection.h"
|
||||
|
||||
#include "ext-idle-notify-v1-client-protocol.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("idle");
|
||||
|
||||
const ext_idle_notification_v1_listener kIdleNotificationListener = {
|
||||
.idled = &IdleManager::handleIdled,
|
||||
.resumed = &IdleManager::handleResumed,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
IdleManager::IdleManager() = default;
|
||||
|
||||
IdleManager::~IdleManager() { clearBehaviors(); }
|
||||
|
||||
bool IdleManager::initialize(WaylandConnection& wayland) {
|
||||
m_wayland = &wayland;
|
||||
m_notifier = m_wayland->idleNotifier();
|
||||
if (m_notifier == nullptr) {
|
||||
kLog.info("idle notify protocol unavailable");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void IdleManager::setCommandRunner(CommandRunner runner) { m_commandRunner = std::move(runner); }
|
||||
|
||||
void IdleManager::reload(const IdleConfig& config) {
|
||||
clearBehaviors();
|
||||
|
||||
if (m_notifier == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (m_wayland == nullptr || m_wayland->seat() == nullptr) {
|
||||
kLog.warn("cannot register idle behaviors without a Wayland seat");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& behavior : config.behaviors) {
|
||||
createBehavior(behavior);
|
||||
}
|
||||
}
|
||||
|
||||
void IdleManager::clearBehaviors() {
|
||||
for (auto& behavior : m_behaviors) {
|
||||
if (behavior->notification != nullptr) {
|
||||
ext_idle_notification_v1_destroy(behavior->notification);
|
||||
behavior->notification = nullptr;
|
||||
}
|
||||
}
|
||||
m_behaviors.clear();
|
||||
}
|
||||
|
||||
void IdleManager::createBehavior(const IdleBehaviorConfig& config) {
|
||||
if (!config.enabled) {
|
||||
return;
|
||||
}
|
||||
if (config.timeoutSeconds < 0) {
|
||||
kLog.warn("idle behavior '{}' ignored: timeout must be >= 0 seconds", config.name);
|
||||
return;
|
||||
}
|
||||
if (config.command.empty()) {
|
||||
kLog.warn("idle behavior '{}' ignored: needs a command", config.name);
|
||||
return;
|
||||
}
|
||||
|
||||
auto behavior = std::make_unique<BehaviorState>();
|
||||
behavior->owner = this;
|
||||
behavior->config = config;
|
||||
const auto timeoutMs = static_cast<std::uint32_t>(config.timeoutSeconds) * 1000u;
|
||||
behavior->notification = ext_idle_notifier_v1_get_idle_notification(m_notifier, timeoutMs, m_wayland->seat());
|
||||
if (behavior->notification == nullptr) {
|
||||
kLog.warn("failed to register idle behavior '{}'", config.name);
|
||||
return;
|
||||
}
|
||||
|
||||
ext_idle_notification_v1_add_listener(behavior->notification, &kIdleNotificationListener, behavior.get());
|
||||
kLog.info("registered idle behavior '{}' timeout={}s", config.name, config.timeoutSeconds);
|
||||
m_behaviors.push_back(std::move(behavior));
|
||||
}
|
||||
|
||||
void IdleManager::runBehavior(BehaviorState& behavior) {
|
||||
const auto& config = behavior.config;
|
||||
if (!runCommand(config.command)) {
|
||||
kLog.warn("idle behavior '{}' command failed", config.name);
|
||||
}
|
||||
}
|
||||
|
||||
bool IdleManager::runCommand(const std::string& command) const {
|
||||
if (command.empty()) {
|
||||
return true;
|
||||
}
|
||||
if (!m_commandRunner) {
|
||||
return false;
|
||||
}
|
||||
return m_commandRunner(command);
|
||||
}
|
||||
|
||||
void IdleManager::handleIdled(void* data, ext_idle_notification_v1* /*notification*/) {
|
||||
auto* behavior = static_cast<BehaviorState*>(data);
|
||||
if (behavior == nullptr || behavior->owner == nullptr || behavior->idled) {
|
||||
return;
|
||||
}
|
||||
|
||||
behavior->idled = true;
|
||||
kLog.info("idle behavior '{}' triggered", behavior->config.name);
|
||||
behavior->owner->runBehavior(*behavior);
|
||||
}
|
||||
|
||||
void IdleManager::handleResumed(void* data, ext_idle_notification_v1* /*notification*/) {
|
||||
auto* behavior = static_cast<BehaviorState*>(data);
|
||||
if (behavior == nullptr || !behavior->idled) {
|
||||
return;
|
||||
}
|
||||
|
||||
behavior->idled = false;
|
||||
kLog.info("idle behavior '{}' resumed", behavior->config.name);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "config/config_service.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class WaylandConnection;
|
||||
struct ext_idle_notification_v1;
|
||||
struct ext_idle_notifier_v1;
|
||||
|
||||
class IdleManager {
|
||||
public:
|
||||
using CommandRunner = std::function<bool(const std::string&)>;
|
||||
|
||||
IdleManager();
|
||||
~IdleManager();
|
||||
|
||||
IdleManager(const IdleManager&) = delete;
|
||||
IdleManager& operator=(const IdleManager&) = delete;
|
||||
|
||||
bool initialize(WaylandConnection& wayland);
|
||||
void setCommandRunner(CommandRunner runner);
|
||||
void reload(const IdleConfig& config);
|
||||
static void handleIdled(void* data, ext_idle_notification_v1* notification);
|
||||
static void handleResumed(void* data, ext_idle_notification_v1* notification);
|
||||
|
||||
private:
|
||||
struct BehaviorState {
|
||||
IdleManager* owner = nullptr;
|
||||
IdleBehaviorConfig config;
|
||||
ext_idle_notification_v1* notification = nullptr;
|
||||
bool idled = false;
|
||||
};
|
||||
|
||||
void clearBehaviors();
|
||||
void createBehavior(const IdleBehaviorConfig& config);
|
||||
void runBehavior(BehaviorState& behavior);
|
||||
bool runCommand(const std::string& command) const;
|
||||
|
||||
WaylandConnection* m_wayland = nullptr;
|
||||
ext_idle_notifier_v1* m_notifier = nullptr;
|
||||
CommandRunner m_commandRunner;
|
||||
std::vector<std::unique_ptr<BehaviorState>> m_behaviors;
|
||||
};
|
||||
+28
-28
@@ -90,6 +90,24 @@ void IpcService::dispatch() {
|
||||
}
|
||||
}
|
||||
|
||||
std::string IpcService::execute(const std::string& line) const {
|
||||
std::string command;
|
||||
std::string args;
|
||||
const auto spacePos = line.find(' ');
|
||||
if (spacePos == std::string::npos) {
|
||||
command = line;
|
||||
} else {
|
||||
command = line.substr(0, spacePos);
|
||||
args = line.substr(spacePos + 1);
|
||||
}
|
||||
|
||||
if (command == "--help" || command == "-h") {
|
||||
return buildHelp();
|
||||
}
|
||||
|
||||
return executeParsed(command, args);
|
||||
}
|
||||
|
||||
void IpcService::handleConnection(int connFd) {
|
||||
// Set receive timeout so a slow client doesn't stall the main loop
|
||||
timeval tv{};
|
||||
@@ -122,34 +140,7 @@ void IpcService::handleConnection(int connFd) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Split on first space: command + optional args
|
||||
const std::string line(buf, static_cast<std::size_t>(total));
|
||||
std::string command;
|
||||
std::string args;
|
||||
const auto spacePos = line.find(' ');
|
||||
if (spacePos == std::string::npos) {
|
||||
command = line;
|
||||
} else {
|
||||
command = line.substr(0, spacePos);
|
||||
args = line.substr(spacePos + 1);
|
||||
}
|
||||
|
||||
// Built-in --help
|
||||
if (command == "--help" || command == "-h") {
|
||||
const std::string response = buildHelp();
|
||||
::send(connFd, response.data(), response.size(), MSG_NOSIGNAL);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto it =
|
||||
std::find_if(m_handlers.begin(), m_handlers.end(), [&command](const auto& e) { return e.first == command; });
|
||||
std::string response;
|
||||
if (it == m_handlers.end()) {
|
||||
response = "error: unknown command (try: noctalia msg --help)\n";
|
||||
} else {
|
||||
response = it->second.fn(args);
|
||||
}
|
||||
|
||||
const std::string response = execute(std::string(buf, static_cast<std::size_t>(total)));
|
||||
::send(connFd, response.data(), response.size(), MSG_NOSIGNAL);
|
||||
}
|
||||
|
||||
@@ -175,6 +166,15 @@ std::string IpcService::buildHelp() const {
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string IpcService::executeParsed(const std::string& command, const std::string& args) const {
|
||||
const auto it =
|
||||
std::find_if(m_handlers.begin(), m_handlers.end(), [&command](const auto& e) { return e.first == command; });
|
||||
if (it == m_handlers.end()) {
|
||||
return "error: unknown command (try: noctalia msg --help)\n";
|
||||
}
|
||||
return it->second.fn(args);
|
||||
}
|
||||
|
||||
std::string IpcService::resolveSocketPath() {
|
||||
const char* runtime = std::getenv("XDG_RUNTIME_DIR");
|
||||
if (runtime == nullptr || runtime[0] == '\0') {
|
||||
|
||||
@@ -26,6 +26,9 @@ public:
|
||||
// Called by IpcPollSource when POLLIN fires on the listening fd.
|
||||
void dispatch();
|
||||
|
||||
// Execute a command line using the same handler registry as socket IPC.
|
||||
[[nodiscard]] std::string execute(const std::string& line) const;
|
||||
|
||||
// Register a handler for a command name. The handler receives everything after
|
||||
// the first space as `args`. Must return a string ending with '\n'.
|
||||
// `usage` describes the command signature, e.g. "toggle-panel <id>".
|
||||
@@ -42,6 +45,7 @@ private:
|
||||
|
||||
void handleConnection(int connFd);
|
||||
std::string buildHelp() const;
|
||||
[[nodiscard]] std::string executeParsed(const std::string& command, const std::string& args) const;
|
||||
[[nodiscard]] static std::string resolveSocketPath();
|
||||
|
||||
int m_listenFd = -1;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "cursor-shape-v1-client-protocol.h"
|
||||
#include "dwl-ipc-unstable-v2-client-protocol.h"
|
||||
#include "ext-data-control-v1-client-protocol.h"
|
||||
#include "ext-idle-notify-v1-client-protocol.h"
|
||||
#include "ext-session-lock-v1-client-protocol.h"
|
||||
#include "ext-workspace-v1-client-protocol.h"
|
||||
#include "idle-inhibit-unstable-v1-client-protocol.h"
|
||||
@@ -33,6 +34,7 @@ namespace {
|
||||
constexpr std::uint32_t kCursorShapeManagerVersion = 1;
|
||||
constexpr std::uint32_t kXdgActivationVersion = 1;
|
||||
constexpr std::uint32_t kExtSessionLockManagerVersion = 1;
|
||||
constexpr std::uint32_t kExtIdleNotifierVersion = 1;
|
||||
constexpr std::uint32_t kIdleInhibitManagerVersion = 1;
|
||||
constexpr std::uint32_t kOutputVersion = 4;
|
||||
|
||||
@@ -298,6 +300,7 @@ bool WaylandConnection::hasExtWorkspaceManager() const noexcept { return m_hasEx
|
||||
bool WaylandConnection::hasMangoWorkspaceManager() const noexcept { return m_hasMangoWorkspaceGlobal; }
|
||||
bool WaylandConnection::hasForeignToplevelManager() const noexcept { return m_hasForeignToplevelManagerGlobal; }
|
||||
bool WaylandConnection::hasSessionLockManager() const noexcept { return m_sessionLockManager != nullptr; }
|
||||
bool WaylandConnection::hasIdleNotifier() const noexcept { return m_idleNotifier != nullptr; }
|
||||
bool WaylandConnection::hasIdleInhibitManager() const noexcept { return m_idleInhibitManager != nullptr; }
|
||||
bool WaylandConnection::hasXdgActivation() const noexcept { return m_xdgActivation != nullptr; }
|
||||
|
||||
@@ -336,11 +339,14 @@ wl_display* WaylandConnection::display() const noexcept { return m_display; }
|
||||
|
||||
wl_compositor* WaylandConnection::compositor() const noexcept { return m_compositor; }
|
||||
|
||||
wl_seat* WaylandConnection::seat() const noexcept { return m_seatHandler.seat(); }
|
||||
|
||||
wl_shm* WaylandConnection::shm() const noexcept { return m_shm; }
|
||||
|
||||
zwlr_layer_shell_v1* WaylandConnection::layerShell() const noexcept { return m_layerShell; }
|
||||
|
||||
ext_session_lock_manager_v1* WaylandConnection::sessionLockManager() const noexcept { return m_sessionLockManager; }
|
||||
ext_idle_notifier_v1* WaylandConnection::idleNotifier() const noexcept { return m_idleNotifier; }
|
||||
zwp_idle_inhibit_manager_v1* WaylandConnection::idleInhibitManager() const noexcept { return m_idleInhibitManager; }
|
||||
|
||||
const std::vector<WaylandOutput>& WaylandConnection::outputs() const noexcept { return m_outputs; }
|
||||
@@ -478,6 +484,13 @@ void WaylandConnection::bindGlobal(wl_registry* registry, std::uint32_t name, co
|
||||
return;
|
||||
}
|
||||
|
||||
if (interfaceName == ext_idle_notifier_v1_interface.name) {
|
||||
const auto bindVersion = std::min(version, kExtIdleNotifierVersion);
|
||||
m_idleNotifier = static_cast<ext_idle_notifier_v1*>(
|
||||
wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, bindVersion));
|
||||
return;
|
||||
}
|
||||
|
||||
if (interfaceName == zwp_idle_inhibit_manager_v1_interface.name) {
|
||||
const auto bindVersion = std::min(version, kIdleInhibitManagerVersion);
|
||||
m_idleInhibitManager = static_cast<zwp_idle_inhibit_manager_v1*>(
|
||||
@@ -573,6 +586,10 @@ void WaylandConnection::cleanup() {
|
||||
ext_session_lock_manager_v1_destroy(m_sessionLockManager);
|
||||
m_sessionLockManager = nullptr;
|
||||
}
|
||||
if (m_idleNotifier != nullptr) {
|
||||
ext_idle_notifier_v1_destroy(m_idleNotifier);
|
||||
m_idleNotifier = nullptr;
|
||||
}
|
||||
if (m_idleInhibitManager != nullptr) {
|
||||
zwp_idle_inhibit_manager_v1_destroy(m_idleInhibitManager);
|
||||
m_idleInhibitManager = nullptr;
|
||||
|
||||
@@ -21,6 +21,7 @@ struct zwlr_layer_shell_v1;
|
||||
struct zxdg_output_manager_v1;
|
||||
struct zxdg_output_v1;
|
||||
struct wp_cursor_shape_manager_v1;
|
||||
struct ext_idle_notifier_v1;
|
||||
struct zwp_idle_inhibit_manager_v1;
|
||||
struct xdg_activation_v1;
|
||||
struct ext_session_lock_manager_v1;
|
||||
@@ -86,12 +87,15 @@ public:
|
||||
[[nodiscard]] bool hasMangoWorkspaceManager() const noexcept;
|
||||
[[nodiscard]] bool hasForeignToplevelManager() const noexcept;
|
||||
[[nodiscard]] bool hasSessionLockManager() const noexcept;
|
||||
[[nodiscard]] bool hasIdleNotifier() const noexcept;
|
||||
[[nodiscard]] bool hasIdleInhibitManager() const noexcept;
|
||||
[[nodiscard]] wl_display* display() const noexcept;
|
||||
[[nodiscard]] wl_compositor* compositor() const noexcept;
|
||||
[[nodiscard]] wl_seat* seat() const noexcept;
|
||||
[[nodiscard]] wl_shm* shm() const noexcept;
|
||||
[[nodiscard]] zwlr_layer_shell_v1* layerShell() const noexcept;
|
||||
[[nodiscard]] ext_session_lock_manager_v1* sessionLockManager() const noexcept;
|
||||
[[nodiscard]] ext_idle_notifier_v1* idleNotifier() const noexcept;
|
||||
[[nodiscard]] zwp_idle_inhibit_manager_v1* idleInhibitManager() const noexcept;
|
||||
[[nodiscard]] const std::vector<WaylandOutput>& outputs() const noexcept;
|
||||
[[nodiscard]] WaylandOutput* findOutputByWl(wl_output* wlOutput);
|
||||
@@ -131,6 +135,7 @@ private:
|
||||
wp_cursor_shape_manager_v1* m_cursorShapeManager = nullptr;
|
||||
xdg_activation_v1* m_xdgActivation = nullptr;
|
||||
ext_session_lock_manager_v1* m_sessionLockManager = nullptr;
|
||||
ext_idle_notifier_v1* m_idleNotifier = nullptr;
|
||||
zwp_idle_inhibit_manager_v1* m_idleInhibitManager = nullptr;
|
||||
void* m_dataControlManager = nullptr;
|
||||
const DataControlOps* m_dataControlOps = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user