package main import ( "context" "crypto/tls" "fmt" "os" "strings" "time" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" pbhuman "github.com/flowy-live/admin-cli/genproto/helios/human" pbkeypad "github.com/flowy-live/admin-cli/genproto/helios/keypad" "github.com/mergestat/timediff" "github.com/sirupsen/logrus" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" ) var ( titleStyle = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#7D56F4")). Padding(0, 1). MarginBottom(1) selectedItemStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFFFFF")). Background(lipgloss.Color("#7D56F4")). Bold(true). Padding(0, 1) normalItemStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FAFAFA")). Padding(0, 1) dimItemStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#666666")). Padding(0, 1) cursorStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FF75B5")). Bold(true) helpStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#626262")). Padding(1, 0, 0, 1) statusStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#7D56F4")). Bold(true) borderStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#3C3C3C")) timestampStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#888888")). Italic(true) 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 { Email string DisplayName string JoinedAt time.Time KeypadConfiguration string } type model struct { humans []*HumanData 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 { return model{ humans: humans, cursor: 0, showKeypad: false, } } func (m model) Init() tea.Cmd { // Just return `nil`, which means "no I/O right now, please." return nil } func (m model) Update(msg tea.Msg) (tea.Model, 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(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 = listWidth m.viewport.Height = msg.Height - verticalMarginHeight m.detailViewport.Width = detailWidth m.detailViewport.Height = msg.Height - verticalMarginHeight } case tea.KeyMsg: switch msg.String() { 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 // Auto-scroll left viewport to keep cursor visible if m.cursor < m.viewport.YOffset { m.viewport.YOffset = m.cursor } } 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 // Auto-scroll left viewport to keep cursor visible if m.cursor >= m.viewport.YOffset+m.viewport.Height { m.viewport.YOffset = m.cursor - m.viewport.Height + 1 } } } } return m, nil } func (m model) renderList() string { var b strings.Builder for i, human := range m.humans { email := human.Email // Get relative time ago timeAgo := timediff.TimeDiff(human.JoinedAt) if m.cursor == i { // Current selection - highlighted cursor := cursorStyle.Render("▸") item := selectedItemStyle.Render(email) timestamp := selectedTimestampStyle.Render(fmt.Sprintf("(%s)", timeAgo)) b.WriteString(fmt.Sprintf("%s %s %s\n", cursor, item, timestamp)) } else { // Normal item cursor := dimItemStyle.Render(" ") item := normalItemStyle.Render(email) 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..." } // Header with main data sevenDaysAgo := time.Now().AddDate(0, 0, -7) pastSevenDaysSignupsCount := 0 for _, human := range m.humans { if human.JoinedAt.After(sevenDaysAgo) { pastSevenDaysSignupsCount++ } } header := titleStyle.Render(fmt.Sprintf("👥 Humans | %d total | %d in the past 7 days", len(m.humans), pastSevenDaysSignupsCount)) // Status bar showing current position statusBar := statusStyle.Render(fmt.Sprintf(" %d/%d", m.cursor+1, len(m.humans))) // Help text help := helpStyle.Render("↑/k up • ↓/j down • ctrl+u/d scroll detail • q quit") // Create two-column layout leftPanel := m.viewport.View() rightPanel := m.detailViewport.View() // 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 { md := metadata.Pairs("authorization", "talksik") ctx = metadata.NewOutgoingContext(ctx, md) return invoker(ctx, method, req, reply, cc, opts...) } func main() { logrus.Infof("hello world") heliosAddr := "helios.flowy.live:443" creds := credentials.NewTLS(&tls.Config{ InsecureSkipVerify: true, }) heliosService, err := grpc.NewClient(heliosAddr, grpc.WithTransportCredentials(creds), grpc.WithUnaryInterceptor(unaryInterceptor)) if err != nil { logrus.Fatalf("connection to aero server invalid: %v", err) } defer heliosService.Close() humanService := pbhuman.NewHumanServiceClient(heliosService) keypadService := pbkeypad.NewKeypadServiceClient(heliosService) humansResponse, err := humanService.ListHumans(context.Background(), &pbhuman.ListHumansRequest{}) if err != nil { logrus.Fatalf("unable to fetch humans: %v", err) os.Exit(1) } keypadsResponse, err := keypadService.ListKeypads(context.Background(), &pbkeypad.ListKeypadsRequest{}) if err != nil { logrus.Fatalf("unable to fetch keypads: %v", err) os.Exit(1) } keypadsResponse.GetHumanKeypads()[0].GetHumanId() keypadsResponse.GetHumanKeypads()[0].GetYamlConfig() logrus.Info("connected to helios") logrus.Infof("fetched %d humans", len(humansResponse.GetHumans())) humansData := []*HumanData{} for _, human := range humansResponse.GetHumans() { keypadConfiguration := "" for _, keypad := range keypadsResponse.GetHumanKeypads() { if keypad.GetHumanId() == human.GetId() { keypadConfiguration = keypad.GetYamlConfig() } } humansData = append(humansData, &HumanData{ Email: human.GetEmail(), DisplayName: human.GetDisplayName(), JoinedAt: human.GetJoinedAt().AsTime(), KeypadConfiguration: keypadConfiguration, }) } p := tea.NewProgram(initialModel(humansData)) if _, err := p.Run(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) os.Exit(1) } }