/* -*- mode: js2 - indent-tabs-mode: nil - js2-basic-offset: 4 -*- */
/* jshint multistr:true */
/* jshint esnext:true */
/* exported CaffeineExtension */
/**
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
**/
'use strict';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';
import Meta from 'gi://Meta';
import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import { Extension, gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js';
import { MprisPlayer } from './mprisMediaPlayer2.js';
import { PopupAnimation } from 'resource:///org/gnome/shell/ui/boxpointer.js';
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import * as QuickSettings from 'resource:///org/gnome/shell/ui/quickSettings.js';
const QuickSettingsMenu = Main.panel.statusArea.quickSettings;
const ShellVersion = parseFloat(Config.PACKAGE_VERSION);
const INHIBIT_APPS_KEY = 'inhibit-apps';
const SHOW_INDICATOR_KEY = 'show-indicator';
const SHOW_NOTIFICATIONS_KEY = 'show-notifications';
const SHOW_TIMER_KEY = 'show-timer';
const SHOW_TOGGLE_KEY = 'show-toggle';
const DURATION_TIMER_LIST = 'duration-timer-list';
const USER_ENABLED_KEY = 'user-enabled';
const RESTORE_KEY = 'restore-state';
const FULLSCREEN_KEY = 'enable-fullscreen';
const MPRIS_KEY = 'enable-mpris';
const NIGHT_LIGHT_KEY = 'nightlight-control';
const TOGGLE_SHORTCUT = 'toggle-shortcut';
const TIMER_KEY = 'countdown-timer';
const SCREEN_BLANK = 'screen-blank';
const TRIGGER_APPS_MODE = 'trigger-apps-mode';
const INDICATOR_POSITION = 'indicator-position';
const INDICATOR_INDEX = 'indicator-position-index';
const INDICATOR_POS_MAX = 'indicator-position-max';
const CLI_TOGGLE_KEY = 'cli-toggle';
const ColorInterface = ' \
\
\
\
';
const ColorProxy = Gio.DBusProxy.makeProxyWrapper(ColorInterface);
const DBusSessionManagerIface = '\
\
\
\
\
\
\
\
\
\
\
\
\
';
const DBusSessionManagerProxy = Gio.DBusProxy.makeProxyWrapper(DBusSessionManagerIface);
const ActionsPath = '/icons/hicolor/scalable/actions/';
const DisabledIcon = 'my-caffeine-off-symbolic';
const EnabledIcon = 'my-caffeine-on-symbolic';
const TimerMenuIcon = 'stopwatch-symbolic';
const TimerIcons = [
'caffeine-short-timer-symbolic',
'caffeine-medium-timer-symbolic',
'caffeine-long-timer-symbolic',
'caffeine-infinite-timer-symbolic'
];
const ControlContext = {
NEVER: 0,
ALWAYS: 1,
FOR_APPS: 2
};
const ShowIndicator = {
ONLY_ACTIVE: 0,
ALWAYS: 1,
NEVER: 2
};
const AppsTrigger = {
ON_RUNNING: 0,
ON_FOCUS: 1,
ON_ACTIVE_WORKSPACE: 2
};
const InhibitorManager = GObject.registerClass({
Signals: {
'update': {}
}
}, class InhibitorManager extends GObject.Object {
_init(settings) {
super._init();
this._isInhibited = false;
this._inhibitorCookie = null;
this._userEnabled = false;
this._triggerApp = null;
this._tempManageLight = false;
this._lastReasons = [];
this._ignoredReasons = [];
// App trigger signal IDs
this._appStateSignal = null;
this._focusWindowSignal = null;
this._workspaceSignal = null;
this._restackedSignal = null;
// DBus proxies
this._sessionManager = new DBusSessionManagerProxy(Gio.DBus.session,
'org.gnome.SessionManager',
'/org/gnome/SessionManager');
this._colorProxy = new ColorProxy(
Gio.DBus.session,
'org.gnome.SettingsDaemon.Color',
'/org/gnome/SettingsDaemon/Color',
(proxy, error) => {
if (error) {
log(error.message);
}
}
);
this._settings = settings;
this._appSystem = Shell.AppSystem.get_default();
// Update state when extension settings changed
this._settings.connectObject(
`changed::${SCREEN_BLANK}`,
() => this._forceUpdate(),
`changed::${FULLSCREEN_KEY}`,
() => this._updateState(),
`changed::${MPRIS_KEY}`,
() => this._onMprisSettingChange(),
`changed::${NIGHT_LIGHT_KEY}`,
() => this._updateState(),
`changed::${INHIBIT_APPS_KEY}`,
() => this._updateState(),
`changed::${TRIGGER_APPS_MODE}`,
() => {
this._disconnectTriggerSignals();
this._connectTriggerSignals();
this._updateState();
}, this);
// Update state when fullscreened
global.display.connectObject('in-fullscreen-changed', () => this._updateState(), this);
// Init mpris
this._onMprisSettingChange();
// Update when possible app triggers change
this._connectTriggerSignals();
this._updateState();
}
_onMprisSettingChange() {
const enable = this._settings.get_boolean(MPRIS_KEY);
if (enable && !MprisPlayer.isActive) {
MprisPlayer.Get().connectIsPlaying((_isPlaying) => this._updateState());
} else {
MprisPlayer.Destroy();
}
this._updateState();
}
_connectTriggerSignals() {
// Only connect to relevant signals for the selected trigger
switch (this._settings.get_enum(TRIGGER_APPS_MODE)) {
case AppsTrigger.ON_RUNNING:
this._appStateSignal = this._appSystem.connect('app-state-changed',
() => this._updateState());
break;
case AppsTrigger.ON_FOCUS:
this._focusWindowSignal = global.display.connect('notify::focus-window',
() => this._updateState());
break;
case AppsTrigger.ON_ACTIVE_WORKSPACE:
this._appStateSignal = this._appSystem.connect('app-state-changed',
() => this._updateState());
this._workspaceSignal = global.workspace_manager.connect('workspace-switched',
() => this._updateState());
this._restackedSignal = global.display.connect('restacked',
() => this._updateState());
break;
}
}
_disconnectTriggerSignals() {
if (this._appStateSignal !== null) {
this._appSystem.disconnect(this._appStateSignal);
this._appStateSignal = null;
}
if (this._focusWindowSignal !== null) {
global.display.disconnect(this._focusWindowSignal);
this._focusWindowSignal = null;
}
if (this._workspaceSignal !== null) {
global.workspace_manager.disconnect(this._workspaceSignal);
this._workspaceSignal = null;
}
if (this._restackedSignal !== null) {
global.display.disconnect(this._restackedSignal);
this._restackedSignal = null;
}
}
_findRunningApp() {
const possibleTriggerApps = this._settings.get_strv(INHIBIT_APPS_KEY);
const runningApps = this._appSystem.get_running();
for (const app of runningApps) {
const appId = app.get_id();
if (possibleTriggerApps.includes(appId)) {
return appId;
}
}
return null;
}
_findFocusedApp() {
const possibleTriggerApps = this._settings.get_strv(INHIBIT_APPS_KEY);
const focusedApp = Shell.WindowTracker.get_default().focus_app;
if (focusedApp !== null) {
const appId = focusedApp.get_id();
if (possibleTriggerApps.includes(appId)) {
return appId;
}
}
return null;
}
_findActiveApp() {
const possibleTriggerApps = this._settings.get_strv(INHIBIT_APPS_KEY);
const activeWorkspace = global.workspace_manager.get_active_workspace();
for (const appId of possibleTriggerApps) {
const app = this._appSystem.lookup_app(appId);
if (app !== null) {
if (app.is_on_workspace(activeWorkspace)) {
return appId;
}
}
}
return null;
}
_getInhibitReasons() {
const reasons = [];
if (this.isFullscreen() && this._settings.get_boolean(FULLSCREEN_KEY)) {
reasons.push('fullscreen');
}
if (MprisPlayer.isActive && MprisPlayer.Get().isPlaying) {
reasons.push('mpris');
}
if (this._userEnabled) {
reasons.push('user');
}
// Find any selected apps that meet the trigger
let triggerApp = null;
if (this._settings.get_strv(INHIBIT_APPS_KEY).length !== 0) {
switch (this._settings.get_enum(TRIGGER_APPS_MODE)) {
case AppsTrigger.ON_RUNNING:
triggerApp = this._findRunningApp();
break;
case AppsTrigger.ON_FOCUS:
triggerApp = this._findFocusedApp();
break;
case AppsTrigger.ON_ACTIVE_WORKSPACE:
triggerApp = this._findActiveApp();
break;
}
}
// Save any responsible app and update reasons
this._triggerApp = triggerApp;
if (triggerApp !== null) {
reasons.push('app');
}
return reasons;
}
_forceUpdate() {
// Remove any inhibitor, as settings / inhibit flags may have changed
if (this._isInhibited) {
this._removeInhibitor();
}
// Update state, as normal
this._updateState();
}
_updateState() {
// Get the reasons for inhibiting
let reasons = this._getInhibitReasons();
// If any ignored reasons are no longer active, stop ignoring them
this._ignoredReasons = this._ignoredReasons.filter(Set.prototype.has, new Set(reasons));
// Ignore any reasons in the ignore list, then save them
reasons = reasons.filter((n) => !this._ignoredReasons.includes(n));
this._lastReasons = [...reasons];
const shouldInhibit = reasons.length !== 0;
/* If no trigger app is running, and the light is in app-only mode,
and we left it blocked, unblock it
*/
if (!reasons.includes('app')) {
if (this._settings.get_enum(NIGHT_LIGHT_KEY) === ControlContext.FOR_APPS) {
if (this._colorProxy.DisabledUntilTomorrow === true) {
this._tempManageLight = true;
}
}
}
// Update inhibitor if required
if (this._isInhibited !== shouldInhibit) {
if (shouldInhibit) {
this._addInhibitor(reasons);
} else {
this._removeInhibitor();
}
}
// Update night light if required
if (this.isNightLightManaged()) {
// If this._tempManageLight is true we're actually disabling
if (shouldInhibit && !this._tempManageLight) {
// Block night light
this._colorProxy.DisabledUntilTomorrow = true;
} else {
// Allow night light
this._colorProxy.DisabledUntilTomorrow = false;
}
}
// Let indicator know that either the state or reasons may have changed
this.emit('update');
// Remove any night light management override now the signal is done
this._tempManageLight = false;
}
_addInhibitor(reasons) {
// Decide whether to allow screen blanking
let allowBlank = this._settings.get_enum(SCREEN_BLANK) === ControlContext.ALWAYS;
if (reasons.includes('app')) {
allowBlank = this._settings.get_enum(SCREEN_BLANK) > ControlContext.NEVER;
}
let inhibitFlags;
if (allowBlank) {
// Inhibit suspending the session or computer
inhibitFlags = 4;
} else {
// Inhibit the session being marked as idle
inhibitFlags = 8;
}
// Pack the parameters for DBus
const params = [
GLib.Variant.new_string('caffeine-gnome-extension'),
GLib.Variant.new_uint32(0),
GLib.Variant.new_string('Inhibit by %s'.format(this._name)),
GLib.Variant.new_uint32(inhibitFlags)
];
const paramsVariant = GLib.Variant.new_tuple(params);
// Synchronously add the inhibitor
const cookieTuple = this._sessionManager.call_sync('Inhibit', paramsVariant,
Gio.DBusCallFlags.NONE, -1, null);
if (cookieTuple !== null) {
this._inhibitorCookie = cookieTuple.get_child_value(0).get_uint32();
this._isInhibited = true;
} else {
log('Failed to add inhibitor');
}
}
_removeInhibitor() {
// Remove the inhibitor if it's active
if (this._isInhibited) {
// Use the cookie to remove the inhibitor
this._sessionManager.UninhibitRemote(this._inhibitorCookie);
this._inhibitorCookie = null;
this._isInhibited = false;
}
}
isFullscreen() {
const monitorCount = global.display.get_n_monitors();
for (let i = 0; i < monitorCount; i++) {
if (global.display.get_monitor_in_fullscreen(i)) {
return true;
}
}
return false;
}
isNightLightManaged() {
// Don't bother checking settings if it's overridden
if (this._tempManageLight) {
return true;
}
// Decide if we should control the night light from user preference
let handleNightLight = this._settings.get_enum(NIGHT_LIGHT_KEY) === ControlContext.ALWAYS;
if (this._lastReasons.includes('app')) {
handleNightLight = this._settings.get_enum(NIGHT_LIGHT_KEY) > ControlContext.NEVER;
}
return handleNightLight;
}
getInhibitState() {
return this._isInhibited;
}
getInhibitApp() {
return this._triggerApp;
}
setUserEnabled(enabled) {
this._userEnabled = enabled;
if (!enabled) {
this._ignoredReasons = this._getInhibitReasons();
}
this._updateState();
}
destroy() {
this._disconnectTriggerSignals();
global.display.disconnectObject(this);
this._settings.disconnectObject(this);
if (this._isInhibited) {
this._removeInhibitor();
}
}
});
const CaffeineToggle = GObject.registerClass({
Signals: {
'timer-clicked': {}
}
}, class CaffeineToggle extends QuickSettings.QuickMenuToggle {
_init(Me) {
super._init({
'title': _('Caffeine'),
toggleMode: false
});
this._settings = Me._settings;
this._path = Me.path;
// Icons
this.finalTimerMenuIcon = TimerMenuIcon;
this._iconActivated = Gio.ThemedIcon.new(EnabledIcon);
this._iconDeactivated = Gio.ThemedIcon.new(DisabledIcon);
this._iconTheme = new St.IconTheme();
if (!this._iconTheme.has_icon(TimerMenuIcon)) {
this.finalTimerMenuIcon =
Gio.icon_new_for_string(`${this._path}${ActionsPath}${TimerMenuIcon}.svg`);
}
if (!this._iconTheme.has_icon(EnabledIcon)) {
this._iconActivated = Gio.icon_new_for_string(`${this._path}${ActionsPath}${EnabledIcon}.svg`);
}
if (!this._iconTheme.has_icon(DisabledIcon)) {
this._iconDeactivated = Gio.icon_new_for_string(`${this._path}${ActionsPath}${DisabledIcon}.svg`);
}
this.updateIcon();
// Set up entry
this.menu.setHeader(this.finalTimerMenuIcon, _('Caffeine Timer'), null);
// Add elements
this._itemsSection = new PopupMenu.PopupMenuSection();
this.menu.addMenuItem(this._itemsSection);
// Add an entry-point for more settings
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
const settingsItem = this.menu.addAction(_('Settings'), () => Me._openPreferences());
// Ensure the settings are unavailable when the screen is locked
settingsItem.visible = Main.sessionMode.allowSettings;
this.menu._settingsActions[Me.uuid] = settingsItem;
// Init Timers
this._timerItems = new Map();
this._syncTimers(false);
// Bind signals
this._settings.connectObject(
`changed::${TIMER_KEY}`,
() => this._sync(),
`changed::${DURATION_TIMER_LIST}`,
() => this._syncTimers(true),
`changed::${SHOW_TOGGLE_KEY}`,
() => {
this.visible = this._settings.get_boolean(SHOW_TOGGLE_KEY);
},
this);
this.connect('destroy', () => {
this._iconActivated = null;
this._iconDeactivated = null;
this.gicon = null;
});
// Set menu visibility
this.visible = this._settings.get_boolean(SHOW_TOGGLE_KEY);
}
_syncTimers(resetDefault) {
this._itemsSection.removeAll();
this._timerItems.clear();
// Get duration list and add '0' for the 'infinite' entry (no timer)
const durationValues = this._settings.get_value(DURATION_TIMER_LIST).deepUnpack();
durationValues.push(0);
// Create menu timer
for (const [index, timer] of durationValues.entries()) {
let label = null;
if (timer === 0) {
label = _('Infinite');
} else {
const hours = Math.floor(timer / 3600);
const minutes = Math.floor((timer % 3600) / 60);
switch (hours) {
case 0:
break;
case 1:
label = hours + _(' hour ');
break;
default:
label = hours + _(' hours ');
break;
}
switch (minutes) {
case 0:
break;
case 1:
label = label + minutes + _(' minute');
break;
default:
label = label + minutes + _(' minutes');
break;
}
}
if (!label) {
continue;
}
let icon = Gio.ThemedIcon.new(TimerIcons[index]);
if (!this._iconTheme.has_icon(TimerIcons[index])) {
icon = Gio.icon_new_for_string(`${this._path}${ActionsPath}${TimerIcons[index]}.svg`);
}
const item = new PopupMenu.PopupImageMenuItem(label, icon);
item.connectObject('activate', () => this._checkTimer(timer), this);
this._timerItems.set(timer, item);
this._itemsSection.addMenuItem(item);
}
// Select active duration
if (resetDefault && this._settings.get_int(TIMER_KEY) !== 0) {
// Set default duration to 0
this._settings.set_int(TIMER_KEY, 0);
} else {
this._sync();
}
}
_sync() {
const activeTimerId = this._settings.get_int(TIMER_KEY);
for (const [timerId, item] of this._timerItems) {
item.setOrnament(timerId === activeTimerId
? PopupMenu.Ornament.CHECK
: PopupMenu.Ornament.NONE);
}
}
_checkTimer(timerId) {
this._settings.set_int(TIMER_KEY, timerId);
this.emit('timer-clicked');
}
updateIcon() {
if (this.checked) {
this.gicon = this._iconActivated;
} else {
this.gicon = this._iconDeactivated;
}
}
});
const Caffeine = GObject.registerClass(
class Caffeine extends QuickSettings.SystemIndicator {
_init(Me) {
super._init();
this._appSystem = Shell.AppSystem.get_default();
this._indicator = this._addIndicator();
this._settings = Me._settings;
this._name = Me.metadata.name;
this._state = false;
// Add indicator label for the timer
this._timerLabel = new St.Label({
y_expand: true,
y_align: Clutter.ActorAlign.CENTER
});
this._timerLabel.visible = false;
this.add_child(this._timerLabel);
// Icons
this._iconActivated = Gio.ThemedIcon.new(EnabledIcon);
this._iconDeactivated = Gio.ThemedIcon.new(DisabledIcon);
this._iconTheme = new St.IconTheme();
if (!this._iconTheme.has_icon(EnabledIcon)) {
this._iconActivated = Gio.icon_new_for_string(`${Me.path}${ActionsPath}${EnabledIcon}.svg`);
}
if (!this._iconTheme.has_icon(DisabledIcon)) {
this._iconDeactivated = Gio.icon_new_for_string(`${Me.path}${ActionsPath}${DisabledIcon}.svg`);
}
this._indicator.gicon = this._iconDeactivated;
// Init Timers
this._timeOut = null;
this._timePrint = null;
this._timerEnable = false;
// Show icon
this._manageShowIndicator();
// Quick Settings
this._caffeineToggle = new CaffeineToggle(Me);
this.quickSettingsItems.push(this._caffeineToggle);
this._updateTimerSubtitle();
this._caffeineToggle.connectObject('clicked', () => this._handleToggleClick(), this);
this._caffeineToggle.connectObject('timer-clicked', () => this._forceToggleClick(), this);
// Bind settings signals
this._settings.connectObject(
`changed::${TIMER_KEY}`,
() => this._startTimer(),
`changed::${SHOW_TIMER_KEY}`,
() => this._showIndicatorLabel(),
`changed::${INDICATOR_POSITION}`,
() => this._updateIndicatorPosition(),
`changed::${SHOW_INDICATOR_KEY}`,
() => {
this._manageShowIndicator();
this._showIndicatorLabel();
},
this);
if (ShellVersion >= 46) {
QuickSettingsMenu._indicators.connectObject(
'child-added', () => this._updateMaxPosition(),
'child-removed', () => this._updateMaxPosition(),
this);
} else {
QuickSettingsMenu._indicators.connectObject(
'actor-added', () => this._updateMaxPosition(),
'actor-removed', () => this._updateMaxPosition(),
this);
}
// Change user state on icon scroll event
this._indicator.reactive = true;
this._indicator.connectObject('scroll-event',
(actor, event) => this._handleScrollEvent(event), this);
// Init position and index of indicator icon
this.indicatorPosition = this._settings.get_int(INDICATOR_POSITION);
this.indicatorIndex = this._settings.get_int(INDICATOR_INDEX);
this.lastIndicatorPosition = this.indicatorPosition;
// Add indicator and toggle
QuickSettingsMenu.addExternalIndicator(this);
if (ShellVersion >= 46) {
QuickSettingsMenu._indicators.remove_child(this);
} else {
QuickSettingsMenu._indicators.remove_actor(this);
}
QuickSettingsMenu._indicators.insert_child_at_index(this, this.indicatorIndex);
// Setup inhibitor manager
this._inhibitorManager = new InhibitorManager(this._settings);
this._inhibitorManager.connectObject('update', () => this._inhibitorUpdated(), this);
// Set manager user state and restore user state, if required
if (this._settings.get_boolean(USER_ENABLED_KEY) &&
this._settings.get_boolean(RESTORE_KEY)) {
// Restore Caffeine as enabled (fake a user click)
this._forceToggleClick();
}
// Connect command line toggle
this._settings.set_boolean(CLI_TOGGLE_KEY, this._state);
this._settings.connectObject(
`changed::${CLI_TOGGLE_KEY}`,
() => this._commandStateChanged(),
this);
}
_commandStateChanged() {
const commandState = this._settings.get_boolean(CLI_TOGGLE_KEY);
if (commandState === this._state) {
return;
}
this._handleToggleClick();
}
_forceToggleClick() {
this._state = false;
this._handleToggleClick();
}
_handleToggleClick() {
// Pass the new user state to the inhibitor manager, causes state to invert
this._inhibitorManager.setUserEnabled(!this._state);
this._settings.set_boolean(USER_ENABLED_KEY, this._state);
if (this._state) {
// Enable timer when toggled on and duration is set
if (this._settings.get_int(TIMER_KEY) !== 0 && !this._timerEnable) {
this._startTimer();
}
} else {
// Enable timer when toggled off
this._removeTimer();
}
this._updateTimerSubtitle();
this._updateMaxPosition();
}
_incrementIndicatorPosIndex() {
if (this.lastIndicatorPosition < this.indicatorPosition) {
this.indicatorIndex += 1;
} else {
this.indicatorIndex -= 1;
}
}
_updateMaxPosition() {
let pos = -1;
const indicators = QuickSettingsMenu._indicators.get_children();
// Count visible items in the status area
indicators.forEach((indicator) => {
if (indicator.is_visible()) {
pos += 1;
}
});
this._settings.set_int(INDICATOR_POS_MAX, pos);
}
_updateIndicatorPosition() {
const newPosition = this._settings.get_int(INDICATOR_POSITION);
if (this.indicatorPosition !== newPosition) {
this.indicatorPosition = newPosition;
this._incrementIndicatorPosIndex();
// Skip invisible indicator
let targetIndicator =
QuickSettingsMenu._indicators.get_child_at_index(this.indicatorIndex);
const maxIndex = QuickSettingsMenu._indicators.get_n_children();
while (this.indicatorIndex < maxIndex && !targetIndicator.is_visible() &&
this.indicatorIndex > -1) {
this._incrementIndicatorPosIndex();
targetIndicator =
QuickSettingsMenu._indicators.get_child_at_index(this.indicatorIndex);
}
// Always reset index to 0 on position 0
if (this.indicatorPosition === 0) {
this.indicatorIndex = 0;
}
// Update last position
this.lastIndicatorPosition = newPosition;
// Update indicator index
if (ShellVersion >= 46) {
QuickSettingsMenu._indicators.remove_child(this);
} else {
QuickSettingsMenu._indicators.remove_actor(this);
}
QuickSettingsMenu._indicators.insert_child_at_index(this, this.indicatorIndex);
this._settings.set_int(INDICATOR_INDEX, this.indicatorIndex);
}
this._updateMaxPosition();
}
_showIndicatorLabel() {
if (this._settings.get_boolean(SHOW_TIMER_KEY) &&
(this._settings.get_enum(SHOW_INDICATOR_KEY) !== ShowIndicator.NEVER) &&
this._timerEnable) {
this._timerLabel.visible = true;
} else {
this._timerLabel.visible = false;
}
}
_startTimer() {
// Reset timer
this._removeTimer();
this._timerEnable = true;
// Get duration
const timerDelay = this._settings.get_int(TIMER_KEY);
// Execute Timer only if duration isn't set on infinite time
if (timerDelay !== 0) {
let secondLeft = timerDelay;
this._showIndicatorLabel();
this._printTimer(secondLeft);
this._timePrint = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => {
secondLeft -= 1;
this._printTimer(secondLeft);
return GLib.SOURCE_CONTINUE;
});
this._timeOut = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timerDelay * 1000, () => {
// Disable Caffeine when timer ended
this._removeTimer();
if (this._state) {
this._handleToggleClick();
}
return GLib.SOURCE_REMOVE;
});
}
}
_printTimer(seconds) {
const hours = Math.floor(seconds / 3600).toString().padStart(2, '0');
const min = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0');
const sec = Math.floor(seconds % 60).toString().padStart(2, '0');
// Print Timer in system Indicator and Toggle menu subLabel
if (hours !== '00') {
this._updateLabelTimer(hours + ':' + min + ':' + sec);
} else {
this._updateLabelTimer(min + ':' + sec);
}
}
_removeTimer() {
// End timer
this._timerEnable = false;
// Flush and hide timer label
this._updateLabelTimer(null);
this._timerLabel.visible = false;
// Remove timer
if ((this._timeOut !== null) || (this._timePrint !== null)) {
GLib.Source.remove(this._timeOut);
GLib.Source.remove(this._timePrint);
this._timeOut = null;
this._timePrint = null;
}
}
_updateLabelTimer(text) {
this._timerLabel.text = text;
this._caffeineToggle.menu.setHeader(this._caffeineToggle.finalTimerMenuIcon,
_('Caffeine Timer'), text);
this._caffeineToggle.subtitle = text;
}
_handleScrollEvent(event) {
switch (event.get_scroll_direction()) {
case Clutter.ScrollDirection.UP:
if (!this._state) {
// User state on - UP
this._handleToggleClick();
}
break;
case Clutter.ScrollDirection.DOWN:
if (this._state) {
// Stop timer
this._removeTimer();
// User state off - DOWN
this._handleToggleClick();
}
break;
}
}
_inhibitorUpdated() {
// Update the tracked state
const oldState = this._state;
this._state = this._inhibitorManager.getInhibitState();
// Sync command state
this._settings.set_boolean(CLI_TOGGLE_KEY, this._state);
// Update the visual state and subtitle
this._caffeineToggle.checked = this._state;
this._caffeineToggle.updateIcon();
this._updateAppSubtitle(this._inhibitorManager.getInhibitApp());
// Send an OSD notification, if enabled and state changed
if (this._state !== oldState) {
if (this._settings.get_boolean(SHOW_NOTIFICATIONS_KEY) &&
!this._inhibitorManager.isFullscreen()) {
this._sendOSDNotification(this._state);
}
}
// Update indicator, icon and subtitle
this._manageShowIndicator();
this._updateTimerSubtitle();
}
_manageShowIndicator() {
if (this._state) {
this._indicator.visible = this._settings.get_enum(SHOW_INDICATOR_KEY) !== ShowIndicator.NEVER;
this._indicator.gicon = this._iconActivated;
} else {
this._indicator.visible = this._settings.get_enum(SHOW_INDICATOR_KEY) === ShowIndicator.ALWAYS;
this._indicator.gicon = this._iconDeactivated;
}
}
_sendOSDNotification(state) {
let message = _('Caffeine enabled');
let icon = this._iconActivated;
if (!state) {
message = _('Caffeine disabled');
icon = this._iconDeactivated;
}
if (this._inhibitorManager.isNightLightManaged()) {
if (state) {
message = message + '. ' + _('Night Light paused');
} else {
message = message + '. ' + _('Night Light resumed');
}
}
if (ShellVersion >= 49) {
Main.osdWindowManager.showAll(icon, message, null, null);
} else {
Main.osdWindowManager.show(-1, icon, message, null, null);
}
}
// Add the name of the app as subtitle
_updateAppSubtitle(appId) {
if (appId === null) {
this._caffeineToggle.subtitle = null;
return;
}
const app = this._appSystem.lookup_app(appId);
if (app === null) {
this._caffeineToggle.subtitle = null;
return;
}
this._caffeineToggle.subtitle = app.get_name();
}
// Add the timer duration selected as subtitle
_updateTimerSubtitle() {
if (!this._state) {
const timerDuration = this._settings.get_int(TIMER_KEY);
const hours = Math.floor(timerDuration / 3600);
const min = Math.floor((timerDuration % 3600) / 60);
let timeLabel = '';
switch (hours) {
case 0:
break;
case 1:
timeLabel = hours + _(' hour ');
break;
default:
timeLabel = hours + _(' hours ');
break;
}
switch (min) {
case 0:
break;
case 1:
timeLabel += min + _(' minute ');
break;
default:
timeLabel += min + _(' minutes ');
break;
}
this._caffeineToggle.subtitle = timerDuration !== 0
? timeLabel
: null;
}
}
destroy() {
// Remove ToggleMenu
this.quickSettingsItems.forEach((item) => item.destroy());
// Disconnect from signals
if (this._timeOut) {
GLib.Source.remove(this._timeOut);
this._timeOut = null;
}
if (this._timePrint) {
GLib.Source.remove(this._timePrint);
this._timePrint = null;
}
MprisPlayer.Destroy();
this._inhibitorManager.destroy();
this._inhibitorManager = null;
this._settings = null;
super.destroy();
}
});
export default class CaffeineExtension extends Extension {
enable() {
this._settings = this.getSettings();
this._caffeineIndicator = new Caffeine(this);
// Register shortcut
Main.wm.addKeybinding(TOGGLE_SHORTCUT, this._settings,
Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
Shell.ActionMode.ALL, () => {
this._caffeineIndicator._handleToggleClick();
});
}
disable() {
this._caffeineIndicator.destroy();
this._caffeineIndicator = null;
// Unregister shortcut
Main.wm.removeKeybinding(TOGGLE_SHORTCUT);
}
_openPreferences() {
this.openPreferences();
QuickSettingsMenu.menu.close(PopupAnimation.FADE);
}
}