Work workspace with runs, change and facts tabs; vim keys; responsive Board; theme file

This commit is contained in:
Sam
2026-11-10 14:00:58 +11:00
parent 1bfb9b05ed
commit 4e3c955350
7 changed files with 416 additions and 20 deletions

View File

@@ -130,13 +130,13 @@ func (m boardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "r":
m.loading = true
return m, m.reload()
case "up":
case "up", "k":
if m.cursor > 0 {
m.cursor--
return m, m.loadDetail(m.selectedID())
}
return m, nil
case "down":
case "down", "j":
if m.cursor < len(m.rows)-1 {
m.cursor++
return m, m.loadDetail(m.selectedID())
@@ -166,12 +166,12 @@ func (m boardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.notice = "showing " + r.Issue.Identifier + " in every workspace"
}
return m, nil
case "[":
case "[", "h":
if m.focus == focusNeeds && m.needsTab > 0 {
m.needsTab--
}
return m, nil
case "]":
case "]", "l":
if m.focus == focusNeeds && m.needsTab < 1 {
m.needsTab++
}
@@ -230,12 +230,36 @@ func (m boardModel) View() string {
footer := m.footerLine()
bodyHeight := m.height - 2
// Three ways to use the same width, chosen by what the terminal actually
// has. Wide screens get the three panels side by side; narrow ones stack.
if m.width >= 140 {
treeW := max(34, m.width*30/100)
sumW := max(38, m.width*38/100)
needsW := max(30, m.width-treeW-sumW-2)
left := panel{title: "tasks", width: treeW, height: bodyHeight, focused: m.focus == focusTree}
mid := panel{title: "the chosen task", width: sumW, height: bodyHeight, focused: m.focus == focusSummary}
rightP := panel{
title: "needs you", width: needsW, height: bodyHeight,
tabs: []string{"decisions", "activity"}, active: m.needsTab, focused: m.focus == focusNeeds,
}
body := m.needsLines(needsW-3, bodyHeight-3)
if m.needsTab == 1 {
body = m.activityLines(needsW-3, bodyHeight-3)
}
page := lipgloss.JoinHorizontal(lipgloss.Top,
left.render(m.treeLines(treeW-3)), " ",
mid.render(m.summaryLines(sumW-3)), " ",
rightP.render(body))
return strings.Join([]string{header, page, footer}, "\n")
}
leftWidth := max(34, m.width*40/100)
rightWidth := max(30, m.width-leftWidth-1)
// Left: the task tree.
tree := panel{title: "tasks", width: leftWidth, height: bodyHeight, focused: m.focus == focusTree}
treeLines := m.treeLines(leftWidth - 2)
treeLines := m.treeLines(leftWidth - 3)
left := tree.render(treeLines)
// Right, upper: the summary of the chosen task.
@@ -247,13 +271,13 @@ func (m boardModel) View() string {
title: "needs you", width: rightWidth, height: needsHeight,
tabs: []string{"decisions", "activity"}, active: m.needsTab, focused: m.focus == focusNeeds,
}
needsBody := m.needsLines(rightWidth-2, needsHeight-2)
needsBody := m.needsLines(rightWidth-3, needsHeight-3)
if m.needsTab == 1 {
needsBody = m.activityLines(rightWidth-2, needsHeight-2)
needsBody = m.activityLines(rightWidth-3, needsHeight-3)
}
right := lipgloss.JoinVertical(lipgloss.Left,
sum.render(m.summaryLines(rightWidth-2)),
sum.render(m.summaryLines(rightWidth-3)),
needs.render(needsBody),
)
@@ -286,7 +310,7 @@ func (m boardModel) footerLine() string {
switch m.focus {
case focusTree:
return hintLine([]string{
keyHint("↑↓", "choose a task"),
keyHint("↑↓ or j k", "choose a task"),
keyHint("enter", "show it everywhere"),
keyHint("a", "assign"),
keyHint("tab", "next panel"),
@@ -295,7 +319,7 @@ func (m boardModel) footerLine() string {
}, m.width)
case focusSummary:
return hintLine([]string{
keyHint("↑↓", "choose a task"),
keyHint("↑↓ or j k", "choose a task"),
keyHint("tab", "next panel"),
keyHint("r", "read again"),
keyHint("q", "leave"),
@@ -328,7 +352,7 @@ func (m boardModel) treeLines(width int) []string {
if len(group) == 0 {
continue
}
out = append(out, groupHeading(fmt.Sprintf("%s %d", strings.ToUpper(g), len(group))))
out = append(out, groupHeading(g, len(group), width))
for _, r := range group {
out = append(out, m.taskLine(r, index, width)...)
index++
@@ -338,8 +362,13 @@ func (m boardModel) treeLines(width int) []string {
return out
}
func groupHeading(s string) string {
return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(active.Mauve)).Render(s)
// groupHeading is a label, a rule, and how many tasks are under it. The rule
// makes the groups visible at a glance without a box around every one.
func groupHeading(name string, count, width int) string {
label := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(active.Mauve)).Render(strings.ToUpper(name))
n := lipgloss.NewStyle().Foreground(lipgloss.Color(active.Muted)).Render(fmt.Sprintf(" %d ", count))
rule := strings.Repeat("─", max(0, width-lipgloss.Width(label)-lipgloss.Width(n)))
return label + dimStyle().Render(rule) + n
}
func (m boardModel) taskLine(r Row, index, width int) []string {