Ensure consistent error handling and improve resource cleanup across widgets and APIs.
This commit is contained in:
parent
af5a39ef75
commit
2a66278cae
16
README.md
16
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.
|
- **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.
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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{
|
||||||
|
|||||||
@ -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{
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user