package main import ( "fmt" "github.com/openziti/zrok/sdk/golang/sdk" "strings" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/muesli/reflow/wordwrap" "github.com/openziti/zrok/endpoints" ) const shareTuiBacklog = 256 var wordwrapCharacters = " -" var wordwrapBreakpoints = map[rune]bool{' ': true, '-': true} type shareModel struct { shrToken string frontendDescriptions []string shareMode sdk.ShareMode backendMode sdk.BackendMode requests []*endpoints.Request log []string showLog bool width int height int headerHeight int prg *tea.Program } type shareLogLine string func newShareModel(shrToken string, frontendEndpoints []string, shareMode sdk.ShareMode, backendMode sdk.BackendMode) *shareModel { return &shareModel{ shrToken: shrToken, frontendDescriptions: frontendEndpoints, shareMode: shareMode, backendMode: backendMode, } } func (m *shareModel) Init() tea.Cmd { return nil } func (m *shareModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case *endpoints.Request: m.requests = append(m.requests, msg) if len(m.requests) > shareTuiBacklog { m.requests = m.requests[1:] } case shareLogLine: m.showLog = true m.adjustPaneHeights() m.log = append(m.log, string(msg)) if len(m.log) > shareTuiBacklog { m.log = m.log[1:] } case tea.WindowSizeMsg: m.width = msg.Width shareHeaderStyle.Width(m.width - 30) shareConfigStyle.Width(26) shareRequestsStyle.Width(m.width - 2) shareLogStyle.Width(m.width - 2) m.height = msg.Height m.headerHeight = len(m.frontendDescriptions) + 4 m.adjustPaneHeights() case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": return m, tea.Quit case "ctrl+l": return m, tea.ClearScreen case "l": m.showLog = !m.showLog m.adjustPaneHeights() } } return m, nil } func (m *shareModel) View() string { topRow := lipgloss.JoinHorizontal(lipgloss.Top, shareHeaderStyle.Render(strings.Join(m.frontendDescriptions, "\n")), shareConfigStyle.Render(m.renderConfig()), ) var panes string if m.showLog { panes = lipgloss.JoinVertical(lipgloss.Left, shareRequestsStyle.Render(m.renderRequests()), shareLogStyle.Render(m.renderLog()), ) } else { panes = shareRequestsStyle.Render(m.renderRequests()) } return lipgloss.JoinVertical(lipgloss.Left, topRow, panes) } func (m *shareModel) adjustPaneHeights() { if !m.showLog { shareRequestsStyle.Height(m.height - m.headerHeight) } else { splitHeight := m.height - m.headerHeight shareRequestsStyle.Height(splitHeight/2 - 1) shareLogStyle.Height(splitHeight/2 - 1) } } func (m *shareModel) renderConfig() string { out := "[" if m.shareMode == "public" { out += shareModePublicStyle.Render(strings.ToUpper(string(m.shareMode))) } else { out += shareModePrivateStyle.Render(strings.ToUpper(string(m.shareMode))) } out += "] [" if m.backendMode == "proxy" { out += backendModeProxyStyle.Render(strings.ToUpper(string(m.backendMode))) } else { out += backendModeWebStyle.Render(strings.ToUpper(string(m.backendMode))) } out += "]" return out } func (m *shareModel) renderRequests() string { var requestLines []string for _, req := range m.requests { reqLine := fmt.Sprintf("%v %v -> %v %v", timeStyle.Render(req.Stamp.Format(time.RFC850)), addressStyle.Render(req.RemoteAddr), m.renderMethod(req.Method), req.Path, ) reqLineWrapped := wordwrap.String(reqLine, m.width-2) splitWrapped := strings.Split(reqLineWrapped, "\n") for _, splitLine := range splitWrapped { splitLine := strings.ReplaceAll(splitLine, "\n", "") if splitLine != "" { requestLines = append(requestLines, splitLine) } } } requestLines = wrap(requestLines, m.width-2) maxRows := shareRequestsStyle.GetHeight() startRow := 0 if len(requestLines) > maxRows { startRow = len(requestLines) - maxRows } out := "" for i := startRow; i < len(requestLines); i++ { outLine := requestLines[i] if i < len(requestLines)-1 { outLine += "\n" } out += outLine } return out } func (m *shareModel) renderMethod(method string) string { switch strings.ToLower(method) { case "get": return getStyle.Render(method) case "post": return postStyle.Render(method) default: return otherMethodStyle.Render(method) } } func (m *shareModel) renderLog() string { var splitLines []string for _, line := range m.log { wrapped := wordwrap.String(line, m.width-2) wrappedLines := strings.Split(wrapped, "\n") for _, wrappedLine := range wrappedLines { splitLine := strings.ReplaceAll(wrappedLine, "\n", "") if splitLine != "" { splitLines = append(splitLines, splitLine) } } } splitLines = wrap(splitLines, m.width-2) maxRows := shareLogStyle.GetHeight() startRow := 0 if len(splitLines) > maxRows { startRow = len(splitLines) - maxRows } out := "" for i := startRow; i < len(splitLines); i++ { outLine := splitLines[i] if i < len(splitLines)-1 { outLine += "\n" } out += outLine } return out } func (m *shareModel) Write(p []byte) (n int, err error) { in := string(p) lines := strings.Split(in, "\n") for _, line := range lines { cleanLine := strings.ReplaceAll(line, "\n", "") if cleanLine != "" && m.prg != nil { m.prg.Send(shareLogLine(cleanLine)) } } return len(p), nil } func (shareModel) Close() error { return nil } func wrap(lines []string, width int) []string { ret := make([]string, 0) for _, line := range lines { if width <= 0 || len(line) <= width { ret = append(ret, line) continue } for i := 0; i <= len(line); { max := i + width if max > len(line) { max = len(line) } if line[i:max] == "" { continue } nextI := i + width if max < len(line)-1 { if !wordwrapBreakpoints[rune(line[max])] || !wordwrapBreakpoints[rune(line[max+1])] { lastSpace := strings.LastIndexAny(line[:max], wordwrapCharacters) if lastSpace > -1 { max = lastSpace nextI = lastSpace } } } ret = append(ret, strings.TrimSpace(line[i:max])) i = nextI } } return ret } var shareHeaderStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("63")). Align(lipgloss.Center) var shareConfigStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("63")). Align(lipgloss.Center) var shareRequestsStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("63")) var shareLogStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("63")) var shareModePublicStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#0F0")) var shareModePrivateStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#F00")) var backendModeProxyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500")) var backendModeWebStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#0CC")) var timeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#444")) var addressStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500")) var getStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("98")) var postStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("101")) var otherMethodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("166"))