#!/usr/local/bin/python

# tabview.py -- View a tab-delimited file in a spreadsheet-like display.
# Contributed by A.M. Kuchling <amk1@bigfoot.com>
#
# The tab-delimited file is displayed on screen.  The highlighted
# position is shown in the top-left corner of the screen; below it are
# shown the contents of that cell.
#
#  Movement keys are:
#    Cursor keys: Move the highlighted cell, scrolling if required.
#    Q or q     : Quit
#    TAB        : Page right a screen
#    Home       : Move to the start of this line
#    End        : Move to the end of this line
#    PgUp/PgDn  : Move a page up or down
#    Insert     : Memorize this position
#    Delete     : Return to memorized position (if any)
#
# TODO : 
#    A 'G' for Goto: enter a cell like AA260 and move there
#    A key to re-read the tab-delimited file
#
# Possible projects:
#    Allow editing of cells, and then saving the modified data
#    Add formula evaluation, and you've got a simple spreadsheet
# program.  (Actually, you should allow displaying both via curses and
# via a Tk widget.)  
#

import curses, re, string

def yx2str(y,x):
    "Convert a coordinate pair like 1,26 to AA2"
    if x<26: s=chr(65+x)
    else:
	x=x-26
	s=chr(65+ (x/26) ) + chr(65+ (x%26) )
    s=s+str(y+1)
    return s

coord_pat = re.compile('^(?P<x>[a-zA-Z]{1,2})(?P<y>\d+)$')

def str2yx(s):
    "Convert a string like A1 to a coordinate pair like 0,0"
    match = coord_pat.match(s)
    if not match: return None
    y,x = match.group('y', 'x')
    x = string.upper(x)
    if len(x)==1: x=ord(x)-65
    else:
	x= (ord(x[0])-65)*26 + ord(x[1])-65 + 26
    return string.atoi(y)-1, x

assert yx2str(0,0) == 'A1'
assert yx2str(1,26) == 'AA2'
assert str2yx('AA2') == (1,26)
assert str2yx('B2') == (1,1)
	
class TabFile:
    def __init__(self, scr, filename, column_width=20):
	self.scr=scr ; self.filename = filename
	self.column_width = column_width
	f=open(filename, 'r')
	self.data = []
	while (1):
	    L=f.readline()
	    if L=="": break
	    self.data.append( string.split(L, '\t') )
#	    if len(self.data)>6: break # XXX
	self.x, self.y = 0,0
	self.win_x, self.win_y = 0,0
	self.max_y, self.max_x = self.scr.getmaxyx()
	self.num_columns = int(self.max_x/self.column_width)
	self.scr.clear()	
	self.display()

    def move_to_end(self):
	"""Move the highlighted location to the end of the current line."""

	# This is a method because I didn't want to have the code to 
	# handle the End key be aware of the internals of the TabFile object.
	yp=self.y+self.win_y ; xp=self.x+self.win_x
	if len(self.data)<=yp: end=0
	else: end=len(self.data[yp])-1
	
	# If the end column is on-screen, just change the
	# .x value appropriately.
	if self.win_x <= end < self.win_x + self.num_columns:
	    self.x = end - self.win_x
	else:
	    if end<self.num_columns:
		self.win_x = 0 ; self.x = end
	    else:
		self.x = self.num_columns-1
		self.win_x = end-self.x
	        
	
    def display(self):
	"""Refresh the current display"""
	
	self.scr.addstr(0,0, 
			yx2str(self.y + self.win_y, self.x+self.win_x)+'    ',
			curses.A_REVERSE)

	for y in range(0, self.max_y-3):
	    self.scr.move(y+2,0) ; self.scr.clrtoeol()
	    for x in range(0, int(self.max_x / self.column_width) ):
		self.scr.attrset(curses.A_NORMAL)
		yp=y+self.win_y ; xp=x+self.win_x
		if len(self.data)<=yp: s=""
		elif len(self.data[yp])<=xp: s=""
		else: s=self.data[yp][xp]
		s = string.ljust(s, 15)[0:15]
		if x==self.x and y==self.y: self.scr.attrset(curses.A_STANDOUT)
		self.scr.addstr(y+2, x*self.column_width, s)

	yp=self.y+self.win_y ; xp=self.x+self.win_x
	if len(self.data)<=yp: s=""
	elif len(self.data[yp])<=xp: s=""
	else: s=self.data[yp][xp]

	self.scr.move(1,0) ; self.scr.clrtoeol()
	self.scr.addstr(s[0:self.max_x])
	self.scr.refresh()

