mirror of
https://github.com/Gouryella/drip.git
synced 2026-02-26 14:21:17 +00:00
feat(tunnel): switch to yamux stream proxying and connection pooling
- Introduce pooled tunnel sessions (TunnelID/DataConnect) on client/server - Proxy HTTP/HTTPS via raw HTTP over yamux streams; pipe TCP streams directly - Move UI/stats into internal/shared; refactor CLI tunnel helpers; drop msgpack/hpack legacy
This commit is contained in:
117
internal/shared/ui/config.go
Normal file
117
internal/shared/ui/config.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// RenderConfigInit renders config initialization UI
|
||||
func RenderConfigInit() string {
|
||||
title := "Drip Configuration Setup"
|
||||
box := boxStyle.Width(50)
|
||||
return "\n" + box.Render(titleStyle.Render(title)) + "\n"
|
||||
}
|
||||
|
||||
// RenderConfigShow renders the config display
|
||||
func RenderConfigShow(server, token string, tokenHidden bool, tlsEnabled bool, configPath string) string {
|
||||
lines := []string{
|
||||
KeyValue("Server", server),
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
if tokenHidden {
|
||||
if len(token) > 10 {
|
||||
displayToken := token[:3] + "***" + token[len(token)-3:]
|
||||
lines = append(lines, KeyValue("Token", Muted(displayToken+" (hidden)")))
|
||||
} else {
|
||||
lines = append(lines, KeyValue("Token", Muted(token[:3]+"*** (hidden)")))
|
||||
}
|
||||
} else {
|
||||
lines = append(lines, KeyValue("Token", token))
|
||||
}
|
||||
} else {
|
||||
lines = append(lines, KeyValue("Token", Muted("(not set)")))
|
||||
}
|
||||
|
||||
tlsStatus := "enabled"
|
||||
if !tlsEnabled {
|
||||
tlsStatus = "disabled"
|
||||
}
|
||||
lines = append(lines, KeyValue("TLS", tlsStatus))
|
||||
lines = append(lines, KeyValue("Config", Muted(configPath)))
|
||||
|
||||
return Info("Current Configuration", lines...)
|
||||
}
|
||||
|
||||
// RenderConfigSaved renders config saved message
|
||||
func RenderConfigSaved(configPath string) string {
|
||||
return SuccessBox(
|
||||
"Configuration Saved",
|
||||
Muted("Config saved to: ")+configPath,
|
||||
"",
|
||||
Muted("You can now use 'drip' without --server and --token flags"),
|
||||
)
|
||||
}
|
||||
|
||||
// RenderConfigUpdated renders config updated message
|
||||
func RenderConfigUpdated(updates []string) string {
|
||||
lines := make([]string, len(updates)+1)
|
||||
for i, update := range updates {
|
||||
lines[i] = Success(update)
|
||||
}
|
||||
lines[len(updates)] = ""
|
||||
lines = append(lines, Muted("Configuration has been updated"))
|
||||
return SuccessBox("Configuration Updated", lines...)
|
||||
}
|
||||
|
||||
// RenderConfigDeleted renders config deleted message
|
||||
func RenderConfigDeleted() string {
|
||||
return SuccessBox("Configuration Deleted", Muted("Configuration file has been removed"))
|
||||
}
|
||||
|
||||
// RenderConfigValidation renders config validation results
|
||||
func RenderConfigValidation(serverValid bool, serverMsg string, tokenSet bool, tokenMsg string, tlsEnabled bool) string {
|
||||
lines := []string{}
|
||||
|
||||
if serverValid {
|
||||
lines = append(lines, Success(serverMsg))
|
||||
} else {
|
||||
lines = append(lines, Error(serverMsg))
|
||||
}
|
||||
|
||||
if tokenSet {
|
||||
lines = append(lines, Success(tokenMsg))
|
||||
} else {
|
||||
lines = append(lines, Warning(tokenMsg))
|
||||
}
|
||||
|
||||
if tlsEnabled {
|
||||
lines = append(lines, Success("TLS is enabled"))
|
||||
} else {
|
||||
lines = append(lines, Warning("TLS is disabled (not recommended for production)"))
|
||||
}
|
||||
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, Muted("Configuration validation complete"))
|
||||
|
||||
if serverValid && tokenSet && tlsEnabled {
|
||||
return SuccessBox("Configuration Valid", lines...)
|
||||
}
|
||||
return WarningBox("Configuration Validation", lines...)
|
||||
}
|
||||
|
||||
// RenderDaemonStarted renders daemon started message
|
||||
func RenderDaemonStarted(tunnelType string, port int, pid int, logPath string) string {
|
||||
lines := []string{
|
||||
KeyValue("Type", Highlight(tunnelType)),
|
||||
KeyValue("Port", fmt.Sprintf("%d", port)),
|
||||
KeyValue("PID", fmt.Sprintf("%d", pid)),
|
||||
"",
|
||||
Muted("Commands:"),
|
||||
Cyan(" drip list") + Muted(" Check tunnel status"),
|
||||
Cyan(fmt.Sprintf(" drip attach %s %d", tunnelType, port)) + Muted(" View logs"),
|
||||
Cyan(fmt.Sprintf(" drip stop %s %d", tunnelType, port)) + Muted(" Stop tunnel"),
|
||||
"",
|
||||
Muted("Logs: ") + mutedStyle.Render(logPath),
|
||||
}
|
||||
return SuccessBox("Tunnel Started in Background", lines...)
|
||||
}
|
||||
184
internal/shared/ui/styles.go
Normal file
184
internal/shared/ui/styles.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var (
|
||||
// Colors inspired by Vercel CLI
|
||||
successColor = lipgloss.Color("#0070F3")
|
||||
warningColor = lipgloss.Color("#F5A623")
|
||||
errorColor = lipgloss.Color("#E00")
|
||||
mutedColor = lipgloss.Color("#888")
|
||||
highlightColor = lipgloss.Color("#0070F3")
|
||||
cyanColor = lipgloss.Color("#50E3C2")
|
||||
|
||||
// Box styles - Vercel-like clean box
|
||||
boxStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
Padding(1, 2).
|
||||
MarginTop(1).
|
||||
MarginBottom(1)
|
||||
|
||||
successBoxStyle = boxStyle.BorderForeground(successColor)
|
||||
|
||||
warningBoxStyle = boxStyle.BorderForeground(warningColor)
|
||||
|
||||
errorBoxStyle = boxStyle.BorderForeground(errorColor)
|
||||
|
||||
// Text styles
|
||||
titleStyle = lipgloss.NewStyle().
|
||||
Bold(true)
|
||||
|
||||
subtitleStyle = lipgloss.NewStyle().
|
||||
Foreground(mutedColor)
|
||||
|
||||
successStyle = lipgloss.NewStyle().
|
||||
Foreground(successColor).
|
||||
Bold(true)
|
||||
|
||||
errorStyle = lipgloss.NewStyle().
|
||||
Foreground(errorColor).
|
||||
Bold(true)
|
||||
|
||||
warningStyle = lipgloss.NewStyle().
|
||||
Foreground(warningColor).
|
||||
Bold(true)
|
||||
|
||||
mutedStyle = lipgloss.NewStyle().
|
||||
Foreground(mutedColor)
|
||||
|
||||
highlightStyle = lipgloss.NewStyle().
|
||||
Foreground(highlightColor).
|
||||
Bold(true)
|
||||
|
||||
cyanStyle = lipgloss.NewStyle().
|
||||
Foreground(cyanColor)
|
||||
|
||||
urlStyle = lipgloss.NewStyle().
|
||||
Foreground(highlightColor).
|
||||
Underline(true).
|
||||
Bold(true)
|
||||
|
||||
labelStyle = lipgloss.NewStyle().
|
||||
Foreground(mutedColor).
|
||||
Width(12)
|
||||
|
||||
valueStyle = lipgloss.NewStyle().
|
||||
Bold(true)
|
||||
|
||||
// Table styles (padding handled manually for consistent Windows output)
|
||||
tableHeaderStyle = lipgloss.NewStyle().
|
||||
Foreground(mutedColor).
|
||||
Bold(true)
|
||||
)
|
||||
|
||||
// Success returns a styled success message
|
||||
func Success(text string) string {
|
||||
return successStyle.Render("✓ " + text)
|
||||
}
|
||||
|
||||
// Error returns a styled error message
|
||||
func Error(text string) string {
|
||||
return errorStyle.Render("✗ " + text)
|
||||
}
|
||||
|
||||
// Warning returns a styled warning message
|
||||
func Warning(text string) string {
|
||||
return warningStyle.Render("⚠ " + text)
|
||||
}
|
||||
|
||||
// Muted returns a styled muted text
|
||||
func Muted(text string) string {
|
||||
return mutedStyle.Render(text)
|
||||
}
|
||||
|
||||
// Highlight returns a styled highlighted text
|
||||
func Highlight(text string) string {
|
||||
return highlightStyle.Render(text)
|
||||
}
|
||||
|
||||
// Cyan returns a styled cyan text
|
||||
func Cyan(text string) string {
|
||||
return cyanStyle.Render(text)
|
||||
}
|
||||
|
||||
// URL returns a styled URL
|
||||
func URL(text string) string {
|
||||
return urlStyle.Render(text)
|
||||
}
|
||||
|
||||
// Title returns a styled title
|
||||
func Title(text string) string {
|
||||
return titleStyle.Render(text)
|
||||
}
|
||||
|
||||
// Subtitle returns a styled subtitle
|
||||
func Subtitle(text string) string {
|
||||
return subtitleStyle.Render(text)
|
||||
}
|
||||
|
||||
// KeyValue returns a styled key-value pair
|
||||
func KeyValue(key, value string) string {
|
||||
return labelStyle.Render(key+":") + " " + valueStyle.Render(value)
|
||||
}
|
||||
|
||||
// Info renders an info box (Vercel-style)
|
||||
func Info(title string, lines ...string) string {
|
||||
content := titleStyle.Render(title)
|
||||
if len(lines) > 0 {
|
||||
content += "\n\n"
|
||||
for i, line := range lines {
|
||||
if i > 0 {
|
||||
content += "\n"
|
||||
}
|
||||
content += line
|
||||
}
|
||||
}
|
||||
return boxStyle.Render(content)
|
||||
}
|
||||
|
||||
// SuccessBox renders a success box
|
||||
func SuccessBox(title string, lines ...string) string {
|
||||
content := successStyle.Render("✓ " + title)
|
||||
if len(lines) > 0 {
|
||||
content += "\n\n"
|
||||
for i, line := range lines {
|
||||
if i > 0 {
|
||||
content += "\n"
|
||||
}
|
||||
content += line
|
||||
}
|
||||
}
|
||||
return successBoxStyle.Render(content)
|
||||
}
|
||||
|
||||
// WarningBox renders a warning box
|
||||
func WarningBox(title string, lines ...string) string {
|
||||
content := warningStyle.Render("⚠ " + title)
|
||||
if len(lines) > 0 {
|
||||
content += "\n\n"
|
||||
for i, line := range lines {
|
||||
if i > 0 {
|
||||
content += "\n"
|
||||
}
|
||||
content += line
|
||||
}
|
||||
}
|
||||
return warningBoxStyle.Render(content)
|
||||
}
|
||||
|
||||
// ErrorBox renders an error box
|
||||
func ErrorBox(title string, lines ...string) string {
|
||||
content := errorStyle.Render("✗ " + title)
|
||||
if len(lines) > 0 {
|
||||
content += "\n\n"
|
||||
for i, line := range lines {
|
||||
if i > 0 {
|
||||
content += "\n"
|
||||
}
|
||||
content += line
|
||||
}
|
||||
}
|
||||
return errorBoxStyle.Render(content)
|
||||
}
|
||||
145
internal/shared/ui/table.go
Normal file
145
internal/shared/ui/table.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Table represents a simple table for CLI output
|
||||
type Table struct {
|
||||
headers []string
|
||||
rows [][]string
|
||||
title string
|
||||
}
|
||||
|
||||
// NewTable creates a new table
|
||||
func NewTable(headers []string) *Table {
|
||||
return &Table{
|
||||
headers: headers,
|
||||
rows: [][]string{},
|
||||
}
|
||||
}
|
||||
|
||||
// WithTitle sets the table title
|
||||
func (t *Table) WithTitle(title string) *Table {
|
||||
t.title = title
|
||||
return t
|
||||
}
|
||||
|
||||
// AddRow adds a row to the table
|
||||
func (t *Table) AddRow(row []string) *Table {
|
||||
t.rows = append(t.rows, row)
|
||||
return t
|
||||
}
|
||||
|
||||
// Render renders the table (Vercel-style)
|
||||
func (t *Table) Render() string {
|
||||
if len(t.rows) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Calculate column widths
|
||||
colWidths := make([]int, len(t.headers))
|
||||
for i, header := range t.headers {
|
||||
colWidths[i] = lipgloss.Width(header)
|
||||
}
|
||||
for _, row := range t.rows {
|
||||
for i, cell := range row {
|
||||
if i < len(colWidths) {
|
||||
width := lipgloss.Width(cell)
|
||||
if width > colWidths[i] {
|
||||
colWidths[i] = width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var output strings.Builder
|
||||
|
||||
// Title
|
||||
if t.title != "" {
|
||||
output.WriteString("\n")
|
||||
output.WriteString(titleStyle.Render(t.title))
|
||||
output.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// Header
|
||||
headerParts := make([]string, len(t.headers))
|
||||
for i, header := range t.headers {
|
||||
styled := tableHeaderStyle.Render(header)
|
||||
headerParts[i] = padRight(styled, colWidths[i])
|
||||
}
|
||||
output.WriteString(strings.Join(headerParts, " "))
|
||||
output.WriteString("\n")
|
||||
|
||||
// Separator line
|
||||
separatorChar := "─"
|
||||
if runtime.GOOS == "windows" {
|
||||
separatorChar = "-"
|
||||
}
|
||||
separatorParts := make([]string, len(t.headers))
|
||||
for i := range t.headers {
|
||||
separatorParts[i] = mutedStyle.Render(strings.Repeat(separatorChar, colWidths[i]))
|
||||
}
|
||||
output.WriteString(strings.Join(separatorParts, " "))
|
||||
output.WriteString("\n")
|
||||
|
||||
// Rows
|
||||
for _, row := range t.rows {
|
||||
rowParts := make([]string, len(t.headers))
|
||||
for i, cell := range row {
|
||||
if i < len(colWidths) {
|
||||
rowParts[i] = padRight(cell, colWidths[i])
|
||||
}
|
||||
}
|
||||
output.WriteString(strings.Join(rowParts, " "))
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
output.WriteString("\n")
|
||||
return output.String()
|
||||
}
|
||||
|
||||
// padRight pads
|
||||
func padRight(text string, targetWidth int) string {
|
||||
visibleWidth := lipgloss.Width(text)
|
||||
if visibleWidth >= targetWidth {
|
||||
return text
|
||||
}
|
||||
padding := strings.Repeat(" ", targetWidth-visibleWidth)
|
||||
return text + padding
|
||||
}
|
||||
|
||||
// Print prints the table
|
||||
func (t *Table) Print() {
|
||||
fmt.Print(t.Render())
|
||||
}
|
||||
|
||||
// RenderList renders a simple list with bullet points
|
||||
func RenderList(items []string) string {
|
||||
bullet := "•"
|
||||
if runtime.GOOS == "windows" {
|
||||
bullet = "*"
|
||||
}
|
||||
var output strings.Builder
|
||||
for _, item := range items {
|
||||
output.WriteString(mutedStyle.Render(" " + bullet + " "))
|
||||
output.WriteString(item)
|
||||
output.WriteString("\n")
|
||||
}
|
||||
return output.String()
|
||||
}
|
||||
|
||||
// RenderNumberedList renders a numbered list
|
||||
func RenderNumberedList(items []string) string {
|
||||
var output strings.Builder
|
||||
for i, item := range items {
|
||||
output.WriteString(mutedStyle.Render(fmt.Sprintf(" %d. ", i+1)))
|
||||
output.WriteString(item)
|
||||
output.WriteString("\n")
|
||||
}
|
||||
return output.String()
|
||||
}
|
||||
251
internal/shared/ui/tunnel.go
Normal file
251
internal/shared/ui/tunnel.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
const (
|
||||
tunnelCardWidth = 76
|
||||
statsColumnWidth = 32
|
||||
)
|
||||
|
||||
var (
|
||||
latencyFastColor = lipgloss.Color("#22c55e") // green
|
||||
latencyYellowColor = lipgloss.Color("#eab308") // yellow
|
||||
latencyOrangeColor = lipgloss.Color("#f97316") // orange
|
||||
latencyRedColor = lipgloss.Color("#ef4444") // red
|
||||
)
|
||||
|
||||
// TunnelStatus represents the status of a tunnel
|
||||
type TunnelStatus struct {
|
||||
Type string // "http", "https", "tcp"
|
||||
URL string // Public URL
|
||||
LocalAddr string // Local address
|
||||
Latency time.Duration // Current latency
|
||||
BytesIn int64 // Bytes received
|
||||
BytesOut int64 // Bytes sent
|
||||
SpeedIn float64 // Download speed
|
||||
SpeedOut float64 // Upload speed
|
||||
TotalRequest int64 // Total requests
|
||||
}
|
||||
|
||||
// RenderTunnelConnected renders the tunnel connection card
|
||||
func RenderTunnelConnected(status *TunnelStatus) string {
|
||||
icon, typeStr, accent := tunnelVisuals(status.Type)
|
||||
|
||||
card := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(accent).
|
||||
Padding(1, 2).
|
||||
Width(tunnelCardWidth)
|
||||
|
||||
typeBadge := lipgloss.NewStyle().
|
||||
Background(accent).
|
||||
Foreground(lipgloss.Color("#f8fafc")).
|
||||
Bold(true).
|
||||
Padding(0, 1).
|
||||
Render(strings.ToUpper(typeStr) + " TUNNEL")
|
||||
|
||||
headline := lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
lipgloss.NewStyle().Foreground(accent).Render(icon),
|
||||
lipgloss.NewStyle().Bold(true).MarginLeft(1).Render("Tunnel Connected"),
|
||||
lipgloss.NewStyle().MarginLeft(2).Render(typeBadge),
|
||||
)
|
||||
|
||||
urlLine := lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
urlStyle.Foreground(accent).Render(status.URL),
|
||||
lipgloss.NewStyle().MarginLeft(1).Foreground(mutedColor).Render("(forwarded address)"),
|
||||
)
|
||||
|
||||
forwardLine := lipgloss.NewStyle().
|
||||
MarginLeft(2).
|
||||
Render(Muted("⇢ ") + valueStyle.Render(status.LocalAddr))
|
||||
|
||||
hint := lipgloss.NewStyle().
|
||||
Foreground(latencyOrangeColor).
|
||||
Render("Ctrl+C to stop • reconnects automatically")
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
headline,
|
||||
"",
|
||||
urlLine,
|
||||
forwardLine,
|
||||
"",
|
||||
hint,
|
||||
)
|
||||
|
||||
return "\n" + card.Render(content) + "\n"
|
||||
}
|
||||
|
||||
// RenderTunnelStats renders real-time tunnel statistics in a card
|
||||
func RenderTunnelStats(status *TunnelStatus) string {
|
||||
latencyStr := formatLatency(status.Latency)
|
||||
trafficStr := fmt.Sprintf("↓ %s ↑ %s", formatBytes(status.BytesIn), formatBytes(status.BytesOut))
|
||||
speedStr := fmt.Sprintf("↓ %s ↑ %s", formatSpeed(status.SpeedIn), formatSpeed(status.SpeedOut))
|
||||
requestsStr := fmt.Sprintf("%d", status.TotalRequest)
|
||||
|
||||
_, _, accent := tunnelVisuals(status.Type)
|
||||
|
||||
requestLabel := "Requests"
|
||||
if status.Type == "tcp" {
|
||||
requestLabel = "Connections"
|
||||
}
|
||||
|
||||
header := lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
lipgloss.NewStyle().Foreground(accent).Render("◉"),
|
||||
lipgloss.NewStyle().Bold(true).MarginLeft(1).Render("Live Metrics"),
|
||||
)
|
||||
|
||||
row1 := lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
statColumn("Latency", latencyStr, statsColumnWidth),
|
||||
statColumn(requestLabel, highlightStyle.Render(requestsStr), statsColumnWidth),
|
||||
)
|
||||
|
||||
row2 := lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
statColumn("Traffic", Cyan(trafficStr), statsColumnWidth),
|
||||
statColumn("Speed", warningStyle.Render(speedStr), statsColumnWidth),
|
||||
)
|
||||
|
||||
card := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(accent).
|
||||
Padding(1, 2).
|
||||
Width(tunnelCardWidth)
|
||||
|
||||
body := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
header,
|
||||
"",
|
||||
row1,
|
||||
row2,
|
||||
)
|
||||
|
||||
return "\n" + card.Render(body) + "\n"
|
||||
}
|
||||
|
||||
// RenderConnecting renders the connecting message
|
||||
func RenderConnecting(serverAddr string, attempt int, maxAttempts int) string {
|
||||
if attempt == 0 {
|
||||
return Highlight("◌") + " Connecting to " + Muted(serverAddr) + "..."
|
||||
}
|
||||
return Warning(fmt.Sprintf("◌ Reconnecting to %s (attempt %d/%d)...", serverAddr, attempt, maxAttempts))
|
||||
}
|
||||
|
||||
// RenderConnectionFailed renders connection failure message
|
||||
func RenderConnectionFailed(err error) string {
|
||||
return Error(fmt.Sprintf("Connection failed: %v", err))
|
||||
}
|
||||
|
||||
// RenderShuttingDown renders shutdown message
|
||||
func RenderShuttingDown() string {
|
||||
return Warning("⏹ Shutting down...")
|
||||
}
|
||||
|
||||
// RenderConnectionLost renders connection lost message
|
||||
func RenderConnectionLost() string {
|
||||
return Error("⚠ Connection lost!")
|
||||
}
|
||||
|
||||
// RenderRetrying renders retry message
|
||||
func RenderRetrying(interval time.Duration) string {
|
||||
return Muted(fmt.Sprintf(" Retrying in %v...", interval))
|
||||
}
|
||||
|
||||
// formatLatency formats latency with color
|
||||
func formatLatency(d time.Duration) string {
|
||||
if d == 0 {
|
||||
return mutedStyle.Render("measuring...")
|
||||
}
|
||||
|
||||
ms := d.Milliseconds()
|
||||
var style lipgloss.Style
|
||||
|
||||
switch {
|
||||
case ms < 50:
|
||||
style = lipgloss.NewStyle().Foreground(latencyFastColor)
|
||||
case ms < 150:
|
||||
style = lipgloss.NewStyle().Foreground(latencyYellowColor)
|
||||
case ms < 300:
|
||||
style = lipgloss.NewStyle().Foreground(latencyOrangeColor)
|
||||
default:
|
||||
style = lipgloss.NewStyle().Foreground(latencyRedColor)
|
||||
}
|
||||
|
||||
if ms == 0 {
|
||||
us := d.Microseconds()
|
||||
return style.Render(fmt.Sprintf("%dµs", us))
|
||||
}
|
||||
|
||||
return style.Render(fmt.Sprintf("%dms", ms))
|
||||
}
|
||||
|
||||
// formatBytes formats bytes to human readable format
|
||||
func formatBytes(bytes int64) string {
|
||||
const unit = 1024
|
||||
if bytes < unit {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := bytes / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
// formatSpeed formats speed to human readable format
|
||||
func formatSpeed(bytesPerSec float64) string {
|
||||
const unit = 1024.0
|
||||
if bytesPerSec < unit {
|
||||
return fmt.Sprintf("%.0f B/s", bytesPerSec)
|
||||
}
|
||||
div, exp := unit, 0
|
||||
for n := bytesPerSec / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB/s", bytesPerSec/div, "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
func statColumn(label, value string, width int) string {
|
||||
labelView := lipgloss.NewStyle().
|
||||
Foreground(mutedColor).
|
||||
Render(strings.ToUpper(label))
|
||||
|
||||
block := lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
labelView,
|
||||
lipgloss.NewStyle().MarginLeft(1).Render(value),
|
||||
)
|
||||
|
||||
if width <= 0 {
|
||||
return block
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().
|
||||
Width(width).
|
||||
Render(block)
|
||||
}
|
||||
|
||||
func tunnelVisuals(tunnelType string) (string, string, lipgloss.Color) {
|
||||
switch tunnelType {
|
||||
case "http":
|
||||
return "🚀", "HTTP", lipgloss.Color("#0070F3")
|
||||
case "https":
|
||||
return "🔒", "HTTPS", lipgloss.Color("#2D8CFF")
|
||||
case "tcp":
|
||||
return "🔌", "TCP", lipgloss.Color("#50E3C2")
|
||||
default:
|
||||
return "🌐", strings.ToUpper(tunnelType), lipgloss.Color("#0070F3")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user