aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorB Stack <bgstack15@gmail.com>2020-03-10 17:23:25 -0400
committerB Stack <bgstack15@gmail.com>2020-03-10 17:23:25 -0400
commitb499fd8bbf3d455cbbdf57acc4091b911d96ee22 (patch)
tree982e24259ddcb45e358d94b2fa3200925fb56f57 /src
parentuse /etc/default/logout-manager and conf files (diff)
downloadlogout-manager-b499fd8bbf3d455cbbdf57acc4091b911d96ee22.tar.gz
logout-manager-b499fd8bbf3d455cbbdf57acc4091b911d96ee22.tar.bz2
logout-manager-b499fd8bbf3d455cbbdf57acc4091b911d96ee22.zip
rearrange src and add readme
Diffstat (limited to 'src')
-rw-r--r--src/etc/default/logout-manager2
-rw-r--r--src/etc/logout-manager.conf16
-rwxr-xr-xsrc/usr/bin/logout-manager-cli.py0
-rwxr-xr-xsrc/usr/bin/logout-manager-gtk.py252
-rwxr-xr-xsrc/usr/bin/logout-manager-ncurses.py193
-rwxr-xr-xsrc/usr/bin/logout-manager-tcl.py417
-rw-r--r--src/usr/share/logout-manager/lmlib.py286
7 files changed, 1166 insertions, 0 deletions
diff --git a/src/etc/default/logout-manager b/src/etc/default/logout-manager
new file mode 100644
index 0000000..1f4ecf6
--- /dev/null
+++ b/src/etc/default/logout-manager
@@ -0,0 +1,2 @@
+LOGOUT_MANAGER_LIBPATH=/usr/share/logout-manager
+LOGOUT_MANAGER_CONF=/etc/logout-manager.conf
diff --git a/src/etc/logout-manager.conf b/src/etc/logout-manager.conf
new file mode 100644
index 0000000..1a14909
--- /dev/null
+++ b/src/etc/logout-manager.conf
@@ -0,0 +1,16 @@
+[logout-manager]
+hibernate_command="printf 'disk' | sudo tee /sys/power/state"
+lock_command="xscreensaver --locknow"
+logout_command="logout from something"
+reboot_command="sudo shutdown -r now"
+shutdown_command="sudo shutdown -h now"
+
+[icons]
+size = 24
+#theme = default
+# use names as used by the icon theme, or give a full path here
+hibernate = system-hibernate
+lock = system-lock-screen
+logout = system-log-out
+reboot = system-reboot
+shutdown = system-shutdown
diff --git a/src/usr/bin/logout-manager-cli.py b/src/usr/bin/logout-manager-cli.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/src/usr/bin/logout-manager-cli.py
diff --git a/src/usr/bin/logout-manager-gtk.py b/src/usr/bin/logout-manager-gtk.py
new file mode 100755
index 0000000..553fc41
--- /dev/null
+++ b/src/usr/bin/logout-manager-gtk.py
@@ -0,0 +1,252 @@
+#!/usr/bin/env python3
+# File: logout-manager-gtk.py
+# License: CC-BY-SA 4.0
+# Author: bgstack15
+# Startdate: 2019-06-01
+# Title: GTK3 based logout manager
+# Purpose: Primarily for fluxbox, this tool provides a graphical menu for various session control commands like shutdown, logout, and reboot
+# History:
+# Usage:
+# This is a bit for reference, but also to provide myself a little shutdown options menu, like xfce4, because fluxbox doesn't really provide one.
+# Reference:
+# https://www.linuxquestions.org/questions/slackware-14/how-do-i-run-menu-and-logout-from-the-command-line-in-fluxbox-864919/
+# /mnt/public/work/python/hotplug2/
+# icon handling https://python-gtk-3-tutorial.readthedocs.io/en/latest/iconview.html
+# accelerator keys https://askubuntu.com/questions/655452/python-gtk3-keyboard-accelerators
+# gtk3 widget signals https://developer.gnome.org/gtk3/unstable/GtkWidget.html#GtkWidget-button-press-event
+# /usr/share/wicd/gtk/gui.py netentry.py wicd.ui
+# combined with next ref: scale down valid icon https://stackoverflow.com/questions/42800482/how-to-set-size-of-a-gtk-image-in-python
+# https://stackoverflow.com/questions/6090241/how-can-i-get-the-full-file-path-of-an-icon-name
+# use custom icon theme https://lazka.github.io/pgi-docs/Gtk-3.0/classes/IconTheme.html#Gtk.IconTheme.set_custom_theme
+# https://stackoverflow.com/questions/4090804/how-can-i-pass-variables-between-two-classes-windows-in-pygtk
+# Improve:
+# actually execute the commands
+# only show debug info when DEBUG=1 or similar.
+# support global conf file, and user conf file
+# far future: provide graphical way to change commands run
+# Dependencies:
+# Devuan: python3-dotenv
+# Documentation:
+
+import gi, os, platform, sys
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository.GdkPixbuf import Pixbuf
+from pathlib import Path
+from dotenv import load_dotenv
+
+# all this to load the libpath
+try:
+ defaultdir="/etc/sysconfig"
+ thisplatform = platform.platform().lower()
+ if 'debian' in thisplatform or 'devuan' in thisplatform:
+ defaultdir="/etc/default"
+ # load_dotenv keeps existing environment variables as higher precedent
+ load_dotenv(os.path.join(defaultdir,"logout-manager"))
+except:
+ pass
+if 'LOGOUT_MANAGER_LIBPATH' in os.environ:
+ for i in os.environ['LOGOUT_MANAGER_LIBPATH'].split(":"):
+ sys.path.append(i)
+import lmlib
+
+# graphical classes and functions
+def get_scaled_icon(icon_name, size=24, fallback_icon_name = "", icon_theme = "default"):
+ # return a Gtk.Image.new_from_pixbuf
+
+ # ripped from https://stackoverflow.com/questions/42800482/how-to-set-size-of-a-gtk-image-in-python and combined with https://stackoverflow.com/questions/6090241/how-can-i-get-the-full-file-path-of-an-icon-name
+ # further ref for lookup_icon function: https://lazka.github.io/pgi-docs/Gtk-3.0/flags.html#Gtk.IconLookupFlags
+ # if a file exists by the specific name, use it.
+ if Path(icon_name).is_file():
+ iconfilename = icon_name
+ else:
+ if icon_theme != "default":
+ this_theme = Gtk.IconTheme.new()
+ this_theme.set_custom_theme(icon_theme)
+ else:
+ this_theme = Gtk.IconTheme.get_default()
+ try:
+ icon_info = this_theme.lookup_icon(icon_name, size, 0)
+ iconfilename = icon_info.get_filename()
+ except:
+ try:
+ icon_info = this_theme.lookup_icon(fallback_icon_name, size, 0)
+ iconfilename = icon_info.get_filename()
+ except:
+ # no icon in the current theme. Try a hard-coded fallback:
+ try:
+ # if debuglev 3
+ print("Error: could not find default icon for", icon_name+", so using fallback.")
+ this_theme = Gtk.IconTheme.new()
+ this_theme.set_custom_theme("Numix-Circle")
+ icon_info = this_theme.lookup_icon(icon_name, size, 0)
+ iconfilename = icon_info.get_filename()
+ except:
+ print("Error: Could not find any icon for", icon_name)
+ return None
+ #print(iconfilename)
+ return Gtk.Image.new_from_pixbuf(Pixbuf.new_from_file_at_scale(
+ filename=iconfilename,
+ width=size, height=size, preserve_aspect_ratio=True))
+
+class MainWindow(Gtk.Window):
+ def __init__(self, config, actions):
+ self.actions = actions
+ self.config = config
+ Gtk.Window.__init__(self, title="Log out options")
+ # for window icon
+ liststore = Gtk.ListStore(Pixbuf, str)
+ iconview = Gtk.IconView.new()
+ iconview.set_model(liststore)
+ iconview.set_pixbuf_column(0)
+ iconview.set_text_column(1)
+ pixbuf24 = Gtk.IconTheme.get_default().load_icon(config.application_icon, 24, 0)
+ pixbuf32 = Gtk.IconTheme.get_default().load_icon(config.application_icon, 32, 0)
+ pixbuf48 = Gtk.IconTheme.get_default().load_icon(config.application_icon, 48, 0)
+ pixbuf64 = Gtk.IconTheme.get_default().load_icon(config.application_icon, 64, 0)
+ pixbuf96 = Gtk.IconTheme.get_default().load_icon(config.application_icon, 96, 0)
+ self.set_icon_list([pixbuf24, pixbuf32, pixbuf48, pixbuf64, pixbuf96]);
+
+ # accel is for when you are not using the "set_use_underline" function.
+ #accel = Gtk.AccelGroup()
+ #accel.connect(Gdk.keyval_from_name('D'), Gdk.ModifierType.MOD1_MASK, 0, self.on_button2_accel)
+ #self.add_accel_group(accel)
+
+ # buttons
+ self.grid = Gtk.Grid()
+ self.add(self.grid)
+
+ self.button0 = Gtk.Button(label="Loc_k")
+ self.button0.connect("button-press-event", self.on_button0_press_event)
+ self.button0.connect("activate", self.on_button0_press_event) # activate covers ALT+L action and spacebar when selected
+ self.buttonicon0 = get_scaled_icon(config.get_lock_icon(), config.get_icon_size(), config.get_lock_fallback_icon(), config.get_icon_theme())
+ self.button0.set_image(self.buttonicon0)
+ self.button0.set_tooltip_text("Hide session and require authentication to return to it")
+ self.button0.set_always_show_image(True)
+ self.button0.set_use_underline(True)
+ self.grid.add(self.button0)
+
+ self.button1 = Gtk.Button(label="_Logout")
+ self.button1.connect("button-press-event", self.on_button1_press_event)
+ self.button1.connect("activate", self.on_button1_press_event) # activate covers ALT+L action and spacebar when selected
+ self.buttonicon1 = get_scaled_icon(config.get_logout_icon(), config.get_icon_size(), config.get_logout_fallback_icon(), config.get_icon_theme())
+ self.button1.set_image(self.buttonicon1)
+ self.button1.set_tooltip_text("Close the current user session")
+ self.button1.set_always_show_image(True)
+ self.button1.set_use_underline(True)
+ self.grid.add(self.button1)
+
+ self.buttonHibernate = Gtk.Button(label="_Hibernate")
+ self.buttonHibernate.connect("button-press-event", self.on_buttonHibernate_press_event)
+ self.buttonHibernate.connect("activate", self.on_buttonHibernate_press_event) # activate covers ALT+L action and spacebar when selected
+ #self.buttoniconHibernate = Gtk.Image()
+ #self.buttoniconHibernate.set_from_icon_name("system-hibernate", 24)
+ self.buttoniconHibernate = get_scaled_icon(config.get_hibernate_icon(), config.get_icon_size(), config.get_hibernate_fallback_icon(), config.get_icon_theme())
+ self.buttonHibernate.set_image(self.buttoniconHibernate)
+ self.buttonHibernate.set_tooltip_text("Save state to disk and power off")
+ self.buttonHibernate.set_always_show_image(True)
+ self.buttonHibernate.set_use_underline(True)
+ self.buttonHibernate.set_sensitive(True if config.get_can_hibernate() else False)
+ self.grid.add(self.buttonHibernate)
+
+ self.button2 = Gtk.Button(label="_Shutdown")
+ self.button2.connect("button-press-event", self.on_button2_press_event)
+ self.button2.connect("activate", self.on_button2_accel)
+ # unnecessary because the "activate" suffices above.
+ #self.button2.connect("mnemonic-activate", self.on_button2_accel)
+ self.buttonicon2 = Gtk.Image()
+ self.buttonicon2 = get_scaled_icon(config.get_shutdown_icon(), config.get_icon_size(), config.get_shutdown_fallback_icon(), config.get_icon_theme())
+ self.button2.set_image(self.buttonicon2)
+ self.button2.set_tooltip_text("Power off the computer")
+ self.button2.set_always_show_image(True)
+ self.button2.set_use_underline(True)
+ self.grid.add(self.button2)
+
+ self.button3 = Gtk.Button(label="_Reboot")
+ self.button3.connect("button-press-event", self.on_button3_press_event)
+ self.button3.connect("activate", self.on_button3_press_event)
+ self.buttonicon3 = Gtk.Image()
+ self.buttonicon3 = get_scaled_icon(config.get_reboot_icon(), config.get_icon_size(), config.get_reboot_fallback_icon(), config.get_icon_theme())
+ self.button3.set_image(self.buttonicon3)
+ self.button3.set_tooltip_text("Reboot the computer back to the login screen")
+ self.button3.set_always_show_image(True)
+ self.button3.set_use_underline(True)
+ self.grid.add(self.button3)
+
+ self.button4 = Gtk.Button(label="_Cancel")
+ self.button4.connect("button-press-event", self.on_button4_press_event)
+ self.button4.connect("activate", self.on_button4_press_event)
+ self.button4.set_tooltip_text("Do nothing; just close this window")
+ self.button4.set_use_underline(True)
+ self.grid.attach(self.button4, 0, 1, 8, 1)
+
+ # hibernate button
+ def on_buttonHibernate_press_event(self, *args):
+ self.do_hibernate(self.buttonHibernate)
+
+ # lock button
+ def on_button0_press_event(self, *args):
+ self.do_lock(self.button0)
+
+ # logout button
+ def on_button1_press_event(self, *args):
+ self.do_logout(self.button1)
+
+ # shutdown button
+ def on_button2_press_event(self, widget, event):
+ # check if left or right click
+ if event.type == Gdk.EventType.BUTTON_PRESS:
+ if event.button == 1:
+ self.do_shutdown(widget)
+ # eventbutton == 3 is the right-click, and its reference is my hello3.py
+ #elif event.button == 3:
+ # self.on_button1_right_clicked(widget)
+
+ # global accelerator key, when not using the set_use_underline function
+ ## shutdown button from accelerator key
+ #def on_button2_accel(self, *args):
+ # self.do_shutdown(self.button2)
+
+ # accelerator key from set_use_underline function
+ # shutdown button from accelerator key
+ def on_button2_accel(self, *args):
+ self.do_shutdown(self.button2)
+
+ # reboot button
+ def on_button3_press_event(self, *args):
+ self.do_reboot(self.button3)
+
+ # cancel button
+ def on_button4_press_event(self, *args):
+ self.cancel(self.button4)
+
+ def do_shutdown(self, *args):
+ #print(dir(self.props))
+ self.actions.shutdown(self.config)
+
+ def do_hibernate(self, widget):
+ self.actions.hibernate(self.config)
+
+ def do_lock(self, widget):
+ self.actions.lock(self.config)
+
+ def do_logout(self, widget):
+ self.actions.logout(self.config)
+
+ def do_reboot(self, widget):
+ self.actions.reboot(self.config)
+
+ def cancel(self, widget):
+ print("Cancel any logout action.")
+ Gtk.main_quit()
+
+# load configs
+config = lmlib.Initialize_config(os.environ['LOGOUT_MANAGER_CONF'])
+actions = lmlib.Actions
+
+# MAIN LOOP
+win = MainWindow(config, actions)
+win.connect("destroy", Gtk.main_quit)
+win.show_all()
+Gtk.main()
diff --git a/src/usr/bin/logout-manager-ncurses.py b/src/usr/bin/logout-manager-ncurses.py
new file mode 100755
index 0000000..1500d85
--- /dev/null
+++ b/src/usr/bin/logout-manager-ncurses.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+# File: logout-manager-ncurses.py
+# License: MIT
+# Author: adamlamers, bgstack15
+# Startdate: 2020-03-09 17:06
+# Title: ncurses based logout manager
+# Usage:
+# logout-manager-ncurses.py
+# Reference:
+# https://docs.python.org/3/howto/curses.html
+# ripped straight from http://adamlamers.com/post/FTPD9KNRA8CT
+# https://stackoverflow.com/questions/3061/calling-a-function-of-a-module-by-using-its-name-a-string/12025554#12025554
+# https://robinislam.me/blog/reading-environment-variables-in-python/
+# Improve:
+# Dependencies:
+# Devuan: python3-dotenv
+# Documentation:
+# Improvements for CursesMenu class over origin:
+# accepts number key inputs
+# accepts enabled attribute
+# add "zeroindex" bool
+
+import curses, os, platform, sys
+from dotenv import load_dotenv
+
+# all this to load the libpath
+try:
+ defaultdir="/etc/sysconfig"
+ thisplatform = platform.platform().lower()
+ if 'debian' in thisplatform or 'devuan' in thisplatform:
+ defaultdir="/etc/default"
+ # load_dotenv keeps existing environment variables as higher precedent
+ load_dotenv(os.path.join(defaultdir,"logout-manager"))
+except:
+ pass
+if 'LOGOUT_MANAGER_LIBPATH' in os.environ:
+ for i in os.environ['LOGOUT_MANAGER_LIBPATH'].split(":"):
+ sys.path.append(i)
+import lmlib
+
+class CursesMenu(object):
+
+ INIT = {'type' : 'init'}
+
+ def __init__(self, menu_options):
+ self.screen = curses.initscr()
+ self.menu_options = menu_options
+ self.selected_option = 0
+ self._previously_selected_option = None
+ self.running = True
+ self._zero_offset = 1
+ try:
+ self._zero_offset = 0 if bool(self.menu_options['zeroindex']) else 1
+ except:
+ pass
+
+ #init curses and curses input
+ curses.noecho()
+ curses.cbreak()
+ curses.start_color()
+ curses.curs_set(0) #Hide cursor
+ self.screen.keypad(1)
+
+ #set up color pair for highlighted option
+ curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
+ self.hilite_color = curses.color_pair(1)
+ self.normal_color = curses.A_NORMAL
+
+ def prompt_selection(self, parent=None):
+ if parent is None:
+ lastoption = "Cancel"
+ else:
+ lastoption = "Return to previous menu ({})".format(parent['title'])
+
+ option_count = len(self.menu_options['options'])
+
+ input_key = None
+
+ ENTER_KEY = ord('\n')
+ NUM_KEYS = [ord(str(i)) for i in range(self._zero_offset,option_count+1+self._zero_offset)]
+ done = False
+ while not done:
+ if self.selected_option != self._previously_selected_option:
+ self._previously_selected_option = self.selected_option
+
+ self.screen.border(0)
+ self._draw_title()
+ for option in range(option_count):
+ if self.selected_option == option:
+ self._draw_option(option, self.hilite_color)
+ else:
+ self._draw_option(option, self.normal_color)
+
+ if self.selected_option == option_count:
+ self.screen.addstr(4 + option_count, 4, "{:2} - {}".format(option_count+self._zero_offset,
+ lastoption), self.hilite_color)
+ else:
+ self.screen.addstr(4 + option_count, 4, "{:2} - {}".format(option_count+self._zero_offset,
+ lastoption), self.normal_color)
+
+ max_y, max_x = self.screen.getmaxyx()
+ if input_key is not None:
+ self.screen.addstr(max_y-3, max_x - 5, "{:3}".format(self.selected_option+self._zero_offset))
+ self.screen.refresh()
+
+
+ input_key = self.screen.getch()
+ down_keys = [curses.KEY_DOWN, ord('j')]
+ up_keys = [curses.KEY_UP, ord('k')]
+ exit_keys = [ord('q')]
+
+ if input_key in down_keys:
+ if self.selected_option < option_count:
+ self.selected_option += 1
+ else:
+ self.selected_option = 0
+
+ if input_key in up_keys:
+ if self.selected_option > 0:
+ self.selected_option -= 1
+ else:
+ self.selected_option = option_count
+
+ if input_key in exit_keys:
+ self.selected_option = option_count #auto select exit and return
+ break
+
+ if input_key == ENTER_KEY or input_key in NUM_KEYS:
+ if input_key in NUM_KEYS:
+ self.selected_option=int(chr(input_key))-self._zero_offset
+ done = True
+ try:
+ done = self.menu_options['options'][self.selected_option]['enabled']
+ except:
+ pass
+ return self.selected_option
+
+ def _draw_option(self, option_number, style):
+ thistext = self.menu_options['options'][option_number]['title']
+ try:
+ if self.menu_options['options'][option_number]['enabled'] == False: thistext += " (disabled)"
+ except:
+ pass
+ self.screen.addstr(4 + option_number,
+ 4,
+ "{:2} - {}".format(option_number+self._zero_offset, thistext),
+ style)
+
+ def _draw_title(self):
+ self.screen.addstr(2, 2, self.menu_options['title'], curses.A_STANDOUT)
+ self.screen.addstr(3, 2, self.menu_options['subtitle'], curses.A_BOLD)
+
+ def display(self):
+ selected_option = self.prompt_selection()
+ i, _ = self.screen.getmaxyx()
+ curses.endwin()
+ #os.system('clear')
+ if selected_option < len(self.menu_options['options']):
+ selected_opt = self.menu_options['options'][selected_option]
+ return selected_opt
+ else:
+ self.running = False
+ return {'title' : 'Cancel', 'type' : 'exitmenu'}
+
+# load configs
+config = lmlib.Initialize_config(os.environ['LOGOUT_MANAGER_CONF'])
+actions = lmlib.Actions
+
+# MAIN LOOP
+menu = {
+ 'title' : 'Logout Manager',
+ 'type' : 'menu',
+ 'subtitle' : 'Use arrows or number keys',
+ 'zeroindex' : False,
+ 'options' : [
+ {'title': 'Lock', 'type': 'action', 'action': 'lock'},
+ {'title': 'Logout', 'type': 'action', 'action': 'logout'},
+ {'title': 'Hibernate', 'type': 'action', 'action': 'hibernate', 'enabled': config.can_hibernate},
+ {'title': 'Shutdown', 'type': 'action', 'action': 'shutdown'},
+ {'title': 'Reboot', 'type': 'action', 'action': 'reboot'}
+ ]
+}
+m = CursesMenu(menu)
+selected_action = m.display()
+
+if selected_action['type'] == 'exitmenu':
+ print("Cancel any logout action.")
+elif selected_action['type'] == 'command':
+ os.system(selected_action['command'])
+elif selected_action['type'] == 'action':
+ #a = selected_action['action']:
+ func = getattr(globals()['actions'],selected_action['action'])
+ func(config)
diff --git a/src/usr/bin/logout-manager-tcl.py b/src/usr/bin/logout-manager-tcl.py
new file mode 100755
index 0000000..127bd54
--- /dev/null
+++ b/src/usr/bin/logout-manager-tcl.py
@@ -0,0 +1,417 @@
+#!/usr/bin/env python3
+# File: logout-manager-tcl.py
+# License: CC-BY-SA 4.0
+# Author: bgstack15
+# Startdate: 2019-06-12 20:05
+# Title: Tcl/tk-based logout manager
+# Purpose: A tcl/tk graphical program for selecting shutdown, logout, etc.
+# History:
+# Usage:
+# logout-manager-tcl.py
+# References:
+# http://effbot.org/tkinterbook/button.htm
+# http://effbot.org/tkinterbook/tkinter-application-windows.htm
+# http://effbot.org/tkinterbook/
+# pass parameters to function of tkinter.Button(command=) https://stackoverflow.com/questions/38749620/python-3-tkinter-button-commands#38750155
+# alternate for passing params https://stackoverflow.com/questions/6920302/how-to-pass-arguments-to-a-button-command-in-tkinter
+# https://stackoverflow.com/questions/18537918/set-window-icon#18538416
+# the exact syntax <Alt-k> for master.bind https://stackoverflow.com/questions/16082243/how-to-bind-ctrl-in-python-tkinter
+# https://pillow.readthedocs.io/en/stable/reference/ImageTk.html
+# gtk-3.0 default icon theme https://coderwall.com/p/no3qfa/setting-gtk2-and-gtk3-theme-via-config-file
+# homedir https://stackoverflow.com/questions/4028904/how-to-get-the-home-directory-in-python
+# natural sort https://stackoverflow.com/questions/46228101/sort-list-of-strings-by-two-substrings-using-lambda-function/46228199#46228199
+# tooltips https://stackoverflow.com/questions/3221956/how-do-i-display-tooltips-in-tkinter/41381685#41381685
+# Improve:
+# Dependencies:
+# Devuan: python3-tk python3-pil.imagetk python3-cairosvg
+# el7: python36-tkinter python36-pillow-tk ( pip3 install cairosvg )
+
+import glob, os, platform, re, sys
+import tkinter as tk
+from functools import partial
+from pathlib import Path
+from sys import path
+from dotenv import load_dotenv
+# loading PIL.ImageTk after tkinter makes ImageTk use the PIL version, which supports PNG. This is important on tcl < 8.6 (that is, el7)
+from PIL import Image, ImageTk
+
+LM_USE_SVG = 0
+try:
+ from cairosvg import svg2png
+ LM_USE_SVG = 1
+except:
+ print("WARNING: Unable to import cairosvg. No svg images will be displayed.")
+ LM_USE_SVG = 0
+
+# all this to load the libpath
+try:
+ defaultdir="/etc/sysconfig"
+ thisplatform = platform.platform().lower()
+ if 'debian' in thisplatform or 'devuan' in thisplatform:
+ defaultdir="/etc/default"
+ # load_dotenv keeps existing environment variables as higher precedent
+ load_dotenv(os.path.join(defaultdir,"logout-manager"))
+except:
+ pass
+if 'LOGOUT_MANAGER_LIBPATH' in os.environ:
+ for i in os.environ['LOGOUT_MANAGER_LIBPATH'].split(":"):
+ sys.path.append(i)
+import lmlib
+
+# graphical classes and functions
+print("Loading graphics...")
+
+class Tooltip:
+
+ '''
+ It creates a tooltip for a given widget as the mouse goes on it.
+
+ see:
+
+ http://stackoverflow.com/questions/3221956/
+ what-is-the-simplest-way-to-make-tooltips-
+ in-tkinter/36221216#36221216
+
+ http://www.daniweb.com/programming/software-development/
+ code/484591/a-tooltip-class-for-tkinter
+
+ - Originally written by vegaseat on 2014.09.09.
+
+ - Modified to include a delay time by Victor Zaccardo on 2016.03.25.
+
+ - Modified
+ - to correct extreme right and extreme bottom behavior,
+ - to stay inside the screen whenever the tooltip might go out on
+ the top but still the screen is higher than the tooltip,
+ - to use the more flexible mouse positioning,
+ - to add customizable background color, padding, waittime and
+ wraplength on creation
+ by Alberto Vassena on 2016.11.05.
+
+ Tested on Ubuntu 16.04/16.10, running Python 3.5.2
+
+ TODO: themes styles support
+ '''
+
+ def __init__(self, widget,
+ *,
+ bg='#FFFFEA',
+ pad=(5, 3, 5, 3),
+ text='widget info',
+ waittime=400,
+ wraplength=250):
+
+ self.waittime = waittime # in miliseconds, originally 500
+ self.wraplength = wraplength # in pixels, originally 180
+ self.widget = widget
+ self.text = text
+ self.widget.bind("<Enter>", self.onEnter)
+ self.widget.bind("<Leave>", self.onLeave)
+ self.widget.bind("<ButtonPress>", self.onLeave)
+ self.bg = bg
+ self.pad = pad
+ self.id = None
+ self.tw = None
+
+ def onEnter(self, event=None):
+ self.schedule()
+
+ def onLeave(self, event=None):
+ self.unschedule()
+ self.hide()
+
+ def schedule(self):
+ self.unschedule()
+ self.id = self.widget.after(self.waittime, self.show)
+
+ def unschedule(self):
+ id_ = self.id
+ self.id = None
+ if id_:
+ self.widget.after_cancel(id_)
+
+ def show(self):
+ def tip_pos_calculator(widget, label,
+ *,
+ tip_delta=(10, 5), pad=(5, 3, 5, 3)):
+
+ w = widget
+
+ s_width, s_height = w.winfo_screenwidth(), w.winfo_screenheight()
+
+ width, height = (pad[0] + label.winfo_reqwidth() + pad[2],
+ pad[1] + label.winfo_reqheight() + pad[3])
+
+ mouse_x, mouse_y = w.winfo_pointerxy()
+
+ x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1]
+ x2, y2 = x1 + width, y1 + height
+
+ x_delta = x2 - s_width
+ if x_delta < 0:
+ x_delta = 0
+ y_delta = y2 - s_height
+ if y_delta < 0:
+ y_delta = 0
+
+ offscreen = (x_delta, y_delta) != (0, 0)
+
+ if offscreen:
+
+ if x_delta:
+ x1 = mouse_x - tip_delta[0] - width
+
+ if y_delta:
+ y1 = mouse_y - tip_delta[1] - height
+
+ offscreen_again = y1 < 0 # out on the top
+
+ if offscreen_again:
+ # No further checks will be done.
+
+ # TIP:
+ # A further mod might automagically augment the
+ # wraplength when the tooltip is too high to be
+ # kept inside the screen.
+ y1 = 0
+
+ return x1, y1
+
+ bg = self.bg
+ pad = self.pad
+ widget = self.widget
+
+ # creates a toplevel window
+ self.tw = tk.Toplevel(widget)
+
+ # Leaves only the label and removes the app window
+ self.tw.wm_overrideredirect(True)
+
+ win = tk.Frame(self.tw,
+ background=bg,
+ borderwidth=0)
+ label = tk.Label(win,
+ text=self.text,
+ justify=tk.LEFT,
+ background=bg,
+ relief=tk.SOLID,
+ borderwidth=0,
+ wraplength=self.wraplength)
+
+ label.grid(padx=(pad[0], pad[2]),
+ pady=(pad[1], pad[3]),
+ sticky=tk.NSEW)
+ win.grid()
+
+ x, y = tip_pos_calculator(widget, label)
+
+ self.tw.wm_geometry("+%d+%d" % (x, y))
+
+ def hide(self):
+ tw = self.tw
+ if tw:
+ tw.destroy()
+ self.tw = None
+
+def tryint(s):
+ try:
+ return int(s)
+ except:
+ return s
+
+def sort_sizes(x):
+ # Original reference so#46228101
+ value = x.split("/")[5]
+ return mynum(value, "all")
+
+def mynum(x, type = "all"):
+ # return the complicated numerical value for the weird size options
+ f = re.split("[^0-9]+",x)
+ try:
+ f0 = int(f[0])
+ except:
+ f0 = 0
+ try:
+ f1 = int(f[1])
+ except:
+ f1 = 0
+ if type == "all":
+ return f0 * 100 + f1 if len(f) >= 2 else 0
+ else:
+ return f0
+
+def find_best_size_match(size, thelist):
+ # return item from sorted thelist whose split("/")[5] is the first to meet or exceed the requested size
+ try:
+ default = thelist[-1]
+ except:
+ default = None
+ return next(( i for i in thelist if mynum(i.split("/")[5],"real") >= size ), default)
+
+def get_filename_of_icon(name, theme = "hicolor", size = 48, category = "actions"):
+ # poor man's attempt at walking through fd.o icon theme
+ filename = None
+ # example: Adwaita system-log-out
+
+ if theme == "default" or theme is None:
+ try:
+ theme = lmlib.get_gtk3_default_icon_theme()
+ except:
+ theme = "hicolor"
+
+ # first, find all files underneath /usr/share/icons/$THEME/$SIZE
+ print("Finding filename of icon, theme=",theme,"category=",category,"name=",name)
+ # to exclude the scalable/ contents, replace dir 5 asterisk with [0-9]*
+ results = []
+ base_dir="/usr/share/icons/"
+ file_filters = ".*"
+ if LM_USE_SVG == 0:
+ file_filters = ".{png,PNG}"
+ # I have no idea if this is xdg icon theme compliant, but it is a valiant attempt.
+ # 1. try (requested) req-theme, req-category, req-name first
+ results = glob.glob(base_dir+theme+"/*/"+category+"/"+name+file_filters)
+ # 2. try req-theme, (generic) gen-category, req-name
+ if len(results) == 0:
+ # no results with that category, so try all categories
+ results = glob.glob(base_dir+theme+"/*/*/"+name+file_filters)
+ # 3. try "gnome", req-category, req-name
+ if len(results) == 0:
+ results = glob.glob(base_dir+"gnome"+"/*/"+category+"/"+name+file_filters)
+ # 4. try "gnome", gen-category, req-name
+ if len(results) == 0:
+ results = glob.glob(base_dir+"gnome"+"/*/*/"+name+file_filters)
+ # 5. try "hicolor", req-category, req-name
+ if len(results) == 0:
+ results = glob.glob(base_dir+"hicolor"+"/*/"+category+"/"+name+file_filters)
+ # 6. try "hicolor", gen-category, req-name
+ if len(results) == 0:
+ results = glob.glob(base_dir+"hicolor"+"/*/*/"+name+file_filters)
+
+ # the sort arranges it so a Numix/24 dir comes before a Numix/24@2x dir
+ results = sorted(results, key=sort_sizes)
+ #print(results)
+ # now find the first one that matches
+ filename = find_best_size_match(size,results)
+ return filename
+
+def photoimage_from_svg(filename = "",size = "48"):
+ # this one works, but does not allow me to set the size.
+ # this is kept as an example of how to open a svg without saving to a file.
+ # open svg
+ item = svg2png(url=filename, parent_width = size, parent_height = size)
+ return ImageTk.PhotoImage(data=item)
+
+def empty_photoimage(size=24):
+ photo = Image.new("RGBA",[size,size])
+ return ImageTk.PhotoImage(image=photo)
+
+def image_from_svg(filename = "",size = "48"):
+ # open svg
+ if LM_USE_SVG == 1:
+ svg2png(url=filename,write_to="/tmp/lm_temp_image.png",parent_width = size,parent_height = size)
+ photo = Image.open("/tmp/lm_temp_image.png")
+ else:
+ photo = Image.new("RGBA",[size,size])
+ return photo
+
+def get_scaled_icon(icon_name, size = 24, icon_theme = "default", fallback_icon_name = ""):
+ iconfilename = None
+
+ # if name is a specific filename, just use it.
+ if Path(icon_name).is_file():
+ #print("This is a file:",icon_name)
+ iconfilename = icon_name
+ else:
+
+ if icon_theme == "default":
+ # this should not happen, because the Initialize_config should have checked gtk3 default value.
+ icon_theme = "hicolor"
+ # so now that icon_theme is defined, let us go find the icon that matches the requested name and size, in the actions category
+ #print("Using icon theme",icon_theme)
+ iconfilename = get_filename_of_icon(name=icon_name, theme=icon_theme, size=size, category=config.get_icon_category())
+
+ # So now we think we have derived the correct filename
+ try:
+ print("Trying icon file",iconfilename)
+ # try an svg
+ if re.compile(".*\.svg").match(iconfilename):
+ print("Trying svg...")
+ photo = image_from_svg(filename=iconfilename, size=size)
+ else:
+ photo = Image.open(iconfilename)
+ except Exception as f:
+ print("Error with icon file.")
+ print(f)
+ return empty_photoimage()
+ photo.thumbnail(size=[size, size])
+ try:
+ photo = ImageTk.PhotoImage(photo)
+ except Exception as e:
+ print("Error was ",e)
+ # If I ever add HiDPI support, multiple size here by the factor. So, size * 1.25
+ return photo
+
+class App:
+ def __init__(self, master):
+ frame = tk.Frame(master)
+ frame.grid(row=0)
+
+ self.photoLock = get_scaled_icon(config.get_lock_icon(), config.get_icon_size(), config.get_icon_theme())
+ self.buttonLock = tk.Button(frame, text="Lock", underline=3, command=partial(actions.lock,config), image=self.photoLock, compound=tk.LEFT)
+ master.bind_all("<Alt-k>", partial(actions.lock,config))
+ Tooltip(self.buttonLock, text="Hide session and require authentication to return to it")
+ self.buttonLock.grid(row=0,column=0)
+
+ self.photoLogout = get_scaled_icon(config.get_logout_icon(), config.get_icon_size(), config.get_icon_theme())
+ self.buttonLogout = tk.Button(frame, text="Logout", underline=0, command=lambda: actions.logout(config), image=self.photoLogout, compound=tk.LEFT)
+ master.bind_all("<Alt-l>", partial(actions.logout,config))
+ Tooltip(self.buttonLogout, text="Close the current user session")
+ self.buttonLogout.grid(row=0,column=1)
+
+ self.photoHibernate = get_scaled_icon(config.get_hibernate_icon(), config.get_icon_size(), config.get_icon_theme())
+ self.buttonHibernate = tk.Button(frame, text="Hibernate", underline=0, command=lambda: actions.hibernate(config), image=self.photoHibernate, compound=tk.LEFT)
+ master.bind_all("<Alt-h>", partial(actions.hibernate,config))
+ Tooltip(self.buttonHibernate, text="Save state to disk and power off")
+ self.buttonHibernate.grid(row=0,column=2)
+
+ self.photoShutdown = get_scaled_icon(config.get_shutdown_icon(), config.get_icon_size(), config.get_icon_theme())
+ self.buttonShutdown = tk.Button(frame, text="Shutdown", underline=0, command=lambda: actions.shutdown(config), image=self.photoShutdown, compound=tk.LEFT)
+ master.bind_all("<Alt-s>", partial(actions.shutdown,config))
+ Tooltip(self.buttonShutdown, text="Power off the computer")
+ self.buttonShutdown.grid(row=0,column=3)
+
+ self.photoReboot = get_scaled_icon(config.get_reboot_icon(), config.get_icon_size(), config.get_icon_theme())
+ self.buttonReboot = tk.Button(frame, text="Reboot", underline=0, command=lambda: actions.reboot(config), image=self.photoReboot, compound=tk.LEFT)
+ master.bind_all("<Alt-r>", partial(actions.reboot,config))
+ Tooltip(self.buttonReboot, text="Reboot the computer back to the login screen")
+ self.buttonReboot.grid(row=0,column=4)
+
+ self.buttonCancel = tk.Button(frame, text="Cancel", underline=0, command=self.quitaction)
+ master.bind_all("<Alt-c>", self.quitaction)
+ Tooltip(self.buttonCancel, text="Do nothing; just close this window")
+ self.buttonCancel.grid(row=1,columnspan=8,sticky=tk.W+tk.E)
+
+ # Found this after trial and error.
+ def quitaction(self,b=None):
+ print("Cancel any logout action.")
+ root.destroy()
+
+# Left here as an example for a mster.bind_all that works.
+#def something(event=None):
+# print("Got here!")
+
+# load configs
+config = lmlib.Initialize_config(os.environ['LOGOUT_MANAGER_CONF'])
+actions = lmlib.Actions
+
+# MAIN LOOP
+root = tk.Tk()
+root.title("Log out options")
+imgicon = get_scaled_icon(config.get_logout_icon(),24,config.get_icon_theme())
+root.tk.call('wm','iconphoto', root._w, imgicon)
+app = App(root)
+root.mainloop()
+try:
+ root.destroy()
+except:
+ pass
diff --git a/src/usr/share/logout-manager/lmlib.py b/src/usr/share/logout-manager/lmlib.py
new file mode 100644
index 0000000..7c9dc1e
--- /dev/null
+++ b/src/usr/share/logout-manager/lmlib.py
@@ -0,0 +1,286 @@
+#!/usr/bin/env python3
+# File: lmlib.py
+# License: CC-BY-SA 4.0
+# Author: bgstack15
+# Startdate: 2019-06-12
+# Title: Python libs for logout-manager
+# Purpose: Store the common elements for operating a logout-manager
+# History:
+# Usage:
+# In a logout-manager-gtk.py program
+# Reference:
+# platform info https://stackoverflow.com/questions/110362/how-can-i-find-the-current-os-in-python/10091465#10091465
+# Improve:
+# Documentation:
+
+import configparser, platform, os
+
+logout_manager_version="2020-03-10a"
+
+class Actions:
+
+ @staticmethod
+ def hibernate(config, event=None):
+ #print("need to run the /sys/power/state trick, if available")
+ print(config.get_hibernate_command())
+
+ @staticmethod
+ def lock(config, event=None):
+ #print("please lock the screen.")
+ print(config.get_lock_command())
+
+ @staticmethod
+ def logout(config, event=None):
+ #print("please log out of current session!")
+ print(config.get_logout_command())
+
+ @staticmethod
+ def reboot(config, event=None):
+ #print("please reboot.")
+ print(config.get_reboot_command())
+
+ @staticmethod
+ def shutdown(config, event=None):
+ #print("please shut yourself down!")
+ print(config.get_shutdown_command())
+
+class Config:
+ def __init__(self):
+ # load defaults which can be overwritten
+ self.hibernate_command = ""
+ self.lock_command = ""
+ self.logout_command = ""
+ self.reboot_command = ""
+ self.shutdown_command = ""
+ self.hibernate_icon = "system-hibernate"
+ self.hibernate_fallback_icon = "system-hibernate"
+ self.lock_icon = "system-lock-screen"
+ self.lock_fallback_icon = "system-lock-screen"
+ self.logout_icon = "system-log-out"
+ self.logout_fallback_icon = "system-log-out"
+ self.reboot_icon = "system-reboot"
+ self.reboot_fallback_icon = "system-reboot"
+ self.shutdown_icon = "system-shutdown"
+ self.shutdown_fallback_icon = "system-shutdown"
+ self.icon_size = 24
+ self.icon_theme = "default"
+ self.gtk3_default_icon_theme = "hicolor"
+ self.icon_category = "actions"
+ self.can_hibernate = False
+ self.application_icon=("system-log-out")
+
+ def set_hibernate_command(self,hibernate_command):
+ self.hibernate_command = hibernate_command
+
+ def set_lock_command(self,lock_command):
+ self.lock_command = lock_command
+
+ def set_logout_command(self,logout_command):
+ self.logout_command = logout_command
+
+ def set_reboot_command(self,reboot_command):
+ self.reboot_command = reboot_command
+
+ def set_shutdown_command(self,shutdown_command):
+ self.shutdown_command = shutdown_command
+
+ def set_hibernate_icon(self,hibernate_icon):
+ self.hibernate_icon = hibernate_icon
+
+ def set_lock_icon(self,lock_icon):
+ self.lock_icon = lock_icon
+
+ def set_logout_icon(self,logout_icon):
+ self.logout_icon = logout_icon
+
+ def set_reboot_icon(self,reboot_icon):
+ self.reboot_icon = reboot_icon
+
+ def set_shutdown_icon(self,shutdown_icon):
+ self.shutdown_icon = shutdown_icon
+
+ def set_icon_size(self,icon_size):
+ self.icon_size = int(icon_size)
+
+ def set_icon_theme(self,icon_theme):
+ self.icon_theme = icon_theme
+
+ def set_gtk3_default_icon_theme(self,icon_theme):
+ self.gtk3_default_icon_theme= icon_theme
+
+ def set_icon_category(self,icon_category):
+ self.icon_category = icon_category
+
+ def set_can_hibernate(self,can_hibernate):
+ print("Setting can_hibernate:",can_hibernate)
+ self.can_hibernate = bool(can_hibernate)
+
+ def get_hibernate_command(self):
+ return self.hibernate_command
+
+ def get_lock_command(self):
+ return self.lock_command
+
+ def get_logout_command(self):
+ return self.logout_command
+
+ def get_reboot_command(self):
+ return self.reboot_command
+
+ def get_shutdown_command(self):
+ return self.shutdown_command
+
+ def get_hibernate_icon(self):
+ return self.hibernate_icon
+
+ def get_lock_icon(self):
+ return self.lock_icon
+
+ def get_logout_icon(self):
+ return self.logout_icon
+
+ def get_reboot_icon(self):
+ return self.reboot_icon
+
+ def get_shutdown_icon(self):
+ return self.shutdown_icon
+
+ def get_hibernate_fallback_icon(self):
+ return self.hibernate_fallback_icon
+
+ def get_lock_fallback_icon(self):
+ return self.lock_fallback_icon
+
+ def get_logout_fallback_icon(self):
+ return self.logout_fallback_icon
+
+ def get_reboot_fallback_icon(self):
+ return self.reboot_fallback_icon
+
+ def get_shutdown_fallback_icon(self):
+ return self.shutdown_fallback_icon
+
+ def get_icon_size(self):
+ return self.icon_size
+
+ def get_icon_theme(self):
+ return self.icon_theme
+
+ def get_gtk3_default_icon_theme(self):
+ return self.gtk3_default_icon_theme
+
+ def get_icon_category(self):
+ return self.icon_category
+
+ def get_can_hibernate(self):
+ return self.can_hibernate
+
+def get_gtk3_default_icon_theme():
+ # abstracted so it does not clutter get_scaled_icon
+ name = "hicolor"
+ gtk3_config_path = os.path.join(os.path.expanduser("~"),".config","gtk-3.0","settings.ini")
+ gtk3_config = configparser.ConfigParser()
+ gtk3_config.read(gtk3_config_path)
+ try:
+ if 'Settings' in gtk3_config:
+ name = gtk3_config['Settings']['gtk-icon-theme-name']
+ elif 'settings' in gtk3_config:
+ name = gtk3_config['settings']['gtk-icon-theme-name']
+ except:
+ # supposed failsafe: keep name = hicolor
+ pass
+ print("Found gtk3 default theme:",name)
+ return name
+
+def Initialize_config(infile):
+ # Read config
+ config_in = configparser.ConfigParser()
+ config_in.read(infile)
+ config = Config()
+ try:
+ ci = config_in['logout-manager']
+ except:
+ # no definition
+ print("Using default commands")
+
+ try:
+ ci_icons = config_in['icons']
+ except:
+ # no definition
+ print("Using default icons")
+
+ # load up our custom class, which stores the defaults in case we do not set them here
+ if 'hibernate_command' in ci:
+ config.set_hibernate_command(ci['hibernate_command'])
+ if 'lock_command' in ci:
+ config.set_lock_command(ci['lock_command'])
+ if 'logout_command' in ci:
+ config.set_logout_command(ci['logout_command'])
+ if 'reboot_command' in ci:
+ config.set_reboot_command(ci['reboot_command'])
+ if 'shutdown_command' in ci:
+ config.set_shutdown_command(ci['shutdown_command'])
+ if 'hibernate' in ci_icons:
+ config.set_hibernate_icon(ci_icons['hibernate'])
+ if 'lock' in ci_icons:
+ config.set_lock_icon(ci_icons['lock'])
+ if 'logout' in ci_icons:
+ config.set_logout_icon(ci_icons['logout'])
+ if 'reboot' in ci_icons:
+ config.set_reboot_icon(ci_icons['reboot'])
+ if 'shutdown' in ci_icons:
+ config.set_shutdown_icon(ci_icons['shutdown'])
+ if 'size' in ci_icons:
+ config.set_icon_size(ci_icons['size'])
+ if 'theme' in ci_icons:
+ config.set_icon_theme(ci_icons['theme'])
+ # store the info about if hibernate is an option
+ can_hibernate = False
+ try:
+ with open('/sys/power/state') as r:
+ line = r.read()
+ if 'disk' in line: can_hibernate = True
+ except:
+ pass
+ config.set_can_hibernate(can_hibernate)
+
+ # read gtk3_default_icon_theme
+ config.set_gtk3_default_icon_theme(get_gtk3_default_icon_theme())
+ if config.get_icon_theme() == "default":
+ config.set_icon_theme(config.get_gtk3_default_icon_theme())
+
+ # set icon category
+ # written primarily for el7 which uses "app" for the system-reboot icons, etc.
+ a = platform.dist()
+ try:
+ if a[0] == "redhat" and int(a[1].split(".")[0]) <= 7:
+ config.set_icon_category("apps")
+ except:
+ pass
+
+ # DEBUG, raw from conf file and system status
+ print("Raw values:")
+ for item in config_in.sections():
+ print("["+item+"]")
+ for key in config_in[item]:
+ print(key+" = "+config_in[item][key])
+ print("Can hibernate:",can_hibernate)
+
+ # DEBUG, stored values
+ print("Stored values:")
+ print(config.get_hibernate_command())
+ print(config.get_lock_command())
+ print(config.get_logout_command())
+ print(config.get_reboot_command())
+ print(config.get_shutdown_command())
+ print(config.get_hibernate_icon())
+ print(config.get_lock_icon())
+ print(config.get_logout_icon())
+ print(config.get_reboot_icon())
+ print(config.get_shutdown_icon())
+ print(config.get_icon_size())
+ print(config.get_icon_theme())
+ print(config.get_icon_category())
+ print("Can hibernate:",config.get_can_hibernate())
+
+ return config
bgstack15