Compare commits
39 commits
Author | SHA1 | Date | |
---|---|---|---|
|
03b616622e | ||
|
50e1da01fe | ||
|
f8f50b26d8 | ||
|
a3bc133bcb | ||
|
50dae22ff4 | ||
|
5a91154eab | ||
|
d5b89d512a | ||
|
97f43f88eb | ||
|
3177af9524 | ||
|
f99d22738c | ||
|
ea7124db52 | ||
|
b1247e5de6 | ||
|
5ab962634e | ||
|
d99944dcff | ||
|
920bcb06dc | ||
|
3679cc21ab | ||
|
07e5626469 | ||
|
6bb6f87800 | ||
|
cb6b52ff4c | ||
|
03c377dff3 | ||
|
0e3f172032 | ||
|
fd704419d9 | ||
|
32ac72f592 | ||
|
9c77b87435 | ||
|
d4d9e94f44 | ||
|
47cef2abf0 | ||
|
0a5cdf380f | ||
|
9da7967158 | ||
|
964744a9ae | ||
|
779304d035 | ||
|
f15d7445bd | ||
|
26d68ba3fc | ||
|
958805a1fd | ||
|
dd74c173a5 | ||
|
bd020c93f5 | ||
|
75555f0426 | ||
|
1483b355af | ||
|
6a5bb635bb | ||
|
0e0aca3844 |
24 changed files with 398 additions and 85 deletions
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.23.6-alpine3.21 AS builder
|
||||
FROM golang:1.24.2-alpine3.21 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<p align="center"><em>What if you could see everything at a...</em></p>
|
||||
<h1 align="center">Glance</h1>
|
||||
<p align="center"><a href="#installation">Install</a> • <a href="docs/configuration.md">Configuration</a> • <a href="docs/preconfigured-pages.md">Preconfigured pages</a> • <a href="docs/themes.md">Themes</a> • <a href="https://discord.com/invite/7KQ7Xa9kJd">Discord</a></p>
|
||||
<p align="center"><a href="#installation">Install</a> • <a href="docs/configuration.md">Configuration</a> • <a href="https://discord.com/invite/7KQ7Xa9kJd">Discord</a> • <a href="https://github.com/sponsors/glanceapp">Sponsor</a></p>
|
||||
<p align="center"><a href="https://github.com/glanceapp/community-widgets">Community widgets</a> • <a href="docs/preconfigured-pages.md">Preconfigured pages</a> • <a href="docs/themes.md">Themes</a></p>
|
||||
|
||||

|
||||
|
||||
|
@ -194,6 +195,7 @@ services:
|
|||
glance:
|
||||
container_name: glance
|
||||
image: glanceapp/glance
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
ports:
|
||||
|
|
|
@ -365,7 +365,7 @@ pages:
|
|||
| show-mobile-header | boolean | no | false |
|
||||
| columns | array | yes | |
|
||||
|
||||
#### `title`
|
||||
#### `name`
|
||||
The name of the page which gets shown in the navigation bar.
|
||||
|
||||
#### `slug`
|
||||
|
@ -1297,6 +1297,7 @@ Examples:
|
|||
| body | any | no | |
|
||||
| frameless | boolean | no | false |
|
||||
| allow-insecure | boolean | no | false |
|
||||
| skip-json-validation | boolean | no | false |
|
||||
| template | string | yes | |
|
||||
| parameters | key (string) & value (string|array) | no | |
|
||||
| subrequests | map of requests | no | |
|
||||
|
@ -1344,6 +1345,9 @@ When set to `true`, removes the border and padding around the widget.
|
|||
##### `allow-insecure`
|
||||
Whether to ignore invalid/self-signed certificates.
|
||||
|
||||
##### `skip-json-validation`
|
||||
When set to `true`, skips the JSON validation step. This is useful when the API returns JSON Lines/newline-delimited JSON, which is a format that consists of several JSON objects separated by newlines.
|
||||
|
||||
##### `template`
|
||||
The template that will be used to display the data. It relies on Go's `html/template` package so it's recommended to go through [its documentation](https://pkg.go.dev/text/template) to understand how to do basic things such as conditionals, loops, etc. In addition, it also uses [tidwall's gjson](https://github.com/tidwall/gjson) package to parse the JSON data so it's worth going through its documentation if you want to use more advanced JSON selectors. You can view additional examples with explanations and function definitions [here](custom-api.md).
|
||||
|
||||
|
@ -1946,7 +1950,7 @@ Whether to hide the swap usage.
|
|||
| Name | Type | Required | Default |
|
||||
| ---- | ---- | -------- | ------- |
|
||||
| cpu-temp-sensor | string | no | |
|
||||
| hide-mointpoints-by-default | boolean | no | false |
|
||||
| hide-mountpoints-by-default | boolean | no | false |
|
||||
| mountpoints | map\[string\]object | no | |
|
||||
|
||||
###### `cpu-temp-sensor`
|
||||
|
|
|
@ -226,10 +226,10 @@ JSON response:
|
|||
}
|
||||
```
|
||||
|
||||
Calculations can be performed, however all numbers must be converted to floats first if they are not already:
|
||||
Calculations can be performed on either ints or floats. If both numbers are ints, an int will be returned, otherwise a float will be returned. If you try to divide by zero, 0 will be returned. If you provide non-numeric values, `NaN` will be returned.
|
||||
|
||||
```html
|
||||
<div>{{ sub (.JSON.Int "price" | toFloat) (.JSON.Int "discount" | toFloat) }}</div>
|
||||
<div>{{ sub (.JSON.Int "price") (.JSON.Int "discount") }}</div>
|
||||
```
|
||||
|
||||
Output:
|
||||
|
@ -309,6 +309,55 @@ You can also access the response headers:
|
|||
<div>{{ .Response.Header.Get "Content-Type" }}</div>
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
JSON response:
|
||||
|
||||
```json
|
||||
{"name": "Steve", "age": 30}
|
||||
{"name": "Alex", "age": 25}
|
||||
{"name": "John", "age": 35}
|
||||
```
|
||||
|
||||
The above format is "[ndjson](https://docs.mulesoft.com/dataweave/latest/dataweave-formats-ndjson)" or "[JSON Lines](https://jsonlines.org/)", where each line is a separate JSON object. To parse this format, you must first disable the JSON validation check in your config, since by default the response is expected to be a single valid JSON object:
|
||||
|
||||
```yaml
|
||||
- type: custom-api
|
||||
skip-json-validation: true
|
||||
```
|
||||
|
||||
Then, to iterate over each object you can use `.JSONLines`:
|
||||
|
||||
```html
|
||||
{{ range .JSONLines }}
|
||||
<p>{{ .String "name" }} is {{ .Int "age" }} years old</p>
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```html
|
||||
<p>Steve is 30 years old</p>
|
||||
<p>Alex is 25 years old</p>
|
||||
<p>John is 35 years old</p>
|
||||
```
|
||||
|
||||
For other ways of selecting data from a JSON Lines response, have a look at the docs for [tidwall/gjson](https://github.com/tidwall/gjson/tree/master?tab=readme-ov-file#json-lines). For example, to get an array of all names, you can use the following:
|
||||
|
||||
```html
|
||||
{{ range .JSON.Array "..#.name" }}
|
||||
<p>{{ .String "" }}</p>
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```html
|
||||
<p>Steve</p>
|
||||
<p>Alex</p>
|
||||
<p>John</p>
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
The following functions are available on the `JSON` object:
|
||||
|
@ -325,6 +374,9 @@ The following helper functions provided by Glance are available:
|
|||
- `toFloat(i int) float`: Converts an integer to a float.
|
||||
- `toInt(f float) int`: Converts a float to an integer.
|
||||
- `toRelativeTime(t time.Time) template.HTMLAttr`: Converts Time to a relative time such as 2h, 1d, etc which dynamically updates. **NOTE:** the value of this function should be used as an attribute in an HTML tag, e.g. `<span {{ toRelativeTime .Time }}></span>`.
|
||||
- `now() time.Time`: Returns the current time.
|
||||
- `offsetNow(offset string) time.Time`: Returns the current time with an offset. The offset can be positive or negative and must be in the format "3h" "-1h" or "2h30m10s".
|
||||
- `duration(str string) time.Duration`: Parses a string such as `1h`, `24h`, `5h30m`, etc into a `time.Duration`.
|
||||
- `parseTime(layout string, s string) time.Time`: Parses a string into time.Time. The layout must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants). You can alternatively use these values instead of the literal format: "unix", "RFC3339", "RFC3339Nano", "DateTime", "DateOnly".
|
||||
- `parseRelativeTime(layout string, s string) time.Time`: A shorthand for `{{ .String "date" | parseTime "rfc3339" | toRelativeTime }}`.
|
||||
- `add(a, b float) float`: Adds two numbers.
|
||||
|
@ -337,12 +389,15 @@ The following helper functions provided by Glance are available:
|
|||
- `trimSuffix(suffix string, str string) string`: Trims the suffix from a string.
|
||||
- `trimSpace(str string) string`: Trims whitespace from a string on both ends.
|
||||
- `replaceAll(old string, new string, str string) string`: Replaces all occurrences of a string in a string.
|
||||
- `replaceMatches(pattern string, replacement string, str string) string`: Replaces all occurrences of a regular expression in a string.
|
||||
- `findMatch(pattern string, str string) string`: Finds the first match of a regular expression in a string.
|
||||
- `findSubmatch(pattern string, str string) string`: Finds the first submatch of a regular expression in a string.
|
||||
- `sortByString(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a string key in either ascending or descending order.
|
||||
- `sortByInt(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by an integer key in either ascending or descending order.
|
||||
- `sortByFloat(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a float key in either ascending or descending order.
|
||||
- `sortByTime(key string, layout string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a time key in either ascending or descending order. The format must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants).
|
||||
- `concat(strings ...string) string`: Concatenates multiple strings together.
|
||||
- `unique(key string, arr []JSON) []JSON`: Returns a unique array of JSON objects based on the given key.
|
||||
|
||||
The following helper functions provided by Go's `text/template` are available:
|
||||
|
||||
|
|
|
@ -26,6 +26,9 @@ If you know how to setup an HTTP server and a bit of HTML and CSS you're ready t
|
|||
### `Widget-Title`
|
||||
Used to specify the title of the widget. If not provided, the widget's title will be "Extension".
|
||||
|
||||
### `Widget-Title-URL`
|
||||
Used to specify the URL that will be opened when the widget's title is clicked. If the user has specified a `title-url` in their config, it will take precedence over this header.
|
||||
|
||||
### `Widget-Content-Type`
|
||||
Used to specify the content type that will be returned by the extension. If not provided, the content will be shown as plain text.
|
||||
|
||||
|
|
BIN
docs/images/themes/dracula.png
Normal file
BIN
docs/images/themes/dracula.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
|
@ -82,6 +82,17 @@ theme:
|
|||
negative-color: 209 88 54
|
||||
```
|
||||
|
||||
### Dracula
|
||||

|
||||
```yaml
|
||||
theme:
|
||||
background-color: 231 15 21
|
||||
primary-color: 265 89 79
|
||||
contrast-multiplier: 1.2
|
||||
positive-color: 135 94 66
|
||||
negative-color: 0 100 67
|
||||
```
|
||||
|
||||
## Light
|
||||
|
||||
### Catppuccin Latte
|
||||
|
|
20
go.mod
20
go.mod
|
@ -1,32 +1,32 @@
|
|||
module github.com/glanceapp/glance
|
||||
|
||||
go 1.23.6
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/mmcdole/gofeed v1.3.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.3
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
golang.org/x/text v0.22.0
|
||||
golang.org/x/text v0.24.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.1 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.10.2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/mmcdole/goxpp v1.1.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.9.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
)
|
||||
|
|
19
go.sum
19
go.sum
|
@ -1,5 +1,7 @@
|
|||
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
|
||||
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
|
||||
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
|
||||
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -9,16 +11,21 @@ github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z
|
|||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
|
||||
github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=
|
||||
github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8=
|
||||
|
@ -34,6 +41,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
|
|||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE=
|
||||
github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
|
@ -47,8 +56,12 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
|||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
|
||||
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
|
@ -74,6 +87,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
|||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -97,6 +112,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
@ -117,6 +134,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
|
|
@ -5,23 +5,39 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
)
|
||||
|
||||
type cliIntent uint8
|
||||
|
||||
const (
|
||||
cliIntentServe cliIntent = iota
|
||||
cliIntentConfigValidate = iota
|
||||
cliIntentConfigPrint = iota
|
||||
cliIntentDiagnose = iota
|
||||
cliIntentVersionPrint cliIntent = iota
|
||||
cliIntentServe
|
||||
cliIntentConfigValidate
|
||||
cliIntentConfigPrint
|
||||
cliIntentDiagnose
|
||||
cliIntentSensorsPrint
|
||||
cliIntentMountpointInfo
|
||||
)
|
||||
|
||||
type cliOptions struct {
|
||||
intent cliIntent
|
||||
configPath string
|
||||
args []string
|
||||
}
|
||||
|
||||
func parseCliOptions() (*cliOptions, error) {
|
||||
var args []string
|
||||
|
||||
args = os.Args[1:]
|
||||
if len(args) == 1 && (args[0] == "--version" || args[0] == "-v" || args[0] == "version") {
|
||||
return &cliOptions{
|
||||
intent: cliIntentVersionPrint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
flags := flag.NewFlagSet("", flag.ExitOnError)
|
||||
flags.Usage = func() {
|
||||
fmt.Println("Usage: glance [options] command")
|
||||
|
@ -32,6 +48,8 @@ func parseCliOptions() (*cliOptions, error) {
|
|||
fmt.Println("\nCommands:")
|
||||
fmt.Println(" config:validate Validate the config file")
|
||||
fmt.Println(" config:print Print the parsed config file with embedded includes")
|
||||
fmt.Println(" sensors:print List all sensors")
|
||||
fmt.Println(" mountpoint:info Print information about a given mountpoint path")
|
||||
fmt.Println(" diagnose Run diagnostic checks")
|
||||
}
|
||||
configPath := flags.String("config", "glance.yml", "Set config path")
|
||||
|
@ -41,7 +59,7 @@ func parseCliOptions() (*cliOptions, error) {
|
|||
}
|
||||
|
||||
var intent cliIntent
|
||||
var args = flags.Args()
|
||||
args = flags.Args()
|
||||
unknownCommandErr := fmt.Errorf("unknown command: %s", strings.Join(args, " "))
|
||||
|
||||
if len(args) == 0 {
|
||||
|
@ -51,11 +69,19 @@ func parseCliOptions() (*cliOptions, error) {
|
|||
intent = cliIntentConfigValidate
|
||||
} else if args[0] == "config:print" {
|
||||
intent = cliIntentConfigPrint
|
||||
} else if args[0] == "sensors:print" {
|
||||
intent = cliIntentSensorsPrint
|
||||
} else if args[0] == "diagnose" {
|
||||
intent = cliIntentDiagnose
|
||||
} else {
|
||||
return nil, unknownCommandErr
|
||||
}
|
||||
} else if len(args) == 2 {
|
||||
if args[0] == "mountpoint:info" {
|
||||
intent = cliIntentMountpointInfo
|
||||
} else {
|
||||
return nil, unknownCommandErr
|
||||
}
|
||||
} else {
|
||||
return nil, unknownCommandErr
|
||||
}
|
||||
|
@ -63,5 +89,51 @@ func parseCliOptions() (*cliOptions, error) {
|
|||
return &cliOptions{
|
||||
intent: intent,
|
||||
configPath: *configPath,
|
||||
args: args,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func cliSensorsPrint() int {
|
||||
tempSensors, err := sensors.SensorsTemperatures()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to retrieve list of sensors: %v\n", err)
|
||||
if warns, ok := err.(*sensors.Warnings); ok {
|
||||
for _, w := range warns.List {
|
||||
fmt.Printf(" - %v\n", w)
|
||||
}
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
if len(tempSensors) == 0 {
|
||||
fmt.Println("No sensors found")
|
||||
return 0
|
||||
}
|
||||
|
||||
for _, sensor := range tempSensors {
|
||||
fmt.Printf("%s: %.1f°C\n", sensor.SensorKey, sensor.Temperature)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func cliMountpointInfo(requestedPath string) int {
|
||||
usage, err := disk.Usage(requestedPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to retrieve info for path %s: %v\n", requestedPath, err)
|
||||
if warns, ok := err.(*disk.Warnings); ok {
|
||||
for _, w := range warns.List {
|
||||
fmt.Printf(" - %v\n", w)
|
||||
}
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
fmt.Println("Path:", usage.Path)
|
||||
fmt.Println("FS type:", ternary(usage.Fstype == "", "unknown", usage.Fstype))
|
||||
fmt.Printf("Used percent: %.1f%%\n", usage.UsedPercent)
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
|
@ -81,7 +81,9 @@ var diagnosticSteps = []diagnosticStep{
|
|||
{
|
||||
name: "fetch data from Yahoo finance API",
|
||||
fn: func() (string, error) {
|
||||
return testHttpRequest("GET", "https://query1.finance.yahoo.com/v8/finance/chart/NVDA", 200)
|
||||
return testHttpRequestWithHeaders("GET", "https://query1.finance.yahoo.com/v8/finance/chart/NVDA", map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0",
|
||||
}, 200)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -18,6 +18,8 @@ func Main() int {
|
|||
}
|
||||
|
||||
switch options.intent {
|
||||
case cliIntentVersionPrint:
|
||||
fmt.Println(buildVersion)
|
||||
case cliIntentServe:
|
||||
// remove in v0.10.0
|
||||
if serveUpdateNoticeIfConfigLocationNotMigrated(options.configPath) {
|
||||
|
@ -47,6 +49,10 @@ func Main() int {
|
|||
}
|
||||
|
||||
fmt.Println(string(contents))
|
||||
case cliIntentSensorsPrint:
|
||||
return cliSensorsPrint()
|
||||
case cliIntentMountpointInfo:
|
||||
return cliMountpointInfo(options.args[1])
|
||||
case cliIntentDiagnose:
|
||||
runDiagnostic()
|
||||
}
|
||||
|
|
|
@ -649,7 +649,7 @@ function setupTruncatedElementTitles() {
|
|||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i];
|
||||
if (element.title === "") element.title = element.textContent;
|
||||
if (element.getAttribute("title") === null) element.title = element.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1839,7 +1839,7 @@ details[open] .summary::after {
|
|||
transform: translateY(calc(100% - var(--mobile-navigation-height)));
|
||||
left: var(--content-bounds-padding);
|
||||
right: var(--content-bounds-padding);
|
||||
z-index: 10;
|
||||
z-index: 11;
|
||||
background-color: var(--color-widget-background);
|
||||
border: 1px solid var(--color-widget-content-border);
|
||||
border-bottom: 0;
|
||||
|
@ -1985,7 +1985,7 @@ details[open] .summary::after {
|
|||
|
||||
@media (max-width: 550px) {
|
||||
:root {
|
||||
font-size: 9px;
|
||||
font-size: 9.4px;
|
||||
--widget-gap: 15px;
|
||||
--widget-content-vertical-padding: 10px;
|
||||
--widget-content-horizontal-padding: 10px;
|
||||
|
@ -2038,6 +2038,7 @@ details[open] .summary::after {
|
|||
.color-primary { color: var(--color-primary); }
|
||||
|
||||
.cursor-help { cursor: help; }
|
||||
.rounded { border-radius: var(--border-radius); }
|
||||
.break-all { word-break: break-all; }
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
|
|
|
@ -27,6 +27,9 @@ var globalTemplateFunctions = template.FuncMap{
|
|||
"formatPrice": func(price float64) string {
|
||||
return intl.Sprintf("%.2f", price)
|
||||
},
|
||||
"formatPriceWithPrecision": func(precision int, price float64) string {
|
||||
return intl.Sprintf("%."+strconv.Itoa(precision)+"f", price)
|
||||
},
|
||||
"dynamicRelativeTimeAttrs": dynamicRelativeTimeAttrs,
|
||||
"formatServerMegabytes": func(mb uint64) template.HTML {
|
||||
var value string
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-width-0">
|
||||
<div class="min-width-0 grow">
|
||||
{{- if .URL }}
|
||||
<a href="{{ .URL | safeURL }}" class="color-highlight size-title-dynamic block text-truncate" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
|
||||
{{- else }}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
<div class="market-values shrink-0">
|
||||
<div class="size-h3 text-right {{ if eq .PercentChange 0.0 }}{{ else if gt .PercentChange 0.0 }}color-positive{{ else }}color-negative{{ end }}">{{ printf "%+.2f" .PercentChange }}%</div>
|
||||
<div class="text-right">{{ .Currency }}{{ .Price | formatPrice }}</div>
|
||||
<div class="text-right">{{ .Currency }}{{ .Price | formatPriceWithPrecision .PriceHint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
|
|
@ -32,6 +32,7 @@ type CustomAPIRequest struct {
|
|||
Method string `yaml:"method"`
|
||||
BodyType string `yaml:"body-type"`
|
||||
Body any `yaml:"body"`
|
||||
SkipJSONValidation bool `yaml:"skip-json-validation"`
|
||||
bodyReader io.ReadSeeker `yaml:"-"`
|
||||
httpRequest *http.Request `yaml:"-"`
|
||||
}
|
||||
|
@ -157,6 +158,17 @@ type customAPITemplateData struct {
|
|||
subrequests map[string]*customAPIResponseData
|
||||
}
|
||||
|
||||
func (data *customAPITemplateData) JSONLines() []decoratedGJSONResult {
|
||||
result := make([]decoratedGJSONResult, 0, 5)
|
||||
|
||||
gjson.ForEachLine(data.JSON.Raw, func(line gjson.Result) bool {
|
||||
result = append(result, decoratedGJSONResult{line})
|
||||
return true
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (data *customAPITemplateData) Subrequest(key string) *customAPIResponseData {
|
||||
req, exists := data.subrequests[key]
|
||||
if !exists {
|
||||
|
@ -190,7 +202,7 @@ func fetchCustomAPIRequest(ctx context.Context, req *CustomAPIRequest) (*customA
|
|||
|
||||
body := strings.TrimSpace(string(bodyBytes))
|
||||
|
||||
if body != "" && !gjson.Valid(body) {
|
||||
if !req.SkipJSONValidation && body != "" && !gjson.Valid(body) {
|
||||
truncatedBody, isTruncated := limitStringLength(body, 100)
|
||||
if isTruncated {
|
||||
truncatedBody += "... <truncated>"
|
||||
|
@ -342,6 +354,23 @@ func (r *decoratedGJSONResult) Bool(key string) bool {
|
|||
return r.Get(key).Bool()
|
||||
}
|
||||
|
||||
func customAPIDoMathOp[T int | float64](a, b T, op string) T {
|
||||
switch op {
|
||||
case "add":
|
||||
return a + b
|
||||
case "sub":
|
||||
return a - b
|
||||
case "mul":
|
||||
return a * b
|
||||
case "div":
|
||||
if b == 0 {
|
||||
return 0
|
||||
}
|
||||
return a / b
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var customAPITemplateFuncs = func() template.FuncMap {
|
||||
var regexpCacheMu sync.Mutex
|
||||
var regexpCache = make(map[string]*regexp.Regexp)
|
||||
|
@ -359,6 +388,31 @@ var customAPITemplateFuncs = func() template.FuncMap {
|
|||
return regex
|
||||
}
|
||||
|
||||
doMathOpWithAny := func(a, b any, op string) any {
|
||||
switch at := a.(type) {
|
||||
case int:
|
||||
switch bt := b.(type) {
|
||||
case int:
|
||||
return customAPIDoMathOp(at, bt, op)
|
||||
case float64:
|
||||
return customAPIDoMathOp(float64(at), bt, op)
|
||||
default:
|
||||
return math.NaN()
|
||||
}
|
||||
case float64:
|
||||
switch bt := b.(type) {
|
||||
case int:
|
||||
return customAPIDoMathOp(at, float64(bt), op)
|
||||
case float64:
|
||||
return customAPIDoMathOp(at, bt, op)
|
||||
default:
|
||||
return math.NaN()
|
||||
}
|
||||
default:
|
||||
return math.NaN()
|
||||
}
|
||||
}
|
||||
|
||||
funcs := template.FuncMap{
|
||||
"toFloat": func(a int) float64 {
|
||||
return float64(a)
|
||||
|
@ -366,21 +420,35 @@ var customAPITemplateFuncs = func() template.FuncMap {
|
|||
"toInt": func(a float64) int {
|
||||
return int(a)
|
||||
},
|
||||
"add": func(a, b float64) float64 {
|
||||
return a + b
|
||||
"add": func(a, b any) any {
|
||||
return doMathOpWithAny(a, b, "add")
|
||||
},
|
||||
"sub": func(a, b float64) float64 {
|
||||
return a - b
|
||||
"sub": func(a, b any) any {
|
||||
return doMathOpWithAny(a, b, "sub")
|
||||
},
|
||||
"mul": func(a, b float64) float64 {
|
||||
return a * b
|
||||
"mul": func(a, b any) any {
|
||||
return doMathOpWithAny(a, b, "mul")
|
||||
},
|
||||
"div": func(a, b float64) float64 {
|
||||
if b == 0 {
|
||||
return math.NaN()
|
||||
"div": func(a, b any) any {
|
||||
return doMathOpWithAny(a, b, "div")
|
||||
},
|
||||
"now": func() time.Time {
|
||||
return time.Now()
|
||||
},
|
||||
"offsetNow": func(offset string) time.Time {
|
||||
d, err := time.ParseDuration(offset)
|
||||
if err != nil {
|
||||
return time.Now()
|
||||
}
|
||||
return time.Now().Add(d)
|
||||
},
|
||||
"duration": func(str string) time.Duration {
|
||||
d, err := time.ParseDuration(str)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return a / b
|
||||
return d
|
||||
},
|
||||
"parseTime": customAPIFuncParseTime,
|
||||
"toRelativeTime": dynamicRelativeTimeAttrs,
|
||||
|
@ -403,6 +471,13 @@ var customAPITemplateFuncs = func() template.FuncMap {
|
|||
"replaceAll": func(old, new, s string) string {
|
||||
return strings.ReplaceAll(s, old, new)
|
||||
},
|
||||
"replaceMatches": func(pattern, replacement, s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return getCachedRegexp(pattern).ReplaceAllString(s, replacement)
|
||||
},
|
||||
"findMatch": func(pattern, s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
|
@ -465,6 +540,21 @@ var customAPITemplateFuncs = func() template.FuncMap {
|
|||
|
||||
return results
|
||||
},
|
||||
"concat": func(items ...string) string {
|
||||
return strings.Join(items, "")
|
||||
},
|
||||
"unique": func(key string, results []decoratedGJSONResult) []decoratedGJSONResult {
|
||||
seen := make(map[string]struct{})
|
||||
out := make([]decoratedGJSONResult, 0, len(results))
|
||||
for _, result := range results {
|
||||
val := result.String(key)
|
||||
if _, ok := seen[val]; !ok {
|
||||
seen[val] = struct{}{}
|
||||
out = append(out, result)
|
||||
}
|
||||
}
|
||||
return out
|
||||
},
|
||||
}
|
||||
|
||||
for key, value := range globalTemplateFunctions {
|
||||
|
|
|
@ -59,6 +59,10 @@ func (widget *extensionWidget) update(ctx context.Context) {
|
|||
widget.Title = extension.Title
|
||||
}
|
||||
|
||||
if widget.TitleURL == "" && extension.TitleURL != "" {
|
||||
widget.TitleURL = extension.TitleURL
|
||||
}
|
||||
|
||||
widget.cachedHTML = widget.renderTemplate(widget, extensionWidgetTemplate)
|
||||
}
|
||||
|
||||
|
@ -70,7 +74,7 @@ type extensionType int
|
|||
|
||||
const (
|
||||
extensionContentHTML extensionType = iota
|
||||
extensionContentUnknown = iota
|
||||
extensionContentUnknown
|
||||
)
|
||||
|
||||
var extensionStringToType = map[string]extensionType{
|
||||
|
@ -79,6 +83,7 @@ var extensionStringToType = map[string]extensionType{
|
|||
|
||||
const (
|
||||
extensionHeaderTitle = "Widget-Title"
|
||||
extensionHeaderTitleURL = "Widget-Title-URL"
|
||||
extensionHeaderContentType = "Widget-Content-Type"
|
||||
extensionHeaderContentFrameless = "Widget-Content-Frameless"
|
||||
)
|
||||
|
@ -93,6 +98,7 @@ type extensionRequestOptions struct {
|
|||
|
||||
type extension struct {
|
||||
Title string
|
||||
TitleURL string
|
||||
Content template.HTML
|
||||
Frameless bool
|
||||
}
|
||||
|
@ -142,6 +148,10 @@ func fetchExtension(options extensionRequestOptions) (extension, error) {
|
|||
extension.Title = response.Header.Get(extensionHeaderTitle)
|
||||
}
|
||||
|
||||
if response.Header.Get(extensionHeaderTitleURL) != "" {
|
||||
extension.TitleURL = response.Header.Get(extensionHeaderTitleURL)
|
||||
}
|
||||
|
||||
contentType, ok := extensionStringToType[response.Header.Get(extensionHeaderContentType)]
|
||||
|
||||
if !ok {
|
||||
|
|
|
@ -79,6 +79,7 @@ type market struct {
|
|||
Name string
|
||||
Currency string
|
||||
Price float64
|
||||
PriceHint int
|
||||
PercentChange float64
|
||||
SvgChartPoints string
|
||||
}
|
||||
|
@ -106,6 +107,7 @@ type marketResponseJson struct {
|
|||
RegularMarketPrice float64 `json:"regularMarketPrice"`
|
||||
ChartPreviousClose float64 `json:"chartPreviousClose"`
|
||||
ShortName string `json:"shortName"`
|
||||
PriceHint int `json:"priceHint"`
|
||||
} `json:"meta"`
|
||||
Indicators struct {
|
||||
Quote []struct {
|
||||
|
@ -152,13 +154,14 @@ func fetchMarketsDataFromYahoo(marketRequests []marketRequest) (marketList, erro
|
|||
continue
|
||||
}
|
||||
|
||||
prices := response.Chart.Result[0].Indicators.Quote[0].Close
|
||||
result := &response.Chart.Result[0]
|
||||
prices := result.Indicators.Quote[0].Close
|
||||
|
||||
if len(prices) > marketChartDays {
|
||||
prices = prices[len(prices)-marketChartDays:]
|
||||
}
|
||||
|
||||
previous := response.Chart.Result[0].Meta.RegularMarketPrice
|
||||
previous := result.Meta.RegularMarketPrice
|
||||
|
||||
if len(prices) >= 2 && prices[len(prices)-2] != 0 {
|
||||
previous = prices[len(prices)-2]
|
||||
|
@ -166,21 +169,22 @@ func fetchMarketsDataFromYahoo(marketRequests []marketRequest) (marketList, erro
|
|||
|
||||
points := svgPolylineCoordsFromYValues(100, 50, maybeCopySliceWithoutZeroValues(prices))
|
||||
|
||||
currency, exists := currencyToSymbol[strings.ToUpper(response.Chart.Result[0].Meta.Currency)]
|
||||
currency, exists := currencyToSymbol[strings.ToUpper(result.Meta.Currency)]
|
||||
if !exists {
|
||||
currency = response.Chart.Result[0].Meta.Currency
|
||||
currency = result.Meta.Currency
|
||||
}
|
||||
|
||||
markets = append(markets, market{
|
||||
marketRequest: marketRequests[i],
|
||||
Price: response.Chart.Result[0].Meta.RegularMarketPrice,
|
||||
Price: result.Meta.RegularMarketPrice,
|
||||
Currency: currency,
|
||||
PriceHint: result.Meta.PriceHint,
|
||||
Name: ternary(marketRequests[i].CustomName == "",
|
||||
response.Chart.Result[0].Meta.ShortName,
|
||||
result.Meta.ShortName,
|
||||
marketRequests[i].CustomName,
|
||||
),
|
||||
PercentChange: percentChange(
|
||||
response.Chart.Result[0].Meta.RegularMarketPrice,
|
||||
result.Meta.RegularMarketPrice,
|
||||
previous,
|
||||
),
|
||||
SvgChartPoints: points,
|
||||
|
|
|
@ -331,6 +331,7 @@ func fetchItemsFromRSSFeeds(requests []rssFeedRequest) (rssFeedItemList, error)
|
|||
|
||||
failed := 0
|
||||
entries := make(rssFeedItemList, 0, len(feeds)*10)
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for i := range feeds {
|
||||
if errs[i] != nil {
|
||||
|
@ -339,7 +340,13 @@ func fetchItemsFromRSSFeeds(requests []rssFeedRequest) (rssFeedItemList, error)
|
|||
continue
|
||||
}
|
||||
|
||||
entries = append(entries, feeds[i]...)
|
||||
for _, item := range feeds[i] {
|
||||
if _, exists := seen[item.Link]; exists {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, item)
|
||||
seen[item.Link] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if failed == len(requests) {
|
||||
|
|
|
@ -196,6 +196,10 @@ func fetchChannelFromTwitchTask(channel string) (twitchChannel, error) {
|
|||
slog.Warn("Failed to parse Twitch stream started at", "error", err, "started_at", streamMetadata.UserOrNull.Stream.StartedAt)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This prevents live channels with 0 viewers from being
|
||||
// incorrectly sorted lower than offline channels
|
||||
result.ViewersCount = -1
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
|
|
@ -181,6 +181,11 @@ func workerPoolDo[I any, O any](job *workerPoolJob[I, O]) ([]O, []error, error)
|
|||
return results, errs, nil
|
||||
}
|
||||
|
||||
if len(job.data) == 1 {
|
||||
results[0], errs[0] = job.task(job.data[0])
|
||||
return results, errs, nil
|
||||
}
|
||||
|
||||
tasksQueue := make(chan *workerPoolTask[I, O])
|
||||
resultsQueue := make(chan *workerPoolTask[I, O])
|
||||
|
||||
|
|
|
@ -227,22 +227,24 @@ func Collect(req *SystemInfoRequest) (*SystemInfo, []error) {
|
|||
}
|
||||
}
|
||||
|
||||
filesystems, err := disk.Partitions(false)
|
||||
if err == nil {
|
||||
for _, fs := range filesystems {
|
||||
mpReq, ok := req.Mountpoints[fs.Mountpoint]
|
||||
addedMountpoints := map[string]struct{}{}
|
||||
addMountpointInfo := func(requestedPath string, mpReq MointpointRequest) {
|
||||
if _, exists := addedMountpoints[requestedPath]; exists {
|
||||
return
|
||||
}
|
||||
|
||||
isHidden := req.HideMountpointsByDefault
|
||||
if ok && mpReq.Hide != nil {
|
||||
if mpReq.Hide != nil {
|
||||
isHidden = *mpReq.Hide
|
||||
}
|
||||
if isHidden {
|
||||
continue
|
||||
return
|
||||
}
|
||||
|
||||
usage, err := disk.Usage(fs.Mountpoint)
|
||||
usage, err := disk.Usage(requestedPath)
|
||||
if err == nil {
|
||||
mpInfo := MountpointInfo{
|
||||
Path: fs.Mountpoint,
|
||||
Path: requestedPath,
|
||||
Name: mpReq.Name,
|
||||
TotalMB: usage.Total / 1024 / 1024,
|
||||
UsedMB: usage.Used / 1024 / 1024,
|
||||
|
@ -250,13 +252,26 @@ func Collect(req *SystemInfoRequest) (*SystemInfo, []error) {
|
|||
}
|
||||
|
||||
info.Mountpoints = append(info.Mountpoints, mpInfo)
|
||||
addedMountpoints[requestedPath] = struct{}{}
|
||||
} else {
|
||||
addErr(fmt.Errorf("getting filesystem usage for %s: %v", fs.Mountpoint, err))
|
||||
addErr(fmt.Errorf("getting filesystem usage for %s: %v", requestedPath, err))
|
||||
}
|
||||
}
|
||||
|
||||
if !req.HideMountpointsByDefault {
|
||||
filesystems, err := disk.Partitions(false)
|
||||
if err == nil {
|
||||
for _, fs := range filesystems {
|
||||
addMountpointInfo(fs.Mountpoint, req.Mountpoints[fs.Mountpoint])
|
||||
}
|
||||
} else {
|
||||
addErr(fmt.Errorf("getting filesystems: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
for mountpoint, mpReq := range req.Mountpoints {
|
||||
addMountpointInfo(mountpoint, mpReq)
|
||||
}
|
||||
|
||||
sort.Slice(info.Mountpoints, func(a, b int) bool {
|
||||
return info.Mountpoints[a].UsedPercent > info.Mountpoints[b].UsedPercent
|
||||
|
|
Loading…
Add table
Reference in a new issue