exit from pyview by ESC
[lfm.git] / lfm / pyview.py
blob511d5a2da158c794b6180836a75e6f60aa6dba54
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 # Copyright (C) 2001-7 Iñigo Serna
5 # Time-stamp: <2007-09-03 23:18:41 inigo>
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 """
22 Copyright (C) 2001-7, Iñigo Serna <inigoserna@telefonica.net>.
23 All rights reserved.
25 This software has been realised under the GPL License, see the COPYING
26 file that comes with this package. There is NO WARRANTY.
28 'pyview' is a simple pager (viewer) to be used with Last File Manager.
29 """
32 import os, os.path
33 import sys
34 import time
35 import thread
36 import getopt
37 import logging
38 import curses, curses.ascii
40 from __init__ import *
41 import messages
44 ########################################################################
45 ##### module variables
46 app = None
47 LOG_FILE = os.path.join(os.getcwd(), 'pyview.log')
50 ########################################################################
51 # read from stdin
52 def read_stdin():
53 """Read from stdin with 1 sec. timeout. Returns text"""
55 from select import select
57 try:
58 fd = select([sys.stdin], [], [], 0.5)[0][0]
59 stdin = ''.join(fd.readlines())
60 # close stdin (pipe) and open terminal for reading
61 os.close(0)
62 sys.stdin = open(os.ttyname(1), 'r')
63 except IndexError:
64 stdin = ''
65 return stdin
68 def create_temp_for_stdin(buf):
69 """Copy stdin in a temporary file. Returns file name"""
71 from tempfile import mkstemp
73 filename = mkstemp()[1]
74 f = open(filename, 'w')
75 f.write(buf)
76 f.close()
77 return filename
80 ##################################################
81 ##### Internal View
82 ##################################################
83 class InternalView:
84 """Internal View class"""
86 def __init__(self, title, buf, center = 1):
87 self.title = title
88 self.__validate_buf(buf, center)
89 self.init_curses()
92 def __validate_buf(self, buf, center):
93 buf = [(l[0][:app.maxw-2], l[1] ) for l in buf]
94 buf2 = [l[0] for l in buf]
95 self.nlines = len(buf2)
96 if self.nlines > app.maxh - 2:
97 self.y0 = 0
98 self.large = 1
99 self.y = 0
100 else:
101 self.y0 = int(((app.maxh-2) - self.nlines)/2)
102 self.large = 0
103 if center:
104 col_max = max(map(len, buf2))
105 self.x0 = int((app.maxw-col_max)/2)
106 else:
107 self.x0 = 1
108 self.y0 = not self.large
109 self.buf = buf
112 def init_curses(self):
113 curses.cbreak()
114 curses.raw()
115 curses.curs_set(0)
116 try:
117 self.win_title = curses.newwin(1, 0, 0, 0)
118 self.win_body = curses.newwin(app.maxh-2, 0, 1, 0) # h, w, y, x
119 self.win_status = curses.newwin(1, 0, app.maxh-1, 0)
120 except curses.error:
121 print 'Can\'t create windows'
122 sys.exit(-1)
124 if curses.has_colors():
125 self.win_title.bkgd(curses.color_pair(1),
126 curses.color_pair(1) | curses.A_BOLD)
127 self.win_body.bkgd(curses.color_pair(2))
128 self.win_status.bkgd(curses.color_pair(1))
129 self.win_body.keypad(1)
131 self.win_title.erase()
132 self.win_status.erase()
133 title = self.title
134 if len(title) - 4 > app.maxw:
135 title = title[:app.maxw-12] + '~' + title[-7:]
136 self.win_title.addstr(0, int((app.maxw-len(title))/2), title)
137 if self.large:
138 status = ''
139 else:
140 status = 'Press a key to continue'
141 self.win_status.addstr(0, int((app.maxw-len(status))/2), status)
143 self.win_title.refresh()
144 self.win_status.refresh()
147 def show(self):
148 """show title, file and status bar"""
150 self.win_body.erase()
151 if self.large:
152 buf = self.buf[self.y:self.y+app.maxh-2]
153 else:
154 buf = self.buf
155 for i, (l, c) in enumerate(buf):
156 self.win_body.addstr(self.y0+i, self.x0, l, curses.color_pair(c))
157 self.win_body.refresh()
160 def run(self):
161 self.show()
162 if self.large:
163 quit = 0
164 while not quit:
165 self.show()
166 ch = self.win_body.getch()
167 if ch in (ord('k'), ord('K'), curses.KEY_UP):
168 self.y = max(self.y-1, 0)
169 if ch in (ord('j'), ord('J'), curses.KEY_DOWN):
170 self. y = min(self.y+1, self.nlines-1)
171 elif ch in (curses.KEY_HOME, 0x01):
172 self.y = 0
173 elif ch in (curses.KEY_END, 0x05):
174 self.y = self.nlines - 1
175 elif ch in (curses.KEY_PPAGE, 0x08, 0x02, curses.KEY_BACKSPACE):
176 self.y = max(self.y-app.maxh-2, 0)
177 elif ch in (curses.KEY_NPAGE, ord(' '), 0x06):
178 self. y = min(self.y+app.maxh-2, self.nlines-1)
179 elif ch in (0x1B, ord('q'), ord('Q'), ord('x'), ord('X'),
180 curses.KEY_F3, curses.KEY_F10):
181 quit = 1
182 else:
183 while not self.win_body.getch():
184 pass
187 ##################################################
188 ##### pyview
189 ##################################################
190 class FileView:
191 """Main application class"""
193 def __init__(self, win, filename, line, mode, stdin_flag):
194 global app
195 app = self
196 self.win = win # root window, need for resizing
197 self.file = filename
198 self.mode = mode
199 self.wrap = 0
200 self.stdin_flag = stdin_flag
201 self.init_curses()
202 self.pos, self.col = 0, 0
203 messages.app = self
204 try:
205 self.__get_file_info(filename)
206 except (IOError, os.error), (errno, strerror):
207 messages.error('%s' % PYVIEW_NAME,
208 '%s (%s)' % (strerror, errno), filename)
209 sys.exit(-1)
210 if self.nbytes == 0:
211 messages.error('View \'%s\'' % filename, 'File is empty')
212 sys.exit(-1)
213 self.fd = open(filename)
214 self.line = 0
215 try:
216 if mode == MODE_TEXT:
217 self.__move_lines((line or 1) - 1)
218 else:
219 self.pos = min(line, self.nbytes)
220 self.__move_hex(0)
221 except IndexError:
222 pass
223 self.pattern = ''
224 self.matches = []
225 self.bookmarks = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1]
228 def __get_file_info(self, filename):
229 """get size and number of lines of the file"""
231 self.nbytes = os.path.getsize(filename)
232 self.lines_pos = [0L]
233 if self.nbytes != 0:
234 pos = 0L
235 f = open(filename)
236 for nlines, l in enumerate(f.readlines()):
237 pos += len(l)
238 self.lines_pos.append(pos)
239 f.close()
240 self.nlines = nlines + 1
243 def init_curses(self):
244 """initialize curses stuff: windows, colors, ..."""
246 self.maxh, self.maxw = self.win.getmaxyx()
247 curses.cbreak()
248 curses.raw()
249 curses.curs_set(0)
250 try:
251 self.win_title = curses.newwin(1, 0, 0, 0)
252 self.win_file = curses.newwin(self.maxh-2, 0, 1, 0) # h, w, y, x
253 self.win_status = curses.newwin(1, 0, self.maxh-1, 0)
254 except curses.error:
255 print 'Can\'t create windows'
256 sys.exit(-1)
258 # colors
259 if curses.has_colors():
260 curses.init_pair(1, curses.COLOR_YELLOW, curses.COLOR_BLUE) # title
261 curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK) # files
262 curses.init_pair(3, curses.COLOR_BLUE, curses.COLOR_CYAN) # current file
263 curses.init_pair(4, curses.COLOR_MAGENTA, curses.COLOR_CYAN) # messages
264 curses.init_pair(5, curses.COLOR_GREEN, curses.COLOR_BLACK) # help
265 curses.init_pair(6, curses.COLOR_RED, curses.COLOR_BLACK) # file info
266 curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_RED) # error messages
267 curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_RED) # error messages
268 curses.init_pair(9, curses.COLOR_YELLOW, curses.COLOR_RED) # button in dialog
269 curses.init_pair(10, curses.COLOR_YELLOW, curses.COLOR_BLACK) # file selected
270 curses.init_pair(11, curses.COLOR_YELLOW, curses.COLOR_CYAN) # file selected and current
271 self.win_title.bkgd(curses.color_pair(1),
272 curses.color_pair(1) | curses.A_BOLD)
273 self.win_file.bkgdset(curses.color_pair(2))
274 self.win_status.bkgdset(curses.color_pair(1))
275 self.win_file.keypad(1)
278 def resize_window(self):
279 h, w = self.win.getmaxyx()
280 self.maxh, self.maxw = h, w
281 if w == 0 or h == 2:
282 return
283 self.win.resize(h, w)
284 self.win_title.resize(1, w)
285 self.win_file.resize(h-2, w)
286 self.win_status.resize(1, w)
287 self.win_status.mvwin(h-1, 0)
288 self.show()
291 def __move_lines(self, lines):
292 if lines >= 0:
293 self.line = min(self.line+lines, self.nlines-1)
294 # # Code that don't show blank lines when in last lines of file
295 # if self.wrap:
296 # if self.line + lines > self.nlines - 1 - (curses.LINES-2):
297 # # get last (curses.LINES -2 ) lines
298 # pos = self.lines_pos[self.nlines - 1 - (curses.LINES-2)]
299 # self.fd.seek(pos)
300 # lines = []
301 # for i in xrange(curses.LINES-2):
302 # l = self.fd.readline().rstrip().replace('\t', ' ' * 4)
303 # lines.append(l)
304 # lines.reverse()
305 # # calculate lines that fit in screen
306 # lno = 0
307 # i= 0
308 # while lno < curses.LINES-2:
309 # lno += int(len(lines[i])/curses.COLS) + 1
310 # i += 1
311 # # calculate horizontal scrolling if needed
312 # self.col = curses.COLS * (lno - (curses.LINES-2))
313 # self.line = self.nlines - 1 - i
314 # else:
315 # self.line += lines
316 # else:
317 # if self.line + lines > self.nlines - 1 - (curses.LINES-2):
318 # self.line = self.nlines - 1 - (curses.LINES-2)
319 # else:
320 # self.line += lines
321 else:
322 self.line = max(0, self.line+lines)
323 self.pos = self.lines_pos[self.line]
324 self.fd.seek(self.lines_pos[self.line], 0)
327 def __get_lines_text(self):
328 lines = []
329 for i in xrange(self.maxh-2):
330 l = self.fd.readline().rstrip().replace('\t', ' ' * 4)
331 lines.append(l)
332 self.fd.seek(self.pos)
333 self.col_max = max(map(len, lines))
334 return lines
337 def __get_prev_lines_text(self):
338 lines = []
339 i = 0
340 for i in xrange(self.maxh-2):
341 line_i = self.line - 1 - i
342 if line_i < 0:
343 break
344 self.fd.seek(self.lines_pos[line_i])
345 lines.append(self.fd.readline().rstrip().replace('\t', ' ' * 4))
346 self.fd.seek(self.pos)
347 return lines
350 def __get_line_length(self):
351 return len(self.__get_1line())
354 def __get_1line(self):
355 line = self.fd.readline().rstrip().replace('\t', ' ' * 4)
356 self.fd.seek(self.pos)
357 return line
360 def show_chr(self, w, c):
361 if curses.ascii.iscntrl(c) or ord(c) in xrange(0x7F, 0xA0):
362 w.addch(ord('.'))
363 elif curses.ascii.isascii(c):
364 w.addch(curses.ascii.ascii(c))
365 elif curses.ascii.ismeta(c):
366 w.addstr(c)
367 else:
368 w.addch(ord('.'))
371 def show_str(self, w, line):
372 for i in xrange(len(line)):
373 c = line[i]
374 if ord(c) == ord('\r'):
375 pass
376 self.show_chr(w, c)
379 def show_text_nowrap(self):
380 lines = self.__get_lines_text()
381 self.win_file.refresh()
382 for y, l in enumerate(lines):
383 lwin = curses.newpad(1, self.maxw+1)
384 lwin.erase()
385 if len(l) - self.col > self.maxw:
386 largeline = True
387 else:
388 largeline = False
389 buf = l[self.col:self.col+self.maxw]
390 self.show_str(lwin, buf)
391 lwin.refresh(0, 0, y+1, 0, y+1, self.maxw-1)
392 if largeline:
393 lwin2 = curses.newpad(1, 2)
394 lwin2.erase()
395 lwin2.addch('>', curses.color_pair(2) | curses.A_BOLD)
396 lwin2.refresh(0, 0, y+1, self.maxw-1, y+1, self.maxw-1)
397 del(lwin2)
398 del(lwin)
399 self.win_file.refresh()
402 def show_text_wrap(self):
403 lines = self.__get_lines_text()
404 lines[0] = lines[0][self.col:] # show remaining chars of 1st line
405 self.win_file.refresh()
406 y = 0
407 for l in lines:
408 if y > self.maxh - 2:
409 break
410 if len(l) <= self.maxw:
411 lwin = curses.newpad(1, self.maxw+1)
412 lwin.erase()
413 self.show_str(lwin, l)
414 lwin.refresh(0, 0, y+1, 0, y+1, self.maxw-1)
415 del(lwin)
416 y += 1
417 else:
418 while len(l) > 0:
419 lwin = curses.newpad(1, self.maxw+1)
420 lwin.erase()
421 self.show_str(lwin, l[:self.maxw])
422 lwin.refresh(0, 0, y+1, 0, y+1, self.maxw-1)
423 del(lwin)
424 y += 1
425 if y > self.maxh - 2:
426 break
427 l = l[self.maxw:]
428 self.win_file.refresh()
431 def __move_hex(self, lines):
432 self.pos = self.pos & 0xFFFFFFF0L
433 if lines > 0:
434 self.pos = min(self.pos+lines*16, self.nbytes-1)
435 else:
436 self.pos = max(0, self.pos+lines*16)
437 self.fd.seek(self.pos, 0)
438 for i in xrange(len(self.lines_pos)):
439 ls = self.lines_pos[i]
440 if self.pos <= ls:
441 self.line = max(0, i-1)
442 break
443 else:
444 self.line = i
447 def __get_lines_hex(self, chars_per_line=16):
448 self.__move_hex(0)
449 lines = self.fd.read(chars_per_line * (self.maxh-2))
450 le = len(lines)
451 if le != chars_per_line * (self.maxh-2):
452 for i in xrange(chars_per_line * (self.maxh-2) - le):
453 lines += chr(0)
454 self.fd.seek(self.pos)
455 return lines
458 def show_hex(self):
459 if self.maxw >= 152:
460 chars_per_line = 32
461 elif self.maxw >= 134:
462 chars_per_line = 28
463 elif self.maxw >= 116:
464 chars_per_line = 24
465 elif self.maxw >= 98:
466 chars_per_line = 20
467 elif self.maxw >= 80:
468 chars_per_line = 16
469 elif self.maxw >= 62:
470 chars_per_line = 12
471 elif self.maxw >= 44:
472 chars_per_line = 8
473 elif self.maxw >= 26:
474 chars_per_line = 4
475 else:
476 return
477 lines = self.__get_lines_hex(chars_per_line)
478 self.win_file.erase()
479 self.win_file.refresh()
480 for y in xrange(self.maxh-2):
481 lwin = curses.newpad(1, self.maxw+1)
482 lwin.erase()
483 attr = curses.color_pair(2) | curses.A_BOLD
484 lwin.addstr(0, 0, '%8.8X ' % (self.pos+chars_per_line*y), attr)
485 for i in xrange(chars_per_line/4):
486 buf = ''.join(['%2.2X ' % (ord(lines[y*chars_per_line+4*i+j]) & 0xFF) \
487 for j in xrange(4)])
488 lwin.addstr(buf)
489 if i != chars_per_line/4 - 1:
490 lwin.addch(curses.ACS_VLINE)
491 lwin.addch(' ')
492 for i in xrange(chars_per_line):
493 c = lines[y*chars_per_line+i]
494 self.show_chr(lwin, c)
495 lwin.refresh(0, 0, y+1, 0, y+1, self.maxw-1)
496 self.win_file.refresh()
499 def show_title(self):
500 if self.maxw > 20:
501 if self.stdin_flag:
502 title = 'STDIN'
503 else:
504 title = os.path.basename(self.file)
505 if len(title) > self.maxw-52:
506 title = title[:self.maxw-58] + '~' + title[-5:]
507 self.win_title.addstr('File: %s' % title)
508 if self.maxw >= 67:
509 if self.col != 0 or self.wrap:
510 self.win_title.addstr(0, int(self.maxw/2)-14, 'Col: %d' % self.col)
511 buf = 'Bytes: %d/%d' % (self.pos, self.nbytes)
512 self.win_title.addstr(0, int(self.maxw/2)-5, buf)
513 buf = 'Lines: %d/%d' % (self.line+1, self.nlines)
514 self.win_title.addstr(0, int(self.maxw*3/4)-4, buf)
515 if self.maxw > 5:
516 self.win_title.addstr(0, self.maxw-5,
517 '%3d%%' % (int(self.pos*100/self.nbytes)))
520 def show_status(self):
521 if self.maxw > 40:
522 if self.stdin_flag:
523 path = 'STDIN'
524 else:
525 path = os.path.dirname(self.file)
526 if not path or path[0] != os.sep:
527 path = os.path.join(os.getcwd(), path)
528 if len(path) > self.maxw - 37:
529 path = '~' + path[-(self.maxw-38):]
530 self.win_status.addstr('Path: %s' % path)
531 if self.maxw > 30:
532 if self.mode == MODE_TEXT:
533 mode = 'TEXT'
534 else:
535 mode = 'HEX'
536 self.win_status.addstr(0, self.maxw-30, 'View mode: %s' % mode)
537 if self.wrap:
538 wrap = 'YES'
539 else:
540 wrap = 'NO'
541 if self.mode == MODE_TEXT:
542 self.win_status.addstr(0, self.maxw-10, 'Wrap: %s' % wrap)
545 def show(self):
546 """show title, file and status bar"""
548 self.win_title.erase()
549 self.win_file.erase()
550 self.win_status.erase()
552 if self.maxh < 3:
553 return
555 # title
556 self.show_title()
557 # file
558 if self.mode == MODE_TEXT:
559 if self.wrap:
560 self.show_text_wrap()
561 else:
562 self.show_text_nowrap()
563 else:
564 self.show_hex()
565 # status
566 self.show_status()
568 self.win_title.refresh()
569 self.win_file.refresh()
570 self.win_status.refresh()
573 def __find(self, title):
574 self.pattern = messages.Entry(title, 'Type search string', '', 1, 0).run()
575 if self.pattern == None or self.pattern == '':
576 self.show()
577 return -1
578 filename = os.path.abspath(self.file)
579 mode = (self.mode == MODE_TEXT) and 'n' or 'b'
580 try:
581 i, o, e = os.popen3('%s -i%c \"%s\" \"%s\"' % \
582 (sysprogs['grep'], mode, self.pattern,
583 filename), 'r')
584 i.close()
585 buf = o.read()
586 err = e.read().strip()
587 o.close(), e.close()
588 except:
589 self.show()
590 messages.error('Find Error', 'Can\'t popen file')
591 return -1
592 if err != '':
593 self.show()
594 messages.error('Find Error', err)
595 self.show()
596 self.matches = []
597 return -1
598 else:
599 try:
600 self.matches = [int(l.split(':')[0]) for l in buf.split('\n') if l]
601 except (ValueError, TypeError):
602 self.matches = []
603 return 0
606 def __find_next(self):
607 pos = (self.mode == MODE_TEXT) and self.line or self.pos+16
608 for next in self.matches:
609 if next <= pos + 1:
610 continue
611 else:
612 break
613 else:
614 self.show()
615 messages.error('Find', 'No more matches <%s>' % self.pattern)
616 self.show()
617 return
618 if self.mode == MODE_TEXT:
619 self.line = next - 1
620 self.__move_lines(0)
621 else:
622 self.pos = next
623 self.__move_hex(0)
624 self.show()
627 def __find_previous(self):
628 pos = (self.mode == MODE_TEXT) and self.line or self.pos
629 rev_matches = [l for l in self.matches]
630 rev_matches.reverse()
631 for prev in rev_matches:
632 if prev >= pos + 1:
633 continue
634 else:
635 break
636 else:
637 self.show()
638 messages.error('Find',
639 'No more matches <%s>' % self.pattern)
640 self.show()
641 return
642 if self.mode == MODE_TEXT:
643 self.line = prev - 1
644 self.__move_lines(0)
645 else:
646 self.pos = prev
647 self.__move_hex(0)
648 self.show()
651 def run(self):
652 """run application, manage keys, etc"""
654 self.show()
655 while 1:
656 ch = self.win_file.getch()
658 # cursor up
659 if ch in (ord('k'), ord('K'), curses.KEY_UP):
660 if self.mode == MODE_TEXT:
661 if self.wrap:
662 if self.col == 0 and self.line > 0:
663 self.__move_lines(-1)
664 self.col = int(self.__get_line_length()/self.maxw)*self.maxw
665 else:
666 self.col -= self.maxw
667 else:
668 self.__move_lines(-1)
669 else:
670 self.__move_hex(-1)
671 self.show()
672 # cursor down
673 elif ch in (ord('j'), ord('J'), curses.KEY_DOWN):
674 if self.mode == MODE_TEXT:
675 if self.wrap:
676 if self.line >= self.nlines - 1 and \
677 self.__get_line_length() < self.maxw*(self.maxh-2):
678 pass
679 else:
680 self.col += self.maxw
681 if self.col >= self.__get_line_length():
682 self.col = 0
683 self.__move_lines(1)
684 else:
685 self.__move_lines(1)
686 else:
687 self.__move_hex(1)
688 self.show()
689 # page previous
690 elif ch in (curses.KEY_PPAGE, curses.KEY_BACKSPACE,
691 0x08, 0x02): # BackSpace, Ctrl-
692 if self.mode == MODE_TEXT:
693 if self.wrap:
694 lines = self.__get_prev_lines_text()
695 if self.col: # if we aren't at 1st char of line
696 line0 = self.__get_1line()[:self.col]
697 lines.insert(0, line0)
698 else:
699 line0 = ''
700 y = self.maxh - 2
701 end = False
702 for i, l in enumerate(lines):
703 y -= 1; dy = 0
704 if y < 0:
705 break
706 len2 = lenz = len(l)
707 while len2 > self.maxw:
708 dy += 1; y -= 1
709 if y < 0:
710 i += 1
711 dy = int(lenz/self.maxw) + 1 - dy
712 end = True
713 break
714 len2 -= self.maxw
715 if end:
716 break
717 else:
718 i += 1
719 if line0:
720 i -= 1
721 if y < 0:
722 self.__move_lines(-i)
723 if i == 0:
724 self.col = (dy-1) * self.maxw
725 else:
726 self.col = dy * self.maxw
727 else:
728 self.__move_lines(-(self.maxh-2))
729 self.col = 0
730 else:
731 self.__move_lines(-(self.maxh-2))
732 else:
733 self.__move_hex(-(self.maxh-2))
734 self.show()
735 # page next
736 elif ch in (curses.KEY_NPAGE, ord(' '), 0x06): # Ctrl-F
737 if self.mode == MODE_TEXT:
738 if self.wrap:
739 lines = self.__get_lines_text()
740 lines[0] = lines[0][self.col:]
741 y = 0
742 end = False
743 for i, l in enumerate(lines):
744 y += 1; dy = 0
745 if y > self.maxh - 2:
746 break
747 len2 = len(l)
748 while len2 > self.maxw:
749 dy += 1; y += 1
750 if y > self.maxh - 2:
751 end = True
752 break
753 len2 -= self.maxw
754 if end:
755 break
756 else:
757 i += 1
758 self.__move_lines(i)
759 if i == 0:
760 self.col += dy * self.maxw
761 else:
762 self.col = dy * self.maxw
763 else:
764 self.__move_lines(self.maxh-2)
765 else:
766 self.__move_hex(self.maxh-2)
767 self.show()
768 # home
769 elif ch in (curses.KEY_HOME, 0x01): # home
770 if self.mode == MODE_TEXT:
771 self.__move_lines(-self.nlines)
772 else:
773 self.__move_hex(-self.nbytes)
774 self.col = 0
775 self.show()
776 # end
777 elif ch in (curses.KEY_END, 0x05): # end
778 if self.mode == MODE_TEXT:
779 self.__move_lines(self.nlines)
780 else:
781 self.__move_hex(self.nbytes)
782 self.col = 0
783 self.show()
785 # cursor left
786 elif ch == curses.KEY_LEFT:
787 if self.mode == MODE_HEX or self.wrap:
788 continue
789 if self.col > 9:
790 self.col -= 10
791 self.show()
792 # cursor right
793 elif ch == curses.KEY_RIGHT:
794 if self.mode == MODE_HEX or self.wrap:
795 continue
796 if self.col+self.maxw < self.col_max + 2:
797 self.col += 10
798 self.show()
800 # un/wrap
801 elif ch in (ord('w'), ord('W'), curses.KEY_F2):
802 if self.mode == MODE_HEX:
803 continue
804 self. wrap = not self.wrap
805 self.__move_lines(0)
806 self.col = 0
807 self.show()
809 # text / hexadecimal mode
810 elif ch in (ord('m'), ord('M'), curses.KEY_F4):
811 if self.mode == MODE_TEXT:
812 self.mode = MODE_HEX
813 self.col = 0
814 else:
815 self.mode = MODE_TEXT
816 self.__move_lines(0)
817 self.show()
819 # goto line/byte
820 elif ch in (ord('g'), ord('G'), curses.KEY_F5):
821 rel = 0
822 if self.mode == MODE_TEXT:
823 title = 'Goto line'
824 help = 'Type line number'
825 else:
826 title = 'Goto byte'
827 help = 'Type byte offset'
828 n = messages.Entry(title, help, '', 1, 0).run()
829 if n == None or n == '':
830 self.show()
831 continue
832 if n[0] in ('+', '-'):
833 rel = 1
834 try:
835 if n[rel:rel+2] == '0x':
836 if rel:
837 n = long(n[0] + str(int(n[1:], 16)))
838 else:
839 n = long(n, 16)
840 else:
841 n = long(n)
842 except ValueError:
843 self.show()
844 if self.mode == MODE_TEXT:
845 mode = 'line'
846 else:
847 mode = 'byte'
848 messages.error('Goto %s' % mode,
849 'Invalid byte number <%s>' % n)
850 self.show()
851 continue
852 if n == 0:
853 self.show()
854 continue
855 if self.mode == MODE_TEXT:
856 if rel:
857 self.line += n
858 else:
859 self.line = n - 1
860 self.line = min(self.line, self.nlines-1)
861 self.__move_lines(0)
862 else:
863 if rel:
864 self.pos += n
865 else:
866 self.pos = n
867 self.pos = min(self.pos, self.nbytes)
868 self.__move_hex(0)
869 self.show()
871 # find
872 elif ch == ord('/'):
873 if self.__find('Find') == -1:
874 continue
875 self.__find_next()
876 # find previous
877 elif ch == curses.KEY_F6:
878 if not self.matches:
879 if self.__find('Find Previous') == -1:
880 continue
881 self.__find_previous()
882 # find next
883 elif ch == curses.KEY_F7:
884 if not self.matches:
885 if self.__find('Find Next') == -1:
886 continue
887 self.__find_next()
889 # go to bookmark
890 elif 0x30 <= ch <= 0x39:
891 bk = self.bookmarks[ch-0x30]
892 if bk == -1:
893 continue
894 self.line = bk
895 self.__move_lines(0)
896 self.show()
897 # set bookmark
898 elif ch in (ord('b'), ord('B')):
899 while 1:
900 ch = messages.get_a_key('Set bookmark',
901 'Press 0-9 to save bookmark, Ctrl-C to quit')
902 if 0x30 <= ch <= 0x39:
903 self.bookmarks[ch-0x30] = self.line
904 break
905 elif ch == -1:
906 break
907 self.show()
909 # shell
910 elif ch == 0x0F: # Ctrl-O
911 curses.endwin()
912 if self.stdin_flag:
913 os.system('sh')
914 else:
915 os.system('cd \"%s\"; sh' % os.path.dirname(self.file))
916 curses.curs_set(0)
917 self.show()
919 # help
920 elif ch in (ord('h'), ord('H'), curses.KEY_F1):
921 buf = [('', 2)]
922 buf.append(('%s v%s (C) %s, by %s' % \
923 (PYVIEW_NAME, VERSION, DATE, AUTHOR), 5))
924 text = PYVIEW_README.split('\n')
925 for l in text:
926 buf.append((l, 6))
927 InternalView('Help for %s' % PYVIEW_NAME, buf).run()
928 self.show()
930 # resize window
931 elif ch == curses.KEY_RESIZE:
932 self.resize_window()
934 # quit
935 elif ch in (0x1B, 0x11, ord('q'), ord('Q'), ord('x'), ord('X'), # Ctrl-Q
936 curses.KEY_F3, curses.KEY_F10):
937 self.fd.close()
938 return
941 ##################################################
942 ##### Main
943 ##################################################
944 def usage(prog, msg = ""):
945 prog = os.path.basename(prog)
946 if msg != "":
947 print '%s:\t%s\n' % (prog, msg)
948 print """\
949 %s v%s - (C) %s, by %s
951 A simple pager (viewer) to be used with Last File Manager.
952 Released under GNU Public License, read COPYING for more details.
954 Usage:\t%s\t[-h | --help]
955 \t\t[-d | --debug]
956 \t\t[-m text|hex | --mode=text|hex]
957 \t\t[+n]
958 \t\tpathtofile
959 Options:
960 -m, --mode\t\tstart in text or hexadecimal mode
961 -d, --debug\t\tcreate debug file
962 -h, --help\t\tshow help
963 +n\t\t\tstart at line (text mode) or byte (hex mode),
964 \t\t\tif n starts with '0x' is considered hexadecimal
965 pathtofile\t\tfile to view
966 """ % (PYVIEW_NAME, VERSION, DATE, AUTHOR, prog)
969 def main(win, filename, line, mode, stdin_flag):
970 app = FileView(win, filename, line, mode, stdin_flag)
971 if app == OSError:
972 sys.exit(-1)
973 return app.run()
976 def PyView(sysargs):
977 # defaults
978 DEBUG = 0
979 filename = ''
980 line = 0
981 mode = MODE_TEXT
983 # args
984 try:
985 opts, args = getopt.getopt(sysargs[1:], 'dhm:',
986 ['debug', 'help', 'mode='])
987 except getopt.GetoptError:
988 usage(sysargs[0], 'Bad argument(s)')
989 sys.exit(-1)
990 for o, a in opts:
991 if o in ('-d', '--debug'):
992 DEBUG = 1
993 elif o in ('-h', '--help'):
994 usage(sysargs[0])
995 sys.exit(2)
996 elif o in ('-m', '--mode'):
997 if a == 'text':
998 mode = MODE_TEXT
999 elif a == 'hex':
1000 mode = MODE_HEX
1001 else:
1002 usage(sysargs[0], '<%s> is not a valid mode' % a)
1003 sys.exit(-1)
1005 stdin = read_stdin()
1006 if stdin == '':
1007 stdin_flag = 0
1008 else:
1009 stdin_flag = 1
1011 if len(args) > 2:
1012 usage(sysargs[0], 'Incorrect number of arguments')
1013 sys.exit(-1)
1014 while True:
1015 try:
1016 arg = args.pop()
1017 if arg[0] == '+':
1018 line = arg[1:]
1019 try:
1020 if line[:2] == '0x':
1021 line = int(line, 16)
1022 else:
1023 line = int(line)
1024 except ValueError:
1025 usage(sysargs[0], '<%s> is not a valid line number' % line)
1026 sys.exit(-1)
1027 else:
1028 filename = arg
1029 except IndexError:
1030 break
1031 if filename == '' and not stdin_flag:
1032 usage(sysargs[0], 'File is missing')
1033 sys.exit(-1)
1034 if stdin_flag:
1035 filename = create_temp_for_stdin(stdin)
1036 else:
1037 if not os.path.isfile(filename):
1038 usage(sysargs[0], '<%s> is not a valid file' % file)
1039 sys.exit(-1)
1041 # logging
1042 if DEBUG:
1043 log_file = os.path.join(os.path.abspath('.'), LOG_FILE)
1044 logging.basicConfig(level=logging.DEBUG,
1045 format='%(asctime)s %(levelname)s\t%(message)s',
1046 datefmt='%Y-%m-%d %H:%M:%S ',
1047 filename=log_file,
1048 filemode='w')
1049 logging.info('Starting PyView...')
1051 # main app
1052 logging.info('Main application call')
1053 curses.wrapper(main, filename, line, mode, stdin_flag)
1054 logging.info('End')
1056 if stdin_flag:
1057 os.unlink(filename)
1060 if __name__ == '__main__':
1061 PyView(sys.argv)