♻️ refactor: split src into internal packages and cmd

This commit is contained in:
2026-07-16 23:51:39 +07:00
parent 2e320b03f3
commit a05d6bb7eb
28 changed files with 1355 additions and 1328 deletions
+152
View File
@@ -0,0 +1,152 @@
package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/YuzuZensai/TrollSSH/internal/logx"
)
type PlaybackMode string
const (
PlaybackLoop PlaybackMode = "loop"
PlaybackRandom PlaybackMode = "random"
)
type Config struct {
Host string
Port int
MaxLoop int
PlaybackMode PlaybackMode
AllowUserControl bool
SwitchDebounce time.Duration
LoginDelay time.Duration
MaxConnections int
MaxTotalConnections int
MaxAuthAttempts int
HandshakeTimeout time.Duration
MaxDimension int
MaxTerminalCells int
SessionTimeout time.Duration
RenderCacheMB int
BrightnessThreshold int
Charset string
Invert bool
ForceGrayscale bool
LogCredentials bool
}
func warnInvalid(name, value string, fallback any) {
logx.Warn(fmt.Sprintf("Invalid %s=%q, using default %v", name, logx.Sanitize(value), fallback))
}
func envString(name, fallback string) string {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
return fallback
}
return value
}
func envInt(name string, fallback, min, max int) int {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
parsed, err := strconv.Atoi(raw)
if err != nil {
warnInvalid(name, raw, fallback)
return fallback
}
if parsed < min {
return min
}
if parsed > max {
return max
}
return parsed
}
func envBool(name string, fallback bool) bool {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
parsed, err := strconv.ParseBool(strings.ToLower(raw))
if err != nil {
warnInvalid(name, raw, fallback)
return fallback
}
return parsed
}
func envDurationMs(name string, fallback time.Duration) time.Duration {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
ms, err := strconv.Atoi(raw)
if err != nil {
warnInvalid(name, raw, fallback)
return fallback
}
if ms < 0 {
ms = 0
}
return time.Duration(ms) * time.Millisecond
}
func envPlaybackMode(name string, fallback PlaybackMode) PlaybackMode {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
switch PlaybackMode(strings.ToLower(raw)) {
case PlaybackLoop:
return PlaybackLoop
case PlaybackRandom:
return PlaybackRandom
}
warnInvalid(name, raw, fallback)
return fallback
}
func Load() Config {
const maxInt = int(^uint(0) >> 1)
return Config{
Host: envString("HOST", "0.0.0.0"),
Port: envInt("PORT", 22, 1, 65535),
MaxLoop: envInt("MAX_LOOP", 5, 0, maxInt),
PlaybackMode: envPlaybackMode("PLAYBACK_MODE", PlaybackLoop),
AllowUserControl: envBool("ALLOW_USER_CONTROL", true),
SwitchDebounce: envDurationMs("SWITCH_DEBOUNCE_MS", 120*time.Millisecond),
LoginDelay: envDurationMs("LOGIN_DELAY", 1500*time.Millisecond),
MaxConnections: envInt("MAX_CONNECTIONS", 10, 1, maxInt),
MaxTotalConnections: envInt("MAX_TOTAL_CONNECTIONS", 1000, 1, maxInt),
MaxAuthAttempts: envInt("MAX_AUTH_ATTEMPTS", 6, 1, maxInt),
HandshakeTimeout: envDurationMs("HANDSHAKE_TIMEOUT", 10*time.Second),
MaxDimension: envInt("MAX_DIMENSION", 512, 1, 4096),
MaxTerminalCells: envInt("MAX_TERMINAL_CELLS", 500*512, 1, maxInt),
SessionTimeout: envDurationMs("SESSION_TIMEOUT", 10*time.Minute),
RenderCacheMB: envInt("RENDER_CACHE_MB", 256, 0, maxInt),
BrightnessThreshold: envInt("BRIGHTNESS_THRESHOLD", 40, 0, 100),
Charset: envString("CHARSET", "detailed"),
Invert: envBool("INVERT", false),
ForceGrayscale: envBool("FORCE_GRAYSCALE", false),
LogCredentials: envBool("LOG_CREDENTIALS", false),
}
}
func LoadOptionalTextFile(filePath string) (string, bool) {
data, err := os.ReadFile(filePath)
if err != nil {
return "", false
}
text := strings.ReplaceAll(string(data), "\r\n", "\n")
text = strings.ReplaceAll(text, "\n", "\r\n")
return text, true
}
+79
View File
@@ -0,0 +1,79 @@
package config
import (
"testing"
"time"
)
func TestLoadConfigDefaults(t *testing.T) {
t.Setenv("HOST", "")
t.Setenv("PORT", "")
t.Setenv("PLAYBACK_MODE", "")
t.Setenv("LOGIN_DELAY", "")
cfg := Load()
if cfg.Host != "0.0.0.0" {
t.Errorf("host = %q", cfg.Host)
}
if cfg.Port != 22 {
t.Errorf("port = %d", cfg.Port)
}
if cfg.PlaybackMode != PlaybackLoop {
t.Errorf("playbackMode = %q", cfg.PlaybackMode)
}
if cfg.Charset != "detailed" {
t.Errorf("charset = %q", cfg.Charset)
}
if cfg.LoginDelay != 1500*time.Millisecond {
t.Errorf("loginDelay = %v", cfg.LoginDelay)
}
}
func TestLoadConfigClamping(t *testing.T) {
t.Setenv("PORT", "999999")
t.Setenv("BRIGHTNESS_THRESHOLD", "-5")
cfg := Load()
if cfg.Port != 65535 {
t.Errorf("port clamp = %d", cfg.Port)
}
if cfg.BrightnessThreshold != 0 {
t.Errorf("brightness clamp = %d", cfg.BrightnessThreshold)
}
}
func TestLoadConfigInvalidFallsBack(t *testing.T) {
t.Setenv("PORT", "not-a-number")
t.Setenv("INVERT", "yes-please")
t.Setenv("PLAYBACK_MODE", "shuffle")
cfg := Load()
if cfg.Port != 22 {
t.Errorf("port = %d, want default 22", cfg.Port)
}
if cfg.Invert {
t.Error("invert should fall back to false")
}
if cfg.PlaybackMode != PlaybackLoop {
t.Errorf("playbackMode = %q, want default loop", cfg.PlaybackMode)
}
}
func TestEnvDurationMs(t *testing.T) {
t.Setenv("D", "250")
if got := envDurationMs("D", time.Second); got != 250*time.Millisecond {
t.Errorf("250 = %v, want 250ms", got)
}
t.Setenv("D", "-10")
if got := envDurationMs("D", time.Second); got != 0 {
t.Errorf("negative = %v, want 0", got)
}
t.Setenv("D", "banana")
if got := envDurationMs("D", time.Second); got != time.Second {
t.Errorf("invalid = %v, want fallback 1s", got)
}
}
func TestPlaybackModeRandom(t *testing.T) {
t.Setenv("PLAYBACK_MODE", "RaNdOm")
if Load().PlaybackMode != PlaybackRandom {
t.Error("expected random")
}
}