Compare commits

...

22 Commits

Author SHA1 Message Date
d4e3b6252b Expand list of forbidden Unicode characters in NewsAPI and refine warning rendering logic in NinaWarnings widget 2026-01-12 14:14:46 +01:00
915087d31a Fix position bounds and spacing logic in Zukitchi widget rendering 2026-01-02 17:02:37 +01:00
21ac083ef0 Add new moods (happy, cry, poop) with animations and ChangeMood function in Zukitchi widget 2026-01-02 16:56:47 +01:00
325144e0d9 Remove redundant return statements in YearMood widget's WriteMood logic 2026-01-02 16:36:29 +01:00
3e765c7b95 Add Zukitchi widget with animations, moods, and frame-based rendering 2026-01-02 10:57:14 +01:00
0a7e89307f Add frequency field to Pi-hole stats API and display queries per second in widget 2026-01-01 12:38:25 +01:00
2a66278cae Ensure consistent error handling and improve resource cleanup across widgets and APIs. 2026-01-01 12:10:33 +01:00
af5a39ef75 Move initialization of selectedMonth and selectedDay inside periodic update loop in YearMood widget 2025-12-31 16:11:48 +01:00
d2500351f9 Update .gitignore to include moods.json file 2025-12-31 16:06:35 +01:00
f05e7395f3 Add mood tracking persistence, keyboard navigation, and yearly mood summary to YearMood widget 2025-12-31 15:53:11 +01:00
2f918332a3 Fix conditional spacing logic in YearMood widget to prevent extra padding after December 2025-12-31 13:16:53 +01:00
3f25a632df Remove redundant left padding from the Every Day Mood container 2025-12-31 13:14:45 +01:00
ef962bbc49 Refine YearMood widget formatting: adjust day spacing and alignment for improved readability 2025-12-31 13:13:30 +01:00
97c26b30e9 Initializes YearMood widget to display daily mood tracking with color-coded calendar 2025-12-31 13:08:45 +01:00
45a8115d30 Refine widget formatting, adjust grid dimensions, and improve calendar functionality 2025-12-31 01:28:00 +01:00
0fc1ea476b Adjust grid layout, optimize widget placement, and refine QR Code rendering logic 2025-12-31 01:04:26 +01:00
7a846ee660 Add Zukitchi widget with animated terminal pet and update README for new features 2025-12-30 23:51:54 +01:00
dfbc6066c9 Add Calendar Events widget, integrate ICS calendar parsing, and refine related UI components 2025-12-30 00:59:56 +01:00
a29beeda43 Fix index wraparound logic in News widget selection 2025-12-29 23:34:48 +01:00
9b5afc3d7c Add News widget and integrate NewsAPI for live updates 2025-12-29 21:19:05 +01:00
d638a8ae97 Add Nina Warnings widget and integrate BBK warning API 2025-12-29 17:56:50 +01:00
52d3b1196b Update README and configuration template to include Weather widget setup details 2025-12-29 07:19:21 +01:00
30 changed files with 2694 additions and 138 deletions

1
.gitignore vendored
View File

@@ -109,5 +109,6 @@ go.work.sum
config.toml config.toml
devices.json devices.json
websites.json websites.json
moods.json
old old

View File

