fix mascot
This commit is contained in:
14
README.md
14
README.md
@@ -304,13 +304,13 @@ showing every repeated `COMMENTED` event.
|
||||
`viewer_label = "login"` shows your GitHub username like every other author.
|
||||
Set it to `"you"` to replace your username with `@you` throughout the UI.
|
||||
|
||||
Difflet is disabled by default. Set `mascot = true` to keep it visible to the
|
||||
right next to the active view's naturally sized header, separated by a
|
||||
small gap. On normal terminal widths Difflet is centered horizontally and the
|
||||
header uses the space to its left. When centering would make the header too
|
||||
narrow, Difflet falls back to a small right-edge inset. Header information
|
||||
wraps when the combined header and mascot do not fit. The layout adds only the
|
||||
vertical rows required to display the four-line mascot.
|
||||
Difflet is disabled by default. Set `mascot = true` to show it on the pull
|
||||
request picker, dashboard, and thread screens. On the dashboard it is centered
|
||||
beside the first metadata rows so it does not add whitespace below the pull
|
||||
request title. Editor and popup views hide it to preserve their full usable
|
||||
height. On other supported screens Difflet sits to the right of the naturally
|
||||
sized header. Header information wraps when the combined header and mascot do
|
||||
not fit.
|
||||
`mascot_animated` controls brief loading, blink, success, and error motion
|
||||
independently from `mascot_expressive`, which permits stronger emotional
|
||||
faces. Disabling animation leaves the appropriate final state visible.
|
||||
|
||||
167
difflet_test.go
167
difflet_test.go
@@ -196,6 +196,173 @@ func TestDiffletDisabledPreservesViewExactly(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiffletDashboardIsCenteredBesideMetadata(t *testing.T) {
|
||||
app := NewAppWithSettings(
|
||||
&recordingService{}, "owner", "repository", false, 10, 10,
|
||||
AppSettings{Mascot: true},
|
||||
)
|
||||
app.screen = dashboardScreen
|
||||
app.loading = false
|
||||
app.details = PRDetails{
|
||||
PullRequest: PullRequest{
|
||||
RepoWithOwner: "owner/repository", Number: 42,
|
||||
Title: "A useful title", Author: "alice",
|
||||
},
|
||||
HeadRef: "feature", BaseRef: "main",
|
||||
}
|
||||
updated, _ := app.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
|
||||
app = updated.(App)
|
||||
|
||||
rendered := strings.Split(app.View(), "\n")
|
||||
headerHeight := len(app.dashboardHeaderLines())
|
||||
if strings.TrimSpace(ansi.Strip(rendered[headerHeight])) == "" {
|
||||
t.Fatal("dashboard left a blank row between its title and metadata")
|
||||
}
|
||||
for row, mascotLine := range (DiffletFrame{Expression: DiffletIdle}).lines() {
|
||||
plain := ansi.Strip(rendered[headerHeight+row])
|
||||
mascotText := strings.TrimRight(mascotLine, " ")
|
||||
mascotIndex := strings.Index(plain, mascotText)
|
||||
left := -1
|
||||
if mascotIndex >= 0 {
|
||||
left = lipgloss.Width(plain[:mascotIndex])
|
||||
}
|
||||
if left != (app.width-diffletWidth)/2 {
|
||||
t.Fatalf("row %d mascot starts at %d, want centered position %d: %q",
|
||||
row, left, (app.width-diffletWidth)/2, plain)
|
||||
}
|
||||
}
|
||||
for row, label := range []string{"author", "branches", "review"} {
|
||||
if !strings.Contains(ansi.Strip(rendered[headerHeight+row]), label) {
|
||||
t.Fatalf("dashboard row %d does not place %q beside mascot: %q",
|
||||
row, label, ansi.Strip(rendered[headerHeight+row]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiffletIsHiddenInEditorAndPopups(t *testing.T) {
|
||||
app := NewAppWithSettings(
|
||||
&recordingService{}, "owner", "repository", false, 10, 10,
|
||||
AppSettings{Mascot: true},
|
||||
)
|
||||
app.screen = dashboardScreen
|
||||
app.loading = false
|
||||
app.details = PRDetails{PullRequest: PullRequest{
|
||||
RepoWithOwner: "owner/repository", Number: 42, Title: "Title",
|
||||
}}
|
||||
updated, _ := app.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
|
||||
app = updated.(App)
|
||||
|
||||
for _, mode := range []writeMode{writePREdit, writeReplyConfirm} {
|
||||
app.writeMode = mode
|
||||
if got, want := app.View(), app.viewContent(); got != want {
|
||||
t.Fatalf("write mode %d changed by enabled mascot:\ngot:\n%q\nwant:\n%q",
|
||||
mode, got, want)
|
||||
}
|
||||
if strings.Contains(ansi.Strip(app.View()), "▄███████▄") {
|
||||
t.Fatalf("write mode %d displayed the mascot", mode)
|
||||
}
|
||||
}
|
||||
app.writeMode = writeNone
|
||||
app.helpVisible = true
|
||||
if got, want := app.View(), app.viewContent(); got != want {
|
||||
t.Fatalf("help popup changed by enabled mascot:\ngot:\n%q\nwant:\n%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiffletEnabledEditorCanRevealLastDescriptionRow(t *testing.T) {
|
||||
app := NewAppWithSettings(
|
||||
&recordingPRService{}, "owner", "repository", false, 10, 10,
|
||||
AppSettings{Mascot: true},
|
||||
)
|
||||
app.screen = dashboardScreen
|
||||
app.loading = false
|
||||
app.width, app.height = 50, 12
|
||||
app.details = PRDetails{
|
||||
PullRequest: PullRequest{
|
||||
ID: "pr", RepoWithOwner: "owner/repository", Number: 42,
|
||||
Title: "Title",
|
||||
},
|
||||
BaseRef: "main",
|
||||
Body: strings.Repeat("description row\n", 20) + "LAST DESCRIPTION ROW",
|
||||
Permissions: ViewerPermissions{
|
||||
CanUpdatePR: true,
|
||||
},
|
||||
}
|
||||
app.startPREdit()
|
||||
app.prEditEditors[prEditBodyField].Cursor =
|
||||
len([]rune(app.prEditEditors[prEditBodyField].Text))
|
||||
app.ensurePREditCursorVisible()
|
||||
|
||||
rendered := ansi.Strip(app.View())
|
||||
if !strings.Contains(rendered, "LAST DESCRIPTION ROW") {
|
||||
t.Fatalf("last description row is outside the editor viewport:\n%s", rendered)
|
||||
}
|
||||
if strings.Contains(rendered, "▄███████▄") {
|
||||
t.Fatal("editor displayed the mascot instead of using its full height")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardDiffletRemainsCenteredAtNarrowWidths(t *testing.T) {
|
||||
mascot := (DiffletFrame{Expression: DiffletIdle}).lines()
|
||||
metadata := []string{"author", "branches", "review", "checks"}
|
||||
for width := diffletWidth; width < 20; width++ {
|
||||
rendered := renderDashboardMetadataWithDifflet(metadata, mascot, width)
|
||||
for row, mascotLine := range mascot {
|
||||
mascotText := strings.TrimRight(mascotLine, " ")
|
||||
var mascotRow string
|
||||
for _, line := range rendered {
|
||||
if strings.Contains(line, mascotText) {
|
||||
mascotRow = line
|
||||
break
|
||||
}
|
||||
}
|
||||
index := strings.Index(mascotRow, mascotText)
|
||||
left := -1
|
||||
if index >= 0 {
|
||||
left = lipgloss.Width(mascotRow[:index])
|
||||
}
|
||||
if want := max(0, (width-diffletWidth)/2); left != want {
|
||||
t.Fatalf("width %d row %d mascot starts at %d, want %d: %q",
|
||||
width, row, left, want, mascotRow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardLoadingDiffletIsCentered(t *testing.T) {
|
||||
app := NewAppWithSettings(
|
||||
&recordingService{}, "owner", "repository", false, 10, 10,
|
||||
AppSettings{Mascot: true},
|
||||
)
|
||||
app.screen = dashboardScreen
|
||||
app.loading = true
|
||||
app.details = PRDetails{PullRequest: PullRequest{
|
||||
RepoWithOwner: "owner/repository", Number: 42, Title: "Title",
|
||||
}}
|
||||
updated, _ := app.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
|
||||
app = updated.(App)
|
||||
|
||||
rendered := strings.Split(app.View(), "\n")
|
||||
mascotLine := strings.TrimRight((DiffletFrame{Expression: DiffletIdle}).lines()[0], " ")
|
||||
for row, line := range rendered {
|
||||
plain := ansi.Strip(line)
|
||||
index := strings.Index(plain, mascotLine)
|
||||
if index < 0 {
|
||||
continue
|
||||
}
|
||||
if left := lipgloss.Width(plain[:index]); left != (app.width-diffletWidth)/2 {
|
||||
t.Fatalf("loading mascot starts at %d, want %d: %q",
|
||||
left, (app.width-diffletWidth)/2, plain)
|
||||
}
|
||||
if row != len(app.dashboardHeaderLines()) {
|
||||
t.Fatalf("loading mascot begins on row %d, want %d",
|
||||
row, len(app.dashboardHeaderLines()))
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatal("loading dashboard did not display the mascot")
|
||||
}
|
||||
|
||||
func TestDiffletHeaderMeasurementMatchesRenderedHeader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
91
tui.go
91
tui.go
@@ -1874,7 +1874,7 @@ func (m App) dashboardViewportHeight() int {
|
||||
}
|
||||
|
||||
func (m App) dashboardMaxScroll() int {
|
||||
return max(0, len(m.dashboardLines())-m.dashboardViewportHeight())
|
||||
return max(0, len(m.dashboardDisplayLines())-m.dashboardViewportHeight())
|
||||
}
|
||||
|
||||
func (m App) detailPaneSize() (int, int) {
|
||||
@@ -1924,6 +1924,12 @@ func (m App) View() string {
|
||||
if len(mascot) != diffletHeight {
|
||||
return m.viewContent()
|
||||
}
|
||||
if m.diffletHiddenForCurrentView() {
|
||||
return m.viewContent()
|
||||
}
|
||||
if m.screen == dashboardScreen {
|
||||
return m.viewDashboardWithLines(m.dashboardLinesWithDifflet(mascot))
|
||||
}
|
||||
gap := diffletGap
|
||||
headerWidth := diffletHeaderWidth(m.width)
|
||||
if headerWidth < 1 {
|
||||
@@ -1959,11 +1965,15 @@ func (m App) View() string {
|
||||
)
|
||||
}
|
||||
|
||||
func (m App) diffletHeaderLineCount() (int, bool) {
|
||||
if m.helpVisible ||
|
||||
func (m App) diffletHiddenForCurrentView() bool {
|
||||
return m.helpVisible ||
|
||||
(m.aiMode != aiNone && m.aiMode != aiDiscussion) ||
|
||||
(m.writeMode != writeNone && m.writeMode != writeReply && m.writeMode != writePREdit) ||
|
||||
m.screen == healthScreen {
|
||||
(m.writeMode != writeNone && m.writeMode != writeReply) ||
|
||||
m.screen == healthScreen
|
||||
}
|
||||
|
||||
func (m App) diffletHeaderLineCount() (int, bool) {
|
||||
if m.diffletHiddenForCurrentView() {
|
||||
return 0, false
|
||||
}
|
||||
switch m.screen {
|
||||
@@ -1973,9 +1983,6 @@ func (m App) diffletHeaderLineCount() (int, bool) {
|
||||
if m.scroll > 0 {
|
||||
return 0, false
|
||||
}
|
||||
if m.writeMode == writePREdit {
|
||||
return 1, true
|
||||
}
|
||||
return len(m.dashboardHeaderLines()), true
|
||||
case threadScreen:
|
||||
return len(m.threadTopLines()), true
|
||||
@@ -2563,7 +2570,10 @@ func groupedPRRows(prs []PullRequest, selected int) ([]prListRow, int) {
|
||||
}
|
||||
|
||||
func (m App) viewDashboard() string {
|
||||
lines := m.dashboardLines()
|
||||
return m.viewDashboardWithLines(m.dashboardLines())
|
||||
}
|
||||
|
||||
func (m App) viewDashboardWithLines(lines []string) string {
|
||||
viewportHeight := m.dashboardViewportHeight()
|
||||
maxScroll := max(0, len(lines)-viewportHeight)
|
||||
scroll := min(m.scroll, maxScroll)
|
||||
@@ -2594,6 +2604,69 @@ func (m App) viewDashboard() string {
|
||||
return view
|
||||
}
|
||||
|
||||
func (m App) dashboardDisplayLines() []string {
|
||||
mascot := m.difflet.frameLines()
|
||||
if len(mascot) == diffletHeight && !m.diffletHiddenForCurrentView() {
|
||||
return m.dashboardLinesWithDifflet(mascot)
|
||||
}
|
||||
return m.dashboardLines()
|
||||
}
|
||||
|
||||
func (m App) dashboardLinesWithDifflet(mascot []string) []string {
|
||||
lines := m.dashboardLines()
|
||||
headerLines := len(m.dashboardHeaderLines())
|
||||
metadataStart := headerLines + 1
|
||||
if headerLines >= len(lines) ||
|
||||
strings.TrimSpace(ansi.Strip(lines[headerLines])) != "" {
|
||||
return lines
|
||||
}
|
||||
if len(lines) < metadataStart+diffletHeight {
|
||||
result := append([]string(nil), lines[:headerLines]...)
|
||||
result = append(result, centeredDiffletLines(mascot, m.width)...)
|
||||
return append(result, lines[metadataStart:]...)
|
||||
}
|
||||
band := renderDashboardMetadataWithDifflet(
|
||||
lines[metadataStart:metadataStart+diffletHeight],
|
||||
mascot,
|
||||
m.width,
|
||||
)
|
||||
result := append([]string(nil), lines[:headerLines]...)
|
||||
result = append(result, band...)
|
||||
return append(result, lines[metadataStart+diffletHeight:]...)
|
||||
}
|
||||
|
||||
func renderDashboardMetadataWithDifflet(metadata, mascot []string, width int) []string {
|
||||
mascotLeft := max(0, (width-diffletWidth)/2)
|
||||
if width <= diffletWidth || mascotLeft < diffletGap {
|
||||
centered := centeredDiffletLines(mascot, width)
|
||||
return append(centered, metadata...)
|
||||
}
|
||||
metadataWidth := max(1, mascotLeft-diffletGap)
|
||||
height := max(len(metadata), len(mascot))
|
||||
rendered := make([]string, 0, height)
|
||||
for row := range height {
|
||||
left, right := "", ""
|
||||
if row < len(metadata) {
|
||||
left = ansi.Truncate(metadata[row], metadataWidth, "…")
|
||||
}
|
||||
if row < len(mascot) {
|
||||
right = mascot[row]
|
||||
}
|
||||
rendered = append(rendered, pad(left, mascotLeft)+right)
|
||||
}
|
||||
return rendered
|
||||
}
|
||||
|
||||
func centeredDiffletLines(mascot []string, width int) []string {
|
||||
centered := make([]string, 0, len(mascot))
|
||||
for _, line := range mascot {
|
||||
centered = append(centered, lipgloss.PlaceHorizontal(
|
||||
width, lipgloss.Center, line,
|
||||
))
|
||||
}
|
||||
return centered
|
||||
}
|
||||
|
||||
func (m App) viewHealth() string {
|
||||
lines := m.healthLines()
|
||||
viewportHeight := m.healthViewportHeight()
|
||||
|
||||
Reference in New Issue
Block a user