Compare commits

...

59 Commits
v0.8.0 ... main

Author SHA1 Message Date
Svilen Markov 6c5b7a3f4c Update docs 2025-12-10 09:44:00 +07:00
Svilen Markov 36d5ae023f
Merge pull request #848 from fullmetalsheep/main
feat(themes): Add theme 'Neon Pink'
2025-10-17 14:18:26 +07:00
fullmetalsheep 478c08f6a7 feat(themes): Add theme 'Neon Pink' 2025-10-17 10:53:25 +07:00
Svilen Markov cae90d16ba Update theme preview 2025-09-28 12:40:33 +07:00
Svilen Markov fbc07bd142
Merge pull request #833 from nicolasluckie/feat/add-shades-of-purple-theme
feat: Add Shades of Purple theme with screenshot
2025-09-28 12:36:36 +07:00
Svilen Markov 4a4d3e1755
Add contrast-multiplier to shades of purple 2025-09-28 12:34:41 +07:00
Svilen Markov f243a4938f Update readme 2025-09-28 12:21:12 +07:00
Svilen Markov 9416de1497 Fix indentation 2025-09-28 11:43:11 +07:00
Nic Luckie 283a5fcfd0 feat: add Shades of Purple theme with screenshot 2025-09-27 22:34:21 +07:00
Svilen Markov c88fd526e5 Merge branch 'dev' 2025-06-10 08:43:26 +07:00
Svilen Markov f0541ea5c8 Refactor icon stuff 2025-06-10 08:25:34 +07:00
Svilen Markov 8f986f1403 Refactor icons field and add mdi 2025-06-10 08:17:38 +07:00
Svilen Markov de9a192ba4 Remove healthcheck #676 2025-06-10 07:45:58 +07:00
Svilen Markov 88d8fa56fb Make auto-invert work with prefixed icons 2025-06-10 07:43:34 +07:00
Svilen Markov 9044e640bc
Merge pull request #699 from septechx/patch-2
Fix typo in docs
2025-06-10 07:13:05 +07:00
Svilen Markov 808f3c1436 Update page.js 2025-06-10 07:11:25 +07:00
Svilen Markov d103c81df1 Fix search shortcut #719 2025-06-10 07:11:09 +07:00
Svilen Markov 429f0be675
Merge pull request #705 from rhijjawi/dev
setupTodos using incorrect logic and skipping to-do item.
2025-06-10 07:11:06 +07:00
Ramzi H 5a093f42b0 setupTodos using incorrect logic and skipping to-do item. 2025-05-27 12:54:22 +07:00
Sep ded435df0e
Fix typo in docs 2025-05-25 23:34:31 +07:00
Svilen Markov e52374fa24 Fix popover triangle misalignment 2025-05-24 14:58:19 +07:00
Svilen Markov b94647efc9
Merge pull request #693 from chenrui333/update-purego
build: bump purego to build against newer go
2025-05-24 05:09:08 +07:00
Rui Chen d512770c10
build: bump purego to build against newer go
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-05-23 23:36:52 +07:00
Svilen Markov ea52318be6
Update FUNDING.yml 2025-05-24 00:03:17 +07:00
Svilen Markov 9e5023522b
Merge pull request #689 from thallada/patch-1
Correct docs for Go's `text/html` built-in template functions
2025-05-22 23:32:33 +07:00
Tyler Hallada 4d0fdd9b11
Correct docs for Go's `text/html` built-in template functions
Source: https://pkg.go.dev/text/template#hdr-Functions

