diff --git a/Dockerfile b/Dockerfile index 4d8cd87..0c4cc63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index cf64d2f..ca16c49 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@
What if you could see everything at a...
Install • Configuration • Preconfigured pages • Themes • Discord
+Install • Configuration • Discord • Sponsor
+Community widgets • Preconfigured pages • Themes
 @@ -194,6 +195,7 @@ services: glance: container_name: glance image: glanceapp/glance + restart: unless-stopped volumes: - ./config:/app/config ports: @@ -264,59 +266,31 @@ Glance can also be installed through the following 3rd party channels:{{ .JSON.String "text" }}
+{{ (.Subrequest "another-one").JSON.String "text" }}
+``` + +The subrequests support all the same properties as the main request, except for `subrequests` itself, so you can use `headers`, `parameters`, etc. + +`(.Subrequest "key")` can be a little cumbersome to write, so you can define a variable to make it easier: + +```yaml + template: | + {{ $anotherOne := .Subrequest "another-one" }} +{{ $anotherOne.JSON.String "text" }}
+``` + +You can also access the `.Response` property of a subrequest as you would with the main request: + +```yaml + template: | + {{ $anotherOne := .Subrequest "another-one" }} +{{ $anotherOne.Response.StatusCode }}
+``` + +> [!NOTE] +> +> Setting this property will override any query parameters that are already in the URL. + +```yaml +parameters: + param1: value1 + param2: + - item1 + - item2 +``` ### Extension Display a widget provided by an external source (3rd party). If you want to learn more about developing extensions, checkout the [extensions documentation](extensions.md) (WIP). @@ -1330,6 +1417,7 @@ Display a widget provided by an external source (3rd party). If you want to lear | url | string | yes | | | fallback-content-type | string | no | | | allow-potentially-dangerous-html | boolean | no | false | +| headers | key & value | no | | | parameters | key & value | no | | ##### `url` @@ -1338,6 +1426,14 @@ The URL of the extension. **Note that the query gets stripped from this URL and ##### `fallback-content-type` Optionally specify the fallback content type of the extension if the URL does not return a valid `Widget-Content-Type` header. Currently the only supported value for this property is `html`. +##### `headers` +Optionally specify the headers that will be sent with the request. Example: + +```yaml +headers: + x-api-key: ${SECRET_KEY} +``` + ##### `allow-potentially-dangerous-html` Whether to allow the extension to display HTML. @@ -1608,7 +1704,7 @@ services: glance: image: glanceapp/glance environment: - - GITHUB_TOKEN:{{ .String "name" }} is {{ .Int "age" }} years old
+{{ end }} +``` + +Output: + +```html +Steve is 30 years old
+Alex is 25 years old
+John is 35 years old
+``` + +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" }} +{{ .String "" }}
+{{ end }} +``` + +Output: + +```html +Steve
+Alex
+John
+``` + ## Functions The following functions are available on the `JSON` object: @@ -273,12 +373,31 @@ 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. ``. +- `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. - `sub(a, b float) float`: Subtracts two numbers. - `mul(a, b float) float`: Multiplies two numbers. - `div(a, b float) float`: Divides two numbers. - `formatApproxNumber(n int) string`: Formats a number to be more human-readable, e.g. 1000 -> 1k. - `formatNumber(n float|int) string`: Formats a number with commas, e.g. 1000 -> 1,000. +- `trimPrefix(prefix string, str string) string`: Trims the prefix from a string. +- `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: diff --git a/docs/extensions.md b/docs/extensions.md index b1fa4fa..b6719c1 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -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. diff --git a/docs/glance.yml b/docs/glance.yml index 92b4de1..35dc7cb 100644 --- a/docs/glance.yml +++ b/docs/glance.yml @@ -66,9 +66,6 @@ pages: # hide-location: true - type: markets - # The link to go to when clicking on the symbol in the UI, - # {SYMBOL} will be substituded with the symbol for each market - symbol-link-template: https://www.tradingview.com/symbols/{SYMBOL}/news markets: - symbol: SPY name: S&P 500 diff --git a/docs/images/themes/dracula.png b/docs/images/themes/dracula.png new file mode 100644 index 0000000..8dba452 Binary files /dev/null and b/docs/images/themes/dracula.png differ diff --git a/docs/images/themes/gruvbox.png b/docs/images/themes/gruvbox.png new file mode 100644 index 0000000..2e5b7a9 Binary files /dev/null and b/docs/images/themes/gruvbox.png differ diff --git a/docs/themes.md b/docs/themes.md index b4185db..fdc10b2 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -53,6 +53,16 @@ theme: primary-color: 97 13 80 ``` +### Gruvbox Dark + +```yaml +theme: + background-color: 0 0 16 + primary-color: 43 59 81 + positive-color: 61 66 44 + negative-color: 6 96 59 +``` + ### Kanagawa Dark  ```yaml @@ -72,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 diff --git a/go.mod b/go.mod index 0ded337..4c19477 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 97af31d..9a79559 100644 --- a/go.sum +++ b/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= diff --git a/internal/glance/cli.go b/internal/glance/cli.go index e231706..f5a16fb 100644 --- a/internal/glance/cli.go +++ b/internal/glance/cli.go @@ -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 +} diff --git a/internal/glance/config-fields.go b/internal/glance/config-fields.go index f3c836e..e2ece3f 100644 --- a/internal/glance/config-fields.go +++ b/internal/glance/config-fields.go @@ -219,3 +219,58 @@ func (p *proxyOptionsField) UnmarshalYAML(node *yaml.Node) error { return nil } + +type queryParametersField map[string][]string + +func (q *queryParametersField) UnmarshalYAML(node *yaml.Node) error { + var decoded map[string]any + + if err := node.Decode(&decoded); err != nil { + return err + } + + *q = make(queryParametersField) + + // TODO: refactor the duplication in the switch cases if any more types get added + for key, value := range decoded { + switch v := value.(type) { + case string: + (*q)[key] = []string{v} + case int, int8, int16, int32, int64, float32, float64: + (*q)[key] = []string{fmt.Sprintf("%v", v)} + case bool: + (*q)[key] = []string{fmt.Sprintf("%t", v)} + case []string: + (*q)[key] = append((*q)[key], v...) + case []any: + for _, item := range v { + switch item := item.(type) { + case string: + (*q)[key] = append((*q)[key], item) + case int, int8, int16, int32, int64, float32, float64: + (*q)[key] = append((*q)[key], fmt.Sprintf("%v", item)) + case bool: + (*q)[key] = append((*q)[key], fmt.Sprintf("%t", item)) + default: + return fmt.Errorf("invalid query parameter value type: %T", item) + } + } + default: + return fmt.Errorf("invalid query parameter value type: %T", value) + } + } + + return nil +} + +func (q *queryParametersField) toQueryString() string { + query := url.Values{} + + for key, values := range *q { + for _, value := range values { + query.Add(key, value) + } + } + + return query.Encode() +} diff --git a/internal/glance/diagnose.go b/internal/glance/diagnose.go index 892aa5f..1ee1bc3 100644 --- a/internal/glance/diagnose.go +++ b/internal/glance/diagnose.go @@ -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) }, }, { @@ -103,7 +105,7 @@ func runDiagnostic() { fmt.Println("Glance version: " + buildVersion) fmt.Println("Go version: " + runtime.Version()) fmt.Printf("Platform: %s / %s / %d CPUs\n", runtime.GOOS, runtime.GOARCH, runtime.NumCPU()) - fmt.Println("In Docker container: " + boolToString(isRunningInsideDockerContainer(), "yes", "no")) + fmt.Println("In Docker container: " + ternary(isRunningInsideDockerContainer(), "yes", "no")) fmt.Printf("\nChecking network connectivity, this may take up to %d seconds...\n\n", int(httpTestRequestTimeout.Seconds())) @@ -129,7 +131,7 @@ func runDiagnostic() { fmt.Printf( "%s %s %s| %dms\n", - boolToString(step.err == nil, "✓ Can", "✗ Can't"), + ternary(step.err == nil, "✓ Can", "✗ Can't"), step.name, extraInfo, step.elapsed.Milliseconds(), diff --git a/internal/glance/glance.go b/internal/glance/glance.go index b1fcc37..8fb3e40 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -68,7 +68,7 @@ func newApplication(config *config) (*application, error) { for w := range column.Widgets { widget := column.Widgets[w] - app.widgetByID[widget.id()] = widget + app.widgetByID[widget.GetID()] = widget widget.setProviders(providers) } diff --git a/internal/glance/main.go b/internal/glance/main.go index baac315..67a980c 100644 --- a/internal/glance/main.go +++ b/internal/glance/main.go @@ -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() } diff --git a/internal/glance/static/js/main.js b/internal/glance/static/js/main.js index a10804e..41d2ae3 100644 --- a/internal/glance/static/js/main.js +++ b/internal/glance/static/js/main.js @@ -284,7 +284,9 @@ function setupGroups() { for (let i = 0; i < titles.length; i++) { titles[i].classList.remove("widget-group-title-current"); + titles[i].setAttribute("aria-selected", "false"); tabs[i].classList.remove("widget-group-content-current"); + tabs[i].setAttribute("aria-hidden", "true"); } if (current < t) { @@ -296,7 +298,9 @@ function setupGroups() { current = t; title.classList.add("widget-group-title-current"); + title.setAttribute("aria-selected", "true"); tabs[t].classList.add("widget-group-content-current"); + tabs[t].setAttribute("aria-hidden", "false"); }); } } @@ -645,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; } } @@ -670,6 +674,7 @@ async function setupPage() { setupLazyImages(); } finally { pageElement.classList.add("content-ready"); + pageElement.setAttribute("aria-busy", "false"); for (let i = 0; i < contentReadyCallbacks.length; i++) { contentReadyCallbacks[i](); diff --git a/internal/glance/static/main.css b/internal/glance/static/main.css index a271d4a..2975a73 100644 --- a/internal/glance/static/main.css +++ b/internal/glance/static/main.css @@ -110,7 +110,7 @@ .visited-indicator:not(.text-truncate)::after, .visited-indicator.text-truncate::before, .bookmarks-link:not(.bookmarks-link-no-arrow)::after { - content: '↗'; + content: '↗' / ""; margin-left: 0.5em; display: inline-block; position: relative; @@ -189,7 +189,7 @@ } .expand-toggle-button-icon::before { - content: ''; + content: '' / ""; font-size: 0.8rem; transform: rotate(90deg); line-height: 1; @@ -341,6 +341,19 @@ html, body, .body-content { height: 100%; } +h1, h2, h3, h4, h5 { + font: inherit; +} + +.visually-hidden { + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + a { text-decoration: none; color: inherit; @@ -539,6 +552,10 @@ kbd:active { z-index: 1; } +.summary::-webkit-details-marker { + display: none; +} + .details[open] .summary { margin-bottom: .8rem; } @@ -563,7 +580,7 @@ kbd:active { } .summary::after { - content: "◀"; + content: "◀" / ""; font-size: 1.2em; position: absolute; top: 0; @@ -822,7 +839,7 @@ details[open] .summary::after { } .list-horizontal-text > *:not(:last-child)::after { - content: '•'; + content: '•' / ""; color: var(--color-text-subdue); margin: 0 0.4rem; position: relative; @@ -1115,7 +1132,7 @@ details[open] .summary::after { .calendar-date { padding: 0.4rem 0; - color: var(--color-text-paragraph); + color: var(--color-text-base); position: relative; border-radius: var(--border-radius); background: none; @@ -1256,6 +1273,7 @@ details[open] .summary::after { .dns-stats-graph-bar > .blocked { background-color: var(--color-negative); + flex-basis: calc(var(--percent) - 1px); } .dns-stats-graph-column:nth-child(even) .dns-stats-graph-time { @@ -1821,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; @@ -1967,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; @@ -2020,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; } diff --git a/internal/glance/templates.go b/internal/glance/templates.go index ed83842..699772d 100644 --- a/internal/glance/templates.go +++ b/internal/glance/templates.go @@ -27,9 +27,10 @@ var globalTemplateFunctions = template.FuncMap{ "formatPrice": func(price float64) string { return intl.Sprintf("%.2f", price) }, - "dynamicRelativeTimeAttrs": func(t interface{ Unix() int64 }) template.HTMLAttr { - return template.HTMLAttr(`data-dynamic-relative-time="` + strconv.FormatInt(t.Unix(), 10) + `"`) + "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 var label string @@ -81,3 +82,7 @@ func formatApproxNumber(count int) string { return strconv.FormatFloat(float64(count)/1_000_000, 'f', 1, 64) + "m" } + +func dynamicRelativeTimeAttrs(t interface{ Unix() int64 }) template.HTMLAttr { + return template.HTMLAttr(`data-dynamic-relative-time="` + strconv.FormatInt(t.Unix(), 10) + `"`) +} diff --git a/internal/glance/templates/dns-stats.html b/internal/glance/templates/dns-stats.html index 8128edf..bb4222c 100644 --- a/internal/glance/templates/dns-stats.html +++ b/internal/glance/templates/dns-stats.html @@ -59,8 +59,8 @@ {{ if ne $column.Queries $column.Blocked }} {{ end }} - {{ if or (gt $column.Blocked 0) (and (lt $column.PercentTotal 15) (lt $column.PercentBlocked 10)) }} - + {{ if gt $column.PercentBlocked 0 }} + {{ end }} {{ end }} @@ -76,7 +76,7 @@