Add a curses terminal interface as an alternative to QT #666

Merged
lwizchz merged 11 commits from master into master 2014-07-14 20:06:21 +02:00
3 changed files with 306 additions and 22 deletions
Showing only changes of commit 813f4c7ed9 - Show all commits

View File

@ -1,27 +1,54 @@
# Copyright (c) 2014 Luke Montalvo <lukemontalvo@gmail.com> # Copyright (c) 2014 Luke Montalvo <lukemontalvo@gmail.com>
# This file adds a alternative commandline interface # This file adds a alternative commandline interface
#
# Dependencies:
# * from python2-pip
# * python2-pythondialog
# * dialog
import curses
import shared
import os import os
import sys import sys
import StringIO import StringIO
import time
from time import strftime, localtime
from threading import Timer
import curses
import dialog
from dialog import Dialog
import shared
import ConfigParser
from addresses import * from addresses import *
quit = False quit = False
menutab = 1 menutab = 1
menu = ["Inbox", "Send", "Sent", "Your Identities", "Subscriptions", "Address Book", "Blacklist", "Network Status"] menu = ["Inbox", "Send", "Sent", "Your Identities", "Subscriptions", "Address Book", "Blacklist", "Network Status"]
log = "" log = ""
logpad = None
inventorydata = 0
startuptime = time.time()
addresses = [] addresses = []
addrcur = 0 addrcur = 0
addrcopy = 0
class printLog: class printLog:
def write(self, output): def write(self, output):
global log global log
log += output log += output
def flush(self):
pass
class errLog:
def write(self, output):
global log
log += "!"+output
def flush(self):
pass
printlog = printLog() printlog = printLog()
errlog = errLog()
def cpair(a): def cpair(a):
r = curses.color_pair(a) r = curses.color_pair(a)
@ -42,18 +69,81 @@ def drawmenu(stdscr):
menustr += " " menustr += " "
stdscr.addstr(2, 5, menustr, curses.A_UNDERLINE) stdscr.addstr(2, 5, menustr, curses.A_UNDERLINE)
def resetlookups():
inventorydata = shared.numberOfInventoryLookupsPerformed
shared.numberOfInventoryLookupsPerformed = 0
Timer(2, resetlookups, ()).start()
def drawtab(stdscr): def drawtab(stdscr):
if menutab in range(0, len(menu)): if menutab in range(1, len(menu)+1):
if menutab == 1: # Inbox if menutab == 1: # Inbox
stdscr.addstr(3, 5, "new messages") pass
elif menutab == 2: # Send elif menutab == 2: # Send
stdscr.addstr(3, 5, "to: from:") pass
elif menutab == 3: # Sent
pass
elif menutab == 4: # Identities elif menutab == 4: # Identities
stdscr.addstr(3, 5, "Label", curses.A_BOLD)
stdscr.addstr(3, 50, "Address", curses.A_BOLD)
stdscr.addstr(3, 100, "Stream", curses.A_BOLD)
for i, item in enumerate(addresses): for i, item in enumerate(addresses):
a = 0 a = 0
if i == addrcur: if i == addrcur: # Highlight current address
a = curses.A_REVERSE a = a | curses.A_REVERSE
stdscr.addstr(3+i, 5, item[0], cpair(item[3]) | a) if item[1] == True and item[3] not in [8,9]: # Embolden enabled, non-special addresses
a = a | curses.A_BOLD
stdscr.addstr(4+i, 5, item[0], a)
stdscr.addstr(4+i, 50, item[2], cpair(item[3]) | a)
stdscr.addstr(4+i, 100, str(1), a)
elif menutab == 5: # Subscriptions
pass
elif menutab == 6: # Address book
pass
elif menutab == 7: # Blacklist
pass
elif menutab == 8: # Network status
# Connection data
stdscr.addstr(4, 5, "Total Connections: "+str(len(shared.connectedHostsList)).ljust(2))
stdscr.addstr(6, 6, "Stream #", curses.A_BOLD)
stdscr.addstr(6, 17, "Connections", curses.A_BOLD)
streamcount = []
for host, stream in shared.connectedHostsList.items():
if stream >= len(streamcount):
streamcount.append(1)
else:
streamcount[stream] += 1
for i, item in enumerate(streamcount):
if i < 5:
if i == 0:
stdscr.addstr(7+i, 6, "?")
else:
stdscr.addstr(7+i, 6, str(i))
stdscr.addstr(7+i, 17, str(item))
# Uptime and processing data
stdscr.addstr(6, 35, "Since startup on "+unicode(strftime(shared.config.get('bitmessagesettings', 'timeformat'), localtime(int(startuptime)))))
stdscr.addstr(7, 40, "Processed "+str(shared.numberOfMessagesProcessed).ljust(4)+" person-to-person messages.")
stdscr.addstr(8, 40, "Processed "+str(shared.numberOfBroadcastsProcessed).ljust(4)+" broadcast messages.")
stdscr.addstr(9, 40, "Processed "+str(shared.numberOfPubkeysProcessed).ljust(4)+" public keys.")
# Inventory data
stdscr.addstr(11, 35, "Inventory lookups per second: "+str(int(inventorydata/2)).ljust(3))
# Log
stdscr.addstr(13, 6, "Log", curses.A_BOLD)
n = log.count('\n')
if n > 0:
l = log.split('\n')
if n > 512:
del l[:(n-256)]
logpad.erase()
n = len(l)
for i, item in enumerate(l):
a = 0
if len(item) > 0 and item[0] == '!':
a = curses.color_pair(1)
item = item[1:]
logpad.addstr(i, 0, item, a)
logpad.refresh(n-curses.LINES+2, 0, 14, 6, curses.LINES-2, curses.COLS-7)
stdscr.refresh() stdscr.refresh()
def redraw(stdscr): def redraw(stdscr):
@ -61,6 +151,10 @@ def redraw(stdscr):
stdscr.border() stdscr.border()
drawmenu(stdscr) drawmenu(stdscr)
stdscr.refresh() stdscr.refresh()
def dialogreset(stdscr):
stdscr.clear()
stdscr.keypad(1)
curses.curs_set(0)
def handlech(c, stdscr): def handlech(c, stdscr):
if c != curses.ERR: if c != curses.ERR:
if c in range(256): if c in range(256):
@ -70,20 +164,176 @@ def handlech(c, stdscr):
elif chr(c) == 'q': elif chr(c) == 'q':
global quit global quit
quit = True quit = True
elif chr(c) == '\n':
if menutab == 4:
curses.curs_set(1)
d = Dialog(dialog="dialog")
d.set_background_title("Your Identities Dialog Box")
r, t = d.menu("Do what with \""+addresses[addrcur][0]+"\" : \""+addresses[addrcur][2]+"\"?",
choices=[("1", "Create new address"),
("2", "Copy address to internal buffer"),
("3", "Rename"),
("4", "Enable"),
("5", "Disable"),
("6", "Delete"),
("7", "Special address behavior")])
if r == d.DIALOG_OK:
if t == "1": # Create new address
d.set_background_title("Create new address")
d.scrollbox(unicode("Here you may generate as many addresses as you like.\n"
"Indeed, creating and abandoning addresses is encouraged.\n"
"Deterministic addresses have several pros and cons:\n"
"\nPros:\n"
" * You can recreate your addresses on any computer from memory\n"
" * You need not worry about backing up your keys.dat file as long as you \n can remember your passphrase\n"
"Cons:\n"
" * You must remember (or write down) your passphrase in order to recreate \n your keys if they are lost\n"
" * You must also remember the address version and stream numbers\n"
" * If you choose a weak passphrase someone may be able to brute-force it \n and then send and receive messages as you"),
exit_label="Continue")
r, t = d.menu("Choose an address generation technique",
choices=[("1", "Use a random number generator"),
("2", "Use a passphrase")])
if r == d.DIALOG_OK:
if t == "1":
d.set_background_title("Randomly generate address")
r, t = d.inputbox("Label (not shown to anyone except you)")
label = ""
if r == d.DIALOG_OK and len(t) > 0:
label = t
r, t = d.menu("Choose a stream",
choices=[("1", "Use the most available stream"),("", "(Best if this is the first of many addresses you will create)"),
("2", "Use the same stream as an existing address"),("", "(Saves you some bandwidth and processing power)")])
if r == d.DIALOG_OK:
if t == "1":
stream = 1
elif t == "2":
addrs = []
for i, item in enumerate(addresses):
addrs.append([str(i), item[2]])
r, t = d.menu("Choose an existing address's stream", choices=addrs)
if r == d.DIALOG_OK:
stream = decodeAddress(addrs[int(t)][1])[2]
shorten = False
r, t = d.checklist("Miscellaneous options",
choices=[("1", "Spend time shortening the address", shorten)])
if r == d.DIALOG_OK and "1" in t:
shorten = True
shared.addressGeneratorQueue.put(("createRandomAddress", 4, stream, label, 1, "", shorten))
elif t == "2":
d.set_background_title("Make deterministic addresses")
r, t = d.passwordform("Enter passphrase",
[("Passphrase", 1, 1, "", 2, 1, 64, 128),
("Confirm passphrase", 3, 1, "", 4, 1, 64, 128)],
form_height=4, insecure=True)
if r == d.DIALOG_OK:
if t[0] == t[1]:
passphrase = t[0]
r, t = d.rangebox("Number of addresses to generate",
width=48, min=1, max=99, init=8)
if r == d.DIALOG_OK:
number = t
stream = 1
shorten = False
r, t = d.checklist("Miscellaneous options",
choices=[("1", "Spend time shortening the address", shorten)])
if r == d.DIALOG_OK and "1" in t:
shorten = True
d.scrollbox(unicode("In addition to your passphrase, be sure to remember the following numbers:\n"
"\n * Address version number: "+str(4)+"\n"
" * Stream number: "+str(stream)),
exit_label="Continue")
shared.addressGeneratorQueue.put(('createDeterministicAddresses', 4, stream, "unused deterministic address", number, str(passphrase), shorten))
else:
d.scrollbox(unicode("Passphrases do not match"), exit_label="Continue")
elif t == "2": # Copy address to internal buffer
addrcopy = addrcur
elif t == "3": # Rename address label
a = addresses[addrcur][2]
label = addresses[addrcur][0]
r, t = d.inputbox("New address label", init=label)
if r == d.DIALOG_OK:
label = t
shared.config.set(a, "label", label)
# Write config
with open(shared.appdata + 'keys.dat', 'wb') as configfile:
shared.config.write(configfile)
addresses[addrcur][0] = label
elif t == "4": # Enable address
a = addresses[addrcur][2]
shared.config.set(a, "enabled", "true") # Set config
# Write config
with open(shared.appdata + 'keys.dat', 'wb') as configfile:
shared.config.write(configfile)
# Change color
if shared.safeConfigGetBoolean(a, 'chan'):
addresses[addrcur][3] = 9 # orange
elif shared.safeConfigGetBoolean(a, 'mailinglist'):
addresses[addrcur][3] = 5 # magenta
else:
addresses[addrcur][3] = 0 # black
addresses[addrcur][1] = True
shared.reloadMyAddressHashes() # Reload address hashes
elif t == "5": # Disable address
a = addresses[addrcur][2]
shared.config.set(a, "enabled", "false") # Set config
addresses[addrcur][3] = 8 # Set color to gray
# Write config
with open(shared.appdata + 'keys.dat', 'wb') as configfile:
shared.config.write(configfile)
addresses[addrcur][1] = False
shared.reloadMyAddressHashes() # Reload address hashes
elif t == "6": # Delete address
pass
elif t == "7": # Special address behavior
a = addresses[addrcur][2]
d.set_background_title("Special address behavior")
if shared.safeConfigGetBoolean(a, "chan"):
d.scrollbox(unicode("This is a chan address. You cannot use it as a pseudo-mailing list."), exit_label="Continue")
else:
m = shared.safeConfigGetBoolean(a, "mailinglist")
r, t = d.radiolist("Select address behavior",
choices=[("1", "Behave as a normal address", not m), ("2", "Behave as a pseudo-mailing-list address", m)])
if r == d.DIALOG_OK:
if t == "1" and m == True:
shared.config.set(a, "mailinglist", "false")
if addresses[addrcur][1]:
addresses[addrcur][3] = 0 # Set color to black
else:
addresses[addrcur][3] = 8 # Set color to gray
elif t == "2" and m == False:
try:
mn = shared.config.get(a, "mailinglistname")
except ConfigParser.NoOptionError:
mn = ""
r, t = d.inputbox("Mailing list name", init=mn)
if r == d.DIALOG_OK:
mn = t
shared.config.set(a, "mailinglist", "true")
shared.config.set(a, "mailinglistname", mn)
addresses[addrcur][3] = 6 # Set color to magenta
# Write config
with open(shared.appdata + 'keys.dat', 'wb') as configfile:
shared.config.write(configfile)
dialogreset(stdscr)
else: else:
global addrcur global addrcur
if c == curses.KEY_UP: if c == curses.KEY_UP:
if (addrcur > 0): if menutab == 4 and addrcur > 0:
addrcur -= 1 addrcur -= 1
elif c == curses.KEY_DOWN: elif c == curses.KEY_DOWN:
if (addrcur < len(addresses)-1): if menutab == 4 and addrcur < len(addresses)-1:
addrcur += 1 addrcur += 1
redraw(stdscr) redraw(stdscr)
def runwrapper(): def runwrapper():
sys.stdout = printlog sys.stdout = printlog
sys.stderr = errlog
stdscr = curses.initscr() stdscr = curses.initscr()
global logpad
logpad = curses.newpad(1024, curses.COLS)
stdscr.nodelay(1) stdscr.nodelay(1)
curses.curs_set(0) curses.curs_set(0)
curses.start_color() curses.start_color()
@ -92,29 +342,43 @@ def runwrapper():
shutdown() shutdown()
def run(stdscr): def run(stdscr):
# Init list of address in 'Your Identities' tab # Schedule inventory lookup data
configSections = shared.config.sections() resetlookups()
# Init color pairs
if curses.has_colors(): if curses.has_colors():
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) # red curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) # red
curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # green
curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) # yellow
curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) # blue
curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK) # magenta
curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK) # cyan
curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK) # white
if curses.can_change_color(): if curses.can_change_color():
curses.init_color(8, 500, 500, 500) # gray curses.init_color(8, 500, 500, 500) # gray
curses.init_pair(8, 8, 0) curses.init_pair(8, 8, 0)
curses.init_color(9, 844, 465, 0) # orange curses.init_color(9, 844, 465, 0) # orange
curses.init_pair(9, 9, 0) curses.init_pair(9, 9, 0)
else: else:
global menutab curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLACK) # grayish
menutab = 4 curses.init_pair(9, curses.COLOR_YELLOW, curses.COLOR_BLACK) # orangish
curses.beep()
# Init list of address in 'Your Identities' tab
configSections = shared.config.sections()
for addressInKeysFile in configSections: for addressInKeysFile in configSections:
if addressInKeysFile != "bitmessagesettings": if addressInKeysFile != "bitmessagesettings":
isEnabled = shared.config.getboolean(addressInKeysFile, "enabled") isEnabled = shared.config.getboolean(addressInKeysFile, "enabled")
addresses.append([shared.config.get(addressInKeysFile, "label"), isEnabled, str(decodeAddress(addressInKeysFile)[2])]) addresses.append([shared.config.get(addressInKeysFile, "label"), isEnabled, addressInKeysFile])
# Set address color
if not isEnabled: if not isEnabled:
addresses[len(addresses)-1].append(8) # gray addresses[len(addresses)-1].append(8) # gray
elif shared.safeConfigGetBoolean(addressInKeysFile, 'chan'): elif shared.safeConfigGetBoolean(addressInKeysFile, 'chan'):
addresses[len(addresses)-1].append(9) # orange addresses[len(addresses)-1].append(9) # orange
elif shared.safeConfigGetBoolean(addressInKeysFile, 'mailinglist'): elif shared.safeConfigGetBoolean(addressInKeysFile, 'mailinglist'):
addresses[len(addresses)-1].append(5) # magenta addresses[len(addresses)-1].append(5) # magenta
else:
addresses[len(addresses)-1].append(0) # black
addresses.reverse()
# Load messages from database # Load messages from database
""" """
@ -140,5 +404,6 @@ def shutdown():
sys.stdout = printlog sys.stdout = printlog
shared.doCleanShutdown() shared.doCleanShutdown()
sys.stdout = sys.__stdout__ sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
os._exit(0) os._exit(0)

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python2.7
# Copyright (c) 2012 Jonathan Warren # Copyright (c) 2012 Jonathan Warren
# Copyright (c) 2012 The Bitmessage developers # Copyright (c) 2012 The Bitmessage developers
# Distributed under the MIT/X11 software license. See the accompanying # Distributed under the MIT/X11 software license. See the accompanying
@ -99,6 +99,11 @@ class Main:
# is the application already running? If yes then exit. # is the application already running? If yes then exit.
thisapp = singleton.singleinstance() thisapp = singleton.singleinstance()
# get curses flag
curses = False
if '-c' in sys.argv:
curses = True
signal.signal(signal.SIGINT, helper_generic.signal_handler) signal.signal(signal.SIGINT, helper_generic.signal_handler)
# signal.signal(signal.SIGINT, signal.SIG_DFL) # signal.signal(signal.SIGINT, signal.SIG_DFL)
@ -159,10 +164,16 @@ class Main:
except Exception as err: except Exception as err:
print 'PyBitmessage requires PyQt unless you want to run it as a daemon and interact with it using the API. You can download PyQt from http://www.riverbankcomputing.com/software/pyqt/download or by searching Google for \'PyQt Download\'. If you want to run in daemon mode, see https://bitmessage.org/wiki/Daemon' print 'PyBitmessage requires PyQt unless you want to run it as a daemon and interact with it using the API. You can download PyQt from http://www.riverbankcomputing.com/software/pyqt/download or by searching Google for \'PyQt Download\'. If you want to run in daemon mode, see https://bitmessage.org/wiki/Daemon'
print 'Error message:', err print 'Error message:', err
print 'You can also run PyBitmessage with the new curses interface by providing \'-c\' as a commandline argument.'
os._exit(0) os._exit(0)
if curses == False:
import bitmessageqt import bitmessageqt
bitmessageqt.run() bitmessageqt.run()
else:
print 'Running with curses'
import bitmessagecurses
bitmessagecurses.runwrapper()
else: else:
shared.config.remove_option('bitmessagesettings', 'dontconnect') shared.config.remove_option('bitmessagesettings', 'dontconnect')

View File

@ -19,6 +19,7 @@ Use: `from debug import logger` to import this facility into whatever module you
import logging import logging
import logging.config import logging.config
import shared import shared
import sys
# TODO(xj9): Get from a config file. # TODO(xj9): Get from a config file.
log_level = 'DEBUG' log_level = 'DEBUG'
@ -69,6 +70,9 @@ def configureLogging():
# TODO (xj9): Get from a config file. # TODO (xj9): Get from a config file.
#logger = logging.getLogger('console_only') #logger = logging.getLogger('console_only')
configureLogging() configureLogging()
if '-c' in sys.argv:
logger = logging.getLogger('file_only')
else:
logger = logging.getLogger('both') logger = logging.getLogger('both')
def restartLoggingInUpdatedAppdataLocation(): def restartLoggingInUpdatedAppdataLocation():
@ -78,4 +82,8 @@ def restartLoggingInUpdatedAppdataLocation():
i.flush() i.flush()
i.close() i.close()
configureLogging() configureLogging()
if '-c' in sys.argv:
logger = logging.getLogger('file_only')
else:
logger = logging.getLogger('both') logger = logging.getLogger('both')