aboutsummaryrefslogtreecommitdiff
path: root/src/usr/bin/logout-manager-tcl.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/usr/bin/logout-manager-tcl.py')
-rwxr-xr-xsrc/usr/bin/logout-manager-tcl.py417
1 files changed, 417 insertions, 0 deletions
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
bgstack15