package tui import ( "image/color" "sort" "strconv" "strings" "sync" "time" "get-container/cmd/hytop/tchart" "get-container/utils" "github.com/NimbleMarkets/ntcharts/canvas/runes" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" zone "github.com/lrstanley/bubblezone" "github.com/lucasb-eyer/go-colorful" ) const ( A = "├" ) var ( axisFStyle = lipgloss.NewStyle().Inline(true).Foreground(lipgloss.Color("#4d4d4dff")) ) // genXAxis 生成X轴,参数l是x轴的长度 func genXAxis(l int) string { t60 := l / 30 t30 := l >= 18 var result string if t30 { result = A + strings.Repeat("─", 14) result = axisFStyle.Render("30s") + result } else { return strings.Repeat("─", l) } // 长度不超过33 if l < 33 { return strings.Repeat("─", l-18) + result } for i := 1; i <= t60+1; i++ { timeStr := strconv.Itoa(i*60) + "s" timeStrLen := len(timeStr) timeStr = axisFStyle.Render(timeStr) targetLen := timeStrLen + i*30 if l < targetLen { // 不渲染标记,仅增加轴长度 result = strings.Repeat("─", l-lipgloss.Width(result)) + result break } // 渲染标记 result = timeStr + A + strings.Repeat("─", targetLen-lipgloss.Width(result)-timeStrLen-1) + result } return result } func genXAxisNoStyle(l int) string { t60 := l / 30 t30 := l >= 18 var result string if t30 { result = A + strings.Repeat("─", 14) result = "30s" + result } else { return strings.Repeat("─", l) } // 长度不超过33 if l < 33 { return strings.Repeat("─", l-18) + result } for i := 1; i <= t60+1; i++ { timeStr := strconv.Itoa(i*60) + "s" timeStrLen := len(timeStr) targetLen := timeStrLen + i*30 if l < targetLen { // 不渲染标记,仅增加轴长度 result = strings.Repeat("─", l-lipgloss.Width(result)) + result break } // 渲染标记 result = timeStr + A + strings.Repeat("─", targetLen-lipgloss.Width(result)-timeStrLen-1) + result } return result } // MyTimeChartMsg 时间流表消息,用于插入数据 type MyTimeChartMsg struct { Points []tchart.TimePoint // 待添加的数据点 Reset bool // 添加数据点前是否清除原有数据点 } func NewTimeCharMsg(point []tchart.TimePoint, reset bool) MyTimeChartMsg { return MyTimeChartMsg{ Points: point, Reset: reset, } } // MyTimeChart 特化的时间流表,时间区域就是宽度的2倍,单位是秒 type MyTimeChart struct { chart *tchart.Model // 原始图表 zM *zone.Manager // 区域管理 points []tchart.TimePoint // 数据点 width, height int // 图表的高度和宽度 max, min float64 // y轴的最值 lockPoints sync.RWMutex // 保护dataSet的并发读写 color []color.Color } // New 新建图表,其中dataSet的Key为数据集名称,value为数据集的颜色 func NewTimeChart(width, height int, vmin, vmax float64, color []color.Color) *MyTimeChart { result := MyTimeChart{} result.max = vmax result.min = vmin result.width = width result.height = height zoneManager := zone.New() result.zM = zoneManager result.lockPoints = sync.RWMutex{} result.lockPoints.Lock() result.points = make([]tchart.TimePoint, (result.width*2)+1) // 准备数据,数据间隔为1秒 now := time.Now() for i := result.width * 2; i >= 0; i-- { result.points[i] = tchart.TimePoint{Time: now.Add(time.Duration(-i) * time.Second), Value: vmin} } result.lockPoints.Unlock() s := tchart.New(width, height+1, tchart.WithLineStyle(runes.ThinLineStyle), tchart.WithZoneManager(zoneManager), tchart.WithYRange(vmin, vmax), tchart.WithXYSteps(0, 0), tchart.WithTimeSeries(result.points), ) result.chart = &s result.color = color return &result } // PutPoint 添加若干数据点 func (m *MyTimeChart) PutPoint(points []tchart.TimePoint) { m.lockPoints.Lock() ops := append(m.points, points...) // 排序 sort.Slice(ops, func(i, j int) bool { return ops[i].Time.Before(ops[j].Time) }) threshold := time.Now().Add(time.Second * time.Duration(-2*m.width)) targetIndex := 0 for i, p := range ops { if !p.Time.Before(threshold) { targetIndex = i break } } nps := append(make([]tchart.TimePoint, 0, targetIndex+1), ops[targetIndex:]...) m.points = nps s := tchart.New(m.width, m.height+1, tchart.WithLineStyle(runes.ThinLineStyle), tchart.WithZoneManager(m.zM), tchart.WithYRange(m.min, m.max), tchart.WithXYSteps(0, 0), tchart.WithTimeSeries(m.points), ) m.lockPoints.Unlock() // 插入数据 m.chart = nil m.chart = &s m.chart.DrawXYAxisAndLabel() m.chart.DrawBrailleAll() } // ResetPutPoint 清除原有的数据,然后插入数据 func (m *MyTimeChart) ResetPutPoint(points []tchart.TimePoint) { // 清除原有的点 m.lockPoints.Lock() m.points = nil // 排序输入数据 sort.Slice(points, func(i, j int) bool { return points[i].Time.Before(points[j].Time) }) // 补充数据 或 剔除数据 now := time.Now() threshold := now.Add(time.Duration(m.width*-2) * time.Second) first := points[0].Time if first.Before(threshold) { // 需要剔除数据 index := 0 for k, v := range points { if v.Time.Before(threshold) { continue } else { index = k break } } m.points = append(make([]tchart.TimePoint, 0, len(points[index:])), points[index:]...) } else if first.After(threshold) { // 需要补充数据 m.points = make([]tchart.TimePoint, 0, len(points)+2*m.width+1) max := m.width * 2 for i := 1; i < max; i++ { ta := now.Add(time.Second * time.Duration(-i)) if !ta.Before(threshold) { m.points = append(m.points, tchart.TimePoint{Time: ta, Value: m.min}) } else { break } } m.points = append(m.points, points...) } s := tchart.New(m.width, m.height+1, tchart.WithLineStyle(runes.ThinLineStyle), tchart.WithZoneManager(m.zM), tchart.WithYRange(m.min, m.max), tchart.WithXYSteps(0, 0), tchart.WithTimeSeries(m.points), ) m.lockPoints.Unlock() // 插入数据 m.chart = nil m.chart = &s m.chart.DrawXYAxisAndLabel() m.chart.DrawBrailleAll() } func (m *MyTimeChart) Init() tea.Cmd { m.chart.DrawXYAxisAndLabel() m.chart.DrawBrailleAll() return nil } func (m *MyTimeChart) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) { switch msg := inputMsg.(type) { case MyTimeChartMsg: if msg.Reset { m.ResetPutPoint(msg.Points) } else { m.PutPoint(msg.Points) } return m, nil } return m, nil } func (m *MyTimeChart) ViewWithColor(color []color.Color) string { str := m.zM.Scan(m.chart.View()) sb := strings.Builder{} sb.WriteString(str[m.width+1:]) str = sb.String() // return str lines := strings.Split(strings.Trim(str, "\n"), "\n") runes := make([][]rune, len(lines)) for k, v := range lines { runes[k] = []rune(v) } width := lipgloss.Width(str) height := lipgloss.Height(str) for col := range width { firstLine := -1 var charStat utils.CharType = utils.CharEmpty for line := range height { c := runes[line][col] charType := utils.GetCharType(c) if charType != utils.CharEmpty && firstLine == -1 { firstLine = line } if firstLine == -1 { continue } if firstLine == line { // 遇到了一列的第一个非空字符 if t, have := utils.MM[c]; have { runes[line][col] = t } charStat = utils.CharTypeOr(charStat, utils.GetCharType(runes[line][col])) } else { // 第一个非空字符下面的字符 switch charType { case utils.CharEmpty: switch charStat { case utils.CharLeft: runes[line][col] = utils.LeftFullMW case utils.CharRight: runes[line][col] = utils.RightFullMW case utils.CharFull: runes[line][col] = utils.FullMW } if t, have := utils.MM[runes[line][col]]; have { runes[line][col] = t } case utils.CharLeft: switch utils.CharTypeOr(charStat, utils.CharLeft) { case utils.CharLeft: runes[line][col] = utils.LeftFullMW case utils.CharRight: runes[line][col] = utils.RightFullMW case utils.CharFull: runes[line][col] = utils.FullMW } charStat = utils.CharTypeOr(charStat, utils.CharLeft) if t, have := utils.MM[runes[line][col]]; have { runes[line][col] = t } case utils.CharRight: switch utils.CharTypeOr(charStat, utils.CharRight) { case utils.CharLeft: runes[line][col] = utils.LeftFullMW case utils.CharRight: runes[line][col] = utils.RightFullMW case utils.CharFull: runes[line][col] = utils.FullMW } charStat = utils.CharTypeOr(charStat, utils.CharRight) if t, have := utils.MM[runes[line][col]]; have { runes[line][col] = t } case utils.CharFull: charStat = utils.CharFull if t, have := utils.MM[runes[line][col]]; have { runes[line][col] = t } } } } } resultLines := make([]string, height) for k, v := range runes { resultLines[k] = string(v) } if len(color) == height { for k, v := range resultLines { c, _ := colorful.MakeColor(color[k]) resultLines[k] = lipgloss.NewStyle().Foreground(lipgloss.Color(c.Hex())).Render(v) } } return strings.Join(resultLines, "\n") } func (m *MyTimeChart) View() string { str := m.zM.Scan(m.chart.View()) sb := strings.Builder{} sb.WriteString(str[m.width+1:]) str = sb.String() // return str lines := strings.Split(strings.Trim(str, "\n"), "\n") runes := make([][]rune, len(lines)) for k, v := range lines { runes[k] = []rune(v) } width := lipgloss.Width(str) height := lipgloss.Height(str) for col := range width { firstLine := -1 var charStat utils.CharType = utils.CharEmpty for line := range height { c := runes[line][col] charType := utils.GetCharType(c) if charType != utils.CharEmpty && firstLine == -1 { firstLine = line } if firstLine == -1 { continue } if firstLine == line { // 遇到了一列的第一个非空字符 if t, have := utils.MM[c]; have { runes[line][col] = t } charStat = utils.CharTypeOr(charStat, utils.GetCharType(runes[line][col])) } else { // 第一个非空字符下面的字符 switch charType { case utils.CharEmpty: switch charStat { case utils.CharLeft: runes[line][col] = utils.LeftFullMW case utils.CharRight: runes[line][col] = utils.RightFullMW case utils.CharFull: runes[line][col] = utils.FullMW } if t, have := utils.MM[runes[line][col]]; have { runes[line][col] = t } case utils.CharLeft: switch utils.CharTypeOr(charStat, utils.CharLeft) { case utils.CharLeft: runes[line][col] = utils.LeftFullMW case utils.CharRight: runes[line][col] = utils.RightFullMW case utils.CharFull: runes[line][col] = utils.FullMW } charStat = utils.CharTypeOr(charStat, utils.CharLeft) if t, have := utils.MM[runes[line][col]]; have { runes[line][col] = t } case utils.CharRight: switch utils.CharTypeOr(charStat, utils.CharRight) { case utils.CharLeft: runes[line][col] = utils.LeftFullMW case utils.CharRight: runes[line][col] = utils.RightFullMW case utils.CharFull: runes[line][col] = utils.FullMW } charStat = utils.CharTypeOr(charStat, utils.CharRight) if t, have := utils.MM[runes[line][col]]; have { runes[line][col] = t } case utils.CharFull: charStat = utils.CharFull if t, have := utils.MM[runes[line][col]]; have { runes[line][col] = t } } } } } resultLines := make([]string, height) for k, v := range runes { resultLines[k] = string(v) } if len(m.color) == height { for k, v := range resultLines { c, _ := colorful.MakeColor(m.color[k]) resultLines[k] = lipgloss.NewStyle().Foreground(lipgloss.Color(c.Hex())).Render(v) } } return strings.Join(resultLines, "\n") }