def main(stdscr):
    import string, curses, sys

    if len(sys.argv)==1:
	print 'Usage: tabview.py <filename>'
	return
    filename=sys.argv[1]

    # Clear the screen and display the menu of keys
    stdscr.clear()
    file = TabFile(stdscr, filename)
    
    # Main loop:
    while (1):
	stdscr.move(file.y+2, file.x*file.column_width)     # Move the cursor
	c=stdscr.getch()		# Get a keystroke
	if 0<c<256:
	    c=chr(c)
	    # Q or q exits
	    if c in 'Qq': break  
	    # Tab pages one screen to the right
	    elif c=='\t':
		file.win_x = file.win_x + file.num_columns
		file.display()
	    else: pass                  # Ignore incorrect keys

	# Cursor keys
	elif c==curses.key_up:
	    if file.y == 0:
		if file.win_y>0: file.win_y = file.win_y - 1
	    else: file.y=file.y-1
	    file.display()
	elif c==curses.key_down:
	    if file.y < file.max_y-3 -1: file.y=file.y+1
	    else: file.win_y = file.win_y+1
	    file.display()
	elif c==curses.key_left:
	    if file.x == 0:
		if file.win_x>0: file.win_x = file.win_x - 1
	    else: file.x=file.x-1
	    file.display()
	elif c==curses.key_right:
	    if file.x < int(file.max_x/file.column_width)-1: file.x=file.x+1
	    else: file.win_x = file.win_x+1
	    file.display()

	# Home key moves to the start of this line
	elif c==curses.key_home:
	    file.win_x = file.x = 0
	    file.display()
	# End key moves to the end of this line
	elif c==curses.key_end:
	    file.move_to_end()
	    file.display()

	# PageUp moves up a page
	elif c==curses.key_ppage:
	    file.win_y = file.win_y - (file.max_y - 2)
	    if file.win_y<0: file.win_y = 0
	    file.display()
	# PageDn moves down a page
	elif c==curses.key_npage:
	    file.win_y = file.win_y + (file.max_y - 2)
	    if file.win_y<0: file.win_y = 0
	    file.display()
	
	# Insert memorizes the current position
	elif c==curses.key_ic:
	    file.save_y, file.save_x = file.y + file.win_y, file.x + file.win_x
	# Delete restores a saved position
	elif c==curses.key_dc:
	    if hasattr(file, 'save_y'):
		file.x = file.y = 0
		file.win_y, file.win_x = file.save_y, file.save_x
		file.display()
        else: 
	    stdscr.addstr(0,50, curses.keyname(c)+ ' pressed')
	    stdscr.refresh()
	    pass			# Ignore incorrect keys

if __name__=='__main__':
    import curses, traceback
    try:
	# Initialize curses
	stdscr=curses.initscr()
	# Turn off echoing of keys, and enter cbreak mode,
	# where no buffering is performed on keyboard input
	curses.noecho() ; curses.cbreak()

	# In keypad mode, escape sequences for special keys
	# (like the cursor keys) will be interpreted and
	# a special value like curses.key_left will be returned
	stdscr.keypad(1)
	main(stdscr)			# Enter the main loop
	# Set everything back to normal
	stdscr.keypad(0)
	curses.echo() ; curses.nocbreak()
	curses.endwin()			# Terminate curses
    except:
        # In the event of an error, restore the terminal
	# to a sane state.
	stdscr.keypad(0)
	curses.echo() ; curses.nocbreak()
	curses.endwin()
	traceback.print_exc()		# Print the exception


