Show a colour-coded team picker instead of cycling with T
CI / test (push) Successful in 15s
Release / test (push) Successful in 4s
Release / binaries (push) Successful in 28s

T used to silently cycle Model.activeTeamID through the caller's teams with
no visible list of choices. It now opens a full picker (modelled on the
existing user picker) listing every team plus "All teams", each with a
stable identity colour from a new six-colour theme palette. The same colour
now also shows as a bullet next to "team: <name>" in the header, so the
active team stays visible without opening the picker.

Adds an Identity palette to the theme package (six hues, skipping the ones
that already mean firing/critical), Styles.TeamColor to pick one by team id,
and identity_1..6 as theme-file tokens alongside the existing twelve so a
fully custom theme can still set every token.
This commit is contained in:
Niklas Ye
2026-09-27 18:14:45 +02:00
parent ee25552a53
commit 024dc095a5
11 changed files with 265 additions and 62 deletions
+53 -17
View File
@@ -34,6 +34,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.rebuildArchivedTable()
m.rebuildScheduleTable()
m.rebuildUserPickerTable()
m.rebuildTeamPickerTable()
m.rebuildUserManageTable()
m.detailViewport.Width = m.width
m.detailViewport.Height = m.detailViewportHeight()
@@ -400,6 +401,12 @@ func (m Model) routeKey(msg tea.KeyMsg) (Model, tea.Cmd) {
m2, ourCmd := m.handleKey(msg)
return m2, tea.Batch(tableCmd, ourCmd)
case modeTeamPicker:
var tableCmd tea.Cmd
m.teamPickerTable, tableCmd = m.teamPickerTable.Update(msg)
m2, ourCmd := m.handleKey(msg)
return m2, tea.Batch(tableCmd, ourCmd)
case modeUserCreate:
var inputCmd tea.Cmd
m.userFormInputs[m.userFormFocus], inputCmd = m.userFormInputs[m.userFormFocus].Update(msg)
@@ -496,6 +503,8 @@ func (m Model) handleKey(msg tea.KeyMsg) (Model, tea.Cmd) {
return m.handleConfirmKey(msg)
case modeUserPicker:
return m.handleUserPickerKey(msg)
case modeTeamPicker:
return m.handleTeamPickerKey(msg)
case modeUserCreate:
return m.handleUserCreateKey(msg)
case modeUserNotifyEdit:
@@ -566,7 +575,7 @@ func (m Model) handleDashboardKey(msg tea.KeyMsg) (Model, tea.Cmd) {
if !m.connected || len(m.teams) == 0 {
return m, nil
}
return m.switchTeam()
return m.openTeamPicker()
case "enter":
switch m.activeSection {
@@ -847,22 +856,21 @@ func (m Model) openUserPicker(target pickerTarget) (Model, tea.Cmd) {
return m, nil
}
// switchTeam steps the active team through all teams, then each of the caller's
// teams in turn, and reloads what depends on it. Sections that are not on screen
// are emptied rather than fetched, so they load when next opened; the incident
// queue is the exception because it is what the caller returns to.
func (m Model) switchTeam() (Model, tea.Cmd) {
next := int64(0)
if m.activeTeamID == 0 {
next = m.teams[0].ID
} else {
for i, t := range m.teams {
if t.ID == m.activeTeamID && i+1 < len(m.teams) {
next = m.teams[i+1].ID
}
}
}
m.activeTeamID = next
// openTeamPicker opens the full list of the caller's teams, plus "All teams",
// for the "T" key to choose among rather than blindly cycling through them.
// Team data is already loaded at connect time, so no fetch is needed.
func (m Model) openTeamPicker() (Model, tea.Cmd) {
m.mode = modeTeamPicker
m.rebuildTeamPickerTable()
return m, nil
}
// selectTeam sets the active team to teamID (0 meaning all teams) and reloads
// what depends on it. Sections that are not on screen are emptied rather than
// fetched, so they load when next opened; the incident queue is the exception
// because it is what the caller returns to.
func (m Model) selectTeam(teamID int64) (Model, tea.Cmd) {
m.activeTeamID = teamID
m.incidents, m.alerts, m.archivedIncidents = nil, nil, nil
m.scheduleEntries, m.scheduleDays, m.currentOnCall = nil, nil, nil
@@ -1253,6 +1261,34 @@ func (m Model) handleUserPickerKey(msg tea.KeyMsg) (Model, tea.Cmd) {
return m, nil
}
// ── Team picker ───────────────────────────────────────────────────────────
// handleTeamPickerKey reads the cursor's team off the table built by
// rebuildTeamPickerTable, where row 0 is always "All teams" and row i (i>=1)
// is m.teams[i-1].
func (m Model) handleTeamPickerKey(msg tea.KeyMsg) (Model, tea.Cmd) {
switch msg.String() {
case "esc":
m.mode = modeDashboard
return m, nil
case "enter":
cursor := m.teamPickerTable.Cursor()
id := int64(0)
if cursor > 0 {
i := cursor - 1
if i < 0 || i >= len(m.teams) {
return m, nil
}
id = m.teams[i].ID
}
m, cmd := m.selectTeam(id)
m.mode = modeDashboard
return m, cmd
}
return m, nil
}
// scheduleConflicts reports which of dates are already held by somebody other
// than newUserID, and the distinct names holding them.
//