@@ -4,12 +4,17 @@ ArinDash is a customizable terminal dashboard (TUI) written in Go, powered by [`
## Features ## Features
- **Clock & Date**: Real-time time and date display. - **Clock, Date & Calendar**: Real-time time, date, and calendar display.
- **Weather**: Current weather information via OpenWeatherMap.
- **Pi-hole Integration**: Monitor your Pi-hole statistics, including query counts and blocked percentages. - **Pi-hole Integration**: Monitor your Pi-hole statistics, including query counts and blocked percentages.
- **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.
- **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. - **Interactive TUI**: Built with `termdash` for a responsive and visually appealing terminal interface.
## Prerequisites ## Prerequisites
@@ -42,7 +47,10 @@ 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/).
- **NINA Warnings**: Set your `gebietsCode` for the area you want to monitor.
- **News**: Provide your News API key and preferred sources.
3. Customize widgets: 3. Customize widgets:
- **Websites**: Copy `websites_template.json` to `websites.json` and update it with the URLs and icons you want to monitor. - **Websites**: Copy `websites_template.json` to `websites.json` and update it with the URLs and icons you want to monitor.
@@ -50,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
@@ -58,6 +66,12 @@ go run main.go
### Controls ### Controls
- **Esc / Ctrl+C**: Exit the application. - **Esc / Ctrl+C**: Exit the application.
- **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
@@ -69,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.

89
apis/calendar/main.go Normal file
View File

@@ -0,0 +1,89 @@
package calendar
import (
"ArinDash/config"
"sort"
"time"
"github.com/arran4/golang-ical"
)
type configFile struct {
Calendar calendarConfig
}
type calendarConfig struct {
ICS []ICS
Icon string
}
type ICS struct {
Icon string
URL string
}
type Event struct {
UID string
Summary string
Description string
Start time.Time
End time.Time
Location string
Icon string
}
func FetchEvents() []Event {
cfg := &configFile{}
config.LoadConfig(cfg)
events := make([]Event, 0)
for _, ic := range cfg.Calendar.ICS {
cal, err := ics.ParseCalendarFromUrl(ic.URL)
if err != nil {
panic(err)
}
for _, event := range cal.Events() {
startAt, err := event.GetStartAt()
if err != nil {
startAt = time.Now()
}
endAt, err := event.GetEndAt()
if err != nil {
endAt = startAt
}
events = append(events, Event{
UID: getValue(event, ics.ComponentPropertyUniqueId),
Icon: ic.Icon,
Summary: getValue(event, ics.ComponentPropertySummary),
Description: getValue(event, ics.ComponentPropertyDescription),
Start: startAt,
End: endAt,
Location: getValue(event, ics.ComponentPropertyLocation),
})
}
}
result := filter(events, func(i Event) bool {
return i.End.After(time.Now())
})
sort.Slice(result, func(i, j int) bool {
return result[i].Start.Before(result[j].Start)
})
return result
}
func getValue(event *ics.VEvent, property ics.ComponentProperty) string {
ianaProperty := event.GetProperty(property)
if ianaProperty == nil {
return ""
}
return ianaProperty.Value
}
func filter[T any](ss []T, test func(T) bool) (ret []T) {
for _, s := range ss {
if test(s) {
ret = append(ret, s)
}
}
return
}

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

@@ -84,6 +84,7 @@ func knownDevices() map[string]Device {
} }
func arpDevices() map[string]Device { func arpDevices() map[string]Device {
exec.Command("ip", "n", "show")
arp := exec.Command("arp", "-a") arp := exec.Command("arp", "-a")
var out bytes.Buffer var out bytes.Buffer
arp.Stdout = &out arp.Stdout = &out

109
apis/newsapi/main.go Normal file
View File

@@ -0,0 +1,109 @@
package news
import (
"ArinDash/config"
"encoding/json"
"io"
"log"
"net/http"
"strings"
)
const apiBaseURL = "https://newsapi.org/v2/top-headlines"
var forbiddenStrings = []string{
"\r", "\\r", "\ufeff", "\u00A0", "\u200b", "\u200c",
"\u200d", // Zero Width Joiner
"\u200e", // Left-to-Right Mark
"\u200f", // Right-to-Left Mark
"\u2060", // Word Joiner
"\ufe0f", // Variation Selector-16
"\u2028",
"\u2029",
}
type configFile struct {
News newsConfig
}
type newsConfig struct {
ApiKey string
Sources string
}
type News struct {
Status string `json:"status"`
TotalResults int `json:"totalResults"`
Articles []Article `json:"articles"`
}
type Article struct {
Source Source `json:"source"`
Author string `json:"author"`
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
URLToImage string `json:"urlToImage"`
PublishedAt string `json:"publishedAt"`
Content string `json:"content"`
}
type Source struct {
ID string `json:"id"`
Name string `json:"name"`
}
func FetchNews() News {
cfg := &configFile{}
config.LoadConfig(cfg)
client := &http.Client{}
if cfg.News.ApiKey == "" {
return News{
Status: "No API key provided",
}
}
req, err := http.NewRequest("GET", apiBaseURL+"?sources="+cfg.News.Sources+"&pageSize=100&apiKey="+cfg.News.ApiKey, nil)
if err != nil {
return News{
Status: err.Error(),
}
}
resp, err := client.Do(req)
if err != nil {
return News{
Status: err.Error(),
}
}
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{
Status: err.Error(),
}
}
news := News{}
err = json.Unmarshal([]byte(removeForbiddenStrings(string(respBody))), &news)
if err != nil {
log.Fatal(err)
}
return news
}
func removeForbiddenStrings(s string) string {
result := s
for _, forbidden := range forbiddenStrings {
result = strings.ReplaceAll(result, forbidden, "")
}
return result
}

111
apis/nina/main.go Normal file
View File

@@ -0,0 +1,111 @@
package nina
import (
"ArinDash/config"
"encoding/json"
"io"
"net/http"
)
const apiBaseURL = "https://warnung.bund.de/api31"
type configFile struct {
Nina ninaConfig
}
type ninaConfig struct {
GebietsCode string
}
type Warning struct {
Id string `json:"id"`
Identifier string `json:"identifier"`
Sender string `json:"sender"`
Sent string `json:"sent"`
Status string `json:"status"`
MsgType string `json:"msgType"`
Scope string `json:"scope"`
Code []string `json:"code"`
Reference string `json:"reference"`
Info []Info `json:"info"`
Errormsg string `json:"errormsg"`
}
type Info struct {
Language string `json:"language"`
Category []string `json:"category"`
Event string `json:"event"`
Urgency string `json:"urgency"`
Severity string `json:"severity"`
Expires string `json:"expires"`
Headline string `json:"headline"`
Description string `json:"description"`
}
func FetchWarnings() []Warning {
cfg := &configFile{}
config.LoadConfig(cfg)
client := &http.Client{}
req, err := http.NewRequest("GET", apiBaseURL+"/dashboard/"+cfg.Nina.GebietsCode+".json", nil)
if err != nil {
panic(err)
}
resp, err := client.Do(req)
if err != nil {
return append(make([]Warning, 0), Warning{
Errormsg: err.Error(),
})
}
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{
Errormsg: err.Error(),
})
}
warnings := make([]Warning, 0)
err = json.Unmarshal(respBody, &warnings)
if err != nil {
return append(make([]Warning, 0), Warning{
Errormsg: err.Error(),
})
}
result := make([]Warning, 0)
for _, warning := range warnings {
req, err := http.NewRequest("GET", apiBaseURL+"/warnings/"+warning.Id+".json", nil)
if err != nil {
return append(make([]Warning, 0), Warning{
Errormsg: err.Error(),
})
}
resp, err := client.Do(req)
if err != nil {
return append(make([]Warning, 0), Warning{
Errormsg: err.Error(),
})
}
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{
Errormsg: err.Error(),
})
}
warning := Warning{}
err = json.Unmarshal(respBody, &warning)
result = append(result, warning)
}
return result
}

View File

@@ -3,21 +3,22 @@ package openWeatherMap
import "time" import "time"
type CurrentWeather struct { type CurrentWeather struct {
Base string `json:"base"` Base string `json:"base"`
Visibility int `json:"visibility"` Visibility int `json:"visibility"`
Dt int `json:"dt"` Dt int `json:"dt"`
Timezone int `json:"timezone"` Timezone int `json:"timezone"`
Id int `json:"id"` Id int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Cod int `json:"cod"` Cod int `json:"cod"`
Coordinates Coordinates `json:"coord"` Coordinates Coordinates `json:"coord"`
Weather []Weather `json:"weather"` Weather []Weather `json:"weather"`
Wind Wind `json:"wind"` Wind Wind `json:"wind"`
Clouds Clouds `json:"clouds"` Clouds Clouds `json:"clouds"`
Rain Rain `json:"rain"` Rain Rain `json:"rain"`
Snow Snow `json:"snow"` Snow Snow `json:"snow"`
Sys Sys `json:"sys"` Sys Sys `json:"sys"`
Main Main `json:"main"` Main Main `json:"main"`
ErrorMessage string
} }
type Coordinates struct { type Coordinates struct {

View File

@@ -4,7 +4,6 @@ import (
"ArinDash/config" "ArinDash/config"
"encoding/json" "encoding/json"
"io" "io"
"log"
"net/http" "net/http"
) )
@@ -24,19 +23,35 @@ func FetchCurrentWeather() CurrentWeather {
config.LoadConfig(cfg) config.LoadConfig(cfg)
client := &http.Client{} client := &http.Client{}
currentWeather := &CurrentWeather{} currentWeather := &CurrentWeather{}
if cfg.OpenWeatherMap.ApiKey == "" {
return CurrentWeather{
ErrorMessage: "No API key provided",
}
}
req, err := http.NewRequest("GET", apiBaseURL+"?id="+cfg.OpenWeatherMap.LocationId+"&units=metric&lang=en&APPID="+cfg.OpenWeatherMap.ApiKey, nil) req, err := http.NewRequest("GET", apiBaseURL+"?id="+cfg.OpenWeatherMap.LocationId+"&units=metric&lang=en&APPID="+cfg.OpenWeatherMap.ApiKey, nil)
if err != nil { if err != nil {
panic(err) return CurrentWeather{
ErrorMessage: err.Error(),
}
} }
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
log.Fatal(err) return 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) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Fatal(err) return CurrentWeather{
ErrorMessage: err.Error(),
}
} }
err = json.Unmarshal(respBody, currentWeather) err = json.Unmarshal(respBody, currentWeather)

View File

@@ -4,7 +4,6 @@ import (
"ArinDash/config" "ArinDash/config"
"encoding/json" "encoding/json"
"io" "io"
"log"
"net/http" "net/http"
"strings" "strings"
) )
@@ -50,7 +49,7 @@ func (ph *PiHConnector) do(method string, endpoint string, body io.Reader) []byt
req, err := http.NewRequest(method, requestString, body) req, err := http.NewRequest(method, requestString, body)
if err != nil { if err != nil {
log.Fatal(err) return make([]byte, 0)
} }
req.Header.Add("X-FTL-SID", ph.Session.SID) req.Header.Add("X-FTL-SID", ph.Session.SID)
@@ -58,13 +57,18 @@ func (ph *PiHConnector) do(method string, endpoint string, body io.Reader) []byt
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
log.Fatal(err) 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 {
log.Fatal(err) return make([]byte, 0)
} }
return respBody return respBody
@@ -79,27 +83,35 @@ func Connect() PiHConnector {
cfg := &configFile{} cfg := &configFile{}
config.LoadConfig(cfg) config.LoadConfig(cfg)
client := &http.Client{} client := &http.Client{}
if cfg.Pihole.Host == "" {
return PiHConnector{}
}
req, err := http.NewRequest("POST", "http://"+cfg.Pihole.Host+"/api/auth", strings.NewReader("{\"password\": \""+cfg.Pihole.Password+"\"}")) req, err := http.NewRequest("POST", "http://"+cfg.Pihole.Host+"/api/auth", strings.NewReader("{\"password\": \""+cfg.Pihole.Password+"\"}"))
if err != nil { if err != nil {
log.Fatal(err) panic(err)
} }
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
log.Fatal(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 {
log.Fatal(err) panic(err)
} }
s := &PiHAuth{} s := &PiHAuth{}
err = json.Unmarshal(respBody, s) err = json.Unmarshal(respBody, s)
if err != nil { if err != nil {
log.Fatal(err) panic(err)
} }
connector = &PiHConnector{ connector = &PiHConnector{
Host: cfg.Pihole.Host, Host: cfg.Pihole.Host,

View File

@@ -17,6 +17,7 @@ type Queries struct {
UniqueDomains int64 `json:"unique_domains"` UniqueDomains int64 `json:"unique_domains"`
Forwarded int64 `json:"forwarded"` Forwarded int64 `json:"forwarded"`
Cached int64 `json:"cached"` Cached int64 `json:"cached"`
Frequency int64 `json:"frequency"`
} }
type Clients struct { type Clients struct {

View File

@@ -1,6 +1,7 @@
package config package config
import ( import (
"fmt"
"os" "os"
"github.com/pelletier/go-toml/v2" "github.com/pelletier/go-toml/v2"
@@ -8,6 +9,7 @@ import (
func LoadConfig(config interface{}) { func LoadConfig(config interface{}) {
if err := toml.Unmarshal(readFile("config.toml"), config); err != nil { if err := toml.Unmarshal(readFile("config.toml"), config); err != nil {
fmt.Println(err)
config = nil config = nil
} }
} }

View File

@@ -8,3 +8,25 @@ Password = "generate-an-app-password-in-pi-hole"
Auth = "WPA" Auth = "WPA"
SSID = "YourSSID" SSID = "YourSSID"
Password = "YourPassword" Password = "YourPassword"
[openWeatherMap]
#ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
LocationId = "0000000"
ApiKey = "YourApiKey"
[nina]
#Code from https://www.xrepository.de/api/xrepository/urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs_2021-07-31/download/Regionalschl_ssel_2021-07-31.json
gebietsCode = "000000000000"
[news]
#apiKey from newsapi.org
ApiKey = "ApiKey"
Sources = "comma,sepparated,sources,from,newsapi"
[[calendar.ics]]
Icon = ""
Url = "https://url.to.ics"
[[calendar.ics]]
Icon = ""
Url = "https://url.to.second.ics"

1
go.mod
View File

@@ -3,6 +3,7 @@ module ArinDash
go 1.25.5 go 1.25.5
require ( require (
github.com/arran4/golang-ical v0.3.2
github.com/docker/docker v28.5.2+incompatible github.com/docker/docker v28.5.2+incompatible
github.com/mum4k/termdash v0.20.0 github.com/mum4k/termdash v0.20.0
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4

2
go.sum
View File

@@ -2,6 +2,8 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/arran4/golang-ical v0.3.2 h1:MGNjcXJFSuCXmYX/RpZhR2HDCYoFuK8vTPFLEdFC3JY=
github.com/arran4/golang-ical v0.3.2/go.mod h1:xblDGxxIUMWwFZk9dlECUlc1iXNV65LJZOTHLVwu8bo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=

214
main.go
View File

@@ -35,6 +35,8 @@ func initWidgets(ctx context.Context, term *tcell.Terminal) {
widgets.New("Clock", widgets.Clock()), widgets.New("Clock", widgets.Clock()),
widgets.New("Date", widgets.Date()), widgets.New("Date", widgets.Date()),
widgets.New("Calendar", widgets.Calendar()), widgets.New("Calendar", widgets.Calendar()),
widgets.New("CalendarEvents", widgets.CalendarEvents()),
widgets.New("YearMood", widgets.YearMood()),
widgets.New("Weather", widgets.Weather()), widgets.New("Weather", widgets.Weather()),
widgets.New("ChangeTitle", widgets.TextInput(titleUpdate, textinput.Label("Update Title: "), textinput.PlaceHolder("New Title"))), widgets.New("ChangeTitle", widgets.TextInput(titleUpdate, textinput.Label("Update Title: "), textinput.PlaceHolder("New Title"))),
widgets.New("Wifi", widgets.WifiQRCode()), widgets.New("Wifi", widgets.WifiQRCode()),
@@ -43,90 +45,176 @@ func initWidgets(ctx context.Context, term *tcell.Terminal) {
widgets.New("HTTPProber", widgets.HTTPProber()), widgets.New("HTTPProber", widgets.HTTPProber()),
widgets.New("PiHole", widgets.PiholeStats()), widgets.New("PiHole", widgets.PiholeStats()),
widgets.New("PiHoleBlocked", widgets.PiholeBlocked()), widgets.New("PiHoleBlocked", widgets.PiholeBlocked()),
widgets.New("NinaWarnings", widgets.NinaWarnings()),
widgets.New("News", widgets.News()),
widgets.New("Zukitchi", widgets.Zukitchi()),
) )
} }
func layout() []container.Option { func layout() []container.Option {
builder := grid.New() builder := grid.New()
builder.Add( builder.Add(
grid.ColWidthFixed(84, grid.ColWidthFixed(84,
grid.RowHeightFixed(44, grid.RowHeightFixed(21,
grid.Widget(widgets.Get["Wifi"], grid.ColWidthFixed(38,
container.BorderTitle("Wifi"), grid.RowHeightFixed(20,
container.Border(linestyle.Light), grid.Widget(widgets.Get["Wifi"],
container.BorderColor(cell.ColorWhite)), container.BorderTitle("Wifi"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
container.PaddingLeft(1),
container.PaddingTop(1),
),
),
),
grid.ColWidthFixed(20,
grid.Widget(widgets.Get["NetworkDevices"],
container.BorderTitle("Network Devices"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
container.PaddingLeft(1),
container.PaddingTop(1),
),
),
), ),
grid.RowHeightFixed(44,
grid.Widget(widgets.Get["NetworkDevices"], grid.RowHeightFixed(25,
container.BorderTitle("Network Devices"), grid.RowHeightFixed(19,
container.Border(linestyle.Light), grid.Widget(widgets.Get["HTTPProber"],
container.BorderColor(cell.ColorWhite)), container.BorderTitle("Website Status"),
), container.Border(linestyle.Light),
grid.RowHeightFixed(1, container.BorderColor(cell.ColorWhite),
grid.Widget(widgets.Get["empty"]), container.PaddingLeft(1),
container.PaddingTop(1),
),
),
grid.RowHeightFixed(40,
grid.Widget(widgets.Get["Docker"],
container.BorderTitle("Docker"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
container.PaddingLeft(1),
container.PaddingTop(1),
),
),
), ),
), ),
grid.ColWidthPerc(20,
grid.RowHeightFixed(8, grid.ColWidthPerc(62,
grid.Widget(widgets.Get["Clock"], grid.RowHeightFixed(40,
container.AlignHorizontal(align.HorizontalCenter), grid.ColWidthFixed(40,
container.BorderTitle("Time"), grid.RowHeightFixed(8,
container.Border(linestyle.Light), grid.Widget(widgets.Get["Clock"],
container.BorderColor(cell.ColorWhite), container.AlignHorizontal(align.HorizontalCenter),
container.BorderTitle("Time"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
),
),
grid.RowHeightFixed(13,
grid.Widget(widgets.Get["PiHole"],
container.BorderTitle("pi-hole"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
container.PaddingLeft(1),
container.PaddingTop(1),
),
),
grid.RowHeightFixed(38,
grid.Widget(widgets.Get["PiHoleBlocked"],
container.BorderTitle("pi-hole (Blocked Percent)"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
),
),
),
grid.ColWidthFixed(26,
grid.RowHeightFixed(21,
grid.Widget(widgets.Get["Calendar"],
container.BorderTitle("Calendar"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
container.PaddingLeft(1),
container.PaddingTop(1),
),
),
grid.RowHeightFixed(25,
grid.Widget(widgets.Get["empty"],
container.Border(linestyle.Light),
container.BorderColor(cell.ColorGreen),
),
),
),
grid.ColWidthFixed(25,
grid.RowHeightFixed(21,
grid.Widget(widgets.Get["CalendarEvents"],
container.BorderTitle("Events"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
container.PaddingLeft(1),
container.PaddingTop(1),
),
),
grid.RowHeightFixed(11,
grid.Widget(widgets.Get["Weather"],
container.BorderTitle("Weather"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
container.PaddingLeft(1),
),
),
grid.RowHeightFixed(85,
grid.Widget(widgets.Get["empty"],
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
),
),
), ),
), ),
grid.RowHeightFixed(12,
grid.Widget(widgets.Get["PiHole"],
container.BorderTitle("pi-hole"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
),
),
grid.RowHeightFixed(24,
grid.Widget(widgets.Get["PiHoleBlocked"],
container.BorderTitle("pi-hole (Blocked Percent)"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
),
),
grid.RowHeightPerc(85,
grid.Widget(widgets.Get["empty"]),
),
),
grid.ColWidthPerc(13,
grid.RowHeightFixed(20, grid.RowHeightFixed(20,
grid.Widget(widgets.Get["Calendar"], grid.RowHeightFixed(20,
container.BorderTitle("Calendar"), grid.Widget(widgets.Get["News"],
container.Border(linestyle.Light), container.BorderTitle("News"),
container.BorderColor(cell.ColorWhite)), container.Border(linestyle.Light),
), container.BorderColor(cell.ColorWhite),
grid.RowHeightPerc(25, container.PaddingLeft(1),
grid.Widget(widgets.Get["empty"]), container.PaddingTop(1),
), ),
), ),
grid.ColWidthPerc(25, grid.RowHeightFixed(20,
grid.RowHeightFixed(20, grid.Widget(widgets.Get["NinaWarnings"],
grid.Widget(widgets.Get["Weather"], container.BorderTitle("BBK Warnings"),
container.BorderTitle("Weather"), container.Border(linestyle.Light),
container.Border(linestyle.Light), container.BorderColor(cell.ColorWhite),
container.BorderColor(cell.ColorWhite), container.PaddingLeft(1),
container.PaddingTop(1),
),
), ),
), ),
grid.RowHeightPerc(25,
grid.Widget(widgets.Get["empty"]),
),
), ),
grid.ColWidthPerc(35, grid.ColWidthPerc(35,
grid.RowHeightPerc(20, grid.RowHeightFixed(21,
grid.Widget(widgets.Get["HTTPProber"], grid.Widget(widgets.Get["Zukitchi"],
container.BorderTitle("Website Status"),
container.Border(linestyle.Light), container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite), container.BorderColor(cell.ColorWhite),
container.PaddingTop(1),
container.PaddingLeft(1),
), ),
), ),
grid.RowHeightPerc(25, grid.RowHeightFixed(39,
grid.Widget(widgets.Get["Docker"], grid.Widget(widgets.Get["YearMood"],
container.BorderTitle("Docker"), container.BorderTitle("Every Day Mood"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
container.PaddingTop(1),
),
),
grid.RowHeightFixed(85,
grid.Widget(widgets.Get["empty"],
container.Border(linestyle.Light), container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite), container.BorderColor(cell.ColorWhite),
), ),

16
test/test.go Normal file
View File

@@ -0,0 +1,16 @@
package main
import "fmt"
func main() {
lines := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
length := (len(lines)) / 2
for i := range length {
fmt.Println("-")
fmt.Printf("%d=>%d\n", lines[i*2], lines[i*2+1])
fmt.Println("-")
}
if len(lines)%2 == 1 {
fmt.Printf("%d", lines[len(lines)-1])
}
}

View File

@@ -24,12 +24,12 @@ func createCalendar(ctx context.Context, _ terminalapi.Terminal, _ interface{})
widget := util.PanicOnErrorWithResult(text.New()) widget := util.PanicOnErrorWithResult(text.New())
go util.Periodic(ctx, 1*time.Hour, func() error { go util.Periodic(ctx, 1*time.Hour, func() error {
date := time.Now()
widget.Reset() widget.Reset()
if err := widget.Write("┌────────────────────┐\n", text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { if err := widget.Write("┌────────────────────┐\n", text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
if err := widget.Write(fmt.Sprintf("│%-20s│\n", time.Now().Format("January 2006")), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { if err := widget.Write(fmt.Sprintf("│%-20s│\n", date.Format("January 2006")), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
if err := widget.Write("├──┬──┬──┬──┬──┬──┬──┤\n", text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { if err := widget.Write("├──┬──┬──┬──┬──┬──┬──┤\n", text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
@@ -38,7 +38,7 @@ func createCalendar(ctx context.Context, _ terminalapi.Terminal, _ interface{})
if err := createRow(widget, "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"); err != nil { if err := createRow(widget, "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"); err != nil {
return err return err
} }
if err := createTableForMonth(widget); err != nil { if err := createTableForMonth(date, widget); err != nil {
return err return err
} }
if err := createLowerBorder(widget); err != nil { if err := createLowerBorder(widget); err != nil {
@@ -50,23 +50,23 @@ func createCalendar(ctx context.Context, _ terminalapi.Terminal, _ interface{})
return widget return widget
} }
func createTableForMonth(widget *text.Text) error { func createTableForMonth(date time.Time, widget *text.Text) error {
table := make([]int, 0) table := make([]int, 0)
now := time.Now() firstMonthday := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, time.UTC)
firstMonthday := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
for range (firstMonthday.Weekday() + 6) % 7 { for range (firstMonthday.Weekday() + 6) % 7 {
table = append(table, 0) table = append(table, 0)
} }
//34
//35
for { for {
day := firstMonthday.Day() day := firstMonthday.Day()
table = append(table, day) table = append(table, day)
firstMonthday = firstMonthday.AddDate(0, 0, 1) firstMonthday = firstMonthday.AddDate(0, 0, 1)
if firstMonthday.Month() != now.Month() { if firstMonthday.Month() != date.Month() {
break break
} }
} }
remainingDays := 8 - len(table)/7 remainingDays := 7 - len(table)%7
for range remainingDays { for range remainingDays {
table = append(table, 0) table = append(table, 0)
} }
@@ -78,7 +78,7 @@ func createTableForMonth(widget *text.Text) error {
} }
} }
var str string var str string
if err := widget.Write("│"); err != nil { if err := widget.Write("│", text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
if field == 0 { if field == 0 {
@@ -87,7 +87,7 @@ func createTableForMonth(widget *text.Text) error {
str = fmt.Sprintf("%2d", field) str = fmt.Sprintf("%2d", field)
} }
var color text.WriteOption var color text.WriteOption
if field == now.Day() { if field == date.Day() {
color = text.WriteCellOpts(cell.FgColor(cell.ColorBlack), cell.BgColor(cell.ColorWhite), cell.Bold()) color = text.WriteCellOpts(cell.FgColor(cell.ColorBlack), cell.BgColor(cell.ColorWhite), cell.Bold())
} else { } else {
color = text.WriteCellOpts(cell.FgColor(cell.ColorWhite), cell.BgColor(cell.ColorDefault)) color = text.WriteCellOpts(cell.FgColor(cell.ColorWhite), cell.BgColor(cell.ColorDefault))
@@ -96,7 +96,7 @@ func createTableForMonth(widget *text.Text) error {
return err return err
} }
if index%7 == 6 { if index%7 == 6 {
if err := widget.Write("│\n"); err != nil { if err := widget.Write("│\n", text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
} }

89
widgets/calendarEvents.go Normal file
View File

@@ -0,0 +1,89 @@
package widgets
import (
"ArinDash/apis/calendar"
"ArinDash/util"
"context"
"fmt"
"time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/text"
)
type CalendarEventsOptions struct {
}
func CalendarEvents() CalendarEventsOptions {
widgetOptions["CalendarEventsOptions"] = createCalendarEventsOptions
return CalendarEventsOptions{}
}
func createCalendarEventsOptions(ctx context.Context, _ terminalapi.Terminal, _ interface{}) widgetapi.Widget {
widget := util.PanicOnErrorWithResult(text.New(text.WrapAtWords()))
go util.Periodic(ctx, 1*time.Hour, func() error {
widget.Reset()
calendarEvents := calendar.FetchEvents()
for _, event := range calendarEvents {
var color cell.Color
onlySummary := false
if isToday(event.Start) {
color = cell.ColorGreen
} else if event.Start.After(time.Now().AddDate(0, 0, 7)) {
color = cell.ColorGray
onlySummary = true
} else {
color = cell.ColorWhite
}
summary := event.Summary
if onlySummary {
summary = fmt.Sprintf("%s %s", event.Start.Format(time.DateOnly), event.Summary)
}
if err := widget.Write(fmt.Sprintf("%s %s\n", event.Icon, summary), text.WriteCellOpts(cell.FgColor(color), cell.Bold())); err != nil {
return err
}
if !onlySummary {
var date string
if event.Start == event.End {
date = formatDate(event.Start)
} else {
date = fmt.Sprintf("%s - %s", formatDate(event.Start), formatDate(event.End))
}
if err := widget.Write(fmt.Sprintf("%s\n", date), text.WriteCellOpts(cell.FgColor(color), cell.Dim())); err != nil {
return err
}
if err := widget.Write(fmt.Sprintf("%s\n", event.Description), text.WriteCellOpts(cell.FgColor(color))); err != nil {
return err
}
if err := widget.Write(fmt.Sprintf("%s\n\n", event.Location), text.WriteCellOpts(cell.FgColor(color))); err != nil {
return err
}
}
}
return nil
})
return widget
}
func isToday(date time.Time) bool {
year, month, day := date.Date()
return year == time.Now().Year() && month == time.Now().Month() && day == time.Now().Day()
}
func isTomorrow(date time.Time) bool {
year, month, day := date.Date()
return year == time.Now().Year() && month == time.Now().Month() && day == time.Now().AddDate(0, 0, 1).Day()
}
func formatDate(date time.Time) string {
if isToday(date) {
return fmt.Sprintf("Today at %s", date.Format(time.TimeOnly))
} else if isTomorrow(date) {
return fmt.Sprintf("Tomorrow at %s", date.Format(time.TimeOnly))
}
return date.Format(time.DateTime)
}

View File

@@ -33,10 +33,10 @@ func createDockerList(ctx context.Context, _ terminalapi.Terminal, _ interface{}
} }
sort.Slice(ms, func(i, j int) bool { return ms[i].CPUPercent > ms[j].CPUPercent }) sort.Slice(ms, func(i, j int) bool { return ms[i].CPUPercent > ms[j].CPUPercent })
if err := list.Write(fmt.Sprintf("%-40s | %-6s | %-6s | %s\n", "Name", "CPU", "MEM", "Status"), text.WriteReplace()); err != nil { if err := list.Write(fmt.Sprintf("%-30s | %-6s | %-6s | %s\n", "Name", "CPU", "MEM", "Status"), text.WriteReplace(), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
if err := list.Write(fmt.Sprintf("─────────────────────────────────────────┼────────┼────────┼─────────────────────\n")); err != nil { if err := list.Write(fmt.Sprintf("───────────────────────────────┼────────┼────────┼─────────────────────\n"), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
for _, m := range ms { for _, m := range ms {
@@ -53,22 +53,28 @@ func createDockerList(ctx context.Context, _ terminalapi.Terminal, _ interface{}
if strings.Contains(m.Status, "Paused") { if strings.Contains(m.Status, "Paused") {
status = cell.ColorBlue status = cell.ColorBlue
} }
if err := list.Write(fmt.Sprintf("%-40s", m.Name), text.WriteCellOpts(cell.FgColor(status))); err != nil { var name string
if len(m.Name) > 30 {
name = m.Name[:27] + "..."
} else {
name = m.Name
}
if err := list.Write(fmt.Sprintf("%-30s", name), text.WriteCellOpts(cell.FgColor(status))); err != nil {
return err return err
} }
if err := list.Write(fmt.Sprint(" | ")); err != nil { if err := list.Write(fmt.Sprint(" "), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
if err := writePercent(m.CPUPercent, list); err != nil { if err := writePercent(m.CPUPercent, list); err != nil {
return err return err
} }
if err := list.Write(fmt.Sprint(" | ")); err != nil { if err := list.Write(fmt.Sprint(" "), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
if err := writePercent(m.MemPercent, list); err != nil { if err := writePercent(m.MemPercent, list); err != nil {
return err return err
} }
if err := list.Write(fmt.Sprint(" | ")); err != nil { if err := list.Write(fmt.Sprint(" "), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
if err := list.Write(fmt.Sprintf("%s\n", m.Status), text.WriteCellOpts(cell.FgColor(status))); err != nil { if err := list.Write(fmt.Sprintf("%s\n", m.Status), text.WriteCellOpts(cell.FgColor(status))); err != nil {

View File

@@ -33,9 +33,9 @@ func createNetworkDevicesList(ctx context.Context, _ terminalapi.Terminal, _ int
if devices[mac].Online { if devices[mac].Online {
status = cell.ColorGreen status = cell.ColorGreen
} }
if err := list.Write(fmt.Sprintf("|%-15s|", mac), text.WriteCellOpts(cell.FgColor(cell.ColorGray))); err != nil { //if err := list.Write(fmt.Sprintf("|%-15s|", mac), text.WriteCellOpts(cell.FgColor(cell.ColorGray))); err != nil {
return err // return err
} //}
if err := list.Write(fmt.Sprintf("%2s ", devices[mac].Icon), text.WriteCellOpts(cell.BgColor(status), cell.FgColor(cell.ColorBlack))); err != nil { if err := list.Write(fmt.Sprintf("%2s ", devices[mac].Icon), text.WriteCellOpts(cell.BgColor(status), cell.FgColor(cell.ColorBlack))); err != nil {
return err return err
} }

106
widgets/news.go Normal file
View File

@@ -0,0 +1,106 @@
package widgets
import (
news "ArinDash/apis/newsapi"
"ArinDash/util"
"context"
"fmt"
"time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/keyboard"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/text"
)
type NewsOptions struct {
}
func News() NewsOptions {
widgetOptions["NewsOptions"] = createNewsOptions
return NewsOptions{}
}
func createNewsOptions(ctx context.Context, _ terminalapi.Terminal, _ interface{}) widgetapi.Widget {
widget := util.PanicOnErrorWithResult(NewNewsText(text.WrapAtWords()))
go util.Periodic(ctx, 1*time.Hour, func() error {
widget.newsArticles = news.FetchNews()
if widget.newsArticles.Status != "ok" {
if err := widget.Write(widget.newsArticles.Status, text.WriteCellOpts(cell.FgColor(cell.ColorRed))); err != nil {
return err
}
}
return nil
})
go util.Periodic(ctx, 30*time.Second, func() error {
err := widget.drawNews()
if err != nil {
return err
}
if len(widget.newsArticles.Articles) > 0 {
widget.Selected = (widget.Selected + 1) % len(widget.newsArticles.Articles)
}
return nil
})
return widget
}
func (widget *NewsText) drawNews() error {
selected := widget.Selected
widget.Reset()
if len(widget.newsArticles.Articles) == 0 {
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 {
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 {
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 {
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 {
return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed)))
}
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 {
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 {
return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed)))
}
return nil
}
type NewsText struct {
*text.Text
Selected int
newsArticles news.News
}
func NewNewsText(opts ...text.Option) (*NewsText, error) {
t, err := text.New(opts...)
if err != nil {
return nil, err
}
return &NewsText{Text: t}, nil
}
func (widget *NewsText) Keyboard(k *terminalapi.Keyboard, _ *widgetapi.EventMeta) error {
if k.Key == keyboard.KeyArrowLeft {
if widget.Selected == 0 {
widget.Selected = len(widget.newsArticles.Articles) - 1
} else {
widget.Selected = widget.Selected - 1
}
} else if k.Key == keyboard.KeyArrowRight {
widget.Selected = (widget.Selected + 1) % len(widget.newsArticles.Articles)
}
return widget.drawNews()
}

71
widgets/ninaWarnings.go Normal file
View File

@@ -0,0 +1,71 @@
package widgets
import (
"ArinDash/apis/nina"
"ArinDash/util"
"context"
"fmt"
"regexp"
"time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/text"
)
type NinaWarningsOptions struct {
}
/*dwd.2.49.0.0.276.0.DWD.PVW.1768147140000.877ab163-e3e6-4e9a-8f79-9ffce1a70629.MUL*/
func NinaWarnings() NinaWarningsOptions {
widgetOptions["NinaWarningsOptions"] = createNinaWarnings
return NinaWarningsOptions{}
}
func createNinaWarnings(ctx context.Context, _ terminalapi.Terminal, _ interface{}) widgetapi.Widget {
widget := util.PanicOnErrorWithResult(text.New(text.WrapAtWords()))
go util.Periodic(ctx, 1*time.Minute, func() error {
warnings := nina.FetchWarnings()
widget.Reset()
for _, warning := range warnings {
for _, info := range warning.Info {
if info.Language == "de-DE" {
var options []cell.Option
if info.Severity == "Moderate" {
options = append(options, cell.FgColor(cell.ColorRed))
} else if info.Severity == "Minor" {
options = append(options, cell.FgColor(cell.ColorYellow))
} else if info.Severity == "Fine" {
options = append(options, cell.FgColor(cell.ColorGray))
} else if info.Severity == "Cancel" {
options = append(options, cell.FgColor(cell.ColorGreen))
} else {
options = append(options, cell.FgColor(cell.ColorRed))
options = append(options, cell.Blink())
}
if err := widget.Write(fmt.Sprintf("%s\n", info.Headline), text.WriteCellOpts(append(options, cell.Bold())...)); err != nil {
return err
}
if err := widget.Write(fmt.Sprintf("Expires: %s\n\n", info.Expires), text.WriteCellOpts(append(options, cell.Dim())...)); err != nil {
return err
}
if err := widget.Write(fmt.Sprintf("%s\n\n\n", removeElements(info.Description)), text.WriteCellOpts(options...)); err != nil {
return err
}
}
}
}
return nil
})
return widget
}
func removeElements(input string) string {
// Pattern to match <tag>content</tag> or <tag/>
re := regexp.MustCompile(`<a .*>.*</a>|<br>|&nbsp;`)
return re.ReplaceAllString(input, "")
}

View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/terminal/terminalapi" "github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi" "github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/text" "github.com/mum4k/termdash/widgets/text"
@@ -26,19 +27,28 @@ func createPiholeStats(ctx context.Context, _ terminalapi.Terminal, _ interface{
ph := pihole.Connect() ph := pihole.Connect()
go util.Periodic(ctx, 1*time.Minute, func() error { go util.Periodic(ctx, 1*time.Minute, func() error {
summary := ph.Summary() summary := ph.Summary()
if err := list.Write(fmt.Sprintf("Blocked Domains: %d\n", summary.Gravity.DomainsBeingBlocked), text.WriteReplace()); err != nil { if err := list.Write(fmt.Sprintf("Blocked Domains: %d\n", summary.Gravity.DomainsBeingBlocked), text.WriteReplace(), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
if err := list.Write(fmt.Sprint("---------------------------\n")); err != nil { if err := list.Write(fmt.Sprint("───────────────────────────\n"), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
if err := list.Write(fmt.Sprintf("Total Queries: %d\n", summary.Queries.Total)); err != nil { if err := list.Write(fmt.Sprintf("Total Queries: %d\n", summary.Queries.Total), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
if err := list.Write(fmt.Sprintf("Blocked Queries: %d\n", summary.Queries.Blocked)); err != nil { if err := list.Write(fmt.Sprintf("Blocked Queries: %d\n", summary.Queries.Blocked), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
if err := list.Write(fmt.Sprintf("Unique Domains: %d\n", summary.Queries.UniqueDomains)); err != nil { if err := list.Write(fmt.Sprintf("Unique Domains: %d\n", summary.Queries.UniqueDomains), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err
}
if err := list.Write(fmt.Sprintf("Forwarded Queries: %d\n", summary.Queries.Forwarded), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err
}
if err := list.Write(fmt.Sprintf("Cached Queries: %d\n", summary.Queries.Cached), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err
}
if err := list.Write(fmt.Sprintf("Queries per Second: %d\n", summary.Queries.Frequency), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
return nil return nil

View File

@@ -25,19 +25,65 @@ func QRCode(data string) QRCodeOptions {
func QrCodeASCII(data string) string { func QrCodeASCII(data string) string {
bitmap := util.PanicOnErrorWithResult(qrcode.New(data, qrcode.Low)).Bitmap() bitmap := util.PanicOnErrorWithResult(qrcode.New(data, qrcode.Low)).Bitmap()
var stringBuilder strings.Builder var stringBuilder strings.Builder
for y := range bitmap { length := (len(bitmap)) / 2
for x := range bitmap[y] { firstLineReached := false
if bitmap[y][x] { lastLineReached := false
stringBuilder.WriteString("██") firstColumnReached := false
} else { firstColumn := 0
stringBuilder.WriteString(" ") lastColumn := 0
for i := range length {
if !firstLineReached {
for x := range bitmap[i] {
u := bitmap[i*2][x]
firstLineReached = firstLineReached || u
if firstLineReached && !firstColumnReached {
firstColumn = x
firstColumnReached = true
}
if firstLineReached && u {
lastColumn = x
}
}
}
if firstLineReached {
linesHaveBits := false
for x := range bitmap[i] {
u := bitmap[i*2][x]
d := bitmap[i*2+1][x]
linesHaveBits = linesHaveBits || u || !d
}
lastLineReached = !linesHaveBits
if lastLineReached {
break
}
if stringBuilder.Len() > 0 {
stringBuilder.WriteByte('\n')
}
for x := range bitmap[i] {
if x < firstColumn {
continue
}
if x > lastColumn {
break
}
u := bitmap[i*2][x]
d := bitmap[i*2+1][x]
if u && d {
stringBuilder.WriteString("█")
} else if u {
stringBuilder.WriteString("▀")
} else if d {
stringBuilder.WriteString("▄")
} else {
stringBuilder.WriteString(" ")
}
} }
} }
stringBuilder.WriteByte('\n')
} }
return stringBuilder.String() return stringBuilder.String()
} }
func createQRCode(_ context.Context, _ terminalapi.Terminal, opt interface{}) widgetapi.Widget { func createQRCode(_ context.Context, _ terminalapi.Terminal, opt interface{}) widgetapi.Widget {

View File

@@ -27,10 +27,13 @@ func createWeather(ctx context.Context, _ terminalapi.Terminal, _ interface{}) w
go util.Periodic(ctx, 1*time.Hour, func() error { go util.Periodic(ctx, 1*time.Hour, func() error {
weather := openWeatherMap.FetchCurrentWeather() weather := openWeatherMap.FetchCurrentWeather()
widget.Reset() widget.Reset()
if weather.ErrorMessage != "" {
return widget.Write(weather.ErrorMessage, text.WriteCellOpts(cell.FgColor(cell.ColorRed)))
}
weatherIcon := getIcon(weather.Weather[0].Icon) weatherIcon := getIcon(weather.Weather[0].Icon)
if err := widget.Write(fmt.Sprintf("\n %s\n\n", weather.Name), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { if err := widget.Write(fmt.Sprintf(" %s\n\n", weather.Name), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }
@@ -97,7 +100,7 @@ func createWeather(ctx context.Context, _ terminalapi.Terminal, _ interface{}) w
} else { } else {
riseSet = `Sunrise: ` + time.Unix(weather.Sys.Sunrise, 0).Format("15:04") riseSet = `Sunrise: ` + time.Unix(weather.Sys.Sunrise, 0).Format("15:04")
} }
if err := widget.Write(fmt.Sprintf(" %s\n\n", riseSet), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil { if err := widget.Write(fmt.Sprintf(" %s\n", riseSet), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err return err
} }

325
widgets/yearMood.go Normal file
View File

@@ -0,0 +1,325 @@
package widgets
import (
"ArinDash/util"
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/keyboard"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/text"
)
const moodsFile = "moods.json"
type YearMoodOptions struct {
}
func YearMood() YearMoodOptions {
widgetOptions["YearMoodOptions"] = createYearMood
return YearMoodOptions{}
}
func createYearMood(ctx context.Context, _ terminalapi.Terminal, _ interface{}) widgetapi.Widget {
widget := util.PanicOnErrorWithResult(NewMoodText())
go util.Periodic(ctx, 1*time.Hour, func() error {
widget.selectedMonth = time.Now().Month()
widget.selectedDay = time.Now().Day()
return widget.drawTable()
})
return widget
}
func (widget *MoodText) drawTable() error {
days := moods()
widget.Reset()
if err := widget.Write(" "); err != nil {
return err
}
for m := range 12 {
mood := moodToColor(widget.moodMonth(m + 1))
if err := widget.Write(fmt.Sprintf("%2s", time.Month(m + 1).String()[0:3]), text.WriteCellOpts(cell.BgColor(mood), cell.FgColor(cell.ColorWhite))); err != nil {
return err
}
if m < 11 {
if err := widget.Write(" "); err != nil {
return err
}
} else {
if err := widget.Write("\n"); err != nil {
return err
}
}
}
for day := range 31 {
for month := range 12 {
date := time.Date(time.Now().Year(), time.Month(month+1), day+1, 0, 0, 0, 0, time.UTC)
if date.Month() != time.Month(month+1) {
if err := widget.Write(" "); err != nil {
return err
}
continue
}
cellOptions := make([]cell.Option, 0)
weekday := time.Date(time.Now().Year(), time.Month(month+1), day+1, 0, 0, 0, 0, time.UTC).Weekday()
if weekday == time.Saturday || weekday == time.Sunday {
cellOptions = append(cellOptions, cell.Dim())
}
cellOptions = append(cellOptions, cell.FgColor(cell.ColorWhite))
cellOptions = append(cellOptions, cell.Bold())
mood := days[strconv.Itoa(time.Now().Year())][strconv.Itoa(month+1)][strconv.Itoa(day+1)]
cellOptions = append(cellOptions, cell.BgColor(moodToColor(mood)))
if day+1 == widget.selectedDay && time.Month(month+1) == widget.selectedMonth {
if err := widget.Write(">", text.WriteCellOpts(append(cellOptions, cell.Blink())...)); err != nil {
return err
}
if err := widget.Write(fmt.Sprintf("%2d", day+1), text.WriteCellOpts(cellOptions...)); err != nil {
return err
}
} else {
if err := widget.Write(fmt.Sprintf(" %2d", day+1), text.WriteCellOpts(cellOptions...)); err != nil {
return err
}
}
if time.Now().Day() == day+1 && time.Now().Month() == time.Month(month+1) {
if err := widget.Write("<", text.WriteCellOpts(cellOptions...)); err != nil {
return err
}
} else {
if err := widget.Write(" ", text.WriteCellOpts(cellOptions...)); err != nil {
return err
}
}
if month+1 < 12 {
if err := widget.Write(" "); err != nil {
return err
}
}
}
if err := widget.Write("\n"); err != nil {
return err
}
}
if err := widget.Write("\n"); err != nil {
return err
}
for k, v := range totalMoodPercents() {
if v == 0 {
continue
}
amount := int(59 * v)
if amount == 0 {
amount = 1
}
if err := widget.Write(strings.Repeat("█", amount), text.WriteCellOpts(cell.BgColor(cell.ColorGray), cell.FgColor(moodToColor(k)))); err != nil {
return err
}
}
if err := widget.Write("\n\n"); err != nil {
return err
}
if err := widget.Write("<-=delete 1=idk(purple) 2=meh(yellow) 3=great(green) other=bad(red)", text.WriteCellOpts(cell.FgColor(cell.ColorGray))); err != nil {
return err
}
return nil
}
func moods() map[string]map[string]map[string]int {
moods := make(map[string]map[string]map[string]int)
file, err := os.ReadFile(moodsFile)
if err == nil {
if err := json.Unmarshal(file, &moods); err != nil {
panic(err)
}
} else {
initMoods, _ := json.Marshal(moods)
if err := os.WriteFile(moodsFile, initMoods, 0644); err != nil {
// ignore
}
}
return moods
}
func moodToColor(mood int) cell.Color {
switch mood {
case 1:
return cell.ColorPurple
case 2:
return cell.ColorYellow
case 3:
return cell.ColorGreen
case -1:
return cell.ColorRed
default:
return cell.ColorGray
}
}
func totalMoodPercents() map[int]float64 {
m := moods()
now := time.Now()
currentYear := strconv.Itoa(now.Year())
// Initialize percentages for all known moods
percents := map[int]float64{
1: 0.0, // idk
2: 0.0, // meh
3: 0.0, // great
-1: 0.0, // bad
0: 0.0, // no mood recorded
}
// Calculate total days in the current year
firstDay := time.Date(now.Year(), time.January, 1, 0, 0, 0, 0, time.UTC)
nextYear := time.Date(now.Year()+1, time.January, 1, 0, 0, 0, 0, time.UTC)
totalDaysInYear := int(nextYear.Sub(firstDay).Hours() / 24)
yearData, ok := m[currentYear]
if !ok {
percents[0] = 1.0
return percents
}
counts := make(map[int]int)
recordedDaysCount := 0
for _, monthData := range yearData {
for _, mood := range monthData {
if mood == 0 {
continue
}
counts[mood]++
recordedDaysCount++
}
}
// Any day that doesn't have a recorded mood (1, 2, 3, or -1) is counted as 0
counts[0] = totalDaysInYear - recordedDaysCount
for mood := range percents {
if count, exists := counts[mood]; exists {
percents[mood] = float64(count) / float64(totalDaysInYear)
}
}
return percents
}
func (widget *MoodText) moodMonth(month int) int {
days := moods()[strconv.Itoa(time.Now().Year())][strconv.Itoa(month)]
counts := make(map[int]int)
mostFrequentMood := 0
maxCount := 0
for _, mood := range days {
if mood == 0 {
continue
}
counts[mood]++
if counts[mood] > maxCount {
maxCount = counts[mood]
mostFrequentMood = mood
}
}
return mostFrequentMood
}
type MoodText struct {
*text.Text
selectedMonth time.Month
selectedDay int
}
func NewMoodText(opts ...text.Option) (*MoodText, error) {
t, err := text.New(opts...)
if err != nil {
return nil, err
}
return &MoodText{Text: t}, nil
}
func (widget *MoodText) Keyboard(k *terminalapi.Keyboard, _ *widgetapi.EventMeta) error {
switch k.Key {
case keyboard.KeyEsc, keyboard.KeyCtrlC:
break
case keyboard.KeyArrowUp:
if widget.selectedDay == 1 {
widget.selectedDay = 31
} else {
widget.selectedDay--
}
break
case keyboard.KeyArrowDown:
if widget.selectedDay == 31 {
widget.selectedDay = 1
} else {
widget.selectedDay++
}
break
case keyboard.KeyArrowLeft:
if widget.selectedMonth == 1 {
widget.selectedMonth = 12
} else {
widget.selectedMonth--
}
break
case keyboard.KeyArrowRight:
if widget.selectedMonth == 12 {
widget.selectedMonth = 1
} else {
widget.selectedMonth++
}
break
case keyboard.KeyBackspace, keyboard.KeyBackspace2:
widget.WriteMood(0)
case '1':
widget.WriteMood(1)
case '2':
widget.WriteMood(2)
case '3':
widget.WriteMood(3)
ChangeMood("happy")
default:
widget.WriteMood(-1)
ChangeMood("cry")
}
return widget.drawTable()
}
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)
}
if m[strconv.Itoa(time.Now().Year())][strconv.Itoa(int(widget.selectedMonth))] == nil {
m[strconv.Itoa(time.Now().Year())][strconv.Itoa(int(widget.selectedMonth))] = make(map[string]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 {
return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed)))
}
if err := os.WriteFile(moodsFile, newMoods, 0644); err != nil {
return widget.Write(fmt.Sprintf("Error: %s", err.Error()), text.WriteCellOpts(cell.FgColor(cell.ColorRed)))
}
return nil
}

63
widgets/zukitchi.go Normal file
View File

@@ -0,0 +1,63 @@
package widgets
import (
"ArinDash/util"
"ArinDash/widgets/zukitchi"
"context"
"time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/text"
)
type ZukitchiOptions struct {
}
func Zukitchi() ZukitchiOptions {
widgetOptions["ZukitchiOptions"] = createZukitchi
return ZukitchiOptions{}
}
var lastFrame = time.Now()
var pet = zukitchi.New()
func createZukitchi(ctx context.Context, _ terminalapi.Terminal, _ interface{}) widgetapi.Widget {
widget := util.PanicOnErrorWithResult(text.New())
go util.Periodic(ctx, 1*time.Minute, pet.Run())
go util.Periodic(ctx, util.RedrawInterval, func() error {
return draw(widget)
})
return widget
}
func draw(widget *text.Text) error {
if time.Since(lastFrame) < 1*time.Second {
return nil
}
widget.Reset()
if err := widget.Write(pet.NextFrame(), text.WriteCellOpts(cell.FgColor(cell.ColorBlack), cell.BgColor(cell.ColorGreen))); err != nil {
return err
}
lastFrame = time.Now()
return nil
}
func ChangeMood(mood string) {
pet.ChangeMood(mood)
go func() {
ticker := time.NewTicker(10 * time.Second)
for {
select {
case <-ticker.C:
pet.ChangeMood("idle")
ticker.Stop()
}
}
}()
}

1342
widgets/zukitchi/assets.go Normal file

File diff suppressed because it is too large Load Diff

1
widgets/zukitchi/main.go Normal file
View File

@@ -0,0 +1 @@
package zukitchi