pull/1018/head
Igor Chubin 1 year ago
commit dd87ab5076

@ -0,0 +1,30 @@
run:
skip-dirs:
- pkg/curlator
linters:
enable-all: true
disable:
- wsl
- wrapcheck
- varnamelen
- gci
- exhaustivestruct
- exhaustruct
- gomnd
- gofmt
# to be fixed:
- ireturn
- gosec
- noctx
- interfacer
# deprecated:
- scopelint
- deadcode
- varcheck
- maligned
- ifshort
- nosnakecase
- structcheck
- golint

@ -13,7 +13,7 @@ RUN go get -u github.com/mattn/go-colorable && \
cd /app && CGO_ENABLED=0 go build .
# Application stage
FROM alpine:3
FROM alpine:3.16
WORKDIR /app

@ -0,0 +1,9 @@
srv: srv.go internal/*/*.go internal/*/*/*.go
go build -o srv -ldflags '-w -linkmode external -extldflags "-static"' ./
#go build -o srv ./
go-test:
go test ./...
lint:
golangci-lint run ./...

@ -11,7 +11,7 @@ intended to demonstrate the power of the console-oriented services,
You can see it running here: [wttr.in](https://wttr.in).
[Documentation](https://wttr.in/:help) | [Usage](https://github.com/chubin/wttr.in#usage) | [One-line output](https://github.com/chubin/wttr.in#one-line-output) | [Data-rich output format](https://github.com/chubin/wttr.in#data-rich-output-format-v2) | [Map view](https://github.com/chubin/wttr.in#map-view-v3) | [Output formats](https://github.com/chubin/wttr.in#different-output-formats) | [Moon phases](https://github.com/chubin/wttr.in#moon-phases) | [Internationalization](https://github.com/chubin/wttr.in#internationalization-and-localization) | [Windows issues](https://github.com/chubin/wttr.in#windows-users) | [Installation](https://github.com/chubin/wttr.in#installation)
[Documentation](https://wttr.in/:help) | [Usage](https://github.com/chubin/wttr.in#usage) | [One-line output](https://github.com/chubin/wttr.in#one-line-output) | [Data-rich output format](https://github.com/chubin/wttr.in#data-rich-output-format-v2) | [Map view](https://github.com/chubin/wttr.in#map-view-v3) | [Output formats](https://github.com/chubin/wttr.in#different-output-formats) | [Moon phases](https://github.com/chubin/wttr.in#moon-phases) | [Internationalization](https://github.com/chubin/wttr.in#internationalization-and-localization) | [Installation](https://github.com/chubin/wttr.in#installation)
## Usage
@ -21,18 +21,15 @@ You can access the service from a shell or from a Web browser like this:
Weather for City: Paris, France
\ / Clear
.-. 10 11 °C
― ( ) ― ↑ 11 km/h
`- 10 km
/ \ 0.0 mm
.-. 10 11 °C
― ( ) ― ↑ 11 km/h
`- 10 km
/ \ 0.0 mm
Here is an actual weather report for your location (it's live!):
Here is an example weather report:
![Weather Report](https://wttr.in/San-Francisco.png?)
(It's not your actual location - GitHub's CDN hides your real IP address with its own IP address,
but it's still a live weather report in your language.)
![Weather Report](San_Francisco.png)
Or in PowerShell:
@ -78,7 +75,6 @@ To get detailed information online, you can access the [/:help](https://wttr.in/
$ curl wttr.in/:help
### Weather Units
By default the USCS units are used for the queries from the USA and the metric system for the rest of the world.
@ -112,6 +108,10 @@ To force plain text, which disables colors:
$ curl wttr.in/?T
To restrict output to glyphs available in standard console fonts (e.g. Consolas and Lucida Console):
$ curl wttr.in/?d
The PNG format can be forced by adding `.png` to the end of the query:
$ wget wttr.in/Paris.png
@ -227,17 +227,19 @@ set -g status-right "$WEATHER ..."
```
![wttr.in in tmux status bar](https://wttr.in/files/example-tmux-status-line.png)
### Weechat
### WeeChat
To embed in to an IRC ([Weechat](https://github.com/weechat/weechat)) client's existing status bar:
To embed in to an IRC ([WeeChat](https://github.com/weechat/weechat)) client's existing status bar:
```
/alias add wttr /exec -pipe "/set plugins.var.python.text_item.wttr all" url:wttr.in/Montreal?format=%l:+%c+%f+%h+%p+%P+%m+%w+%S+%s
/alias add wttr /exec -pipe "/mute /set plugins.var.wttr" url:wttr.in/Montreal?format=%l:+%c+%f+%h+%p+%P+%m+%w+%S+%s;/wait 3 /item refresh wttr
/trigger add wttr timer 60000;0;0 "" "" "/wttr"
/eval /set weechat.bar.status.items ${weechat.bar.status.items},wttr
/item add wttr "" "${plugins.var.wttr}"
/eval /set weechat.bar.status.items ${weechat.bar.status.items},spacer,wttr
/eval /set weechat.startup.command_after_plugins ${weechat.startup.command_after_plugins};/wttr
/wttr
```
![wttr.in in weechat status bar](https://i.imgur.com/IyvbxjL.png)
![wttr.in in WeeChat status bar](https://i.imgur.com/XkYiRU7.png)
### conky
@ -245,12 +247,12 @@ To embed in to an IRC ([Weechat](https://github.com/weechat/weechat)) client's e
Conky usage example:
```
${texeci 1800 curl wttr.in/kyiv_0pq_lang=uk.png
${texeci 1800 curl wttr.in/kyiv_0pq_lang=uk.png
| convert - -transparent black $HOME/.config/conky/out.png}
${image $HOME/.config/conky/out.png -p 0,0}
```
![wttr.in in weechat status bar](https://user-images.githubusercontent.com/3875145/172178453-9e9ed9e3-9815-426a-9a21-afdd6e279fc8.png)
![wttr.in in conky](https://user-images.githubusercontent.com/3875145/172178453-9e9ed9e3-9815-426a-9a21-afdd6e279fc8.png)
### Emojis support
@ -443,7 +445,7 @@ Most of these values are self-explanatory, aside from `weatherCode`. The `weathe
### Prometheus Metrics Output
The [Prometheus](https://github.com/prometheus/prometheus) Metrics format is a feature providing access to *wttr.in* data through an easy-to-parse format for monitoring systems, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.
The [Prometheus](https://github.com/prometheus/prometheus) Metrics format is a feature providing access to *wttr.in* data through an easy-to-parse format for monitoring systems, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.
To fetch information in Prometheus format, use the following syntax:
@ -549,19 +551,6 @@ in your language.
![Queries to wttr.in in various languages](https://pbs.twimg.com/media/C7hShiDXQAES6z1.jpg)
## Windows Users
There are currently two Windows related issues that prevent the examples found on this page from working exactly as expected out of the box. Until Microsoft fixes the issues, there are a few workarounds. To circumvent both issues you may use a shell such as `bash` on the [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install-win10) or read on for alternative solutions.
### Garbage characters in the output
There is a limitation of the current Win32 version of `curl`. Until the [Win32 curl issue](https://github.com/chubin/wttr.in/issues/18#issuecomment-474145551) is resolved and rolled out in a future Windows release, it is recommended that you use Powershells `Invoke-Web-Request` command instead:
- `(Invoke-WebRequest https://wttr.in).Content`
### Missing or double wide diagonal wind direction characters
The second issue is regarding the width of the diagonal arrow glyphs that some Windows Terminal Applications such as the default `conhost.exe` use. At the time of writing this, `ConEmu.exe`, `ConEmu64.exe` and Terminal Applications built on top of ConEmu such as Cmder (`cmder.exe`) use these double-wide glyphs by default. The result is the same with all of these programs, either a missing character for certain wind directions or a broken table in the output or both. Some third-party Terminal Applications have addressed the wind direction glyph issue but that fix depends on the font and the Terminal Application you are using.
One way to display the diagonal wind direction glyphs in your Terminal Application is to use [Windows Terminal](https://www.microsoft.com/en-us/p/windows-terminal-preview/9n0dx20hk701?activetab=pivot:overviewtab) which is currently available in the [Microsoft Store](https://www.microsoft.com/en-us/p/windows-terminal-preview/9n0dx20hk701?activetab=pivot:overviewtab). Windows Terminal is currently a preview release and will be rolled out as the default Terminal Application in an upcoming release. If your output is still skewed after using Windows Terminal then try maximizing the terminal window.
Another way you can display the diagonal wind direction is to swap out the problematic characters with forward and backward slashes as shown [here](https://github.com/chubin/wttr.in/issues/18#issuecomment-405640892).
## Installation
To install the application:
@ -581,9 +570,9 @@ wttr.in has the following external dependencies:
* [wego](https://github.com/schachmat/wego), weather client for terminal
After you install [golang](https://golang.org/doc/install), install `wego`:
$ go get -u github.com/schachmat/wego
$ go install github.com/schachmat/wego
```bash
go install github.com/schachmat/wego@latest
```
### Install Python dependencies
@ -605,13 +594,15 @@ You can install most of them using `pip`.
Some python package use LLVM, so install it first:
$ apt-get install llvm-7 llvm-7-dev
```bash
apt-get install llvm-7 llvm-7-dev
```
If `virtualenv` is used:
$ virtualenv -p python3 ve
$ ve/bin/pip3 install -r requirements.txt
$ ve/bin/python3 bin/srv.py
```bash
virtualenv -p python3 ve
ve/bin/pip3 install -r requirements.txt
ve/bin/python3 bin/srv.py
```
Also, you need to install the geoip2 database.
You can use a free database GeoLite2 that can be downloaded from (http://dev.maxmind.com/geoip/geoip2/geolite2/).

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

@ -1,79 +0,0 @@
package main
import (
"log"
"net/http"
"sync"
"time"
"github.com/robfig/cron"
)
var peakRequest30 sync.Map
var peakRequest60 sync.Map
func initPeakHandling() {
c := cron.New()
// cronTime := fmt.Sprintf("%d,%d * * * *", 30-prefetchInterval/60, 60-prefetchInterval/60)
c.AddFunc("24 * * * *", prefetchPeakRequests30)
c.AddFunc("54 * * * *", prefetchPeakRequests60)
c.Start()
}
func savePeakRequest(cacheDigest string, r *http.Request) {
_, min, _ := time.Now().Clock()
if min == 30 {
peakRequest30.Store(cacheDigest, *r)
} else if min == 0 {
peakRequest60.Store(cacheDigest, *r)
}
}
func prefetchRequest(r *http.Request) {
processRequest(r)
}
func syncMapLen(sm *sync.Map) int {
count := 0
f := func(key, value interface{}) bool {
// Not really certain about this part, don't know for sure
// if this is a good check for an entry's existence
if key == "" {
return false
}
count++
return true
}
sm.Range(f)
return count
}
func prefetchPeakRequests(peakRequestMap *sync.Map) {
peakRequestLen := syncMapLen(peakRequestMap)
log.Printf("PREFETCH: Prefetching %d requests\n", peakRequestLen)
if peakRequestLen == 0 {
return
}
sleepBetweenRequests := time.Duration(prefetchInterval*1000/peakRequestLen) * time.Millisecond
peakRequestMap.Range(func(key interface{}, value interface{}) bool {
go func(r http.Request) {
prefetchRequest(&r)
}(value.(http.Request))
peakRequestMap.Delete(key)
time.Sleep(sleepBetweenRequests)
return true
})
}
func prefetchPeakRequests30() {
prefetchPeakRequests(&peakRequest30)
}
func prefetchPeakRequests60() {
prefetchPeakRequests(&peakRequest60)
}

@ -1,199 +0,0 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"strings"
"time"
)
func processRequest(r *http.Request) responseWithHeader {
var response responseWithHeader
if response, ok := redirectInsecure(r); ok {
return *response
}
if dontCache(r) {
return get(r)
}
cacheDigest := getCacheDigest(r)
foundInCache := false
savePeakRequest(cacheDigest, r)
cacheBody, ok := lruCache.Get(cacheDigest)
if ok {
cacheEntry := cacheBody.(responseWithHeader)
// if after all attempts we still have no answer,
// we try to make the query on our own
for attempts := 0; attempts < 300; attempts++ {
if !ok || !cacheEntry.InProgress {
break
}
time.Sleep(30 * time.Millisecond)
cacheBody, ok = lruCache.Get(cacheDigest)
cacheEntry = cacheBody.(responseWithHeader)
}
if cacheEntry.InProgress {
log.Printf("TIMEOUT: %s\n", cacheDigest)
}
if ok && !cacheEntry.InProgress && cacheEntry.Expires.After(time.Now()) {
response = cacheEntry
foundInCache = true
}
}
if !foundInCache {
lruCache.Add(cacheDigest, responseWithHeader{InProgress: true})
response = get(r)
if response.StatusCode == 200 || response.StatusCode == 304 || response.StatusCode == 404 {
lruCache.Add(cacheDigest, response)
} else {
log.Printf("REMOVE: %d response for %s from cache\n", response.StatusCode, cacheDigest)
lruCache.Remove(cacheDigest)
}
}
return response
}
func get(req *http.Request) responseWithHeader {
client := &http.Client{}
queryURL := fmt.Sprintf("http://%s%s", req.Host, req.RequestURI)
proxyReq, err := http.NewRequest(req.Method, queryURL, req.Body)
if err != nil {
log.Printf("Request: %s\n", err)
}
// proxyReq.Header.Set("Host", req.Host)
// proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr)
for header, values := range req.Header {
for _, value := range values {
proxyReq.Header.Add(header, value)
}
}
res, err := client.Do(proxyReq)
if err != nil {
panic(err)
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Println(err)
}
return responseWithHeader{
InProgress: false,
Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
Body: body,
Header: res.Header,
StatusCode: res.StatusCode,
}
}
// implementation of the cache.get_signature of original wttr.in
func getCacheDigest(req *http.Request) string {
userAgent := req.Header.Get("User-Agent")
queryHost := req.Host
queryString := req.RequestURI
clientIPAddress := readUserIP(req)
lang := req.Header.Get("Accept-Language")
return fmt.Sprintf("%s:%s%s:%s:%s", userAgent, queryHost, queryString, clientIPAddress, lang)
}
// return true if request should not be cached
func dontCache(req *http.Request) bool {
// dont cache cyclic requests
loc := strings.Split(req.RequestURI, "?")[0]
return strings.Contains(loc, ":")
}
// redirectInsecure returns redirection response, and bool value, if redirection was needed,
// if the query comes from a browser, and it is insecure.
//
// Insecure queries are marked by the frontend web server
// with X-Forwarded-Proto header:
//
// proxy_set_header X-Forwarded-Proto $scheme;
//
//
func redirectInsecure(req *http.Request) (*responseWithHeader, bool) {
if isPlainTextAgent(req.Header.Get("User-Agent")) {
return nil, false
}
if strings.ToLower(req.Header.Get("X-Forwarded-Proto")) == "https" {
return nil, false
}
target := "https://" + req.Host + req.URL.Path
if len(req.URL.RawQuery) > 0 {
target += "?" + req.URL.RawQuery
}
body := []byte(fmt.Sprintf(`<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="%s">here</A>.
</BODY></HTML>
`, target))
return &responseWithHeader{
InProgress: false,
Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
Body: body,
Header: http.Header{"Location": []string{target}},
StatusCode: 301,
}, true
}
// isPlainTextAgent returns true if userAgent is a plain-text agent
func isPlainTextAgent(userAgent string) bool {
userAgentLower := strings.ToLower(userAgent)
for _, signature := range plainTextAgents {
if strings.Contains(userAgentLower, signature) {
return true
}
}
return false
}
func readUserIP(r *http.Request) string {
IPAddress := r.Header.Get("X-Real-Ip")
if IPAddress == "" {
IPAddress = r.Header.Get("X-Forwarded-For")
}
if IPAddress == "" {
IPAddress = r.RemoteAddr
var err error
IPAddress, _, err = net.SplitHostPort(IPAddress)
if err != nil {
log.Printf("ERROR: userip: %q is not IP:port\n", IPAddress)
}
}
return IPAddress
}
func randInt(min int, max int) int {
return min + rand.Intn(max-min)
}

@ -1,87 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"time"
lru "github.com/hashicorp/golang-lru"
)
const serverPort = 8083
const uplinkSrvAddr = "127.0.0.1:9002"
const uplinkTimeout = 30
const prefetchInterval = 300
const lruCacheSize = 12800
// plainTextAgents contains signatures of the plain-text agents
var plainTextAgents = []string{
"curl",
"httpie",
"lwp-request",
"wget",
"python-httpx",
"python-requests",
"openbsd ftp",
"powershell",
"fetch",
"aiohttp",
"http_get",
"xh",
}
var lruCache *lru.Cache
type responseWithHeader struct {
InProgress bool // true if the request is being processed
Expires time.Time // expiration time of the cache entry
Body []byte
Header http.Header
StatusCode int // e.g. 200
}
func init() {
var err error
lruCache, err = lru.New(lruCacheSize)
if err != nil {
panic(err)
}
dialer := &net.Dialer{
Timeout: uplinkTimeout * time.Second,
KeepAlive: uplinkTimeout * time.Second,
DualStack: true,
}
http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, network, uplinkSrvAddr)
}
initPeakHandling()
}
func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// printStat()
response := processRequest(r)
copyHeader(w.Header(), response.Header)
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(response.StatusCode)
w.Write(response.Body)
})
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", serverPort), nil))
}

@ -1,40 +0,0 @@
package main
// import (
// "log"
// "sync"
// "time"
// )
//
// type safeCounter struct {
// v map[int]int
// mux sync.Mutex
// }
//
// func (c *safeCounter) inc(key int) {
// c.mux.Lock()
// c.v[key]++
// c.mux.Unlock()
// }
//
// // func (c *safeCounter) val(key int) int {
// // c.mux.Lock()
// // defer c.mux.Unlock()
// // return c.v[key]
// // }
// //
// // func (c *safeCounter) reset(key int) int {
// // c.mux.Lock()
// // defer c.mux.Unlock()
// // result := c.v[key]
// // c.v[key] = 0
// // return result
// // }
//
// var queriesPerMinute safeCounter
//
// func printStat() {
// _, min, _ := time.Now().Clock()
// queriesPerMinute.inc(min)
// log.Printf("Processed %d requests\n", min)
// }

@ -0,0 +1,26 @@
module github.com/chubin/wttr.in
go 1.16
require (
github.com/alecthomas/kong v0.7.1 // indirect
github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204 // indirect
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
github.com/hashicorp/golang-lru v0.6.0
github.com/itchyny/gojq v0.12.11 // indirect
github.com/klauspost/lctime v0.1.0 // indirect
github.com/lib/pq v1.8.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/robfig/cron v1.2.0
github.com/samonzeweb/godb v1.0.8 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/smartystreets/assertions v1.2.0 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.8.1 // indirect
github.com/zsefvlol/timezonemapper v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

@ -0,0 +1,80 @@
github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4=
github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/itchyny/gojq v0.12.11 h1:YhLueoHhHiN4mkfM+3AyJV6EPcCxKZsOnYf+aVSwaQw=
github.com/itchyny/gojq v0.12.11/go.mod h1:o3FT8Gkbg/geT4pLI0tF3hvip5F3Y/uskjRz9OYa38g=
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70=
github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/samonzeweb/godb v1.0.8 h1:WRn6nq0FChYOzh+w8SgpXHUkEhL7W6ZqkCf5Ninx7Uc=
github.com/samonzeweb/godb v1.0.8/go.mod h1:LNDt3CakfBwpRY4AD0y/QPTbj+jB6O17tSxQES0p47o=
github.com/samonzeweb/godb v1.0.15 h1:HyNb8o1w109as9KWE8ih1YIBe8jC4luJ22f1XNacW38=
github.com/samonzeweb/godb v1.0.15/go.mod h1:SxCHqyireDXNrZApknS9lGUEutA43x9eJF632ecbK5Q=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/zsefvlol/timezonemapper v1.0.0 h1:HXqkOzf01gXYh2nDQcDSROikFgMaximnhE8BY9SyF6E=
github.com/zsefvlol/timezonemapper v1.0.0/go.mod h1:cVUCOLEmc/VvOMusEhpd2G/UBtadL26ZVz2syODXDoQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -0,0 +1,179 @@
package config
import (
"log"
"os"
"gopkg.in/yaml.v3"
"github.com/chubin/wttr.in/internal/types"
"github.com/chubin/wttr.in/internal/util"
)
// Config of the program.
type Config struct {
Cache
Geo
Logging
Server
Uplink
}
// Logging configuration.
type Logging struct {
// AccessLog path.
AccessLog string `yaml:"accessLog,omitempty"`
// ErrorsLog path.
ErrorsLog string `yaml:"errorsLog,omitempty"`
// Interval between access log flushes, in seconds.
Interval int `yaml:"interval,omitempty"`
}
// Server configuration.
type Server struct {
// PortHTTP is port where HTTP server must listen.
// If 0, HTTP is disabled.
PortHTTP int `yaml:"portHttp,omitempty"`
// PortHTTP is port where the HTTPS server must listen.
// If 0, HTTPS is disabled.
PortHTTPS int `yaml:"portHttps,omitempty"`
// TLSCertFile contains path to cert file for TLS Server.
TLSCertFile string `yaml:"tlsCertFile,omitempty"`
// TLSCertFile contains path to key file for TLS Server.
TLSKeyFile string `yaml:"tlsKeyFile,omitempty"`
}
// Uplink configuration.
type Uplink struct {
// Address contains address of the uplink server in form IP:PORT.
Address string `yaml:"address,omitempty"`
// Timeout for upstream queries.
Timeout int `yaml:"timeout,omitempty"`
// PrefetchInterval contains time (in milliseconds) indicating,
// how long the prefetch procedure should take.
PrefetchInterval int `yaml:"prefetchInterval,omitempty"`
}
// Cache configuration.
type Cache struct {
// Size of the main cache.
Size int `yaml:"size,omitempty"`
}
// Geo contains geolocation configuration.
type Geo struct {
// IPCache contains the path to the IP Geodata cache.
IPCache string `yaml:"ipCache,omitempty"`
// IPCacheDB contains the path to the SQLite DB with the IP Geodata cache.
IPCacheDB string `yaml:"ipCacheDb,omitempty"`
IPCacheType types.CacheType `yaml:"ipCacheType,omitempty"`
// LocationCache contains the path to the Location Geodata cache.
LocationCache string `yaml:"locationCache,omitempty"`
// LocationCacheDB contains the path to the SQLite DB with the Location Geodata cache.
LocationCacheDB string `yaml:"locationCacheDb,omitempty"`
LocationCacheType types.CacheType `yaml:"locationCacheType,omitempty"`
Nominatim []Nominatim
}
type Nominatim struct {
Name string
// Type describes the type of the location service.
// Supported types: iq.
Type string
URL string
Token string
}
// Default contains the default configuration.
func Default() *Config {
return &Config{
Cache{
Size: 12800,
},
Geo{
IPCache: "/wttr.in/cache/ip2l",
IPCacheDB: "/wttr.in/cache/geoip.db",
IPCacheType: types.CacheTypeDB,
LocationCache: "/wttr.in/cache/loc",
LocationCacheDB: "/wttr.in/cache/geoloc.db",
LocationCacheType: types.CacheTypeDB,
Nominatim: []Nominatim{
{
Name: "locationiq",
Type: "iq",
URL: "https://eu1.locationiq.com/v1/search",
Token: os.Getenv("NOMINATIM_LOCATIONIQ"),
},
{
Name: "opencage",
Type: "opencage",
URL: "https://api.opencagedata.com/geocode/v1/json",
Token: os.Getenv("NOMINATIM_OPENCAGE"),
},
},
},
Logging{
AccessLog: "/wttr.in/log/access.log",
ErrorsLog: "/wttr.in/log/errors.log",
Interval: 300,
},
Server{
PortHTTP: 8083,
PortHTTPS: 8084,
TLSCertFile: "/wttr.in/etc/fullchain.pem",
TLSKeyFile: "/wttr.in/etc/privkey.pem",
},
Uplink{
Address: "127.0.0.1:9002",
Timeout: 30,
PrefetchInterval: 300,
},
}
}
// Load config from file.
func Load(filename string) (*Config, error) {
var (
config Config
data []byte
err error
)
data, err = os.ReadFile(filename)
if err != nil {
return nil, err
}
err = util.YamlUnmarshalStrict(data, &config)
if err != nil {
return nil, err
}
return &config, nil
}
func (c *Config) Dump() []byte {
data, err := yaml.Marshal(c)
if err != nil {
// should never happen.
log.Fatalln("config.Dump():", err)
}
return data
}

@ -0,0 +1,774 @@
package main
// Source: https://www.ditig.com/downloads/256-colors.json
var ansiColorsDB = [][3]float64{
{
0, 0, 0,
},
{
128, 0, 0,
},
{
0, 128, 0,
},
{
128, 128, 0,
},
{
0, 0, 128,
},
{
128, 0, 128,
},
{
0, 128, 128,
},
{
192, 192, 192,
},
{
128, 128, 128,
},
{
255, 0, 0,
},
{
0, 255, 0,
},
{
255, 255, 0,
},
{
0, 0, 255,
},
{
255, 0, 255,
},
{
0, 255, 255,
},
{
255, 255, 255,
},
{
0, 0, 0,
},
{
0, 0, 95,
},
{
0, 0, 135,
},
{
0, 0, 175,
},
{
0, 0, 215,
},
{
0, 0, 255,
},
{
0, 95, 0,
},
{
0, 95, 95,
},
{
0, 95, 135,
},
{
0, 95, 175,
},
{
0, 95, 215,
},
{
0, 95, 255,
},
{
0, 135, 0,
},
{
0, 135, 95,
},
{
0, 135, 135,
},
{
0, 135, 175,
},
{
0, 135, 215,
},
{
0, 135, 255,
},
{
0, 175, 0,
},
{
0, 175, 95,
},
{
0, 175, 135,
},
{
0, 175, 175,
},
{
0, 175, 215,
},
{
0, 175, 255,
},
{
0, 215, 0,
},
{
0, 215, 95,
},
{
0, 215, 135,
},
{
0, 215, 175,
},
{
0, 215, 215,
},
{
0, 215, 255,
},
{
0, 255, 0,
},
{
0, 255, 95,
},
{
0, 255, 135,
},
{
0, 255, 175,
},
{
0, 255, 215,
},
{
0, 255, 255,
},
{
95, 0, 0,
},
{
95, 0, 95,
},
{
95, 0, 135,
},
{
95, 0, 175,
},
{
95, 0, 215,
},
{
95, 0, 255,
},
{
95, 95, 0,
},
{
95, 95, 95,
},
{
95, 95, 135,
},
{
95, 95, 175,
},
{
95, 95, 215,
},
{
95, 95, 255,
},
{
95, 135, 0,
},
{
95, 135, 95,
},
{
95, 135, 135,
},
{
95, 135, 175,
},
{
95, 135, 215,
},
{
95, 135, 255,
},
{
95, 175, 0,
},
{
95, 175, 95,
},
{
95, 175, 135,
},
{
95, 175, 175,
},
{
95, 175, 215,
},
{
95, 175, 255,
},
{
95, 215, 0,
},
{
95, 215, 95,
},
{
95, 215, 135,
},
{
95, 215, 175,
},
{
95, 215, 215,
},
{
95, 215, 255,
},
{
95, 255, 0,
},
{
95, 255, 95,
},
{
95, 255, 135,
},
{
95, 255, 175,
},
{
95, 255, 215,
},
{
95, 255, 255,
},
{
135, 0, 0,
},
{
135, 0, 95,
},
{
135, 0, 135,
},
{
135, 0, 175,
},
{
135, 0, 215,
},
{
135, 0, 255,
},
{
135, 95, 0,
},
{
135, 95, 95,
},
{
135, 95, 135,
},
{
135, 95, 175,
},
{
135, 95, 215,
},
{
135, 95, 255,
},
{
135, 135, 0,
},
{
135, 135, 95,
},
{
135, 135, 135,
},
{
135, 135, 175,
},
{
135, 135, 215,
},
{
135, 135, 255,
},
{
135, 175, 0,
},
{
135, 175, 95,
},
{
135, 175, 135,
},
{
135, 175, 175,
},
{
135, 175, 215,
},
{
135, 175, 255,
},
{
135, 215, 0,
},
{
135, 215, 95,
},
{
135, 215, 135,
},
{
135, 215, 175,
},
{
135, 215, 215,
},
{
135, 215, 255,
},
{
135, 255, 0,
},
{
135, 255, 95,
},
{
135, 255, 135,
},
{
135, 255, 175,
},
{
135, 255, 215,
},
{
135, 255, 255,
},
{
175, 0, 0,
},
{
175, 0, 95,
},
{
175, 0, 135,
},
{
175, 0, 175,
},
{
175, 0, 215,
},
{
175, 0, 255,
},
{
175, 95, 0,
},
{
175, 95, 95,
},
{
175, 95, 135,
},
{
175, 95, 175,
},
{
175, 95, 215,
},
{
175, 95, 255,
},
{
175, 135, 0,
},
{
175, 135, 95,
},
{
175, 135, 135,
},
{
175, 135, 175,
},
{
175, 135, 215,
},
{
175, 135, 255,
},
{
175, 175, 0,
},
{
175, 175, 95,
},
{
175, 175, 135,
},
{
175, 175, 175,
},
{
175, 175, 215,
},
{
175, 175, 255,
},
{
175, 215, 0,
},
{
175, 215, 95,
},
{
175, 215, 135,
},
{
175, 215, 175,
},
{
175, 215, 215,
},
{
175, 215, 255,
},
{
175, 255, 0,
},
{
175, 255, 95,
},
{
175, 255, 135,
},
{
175, 255, 175,
},
{
175, 255, 215,
},
{
175, 255, 255,
},
{
215, 0, 0,
},
{
215, 0, 95,
},
{
215, 0, 135,
},
{
215, 0, 175,
},
{
215, 0, 215,
},
{
215, 0, 255,
},
{
215, 95, 0,
},
{
215, 95, 95,
},
{
215, 95, 135,
},
{
215, 95, 175,
},
{
215, 95, 215,
},
{
215, 95, 255,
},
{
215, 135, 0,
},
{
215, 135, 95,
},
{
215, 135, 135,
},
{
215, 135, 175,
},
{
215, 135, 215,
},
{
215, 135, 255,
},
{
215, 175, 0,
},
{
215, 175, 95,
},
{
215, 175, 135,
},
{
215, 175, 175,
},
{
215, 175, 215,
},
{
215, 175, 255,
},
{
215, 215, 0,
},
{
215, 215, 95,
},
{
215, 215, 135,
},
{
215, 215, 175,
},
{
215, 215, 215,
},
{
215, 215, 255,
},
{
215, 255, 0,
},
{
215, 255, 95,
},
{
215, 255, 135,
},
{
215, 255, 175,
},
{
215, 255, 215,
},
{
215, 255, 255,
},
{
255, 0, 0,
},
{
255, 0, 95,
},
{
255, 0, 135,
},
{
255, 0, 175,
},
{
255, 0, 215,
},
{
255, 0, 255,
},
{
255, 95, 0,
},
{
255, 95, 95,
},
{
255, 95, 135,
},
{
255, 95, 175,
},
{
255, 95, 215,
},
{
255, 95, 255,
},
{
255, 135, 0,
},
{
255, 135, 95,
},
{
255, 135, 135,
},
{
255, 135, 175,
},
{
255, 135, 215,
},
{
255, 135, 255,
},
{
255, 175, 0,
},
{
255, 175, 95,
},
{
255, 175, 135,
},
{
255, 175, 175,
},
{
255, 175, 215,
},
{
255, 175, 255,
},
{
255, 215, 0,
},
{
255, 215, 95,
},
{
255, 215, 135,
},
{
255, 215, 175,
},
{
255, 215, 215,
},
{
255, 215, 255,
},
{
255, 255, 0,
},
{
255, 255, 95,
},
{
255, 255, 135,
},
{
255, 255, 175,
},
{
255, 255, 215,
},
{
255, 255, 255,
},
{
8, 8, 8,
},
{
18, 18, 18,
},
{
28, 28, 28,
},
{
38, 38, 38,
},
{
48, 48, 48,
},
{
58, 58, 58,
},
{
68, 68, 68,
},
{
78, 78, 78,
},
{
88, 88, 88,
},
{
98, 98, 98,
},
{
108, 108, 108,
},
{
118, 118, 118,
},
{
128, 128, 128,
},
{
138, 138, 138,
},
{
148, 148, 148,
},
{
158, 158, 158,
},
{
168, 168, 168,
},
{
178, 178, 178,
},
{
188, 188, 188,
},
{
198, 198, 198,
},
{
208, 208, 208,
},
{
218, 218, 218,
},
{
228, 228, 228,
},
{
238, 238, 238,
},
}

@ -0,0 +1,10 @@
module example.com/m/v2
go 1.20
require (
github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
golang.org/x/image v0.14.0 // indirect
)

@ -0,0 +1,8 @@
github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1 h1:CHg5BTAJZmCjBaAAQrD92s248JHH3JTsLlaC6QBJo/Y=
github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1/go.mod h1:mQssL2gI1LTqWgbffl6DESqe6QkAF67ujBdzSe4bWkU=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=

@ -0,0 +1,224 @@
package main
import (
"fmt"
"log"
"os"
"strings"
"github.com/chubin/vt10x"
"github.com/fogleman/gg"
)
func StringSliceToRuneSlice(s string) [][]rune {
strings := strings.Split(s, "\n")
result := make([][]rune, len(strings))
i := 0