1
0
Fork 0
mirror of https://github.com/Luzifer/runemetrics.git synced 2024-12-22 12:11:20 +00:00
runemetrics/main.go
Knut Ahlers f6d22c9e35
Add editing for target level setting
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2019-09-28 22:47:18 +02:00

345 lines
8.3 KiB
Go

package main
import (
"fmt"
"math"
"os"
"strconv"
"strings"
"time"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
"github.com/gorhill/cronexpr"
log "github.com/sirupsen/logrus"
"github.com/Luzifer/rconfig/v2"
)
const (
updateKeyTotalXP = "total_xp"
updateKeyGeneral = "general"
updateKeyFeed = "feed"
)
var (
cfg = struct {
MarkerTime time.Duration `flag:"marker-time" default:"30m" description:"How long to highlight new entries"`
Update string `flag:"update" default:"* * * * *" description:"When to fetch metrics (cron syntax)"`
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
}{}
eventsPage = 0
lastUpdate = map[string]time.Time{}
playerData *playerInfo
selectedMetric = 0
inputPrompt string
inputBuffer string
version = "dev"
)
func init() {
if err := rconfig.ParseAndValidate(&cfg); err != nil {
log.Fatalf("Unable to parse commandline options: %s", err)
}
if cfg.VersionAndExit {
fmt.Printf("git-changerelease %s\n", version)
os.Exit(0)
}
if l, err := log.ParseLevel(cfg.LogLevel); err != nil {
log.WithError(err).Fatal("Unable to parse log level")
} else {
log.SetLevel(l)
}
}
func main() {
var err error
if len(rconfig.Args()) != 2 {
log.Fatal("Usage: runemetrics <player>")
}
if playerInfoCache, err = loadPlayerInfoCache(); err != nil {
log.WithError(err).Fatal("Unable to load cache")
}
if err = ui.Init(); err != nil {
log.WithError(err).Fatal("Unable to initialize termui")
}
defer ui.Close()
var (
cron = cronexpr.MustParse(cfg.Update)
player = rconfig.Args()[1]
updateTicker = time.NewTimer(0)
)
for {
select {
case evt := <-ui.PollEvents():
switch evt.ID {
case "1", "2", "3", "4", "5", "6", "7", "8", "9", "0":
if inputPrompt != "" {
inputBuffer = inputBuffer + evt.ID
}
updateUI(playerData, nil)
case "q", "<C-c>":
return
case "t":
if inputPrompt != "" {
continue
}
inputPrompt = "Enter target level"
inputBuffer = ""
updateUI(playerData, nil)
case "<C-r>":
updateTicker.Reset(0)
case "<Down>":
selectedMetric++
if selectedMetric >= len(playerData.SkillValues) {
selectedMetric = len(playerData.SkillValues) - 1
}
updateUI(playerData, nil)
case "<Enter>":
if inputPrompt == "" {
continue
}
var tlvl int
inputPrompt = ""
if inputBuffer != "" {
tlvl, err = strconv.Atoi(inputBuffer)
}
if err == nil {
if tlvl < playerData.SkillValues[selectedMetric].Level {
tlvl = 0
}
playerData.SkillValues[selectedMetric].TargetLevel = tlvl
}
updateUI(playerData, err)
case "<Escape>":
inputPrompt = ""
updateUI(playerData, nil)
case "<PageDown>":
eventsPage++
updateUI(playerData, nil)
case "<PageUp>":
eventsPage--
updateUI(playerData, nil)
case "<Resize>":
ui.Clear()
updateUI(playerData, nil)
case "<Up>":
selectedMetric--
if selectedMetric < 0 {
selectedMetric = 0
}
updateUI(playerData, nil)
}
case <-updateTicker.C:
if playerData, err = getPlayerInfo(player, 20); err != nil {
log.WithError(err).Error("Unable to fetch metrics")
}
if err := updateUI(playerData, err); err != nil {
log.WithError(err).Error("Unable to update UI")
return
}
updateTicker.Reset(time.Until(cron.Next(time.Now())))
if err := playerInfoCache.storeCache(); err != nil {
log.WithError(err).Error("Unable to write cache")
}
}
}
}
func updateUI(playerData *playerInfo, err error) error {
termWidth, termHeight := ui.TerminalDimensions()
// Status-bar
status := widgets.NewParagraph()
status.Title = "Status"
status.Text = fmt.Sprintf("Last Refresh: %s | XP Change: %s | Feed Change: %s",
lastUpdate[updateKeyGeneral].Format("15:04:05"),
lastUpdate[updateKeyTotalXP].Format("15:04:05"),
lastUpdate[updateKeyFeed].Format("15:04:05"),
)
status.SetRect(0, termHeight-3, termWidth, termHeight)
defer ui.Render(status)
if err != nil {
status.Text = fmt.Sprintf("Error: %s", err.Error())
status.BorderStyle.Fg = ui.ColorRed
if playerData == nil {
return nil
}
}
// Header
hdrText := widgets.NewParagraph()
hdrText.Title = "Player"
hdrText.Text = playerData.Name
hdrText.SetRect(0, 0, termWidth, 3)
ui.Render(hdrText)
// General stats
combatLevel := widgets.NewParagraph()
combatLevel.Title = "Combat Level"
combatLevel.Text = strconv.Itoa(playerData.CombatLevel)
totalXP := widgets.NewParagraph()
totalXP.Title = "Total XP"
totalXP.Text = strconv.FormatInt(playerData.TotalXP, 10)
totalLevel := widgets.NewParagraph()
totalLevel.Title = "Total Level"
totalLevel.Text = strconv.FormatInt(playerData.TotalSkill, 10)
rank := widgets.NewParagraph()
rank.Title = "Rank"
rank.Text = strconv.FormatInt(playerData.NumericRank(), 10)
statsGrid := ui.NewGrid()
statsGrid.SetRect(0, 3, termWidth, 6)
statsGrid.Set(
ui.NewRow(1.0,
ui.NewCol(1.0/4, combatLevel),
ui.NewCol(1.0/4, totalXP),
ui.NewCol(1.0/4, totalLevel),
ui.NewCol(1.0/4, rank),
),
)
ui.Render(statsGrid)
// Levels
levelTable := widgets.NewTable()
levelTable.Title = "Levels"
//levelTable.TextAlignment = ui.AlignRight
levelTable.RowStyles[0] = ui.Style{Fg: ui.ColorWhite, Modifier: ui.ModifierBold}
levelTable.SetRect(0, 6, termWidth, 6+2+len(playerData.SkillValues)+1)
levelTable.RowSeparator = false
levelTable.ColumnWidths = []int{termWidth - 2 - 6 - 6 - 8 - 11 - 13 - 9, 6, 8, 11, 13, 9}
levelTable.Rows = [][]string{{
" Skill",
fmt.Sprintf("%*s", 6, "Level"),
fmt.Sprintf("%*s", 8, "Level %"),
fmt.Sprintf("%*s", 11, "Current XP"),
fmt.Sprintf("%*s", 13, "XP remaining"),
fmt.Sprintf("%*s", 9, "To Level"),
}}
for i, s := range playerData.SkillValues {
var (
name = s.ID.String()
remaining = strconv.FormatInt(s.ID.Info().XPToNextLevel(s.XP/10), 10)
percentage = strconv.FormatFloat(s.ID.Info().LevelPercentage(s.XP/10), 'f', 1, 64)
target = strconv.Itoa(s.Level + 1)
rowStyle = ui.Style{Fg: ui.ColorWhite}
)
if i == selectedMetric {
name = "> " + name
} else {
name = " " + name
}
if s.TargetLevel > 0 {
remaining = strconv.FormatInt(s.ID.Info().XPToTargetLevel(s.TargetLevel, s.XP/10), 10)
percentage = strconv.FormatFloat(s.ID.Info().TargetPercentage(s.TargetLevel, s.XP/10), 'f', 1, 64)
target = strconv.Itoa(s.TargetLevel)
rowStyle.Fg = ui.ColorYellow
}
levelTable.Rows = append(levelTable.Rows, []string{
name,
fmt.Sprintf("%*s", 6, strconv.Itoa(s.Level)),
fmt.Sprintf("%*s", 8, percentage),
fmt.Sprintf("%*s", 11, strconv.FormatInt(s.XP/10, 10)),
fmt.Sprintf("%*s", 13, remaining),
fmt.Sprintf("%*s", 9, target),
})
if time.Since(s.Updated) < cfg.MarkerTime {
rowStyle.Fg = ui.ColorGreen
}
levelTable.RowStyles[i+1] = rowStyle
}
ui.Render(levelTable)
// Latest events
events := widgets.NewTable()
events.RowSeparator = false
events.ColumnWidths = []int{12, termWidth - 3 - 12}
events.SetRect(0, 6+2+len(playerData.SkillValues)+1, termWidth, termHeight-3)
eventsPerPage := termHeight - 3 - (6 + 2 + len(playerData.SkillValues) + 1)
eventPages := int(math.Ceil(float64(len(playerData.Activities)) / float64(eventsPerPage)))
if eventsPage < 0 {
eventsPage = 0
}
if eventsPage >= eventPages {
eventsPage = eventPages - 1
}
events.Title = fmt.Sprintf("Event Log (%d / %d)", eventsPage+1, eventPages)
for i, logEntry := range playerData.Activities[eventsPage*eventsPerPage:] {
date, _ := logEntry.GetParsedDate()
events.Rows = append(
events.Rows,
[]string{
date.Local().Format("01/02 15:04"),
strings.Replace(logEntry.Details, " ", " ", -1),
},
)
if time.Since(date) < cfg.MarkerTime {
events.RowStyles[i] = ui.Style{Fg: ui.ColorGreen}
}
}
ui.Render(events)
// Input box
if inputPrompt != "" {
input := widgets.NewParagraph()
input.Title = inputPrompt
input.Text = inputBuffer + "_"
inputTop := int(math.Floor(float64(termHeight-3)) / 2)
inputMargin := int(math.Floor(float64(termWidth) / 4))
input.SetRect(inputMargin, inputTop, termWidth-inputMargin, inputTop+3)
ui.Render(input)
}
return nil
}