#!/usr/bin/env python3 # File: logout-manager-ncurses # License: MIT # Author: adamlamers, bgstack15 # Startdate: 2020-03-09 17:06 # Title: ncurses based logout manager # Usage: # logout-manager-ncurses # 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: # dep-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)