360 lines
9.5 KiB
Go
360 lines
9.5 KiB
Go
package config
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"vrc_osc_go/internal/common"
|
|
)
|
|
|
|
type Config struct {
|
|
OSC OSCConfig
|
|
VrcLog VrcLogConfig
|
|
GUI GUIConfig
|
|
}
|
|
|
|
type OSCConfig struct {
|
|
Host string
|
|
Port int
|
|
}
|
|
|
|
type VrcLogConfig struct {
|
|
SelfName string
|
|
GuestNames []string
|
|
GuestFile string
|
|
StaffNames []string
|
|
MissingCount int
|
|
LogPatterns []string
|
|
}
|
|
|
|
type GUIConfig struct {
|
|
TopMost bool
|
|
FontSize int
|
|
AutoUnmuteOnSelfLeave bool
|
|
RefreshIntervalValue int
|
|
RefreshIntervalUnit string
|
|
HistoryRegex string
|
|
HistoryFromDate string
|
|
HistoryToDate string
|
|
HistoryExportDir string
|
|
HistoryExportIncludeTime bool
|
|
HistoryExportIncludeWorld bool
|
|
HistoryExportIncludeJoinLeave bool
|
|
HistoryExportCustomEnabled bool
|
|
HistoryExportCustom string
|
|
}
|
|
|
|
func Load(path string) (*Config, error) {
|
|
cfg := &Config{
|
|
OSC: OSCConfig{Host: "127.0.0.1", Port: 9001},
|
|
GUI: GUIConfig{
|
|
FontSize: 18,
|
|
RefreshIntervalValue: 2,
|
|
RefreshIntervalUnit: "sec",
|
|
HistoryExportIncludeTime: true,
|
|
HistoryExportIncludeWorld: true,
|
|
HistoryExportIncludeJoinLeave: true,
|
|
},
|
|
}
|
|
if path == "" {
|
|
path = filepath.Join(common.RootDir(), "config", "config.toml")
|
|
} else if !filepath.IsAbs(path) {
|
|
path = filepath.Join(common.RootDir(), path)
|
|
}
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return cfg, nil
|
|
}
|
|
defer file.Close()
|
|
|
|
var section string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
line = strings.TrimPrefix(line, "\ufeff")
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
|
section = strings.Trim(line, "[]")
|
|
continue
|
|
}
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
key := strings.TrimSpace(parts[0])
|
|
val := parseConfigValue(parts[1])
|
|
switch section {
|
|
case "osc":
|
|
switch key {
|
|
case "host":
|
|
cfg.OSC.Host = val
|
|
case "port":
|
|
if n, err := strconv.Atoi(val); err == nil {
|
|
cfg.OSC.Port = n
|
|
}
|
|
}
|
|
case "self":
|
|
if key == "name" {
|
|
cfg.VrcLog.SelfName = val
|
|
}
|
|
case "notice":
|
|
if key == "missing_count" {
|
|
if n, err := strconv.Atoi(val); err == nil {
|
|
cfg.VrcLog.MissingCount = n
|
|
}
|
|
}
|
|
case "vrc_log":
|
|
if key == "patterns" {
|
|
cfg.VrcLog.LogPatterns = parseList(val)
|
|
}
|
|
case "staff":
|
|
if key == "names" {
|
|
cfg.VrcLog.StaffNames = parseList(val)
|
|
}
|
|
case "guest":
|
|
switch key {
|
|
case "names":
|
|
cfg.VrcLog.GuestNames = parseList(val)
|
|
case "file":
|
|
cfg.VrcLog.GuestFile = val
|
|
}
|
|
case "gui":
|
|
switch key {
|
|
case "top_most":
|
|
cfg.GUI.TopMost = parseBool(val, cfg.GUI.TopMost)
|
|
case "font_size":
|
|
if n, err := strconv.Atoi(val); err == nil && n > 0 {
|
|
cfg.GUI.FontSize = n
|
|
}
|
|
case "auto_unmute_on_self_leave":
|
|
cfg.GUI.AutoUnmuteOnSelfLeave = parseBool(val, cfg.GUI.AutoUnmuteOnSelfLeave)
|
|
case "refresh_interval_value":
|
|
if n, err := strconv.Atoi(val); err == nil && n > 0 {
|
|
cfg.GUI.RefreshIntervalValue = n
|
|
}
|
|
case "refresh_interval_unit":
|
|
cfg.GUI.RefreshIntervalUnit = normalizeRefreshIntervalUnit(val)
|
|
case "history_regex":
|
|
cfg.GUI.HistoryRegex = val
|
|
case "history_from":
|
|
cfg.GUI.HistoryFromDate = normalizeHistoryDate(val)
|
|
case "history_to":
|
|
cfg.GUI.HistoryToDate = normalizeHistoryDate(val)
|
|
case "history_export_dir":
|
|
cfg.GUI.HistoryExportDir = strings.TrimSpace(val)
|
|
case "history_export_include_time":
|
|
cfg.GUI.HistoryExportIncludeTime = parseBool(val, true)
|
|
case "history_export_include_world":
|
|
cfg.GUI.HistoryExportIncludeWorld = parseBool(val, true)
|
|
case "history_export_include_join_leave":
|
|
cfg.GUI.HistoryExportIncludeJoinLeave = parseBool(val, true)
|
|
case "history_export_custom_enabled":
|
|
cfg.GUI.HistoryExportCustomEnabled = parseBool(val, false)
|
|
case "history_export_custom":
|
|
cfg.GUI.HistoryExportCustom = strings.TrimSpace(val)
|
|
}
|
|
}
|
|
}
|
|
if cfg.GUI.RefreshIntervalValue <= 0 {
|
|
cfg.GUI.RefreshIntervalValue = 2
|
|
}
|
|
cfg.GUI.RefreshIntervalUnit = normalizeRefreshIntervalUnit(cfg.GUI.RefreshIntervalUnit)
|
|
cfg.GUI.HistoryFromDate = normalizeHistoryDate(cfg.GUI.HistoryFromDate)
|
|
cfg.GUI.HistoryToDate = normalizeHistoryDate(cfg.GUI.HistoryToDate)
|
|
cfg.GUI.HistoryExportDir = strings.TrimSpace(cfg.GUI.HistoryExportDir)
|
|
cfg.GUI.HistoryExportCustom = strings.TrimSpace(cfg.GUI.HistoryExportCustom)
|
|
if cfg.VrcLog.GuestFile != "" {
|
|
if !filepath.IsAbs(cfg.VrcLog.GuestFile) {
|
|
cfg.VrcLog.GuestFile = filepath.Join(common.RootDir(), cfg.VrcLog.GuestFile)
|
|
}
|
|
if names, err := loadLines(cfg.VrcLog.GuestFile); err == nil {
|
|
cfg.VrcLog.GuestNames = names
|
|
}
|
|
}
|
|
return cfg, scanner.Err()
|
|
}
|
|
|
|
func parseConfigValue(raw string) string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return ""
|
|
}
|
|
if unquoted, err := strconv.Unquote(raw); err == nil {
|
|
return strings.TrimSpace(unquoted)
|
|
}
|
|
return strings.TrimSpace(strings.Trim(raw, "\"'"))
|
|
}
|
|
|
|
func SaveGUI(path string, gui GUIConfig) error {
|
|
path = resolvePath(path)
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
content := upsertGUISection(string(data), gui)
|
|
return os.WriteFile(path, []byte(content), 0o644)
|
|
}
|
|
|
|
func parseList(value string) []string {
|
|
value = strings.TrimSpace(value)
|
|
value = strings.TrimPrefix(value, "[")
|
|
value = strings.TrimSuffix(value, "]")
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(value, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(strings.Trim(part, "\"'"))
|
|
if part != "" {
|
|
out = append(out, part)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseBool(value string, fallback bool) bool {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case "true", "1", "on", "yes":
|
|
return true
|
|
case "false", "0", "off", "no":
|
|
return false
|
|
default:
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
func resolvePath(path string) string {
|
|
if path == "" {
|
|
return filepath.Join(common.RootDir(), "config", "config.toml")
|
|
}
|
|
if !filepath.IsAbs(path) {
|
|
return filepath.Join(common.RootDir(), path)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func upsertGUISection(existing string, gui GUIConfig) string {
|
|
normalized := strings.ReplaceAll(existing, "\r\n", "\n")
|
|
lines := strings.Split(normalized, "\n")
|
|
out := make([]string, 0, len(lines)+6)
|
|
inGUI := false
|
|
replaced := false
|
|
for _, raw := range lines {
|
|
line := raw
|
|
trimmed := strings.TrimSpace(strings.TrimPrefix(line, "\ufeff"))
|
|
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
|
section := strings.Trim(trimmed, "[]")
|
|
if section == "gui" {
|
|
if !replaced {
|
|
appendGUISection(&out, gui)
|
|
replaced = true
|
|
}
|
|
inGUI = true
|
|
continue
|
|
}
|
|
if inGUI {
|
|
inGUI = false
|
|
}
|
|
out = append(out, line)
|
|
continue
|
|
}
|
|
if inGUI {
|
|
continue
|
|
}
|
|
out = append(out, line)
|
|
}
|
|
if !replaced {
|
|
if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) != "" {
|
|
out = append(out, "")
|
|
}
|
|
appendGUISection(&out, gui)
|
|
}
|
|
content := strings.Join(out, "\n")
|
|
content = strings.TrimRight(content, "\n")
|
|
return content + "\n"
|
|
}
|
|
|
|
func appendGUISection(out *[]string, gui GUIConfig) {
|
|
gui.RefreshIntervalUnit = normalizeRefreshIntervalUnit(gui.RefreshIntervalUnit)
|
|
if gui.RefreshIntervalValue <= 0 {
|
|
gui.RefreshIntervalValue = 2
|
|
}
|
|
*out = append(*out,
|
|
"[gui]",
|
|
fmt.Sprintf("top_most = %t", gui.TopMost),
|
|
fmt.Sprintf("font_size = %d", gui.FontSize),
|
|
fmt.Sprintf("auto_unmute_on_self_leave = %t", gui.AutoUnmuteOnSelfLeave),
|
|
fmt.Sprintf("refresh_interval_value = %d", gui.RefreshIntervalValue),
|
|
fmt.Sprintf("refresh_interval_unit = %q", gui.RefreshIntervalUnit),
|
|
fmt.Sprintf("history_regex = %q", gui.HistoryRegex),
|
|
fmt.Sprintf("history_from = %q", gui.HistoryFromDate),
|
|
fmt.Sprintf("history_to = %q", gui.HistoryToDate),
|
|
fmt.Sprintf("history_export_dir = %q", strings.TrimSpace(gui.HistoryExportDir)),
|
|
fmt.Sprintf("history_export_include_time = %t", gui.HistoryExportIncludeTime),
|
|
fmt.Sprintf("history_export_include_world = %t", gui.HistoryExportIncludeWorld),
|
|
fmt.Sprintf("history_export_include_join_leave = %t", gui.HistoryExportIncludeJoinLeave),
|
|
fmt.Sprintf("history_export_custom_enabled = %t", gui.HistoryExportCustomEnabled),
|
|
fmt.Sprintf("history_export_custom = %q", strings.TrimSpace(gui.HistoryExportCustom)),
|
|
)
|
|
}
|
|
|
|
func normalizeRefreshIntervalUnit(unit string) string {
|
|
switch strings.ToLower(strings.TrimSpace(unit)) {
|
|
case "min", "minute", "minutes":
|
|
return "min"
|
|
default:
|
|
return "sec"
|
|
}
|
|
}
|
|
|
|
func (g GUIConfig) RefreshInterval() time.Duration {
|
|
value := g.RefreshIntervalValue
|
|
if value <= 0 {
|
|
value = 2
|
|
}
|
|
switch normalizeRefreshIntervalUnit(g.RefreshIntervalUnit) {
|
|
case "min":
|
|
return time.Duration(value) * time.Minute
|
|
default:
|
|
return time.Duration(value) * time.Second
|
|
}
|
|
}
|
|
|
|
func normalizeHistoryDate(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
if _, err := time.Parse("2006-01-02", value); err != nil {
|
|
return ""
|
|
}
|
|
return value
|
|
}
|
|
|
|
func loadLines(path string) ([]string, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lines := strings.Split(string(data), "\n")
|
|
out := make([]string, 0, len(lines))
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(strings.TrimPrefix(line, "\ufeff"))
|
|
if line != "" && !strings.HasPrefix(line, "#") {
|
|
out = append(out, line)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|