From d7bfc8307e1d1fdf0fc63f44c7b8758f4c8f9be0 Mon Sep 17 00:00:00 2001 From: MMGen Date: Tue, 28 May 2019 14:49:44 +0000 Subject: [PATCH] support 80-screen-width tracking wallet labels Labels with double-wide CJK characters can already occupy 80 terminal cells. Extend the same privilege to all labels. --- mmgen/obj.py | 13 ++++++++++--- mmgen/tw.py | 8 ++++---- mmgen/util.py | 5 +---- test/common.py | 24 +++++++++++++++++++----- test/objtest_py_d/ot_btc_mainnet.py | 15 +++++++++++---- test/objtest_py_d/ot_common.py | 4 +--- test/test_py_d/common.py | 23 +++++++++++------------ test/test_py_d/ts_ethdev.py | 29 ++++++++++++++++++----------- test/test_py_d/ts_main.py | 2 +- test/test_py_d/ts_regtest.py | 15 ++++++++------- 10 files changed, 84 insertions(+), 54 deletions(-) diff --git a/mmgen/obj.py b/mmgen/obj.py index ecce9752..b9117471 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -777,6 +777,7 @@ class MMGenLabel(str,Hilite,InitErrors): forbidden = [] max_len = 0 min_len = 0 + max_screen_width = 0 # if != 0, overrides max_len desc = 'label' def __new__(cls,s,on_fail='die',msg=None): if type(s) == cls: return s @@ -795,13 +796,19 @@ class MMGenLabel(str,Hilite,InitErrors): if unicodedata.category(ch)[0] in 'CM': t = { 'C':'control', 'M':'combining' }[unicodedata.category(ch)[0]] raise ValueError('{}: {} characters not allowed'.format(ascii(ch),t)) - assert len(s) <= cls.max_len, 'too long (>{} symbols)'.format(cls.max_len) + me = str.__new__(cls,s) + if cls.max_screen_width: + me.screen_width = len(s) + len([1 for ch in s if unicodedata.east_asian_width(ch) in ('F','W')]) + assert me.screen_width <= cls.max_screen_width,( + 'too wide (>{} screen width)'.format(cls.max_screen_width)) + else: + assert len(s) <= cls.max_len, 'too long (>{} symbols)'.format(cls.max_len) assert len(s) >= cls.min_len, 'too short (<{} symbols)'.format(cls.min_len) assert not cls.allowed or set(list(s)).issubset(set(cls.allowed)),\ 'contains non-allowed symbols: {}'.format(' '.join(set(list(s)) - set(cls.allowed))) assert not cls.forbidden or not any(ch in s for ch in cls.forbidden),\ "contains one of these forbidden symbols: '{}'".format("', '".join(cls.forbidden)) - return str.__new__(cls,s) + return me except Exception as e: return cls.init_fail(e,s) @@ -810,7 +817,7 @@ class MMGenWalletLabel(MMGenLabel): desc = 'wallet label' class TwComment(MMGenLabel): - max_len = 40 + max_screen_width = 80 desc = 'tracking wallet comment' class MMGenTXLabel(MMGenLabel): diff --git a/mmgen/tw.py b/mmgen/tw.py index ae16df0d..5492560d 100755 --- a/mmgen/tw.py +++ b/mmgen/tw.py @@ -182,7 +182,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program. # allow for 7-digit confirmation nums col1_w = max(3,len(str(len(unsp)))+1) # num + ')' mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in unsp) or 12 # DEADBEEF:S:1 - max_acct_w = max(len(i.label) for i in unsp) + mmid_w + 1 + max_acct_w = max(i.label.screen_width for i in unsp) + mmid_w + 1 max_btcaddr_w = max(len(i.addr) for i in unsp) min_addr_w = self.cols - self.col_adj addr_w = min(max_btcaddr_w + (0,1+max_acct_w)[self.show_mmid],min_addr_w) @@ -472,7 +472,7 @@ class TwAddrList(MMGenDict): fs = '{{mid}}{} {{cmt}} {{amt}}{}'.format(('',' {addr}')[showbtcaddrs],('',' {age}')[show_age]) mmaddrs = [k for k in self.keys() if k.type == 'mmgen'] max_mmid_len = max(len(k) for k in mmaddrs) + 2 if mmaddrs else 10 - max_cmt_len = max(max(screen_width(v['lbl'].comment) for v in self.values()),7) + max_cmt_width = max(max(v['lbl'].comment.screen_width for v in self.values()),7) addr_width = max(len(self[mmid]['addr']) for mmid in self) # fp: fractional part @@ -480,7 +480,7 @@ class TwAddrList(MMGenDict): out += [fs.format( mid=MMGenID.fmtc('MMGenID',width=max_mmid_len), addr=(CoinAddr.fmtc('ADDRESS',width=addr_width) if showbtcaddrs else None), - cmt=TwComment.fmtc('COMMENT',width=max_cmt_len+1), + cmt=TwComment.fmtc('COMMENT',width=max_cmt_width+1), amt='BALANCE'.ljust(max_fp_len+4), age=('CONFS','DAYS')[age_fmt=='days'], )] @@ -512,7 +512,7 @@ class TwAddrList(MMGenDict): out.append(fs.format( mid=MMGenID.fmtc(mmid_disp,width=max_mmid_len,color=True), addr=(e['addr'].fmt(color=True,width=addr_width) if showbtcaddrs else None), - cmt=e['lbl'].comment.fmt(width=max_cmt_len,color=True,nullrepl='-'), + cmt=e['lbl'].comment.fmt(width=max_cmt_width,color=True,nullrepl='-'), amt=e['amt'].fmt('4.{}'.format(max(max_fp_len,3)),color=True), age=mmid.confs // (1,confs_per_day)[age_fmt=='days'] if hasattr(mmid,'confs') and mmid.confs != None else '-' )) diff --git a/mmgen/util.py b/mmgen/util.py index 2846afc3..f0d3752f 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -20,7 +20,7 @@ util.py: Low-level routines imported by other modules in the MMGen suite """ -import sys,os,time,stat,re,unicodedata +import sys,os,time,stat,re from hashlib import sha256 from string import hexdigits,digits from mmgen.color import * @@ -231,9 +231,6 @@ def split3(s,sep=None): return splitN(s,3,sep) # always return a 3-element list def split_into_cols(col_wid,s): return ' '.join([s[col_wid*i:col_wid*(i+1)] for i in range(len(s)//col_wid+1)]).rstrip() -def screen_width(s): - return len(s) + len([1 for ch in s if unicodedata.east_asian_width(ch) in ('F','W')]) - def capfirst(s): # different from str.capitalize() - doesn't downcase any uc in string return s if len(s) == 0 else s[0].upper() + s[1:] diff --git a/test/common.py b/test/common.py index ab464d68..4ea2eb8f 100755 --- a/test/common.py +++ b/test/common.py @@ -20,17 +20,31 @@ common.py: Shared routines and data for the MMGen test suites """ -sample_text = 'The Times 03/Jan/2009 Chancellor on brink of second bailout for banks' - -ref_kafile_pass = 'kafile password' -ref_kafile_hash_preset = '1' - class TestSuiteException(Exception): pass class TestSuiteFatalException(Exception): pass import os from mmgen.common import * +ascii_uc = ''.join(map(chr,list(range(65,91)))) # 26 chars +ascii_lc = ''.join(map(chr,list(range(97,123)))) # 26 chars +lat_accent = ''.join(map(chr,list(range(192,383)))) # 191 chars +ru_uc = ''.join(map(chr,list(range(1040,1072)))) # 32 chars +gr_uc = ''.join(map(chr,list(range(913,930)) + list(range(931,940)))) # 26 chars (930 is ctrl char) +lat_cyr_gr = lat_accent[:130:5] + ru_uc + gr_uc # 84 chars + +utf8_text = '[α-$ample UTF-8 text-ω]' * 10 # 230 chars, unicode types L,N,P,S,Z +utf8_combining = '[α-$ámple UTF-8 téxt-ω]' * 10 # L,N,P,S,Z,M +utf8_ctrl = '[α-$ample\nUTF-8\ntext-ω]' * 10 # L,N,P,S,Z,C + +text_jp = '必要なのは、信用ではなく暗号化された証明に基づく電子取引システムであり、これにより希望する二者が信用できる第三者機関を介さずに直接取引できるよう' # 72 chars ('W'ide) +text_zh = '所以,我們非常需要這樣一種電子支付系統,它基於密碼學原理而不基於信用,使得任何達成一致的雙方,能夠直接進行支付,從而不需要協力廠商仲介的參與。。' # 72 chars ('F'ull + 'W'ide) + +sample_text = 'The Times 03/Jan/2009 Chancellor on brink of second bailout for banks' + +ref_kafile_pass = 'kafile password' +ref_kafile_hash_preset = '1' + def getrandnum(n): return int(os.urandom(n).hex(),16) def getrandhex(n): return os.urandom(n).hex() def getrandnum_range(nbytes,rn_max): diff --git a/test/objtest_py_d/ot_btc_mainnet.py b/test/objtest_py_d/ot_btc_mainnet.py index 3026baff..06c6a2f0 100755 --- a/test/objtest_py_d/ot_btc_mainnet.py +++ b/test/objtest_py_d/ot_btc_mainnet.py @@ -144,15 +144,22 @@ tests = OrderedDict([ ) }), ('MMGenWalletLabel', { - 'bad': (utf8_text[:49],utf8_text_combining[:48],utf8_text_control[:48]), + 'bad': (utf8_text[:49],utf8_combining[:48],utf8_ctrl[:48]), 'good': (utf8_text[:48],) }), ('TwComment', { - 'bad': (utf8_text[:41],utf8_text_combining[:40],utf8_text_control[:40]), - 'good': (utf8_text[:40],) + 'bad': ( utf8_combining[:40], + utf8_ctrl[:40], + text_jp[:41], + text_zh[:41], + utf8_text[:81] ), + 'good': ( utf8_text[:80], + (ru_uc + gr_uc + utf8_text)[:80], + text_jp[:40], + text_zh[:40] ) }), ('MMGenTXLabel',{ - 'bad': (utf8_text[:73],utf8_text_combining[:72],utf8_text_control[:72]), + 'bad': (utf8_text[:73],utf8_combining[:72],utf8_ctrl[:72]), 'good': (utf8_text[:72],) }), ('MMGenPWIDString', { # forbidden = list(u' :/\\') diff --git a/test/objtest_py_d/ot_common.py b/test/objtest_py_d/ot_common.py index fc92ab02..74dd409f 100755 --- a/test/objtest_py_d/ot_common.py +++ b/test/objtest_py_d/ot_common.py @@ -9,9 +9,7 @@ test.objtest_py_d.ot_common: shared data for MMGen data objects tests import os from mmgen.globalvars import g +from ..common import * r32,r24,r16,r17,r18 = os.urandom(32),os.urandom(24),os.urandom(16),os.urandom(17),os.urandom(18) tw_pfx = g.proto.base_coin.lower()+':' -utf8_text = '[α-$ample UTF-8 text-ω]' * 10 # 230 chars, unicode types L,N,P,S,Z -utf8_text_combining = '[α-$ámple UTF-8 téxt-ω]' * 10 # L,N,P,S,Z,M -utf8_text_control = '[α-$ample\nUTF-8\ntext-ω]' * 10 # L,N,P,S,Z,C diff --git a/test/test_py_d/common.py b/test/test_py_d/common.py index 56d7a64d..b8b2cedc 100755 --- a/test/test_py_d/common.py +++ b/test/test_py_d/common.py @@ -22,6 +22,7 @@ common.py: Shared routines and data for the test.py test suite import os,time,subprocess from mmgen.common import * +from ..common import * log_file = 'test.py.log' @@ -46,17 +47,15 @@ non_mmgen_fn = 'coinkey' ref_dir = os.path.join('test','ref') dfl_words_file = os.path.join(ref_dir,'98831F3A.mmwords') -from mmgen.obj import MMGenTXLabel +from mmgen.obj import MMGenTXLabel,TwComment -ref_tx_label_jp = '必要なのは、信用ではなく暗号化された証明に基づく電子取引システムであり、これにより希望する二者が信用できる第三者機関を介さずに直接取引できるよう' # 72 chars ('W'ide) -ref_tx_label_zh = '所以,我們非常需要這樣一種電子支付系統,它基於密碼學原理而不基於信用,使得任何達成一致的雙方,能夠直接進行支付,從而不需要協力廠商仲介的參與。。' # 72 chars ('F'ull + 'W'ide) -ref_tx_label_lat_cyr_gr = ''.join(map(chr, - list(range(65,91)) + - list(range(1040,1072)) + # cyrillic - list(range(913,939)) + # greek - list(range(97,123))))[:MMGenTXLabel.max_len] # 72 chars -utf8_label = ref_tx_label_zh[:40] -utf8_label_pat = utf8_label +tx_label_jp = text_jp +tx_label_zh = text_zh + +tx_label_lat_cyr_gr = lat_cyr_gr[:MMGenTXLabel.max_len] # 72 chars + +tw_label_zh = text_zh[:TwComment.max_screen_width // 2] +tw_label_lat_cyr_gr = lat_cyr_gr[:TwComment.max_screen_width] # 80 chars ref_bw_hash_preset = '1' ref_bw_file = 'wallet.mmbrain' @@ -140,8 +139,8 @@ labels = [ "Automotive", "Travel expenses", "Healthcare", - ref_tx_label_jp[:40], - ref_tx_label_zh[:40], + tx_label_jp[:40], + tx_label_zh[:40], "Alice's allowance", "Bob's bequest", "House purchase", diff --git a/test/test_py_d/ts_ethdev.py b/test/test_py_d/ts_ethdev.py index 0d8491b3..ddd48632 100755 --- a/test/test_py_d/ts_ethdev.py +++ b/test/test_py_d/ts_ethdev.py @@ -156,8 +156,10 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): ('addrimport_burn_addr',"importing burn address"), ('bal5', 'the {} balance'.format(g.coin)), - ('add_label', 'adding a UTF-8 label'), - ('chk_label', 'the label'), + ('add_label1', 'adding a UTF-8 label (zh)'), + ('chk_label1', 'the label'), + ('add_label2', 'adding a UTF-8 label (lat+cyr+gr)'), + ('chk_label2', 'the label'), ('remove_label', 'removing the label'), ('token_compile1', 'compiling ERC20 token #1'), @@ -241,8 +243,8 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): ('token_twview2','twview --token=mm1 wide=1'), ('token_twview3','twview --token=mm1 wide=1 sort=age (ignored)'), - ('edit_label1','adding label to addr #{} in {} tracking wallet'.format(del_addrs[0],g.coin)), - ('edit_label2','adding label to addr #{} in {} tracking wallet'.format(del_addrs[1],g.coin)), + ('edit_label1','adding label to addr #{} in {} tracking wallet (zh)'.format(del_addrs[0],g.coin)), + ('edit_label2','adding label to addr #{} in {} tracking wallet (lat+cyr+gr)'.format(del_addrs[1],g.coin)), ('edit_label3','removing label from addr #{} in {} tracking wallet'.format(del_addrs[0],g.coin)), ('remove_addr1','removing addr #{} from {} tracking wallet'.format(del_addrs[0],g.coin)), @@ -349,7 +351,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): fee_res = fee_res, fee_desc = fee_desc, eth_fee_res = eth_fee_res, - add_comment = ref_tx_label_jp ) + add_comment = tx_label_jp ) def txsign(self,ni=False,ext='{}.rawtx',add_args=[]): ext = ext.format('-α' if g.debug_utf8 else '') @@ -472,16 +474,21 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): assert Decimal(bal1) + Decimal(bal2) == Decimal(total) return t - def add_label(self,addr='98831F3A:E:3',lbl=utf8_label): + def add_label(self,lbl,addr='98831F3A:E:3'): t = self.spawn('mmgen-tool', self.eth_args + ['add_label',addr,lbl]) t.expect('Added label.*in tracking wallet',regex=True) return t - def chk_label(self,addr='98831F3A:E:3',label_pat=utf8_label_pat): + def chk_label(self,lbl_pat,addr='98831F3A:E:3'): t = self.spawn('mmgen-tool', self.eth_args + ['listaddresses','all_labels=1']) - t.expect(r'{}\s+\S{{30}}\S+\s+{}\s+'.format(addr,(label_pat or label)),regex=True) + t.expect(r'{}\s+\S{{30}}\S+\s+{}\s+'.format(addr,lbl_pat),regex=True) return t + def add_label1(self): return self.add_label(lbl=tw_label_zh) + def chk_label1(self): return self.chk_label(lbl_pat=tw_label_zh) + def add_label2(self): return self.add_label(lbl=tw_label_lat_cyr_gr) + def chk_label2(self): return self.chk_label(lbl_pat=tw_label_lat_cyr_gr) + def remove_label(self,addr='98831F3A:E:3'): t = self.spawn('mmgen-tool', self.eth_args + ['remove_label',addr]) t.expect('Removed label.*in tracking wallet',regex=True) @@ -646,7 +653,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): inputs = inputs, input_sels_prompt = 'to spend from', file_desc = 'Ethereum token transaction', - add_comment = ref_tx_label_lat_cyr_gr) + add_comment = tx_label_lat_cyr_gr) def token_txsign(self,ext='',token=''): return self.txsign(ni=True,ext=ext,add_args=['--token='+token]) def token_txsend(self,ext='',token=''): @@ -768,9 +775,9 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): return t def edit_label1(self): - return self.edit_label(out_num=del_addrs[0],label_text='First added label-α') + return self.edit_label(out_num=del_addrs[0],label_text=tw_label_zh) def edit_label2(self): - return self.edit_label(out_num=del_addrs[1],label_text='Second added label') + return self.edit_label(out_num=del_addrs[1],label_text=tw_label_lat_cyr_gr) def edit_label3(self): return self.edit_label(out_num=del_addrs[0],label_text='') diff --git a/test/test_py_d/ts_main.py b/test/test_py_d/ts_main.py index 7320906c..d027dc51 100755 --- a/test/test_py_d/ts_main.py +++ b/test/test_py_d/ts_main.py @@ -435,7 +435,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): self.txcreate_ui_common(t, menu=(['M'],['M','D','m','g'])[self.test_name=='txcreate'], inputs=' '.join(map(str,outputs_list)), - add_comment=('',ref_tx_label_lat_cyr_gr)[do_label], + add_comment=('',tx_label_lat_cyr_gr)[do_label], non_mmgen_inputs=(0,1)[bool(non_mmgen_input and not txdo_args)], view=view) diff --git a/test/test_py_d/ts_regtest.py b/test/test_py_d/ts_regtest.py index 35e676b2..678174b0 100755 --- a/test/test_py_d/ts_regtest.py +++ b/test/test_py_d/ts_regtest.py @@ -82,7 +82,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): ('fund_bob', "funding Bob's wallet"), ('fund_alice', "funding Alice's wallet"), ('bob_bal1', "Bob's balance"), - ('bob_add_label', "adding a 40-character UTF-8 encoded label"), + ('bob_add_label', "adding an 80-screen-width label (lat+cyr+gr)"), ('bob_twview1', "viewing Bob's tracking wallet"), ('bob_split1', "splitting Bob's funds"), ('generate', 'mining a block'), @@ -136,7 +136,8 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): ('alice_chk_label1', 'the label'), ('alice_add_label2', 'adding a label'), ('alice_chk_label2', 'the label'), - ('alice_edit_label1', 'editing a label'), + ('alice_edit_label1', 'editing a label (zh)'), + ('alice_edit_label2', 'editing a label (lat+cyr+gr)'), ('alice_chk_label3', 'the label'), ('alice_remove_label1', 'removing a label'), ('alice_chk_label4', 'the label'), @@ -432,7 +433,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): inputs = outputs_list, file_desc = 'Signed transaction', interactive_fee = (tx_fee,'')[bool(fee)], - add_comment = ref_tx_label_jp, + add_comment = tx_label_jp, view = 't',save=True) t.passphrase('MMGen wallet',rt_pw) @@ -593,7 +594,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): def bob_add_label(self): sid = self._user_sid('bob') - return self.user_add_label('bob',sid+':C:1',utf8_label) + return self.user_add_label('bob',sid+':C:1',tw_label_lat_cyr_gr) def alice_add_label1(self): sid = self._user_sid('alice') @@ -663,13 +664,13 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): sid = self._user_sid('alice') return self.user_chk_label('alice',sid+':C:1','Replacement Label') - def alice_edit_label1(self): - return self.user_edit_label('alice','4',utf8_label) + def alice_edit_label1(self): return self.user_edit_label('alice','4',tw_label_lat_cyr_gr) + def alice_edit_label2(self): return self.user_edit_label('alice','3',tw_label_zh) def alice_chk_label3(self): sid = self._user_sid('alice') mmid = sid + (':S:3',':L:3')[g.coin=='BCH'] - return self.user_chk_label('alice',mmid,utf8_label,label_pat=utf8_label_pat) + return self.user_chk_label('alice',mmid,tw_label_zh,label_pat=tw_label_lat_cyr_gr) def alice_chk_label4(self): sid = self._user_sid('alice')