## $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 

