Ensure consistent error handling and improve resource cleanup across widgets and APIs.

This commit is contained in:
Arindy 2026-01-01 12:10:33 +01:00
parent af5a39ef75
commit 2a66278cae
9 changed files with 90 additions and 48 deletions

View File

@ -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. - **Docker Monitoring**: View the status of your Docker containers.
- **HTTP Prober**: Check the availability and status codes of your favorite websites. - **HTTP Prober**: Check the availability and status codes of your favorite websites.
- **Network Device Monitoring**: Track which devices are online in your local network. - **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. - **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. - **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. - **Zukitchi**: A cute animated terminal pet/sprite to keep you company.
- **Interactive TUI**: Built with `termdash` for a responsive and visually appealing terminal interface. - **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: 2. Edit `config.toml` with your specific details:
- **Pi-hole**: Set your host and API password. - **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/). - **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. - **NINA Warnings**: Set your `gebietsCode` for the area you want to monitor.
- **News**: Provide your News API key and preferred sources. - **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 ## Usage
To run ArinDash, simply execute: To run ArinDash, execute:
```bash ```bash
go run main.go go run main.go
@ -65,7 +66,12 @@ go run main.go
### Controls ### Controls
- **Esc / Ctrl+C**: Exit the application. - **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 ## Project Structure
@ -77,4 +83,4 @@ go run main.go
## License ## 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.

View File

