pwr_tray 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pwr_tray/main.py ADDED
@@ -0,0 +1,974 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ This code is designed to control power matters from the tray.
5
+ """
6
+ # pylint: disable=invalid-name,wrong-import-position,missing-function-docstring
7
+ # pylint: disable=broad-except,too-many-instance-attributes
8
+ # pylint: disable=global-statement,consider-using-with,too-many-lines
9
+ # pylint: disable=too-many-statements,too-few-public-methods
10
+ # pylint: disable=too-many-branches,too-many-public-methods
11
+ # pylint: disable=consider-using-from-import
12
+
13
+ import os
14
+ import sys
15
+ needed = '/usr/lib/python3/dist-packages'
16
+ if needed not in sys.path:
17
+ sys.path.append(needed) # pick up external dependencies
18
+ import signal
19
+ import re
20
+ import subprocess
21
+ import json
22
+ import shutil
23
+ import atexit
24
+ import time
25
+ import traceback
26
+ from types import SimpleNamespace
27
+ import psutil
28
+ from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QAction #, QMessageBox
29
+ from PyQt5.QtGui import QIcon, QCursor
30
+ from PyQt5.QtCore import QTimer
31
+
32
+ import pwr_tray.Utils as Utils
33
+ from pwr_tray.Utils import prt, PyKill
34
+ from pwr_tray.SwayIdleMgr import SwayIdleManager
35
+ from pwr_tray.IniTool import IniTool
36
+
37
+ class PwrTray:
38
+ """ pwr-tray main class.
39
+ NOTES:
40
+ - when icons are moved/edited, rename them or reboot to avoid cache confusion
41
+ """
42
+ svg_info = SimpleNamespace(version='03', subdir='resources/SetD'
43
+ , bases= ['SettingSun', # Normal (SleepAfterLock)
44
+ 'FullSun', # Presentation Mode
45
+ 'Unlocked', # LockOnly Mode
46
+ 'GoingDown', # LowBattery Mode
47
+ 'PlayingNow', # inhibited by a/v player
48
+ 'RisingMoon', # Normal and Locking Soon
49
+ 'UnlockedMoon', # LockOnly and Locking Soon
50
+ 'StopSign', # systemd inhibited
51
+ ] )
52
+ singleton = None
53
+ @staticmethod
54
+ def get_environment():
55
+ desktop_session = os.environ.get('DESKTOP_SESSION', '').lower()
56
+ xdg_session_desktop = os.environ.get('XDG_SESSION_DESKTOP', '').lower()
57
+ xdg_current_desktop = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
58
+ sway_socket = os.environ.get('SWAYSOCK')
59
+ wayland_display = os.environ.get('WAYLAND_DISPLAY')
60
+ is_wayland = bool(wayland_display)
61
+ display = os.environ.get('DISPLAY')
62
+
63
+ if 'i3' in desktop_session and display: # Check for i3
64
+ prt(f'ENV: i3 {desktop_session=} {display=}')
65
+ return 'i3', is_wayland
66
+ if ('sway' in desktop_session or 'sway' in xdg_session_desktop
67
+ or 'sway' in xdg_current_desktop) and sway_socket: # Check for Sway
68
+ prt(f'ENV: sway {desktop_session=} {sway_socket=}')
69
+ return 'sway', is_wayland
70
+ if 'plasma' in desktop_session or 'kde' in xdg_current_desktop:
71
+ if is_wayland:
72
+ env = 'kde-wayland'
73
+ prt(f'ENV: {env} {desktop_session=} {wayland_display=}')
74
+ assert False, f'unsupported env: {env}'
75
+ return 'kde-wayland', is_wayland
76
+ if display:
77
+ env = 'kde-x11'
78
+ prt(f'ENV: {env} {desktop_session=} {display=}')
79
+ return env, is_wayland
80
+ if 'gnome' in desktop_session:
81
+ if is_wayland:
82
+ env='gnome-wayland'
83
+ prt(f'ENV: {env} {desktop_session=} {wayland_display=}')
84
+ assert False, f'unsupported env: {env}'
85
+ return env, is_wayland
86
+ if display:
87
+ env='gnome-x11'
88
+ prt(f'ENV: {env} {desktop_session=} {display=}')
89
+ assert False, f'unsupported env: {env}'
90
+ return 'gnome-x11', is_wayland
91
+ # Default case: no known environment detected
92
+ assert False, 'cannot determine if i3/sway/(kde|gnome)-(x11|wayland)'
93
+
94
+ default_variables = {
95
+ 'suspend': 'systemctl suspend',
96
+ 'poweroff': 'systemctl poweroff',
97
+ 'reboot': 'systemctl reboot',
98
+ # 'dimmer': 'brightnessctl set {percent}%',
99
+ # 'undim': 'brightnessctl set 100%',
100
+ 'logoff': '',
101
+ 'monitors_off': '',
102
+ 'locker': '',
103
+ 'get_idle_ms': '',
104
+ 'reset_idle': '',
105
+ 'reload_wm': '',
106
+ 'restart_wm': '',
107
+ # 'must_haves': 'systemctl brightnessctl'.split(),
108
+ 'must_haves': 'systemctl'.split(),
109
+
110
+ }
111
+ overrides = {
112
+ 'x11': {
113
+ 'reset_idle': 'xset s reset',
114
+ 'get_idle_ms': 'xprintidle',
115
+ 'monitors_off': 'sleep 1.0; exec xset dpms force off',
116
+ 'must_haves': 'xset xprintidle'.split(),
117
+ }, 'sway': {
118
+ # swayidle timeout 300 'swaylock' resume 'swaymsg "exec kill -USR1 $(pgrep swayidle)"' &
119
+ # kill -USR1 $(pgrep swayidle)
120
+ 'reload_wm': 'swaymsg reload',
121
+ 'logoff': 'swaymsg exit',
122
+ 'locker': 'swaylock --ignore-empty-password --show-failed-attempt',
123
+ # 'monitors_off': """sleep 1.0; swaymsg 'output * dpms off'""",
124
+ 'must_haves': 'swaymsg i3lock'.split(),
125
+
126
+ }, 'i3': {
127
+ 'reload_wm': 'i3-msg reload',
128
+ 'restart_wm': 'i3-msg restart',
129
+ 'logoff': 'i3-msg exit',
130
+ 'locker': 'pkill i3lock; sleep 0.5; i3lock --ignore-empty-password --show-failed-attempt',
131
+ 'must_haves': 'i3-msg i3lock'.split(),
132
+
133
+ }, 'kde-x11': {
134
+ 'locker': 'loginctl lock-session',
135
+ # 'logoff': 'loginctl terminate-session {XDG_SESSION_ID}',
136
+ 'logoff': 'qdbus org.kde.ksmserver /KSMServer org.kde.KSMServerInterface.logout 0 0 0',
137
+ 'restart_wm': 'killall plasmashell && kstart5 plasmashell && sleep 3 && pwr-tray',
138
+ 'must_haves': 'loginctl qdbus'.split(),
139
+ }, 'kde-wayland': {
140
+ # sudo apt-get install xdg-utils
141
+ 'reset_idle': 'qdbus org.freedesktop.ScreenSaver /ScreenSaver SimulateUserActivity',
142
+ # gdbus introspect --session --dest org.gnome.SessionManager
143
+ # --object-path /org/gnome/SessionManager
144
+ # gdbus call --session --dest org.gnome.SessionManager
145
+ # --object-path /org/gnome/SessionManager
146
+ # --method org.gnome.SessionManager.GetIdleTime
147
+ # from pydbus import SessionBus
148
+ # import time
149
+ # def get_idle_time():
150
+ # bus = SessionBus()
151
+ # screensaver = bus.get("org.freedesktop.ScreenSaver")
152
+ # # The GetSessionIdleTime method returns the idle time in seconds.
153
+ # idle_time = screensaver.GetSessionIdleTime()
154
+ # return idle_time
155
+ # if __name__ == "__main__":
156
+ # while True:
157
+ # idle_time = get_idle_time()
158
+ # print(f"Idle time in seconds: {idle_time}")
159
+ # time.sleep(5)
160
+ 'locker': 'loginctl lock-session',
161
+ 'must_haves': 'loginctl qdbus'.split(),
162
+
163
+ }, 'gnome-x11': {
164
+
165
+ }, 'gnome-wayland': {
166
+ # sudo apt-get install xdg-utils
167
+ 'reset_idle': ('gdbus call --session --dest org.gnome.ScreenSaver --object-path'
168
+ ' /org/gnome/ScreenSaver --method org.gnome.ScreenSaver.SimulateUserActivity'),
169
+ # - idle time:
170
+ # pip install pydbus
171
+ # from pydbus import SessionBus
172
+ # bus = SessionBus()
173
+ # screensaver = bus.get("org.gnome.Mutter.IdleMonitor",
174
+ # "/org/gnome/Mutter/IdleMonitor/Core")
175
+ # idle_time = screensaver.GetIdletime()
176
+
177
+ # print(f"Idle time in milliseconds: {idle_time}")
178
+ 'locker': 'loginctl lock-session',
179
+ 'must_haves': 'qdbus gnome-screensaver-command'.split(),
180
+
181
+ }
182
+ }
183
+
184
+ def __init__(self, ini_tool, quick=False):
185
+ PwrTray.singleton = self
186
+ self.app = QApplication([])
187
+ self.app.setQuitOnLastWindowClosed(False)
188
+ while not QSystemTrayIcon.isSystemTrayAvailable():
189
+ prt("System tray is not available. Retry in 1 second...")
190
+ time.sleep(1.0)
191
+ prt("System tray is available. Continuing...")
192
+
193
+ self.ini_tool = ini_tool
194
+ self.battery = SimpleNamespace(present=None,
195
+ plugged=True, percent=100, selector='Settings')
196
+ self.reconfig()
197
+ self.quick = quick
198
+
199
+ self.loop = 0
200
+ self.loop_sample = (1 if quick else 15)
201
+
202
+ ## self.singleton.presentation_mode = False
203
+ self.singleton.mode = 'SleepAfterLock' # or 'LockOnly' or 'Presentation'
204
+ self.was_effective_mode = None
205
+ self.was_inhibited = None
206
+ self.was_play_state = ''
207
+ self.was_selector = None
208
+ self.was_output = ''
209
+ self.here_dir = os.path.dirname(os.path.abspath(__file__))
210
+ self.svgs = []
211
+ self.icons = []
212
+ for base in self.svg_info.bases:
213
+ self.svgs.append(f'{base}-v{self.svg_info.version}.svg')
214
+ for resource in self.svgs + ['lockpaper.png']:
215
+ if not os.path.isfile(resource):
216
+ Utils.copy_to_folder(resource, ini_tool.folder)
217
+ if not os.path.isfile(resource):
218
+ prt(f'WARN: cannot find {repr(resource)}')
219
+ continue
220
+ self.icons.append(QIcon(os.path.join(self.ini_tool.folder, resource)))
221
+
222
+ # states are Awake, Locked, Blanked, Asleep
223
+ # when is idle time
224
+ self.tray_icon = QSystemTrayIcon(self.icons[0], self.app)
225
+ self.tray_icon.setToolTip("pwr-tray")
226
+ self.tray_icon.setVisible(True)
227
+ self.state = SimpleNamespace(name='Awake', when=0)
228
+
229
+ self.running_idle_s = 0.000
230
+ self.poll_s = 2.000
231
+ self.poll_100ms = False
232
+ self.lock_began_secs = None # TBD: remove
233
+ self.inh_lock_began_secs = False # TBD: refactor?
234
+ self.rebuild_menu = False
235
+ self.picks_file = ini_tool.picks_path
236
+ self.current_icon_num = -1 # triggers immediate icon update
237
+ self.enable_playerctl = True
238
+
239
+ self.restore_picks()
240
+ if quick:
241
+ for selector in self.ini_tool.get_selectors():
242
+ params = self.ini_tool.params_by_selector[selector]
243
+ params.lock_min_list = [1, 2, 4, 8, 32, 128]
244
+ params.sleep_min_list = [1, 2, 4, 8, 32, 128]
245
+
246
+ # self.down_state = self.opts.down_state
247
+
248
+ self.graphical, self.is_wayland = self.get_environment()
249
+ self.variables = self.default_variables
250
+ must_haves = self.default_variables['must_haves']
251
+ if self.graphical in ('i3', 'kde-x11'):
252
+ self.variables.update(self.overrides['x11'])
253
+ must_haves += self.default_variables['must_haves']
254
+
255
+ self.variables.update(self.overrides[self.graphical])
256
+ must_haves += self.variables['must_haves']
257
+
258
+ dont_haves = []
259
+ for must_have in set(must_haves):
260
+ if shutil.which(must_have) is None:
261
+ dont_haves.append(must_have)
262
+ assert not dont_haves, f'commands NOT on $PATH: {dont_haves}'
263
+ self.has_playerctl = bool(shutil.which('playerctl'))
264
+
265
+
266
+ self.idle_manager = SwayIdleManager(self) if self.graphical == 'sway' else None
267
+
268
+ self.menu_items = []
269
+ self.menu = None
270
+ self.build_menu()
271
+ self.tray_icon.activated.connect(self.on_tray_icon_activated)
272
+ if self.idle_manager:
273
+ self.idle_manager_start()
274
+ self.timer = QTimer()
275
+ self.timer.setInterval(100) # 100 is initial ... gets recomputed
276
+ self.timer.timeout.connect(self.on_timeout)
277
+ self.timer.start()
278
+
279
+ def get_params(self, selector=None):
280
+ selector = self.battery.selector if selector is None else selector
281
+ return self.ini_tool.params_by_selector[selector]
282
+
283
+ def get_lock_min_list(self, selector=None):
284
+ """TBD"""
285
+ selector = self.battery.selector if selector is None else selector
286
+ return self.ini_tool.get_current_vals(selector, 'lock_min_list')
287
+
288
+ def get_lock_rotated_list(self, selector=None, first=None):
289
+ """TBD"""
290
+ selector = self.battery.selector if selector is None else selector
291
+ return self.ini_tool.get_rotated_vals(selector, 'lock_min_list', first)
292
+
293
+ def get_sleep_min_list(self, selector=None):
294
+ """TBD"""
295
+ selector = self.battery.selector if selector is None else selector
296
+ return self.ini_tool.get_current_vals(selector, 'sleep_min_list')
297
+
298
+ def get_sleep_rotated_list(self, selector=None, first=None):
299
+ """TBD"""
300
+ selector = self.battery.selector if selector is None else selector
301
+ return self.ini_tool.get_rotated_vals(selector, 'sleep_min_list', first)
302
+
303
+ def get_effective_mode(self):
304
+ """TBD"""
305
+ return 'SleepAfterLock' if self.battery.selector == 'LoBattery' else self.mode
306
+
307
+ def reconfig(self):
308
+ """ update/fix config """
309
+ if self.ini_tool.update_config():
310
+ self.rebuild_menu = True
311
+
312
+ @staticmethod
313
+ def save_picks():
314
+ this = PwrTray.singleton
315
+ if this and not this.quick:
316
+ picks = { 'mode': this.mode,
317
+ 'enable_playerctl': this.enable_playerctl,
318
+ 'lock_mins': {
319
+ 'Settings': this.get_lock_min_list('Settings')[0],
320
+ 'HiBattery': this.get_lock_min_list('HiBattery')[0],
321
+ 'LoBattery': this.get_lock_min_list('LoBattery')[0],
322
+
323
+ }, 'sleep_mins': {
324
+ 'Settings': this.get_sleep_min_list('Settings')[0],
325
+ 'HiBattery': this.get_sleep_min_list('HiBattery')[0],
326
+ 'LoBattery': this.get_sleep_min_list('LoBattery')[0],
327
+ },
328
+ }
329
+ try:
330
+ picks_str = json.dumps(picks)
331
+ with open(this.picks_file, 'w', encoding='utf-8') as f:
332
+ f.write(picks_str + '\n')
333
+ print("Picks saved:", picks_str)
334
+ this.poll_100ms = True
335
+ except Exception as e:
336
+ print(f"An error occurred while saving picks: {e}", file=sys.stderr)
337
+
338
+
339
+ def restore_picks(self):
340
+ try:
341
+ with open(self.picks_file, 'r', encoding='utf-8') as handle:
342
+ picks = json.load(handle)
343
+ self.mode = picks.get('mode', 'SleepAfterLock')
344
+ self.enable_playerctl = picks.get('enable_playerctl', True)
345
+ for attr in 'lock_mins sleep_mins'.split():
346
+ for selector in 'LoBattery HiBattery Settings'.split():
347
+ self.get_lock_rotated_list(selector, first=picks[attr][selector])
348
+ prt('restored Picks OK:', picks)
349
+ return True
350
+
351
+ except Exception as e:
352
+ prt(f'restored picks FAILED: {e}')
353
+ prt(f'mode=self.mode lock_mins={self.get_lock_min_list()}'
354
+ f' sleep_mins={self.get_sleep_min_list()}')
355
+ return True
356
+
357
+
358
+
359
+ def _get_down_state(self):
360
+ return 'PowerDown' if self.get_params().power_down else 'Suspend'
361
+
362
+ def update_running_idle_s(self):
363
+ """ Update the running idle seconds (called after each regular timeout) """
364
+ cmd = self.variables['get_idle_ms']
365
+ if cmd:
366
+ xidle_ms = int(subprocess.check_output(cmd.split()).strip())
367
+ xidle_ms *= 2 if self.quick else 1 # time warp
368
+ self.running_idle_s = round(xidle_ms/1000, 3)
369
+
370
+ def DB(self):
371
+ """ is debug on? """
372
+ rv = self.get_params().debug_mode
373
+ return rv
374
+
375
+ def effective_mode(self):
376
+ """ TBD """
377
+ return 'SleepAfterLock' if self.battery.selector == 'LoBattery' else self.mode
378
+
379
+ def show_icon(self, inhibited=''):
380
+ """ Display Icon if updated """
381
+ emode = self.get_effective_mode()
382
+ num = (3 if self.battery.selector == 'LoBattery'
383
+ else 1 if emode in ('Presentation', )
384
+ else 7 if inhibited == 'systemd'
385
+ else 4 if inhibited == 'player'
386
+ else 0 if emode in ('SleepAfterLock',)
387
+ else 2)
388
+ lock_secs = self.get_lock_min_list()[0]*60
389
+ # down_secs = self.get_sleep_min_list()[0]*60 + lock_secs
390
+ moon_when = lock_secs - min(60 , lock_secs/8)
391
+ if num == 0 and self.running_idle_s >= moon_when:
392
+ num = 5
393
+ elif num == 2 and self.running_idle_s >= moon_when:
394
+ num = 6
395
+ # prt(f'{num=} {self.running_idle_s=} {moon_when=}')
396
+
397
+ if num != self.current_icon_num:
398
+ self.tray_icon.setIcon(self.icons[num])
399
+ self.current_icon_num = num
400
+ return True # changed
401
+ return False # unchanged
402
+
403
+ def check_inhibited(self):
404
+ pipe = subprocess.Popen(
405
+ # ['systemd-inhibit', '--no-legend', '--no-pager', '--mode=block'],
406
+ ['systemd-inhibit', '--no-pager', '--mode=block'],
407
+ stdout=subprocess.PIPE)
408
+ output, _ = pipe.communicate()
409
+ output = output.decode('utf-8')
410
+ lines = output.splitlines()
411
+ if self.DB() and 'No inhibitors' not in output:
412
+ prt('DB', 'systemd-inhibit:', output.strip())
413
+ rows = []
414
+ inhibited = ''
415
+ for count, line in enumerate(lines):
416
+ if count == 0:
417
+ rows.append(line.strip())
418
+ continue
419
+ if 'block' not in line:
420
+ continue
421
+ if count == 1:
422
+ if ('xfce4-power-man' not in line
423
+ and 'org_kde_powerde' not in line):
424
+ inhibited = 'systemd'
425
+ rows.append(line)
426
+ else:
427
+ inhibited = 'systemd'
428
+ rows.append(line)
429
+ if len(rows) == 1:
430
+ rows = []
431
+ if self.has_playerctl and self.enable_playerctl:
432
+ child = subprocess.run('playerctl status'.split(), check=False,
433
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
434
+ play_state = child.stdout.decode('utf-8').strip().lower()
435
+ if play_state == 'playing':
436
+ inhibited = 'player'
437
+ if self.was_play_state != play_state:
438
+ prt(f'{play_state=}')
439
+ self.was_play_state = play_state
440
+
441
+ emode = self.effective_mode()
442
+
443
+ if self.show_icon(inhibited=inhibited):
444
+ self.poll_100ms = True
445
+ self.idle_manager_start()
446
+
447
+ self.was_effective_mode = emode
448
+ self.was_selector = self.battery.selector
449
+ was_output = self.was_output
450
+ self.was_output = output
451
+ self.was_inhibited = inhibited
452
+ return rows, bool(was_output != output)
453
+
454
+ def update_battery_status(self):
455
+ if self.battery.present is False:
456
+ return
457
+ battery = psutil.sensors_battery()
458
+ if battery is None:
459
+ self.battery.present = False
460
+ return
461
+ was_plugged = self.battery.plugged
462
+ was_selector = self.battery.selector
463
+
464
+ self.battery.plugged = battery.power_plugged
465
+ self.battery.percent = round(battery.percent, 1)
466
+ if self.battery.plugged:
467
+ self.battery.selector = 'Settings'
468
+ elif self.battery.percent > self.get_params().lo_battery_pct:
469
+ self.battery.selector = 'HiBattery'
470
+ else:
471
+ self.battery.selector = 'LoBattery'
472
+ if was_plugged != self.battery.plugged or was_selector != self.battery.selector:
473
+ self.rebuild_menu = True
474
+ # IF WANTING TIME LEFT
475
+ # secsleft = battery.secsleft
476
+ # if secsleft == psutil.POWER_TIME_UNLIMITED:
477
+ # time_left = "Calculating..."
478
+ # elif secsleft == psutil.POWER_TIME_UNKNOWN:
479
+ # time_left = "Unknown"
480
+ # else:
481
+ # hours, remainder = divmod(secsleft, 3600)
482
+ # minutes, seconds = divmod(remainder, 60)
483
+ # time_left = f"{hours:02}:{minutes:02}"
484
+
485
+ def reset_xidle_ms(self):
486
+ """ TBD"""
487
+ self.run_command('reset_idle')
488
+
489
+ def set_state(self, name):
490
+ """ Set the state of the applet """
491
+ prt(f'set_state: {name},{self.running_idle_s}s',
492
+ f'was: {self.state.name},{self.state.when}s')
493
+ self.state.name = name
494
+ self.state.when = self.running_idle_s
495
+
496
+ def on_timeout(self):
497
+ """TBD"""
498
+ self.loop += 1
499
+ if self.DB():
500
+ prt('DB', f'on_timeout() {self.loop=}/{self.loop_sample} ...')
501
+ if not QSystemTrayIcon.isSystemTrayAvailable():
502
+ prt('SystemTray is gone ... exiting')
503
+ self.exit_wm(None)
504
+
505
+ self.reconfig()
506
+ if self.idle_manager:
507
+ self.idle_manager.checkup()
508
+ self.update_battery_status()
509
+
510
+ rows, updated = self.check_inhibited()
511
+ if updated or self.rebuild_menu:
512
+ self.build_menu(rows)
513
+ self.rebuild_menu = False
514
+ prt('re-built menu')
515
+
516
+ if self.loop >= self.loop_sample:
517
+ self.update_running_idle_s()
518
+ lock_secs = self.get_lock_min_list()[0]*60
519
+ down_secs = self.get_sleep_min_list()[0]*60 + lock_secs
520
+ blank_secs = 5 if self.quick else 20
521
+ # if 0 <= int(self.get_params().dim_pct_brightness) < 100:
522
+ # dim_secs = int(round(lock_secs * int(self.get_params().dim_pct_lock_min) / 100, 0))
523
+ # else:
524
+ # dim_secs = 2000 + lock_secs * 2 # make it never happen
525
+
526
+ emit = f'idle_s={self.running_idle_s} state={self.state.name},{self.state.when}s'
527
+ emode = self.get_effective_mode()
528
+ if emode in ('LockOnly', 'SleepAfterLock'):
529
+ emit += f' @{self.get_lock_min_list()[0]}m'
530
+ if emode in ('SleepAfterLock', ):
531
+ emit += f'+{self.get_sleep_min_list()[0]}m'
532
+ if self.battery.selector != 'Settings':
533
+ emit += f' {self.battery.selector}'
534
+ prt(emit)
535
+
536
+ if emode in ('Presentation',) or self.was_inhibited:
537
+ if 'sway' in self.graphical:
538
+ self.reset_xidle_ms() # we don't know when
539
+ elif self.running_idle_s > min(50, lock_secs*0.40):
540
+ self.reset_xidle_ms() # we don't know when
541
+
542
+ elif (self.running_idle_s >= down_secs and emode not in ('LockOnly',)
543
+ and self.state.name in ('Awake', 'Locked', 'Blanked')):
544
+ if self._get_down_state() == 'PowerOff':
545
+ self.poweroff(None)
546
+ else:
547
+ self.suspend(None)
548
+
549
+ elif (self.running_idle_s >= lock_secs and emode not in ('Presentation',)
550
+ and self.state.name in ('Awake', 'Dim')):
551
+ self.lock_screen(None)
552
+
553
+ # elif (self.running_idle_s >= dim_secs and emode not in ('Presentation',)
554
+ # and self.state.name in ('Awake',)):
555
+ # self.dimmer(None)
556
+
557
+ elif (self.running_idle_s >= self.state.when + blank_secs
558
+ and self.get_params().turn_off_monitors
559
+ and emode not in ('Presentation',)
560
+ and self.state.name in ('Locked',)):
561
+ self.blank_primitive()
562
+
563
+ elif self.inh_lock_began_secs:
564
+ self.inh_lock_began_secs = False
565
+
566
+ elif self.running_idle_s < lock_secs and self.state.name not in ('Awake', ):
567
+ self.set_state('Awake')
568
+
569
+ self.loop = 0
570
+
571
+ if self.poll_100ms:
572
+ poll_ms = 100
573
+ self.poll_100ms = False
574
+ self.loop = self.loop_sample
575
+ prt(f'{poll_ms=}')
576
+ else:
577
+ poll_ms = int(self.poll_s * 1000)
578
+ self.timer.setInterval(poll_ms)
579
+
580
+ def _toggle_battery(self, _=None):
581
+ if self.battery.present is False:
582
+ # lets you either use the lo/hi battery setting for another
583
+ # purpose or test out your battery settings
584
+ selector = self.battery.selector
585
+ selector = ('LoBattery' if selector == 'HiBattery'
586
+ else 'Settings' if selector == 'LoBattery' else 'HiBattery')
587
+ self.battery.selector = selector
588
+ # self.ini_tool.set_effective_params(selector)
589
+ self.rebuild_menu = True
590
+
591
+ def _lock_rotate_next(self, advance=None):
592
+ advance = True if advance is None else advance
593
+ mins = self.get_lock_min_list()
594
+ if len(mins) < 1 or (len(mins) == 2 and mins[0] == mins[1]):
595
+ return mins[0]
596
+ next_mins = mins[1]
597
+ if advance:
598
+ mins0 = mins[0]
599
+ mins = self.get_lock_rotated_list(first=next_mins)
600
+ self.rebuild_menu = bool(mins0 != mins[0])
601
+ self.save_picks()
602
+ self.idle_manager_start()
603
+ return next_mins
604
+
605
+ def _lock_rotate_str(self):
606
+ mins = self.get_lock_min_list()
607
+ rv = f'{mins[0]}m' + ('' if mins[0] == mins[1] else f'->{mins[1]}m')
608
+ return rv
609
+
610
+ def toggle_playerctl(self):
611
+ if self.has_playerctl:
612
+ self.enable_playerctl = not bool(self.enable_playerctl)
613
+ self.save_picks()
614
+ self.rebuild_menu = True
615
+
616
+ def _sleep_rotate_next(self, advance=None):
617
+ advance = True if advance is None else advance
618
+ mins = self.get_sleep_min_list()
619
+ if len(mins) < 1 or (len(mins) == 2 and mins[0] == mins[1]):
620
+ return mins[0]
621
+ next_mins = mins[1]
622
+ if advance:
623
+ mins0 = mins[0]
624
+ mins = self.get_sleep_rotated_list(first=next_mins)
625
+ self.rebuild_menu = bool(mins0 != mins[0])
626
+ self.save_picks()
627
+ self.idle_manager_start()
628
+ return next_mins
629
+
630
+ def _sleep_rotate_str(self):
631
+ mins = self.get_sleep_min_list()
632
+ rv = f'{mins[0]}m' + ('' if mins[0] == mins[1] else f'->{mins[1]}m')
633
+ return rv
634
+
635
+ def build_menu(self, rows=None):
636
+ """TBD"""
637
+ # pylint: disable=unnecessary-lambda
638
+ def has_cmd(label):
639
+ return bool(self.variables.get(label, None))
640
+
641
+ def add_item(text, callback):
642
+ nonlocal self
643
+ item = QAction(text)
644
+ item.triggered.connect(callback)
645
+ self.menu.addAction(item)
646
+ self.menu_items.append(item)
647
+
648
+ self.menu = QMenu()
649
+ self.menu_items = []
650
+
651
+ if rows:
652
+ for row in rows:
653
+ add_item(row, self.dummy)
654
+
655
+ if self.mode not in ('Presentation',):
656
+ add_item(f'🅟 Presentation ⮜ {self.mode} Mode', self.enable_presentation_mode)
657
+
658
+ if self.mode not in ('LockOnly',):
659
+ add_item(f'🅛 LockOnly ⮜ {self.mode} Mode', self.enable_nosleep_mode)
660
+
661
+ if self.mode not in ('SleepAfterLock',):
662
+ add_item(f'🅢 SleepAfterLock ⮜ {self.mode} Mode', self.enable_normal_mode)
663
+
664
+ add_item(f'{self.graphical}:  ▷ Lock Screen', self.lock_screen)
665
+
666
+ if (self.get_params().turn_off_monitors and self.variables['monitors_off']):
667
+ add_item('   ▷ Blank Monitors', self.blank_quick)
668
+
669
+ if has_cmd('reload_wm'):
670
+ add_item('   ▷ Reload', self.reload_wm)
671
+
672
+ if has_cmd('restart_wm'):
673
+ add_item('   ▷ Restart', self.restart_wm)
674
+
675
+ add_item('   ▷ Log Off', self.exit_wm)
676
+ add_item('System:  ▼ Suspend', self.suspend)
677
+ add_item('    ▼ Reboot', self.reboot)
678
+ add_item('    ▼ PowerOff', self.poweroff)
679
+
680
+ selector, percent = self.battery.selector, self.battery.percent
681
+ add_item('🗲 Plugged In' if selector == 'Settings'
682
+ else (('█' if selector == 'HiBattery' else '▃') + f' {selector}')
683
+ + (f' {percent}%' if percent < 100 or selector != 'Settings' else '')
684
+ , self._toggle_battery)
685
+
686
+ # if self.mode not in ('Presentation',) and len(self.opts.lock_min_list) > 1:
687
+ add_item(f'  ♺ Lock: {self._lock_rotate_str()}',
688
+ lambda: self._lock_rotate_next())
689
+
690
+ # if self.mode in ('SleepAfterLock',) and len(self.opts.sleep_min_list) > 1:
691
+ add_item(f'  ♺ Sleep (after Lock): {self._sleep_rotate_str()}',
692
+ lambda: self._sleep_rotate_next())
693
+
694
+ label = '🎝 PlayerCtl: '
695
+ label += ('not installed' if not self.has_playerctl
696
+ else 'Enabled' if self.enable_playerctl
697
+ else 'Disabled')
698
+ add_item(label, self.toggle_playerctl)
699
+ if self.get_params().gui_editor:
700
+ add_item('🖹 Edit Applet Config', self.edit_config)
701
+
702
+ add_item('☓ Quit this Applet', self.quit_self)
703
+
704
+ add_item('↺ Restart this Applet', self.restart_self)
705
+
706
+ self.tray_icon.setContextMenu(self.menu)
707
+
708
+ if not self.tray_icon.isVisible():
709
+ prt('self.tray_icon.isVisible() is False ... restarting app')
710
+ time.sleep(0.5)
711
+ self.restart_self(None)
712
+
713
+ def on_tray_icon_activated(self, reason):
714
+ if reason == QSystemTrayIcon.Context: # Right click
715
+ self.tray_icon.contextMenu().exec_(QCursor.pos()) # Show the context menu
716
+ elif (reason == QSystemTrayIcon.Trigger # Left click
717
+ and not self.is_wayland): # wayland behaves badly
718
+ self.tray_icon.contextMenu().exec_(QCursor.pos()) # Show the context menu
719
+
720
+
721
+ def idle_manager_start(self):
722
+ """ For any mode, get the idle manager started"""
723
+ if self.idle_manager:
724
+ self.idle_manager.start()
725
+
726
+ @staticmethod
727
+ def dummy(_):
728
+ """TBD"""
729
+
730
+ @staticmethod
731
+ def run_command(key):
732
+ this = PwrTray.singleton
733
+ command = this.variables.get(key, None)
734
+ if command and key == 'locker' and this.graphical in ('i3', 'sway'):
735
+ if this.graphical == 'i3':
736
+ append = this.get_params().i3lock_args
737
+ elif this.graphical == 'sway':
738
+ append = this.get_params().swaylock_args
739
+ if not append:
740
+ append = '-t -i ./lockpaper.png'
741
+ command += ' ' + append
742
+
743
+ # elif key == 'dimmer':
744
+ # percent = int(round(int(thisget_params()params.dim_pct_brightness), 0))
745
+ # command = command.replace('{percent}', str(percent))
746
+
747
+ if command:
748
+ prt(f'+ {command}')
749
+ if re.match(r'^(\s\w\-)*$', command):
750
+ result = subprocess.run(command.split(), check=False)
751
+ else:
752
+ result = subprocess.run(command, check=False, shell=True)
753
+ if result.returncode != 0:
754
+ prt(f' NOTE: returncode={result.returncode}')
755
+
756
+ @staticmethod
757
+ def quit_self(_):
758
+ """TBD"""
759
+ prt('+', 'quitting applet...')
760
+ this = PwrTray.singleton
761
+ if this:
762
+ this.tray_icon.hide()
763
+ sys.exit()
764
+
765
+ @staticmethod
766
+ def restart_self(_):
767
+ """TBD"""
768
+ prt('+', 'restarting applet...')
769
+ this = PwrTray.singleton
770
+ if this:
771
+ this.tray_icon.hide()
772
+ PwrTray.save_picks()
773
+ subprocess.Popen([sys.executable] + sys.argv)
774
+ os._exit(0)
775
+
776
+ @staticmethod
777
+ def edit_config(_):
778
+ this = PwrTray.singleton
779
+ if this.get_params().gui_editor:
780
+ try:
781
+ ini_path = this.ini_tool.ini_path
782
+ arguments = this.get_params().gui_editor.split()
783
+ arguments.append(ini_path)
784
+ prt('+', 'running:', arguments)
785
+ subprocess.run(arguments, check=True)
786
+ except Exception as e:
787
+ prt(f"Edit Config ERR: {e}")
788
+
789
+ @staticmethod
790
+ def suspend(_):
791
+ """TBD"""
792
+ this = PwrTray.singleton
793
+ this.set_state('Asleep')
794
+ this.reset_xidle_ms()
795
+ if this.graphical in ('i3', ):
796
+ PwrTray.run_command('locker')
797
+ PwrTray.run_command('suspend')
798
+
799
+ @staticmethod
800
+ def poweroff(_):
801
+ """TBD"""
802
+ PwrTray.run_command('poweroff')
803
+
804
+ @staticmethod
805
+ def reboot(_):
806
+ """TBD"""
807
+ PwrTray.run_command('reboot')
808
+
809
+ @staticmethod
810
+ def dimmer(_):
811
+ """TBD"""
812
+ PwrTray.run_command('dimmer')
813
+
814
+ @staticmethod
815
+ def undim(_):
816
+ """TBD"""
817
+ PwrTray.run_command('undim')
818
+
819
+ @staticmethod
820
+ def lock_screen(_):
821
+ this = PwrTray.singleton
822
+ PwrTray.run_command('locker')
823
+ this.update_running_idle_s()
824
+ # if 0 <= int(thisget_params()params.dim_pct_brightness) < 100:
825
+ # this.undim(None)
826
+ this.set_state('Locked')
827
+
828
+ def blank_primitive(self, lock_screen=False):
829
+ """TBD"""
830
+ cmd = self.variables['monitors_off']
831
+ if cmd and self.get_params().turn_off_monitors:
832
+ if lock_screen:
833
+ # self.lock_screen(None, before='sleep 1.5; ')
834
+ self.lock_screen(None)
835
+ prt('+', cmd)
836
+ result = subprocess.run(cmd, shell=True, check=False)
837
+ if result.returncode != 0:
838
+ prt(f' NOTE: returncode={result.returncode}')
839
+ self.set_state('Blanked')
840
+
841
+ else:
842
+ prt('NOTE: blanking screen unsupported')
843
+
844
+ @staticmethod
845
+ def blank_quick(_):
846
+ this = PwrTray.singleton
847
+ this.blank_primitive(lock_screen=True)
848
+
849
+ @staticmethod
850
+ def reload_wm(_):
851
+ PwrTray.run_command('reload_wm')
852
+
853
+ @staticmethod
854
+ def restart_wm(_):
855
+ PwrTray.run_command('restart_wm')
856
+
857
+ @staticmethod
858
+ def exit_wm(_):
859
+ PwrTray.run_command('logoff')
860
+
861
+ @staticmethod
862
+ def enable_presentation_mode(_):
863
+ """TBD"""
864
+ this = PwrTray.singleton
865
+ this.mode = 'Presentation'
866
+ this.was_output = 'Changed Mode'
867
+ prt('+', f'{this.mode=}')
868
+ this.save_picks()
869
+ this.idle_manager_start()
870
+
871
+ @staticmethod
872
+ def enable_nosleep_mode(_):
873
+ """TBD"""
874
+ this = PwrTray.singleton
875
+ this.mode = 'LockOnly'
876
+ this.was_output = 'Changed Mode'
877
+ prt('+', f'{this.mode=}')
878
+ this.save_picks()
879
+ this.idle_manager_start()
880
+
881
+ @staticmethod
882
+ def enable_normal_mode(_):
883
+ """TBD"""
884
+ this = PwrTray.singleton
885
+ this.mode = 'SleepAfterLock'
886
+ this.was_output = 'Changed Mode'
887
+ prt('+', f'{this.mode=}')
888
+ this.save_picks()
889
+ this.idle_manager_start()
890
+
891
+ # Function to check for ACPI events
892
+ @staticmethod
893
+ def acpi_event_listener():
894
+ acpi_pipe = subprocess.Popen(["acpi_listen"], stdout=subprocess.PIPE)
895
+ for line in acpi_pipe.stdout:
896
+ this = PwrTray.singleton
897
+ if this and b"resume" in line:
898
+ prt('acpi event:', line)
899
+ # Reset idle timer
900
+ this.reset_xidle_ms()
901
+ elif this:
902
+ prt('UNSELECTED acpi event:', line)
903
+ # Reset idle timer
904
+ this.reset_xidle_ms()
905
+ @staticmethod
906
+ def goodbye(message=''):
907
+ prt(f'ENDED {message}')
908
+
909
+
910
+ def main():
911
+ # pylint: disable=import-outside-toplevel
912
+ import argparse
913
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
914
+ # os.chdir(os.path.dirname(os.path.abspath(__file__)))
915
+
916
+ parser = argparse.ArgumentParser()
917
+ parser.add_argument('-D', '--debug', action='store_true',
918
+ help='override debug_mode from .ini initially')
919
+ parser.add_argument('-o', '--stdout', action='store_true',
920
+ help='log to stdout (if a tty)')
921
+ parser.add_argument('-f', '--follow-log', action='store_true',
922
+ help='exec tail -n50 -F on log file')
923
+ parser.add_argument('-e', '--edit-config', action='store_true',
924
+ help='exec ${EDITOR:-vim} on config.ini file')
925
+ parser.add_argument('-q', '--quick', action='store_true',
926
+ help='quick mode (1m lock + 1m sleep')
927
+ opts = parser.parse_args()
928
+
929
+ if opts.edit_config:
930
+ ini_tool = IniTool(paths_only=True)
931
+ editor = os.getenv('EDITOR', 'vim')
932
+ args = [editor, ini_tool.ini_path]
933
+ print(f'RUNNING: {args}')
934
+ os.execvp(editor, args)
935
+ sys.exit(1) # just in case ;-)
936
+
937
+ if opts.follow_log:
938
+ ini_tool = IniTool(paths_only=True)
939
+ args = ['tail', '-n50', '-F', ini_tool.log_path]
940
+ print(f'RUNNING: {args}')
941
+ os.execvp('tail', args)
942
+ sys.exit(1) # just in case ;-)
943
+
944
+ # os.environ['DISPLAY'] = ':0'
945
+
946
+ ini_tool = IniTool(paths_only=False)
947
+ Utils.prt_path = ini_tool.log_path
948
+ prt('START-UP', to_stdout=opts.stdout)
949
+ PyKill().kill_loop('pwr-tray')
950
+ atexit.register(PwrTray.goodbye)
951
+
952
+
953
+ if opts.debug:
954
+ for selector in ini_tool.get_selectors():
955
+ ini_tool.params_by_selector[selector].debug_mode = True # one-time override
956
+
957
+
958
+ tray = PwrTray(ini_tool=ini_tool, quick=opts.quick)
959
+ tray.app.exec_()
960
+
961
+ if __name__ == "__main__":
962
+ try:
963
+ main()
964
+ sys.exit(0)
965
+
966
+ ###### AppIndicator3 is catching most of these so may not get many exceptions
967
+ except KeyboardInterrupt:
968
+ prt("Shutdown requested, so exiting ...")
969
+ sys.exit(1)
970
+
971
+ except Exception as exc:
972
+ prt("Caught exception running main(), so exiting ...\n",
973
+ traceback.format_exc(limit=24))
974
+ sys.exit(9)