diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Makefile | 94 | ||||
-rw-r--r-- | src/etc/logout-manager.conf | 16 | ||||
-rw-r--r-- | src/etc/sudoers.d/30_logout-manager_sudo | 3 | ||||
-rw-r--r-- | src/etc/sysconfig/logout-manager | 2 | ||||
-rwxr-xr-x | src/usr/bin/logout-manager-cli.py | 69 | ||||
-rwxr-xr-x | src/usr/bin/logout-manager-gtk.py | 252 | ||||
-rwxr-xr-x | src/usr/bin/logout-manager-ncurses.py | 193 | ||||
-rwxr-xr-x | src/usr/bin/logout-manager-tcl.py | 417 | ||||
-rwxr-xr-x | src/usr/libexec/logout-manager/lm-helper | 74 | ||||
-rw-r--r-- | src/usr/share/applications/logout-manager.desktop | 12 | ||||
-rw-r--r-- | src/usr/share/bash-completion/completions/logout-manager | 12 | ||||
-rw-r--r-- | src/usr/share/doc/logout-manager/README.md | 40 | ||||
-rw-r--r-- | src/usr/share/doc/logout-manager/logout-manager.conf.example | 16 | ||||
-rw-r--r-- | src/usr/share/logout-manager/lmlib.py | 295 |
14 files changed, 1495 insertions, 0 deletions
diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..96607d2 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,94 @@ +# File: Makefile for logout-manager +# Location: logout-manager source package +# Author: bgstack15 +# Startdate: 2020-03-10 +# Title: Makefile for logout-manager source package +# Purpose: To use traditional Unix make utility +# History: +# Usage: +# Reference: +# https://stackoverflow.com/questions/4219255/how-do-you-get-the-list-of-targets-in-a-makefile/26339924#26339924 +# https://stackoverflow.com/questions/19105241/how-do-you-conditionally-call-a-target-based-on-a-target-variable-makefile/19107231#19107231 +# https://stackoverflow.com/questions/5917576/sort-a-text-file-by-line-length-including-spaces +# https://superuser.com/questions/352289/bash-scripting-test-for-empty-directory/667100#667100 +# bgscripts Makefile +# Improve: +# Document: +# Includes a nice way to dynamically generate dependencies as self-reported by all the files. +# Dependencies: + +APPNAME = logout-manager +APPVERSION = 0.0.1 +SRCDIR = $(CURDIR) +prefix = /usr +SYSCONFDIR = $(DESTDIR)/etc +DEFAULTDIR = $(DESTDIR)/etc/sysconfig +# for debian use '$(DESTDIR)/etc/default' +BINDIR = $(DESTDIR)$(prefix)/bin +SHAREDIR = $(DESTDIR)$(prefix)/share +LIBEXECDIR = $(DESTDIR)$(prefix)/libexec +DOCDIR = $(SHAREDIR)/doc/$(APPNAME) +APPDIR = $(SHAREDIR)/$(APPNAME) +APPSDIR = $(SHAREDIR)/applications +BASHCDIR = $(SHAREDIR)/bash-completion/completions +SUDOERSDIR = $(SYSCONFDIR)/sudoers.d + +awkbin :=$(shell which awk) +cpbin :=$(shell which cp) +echobin :=$(shell which echo) +findbin :=$(shell which find) +grepbin :=$(shell which grep) +installbin :=$(shell which install) +lnbin :=$(shell which ln) +rmbin :=$(shell which rm) +sedbin :=$(shell which sed) +sortbin :=$(shell which sort) +truebin :=$(shell which true) +uniqbin :=$(shell which uniq) +xargsbin :=$(shell which xargs) + +all: + ${echobin} "No compilation in this package." + +.PHONY: clean install uninstall list deplist deplist_opts + +list: + @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | ${awkbin} -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | ${sortbin} | ${grepbin} -E -v -e '^[^[:alnum:]]' -e '^$@$$' + +deplist: + @if test -z "$(DISTRO)" ; then ${echobin} "Please run \`make deplist\` with DISTRO= one of: `make deplist_opts 2>&1 1>/dev/null | ${xargsbin}`. Aborted." ; exit 1 ; fi + @${grepbin} -h --exclude='Makefile' --exclude-dir='doc' -A5 -riIE dependencies $(SRCDIR) | ${awkbin} -v 'distro=$(DISTRO)' 'tolower($$0) ~ distro {$$1="";$$2="";print}' | ${awkbin} 'BEGIN{cmd="${xargsbin} -n1"} $$0 !~ /\(/{print $$0 | cmd ; close(cmd);} $$0 ~ /\(/{print;}' | ${sortbin} | ${uniqbin} | ${sedbin} -r -e 's/$$/$(SEPARATOR)/' | ${xargsbin} + +deplist_opts: + @${echobin} "el7" 1>&2 + @${echobin} "devuan" 1>&2 + +install: + @${echobin} Installing files to ${DESTDIR} + ${installbin} -d ${SYSCONFDIR} ${DEFAULTDIR} ${BINDIR} \ + ${APPSDIR} ${APPDIR} ${DOCDIR} ${BASHCDIR} ${SUDOERSDIR} \ + ${LIBEXECDIR}/${APPNAME} + ${cpbin} -pr ${SRCDIR}/etc/*.* ${SYSCONFDIR} + ${cpbin} -pr ${SRCDIR}/etc/sysconfig/* ${DEFAULTDIR} + ${cpbin} -pr ${SRCDIR}/usr/bin/* ${BINDIR} + ${cpbin} -pr ${SRCDIR}/usr/share/applications/* ${APPSDIR} + ${cpbin} -pr ${SRCDIR}/usr/share/${APPNAME}/*.* ${APPDIR} + ${cpbin} -pr ${SRCDIR}/usr/share/doc/${APPNAME}/* ${DOCDIR} + ${installbin} -m 0644 -t ${BASHCDIR} ${SRCDIR}/usr/share/bash-completion/completions/* + ${installbin} -m 0640 -t ${SUDOERSDIR} ${SRCDIR}/etc/sudoers.d/* + ${installbin} -m 0755 -t ${LIBEXECDIR}/${APPNAME} ${SRCDIR}/usr/libexec/${APPNAME}/* + # symlink, when alternatives is not being used + ${lnbin} -s logout-manager-gtk.py ${BINDIR}/logout-manager + +uninstall: + @${echobin} SRCDIR=${SRCDIR} + ${rmbin} -f $$( ${findbin} ${SRCDIR} -mindepth 1 ! -type d -printf '%p\n' | ${sedbin} -r -e "s:^${SRCDIR}:${DESTDIR}:" ) ${DEFAULTDIR}/${APPNAME} ${BINDIR}/logout-manager + + # absolute minimum directories to remove + #${rmbin} -rf ${APPDIR} ${SYSCONFDIR}/${APPNAME} ${DOCDIR} + + # remove all installed directories that are now blank. + rmdir ${DEFAULTDIR} 2>/dev/null ; for word in $$( ${findbin} ${SRCDIR} -mindepth 1 -type d -printf '%p\n' | ${sedbin} -r -e "s:^${SRCDIR}:${DESTDIR}:" | ${awkbin} '{ print length, $$0 }' | sort -rn | ${awkbin} '{print $$2}' ) ; do ${findbin} $${word} -mindepth 1 1>/dev/null 2>&1 | read 1>/dev/null 2>&1 || { rmdir "$${word}" 2>/dev/null || ${truebin} ; } ; done + +clean: + -${echobin} "target $@ not implemented yet! Gotta say unh." diff --git a/src/etc/logout-manager.conf b/src/etc/logout-manager.conf new file mode 100644 index 0000000..51b86d1 --- /dev/null +++ b/src/etc/logout-manager.conf @@ -0,0 +1,16 @@ +[logout-manager] +lock_command="/usr/libexec/logout-manager/lm-helper lock" +logout_command="/usr/libexec/logout-manager/lm-helper logout" +hibernate_command="sudo /usr/libexec/logout-manager/lm-helper hibernate" +reboot_command="sudo /usr/libexec/logout-manager/lm-helper reboot" +shutdown_command="sudo /usr/libexec/logout-manager/lm-helper shutdown" + +[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/etc/sudoers.d/30_logout-manager_sudo b/src/etc/sudoers.d/30_logout-manager_sudo new file mode 100644 index 0000000..ba621eb --- /dev/null +++ b/src/etc/sudoers.d/30_logout-manager_sudo @@ -0,0 +1,3 @@ +# File: /etc/sudoers.d/30_logout-manager_sudo +Defaults env_keep += "DRYRUN VERBOSE" +ALL ALL = (root) NOPASSWD: /usr/libexec/logout-manager/lm-helper * diff --git a/src/etc/sysconfig/logout-manager b/src/etc/sysconfig/logout-manager new file mode 100644 index 0000000..1f4ecf6 --- /dev/null +++ b/src/etc/sysconfig/logout-manager @@ -0,0 +1,2 @@ +LOGOUT_MANAGER_LIBPATH=/usr/share/logout-manager +LOGOUT_MANAGER_CONF=/etc/logout-manager.conf diff --git a/src/usr/bin/logout-manager-cli.py b/src/usr/bin/logout-manager-cli.py new file mode 100755 index 0000000..64ea133 --- /dev/null +++ b/src/usr/bin/logout-manager-cli.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# File: logout-manager-cli.py +# License: CC-BY-SA 4.0 +# Author: bgstack15 +# Startdate: 2020-03-10 18:40 +# Title: cli logout manager +# Purpose: Feature completeness in this package +# History: +# Usage: +# logout-manager-cli.py +# Reference: +# https://stackoverflow.com/questions/39092149/argparse-how-to-make-mutually-exclusive-arguments-optional/39092229#39092229 +# https://stackoverflow.com/questions/3061/calling-a-function-of-a-module-by-using-its-name-a-string/12025554#12025554 +# Improve: +# Dependencies: +# Devuan: python3-dotenv python3 +# Documentation: + +import os, platform, sys, argparse +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 + +def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + +logout_manager_cli_version="2020-03-10" + +parser = argparse.ArgumentParser(description="run logout-manager commands using cli") +parser.add_argument('action', help='which action to take',nargs='?', choices=('lock','logout','hibernate','shutdown','reboot')) +parser.add_argument("-d","--debug", nargs='?', default=0, type=int, choices=range(0,11), help="Set debug level.") +parser.add_argument("-n","--dryrun", action='store_true', help="only report. Useful for checking if hibernate is allowed.") +parser.add_argument("-V","--version", action="version", version="%(prog)s " + logout_manager_cli_version) + +args = parser.parse_args() + +# load configs +# in cli, must happen after arparse to benefit from debug value +config = lmlib.Initialize_config(os.environ['LOGOUT_MANAGER_CONF']) +actions = lmlib.Actions + +# MAIN LOOP +allowed_actions=['lock','logout','shutdown','reboot'] +if config.can_hibernate: + allowed_actions.append('hibernate') + +if args.action in allowed_actions: + func = getattr(globals()['actions'],args.action) + func(config) +elif args.action: + eprint("Unable to take action: %s" % str(args.action)) + sys.exit(1) + +# if we get here, no action was used +parser.print_help() +sys.exit(2) 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/libexec/logout-manager/lm-helper b/src/usr/libexec/logout-manager/lm-helper new file mode 100755 index 0000000..6372827 --- /dev/null +++ b/src/usr/libexec/logout-manager/lm-helper @@ -0,0 +1,74 @@ +#!/bin/sh +# Dependencies: +# Devuan: wmctrl sudo +# el7: wmctrl sudo +case "${1}" in + help) # show this help screen + { + echo "Usage: ${0}: [command]" + echo "used by logout-manager to perform actions like reboot, lock screen, etc." + echo "" + echo "Commands:" + grep -E '^\s{3}[A-Za-z]+\)' "${0}" | tr -dc '[A-Za-z\n ]' | sed -r -e 's/\s+/ /g;' | grep -v "HIDDEN\s*$" | while read a therest ; do echo " ${a}: ${therest}" ; done + } + ;; + options) # used by bash_completion function HIDDEN + grep -E '^\s{3}[A-Za-z]+\)' "${0}" | awk '{print $1}' | tr -dc '[A-Za-z\n]' | grep -vE 'options|help' + ;; + lock) # lock the current screen + if test -z "${DRYRUN}" ; + then + xscreensaver --locknow + else + echo "xscreensaver --locknow" + fi + ;; + logout) # log out the current user of the graphical session + # determine DE/WM and act accordingly + _wm="$( wmctrl -m | awk '/Name:/{$1="";print;}' | xargs )" + case "${_wm}" in + Fluxbox) + if test -z "${DRYRUN}" ; + then + fluxbox-remote exit + else + echo "fluxbox-remote exit" + fi + ;; + *) + echo "Gotta say unh! Feature not yet implemented for \"${_wm}\"" 1>&2 + exit 1 + ;; + esac + ;; + hibernate) # save system state to disk and power off + # this method is linux only + if test -z "${DRYRUN}" ; + then + printf 'disk' | tee /sys/power/state + else + echo "printf 'disk' | tee /sys/power/state" + fi + ;; + shutdown) # power off + if test -z "${DRYRUN}" ; + then + shutdown -h now + else + echo "shutdown -h now" + fi + ;; + reboot) # restart the system + if test -z "${DRYRUN}" ; + then + shutdown -r now + else + echo "shutdown -r now" + fi + ;; + *) # HIDE + echo "invalid choice: ${1}" 1>&2 + exit 1 + ;; +esac +: diff --git a/src/usr/share/applications/logout-manager.desktop b/src/usr/share/applications/logout-manager.desktop new file mode 100644 index 0000000..4522a66 --- /dev/null +++ b/src/usr/share/applications/logout-manager.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Categories=Settings;HardwareSettings; +Comment=Prompt for common actions including lock screen, logout, etc. +Exec=/usr/bin/logout-manager +GenericName=Logout menu +Icon=system-log-out +Keywords=shutdown;hibernate;lockscreen;logout;reboot; +Name=Logout... +StartupNotify=true +Terminal=false +Type=Application +Version=1.0 diff --git a/src/usr/share/bash-completion/completions/logout-manager b/src/usr/share/bash-completion/completions/logout-manager new file mode 100644 index 0000000..fd1267f --- /dev/null +++ b/src/usr/share/bash-completion/completions/logout-manager @@ -0,0 +1,12 @@ +# File: /etc/bash_completion.d/logout-manager +# Reference: +# bgscripts-core: /usr/bin/bp +# man complete + +_lm_helper() { + local cur prev words cword; + _init_completion || return + COMPREPLY=($( compgen -W "$( ~/dev/logout-manager/src/usr/libexec/logout-manager/lm-helper options )" -- "$cur" )) + return 0 +} && \ +complete -F _lm_helper -o bashdefault lm-helper diff --git a/src/usr/share/doc/logout-manager/README.md b/src/usr/share/doc/logout-manager/README.md new file mode 100644 index 0000000..d34f02c --- /dev/null +++ b/src/usr/share/doc/logout-manager/README.md @@ -0,0 +1,40 @@ +# README for logout-manager +## Introduction +Logout Manager is a python3 utility that provides a simple menu for logout-type actions. The supported actions are presented: + * Lock + * Logout + * Hibernate (if supported by hardware) + * Shutdown + * Reboot + +## Customization +The `lm-helper` logout command needs to be customized for every desktop environment. Some may need extra configurationon the window manager/desktop environment side. + +### Fluxbox +For Fluxbox, you need to set a value in ~/.fluxbox/init + + session.screen0.allowRemoteActions: true + +Be aware that this is insecure. See man `fluxbox-remote(1)`. + +## Alternatives +[oblogout](https://launchpad.net/oblogout) looks really old so I did not investigate personally, but it sounds like it does the same thing I am trying to do. +`apt-cache search logout` shows [lxsession-logout](http://manpages.ubuntu.com/manpages/precise/en/man1/lxsession-logout.1.html) which was compiled, as well as does not provide configurable options for changing executed commands or icons. + +## License +[logout-manager-ncurses.py](src/usr/bin/logout-manager-ncurses.py) is licensed under the [MIT license](http://choosealicense.com/licenses/mit) and is derived almost entirely from [adamlamers](http://adamlamers.com/post/FTPD9KNRA8CT). +Everything else is licensed under [CC-BY-SA 4.0](https://choosealicense.com/licenses/cc-by-sa-4.0/). + +## Description +This project is partially a programming playground for the [original author](https://bgstack15.wordpress.com) and partially a useful project for his migration to [Fluxbox](http://fluxbox.org/) on the desktop. + +## Upsides +* This project is the first to [demonstrate SVG images in tkinter in python3](https://bgstack15.wordpress.com/2019/07/13/display-svg-in-tkinter-python3/) that I could find on the Internet. +* This project demonstrates how to have the Makefile and debian/rules build a dependency list, from the Dependencies tags of the files themselves. +* I have learned how to work with ncurses, gtk, and tcl in python3. +* This will make Fluxbox systems easier to use for general users. +* Does not use dbus! + +## Downsides +* This whole thing is more complex than just logging out of my user session, and selecting a logout-type action from the display manager. +* Depends on sudo instead of using native tools. diff --git a/src/usr/share/doc/logout-manager/logout-manager.conf.example b/src/usr/share/doc/logout-manager/logout-manager.conf.example new file mode 100644 index 0000000..1a14909 --- /dev/null +++ b/src/usr/share/doc/logout-manager/logout-manager.conf.example @@ -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/share/logout-manager/lmlib.py b/src/usr/share/logout-manager/lmlib.py new file mode 100644 index 0000000..40ee3a0 --- /dev/null +++ b/src/usr/share/logout-manager/lmlib.py @@ -0,0 +1,295 @@ +#!/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, subprocess + +logout_manager_version="2020-03-10a" + +class Actions: + + def __take_action(command): + print(command) + command=str(command).split() + command2=[] + for i in command: + command2.append(str(i.strip('"'))) + command=command2 + subprocess.run(command) + + @staticmethod + def hibernate(config, event=None): + #print("need to run the /sys/power/state trick, if available") + Actions.__take_action(config.get_hibernate_command()) + + @staticmethod + def lock(config, event=None): + #print("please lock the screen.") + Actions.__take_action(config.get_lock_command()) + + @staticmethod + def logout(config, event=None): + #print("please log out of current session!") + Actions.__take_action(config.get_logout_command()) + + @staticmethod + def reboot(config, event=None): + #print("please reboot.") + Actions.__take_action(config.get_reboot_command()) + + @staticmethod + def shutdown(config, event=None): + #print("please shut yourself down!") + Actions.__take_action(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 |