Library: cut long titles to the width of the list

Long titles made the movies and series tables wider than their pane, so
they scrolled sideways and status and size left the view. The title
column is now cut on every draw to what the other columns leave, ending
in an ellipsis (done by hand: tview only draws its ellipsis for untagged
text, and every title starts with the colored monitored mark). Titles are
now escaped, so square brackets in them are not taken as color tags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
This commit is contained in:
2026-10-21 14:26:09 +02:00
co-authored by Claude Opus 5.5
parent 12e5d5aa33
commit 15f7fc7ee8
3 changed files with 128 additions and 2 deletions
+74 -2
View File
@@ -249,7 +249,7 @@ func newLibraryView(app *App, r *radarr.Client, s *sonarr.Client) *libraryView {
})
p.table.SetBlurFunc(func() { p.table.SetBorderColor(tview.Styles.BorderColor) })
v.panes[kind] = p
lists.AddItem(p.table, 0, 1, kind == kindMovies)
lists.AddItem(fitTitles{p.table}, 0, 1, kind == kindMovies)
}
v.root = tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(v.header, 1, 0, false).
@@ -401,7 +401,8 @@ func (v *libraryView) renderPane(p *libPane) {
header(p.table, 0, "Title", "Status", "Size")
selRow := 0
for i, it := range p.shown {
p.table.SetCell(i+1, 0, tview.NewTableCell(monMark(it.monitored)+" "+it.label).SetExpansion(1))
title := titleCell{prefix: monMark(it.monitored) + " ", title: it.label}
p.table.SetCell(i+1, 0, tview.NewTableCell(title.fit(0)).SetExpansion(1).SetReference(title))
p.table.SetCell(i+1, 1, tview.NewTableCell(it.status))
p.table.SetCell(i+1, 2, tview.NewTableCell(it.size).SetAlign(tview.AlignRight))
if it.id == selID {
@@ -444,6 +445,77 @@ func (v *libraryView) renderPane(p *libPane) {
p.table.SetTitle(title + " ")
}
// fitTitles draws a table whose first column (the title) is cut to the width
// the other columns leave, with an ellipsis, so long titles never make the
// list scroll sideways. The cut is worked out on every draw, so it follows
// the window size.
type fitTitles struct{ *tview.Table }
func (f fitTitles) Draw(screen tcell.Screen) {
fitFirstColumn(f.Table)
f.Table.Draw(screen)
}
// fitFirstColumn cuts the title cells (those with a titleCell reference) to
// the width the table leaves after the other columns and their separators.
// The cut is done here rather than with the cell's maximum width, because tview
// only marks a cut with an ellipsis for text without color tags, and every
// title starts with the colored monitored mark.
func fitFirstColumn(t *tview.Table) {
_, _, width, _ := t.GetInnerRect()
rows, cols := t.GetRowCount(), t.GetColumnCount()
rest := 0
for c := 1; c < cols; c++ {
w := 0
for r := 0; r < rows; r++ {
if cell := t.GetCell(r, c); cell != nil {
w = max(w, tview.TaggedStringWidth(cell.Text))
}
}
rest += w + 1 // the column and the separator before it
}
limit := max(width-rest, 8)
for r := 0; r < rows; r++ {
cell := t.GetCell(r, 0)
if title, ok := cell.GetReference().(titleCell); ok {
cell.SetText(title.fit(limit))
}
}
}
// titleCell is the text of a title cell: a tagged prefix (the monitored mark)
// and the plain title, kept whole so it can be cut again for another width.
type titleCell struct{ prefix, title string }
// fit returns the cell text at most width columns wide, the title cut with an
// ellipsis if needed; width 0 means no limit.
func (c titleCell) fit(width int) string {
title := c.title
if width > 0 {
title = cutToWidth(title, width-tview.TaggedStringWidth(c.prefix))
}
return c.prefix + tview.Escape(title)
}
// cutToWidth shortens s to at most w screen columns, ending with "…" when
// anything was cut.
func cutToWidth(s string, w int) string {
width := func(s string) int { return tview.TaggedStringWidth(tview.Escape(s)) }
if width(s) <= w {
return s
}
if w < 1 {
return ""
}
runes := []rune(s)
for n := len(runes); n > 0; n-- {
if cut := string(runes[:n]) + "…"; width(cut) <= w {
return cut
}
}
return "…"
}
func containsItem(items []libItem, id int) bool {
for _, it := range items {
if it.id == id {