#!/usr/bin/env python3 import math import os import readline import subprocess import sys import termios import time import tty readline.parse_and_bind("tab: complete") readline.parse_and_bind("set editing-mode vi") MENUFILE = ".foldermenu" DEFAULTFILE = os.path.expanduser(os.path.join("~", ".config", "foldermenu", "default")) VERSION = "0.5" DEBUG = False def setup_options(): """ Setup the command line options """ from argparse import ArgumentParser parser = ArgumentParser( description="Prints folder specific shell commands stored in '" + MENUFILE + "' file, and in addition the executables in the current folder. " + "Menufile format for each line: 'description:command'. " + "If the command ends in '&' it is run in the background. " + "If the command ends in '/' it is a folder, and selecting will enter. " + "Extra keyboard shortcuts: / Edit, $ Shell " ) parser.add_argument( "-1", "--one-shot", action="store_true", dest="once", default=False, help="Launch only once, then exit", ) parser.add_argument( "-b", "--no-banner", action="store_false", dest="banner", default=True, help="Do not show banners. Banners are ## starting lines in the file", ) parser.add_argument( "-d", "--no-defaults", action="store_false", dest="defaults", default=True, help="Do not show default entries from " + DEFAULTFILE, ) parser.add_argument( "-x", "--no-exec", action="store_false", dest="executables", default=True, help="Do not show executables in the listing.", ) parser.add_argument( "--columns", "-f", "-C", type=int, action="store", dest="columns", default=0, help="Number of columns. 0 for automatic", ) parser.add_argument( "-l", "--list", action="store_true", dest="list", default=False, help="Print the list, don't wait for keypress.", ) parser.add_argument( "--command", "-c", type=str, action="store", dest="command", help="Command to run (1-9a-z..), any argumets after -- are forwarded to the command ", ) parser.add_argument( "--no-colors", "--nc", action="store_false", dest="colors", default=True, help="Disable colored output", ) parser.add_argument( "--horizontal", "-H", action="store_true", dest="horizontal", default=False, help="Horizontal order of items, only valid for -l listing.", ) parser.add_argument("--version", action="version", version=VERSION) parser.add_argument( "args", type=str, action="store", default="", nargs="?", help="Arguments for the command, if -c used. The string will be re-parsed with shutils. Use '--' to skip local parsing.", ) options = parser.parse_args() if not os.path.exists(DEFAULTFILE): options.defaults = False return options def termsize(): rows, columns = os.popen("stty size", "r").read().split() return (int(rows), int(columns)) def ichr(i): """ convert integer to 1-9, a-z, A-Z, omitting q,x """ if i < 10: return str(i) i += 87 if i > 112: i += 1 if i > 119: i += 1 if i > 122: i += 64 - 122 return chr(i) class bc: MAG = "\033[35m" BLU = "\033[34m" GRE = "\033[32m" YEL = "\033[33m" RED = "\033[31m" CYA = "\033[36m" WHI = "\033[1m" BG_BLK = "\033[40m" END = "\033[0m" CLR = "\033[2J" INV = "\033[7m" def disable(self): self.MAG = "" self.BLU = "" self.GRE = "" self.YEL = "" self.RED = "" self.CYA = "" self.WHI = "" self.END = "" self.BG_BLK = "" self.INV = "" def pos(self, y, x): return "\033[%s;%sH" % (y, x) def posprint(self, y, x, s): sys.stdout.write(self.pos(y, x) + str(s)) class getch: def __init__(self): import sys, tty, termios def get(self): fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) ch = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch class launch_item: """ Class for launchable items """ def __init__(self, command, description, launcher): self.command = command self.description = description self.launcher = launcher class entry_collection: """ Object containing the list items, and the printing methods """ def __init__(self, options): self.options = options self.menu_keys = [ichr(i + 1) for i in range(60)] self.args = "" self.co = bc() self.dir_mode = False self.max_length = 0 self.selected = -1 self.rows = 0 self.initialize() if not self.options.colors: self.co.disable() def initialize(self): self.entries = [] self.dirs = [] self.banner = [] self.selected = -1 if self.options.defaults: self.read_menu(DEFAULTFILE) self.read_menu() self.read_folder() self.entries = self.entries[0:60] self.dirs = self.dirs[0:60] if len(self.entries) > 0: self.max_length = max([len(e.description) for e in self.entries]) + 1 def set_args(self, args): self.args = args def read_menu(self, menu_file=MENUFILE): """ Read the menu file """ if os.path.exists(menu_file): with open(menu_file, "rt") as f: for row in f: if row.strip() == "": continue if row[0:2] == "##": self.banner.append(row[2:].replace("\n", "").replace("\r", "")) continue if row[0] == "#": continue row = row.strip().split(":", 1) if len(row) == 1: row = [row[0].strip(), row[0]] else: row = [row[0].strip(), row[1]] if len(row[1]) == 0: row[1] = " " launcher = "menu" if row[1][-1] == "/" and os.path.isdir(row[1]): launcher = "dir" if row[0][-1] != "/": row[0] += "/" self.entries.append( launch_item( command=row[1], description=row[0], launcher=launcher ) ) def read_folder(self): """ Read folder contents, return executable files and dirs """ self.dirs.append(launch_item(command="..", description="..", launcher="dir")) dirs = [] executables = [] for f in os.listdir("."): if os.path.isfile(f): if os.access(f, os.X_OK): executables.append(f) if os.path.isdir(f) and f[0] != ".": dirs.append(f) dirs.sort() for d in dirs: self.dirs.append( launch_item(command=d, description=d + "/", launcher="dir") ) if self.options.executables: executables.sort() for e in executables: self.entries.append( launch_item(command=e, description=e, launcher="exec") ) def entry_color(self, launcher): if launcher == "dir": return self.co.WHI if launcher == "exec": return self.co.GRE if launcher == "menu": return self.co.CYA def menu(self): """ draws the menu at the top of the screen """ if self.dir_mode: helptext = "[.]commands" my_entries = self.dirs else: helptext = "[.]folders [-]args %s(%s%s%s)" % ( self.co.END + self.co.MAG, self.co.END, self.args, self.co.MAG, ) my_entries = self.entries maxrows, maxcolumns = termsize() rows = maxrows - 5 maxcolumns -= 10 if self.options.columns == 0: pars = 1 if len(my_entries) > 9: pars = 2 if len(my_entries) > 30: pars = 3 pars = float(pars) else: pars = float(self.options.columns) if self.options.banner: banner = self.banner else: banner = [] blen = len(banner) cwd = os.path.basename(os.getcwd())[0:15] self.co.posprint(1, 1, self.co.END + self.co.CLR) self.co.posprint( 1, 3, "%s%s%s [q/x]exit %s%s" % (self.co.WHI, cwd, self.co.MAG, helptext, self.co.END), ) for i, e in enumerate(banner): self.co.posprint(i + 2, 0, e) rows = int(math.ceil(len(my_entries) / pars)) while rows > maxrows: pars += 1 rows = int(math.ceil(len(my_entries) / pars)) maxcolumns = int(math.ceil(maxcolumns / pars)) r = 1 + blen par = 1 for index, (entry, key) in enumerate(zip(my_entries, self.menu_keys)): if r - blen > rows: par += 1 r = 1 + blen printline = entry.description if len(printline) > maxcolumns: printline = printline[:maxcolumns] + "..." if par == 1: column = 2 border = "" else: column = maxcolumns * (par - 1) border = "| " if self.selected == index: highlight = self.co.INV + ">" else: highlight = " " self.co.posprint( r + 1, column, "%s%s%s%s%s%s%s%s" % ( border, self.co.WHI, key, self.co.END, self.entry_color(entry.launcher), highlight, printline, self.co.END, ), ) r += 1 self.co.posprint(rows + 2 + blen, 0, "#") self.rows = rows sys.stdout.flush() def list(self): """ draws the list at cursor """ maxrows, maxcolumns = termsize() rows = maxrows - 5 maxcolumns -= 10 # heuristics for guessing column count if self.options.columns == 0: pars = 1.0 if len(self.entries) > 9: pars = max(1.0, math.floor(maxcolumns / float(self.max_length))) while len(self.entries) / pars < pars: pars -= 1 else: pars = float(self.options.columns) rows = int(math.ceil(len(self.entries) / float(pars))) maxcolumns = int(math.floor(maxcolumns / pars)) - 2 # If names won't fit the columns, make sure at least 3 characters are visible if maxcolumns < 6: origmaxrows, origmaxcolumns = termsize() origmaxrows -= 5 origmaxcolumns -= 10 while maxcolumns < 6: pars -= 1 rows = int(math.ceil(len(self.entries) / float(pars))) maxcolumns = int(math.floor(origmaxcolumns / pars)) - 2 self.max_length = min(maxcolumns, self.max_length) if self.options.horizontal: pars, rows = rows, pars formatted = [] for r in range(int(rows)): formatted.append([]) for p in range(int(pars)): formatted[r].append(" " * (self.max_length)) if self.options.horizontal: formatted[r][p] += " " r = 0 par = 0 for entry, key in zip(self.entries, self.menu_keys): if r >= rows: par += 1 r = 0 printline = entry.description[: (maxcolumns - 3)] printline += " " * (self.max_length - len(printline) - 1) formatted[r][par] = "%s%s%s %s%s%s" % ( self.co.WHI, key, self.co.END, self.entry_color(entry.launcher), printline, self.co.END, ) r += 1 if self.options.horizontal: # let's shuffle the deck, and print values in horizontal order: formatted = zip(*formatted) if self.options.banner: if self.banner: print("\n".join(self.banner)) for row in formatted: print("|".join(row)) def launch(self, key): """ launch the given entry """ bg = False idx = self.menu_keys.index(key) chdir = False if self.dir_mode: command_str = self.dirs[idx].command chdir = True else: command_str = self.entries[idx].command chdir = self.entries[idx].launcher == "dir" if chdir: if os.path.isdir(command_str): os.chdir(command_str) self.selected = -1 return if command_str[-1] == "&": # Run the program in background command_str = command_str[:-1] bg = True if len(self.args) > 0: command_str += " " + self.args if self.entries[idx].launcher == "exec": command_str = "./" + command_str if not self.options.command: print(command_str) try: if bg: subprocess.Popen( command_str, stderr=subprocess.PIPE, shell=True, executable="/bin/bash", ) else: subprocess.call( command_str, stderr=subprocess.STDOUT, shell=True, executable="/bin/bash", ) except: print('Non-zero exit code: "' + command_str + '"') if not (self.options.command or self.options.once or bg): print("Press any key...") ch = getch() inkey = ord(ch.get()) def flip_mode(self): self.dir_mode = not self.dir_mode self.selected = 0 def is_key(self, key): if self.dir_mode: my_len = len(self.dirs) else: my_len = len(self.entries) try: idx = self.menu_keys.index(key) except ValueError: return (False, "Not a possible key") if idx + 1 > my_len: return (False, "No such entry") return (True, "") def select_move(self, delta): new_value = self.selected + delta if new_value < 0: return if self.dir_mode: max_value = len(self.dirs) - 1 else: max_value = len(self.entries) - 1 if new_value > max_value: return self.selected = new_value def edit_menu(self): subprocess.call( "vim %s" % (MENUFILE,), stderr=subprocess.STDOUT, shell=True, executable="/bin/bash", ) self.initialize() def shell(self): # env = os.environ.copy() # env["PS1"] = "fM:" + env.get("PS1","") subprocess.call( "bash --rcfile <(cat ~/.bashrc; echo \"PS1='folderMenu: '\$PS1\") -i", stderr=subprocess.STDOUT, shell=True, executable="/bin/bash", ) self.initialize() def debug_code_print(c): print("- code: %d, str: %s -" % (c, str(c))) time.sleep(1) def start_engines(): options = setup_options() entries = entry_collection(options) if options.list: entries.list() if not options.command: sys.exit(0) if options.command: found, message = entries.is_key(options.command) if not found: print(message) sys.exit(1) entries.set_args(options.args) entries.launch(options.command) sys.exit(0) ch = getch() while True: entries.menu() inkey = ord(ch.get()) if DEBUG: debug_code_print(inkey) if inkey == 27: inkey2 = ord(ch.get()) if DEBUG: debug_code_print(inkey2) if inkey2 == 91: inkey3 = ord(ch.get()) if DEBUG: debug_code_print(inkey3) if inkey3 == 66: entries.select_move(1) if inkey3 == 65: entries.select_move(-1) if inkey3 == 67: entries.select_move(entries.rows) if inkey3 == 68: entries.select_move(-entries.rows) # 66 = down # 65 = up # 67 = right # 68 = left # 53 = pg up # 54 = pg down # # ~ print(inkey3) # ~ sys.exit(0) if inkey in (113, 120, 3, 24, 4): # q, x print("Exited in: " + os.getcwd()) sys.exit(0) if inkey == 45: # - print("") readline.set_startup_hook(lambda: readline.insert_text(entries.args)) args = raw_input("args: ") entries.set_args(args) readline.set_startup_hook(None) if inkey == 46: # . entries.flip_mode() if inkey == 47: # / entries.edit_menu() if inkey == 36: # $ entries.shell() if inkey == 13: # enter inkey = ord(entries.menu_keys[entries.selected]) found, message = entries.is_key(chr(inkey)) if found: entries.launch(chr(inkey)) if options.once and not entries.dir_mode: sys.exit(0) entries.initialize() if __name__ == "__main__": start_engines()