From 37da2c8768ec2a05a62c1ebea572b9326eb76eb2 Mon Sep 17 00:00:00 2001 From: Dmitrii Tarasov Date: Thu, 25 Sep 2025 13:48:56 +0300 Subject: [PATCH 1/8] fix eta calc. less exceptions --- tbview/viewer.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tbview/viewer.py b/tbview/viewer.py index 8771b6f..4930507 100644 --- a/tbview/viewer.py +++ b/tbview/viewer.py @@ -252,6 +252,7 @@ def plot(self, tbox): speed_str = None try: eta_sec, steps_per_sec = self._compute_run_epoch_eta(run_tag) + self.log(f'eta_sec: {eta_sec}, steps_per_sec: {steps_per_sec}', DEBUG) if eta_sec is not None: eta_str = self._format_duration(eta_sec) if steps_per_sec is not None and steps_per_sec > 0: @@ -306,10 +307,6 @@ def plot(self, tbox): last_step = global_last_step plt.title(f"{key} (smooth={self.smoothing_window}, last_step={last_step})") - try: - plt.legend(True) - except Exception: - pass plt.xfrequency(10) plt.xlabel(xlabel) # Apply xlim after plotting @@ -318,15 +315,15 @@ def plot(self, tbox): if x_mode == 'step': try: plt.xlim(start_s, end_s) - except Exception: - self.log(f'failed to set xlim for steps: {global_xlim_min} {global_xlim_max}', WARN) + except Exception as e: + self.log(f'failed to set xlim for steps: {global_xlim_min} {global_xlim_max}: {e}', WARN) pass else: if global_xlim_min is not None and global_xlim_max is not None: try: plt.xlim(global_xlim_min, global_xlim_max) - except Exception: - self.log(f'failed to set xlim for {x_mode}: {global_xlim_min} {global_xlim_max}', WARN) + except Exception as e: + self.log(f'failed to set xlim for {x_mode}: {global_xlim_min} {global_xlim_max}: {e}', WARN) pass plt.show() if self._profile_enabled: @@ -358,7 +355,8 @@ def _finalize_xlim_input(self): def _render_xlim_prompt(self): try: self.logger.replace_last(self.term.white(f"{INFO} Enter xlim in steps as start:end (ESC to cancel): {self._xlim_input_buffer}")) - except Exception: + except Exception as e: + self.log(f'failed to render xlim prompt: {e}', WARN) pass def _moving_average(self, values, window): @@ -378,7 +376,8 @@ def _moving_average(self, values, window): def _format_duration(self, seconds): try: secs = max(0, int(round(seconds))) - except Exception: + except Exception as e: + self.log(f'failed to format duration: {e}', WARN) return "?" h = secs // 3600 m = (secs % 3600) // 60 @@ -409,7 +408,8 @@ def _compute_run_epoch_eta(self, run_tag): if v is not None and float(v) >= 1.0 and t is not None: idx_ge1 = i break - except Exception: + except Exception as e: + self.log(f'failed to compute run epoch eta: {e}', WARN) continue if idx_ge1 is not None: eta = max(0.0, float(times_abs[idx_ge1] - t0_abs)) @@ -427,7 +427,8 @@ def _compute_run_epoch_eta(self, run_tag): if v is not None and float(v) > 0 and t is not None: last_idx = i break - except Exception: + except Exception as e: + self.log(f'failed to compute run epoch eta: {e}', WARN) continue if last_idx is None: return None @@ -439,7 +440,7 @@ def _compute_run_epoch_eta(self, run_tag): speed = (steps_elapsed / time_elapsed) if time_elapsed > 0 else None if frac <= 0: return None - eta = max(0.0, t_rel * (1.0 / frac)) + eta = max(0.0, t_rel * (1.0 / frac) - time_elapsed) return eta, speed def run(self): From 91f2b8f36f8fdf4ca538768b9dfbf095dbf30fb3 Mon Sep 17 00:00:00 2001 From: Dmitrii Tarasov Date: Thu, 25 Sep 2025 13:53:25 +0300 Subject: [PATCH 2/8] y limits resize --- tbview/viewer.py | 72 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/tbview/viewer.py b/tbview/viewer.py index 4930507..54d2a65 100644 --- a/tbview/viewer.py +++ b/tbview/viewer.py @@ -38,10 +38,13 @@ def __init__(self, event_path, event_tag) -> None: self._xlim_steps = None # tuple (start_step, end_step) or None self._awaiting_xlim_input = False self._xlim_input_buffer = '' + self._ylim = None # tuple (ymin, ymax) or None + self._awaiting_ylim_input = False + self._ylim_input_buffer = '' self.ui = RatioHSplit( PlotextTile(self.plot, title='Plot', border_color=15), RatioVSplit( - Text(" 1.Press arrow keys to locate coordinates.\n\n 2.Use number 1-9 or W/S to select tag.\n\n 3.Press 'q' to go back to selection.\n\n 4.Ctrl+C to quit.\n\n 5.Press 's' to toggle smoothing (0/10/50/100/200).\n\n 6.Press 'x' to toggle X axis (step/rel/abs).\n\n 7.Press 'l' to set xlim in steps (start:end), ESC to cancel.", color=15, title=' Tips', border_color=15), + Text(" 1.Press arrow keys to locate coordinates.\n\n 2.Use number 1-9 or W/S to select tag.\n\n 3.Press 'q' to go back to selection.\n\n 4.Ctrl+C to quit.\n\n 5.Press 's' to toggle smoothing (0/10/50/100/200).\n\n 6.Press 'x' to toggle X axis (step/rel/abs).\n\n 7.Press 'l' to set xlim in steps (start:end), ESC to cancel.\n\n 8.Press 'y' to set ylim (min:max), ESC to cancel.", color=15, title=' Tips', border_color=15), self.tag_selector, self.logger, ratios=(2, 4, 2), @@ -122,6 +125,30 @@ def scan_events(self, initial=False): def handle_input(self, key): if key is None: return + # Handle ylim interactive input mode + if self._awaiting_ylim_input: + if key.is_sequence: + name = getattr(key, 'name', '') + if name in ('KEY_BACKSPACE', 'KEY_DELETE'): + if self._ylim_input_buffer: + self._ylim_input_buffer = self._ylim_input_buffer[:-1] + self._render_ylim_prompt() + elif name in ('KEY_ENTER',): + self._finalize_ylim_input() + elif name in ('KEY_ESCAPE',): + self._awaiting_ylim_input = False + self._ylim_input_buffer = '' + self.log('ylim input cancelled', INFO) + else: + pass + else: + ch = str(key) + if ch in ('\n', '\r'): + self._finalize_ylim_input() + elif ch.isprintable(): + self._ylim_input_buffer += ch + self._render_ylim_prompt() + return # Handle xlim interactive input mode if self._awaiting_xlim_input: # Accept digits, colon, minus, backspace, enter @@ -170,6 +197,11 @@ def handle_input(self, key): self.log("Enter xlim in steps as start:end (empty to clear). Press Enter to apply.", INFO) # Echo interactive prompt line self._render_xlim_prompt() + elif str(key).lower() == 'y': + self._awaiting_ylim_input = True + self._ylim_input_buffer = '' + self.log("Enter ylim as min:max (empty to clear). Press Enter to apply.", INFO) + self._render_ylim_prompt() def log(self, msg, level=''): self.logger.append(self.term.white(f'{level} {msg}')) @@ -252,7 +284,7 @@ def plot(self, tbox): speed_str = None try: eta_sec, steps_per_sec = self._compute_run_epoch_eta(run_tag) - self.log(f'eta_sec: {eta_sec}, steps_per_sec: {steps_per_sec}', DEBUG) + # self.log(f'eta_sec: {eta_sec}, steps_per_sec: {steps_per_sec}', DEBUG) if eta_sec is not None: eta_str = self._format_duration(eta_sec) if steps_per_sec is not None and steps_per_sec > 0: @@ -325,6 +357,14 @@ def plot(self, tbox): except Exception as e: self.log(f'failed to set xlim for {x_mode}: {global_xlim_min} {global_xlim_max}: {e}', WARN) pass + # Apply ylim after plotting + if self._ylim is not None: + y_min, y_max = self._ylim + try: + plt.ylim(y_min, y_max) + except Exception as e: + self.log(f'failed to set ylim {y_min} {y_max}: {e}', WARN) + pass plt.show() if self._profile_enabled: self.log(f'plot took {(time.perf_counter()-t0)*1000:.1f}ms', DEBUG) @@ -353,11 +393,33 @@ def _finalize_xlim_input(self): self.log(f'failed to parse xlim: {e}', WARN) def _render_xlim_prompt(self): + self.logger.replace_last(self.term.white(f"{INFO} Enter xlim in steps as start:end (ESC to cancel): {self._xlim_input_buffer}")) + + def _finalize_ylim_input(self): + raw = (self._ylim_input_buffer or '').strip() + self._awaiting_ylim_input = False + self._ylim_input_buffer = '' + if raw == '': + self._ylim = None + self.log('ylim cleared', INFO) + return try: - self.logger.replace_last(self.term.white(f"{INFO} Enter xlim in steps as start:end (ESC to cancel): {self._xlim_input_buffer}")) + if ':' in raw: + start_s, end_s = raw.split(':', 1) + y_min = float(start_s.strip()) + y_max = float(end_s.strip()) + else: + y_min = 0.0 + y_max = float(raw) + if y_min > y_max: + y_min, y_max = y_max, y_min + self._ylim = (y_min, y_max) + self.log(f'set ylim to {self._ylim[0]}:{self._ylim[1]}', INFO) except Exception as e: - self.log(f'failed to render xlim prompt: {e}', WARN) - pass + self.log(f'failed to parse ylim: {e}', WARN) + + def _render_ylim_prompt(self): + self.logger.replace_last(self.term.white(f"{INFO} Enter ylim as min:max (ESC to cancel): {self._ylim_input_buffer}")) def _moving_average(self, values, window): if window <= 1 or not values: From 8c152e0ad937fcfa83b30ce2a8ca1e804c178288 Mon Sep 17 00:00:00 2001 From: Dmitrii Tarasov Date: Thu, 25 Sep 2025 13:57:16 +0300 Subject: [PATCH 3/8] update list of logs on q --- tbview/cli.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tbview/cli.py b/tbview/cli.py index be9ad86..e046afc 100644 --- a/tbview/cli.py +++ b/tbview/cli.py @@ -38,21 +38,20 @@ def run_main(args): target_event_name = os.path.basename(path) target_event_dir = None elif os.path.isdir(path): - target_options = [] - for root, dirs, files in os.walk(path): - for file in files: - if is_event_file(file): - size = os.path.getsize(os.path.join(root, file)) - - display_path_no_prefix = root.replace(path, '').lstrip(os.sep) - target_options.append((root, file, size, display_path_no_prefix)) - if len(target_options) == 0: - raise RuntimeError(f"No event file found in directory {path}") - target_options = sorted(target_options, key=lambda x:x[1], reverse=True) - options = [f'[{i}] {op[3]}/{local_event_name(op[1])}' for i, op in enumerate(target_options)] - - # Loop to support going back from viewer with 'q' + # Loop to support going back from viewer with 'q' and refreshing available logs while True: + target_options = [] + for root, dirs, files in os.walk(path): + for file in files: + if is_event_file(file): + size = os.path.getsize(os.path.join(root, file)) + display_path_no_prefix = root.replace(path, '').lstrip(os.sep) + target_options.append((root, file, size, display_path_no_prefix)) + if len(target_options) == 0: + raise RuntimeError(f"No event file found in directory {path}") + target_options = sorted(target_options, key=lambda x:x[1], reverse=True) + options = [f'[{i}] {op[3]}/{local_event_name(op[1])}' for i, op in enumerate(target_options)] + questions = [ inquirer.Checkbox('choices', message="Select one or more event files (space to toggle, enter to view)", From 8d472e7e73765544b578f30fc0ebe0145ad7aacd Mon Sep 17 00:00:00 2001 From: Dmitrii Tarasov Date: Thu, 25 Sep 2025 14:04:22 +0300 Subject: [PATCH 4/8] fix ylim --- tbview/viewer.py | 63 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/tbview/viewer.py b/tbview/viewer.py index 54d2a65..f9114f1 100644 --- a/tbview/viewer.py +++ b/tbview/viewer.py @@ -233,6 +233,10 @@ def plot(self, tbox): global_last_step = None global_xlim_min = None global_xlim_max = None + global_ymin = None + global_ymax = None + global_xmin_step = None + global_xmax_step = None for idx, (run_tag, path) in enumerate(zip(self.run_tags, self.event_paths)): per_run_records = self.records_by_run.get(run_tag, {}) if key not in per_run_records: @@ -316,6 +320,20 @@ def plot(self, tbox): s_last = sorted_steps[-1] if global_last_step is None or s_last > global_last_step: global_last_step = s_last + # track global x range in step space + s_first = sorted_steps[0] + if global_xmin_step is None or s_first < global_xmin_step: + global_xmin_step = s_first + if global_xmax_step is None or s_last > global_xmax_step: + global_xmax_step = s_last + # track global y range for ylim validation + if values: + vmin = min(values) + vmax = max(values) + if global_ymin is None or vmin < global_ymin: + global_ymin = vmin + if global_ymax is None or vmax > global_ymax: + global_ymax = vmax # Compute desired axis-space xlim from step-based limits without filtering if self._xlim_steps is not None: @@ -345,26 +363,43 @@ def plot(self, tbox): if self._xlim_steps is not None: start_s, end_s = self._xlim_steps if x_mode == 'step': - try: - plt.xlim(start_s, end_s) - except Exception as e: - self.log(f'failed to set xlim for steps: {global_xlim_min} {global_xlim_max}: {e}', WARN) - pass + # clamp to available step range to avoid plotext errors + if global_xmin_step is not None and global_xmax_step is not None: + x0 = min(start_s, end_s) + x1 = max(start_s, end_s) + cx0 = max(x0, global_xmin_step) + cx1 = min(x1, global_xmax_step) + if cx1 > cx0: + plt.xlim(cx0, cx1) + else: + self.log('requested xlim is outside data range; ignoring', WARN) + self._xlim_steps = None + else: + self.log('no data range available for xlim; ignoring', WARN) + self._xlim_steps = None else: if global_xlim_min is not None and global_xlim_max is not None: - try: + if global_xlim_max > global_xlim_min: plt.xlim(global_xlim_min, global_xlim_max) - except Exception as e: - self.log(f'failed to set xlim for {x_mode}: {global_xlim_min} {global_xlim_max}: {e}', WARN) - pass + self._xlim_steps = None + else: + self.log('computed xlim has non-positive width; ignoring', WARN) # Apply ylim after plotting if self._ylim is not None: y_min, y_max = self._ylim - try: - plt.ylim(y_min, y_max) - except Exception as e: - self.log(f'failed to set ylim {y_min} {y_max}: {e}', WARN) - pass + if global_ymin is not None and global_ymax is not None: + a = min(y_min, y_max) + b = max(y_min, y_max) + cy0 = max(a, global_ymin) + cy1 = min(b, global_ymax) + if cy1 > cy0: + plt.ylim(cy0, cy1) + else: + self.log('requested ylim is outside data range; ignoring', WARN) + self._ylim = None + else: + self.log('no data range available for ylim; ignoring', WARN) + self._ylim = None plt.show() if self._profile_enabled: self.log(f'plot took {(time.perf_counter()-t0)*1000:.1f}ms', DEBUG) From 7c5e90cffab4b4c45dbae296365dd95e11896e3b Mon Sep 17 00:00:00 2001 From: Dmitrii Tarasov Date: Thu, 25 Sep 2025 14:05:42 +0300 Subject: [PATCH 5/8] fix hotkeys --- tbview/viewer.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tbview/viewer.py b/tbview/viewer.py index f9114f1..5230afb 100644 --- a/tbview/viewer.py +++ b/tbview/viewer.py @@ -44,7 +44,7 @@ def __init__(self, event_path, event_tag) -> None: self.ui = RatioHSplit( PlotextTile(self.plot, title='Plot', border_color=15), RatioVSplit( - Text(" 1.Press arrow keys to locate coordinates.\n\n 2.Use number 1-9 or W/S to select tag.\n\n 3.Press 'q' to go back to selection.\n\n 4.Ctrl+C to quit.\n\n 5.Press 's' to toggle smoothing (0/10/50/100/200).\n\n 6.Press 'x' to toggle X axis (step/rel/abs).\n\n 7.Press 'l' to set xlim in steps (start:end), ESC to cancel.\n\n 8.Press 'y' to set ylim (min:max), ESC to cancel.", color=15, title=' Tips', border_color=15), + Text(" 1.Press arrow keys to locate coordinates.\n\n 2.Use number 1-9 or W/S to select tag.\n\n 3.Press 'q' to go back to selection.\n\n 4.Ctrl+C to quit.\n\n 5.Press 's' to toggle smoothing (0/10/50/100/200).\n\n 6.Press 'm' to toggle X axis (step/rel/abs).\n\n 7.Press 'x' to set xlim in steps (start:end), ESC to cancel.\n\n 8.Press 'y' to set ylim (min:max), ESC to cancel.", color=15, title=' Tips', border_color=15), self.tag_selector, self.logger, ratios=(2, 4, 2), @@ -186,12 +186,12 @@ def handle_input(self, key): self.smoothing_index = (self.smoothing_index + 1) % len(self.smoothing_levels) self.smoothing_window = self.smoothing_levels[self.smoothing_index] self.log(f'smoothing set to {self.smoothing_window}', INFO) - elif str(key).lower() == 'x': + elif str(key).lower() == 'm': self.x_mode_index = (self.x_mode_index + 1) % len(self.x_axis_modes) self.log(f"X axis set to {self.x_axis_modes[self.x_mode_index]}", INFO) elif str(key).lower() == 'q': self._quit_and_reselect = True - elif str(key).lower() == 'l': + elif str(key).lower() == 'x': self._awaiting_xlim_input = True self._xlim_input_buffer = '' self.log("Enter xlim in steps as start:end (empty to clear). Press Enter to apply.", INFO) @@ -380,8 +380,10 @@ def plot(self, tbox): else: if global_xlim_min is not None and global_xlim_max is not None: if global_xlim_max > global_xlim_min: - plt.xlim(global_xlim_min, global_xlim_max) - self._xlim_steps = None + try: + plt.xlim(global_xlim_min, global_xlim_max) + except Exception as e: + self.log(f'failed to set xlim for {x_mode}: {global_xlim_min} {global_xlim_max}: {e}', WARN) else: self.log('computed xlim has non-positive width; ignoring', WARN) # Apply ylim after plotting From cfab2f4a092d640f403f06f42308af6b39c0df9f Mon Sep 17 00:00:00 2001 From: Dmitrii Tarasov Date: Thu, 25 Sep 2025 14:22:23 +0300 Subject: [PATCH 6/8] fix xlims --- tbview/viewer.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/tbview/viewer.py b/tbview/viewer.py index 5230afb..7f464ef 100644 --- a/tbview/viewer.py +++ b/tbview/viewer.py @@ -386,6 +386,10 @@ def plot(self, tbox): self.log(f'failed to set xlim for {x_mode}: {global_xlim_min} {global_xlim_max}: {e}', WARN) else: self.log('computed xlim has non-positive width; ignoring', WARN) + else: + # No points within requested range in time-based x axis; clear invalid selection + self.log('requested xlim selects no points in current x mode; clearing', WARN) + self._xlim_steps = None # Apply ylim after plotting if self._ylim is not None: y_min, y_max = self._ylim @@ -402,7 +406,14 @@ def plot(self, tbox): else: self.log('no data range available for ylim; ignoring', WARN) self._ylim = None - plt.show() + # Safeguard rendering to avoid crashing the UI on plotting errors + try: + plt.show() + except Exception as e: + self.log(f'plot rendering failed: {e}', ERROR) + # Clear potentially invalid limits to recover next frame + self._xlim_steps = None + self._ylim = None if self._profile_enabled: self.log(f'plot took {(time.perf_counter()-t0)*1000:.1f}ms', DEBUG) @@ -424,8 +435,25 @@ def _finalize_xlim_input(self): end_v = int(raw) if start_v > end_v: start_v, end_v = end_v, start_v - self._xlim_steps = (start_v, end_v) - self.log(f'set xlim (steps) to {self._xlim_steps[0]}:{self._xlim_steps[1]}', INFO) + # Validate against available step range for currently selected tag + selected_tag = self._get_selected_tag() + gmin, gmax = self._get_global_step_range_for_tag(selected_tag) + if gmin is None or gmax is None: + self._xlim_steps = None + self.log('no data available to apply xlim; ignoring', WARN) + return + # Clamp to data range + cx0 = max(start_v, gmin) + cx1 = min(end_v, gmax) + if cx1 <= cx0: + self._xlim_steps = None + self.log('requested xlim is outside data range; ignoring', WARN) + return + self._xlim_steps = (cx0, cx1) + if (cx0, cx1) != (start_v, end_v): + self.log(f'clamped xlim (steps) to {cx0}:{cx1}', INFO) + else: + self.log(f'set xlim (steps) to {cx0}:{cx1}', INFO) except Exception as e: self.log(f'failed to parse xlim: {e}', WARN) @@ -458,6 +486,43 @@ def _finalize_ylim_input(self): def _render_ylim_prompt(self): self.logger.replace_last(self.term.white(f"{INFO} Enter ylim as min:max (ESC to cancel): {self._ylim_input_buffer}")) + def _get_selected_tag(self): + """Return the currently selected tag name or None if unavailable.""" + # Collect union of tags across runs + all_tags = OrderedDict() + for run_tag in self.run_tags: + for t in self.records_by_run.get(run_tag, {}): + all_tags.setdefault(t, None) + keys = list(all_tags.keys()) + if not keys: + return None + safe_idx = max(0, min(self.tag_selector.current, len(keys)-1)) + return keys[safe_idx] + + def _get_global_step_range_for_tag(self, tag): + """Compute global min/max step across runs for the given tag. + + Returns (min_step, max_step) or (None, None) if no data. + """ + if tag is None: + return None, None + global_xmin_step = None + global_xmax_step = None + for run_tag in self.run_tags: + per_run_records = self.records_by_run.get(run_tag, {}) + if tag not in per_run_records: + continue + steps = list(per_run_records[tag].keys()) + if not steps: + continue + s_first = min(steps) + s_last = max(steps) + if global_xmin_step is None or s_first < global_xmin_step: + global_xmin_step = s_first + if global_xmax_step is None or s_last > global_xmax_step: + global_xmax_step = s_last + return global_xmin_step, global_xmax_step + def _moving_average(self, values, window): if window <= 1 or not values: return values From 4375cd35e2099d5b4b59c9020bf0d1539a61c6a0 Mon Sep 17 00:00:00 2001 From: Dmitrii Tarasov Date: Thu, 25 Sep 2025 14:30:09 +0300 Subject: [PATCH 7/8] save selected tags --- tbview/cli.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tbview/cli.py b/tbview/cli.py index e046afc..5d49045 100644 --- a/tbview/cli.py +++ b/tbview/cli.py @@ -39,6 +39,7 @@ def run_main(args): target_event_dir = None elif os.path.isdir(path): # Loop to support going back from viewer with 'q' and refreshing available logs + previously_selected = set() while True: target_options = [] for root, dirs, files in os.walk(path): @@ -51,11 +52,19 @@ def run_main(args): raise RuntimeError(f"No event file found in directory {path}") target_options = sorted(target_options, key=lambda x:x[1], reverse=True) options = [f'[{i}] {op[3]}/{local_event_name(op[1])}' for i, op in enumerate(target_options)] + # Pre-select previously chosen items if returning from viewer + default_selected = [] + if previously_selected: + for i, op in enumerate(target_options): + root, file, _size, _disp = op + if (root, file) in previously_selected: + default_selected.append(options[i]) questions = [ inquirer.Checkbox('choices', message="Select one or more event files (space to toggle, enter to view)", choices=options, + default=default_selected if default_selected else None, carousel=True, ) ] @@ -84,6 +93,11 @@ def run_main(args): should_reselect = tbviewer.run() if not should_reselect: return + # Remember selected items for next loop iteration + previously_selected = set() + for idx in selected_indices: + root, file, _size, _disp = target_options[idx] + previously_selected.add((root, file)) target_event_tag = target_event_name if target_event_dir is None else target_event_dir From 037dde04dc5a85ad42f6ba714aad2c009f8aba81 Mon Sep 17 00:00:00 2001 From: Dmitrii Tarasov Date: Thu, 25 Sep 2025 14:41:51 +0300 Subject: [PATCH 8/8] any fix widget --- tbview/dashing_lib/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tbview/dashing_lib/widgets.py b/tbview/dashing_lib/widgets.py index f12bb27..34111cb 100644 --- a/tbview/dashing_lib/widgets.py +++ b/tbview/dashing_lib/widgets.py @@ -11,6 +11,7 @@ def __init__(self, plot_fn: Callable[[TBox], None], *args, **kw): def _display(self, tbox, parent): tbox = self._draw_borders_and_title(tbox) st = self.plot_to_string(TBox(tbox.t, 0, 0, w=tbox.w-4, h=tbox.h-2 )) + dx = 0 for dx, line in enumerate(st.splitlines()): print( tbox.t.move(tbox.x + dx + 1, tbox.y + 2)