Compare commits
24 Commits
6b0d9b229f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d4e3b6252b | |||
| 915087d31a | |||
| 21ac083ef0 | |||
| 325144e0d9 | |||
| 3e765c7b95 | |||
| 0a7e89307f | |||
| 2a66278cae | |||
| af5a39ef75 | |||
| d2500351f9 | |||
| f05e7395f3 | |||
| 2f918332a3 | |||
| 3f25a632df | |||
| ef962bbc49 | |||
| 97c26b30e9 | |||
| 45a8115d30 | |||
| 0fc1ea476b | |||
| 7a846ee660 | |||
| dfbc6066c9 | |||
| a29beeda43 | |||
| 9b5afc3d7c | |||
| d638a8ae97 | |||
| 52d3b1196b | |||
| 57f7898d65 | |||
| e77e1d72b8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -109,5 +109,6 @@ go.work.sum
|
|||||||
config.toml
|
config.toml
|
||||||
devices.json
|
devices.json
|
||||||
websites.json
|
websites.json
|
||||||
|
moods.json
|
||||||
|
|
||||||
old
|
old
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -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
89
apis/calendar/main.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package httpprober
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -69,13 +68,11 @@ func buildHTTPClient() (*http.Client, error) {
|
|||||||
func testMethod(client *http.Client, target Website) int {
|
func testMethod(client *http.Client, target Website) int {
|
||||||
req, err := http.NewRequest("GET", target.URL, nil)
|
req, err := http.NewRequest("GET", target.URL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
|
||||||
return -2
|
return -2
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
defer func(body io.ReadCloser) {
|
defer func(body io.ReadCloser) {
|
||||||
|
|||||||
@@ -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
109
apis/newsapi/main.go
Normal 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
111
apis/nina/main.go
Normal 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
|
||||||
|
}
|
||||||
112
apis/openweathermap/currentWeather.go
Normal file
112
apis/openweathermap/currentWeather.go
Normal 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))
|
||||||
|
}
|
||||||
59
apis/openweathermap/main.go
Normal file
59
apis/openweathermap/main.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||||
|
|||||||
154
main.go
154
main.go
@@ -35,6 +35,9 @@ 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("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()),
|
||||||
widgets.New("NetworkDevices", widgets.NetworkDevices()),
|
widgets.New("NetworkDevices", widgets.NetworkDevices()),
|
||||||
@@ -42,30 +45,65 @@ 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.ColWidthFixed(38,
|
||||||
|
grid.RowHeightFixed(20,
|
||||||
grid.Widget(widgets.Get["Wifi"],
|
grid.Widget(widgets.Get["Wifi"],
|
||||||
container.BorderTitle("Wifi"),
|
container.BorderTitle("Wifi"),
|
||||||
container.Border(linestyle.Light),
|
container.Border(linestyle.Light),
|
||||||
container.BorderColor(cell.ColorWhite)),
|
container.BorderColor(cell.ColorWhite),
|
||||||
|
container.PaddingLeft(1),
|
||||||
|
container.PaddingTop(1),
|
||||||
),
|
),
|
||||||
grid.RowHeightFixed(44,
|
),
|
||||||
|
),
|
||||||
|
grid.ColWidthFixed(20,
|
||||||
grid.Widget(widgets.Get["NetworkDevices"],
|
grid.Widget(widgets.Get["NetworkDevices"],
|
||||||
container.BorderTitle("Network Devices"),
|
container.BorderTitle("Network Devices"),
|
||||||
container.Border(linestyle.Light),
|
container.Border(linestyle.Light),
|
||||||
container.BorderColor(cell.ColorWhite)),
|
container.BorderColor(cell.ColorWhite),
|
||||||
),
|
container.PaddingLeft(1),
|
||||||
grid.RowHeightFixed(1,
|
container.PaddingTop(1),
|
||||||
grid.Widget(widgets.Get["empty"]),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
grid.ColWidthPerc(20,
|
),
|
||||||
|
|
||||||
|
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(62,
|
||||||
|
grid.RowHeightFixed(40,
|
||||||
|
grid.ColWidthFixed(40,
|
||||||
grid.RowHeightFixed(8,
|
grid.RowHeightFixed(8,
|
||||||
grid.Widget(widgets.Get["Clock"],
|
grid.Widget(widgets.Get["Clock"],
|
||||||
container.AlignHorizontal(align.HorizontalCenter),
|
container.AlignHorizontal(align.HorizontalCenter),
|
||||||
@@ -74,51 +112,109 @@ func layout() []container.Option {
|
|||||||
container.BorderColor(cell.ColorWhite),
|
container.BorderColor(cell.ColorWhite),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
grid.RowHeightFixed(12,
|
grid.RowHeightFixed(13,
|
||||||
grid.Widget(widgets.Get["PiHole"],
|
grid.Widget(widgets.Get["PiHole"],
|
||||||
container.BorderTitle("pi-hole"),
|
container.BorderTitle("pi-hole"),
|
||||||
container.Border(linestyle.Light),
|
container.Border(linestyle.Light),
|
||||||
container.BorderColor(cell.ColorWhite),
|
container.BorderColor(cell.ColorWhite),
|
||||||
|
container.PaddingLeft(1),
|
||||||
|
container.PaddingTop(1),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
grid.RowHeightFixed(24,
|
grid.RowHeightFixed(38,
|
||||||
grid.Widget(widgets.Get["PiHoleBlocked"],
|
grid.Widget(widgets.Get["PiHoleBlocked"],
|
||||||
container.BorderTitle("pi-hole (Blocked Percent)"),
|
container.BorderTitle("pi-hole (Blocked Percent)"),
|
||||||
container.Border(linestyle.Light),
|
container.Border(linestyle.Light),
|
||||||
container.BorderColor(cell.ColorWhite),
|
container.BorderColor(cell.ColorWhite),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
grid.RowHeightPerc(85,
|
|
||||||
grid.Widget(widgets.Get["empty"]),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
grid.ColWidthPerc(13,
|
grid.ColWidthFixed(26,
|
||||||
grid.RowHeightFixed(20,
|
grid.RowHeightFixed(21,
|
||||||
grid.Widget(widgets.Get["Calendar"],
|
grid.Widget(widgets.Get["Calendar"],
|
||||||
container.BorderTitle("Calendar"),
|
container.BorderTitle("Calendar"),
|
||||||
container.Border(linestyle.Light),
|
container.Border(linestyle.Light),
|
||||||
container.BorderColor(cell.ColorWhite)),
|
container.BorderColor(cell.ColorWhite),
|
||||||
),
|
container.PaddingLeft(1),
|
||||||
grid.RowHeightPerc(25,
|
container.PaddingTop(1),
|
||||||
grid.Widget(widgets.Get["empty"]),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
grid.ColWidthPerc(25,
|
grid.RowHeightFixed(25,
|
||||||
grid.RowHeightPerc(25,
|
grid.Widget(widgets.Get["empty"],
|
||||||
grid.Widget(widgets.Get["empty"]),
|
container.Border(linestyle.Light),
|
||||||
|
container.BorderColor(cell.ColorGreen),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
grid.ColWidthPerc(35,
|
),
|
||||||
grid.RowHeightPerc(20,
|
|
||||||
grid.Widget(widgets.Get["HTTPProber"],
|
grid.ColWidthFixed(25,
|
||||||
container.BorderTitle("Website Status"),
|
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.Border(linestyle.Light),
|
||||||
container.BorderColor(cell.ColorWhite),
|
container.BorderColor(cell.ColorWhite),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
grid.RowHeightPerc(25,
|
),
|
||||||
grid.Widget(widgets.Get["Docker"],
|
),
|
||||||
container.BorderTitle("Docker"),
|
grid.RowHeightFixed(20,
|
||||||
|
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.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.Border(linestyle.Light),
|
||||||
container.BorderColor(cell.ColorWhite),
|
container.BorderColor(cell.ColorWhite),
|
||||||
),
|
),
|
||||||
|
|||||||
16
test/test.go
Normal file
16
test/test.go
Normal 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
89
widgets/calendarEvents.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
106
widgets/news.go
Normal 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
71
widgets/ninaWarnings.go
Normal 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>| `)
|
||||||
|
return re.ReplaceAllString(input, "")
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
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 {
|
} else {
|
||||||
stringBuilder.WriteString(" ")
|
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 {
|
||||||
|
|||||||
264
widgets/weather.go
Normal file
264
widgets/weather.go
Normal 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
325
widgets/yearMood.go
Normal 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
63
widgets/zukitchi.go
Normal 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
1342
widgets/zukitchi/assets.go
Normal file
File diff suppressed because it is too large
Load Diff
1
widgets/zukitchi/main.go
Normal file
1
widgets/zukitchi/main.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package zukitchi
|
||||||
Reference in New Issue
Block a user