Commit 771221d9 authored by liming6's avatar liming6
Browse files

feature 添加TUI组件

parent b632c3a7
# Done
## tui库调研
- TUI库:https://github.com/charmbracelet/bubbletea ,用户定义TUI和运行组件
- TUI样式库:https://github.com/charmbracelet/lipgloss ,组件样式库,允许组合组件
- TUI组件库:https://github.com/NimbleMarkets/ntcharts ,TUI图表库
\ No newline at end of file
package main
import (
"fmt"
"get-container/gpu"
"log"
)
func main() {
v, e := gpu.GetHYVersionInfo()
if e != nil {
log.Fatal(e)
}
fmt.Printf("HYVersionInfo: %+v\n", v)
}
package tui
import (
"fmt"
"get-container/gpu"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"time"
)
type ModelHeader struct {
t time.Time
DCUTopVersion string
SMIVersion string
DriverVersion string
}
func (mh *ModelHeader) Init() tea.Cmd {
v, err := gpu.GetHYVersionInfo()
if err != nil || v == nil {
return tea.Quit
}
mh.DCUTopVersion = "1.0.0"
//mh.SMIVersion = v.SMIVersion // 1.14.0
//mh.DriverVersion = v.DriverVersion // 6.3.6-V1.9.0
mh.SMIVersion = "1.14.0"
mh.DriverVersion = "6.3.6-V1.9.0"
mh.t = time.Now()
return func() tea.Msg {
return TickMsg(time.Now())
}
}
func (mh *ModelHeader) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := inputMsg.(type) {
case TickMsg:
mh.t = time.Time(msg)
return mh, tea.Every(time.Second, func(t time.Time) tea.Msg {
return TickMsg(t)
})
}
return mh, nil
}
func (mh *ModelHeader) View() string {
header := fmt.Sprintf("%s (Press h for help or q for quit)\n", mh.t.Format("2006-01-02 15:04:05"))
style := lipgloss.NewStyle().Width(40)
borderStyle := lipgloss.NewStyle().Border(myBorder, true, true, false, true)
hyv := fmt.Sprintf("hytop %s", mh.DCUTopVersion)
drv := fmt.Sprintf("Driver Version: %s", mh.DriverVersion)
smiv := fmt.Sprintf("SMI Version: %s", mh.SMIVersion)
return header + borderStyle.Render(style.Render(hyv)+style.Render(drv)+style.Render(smiv)) + "\n"
}
package tui
import (
"get-container/gpu"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"time"
)
// ModelMsg 模型信息,在父组件和各个子组件间共享信息
type ModelMsg struct {
t time.Time // 当前时间
index uint64 // update次数
Version *gpu.HYVersionInfo // gpu版本相关信息
DCUInfo []gpu.DCUInfo // DCU全量信息
DCUPidInfo []gpu.DCUPidInfo // 使用dcu的进程信息
}
type TickMsg time.Time
// ModelMain tui主模型类
type ModelMain struct {
Header *ModelHeader
GPUInfo *ModelDCUInfo
SysLoad *ModelSysLoad
ProcessInfo *ModelProcess
index uint64 // 记录update次数的值
modelMsg *ModelMsg // 记录模型信息
}
func tickCmd() tea.Cmd {
return tea.Every(time.Second, func(t time.Time) tea.Msg {
return TickMsg(t)
})
}
func sendMsgCmd(modelMsg *ModelMsg) tea.Cmd {
return func() tea.Msg {
return *modelMsg
}
}
// Init 初始化信息
func (m *ModelMain) Init() tea.Cmd {
// todo 初始化一个ModelMsg实例
modelMsg := &ModelMsg{}
cmds := make([]tea.Cmd, 0)
if c := m.Header.Init(); c != nil {
cmds = append(cmds, c)
}
m.modelMsg = modelMsg
// 将调用初始化方法
cmds = append(cmds, tickCmd(), sendMsgCmd(modelMsg))
return tea.Batch(cmds...)
}
func (m *ModelMain) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := inputMsg.(type) {
case tea.KeyMsg: // 键盘事件
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
}
case TickMsg: // 定时事件
m.index++
updateModelInfo(m.modelMsg, m.index, time.Time(msg))
cmds := make([]tea.Cmd, 0)
header, cmdHeader := m.Header.Update(m.modelMsg)
m.Header = header.(*ModelHeader)
cmds = append(cmds, cmdHeader, tickCmd())
return m, tea.Batch(cmds...)
}
return m, nil
}
func (m *ModelMain) View() string {
return m.Header.View()
}
type ModelDCUInfo struct{}
type ModelSysLoad struct{}
type ModelProcess struct{}
var myBorder = lipgloss.Border{
Top: "═",
TopLeft: "╒",
TopRight: "╕",
Bottom: "═",
BottomLeft: "╘",
BottomRight: "╛",
Left: "│",
Right: "│",
MiddleLeft: "├",
MiddleRight: "┤",
Middle: "┼",
MiddleTop: "┬",
MiddleBottom: "┴",
}
// updateModelInfo 更新模型信息
func updateModelInfo(modelMsg *ModelMsg, index uint64, t time.Time) {
modelMsg.index = index
modelMsg.t = t
}
package tui
import (
"fmt"
"testing"
)
func TestHeader(t *testing.T) {
m := ModelHeader{}
cmd := m.Init()
m.Update(cmd)
fmt.Println(m.View())
}
...@@ -3,6 +3,7 @@ module get-container ...@@ -3,6 +3,7 @@ module get-container
go 1.24.2 go 1.24.2
require ( require (
github.com/NimbleMarkets/ntcharts v0.3.1
github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/moby/moby/api v1.52.0-beta.2 github.com/moby/moby/api v1.52.0-beta.2
...@@ -14,6 +15,7 @@ require ( ...@@ -14,6 +15,7 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbles v0.20.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
...@@ -29,6 +31,7 @@ require ( ...@@ -29,6 +31,7 @@ require (
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
...@@ -53,5 +56,5 @@ require ( ...@@ -53,5 +56,5 @@ require (
go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.3.8 // indirect golang.org/x/text v0.20.0 // indirect
) )
...@@ -58,6 +58,9 @@ var ( ...@@ -58,6 +58,9 @@ var (
ReSMCFWVersion = regexp.MustCompile(`(?mi)^SMC\s*Firmware\s*Version\s*:\s*([0-9.]*)$`) ReSMCFWVersion = regexp.MustCompile(`(?mi)^SMC\s*Firmware\s*Version\s*:\s*([0-9.]*)$`)
ReCardSerial = regexp.MustCompile(`(?mi)^Card\s*Series\s*:\s*(.*)$`) ReCardSerial = regexp.MustCompile(`(?mi)^Card\s*Series\s*:\s*(.*)$`)
ReCardVendor = regexp.MustCompile(`(?mi)^Card\s*Vendor\s*:\s*(.*)$`) ReCardVendor = regexp.MustCompile(`(?mi)^Card\s*Vendor\s*:\s*(.*)$`)
ReVersion = regexp.MustCompile(`(?i)^version\s*([0-9a-zA-Z.]*)\s*\(.*\)$`)
ReDriVersion = regexp.MustCompile(`(?mi)^driver\s*version\s*:\s*(.*)$`)
) )
type HYVersionInfo struct { type HYVersionInfo struct {
...@@ -74,10 +77,16 @@ func GetHYVersionInfo() (*HYVersionInfo, error) { ...@@ -74,10 +77,16 @@ func GetHYVersionInfo() (*HYVersionInfo, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &HYVersionInfo{ result := HYVersionInfo{}
SMIVersion: strings.Trim(strings.TrimSpace(string(versionBytes)), "\n"), mv := ReVersion.FindStringSubmatch(strings.TrimSpace(strings.Trim(string(versionBytes), "\n")))
DriverVersion: strings.Trim(strings.TrimSpace(string(driBytes)), "\n"), if len(mv) >= 2 {
}, nil result.SMIVersion = mv[1]
}
mrv := ReDriVersion.FindStringSubmatch(strings.TrimSpace(strings.Trim(string(driBytes), "\n")))
if len(mrv) >= 2 {
result.DriverVersion = mrv[1]
}
return &result, nil
} }
type DCUInfo struct { type DCUInfo struct {
...@@ -85,7 +94,7 @@ type DCUInfo struct { ...@@ -85,7 +94,7 @@ type DCUInfo struct {
Name string // DCU名称 Name string // DCU名称
PerformanceLevel string // 性能等级 PerformanceLevel string // 性能等级
FanSpeed float32 // 风扇转速 FanSpeed float32 // 风扇转速
Temperature float32 // 平均温 Temperature float32 // 平均温
PwrUsage int16 PwrUsage int16
PwrCapacity int16 PwrCapacity int16
BusId string BusId string
...@@ -140,7 +149,7 @@ type SMIAllOutput struct { ...@@ -140,7 +149,7 @@ type SMIAllOutput struct {
} }
func GetSMIAllOutput() ([]*SMIAllOutput, error) { func GetSMIAllOutput() ([]*SMIAllOutput, error) {
b, err := exec.Command(DCUBinaryFile, "-a").Output() b, err := exec.Command(DCUBinaryFile, "-a").CombinedOutput()
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -359,7 +368,7 @@ type DCURunningInfo struct { ...@@ -359,7 +368,7 @@ type DCURunningInfo struct {
// GetRunningInfo 获取DCU运行相关信息 // GetRunningInfo 获取DCU运行相关信息
func GetRunningInfo() ([]DCURunningInfo, error) { func GetRunningInfo() ([]DCURunningInfo, error) {
output, err := exec.Command(DCUBinaryFile).Output() output, err := exec.Command(DCUBinaryFile).CombinedOutput()
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
...@@ -239,8 +239,8 @@ func TestParseRunningInfo(t *testing.T) { ...@@ -239,8 +239,8 @@ func TestParseRunningInfo(t *testing.T) {
} }
} }
func TestAbc(t *testing.T) { func TestParseSMIAllOutput(t *testing.T) {
b, e := os.ReadFile("../test-data/hy.log") b, e := os.ReadFile("../tui-data/hy.log")
if e != nil { if e != nil {
t.Error(e) t.Error(e)
} }
......
package utils
import (
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/load"
"github.com/shirou/gopsutil/v4/mem"
"os"
"strconv"
"time"
)
/*
获取系统相关信息
*/
type SysInfo struct {
Hostname string // 主机名
CPUPercent float64 // CPU使用率
LoadAverage1 float64 // 1分钟内平均负载
LoadAverage5 float64 // 5分钟平均负载
LoadAverage15 float64 // 15分钟平均负载
MemTotal uint64 // 总内存
SwapTotal uint64 // 总swap
MemUsage uint64 // 已使用内存
SwapUsage uint64 // 已使用swap
MemUsagePercent float64 // 已使用内存百分比
SwapUsagePercent float64 // 已使用swap百分比
DateTime time.Time // 系统时间
SysUser string // 当前用户
}
func GetSysInfo() (*SysInfo, error) {
hn, err := os.Hostname()
if err != nil {
return nil, err
}
result := SysInfo{}
result.Hostname = hn
l, err := load.Avg()
if err != nil {
return nil, err
}
c, err := cpu.Percent(0, false)
if err != nil {
return nil, err
}
result.CPUPercent = c[0]
result.LoadAverage1 = l.Load1
result.LoadAverage5 = l.Load5
result.LoadAverage15 = l.Load15
sm, err := mem.SwapMemory()
if err != nil {
return nil, err
}
result.SwapTotal = sm.Total
result.SwapUsage = sm.Used
result.SwapUsagePercent = sm.UsedPercent
vm, err := mem.VirtualMemory()
if err != nil {
return nil, err
}
result.MemTotal = vm.Total
result.MemUsage = vm.Used
result.MemUsagePercent = vm.UsedPercent
result.DateTime = time.Now()
sysu, err := GetSysUserById(os.Getuid())
if err != nil || sysu == nil {
result.SysUser = strconv.Itoa(os.Getuid())
} else {
result.SysUser = sysu.Name
}
return &result, nil
}
package utils
import (
"github.com/shirou/gopsutil/v4/cpu"
"testing"
)
func TestGetSysUsers(t *testing.T) {
a, e := cpu.Percent(0, false)
if e != nil {
t.Error(e)
}
t.Logf("%+v", a)
}
package utils package utils
import ( import (
"fmt"
"os" "os"
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
"sync"
) )
/* /*
...@@ -16,6 +18,8 @@ const ( ...@@ -16,6 +18,8 @@ const (
NISCat = "ypcat" NISCat = "ypcat"
) )
var SysUserInfo = sync.Map{}
// detectNis 探测系统是否为Nis的客户端,即解析是否存在ypbind命令,且ypbind命令在运行 // detectNis 探测系统是否为Nis的客户端,即解析是否存在ypbind命令,且ypbind命令在运行
func detectNis() (bool, error) { func detectNis() (bool, error) {
haveCmd, _ := DetectCmd(NISClient) haveCmd, _ := DetectCmd(NISClient)
...@@ -93,10 +97,10 @@ func parseSysUser(str string) ([]SysUser, error) { ...@@ -93,10 +97,10 @@ func parseSysUser(str string) ([]SysUser, error) {
} }
// GetSysUsers 获取系统上所有的用户 // GetSysUsers 获取系统上所有的用户
func GetSysUsers() ([]SysUser, error) { func GetSysUsers() error {
detect, err := detectNis() detect, err := detectNis()
if err != nil { if err != nil {
return nil, err return err
} }
result := make([]SysUser, 0) result := make([]SysUser, 0)
if detect { if detect {
...@@ -105,12 +109,42 @@ func GetSysUsers() ([]SysUser, error) { ...@@ -105,12 +109,42 @@ func GetSysUsers() ([]SysUser, error) {
} }
u, err := os.ReadFile("/etc/passwd") u, err := os.ReadFile("/etc/passwd")
if err != nil { if err != nil {
return result, nil return nil
} }
su, err := parseSysUser(string(u)) su, err := parseSysUser(string(u))
if err != nil { if err != nil {
return result, nil return nil
} }
result = append(result, su...) result = append(result, su...)
return result, nil for _, user := range result {
SysUserInfo.Store(user.Uid, &user)
}
return nil
}
func GetSysUserById(uid int) (*SysUser, error) {
u, ok := SysUserInfo.Load(uid)
if !ok {
err := GetSysUsers()
if err != nil {
return nil, err
}
uu, o := SysUserInfo.Load(uid)
if o && uu != nil {
sysu, so := uu.(*SysUser)
if so {
return sysu, nil
} else {
return nil, fmt.Errorf("error loading sysuser, type error")
}
} else {
return nil, nil
}
} else {
sysu, o := u.(*SysUser)
if !o {
return nil, fmt.Errorf("error loading sysuser, type error")
}
return sysu, nil
}
} }
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment