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.
- **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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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{

View File

@ -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{

View File

@ -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 {

View File

@ -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
}

View File

@ -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)

View File

@ -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
}