Compare commits

...

24 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
57f7898d65 Add Weather widget and integrate OpenWeatherMap API for dynamic updates 2025-12-29 00:57:38 +01:00
e77e1d72b8 Remove redundant error logging in HTTP Prober 2025-12-29 00:57:13 +01:00
31 changed files with 3095 additions and 118 deletions

1
.gitignore vendored
View File

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

View File

@@ -4,12 +4,17 @@ ArinDash is a customizable terminal dashboard (TUI) written in Go, powered by [`
## 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.
- **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.
## 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:
- **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:
- **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
To run ArinDash, simply execute:
To run ArinDash, execute:
```bash
go run main.go
@@ -58,6 +66,12 @@ go run main.go
### Controls
- **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
@@ -69,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.

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 (
"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

@@ -3,7 +3,6 @@ package httpprober
import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
@@ -69,13 +68,11 @@ func buildHTTPClient() (*http.Client, error) {
func testMethod(client *http.Client, target Website) int {
req, err := http.NewRequest("GET", target.URL, nil)
if err != nil {
fmt.Println(err)
return -2
}
resp, err := client.Do(req)
if err != nil {
fmt.Println(err)
return -1
}
defer func(body io.ReadCloser) {

View File

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

@@ -0,0 +1,112 @@
package openWeatherMap
import "time"
type CurrentWeather struct {
Base string `json:"base"`
Visibility int `json:"visibility"`
Dt int `json:"dt"`
Timezone int `json:"timezone"`
Id int `json:"id"`
Name string `json:"name"`
Cod int `json:"cod"`
Coordinates Coordinates `json:"coord"`
Weather []Weather `json:"weather"`
Wind Wind `json:"wind"`
Clouds Clouds `json:"clouds"`
Rain Rain `json:"rain"`
Snow Snow `json:"snow"`
Sys Sys `json:"sys"`
Main Main `json:"main"`
ErrorMessage string
}
type Coordinates struct {
Lon float64 `json:"lon"`
Lat float64 `json:"lat"`
}
type Weather struct {
Id int `json:"id"`
Main string `json:"main"`
Description string `json:"description"`
Icon string `json:"icon"`
}
type Wind struct {
Speed float64 `json:"speed"`
Deg float64 `json:"deg"`
}
type Clouds struct {
All int `json:"all"`
}
type Rain struct {
OneH float64 `json:"1h"`
}
type Snow struct {
OneH float64 `json:"1h"`
}
type Sys struct {
Type int `json:"type"`
Id int `json:"id"`
Country string `json:"country"`
Sunrise int64 `json:"sunrise"`
Sunset int64 `json:"sunset"`
}
type Main struct {
Temp float64 `json:"temp"`
FeelsLike float64 `json:"feels_like"`
TempMin float64 `json:"temp_min"`
TempMax float64 `json:"temp_max"`
Pressure int `json:"pressure"`
Humidity int `json:"humidity"`
SeaLevel int `json:"sea_level"`
GrndLevel int `json:"grnd_level"`
}
func (currentWeather *CurrentWeather) CardinalWindDirection() string {
windDeg := currentWeather.Wind.Deg
if windDeg > 11.25 && windDeg <= 33.75 {
return `NNE`
} else if windDeg > 33.75 && windDeg <= 56.25 {
return "NE"
} else if windDeg > 56.25 && windDeg <= 78.75 {
return "ENE"
} else if windDeg > 78.75 && windDeg <= 101.25 {
return "E"
} else if windDeg > 101.25 && windDeg <= 123.75 {
return "ESE"
} else if windDeg > 123.75 && windDeg <= 146.25 {
return "SE"
} else if windDeg > 146.25 && windDeg <= 168.75 {
return "SSE"
} else if windDeg > 168.75 && windDeg <= 191.25 {
return "S"
} else if windDeg > 191.25 && windDeg <= 213.75 {
return "SSW"
} else if windDeg > 213.75 && windDeg <= 236.25 {
return "SW"
} else if windDeg > 236.25 && windDeg <= 258.75 {
return "WSW"
} else if windDeg > 258.75 && windDeg <= 281.25 {
return "W"
} else if windDeg > 281.25 && windDeg <= 303.75 {
return "WNW"
} else if windDeg > 303.75 && windDeg <= 326.25 {
return "NW"
} else if windDeg > 326.25 && windDeg <= 348.75 {
return "NNW"
}
return `N`
}
func (currentWeather *CurrentWeather) IsDayTime() bool {
now := time.Now()
return now.After(time.Unix(currentWeather.Sys.Sunrise, 0)) && now.Before(time.Unix(currentWeather.Sys.Sunset, 0))
}

View File

@@ -0,0 +1,59 @@
package openWeatherMap
import (
"ArinDash/config"
"encoding/json"
"io"
"net/http"
)
const apiBaseURL = "https://api.openweathermap.org/data/2.5/weather"
type configFile struct {
OpenWeatherMap openWeatherMapConfig
}
type openWeatherMapConfig struct {
LocationId string
ApiKey string
}
func FetchCurrentWeather() CurrentWeather {
cfg := &configFile{}
config.LoadConfig(cfg)
client := &http.Client{}
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)
if err != nil {
return CurrentWeather{
ErrorMessage: err.Error(),
}
}
resp, err := client.Do(req)
if err != nil {
return CurrentWeather{
ErrorMessage: 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 CurrentWeather{
ErrorMessage: err.Error(),
}
}
err = json.Unmarshal(respBody, currentWeather)
return *currentWeather
}

View File

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

View File

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

View File

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

View File

@@ -8,3 +8,25 @@ Password = "generate-an-app-password-in-pi-hole"
Auth = "WPA"
SSID = "YourSSID"
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
require (
github.com/arran4/golang-ical v0.3.2
github.com/docker/docker v28.5.2+incompatible
github.com/mum4k/termdash v0.20.0
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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
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/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=

220
main.go
View File

@@ -35,6 +35,9 @@ func initWidgets(ctx context.Context, term *tcell.Terminal) {
widgets.New("Clock", widgets.Clock()),
widgets.New("Date", widgets.Date()),
widgets.New("Calendar", widgets.Calendar()),
widgets.New("CalendarEvents", widgets.CalendarEvents()),
widgets.New("YearMood", widgets.YearMood()),
widgets.New("Weather", widgets.Weather()),
widgets.New("ChangeTitle", widgets.TextInput(titleUpdate, textinput.Label("Update Title: "), textinput.PlaceHolder("New Title"))),
widgets.New("Wifi", widgets.WifiQRCode()),
widgets.New("NetworkDevices", widgets.NetworkDevices()),
@@ -42,83 +45,176 @@ func initWidgets(ctx context.Context, term *tcell.Terminal) {
widgets.New("HTTPProber", widgets.HTTPProber()),
widgets.New("PiHole", widgets.PiholeStats()),
widgets.New("PiHoleBlocked", widgets.PiholeBlocked()),
widgets.New("NinaWarnings", widgets.NinaWarnings()),
widgets.New("News", widgets.News()),
widgets.New("Zukitchi", widgets.Zukitchi()),
)
}
func layout() []container.Option {
builder := grid.New()
builder.Add(
grid.ColWidthFixed(84,
grid.RowHeightFixed(44,
grid.Widget(widgets.Get["Wifi"],
container.BorderTitle("Wifi"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite)),
grid.RowHeightFixed(21,
grid.ColWidthFixed(38,
grid.RowHeightFixed(20,
grid.Widget(widgets.Get["Wifi"],
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"],
container.BorderTitle("Network Devices"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite)),
),
grid.RowHeightFixed(1,
grid.Widget(widgets.Get["empty"]),
grid.RowHeightFixed(25,
grid.RowHeightFixed(19,
grid.Widget(widgets.Get["HTTPProber"],
container.BorderTitle("Website Status"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
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.Widget(widgets.Get["Clock"],
container.AlignHorizontal(align.HorizontalCenter),
container.BorderTitle("Time"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
grid.ColWidthPerc(62,
grid.RowHeightFixed(40,
grid.ColWidthFixed(40,
grid.RowHeightFixed(8,
grid.Widget(widgets.Get["Clock"],
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.Widget(widgets.Get["Calendar"],
container.BorderTitle("Calendar"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite)),
),
grid.RowHeightPerc(25,
grid.Widget(widgets.Get["empty"]),
),
),
grid.ColWidthPerc(25,
grid.RowHeightPerc(25,
grid.Widget(widgets.Get["empty"]),
),
),
grid.ColWidthPerc(35,
grid.RowHeightPerc(20,
grid.Widget(widgets.Get["HTTPProber"],
container.BorderTitle("Website Status"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
grid.RowHeightFixed(20,
grid.Widget(widgets.Get["News"],
container.BorderTitle("News"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
container.PaddingLeft(1),
container.PaddingTop(1),
),
),
grid.RowHeightFixed(20,
grid.Widget(widgets.Get["NinaWarnings"],
container.BorderTitle("BBK Warnings"),
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
container.PaddingLeft(1),
container.PaddingTop(1),
),
),
),
grid.RowHeightPerc(25,
grid.Widget(widgets.Get["Docker"],
container.BorderTitle("Docker"),
),
grid.ColWidthPerc(35,
grid.RowHeightFixed(21,
grid.Widget(widgets.Get["Zukitchi"],
container.Border(linestyle.Light),
container.BorderColor(cell.ColorWhite),
container.PaddingTop(1),
container.PaddingLeft(1),
),
),
grid.RowHeightFixed(39,
grid.Widget(widgets.Get["YearMood"],
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.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())
go util.Periodic(ctx, 1*time.Hour, func() error {
date := time.Now()
widget.Reset()
if err := widget.Write("┌────────────────────┐\n", text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
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
}
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 {
return err
}
if err := createTableForMonth(widget); err != nil {
if err := createTableForMonth(date, widget); err != nil {
return err
}
if err := createLowerBorder(widget); err != nil {
@@ -50,23 +50,23 @@ func createCalendar(ctx context.Context, _ terminalapi.Terminal, _ interface{})
return widget
}
func createTableForMonth(widget *text.Text) error {
func createTableForMonth(date time.Time, widget *text.Text) error {
table := make([]int, 0)
now := time.Now()
firstMonthday := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
firstMonthday := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, time.UTC)
for range (firstMonthday.Weekday() + 6) % 7 {
table = append(table, 0)
}
//34
//35
for {
day := firstMonthday.Day()
table = append(table, day)
firstMonthday = firstMonthday.AddDate(0, 0, 1)
if firstMonthday.Month() != now.Month() {
if firstMonthday.Month() != date.Month() {
break
}
}
remainingDays := 8 - len(table)/7
remainingDays := 7 - len(table)%7
for range remainingDays {
table = append(table, 0)
}
@@ -78,7 +78,7 @@ func createTableForMonth(widget *text.Text) error {
}
}
var str string
if err := widget.Write("│"); err != nil {
if err := widget.Write("│", text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err
}
if field == 0 {
@@ -87,7 +87,7 @@ func createTableForMonth(widget *text.Text) error {
str = fmt.Sprintf("%2d", field)
}
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())
} else {
color = text.WriteCellOpts(cell.FgColor(cell.ColorWhite), cell.BgColor(cell.ColorDefault))
@@ -96,7 +96,7 @@ func createTableForMonth(widget *text.Text) error {
return err
}
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
}
}

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 })
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
}
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
}
for _, m := range ms {
@@ -53,22 +53,28 @@ func createDockerList(ctx context.Context, _ terminalapi.Terminal, _ interface{}
if strings.Contains(m.Status, "Paused") {
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
}
if err := list.Write(fmt.Sprint(" | ")); err != nil {
if err := list.Write(fmt.Sprint(" "), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err
}
if err := writePercent(m.CPUPercent, list); err != nil {
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
}
if err := writePercent(m.MemPercent, list); err != nil {
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
}
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 {
status = cell.ColorGreen
}
if err := list.Write(fmt.Sprintf("|%-15s|", mac), text.WriteCellOpts(cell.FgColor(cell.ColorGray))); err != nil {
return err
}
//if err := list.Write(fmt.Sprintf("|%-15s|", mac), text.WriteCellOpts(cell.FgColor(cell.ColorGray))); err != nil {
// return err
//}
if err := list.Write(fmt.Sprintf("%2s ", devices[mac].Icon), text.WriteCellOpts(cell.BgColor(status), cell.FgColor(cell.ColorBlack))); err != nil {
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"
"time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
"github.com/mum4k/termdash/widgets/text"
@@ -26,19 +27,28 @@ func createPiholeStats(ctx context.Context, _ terminalapi.Terminal, _ interface{
ph := pihole.Connect()
go util.Periodic(ctx, 1*time.Minute, func() error {
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
}
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
}
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
}
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
}
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 nil

View File

@@ -25,19 +25,65 @@ func QRCode(data string) QRCodeOptions {
func QrCodeASCII(data string) string {
bitmap := util.PanicOnErrorWithResult(qrcode.New(data, qrcode.Low)).Bitmap()
var stringBuilder strings.Builder
for y := range bitmap {
for x := range bitmap[y] {
if bitmap[y][x] {
stringBuilder.WriteString("██")
} else {
stringBuilder.WriteString(" ")
length := (len(bitmap)) / 2
firstLineReached := false
lastLineReached := false
firstColumnReached := false
firstColumn := 0
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()
}
func createQRCode(_ context.Context, _ terminalapi.Terminal, opt interface{}) widgetapi.Widget {

264
widgets/weather.go Normal file
View File

@@ -0,0 +1,264 @@
package widgets
import (
"ArinDash/apis/openweathermap"
"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 WeatherOptions struct {
}
func Weather() WeatherOptions {
widgetOptions["WeatherOptions"] = createWeather
return WeatherOptions{}
}
func createWeather(ctx context.Context, _ terminalapi.Terminal, _ interface{}) widgetapi.Widget {
widget := util.PanicOnErrorWithResult(text.New())
go util.Periodic(ctx, 1*time.Hour, func() error {
weather := openWeatherMap.FetchCurrentWeather()
widget.Reset()
if weather.ErrorMessage != "" {
return widget.Write(weather.ErrorMessage, text.WriteCellOpts(cell.FgColor(cell.ColorRed)))
}
weatherIcon := getIcon(weather.Weather[0].Icon)
if err := widget.Write(fmt.Sprintf(" %s\n\n", weather.Name), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err
}
for index, line := range weatherIcon {
if err := widget.Write(fmt.Sprintf("%s ", line), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err
}
switch index {
case 0:
if err := printTemperature(weather.Main.Temp, widget); err != nil {
return err
}
if err := widget.Write(fmt.Sprint(" ")); err != nil {
return err
}
if err := widget.Write(weather.Weather[0].Description, text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err
}
break
case 1:
if err := widget.Write(fmt.Sprint("Feels like: ")); err != nil {
return err
}
if err := printTemperature(weather.Main.FeelsLike, widget); err != nil {
return err
}
break
case 2:
if err := widget.Write(fmt.Sprintf("\uE34B %s %.2f km/h", weather.CardinalWindDirection(), weather.Wind.Speed*3.6)); err != nil {
return err
}
break
case 3:
if weather.Rain.OneH > 0 {
if err := widget.Write(fmt.Sprintf("Rain: %.2f mm/h ", weather.Rain.OneH)); err != nil {
return err
}
}
if weather.Snow.OneH > 0 {
if err := widget.Write(fmt.Sprintf("Snow: %.2f mm/h ", weather.Snow.OneH)); err != nil {
return err
}
}
if weather.Clouds.All > 0 {
if err := widget.Write(fmt.Sprintf("Clouds: %d %% ", weather.Clouds.All)); err != nil {
return err
}
}
break
case 4:
if err := widget.Write(fmt.Sprintf("Pr: %d hPa | Hum: %d %%", weather.Main.Pressure, weather.Main.Humidity)); err != nil {
return err
}
break
}
if err := widget.Write(fmt.Sprint("\n")); err != nil {
return err
}
}
var riseSet string
if weather.IsDayTime() {
riseSet = `Sunset: ` + time.Unix(weather.Sys.Sunset, 0).Format("15:04")
} else {
riseSet = `Sunrise: ` + time.Unix(weather.Sys.Sunrise, 0).Format("15:04")
}
if err := widget.Write(fmt.Sprintf(" %s\n", riseSet), text.WriteCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
return err
}
return nil
})
return widget
}
func printTemperature(temp float64, widget *text.Text) error {
var color cell.Color
if temp < 0 {
color = cell.ColorBlue
} else if temp < 5 {
color = cell.ColorAqua
} else if temp < 15 {
color = cell.ColorGreen
} else if temp < 25 {
color = cell.ColorYellow
} else {
color = cell.ColorRed
}
if err := widget.Write(fmt.Sprintf("%.2f °C", temp), text.WriteCellOpts(cell.FgColor(color), cell.Bold())); err != nil {
return err
}
return nil
}
// ... existing code ...
func getIcon(name string) []string {
icon := map[string][]string{
"01d": { // wi-day-sunny
" \\ / ",
" .-. ",
" ― ( ) ― ",
" `- ",
" / \\ ",
},
"01n": { // wi-night-clear
" _ ",
" ( `\\ ",
" | | ",
" (_./ ",
" ",
},
"02d": { // wi-day-cloudy
" \\ / ",
" _ /\"\".-. ",
" \\_( ). ",
" /(___(__) ",
" ",
},
"02n": { // wi-night-cloudy
" _ ",
" ( `\\ .-. ",
" | _/( ). ",
" (_/(___(__) ",
" ",
},
"03d": { // wi-cloudy
" ",
" .--. ",
" .-( ). ",
" (___.__)__) ",
" ",
},
"03n": { // wi-night-cloudy (same as 02n/04n usually)
" _ ",
" ( `\\ .-. ",
" | _/( ). ",
" (_/(___(__) ",
" ",
},
"04d": { // wi-cloudy-windy
" .--. ",
" .-( ). __ ",
" (___.__)__) _ ",
" _ - _ - _ - ",
" ",
},
"04n": { // wi-night-cloudy
" _ ",
" ( `\\ .-. ",
" | _/( ). ",
" (_/(___(__) ",
" ",
},
"09d": { // wi-showers
" .-. ",
" ( ). ",
" (___(__) ",
" ",
" ",
},
"09n": { // wi-night-showers
" _ .-. ",
" ( `\\( ). ",
" (_/(___(__) ",
" ",
" ",
},
"10d": { // wi-rain
" .-. ",
" ( ). ",
" (___(__) ",
" ",
" ",
},
"10n": { // wi-night-rain
" _ .-. ",
" ( `\\( ). ",
" (_/(___(__) ",
" ",
" ",
},
"11d": { // wi-thunderstorm
" .-. ",
" ( ). ",
" (___(__) ",
" /_ /_ ",
" / / ",
},
"11n": { // wi-night-thunderstorm
" _ .-. ",
" ( `\\( ). ",
" (_/(___(__) ",
" /_ /_ ",
" / / ",
},
"13d": { // wi-snow
" .-. ",
" ( ). ",
" (___(__) ",
" * * * ",
" * * * ",
},
"13n": { // wi-night-snow
" _ .-. ",
" ( `\\( ). ",
" (_/(___(__) ",
" * * * ",
" * * * ",
},
"50d": { // wi-fog
" ",
" _ - _ - _ - ",
" _ - _ - _ ",
" _ - _ - _ - ",
" ",
},
"50n": { // wi-night-alt-cloudy-windy
" _ ",
" ( `\\ .-. ",
" | _/( ). ",
" (_/(___(__) ",
" _ - _ - _ ",
},
}
return icon[name]
}

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