585 lines
18 KiB
Python
Executable File
585 lines
18 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
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 ")
|
|
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
|
|
|
|
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 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 == 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()
|
|
|
|
|