475 lines
15 KiB
Python
475 lines
15 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
|
|
|
|
import filecheck
|
|
import colors
|
|
|
|
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):
|
|
self.canvas.before.clear()
|
|
|
|
# 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" or self.argv[0]=="export":
|
|
# 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()
|
|
|
|
# commands that cannot be compounded
|
|
if self.argv[0]=="set":
|
|
self.run_set(self.argv)
|
|
return
|
|
|
|
# single letter commands (which can be combined)
|
|
# write
|
|
if self.argv[0]=="w" or self.argv[0]=="w!" or self.argv[0]=="wq":
|
|
self.run_write(self.argv)
|
|
# edit file
|
|
if self.argv[0]=="e":
|
|
self.run_edit(self.argv)
|
|
# export to file
|
|
if self.argv[0]=="export":
|
|
self.run_export(self.argv)
|
|
# quit
|
|
if self.argv[0]=="q" or self.argv=="wq":
|
|
self.app.stop()
|
|
|
|
# set properties of particles
|
|
def run_set(self,argv):
|
|
if len(argv)<2:
|
|
self.message="error: 'set' command was run with too few arguments -- usage: 'set <property> <value>'"
|
|
return
|
|
|
|
if argv[1]=="color":
|
|
self.run_set_color(argv)
|
|
return
|
|
elif argv[1]=="grid":
|
|
self.run_set_grid(argv)
|
|
return
|
|
elif argv[1]=="zoom":
|
|
self.run_set_zoom(argv)
|
|
else:
|
|
self.message="error: unrecognized command '"+argv[1]+"'"
|
|
return
|
|
|
|
# set color
|
|
def run_set_color(self,argv):
|
|
if len(argv)<3:
|
|
self.message="error: 'set color' command was run with without anargument -- usage: 'set color <color_descriptor>'"
|
|
return
|
|
# find color name in list
|
|
for color in colors.xcolor_names:
|
|
if argv[2]==color[0]:
|
|
self.app.painter.set_color(color[1:])
|
|
return
|
|
|
|
# numerical rgb values
|
|
color_str=argv[2].split(",")
|
|
# error if improperly formatted
|
|
if len(color_str)!=3:
|
|
self.message="error: unrecognized color specification '"+argv[2]+"'; supported format is 'r,g,b' or one of red|green|blue|brown|lime|orange|pink|purple|teal|violet|cyan|magenta|yellow|olive|black|darkgray|gray|lightgray|white or an SVG color name"
|
|
|
|
return
|
|
try:
|
|
color=(float(color_str[0]),float(color_str[1]),float(color_str[2]))
|
|
except:
|
|
self.message="error: unrecognized color specification '"+argv[2]+"'; supported format is 'r,g,b' or one of red|green|blue|brown|lime|orange|pink|purple|teal|violet|cyan|magenta|yellow|olive|black|darkgray|gray|lightgray|white or an SVG color name"
|
|
return
|
|
self.app.painter.set_color(color)
|
|
|
|
# toggle grid
|
|
def run_set_grid(self,argv):
|
|
if len(argv)==2:
|
|
# no argument: set to toggle
|
|
self.app.painter.set_grid(-1)
|
|
elif argv[2]=="on":
|
|
self.app.painter.set_grid(1)
|
|
elif argv[2]=="off":
|
|
self.app.painter.set_grid(0)
|
|
else:
|
|
try:
|
|
mesh=float(argv[2])
|
|
except:
|
|
self.message="error: unrecognized argument '"+argv[2]+"' -- usage 'set grid [on|off|<float>]'"
|
|
return
|
|
if mesh<0:
|
|
self.message="error: grid size cannot be negative: '"+argv[2]+"'"
|
|
return
|
|
self.app.painter.set_grid(mesh)
|
|
|
|
# set zoom level (changes size of elements)
|
|
def run_set_zoom(self,argv):
|
|
if len(argv)==2:
|
|
self.message="error: missing argument in 'set zoom'"
|
|
return
|
|
else:
|
|
try:
|
|
zoom_level=float(argv[2])
|
|
except:
|
|
self.message="error: unrecognized argument '"+argv[2]+"' -- usage 'set zoom <zoom_level>'"
|
|
return
|
|
if zoom_level<0:
|
|
self.message="error: zoom level cannot be negative: '"+argv[2]+"'"
|
|
return
|
|
self.app.painter.set_zoom(zoom_level)
|
|
|
|
|
|
|
|
# write to file
|
|
def run_write(self,argv):
|
|
if len(argv)>2:
|
|
self.message="error: could not write to file: too many arguments -- usage: ':w [path to file]'"
|
|
return
|
|
elif len(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
|
|
|
|
(ret,self.message)=filecheck.check_write(argv[1],argv[0]=="w!")
|
|
# add comment if no overwrite
|
|
if ret==-2:
|
|
self.message+=" (use ':w!' to overwrite)"
|
|
if ret<0:
|
|
return
|
|
|
|
self.app.openfile=argv[1]
|
|
self.app.readonly=False
|
|
self.app.painter.write(argv[1])
|
|
# update status bar
|
|
self.app.status_bar.draw()
|
|
|
|
# edit file
|
|
def run_edit(self,argv):
|
|
if len(argv)>2:
|
|
self.message="error: could not open file: too many arguments -- usage: ':e <path to file>'"
|
|
return
|
|
elif len(argv)==1:
|
|
self.message="error: could not open file: no argument found -- usage: ':e <path to file>'"
|
|
return
|
|
|
|
# check that the file can be edited
|
|
(ret,self.message)=filecheck.check_edit(argv[1])
|
|
# error
|
|
if ret<0:
|
|
return
|
|
|
|
if os.path.isfile(argv[1]):
|
|
# read the file
|
|
self.app.painter.read(argv[1])
|
|
# set readonly mode
|
|
self.app.readonly=not os.access(argv[1],os.W_OK)
|
|
else:
|
|
# new file, reset painter
|
|
self.app.painter.reset()
|
|
self.app.readonly=False
|
|
|
|
# select openfile in app
|
|
self.app.openfile=argv[1]
|
|
# update status bar
|
|
self.app.status_bar.draw()
|
|
|
|
# export to file in Tikz format
|
|
def run_export(self,argv):
|
|
if len(argv)>2:
|
|
self.message="error: could not open file: too many arguments -- usage: ':export <path to file>'"
|
|
return
|
|
elif len(argv)==1:
|
|
self.message="error: could not open file: no argument found -- usage: ':export <path to file>'"
|
|
return
|
|
|
|
# check that the file can be edited
|
|
(ret,self.message)=filecheck.check_edit(argv[1])
|
|
# error
|
|
if ret<0:
|
|
return
|
|
|
|
# export
|
|
self.app.painter.export_tikz(argv[1])
|