@ -3,6 +3,7 @@ package docker
import ( import (
"ArinDash/util" "ArinDash/util"
"context" "context"
"io"
"strings" "strings"
containertypes "github.com/docker/docker/api/types/container" containertypes "github.com/docker/docker/api/types/container"
@ -34,7 +35,12 @@ func FetchDockerMetrics(ctx context.Context) ([]ContainerMetrics, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer cli.Close() defer func(cli *client.Client) {
err := cli.Close()
if err != nil {
return
}
}(cli)
_, err = cli.Ping(ctx) _, err = cli.Ping(ctx)
if err != nil { if err != nil {
@ -60,7 +66,12 @@ func FetchDockerMetrics(ctx context.Context) ([]ContainerMetrics, error) {
continue continue
} }
func() { func() {
defer stats.Body.Close() defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
}
}(stats.Body)
var sj containertypes.StatsResponse var sj containertypes.StatsResponse
if err := util.DecodeJSON(stats.Body, &sj); err != nil { if err := util.DecodeJSON(stats.Body, &sj); err != nil {
return return
@ -109,9 +120,7 @@ func cpuPercentFromStats(s containertypes.StatsResponse) float64 {
func memoryFromStats(s containertypes.StatsResponse) (usage, limit uint64, percent float64) { func memoryFromStats(s containertypes.StatsResponse) (usage, limit uint64, percent float64) {
usage = s.MemoryStats.Usage usage = s.MemoryStats.Usage
limit = s.MemoryStats.Limit limit = s.MemoryStats.Limit
// Optionally discount cached memory if stats present.
if stats := s.MemoryStats.Stats; stats != nil { if stats := s.MemoryStats.Stats; stats != nil {
// The common Linux approach: usage - cache
if cache, ok := stats["cache"]; ok && cache <= usage { if cache, ok := stats["cache"]; ok && cache <= usage {
usage -= cache usage -= cache
} }

View File

@ -11,6 +11,8 @@ import (
const apiBaseURL = "https://newsapi.org/v2/top-headlines" const apiBaseURL = "https://newsapi.org/v2/top-headlines"
var forbiddenStrings = []string{"\r", "\\r", "\ufeff", "\u00A0"}
type configFile struct { type configFile struct {
News newsConfig News newsConfig
} }
@ -65,7 +67,12 @@ func FetchNews() News {
Status: err.Error(), 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) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return News{ return News{
@ -75,7 +82,7 @@ func FetchNews() News {
news := 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -83,3 +90,11 @@ func FetchNews() News {
return news return news
} }
func removeForbiddenStrings(s string) string {
result := s
for _, forbidden := range forbiddenStrings {
result = strings.ReplaceAll(result, forbidden, "")
}
return result
}

View File

@ -57,7 +57,12 @@ func FetchWarnings() []Warning {
Errormsg: err.Error(), 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) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return append(make([]Warning, 0), Warning{ return append(make([]Warning, 0), Warning{
@ -86,7 +91,12 @@ func FetchWarnings() []Warning {
Errormsg: err.Error(), 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) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return append(make([]Warning, 0), Warning{ return append(make([]Warning, 0), Warning{

View File

@ -41,7 +41,12 @@ func FetchCurrentWeather() CurrentWeather {
ErrorMessage: err.Error(), 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) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return CurrentWeather{ return CurrentWeather{

View File

@ -59,7 +59,12 @@ func (ph *PiHConnector) do(method string, endpoint string, body io.Reader) []byt
if err != nil { if err != nil {
return make([]byte, 0) 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) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
@ -90,7 +95,12 @@ func Connect() PiHConnector {
if err != nil { if err != nil {
panic(err) 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) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {

View File

@ -5,7 +5,6 @@ import (
"ArinDash/util" "ArinDash/util"
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/mum4k/termdash/cell" "github.com/mum4k/termdash/cell"
@ -55,33 +54,26 @@ func (widget *NewsText) drawNews() error {
return nil 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 { 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 widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed)))
return err
} }
article := widget.newsArticles.Articles[selected] article := widget.newsArticles.Articles[selected]
if err := widget.Write(fmt.Sprintf("%s\n", article.Author), text.WriteCellOpts(cell.FgColor(cell.ColorGray))); err != nil { 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 widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed)))
return err
} }
if err := widget.Write(fmt.Sprintf("%s\n\n", article.Title), text.WriteCellOpts(cell.FgColor(cell.ColorWhite), cell.Bold())); err != nil { 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 widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed)))
return err
} }
if err := widget.Write(fmt.Sprintf("%s\n\n", article.Description), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { 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 widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed)))
return err
} }
if err := widget.Write(fmt.Sprintf("%s\n", strings.ReplaceAll(article.Content, "\r", "")), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { if err := widget.Write(fmt.Sprintf("%s\n", article.Content), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); 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)))
return err
} }
if err := widget.Write(fmt.Sprintf("%s\n\n", article.PublishedAt), text.WriteCellOpts(cell.FgColor(cell.ColorGray))); err != nil { 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 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.URL)); err != nil { 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 widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed)))
return err
} }
return nil return nil
} }

View File

@ -28,8 +28,7 @@ func createWeather(ctx context.Context, _ terminalapi.Terminal, _ interface{}) w
weather := openWeatherMap.FetchCurrentWeather() weather := openWeatherMap.FetchCurrentWeather()
widget.Reset() widget.Reset()
if weather.ErrorMessage != "" { if weather.ErrorMessage != "" {
widget.Write(weather.ErrorMessage, text.WriteCellOpts(cell.FgColor(cell.ColorRed))) return widget.Write(weather.ErrorMessage, text.WriteCellOpts(cell.FgColor(cell.ColorRed)))
return nil
} }
weatherIcon := getIcon(weather.Weather[0].Icon) weatherIcon := getIcon(weather.Weather[0].Icon)

View File

@ -289,25 +289,20 @@ func (widget *MoodText) Keyboard(k *terminalapi.Keyboard, _ *widgetapi.EventMeta
} }
break break
case keyboard.KeyBackspace, keyboard.KeyBackspace2: case keyboard.KeyBackspace, keyboard.KeyBackspace2:
widget.WriteMood(0) return widget.WriteMood(0)
break
case '1': case '1':
widget.WriteMood(1) return widget.WriteMood(1)
break
case '2': case '2':
widget.WriteMood(2) return widget.WriteMood(2)
break
case '3': case '3':
widget.WriteMood(3) return widget.WriteMood(3)
break
default: default:
widget.WriteMood(-1) return widget.WriteMood(-1)
break
} }
return widget.drawTable() return widget.drawTable()
} }
func (widget *MoodText) WriteMood(mood int) { func (widget *MoodText) WriteMood(mood int) error {
m := moods() m := moods()
if m[strconv.Itoa(time.Now().Year())] == nil { if m[strconv.Itoa(time.Now().Year())] == nil {
m[strconv.Itoa(time.Now().Year())] = make(map[string]map[string]int) 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 m[strconv.Itoa(time.Now().Year())][strconv.Itoa(int(widget.selectedMonth))][strconv.Itoa(widget.selectedDay)] = mood
newMoods, err := json.MarshalIndent(m, "", " ") newMoods, err := json.MarshalIndent(m, "", " ")
if err != nil { 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 { if err := os.WriteFile(moodsFile, newMoods, 0644); 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)))
// ignore
} }
return nil
} }