From 2a66278caec50f324f954427e94f9dcbb29b2dda Mon Sep 17 00:00:00 2001 From: Arindy Date: Thu, 1 Jan 2026 12:10:33 +0100 Subject: [PATCH] Ensure consistent error handling and improve resource cleanup across widgets and APIs. --- README.md | 16 +++++++++++----- apis/docker/docker.go | 17 +++++++++++++---- apis/newsapi/main.go | 19 +++++++++++++++++-- apis/nina/main.go | 14 ++++++++++++-- apis/openweathermap/main.go | 7 ++++++- apis/pihole/pihole.go | 14 ++++++++++++-- widgets/news.go | 24 ++++++++---------------- widgets/weather.go | 3 +-- widgets/yearMood.go | 24 ++++++++++-------------- 9 files changed, 90 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index a95bf4f..42f4271 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,10 @@ ArinDash is a customizable terminal dashboard (TUI) written in Go, powered by [` - **Docker Monitoring**: View the status of your Docker containers. - **HTTP Prober**: Check the availability and status codes of your favorite websites. - **Network Device Monitoring**: Track which devices are online in your local network. -- **WiFi QR Code**: Display a QR code for easy WiFi connection sharing. +- **Wi-Fi QR Code**: Display a QR code for easy Wi-Fi connection sharing. - **NINA Warnings**: Stay informed about emergency warnings in Germany via the NINA-Service. - **News**: Get the latest news headlines from various sources via News API. +- **Every Day Mood**: Track your daily mood with an interactive calendar. - **Zukitchi**: A cute animated terminal pet/sprite to keep you company. - **Interactive TUI**: Built with `termdash` for a responsive and visually appealing terminal interface. @@ -46,7 +47,7 @@ ArinDash uses a `config.toml` file for its settings. A template is provided as ` 2. Edit `config.toml` with your specific details: - **Pi-hole**: Set your host and API password. - - **WiFi**: Configure your SSID and password for the QR code widget. + - **Wi-Fi**: Configure your SSID and password for the QR code widget. - **Weather**: Provide your OpenWeatherMap API key and Location ID. You can find your Location ID in the `city.list.json.gz` from [OpenWeatherMap bulk data](http://bulk.openweathermap.org/sample/). - **NINA Warnings**: Set your `gebietsCode` for the area you want to monitor. - **News**: Provide your News API key and preferred sources. @@ -57,7 +58,7 @@ ArinDash uses a `config.toml` file for its settings. A template is provided as ` ## Usage -To run ArinDash, simply execute: +To run ArinDash, execute: ```bash go run main.go @@ -65,7 +66,12 @@ go run main.go ### Controls - **Esc / Ctrl+C**: Exit the application. -- **Left / Right Arrow Keys**: Navigate through news articles (in the News widget). +- **News Widget**: + - **Left / Right Arrow Keys**: Navigate through news articles. +- **Every Day Mood Widget**: + - **Arrow Keys**: Navigate through the calendar days and months. + - **1, 2, 3**: Assign a mood. + - **Backspace**: Clear the mood for the selected day. ## Project Structure @@ -77,4 +83,4 @@ go run main.go ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +This project is licensed under the MIT License – see the [LICENSE](LICENSE) file for details. diff --git a/apis/docker/docker.go b/apis/docker/docker.go index 1d1b38f..9e4b45e 100644 --- a/apis/docker/docker.go +++ b/apis/docker/docker.go @@ -3,6 +3,7 @@ package docker import ( "ArinDash/util" "context" + "io" "strings" containertypes "github.com/docker/docker/api/types/container" @@ -34,7 +35,12 @@ func FetchDockerMetrics(ctx context.Context) ([]ContainerMetrics, error) { if err != nil { return nil, err } - defer cli.Close() + defer func(cli *client.Client) { + err := cli.Close() + if err != nil { + return + } + }(cli) _, err = cli.Ping(ctx) if err != nil { @@ -60,7 +66,12 @@ func FetchDockerMetrics(ctx context.Context) ([]ContainerMetrics, error) { continue } func() { - defer stats.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + + } + }(stats.Body) var sj containertypes.StatsResponse if err := util.DecodeJSON(stats.Body, &sj); err != nil { return @@ -109,9 +120,7 @@ func cpuPercentFromStats(s containertypes.StatsResponse) float64 { func memoryFromStats(s containertypes.StatsResponse) (usage, limit uint64, percent float64) { usage = s.MemoryStats.Usage limit = s.MemoryStats.Limit - // Optionally discount cached memory if stats present. if stats := s.MemoryStats.Stats; stats != nil { - // The common Linux approach: usage - cache if cache, ok := stats["cache"]; ok && cache <= usage { usage -= cache } diff --git a/apis/newsapi/main.go b/apis/newsapi/main.go index 5673369..2390cf0 100644 --- a/apis/newsapi/main.go +++ b/apis/newsapi/main.go @@ -11,6 +11,8 @@ import ( const apiBaseURL = "https://newsapi.org/v2/top-headlines" +var forbiddenStrings = []string{"\r", "\\r", "\ufeff", "\u00A0"} + type configFile struct { News newsConfig } @@ -65,7 +67,12 @@ func FetchNews() News { Status: err.Error(), } } - defer resp.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) respBody, err := io.ReadAll(resp.Body) if err != nil { return News{ @@ -75,7 +82,7 @@ func FetchNews() News { news := News{} - err = json.Unmarshal([]byte(strings.ReplaceAll(strings.ReplaceAll(string(respBody), "\r", ""), "\u00A0", "")), &news) + err = json.Unmarshal([]byte(removeForbiddenStrings(string(respBody))), &news) if err != nil { log.Fatal(err) @@ -83,3 +90,11 @@ func FetchNews() News { return news } + +func removeForbiddenStrings(s string) string { + result := s + for _, forbidden := range forbiddenStrings { + result = strings.ReplaceAll(result, forbidden, "") + } + return result +} diff --git a/apis/nina/main.go b/apis/nina/main.go index b83c03b..694e46a 100644 --- a/apis/nina/main.go +++ b/apis/nina/main.go @@ -57,7 +57,12 @@ func FetchWarnings() []Warning { Errormsg: err.Error(), }) } - defer resp.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) respBody, err := io.ReadAll(resp.Body) if err != nil { return append(make([]Warning, 0), Warning{ @@ -86,7 +91,12 @@ func FetchWarnings() []Warning { Errormsg: err.Error(), }) } - defer resp.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) respBody, err := io.ReadAll(resp.Body) if err != nil { return append(make([]Warning, 0), Warning{ diff --git a/apis/openweathermap/main.go b/apis/openweathermap/main.go index b56398e..ca7a871 100644 --- a/apis/openweathermap/main.go +++ b/apis/openweathermap/main.go @@ -41,7 +41,12 @@ func FetchCurrentWeather() CurrentWeather { ErrorMessage: err.Error(), } } - defer resp.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) respBody, err := io.ReadAll(resp.Body) if err != nil { return CurrentWeather{ diff --git a/apis/pihole/pihole.go b/apis/pihole/pihole.go index 29c3ac3..d2825fe 100644 --- a/apis/pihole/pihole.go +++ b/apis/pihole/pihole.go @@ -59,7 +59,12 @@ func (ph *PiHConnector) do(method string, endpoint string, body io.Reader) []byt if err != nil { return make([]byte, 0) } - defer resp.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) respBody, err := io.ReadAll(resp.Body) if err != nil { @@ -90,7 +95,12 @@ func Connect() PiHConnector { if err != nil { panic(err) } - defer resp.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) respBody, err := io.ReadAll(resp.Body) if err != nil { diff --git a/widgets/news.go b/widgets/news.go index 579e44f..4fdd01f 100644 --- a/widgets/news.go +++ b/widgets/news.go @@ -5,7 +5,6 @@ import ( "ArinDash/util" "context" "fmt" - "strings" "time" "github.com/mum4k/termdash/cell" @@ -55,33 +54,26 @@ func (widget *NewsText) drawNews() error { return nil } if err := widget.Write(fmt.Sprintf("%d/%d\n", selected+1, len(widget.newsArticles.Articles)), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { - widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) - return err + return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) } article := widget.newsArticles.Articles[selected] if err := widget.Write(fmt.Sprintf("%s\n", article.Author), text.WriteCellOpts(cell.FgColor(cell.ColorGray))); err != nil { - widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) - return err + return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) } if err := widget.Write(fmt.Sprintf("%s\n\n", article.Title), text.WriteCellOpts(cell.FgColor(cell.ColorWhite), cell.Bold())); err != nil { - widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) - return err + return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) } if err := widget.Write(fmt.Sprintf("%s\n\n", article.Description), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { - widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) - return err + return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) } - if err := widget.Write(fmt.Sprintf("%s\n", strings.ReplaceAll(article.Content, "\r", "")), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { - widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) - return err + if err := widget.Write(fmt.Sprintf("%s\n", article.Content), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { + return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) } if err := widget.Write(fmt.Sprintf("%s\n\n", article.PublishedAt), text.WriteCellOpts(cell.FgColor(cell.ColorGray))); err != nil { - widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) - return err + return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) } if err := widget.Write(fmt.Sprintf("%s\n", article.URL)); err != nil { - widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) - return err + return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) } return nil } diff --git a/widgets/weather.go b/widgets/weather.go index 9782815..fe256a0 100644 --- a/widgets/weather.go +++ b/widgets/weather.go @@ -28,8 +28,7 @@ func createWeather(ctx context.Context, _ terminalapi.Terminal, _ interface{}) w weather := openWeatherMap.FetchCurrentWeather() widget.Reset() if weather.ErrorMessage != "" { - widget.Write(weather.ErrorMessage, text.WriteCellOpts(cell.FgColor(cell.ColorRed))) - return nil + return widget.Write(weather.ErrorMessage, text.WriteCellOpts(cell.FgColor(cell.ColorRed))) } weatherIcon := getIcon(weather.Weather[0].Icon) diff --git a/widgets/yearMood.go b/widgets/yearMood.go index a1535ab..5681c12 100644 --- a/widgets/yearMood.go +++ b/widgets/yearMood.go @@ -289,25 +289,20 @@ func (widget *MoodText) Keyboard(k *terminalapi.Keyboard, _ *widgetapi.EventMeta } break case keyboard.KeyBackspace, keyboard.KeyBackspace2: - widget.WriteMood(0) - break + return widget.WriteMood(0) case '1': - widget.WriteMood(1) - break + return widget.WriteMood(1) case '2': - widget.WriteMood(2) - break + return widget.WriteMood(2) case '3': - widget.WriteMood(3) - break + return widget.WriteMood(3) default: - widget.WriteMood(-1) - break + return widget.WriteMood(-1) } return widget.drawTable() } -func (widget *MoodText) WriteMood(mood int) { +func (widget *MoodText) WriteMood(mood int) error { m := moods() if m[strconv.Itoa(time.Now().Year())] == nil { m[strconv.Itoa(time.Now().Year())] = make(map[string]map[string]int) @@ -318,10 +313,11 @@ func (widget *MoodText) WriteMood(mood int) { m[strconv.Itoa(time.Now().Year())][strconv.Itoa(int(widget.selectedMonth))][strconv.Itoa(widget.selectedDay)] = mood newMoods, err := json.MarshalIndent(m, "", " ") if err != nil { - widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) + return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) + } if err := os.WriteFile(moodsFile, newMoods, 0644); err != nil { - widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) - // ignore + return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed))) } + return nil }