Browse Source

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 5 years ago
parent
commit
d7bfc8307e

+ 10 - 3
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):

+ 4 - 4
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 '-'
 				))

+ 1 - 4
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:]
 

+ 19 - 5
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):

+ 11 - 4
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' :/\\')

+ 1 - 3
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

+ 11 - 12
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",

+ 18 - 11
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='')
 

+ 1 - 1
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)
 

+ 8 - 7
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')