diff --git a/src/bitmessagecurses/__init__.py b/src/bitmessagecurses/__init__.py index f4d292d7..b030f24c 100644 --- a/src/bitmessagecurses/__init__.py +++ b/src/bitmessagecurses/__init__.py @@ -1,27 +1,54 @@ # Copyright (c) 2014 Luke Montalvo # This file adds a alternative commandline interface +# +# Dependencies: +# * from python2-pip +# * python2-pythondialog +# * dialog -import curses -import shared import os import sys 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 * quit = False menutab = 1 menu = ["Inbox", "Send", "Sent", "Your Identities", "Subscriptions", "Address Book", "Blacklist", "Network Status"] log = "" +logpad = None +inventorydata = 0 +startuptime = time.time() addresses = [] addrcur = 0 +addrcopy = 0 class printLog: def write(self, output): global log log += output + def flush(self): + pass +class errLog: + def write(self, output): + global log + log += "!"+output + def flush(self): + pass printlog = printLog() +errlog = errLog() + def cpair(a): r = curses.color_pair(a) @@ -42,18 +69,81 @@ def drawmenu(stdscr): menustr += " " stdscr.addstr(2, 5, menustr, curses.A_UNDERLINE) +def resetlookups(): + inventorydata = shared.numberOfInventoryLookupsPerformed + shared.numberOfInventoryLookupsPerformed = 0 + Timer(2, resetlookups, ()).start() def drawtab(stdscr): - if menutab in range(0, len(menu)): + if menutab in range(1, len(menu)+1): if menutab == 1: # Inbox - stdscr.addstr(3, 5, "new messages") + pass elif menutab == 2: # Send - stdscr.addstr(3, 5, "to: from:") + pass + elif menutab == 3: # Sent + pass 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): a = 0 - if i == addrcur: - a = curses.A_REVERSE - stdscr.addstr(3+i, 5, item[0], cpair(item[3]) | a) + if i == addrcur: # Highlight current address + a = a | curses.A_REVERSE + 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() def redraw(stdscr): @@ -61,6 +151,10 @@ def redraw(stdscr): stdscr.border() drawmenu(stdscr) stdscr.refresh() +def dialogreset(stdscr): + stdscr.clear() + stdscr.keypad(1) + curses.curs_set(0) def handlech(c, stdscr): if c != curses.ERR: if c in range(256): @@ -70,20 +164,176 @@ def handlech(c, stdscr): elif chr(c) == 'q': global quit 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: global addrcur if c == curses.KEY_UP: - if (addrcur > 0): + if menutab == 4 and addrcur > 0: addrcur -= 1 elif c == curses.KEY_DOWN: - if (addrcur < len(addresses)-1): + if menutab == 4 and addrcur < len(addresses)-1: addrcur += 1 redraw(stdscr) def runwrapper(): sys.stdout = printlog + sys.stderr = errlog stdscr = curses.initscr() + global logpad + logpad = curses.newpad(1024, curses.COLS) + stdscr.nodelay(1) curses.curs_set(0) curses.start_color() @@ -92,29 +342,43 @@ def runwrapper(): shutdown() def run(stdscr): - # Init list of address in 'Your Identities' tab - configSections = shared.config.sections() + # Schedule inventory lookup data + resetlookups() + + # Init color pairs if curses.has_colors(): 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(): curses.init_color(8, 500, 500, 500) # gray curses.init_pair(8, 8, 0) curses.init_color(9, 844, 465, 0) # orange curses.init_pair(9, 9, 0) - else: - global menutab - menutab = 4 - curses.beep() + else: + curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLACK) # grayish + curses.init_pair(9, curses.COLOR_YELLOW, curses.COLOR_BLACK) # orangish + + # Init list of address in 'Your Identities' tab + configSections = shared.config.sections() for addressInKeysFile in configSections: if addressInKeysFile != "bitmessagesettings": 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: addresses[len(addresses)-1].append(8) # gray elif shared.safeConfigGetBoolean(addressInKeysFile, 'chan'): addresses[len(addresses)-1].append(9) # orange elif shared.safeConfigGetBoolean(addressInKeysFile, 'mailinglist'): addresses[len(addresses)-1].append(5) # magenta + else: + addresses[len(addresses)-1].append(0) # black + addresses.reverse() # Load messages from database """ @@ -140,5 +404,6 @@ def shutdown(): sys.stdout = printlog shared.doCleanShutdown() sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ os._exit(0) diff --git a/src/bitmessagemain.py b/src/bitmessagemain.py index e4a073d9..b92c2795 100755 --- a/src/bitmessagemain.py +++ b/src/bitmessagemain.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2.7 # Copyright (c) 2012 Jonathan Warren # Copyright (c) 2012 The Bitmessage developers # 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. 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, signal.SIG_DFL) @@ -159,10 +164,16 @@ class Main: 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 'Error message:', err + print 'You can also run PyBitmessage with the new curses interface by providing \'-c\' as a commandline argument.' os._exit(0) - import bitmessageqt - bitmessageqt.run() + if curses == False: + import bitmessageqt + bitmessageqt.run() + else: + print 'Running with curses' + import bitmessagecurses + bitmessagecurses.runwrapper() else: shared.config.remove_option('bitmessagesettings', 'dontconnect') diff --git a/src/debug.py b/src/debug.py index fe7815e7..02ab94f8 100644 --- a/src/debug.py +++ b/src/debug.py @@ -19,6 +19,7 @@ Use: `from debug import logger` to import this facility into whatever module you import logging import logging.config import shared +import sys # TODO(xj9): Get from a config file. log_level = 'DEBUG' @@ -69,7 +70,10 @@ def configureLogging(): # TODO (xj9): Get from a config file. #logger = logging.getLogger('console_only') configureLogging() -logger = logging.getLogger('both') +if '-c' in sys.argv: + logger = logging.getLogger('file_only') +else: + logger = logging.getLogger('both') def restartLoggingInUpdatedAppdataLocation(): global logger @@ -78,4 +82,8 @@ def restartLoggingInUpdatedAppdataLocation(): i.flush() i.close() configureLogging() - logger = logging.getLogger('both') \ No newline at end of file + if '-c' in sys.argv: + logger = logging.getLogger('file_only') + else: + logger = logging.getLogger('both') +