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.
|
`viewer_label = "login"` shows your GitHub username like every other author.
|
||||||
Set it to `"you"` to replace your username with `@you` throughout the UI.
|
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
|
Difflet is disabled by default. Set `mascot = true` to show it on the pull
|
||||||
right next to the active view's naturally sized header, separated by a
|
request picker, dashboard, and thread screens. On the dashboard it is centered
|
||||||
small gap. On normal terminal widths Difflet is centered horizontally and the
|
beside the first metadata rows so it does not add whitespace below the pull
|
||||||
header uses the space to its left. When centering would make the header too
|
request title. Editor and popup views hide it to preserve their full usable
|
||||||
narrow, Difflet falls back to a small right-edge inset. Header information
|
height. On other supported screens Difflet sits to the right of the naturally
|
||||||
wraps when the combined header and mascot do not fit. The layout adds only the
|
sized header. Header information wraps when the combined header and mascot do
|
||||||
vertical rows required to display the four-line mascot.
|
not fit.
|
||||||
`mascot_animated` controls brief loading, blink, success, and error motion
|
`mascot_animated` controls brief loading, blink, success, and error motion
|
||||||
independently from `mascot_expressive`, which permits stronger emotional
|
independently from `mascot_expressive`, which permits stronger emotional
|
||||||
faces. Disabling animation leaves the appropriate final state visible.
|
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) {
|
func TestDiffletHeaderMeasurementMatchesRenderedHeader(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
91
tui.go
91
tui.go
@@ -1874,7 +1874,7 @@ func (m App) dashboardViewportHeight() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m App) dashboardMaxScroll() 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) {
|
func (m App) detailPaneSize() (int, int) {
|
||||||
@@ -1924,6 +1924,12 @@ func (m App) View() string {
|
|||||||
if len(mascot) != diffletHeight {
|
if len(mascot) != diffletHeight {
|
||||||
return m.viewContent()
|
return m.viewContent()
|
||||||
}
|
}
|
||||||
|
if m.diffletHiddenForCurrentView() {
|
||||||
|
return m.viewContent()
|
||||||
|
}
|
||||||
|
if m.screen == dashboardScreen {
|
||||||
|
return m.viewDashboardWithLines(m.dashboardLinesWithDifflet(mascot))
|
||||||
|
}
|
||||||
gap := diffletGap
|
gap := diffletGap
|
||||||
headerWidth := diffletHeaderWidth(m.width)
|
headerWidth := diffletHeaderWidth(m.width)
|
||||||
if headerWidth < 1 {
|
if headerWidth < 1 {
|
||||||
@@ -1959,11 +1965,15 @@ func (m App) View() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m App) diffletHeaderLineCount() (int, bool) {
|
func (m App) diffletHiddenForCurrentView() bool {
|
||||||
if m.helpVisible ||
|
return m.helpVisible ||
|
||||||
(m.aiMode != aiNone && m.aiMode != aiDiscussion) ||
|
(m.aiMode != aiNone && m.aiMode != aiDiscussion) ||
|
||||||
(m.writeMode != writeNone && m.writeMode != writeReply && m.writeMode != writePREdit) ||
|
(m.writeMode != writeNone && m.writeMode != writeReply) ||
|
||||||
m.screen == healthScreen {
|
m.screen == healthScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m App) diffletHeaderLineCount() (int, bool) {
|
||||||
|
if m.diffletHiddenForCurrentView() {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
switch m.screen {
|
switch m.screen {
|
||||||
@@ -1973,9 +1983,6 @@ func (m App) diffletHeaderLineCount() (int, bool) {
|
|||||||
if m.scroll > 0 {
|
if m.scroll > 0 {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
if m.writeMode == writePREdit {
|
|
||||||
return 1, true
|
|
||||||
}
|
|
||||||
return len(m.dashboardHeaderLines()), true
|
return len(m.dashboardHeaderLines()), true
|
||||||
case threadScreen:
|
case threadScreen:
|
||||||
return len(m.threadTopLines()), true
|
return len(m.threadTopLines()), true
|
||||||
@@ -2563,7 +2570,10 @@ func groupedPRRows(prs []PullRequest, selected int) ([]prListRow, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m App) viewDashboard() string {
|
func (m App) viewDashboard() string {
|
||||||
lines := m.dashboardLines()
|
return m.viewDashboardWithLines(m.dashboardLines())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m App) viewDashboardWithLines(lines []string) string {
|
||||||
viewportHeight := m.dashboardViewportHeight()
|
viewportHeight := m.dashboardViewportHeight()
|
||||||
maxScroll := max(0, len(lines)-viewportHeight)
|
maxScroll := max(0, len(lines)-viewportHeight)
|
||||||
scroll := min(m.scroll, maxScroll)
|
scroll := min(m.scroll, maxScroll)
|
||||||
@@ -2594,6 +2604,69 @@ func (m App) viewDashboard() string {
|
|||||||
return view
|
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 {
|
func (m App) viewHealth() string {
|
||||||
lines := m.healthLines()
|
lines := m.healthLines()
|
||||||
viewportHeight := m.healthViewportHeight()
|
viewportHeight := m.healthViewportHeight()
|
||||||
|
|||||||
Reference in New Issue
Block a user