The comparison functions the greater-than-or-equal-to and less-than-or-equal-to are `ge` and `le`, not `gte` and `lte`. I got an error rendering my template before I realized this.
2025-05-22 18:03:45 +07:00
Svilen Markov 78725c8591
Merge pull request #687 from chenkhuaning0816/config
Docs: Add how to get playlist ID in configuration.md
2025-05-22 22:53:48 +07:00
acidburn f7adaad1c5 Docs: Add how to get playlist ID 2025-05-22 20:26:35 +07:00
Svilen Markov ab093cb232 Fix extra whitespace in titles (again) 2025-05-21 09:41:36 +07:00
Svilen Markov 2aaff02db8 Fix for extra whitespace in titles 2025-05-20 16:46:06 +07:00
Svilen Markov f5bfd9d4d1 Merge branch 'dev' 2025-05-19 21:25:48 +07:00
Svilen Markov 9e3639eb54 Update readme 2025-05-19 21:25:33 +07:00
Svilen Markov b4094b28bd Allow disabling theme picker 2025-05-19 21:25:33 +07:00
Svilen Markov b294839b79 Make theme key accessible via CSS 2025-05-19 21:25:33 +07:00
Svilen Markov 14a21de37f Add user agent to test reddit request 2025-05-19 21:23:13 +07:00
Svilen Markov d9239acbce Increase diagnose command timeout 2025-05-19 21:23:13 +07:00
Svilen Markov a2247c0b6c Use default client in diagnose command
defaultHTTPClient may behave differently to http.DefaultClient and is used for almost all widgets, using it within the diagnose
command will make debugging and replicating issues easier
2025-05-19 21:23:13 +07:00
Svilen Markov c1aaec5ffc Add .Options.JSON to custom API 2025-05-19 21:23:13 +07:00
Svilen Markov bcef9fbd61 Simplify implementation of func 2025-05-19 21:23:12 +07:00
Svilen Markov 32db59fda2
Merge pull request #678 from ralphocdol/fix-text-truncate-formatting
Fix text-truncate or related formatting
2025-05-19 21:12:21 +07:00
Svilen Markov 92bc68b61a
Use innerText instead of textContent 2025-05-19 21:11:11 +07:00
Svilen Markov a6382b2e1d
Merge pull request #681 from ralphocdol/update-the-and-or-doc-description
Docs: The 'and' and 'or' accepts more than 2 boolean arguments
2025-05-19 21:07:05 +07:00
Ralph Ocdol 571cdaf618 The 'and' and 'or' accepts more than 2 boolean arguments 2025-05-19 19:46:26 +07:00
Ralph Ocdol aa3b2c3b1b Fix text-truncate or related formatting 2025-05-18 20:33:03 +07:00
Svilen Markov 9bbf73db97
Merge pull request #661 from anant-j/dev
Add Mod operation
2025-05-16 17:33:58 +07:00
Svilen Markov dc950e7ec2
Merge pull request #668 from Xevion/main
Add Anchors to Links to Configuration Documentation within README
2025-05-16 17:30:14 +07:00
Svilen Markov 91ca57e242
Merge pull request #662 from mazzz1y/dev
Add http proxy support
2025-05-16 17:21:53 +07:00
Xevion 95541ef5e1 Use an anchor for links to root configuration 2025-05-15 23:19:54 +07:00
Svilen Markov 1ace129a58 Add safeHTML function 2025-05-15 14:21:59 +07:00
Svilen Markov af04605d7d Remove unused function 2025-05-15 13:57:27 +07:00
Dmitry Rubtsov 0ec9cf4aaa
Add http proxy support 2025-05-15 13:34:17 +07:00
Anant Jain a5b0664b9c Add Mod operation 2025-05-14 22:38:13 +07:00
Svilen Markov c67eb4d2c0 Add startOfDay and endOfDay funcs 2025-05-14 22:05:35 +07:00
Svilen Markov e99ee4f774 Fix server crash when using head-widgets 2025-05-14 22:00:02 +07:00
Svilen Markov 74e6c5c960 Update docs 2025-05-14 01:16:53 +07:00
Svilen Markov f801da88a7 Fix center-vertically not working 2025-05-14 00:48:16 +07:00
Svilen Markov 66dcd1fa34 Don't escape safe URL 2025-05-13 23:44:47 +07:00
Svilen Markov 4f9e48cc17 Remove fallback favicon #653 2025-05-13 22:07:57 +07:00
Svilen Markov 95702a2e53 Fix error when hide-desktop-navigation is true 2025-05-13 20:35:34 +07:00
29 changed files with 341 additions and 264 deletions

@ -1 +1,2 @@
github: [glanceapp]
patreon: glanceapp

@ -1,4 +1,4 @@
FROM golang:1.24.2-alpine3.21 AS builder
FROM golang:1.24.3-alpine3.21 AS builder
WORKDIR /app
COPY . /app
@ -9,8 +9,5 @@ FROM alpine:3.21
WORKDIR /app
COPY --from=builder /app/glance .
HEALTHCHECK --timeout=10s --start-period=60s --interval=60s \
CMD wget --spider -q http://localhost:8080/api/healthz
EXPOSE 8080/tcp
ENTRYPOINT ["/app/glance", "--config", "/app/config/glance.yml"]

@ -3,8 +3,5 @@ FROM alpine:3.21
WORKDIR /app
COPY glance .
HEALTHCHECK --timeout=10s --start-period=60s --interval=60s \
CMD wget --spider -q http://localhost:8080/api/healthz
EXPOSE 8080/tcp
ENTRYPOINT ["/app/glance", "--config", "/app/config/glance.yml"]

