tinybox

TUI Library written in Go

Files | Log | Commits | Refs | README


example.go

Size: 7656 bytes

package main

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"os/user"
	"runtime"
	"strings"
	"syscall"
	tb "tinybox-example/tinybox"
	"time"
)

type SystemInfo struct {
	Hostname    string
	Username    string
	OS          string
	Arch        string
	CPUs        int
	Uptime      string
	LoadAvg     string
	MemoryTotal uint64
	MemoryFree  uint64
	DiskUsage   string
	CurrentTime string
}

func getSystemInfo() *SystemInfo {
	info := &SystemInfo{}

	if hostname, err := os.Hostname(); err == nil {
		info.Hostname = hostname
	}

	if currentUser, err := user.Current(); err == nil {
		info.Username = currentUser.Username
	}

	info.OS = runtime.GOOS
	info.Arch = runtime.GOARCH
	info.CPUs = runtime.NumCPU()

	info.CurrentTime = time.Now().Format("2006-01-02 15:04:05")

	if data, err := os.ReadFile("/proc/uptime"); err == nil {
		parts := strings.Fields(string(data))
		if len(parts) > 0 {
			info.Uptime = parts[0] + " seconds"
		}
	}

	if data, err := os.ReadFile("/proc/loadavg"); err == nil {
		parts := strings.Fields(string(data))
		if len(parts) >= 3 {
			info.LoadAvg = fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2])
		}
	}

	if file, err := os.Open("/proc/meminfo"); err == nil {
		defer file.Close()
		scanner := bufio.NewScanner(file)
		for scanner.Scan() {
			line := scanner.Text()
			if strings.HasPrefix(line, "MemTotal:") {
				fmt.Sscanf(line, "MemTotal: %d kB", &info.MemoryTotal)
				info.MemoryTotal *= 1024
			} else if strings.HasPrefix(line, "MemAvailable:") {
				fmt.Sscanf(line, "MemAvailable: %d kB", &info.MemoryFree)
				info.MemoryFree *= 1024
			}
		}
	}

	var stat syscall.Statfs_t
	if err := syscall.Statfs("/", &stat); err == nil {
		total := stat.Blocks * uint64(stat.Bsize)
		free := stat.Bavail * uint64(stat.Bsize)
		used := total - free
		usedPercent := float64(used) / float64(total) * 100
		info.DiskUsage = fmt.Sprintf("%.1f%% used (%s / %s)",
			usedPercent,
			formatBytes(used),
			formatBytes(total))
	}

	return info
}

func formatBytes(bytes uint64) 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])
}

func drawHeader(width int) {
	tb.SetColor(11, 4)
	tb.Fill(0, 0, width, 3, ' ')

	tb.SetAttr(true, false, false, false)
	tb.SetColor(15, 4)
	tb.DrawTextCenter(1, "Demo using tinybox", 15, 4)
	tb.ResetAttr()
}

func drawSystemInfo(info *SystemInfo, startY int) int {
	y := startY

	tb.Box(1, y, 50, 12)

	tb.SetAttr(true, false, true, false)
	tb.SetColor(14, 0)
	tb.PrintAt(3, y+1, "SYSTEM INFORMATION")
	tb.ResetAttr()
	y += 3

	fields := []struct{ label, value string }{
		{"Hostname:", info.Hostname},
		{"User:", info.Username},
		{"OS/Arch:", fmt.Sprintf("%s/%s", info.OS, info.Arch)},
		{"CPUs:", fmt.Sprintf("%d", info.CPUs)},
		{"Uptime:", info.Uptime},
		{"Load Avg:", info.LoadAvg},
		{"Time:", info.CurrentTime},
	}

	for _, field := range fields {
		if field.value != "" {
			tb.SetColor(10, 0)
			tb.PrintAt(3, y, field.label)
			tb.SetColor(15, 0)
			tb.PrintAt(15, y, field.value)
			y++
		}
	}

	return y + 2
}

func drawMemoryUsage(info *SystemInfo, x, y int) {
	if info.MemoryTotal == 0 {
		return
	}

	tb.Box(x, y, 35, 6)

	tb.SetAttr(true, false, false, false)
	tb.SetColor(13, 0)
	tb.PrintAt(x+2, y+1, "MEMORY USAGE")
	tb.ResetAttr()

	memUsed := info.MemoryTotal - info.MemoryFree
	memPercent := float64(memUsed) / float64(info.MemoryTotal) * 100

	tb.SetColor(15, 0)
	tb.PrintAt(x+2, y+3, fmt.Sprintf("Used: %s (%.1f%%)", formatBytes(memUsed), memPercent))
	tb.PrintAt(x+2, y+4, fmt.Sprintf("Total: %s", formatBytes(info.MemoryTotal)))

	barWidth := 25
	usedWidth := int(float64(barWidth) * memPercent / 100.0)

	tb.SetColor(2, 0)
	for i := 0; i < usedWidth; i++ {
		tb.PrintAt(x+2+i, y+5, "█")
	}
	tb.SetColor(8, 0)
	for i := usedWidth; i < barWidth; i++ {
		tb.PrintAt(x+2+i, y+5, "░")
	}
}

