Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ or view a result directory:
tbview path/to/events/dir
```

## Controls (in viewer)

- **Switch tag**: Up/Down, PgUp/PgDn, Home/End, Tab/Shift-Tab, or `[` / `]`
- **Quick-select tag 1-9**: number keys `1`-`9`
- **Toggle smoothing**: `s`
- **Toggle X axis**: `m`
- **Set xlim (steps)**: `x` then type `start:end` (ESC cancels)
- **Set ylim**: `y` then type `min:max` (ESC cancels)
- **Back to selection**: `q`
- **Quit**: Ctrl+C

## Acknowledgement

This project is still in progress, and some features may not be complete.
42 changes: 41 additions & 1 deletion tbview/dashing_lib/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,26 @@ def __init__(self, options, current=0, color=0, *args, **kw):
super().__init__('', color, *args, **kw)
self._current = current
self._options = options
self._scroll_offset = 0

@property
def current(self):
return self._current

@current.setter
def current(self, c):
try:
c = int(c)
except Exception:
c = 0
if not self._options:
self._current = 0
self._scroll_offset = 0
return
if c < 0:
c = 0
if c >= len(self._options):
c = len(self._options) - 1
self._current = c

@property
Expand All @@ -52,6 +65,19 @@ def options(self):
@options.setter
def options(self, options):
self._options = options
# Keep selection and scroll offset in a valid range.
if not self._options:
self._current = 0
self._scroll_offset = 0
else:
if self._current < 0:
self._current = 0
if self._current >= len(self._options):
self._current = len(self._options) - 1
if self._scroll_offset < 0:
self._scroll_offset = 0
if self._scroll_offset >= len(self._options):
self._scroll_offset = max(0, len(self._options) - 1)

def _apply_options_to_text(self, tbox:TBox):
t = tbox.t
Expand All @@ -65,10 +91,24 @@ def _display(self, tbox, parent):
# Render options without wrapping to avoid breaking ANSI sequences
tbox = self._draw_borders_and_title(tbox)
t = tbox.t
# Ensure current selection is visible within the viewport.
viewport_h = max(0, tbox.h)
if self._options and viewport_h > 0:
if self._current < self._scroll_offset:
self._scroll_offset = self._current
elif self._current >= self._scroll_offset + viewport_h:
self._scroll_offset = self._current - viewport_h + 1
max_offset = max(0, len(self._options) - viewport_h)
if self._scroll_offset > max_offset:
self._scroll_offset = max_offset

dx = 0
for i, opt in enumerate(self._options):
start = self._scroll_offset
end = len(self._options)
for i in range(start, end):
if dx >= tbox.h:
break
opt = self._options[i]
visible_text = opt[:tbox.w]
styled = (t.on_white if i == self._current else t.white)(visible_text)
print(
Expand Down
81 changes: 79 additions & 2 deletions tbview/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,19 @@ 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 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),
Text(
" 1.Use Up/Down, PgUp/PgDn, Home/End, Tab/Shift-Tab, or [ / ] to switch tags.\n\n"
" 2.Use number 1-9 to quick-select tags 1-9.\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),
Expand Down Expand Up @@ -73,6 +85,31 @@ def __init__(self, event_path, event_tag) -> None:
self._quit_and_reselect = False
self.scan_events(initial=True)

def _tag_count(self):
return len(self.tag_selector.options or [])

def _select_tag_index(self, idx):
n = self._tag_count()
if n <= 0:
self.tag_selector.current = 0
return
if idx < 0:
idx = 0
if idx >= n:
idx = n - 1
self.tag_selector.current = idx

def _move_tag_selection(self, delta, wrap=False):
n = self._tag_count()
if n <= 0:
self.tag_selector.current = 0
return
cur = int(getattr(self.tag_selector, 'current', 0) or 0)
nxt = cur + int(delta)
if wrap:
nxt %= n
self._select_tag_index(nxt)


def scan_events(self, initial=False):
import os, time
Expand Down Expand Up @@ -176,12 +213,32 @@ def handle_input(self, key):
return

if key.is_sequence:
pass
name = getattr(key, 'name', '')
if name in ('KEY_UP',):
self._move_tag_selection(-1, wrap=False)
elif name in ('KEY_DOWN',):
self._move_tag_selection(+1, wrap=False)
elif name in ('KEY_PGUP', 'KEY_PAGEUP'):
self._move_tag_selection(-10, wrap=False)
elif name in ('KEY_PGDOWN', 'KEY_PAGEDOWN'):
self._move_tag_selection(+10, wrap=False)
elif name in ('KEY_HOME',):
self._select_tag_index(0)
elif name in ('KEY_END',):
self._select_tag_index(self._tag_count() - 1)
elif name in ('KEY_TAB',):
self._move_tag_selection(+1, wrap=True)
elif name in ('KEY_BTAB',):
self._move_tag_selection(-1, wrap=True)
else:
if key.isdigit():
digit = int(key)
if digit > 0 and digit <= len(self.tag_selector.options):
self.tag_selector.current = digit - 1
elif str(key) == '[':
self._move_tag_selection(-1, wrap=True)
elif str(key) == ']':
self._move_tag_selection(+1, wrap=True)
elif str(key).lower() == 's':
self.smoothing_index = (self.smoothing_index + 1) % len(self.smoothing_levels)
self.smoothing_window = self.smoothing_levels[self.smoothing_index]
Expand All @@ -206,6 +263,22 @@ def handle_input(self, key):
def log(self, msg, level=''):
self.logger.append(self.term.white(f'{level} {msg}'))

def _truncate_label(self, label, max_width):
"""Truncate label to fit max_width, showing first N and last N characters."""
if len(label) <= max_width:
return label
# Reserve space for "..."
ellipsis_len = 3
# Calculate how many characters we can show on each side
# We want to show equal amounts on both sides
available_chars = max_width - ellipsis_len
if available_chars < 2:
# Too narrow, just truncate
return label[:max_width]
n = available_chars // 2
# Show first n and last n characters with "..." in between
return label[:n] + "..." + label[-n:]

def plot(self, tbox):
import time
t0 = time.perf_counter()
Expand Down Expand Up @@ -312,6 +385,10 @@ def plot(self, tbox):
extra_parts.append(speed_str)
if extra_parts:
plot_label = f"{plot_label} (" + ", ".join(extra_parts) + ")"
# Truncate label if it doesn't fit on screen
# Estimate available width: leave ~30% for plot, rest for legend
max_label_width = max(20, int(tbox.w * 0.7))
plot_label = self._truncate_label(plot_label, max_label_width)
plt.plot(x_vals, values, label=plot_label, color=color)
except Exception:
plt.plot(x_vals, values, color=color)
Expand Down