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:
@@ -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},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user