feat: click task numbers to open detail view
Click on any #NNNN task number in the briefing to load its full detail view. Uses SGR mouse protocol for accurate click position, strips ANSI to find task IDs in visible text. Tolerates clicks within 5 chars of the # symbol. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
87
briefing
87
briefing
@@ -528,25 +528,57 @@ def get_terminal_size():
|
||||
|
||||
|
||||
def read_key(fd):
|
||||
"""Read a keypress from raw terminal. Returns key string."""
|
||||
"""Read a keypress from raw terminal. Returns key string or ('CLICK', row, col) tuple."""
|
||||
ch = os.read(fd, 1)
|
||||
if not ch:
|
||||
return ''
|
||||
if ch == b'\x1b':
|
||||
# Escape sequence
|
||||
seq = os.read(fd, 2)
|
||||
if seq == b'[A': return 'UP'
|
||||
if seq == b'[B': return 'DOWN'
|
||||
if seq == b'[5': os.read(fd, 1); return 'PGUP'
|
||||
if seq == b'[6': os.read(fd, 1); return 'PGDN'
|
||||
if seq == b'[H': return 'HOME'
|
||||
if seq == b'[F': return 'END'
|
||||
if seq == b'[M':
|
||||
# Mouse event: read 3 more bytes
|
||||
# Read next bytes
|
||||
b2 = os.read(fd, 1)
|
||||
if b2 != b'[':
|
||||
return 'ESC'
|
||||
b3 = os.read(fd, 1)
|
||||
|
||||
# SGR mouse: \033[<btn;x;y;M or \033[<btn;x;y;m
|
||||
if b3 == b'<':
|
||||
# Read until M or m
|
||||
buf = b''
|
||||
while True:
|
||||
c = os.read(fd, 1)
|
||||
if c in (b'M', b'm'):
|
||||
break
|
||||
buf += c
|
||||
parts = buf.decode().split(';')
|
||||
if len(parts) == 3:
|
||||
btn = int(parts[0])
|
||||
col = int(parts[1])
|
||||
row = int(parts[2])
|
||||
# btn 64/65 = scroll up/down
|
||||
if btn == 64: return 'SCROLLUP'
|
||||
if btn == 65: return 'SCROLLDN'
|
||||
# btn 0 = left click (on release: c == b'm')
|
||||
if btn == 0 and c == b'M':
|
||||
return ('CLICK', row, col)
|
||||
return ''
|
||||
|
||||
# Arrow keys and other sequences
|
||||
if b3 == b'A': return 'UP'
|
||||
if b3 == b'B': return 'DOWN'
|
||||
if b3 == b'5': os.read(fd, 1); return 'PGUP'
|
||||
if b3 == b'6': os.read(fd, 1); return 'PGDN'
|
||||
if b3 == b'H': return 'HOME'
|
||||
if b3 == b'F': return 'END'
|
||||
|
||||
# Old-style X10 mouse: \033[M + 3 bytes
|
||||
if b3 == b'M':
|
||||
mouse = os.read(fd, 3)
|
||||
btn = mouse[0] - 32
|
||||
if btn == 64: return 'SCROLLUP'
|
||||
if btn == 65: return 'SCROLLDN'
|
||||
if btn == 0:
|
||||
col = mouse[1] - 32
|
||||
row = mouse[2] - 32
|
||||
return ('CLICK', row, col)
|
||||
return ''
|
||||
return 'ESC'
|
||||
return ch.decode('utf-8', errors='replace')
|
||||
@@ -578,6 +610,30 @@ def tui_viewer(range_days, client):
|
||||
needs_redraw[0] = True
|
||||
signal.signal(signal.SIGWINCH, on_resize)
|
||||
|
||||
TASK_ID_RE = re.compile(r'#(\d{3,5})')
|
||||
|
||||
def find_task_at_click(row, col):
|
||||
"""Find a #NNNN task number on the clicked line, closest to click col."""
|
||||
line_idx = scroll + row - 1 # row is 1-based
|
||||
if line_idx < 0 or line_idx >= len(lines):
|
||||
return None
|
||||
# Strip ANSI to get visible text and map positions
|
||||
raw = lines[line_idx]
|
||||
clean = ANSI_ESC.sub('', raw)
|
||||
# Find all #NNNN in the visible text
|
||||
best = None
|
||||
best_dist = 999
|
||||
for m in TASK_ID_RE.finditer(clean):
|
||||
start, end = m.start(), m.end()
|
||||
# Distance from click to the match
|
||||
if start <= col <= end:
|
||||
return int(m.group(1))
|
||||
dist = min(abs(col - start), abs(col - end))
|
||||
if dist < best_dist and dist <= 5:
|
||||
best_dist = dist
|
||||
best = int(m.group(1))
|
||||
return best
|
||||
|
||||
def split_lines(content):
|
||||
return content.split('\n')
|
||||
|
||||
@@ -686,6 +742,15 @@ def tui_viewer(range_days, client):
|
||||
input_buf += key
|
||||
continue
|
||||
|
||||
# Mouse click — find task number
|
||||
if isinstance(key, tuple) and key[0] == 'CLICK':
|
||||
_, click_row, click_col = key
|
||||
task_id = find_task_at_click(click_row, click_col)
|
||||
if task_id:
|
||||
viewing_task = True
|
||||
load_task(task_id)
|
||||
continue
|
||||
|
||||
if key == 'q':
|
||||
break
|
||||
elif key == 'r':
|
||||
|
||||
Reference in New Issue
Block a user