func drawDiskUsage(info *SystemInfo, x, y int) {
	if info.DiskUsage == "" {
		return
	}

	tb.Box(x, y, 35, 4)

	tb.SetAttr(true, false, false, false)
	tb.SetColor(12, 0)
	tb.PrintAt(x+2, y+1, "DISK USAGE (/)")
	tb.ResetAttr()

	tb.SetColor(15, 0)
	tb.PrintAt(x+2, y+2, info.DiskUsage)
}

func drawControls(y, width int) int {
	tb.HLine(0, y, width, '─')
	y++

	tb.SetColor(8, 0)
	controls := []string{
		"R - Refresh", "M - Mouse", "S - Suspend", "B - Bell", "Q - Quit",
	}

	x := 2
	for i, ctrl := range controls {
		if i > 0 {
			tb.PrintAt(x, y, " | ")
			x += 3
		}
		tb.PrintAt(x, y, ctrl)
		x += len(ctrl)
	}

	return y + 2
}

func drawStatusLine(message string, width, height int) {
	tb.SetColor(0, 7)
	tb.Fill(0, height-1, width, 1, ' ')
	tb.PrintAt(1, height-1, fmt.Sprintf(" Status: %s", message))
}

func main() {
	if err := tb.Init(); err != nil {
		log.Fatal(err)
	}
	defer tb.Close()

	info := getSystemInfo()
	mouseEnabled := false
	status := "Ready - Press keys to interact"

	for {
		tb.Clear()
		width, height := tb.Size()

		drawHeader(width)

		y := drawSystemInfo(info, 4)

		drawMemoryUsage(info, 55, 4)
		drawDiskUsage(info, 55, 11)

		tb.SaveBuffer()

		tb.SetColor(6, 0)
		tb.PrintAt(3, y, "Buffer saved - demonstrating save/restore")
		tb.Present()
		time.Sleep(500 * time.Millisecond)

		tb.RestoreBuffer()

		controlY := drawControls(height-4, width)

		tb.SetColor(11, 0)
		mouseStatus := "OFF"
		if mouseEnabled {
			mouseStatus = "ON"
		}
		tb.PrintAt(2, controlY, fmt.Sprintf("Mouse: %s | Terminal: %dx%d | Raw Mode: %t",
			mouseStatus, width, height, tb.IsRawMode()))

		drawStatusLine(status, width, height)

		tb.Present()

		event, err := tb.PollEventTimeout(time.Second)
		if err != nil && err.Error() == "timeout" {
			info.CurrentTime = time.Now().Format("2006-01-02 15:04:05")
			status = "Clock updated"
			continue
		} else if err != nil {
			status = "Error: " + err.Error()
			continue
		}

		switch event.Type {
		case tb.EventKey:
			switch event.Ch {
			case 'q', 'Q':
				return
			case 'r', 'R':
				info = getSystemInfo()
				status = "System information refreshed"
			case 'm', 'M':
				if mouseEnabled {
					tb.DisableMouseFunc()
					mouseEnabled = false
					status = "Mouse disabled"
				} else {
					tb.EnableMouseFunc()
					mouseEnabled = true
					status = "Mouse enabled - try clicking!"
				}
			case 's', 'S':
				status = "Suspending... (Ctrl+Z will work too)"
				tb.Present()
				time.Sleep(500 * time.Millisecond)
				tb.Suspend()
				status = "Resumed from suspension"
			case 'b', 'B':
				tb.Bell()
				status = "Bell rung"
			default:
				if event.Ch != 0 {
					status = fmt.Sprintf("Key pressed: '%c' (code: %d)", event.Ch, event.Ch)
				}
			}

			switch event.Key {
			case tb.KeyCtrlC:
				return
			case tb.KeyArrowUp:
				tb.Scroll(-1)
				status = "Scrolled up"
			case tb.KeyArrowDown:
				tb.Scroll(1)
				status = "Scrolled down"
			case tb.KeyArrowLeft, tb.KeyArrowRight:
				status = fmt.Sprintf("Arrow key: %v", event.Key)
			case tb.KeyEnter:
				x, y := tb.GetCursorPos()
				status = fmt.Sprintf("Cursor position: %d,%d", x, y)
			}

		case tb.EventMouse:
			buttonName := map[tb.MouseButton]string{
				tb.MouseLeft:      "LEFT",
				tb.MouseMiddle:    "MIDDLE",
				tb.MouseRight:     "RIGHT",
				tb.MouseWheelUp:   "WHEEL_UP",
				tb.MouseWheelDown: "WHEEL_DOWN",
			}[event.Button]

			if buttonName == "" {
				buttonName = "UNKNOWN"
			}

			status = fmt.Sprintf("Mouse %s at (%d,%d)", buttonName, event.X, event.Y)

		case tb.EventResize:
			status = fmt.Sprintf("Terminal resized to %dx%d", width, height)

		case tb.EventPaste:
			status = "Paste event detected"
		}
	}
}