Files
tia-love/internal/config/config.go
T

161 lines
4.1 KiB
Go

// Package config — baca/tulis config tia (PRD §7.1).
//
// Sesuai prinsip ponytail: struct ini memuat subset schema PRD §7.2 yang
// SUDAH dipakai; field lain ditambahkan saat fiturnya benar-benar masuk
// (supaya config tidak pernah berisi opsi mati).
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
type Config struct {
Version int `yaml:"version"`
StacksDir string `yaml:"stacks_dir"`
UI UI `yaml:"ui"`
Docker DockerCfg `yaml:"docker"`
}
// DockerCfg — pengaturan interaksi Docker.
type DockerCfg struct {
ComposeCommand string `yaml:"compose_command"` // "docker compose" atau "docker-compose"
}
type UI struct {
Language string `yaml:"language"` // id | en
ThemeMode string `yaml:"theme_mode"` // auto | dark | light | terminal
ThemeAccent string `yaml:"theme_accent"` // 14 warna aksen Catppuccin (validasi di theme)
SoundEnabled bool `yaml:"sound_enabled"`
SoundVolume int `yaml:"sound_volume"` // 0-100
SoundMuteOnSSH bool `yaml:"sound_mute_on_ssh"`
AnimationEnabled bool `yaml:"animation_enabled"`
LogTailLines int `yaml:"log_tail_lines"` // jumlah baris log yang disimpan
ShowTimestamps bool `yaml:"show_timestamps"` // timestamp di log kontainer
}
func Default() Config {
home, _ := os.UserHomeDir()
return Config{
Version: 1,
StacksDir: filepath.Join(home, ".tia", "stacks"),
UI: UI{
Language: "id",
ThemeMode: "auto",
ThemeAccent: "mauve",
SoundEnabled: true,
SoundVolume: 70,
SoundMuteOnSSH: true,
AnimationEnabled: true,
LogTailLines: 300,
ShowTimestamps: true,
},
Docker: DockerCfg{ComposeCommand: "docker compose"},
}
}
// Path — ~/.config/tia/config.yaml (mengikuti XDG_CONFIG_HOME).
func Path() string {
dir, err := os.UserConfigDir()
if err != nil {
dir = filepath.Join(os.Getenv("HOME"), ".config")
}
return filepath.Join(dir, "tia", "config.yaml")
}
// Load membaca config; file belum ada BUKAN error (memakai default).
func Load() (Config, error) { return LoadFrom(Path()) }
func LoadFrom(path string) (Config, error) {
c := Default()
b, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return c, nil
}
return c, err
}
if err := yaml.Unmarshal(b, &c); err != nil {
return Default(), fmt.Errorf("config: parse %s: %w", path, err)
}
c.normalize()
return c, nil
}
// Save menulis config secara atomik (temp + rename) — tidak pernah setengah jadi.
func Save(c Config) error { return SaveTo(Path(), c) }
func SaveTo(path string, c Config) error {
c.normalize()
b, err := yaml.Marshal(c)
if err != nil {
return fmt.Errorf("config: marshal: %w", err)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
tmp := path + ".tmp"
if err := os.WriteFile(tmp, b, 0o644); err != nil {
return err
}
if err := os.Rename(tmp, path); err != nil {
_ = os.Remove(tmp)
return err
}
return nil
}
// normalize — validasi nilai (file config adalah batas kepercayaan:
// bisa diedit tangan; nilai rusak tidak boleh menjatuhkan aplikasi).
func (c *Config) normalize() {
switch c.UI.Language {
case "id", "en":
default:
c.UI.Language = "id"
}
switch c.UI.ThemeMode {
case "auto", "dark", "light", "terminal":
default:
c.UI.ThemeMode = "auto"
}
if c.UI.ThemeAccent == "" {
c.UI.ThemeAccent = "mauve"
}
if c.UI.SoundVolume < 0 {
c.UI.SoundVolume = 0
}
if c.UI.SoundVolume > 100 {
c.UI.SoundVolume = 100
}
if c.UI.LogTailLines < 10 {
c.UI.LogTailLines = 10
}
if c.UI.LogTailLines > 10000 {
c.UI.LogTailLines = 10000
}
if c.StacksDir == "" {
c.StacksDir = Default().StacksDir
}
if c.Version == 0 {
c.Version = 1
}
if c.Docker.ComposeCommand == "" {
c.Docker.ComposeCommand = "docker compose"
}
}
// ExpandPath — ubah "~/..." jadi absolut (config menyimpan path gaya user).
func ExpandPath(p string) string {
if p == "~" || strings.HasPrefix(p, "~/") {
home, err := os.UserHomeDir()
if err == nil {
return filepath.Join(home, strings.TrimPrefix(strings.TrimPrefix(p, "~"), "/"))
}
}
return p
}