Skip to content

Commit d37eef7

Browse files
Merge pull request #52 from TensorTemplar/fix-pager-for-narrow-terminals
fix pager counting for rich rendering
2 parents 61030ee + 24adf00 commit d37eef7

4 files changed

Lines changed: 73 additions & 12 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "slopometry"
7-
version = "2026.4.7"
7+
version = "2026.4.15"
88
description = "Opinionated code quality metrics for code agents and humans"
99
readme = "README.md"
1010
requires-python = ">=3.13"

src/slopometry/display/console.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,68 @@
11
"""Singleton Rich Console instance for consistent output across the application."""
22

3+
import logging
4+
import shutil
5+
import subprocess
6+
import sys
7+
38
from rich.console import Console
9+
from rich.pager import Pager
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
def _show_with_less(content: str) -> None:
15+
"""Page content through less -R, falling back to direct stdout on failure.
16+
17+
Uses -R so less correctly treats ANSI color codes as zero-width.
18+
Without it, less counts escape-code bytes as visual width, which in
19+
narrow terminals makes it miscalculate line positions and show (END)
20+
before all content is reachable.
21+
"""
22+
terminal_height = shutil.get_terminal_size().lines
23+
content_lines = content.count("\n")
24+
if content_lines <= terminal_height - 1:
25+
sys.stdout.write(content)
26+
sys.stdout.flush()
27+
return
28+
29+
try:
30+
proc = subprocess.Popen(
31+
["less", "-R"],
32+
stdin=subprocess.PIPE,
33+
errors="backslashreplace",
34+
)
35+
pipe = proc.stdin
36+
assert pipe is not None
37+
try:
38+
with pipe:
39+
try:
40+
pipe.write(content)
41+
except KeyboardInterrupt:
42+
pass
43+
except OSError:
44+
# Broken pipe: user quit less before all content was written
45+
logger.debug("Broken pipe writing to less (user quit early)")
46+
while True:
47+
try:
48+
proc.wait()
49+
break
50+
except KeyboardInterrupt:
51+
pass
52+
except FileNotFoundError:
53+
logger.debug("less not found, writing directly to stdout")
54+
sys.stdout.write(content)
55+
sys.stdout.flush()
56+
57+
58+
class _LessPager(Pager):
59+
"""Adapter to satisfy Rich's Pager protocol."""
60+
61+
def show(self, content: str) -> None:
62+
_show_with_less(content)
63+
64+
65+
styled_pager = _LessPager()
466

567
# Single console instance used throughout the application
668
# This ensures pager context works correctly across modules

src/slopometry/solo/cli/commands.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import click
88

9-
from slopometry.display.console import console
9+
from slopometry.display.console import console, styled_pager
1010

1111
if TYPE_CHECKING:
1212
from slopometry.core.models import ImpactAssessment, RepoBaseline, SessionStatistics
@@ -156,7 +156,7 @@ def list_sessions(limit: int, show_all: bool, pager: bool) -> None:
156156

157157
table = create_sessions_table(sessions_data)
158158
if pager:
159-
with console.pager(styles=True):
159+
with console.pager(pager=styled_pager, styles=True):
160160
console.print(table)
161161
else:
162162
console.print(table)
@@ -209,7 +209,7 @@ def _display() -> None:
209209
console.print(f"\n[dim]Analysis completed in {elapsed:.1f}s[/dim]")
210210

211211
if pager:
212-
with console.pager(styles=True):
212+
with console.pager(pager=styled_pager, styles=True):
213213
_display()
214214
else:
215215
_display()
@@ -304,7 +304,7 @@ def _display() -> None:
304304
console.print(f"\n[dim]Analysis completed in {elapsed:.1f}s[/dim]")
305305

306306
if pager:
307-
with console.pager(styles=True):
307+
with console.pager(pager=styled_pager, styles=True):
308308
_display()
309309
else:
310310
_display()
@@ -748,10 +748,9 @@ def save_transcript(session_id: str | None, output_dir: str, yes: bool) -> None:
748748
try:
749749
meta = json.loads(row["metadata"])
750750
model_id = meta.get("model_id")
751-
except (json.JSONDecodeError, ValueError):
752-
pass
751+
except (json.JSONDecodeError, ValueError) as e:
752+
logger.debug(f"Failed to parse MessageUpdated metadata for session {session_id}: {e}")
753753

754-
# Extract opencode_version from Stop event metadata
755754
stop_row = conn.execute(
756755
"""
757756
SELECT metadata FROM hook_events
@@ -764,8 +763,8 @@ def save_transcript(session_id: str | None, output_dir: str, yes: bool) -> None:
764763
try:
765764
stop_meta = json.loads(stop_row["metadata"])
766765
opencode_version = stop_meta.get("opencode_version")
767-
except (json.JSONDecodeError, ValueError):
768-
pass
766+
except (json.JSONDecodeError, ValueError) as e:
767+
logger.debug(f"Failed to parse Stop event metadata for session {session_id}: {e}")
769768

770769
metadata = SessionMetadata(
771770
session_id=stats.session_id,

src/slopometry/summoner/cli/commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import click
1111
from click.shell_completion import CompletionItem
1212

13-
from slopometry.display.console import console
13+
from slopometry.display.console import console, styled_pager
1414

1515
# Imports moved inside functions to optimize startup time
1616

@@ -547,7 +547,7 @@ def current_impact(
547547
summary = CurrentImpactSummary.from_analysis(analysis)
548548
print(summary.model_dump_json(indent=2))
549549
elif pager:
550-
with console.pager(styles=True):
550+
with console.pager(pager=styled_pager, styles=True):
551551
display_current_impact_analysis(
552552
analysis, show_file_details=file_details, behavioral_trends=behavioral_trends
553553
)

0 commit comments

Comments
 (0)