Initial commit: Azure DevOps TUI client

A terminal-based user interface for browsing and managing Azure DevOps
work items. Features include:

- Browse work items with filtering by area, iteration, state, and type
- View work item details with markdown rendering
- Open work items in browser
- Create git branches from work items
- Update work item state
- Keyboard-driven navigation

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Samuel Enocsson
2025-12-04 09:56:11 +01:00
commit 2555afce19
30 changed files with 4906 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
package theme
import "github.com/charmbracelet/bubbles/key"
// KeyMap defines all keyboard shortcuts
type KeyMap struct {
// Navigation
Up key.Binding
Down key.Binding
Left key.Binding
Right key.Binding
Top key.Binding
Bottom key.Binding
NextPanel key.Binding
PrevPanel key.Binding
// Actions
Select key.Binding
Open key.Binding
View key.Binding
Search key.Binding
Refresh key.Binding
Help key.Binding
Back key.Binding
Quit key.Binding
ChangeState key.Binding
CreateBranch key.Binding
}
// DefaultKeyMap returns the default key bindings
func DefaultKeyMap() KeyMap {
return KeyMap{
Up: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑/k", "up"),
),
Down: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓/j", "down"),
),
Left: key.NewBinding(
key.WithKeys("left", "h"),
key.WithHelp("←/h", "left"),
),
Right: key.NewBinding(
key.WithKeys("right", "l"),
key.WithHelp("→/l", "right"),
),
Top: key.NewBinding(
key.WithKeys("g"),
key.WithHelp("g", "top"),
),
Bottom: key.NewBinding(
key.WithKeys("G"),
key.WithHelp("G", "bottom"),
),
NextPanel: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("Tab", "next panel"),
),
PrevPanel: key.NewBinding(
key.WithKeys("shift+tab"),
key.WithHelp("Shift+Tab", "prev panel"),
),
Select: key.NewBinding(
key.WithKeys("enter", " "),
key.WithHelp("Enter/Space", "select"),
),
Open: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("Enter", "open in browser"),
),
View: key.NewBinding(
key.WithKeys("v"),
key.WithHelp("v", "view details"),
),
Search: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "search"),
),
Refresh: key.NewBinding(
key.WithKeys("ctrl+r"),
key.WithHelp("Ctrl+r", "refresh"),
),
Help: key.NewBinding(
key.WithKeys("?"),
key.WithHelp("?", "help"),
),
Back: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("Esc", "back"),
),
Quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
key.WithHelp("q", "quit"),
),
ChangeState: key.NewBinding(
key.WithKeys("s"),
key.WithHelp("s", "change state"),
),
CreateBranch: key.NewBinding(
key.WithKeys("b"),
key.WithHelp("b", "create branch"),
),
}
}
// ShortHelp returns a short help text for the status bar
func (k KeyMap) ShortHelp() []key.Binding {
return []key.Binding{
k.NextPanel,
k.Up, k.Down,
k.Top, k.Bottom,
k.Open,
k.Help,
}
}
// FullHelp returns all key bindings for the help panel
func (k KeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Up, k.Down, k.Top, k.Bottom},
{k.NextPanel, k.PrevPanel},
{k.Select, k.Open, k.View},
{k.ChangeState, k.CreateBranch},
{k.Search, k.Refresh},
{k.Help, k.Back, k.Quit},
}
}
+234
View File
@@ -0,0 +1,234 @@
package theme
import "github.com/charmbracelet/lipgloss"
// Colors
var (
colorPrimary = lipgloss.Color("#7C3AED") // Purple
colorSecondary = lipgloss.Color("#06B6D4") // Cyan
colorSuccess = lipgloss.Color("#10B981") // Green
colorWarning = lipgloss.Color("#F59E0B") // Yellow
colorError = lipgloss.Color("#EF4444") // Red
colorMuted = lipgloss.Color("#6B7280") // Gray
colorBorder = lipgloss.Color("#374151") // Dark gray
colorHighlight = lipgloss.Color("#1F2937") // Very dark gray
colorText = lipgloss.Color("#F9FAFB") // White
colorTextMuted = lipgloss.Color("#9CA3AF") // Light gray
)
// Work item type colors
var typeColors = map[string]lipgloss.Color{
"User Story": lipgloss.Color("#3B82F6"), // Blue
"Story": lipgloss.Color("#3B82F6"),
"Task": lipgloss.Color("#F59E0B"), // Yellow
"Bug": lipgloss.Color("#EF4444"), // Red
"Feature": lipgloss.Color("#8B5CF6"), // Purple
"Epic": lipgloss.Color("#EC4899"), // Pink
}
// Work item state colors
var stateColors = map[string]lipgloss.Color{
// Common states
"New": lipgloss.Color("#6B7280"), // Gray
"Active": lipgloss.Color("#3B82F6"), // Blue
"Resolved": lipgloss.Color("#10B981"), // Green
"Closed": lipgloss.Color("#6B7280"), // Gray
// Agile states
"To Do": lipgloss.Color("#F97316"), // Orange
"In Progress": lipgloss.Color("#8B5CF6"), // Purple
"Done": lipgloss.Color("#10B981"), // Green
"Testing": lipgloss.Color("#FBBF24"), // Yellow
// Additional states
"Removed": lipgloss.Color("#6B7280"), // Gray
"Approved": lipgloss.Color("#10B981"), // Green
}
// Styles defines all UI styles
type Styles struct {
// Base styles
App lipgloss.Style
Title lipgloss.Style
Subtitle lipgloss.Style
StatusBar lipgloss.Style
// Panel styles
PanelActive lipgloss.Style
PanelInactive lipgloss.Style
PanelTitle lipgloss.Style
// Filter styles
FilterGroup lipgloss.Style
FilterGroupTitle lipgloss.Style
FilterOption lipgloss.Style
FilterSelected lipgloss.Style
FilterCursor lipgloss.Style
// Work items list styles
ListHeader lipgloss.Style
ListItem lipgloss.Style
ListItemSelected lipgloss.Style
ListItemCursor lipgloss.Style
// Detail view styles
DetailTitle lipgloss.Style
DetailSection lipgloss.Style
DetailSectionTitle lipgloss.Style
DetailLabel lipgloss.Style
DetailValue lipgloss.Style
DetailDescription lipgloss.Style
DetailTag lipgloss.Style
// Help styles
HelpKey lipgloss.Style
HelpDesc lipgloss.Style
HelpPanel lipgloss.Style
// Type and state badges
TypeBadge func(string) lipgloss.Style
StateBadge func(string) lipgloss.Style
}
// DefaultStyles returns the default styles
func DefaultStyles() Styles {
return Styles{
// Base styles
App: lipgloss.NewStyle().
Background(lipgloss.Color("#111827")),
Title: lipgloss.NewStyle().
Bold(true).
Foreground(colorText).
Padding(0, 1),
Subtitle: lipgloss.NewStyle().
Foreground(colorTextMuted),
StatusBar: lipgloss.NewStyle().
Foreground(colorTextMuted).
Background(lipgloss.Color("#1F2937")).
Padding(0, 1),
// Panel styles
PanelActive: lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(colorPrimary).
Padding(0, 1),
PanelInactive: lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(colorBorder).
Padding(0, 1),
PanelTitle: lipgloss.NewStyle().
Bold(true).
Foreground(colorText).
Background(colorHighlight).
Padding(0, 1),
// Filter styles
FilterGroup: lipgloss.NewStyle().
MarginBottom(1),
FilterGroupTitle: lipgloss.NewStyle().
Bold(true).
Foreground(colorText).
MarginBottom(0),
FilterOption: lipgloss.NewStyle().
Foreground(colorTextMuted).
PaddingLeft(1),
FilterSelected: lipgloss.NewStyle().
Foreground(colorSuccess).
PaddingLeft(1),
FilterCursor: lipgloss.NewStyle().
Foreground(colorPrimary).
Bold(true),
// Work items list styles
ListHeader: lipgloss.NewStyle().
Bold(true).
Foreground(colorTextMuted).
BorderBottom(true).
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(colorBorder),
ListItem: lipgloss.NewStyle().
Foreground(colorText),
ListItemSelected: lipgloss.NewStyle().
Foreground(colorText).
Background(colorHighlight),
ListItemCursor: lipgloss.NewStyle().
Foreground(colorPrimary).
Bold(true),
// Detail view styles
DetailTitle: lipgloss.NewStyle().
Bold(true).
Foreground(colorText).
MarginBottom(1),
DetailSection: lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(colorBorder).
Padding(0, 1).
MarginBottom(1),
DetailSectionTitle: lipgloss.NewStyle().
Bold(true).
Foreground(colorTextMuted).
MarginBottom(0),
DetailLabel: lipgloss.NewStyle().
Foreground(colorTextMuted).
Width(12),
DetailValue: lipgloss.NewStyle().
Foreground(colorText),
DetailDescription: lipgloss.NewStyle().
Foreground(colorText),
DetailTag: lipgloss.NewStyle().
Foreground(colorSecondary).
Background(lipgloss.Color("#1E3A5F")).
Padding(0, 1).
MarginRight(1),
// Help styles
HelpKey: lipgloss.NewStyle().
Foreground(colorPrimary).
Bold(true),
HelpDesc: lipgloss.NewStyle().
Foreground(colorTextMuted),
HelpPanel: lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(colorBorder).
Padding(1, 2),
// Type and state badge generators
TypeBadge: func(t string) lipgloss.Style {
color := typeColors[t]
if color == "" {
color = colorMuted
}
return lipgloss.NewStyle().
Foreground(color).
Bold(true)
},
StateBadge: func(s string) lipgloss.Style {
color := stateColors[s]
if color == "" {
color = colorMuted
}
return lipgloss.NewStyle().
Foreground(color)
},
}
}