diff --git a/docs/configuration.md b/docs/configuration.md
index 2ddc862..1f2ad89 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1701,11 +1701,8 @@ Display the status of your Docker containers along with an icon and an optional
```yaml
- type: docker-containers
hide-by-default: false
- readable-names: false
```
-The `readable-names` will try to auto format your container names by capitalizing the first letter and converting `-` and `_` characters to spaces.
-
> [!NOTE]
>
> The widget requires access to `docker.sock`. If you're running Glance inside a container, this can be done by mounting the socket as a volume:
@@ -1730,18 +1727,16 @@ Configuration of the containers is done via labels applied to each container:
glance.description: Movies & shows
```
-Configuration of the containers can also be overridden using `glance.yml`. Containers are specified by their container names, these will take preference over any docker labels that are set:
+Alternatively, you can also define the values within your `glance.yml` via the `containers` property, where the key is the container name and each value is the same as the labels but without the "glance." prefix:
```yaml
- type: docker-containers
- hide-by-default: false
- readable-names: false
- containers: # Alternative to using docker labels
- container_name_1: # This is the actual container name
- title: "Test Container Name"
- description: "test-description"
- url: "127.0.0.1:3011/test"
- icon: "si:jellyfin"
+ containers:
+ container_name_1:
+ title: Container Name
+ description: Description of the container
+ url: https://container.domain.com
+ icon: si:container-icon
hide: false
```
@@ -1796,11 +1791,15 @@ If any of the child containers are down, their status will propagate up to the p
| Name | Type | Required | Default |
| ---- | ---- | -------- | ------- |
| hide-by-default | boolean | no | false |
+| format-container-names | boolean | no | false |
| sock-path | string | no | /var/run/docker.sock |
##### `hide-by-default`
Whether to hide the containers by default. If set to `true` you'll have to manually add a `glance.hide: false` label to each container you want to display. By default all containers will be shown and if you want to hide a specific container you can add a `glance.hide: true` label.
+##### `format-container-names`
+When set to `true`, automatically converts container names such as `container_name_1` into `Container Name 1`.
+
##### `sock-path`
The path to the Docker socket.
diff --git a/internal/glance/templates/docker-containers.html b/internal/glance/templates/docker-containers.html
index 66c79fd..991d66d 100644
--- a/internal/glance/templates/docker-containers.html
+++ b/internal/glance/templates/docker-containers.html
@@ -14,7 +14,7 @@
{{- range .Children }}
{{- if .URL }}
-
{{ .Title }}
+
{{ .Name }}
{{- else }}
-
{{ .Title }}
+
{{ .Name }}
{{- end }}
{{- if .Description }}
{{ .Description }}
diff --git a/internal/glance/widget-docker-containers.go b/internal/glance/widget-docker-containers.go
index 4ba7725..61cb388 100644
--- a/internal/glance/widget-docker-containers.go
+++ b/internal/glance/widget-docker-containers.go
@@ -7,6 +7,7 @@ import (
"html/template"
"net"
"net/http"
+ "net/url"
"sort"
"strings"
"time"
@@ -15,12 +16,14 @@ import (
var dockerContainersWidgetTemplate = mustParseTemplate("docker-containers.html", "widget-base.html")
type dockerContainersWidget struct {
- widgetBase `yaml:",inline"`
- HideByDefault bool `yaml:"hide-by-default"`
- SockPath string `yaml:"sock-path"`
- ReadableNames bool `yaml:"readable-names"`
- Containers dockerContainerList `yaml:"-"`
- ContainerMap map[string]dockerContainerConfig `yaml:"containers,omitempty"`
+ widgetBase `yaml:",inline"`
+ HideByDefault bool `yaml:"hide-by-default"`
+ RunningOnly bool `yaml:"running-only"`
+ Category string `yaml:"category"`
+ SockPath string `yaml:"sock-path"`
+ FormatContainerNames bool `yaml:"format-container-names"`
+ Containers dockerContainerList `yaml:"-"`
+ LabelOverrides map[string]map[string]string `yaml:"containers"`
}
func (widget *dockerContainersWidget) initialize() error {
@@ -34,7 +37,14 @@ func (widget *dockerContainersWidget) initialize() error {
}
func (widget *dockerContainersWidget) update(ctx context.Context) {
- containers, err := fetchDockerContainers(widget.SockPath, widget.HideByDefault, widget.ReadableNames, widget.ContainerMap)
+ containers, err := fetchDockerContainers(
+ widget.SockPath,
+ widget.HideByDefault,
+ widget.Category,
+ widget.RunningOnly,
+ widget.FormatContainerNames,
+ widget.LabelOverrides,
+ )
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
@@ -56,6 +66,7 @@ const (
dockerContainerLabelIcon = "glance.icon"
dockerContainerLabelID = "glance.id"
dockerContainerLabelParent = "glance.parent"
+ dockerContainerLabelCategory = "glance.category"
)
const (
@@ -100,7 +111,7 @@ func (l *dockerContainerLabels) getOrDefault(label, def string) string {
}
type dockerContainer struct {
- Title string
+ Name string
URL string
SameTab bool
Image string
@@ -112,11 +123,6 @@ type dockerContainer struct {
Children dockerContainerList
}
-type dockerContainerConfig struct {
- dockerContainer `yaml:",inline"`
- Hide bool `yaml:"hide,omitempty"`
-}
-
type dockerContainerList []dockerContainer
func (containers dockerContainerList) sortByStateIconThenTitle() {
@@ -127,7 +133,7 @@ func (containers dockerContainerList) sortByStateIconThenTitle() {
return (*p)[containers[a].StateIcon] < (*p)[containers[b].StateIcon]
}
- return strings.ToLower(containers[a].Title) < strings.ToLower(containers[b].Title)
+ return strings.ToLower(containers[a].Name) < strings.ToLower(containers[b].Name)
})
}
@@ -144,17 +150,15 @@ func dockerContainerStateToStateIcon(state string) string {
}
}
-func formatReadableName(name string) string {
- name = strings.NewReplacer("-", " ", "_", " ").Replace(name)
- words := strings.Fields(name)
- for i, word := range words {
- words[i] = strings.Title(word)
- }
- return strings.Join(words, " ")
-}
-
-func fetchDockerContainers(socketPath string, hideByDefault bool, readableNames bool, containerOverrides map[string]dockerContainerConfig) (dockerContainerList, error) {
- containers, err := fetchAllDockerContainersFromSock(socketPath)
+func fetchDockerContainers(
+ socketPath string,
+ hideByDefault bool,
+ category string,
+ runningOnly bool,
+ formatNames bool,
+ labelOverrides map[string]map[string]string,
+) (dockerContainerList, error) {
+ containers, err := fetchDockerContainersFromSource(socketPath, category, runningOnly, labelOverrides)
if err != nil {
return nil, fmt.Errorf("fetching containers: %w", err)
}
@@ -165,48 +169,15 @@ func fetchDockerContainers(socketPath string, hideByDefault bool, readableNames
for i := range containers {
container := &containers[i]
- containerName := ""
- if len(container.Names) > 0 {
- containerName = strings.TrimLeft(container.Names[0], "/")
- }
-
dc := dockerContainer{
- Image: container.Image,
- State: strings.ToLower(container.State),
- StateText: strings.ToLower(container.Status),
- SameTab: stringToBool(container.Labels.getOrDefault(dockerContainerLabelSameTab, "false")),
- }
-
- if override, exists := containerOverrides[containerName]; exists {
- if override.Hide {
- continue
- }
-
- if override.Title != "" {
- dc.Title = override.Title
- } else {
- title := deriveDockerContainerTitle(container)
- if readableNames {
- title = formatReadableName(title)
- }
- dc.Title = title
- }
- dc.URL = override.URL
- dc.Description = override.Description
- if override.Icon != (customIconField{}) {
- dc.Icon = override.Icon
- } else {
- dc.Icon = newCustomIconField(container.Labels.getOrDefault(dockerContainerLabelIcon, "si:docker"))
- }
- } else {
- title := deriveDockerContainerTitle(container)
- if readableNames {
- title = formatReadableName(title)
- }
- dc.Title = title
- dc.URL = container.Labels.getOrDefault(dockerContainerLabelURL, "")
- dc.Description = container.Labels.getOrDefault(dockerContainerLabelDescription, "")
- dc.Icon = newCustomIconField(container.Labels.getOrDefault(dockerContainerLabelIcon, "si:docker"))
+ Name: deriveDockerContainerName(container, formatNames),
+ URL: container.Labels.getOrDefault(dockerContainerLabelURL, ""),
+ Description: container.Labels.getOrDefault(dockerContainerLabelDescription, ""),
+ SameTab: stringToBool(container.Labels.getOrDefault(dockerContainerLabelSameTab, "false")),
+ Image: container.Image,
+ State: strings.ToLower(container.State),
+ StateText: strings.ToLower(container.Status),
+ Icon: newCustomIconField(container.Labels.getOrDefault(dockerContainerLabelIcon, "si:docker")),
}
if idValue := container.Labels.getOrDefault(dockerContainerLabelID, ""); idValue != "" {
@@ -214,7 +185,7 @@ func fetchDockerContainers(socketPath string, hideByDefault bool, readableNames
for i := range children {
child := &children[i]
dc.Children = append(dc.Children, dockerContainer{
- Title: deriveDockerContainerTitle(child),
+ Name: deriveDockerContainerName(child, formatNames),
StateText: child.Status,
StateIcon: dockerContainerStateToStateIcon(strings.ToLower(child.State)),
})
@@ -242,12 +213,31 @@ func fetchDockerContainers(socketPath string, hideByDefault bool, readableNames
return dockerContainers, nil
}
-func deriveDockerContainerTitle(container *dockerContainerJsonResponse) string {
+func deriveDockerContainerName(container *dockerContainerJsonResponse, formatNames bool) string {
if v := container.Labels.getOrDefault(dockerContainerLabelName, ""); v != "" {
return v
}
- return strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, "n/a"), "/")
+ if len(container.Names) == 0 || container.Names[0] == "" {
+ return "n/a"
+ }
+
+ name := strings.TrimLeft(container.Names[0], "/")
+
+ if formatNames {
+ name = strings.ReplaceAll(name, "_", " ")
+ name = strings.ReplaceAll(name, "-", " ")
+
+ words := strings.Split(name, " ")
+ for i := range words {
+ if len(words[i]) > 0 {
+ words[i] = strings.ToUpper(words[i][:1]) + words[i][1:]
+ }
+ }
+ name = strings.Join(words, " ")
+ }
+
+ return name
}
func groupDockerContainerChildren(
@@ -288,17 +278,44 @@ func isDockerContainerHidden(container *dockerContainerJsonResponse, hideByDefau
return hideByDefault
}
-func fetchAllDockerContainersFromSock(socketPath string) ([]dockerContainerJsonResponse, error) {
- client := &http.Client{
- Timeout: 5 * time.Second,
- Transport: &http.Transport{
- DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
- return net.Dial("unix", socketPath)
+func fetchDockerContainersFromSource(
+ source string,
+ category string,
+ runningOnly bool,
+ labelOverrides map[string]map[string]string,
+) ([]dockerContainerJsonResponse, error) {
+ var hostname string
+
+ var client *http.Client
+ if strings.HasPrefix(source, "tcp://") || strings.HasPrefix(source, "http://") {
+ client = &http.Client{}
+ parsed, err := url.Parse(source)
+ if err != nil {
+ return nil, fmt.Errorf("parsing URL: %w", err)
+ }
+
+ port := parsed.Port()
+ if port == "" {
+ port = "80"
+ }
+
+ hostname = parsed.Hostname() + ":" + port
+ } else {
+ hostname = "docker"
+ client = &http.Client{
+ Transport: &http.Transport{
+ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
+ return net.Dial("unix", source)
+ },
},
- },
+ }
}
- request, err := http.NewRequest("GET", "http://docker/containers/json?all=true", nil)
+ fetchAll := ternary(runningOnly, "false", "true")
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ request, err := http.NewRequestWithContext(ctx, "GET", "http://"+hostname+"/containers/json?all="+fetchAll, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
@@ -318,26 +335,43 @@ func fetchAllDockerContainersFromSock(socketPath string) ([]dockerContainerJsonR
return nil, fmt.Errorf("decoding response: %w", err)
}
- return containers, nil
-}
+ for i := range containers {
+ container := &containers[i]
+ name := strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, ""), "/")
-func (widget *dockerContainersWidget) GetContainerNames() ([]string, error) {
- containers, err := fetchAllDockerContainersFromSock(widget.SockPath)
- if err != nil {
- return nil, fmt.Errorf("fetching containers: %w", err)
- }
+ if name == "" {
+ continue
+ }
- names := make([]string, 0, len(containers))
- for _, container := range containers {
- if !isDockerContainerHidden(&container, widget.HideByDefault) {
- // Get the clean container name without the leading '/'
- name := strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, ""), "/")
- if name != "" {
- names = append(names, name)
- }
+ overrides, ok := labelOverrides[name]
+ if !ok {
+ continue
+ }
+
+ if container.Labels == nil {
+ container.Labels = make(dockerContainerLabels)
+ }
+
+ for label, value := range overrides {
+ container.Labels["glance."+label] = value
}
}
- sort.Strings(names)
- return names, nil
+ // We have to filter here instead of using the `filters` parameter of Docker's API
+ // because the user may define a category override within their config
+ if category != "" {
+ filtered := make([]dockerContainerJsonResponse, 0, len(containers))
+
+ for i := range containers {
+ container := &containers[i]
+
+ if container.Labels.getOrDefault(dockerContainerLabelCategory, "") == category {
+ filtered = append(filtered, *container)
+ }
+ }
+
+ containers = filtered
+ }
+
+ return containers, nil
}