package main import ( "strings" "github.com/charmbracelet/lipgloss" ) func renderLayout(m model) string { serverBarWidth := 8 channelWidth := 24 memberWidth := 20 statusHeight := 1 chatHeight := m.height - statusHeight - 2 if chatHeight < 5 { chatHeight = 5 } chatWidth := m.width - serverBarWidth - channelWidth if m.showMembers { chatWidth -= memberWidth } if chatWidth < 20 { chatWidth = 20 } // ---- Server bar (left column) ---- serverBar := ServerBar(m.servers, m.currentServer, m.hoverServer, m.height-statusHeight) // ---- Channel list ---- channelList := ChannelList(m.channels, m.currentChannel, m.hoverChannel, channelWidth-2, m.height-statusHeight) // Apply focus border to channel list if m.focus == focusChannels { channelList = FocusedBorder.Render(channelList) } // ---- Chat pane ---- // Channel header ch := ChannelByFlatIndex(m.channels, m.currentChannel) headerText := "# general" if ch != nil { headerText = formatChannelHeader(*ch) } header := TitleStyle. Width(chatWidth - 2). Render(headerText) // Typing indicator typingBar := "" if m.typing != "" { typingBar = HelpStyle.Render(" " + m.typing + " is typing...") } // Voice status voiceBar := "" if m.voice.Connected { voiceBar = VoiceStatusStyle. Width(chatWidth - 2). Render(m.voice.StatusText()) } // Input inputStyle := InputPaneStyle.Width(chatWidth - 2) inputView := inputStyle.Render("> " + m.input.View()) // Chat viewport chatPane := lipgloss.JoinVertical(lipgloss.Left, header, m.viewport.View(), ) if typingBar != "" { chatPane = lipgloss.JoinVertical(lipgloss.Left, chatPane, typingBar) } if voiceBar != "" { chatPane = lipgloss.JoinVertical(lipgloss.Left, chatPane, voiceBar) } chatPane = lipgloss.JoinVertical(lipgloss.Left, chatPane, inputView) // Focus border for chat if m.focus == focusChat || m.focus == focusInput { chatPane = FocusedBorder.Width(chatWidth).Render(chatPane) } // ---- Member list ---- var memberView string if m.showMembers { memberView = MemberList(m.user, m.members, memberWidth-2, m.height-statusHeight, true) if m.focus == focusMembers { memberView = FocusedBorder.Render(memberView) } } // ---- Status bar ---- statusParts := []string{} if m.err != "" { statusParts = append(statusParts, ErrorStyle.Render("⚠ "+m.err)) } else if m.statusMsg != "" { statusParts = append(statusParts, SuccessStyle.Render(m.statusMsg)) } else if m.typing != "" { statusParts = append(statusParts, HelpStyle.Render(m.typing+" is typing...")) } // Show current user if m.user.Username != "" { right := UsernameStyle.Render(m.user.Username) statusParts = append(statusParts, lipgloss.PlaceHorizontal( m.width-lipgloss.Width(right)-2, lipgloss.Right, right)) } statusBar := lipgloss.NewStyle(). Background(bgT). Foreground(fg). Width(m.width). Padding(0, 1). Render(strings.Join(statusParts, " ")) // ---- Help overlay ---- if m.showHelp { return renderHelp(m) } // ---- Compose layout ---- var rows []string // Top row: servers | channels | chat | members topRow := lipgloss.JoinHorizontal(lipgloss.Top, serverBar, channelList, chatPane, ) if m.showMembers && memberView != "" { topRow = lipgloss.JoinHorizontal(lipgloss.Top, topRow, memberView) } rows = append(rows, topRow) rows = append(rows, statusBar) return lipgloss.JoinVertical(lipgloss.Left, rows...) } func renderHelp(m model) string { helpText := []string{ "", TitleStyle.Render("🗑 Dumpster Chat — Help"), "", " Navigation", " ──────────", " j/k or ↑/↓ Navigate up/down", " g / G Jump to top/bottom", " Tab Cycle focus between panes", " Enter Select server/channel", "", " Chat", " ──────────", " i Focus message input", " Enter Send message (in input)", " Esc Unfocus input", "", " Panels", " ──────────", " m Toggle member list / mute (in voice)", " ? Toggle this help", " q / Ctrl+C Quit", "", HelpStyle.Render(" Press ? to close"), } return lipgloss.NewStyle(). Background(bg). Foreground(fg). Width(m.width). Height(m.height). Padding(1, 2). Render(strings.Join(helpText, "\n")) }