Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions data/io.elementary.settings-daemon.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@
</key>
</schema>

<schema path="/io/elementary/settings-daemon/focus-modes/" id="io.elementary.settings-daemon.focus-modes">
<key type="a(ssbba{sv}a{sv})" name="focus-modes">
<default>[]</default>
<summary>The current focus modes.</summary>
<description>a(a{sv}a{sv})</description>
</key>
</schema>

<schema path="/io/elementary/settings-daemon/system-update/" id="io.elementary.settings-daemon.system-update">
<key type="b" name="automatic-updates">
<default>false</default>
Expand Down
5 changes: 5 additions & 0 deletions src/Application.vala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public sealed class SettingsDaemon.Application : Gtk.Application {
private Backends.PowerProfilesSync power_profiles_sync;
private Backends.ApplicationShortcuts application_shortcuts;

private Backends.FocusModes.Manager focus_modes_manager;

private const string FDO_ACCOUNTS_NAME = "org.freedesktop.Accounts";
private const string FDO_ACCOUNTS_PATH = "/org/freedesktop/Accounts";

Expand All @@ -40,6 +42,8 @@ public sealed class SettingsDaemon.Application : Gtk.Application {
GLib.Intl.textdomain (Build.GETTEXT_PACKAGE);

add_main_option ("version", 'v', NONE, NONE, "Display the version", null);

focus_modes_manager = new Backends.FocusModes.Manager ();
}

protected override int handle_local_options (VariantDict options) {
Expand Down Expand Up @@ -80,6 +84,7 @@ public sealed class SettingsDaemon.Application : Gtk.Application {
protected override bool dbus_register (DBusConnection connection, string object_path) throws Error {
base.dbus_register (connection, object_path);

connection.register_object (object_path, focus_modes_manager);
connection.register_object (object_path, new Backends.SystemUpdate ());

#if UBUNTU_DRIVERS
Expand Down
37 changes: 37 additions & 0 deletions src/Backends/FocusModes/GLibSetting.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

public class SettingsDaemon.Backends.FocusModes.GLibSetting : Object, Setting {
public Settings settings { get; construct; }
public string key { get; construct; }
public Variant active_value { get; construct; }

private int applied = 0;
private Variant? last_value = null;

public GLibSetting (string schema_id, string key, Variant active_value) {
Object (settings: new Settings (schema_id), key: key, active_value: active_value);
}

public void apply (Variant value) {
applied++;

if (applied == 1) {
last_value = settings.get_value (key);
settings.set_value (key, active_value);
}
}

public void unapply () {
applied--;

assert (applied >= 0);

if (applied == 0 && settings.get_value (key).equal (active_value) && last_value != null) {
settings.set_value (key, last_value);
}
}
}
87 changes: 87 additions & 0 deletions src/Backends/FocusModes/Manager.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

[DBus (name="io.elementary.settings_daemon.ModeManager")]
public class SettingsDaemon.Backends.FocusModes.Manager : GLib.Object {
private static Settings settings = new Settings ("io.elementary.settings-daemon.focus-modes");

public signal void items_changed (uint pos, uint removed, uint added);
public signal void properties_changed (uint pos);

private HashTable<string, Mode> modes = new HashTable<string, Mode> (str_hash, str_equal);
private ListStore modes_list;

construct {
modes_list = new ListStore (typeof (Mode));
modes_list.items_changed.connect ((pos, rem, add) => items_changed (pos, rem, add));

foreach (var parsed_mode in (Mode.Parsed[]) settings.get_value ("focus-modes")) {
add_mode (parsed_mode);
}
}

public uint get_n_modes () throws DBusError, IOError {
return modes_list.n_items;
}

public Mode.Parsed get_mode (uint pos) throws DBusError, IOError {
return ((Mode) modes_list.get_item (pos)).parsed;
}

public void update_mode (Mode.Parsed parsed) throws DBusError, IOError {
if (parsed.id in modes) {
modes[parsed.id].parsed = parsed;
} else {
add_mode (parsed);
}

save_modes ();
}

private void add_mode (Mode.Parsed parsed) {
var mode = new Mode (parsed);

mode.notify.connect (on_mode_notify);

modes[mode.id] = mode;
modes_list.append (mode);
}

private void on_mode_notify (Object obj, ParamSpec pspec) requires (obj is Mode) {
var mode = (Mode) obj;

uint pos;
if (modes_list.find (mode, out pos)) {
properties_changed (pos);
} else {
warning ("Unknown mode notified");
}
}

public void delete_mode (string id) throws DBusError, IOError {
if (!(id in modes)) {
throw new IOError.NOT_FOUND ("Mode with the same name not found");
}

uint pos;
if (modes_list.find (modes[id], out pos)) {
modes_list.remove (pos);
}

modes.remove (id);

save_modes ();
}

private void save_modes () {
Mode.Parsed[] parsed_modes = {};
foreach (var mode in modes.get_values ()) {
parsed_modes += mode.parsed;
}

settings.set_value ("focus-modes", parsed_modes);
}
}
141 changes: 141 additions & 0 deletions src/Backends/FocusModes/Mode.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

public class SettingsDaemon.Backends.FocusModes.Mode : Object {
public enum Type {
MANUAL,
DAYLIGHT
}

public struct Parsed {
string id;
string name;
bool enabled;
bool active;
HashTable<string, Variant> schedule;
HashTable<string, Variant> settings;
}

private const string DARK_MODE = "dark-mode";
private const string DND = "dnd";
private const string MONOCHROME = "monochrome";

private static HashTable<string, Setting> setting_handlers;

static construct {
setting_handlers = new HashTable<string, Setting> (str_hash, str_equal);

setting_handlers[DARK_MODE] = new GLibSetting ("io.elementary.settings-daemon.prefers-color-scheme", "color-scheme", "prefer-dark");
setting_handlers[DND] = new GLibSetting ("io.elementary.notifications", "do-not-disturb", true);
setting_handlers[MONOCHROME] = new GLibSetting ("io.elementary.desktop.wm.accessibility", "enable-monochrome-filter", true);
}

private Parsed _parsed;
public Parsed parsed {
get { return _parsed; }
set {
if (is_active) {
// If we're currently active unapply the old settings and reapply the new ones
unapply_settings ();
_parsed = value;
apply_settings ();
} else {
_parsed = value;
}

check_triggers ();
}
}

public string id { get { return parsed.id; } }
public string name { get { return parsed.name; } }
public bool enabled { get { return parsed.enabled; } }
public bool active { get { return parsed.active; } set { _parsed.active = value; } }
public HashTable<string, Variant> schedule { get { return parsed.schedule; } }
public HashTable<string, Variant> settings { get { return parsed.settings; } }

private TimeTracker time_tracker;
private bool is_active = false;
private bool user_override = false;

public Mode (Parsed parsed) {
Object (parsed: parsed);
}

construct {
time_tracker = new TimeTracker ();
Timeout.add_seconds (1, () => {
check_triggers ();
return Source.CONTINUE;
});
}

private void check_triggers () {
var is_in = false;

if ("daylight" in schedule) {
is_in = time_tracker.is_in_time_window_daylight ();
} else if ("manual" in schedule && schedule["manual"].n_children () == 2) {
var from_time = schedule["manual"].get_child_value (0).get_double ();
var to_time = schedule["manual"].get_child_value (1).get_double ();
is_in = time_tracker.is_in_time_window_manual (from_time, to_time);
}

if (active != is_active) {
// The user toggled the mode manually
user_override = true;
}

if (user_override && is_in == active) {
// The schedule agrees with the user again so we disable the override and
// use the schedule as the source of truth again
user_override = false;
}

var should_activate = user_override ? active : is_in;

if (should_activate && !is_active) {
if (!active) {
active = true;
}

apply_settings ();
} else if (!should_activate && is_active) {
if (active) {
active = false;
}

unapply_settings ();
}

assert (user_override ^ active == is_in);
assert (active == is_active);
}

private void apply_settings () requires (!is_active) {
is_active = true;

foreach (var key in settings.get_keys ()) {
if (key in setting_handlers) {
setting_handlers[key].apply (settings[key]);
} else {
warning ("Tried to apply unknown setting: %s", key);
}
}
}

private void unapply_settings () requires (is_active) {
is_active = false;

foreach (var key in settings.get_keys ()) {
if (key in setting_handlers) {
setting_handlers[key].unapply ();
} else {
warning ("Tried to unapply unknown setting: %s", key);
}
}
}
}
10 changes: 10 additions & 0 deletions src/Backends/FocusModes/Setting.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

public interface SettingsDaemon.Backends.FocusModes.Setting : Object {
public abstract void apply (Variant value);
public abstract void unapply ();
}
71 changes: 71 additions & 0 deletions src/Backends/FocusModes/TimeTracker.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

[SingleInstance]
public class SettingsDaemon.Backends.FocusModes.TimeTracker : Object {
private GClue.Simple simple;

private double sunrise = 6.0;
private double sunset = 20.0;

construct {
get_location.begin ();
}

private async void get_location () {
try {
simple = yield new GClue.Simple (Build.PROJECT_NAME, GClue.AccuracyLevel.CITY, null);

simple.notify["location"].connect (() => {
on_location_updated (simple.location.latitude, simple.location.longitude);
});

on_location_updated (simple.location.latitude, simple.location.longitude);
} catch (Error e) {
warning ("Failed to connect to GeoClue2 service: %s", e.message);
return;
}
}

private void on_location_updated (double latitude, double longitude) {
var now = new DateTime.now_local ();
double _sunrise, _sunset;
if (SettingsDaemon.Utils.SunriseSunsetCalculator.get_sunrise_and_sunset (now, latitude, longitude, out _sunrise, out _sunset)) {
sunrise = _sunrise;
sunset = _sunset;
}
}

public bool is_in_time_window_daylight () {
var date_time = new DateTime.now_local ();
double time_double = 0;
time_double += date_time.get_hour ();
time_double += (double) date_time.get_minute () / 60;

// PM to AM
if (sunset > sunrise) {
return time_double < sunrise ? time_double <= sunset : time_double >= sunset;
}

// AM to AM, PM to PM, AM to PM
return (time_double >= sunset && time_double <= sunrise);
}

public bool is_in_time_window_manual (double from, double to) {
var date_time = new DateTime.now_local ();
double time_double = 0;
time_double += date_time.get_hour ();
time_double += (double) date_time.get_minute () / 60;

// PM to AM
if (from > to) {
return time_double < to ? time_double <= from : time_double >= from;
}

// AM to AM, PM to PM, AM to PM
return (time_double >= from && time_double <= to);
}
}
Loading