|
|
@@ -27,7 +27,7 @@ from ..cfg import gv
|
|
|
from ..objmethods import MMGenObject
|
|
|
from ..obj import get_obj, MMGenIdx, MMGenList
|
|
|
from ..color import nocolor, yellow, orange, green, red, blue
|
|
|
-from ..util import msg, msg_r, fmt, die, capfirst, suf, make_timestr, isAsync
|
|
|
+from ..util import msg, msg_r, fmt, die, capfirst, suf, make_timestr, isAsync, is_int
|
|
|
from ..rpc import rpc_init
|
|
|
from ..base_obj import AsyncInit
|
|
|
|
|
|
@@ -80,11 +80,13 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
def do(method, data, cw, fs, color, fmt_method):
|
|
|
return [l.rstrip() for l in method(data, cw, fs, color, fmt_method)]
|
|
|
|
|
|
+ account_based = False
|
|
|
has_wallet = True
|
|
|
has_amt2 = False
|
|
|
dates_set = False
|
|
|
reverse = False
|
|
|
group = False
|
|
|
+ groupable = {}
|
|
|
use_cached = False
|
|
|
minconf = 1
|
|
|
txid_w = 0
|
|
|
@@ -118,6 +120,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
'comment': fp('c', True, False, ' {c:%s}', ' {c}'),
|
|
|
'amt': fp('A', True, False, ' {A:%s}', ' {A}'),
|
|
|
'amt2': fp('B', True, False, ' {B:%s}', ' {B}'),
|
|
|
+ 'addr_idx': fp('I', True, False, ' {I:%s}', ' {I}'),
|
|
|
'date': fp('d', True, True, ' {d:%s}', ' {d:<%s}'),
|
|
|
'date_time': fp('D', True, True, ' {D:%s}', ' {D:%s}'),
|
|
|
'block': fp('b', True, True, ' {b:%s}', ' {b:<%s}'),
|
|
|
@@ -260,24 +263,22 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
def sort_info(self, *, include_group=True):
|
|
|
ret = ([], ['Reverse'])[self.reverse]
|
|
|
ret.append(self.sort_disp[self.sort_key])
|
|
|
- if include_group and self.group and (self.sort_key in ('addr', 'txid', 'twmmid')):
|
|
|
+ if include_group and self.group and self.sort_key in self.groupable:
|
|
|
ret.append('Grouped')
|
|
|
return ret
|
|
|
|
|
|
- def do_sort(self, key=None, *, reverse=False):
|
|
|
+ def sort_data(self, key):
|
|
|
if key == 'txid' and not self.txid_w:
|
|
|
return
|
|
|
- key = key or self.sort_key
|
|
|
if key not in self.sort_funcs:
|
|
|
die(1, f'{key!r}: invalid sort key. Valid options: {" ".join(self.sort_funcs)}')
|
|
|
self.sort_key = key
|
|
|
- assert isinstance(reverse, bool)
|
|
|
save = self.data.copy()
|
|
|
- self.data.sort(key=self.sort_funcs[key], reverse=reverse or self.reverse)
|
|
|
+ self.data.sort(key=self.sort_funcs[key], reverse=self.reverse)
|
|
|
if self.data != save:
|
|
|
self.pos = 0
|
|
|
|
|
|
- async def get_data(self, *, sort_key=None, reverse_sort=False):
|
|
|
+ async def get_data(self):
|
|
|
|
|
|
rpc_data = await self.get_rpc_data()
|
|
|
|
|
|
@@ -290,12 +291,12 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
await self.gen_data(rpc_data, lbl_id) if isAsync(self.gen_data) else
|
|
|
self.gen_data(rpc_data, lbl_id))
|
|
|
|
|
|
- self.disp_data = list(self.filter_data())
|
|
|
-
|
|
|
if not self.data:
|
|
|
die(1, f'No {self.item_desc_pl} in tracking wallet!')
|
|
|
|
|
|
- self.do_sort(key=sort_key, reverse=reverse_sort)
|
|
|
+ self.sort_data(self.sort_key)
|
|
|
+
|
|
|
+ self.disp_data = tuple(self.get_disp_data())
|
|
|
|
|
|
# get_data() is immediately followed by display header, and get_rpc_data() produces output,
|
|
|
# so add NL here (' ' required because CUR_HOME erases preceding blank lines)
|
|
|
@@ -314,9 +315,13 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
return _term_dimensions(cols, ts.height)
|
|
|
if sys.stdout.isatty():
|
|
|
if self.cfg.columns and cols < min_cols:
|
|
|
- die(1, '\n'+fmt(self.twidth_diemsg.format(self.cfg.columns, self.desc, min_cols), indent=' '))
|
|
|
+ die(1, '\n'+fmt(
|
|
|
+ self.twidth_diemsg.format(self.cfg.columns, self.desc, min_cols),
|
|
|
+ indent = ' '))
|
|
|
else:
|
|
|
- m, dim = (self.twidth_errmsg, min_cols) if cols < min_cols else (self.theight_errmsg, min_lines)
|
|
|
+ m, dim = (
|
|
|
+ (self.twidth_errmsg, min_cols) if cols < min_cols else
|
|
|
+ (self.theight_errmsg, min_lines))
|
|
|
get_char_raw(CUR_HOME + ERASE_ALL + fmt(m.format(self.desc, dim), append=''))
|
|
|
user_resized = True
|
|
|
else:
|
|
|
@@ -325,12 +330,10 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
def compute_column_widths(self, widths, maxws, minws, maxws_nice, *, wide, interactive):
|
|
|
|
|
|
def do_ret(freews):
|
|
|
- widths.update({k: minws[k] + freews.get(k, 0) for k in minws})
|
|
|
- widths.update({ikey: widths[key] - self.disp_prec - 1 for key, ikey in self.amt_keys.items()})
|
|
|
- return namedtuple('column_widths', widths.keys())(*widths.values())
|
|
|
-
|
|
|
- def do_ret_max():
|
|
|
- widths.update({k: max(minws[k], maxws[k]) for k in minws})
|
|
|
+ if freews:
|
|
|
+ widths.update({k: minws[k] + freews.get(k, 0) for k in minws})
|
|
|
+ else:
|
|
|
+ widths.update({k: max(minws[k], maxws[k]) for k in minws})
|
|
|
widths.update({ikey: widths[key] - self.disp_prec - 1 for key, ikey in self.amt_keys.items()})
|
|
|
return namedtuple('column_widths', widths.keys())(*widths.values())
|
|
|
|
|
|
@@ -362,7 +365,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
self.cols = min(self.term_width, minw + varw)
|
|
|
|
|
|
if wide or self.cols == minw + varw:
|
|
|
- return do_ret_max()
|
|
|
+ return do_ret(None)
|
|
|
|
|
|
if maxws_nice:
|
|
|
# compute high-priority widths:
|
|
|
@@ -422,8 +425,13 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
|
|
|
def gen_hdr(spc):
|
|
|
|
|
|
- Blue, Green = (blue, green) if color else (nocolor, nocolor)
|
|
|
- Yes, No, All = (green('yes'), red('no'), yellow('all')) if color else ('yes', 'no', 'all')
|
|
|
+ if color:
|
|
|
+ Blue, Green = (blue, green)
|
|
|
+ Yes, No, All = (green('yes'), red('no'), yellow('all'))
|
|
|
+ else:
|
|
|
+ Blue, Green = (nocolor, nocolor)
|
|
|
+ Yes, No, All = ('yes', 'no', 'all')
|
|
|
+
|
|
|
sort_info = ' '.join(self.sort_info())
|
|
|
|
|
|
def fmt_filter(k):
|
|
|
@@ -454,20 +462,20 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
|
|
|
yield spc * self.term_width
|
|
|
|
|
|
- if data and dt.colhdr_fmt_method:
|
|
|
+ if self.disp_data and dt.colhdr_fmt_method:
|
|
|
col_hdr = getattr(self, dt.colhdr_fmt_method)(cw, hdr_fs, color)
|
|
|
yield col_hdr.rstrip() if line_processing == 'print' else col_hdr
|
|
|
|
|
|
def get_body(method):
|
|
|
if line_processing:
|
|
|
return getattr(self.line_processing, line_processing).do(
|
|
|
- method, data, cw, fs, color, getattr(self, dt.line_fmt_method))
|
|
|
+ method, self.disp_data, cw, fs, color, getattr(self, dt.line_fmt_method))
|
|
|
else:
|
|
|
- return method(data, cw, fs, color, getattr(self, dt.line_fmt_method))
|
|
|
+ return method(self.disp_data, cw, fs, color, getattr(self, dt.line_fmt_method))
|
|
|
|
|
|
- if data and dt.need_column_widths:
|
|
|
- self.set_amt_widths(data)
|
|
|
- cw = self.get_column_widths(data, wide=dt.detail, interactive=interactive)
|
|
|
+ if self.disp_data and dt.need_column_widths:
|
|
|
+ self.set_amt_widths(self.disp_data)
|
|
|
+ cw = self.get_column_widths(self.disp_data, wide=dt.detail, interactive=interactive)
|
|
|
cwh = cw._asdict()
|
|
|
fp = self.fs_params
|
|
|
rfill = ' ' * (self.term_width - self.cols) if scroll else ''
|
|
|
@@ -481,7 +489,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
return (
|
|
|
tuple(gen_hdr(spc='' if line_processing == 'print' else ' ')),
|
|
|
tuple(
|
|
|
- get_body(getattr(self, dt.fmt_method)) if data else
|
|
|
+ get_body(getattr(self, dt.fmt_method)) if self.disp_data else
|
|
|
[(nocolor, yellow)[color](self.nodata_msg.ljust(self.term_width))]))
|
|
|
|
|
|
if not gv.stdout.isatty():
|
|
|
@@ -499,10 +507,10 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
if self.has_age and (self.age_fmt in self.age_fmts_date_dependent or dt.detail):
|
|
|
await self.set_dates(self.data)
|
|
|
|
|
|
- dsave = self.disp_data
|
|
|
- data = self.disp_data = list(self.filter_data()) # method could be a generator
|
|
|
+ disp_data_save = self.disp_data
|
|
|
+ self.disp_data = tuple(self.get_disp_data()) # method could be a generator
|
|
|
|
|
|
- if data != dsave:
|
|
|
+ if self.disp_data != disp_data_save:
|
|
|
self.pos = 0
|
|
|
|
|
|
display_hdr, display_body = make_display()
|
|
|
@@ -694,36 +702,61 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
if not parent.disp_data:
|
|
|
return
|
|
|
|
|
|
- from ..ui import line_input
|
|
|
+ async def do_error_msg(data, is_addr_idx):
|
|
|
+ msg_r(
|
|
|
+ 'Choice must be a single number between {n} and {m} inclusive{s}'.format(
|
|
|
+ n = list(data.keys())[0] if is_addr_idx else 1,
|
|
|
+ m = list(data.keys())[-1] if is_addr_idx else len(data),
|
|
|
+ s = ' ' if parent.scroll else ''))
|
|
|
+ if parent.scroll:
|
|
|
+ await asyncio.sleep(1.5)
|
|
|
+ msg_r(CUR_UP(1) + '\r' + ERASE_ALL)
|
|
|
+
|
|
|
+ async def get_idx(desc, data, *, is_addr_idx=False):
|
|
|
+ from ..ui import line_input
|
|
|
+ ur = namedtuple('usr_idx_data', ['idx', 'addr_idx'])
|
|
|
+ while True:
|
|
|
+ msg_r(parent.blank_prompt if parent.scroll else '\n')
|
|
|
+ usr_ret = line_input(
|
|
|
+ parent.cfg,
|
|
|
+ f'Enter {desc} (or ENTER to return to main menu): ')
|
|
|
+ if usr_ret == '':
|
|
|
+ if parent.scroll:
|
|
|
+ msg_r(CUR_UP(1) + '\r' + ''.ljust(parent.term_width))
|
|
|
+ return None
|
|
|
+ if is_addr_idx:
|
|
|
+ if is_int(usr_ret) and int(usr_ret) in data:
|
|
|
+ return ur(MMGenIdx(data[int(usr_ret)].disp_data_idx + 1), int(usr_ret))
|
|
|
+ else:
|
|
|
+ idx = get_obj(MMGenIdx, n=usr_ret, silent=True)
|
|
|
+ if idx and idx <= len(data):
|
|
|
+ return ur(idx, None)
|
|
|
+ await do_error_msg(data, is_addr_idx)
|
|
|
+
|
|
|
+ async def get_idx_from_user():
|
|
|
+ if parent.account_based:
|
|
|
+ if res := await get_idx(f'{parent.item_desc} number', parent.accts_data):
|
|
|
+ return await get_idx(
|
|
|
+ 'address index',
|
|
|
+ list(parent.accts_data.values())[res.idx - 1].data,
|
|
|
+ is_addr_idx = True)
|
|
|
+ else:
|
|
|
+ return await get_idx(f'{parent.item_desc} number', parent.disp_data)
|
|
|
+
|
|
|
while True:
|
|
|
- msg_r(parent.blank_prompt if parent.scroll else '\n')
|
|
|
- ret = line_input(
|
|
|
- parent.cfg,
|
|
|
- f'Enter {parent.item_desc} number (or ENTER to return to main menu): ')
|
|
|
- if ret == '':
|
|
|
- if parent.scroll:
|
|
|
- msg_r(CUR_UP(1) + '\r' + ''.ljust(parent.term_width))
|
|
|
- return
|
|
|
- idx = get_obj(MMGenIdx, n=ret, silent=True)
|
|
|
- if not idx or idx < 1 or idx > len(parent.disp_data):
|
|
|
- msg_r(
|
|
|
- 'Choice must be a single number between 1 and {n}{s}'.format(
|
|
|
- n = len(parent.disp_data),
|
|
|
- s = ' ' if parent.scroll else ''))
|
|
|
- if parent.scroll:
|
|
|
- await asyncio.sleep(1.5)
|
|
|
- msg_r(CUR_UP(1) + '\r' + ERASE_ALL)
|
|
|
+ # action_method return values:
|
|
|
+ # True: action successfully performed
|
|
|
+ # False: an error occurred
|
|
|
+ # None: action aborted by user or no action performed
|
|
|
+ # 'redo': user will be re-prompted for item number
|
|
|
+ # 'redraw': action successfully performed, screen will be redrawn
|
|
|
+ if usr_ret := await get_idx_from_user():
|
|
|
+ ret = await action_method(parent, usr_ret.idx, usr_ret.addr_idx)
|
|
|
else:
|
|
|
- # action return values:
|
|
|
- # True: action successfully performed
|
|
|
- # None: action aborted by user or no action performed
|
|
|
- # False: an error occurred
|
|
|
- # 'redo': user will be re-prompted for item number
|
|
|
- # 'redraw': action successfully performed, screen will be redrawn
|
|
|
- ret = await action_method(parent, idx)
|
|
|
- if ret != 'redo':
|
|
|
- break
|
|
|
- await asyncio.sleep(0.5)
|
|
|
+ ret = None
|
|
|
+ if ret != 'redo':
|
|
|
+ break
|
|
|
+ await asyncio.sleep(0.5)
|
|
|
|
|
|
if parent.scroll and ret is False or ret == 'redraw':
|
|
|
# error messages could leave screen in messy state, so do complete redraw:
|
|
|
@@ -731,7 +764,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
CUR_HOME + ERASE_ALL +
|
|
|
await parent.format(display_type='squeezed', interactive=True, scroll=True))
|
|
|
|
|
|
- async def i_balance_refresh(self, parent, idx):
|
|
|
+ async def i_balance_refresh(self, parent, idx, addr_idx=None):
|
|
|
if not parent.keypress_confirm(
|
|
|
f'Refreshing tracking wallet {parent.item_desc} #{idx}. OK?'):
|
|
|
return 'redo'
|
|
|
@@ -748,7 +781,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
if res == 0:
|
|
|
return 'redraw' # zeroing balance may mess up display
|
|
|
|
|
|
- async def i_addr_delete(self, parent, idx):
|
|
|
+ async def i_addr_delete(self, parent, idx, addr_idx=None):
|
|
|
if not parent.keypress_confirm(
|
|
|
'Removing {} {} from tracking wallet. OK?'.format(
|
|
|
parent.item_desc, red(f'#{idx}'))):
|
|
|
@@ -762,7 +795,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
parent.oneshot_msg = red('Address could not be removed')
|
|
|
return False
|
|
|
|
|
|
- async def i_comment_add(self, parent, idx):
|
|
|
+ async def i_comment_add(self, parent, idx, addr_idx=None):
|
|
|
|
|
|
async def do_comment_add(comment_in):
|
|
|
from ..obj import TwComment
|
|
|
@@ -784,27 +817,32 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
await asyncio.sleep(3)
|
|
|
parent.oneshot_msg = red('Label for {desc} could not be {action}'.format(
|
|
|
desc = desc,
|
|
|
- action = 'edited' if cur_comment and comment else 'added' if comment else 'removed'
|
|
|
- ))
|
|
|
+ action =
|
|
|
+ 'edited' if cur_comment and comment else
|
|
|
+ 'added' if comment else
|
|
|
+ 'removed'))
|
|
|
return False
|
|
|
|
|
|
entry = parent.disp_data[idx-1]
|
|
|
- desc = f'{parent.item_desc} #{idx}'
|
|
|
+ if addr_idx is None:
|
|
|
+ desc = f'{parent.item_desc} #{idx}'
|
|
|
+ color_desc = f'{parent.item_desc} {red("#" + str(idx))}'
|
|
|
+ else:
|
|
|
+ desc = f'address #{addr_idx}'
|
|
|
+ color_desc = f'address {red("#" + str(addr_idx))}'
|
|
|
+
|
|
|
cur_comment = parent.disp_data[idx-1].comment
|
|
|
msg('Current label: {}'.format(cur_comment.hl() if cur_comment else '(none)'))
|
|
|
|
|
|
from ..ui import line_input
|
|
|
- res = line_input(
|
|
|
- parent.cfg,
|
|
|
- 'Enter label text for {} {}: '.format(parent.item_desc, red(f'#{idx}')),
|
|
|
- insert_txt = cur_comment)
|
|
|
+ res = line_input(parent.cfg, f'Enter label text for {color_desc}: ', insert_txt=cur_comment)
|
|
|
|
|
|
match res:
|
|
|
case s if s == cur_comment:
|
|
|
parent.oneshot_msg = yellow(f'Label for {desc} unchanged')
|
|
|
return None
|
|
|
case '':
|
|
|
- if not parent.keypress_confirm(f'Removing label for {desc}. OK?'):
|
|
|
+ if not parent.keypress_confirm(f'Removing label for {color_desc}. OK?'):
|
|
|
return 'redo'
|
|
|
|
|
|
return await do_comment_add(res)
|
|
|
@@ -839,19 +877,19 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|
|
return action_method(parent)
|
|
|
|
|
|
def s_addr(self, parent):
|
|
|
- parent.do_sort('addr')
|
|
|
+ parent.sort_data('addr')
|
|
|
|
|
|
def s_age(self, parent):
|
|
|
- parent.do_sort('age')
|
|
|
+ parent.sort_data('age')
|
|
|
|
|
|
def s_amt(self, parent):
|
|
|
- parent.do_sort('amt')
|
|
|
+ parent.sort_data('amt')
|
|
|
|
|
|
def s_txid(self, parent):
|
|
|
- parent.do_sort('txid')
|
|
|
+ parent.sort_data('txid')
|
|
|
|
|
|
def s_twmmid(self, parent):
|
|
|
- parent.do_sort('twmmid')
|
|
|
+ parent.sort_data('twmmid')
|
|
|
|
|
|
def s_reverse(self, parent):
|
|
|
parent.data.reverse()
|