Skip to content

Commit 6fdc566

Browse files
cpcloudcursoragent
andcommitted
fix(calendar): preserve day column alignment when centering grid
lipgloss.PlaceHorizontal centers each line independently, which shifts short last rows (e.g. "29 30" in Nov 2026) into wrong columns. Fix by padding all grid lines to uniform width first via padLines(), then indenting the whole block with a uniform left margin. Added TestCalendarGridColumnAlignment to verify Nov 2026 day 29 (Sun) and day 30 (Mon) align with their respective column headers. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f6ee6b9 commit 6fdc566

File tree

2 files changed

+96
-7
lines changed

2 files changed

+96
-7
lines changed

internal/app/calendar.go

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,27 +84,47 @@ func calendarGrid(cal calendarState, styles Styles) string {
8484
hintStyle.Render("enter pick · esc cancel"),
8585
)
8686

87-
// The grid is 20 chars wide ("Su Mo Tu We Th Fr Sa"); center everything
88-
// to the wider of the grid and hints so the box looks balanced.
87+
// The day-label row defines the grid's intrinsic width (20 visible cols).
88+
// Pad every grid line to that width so the block is rectangular, then
89+
// center the whole block within the wider hint line.
8990
calW := lipgloss.Width(dayLabels)
91+
gridBlock := padLines(grid.String(), calW)
92+
9093
hintW := lipgloss.Width(hints)
9194
boxW := calW
9295
if hintW > boxW {
9396
boxW = hintW
9497
}
9598

96-
center := func(s string) string {
99+
// centerBlock centers a pre-padded rectangular block (all lines same
100+
// width) by indenting every line uniformly. Unlike PlaceHorizontal this
101+
// preserves internal column alignment.
102+
centerBlock := func(s string, blockW int) string {
103+
pad := (boxW - blockW) / 2
104+
if pad <= 0 {
105+
return s
106+
}
107+
indent := strings.Repeat(" ", pad)
108+
lines := strings.Split(s, "\n")
109+
for i, line := range lines {
110+
lines[i] = indent + line
111+
}
112+
return strings.Join(lines, "\n")
113+
}
114+
115+
// Single-line items can use PlaceHorizontal safely.
116+
centerLine := func(s string) string {
97117
return lipgloss.PlaceHorizontal(boxW, lipgloss.Center, s)
98118
}
99119

100120
return lipgloss.JoinVertical(
101121
lipgloss.Left,
102-
center(header),
122+
centerLine(header),
103123
"",
104-
center(dayLabels),
105-
center(grid.String()),
124+
centerBlock(dayLabels, calW),
125+
centerBlock(gridBlock, calW),
106126
"",
107-
center(hints),
127+
centerLine(hints),
108128
)
109129
}
110130

@@ -116,6 +136,19 @@ func sameDay(a, b time.Time) bool {
116136
return a.Year() == b.Year() && a.Month() == b.Month() && a.Day() == b.Day()
117137
}
118138

139+
// padLines right-pads each line in s so every line is exactly width visible
140+
// columns. This makes the block rectangular so uniform indentation preserves
141+
// internal column alignment.
142+
func padLines(s string, width int) string {
143+
lines := strings.Split(s, "\n")
144+
for i, line := range lines {
145+
if w := lipgloss.Width(line); w < width {
146+
lines[i] = line + strings.Repeat(" ", width-w)
147+
}
148+
}
149+
return strings.Join(lines, "\n")
150+
}
151+
119152
// calendarMove adjusts the calendar cursor by the given number of days.
120153
func calendarMove(cal *calendarState, days int) {
121154
cal.Cursor = cal.Cursor.AddDate(0, 0, days)

internal/app/calendar_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,62 @@ func TestCalendarYearNavigation(t *testing.T) {
242242
}
243243
}
244244

245+
func TestCalendarGridColumnAlignment(t *testing.T) {
246+
// Nov 2026 starts on Sunday, so days 29 (Mon) and 30 (Tue) are on the
247+
// last row. They must appear in the Mon and Tue columns, not shifted by
248+
// centering logic.
249+
styles := DefaultStyles()
250+
cal := calendarState{
251+
Cursor: time.Date(2026, 11, 1, 0, 0, 0, 0, time.Local),
252+
HasValue: false,
253+
}
254+
grid := calendarGrid(cal, styles)
255+
256+
// Find the day-label line and the last-row line.
257+
lines := strings.Split(grid, "\n")
258+
labelIdx := -1
259+
lastDayIdx := -1
260+
for i, line := range lines {
261+
if strings.Contains(line, "Su Mo Tu We Th Fr Sa") {
262+
labelIdx = i
263+
}
264+
if strings.Contains(line, "29") && strings.Contains(line, "30") {
265+
lastDayIdx = i
266+
}
267+
}
268+
if labelIdx < 0 {
269+
t.Fatal("day-label line not found")
270+
}
271+
if lastDayIdx < 0 {
272+
t.Fatal("line with 29 and 30 not found")
273+
}
274+
275+
// Nov 1, 2026 is a Sunday, so day 29 = Sunday ("Su" column) and
276+
// day 30 = Monday ("Mo" column). Verify both align correctly.
277+
suPos := strings.Index(lines[labelIdx], "Su")
278+
moPos := strings.Index(lines[labelIdx], "Mo")
279+
pos29 := strings.Index(lines[lastDayIdx], "29")
280+
pos30 := strings.Index(lines[lastDayIdx], "30")
281+
if suPos < 0 || pos29 < 0 {
282+
t.Fatalf("could not find Su (%d) or 29 (%d)", suPos, pos29)
283+
}
284+
if suPos != pos29 {
285+
t.Errorf(
286+
"column misalignment: Su at col %d but 29 at col %d\nlabels: %q\nlast: %q",
287+
suPos, pos29, lines[labelIdx], lines[lastDayIdx],
288+
)
289+
}
290+
if moPos < 0 || pos30 < 0 {
291+
t.Fatalf("could not find Mo (%d) or 30 (%d)", moPos, pos30)
292+
}
293+
if moPos != pos30 {
294+
t.Errorf(
295+
"column misalignment: Mo at col %d but 30 at col %d\nlabels: %q\nlast: %q",
296+
moPos, pos30, lines[labelIdx], lines[lastDayIdx],
297+
)
298+
}
299+
}
300+
245301
func TestOpenCalendarWithEmptyValue(t *testing.T) {
246302
m := newTestModel()
247303
dateVal := ""

0 commit comments

Comments
 (0)