From 217cd7ad39e9dd2ae3ff5642f4fce301fd56d92e Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 3 Dec 2022 17:40:44 +0000 Subject: [PATCH] Hilite: new hl2(), fmt2() methods; optimize fmt(), fmtc() --- mmgen/addr.py | 11 +++- mmgen/main_seedjoin.py | 2 +- mmgen/obj.py | 15 ++--- mmgen/objmethods.py | 98 +++++++++++++++++++-------------- mmgen/proto/btc/tw/txhistory.py | 16 +++--- mmgen/proto/btc/tx/info.py | 2 +- mmgen/seedsplit.py | 2 +- mmgen/tw/addresses.py | 4 +- mmgen/tw/ctl.py | 2 +- mmgen/tw/shared.py | 5 +- mmgen/tw/txhistory.py | 4 +- mmgen/tw/unspent.py | 12 ++-- mmgen/wallet/mmgen.py | 8 +-- test/test_py_d/ts_ethdev.py | 2 +- test/test_py_d/ts_seedsplit.py | 4 +- 15 files changed, 106 insertions(+), 81 deletions(-) diff --git a/mmgen/addr.py b/mmgen/addr.py index 83967983..e3f13a31 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -172,9 +172,14 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject): return self._parsed @classmethod - def fmtc(cls,addr,**kwargs): - w = kwargs['width'] or cls.width - return super().fmtc(addr[:w-2]+'..' if w < len(addr) else addr, **kwargs) + def fmtc(cls,s,width,**kwargs): + return super().fmtc( s=s[:width-2]+'..' if len(s) > width else s, width=width, **kwargs ) + + def fmt(self,width,**kwargs): + return ( + super().fmtc( s=self[:width-2]+'..', width=width, **kwargs ) if len(self) > width else + super().fmt( width=width, **kwargs ) + ) def is_coin_addr(proto,s): return get_obj( CoinAddr, proto=proto, addr=s, silent=True, return_bool=True ) diff --git a/mmgen/main_seedjoin.py b/mmgen/main_seedjoin.py index 8d8f96f5..25fed964 100755 --- a/mmgen/main_seedjoin.py +++ b/mmgen/main_seedjoin.py @@ -99,7 +99,7 @@ def print_shares_info(): shares[0].sid, share1.sid, master_idx, - id_str.hl(encl="''"), + id_str.hl2(encl='‘’'), len(shares) ) si = 1 for n,s in enumerate(shares[si:],si+1): diff --git a/mmgen/obj.py b/mmgen/obj.py index ecd73e2a..4d20f66a 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -272,13 +272,15 @@ class Int(int,Hilite,InitErrors): except Exception as e: return cls.init_fail(e,n) + def fmt(self,**kwargs): + return super().fmtc(self.__str__(),**kwargs) + @classmethod def fmtc(cls,s,**kwargs): return super().fmtc(s.__str__(),**kwargs) - @classmethod - def colorize(cls,s,**kwargs): - return super().colorize(s.__str__(),**kwargs) + def hl(self,**kwargs): + return super().colorize(self.__str__(),**kwargs) class NonNegativeInt(Int): min_val = 0 @@ -315,11 +317,10 @@ class HexStr(str,Hilite,InitErrors): except Exception as e: return cls.init_fail(e,s) - def truncate(self,width,color=True,color_override=''): + def truncate(self,width,color=True): return self.colorize( - self if width == None or width >= self.width else self[:width-2] + '..', - color = color, - color_override = color_override ) + self if width >= self.width else self[:width-2] + '..', + color = color ) class CoinTxID(HexStr): color,width,hexcase = ('purple',64,'lower') diff --git a/mmgen/objmethods.py b/mmgen/objmethods.py index ef458a26..03060659 100755 --- a/mmgen/objmethods.py +++ b/mmgen/objmethods.py @@ -48,61 +48,79 @@ class Hilite: width = 0 trunc_ok = True + # supports single-width characters only + def fmt( self, width, color=False ): + if len(self) > width: + assert self.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= width of string" + return self.colorize( self[:width].ljust(width), color=color ) + else: + return self.colorize( self.ljust(width), color=color ) + + # class method equivalent of fmt() @classmethod - # 'width' is screen width (greater than len(s) for CJK strings) - # 'append_chars' and 'encl' must consist of single-width chars only - def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None, - center=False,nullrepl='',append_chars='',append_color=False,color_override=''): - s_wide_count = len([1 for ch in s if unicodedata.east_asian_width(ch) in ('F','W')]) - if encl: - a,b = list(encl) - add_len = len(append_chars) + 2 + def fmtc( cls, s, width, color=False ): + if len(s) > width: + assert cls.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= width of string" + return cls.colorize( s[:width].ljust(width), color=color ) else: - a,b = ('','') - add_len = len(append_chars) - if width == None: - width = cls.width - if trunc_ok == None: - trunc_ok = cls.trunc_ok - if g.test_suite: - assert isinstance(encl,str) and len(encl) in (0,2),"'encl' must be 2-character str" - assert width >= 2 + add_len, f'{s!r}: invalid width ({width}) (must be at least 2)' # CJK: 2 cells - if len(s) + s_wide_count + add_len > width: - 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 + return cls.colorize( s.ljust(width), color=color ) + + # an alternative to fmt(), with double-width char support and other features + def fmt2( + self, + width, # screen width - must be at least 2 (one wide char) + color = False, + encl = '', # if set, must be exactly 2 single-width chars + nullrepl = '', + append_chars = '', # single-width chars only + append_color = False, + color_override = '' ): + + if self == '': + return getattr( color_mod, self.color )(nullrepl.ljust(width)) if color else nullrepl.ljust(width) + + s_wide_count = len(['' for ch in self if unicodedata.east_asian_width(ch) in ('F','W')]) + + a,b = encl or ('','') + add_len = len(append_chars) + len(encl) + + if len(self) + s_wide_count + add_len > width: + assert self.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= screen width of string" + s = a + (truncate_str(self,width-add_len) if s_wide_count else self[:width-add_len]) + b else: - s = a+s+b - if center: - s = s.center(width) + s = a + self + b + if append_chars: return ( - cls.colorize(s,color=color) - + cls.colorize( + self.colorize(s,color=color) + + self.colorize2( append_chars.ljust(width-len(s)-s_wide_count), color_override = append_color )) else: - return cls.colorize(s.ljust(width-s_wide_count),color=color,color_override=color_override) + return self.colorize2( s.ljust(width-s_wide_count), color=color, color_override=color_override ) @classmethod - def colorize(cls,s,color=True,color_override=''): + def colorize(cls,s,color=True): + return getattr( color_mod, cls.color )(s) if color else s + + @classmethod + def colorize2(cls,s,color=True,color_override=''): return getattr( color_mod, color_override or cls.color )(s) if color else s - def fmt(self,*args,**kwargs): - assert args == () # forbid invocation w/o keywords - return self.fmtc(self,*args,**kwargs) + def hl(self,color=True): + return getattr( color_mod, self.color )(self) if color else self @classmethod - def hlc(cls,s,color=True,encl='',color_override=''): - if encl: - assert isinstance(encl,str) and len(encl) == 2, "'encl' must be 2-character str" - s = encl[0] + s + encl[1] - return cls.colorize(s,color=color,color_override=color_override) + def hlc(cls,s,color=True): + return getattr( color_mod, cls.color )(s) if color else s - def hl(self,*args,**kwargs): - assert args == () # forbid invocation w/o keywords - return self.hlc(self,*args,**kwargs) + # an alternative to hl(), with enclosure and color override + # can be called as an unbound method with class as first argument + def hl2(self,s=None,color=True,encl='',color_override=''): + if encl: + return self.colorize2( encl[0]+(s or self)+encl[1], color=color, color_override=color_override ) + else: + return self.colorize2( (s or self), color=color, color_override=color_override ) class InitErrors: diff --git a/mmgen/proto/btc/tw/txhistory.py b/mmgen/proto/btc/tw/txhistory.py index bdb21537..45aef990 100755 --- a/mmgen/proto/btc/tw/txhistory.py +++ b/mmgen/proto/btc/tw/txhistory.py @@ -141,8 +141,8 @@ class BitcoinTwTransaction: def txdate_disp(self,age_fmt): return self.parent.date_formatter[age_fmt](self.rpc,self.time) - def txid_disp(self,width,color): - return self.txid.truncate(width=width,color=color) + def txid_disp(self,color,width=None): + return self.txid.hl(color=color) if width == None else self.txid.truncate(width=width,color=color) def vouts_list_disp(self,src,color,indent=''): @@ -167,8 +167,9 @@ class BitcoinTwTransaction: yield fs2.format( i = CoinTxID(e.txid).hl(color=color), n = (nocolor,red)[color](str(e.data['n']).ljust(3)), - a = TwMMGenID.hlc( - '{:{w}}'.format( addr_out + bal_star, w=self.max_addrlen[src] ), + a = TwMMGenID.hl2( + TwMMGenID, + s = '{:{w}}'.format( addr_out + bal_star, w=self.max_addrlen[src] ), color = color, color_override = co ), A = self.proto.coin_amt( e.data['value'] ).fmt(color=color), @@ -194,13 +195,14 @@ class BitcoinTwTransaction: mmid_disp = mmid + bal_star if width and x.space_left < len(mmid_disp): break - yield TwMMGenID.hlc( mmid_disp, color=color, color_override=co ) + yield TwMMGenID.hl2( TwMMGenID, s=mmid_disp, color=color, color_override=co ) x.space_left -= len(mmid_disp) else: if width and x.space_left < addr_w: break - yield TwMMGenID.hlc( - CoinAddr.fmtc( mmid.split(':',1)[1] + bal_star, width=addr_w ), + yield TwMMGenID.hl2( + TwMMGenID, + s = CoinAddr.fmtc( mmid.split(':',1)[1] + bal_star, width=addr_w ), color = color, color_override = co ) x.space_left -= addr_w diff --git a/mmgen/proto/btc/tx/info.py b/mmgen/proto/btc/tx/info.py index 4aa0592d..2239533e 100755 --- a/mmgen/proto/btc/tx/info.py +++ b/mmgen/proto/btc/tx/info.py @@ -74,7 +74,7 @@ class TxInfo(TxInfo): confs = e.confs + blockcount - tx.blockcount days = int(confs // confs_per_day) if e.mmid: - mmid_fmt = e.mmid.fmt( + mmid_fmt = e.mmid.fmt2( width=max_mmwid, encl='()', color=True, diff --git a/mmgen/seedsplit.py b/mmgen/seedsplit.py index ce8c8f11..fd87bbc2 100755 --- a/mmgen/seedsplit.py +++ b/mmgen/seedsplit.py @@ -183,7 +183,7 @@ class SeedShareBase(MMGenObject): m = ( yellow("(share {} of {} of ") + pl.parent_seed.sid.hl() + yellow(', split id ') - + pl.id_str.hl(encl="''") + + pl.id_str.hl2(encl='‘’') + yellow('{})') ) else: m = "share {} of {} of " + pl.parent_seed.sid + ", split id '" + pl.id_str + "'{}" diff --git a/mmgen/tw/addresses.py b/mmgen/tw/addresses.py index 19541fdf..a3f6f8cc 100755 --- a/mmgen/tw/addresses.py +++ b/mmgen/tw/addresses.py @@ -184,7 +184,7 @@ class TwAddresses(TwView): m = d.twmmid.fmt( width=cw.mmid, color=color ), u = yes if d.recvd else no, a = d.addr.fmt( color=color, width=cw.addr ), - c = d.comment.fmt( width=cw.comment, color=color, nullrepl='-' ), + c = d.comment.fmt2( width=cw.comment, color=color, nullrepl='-' ), A = d.amt.fmt( color=color, iwidth=cw.iwidth, prec=self.disp_prec ), d = self.age_disp( d, self.age_fmt ) ) @@ -195,7 +195,7 @@ class TwAddresses(TwView): m = d.twmmid.fmt( width=cw.mmid, color=color ), u = yes if d.recvd else no, a = d.addr.fmt( color=color, width=cw.addr ), - c = d.comment.fmt( width=cw.comment, color=color, nullrepl='-' ), + c = d.comment.fmt2( width=cw.comment, color=color, nullrepl='-' ), A = d.amt.fmt( color=color, iwidth=cw.iwidth, prec=self.disp_prec ), b = self.age_disp( d, 'block' ), D = self.age_disp( d, 'date_time' )) diff --git a/mmgen/tw/ctl.py b/mmgen/tw/ctl.py index e5dd746b..52ba9125 100755 --- a/mmgen/tw/ctl.py +++ b/mmgen/tw/ctl.py @@ -279,7 +279,7 @@ class TwCtl(MMGenObject,metaclass=AsyncInit): res.twmmid.type.replace('mmgen','MMGen'), res.twmmid.addr.hl() ) if comment: - msg('Added label {} to {}'.format(comment.hl(encl="''"),desc)) + msg('Added label {} to {}'.format(comment.hl2(encl='‘’'),desc)) else: msg(f'Removed label from {desc}') return True diff --git a/mmgen/tw/shared.py b/mmgen/tw/shared.py index 636a8a0c..4f0f6a44 100755 --- a/mmgen/tw/shared.py +++ b/mmgen/tw/shared.py @@ -46,9 +46,8 @@ class TwMMGenID(str,Hilite,InitErrors,MMGenObject): me.proto = proto return me - @classmethod - def fmtc(cls,twmmid,*args,**kwargs): - return super().fmtc(twmmid.disp,*args,**kwargs) + def fmt(self,**kwargs): + return super().fmtc(self.disp,**kwargs) # non-displaying container for TwMMGenID,TwComment class TwLabel(str,InitErrors,MMGenObject): diff --git a/mmgen/tw/txhistory.py b/mmgen/tw/txhistory.py index 5e1d8837..354e7057 100755 --- a/mmgen/tw/txhistory.py +++ b/mmgen/tw/txhistory.py @@ -133,7 +133,7 @@ class TwTxHistory(TwView): i = d.vouts_disp( 'inputs', width=cw.inputs, color=color ), A = d.amt_disp(self.show_total_amt).fmt( iwidth=cw.iwidth, prec=self.disp_prec, color=color ), o = d.vouts_disp( 'outputs', width=cw.outputs, color=color ), - c = d.comment.fmt( width=cw.comment, color=color, nullrepl='-' ) ) + c = d.comment.fmt2( width=cw.comment, color=color, nullrepl='-' ) ) def gen_detail_display(self,data,cw,fs,color,fmt_method): @@ -156,7 +156,7 @@ class TwTxHistory(TwView): d = d.age_disp( 'date_time', width=None, color=None ), b = d.blockheight_disp(color=color), D = d.txdate_disp( 'date_time' ), - t = d.txid_disp( width=None, color=color ), + t = d.txid_disp( color=color ), A = d.amt_disp(show_total_amt=True).hl( color=color ), B = d.amt_disp(show_total_amt=False).hl( color=color ), f = d.fee_disp( color=color ), diff --git a/mmgen/tw/unspent.py b/mmgen/tw/unspent.py index 961b4360..121421d2 100755 --- a/mmgen/tw/unspent.py +++ b/mmgen/tw/unspent.py @@ -31,7 +31,7 @@ from ..obj import ( HexStr, CoinTxID, NonNegativeInt ) -from ..addr import CoinAddr,MMGenID +from ..addr import CoinAddr from .shared import TwMMGenID,get_tw_label from .view import TwView @@ -183,14 +183,14 @@ class TwUnspentOutputs(TwView): for n,d in enumerate(data): yield fs.format( n = str(n+1) + ')', - t = (CoinTxID.fmtc('|' + '.'*(cw.txid-1),color=color) if d.skip == 'txid' + t = (d.txid.fmtc( '|' + '.'*(cw.txid-1), width=d.txid.width, color=color ) if d.skip == 'txid' else d.txid.truncate( width=cw.txid, color=color )) if cw.txid else None, v = ' ' + d.vout.fmt( width=cw.vout-1, color=color ) if cw.vout else None, a = d.addr.fmtc( '|' + '.'*(cw.addr-1), width=cw.addr, color=color ) if d.skip == 'addr' else d.addr.fmt( width=cw.addr, color=color ), - m = (MMGenID.fmtc( '.'*cw.mmid, color=color ) if d.skip == 'addr' + m = (d.twmmid.fmtc( '.'*cw.mmid, color=color ) if d.skip == 'addr' else d.twmmid.fmt( width=cw.mmid, color=color )) if cw.mmid else None, - c = d.comment.fmt( width=cw.comment, color=color, nullrepl='-' ) if cw.comment else None, + c = d.comment.fmt2( width=cw.comment, color=color, nullrepl='-' ) if cw.comment else None, A = d.amt.fmt( color=color, iwidth=cw.iwidth, prec=self.disp_prec ), B = d.amt2.fmt( color=color, iwidth=cw.iwidth2, prec=self.disp_prec ) if cw.amt2 else None, d = self.age_disp(d,self.age_fmt), @@ -201,7 +201,7 @@ class TwUnspentOutputs(TwView): for n,d in enumerate(data): yield fs.format( n = str(n+1) + ')', - t = d.txid.fmt( color=color ) if cw.txid else None, + t = d.txid.fmt( width=d.txid.width, color=color ) if cw.txid else None, v = ' ' + d.vout.fmt( width=cw.vout-1, color=color ) if cw.vout else None, a = d.addr.fmt( width=cw.addr, color=color ), m = d.twmmid.fmt( width=cw.mmid, color=color ), @@ -209,7 +209,7 @@ class TwUnspentOutputs(TwView): B = d.amt2.fmt( color=color, iwidth=cw.iwidth2, prec=self.disp_prec ) if cw.amt2 else None, b = self.age_disp(d,'block'), D = self.age_disp(d,'date_time'), - c = d.comment.fmt( width=cw.comment, color=color, nullrepl='-' )) + c = d.comment.fmt2( width=cw.comment, color=color, nullrepl='-' )) def display_total(self): msg('\nTotal unspent: {} {} ({} output{})'.format( diff --git a/mmgen/wallet/mmgen.py b/mmgen/wallet/mmgen.py index d8e7ab98..83f63999 100755 --- a/mmgen/wallet/mmgen.py +++ b/mmgen/wallet/mmgen.py @@ -41,7 +41,7 @@ class wallet(wallet): # logic identical to _get_hash_preset_from_user() def _get_label_from_user(self,old_lbl=''): prompt = 'Enter a wallet label, or hit ENTER {}: '.format( - 'to reuse the label {}'.format(old_lbl.hl(encl="''")) if old_lbl else + 'to reuse the label {}'.format(old_lbl.hl2(encl='‘’')) if old_lbl else 'for no label' ) from ..ui import line_input while True: @@ -61,17 +61,17 @@ class wallet(wallet): old_lbl = self.ss_in.ssdata.label if opt.keep_label: lbl = old_lbl - qmsg('Reusing label {} at user request'.format( lbl.hl(encl="''") )) + qmsg('Reusing label {} at user request'.format( lbl.hl2(encl='‘’') )) elif self.label: lbl = self.label - qmsg('Using label {} requested on command line'.format( lbl.hl(encl="''") )) + qmsg('Using label {} requested on command line'.format( lbl.hl2(encl='‘’') )) else: # Prompt, using old value as default lbl = self._get_label_from_user(old_lbl) if (not opt.keep_label) and self.op == 'pwchg_new': qmsg('Label {}'.format( 'unchanged' if lbl == old_lbl else f'changed to {lbl!r}' )) elif self.label: lbl = self.label - qmsg('Using label {} requested on command line'.format( lbl.hl(encl="''") )) + qmsg('Using label {} requested on command line'.format( lbl.hl2(encl='‘’') )) else: lbl = self._get_label_from_user() self.ssdata.label = lbl diff --git a/test/test_py_d/ts_ethdev.py b/test/test_py_d/ts_ethdev.py index a67a5253..2b0c4a52 100755 --- a/test/test_py_d/ts_ethdev.py +++ b/test/test_py_d/ts_ethdev.py @@ -910,7 +910,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): res = await tx.get_receipt(txid) imsg(f'Gas sent: {res.gas_sent.hl():<9} {(res.gas_sent*res.gas_price).hl2(encl="()")}') imsg(f'Gas used: {res.gas_used.hl():<9} {(res.gas_used*res.gas_price).hl2(encl="()")}') - imsg(f'Gas price: {res.gas_price.hl2()}') + imsg(f'Gas price: {res.gas_price.hl()}') if res.gas_used == res.gas_sent: omsg(yellow(f'Warning: all gas was used!')) return res diff --git a/test/test_py_d/ts_seedsplit.py b/test/test_py_d/ts_seedsplit.py index 93b6f291..40059020 100755 --- a/test/test_py_d/ts_seedsplit.py +++ b/test/test_py_d/ts_seedsplit.py @@ -107,7 +107,7 @@ class TestSuiteSeedSplit(TestSuiteBase): if spec: from mmgen.seedsplit import SeedSplitSpecifier sss = SeedSplitSpecifier(spec) - pat = rf'Processing .*\b{sss.idx}\b of \b{sss.count}\b of .* id .*{sss.id!r}' + pat = rf'Processing .*\b{sss.idx}\b of \b{sss.count}\b of .* id .*‘{sss.id}’' else: pat = f'master share #{master}' t.expect(pat,regex=True) @@ -144,7 +144,7 @@ class TestSuiteSeedSplit(TestSuiteBase): if icls: t.passphrase(icls.desc,sh1_passwd) if master: - fs = "master share #{}, split id.*'{}'.*, share count {}" + fs = "master share #{}, split id.*‘{}’.*, share count {}" pat = fs.format( master, id_str or 'default',