Jam/command_prompt.py

393 lines
13 KiB
Python

from kivy.uix.label import Label
from kivy.core.window import Window
from kivy.graphics import Color,Rectangle
import glob
import os.path
class Command_prompt(Label):
def __init__(self,app,**kwargs):
# app is used to share information between widgets
self.app=app
# insert mode
self.insert=False
# width of a letter
self.char_width=9
# cursor position
self.cursor=0
# the text, with no color information
self.raw_text=""
# a one-time message that shows up over the bar
self.message=""
# array of command with arguments
self.argv=[]
# display characters from window_offset onwards (for text wrapping)
self.window_offset=0
# init Label
super(Command_prompt,self).__init__(**kwargs)
self.keyboard = Window.request_keyboard(None,self,"text")
self.keyboard.bind(on_textinput=self.on_textinput,on_key_down=self.on_key_down)
self.draw()
def draw(self):
# background
with self.canvas.before:
Color(0,0,0)
Rectangle(pos=self.pos,size=self.size)
# if message is not empty, draw message instead
if self.message!="":
# do not wrap
self.text=self.message[:min(len(self.message),int(self.width/self.char_width))]
self.message=""
return
# wrap text
window_size=int(self.width/self.char_width)
if self.cursor>=self.window_offset+window_size:
self.window_offset=self.cursor-window_size+1
elif self.cursor<self.window_offset:
self.window_offset=self.cursor
# do not stop right before ':'
if self.window_offset==1:
self.window_offset=0
# cursor
with self.canvas.before:
Color(1,1,1)
# wrap cursor position
Rectangle(pos=(self.pos[0]+(self.cursor-self.window_offset)*self.char_width,self.pos[1]),size=(self.char_width,self.height))
# make text under cursor black
end=min(len(self.raw_text),self.window_offset+window_size)
if self.cursor<end:
self.text=self.raw_text[self.window_offset:self.cursor]+"[color=000]"+self.raw_text[self.cursor]+"[/color]"+self.raw_text[self.cursor+1:end]
else:
self.text=self.raw_text[self.window_offset:end]
def on_key_down(self, keyboard, keycode, text, modifiers):
#print(keycode,text,modifiers)
if self.insert:
# process modifiers
mods=self.process_modifiers(modifiers)
# process command
if keycode[1]=="enter":
self.insert=False
self.run_command()
self.set_text("")
elif keycode[1]=="escape":
self.insert=False
self.set_text("")
# move
elif keycode[1]=="left":
self.move_cursor_relative(-1)
elif keycode[1]=="right":
self.move_cursor_relative(1)
elif mods==['c'] and text=="a":
self.move_cursor(1)
elif mods==['c'] and text=="e":
self.move_cursor(len(self.raw_text))
# delete
elif keycode[1]=="backspace":
self.backspace()
elif keycode[1]=="delete":
self.delete_forward()
elif mods==['c'] and text=="k":
self.delete_all_forward()
# tab completion
elif keycode[1]=="tab":
self.tab_complete()
def on_textinput(self,window,text):
# reset status_bar
self.app.status_bar.draw()
if self.insert:
self.append_text_at_cursor(text)
elif text==':':
self.append_text_at_cursor(text)
self.insert=True
# get modifiers from list
# returns an alphabetically ordered array of characters
# 'a' is alt
# 'c' is ctrl
# 'm' is meta
# 's' is shift
# ignore all others
def process_modifiers(self,modifiers):
out=[]
for mod in modifiers:
if mod=="alt":
out.append('a')
elif mod=="ctrl":
out.append('c')
elif mod=="meta":
out.append('m')
elif mod=="shift":
out.append('s')
out.sort()
return out
# append to text at position of cursor
def append_text_at_cursor(self,text):
if self.cursor==len(self.raw_text):
self.raw_text+=text
else:
self.raw_text=self.raw_text[:self.cursor]+text+self.raw_text[self.cursor:]
self.cursor+=len(text)
self.draw()
# delete before cursor
def backspace(self):
# do not delete leading ':'
if self.cursor==1:
return
if self.cursor==len(self.raw_text):
self.raw_text=self.raw_text[:self.cursor-1]
else:
self.raw_text=self.raw_text[:self.cursor-1]+self.raw_text[self.cursor:]
self.cursor-=1
self.draw()
# delete after cursor
def delete_forward(self):
if self.cursor==len(self.raw_text):
return
else:
self.raw_text=self.raw_text[:self.cursor]+self.raw_text[self.cursor+1:]
self.draw()
# delete until end of line(self):
def delete_all_forward(self):
if self.cursor==len(self.raw_text):
return
else:
self.raw_text=self.raw_text[:self.cursor]
self.draw()
# set text in prompt
def set_text(self,text):
self.raw_text=text
self.cursor=len(text)
self.draw()
# move cursor n steps to right (left if negative)
def move_cursor_relative(self,n):
if n==0 or (n>0 and self.cursor==len(self.raw_text)) or (n<0 and self.cursor==0):
return
self.cursor=max(1,min(len(self.raw_text),self.cursor+n))
self.draw()
# move cursor to absolute position
def move_cursor(self,n):
self.cursor=max(1,min(len(self.raw_text),n))
self.draw()
# parse text as argv
def parse_argv(self):
# init
self.argv=[""]
# return position of cursor
cursor=(0,0)
# whether inside quotes
single_quoted=False
double_quoted=False
# whether after '\'
backslashed=False
# start after ':'
# proceed one character at a time
for i in range(1,len(self.raw_text)):
char=self.raw_text[i]
# split argument
if single_quoted==False and double_quoted==False and backslashed==False and char==' ':
# new argv
self.argv.append("")
# quotes or '\'
elif double_quoted==False and backslashed==False and char=='\'':
single_quoted=not single_quoted
elif single_quoted==False and backslashed==False and char=='"':
double_quoted=not double_quoted
elif single_quoted==False and backslashed==False and char=='\\':
backslashed=True
# write character
else:
self.argv[len(self.argv)-1]+=char
# reset backslash
backslashed=False
# record position of cursor
if self.cursor==i+1:
cursor=(len(self.argv)-1,len(self.argv[len(self.argv)-1]))
return cursor
# tab completion
def tab_complete(self):
# parse argv
cursor=self.parse_argv()
# write and edit commands
if self.argv[0]=="w" or self.argv[0]=="e":
# check that cursor is in first argument
if cursor[0]==1:
# complete filesystem path
self.append_text_at_cursor(self.complete_path(self.argv[cursor[0]],cursor[1]))
# complete filesystem path
def complete_path(self,base,end):
paths=glob.glob(glob.escape(base[:end])+"*")
print(glob.escape(base[:end])+"*", paths)
if len(paths)==0:
return ""
elif len(paths)==1:
# append '/' to directories
if os.path.isdir(paths[0]):
return paths[0][end:]+"/"
else:
return paths[0][end:]
else:
# display in status bar
self.app.status_bar.message=""
for path in paths:
name=os.path.basename(path)
# add quotes if needed
if " " in name:
name="\""+name+"\""
self.app.status_bar.message+=name+" "
self.app.status_bar.draw()
return os.path.commonprefix(paths)[end:]
# run command
def run_command(self):
# parse command line
self.parse_argv()
# write
if self.argv[0]=="w" or self.argv[0]=="w!" or self.argv[0]=="wq":
if len(self.argv)>2:
self.message="error: could not write to file: too many arguments -- usage: ':w [path to file]'"
return
elif len(self.argv)==1:
if self.app.openfile!="":
if self.app.readonly:
self.message="error: open file is readonly"
return
self.app.painter.write(self.app.openfile)
return
else:
self.message="error: no file is open for editing, specify a path"
return
# check that the file is not a directory
if os.path.isdir(self.argv[1]):
self.message="error: '"+self.argv[1]+"' is a directory"
return
# check that file does not already exist
if self.argv[0]!="w!" and self.argv[1]!= self.app.openfile and os.path.isfile(self.argv[1]):
self.message="error: '"+self.argv[1]+"' already exists (use ':w!' to overwrite)"
return
# check that the containing directory exists
if len(os.path.dirname(self.argv[1]))>0 and not os.path.isdir(os.path.dirname(self.argv[1])):
self.message="error: could not find directory '"+os.path.dirname(self.argv[1])+"'"
return
# check permissions
if os.path.isfile(self.argv[1]):
if not os.access(self.argv[1],os.W_OK):
self.message="error: permission denied: cannot write to file '"+self.argv[1]+"'"
return
else:
# check write to directory
if len(os.path.dirname(self.argv[1]))>0:
if not os.access(os.path.dirname(self.argv[1]),os.W_OK):
self.message="error: permission denied: cannot write to parent directory '"+os.path.dirname(self.argv[1])+"'"
return
else:
if not os.access(os.getcwd(),os.W_OK):
self.message="error: permission denied: cannot write to parent directory '"+os.getcwd()+"'"
return
# if we haven't returned yet
self.app.openfile=self.argv[1]
self.app.readonly=False
self.app.painter.write(self.argv[1])
# update status bar
self.app.status_bar.draw()
# edit file
if self.argv[0]=="e":
if len(self.argv)>2:
self.message="error: could not open file: too many arguments -- usage: ':e <path to file>'"
return
elif len(self.argv)==1:
self.message="error: could not open file: no argument found -- usage: ':e <path to file>'"
return
# check that the file is not a directory
if os.path.isdir(self.argv[1]):
self.message="error: '"+self.argv[1]+"' is a directory"
return
if os.path.isfile(self.argv[1]):
# check permissions
if not os.access(self.argv[1],os.R_OK):
self.message="error: permission denied: cannot read file '"+self.argv[1]+"'"
return
# read the file
self.app.painter.read(self.argv[1])
# set readonly mode
self.app.readonly=not os.access(self.argv[1],os.W_OK)
else:
# check write to directory
if len(os.path.dirname(self.argv[1]))>0:
if not os.access(os.path.dirname(self.argv[1]),os.W_OK):
self.message="error: permission denied: cannot write to parent directory '"+os.path.dirname(self.argv[1])+"'"
return
else:
if not os.access(os.getcwd(),os.W_OK):
self.message="error: permission denied: cannot write to parent directory '"+os.getcwd()+"'"
return
# new file, reset painter
self.app.painter.reset()
self.app.readonly=False
# if we haven't returned yet
# select openfile
self.app.openfile=self.argv[1]
# update status bar
self.app.status_bar.draw()
# quit
if self.argv[0]=="q" or self.argv=="wq":
self.app.stop()