aboutsummaryrefslogtreecommitdiff
path: root/elogv
diff options
context:
space:
mode:
authorLuca Marturana <lucamarturana@me.com>2013-05-04 23:23:23 +0200
committerLuca Marturana <lucamarturana@me.com>2013-05-04 23:23:23 +0200
commitb0aede5ce0c1d303d4488bc4d7e60411c34b7049 (patch)
treeb64ea15a1fc99a13376e4cb49f3b981b13450724 /elogv
downloadelogv-b0aede5ce0c1d303d4488bc4d7e60411c34b7049.tar.gz
elogv-b0aede5ce0c1d303d4488bc4d7e60411c34b7049.tar.bz2
elogv-b0aede5ce0c1d303d4488bc4d7e60411c34b7049.zip
Import of elogv version 0.7.50.7.5
Diffstat (limited to 'elogv')
-rwxr-xr-xelogv574
1 files changed, 574 insertions, 0 deletions
diff --git a/elogv b/elogv
new file mode 100755
index 0000000..53c37cd
--- /dev/null
+++ b/elogv
@@ -0,0 +1,574 @@
+#!/usr/bin/python
+# Author: Luca Marturana (luca89) <lucamarturana@gmail.com>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import sys
+import re
+from datetime import datetime
+import curses
+import curses.ascii
+import textwrap
+from portage import settings as port_settings
+from glob import glob
+import gettext
+import locale
+
+# Setup default locale
+locale.setlocale(locale.LC_ALL, '')
+
+# Setup gettext. Note that lgettext() is used instead of gettext()
+# because it always returns strings encoded with the preferred system
+# encoding, not encoded with the same codeset used in the translation
+# file
+gettext.textdomain('elogv')
+_ = gettext.lgettext
+
+# This text is used on the in-line help
+helptext = _("""
+Elogv is a portage elog viewer based on curses and python,
+you can use these keys to control the behavior of the program:
+
+ - Down arrow or j -> scroll the list of files down by 1 unit
+ - Up arrow or k -> opposite of Down arrow
+ - PageDown -> scroll the list down by 10 unit
+ - PageUp -> opposite of PageDown
+ - End -> go to the last file of the list
+ - Home -> go to the first file of the list
+ - t -> order the list of files by date, most recent on top
+ - a -> order the list of files alphabetically, the first time by category,
+ the second time (pressing the key again) by package name
+ - c -> order the list of files by log class warning level
+ - r -> reverse the list of files
+ - SpaceBar -> scroll the selected file
+ - h or F1 -> show the help screen, press Page Up/Down to scroll up and down,
+ h or F1 again to hide
+ - d -> removes selected files, usage is similar to vim "d" command,
+ here are same examples:
+ da -> removes all files
+ de -> removes from selected item to the end of the list
+ ds -> remove from selected item to the start of the list
+ d1d or dd -> removes selected file only
+ d4d -> removes 4 files starting from selected one
+ - / -> starts a search prompt, write a string and will be showed the next
+ package that contains your string, use ESC to exit
+ - q -> quit
+""")
+
+(normal, selected, einfo, elog, ewarn, eerror) = range(6)
+(PATH, CAT, PN, DATE, CLASS) = range(5)
+
+# Character used to print the class of an elog
+class_char = "*"
+list_format = "%s/%s - %s"
+date_format = "%x"
+
+# Exceptions classes
+class TermTooSmall(Exception):
+ def __init__(self):
+ pass
+
+class NoLogFiles(Exception):
+ def __init__(self):
+ pass
+
+class CannotOpenElogdir(Exception):
+ def __init__(self):
+ pass
+
+# Main class (called with curses.wrapper later)
+class ElogViewer:
+ def __init__(self, screen):
+ #curses.curs_set(0)
+ self.screen = screen
+
+ # Our color pairs
+ curses.use_default_colors()
+ curses.init_pair(selected, curses.COLOR_BLACK, curses.COLOR_WHITE)
+ curses.init_pair(einfo, curses.COLOR_GREEN, curses.COLOR_BLACK)
+ curses.init_pair(ewarn, curses.COLOR_YELLOW, curses.COLOR_BLACK)
+ curses.init_pair(eerror, curses.COLOR_RED, curses.COLOR_BLACK)
+ curses.init_pair(elog, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
+
+ # This attributes are used to manage the scrolling of the list
+ # of files
+ self.pposy = 0
+ self.usel = 0
+
+ # Method used to order the list: use DATE for order by date, CAT to
+ # order by category name, PN to order by package name, CLASS to order
+ # by class second value set if the list should be reversed (True o False)
+ self.sort_method = [ DATE, False]
+
+ # Initialize screen
+ self.init_screen()
+
+ c = self.screen.getch()
+ while c not in ( ord("q"), curses.ascii.ESC):
+ ## Scrolling keys ##
+ if c in (curses.KEY_DOWN, ord("j")):
+ self.change_usel(1)
+
+ elif c in (curses.KEY_UP, ord("k")):
+ self.change_usel(-1)
+
+ elif c in (curses.KEY_NPAGE, ord("f")):
+ self.change_usel(10)
+
+ elif c in (curses.KEY_PPAGE, ord("b")):
+ self.change_usel(-10)
+
+ elif c in (curses.KEY_END, ord("G")):
+ self.change_usel(len(self.packages)-1, False)
+
+ elif c in (curses.KEY_HOME, ord("g")):
+ self.change_usel(0, False)
+ ## End Scrolling keys ##
+
+ ## Sorting the list ##
+ elif c == ord("a"):
+ if self.sort_method[0] == CAT:
+ self.sort_method[0] = PN
+ else:
+ self.sort_method[0] = CAT
+ self.fill_file_pad()
+ self.refresh_file_pad()
+
+ elif c == ord("t"):
+ self.sort_method[0] = DATE
+ self.fill_file_pad()
+ self.refresh_file_pad()
+
+ elif c == ord("c"):
+ self.sort_method[0] = CLASS
+ self.fill_file_pad()
+ self.refresh_file_pad()
+
+ elif c == ord("r"):
+ self.sort_method[1] = not self.sort_method[1]
+ self.fill_file_pad()
+ self.refresh_file_pad()
+ ## End Sorting the list ##
+
+ elif c == ord(" "):
+ # Now is used only for scrolling the text
+ self.show_log()
+
+ elif c == curses.KEY_RESIZE:
+ # Reinitialize screen
+ self.init_screen()
+
+ elif c in (curses.KEY_F1, ord("h")):
+ self.show_help()
+ # We need to reinitialize the screen
+ self.init_screen()
+
+ elif c == ord("d"):
+ subc = self.screen.getch()
+ if subc == ord("a"):
+ n = "all"
+ elif subc == ord("e"):
+ n = "end"
+ elif subc == ord("s"):
+ n = "start"
+ elif subc == ord("d"):
+ n = "1"
+ else:
+ n = ""
+ while curses.ascii.isdigit(subc):
+ n += chr(subc)
+ subc = self.screen.getch()
+ if n:
+ self.remove_file(n)
+ self.file_pad.erase()
+ self.refresh_file_pad()
+ # If the user deleted files to start, move the selection
+ # to the first item
+ if n == "start":
+ self.usel = 0
+ self.pposy = 0
+ self.fill_file_pad()
+ self.refresh_file_pad()
+ self.logf_wrap = self.wrap_logf_lines()
+ self.show_log()
+
+ elif c == ord("/"):
+ word = ""
+ self.screen.move(self.height-1,2)
+ self.screen.addstr("/")
+ subc = self.screen.getch()
+ while subc != curses.ascii.ESC:
+ if subc == ord("\n"):
+ self.search(word,1)
+ elif subc == curses.ascii.BS:
+ word = word[:-1]
+ self.screen.delch()
+ self.search(word)
+ elif curses.ascii.isalpha(subc):
+ word += chr(subc)
+ self.screen.addstr(chr(subc))
+ self.search(word)
+ subc = self.screen.getch()
+ self.screen.hline(self.height-1,2,curses.ACS_HLINE,(len(word)+1))
+ self.screen.addstr(self.height-2,2," "*20)
+
+ # Get another key from the user
+ c = self.screen.getch()
+
+ def init_screen(self):
+ """
+ Init the screen and wins, it's also used to reinizialize screen
+ after a terminal resizing
+ """
+ (self.height, self.width) = self.screen.getmaxyx()
+
+ # Check if the terminal window is too small
+ if self.height < 12 or self.width < 80:
+ raise TermTooSmall()
+
+ # Screen Look&Feel
+ self.screen.border()
+ self.screen.hline(self.height//2,1, "_", self.width-2)
+ m = _(" Press F1 or h to show the help screen ")
+ self.screen.addstr(self.height-1, self.width-len(m)-1, m)
+ self.screen.refresh()
+
+ # Initialize log file window
+ self.log_win = curses.newwin(self.height//2-2, self.width-2,
+ self.height//2+1, 1)
+
+ # Draw other window of the screen
+ self.fill_file_pad()
+ self.refresh_file_pad()
+
+ self.logf_wrap = self.wrap_logf_lines()
+ self.show_log()
+
+ def change_usel(self,n,relative=True):
+ prev_usel = self.usel
+ if not relative:
+ self.usel = n
+ elif n < 0 and self.usel+n < 0:
+ self.usel = 0
+ elif n > 0 and self.usel+n > len(self.packages)-1:
+ self.usel = len(self.packages)-1
+ else:
+ self.usel += n
+
+ prev_pkg = self.packages[prev_usel]
+ pkg = self.packages[self.usel]
+ self.file_pad.addstr(prev_usel,1,
+ class_char,
+ curses.A_BOLD + curses.color_pair(prev_pkg[CLASS]))
+ self.file_pad.addstr(prev_usel,3,
+ list_format % (prev_pkg[CAT], prev_pkg[PN], prev_pkg[DATE].strftime(date_format) ),
+ curses.color_pair(normal))
+ self.file_pad.addstr(self.usel,1,
+ class_char,
+ curses.A_BOLD + curses.color_pair(pkg[CLASS]))
+ self.file_pad.addstr(self.usel,3,
+ list_format % (pkg[CAT], pkg[PN], pkg[DATE].strftime(date_format) ),
+ curses.color_pair(selected))
+
+ first = self.pposy
+ last = (self.height / 2 - 2) + first
+
+ if self.usel < first:
+ self.pposy -= first - self.usel
+
+ if self.usel > last:
+ self.pposy += self.usel - last
+ self.refresh_file_pad()
+ self.logf.close()
+ try:
+ self.logf = open(pkg[PATH])
+ except IOError:
+ # print ("Logfile not found at '%s'. Did it get deleted somehow?"
+ # % os.path.join(elogdir,pkg[PATH]))
+ self.init_screen()
+ self.change_usel(prev_usel, False)
+ self.logf_wrap = self.wrap_logf_lines()
+ self.show_log()
+
+ def refresh_file_pad(self):
+ """
+ Redraws file pad, first half of the screen.
+ Can be used to scroll or simply update
+ """
+ self.file_pad.refresh(self.pposy,0,1,1,self.height//2-1,self.width-2)
+
+ def get_packages_key(self, k):
+ return k[self.sort_method[0]]
+
+ def fill_file_pad(self):
+ """
+ Fill the list of files, colorize the selected row and order files by
+ method specified
+ """
+ # Get the list of files
+ try:
+ file_list = glob(os.path.join(elogdir,"*:*:*.log")) + glob(os.path.join(elogdir,"*","*:*.log"))
+ except OSError:
+ raise CannotOpenElogdir()
+
+ if not file_list:
+ raise NoLogFiles()
+
+ # self.packages contains all info extracted from each file, this is the
+ # structure:
+ # [ ("filename", "category", "package name", date:datetime_obj, class:int), ... ]
+ self.packages = []
+ for filepath in file_list:
+ # This istruction splits the information about the package from the file path
+ # If the user don't use split-elog feature the format used by portage is:
+ # <elogdir>/x11-themes:haematite-xcursors-1.0:20091018-195827.log
+ # else with the split-elog feature:
+ # <elogdir>/x11-themes/haematite-xcursors-1.0:20091018-195827.log
+ # So first we remove the elogdir from the filepath to obtain
+ # x11-themes:haematite-xcursors-1.0:20091018-195827.log
+ # or
+ # x11-themes/haematite-xcursors-1.0:20091018-195827.log
+ # then we split the string using as pattern / or : to obtain in any
+ # case
+ # ( "x11-themes", "haematite-xcursors", "1.0:20091018-195827.log")
+ (cat,pn,other) = re.split(":|" + os.path.sep, filepath.replace(elogdir + os.path.sep, "") )
+ if sys.version_info[:2] >= (2, 5):
+ date = datetime.strptime(other, "%Y%m%d-%H%M%S.log")
+ else:
+ # This is for backward compatibility with older version of python
+ # it will be removed in the future
+ date_str = other[:8]
+ date = datetime(int(date_str[:4]),int(date_str[4:6]),int(date_str[6:]))
+ self.packages.append( (filepath, cat, pn, date, self.get_class(filepath)) )
+
+ # Maybe that after removing files self.usel points to a wrong index,
+ # so this will prevent a crash
+ if self.usel >= len(self.packages):
+ self.usel = len(self.packages)-1
+ # We also have to update self.pposy
+ if self.pposy > self.usel:
+ self.pposy = max(0, self.usel-10)
+
+ # Sort the list
+ if self.sort_method[0] in (DATE, CLASS):
+ self.packages.sort(key=self.get_packages_key,reverse=not self.sort_method[1])
+ else:
+ self.packages.sort(key=self.get_packages_key, reverse=self.sort_method[1])
+
+ self.file_pad = curses.newpad(len(self.packages),self.width)
+ self.file_pad.erase()
+
+ for i in range(len(self.packages)):
+ pkg = self.packages[i]
+ if i == self.usel:
+ cp = selected
+ # Maybe that the logf pointed by self.usel changed, (example
+ # when same files are removed) so reload the self.logf file
+ # pointer, this work is done here for convenience
+ try:
+ self.logf.close()
+ except AttributeError:
+ pass
+ self.logf = open(pkg[PATH])
+ else:
+ cp = normal
+
+ self.file_pad.addstr(i,1,
+ class_char,
+ curses.A_BOLD + curses.color_pair(pkg[CLASS]))
+ self.file_pad.addstr(i,3,
+ list_format % (pkg[CAT], pkg[PN], pkg[DATE].strftime(date_format) ),
+ curses.color_pair(cp))
+
+ def get_class(self,filepath):
+ """
+ Get the highest elog class in a file
+ """
+ f = file(filepath)
+
+ classes = re.findall("LOG:|INFO:|WARN:|ERROR:", f.read())
+ f.close()
+
+ if "ERROR:" in classes:
+ return eerror
+ elif "WARN:" in classes:
+ return ewarn
+ elif "LOG:" in classes:
+ return elog
+ else:
+ return einfo
+
+ def wrap_logf_lines(self):
+ """
+ Takes a file-like object and wraps long lines. Returns a list iterator.
+ """
+ result = []
+ self.logf.seek(0)
+ logf = self.logf.readlines()
+ for line in logf:
+ if not line.strip():
+ # textwrap eats newlines
+ result.append("\n")
+ else:
+ # Returns a list of new lines minus the line ending \n
+ wrapped_line = textwrap.wrap(line, width=self.width-2)
+ for l in wrapped_line:
+ l += "\n"
+ result.append(l)
+
+ return iter(result)
+
+ def show_log(self):
+ """
+ Display the selected file, if the length of the file is bigger than
+ the height of the window, interrupt the drawing and resume it when the
+ user press again the SpaceBar key
+ """
+ self.log_win.erase()
+ shown_all = False
+
+ for i in range(0,self.height//2-4):
+ try:
+ x = self.logf_wrap.next()
+ except StopIteration:
+ shown_all = True
+ # Restart the iterator
+ self.logf_wrap = self.wrap_logf_lines()
+ break
+
+ try:
+ if x.startswith('INFO:'):
+ self.log_win.addstr(x[:self.width-2],curses.color_pair(einfo))
+ elif x.startswith('WARN:'):
+ self.log_win.addstr(x[:self.width-2],curses.color_pair(ewarn))
+ elif x.startswith('ERROR:'):
+ self.log_win.addstr(x[:self.width-2],curses.color_pair(eerror))
+ elif x.startswith('LOG:'):
+ self.log_win.addstr(x[:self.width-2],curses.color_pair(elog))
+ else:
+ self.log_win.addstr(x[:self.width-2],curses.color_pair(normal))
+ except curses.error:
+ pass
+
+ if not shown_all:
+ s = _("Continue...")
+ self.log_win.addstr(self.height//2-3, self.width-len(s)-4, s,
+ curses.color_pair(normal))
+
+ self.log_win.refresh()
+
+ def remove_file(self,n):
+ """
+ Delete from the filesystem a slice of elog files
+ n can be:
+ "all" -> all files will be deleted
+ "end" -> files from selected one to the end of the list will be deleted
+ <int> -> will be deleted the selected file and the next <int>-1
+ """
+
+ if n == "all":
+ start = None
+ end = None
+ elif n == "end":
+ start = self.usel
+ end = None
+ elif n == "start":
+ start = None
+ end = self.usel
+ else:
+ start = self.usel
+ end = self.usel + int(n)
+
+ for item in self.packages[start:end]:
+ os.remove(os.path.join(elogdir,item[0]))
+
+ def show_help(self):
+ # Setup help window
+ helpwin_height = self.height / 3 * 2
+ helpwin_corner = (self.height / 6, self.width// 2 - 40)
+ helpwin = curses.newwin(helpwin_height, 80,
+ helpwin_corner[0], helpwin_corner[1])
+ helplines = helptext.splitlines()
+
+ # Setup help pad
+ row = 0
+ maxrow = len(helplines)
+ helppad = curses.newpad(maxrow,80)
+
+ # Insert helptext on the pad
+ for i in range(maxrow):
+ helppad.addstr(i,0,helplines[i])
+
+ # Loop to manage user actions
+ c = None
+ while c not in (ord("h"), curses.KEY_F1, ord("q")):
+ helpwin.erase()
+ helpwin.border()
+ helpwin.refresh()
+
+ helppad.refresh(row,0,helpwin_corner[0]+1,helpwin_corner[1]+1,
+ helpwin_height+helpwin_corner[0]-2,80+helpwin_corner[1]-2)
+
+ c = self.screen.getch()
+ if c == curses.KEY_NPAGE:
+ if row+10 <= maxrow:
+ row += 10
+ elif c == curses.KEY_PPAGE:
+ if row-10 >= 0:
+ row -= 10
+ elif c in (curses.KEY_DOWN, ord("j")):
+ if row+1 < maxrow:
+ row += 1
+ elif c in (curses.KEY_UP, ord("k")):
+ if row > 0:
+ row -= 1
+
+ def search(self, word, div=0):
+ for x in self.packages[self.usel+div:]:
+ if re.search(word, "%s/%s" % x[1:3]):
+ self.change_usel(self.packages.index(x),False)
+ break
+ else:
+ self.screen.addstr(self.height-2,2,_("Not Found!"),
+ curses.color_pair(eerror))
+
+if __name__ == "__main__":
+ if "--help" in sys.argv:
+ print helptext
+ sys.exit()
+
+ # Get the path of the elogdir
+ if port_settings['PORT_LOGDIR']:
+ elogdir = os.path.join(port_settings['PORT_LOGDIR'],"elog")
+ else:
+ elogdir = os.path.join(os.sep,port_settings['EPREFIX'],"var","log","portage","elog")
+
+ # Launch curses interface
+ try:
+ curses.wrapper(ElogViewer)
+ except TermTooSmall:
+ print _("Your terminal window is too small, try to enlarge it")
+ sys.exit(1)
+ except NoLogFiles:
+ print _("There aren't any elog files on"),elogdir
+ sys.exit(1)
+ except CannotOpenElogdir:
+ print _("Cannot open"),elogdir
+ print _("Please check if the directory exists and if it's readable by your user.")
+ sys.exit(1)
+ except KeyboardInterrupt:
+ pass
+# vim: set shiftwidth=4 tabstop=4 expandtab: