#!/usr/bin/python import os import sys import subprocess import termios import tty import readline import math 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.3" 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.") parser.add_argument("-1","--one-shot",action='store_true', dest='once',default=False, help="Launch only once, then exit") 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 x ''' if i < 10: return str(i) i=i + 87 if i>119: i=i+1 if i>122: i=i-122+64 return chr(i) class bc: MAG = '\033[95m' BLU = '\033[94m' GRE = '\033[92m' YEL = '\033[93m' RED = '\033[91m' CYA = '\033[96m' WHI = '\033[1m' END = '\033[0m' CLR = '\033[2J' def disable(self): self.MAG = '' self.BLU = '' self.GRE = '' self.YEL = '' self.RED = '' self.CYA = '' self.WHI = '' self.END = '' def pos(self,y,x): return "\033["+str(y)+";"+str(x)+"H" 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 ''' description='' command='' launcher='' 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 ''' options=[] entries=[] dirs=[] menu_keys=[ichr(i+1) for i in range(60)] args='' dir_mode=False co=bc() max_length=0 def __init__(self, options): self.options=options self.initialize() if not self.options.colors: self.co.disable() def initialize(self): self.entries=[] self.dirs=[] 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): f=file(menu_file,'r') for row in f: if row.strip()=='': continue if row[0]=='#': continue row=row.strip().split(':',1) if len(row)==1: row=[row[0], row[0]] else: row=[row[0].strip()+' ('+row[1].strip()+')',row[1]] self.entries.append(launch_item(command=row[1], description=row[0], launcher="menu")) 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=".:executables" my_entries=self.dirs else: helptext='-:args ('+self.co.END+self.args+self.co.YEL+') .:folders' 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) print(self.co.END+self.co.CLR+self.co.pos(1,3)+self.co.YEL+'FolderMenu x:exit '+helptext+self.co.END) 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 par=1 for e,i in zip(my_entries,self.menu_keys): if r>rows: par=1+par r=1 printline=e.description if len(printline)>maxcolumns: printline=printline[:maxcolumns]+"..." if par==1: print(self.co.WHI+i+self.co.END+' '+self.entry_color(e.launcher)+printline+self.co.END) else: print(self.co.pos(r+1,maxcolumns*(par-1))+'| '+self.co.WHI+i+self.co.END+' '+self.entry_color(e.launcher)+printline+self.co.END) r=1+r print(self.co.pos(rows+2,0)) 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=float(1) if len(self.entries)>9: pars=math.floor(maxcolumns/float(self.max_length)) pars=max(1,pars) if len(self.entries)/pars < pars: 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=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: foo=pars pars=rows rows=foo 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 e,i in zip(self.entries,self.menu_keys): if r>=rows: par=1+par r=0 printline=e.description[:(maxcolumns-3)] printline=printline+' '*(self.max_length-len(printline)-1) formatted[r][par]=self.co.WHI+i+self.co.END+' '+self.entry_color(e.launcher)+printline+self.co.END r=1+r if self.options.horizontal: # let's shuffle the deck, and print values in horizontal order: formatted=zip(*formatted) for row in formatted: print '|'.join(row) def launch(self,key): ''' launch the given entry ''' bg=False wait=True #Run the program in background idx=self.menu_keys.index(key) # note, no error checking here if self.dir_mode: command_str=self.dirs[idx].command os.chdir(command_str) return # continue here if not changing folders command_str=self.entries[idx].command if command_str[-1]=='&': command_str=command_str[:-1] bg=True if len(self.args)>0: command_str=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('Unable to run: "'+command_str+'"') if (self.options.command or self.options.once or bg): wait=False if wait: print('Press any key...') ch=getch() inkey=ord(ch.get()) def flip_mode(self): self.dir_mode=not self.dir_mode 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 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()) #print('-'+str((inkey))+'-') if inkey in [120,27,3,24,4]: print('Exited in: '+os.getcwd()) sys.exit(0) if inkey==45: # - 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() 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() start_engines()