▍ humdrum codex / ticktock v0.0.2

fix(grid): cursor marker follows active column; keep editable start/end last in add form

aae4bf89b081f84e73915e446559944da6c0b7e7
Kevin Kortum <kevinkortum@me.com> · 2026-07-07 22:04

parent bc8bdbcb

fix(grid): cursor marker follows active column; keep editable start/end last in add form

- when the LOGGED column is active the cursor > sits to its left instead of
  staying pinned in the far-left TIME gutter
- add form keeps start/end (prefilled from the selection) but places them last,
  so the cursor still lands on project while the times remain editable

2 files changed

internal/tui/form/form.go +4 −3
@@ -71,9 +71,8 @@ 	return huh.NewForm(huh.NewGroup(inputs(v)...))
 }
 
 // AddForm builds a form for a new entry whose start/end are already fixed by a
-// grid selection. It stores start/end/project into *v but shows only the
-// project/description/tags/note inputs, so the cursor lands on project instead
-// of tabbing through the pre-decided times.
+// grid selection. Start/end are prefilled and placed last so the cursor lands
+// on project, yet the times remain editable if the selection needs tweaking.
 func AddForm(start, end, project string, v *Values) *huh.Form {
 	*v = Values{Start: start, End: end, Project: project}
 	return huh.NewForm(huh.NewGroup(
@@ -81,5 +80,7 @@ 		huh.NewInput().Title("project").Value(&v.Project),
 		huh.NewInput().Title("description").Value(&v.Desc),
 		huh.NewInput().Title("tags (comma-sep)").Value(&v.Tags),
 		huh.NewInput().Title("note").Value(&v.Note),
+		huh.NewInput().Title("start (HH:MM)").Value(&v.Start),
+		huh.NewInput().Title("end (HH:MM)").Value(&v.End),
 	))
 }
internal/tui/grid/view.go +13 −8
@@ -53,10 +53,11 @@ 	}
 	return string(r[:w-1]) + "…"
 }
 
-// gridRow renders one line with a fixed 1-col gutter so every row's separators
-// align: [gutter] time(5) │ activity(actWidth) │ logged.
-func gridRow(gutter, timeStr, actCell, logCell string) string {
-	return fmt.Sprintf("%s %-5s │ %-*s │ %s", gutter, timeStr, actWidth, actCell, logCell)
+// gridRow renders one line with fixed 1-col gutters on both sides of the
+// dividers so separators align and the cursor marker can sit next to whichever
+// column is active: [gutterL] time(5) │ activity(actWidth) │[gutterR] logged.
+func gridRow(gutterL, timeStr, actCell, gutterR, logCell string) string {
+	return fmt.Sprintf("%s %-5s │ %-*s │%s %s", gutterL, timeStr, actWidth, actCell, gutterR, logCell)
 }
 
 // ruleLine draws the header underline with ┼ crossings at the │ columns.
@@ -83,7 +84,7 @@ 	}
 
 	var b strings.Builder
 	b.WriteString(gHeaderLine(m.date) + "\n\n")
-	b.WriteString(gridRow(" ", "TIME", "ACTIVITY", "LOGGED → tock") + "\n")
+	b.WriteString(gridRow(" ", "TIME", "ACTIVITY", " ", "LOGGED → tock") + "\n")
 	b.WriteString(ruleLine() + "\n")
 
 	lo, hi, selecting := m.selectionRange()
@@ -118,12 +119,16 @@ 				logged = gDim.Render(logged)
 			}
 		}
 
-		gutter := " "
+		gutterL, gutterR := " ", " "
 		if onCursor {
-			gutter = gCursor.Render(">")
+			if m.col == ColActivity {
+				gutterL = gCursor.Render(">")
+			} else {
+				gutterR = gCursor.Render(">")
+			}
 		}
 		timeStr := gDim.Render(minuteLabel(m.grid.SlotMinute(row)))
-		b.WriteString(gridRow(gutter, timeStr, act, logged) + "\n")
+		b.WriteString(gridRow(gutterL, timeStr, act, gutterR, logged) + "\n")
 	}
 
 	b.WriteString("\n")