package tui import ( "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" linkedlist "github.com/emirpasic/gods/v2/lists/doublylinkedlist" zone "github.com/lrstanley/bubblezone" ) 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 *linkedlist.List[tchart.TimePoint] // 数据点,这里只存储put的数据,不存储自动添加的数据 width, height int // 图表的高度和宽度 max, min float64 // y轴的最值 lockPoints sync.RWMutex // 保护dataSet的并发读写 color []lipgloss.Color } // New 新建图表,其中dataSet的Key为数据集名称,value为数据集的颜色 func NewTimeChart(width, height int, vmin, vmax float64, color []lipgloss.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 = linkedlist.New[tchart.TimePoint]() now := time.Now() t := result.width*2 + 1 tmpPoints := make([]tchart.TimePoint, 0, t) for i := range t { tmpPoints = append(tmpPoints, tchart.TimePoint{Time: now.Add(time.Duration(-i) * time.Second)}) } 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(tmpPoints), ) result.chart = &s result.color = color return &result } func (m *MyTimeChart) SortPoints() { m.points.Sort(func(x, y tchart.TimePoint) int { if x.Time.After(y.Time) { return 1 } if x.Time.Before(y.Time) { return -1 } return 0 }) } func (m *MyTimeChart) RemoveUselessPoint() { m.SortPoints() th := time.Now().Add(time.Duration(m.width*-2) * time.Second) for { t, b := m.points.Get(0) if !b { break } if t.Time.Before(th) { m.points.Remove(0) } else { break } } } // PutPoint 添加若干数据点 func (m *MyTimeChart) PutPoint(points []tchart.TimePoint) { m.lockPoints.Lock() if len(points) > 0 { m.points.Add(points...) } m.RemoveUselessPoint() threshold := time.Now().Add(time.Second * time.Duration(-2*m.width-1)) points = m.points.Values() tmpPoint := make([]tchart.TimePoint, 0, m.width*2) // 判断是否需要补充空点 if !points[0].Time.Before(threshold) { t := points[0].Time.Add(-time.Second) for { if !t.Before(threshold) { tmpPoint = append(tmpPoint, tchart.TimePoint{Time: t, Value: m.min}) t = t.Add(-time.Second) } else { break } } } tmpPoint = append(tmpPoint, points...) sort.Slice(tmpPoint, func(i, j int) bool { return tmpPoint[i].Time.Before(tmpPoint[j].Time) }) 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(tmpPoint), ) 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.Clear() if len(points) > 0 { m.points.Add(points...) } m.RemoveUselessPoint() threshold := time.Now().Add(time.Second * time.Duration(-2*m.width-1)) points = m.points.Values() tmpPoint := make([]tchart.TimePoint, 0, m.width*2) // 判断是否需要补充空点 if !points[0].Time.Before(threshold) { t := points[0].Time.Add(-time.Second) for { if !t.Before(threshold) { tmpPoint = append(tmpPoint, tchart.TimePoint{Time: t, Value: m.min}) t = t.Add(-time.Second) } else { break } } } tmpPoint = append(tmpPoint, points...) sort.Slice(tmpPoint, func(i, j int) bool { return tmpPoint[i].Time.Before(tmpPoint[j].Time) }) 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(tmpPoint), ) 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 []lipgloss.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) } style := lipgloss.NewStyle() if len(color) == height { for k, v := range resultLines { resultLines[k] = style.Foreground(color[k]).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 { style := lipgloss.NewStyle() for k, v := range resultLines { resultLines[k] = style.Foreground(m.color[k]).Render(v) } } else if len(m.color) == 1 { style := lipgloss.NewStyle().Foreground(m.color[0]) return style.Render(strings.Join(resultLines, "\n")) } return strings.Join(resultLines, "\n") }