summaryrefslogtreecommitdiff
path: root/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js')
-rw-r--r--raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js286
1 files changed, 286 insertions, 0 deletions
diff --git a/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js b/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js
new file mode 100644
index 0000000..6c439ab
--- /dev/null
+++ b/raveos-gnome-theme/theme-data/extensions/installed/ShutdownTimer@deminder/dbus-service/timer.js
@@ -0,0 +1,286 @@
+// SPDX-FileCopyrightText: 2023 Deminder <tremminder@gmail.com>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
+import {
+ gettext as _,
+ ngettext as _n,
+ pgettext as C_,
+} from '../modules/translation.js';
+
+import { logDebug, throttleTimeout } from '../modules/util.js';
+import * as Control from './control.js';
+import {
+ longDurationString,
+ ScheduleInfo,
+ getShutdownScheduleFromSettings,
+ getSliderMinutesFromSettings,
+} from '../modules/schedule-info.js';
+import {
+ actionLabel,
+ Action,
+ mapLegacyAction,
+ UnsupportedActionError,
+} from './action.js';
+
+const Signals = imports.signals;
+
+export class Timer {
+ _timerCancellable = null;
+ _updateScheduleCancel = null;
+ _action = new Action();
+ info = {
+ externalShutdown: new ScheduleInfo({
+ external: true,
+ }),
+ externalWake: new ScheduleInfo({
+ external: false,
+ mode: 'wake',
+ }),
+ internalShutdown: new ScheduleInfo({ mode: 'PowerOff' }),
+ };
+
+ constructor({ settings }) {
+ this._settings = settings;
+
+ const [updateScheduleThrottled, updateScheduleCancel] = throttleTimeout(
+ () => this.updateSchedule(),
+ 20
+ );
+ this._updateScheduleCancel = updateScheduleCancel;
+ this._settingsIds = [
+ 'root-mode-value',
+ 'shutdown-timestamp-value',
+ 'shutdown-mode-value',
+ ].map(settingName =>
+ settings.connect(`changed::${settingName}`, updateScheduleThrottled)
+ );
+
+ this.updateSchedule();
+ }
+
+ destroy() {
+ if (this._settings === null) {
+ throw new Error('should not destroy twice');
+ }
+ // Disconnect settings
+ this._settingsIds.forEach(id => this._settings.disconnect(id));
+ this._settingsIds = [];
+ this._settings = null;
+
+ // Cancel schedule updates
+ if (this._updateScheduleCancel !== null) {
+ this._updateScheduleCancel();
+ this._updateScheduleCancel = null;
+ }
+
+ // Cancel internal timer
+ if (this._timerCancellable !== null) {
+ this._timerCancellable.cancel();
+ this._timerCancellable = null;
+ }
+
+ // External schedules (for 'shutdown' and 'wake') are not stopped
+ }
+
+ updateSchedule() {
+ const oldInternal = this.info.internalShutdown;
+ const internal = getShutdownScheduleFromSettings(this._settings);
+ this.info.internalShutdown = internal;
+ logDebug(
+ `[updateSchedule] internal schedule: ${internal.label} (old internal schedule: ${oldInternal.label})`
+ );
+ this._updateRootModeProtection(oldInternal);
+ if (
+ internal.mode !== oldInternal.mode ||
+ internal.deadline !== oldInternal.deadline
+ ) {
+ this.emit('change');
+ if (internal.scheduled) {
+ if (internal.minutes > 0) {
+ // Show schedule info
+ this.emit(
+ 'message',
+ C_('StartSchedulePopup', '%s in %s').format(
+ actionLabel(internal.mode),
+ longDurationString(
+ internal.minutes,
+ h => _n('%s hour', '%s hours', h),
+ m => _n('%s minute', '%s minutes', m)
+ )
+ )
+ );
+ } else {
+ logDebug(`[updateSchedule] hidden message for '< 1 minute' schedule`);
+ }
+ if (this._timerCancellable !== null) {
+ this._timerCancellable.cancel();
+ this._timerCancellable = null;
+ }
+ this.executeActionDelayed()
+ .then(() => {
+ logDebug('[executeActionDelayed] done');
+ })
+ .catch(err => {
+ console.error('executeActionDelayed', err);
+ });
+ } else {
+ if (this._timerCancellable !== null) {
+ this._timerCancellable.cancel();
+ this._timerCancellable = null;
+ }
+ if (oldInternal.scheduled) {
+ this.emit('message', _('Shutdown Timer stopped'));
+ }
+ }
+ }
+ }
+
+ async executeAction() {
+ const internal = this.info.internalShutdown;
+ if (!internal.scheduled) {
+ logDebug(`Refusing to exectute non scheduled action! '${internal.mode}'`);
+ return;
+ }
+ logDebug(`Running '${internal.mode}' timer action...`);
+ try {
+ this.emit('change');
+ // Refresh root mode protection
+ await Promise.all([
+ this._updateRootModeProtection(),
+ // Do shutdown
+ this._action.shutdownAction(
+ internal.mode,
+ this._settings.get_boolean('show-end-session-dialog-value')
+ ),
+ ]);
+ } catch (err) {
+ if (/* destroyed */ this._settings === null) {
+ throw err;
+ }
+ const newInternal = this.info.internalShutdown;
+ if (newInternal.scheduled && newInternal.secondsLeft > 0) {
+ logDebug('[timer] Replaced by new schedule.');
+ return;
+ }
+ if (err instanceof UnsupportedActionError) {
+ this.emit(
+ 'message',
+ _('%s is not supported!').format(actionLabel(internal.mode))
+ );
+ }
+ }
+ await this.toggleShutdown(false, '');
+ }
+
+ async executeActionDelayed() {
+ const internal = this.info.internalShutdown;
+ const secs = internal.secondsLeft;
+ if (secs > 0) {
+ logDebug(`Started delayed action: ${internal.minutes}min remaining`);
+ try {
+ this._timerCancellable = new Gio.Cancellable();
+ await Control.sleepUntilDeadline(
+ internal.deadline,
+ this._timerCancellable
+ );
+ this._timerCancellable = null;
+ } catch {
+ logDebug(`Canceled delayed action: ${internal.minutes}min remaining`);
+ return;
+ }
+ }
+ await this.executeAction();
+ }
+
+ async toggleWake(wake) {
+ try {
+ logDebug('[toggleWake] wake', wake);
+ await this._action.wakeAction(
+ wake,
+ getSliderMinutesFromSettings(this._settings, 'wake')
+ );
+ this.emit('change-external');
+ } catch (err) {
+ this.emit(
+ 'message',
+ C_('Error', '%s\n%s').format(_('Wake action failed!'), err)
+ );
+ this._settings.set_int('shutdown-timestamp-value', -1);
+ }
+ }
+
+ async toggleShutdown(shutdown, legacyAction) {
+ // Update shutdown action
+ const action = mapLegacyAction(legacyAction);
+ if (action === undefined) {
+ this.emit(
+ 'message',
+ _('Unknown shutdown action: "%s"!').format(legacyAction)
+ );
+ return;
+ }
+ logDebug('[toggleShutdown] shutdown', shutdown, 'action', action);
+ if (action !== '') {
+ this._settings.set_string('shutdown-mode-value', action);
+ }
+
+ // Update shutdown timestamp
+ this._settings.set_int(
+ 'shutdown-timestamp-value',
+ shutdown
+ ? GLib.DateTime.new_now_utc().to_unix() +
+ Math.max(
+ 1,
+ getSliderMinutesFromSettings(this._settings, 'shutdown') * 60
+ )
+ : -1
+ );
+ if (shutdown) {
+ await this._action.inhibitSuspend();
+ } else {
+ await this._action.uninhibitSuspend();
+ }
+ if (this._settings.get_boolean('auto-wake-value')) {
+ await this.toggleWake(shutdown);
+ }
+ }
+
+ get state() {
+ return this.info.internalShutdown.scheduled
+ ? this.info.internalShutdown.secondsLeft > 0
+ ? 'active'
+ : 'action'
+ : 'inactive';
+ }
+
+ /**
+ * Ensure that shutdown/reboot is executed even if the Timer fails by running
+ * the `shutdown` command delayed by 1 minute.
+ */
+ async _updateRootModeProtection(oldInternal) {
+ if (this._settings.get_boolean('root-mode-value')) {
+ const internal = this.info.internalShutdown;
+ logDebug('[updateRootModeProtection] mode ', internal.mode);
+ try {
+ if (oldInternal?.scheduled && oldInternal.mode !== internal.mode) {
+ await Control.stopRootModeProtection(oldInternal);
+ }
+ if (internal.scheduled) {
+ await Control.startRootModeProtection(internal);
+ } else {
+ await Control.stopRootModeProtection(internal);
+ }
+ } catch (err) {
+ this.emit(
+ 'message',
+ C_('Error', '%s\n%s').format(_('Root mode protection failed!'), err)
+ );
+ console.error('[updateRootModeProtection]', err);
+ }
+ this.emit('change-external');
+ }
+ }
+}
+Signals.addSignalMethods(Timer.prototype);