## $Id: externaleditor.py,v 1.2 2001/06/11 21:34:48 jdhildeb Exp $ ## System modules from gtk import * import gnome.zvt import gnome.ui import os, os.path, stat, tempfile, signal from time import sleep import string ## Local modules import editor # here we list the different kinds of hints that # we know about for external editors. To add the hints for some editor, # simply add it to the lists here. The tricky bit is figuring out the # hex/octal codes for non-printing characters. I used the following # procedure: # $ cat >out (to send standard input to a file) # type characters, followed by CTRL-D to end input # $ xdd out (to get a hex dump of the file, or use od to get octal dump) # another hint: use ESC for Meta key in Emacs # another hint: use vim to create a file contain the special characters; # they can be composed by using CTRL-V and then the character, then hex dump # the file. # the list of hints that appear in the preferences, with corresponding id's hintlist = [ ('No editor hints', 'none'), ('Vi hints', 'vi'), ('Emacs hints', 'emacs') ] # the hint sets vicmds = { 'default-args': '', 'insert-file-at-top': '\0331GO\033:r %s\n', 'insert-file-at-cursor': '\033O\033:r %s\n', 'insert-file-at-bottom': '\033G:r %s\n', 'write-file':'\033:w\n', 'write-file-as':'\033:w %s\n', 'before-insert-text': '\033i', 'after-insert-text': '\033', 'jump-to-top': '\0331G', 'jump-to-bottom': '\033G', 'format-current-paragraph': "\033}mj{:.,'j!fmt -w %d\n", 'exit': '\033:q\n' } emacscmds = { 'default-args': '-nw', # tell emacs not to open its own window 'insert-file-at-top': '\033<\x18i\x01\x0b%s\n', 'insert-file-at-cursor': '\x18i\x01\x0b%s\n', 'insert-file-at-bottom': '\033>\x05\n\x18i\x01\x0b%s\n', 'write-file':'\x18\x13', 'write-file-as':'\x18\x17\x01\x0b%s\n', 'before-insert-text': '', 'after-insert-text': '', 'jump-to-top': '\033<', 'jump-to-bottom': '\033>', 'exit': '\x18\x03' } # mapping from id to hint set hints = { 'none':{}, 'vi':vicmds, 'emacs':emacscmds } class ExternalEditor(editor.Editor): # create the widget and return it def widget( self ): return self.box # create the widget, use the given font if possible def __init__( self, prefs, font ): cmd = string.split( prefs.externaledentry )[0] if not os.access( cmd, 5 ): exc = "BadEditorPath" raise exc, cmd self.hints = hints[ prefs.editorhints ] self.bincmd = prefs.externaledentry font_name = prefs.externaled_font # set TERM=xterm if not set; otherwise most keybindings won't work if not os.environ.has_key('TERM') or os.environ['TERM'] in ['','dumb']: os.environ['TERM'] = 'xterm' self.term = gnome.zvt.ZvtTerm(80, 25) self.term.set_blink( FALSE ) self.term.set_scrollback(0) self.term.set_font_name(font_name); self.term.connect("child_died", self.child_died_event ) self.term.connect("button_press_event", self.button_event ) self.term.connect("focus_in_event", self.focus_in_event ) self.term.connect("focus_out_event", self.focus_out_event ) self.term.show() self.box = GtkVBox() self.box.pack_start( self.term, expand=TRUE) self.box.show() #charwidth = term.charwidth #charheight = term.charheight #win.set_geometry_hints(geometry_widget=term, #min_width=2*charwidth, min_height=2*charheight, #base_width=charwidth, base_height=charheight, #width_inc=charwidth, height_inc=charheight) # don't start the editor yet. Wait for insert_text to be # called, and then start the editor on that file. This allows # us to have at least minimal support for pretty much any editor # create a temporary dir in which we can save our files self.tempdir = tempfile.mktemp(); os.mkdir( self.tempdir, 0700 ) self.child_alive = 0 self.finished = 0 # we're not done editing yet def start_editor( self ): pid = self.term.forkpty() if pid == -1: print "Couldn't fork" os._exit(1) if pid == 0: # break up the command and the arguments cmd = string.split( self.bincmd )[0] args = string.split( self.bincmd )[1:] # if no arguments were specified explicitly, add any that # we have from our hints if args == [] and self.hints.has_key( "default-args" ): args = string.split( self.hints["default-args"] ) args.insert( 0, cmd ) args.append( self.tfilename ) os.execvp( cmd, args ) print "Couldn't exec external editor" os._exit(1) self.child_alive = 1 # release all resources def destroy( self ): self.finished = 1 # politely ask the child to exit if( self.child_alive ): self.use_hint( "exit" ) try: sleep( .3 ) except IOError: # sometimes I get "interrupted system call" here, presumably # because a signal is sent to our process when the child exits pass while events_pending(): # if the child exits, the child_died_event will be called, # so we need to give it a chance to run mainiteration(FALSE) # if child is still alive, kill it if( self.child_alive ): self.term.killchild( signal.SIGHUP ) # unlink all files in temp dir, ignoring errors # and delete the directory, too try: files = os.listdir( self.tempdir ) for f in files: os.unlink( os.path.join( self.tempdir, f ) ) os.rmdir( self.tempdir ) except OSError: pass def child_died_event( self, button=None ): # if we're not finished, restart the editor if( self.finished ): self.child_alive = 0 else: self.start_editor() # jump to top or bottom of text # pos='top' or pos='bottom' def set_scroll_position( self, pos ): if pos == 'top': self.use_hint( 'jump-to-top' ) elif pos == 'bottom': self.use_hint( 'jump-to-bottom' ) def button_event( self, button, arg ): self.grab_focus() def focus_in_event( self, button, arg ): self.term.set_blink( TRUE ) def focus_out_event( self, button, arg ): self.term.set_blink( FALSE ) def grab_focus( self ): self.term.grab_focus() def freeze( self ): pass def thaw( self ): pass def redraw( self ): pass # insert text. Possible values for 'pos' # are 'top', 'bottom', and 'cursor' def insert_text( self, text, pos='cursor' ): # if it's a one-line string, the editor already has a file open, # and we have a hint for inserting the text directly, then do it if self.child_alive and string.find( text, '\n' ) == -1 \ and self.hints.has_key('before-insert-text'): self.use_hint( "before-insert-text" ) self.term.writechild( text ) self.use_hint( "after-insert-text" ) return # otherwise save text into a file txtfname = self.mktemp(); txtfile = open( txtfname, 'w' ) txtfile.write( text ) txtfile.close() # if the editor hasn't been started, start it on this file if( not self.child_alive ): self.tfilename = txtfname self.start_editor() return self.insert_file( txtfname, pos='cursor' ) def cut_clipboard( self ): pass def copy_clipboard( self ): pass def paste_clipboard( self ): pass def insert_file( self, fname, pos ): # cause the editor to insert the file at the correct position if pos == 'top': self.use_hint( "insert-file-at-top", fname ) elif pos == 'bottom': self.use_hint( "insert-file-at-bottom", fname ) elif pos == 'cursor': self.use_hint( "insert-file-at-cursor", fname ) def format_paragraph( self, width ): self.use_hint( 'format-current-paragraph', width ) def get_text( self ): self.write_file() # load the file into a string and return it file = open( self.tfilename, 'r' ) msg = file.read() file.close() return msg def write_file( self ): # if we can't tell the editor how to save the file, we assume # that the user has saved it manually. if not self.hints.has_key( "write-file" ): return # cause editor to write the file self.use_hint( "write-file" ) if self.hints.has_key( "write-file-as" ): # cause the editor to write a second file, so # we can tell when the first has been written f2 = self.tfilename + ".2" self.use_hint( "write-file-as", f2 ) while not os.path.isfile( f2 ): sleep( 0.25 ) os.unlink( f2 ) else: # if we can't tell the editor to write a second file, # we'd better just wait a bit and hope for good luck sleep( 3 ) def get_length( self ): self.write_file() size = os.stat( self.tfilename )[stat.ST_SIZE] return size # the following functions are private def use_hint( self, key, args=None ): if( self.hints.has_key( key ) ): # turn bell off to eliminate beeps self.term.set_bell( FALSE ) if( args ): self.term.writechild( self.hints[key] % args ) else: self.term.writechild( self.hints[key] ) self.term.set_bell( TRUE ) def mktemp( self ): # name the file with a .txt extension so that emacs knows doesn't # pick an inappropriate mode s = tempfile.tempdir tempfile.tempdir = self.tempdir f = tempfile.mktemp() + ".txt" tempfile.tempdir = s return f