diff --git a/go-cli/cmd/main.go b/go-cli/cmd/main.go index af95561..c78e321 100644 --- a/go-cli/cmd/main.go +++ b/go-cli/cmd/main.go @@ -63,6 +63,29 @@ var ( selectedTimestampStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#CCCCCC")). Italic(true) + + detailPanelStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#7D56F4")). + Padding(1, 2) + + detailTitleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#7D56F4")). + Bold(true). + Underline(true) + + detailLabelStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#888888")). + Bold(true) + + detailValueStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FAFAFA")) + + codeBlockStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("#1a1a1a")). + Foreground(lipgloss.Color("#00FF00")). + Padding(1, 1). + MarginTop(1) ) type HumanData struct { @@ -77,8 +100,12 @@ type model struct { cursor int selectedHumanEmail string viewport viewport.Model + detailViewport viewport.Model ready bool showKeypad bool + terminalWidth int + terminalHeight int + focusOnDetail bool // true = detail panel has focus, false = list has focus } func initialModel(humans []*HumanData) model { @@ -95,25 +122,35 @@ func (m model) Init() tea.Cmd { } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var ( - cmd tea.Cmd - cmds []tea.Cmd - ) - switch msg := msg.(type) { case tea.WindowSizeMsg: + m.terminalWidth = msg.Width + m.terminalHeight = msg.Height + headerHeight := 3 // title + status + separator footerHeight := 2 // help text verticalMarginHeight := headerHeight + footerHeight + // Split width: 50% for list, 50% for details + listWidth := msg.Width / 2 + detailWidth := msg.Width - listWidth - 3 // -3 for padding/border + if !m.ready { - m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) + m.viewport = viewport.New(listWidth, msg.Height-verticalMarginHeight) m.viewport.YPosition = headerHeight m.viewport.SetContent(m.renderList()) + + m.detailViewport = viewport.New(detailWidth, msg.Height-verticalMarginHeight) + m.detailViewport.YPosition = headerHeight + m.detailViewport.SetContent(m.renderDetails()) + m.ready = true } else { - m.viewport.Width = msg.Width + m.viewport.Width = listWidth m.viewport.Height = msg.Height - verticalMarginHeight + + m.detailViewport.Width = detailWidth + m.detailViewport.Height = msg.Height - verticalMarginHeight } case tea.KeyMsg: @@ -121,27 +158,35 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "ctrl+c", "q": return m, tea.Quit + case "ctrl+u": + // Scroll detail panel up only + m.detailViewport.LineUp(3) + return m, nil + + case "ctrl+d": + // Scroll detail panel down only + m.detailViewport.LineDown(3) + return m, nil + case "up", "k": if m.cursor > 0 { m.cursor-- m.viewport.SetContent(m.renderList()) + m.detailViewport.SetContent(m.renderDetails()) + m.detailViewport.GotoTop() // Reset detail scroll position } case "down", "j": if m.cursor < len(m.humans)-1 { m.cursor++ m.viewport.SetContent(m.renderList()) + m.detailViewport.SetContent(m.renderDetails()) + m.detailViewport.GotoTop() // Reset detail scroll position } - case "t": - m.showKeypad = !m.showKeypad - m.viewport.SetContent(m.renderList()) } } - m.viewport, cmd = m.viewport.Update(msg) - cmds = append(cmds, cmd) - - return m, tea.Batch(cmds...) + return m, nil } func (m model) renderList() string { @@ -156,22 +201,58 @@ func (m model) renderList() string { // Current selection - highlighted cursor := cursorStyle.Render("▸") item := selectedItemStyle.Render(email) - timestamp := selectedTimestampStyle.Render(fmt.Sprintf("joined %s", timeAgo)) + timestamp := selectedTimestampStyle.Render(fmt.Sprintf("(%s)", timeAgo)) b.WriteString(fmt.Sprintf("%s %s %s\n", cursor, item, timestamp)) - if m.showKeypad { - b.WriteString(fmt.Sprintf("%s\n", selectedItemStyle.Render(human.KeypadConfiguration))) - } } else { // Normal item cursor := dimItemStyle.Render(" ") item := normalItemStyle.Render(email) - timestamp := timestampStyle.Render(fmt.Sprintf("joined %s", timeAgo)) + timestamp := timestampStyle.Render(fmt.Sprintf("(%s)", timeAgo)) b.WriteString(fmt.Sprintf("%s %s %s\n", cursor, item, timestamp)) } } return b.String() } +func (m model) renderDetails() string { + if len(m.humans) == 0 || m.cursor >= len(m.humans) { + return "" + } + + human := m.humans[m.cursor] + var b strings.Builder + + // Title + b.WriteString(detailTitleStyle.Render("Human Details") + "\n\n") + + // Email + b.WriteString(detailLabelStyle.Render("Email: ")) + b.WriteString(detailValueStyle.Render(human.Email) + "\n\n") + + // Display Name + if human.DisplayName != "" { + b.WriteString(detailLabelStyle.Render("Display Name: ")) + b.WriteString(detailValueStyle.Render(human.DisplayName) + "\n\n") + } + + // Joined At + b.WriteString(detailLabelStyle.Render("Joined: ")) + timeAgo := timediff.TimeDiff(human.JoinedAt) + joinedFormatted := human.JoinedAt.Format("Jan 2, 2006 at 3:04 PM") + b.WriteString(detailValueStyle.Render(fmt.Sprintf("%s (%s)", joinedFormatted, timeAgo)) + "\n\n") + + // Keypad Configuration + if human.KeypadConfiguration != "" { + b.WriteString(detailLabelStyle.Render("Keypad Configuration:") + "\n") + b.WriteString(codeBlockStyle.Render(human.KeypadConfiguration)) + } else { + b.WriteString(detailLabelStyle.Render("Keypad Configuration: ")) + b.WriteString(dimItemStyle.Render("(none)")) + } + + return b.String() +} + func (m model) View() string { if !m.ready { return "\n Initializing..." @@ -191,12 +272,21 @@ func (m model) View() string { statusBar := statusStyle.Render(fmt.Sprintf(" %d/%d", m.cursor+1, len(m.humans))) // Help text - help := helpStyle.Render("↑/k up • ↓/j down • t toggle keypads • q quit") + help := helpStyle.Render("↑/k up • ↓/j down • ctrl+u/d scroll detail • q quit") - // Separator - separator := borderStyle.Render(strings.Repeat("─", m.viewport.Width)) + // Create two-column layout + leftPanel := m.viewport.View() + rightPanel := m.detailViewport.View() - return fmt.Sprintf("%s%s\n%s\n%s\n%s", header, statusBar, separator, m.viewport.View(), help) + // Combine panels side by side + mainContent := lipgloss.JoinHorizontal( + lipgloss.Top, + leftPanel, + " ", // spacing + rightPanel, + ) + + return fmt.Sprintf("%s%s\n%s\n%s", header, statusBar, mainContent, help) } func unaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {