Skip to content

Commit 38833df

Browse files
committed
Add comprehensive JSON configuration system and weather feature
- Add Config struct with all customizable options - Implement automatic config file creation (gophetch.json) - Add config loading with fallback to defaults - Integrate config options throughout the application: - Display settings (fps, color_scheme, show_* options) - Frame/animation settings (frame_file, loop_animation, center_content) - Output mode (static_mode, hide_animation) - Misc options (show_fps_counter, show_weather) - Add weather information from wttr.in API - Update renderSystemInfo to respect show_* config options - Set default FPS to 5 for better animation experience - Update README with complete configuration documentation - Command line arguments still override config settings - Remove custom_text feature as requested
1 parent b99302f commit 38833df

3 files changed

Lines changed: 262 additions & 29 deletions

File tree

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ A terminal-based system monitor with ASCII animation built in Go using Bubble Te
1515
- Real-time system information display
1616
- Animated rain cloud ASCII art (default)
1717
- Custom ASCII frame file support for personalized animations
18+
- JSON configuration file for customizable display options
1819
- Cross-platform compatibility (Windows, Linux, macOS, Android/Termux)
1920
- Color palette with animated wave effects
2021
- System metrics including CPU, memory, disk usage, and load average
@@ -75,6 +76,50 @@ The project includes build scripts for different platforms:
7576
./gophetch frames.txt 500ms
7677
```
7778

79+
## Configuration
80+
81+
Gophetch supports a JSON configuration file (`gophetch.json`) that is automatically created on first run. You can customize all display options:
82+
83+
```json
84+
{
85+
"fps": 5,
86+
"color_scheme": "blue",
87+
"show_cpu": true,
88+
"show_memory": true,
89+
"show_disk": true,
90+
"show_uptime": true,
91+
"show_kernel": true,
92+
"show_os": true,
93+
"show_hostname": true,
94+
"frame_file": "default",
95+
"loop_animation": true,
96+
"center_content": true,
97+
"static_mode": false,
98+
"hide_animation": false,
99+
"show_fps_counter": false,
100+
"show_weather": false
101+
}
102+
```
103+
104+
### Configuration Options
105+
106+
- **fps**: Animation frame rate (default: 5)
107+
- **color_scheme**: Main color theme (default: "blue")
108+
- **show_cpu**: Display CPU information (default: true)
109+
- **show_memory**: Display memory information (default: true)
110+
- **show_disk**: Display disk usage (default: true)
111+
- **show_uptime**: Display system uptime (default: true)
112+
- **show_kernel**: Display kernel/Go version (default: true)
113+
- **show_os**: Display OS and architecture (default: true)
114+
- **show_hostname**: Display username (default: true)
115+
- **frame_file**: Path to custom ASCII frames file (default: "default")
116+
- **loop_animation**: Loop animation frames (default: true)
117+
- **center_content**: Center ASCII art (default: true)
118+
- **static_mode**: One-shot info dump like Neofetch (default: false)
119+
- **hide_animation**: Skip animation even if frames exist (default: false)
120+
- **show_fps_counter**: Show FPS overlay (default: false)
121+
- **show_weather**: Display weather info (default: false)
122+
78123
## Frame File Format
79124

80125
Custom ASCII frames can be loaded from text files. Each frame is separated by `---FRAME---`:

gophetch.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"fps": 5,
3+
"color_scheme": "blue",
4+
"show_cpu": true,
5+
"show_memory": true,
6+
"show_disk": true,
7+
"show_uptime": true,
8+
"show_kernel": true,
9+
"show_os": true,
10+
"show_hostname": true,
11+
"frame_file": "default",
12+
"loop_animation": true,
13+
"center_content": true,
14+
"static_mode": false,
15+
"hide_animation": false,
16+
"show_fps_counter": false,
17+
"show_weather": false
18+
}

main.go

Lines changed: 199 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package main
33
import (
44
"bufio"
55
"context"
6+
"encoding/json"
67
"fmt"
8+
"io"
79
"math"
810
"math/rand"
11+
"net/http"
912
"os"
1013
"os/exec"
1114
"runtime"
@@ -36,6 +39,34 @@ type SystemInfo struct {
3639
Processes int
3740
LoadAvg string
3841
Username string
42+
Weather string
43+
}
44+
45+
// Config holds all configuration options
46+
type Config struct {
47+
// Display settings
48+
FPS int `json:"fps"`
49+
ColorScheme string `json:"color_scheme"`
50+
ShowCPU bool `json:"show_cpu"`
51+
ShowMemory bool `json:"show_memory"`
52+
ShowDisk bool `json:"show_disk"`
53+
ShowUptime bool `json:"show_uptime"`
54+
ShowKernel bool `json:"show_kernel"`
55+
ShowOS bool `json:"show_os"`
56+
ShowHostname bool `json:"show_hostname"`
57+
58+
// Frame / animation settings
59+
FrameFile string `json:"frame_file"`
60+
LoopAnimation bool `json:"loop_animation"`
61+
CenterContent bool `json:"center_content"`
62+
63+
// Output mode
64+
StaticMode bool `json:"static_mode"`
65+
HideAnimation bool `json:"hide_animation"`
66+
67+
// Misc
68+
ShowFPSCounter bool `json:"show_fps_counter"`
69+
ShowWeather bool `json:"show_weather"`
3970
}
4071

4172
// Model represents the Bubble Tea model
@@ -45,6 +76,7 @@ type Model struct {
4576
frameRate time.Duration
4677
startTime time.Time
4778
sysInfo SystemInfo
79+
config Config
4880
ctx context.Context
4981
cancel context.CancelFunc
5082
width int
@@ -333,25 +365,35 @@ func (m Model) renderSystemInfo() string {
333365
info.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render("─────────────────────"))
334366
info.WriteString("\n\n")
335367

336-
info.WriteString(fmt.Sprintf("%s %s\n",
337-
infoStyle.Render("OS:"),
338-
valueStyle.Render(fmt.Sprintf("%s (%s)", m.sysInfo.OS, m.sysInfo.Architecture))))
368+
if m.config.ShowOS {
369+
info.WriteString(fmt.Sprintf("%s %s\n",
370+
infoStyle.Render("OS:"),
371+
valueStyle.Render(fmt.Sprintf("%s (%s)", m.sysInfo.OS, m.sysInfo.Architecture))))
372+
}
339373

340-
info.WriteString(fmt.Sprintf("%s %s\n",
341-
infoStyle.Render("User:"),
342-
valueStyle.Render(m.sysInfo.Username)))
374+
if m.config.ShowHostname {
375+
info.WriteString(fmt.Sprintf("%s %s\n",
376+
infoStyle.Render("User:"),
377+
valueStyle.Render(m.sysInfo.Username)))
378+
}
343379

344-
info.WriteString(fmt.Sprintf("%s %s\n",
345-
infoStyle.Render("CPU:"),
346-
valueStyle.Render(fmt.Sprintf("%d cores", m.sysInfo.CPUCount))))
380+
if m.config.ShowCPU {
381+
info.WriteString(fmt.Sprintf("%s %s\n",
382+
infoStyle.Render("CPU:"),
383+
valueStyle.Render(fmt.Sprintf("%d cores", m.sysInfo.CPUCount))))
384+
}
347385

348-
info.WriteString(fmt.Sprintf("%s %s\n",
349-
infoStyle.Render("Memory:"),
350-
valueStyle.Render(m.sysInfo.Memory)))
386+
if m.config.ShowMemory {
387+
info.WriteString(fmt.Sprintf("%s %s\n",
388+
infoStyle.Render("Memory:"),
389+
valueStyle.Render(m.sysInfo.Memory)))
390+
}
351391

352-
info.WriteString(fmt.Sprintf("%s %s\n",
353-
infoStyle.Render("Go Version:"),
354-
valueStyle.Render(m.sysInfo.GoVersion)))
392+
if m.config.ShowKernel {
393+
info.WriteString(fmt.Sprintf("%s %s\n",
394+
infoStyle.Render("Go Version:"),
395+
valueStyle.Render(m.sysInfo.GoVersion)))
396+
}
355397

356398
if m.sysInfo.Processes > 0 {
357399
info.WriteString(fmt.Sprintf("%s %s\n",
@@ -365,9 +407,11 @@ func (m Model) renderSystemInfo() string {
365407
valueStyle.Render(strings.TrimPrefix(m.sysInfo.LoadAvg, "Load: "))))
366408
}
367409

368-
info.WriteString(fmt.Sprintf("%s %s\n",
369-
infoStyle.Render("Disk:"),
370-
valueStyle.Render(strings.TrimPrefix(m.sysInfo.DiskUsage, "Disk: "))))
410+
if m.config.ShowDisk {
411+
info.WriteString(fmt.Sprintf("%s %s\n",
412+
infoStyle.Render("Disk:"),
413+
valueStyle.Render(strings.TrimPrefix(m.sysInfo.DiskUsage, "Disk: "))))
414+
}
371415

372416
// Runtime information
373417
info.WriteString("\n")
@@ -376,18 +420,28 @@ func (m Model) renderSystemInfo() string {
376420
info.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render("─────────────────────"))
377421
info.WriteString("\n\n")
378422

379-
uptime := time.Since(m.startTime)
380-
info.WriteString(fmt.Sprintf("%s %s\n",
381-
infoStyle.Render("Uptime:"),
382-
valueStyle.Render(uptime.Truncate(time.Second).String())))
423+
if m.config.ShowUptime {
424+
uptime := time.Since(m.startTime)
425+
info.WriteString(fmt.Sprintf("%s %s\n",
426+
infoStyle.Render("Uptime:"),
427+
valueStyle.Render(uptime.Truncate(time.Second).String())))
428+
}
383429

384430
info.WriteString(fmt.Sprintf("%s %s\n",
385431
infoStyle.Render("Time:"),
386432
valueStyle.Render(time.Now().Format("15:04:05"))))
387433

388-
info.WriteString(fmt.Sprintf("%s %s\n",
389-
infoStyle.Render("FPS:"),
390-
valueStyle.Render(fmt.Sprintf("%.1f", float64(time.Second)/float64(m.frameRate)))))
434+
if m.config.ShowWeather {
435+
info.WriteString(fmt.Sprintf("%s %s\n",
436+
infoStyle.Render("Weather:"),
437+
valueStyle.Render(strings.TrimPrefix(m.sysInfo.Weather, "Weather: "))))
438+
}
439+
440+
if m.config.ShowFPSCounter {
441+
info.WriteString(fmt.Sprintf("%s %s\n",
442+
infoStyle.Render("FPS:"),
443+
valueStyle.Render(fmt.Sprintf("%.1f", float64(time.Second)/float64(m.frameRate)))))
444+
}
391445

392446
return info.String()
393447
}
@@ -517,6 +571,9 @@ func GetSystemInfo() SystemInfo {
517571
// Get load average (Unix-like systems)
518572
info.LoadAvg = getLoadAverage()
519573

574+
// Get weather information
575+
info.Weather = getWeather()
576+
520577
return info
521578
}
522579

@@ -920,14 +977,113 @@ func extractColor(line string) lipgloss.Color {
920977
return lipgloss.Color("252")
921978
}
922979

980+
// getWeather gets weather information from wttr.in
981+
func getWeather() string {
982+
client := &http.Client{
983+
Timeout: 5 * time.Second,
984+
}
985+
986+
resp, err := client.Get("https://wttr.in/?format=%C+%t")
987+
if err != nil {
988+
return "Weather: N/A"
989+
}
990+
defer resp.Body.Close()
991+
992+
if resp.StatusCode != http.StatusOK {
993+
return "Weather: N/A"
994+
}
995+
996+
body, err := io.ReadAll(resp.Body)
997+
if err != nil {
998+
return "Weather: N/A"
999+
}
1000+
1001+
weather := strings.TrimSpace(string(body))
1002+
if weather == "" {
1003+
return "Weather: N/A"
1004+
}
1005+
1006+
return fmt.Sprintf("Weather: %s", weather)
1007+
}
1008+
1009+
// getDefaultConfig returns the default configuration
1010+
func getDefaultConfig() Config {
1011+
return Config{
1012+
// Display settings
1013+
FPS: 5,
1014+
ColorScheme: "blue",
1015+
ShowCPU: true,
1016+
ShowMemory: true,
1017+
ShowDisk: true,
1018+
ShowUptime: true,
1019+
ShowKernel: true,
1020+
ShowOS: true,
1021+
ShowHostname: true,
1022+
1023+
// Frame / animation settings
1024+
FrameFile: "default",
1025+
LoopAnimation: true,
1026+
CenterContent: true,
1027+
1028+
// Output mode
1029+
StaticMode: false,
1030+
HideAnimation: false,
1031+
1032+
// Misc
1033+
ShowFPSCounter: false,
1034+
ShowWeather: false,
1035+
}
1036+
}
1037+
1038+
// loadConfig loads configuration from file or creates default
1039+
func loadConfig() (Config, error) {
1040+
configPath := "gophetch.json"
1041+
1042+
// Check if config file exists
1043+
if _, err := os.Stat(configPath); os.IsNotExist(err) {
1044+
// Create default config file
1045+
defaultConfig := getDefaultConfig()
1046+
data, err := json.MarshalIndent(defaultConfig, "", " ")
1047+
if err != nil {
1048+
return defaultConfig, fmt.Errorf("failed to marshal default config: %v", err)
1049+
}
1050+
1051+
if err := os.WriteFile(configPath, data, 0644); err != nil {
1052+
return defaultConfig, fmt.Errorf("failed to write default config: %v", err)
1053+
}
1054+
1055+
fmt.Printf("Created default config file: %s\n", configPath)
1056+
return defaultConfig, nil
1057+
}
1058+
1059+
// Load existing config
1060+
data, err := os.ReadFile(configPath)
1061+
if err != nil {
1062+
return getDefaultConfig(), fmt.Errorf("failed to read config file: %v", err)
1063+
}
1064+
1065+
var config Config
1066+
if err := json.Unmarshal(data, &config); err != nil {
1067+
return getDefaultConfig(), fmt.Errorf("failed to parse config file: %v", err)
1068+
}
1069+
1070+
return config, nil
1071+
}
1072+
9231073
func main() {
1074+
// Load configuration
1075+
config, err := loadConfig()
1076+
if err != nil {
1077+
fmt.Printf("Warning: %v, using defaults\n", err)
1078+
config = getDefaultConfig()
1079+
}
1080+
9241081
var frames []Frame
925-
var err error
926-
frameRate := 200 * time.Millisecond
1082+
frameRate := time.Duration(1000/config.FPS) * time.Millisecond
9271083

928-
// Parse command line arguments
1084+
// Load frames based on config or command line arguments
9291085
if len(os.Args) > 1 {
930-
// Check if first argument is a filename (contains .txt or doesn't parse as duration)
1086+
// Command line arguments override config
9311087
if strings.Contains(os.Args[1], ".txt") || strings.Contains(os.Args[1], ".frames") {
9321088
// Load frames from file
9331089
filename := os.Args[1]
@@ -953,6 +1109,19 @@ func main() {
9531109
frameRate = duration
9541110
}
9551111
}
1112+
} else {
1113+
// Use config file setting
1114+
if config.FrameFile != "default" && config.FrameFile != "" {
1115+
fmt.Printf("Loading frames from config file: %s\n", config.FrameFile)
1116+
frames, err = LoadFramesFromFile(config.FrameFile)
1117+
if err != nil {
1118+
fmt.Printf("Error loading config frame file: %v\n", err)
1119+
fmt.Printf("Falling back to rain animation...\n")
1120+
frames = []Frame{} // Use rain animation as fallback
1121+
} else {
1122+
fmt.Printf("Successfully loaded %d frames from config\n", len(frames))
1123+
}
1124+
}
9561125
}
9571126

9581127
// If no frames loaded, use rain animation
@@ -970,6 +1139,7 @@ func main() {
9701139
frameRate: frameRate,
9711140
startTime: time.Now(),
9721141
sysInfo: GetSystemInfo(),
1142+
config: config,
9731143
ctx: ctx,
9741144
cancel: cancel,
9751145
mutex: &sync.RWMutex{},

0 commit comments

Comments
 (0)