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 = "├" B = "└" ) var ( axisFStyle = lipgloss.NewStyle().Inline(true).Foreground(lipgloss.Color("#4d4d4dff")) ) // genXAxis 生成X轴,参数l是x轴的长度 func genXAxis(l int, s ...string) string { if s == nil { s = make([]string, 0, 1) s = append(s, A) } t60 := l / 30 t30 := l >= 18 var result string if t30 { result = s[0] + 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 + s[0] + 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 middleLine bool // 是否添加中间线 } // New 新建图表,color的长度如果为1,则图表的颜色为color[0],如果color的长度为heigh,则图表的颜色随高度变化 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]() result.middleLine = false 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), Value: result.min}) } 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 if len(color) == 2 { result.color = GenGradientColor(color[0], color[1], height) } else { result.color = color } return &result } // SortPoints 对不是自动填充的点进行排序 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 }) } // RemoveUselessPoint 删除无用的点(即超出x轴范围的点) func (m *MyTimeChart) RemoveUselessPoint() { m.SortPoints() // th为时间阈值,在此之前的点需要删除 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) genLines() []string { str := m.zM.Scan(m.chart.View()) sb := strings.Builder{} sb.WriteString(str[m.width+1:]) str = sb.String() 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) } return resultLines } func (m *MyTimeChart) ViewWithColor(color []lipgloss.Color) string { resultLines := m.genLines() height := len(resultLines) 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 { resultLines := m.genLines() height := len(resultLines) switch len(m.color) { case 1: style := lipgloss.NewStyle().Foreground(m.color[0]) return style.Render(strings.Join(resultLines, "\n")) case height: style := lipgloss.NewStyle() for k, v := range resultLines { resultLines[k] = style.Foreground(m.color[k]).Render(v) } return strings.Join(resultLines, "\n") default: return strings.Join(resultLines, "\n") } } // ViewWithMiddleLine func (m *MyTimeChart) ViewWithMiddleLine() string { resultLines := m.genLines() height := len(resultLines) targetLine := 0 isMiddle := false if height%2 == 0 { targetLine = height/2 - 1 } else { targetLine = height / 2 isMiddle = true } switch len(m.color) { case 1: style := lipgloss.NewStyle().Foreground(m.color[0]) for k, v := range resultLines { if k == targetLine { if isMiddle { resultLines[k] = style.Strikethrough(true).Render(v) style.Strikethrough(false) } else { resultLines[k] = style.Underline(true).Render(v) style.Underline(false) } } else { resultLines[k] = style.Render(v) } } return strings.Join(resultLines, "\n") case height: style := lipgloss.NewStyle() for k, v := range resultLines { if k == targetLine { if isMiddle { resultLines[k] = style.Foreground(m.color[k]).Strikethrough(true).Render(v) style.Strikethrough(false) } else { resultLines[k] = style.Foreground(m.color[k]).Underline(true).Render(v) style.Underline(false) } } else { resultLines[k] = style.Foreground(m.color[k]).Render(v) } } return strings.Join(resultLines, "\n") default: style := lipgloss.NewStyle() for k, v := range resultLines { if k == targetLine { if isMiddle { resultLines[k] = style.Strikethrough(true).Render(v) style.Strikethrough(false) } else { resultLines[k] = style.Underline(true).Render(v) style.Underline(false) } } else { resultLines[k] = style.Render(v) } } return strings.Join(resultLines, "\n") } }