diff --git a/foldermenu.py b/foldermenu.py index bd0f59e..d972fc1 100755 --- a/foldermenu.py +++ b/foldermenu.py @@ -12,11 +12,53 @@ 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')) + +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("-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',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",action="store_false",dest="colors",default=True, + help="Disable colored output") + parser.add_argument("--horizontal",action="store_true",dest="horizontal",default=False, + help="Horizontal order of items, only valid for -l listing.") + 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' @@ -34,10 +76,9 @@ class bc: self.GRE = '' self.YEL = '' self.RED = '' - self.END = '' - self.CLR = '' self.CYA = '' self.WHI = '' + self.END = '' def pos(self,y,x): return "\033["+str(y)+";"+str(x)+"H" @@ -57,172 +98,283 @@ class getch: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch -def read_menu(): - ''' Read menu file ''' +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=[] - if os.path.exists(MENUFILE): - f=file(MENUFILE,'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]+')', row[1]] - entries.append(row) - return entries - -def read_folder(): - ''' Read folder contents, return executable files and dirs ''' - dirs=[] - dirs.append(['..','..']) - executables=[] - for f in os.listdir('.'): - if os.path.isfile(f): - if os.access(f,os.X_OK): - executables.append([f,f]) - if os.path.isdir(f) and f[0]!='.': - dirs.append([f,f]) - dirs.sort(key=lambda x: x[0]) - executables.sort(key=lambda x: x[0]) - - return (executables,dirs) - -def print_help(): - if not os.path.exists(MENUFILE): - print('Consider having a '+MENUFILE+' file containing shell commands / line.') - print('Command may be of format "My Description: my_command" or simply "my_command -switch"') - -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) - -def drawmenu(entries,dir_mode,args=""): - maxrows,maxcolumns = termsize() - maxrows-=5 - maxcolumns-=10 - twocol=False + menu_keys=[ichr(i+1) for i in range(60)] + args='' + dir_mode=False co=bc() - if dir_mode: - helptext=".:execs" - else: - helptext='-:args ('+co.END+args+co.YEL+') .:folders' + 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() + 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; - print(co.END+co.CLR+co.pos(1,3)+co.YEL+'FolderMenu x:exit '+helptext+co.END) - if len(entries)>10: - twocol=True - maxrows=int(math.ceil(min(maxrows/2.0, len(entries)/2.0))) - maxcolumns=int(math.ceil(maxcolumns/2.0)) - r=1 - for e in range(len(entries)): - if r>maxrows: - break - printline=entries[e][1] - if len(printline)>maxcolumns: - printline=printline[:maxcolumns]+"..." - print(co.WHI+entries[e][0]+co.END+' '+entries[e][3]+printline+co.END) - r=1+r - if twocol: + 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], row[1].strip()+' ('+row[0].strip()+')'] + self.entries.append(launch_item(command=row[0], + description=row[1], + 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() + maxrows-=5 + maxcolumns-=10 + if self.options.columns==0: + pars=float(1) + if len(my_entries)>9: + pars=float(2) + 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) + maxrows=int(math.ceil(min(maxrows/pars, len(my_entries)/pars))) + maxcolumns=int(math.ceil(maxcolumns/pars)) r=1 - for e in range(e,len(entries)): + par=1 + for e,i in zip(my_entries,self.menu_keys): if r>maxrows: - break - printline=entries[e][1] + par=1+par + r=1 + printline=e.description if len(printline)>maxcolumns: printline=printline[:maxcolumns]+"..." - print(co.pos(r+1,maxcolumns)+'| '+co.WHI+entries[e][0]+co.END+' '+entries[e][3]+printline+co.END) + 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(co.pos(maxrows+2,0)) - -def append_index(entries,offset=0,color=None,t='menu'): - e=1+offset - for el in range(len(entries)): - entries[el]=[ichr(e), entries[el][0], entries[el][1], color,t] - e=e+1 - return entries + print(self.co.pos(maxrows+2,0)) -def launch(key,entries,args=""): - ''' launch the given program ''' - bg=False - idx=[i for i in range(len(entries)) if entries[i][0]==key][0] - command_str=entries[idx][2] - if command_str[-1]=='&': - command_str=command_str[:-1] - bg=True - if len(args)>0: - command_str=command_str+" "+args - if entries[idx][4]=='exec': - command_str='./'+command_str - if entries[idx][4]=='dir': - os.chdir(command_str) - return - try: - print('#$ '+command_str) - if bg: - subprocess.Popen(command_str, stderr=subprocess.PIPE, shell=True,executable="/bin/bash") + def list(self): + """ draws the list at cursor """ + maxrows,maxcolumns = termsize() + 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: - subprocess.call(command_str, stderr=subprocess.STDOUT, shell=True,executable="/bin/bash") - except: - print('Unable to run: "'+command_str+'"') + pars=float(self.options.columns) + maxrows=int(math.ceil(min(maxrows/pars, len(self.entries)/pars))) + maxcolumns=int(math.ceil(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 + maxrows=int(math.ceil(min(origmaxrows/pars, len(self.entries)/pars))) + maxcolumns=int(math.ceil(origmaxcolumns/pars))-2 - print('Press any key...') - ch=getch() - inkey=ord(ch.get()) - -def initialize(): - entries=read_menu() - entries=append_index(entries, color=bc.CYA,t='menu') - [execs,dirs]=read_folder() - execs=append_index(execs, offset=len(entries), color=bc.GRE,t='exec') - entries.extend(execs) - dirs=append_index(dirs,color=bc.BLU+bc.WHI,t='dir') - return (entries,dirs) + self.max_length=min(maxcolumns,self.max_length) + if self.options.horizontal: + foo=pars + pars=maxrows + maxrows=foo + formatted=[] + for r in range(int(maxrows)): + 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>=maxrows: + 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 main(): - [entries,dirs]=initialize() - show_entries=entries - dir_mode=False + def launch(self,key): + ''' launch the given entry ''' + + bg=False + #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 not (self.options.command or bg): + 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() - args="" - drawmenu(show_entries,dir_mode,args) while True: + entries.menu() inkey=ord(ch.get()) #print('-'+str((inkey))+'-') if inkey in [120,27,3,24,4]: - print_help() print('Exited in: '+os.getcwd()) sys.exit(0) if inkey==45: # - - readline.set_startup_hook(lambda: readline.insert_text(args)) + 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: # . - dir_mode = not dir_mode - - if chr(inkey) in [x[0] for x in show_entries]: - launch(chr(inkey),show_entries,args) - [entries,dirs]=initialize() - if dir_mode: - show_entries=dirs - else: - show_entries=entries + entries.flip_mode() + found,message=entries.is_key(chr(inkey)) + if found: + entries.launch(chr(inkey)) + entries.initialize() - drawmenu(show_entries,dir_mode,args) - -main() +start_engines()