[PATCH/PROGRAM] Reportbug-Frontend in X-Dialog
Hi,
I read the discussion about "We need a web-based BTS" and today had some spare
time and thought well I could look if reportbug could not be able to use
another frontend - as both newt _and_ gnome do not work reliable anymore.
Well the result of some hours of work (well I did not program in python before
much ...) can be seen attached.
Everything at least seems to work in Xdialog-mode, although a console is still
needed.
The problem now is:
- How to design the "GUI" (well we just have some commands for (X)dialog) for
the Choose of the BTS-Entries ?!
- Would users use such a frontend, which could also be available in a menu
item then ...
Yes, and the last "Bug", which cannot so easily overcome: The console
reportbug had some nice "autocomplete" features (I just noticed this, when
developing) ...
Of course its _not_ so easily possible to just hack the dialog and have it
also have custom readline-support with completion ...
But what would work, was to let the user specifiy the first x chars, then he
clicks 'ok', and if its not to many items, can choose from a list including
the original input ...
Well of course, what would be even nicer was if there were like with debconf
KDE / GNOME2 / ... and perhaps Web Wizards, that were really user friendly.
If its needed is another question ...
cu
Fabian
# Text user interface for reportbug
# Written by Chris Lawrence <lawrencc@debian.org>
# (C) 2001-02 Chris Lawrence
#
# This program is freely distributable per the following license:
#
## Permission to use, copy, modify, and distribute this software and its
## documentation for any purpose and without fee is hereby granted,
## provided that the above copyright notice appears in all copies and that
## both that copyright notice and this permission notice appear in
## supporting documentation.
##
## I DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
## IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL I
## BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
## DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
## WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
## ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
## SOFTWARE.
import commands, sys, os, re, math, string, debianbts, errno
from reportbug_exceptions import *
from urlutils import launch_browser
from types import StringTypes
import dircache
import glob
import getpass
# Simple wrapper around (X)dialog.
# If the environment variable DISPLAY is set, then the X version is used, the
# text based otherwise.
# (C) 2003 Chris Liechti <cliechti@gmx.net>
#
# License: GPL
# To make it work with reportbug:
# Copyright (c) 2004 by Fabian Franz <debian@fabian-franz.de>
#find out which version to use
if os.getenv('DISPLAY')!="":
DIALOG = 'Xdialog'
GUI = 1
else:
DIALOG = 'dialog'
GUI = 0
title="reportbug gui"
def _checkresult(exitcode, result):
"""Check exit code of (X)dialog and raise an exeception if aborted."""
if exitcode == 1 or exitcode >= 255:
raise KeyboardInterrupt, "User aborted"
return result
def start_dialog(exit, text,*args):
"""Wrap the starting"""
start=(text%args).replace("`","'")
#sys.stderr.write(start)
fin = os.popen(start)
result = fin.read()
status = fin.close()
if exit:
return _checkresult(status, result.replace("\n",""))
else:
return result.replace("\n","")
def get_file(path=os.curdir+'/', title="Please choose a file"):
"""Open a file entry dialog."""
#sizing is different...
fin = os.popen('%s --stdout --title %r --fselect %s %s' % (DIALOG, title, path, GUI and '0 0' or '14 48'))
result = fin.read()
status = fin.close()
return _checkresult(status, result)
try:
import textwrap
except:
from optik import textwrap
ISATTY = sys.stdin.isatty()
try:
r, c = commands.getoutput('stty size').split()
rows, columns = int(r) or 24, int(c) or 79
except:
rows, columns = 24, 79
def ewrite(message, *args):
if not ISATTY:
return
sys.stderr.write(message % args)
log_message = ewrite
def indent_wrap_text(text, starttext='', indent=0, linelen=None):
"""Wrapper for textwrap.fill to the existing API."""
if not linelen:
linelen = columns-1
if indent:
si = ' '*indent
else:
si = ''
text = ' '.join(text.split())
output = textwrap.fill(text, width=linelen, initial_indent=starttext,
subsequent_indent=si)
if output[-1] == '\n':
return output
return output + '\n'
# Readline support, if available
try:
import readline
readline.parse_and_bind("tab: complete")
try:
# minimize the word delimeter list if possible
readline.set_completer_delims(' ')
except:
pass
except:
readline = None
class our_completer(object):
def __init__(self, completions=None):
self.completions = None
if completions:
self.completions = tuple(map(str, completions))
def complete(self, text, i):
if not self.completions: return None
matching = [x for x in self.completions if x.startswith(text)]
if i < len(matching):
return matching[i]
else:
return None
def our_raw_input(prompt = None, completions=None, completer=None):
istty = sys.stdout.isatty()
if not istty:
sys.stderr.write(prompt)
sys.stderr.flush()
if readline:
if completions and not completer:
completer = our_completer(completions).complete
if completer:
readline.set_completer(completer)
try:
if istty:
ret = raw_input(prompt)
else:
ret = raw_input()
except EOFError:
ewrite('\nUser interrupt (^D).\n')
raise SystemExit
if readline:
readline.set_completer(None)
return ret.strip()
def select_options(msg, ok, help, allow_numbers=None, nowrap=False):
return start_dialog(True,'%s --stdout --title %r --menu %r %s %s %s',DIALOG, title, msg, "0 0", 10, ' '.join(["%r %r" % (x,help[x]) for x in help.keys()]))
err_message = ''
for option in ok:
if option in string.ascii_uppercase:
default=option
break
if not help: help = {}
if '?' not in ok: ok = ok+'?'
if nowrap:
longmsg = msg+' ['+'|'.join(ok)+']?'+' '
else:
longmsg = indent_wrap_text(msg+' ['+'|'.join(ok)+']?').strip()+' '
ch = our_raw_input(longmsg, allow_numbers)
# Allow entry of a bug number here
if allow_numbers:
while ch and ch[0] == '#': ch = ch[1:]
if type(allow_numbers) == type(1):
try:
return str(int(ch))
except ValueError:
pass
else:
try:
number = int(ch)
if number in allow_numbers:
return str(number)
else:
nums = list(allow_numbers)
nums.sort()
err_message = 'Only the following entries are allowed: '+\
', '.join(map(str, nums))
except (ValueError, TypeError):
pass
if not ch: ch = default
ch = ch[0]
if ch=='?':
help['?'] = 'Display this help.'
for ch in ok:
if ch in string.ascii_uppercase:
desc = '(default) '
else:
desc = ''
desc += help.get(ch, help.get(ch.lower(),
'No help for this option.'))
ewrite(indent_wrap_text(desc+'\n', '%s - '% ch, 4))
return select_options(msg, ok, help, allow_numbers)
elif (ch.lower() in ok) or (ch.upper() in ok):
return ch.lower()
elif err_message:
ewrite(indent_wrap_text(err_message))
else:
ewrite('Invalid selection.\n')
return select_options(msg, ok, help, allow_numbers, nowrap)
def yes_no(msg, yeshelp, nohelp, default=1, nowrap=False):
"""Ask a yes/no question, return 1 == yes, 0 == no."""
fin = os.popen('%s --stdout %r --title %r --yesno %r %s' % (DIALOG, default and '' or (GUI and '--default-no' or '--defaultno'), title, indent_wrap_text(msg)+"\nYes: "+indent_wrap_text(yeshelp)+"\nNo: "+indent_wrap_text(nohelp), "0 0"))
result = fin.read()
status = fin.close()
return not status
def long_message(text, *args):
return start_dialog(True,'%s --stdout --title %r --msgbox %r %s', DIALOG, title,indent_wrap_text(text % args), '0 0')
def get_string(prompt, options=None, title="", force_prompt=False,
default='', completer=None):
"""Open a input dialog."""
if (options!=None and len(options)<=1):
return start_dialog(True,'%s --stdout --title %r --inputbox %r %s %r', DIALOG, title, indent_wrap_text(prompt), '0 0', len(options)==1 and options[0])
else:
return start_dialog(True,'%s --stdout --title %r --inputbox %r %s %r', DIALOG, title, indent_wrap_text(prompt), '0 0', "")
def get_password(prompt=None):
return start_dialog(True,'%s --stdout --insecure --title %r --passwordbox %r %s', DIALOG, title, indent_wrap_text(prompt), '0 0')
def get_filename(prompt, title=None, force_prompt=False, default=''):
return start_dialog(False,'%s --stdout --title %r --fselect %r %s', DIALOG, prompt, default or '', '0 0')
def select_multiple(par, options, prompt, title=None, order=None, extras=None):
return menu(par, options, prompt, title=title, order=order, extras=extras,
multiple=True, empty_ok=False)
def menu(par, options, prompt, default=None, title=None, any_ok=False,
order=None, extras=None, multiple=False, empty_ok=False):
selected = {}
if not extras:
extras = []
else:
extras = list(extras)
if title:
ewrite(title+'\n\n')
ewrite(indent_wrap_text(par, linelen=columns)+'\n')
if type(options) == type({}):
options = options.copy()
# Convert to a list
if order:
olist = []
for key in order:
if options.has_key(key):
olist.append( (key, indent_wrap_text(options[key])) )
del options[key]
# Append anything out of order
options = options.items()
options.sort()
for option in options:
olist.append( option )
options = olist
else:
options = options.items()
options.sort()
if multiple:
# fixme: parse checklist
ret_text=start_dialog(True,'%s --stdout --title %r --checklist %r %s %s %s',DIALOG, title, prompt, "0 0", 10, ' '.join(["%r %r off" % x for x in options]))
return ret_text.replace("/"," ").split(" ")
else:
return start_dialog(True,'%s --stdout --title %r %r %r --menu %r %s %s %s',DIALOG, title, default and "--default-item", default and default, prompt, "0 0", 10, ' '.join(["%r %r" % x for x in options]))
# Things that are very UI dependent go here
def show_report(number, system, mirrors,
http_proxy, screen=None, queryonly=False, title='',
archived='no'):
import debianbts
sysinfo = debianbts.SYSTEMS[system]
ewrite('Retrieving report #%d from %s bug tracking system...\n',
number, sysinfo['name'])
try:
info = debianbts.get_report(number, system, mirrors=mirrors,
followups=1,
http_proxy=http_proxy, archived=archived)
except:
info = None
if not info:
ewrite('No report available: #%s\n', number)
return
(title, messages) = info
current_message = 0
skip_pager = False
while 1:
if current_message:
text = 'Followup %d - %s\n\n%s' % (current_message, title,
messages[current_message])
else:
text = 'Original report - %s\n\n%s' % (title, messages[0])
if not skip_pager:
fd = os.popen('sensible-pager', 'w')
try:
fd.write(text)
fd.close()
except IOError, x:
if x.errno == errno.EPIPE:
pass
else:
raise
skip_pager = False
if queryonly:
options = 'Orbq'
else:
options = 'xOrbq'
if (current_message+1) < len(messages):
options = 'N'+options.lower()
if (current_message):
options = 'p'+options
x = select_options("What do you want to do now?", options,
{'x' : 'Provide extra information.',
'o' : 'Show other bug reports (return to '
'bug listing).',
'n' : 'Show next message (followup).',
'p' : 'Show previous message (followup).',
'r' : 'Redisplay this message.',
'b' : 'Launch web browser to read '
'full log.',
'q' : "I'm bored; quit please."},
allow_numbers = range(1, len(messages)+1))
if x == 'x':
return number
elif x == 'q':
raise NoReport
elif x == 'b':
launch_browser(debianbts.get_report_url(
system, number, mirrors, archived))
skip_pager = True
elif x == 'o':
break
elif x == 'n':
current_message += 1
elif x == 'p':
current_message -= 1
return
def handle_bts_query(package, bts, mirrors=None, http_proxy="",
queryonly=False, title="", screen=None, archived='no',
source=False):
import debianbts
root = debianbts.SYSTEMS[bts].get('btsroot')
if not root:
ewrite('%s bug tracking system has no web URL; bypassing query\n',
debianbts.SYSTEMS[bts]['name'])
return
if isinstance(package, StringTypes):
ewrite('Querying %s bug tracking system for reports on %s\n',
debianbts.SYSTEMS[bts]['name'], package)
else:
ewrite('Querying %s bug tracking system for reports %s\n',
debianbts.SYSTEMS[bts]['name'], ' '.join([str(x) for x in
package]))
#ewrite('(Use ? for help at prompts.)\n')
bugs = []
try:
(count, title, hierarchy)=debianbts.get_reports(
package, bts, mirrors=mirrors,
source=source, http_proxy=http_proxy, archived=archived)
if debianbts.SYSTEMS[bts].has_key('namefmt'):
package2 = debianbts.SYSTEMS[bts]['namefmt'] % package
(count2, title2, hierarchy2) = \
debianbts.get_reports(package2, bts,
mirrors=mirrors, source=source,
http_proxy=http_proxy)
count = count+count2
for entry in hierarchy2:
hierarchy.append( (package2+' '+entry[0], entry[1]) )
exp = re.compile(r'\#(\d+):')
for entry in hierarchy or []:
for bug in entry[1]:
match = exp.match(bug)
if match:
bugs.append(int(match.group(1)))
if not count:
if hierarchy == None:
raise NoPackage
else:
raise NoBugs
elif count == 1:
ewrite('%d bug report found:\n\n', count)
else:
ewrite('%d bug reports found:\n\n', count)
return browse_bugs(hierarchy, count, bugs, bts, queryonly,
mirrors, http_proxy, screen, title)
except IOError:
res = select_options('Unable to connect to BTS; continue', 'yN',
{'y': 'Keep going.',
'n': 'Abort.'})
if res == 'n':
raise NoNetwork
def browse_bugs(hierarchy, count, bugs, bts, queryonly, mirrors,
http_proxy, screen, title):
endcount = catcount = 0
scount = startcount = 1
category = hierarchy[0]
lastpage = []
digits = len(str(len(bugs)))
linefmt = ' %'+str(digits)+'d) %s\n'
while category:
scount = scount + 1
catname, reports = category[0:2]
while catname[-1] == ':': catname = catname[:-1]
total = len(reports)
while len(reports):
these = reports[:rows-2]
reports = reports[rows-2:]
remain = len(reports)
tplural = rplural = 's'
if total == 1: tplural=''
if remain != 1: rplural=''
if remain:
lastpage.append(' %s: %d remain%s\n' %
(catname, remain, rplural))
else:
lastpage.append(catname+'\n')
oldscount, oldecount = scount, endcount
for report in these:
scount = scount + 1
endcount = endcount + 1
lastpage.append(linefmt % (endcount,report[:columns-digits-5]))
if category == hierarchy[-1] or \
(scount >= (rows - len(hierarchy[catcount+1][1]) - 1)):
skipmsg = ' (s to skip rest)'
if endcount == count:
skipmsg = ''
options = 'yNmrqsf'
if queryonly: options = 'Nmrqf'
rstr = "(%d-%d/%d) " % (startcount, endcount, count)
pstr = rstr + "Is the bug you found listed above"
if queryonly:
pstr = rstr + "What would you like to do next"
allowed = bugs+range(1, count+1)
helptext = {
'y' : 'Problem already reported; optionally '
'add extra information.',
'n' : 'Problem not listed above; possibly '
'check more.',
'm' : 'Get more information about a bug (you '
'can also enter a number\n'
' without selecting "m" first).',
'r' : 'Redisplay the last bugs shown.',
'q' : "I'm bored; quit please.",
's' : 'Skip remaining problems; file a new '
'report immediately.',
'f' : 'Filter bug list using a pattern.'}
if skipmsg:
helptext['n'] = helptext['n'][:-1]+' (skip to Next page).'
while 1:
sys.stderr.writelines(lastpage)
x = select_options(pstr, options, helptext,
allow_numbers=allowed)
if x == 'n':
lastpage = []
break
elif x == 'r':
continue
elif x == 'q':
raise NoReport
elif x == 's':
return
elif x == 'y':
if queryonly:
return
if len(bugs) == 1:
number = '1'
else:
number = our_raw_input(
'Enter the number of the bug report '
'you want to give more info on,\n'
'or press ENTER to exit: #', allowed)
while number and number[0] == '#':
number=number[1:]
if number:
try:
number = int(number)
if number not in bugs and 1 <= number <= len(bugs):
number = bugs[number-1]
return number
except ValueError:
ewrite('Invalid report number: %s\n',
number)
else:
raise NoReport
elif x == 'f':
# Do filter. Recursive done.
retval = search_bugs(hierarchy,bts, queryonly, mirrors,
http_proxy, screen, title)
if retval in ["FilterEnd", "Top"]:
continue
else:
return retval
else:
if x == 'm' or x == 'i':
if len(bugs) == 1:
number = '1'
else:
number = our_raw_input(
'Please enter the number of the bug '
'you would like more info on: #',
allowed)
else:
number = x
while number and number[0] == '#':
number=number[1:]
if number:
try:
number = int(number)
if number not in bugs and 1 <= number <= len(bugs):
number = bugs[number-1]
res = show_report(number, bts, mirrors,
http_proxy,
queryonly=queryonly,
screen=screen,
title=title)
if res:
return res
except ValueError:
ewrite('Invalid report number: %s\n',
number)
startcount = endcount+1
scount = 0
# these now empty
if category == hierarchy[-1]: break
catcount = catcount+1
category = hierarchy[catcount]
if scount:
lastpage.append('\n')
scount = scount + 1
def proc_hierarchy(hierarchy):
"""Find out bug count and bug # in the hierarchy."""
lenlist = [len(i[1]) for i in hierarchy]
if lenlist:
count = reduce(lambda x, y: x+y, lenlist)
else:
return 0, 0
# Copy & paste from handle_bts_query()
bugs = []
exp = re.compile(r'\#(\d+):')
for entry in hierarchy or []:
for bug in entry[1]:
match = exp.match(bug)
if match:
bugs.append(int(match.group(1)))
return count, bugs
def search_bugs(hierarchyfull, bts, queryonly, mirrors,
http_proxy, screen, title):
"""Search for the bug list using a pattern."""
"""Return string "FilterEnd" when we are done with search."""
pattern = our_raw_input(
'Enter the search pattern (a Perl-compatible regular expression)\n'
'or press ENTER to exit: ')
if not pattern:
return "FilterEnd"
" Create new hierarchy match the pattern."
import hiermatch
try:
hierarchy = hiermatch.matched_hierarchy(hierarchyfull, pattern)
except InvalidRegex:
our_raw_input('Invalid regular expression, press ENTER to continue.')
return "FilterEnd"
count, bugs = proc_hierarchy(hierarchy)
if not count:
our_raw_input('No match found, press ENTER to continue.')
return "FilterEnd"
endcount = catcount = 0
scount = startcount = 1
category = hierarchy[0]
lastpage = []
digits = len(str(len(bugs)))
linefmt = ' %'+str(digits)+'d) %s\n'
while category:
scount = scount + 1
catname, reports = category[0:2]
while catname[-1] == ':': catname = catname[:-1]
total = len(reports)
while len(reports):
these = reports[:rows-2]
reports = reports[rows-2:]
remain = len(reports)
tplural = rplural = 's'
if total == 1: tplural=''
if remain != 1: rplural=''
if remain:
lastpage.append(' %s: %d report%s (%d remain%s)\n' %
(catname, total, tplural, remain, rplural))
else:
lastpage.append(' %s: %d report%s\n' %
(catname, total, tplural))
oldscount, oldecount = scount, endcount
for report in these:
scount = scount + 1
endcount = endcount + 1
lastpage.append(linefmt % (endcount,report[:columns-digits-5]))
if category == hierarchy[-1] or \
(scount >= (rows - len(hierarchy[catcount+1][1]) - 1)):
skipmsg = ' (s to skip rest)'
if endcount == count:
skipmsg = ''
options = 'yNmrqsfut'
if queryonly: options = 'Nmrqfut'
rstr = "(%d-%d/%d) " % (startcount, endcount, count)
pstr = rstr + "Is the bug you found listed above"
if queryonly:
pstr = rstr + "What would you like to do next"
allowed = bugs+range(1, count+1)
helptext = {
'y' : 'Problem already reported; optionally '
'add extra information.',
'n' : 'Problem not listed above; possibly '
'check more.',
'm' : 'Get more information about a bug (you '
'can also enter a number\n'
' without selecting "m" first).',
'r' : 'Redisplay the last bugs shown.',
'q' : "I'm bored; quit please.",
's' : 'Skip remaining problems; file a new '
'report immediately.',
'f' : 'Filter bug list using a pattern.',
'u' : 'Up one level of filter.',
't' : 'Top of the bug list (remove all filters).'}
if skipmsg:
helptext['n'] = helptext['n'][:-1]+' (skip to Next page).'
while 1:
sys.stderr.writelines(lastpage)
x = select_options(pstr, options, helptext,
allow_numbers=allowed)
if x == 'n':
lastpage = []
break
elif x == 'r':
continue
elif x == 'q':
raise NoReport
elif x == 's':
return
elif x == 'y':
if queryonly:
return
number = our_raw_input(
'Enter the number of the bug report '
'you want to give more info on,\n'
'or press ENTER to exit: #', allowed)
while number and number[0] == '#':
number=number[1:]
if number:
try:
number = int(number)
if number not in bugs and 1 <= number <= len(bugs):
number = bugs[number-1]
return number
except ValueError:
ewrite('Invalid report number: %s\n',
number)
else:
raise NoReport
elif x == 'f':
# Do filter. Recursive done.
retval = search_bugs(hierarchy, bts, queryonly, mirrors,
http_proxy, screen, title)
if retval == "FilterEnd":
continue
else:
return retval
elif x == 'u':
# Up a level
return "FilterEnd"
elif x == 't':
# go back to the Top level.
return "Top"
else:
if x == 'm' or x == 'i':
number = our_raw_input(
'Please enter the number of the bug '
'you would like more info on: #',
allowed)
else:
number = x
while number and number[0] == '#':
number=number[1:]
if number:
try:
number = int(number)
if number not in bugs and 1 <= number <= len(bugs):
number = bugs[number-1]
res = show_report(number, bts, mirrors,
http_proxy,
queryonly=queryonly,
screen=screen,
title=title)
if res:
return res
except ValueError:
ewrite('Invalid report number: %s\n',
number)
startcount = endcount+1
scount = 0
# these now empty
if category == hierarchy[-1]: break
catcount = catcount+1
category = hierarchy[catcount]
if scount:
lastpage.append('\n')
scount = scount + 1
return "FilterEnd"
--- /usr/bin/reportbug 2004-03-30 01:00:22.000000000 +0200
+++ reportbug 2004-04-13 02:01:28.000000000 +0200
@@ -393,7 +393,7 @@
'interface for reportbug.', reportbug.UIS,
'Select interface: ', options.interface)
else:
- interface = 'text'
+ interface = 'dialog'
online = ui.yes_no('Will reportbug often have direct '
'Internet access? (You should answer yes to this '
@@ -568,6 +568,10 @@
if charset.lower() == 'ansi_x3.4-1968':
charset = 'us-ascii'
+ iface = 'reportbug_ui_dialog'
+ exec 'import '+iface
+ ui = eval(iface)
+
notatty = not ui.ISATTY
body = ''
filename = None
@@ -869,7 +873,7 @@
# Fix up command line options for GNOME interface
sys.argv = sys.argv[:1] + list(args)
if options.interface:
- if options.interface in ('gnome', 'newt'):
+ if options.interface in ('gnome', 'newt','dialog'):
ui.long_message("The %s interface is not supported. Unless you "
"are debugging reportbug, please do not use it. "
"If you are debugging reportbug, please DO NOT "
@@ -880,6 +884,10 @@
iface = 'reportbug_ui_'+options.interface
exec 'import '+iface
ui = eval(iface)
+
+ iface = 'reportbug_ui_dialog'
+ exec 'import '+iface
+ ui = eval(iface)
if (reportbug.first_run() and not args):
offer_configuration(options)
Reply to: