From 5d3ed7d9761e4a7c5d78cc4da5f0f8bbff119522 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Wed, 9 Nov 2022 13:05:09 +0000 Subject: [PATCH] mmgen.tw.common: new format(), header(), subheader() methods --- mmgen/obj.py | 3 + mmgen/objmethods.py | 2 +- mmgen/proto/btc/tw/txhistory.py | 2 +- mmgen/proto/btc/tw/unspent.py | 2 +- mmgen/proto/eth/tw/unspent.py | 14 ++-- mmgen/rpc.py | 3 + mmgen/tool/rpc.py | 4 +- mmgen/tw/common.py | 125 +++++++++++++++++------------ mmgen/tw/txhistory.py | 26 +++--- mmgen/tw/unspent.py | 34 +++++--- test/overlay/fakemods/mmgen/rpc.py | 3 +- 11 files changed, 127 insertions(+), 91 deletions(-) diff --git a/mmgen/obj.py b/mmgen/obj.py index 088eacfa..320b3e0e 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -280,6 +280,9 @@ class Int(int,Hilite,InitErrors): def colorize(cls,n,**kwargs): return super().colorize(repr(n),**kwargs) +class NonNegativeInt(Int): + min_val = 0 + class MMGenIdx(Int): min_val = 1 diff --git a/mmgen/objmethods.py b/mmgen/objmethods.py index 4b0c2b08..05ced4c3 100755 --- a/mmgen/objmethods.py +++ b/mmgen/objmethods.py @@ -71,7 +71,7 @@ class Hilite: assert trunc_ok, "If 'trunc_ok' is false, 'width' must be >= screen width of string" s = truncate_str(s,width-add_len) if s == '' and nullrepl: - s = nullrepl.ljust(width) + s = nullrepl else: s = a+s+b if center: diff --git a/mmgen/proto/btc/tw/txhistory.py b/mmgen/proto/btc/tw/txhistory.py index e01033f7..9e21d35c 100755 --- a/mmgen/proto/btc/tw/txhistory.py +++ b/mmgen/proto/btc/tw/txhistory.py @@ -224,7 +224,7 @@ class BitcoinTwTransaction(BitcoinTwCommon): class BitcoinTwTxHistory(TwTxHistory,BitcoinTwCommon): has_age = True - hdr_fmt = 'TRANSACTION HISTORY (sort order: {a})' + hdr_lbl = 'transaction history' desc = 'transaction history' item_desc = 'transaction' no_data_errmsg = 'No transactions in tracking wallet!' diff --git a/mmgen/proto/btc/tw/unspent.py b/mmgen/proto/btc/tw/unspent.py index cf101eb9..42ef10ae 100755 --- a/mmgen/proto/btc/tw/unspent.py +++ b/mmgen/proto/btc/tw/unspent.py @@ -24,7 +24,7 @@ class BitcoinTwUnspentOutputs(TwUnspentOutputs): has_age = True can_group = True - hdr_fmt = 'UNSPENT OUTPUTS (sort order: {a}) Total {b}: {c}' + hdr_lbl = 'unspent outputs' desc = 'unspent outputs' item_desc = 'unspent output' no_data_errmsg = 'No unspent outputs in tracking wallet!' diff --git a/mmgen/proto/eth/tw/unspent.py b/mmgen/proto/eth/tw/unspent.py index 869b4714..d3f1cd96 100755 --- a/mmgen/proto/eth/tw/unspent.py +++ b/mmgen/proto/eth/tw/unspent.py @@ -20,6 +20,7 @@ proto.eth.twuo: Ethereum tracking wallet unspent outputs class """ +from ....globalvars import g from ....tw.common import TwLabel from ....tw.unspent import TwUnspentOutputs @@ -33,7 +34,7 @@ class EthereumTwUnspentOutputs(TwUnspentOutputs): has_age = False can_group = False col_adj = 29 - hdr_fmt = 'TRACKED ACCOUNTS (sort order: {a})\nTotal {b}: {c}' + hdr_lbl = 'tracked accounts' desc = 'account balances' item_desc = 'account' dump_fn_pfx = 'balances' @@ -62,12 +63,13 @@ Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view, wide_fs_fs = ' {{n:4}} {{a}} {{m}} {{A:{aw}}} {{l}}' no_data_errmsg = 'No accounts in tracking wallet!' - async def __init__(self,proto,*args,**kwargs): - from ....globalvars import g + def subheader(self,color): if g.cached_balances: - from ....color import yellow - self.hdr_fmt += '\n' + yellow('WARNING: Using cached balances. These may be out of date!') - await super().__init__(proto,*args,**kwargs) + from ....color import nocolor,yellow + return (nocolor,yellow)[color]( + 'WARNING: Using cached balances. These may be out of date!') + '\n' + else: + return '' def do_sort(self,key=None,reverse=False): if key == 'txid': return diff --git a/mmgen/rpc.py b/mmgen/rpc.py index bf855c53..7519e995 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -26,6 +26,7 @@ from collections import namedtuple from .common import * from .base_obj import AsyncInit +from .obj import NonNegativeInt from .objmethods import Hilite,InitErrors,MMGenObject auth_data = namedtuple('rpc_auth_data',['user','passwd']) @@ -468,4 +469,6 @@ async def rpc_init( RPC client chain: {rpc.chain} """,indent=' ').rstrip() ) + rpc.blockcount = NonNegativeInt(rpc.blockcount) + return rpc diff --git a/mmgen/tool/rpc.py b/mmgen/tool/rpc.py index 117cf8a7..47bb1ec2 100755 --- a/mmgen/tool/rpc.py +++ b/mmgen/tool/rpc.py @@ -112,10 +112,8 @@ class tool_cmd(tool_cmd_base): if interactive: await obj.view_and_sort() return True - elif detail: - return await obj.format_detail( color=True ) else: - return await obj.format_squeezed() + return await obj.format('detail' if detail else 'squeezed') async def twview(self, pager: 'send output to pager' = False, diff --git a/mmgen/tw/common.py b/mmgen/tw/common.py index b8758d3c..d9b7ee98 100755 --- a/mmgen/tw/common.py +++ b/mmgen/tw/common.py @@ -26,7 +26,7 @@ from collections import namedtuple from ..globalvars import g from ..objmethods import Hilite,InitErrors,MMGenObject from ..obj import TwComment,get_obj,MMGenIdx,MMGenList -from ..color import nocolor,yellow,green +from ..color import nocolor,yellow,green,red,blue from ..util import msg,msg_r,fmt,die,capfirst,make_timestr from ..addr import MMGenID @@ -39,6 +39,8 @@ class TwCommon: group = False sort_key = 'age' interactive = False + _display_data = {} + filters = () age_fmts = ('confs','block','days','date','date_time') age_fmts_date_dependent = ('days','date','date_time') @@ -72,6 +74,22 @@ class TwCommon: Please resize your screen to at least {} characters and hit any key: """ + class display_type: + + class squeezed: + detail = False + fmt_method = 'gen_squeezed_display' + need_column_widths = True + item_separator = '\n' + print_header = '[screen print truncated to width {}]\n' + + class detail: + detail = True + fmt_method = 'gen_detail_display' + need_column_widths = True + item_separator = '\n' + print_header = '' + def age_disp(self,o,age_fmt): if age_fmt == 'confs': return o.confs @@ -237,53 +255,60 @@ class TwCommon: else: return do_ret(get_freews(self.cols,varws,varw,minw)) - async def format_squeezed(self,color=True,cached=False): + def header(self,color): + + Blue,Green = (blue,green) if color else (nocolor,nocolor) + yes,no = green('yes'),red('no') if color else ('yes','no') + + def fmt_filter(k): + return '{}:{}'.format(k,yes if getattr(self,k) else no) + + return '{h} (sort order: {s}){f}\nNetwork: {n}\nBlock {b} [{d}]\n{t}'.format( + h = self.hdr_lbl.upper(), + f = '\nFilters: '+' '.join(fmt_filter(k) for k in self.filters) if self.filters else '', + s = Blue(' '.join(self.sort_info())), + n = Green(self.proto.coin + ' ' + self.proto.chain_name.upper()), + b = self.rpc.blockcount.hl(color=color), + d = make_timestr(self.rpc.cur_date), + t = f'Total {self.proto.dcoin}: {self.total.hl(color=color)}\n' if hasattr(self,'total') else '', + ) + + def subheader(self,color): + return '' + + def filter_data(self): + return self.data + + async def format(self,display_type,color=True,cached=False,interactive=False): if not cached: - data = self.data - if self.has_age and self.age_fmt in self.age_fmts_date_dependent: - await self.set_dates(data) - if not getattr(self,'column_widths',None): - self.set_column_params() + data = list(self.filter_data()) # method could be a generator - if self.group and (self.sort_key in ('addr','txid','twmmid')): - for a,b in [(data[i],data[i+1]) for i in range(len(data)-1)]: - for k in ('addr','txid','twmmid'): - if self.sort_key == k and getattr(a,k) == getattr(b,k): - b.skip = (k,'addr')[k=='twmmid'] + if data: - self._format_squeezed_display_data = ( - self.hdr_fmt.format( - a = ' '.join(self.sort_info()), - b = self.proto.dcoin, - c = self.total.hl() if hasattr(self,'total') else None ) - + '\nNetwork: {}'.format((nocolor,green)[color]( - self.proto.coin + ' ' + - self.proto.chain_name.upper() )) - + '\n' + '\n'.join(self.gen_squeezed_display(self.column_widths,color=color)) - + '\n' + dt = getattr(self.display_type,display_type) + + cw = self.get_column_widths(data,wide=dt.detail) if dt.need_column_widths else None + + if self.has_age and (self.age_fmt in self.age_fmts_date_dependent or dt.detail): + await self.set_dates(data) + + self._display_data[display_type] = ( + self.header(color) + self.subheader(color) + '\n' + + ( + dt.item_separator.join(getattr(self,dt.fmt_method)(data,cw,color=color)) + '\n' + if data else (nocolor,yellow)[color]('[no data for requested parameters]') + '\n' + ) ) - return self._format_squeezed_display_data + return self._display_data[display_type] + ('' if interactive else self.footer(color)) - async def format_detail(self,color): - if self.has_age: - await self.set_dates(self.data) - - sep = self.detail_display_separator - - return self.print_hdr_fs.format( - a = capfirst(self.desc), - b = self.rpc.blockcount, - c = make_timestr(self.rpc.cur_date), - d = 'Network: {}\n'.format((nocolor,green)[color]( - self.proto.coin + ' ' + - self.proto.chain_name.upper() )), - e = ' '.join(self.sort_info(include_group=False)), - f = sep.join(self.gen_detail_display(color)), - g = self.proto.dcoin, - h = self.total.hl(color=color) if hasattr(self,'total') else None ) + def footer(self,color): + return '\nTOTAL: {} {}\n'.format( + self.total.hl(color=color) if hasattr(self,'total') else None, + self.proto.dcoin + ) if hasattr(self,'total') else '' async def view_and_sort(self): from ..opts import opt @@ -304,7 +329,7 @@ class TwCommon: reply = get_char( '' if self.no_output else ( clear_screen - + await self.format_squeezed() + + await self.format('squeezed',interactive=True) + '\n' + (self.oneshot_msg or '') + prompt @@ -324,7 +349,6 @@ class TwCommon: self.do_sort(action[2:]) elif hasattr(self.item_action,action): await self.item_action().run(self,action) - self.set_column_params() elif action == 'a_quit': msg('') return self.data @@ -339,11 +363,11 @@ class TwCommon: def d_days(self,parent): af = parent.age_fmts_interactive parent.age_fmt = af[(af.index(parent.age_fmt) + 1) % len(af)] - if parent.update_widths_on_age_toggle: - parent.set_column_params() + if parent.update_widths_on_age_toggle: # TODO + pass def d_redraw(self,parent): - parent.set_column_params() + pass def d_reverse(self,parent): parent.data.reverse() @@ -365,14 +389,11 @@ class TwCommon: msg('') from ..fileutil import write_data_to_file from ..exception import UserNonConfirmation - hdr = { - 'squeezed': f'[screen print truncated to width {parent.cols}]\n', - 'detail': '', - }[output_type] + hdr = getattr(parent.display_type,output_type).print_header.format(parent.cols) try: write_data_to_file( outfile = outfile, - data = hdr + await getattr(parent,f'format_{output_type}')(color=False), + data = hdr + await parent.format(display_type=output_type,color=False), desc = f'{parent.desc} listing' ) except UserNonConfirmation as e: parent.oneshot_msg = yellow(f'File {outfile!r} not overwritten by user request\n\n') @@ -381,12 +402,12 @@ class TwCommon: async def a_view(self,parent): from ..ui import do_pager - do_pager( await parent.format_squeezed(color=True,cached=True) ) + do_pager( await parent.format('squeezed',color=True,cached=True) ) self.post_view(parent) async def a_view_detail(self,parent): from ..ui import do_pager - do_pager( await parent.format_detail(color=True) ) + do_pager( await parent.format('detail',color=True) ) self.post_view(parent) def post_view(self,parent): diff --git a/mmgen/tw/txhistory.py b/mmgen/tw/txhistory.py index 957c591f..5bd7d8e2 100755 --- a/mmgen/tw/txhistory.py +++ b/mmgen/tw/txhistory.py @@ -23,6 +23,12 @@ from .common import TwCommon class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): + class display_type(TwCommon.display_type): + + class detail(TwCommon.display_type.detail): + need_column_widths = False + item_separator = '\n\n' + def __new__(cls,proto,*args,**kwargs): return MMGenObject.__new__(proto.base_proto_subclass(cls,'tw','txhistory')) @@ -30,11 +36,10 @@ class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): show_txid = False show_unconfirmed = False show_total_amt = False - print_hdr_fs = '{a} (block #{b}, {c} UTC)\n{d}Sort order: {e}\n{f}\n' age_fmts_interactive = ('confs','block','days','date','date_time') update_widths_on_age_toggle = True - detail_display_separator = '\n\n' print_output_types = ('squeezed','detail') + filters = ('show_unconfirmed',) async def __init__(self,proto,sinceblock=0): self.proto = proto @@ -47,11 +52,7 @@ class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): return 'No transaction history {}found!'.format( f'from block {self.sinceblock} ' if self.sinceblock else '') - def set_column_params(self): - data = self.data - show_txid = self.show_txid - for d in data: - d.skip = '' + def get_column_widths(self,data,wide=False): # var cols: addr1 addr2 comment [txid] if not hasattr(self,'varcol_maxwidths'): @@ -81,9 +82,9 @@ class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): 'spc': 6 + self.show_txid, # 5(6) spaces between cols + 1 leading space in fs } - self.column_widths = self.compute_column_widths(widths,maxws,minws,maxws_nice) + return self.compute_column_widths(widths,maxws,minws,maxws_nice,wide=wide) - def gen_squeezed_display(self,cw,color): + def gen_squeezed_display(self,data,cw,color): if self.sinceblock: yield f'Displaying transactions since block {self.sinceblock.hl(color=color)}' @@ -116,7 +117,7 @@ class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): l = 'Comment' ).rstrip() n = 0 - for d in self.data: + for d in data: if d.confirmations > 0 or self.show_unconfirmed: n += 1 yield fs.format( @@ -128,7 +129,7 @@ class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): a2 = d.vouts_disp( 'outputs', width=cw.addr2, color=color ), l = d.comment.fmt( width=cw.comment, color=color ) ).rstrip() - def gen_detail_display(self,color): + def gen_detail_display(self,data,cw,color): yield ( (f'Displaying transactions since block {self.sinceblock.hl(color=color)}\n' @@ -150,7 +151,7 @@ class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): """,strip_char='\t').strip() n = 0 - for d in self.data: + for d in data: if d.confirmations > 0 or self.show_unconfirmed: n += 1 yield fs.format( @@ -202,7 +203,6 @@ class TwTxHistory(MMGenObject,TwCommon,metaclass=AsyncInit): def d_show_txid(self,parent): parent.show_txid = not parent.show_txid - parent.set_column_params() def d_show_unconfirmed(self,parent): parent.show_unconfirmed = not parent.show_unconfirmed diff --git a/mmgen/tw/unspent.py b/mmgen/tw/unspent.py index f5f08b82..db3307b7 100755 --- a/mmgen/tw/unspent.py +++ b/mmgen/tw/unspent.py @@ -39,13 +39,11 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): return MMGenObject.__new__(proto.base_proto_subclass(cls,'tw','unspent')) txid_w = 64 - print_hdr_fs = '{a} (block #{b}, {c} UTC)\n{d}Sort order: {e}\n{f}\n\nTotal {g}: {h}\n' - no_rpcdata_errmsg = f""" + no_rpcdata_errmsg = """ No spendable outputs found! Import addresses with balances into your watch-only wallet using 'mmgen-addrimport' and then re-run this program. """ update_widths_on_age_toggle = False - detail_display_separator = '\n' print_output_types = ('detail',) class MMGenTwUnspentOutput(MMGenListItem): @@ -104,10 +102,23 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): self.proto, **{ k:v for k,v in o.items() if k in self.MMGenTwUnspentOutput.valid_attrs } ) - def set_column_params(self): + def filter_data(self): + data = self.data - for i in data: - i.skip = '' + + for d in data: + d.skip = '' + + gkeys = {'addr':'addr','twmmid':'addr','txid':'txid'} + if self.group and self.sort_key in gkeys: + for a,b in [(data[i],data[i+1]) for i in range(len(data)-1)]: + for k in gkeys: + if self.sort_key == k and getattr(a,k) == getattr(b,k): + b.skip = gkeys[k] + + return data + + def get_column_widths(self,data,wide=False): self.cols = self.get_term_columns(g.min_screen_width) @@ -122,14 +133,13 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): btaddr_w = addr_w - acct_w - 1 comment_w = acct_w - mmid_w - 1 tx_w = min(self.txid_w,self.cols-addr_w-29-col1_w) # min=6 TODO - txdots = ('','..')[tx_w < self.txid_w] - self.column_widths = namedtuple( + return namedtuple( 'column_widths', ['num','mmid','addr','btaddr','comment','tx'] )(col1_w, mmid_w, addr_w, btaddr_w, comment_w, tx_w) - def gen_squeezed_display(self,cw,color): + def gen_squeezed_display(self,data,cw,color): fs = self.squeezed_fs_fs.format( cw=cw.num, tw=cw.tx ) hdr_fs = self.squeezed_hdr_fs_fs.format( cw=cw.num, tw=cw.tx ) yield hdr_fs.format( @@ -140,7 +150,7 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): A2 = f' Amt({self.proto.coin})'.ljust(self.disp_prec+4), c = self.age_hdr ).rstrip() - for n,i in enumerate(self.data): + for n,i in enumerate(data): addr_dots = '|' + '.'*(cw.addr-1) mmid_disp = MMGenID.fmtc( ( @@ -177,9 +187,7 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): c = self.age_disp(i,self.age_fmt), ).rstrip() - def gen_detail_display(self,color): - - data = self.data + def gen_detail_display(self,data,cw,color): addr_w = max(len(i.addr) for i in data) mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in data) or 12 # DEADBEEF:S:1 diff --git a/test/overlay/fakemods/mmgen/rpc.py b/test/overlay/fakemods/mmgen/rpc.py index 233c564e..75da99cc 100644 --- a/test/overlay/fakemods/mmgen/rpc.py +++ b/test/overlay/fakemods/mmgen/rpc.py @@ -7,6 +7,7 @@ if overlay_fake_os.getenv('MMGEN_BOGUS_UNSPENT_DATA'): rpc_init = rpc_init async def rpc_init(*args,**kwargs): + from .obj import NonNegativeInt ret = await overlay_fake_data.rpc_init(*args,**kwargs) - ret.blockcount = 1000000 + ret.blockcount = NonNegativeInt(1000000) return ret