@ -1,7 +1,18 @@
<p align="center"><em>What if you could see everything at a...</em></p>
<p align="center"><img src="docs/logo.png"></p>
<h1 align="center">Glance</h1>
<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>
<p align="center">
<a href="#installation">Install</a>
<a href="docs/configuration.md#configuring-glance">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>
<p align="center">A lightweight, highly customizable dashboard that displays<br> your feeds in a beautiful, streamlined interface</p>
![](docs/images/readme-main-image.png)
@ -17,7 +28,7 @@
* Docker containers status
* Server stats
* Custom widgets
* [and many more...](docs/configuration.md)
* [and many more...](docs/configuration.md#configuring-glance)
### Fast and lightweight
* Low memory usage
@ -46,8 +57,7 @@ Easily create your own theme by tweaking a few numbers or choose from one of the
<br>
## Configuration
Configuration is done through YAML files, to learn more about how the layout works, how to add more pages and how to configure widgets, visit the [configuration documentation](docs/configuration.md).
Configuration is done through YAML files, to learn more about how the layout works, how to add more pages and how to configure widgets, visit the [configuration documentation](docs/configuration.md#configuring-glance).
<details>
<summary><strong>Preview example configuration file</strong></summary>
<br>

@ -6,6 +6,7 @@
- [Environment variables](#environment-variables)
- [Other ways of providing tokens/passwords/secrets](#other-ways-of-providing-tokenspasswordssecrets)
- [Including other config files](#including-other-config-files)
- [Icons](#icons)
- [Config schema](#config-schema)
- [Authentication](#authentication)
- [Server](#server)
@ -148,14 +149,14 @@ pages:
columns:
- size: full
widgets:
$include: rss.yml
- $include: rss.yml
- name: News
columns:
- size: full
widgets:
- type: group
widgets:
$include: rss.yml
- $include: rss.yml
- type: reddit
subreddit: news
```
@ -185,6 +186,30 @@ docker run --rm -v ./glance.yml:/app/config/glance.yml glanceapp/glance config:p
This assumes that the config you want to print is in your current working directory and is named `glance.yml`.
## Icons
For widgets which provide you with the ability to specify icons such as the monitor, bookmarks, docker containers, etc, you can use the `icon` property to specify a URL to an image or use icon names from multiple libraries via prefixes:
```yml
icon: si:immich # si for Simple icons https://simpleicons.org/
icon: sh:immich # sh for selfh.st icons https://selfh.st/icons/
icon: di:immich # di for Dashboard icons https://github.com/homarr-labs/dashboard-icons
icon: mdi:camera # mdi for Material Design icons https://pictogrammers.com/library/mdi/
```
> [!NOTE]
>
> The icons are loaded externally and are hosted on `cdn.jsdelivr.net`, if you do not wish to depend on a 3rd party you are free to download the icons individually and host them locally.
Icons from the Simple icons library as well as Material Design icons will automatically invert their color to match your light or dark theme, however you may want to enable this manually for other icons. To do this, you can use the `auto-invert` prefix:
```yaml
icon: auto-invert https://example.com/path/to/icon.png # with a URL
icon: auto-invert sh:glance-dark # with a selfh.st icon
```
This expects the icon to be black and will automatically invert it to white when using a dark theme.
## Config schema
For property descriptions, validation and autocompletion of the config within your IDE, @not-first has kindly created a [schema](https://github.com/not-first/glance-schema). Massive thanks to them for this, go check it out and give them a star!
@ -389,10 +414,12 @@ Example:
```yaml
theme:
# This will be the default theme
background-color: 100 20 10
primary-color: 40 90 40
contrast-multiplier: 1.1
disable-picker: false
presets:
gruvbox-dark:
background-color: 0 0 16
@ -421,6 +448,7 @@ If you don't want to spend time configuring your own theme, there are [several a
| contrast-multiplier | number | no | 1 |
| text-saturation-multiplier | number | no | 1 |
| custom-css-file | string | no | |
| disable-picker | bool | false | |
| presets | object | no | |
#### `light`
@ -466,8 +494,11 @@ theme:
>
> In addition, you can also use the `css-class` property which is available on every widget to set custom class names for individual widgets.
#### `disable-picker`
When set to `true` hides the theme picker and disables the abiltity to switch between themes. All users who previously picked a non-default theme will be switched over to the default theme.
#### `presets`
Define additional theme presets that can be selected from the theme switcher on the page. For each preset, you can specify the same properties as for the default theme, such as `background-color`, `primary-color`, `positive-color`, `negative-color`, `contrast-multiplier`, etc., except for the `custom-css-file` property.
Define additional theme presets that can be selected from the theme picker on the page. For each preset, you can specify the same properties as for the default theme, such as `background-color`, `primary-color`, `positive-color`, `negative-color`, `contrast-multiplier`, etc., except for the `custom-css-file` property.
Example:
@ -866,6 +897,11 @@ A list of playlist IDs:
- PL8mG-RkN2uTxTK4m_Vl2dYR9yE41kRdBg
```
The playlist ID can be found in its link which is in the form of
```
https://www.youtube.com...&list={ID}&...
```
##### `limit`
The maximum number of videos to show.
@ -1951,17 +1987,7 @@ If the monitored service returns an error, the user will be redirected here. If
`icon`
Optional URL to an image which will be used as the icon for the site. Can be an external URL or internal via [server configured assets](#assets-path). You can also directly use [Simple Icons](https://simpleicons.org/) via a `si:` prefix or [Dashboard Icons](https://github.com/walkxcode/dashboard-icons) via a `di:` prefix:
```yaml
icon: si:jellyfin
icon: si:gitea
icon: si:adguard
```
> [!WARNING]
>
> Simple Icons are loaded externally and are hosted on `cdn.jsdelivr.net`, if you do not wish to depend on a 3rd party you are free to download the icons individually and host them locally.
See [Icons](#icons) for more information on how to specify icons.
`timeout`
@ -1990,7 +2016,7 @@ HTTP Basic Authentication credentials for protected sites.
```yaml
basic-auth:
usename: your-username
username: your-username
password: your-password
```
@ -2141,7 +2167,7 @@ Alternatively, you can also define the values within your `glance.yml` via the `
- type: docker-containers
containers:
container_name_1:
title: Container Name
name: Container Name
description: Description of the container
url: https://container.domain.com
icon: si:container-icon
@ -2269,7 +2295,7 @@ Whether to only show running containers. If set to `true` only containers that a
| Name | Description |
| ---- | ----------- |
| glance.name | The name displayed in the UI. If not specified, the name of the container will be used. |
| glance.icon | The icon displayed in the UI. Can be an external URL or an icon prefixed with si:, sh: or di: like with the bookmarks and monitor widgets |
| glance.icon | See [Icons](#icons) for more information on how to specify icons |
| glance.url | The URL that the user will be redirected to when clicking on the container. |
| glance.same-tab | Whether to open the link in the same or a new tab. Default is `false`. |
| glance.description | A short description displayed in the UI. Default is empty. |
@ -2584,17 +2610,7 @@ An array of groups which can optionally have a title and a custom color.
`icon`
URL pointing to an image. You can also directly use [Simple Icons](https://simpleicons.org/) via a `si:` prefix or [Dashboard Icons](https://github.com/walkxcode/dashboard-icons) via a `di:` prefix:
```yaml
icon: si:gmail
icon: si:youtube
icon: si:reddit
```
> [!WARNING]
>
> Simple Icons are loaded externally and are hosted on `cdn.jsdelivr.net`, if you do not wish to depend on a 3rd party you are free to download the icons individually and host them locally.
See [Icons](#icons) for more information on how to specify icons.
`same-tab`

@ -238,7 +238,7 @@ Output:
<div>90</div>
```
Other operations include `add`, `mul`, and `div`.
Other operations include `add`, `mul`, `div` and `mod`.
<hr>
@ -415,6 +415,14 @@ The following functions are available on the `JSON` object:
- `Array(key string) []JSON`: Returns the value of the key as an array of `JSON` objects.
- `Exists(key string) bool`: Returns true if the key exists in the JSON object.
The following functions are available on the `Options` object:
- `StringOr(key string, default string) string`: Returns the value of the key as a string, or the default value if the key does not exist.
- `IntOr(key string, default int) int`: Returns the value of the key as an integer, or the default value if the key does not exist.
- `FloatOr(key string, default float) float`: Returns the value of the key as a float, or the default value if the key does not exist.
- `BoolOr(key string, default bool) bool`: Returns the value of the key as a boolean, or the default value if the key does not exist.
- `JSON(key string) JSON`: Returns the value of the key as a stringified `JSON` object, or throws an error if the key does not exist.
The following helper functions provided by Glance are available:
- `toFloat(i int) float`: Converts an integer to a float.
@ -431,6 +439,7 @@ The following helper functions provided by Glance are available:
- `sub(a, b float) float`: Subtracts two numbers.
- `mul(a, b float) float`: Multiplies two numbers.
- `div(a, b float) float`: Divides two numbers.
- `mod(a, b int) int`: Remainder after dividing a by b (a % b).
- `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.
@ -447,17 +456,19 @@ The following helper functions provided by Glance are available:
- `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.
- `percentChange(current float, previous float) float`: Calculates the percentage change between two numbers.
- `startOfDay(t time.Time) time.Time`: Returns the start of the day for a given time.
- `endOfDay(t time.Time) time.Time`: Returns the end of the day for a given time.
The following helper functions provided by Go's `text/template` are available:
- `eq(a, b any) bool`: Compares two values for equality.
- `ne(a, b any) bool`: Compares two values for inequality.
- `lt(a, b any) bool`: Compares two values for less than.
- `lte(a, b any) bool`: Compares two values for less than or equal to.
- `le(a, b any) bool`: Compares two values for less than or equal to.
- `gt(a, b any) bool`: Compares two values for greater than.
- `gte(a, b any) bool`: Compares two values for greater than or equal to.
- `and(a, b bool) bool`: Returns true if both values are true.
- `or(a, b bool) bool`: Returns true if either value is true.
- `ge(a, b any) bool`: Compares two values for greater than or equal to.
- `and(args ...bool) bool`: Returns true if **all** arguments are true; accepts two or more boolean values.
- `or(args ...bool) bool`: Returns true if **any** argument is true; accepts two or more boolean values.
- `not(a bool) bool`: Returns the opposite of the value.
- `index(a any, b int) any`: Returns the value at the specified index of an array.
- `len(a any) int`: Returns the length of an array.

@ -27,7 +27,7 @@ pages:
channels:
- theprimeagen
- j_blow
- piratesoftware
- giantwaffle
- cohhcarnage
- christitustech
- EJ_SA

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -86,92 +86,92 @@ Pull requests with your page configurations are welcome!
<summary>View config (requires Glance <code>v0.6.0</code> or higher)</summary>
```yaml
- name: Markets
columns:
- size: small
widgets:
- type: markets
title: Indices
markets:
- symbol: SPY
name: S&P 500
- symbol: DX-Y.NYB
name: Dollar Index
- type: markets
title: Crypto
markets:
- symbol: BTC-USD
name: Bitcoin
- symbol: ETH-USD
name: Ethereum
- type: markets
title: Stocks
sort-by: absolute-change
markets:
- symbol: NVDA
name: NVIDIA
- symbol: AAPL
name: Apple
- symbol: MSFT
name: Microsoft
- symbol: GOOGL
name: Google
- symbol: AMD
name: AMD
- symbol: RDDT
name: Reddit
- symbol: AMZN
name: Amazon
- symbol: TSLA
name: Tesla
- symbol: INTC
name: Intel
- symbol: META
name: Meta
- size: full
widgets:
- type: rss
title: News
style: horizontal-cards
feeds:
- url: https://feeds.bloomberg.com/markets/news.rss
title: Bloomberg
- url: https://moxie.foxbusiness.com/google-publisher/markets.xml
title: Fox Business
- url: https://moxie.foxbusiness.com/google-publisher/technology.xml
title: Fox Business
- type: group
widgets:
- type: reddit
show-thumbnails: true
subreddit: technology
- type: reddit
show-thumbnails: true
subreddit: wallstreetbets
- type: videos
style: grid-cards
collapse-after-rows: 3
channels:
- UCvSXMi2LebwJEM1s4bz5IBA # New Money
- UCV6KDgJskWaEckne5aPA0aQ # Graham Stephan
- UCAzhpt9DmG6PnHXjmJTvRGQ # Federal Reserve
- size: small
widgets:
- type: rss
title: News
limit: 30
collapse-after: 13
feeds:
- url: https://www.ft.com/technology?format=rss
title: Financial Times
- url: https://feeds.a.dj.com/rss/RSSMarketsMain.xml
title: Wall Street Journal
- name: Markets
columns:
- size: small
widgets:
- type: markets
title: Indices
markets:
- symbol: SPY
name: S&P 500
- symbol: DX-Y.NYB
name: Dollar Index
- type: markets
title: Crypto
markets:
- symbol: BTC-USD
name: Bitcoin
- symbol: ETH-USD
name: Ethereum
- type: markets
title: Stocks
sort-by: absolute-change
markets:
- symbol: NVDA
name: NVIDIA
- symbol: AAPL
name: Apple
- symbol: MSFT
name: Microsoft
- symbol: GOOGL
name: Google
- symbol: AMD
name: AMD
- symbol: RDDT
name: Reddit
- symbol: AMZN
name: Amazon
- symbol: TSLA
name: Tesla
- symbol: INTC
name: Intel
- symbol: META
name: Meta
- size: full
widgets:
- type: rss
title: News
style: horizontal-cards
feeds:
- url: https://feeds.bloomberg.com/markets/news.rss
title: Bloomberg
- url: https://moxie.foxbusiness.com/google-publisher/markets.xml
title: Fox Business
- url: https://moxie.foxbusiness.com/google-publisher/technology.xml
title: Fox Business
- type: group
widgets:
- type: reddit
show-thumbnails: true
subreddit: technology
- type: reddit
show-thumbnails: true
subreddit: wallstreetbets
- type: videos
style: grid-cards
collapse-after-rows: 3
channels:
- UCvSXMi2LebwJEM1s4bz5IBA # New Money
- UCV6KDgJskWaEckne5aPA0aQ # Graham Stephan
- UCAzhpt9DmG6PnHXjmJTvRGQ # Federal Reserve
- size: small
widgets:
- type: rss
title: News
limit: 30
collapse-after: 13
feeds:
- url: https://www.ft.com/technology?format=rss
title: Financial Times
- url: https://feeds.a.dj.com/rss/RSSMarketsMain.xml
title: Wall Street Journal
```
</details>

@ -93,6 +93,28 @@ theme:
negative-color: 0 100 67
```
### Shades of Purple
![screenshot](images/themes/shades-of-purple.png)
```yaml
theme:
background-color: 243 33 25
contrast-multiplier: 1.2
primary-color: 50 100 49
positive-color: 98 82 71
negative-color: 12 77 52
```
### Neon Pink
![screenshot](images/themes/neon-pink.png)
```yaml
theme:
background-color: 240 27 11
contrast-multiplier: 1.5
primary-color: 321 100 71
positive-color: 165 78 51
negative-color: 360 100 71
```
## Light
### Catppuccin Latte

@ -1,6 +1,6 @@
module github.com/glanceapp/glance
go 1.24.2
go 1.24.3
require (
github.com/fsnotify/fsnotify v1.9.0
@ -15,7 +15,7 @@ require (
require (
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/ebitengine/purego v0.8.4 // 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-20250317134145-8bc96cf8fc35 // indirect

@ -1,7 +1,3 @@
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/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
@ -9,10 +5,8 @@ github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmg
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/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
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/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
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=
@ -20,11 +14,10 @@ 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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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=
@ -40,10 +33,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
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/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -57,12 +46,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
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=
@ -74,8 +59,6 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -92,10 +75,6 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
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/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -119,10 +98,6 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
@ -143,10 +118,6 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

@ -131,52 +131,45 @@ func (d *durationField) UnmarshalYAML(node *yaml.Node) error {
type customIconField struct {
URL template.URL
IsFlatIcon bool
// TODO: along with whether the icon is flat, we also need to know
// whether the icon is black or white by default in order to properly
// invert the color based on the theme being light or dark
AutoInvert bool
}
func newCustomIconField(value string) customIconField {
const autoInvertPrefix = "auto-invert "
field := customIconField{}
if strings.HasPrefix(value, autoInvertPrefix) {
field.AutoInvert = true
value = strings.TrimPrefix(value, autoInvertPrefix)
}
prefix, icon, found := strings.Cut(value, ":")
if !found {
if strings.HasPrefix(value, autoInvertPrefix) {
field.IsFlatIcon = true
value = strings.TrimPrefix(value, autoInvertPrefix)
}
field.URL = template.URL(value)
return field
}
switch prefix {
case "si":
field.URL = template.URL("https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/" + icon + ".svg")
field.IsFlatIcon = true
case "di", "sh":
// syntax: di:<icon_name>[.svg|.png]
// syntax: sh:<icon_name>[.svg|.png]
// if the icon name is specified without extension, it is assumed to be wanting the SVG icon
// otherwise, specify the extension of either .svg or .png to use either of the CDN offerings
// any other extension will be interpreted as .svg
basename, ext, found := strings.Cut(icon, ".")
if !found {
ext = "svg"
basename = icon
}
basename, ext, found := strings.Cut(icon, ".")
if !found {
ext = "svg"
basename = icon
}
if ext != "svg" && ext != "png" {
ext = "svg"
}
if ext != "svg" && ext != "png" {
ext = "svg"
}
if prefix == "di" {
field.URL = template.URL("https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/" + ext + "/" + basename + "." + ext)
} else {
field.URL = template.URL("https://cdn.jsdelivr.net/gh/selfhst/icons/" + ext + "/" + basename + "." + ext)
}
switch prefix {
case "si":
field.AutoInvert = true
field.URL = template.URL("https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/" + basename + ".svg")
case "di":
field.URL = template.URL("https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/" + ext + "/" + basename + "." + ext)
case "mdi":
field.AutoInvert = true
field.URL = template.URL("https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/" + basename + ".svg")
case "sh":
field.URL = template.URL("https://cdn.jsdelivr.net/gh/selfhst/icons/" + ext + "/" + basename + "." + ext)
default:
field.URL = template.URL(value)
}

@ -47,8 +47,10 @@ type config struct {
Theme struct {
themeProperties `yaml:",inline"`
CustomCSSFile string `yaml:"custom-css-file"`
Presets orderedYAMLMap[string, *themeProperties] `yaml:"presets"`
CustomCSSFile string `yaml:"custom-css-file"`
DisablePicker bool `yaml:"disable-picker"`
Presets orderedYAMLMap[string, *themeProperties] `yaml:"presets"`
} `yaml:"theme"`
Branding struct {

@ -12,7 +12,7 @@ import (
"time"
)
const httpTestRequestTimeout = 10 * time.Second
const httpTestRequestTimeout = 15 * time.Second
var diagnosticSteps = []diagnosticStep{
{
@ -75,7 +75,9 @@ var diagnosticSteps = []diagnosticStep{
{
name: "fetch data from Reddit API",
fn: func() (string, error) {
return testHttpRequest("GET", "https://www.reddit.com/search.json", 200)
return testHttpRequestWithHeaders("GET", "https://www.reddit.com/search.json", map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0",
}, 200)
},
},
{
@ -165,7 +167,7 @@ func testHttpRequestWithHeaders(method, url string, headers map[string]string, e
request.Header.Add(key, value)
}
response, err := http.DefaultClient.Do(request)
response, err := defaultHTTPClient.Do(request)
if err != nil {
return "", err
}

@ -101,35 +101,37 @@ func newApplication(c *config) (*application, error) {
// Init themes
//
themeKeys := make([]string, 0, 2)
themeProps := make([]*themeProperties, 0, 2)
defaultDarkTheme, ok := config.Theme.Presets.Get("default-dark")
if ok && !config.Theme.SameAs(defaultDarkTheme) || !config.Theme.SameAs(&themeProperties{}) {
themeKeys = append(themeKeys, "default-dark")
themeProps = append(themeProps, &themeProperties{})
}
themeKeys = append(themeKeys, "default-light")
themeProps = append(themeProps, &themeProperties{
Light: true,
BackgroundColor: &hslColorField{240, 13, 95},
PrimaryColor: &hslColorField{230, 100, 30},
NegativeColor: &hslColorField{0, 70, 50},
ContrastMultiplier: 1.3,
TextSaturationMultiplier: 0.5,
})
if !config.Theme.DisablePicker {
themeKeys := make([]string, 0, 2)
themeProps := make([]*themeProperties, 0, 2)
defaultDarkTheme, ok := config.Theme.Presets.Get("default-dark")
if ok && !config.Theme.SameAs(defaultDarkTheme) || !config.Theme.SameAs(&themeProperties{}) {
themeKeys = append(themeKeys, "default-dark")
themeProps = append(themeProps, &themeProperties{})
}
themePresets, err := newOrderedYAMLMap(themeKeys, themeProps)
if err != nil {
return nil, fmt.Errorf("creating theme presets: %v", err)
}
config.Theme.Presets = *themePresets.Merge(&config.Theme.Presets)
themeKeys = append(themeKeys, "default-light")
themeProps = append(themeProps, &themeProperties{
Light: true,
BackgroundColor: &hslColorField{240, 13, 95},
PrimaryColor: &hslColorField{230, 100, 30},
NegativeColor: &hslColorField{0, 70, 50},
ContrastMultiplier: 1.3,
TextSaturationMultiplier: 0.5,
})
themePresets, err := newOrderedYAMLMap(themeKeys, themeProps)
if err != nil {
return nil, fmt.Errorf("creating theme presets: %v", err)
}
config.Theme.Presets = *themePresets.Merge(&config.Theme.Presets)
for key, properties := range config.Theme.Presets.Items() {
properties.Key = key
if err := properties.init(); err != nil {
return nil, fmt.Errorf("initializing preset theme %s: %v", key, err)
for key, properties := range config.Theme.Presets.Items() {
properties.Key = key
if err := properties.init(); err != nil {
return nil, fmt.Errorf("initializing preset theme %s: %v", key, err)
}
}
}
@ -170,6 +172,12 @@ func newApplication(c *config) (*application, error) {
page.DesktopNavigationWidth = page.Width
}
for i := range page.HeadWidgets {
widget := page.HeadWidgets[i]
app.widgetByID[widget.GetID()] = widget
widget.setProviders(providers)
}
for c := range page.Columns {
column := &page.Columns[c]
@ -180,7 +188,6 @@ func newApplication(c *config) (*application, error) {
for w := range column.Widgets {
widget := column.Widgets[w]
app.widgetByID[widget.GetID()] = widget
widget.setProviders(providers)
}
}
@ -283,11 +290,13 @@ type templateData struct {
func (a *application) populateTemplateRequestData(data *templateRequestData, r *http.Request) {
theme := &a.Config.Theme.themeProperties
selectedTheme, err := r.Cookie("theme")
if err == nil {
preset, exists := a.Config.Theme.Presets.Get(selectedTheme.Value)
if exists {
theme = preset
if !a.Config.Theme.DisablePicker {
selectedTheme, err := r.Cookie("theme")
if err == nil {
preset, exists := a.Config.Theme.Presets.Get(selectedTheme.Value)
if exists {
theme = preset
}
}
}
@ -431,7 +440,11 @@ func (a *application) server() (func() error, func() error) {
mux.HandleFunc("GET /{page}", a.handlePageRequest)
mux.HandleFunc("GET /api/pages/{page}/content/{$}", a.handlePageContentRequest)
mux.HandleFunc("POST /api/set-theme/{key}", a.handleThemeChangeRequest)
if !a.Config.Theme.DisablePicker {
mux.HandleFunc("POST /api/set-theme/{key}", a.handleThemeChangeRequest)
}
mux.HandleFunc("/api/widgets/{widget}/{path...}", a.handleWidgetRequest)
mux.HandleFunc("GET /api/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)

@ -194,7 +194,7 @@ function setupSearchBoxes() {
document.addEventListener("keydown", (event) => {
if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) return;
if (event.key != "s") return;
if (event.code != "KeyS") return;
inputElement.focus();
event.preventDefault();
@ -643,13 +643,14 @@ async function setupCalendars() {
}
async function setupTodos() {
const elems = document.getElementsByClassName("todo");
const elems = Array.from(document.getElementsByClassName("todo"));
if (elems.length == 0) return;
const todo = await import ('./todo.js');
for (let i = 0; i < elems.length; i++)
for (let i = 0; i < elems.length; i++){
todo.default(elems[i]);
}
}
function setupTruncatedElementTitles() {
@ -661,7 +662,8 @@ function setupTruncatedElementTitles() {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (element.getAttribute("title") === null) element.title = element.textContent;
if (element.getAttribute("title") === null)
element.title = element.innerText.trim().replace(/\s+/g, " ");
}
}
@ -683,15 +685,23 @@ async function changeTheme(key, onChanged) {
.appendTo(document.head);
themeStyleElem.html(newThemeStyle);
document.documentElement.setAttribute("data-theme", key);
document.documentElement.setAttribute("data-scheme", response.headers.get("X-Scheme"));
typeof onChanged == "function" && onChanged();
setTimeout(() => { tempStyle.remove(); }, 10);
}
function initThemeSwitcher() {
find(".mobile-navigation .theme-choices").replaceWith(
find(".header-container .theme-choices").cloneNode(true)
);
function initThemePicker() {
const themeChoicesInMobileNav = find(".mobile-navigation .theme-choices");
if (!themeChoicesInMobileNav) return;
const themeChoicesInHeader = find(".header-container .theme-choices");
if (themeChoicesInHeader) {
themeChoicesInHeader.replaceWith(
themeChoicesInMobileNav.cloneNode(true)
);
}
const presetElems = findAll(".theme-choices .theme-preset");
let themePreviewElems = document.getElementsByClassName("current-theme-preview");
@ -734,7 +744,7 @@ function initThemeSwitcher() {
}
async function setupPage() {
initThemeSwitcher();
initThemePicker();
const pageElement = document.getElementById("page");
const pageContentElement = document.getElementById("page-content");

@ -130,7 +130,7 @@ function repositionContainer() {
} else if (left + containerBounds.width > window.innerWidth) {
containerElement.style.removeProperty("left");
containerElement.style.right = 0;
containerElement.style.setProperty("--triangle-offset", containerBounds.width - containerInlinePadding - (window.innerWidth - targetBounds.left - targetBoundsWidthOffset) + -1 + "px");
containerElement.style.setProperty("--triangle-offset", containerBounds.width - containerInlinePadding - (document.documentElement.clientWidth - targetBounds.left - targetBoundsWidthOffset) + -1 + "px");
} else {
containerElement.style.removeProperty("right");
containerElement.style.left = left + "px";

@ -5,7 +5,6 @@ import (
"html/template"
"math"
"strconv"
"strings"
"golang.org/x/text/language"
"golang.org/x/text/message"
@ -22,6 +21,9 @@ var globalTemplateFunctions = template.FuncMap{
"safeURL": func(str string) template.URL {
return template.URL(str)
},
"safeHTML": func(str string) template.HTML {
return template.HTML(str)
},
"absInt": func(i int) int {
return int(math.Abs(float64(i)))
},
@ -54,7 +56,6 @@ var globalTemplateFunctions = template.FuncMap{
return template.HTML(value + ` <span class="color-base size-h5">` + label + `</span>`)
},
"hasPrefix": strings.HasPrefix,
}
func mustParseTemplate(primary string, dependencies ...string) *template.Template {

@ -13,7 +13,7 @@
<div class="flex items-center gap-10">
{{- if ne "" .Icon.URL }}
<div class="bookmarks-icon-container">
<img class="bookmarks-icon{{ if .Icon.IsFlatIcon }} flat-icon{{ end }}" src="{{ .Icon.URL }}" alt="" loading="lazy">
<img class="bookmarks-icon{{ if .Icon.AutoInvert }} flat-icon{{ end }}" src="{{ .Icon.URL }}" alt="" loading="lazy">
</div>
{{- end }}
<a href="{{ .URL | safeURL }}" class="bookmarks-link {{ if .HideArrow }}bookmarks-link-no-arrow {{ end }}color-highlight size-h4" {{ if .Target }}target="{{ .Target }}"{{ end }} rel="noreferrer">{{ .Title }}</a>

@ -5,7 +5,7 @@
{{- range .Containers }}
<li class="docker-container flex items-center gap-15">
<div class="shrink-0" data-popover-type="html" data-popover-position="above" data-popover-offset="0.25" data-popover-margin="0.1rem" data-popover-max-width="400px" aria-hidden="true">
<img class="docker-container-icon{{ if .Icon.IsFlatIcon }} flat-icon{{ end }}" src="{{ .Icon.URL }}" alt="" loading="lazy">
<img class="docker-container-icon{{ if .Icon.AutoInvert }} flat-icon{{ end }}" src="{{ .Icon.URL }}" alt="" loading="lazy">
<div data-popover-html>
<div class="color-highlight text-truncate block">{{ .Image }}</div>
<div>{{ .StateText }}</div>

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" id="top" data-scheme="{{ if .Request.Theme.Light }}light{{ else }}dark{{ end }}">
<html lang="en" id="top" data-theme="{{ .Request.Theme.Key }}" data-scheme="{{ if .Request.Theme.Light }}light{{ else }}dark{{ end }}">
<head>
{{ block "document-head-before" . }}{{ end }}
<script>
@ -21,7 +21,6 @@
<meta name="theme-color" content="{{ .Request.Theme.BackgroundColorAsHex }}">
<link rel="apple-touch-icon" sizes="512x512" href='{{ .App.Config.Branding.AppIconURL }}'>
<link rel="manifest" href='{{ .App.VersionedAssetPath "manifest.json" }}'>
<link rel="icon" type="image/png" href='{{ .App.StaticAssetPath "favicon.png" }}' />
<link rel="icon" type="{{ .App.Config.Branding.FaviconType }}" href="{{ .App.Config.Branding.FaviconURL }}" />
<link rel="stylesheet" href='{{ .App.StaticAssetPath "css/bundle.css" }}'>
<style id="theme-style">{{ .Request.Theme.CSS }}</style>

@ -22,7 +22,7 @@
{{ define "site" }}
{{ if .Icon.URL }}
<img class="monitor-site-icon{{ if .Icon.IsFlatIcon }} flat-icon{{ end }}" src="{{ .Icon.URL }}" alt="" loading="lazy">
<img class="monitor-site-icon{{ if .Icon.AutoInvert }} flat-icon{{ end }}" src="{{ .Icon.URL }}" alt="" loading="lazy">
{{ end }}
<div class="grow min-width-0">
<a class="size-h3 color-highlight text-truncate block" href="{{ .URL | safeURL }}" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>

@ -33,19 +33,16 @@
<nav class="nav flex grow hide-scrollbars">
{{ template "navigation-links" . }}
</nav>
{{ if not .App.Config.Theme.DisablePicker }}
<div class="theme-picker self-center" data-popover-type="html" data-popover-position="below" data-popover-show-delay="0">
<div class="current-theme-preview">
{{ .Request.Theme.PreviewHTML }}
</div>
<div data-popover-html>
<div class="theme-choices">
{{ .App.Config.Theme.PreviewHTML }}
{{ range $_, $preset := .App.Config.Theme.Presets.Items }}
{{ $preset.PreviewHTML }}
{{ end }}
</div>
<div class="theme-choices"></div>
</div>
</div>
{{ end }}
{{- if .App.RequiresAuth }}
<a class="block self-center" href="{{ .App.Config.Server.BaseURL }}/logout" title="Logout">
<svg class="logout-button" stroke="var(--color-text-subdue)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
@ -71,9 +68,15 @@
</div>
<div class="mobile-navigation-actions flex flex-column margin-block-10">
{{ if not .App.Config.Theme.DisablePicker }}
<div class="theme-picker flex justify-between items-center" data-popover-type="html" data-popover-position="above" data-popover-show-delay="0" data-popover-hide-delay="100" data-popover-anchor=".current-theme-preview" data-popover-trigger="click">
<div data-popover-html>
<div class="theme-choices"></div>
<div class="theme-choices">
{{ .App.Config.Theme.PreviewHTML }}
{{ range $_, $preset := .App.Config.Theme.Presets.Items }}
{{ $preset.PreviewHTML }}
{{ end }}
</div>
</div>
<div class="size-h3 pointer-events-none select-none">Change theme</div>
@ -87,6 +90,7 @@
</svg>
</div>
</div>
{{ end }}
{{ if .App.RequiresAuth }}
<a href="{{ .App.Config.Server.BaseURL }}/logout" class="flex justify-between items-center">
@ -100,7 +104,7 @@
</div>
<div class="content-bounds grow{{ if .Page.Width }} content-bounds-{{ .Page.Width }}{{ end }}">
<main class="page{{ if .Page.CenterVertically }} page-center-vertically{{ end }}" id="page" aria-live="polite" aria-busy="true">
<main class="page{{ if .Page.CenterVertically }} center-vertically{{ end }}" id="page" aria-live="polite" aria-busy="true">
<h1 class="visually-hidden">{{ .Page.Title }}</h1>
<div class="page-content" id="page-content"></div>
<div class="page-loading-container">

@ -6,7 +6,7 @@
<li class="flex thumbnail-parent gap-10 items-center">
<img class="video-horizontal-list-thumbnail thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt="">
<div class="min-width-0">
<a class="block text-truncate color-primary-if-not-visited" href="{{ .Url }}" target="_blank" rel="noreferrer">{{ .Title }}</a>
<a class="block text-truncate color-primary-if-not-visited" href="{{ .Url | safeURL }}" target="_blank" rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text flex-nowrap">
<li class="shrink-0" {{ dynamicRelativeTimeAttrs .TimePosted }}></li>
<li class="min-width-0">

@ -108,6 +108,20 @@ func (o *customAPIOptions) BoolOr(key string, defaultValue bool) bool {
return customAPIGetOptionOrDefault(*o, key, defaultValue)
}
func (o *customAPIOptions) JSON(key string) string {
value, exists := (*o)[key]
if !exists {
panic(fmt.Sprintf("key %q does not exist in options", key))
}
encoded, err := json.Marshal(value)
if err != nil {
panic(fmt.Sprintf("marshaling %s: %v", key, err))
}
return string(encoded)
}
func customAPIGetOptionOrDefault[T any](o customAPIOptions, key string, defaultValue T) T {
if value, exists := o[key]; exists {
if typedValue, ok := value.(T); ok {
@ -479,6 +493,12 @@ var customAPITemplateFuncs = func() template.FuncMap {
"div": func(a, b any) any {
return doMathOpWithAny(a, b, "div")
},
"mod": func(a, b int) int {
if b == 0 {
return 0
}
return a % b
},
"now": func() time.Time {
return time.Now()
},
@ -509,6 +529,12 @@ var customAPITemplateFuncs = func() template.FuncMap {
// Shorthand to do both of the above with a single function call
return dynamicRelativeTimeAttrs(customAPIFuncParseTimeInLocation(layout, value, time.UTC))
},
"startOfDay": func(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
},
"endOfDay": func(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location())
},
// The reason we flip the parameter order is so that you can chain multiple calls together like this:
// {{ .JSON.String "foo" | trimPrefix "bar" | doSomethingElse }}
// instead of doing this:

@ -26,6 +26,7 @@ const defaultClientTimeout = 5 * time.Second
var defaultHTTPClient = &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: 10,
Proxy: http.ProxyFromEnvironment,
},
Timeout: defaultClientTimeout,
}
@ -34,6 +35,7 @@ var defaultInsecureHTTPClient = &http.Client{
Timeout: defaultClientTimeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
},
}