mirror of
https://github.com/uprightbass360/AzerothCore-RealmMaster.git
synced 2026-01-13 00:58:34 +00:00
feat: comprehensive module system and database management improvements
This commit introduces major enhancements to the module installation system, database management, and configuration handling for AzerothCore deployments. ## Module System Improvements ### Module SQL Staging & Installation - Refactor module SQL staging to properly handle AzerothCore's sql/ directory structure - Fix SQL staging path to use correct AzerothCore format (sql/custom/db_*/*) - Implement conditional module database importing based on enabled modules - Add support for both cpp-modules and lua-scripts module types - Handle rsync exit code 23 (permission warnings) gracefully during deployment ### Module Manifest & Automation - Add automated module manifest generation via GitHub Actions workflow - Implement Python-based module manifest updater with comprehensive validation - Add module dependency tracking and SQL file discovery - Support for blocked modules and module metadata management ## Database Management Enhancements ### Database Import System - Add db-guard container for continuous database health monitoring and verification - Implement conditional database import that skips when databases are current - Add backup restoration and SQL staging coordination - Support for Playerbots database (4th database) in all import operations - Add comprehensive database health checking and status reporting ### Database Configuration - Implement 10 new dbimport.conf settings from environment variables: - Database.Reconnect.Seconds/Attempts for connection reliability - Updates.AllowedModules for module auto-update control - Updates.Redundancy for data integrity checks - Worker/Synch thread settings for all three core databases - Auto-apply dbimport.conf settings via auto-post-install.sh - Add environment variable injection for db-import and db-guard containers ### Backup & Recovery - Fix backup scheduler to prevent immediate execution on container startup - Add backup status monitoring script with detailed reporting - Implement backup import/export utilities - Add database verification scripts for SQL update tracking ## User Import Directory - Add new import/ directory for user-provided database files and configurations - Support for custom SQL files, configuration overrides, and example templates - Automatic import of user-provided databases and configs during initialization - Documentation and examples for custom database imports ## Configuration & Environment - Eliminate CLIENT_DATA_VERSION warning by adding default value syntax - Improve CLIENT_DATA_VERSION documentation in .env.template - Add comprehensive database import settings to .env and .env.template - Update setup.sh to handle new configuration variables with proper defaults ## Monitoring & Debugging - Add status dashboard with Go-based terminal UI (statusdash.go) - Implement JSON status output (statusjson.sh) for programmatic access - Add comprehensive database health check script - Add repair-storage-permissions.sh utility for permission issues ## Testing & Documentation - Add Phase 1 integration test suite for module installation verification - Add comprehensive documentation for: - Database management (DATABASE_MANAGEMENT.md) - Module SQL analysis (AZEROTHCORE_MODULE_SQL_ANALYSIS.md) - Implementation mapping (IMPLEMENTATION_MAP.md) - SQL staging comparison and path coverage - Module assets and DBC file requirements - Update SCRIPTS.md, ADVANCED.md, and troubleshooting documentation - Update references from database-import/ to import/ directory ## Breaking Changes - Renamed database-import/ directory to import/ for clarity - Module SQL files now staged to AzerothCore-compatible paths - db-guard container now required for proper database lifecycle management ## Bug Fixes - Fix module SQL staging directory structure for AzerothCore compatibility - Handle rsync exit code 23 gracefully during deployments - Prevent backup from running immediately on container startup - Correct SQL staging paths for proper module installation
This commit is contained in:
10
scripts/go/go.mod
Normal file
10
scripts/go/go.mod
Normal file
@@ -0,0 +1,10 @@
|
||||
module acore-compose/statusdash
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/gizak/termui/v3 v3.1.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.2 // indirect
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
|
||||
)
|
||||
8
scripts/go/go.sum
Normal file
8
scripts/go/go.sum
Normal file
@@ -0,0 +1,8 @@
|
||||
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
|
||||
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
|
||||
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
|
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||
373
scripts/go/statusdash.go
Normal file
373
scripts/go/statusdash.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ui "github.com/gizak/termui/v3"
|
||||
"github.com/gizak/termui/v3/widgets"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Status string `json:"status"`
|
||||
Health string `json:"health"`
|
||||
StartedAt string `json:"started_at"`
|
||||
Image string `json:"image"`
|
||||
ExitCode string `json:"exit_code"`
|
||||
}
|
||||
|
||||
type ContainerStats struct {
|
||||
CPU float64 `json:"cpu"`
|
||||
Memory string `json:"memory"`
|
||||
MemoryPercent float64 `json:"memory_percent"`
|
||||
}
|
||||
|
||||
type Port struct {
|
||||
Name string `json:"name"`
|
||||
Port string `json:"port"`
|
||||
Reachable bool `json:"reachable"`
|
||||
}
|
||||
|
||||
type DirInfo struct {
|
||||
Path string `json:"path"`
|
||||
Exists bool `json:"exists"`
|
||||
Size string `json:"size"`
|
||||
}
|
||||
|
||||
type VolumeInfo struct {
|
||||
Name string `json:"name"`
|
||||
Exists bool `json:"exists"`
|
||||
Mountpoint string `json:"mountpoint"`
|
||||
}
|
||||
|
||||
type UserStats struct {
|
||||
Accounts int `json:"accounts"`
|
||||
Online int `json:"online"`
|
||||
Characters int `json:"characters"`
|
||||
Active7d int `json:"active7d"`
|
||||
}
|
||||
|
||||
type Module struct {
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Project string `json:"project"`
|
||||
Network string `json:"network"`
|
||||
Services []Service `json:"services"`
|
||||
Ports []Port `json:"ports"`
|
||||
Modules []Module `json:"modules"`
|
||||
Storage map[string]DirInfo `json:"storage"`
|
||||
Volumes map[string]VolumeInfo `json:"volumes"`
|
||||
Users UserStats `json:"users"`
|
||||
Stats map[string]ContainerStats `json:"stats"`
|
||||
}
|
||||
|
||||
func runSnapshot() (*Snapshot, error) {
|
||||
cmd := exec.Command("./scripts/bash/statusjson.sh")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
snap := &Snapshot{}
|
||||
if err := json.Unmarshal(output, snap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
func buildServicesTable(s *Snapshot) *TableNoCol {
|
||||
table := NewTableNoCol()
|
||||
rows := [][]string{{"Service", "Status", "Health", "CPU%", "Memory"}}
|
||||
for _, svc := range s.Services {
|
||||
cpu := "-"
|
||||
mem := "-"
|
||||
if stats, ok := s.Stats[svc.Name]; ok {
|
||||
cpu = fmt.Sprintf("%.1f", stats.CPU)
|
||||
mem = strings.Split(stats.Memory, " / ")[0] // Just show used, not total
|
||||
}
|
||||
// Combine health with exit code for stopped containers
|
||||
health := svc.Health
|
||||
if svc.Status != "running" && svc.ExitCode != "0" && svc.ExitCode != "" {
|
||||
health = fmt.Sprintf("%s (%s)", svc.Health, svc.ExitCode)
|
||||
}
|
||||
rows = append(rows, []string{svc.Label, svc.Status, health, cpu, mem})
|
||||
}
|
||||
table.Rows = rows
|
||||
table.RowSeparator = false
|
||||
table.Border = true
|
||||
table.Title = "Services"
|
||||
return table
|
||||
}
|
||||
|
||||
func buildPortsTable(s *Snapshot) *TableNoCol {
|
||||
table := NewTableNoCol()
|
||||
rows := [][]string{{"Port", "Number", "Reachable"}}
|
||||
for _, p := range s.Ports {
|
||||
state := "down"
|
||||
if p.Reachable {
|
||||
state = "up"
|
||||
}
|
||||
rows = append(rows, []string{p.Name, p.Port, state})
|
||||
}
|
||||
table.Rows = rows
|
||||
table.RowSeparator = true
|
||||
table.Border = true
|
||||
table.Title = "Ports"
|
||||
return table
|
||||
}
|
||||
|
||||
func buildModulesList(s *Snapshot) *widgets.List {
|
||||
list := widgets.NewList()
|
||||
list.Title = fmt.Sprintf("Modules (%d)", len(s.Modules))
|
||||
rows := make([]string, len(s.Modules))
|
||||
for i, mod := range s.Modules {
|
||||
rows[i] = mod.Name
|
||||
}
|
||||
list.Rows = rows
|
||||
list.WrapText = false
|
||||
list.Border = true
|
||||
list.BorderStyle = ui.NewStyle(ui.ColorCyan)
|
||||
list.SelectedRowStyle = ui.NewStyle(ui.ColorCyan)
|
||||
return list
|
||||
}
|
||||
|
||||
func buildStorageParagraph(s *Snapshot) *widgets.Paragraph {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "STORAGE:\n")
|
||||
entries := []struct {
|
||||
Key string
|
||||
Label string
|
||||
}{
|
||||
{"storage", "Storage"},
|
||||
{"local_storage", "Local Storage"},
|
||||
{"client_data", "Client Data"},
|
||||
{"modules", "Modules"},
|
||||
{"local_modules", "Local Modules"},
|
||||
}
|
||||
for _, item := range entries {
|
||||
info, ok := s.Storage[item.Key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
mark := "○"
|
||||
if info.Exists {
|
||||
mark = "●"
|
||||
}
|
||||
fmt.Fprintf(&b, " %-15s %s %s (%s)\n", item.Label, mark, info.Path, info.Size)
|
||||
}
|
||||
par := widgets.NewParagraph()
|
||||
par.Title = "Storage"
|
||||
par.Text = b.String()
|
||||
par.Border = true
|
||||
par.BorderStyle = ui.NewStyle(ui.ColorYellow)
|
||||
return par
|
||||
}
|
||||
|
||||
func buildVolumesParagraph(s *Snapshot) *widgets.Paragraph {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "VOLUMES:\n")
|
||||
entries := []struct {
|
||||
Key string
|
||||
Label string
|
||||
}{
|
||||
{"client_cache", "Client Cache"},
|
||||
{"mysql_data", "MySQL Data"},
|
||||
}
|
||||
for _, item := range entries {
|
||||
info, ok := s.Volumes[item.Key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
mark := "○"
|
||||
if info.Exists {
|
||||
mark = "●"
|
||||
}
|
||||
fmt.Fprintf(&b, " %-13s %s %s\n", item.Label, mark, info.Mountpoint)
|
||||
}
|
||||
par := widgets.NewParagraph()
|
||||
par.Title = "Volumes"
|
||||
par.Text = b.String()
|
||||
par.Border = true
|
||||
par.BorderStyle = ui.NewStyle(ui.ColorYellow)
|
||||
return par
|
||||
}
|
||||
|
||||
func renderSnapshot(s *Snapshot, selectedModule int) (*widgets.List, *ui.Grid) {
|
||||
servicesTable := buildServicesTable(s)
|
||||
for i := 1; i < len(servicesTable.Rows); i++ {
|
||||
if servicesTable.RowStyles == nil {
|
||||
servicesTable.RowStyles = make(map[int]ui.Style)
|
||||
}
|
||||
state := strings.ToLower(servicesTable.Rows[i][1])
|
||||
switch state {
|
||||
case "running", "healthy":
|
||||
servicesTable.RowStyles[i] = ui.NewStyle(ui.ColorGreen)
|
||||
case "restarting", "unhealthy":
|
||||
servicesTable.RowStyles[i] = ui.NewStyle(ui.ColorRed)
|
||||
case "exited":
|
||||
servicesTable.RowStyles[i] = ui.NewStyle(ui.ColorYellow)
|
||||
default:
|
||||
servicesTable.RowStyles[i] = ui.NewStyle(ui.ColorWhite)
|
||||
}
|
||||
}
|
||||
portsTable := buildPortsTable(s)
|
||||
for i := 1; i < len(portsTable.Rows); i++ {
|
||||
if portsTable.RowStyles == nil {
|
||||
portsTable.RowStyles = make(map[int]ui.Style)
|
||||
}
|
||||
if portsTable.Rows[i][2] == "up" {
|
||||
portsTable.RowStyles[i] = ui.NewStyle(ui.ColorGreen)
|
||||
} else {
|
||||
portsTable.RowStyles[i] = ui.NewStyle(ui.ColorRed)
|
||||
}
|
||||
}
|
||||
modulesList := buildModulesList(s)
|
||||
if selectedModule >= 0 && selectedModule < len(modulesList.Rows) {
|
||||
modulesList.SelectedRow = selectedModule
|
||||
}
|
||||
helpPar := widgets.NewParagraph()
|
||||
helpPar.Title = "Controls"
|
||||
helpPar.Text = " ↓ : Down\n ↑ : Up"
|
||||
helpPar.Border = true
|
||||
helpPar.BorderStyle = ui.NewStyle(ui.ColorMagenta)
|
||||
|
||||
moduleInfoPar := widgets.NewParagraph()
|
||||
moduleInfoPar.Title = "Module Info"
|
||||
if selectedModule >= 0 && selectedModule < len(s.Modules) {
|
||||
mod := s.Modules[selectedModule]
|
||||
moduleInfoPar.Text = fmt.Sprintf("%s\n\nCategory: %s\nType: %s", mod.Description, mod.Category, mod.Type)
|
||||
} else {
|
||||
moduleInfoPar.Text = "Select a module to view info"
|
||||
}
|
||||
moduleInfoPar.Border = true
|
||||
moduleInfoPar.BorderStyle = ui.NewStyle(ui.ColorMagenta)
|
||||
storagePar := buildStorageParagraph(s)
|
||||
storagePar.Border = true
|
||||
storagePar.BorderStyle = ui.NewStyle(ui.ColorYellow)
|
||||
storagePar.PaddingLeft = 1
|
||||
storagePar.PaddingRight = 1
|
||||
volumesPar := buildVolumesParagraph(s)
|
||||
|
||||
header := widgets.NewParagraph()
|
||||
header.Text = fmt.Sprintf("Project: %s\nNetwork: %s\nUpdated: %s", s.Project, s.Network, s.Timestamp)
|
||||
header.Border = true
|
||||
|
||||
usersPar := widgets.NewParagraph()
|
||||
usersPar.Text = fmt.Sprintf("USERS:\n Accounts: %d\n Online: %d\n Characters: %d\n Active 7d: %d", s.Users.Accounts, s.Users.Online, s.Users.Characters, s.Users.Active7d)
|
||||
usersPar.Border = true
|
||||
|
||||
grid := ui.NewGrid()
|
||||
termWidth, termHeight := ui.TerminalDimensions()
|
||||
grid.SetRect(0, 0, termWidth, termHeight)
|
||||
grid.Set(
|
||||
ui.NewRow(0.18,
|
||||
ui.NewCol(0.6, header),
|
||||
ui.NewCol(0.4, usersPar),
|
||||
),
|
||||
ui.NewRow(0.42,
|
||||
ui.NewCol(0.6, servicesTable),
|
||||
ui.NewCol(0.4, portsTable),
|
||||
),
|
||||
ui.NewRow(0.40,
|
||||
ui.NewCol(0.25, modulesList),
|
||||
ui.NewCol(0.15,
|
||||
ui.NewRow(0.30, helpPar),
|
||||
ui.NewRow(0.70, moduleInfoPar),
|
||||
),
|
||||
ui.NewCol(0.6,
|
||||
ui.NewRow(0.55,
|
||||
ui.NewCol(1.0, storagePar),
|
||||
),
|
||||
ui.NewRow(0.45,
|
||||
ui.NewCol(1.0, volumesPar),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
ui.Render(grid)
|
||||
return modulesList, grid
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := ui.Init(); err != nil {
|
||||
log.Fatalf("failed to init termui: %v", err)
|
||||
}
|
||||
defer ui.Close()
|
||||
|
||||
snapshot, err := runSnapshot()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to fetch snapshot: %v", err)
|
||||
}
|
||||
selectedModule := 0
|
||||
modulesWidget, currentGrid := renderSnapshot(snapshot, selectedModule)
|
||||
|
||||
snapCh := make(chan *Snapshot, 1)
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
snap, err := runSnapshot()
|
||||
if err != nil {
|
||||
log.Printf("snapshot error: %v", err)
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case snapCh <- snap:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
events := ui.PollEvents()
|
||||
for {
|
||||
select {
|
||||
case e := <-events:
|
||||
switch e.ID {
|
||||
case "q", "<C-c>":
|
||||
return
|
||||
case "<Down>", "j":
|
||||
if selectedModule < len(snapshot.Modules)-1 {
|
||||
selectedModule++
|
||||
modulesWidget, currentGrid = renderSnapshot(snapshot, selectedModule)
|
||||
}
|
||||
case "<Up>", "k":
|
||||
if selectedModule > 0 {
|
||||
selectedModule--
|
||||
modulesWidget, currentGrid = renderSnapshot(snapshot, selectedModule)
|
||||
}
|
||||
case "<Resize>":
|
||||
modulesWidget, currentGrid = renderSnapshot(snapshot, selectedModule)
|
||||
continue
|
||||
}
|
||||
if modulesWidget != nil {
|
||||
if selectedModule >= 0 && selectedModule < len(modulesWidget.Rows) {
|
||||
modulesWidget.SelectedRow = selectedModule
|
||||
}
|
||||
}
|
||||
if currentGrid != nil {
|
||||
ui.Render(currentGrid)
|
||||
}
|
||||
case snap := <-snapCh:
|
||||
snapshot = snap
|
||||
if selectedModule >= len(snapshot.Modules) {
|
||||
selectedModule = len(snapshot.Modules) - 1
|
||||
if selectedModule < 0 {
|
||||
selectedModule = 0
|
||||
}
|
||||
}
|
||||
modulesWidget, currentGrid = renderSnapshot(snapshot, selectedModule)
|
||||
}
|
||||
}
|
||||
}
|
||||
101
scripts/go/table_nocol.go
Normal file
101
scripts/go/table_nocol.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
ui "github.com/gizak/termui/v3"
|
||||
"github.com/gizak/termui/v3/widgets"
|
||||
)
|
||||
|
||||
// TableNoCol is a modified table widget that doesn't draw column separators
|
||||
type TableNoCol struct {
|
||||
widgets.Table
|
||||
}
|
||||
|
||||
func NewTableNoCol() *TableNoCol {
|
||||
t := &TableNoCol{}
|
||||
t.Table = *widgets.NewTable()
|
||||
return t
|
||||
}
|
||||
|
||||
// Draw overrides the default Draw to skip column separators
|
||||
func (self *TableNoCol) Draw(buf *ui.Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
if len(self.Rows) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
self.ColumnResizer()
|
||||
|
||||
columnWidths := self.ColumnWidths
|
||||
if len(columnWidths) == 0 {
|
||||
columnCount := len(self.Rows[0])
|
||||
columnWidth := self.Inner.Dx() / columnCount
|
||||
for i := 0; i < columnCount; i++ {
|
||||
columnWidths = append(columnWidths, columnWidth)
|
||||
}
|
||||
}
|
||||
|
||||
yCoordinate := self.Inner.Min.Y
|
||||
|
||||
// draw rows
|
||||
for i := 0; i < len(self.Rows) && yCoordinate < self.Inner.Max.Y; i++ {
|
||||
row := self.Rows[i]
|
||||
colXCoordinate := self.Inner.Min.X
|
||||
|
||||
rowStyle := self.TextStyle
|
||||
// get the row style if one exists
|
||||
if style, ok := self.RowStyles[i]; ok {
|
||||
rowStyle = style
|
||||
}
|
||||
|
||||
if self.FillRow {
|
||||
blankCell := ui.NewCell(' ', rowStyle)
|
||||
buf.Fill(blankCell, image.Rect(self.Inner.Min.X, yCoordinate, self.Inner.Max.X, yCoordinate+1))
|
||||
}
|
||||
|
||||
// draw row cells
|
||||
for j := 0; j < len(row); j++ {
|
||||
col := ui.ParseStyles(row[j], rowStyle)
|
||||
// draw row cell
|
||||
if len(col) > columnWidths[j] || self.TextAlignment == ui.AlignLeft {
|
||||
for _, cx := range ui.BuildCellWithXArray(col) {
|
||||
k, cell := cx.X, cx.Cell
|
||||
if k == columnWidths[j] || colXCoordinate+k == self.Inner.Max.X {
|
||||
cell.Rune = ui.ELLIPSES
|
||||
buf.SetCell(cell, image.Pt(colXCoordinate+k-1, yCoordinate))
|
||||
break
|
||||
} else {
|
||||
buf.SetCell(cell, image.Pt(colXCoordinate+k, yCoordinate))
|
||||
}
|
||||
}
|
||||
} else if self.TextAlignment == ui.AlignCenter {
|
||||
xCoordinateOffset := (columnWidths[j] - len(col)) / 2
|
||||
stringXCoordinate := xCoordinateOffset + colXCoordinate
|
||||
for _, cx := range ui.BuildCellWithXArray(col) {
|
||||
k, cell := cx.X, cx.Cell
|
||||
buf.SetCell(cell, image.Pt(stringXCoordinate+k, yCoordinate))
|
||||
}
|
||||
} else if self.TextAlignment == ui.AlignRight {
|
||||
stringXCoordinate := ui.MinInt(colXCoordinate+columnWidths[j], self.Inner.Max.X) - len(col)
|
||||
for _, cx := range ui.BuildCellWithXArray(col) {
|
||||
k, cell := cx.X, cx.Cell
|
||||
buf.SetCell(cell, image.Pt(stringXCoordinate+k, yCoordinate))
|
||||
}
|
||||
}
|
||||
colXCoordinate += columnWidths[j] + 1
|
||||
}
|
||||
|
||||
// SKIP drawing vertical separators - this is the key change
|
||||
|
||||
yCoordinate++
|
||||
|
||||
// draw horizontal separator
|
||||
horizontalCell := ui.NewCell(ui.HORIZONTAL_LINE, self.Block.BorderStyle)
|
||||
if self.RowSeparator && yCoordinate < self.Inner.Max.Y && i != len(self.Rows)-1 {
|
||||
buf.Fill(horizontalCell, image.Rect(self.Inner.Min.X, yCoordinate, self.Inner.Max.X, yCoordinate+1))
|
||||
yCoordinate++
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user