Files
VRCWT-OSC/internal/config/config.go
messypy f9968895ac
All checks were successful
build-windows-exe / build (push) Successful in 1m31s
Allow smaller GUI font sizes
2026-07-27 23:57:57 +09:00

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: 14,
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
}