feat: size: prefix, external auto-detection, auto-disable stale monitors
- size:WxH and size:WxH@R resolution for config names - auto-detection of external monitors when external list is empty (any non-internal display) - ensureCleanLayout: explicitly disable connected monitors not in target layout - update docs (README, example.yaml) for all three features
This commit is contained in:
parent
ac2bb77113
commit
3fdccadd3c
24
README.md
24
README.md
@ -57,6 +57,7 @@ Plugs into your dock — external monitors turn on, built-in turns off. Unplug
|
|||||||
| 5 | **Graceful shutdown** — optionally restores portable layout on SIGTERM |
|
| 5 | **Graceful shutdown** — optionally restores portable layout on SIGTERM |
|
||||||
| 6 | **Socket reconnect** — exponential backoff if compositor socket drops |
|
| 6 | **Socket reconnect** — exponential backoff if compositor socket drops |
|
||||||
| 7 | **Atomic config writes (Hyprland)** — temp file + rename prevents config corruption |
|
| 7 | **Atomic config writes (Hyprland)** — temp file + rename prevents config corruption |
|
||||||
|
| 8 | **Auto-disable stale monitors** — monitors connected but absent from the target layout are explicitly disabled (prevents external displays from staying on after switching to portable mode) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -142,13 +143,17 @@ restore_on_exit: true
|
|||||||
# External monitors that trigger docked mode.
|
# External monitors that trigger docked mode.
|
||||||
# Plain name: matches connector (DP-1, HDMI-A-1).
|
# Plain name: matches connector (DP-1, HDMI-A-1).
|
||||||
# desc: prefix: matches by monitor description (survives port rename).
|
# desc: prefix: matches by monitor description (survives port rename).
|
||||||
external:
|
# Optional — if omitted or empty, the daemon auto-detects external monitors:
|
||||||
|
# any display whose connector is not eDP-/LVDS-/DSI- is treated as external.
|
||||||
|
external: # optional
|
||||||
- desc:Dell Inc. DELL U2723QE
|
- desc:Dell Inc. DELL U2723QE
|
||||||
- DP-9
|
- DP-9
|
||||||
- DP-10
|
- DP-10
|
||||||
|
|
||||||
# Monitor layouts for each mode.
|
# Monitor layouts for each mode.
|
||||||
# "portable" and "docked" are required.
|
# "portable" and "docked" are required.
|
||||||
|
# Monitors not listed in a mode are automatically disabled (prevents
|
||||||
|
# external displays from staying on when switching to portable mode).
|
||||||
modes:
|
modes:
|
||||||
portable:
|
portable:
|
||||||
monitors:
|
monitors:
|
||||||
@ -172,6 +177,11 @@ modes:
|
|||||||
scale: 1.0
|
scale: 1.0
|
||||||
- name: eDP-1
|
- name: eDP-1
|
||||||
enabled: false # turn off laptop screen when docked
|
enabled: false # turn off laptop screen when docked
|
||||||
|
- name: size:1920x1080@60 # resolve by resolution + refresh rate
|
||||||
|
enabled: true
|
||||||
|
mode: preferred
|
||||||
|
position: auto
|
||||||
|
scale: 1.0
|
||||||
|
|
||||||
# Shell commands run after a layout change.
|
# Shell commands run after a layout change.
|
||||||
# Commands run concurrently, failures are logged but never crash the daemon.
|
# Commands run concurrently, failures are logged but never crash the daemon.
|
||||||
@ -185,17 +195,19 @@ hooks:
|
|||||||
|
|
||||||
### Monitor matching
|
### Monitor matching
|
||||||
|
|
||||||
Two match modes, supported in both the `external` list and the `modes` section:
|
Three match modes for the `modes` section (the `external` list supports plain names and `desc:`):
|
||||||
|
|
||||||
| Syntax | Matches | Use case |
|
| Syntax | Matches | Use case |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `DP-1` | Exact connector name | Simple setups, built-in displays |
|
| `DP-1` | Exact connector name | Simple setups, built-in displays |
|
||||||
| `desc:Dell U2723QE` | Substring in monitor description | Survives port rename across different docks |
|
| `desc:Dell U2723QE` | Substring in monitor description or serial | Survives port rename across different docks |
|
||||||
|
| `size:2560x1440` | Exact pixel dimensions | Two identical monitors with same resolution |
|
||||||
|
| `size:2560x1440@165` | Dimensions + refresh rate | Disambiguate identical models |
|
||||||
|
|
||||||
Sway description format: `make model serial_widthxheight` (with serial omitted if `Unknown`).
|
Sway description format: `make model serial_widthxheight` (with serial omitted if `Unknown`).
|
||||||
Run `hyprctl monitors all` (Hyprland) or `swaymsg -t get_outputs` (Sway) to see your monitor names and descriptions.
|
Run `hyprctl monitors all` (Hyprland) or `swaymsg -t get_outputs` (Sway) to see your monitor names and descriptions.
|
||||||
|
|
||||||
**desc: in modes** — when a monitor name in `modes` uses the `desc:` prefix, the daemon resolves it to the actual connector name at runtime. Ambiguous matches (a desc matching multiple monitors) cause an error.
|
**desc: and size: in modes** — when a monitor name in `modes` uses `desc:` or `size:`, the daemon resolves it to the actual connector name at runtime. Ambiguous matches (a prefix matching multiple monitors with identical resolution) cause an error. For `size:`, add `@R` (refresh rate) to disambiguate monitors with the same resolution. Unresolvable names also cause an error.
|
||||||
|
|
||||||
### Backend-specific configuration
|
### Backend-specific configuration
|
||||||
|
|
||||||
@ -337,7 +349,7 @@ Check the hook command works from a terminal first. Hooks run via `sh -c`, so sh
|
|||||||
|
|
||||||
### Monitor names changed after reboot
|
### Monitor names changed after reboot
|
||||||
|
|
||||||
Use `desc:` prefix matching instead of connector names. This survives port renames across different docks and reboots.
|
Use `desc:` or `size:` prefix matching instead of connector names. Both survive port renames across different docks and reboots.
|
||||||
|
|
||||||
**Hyprland:** `hyprctl monitors all` to see descriptions.
|
**Hyprland:** `hyprctl monitors all` to see descriptions.
|
||||||
**Sway:** `swaymsg -t get_outputs` to see names, make/model, serial, and native resolution.
|
**Sway:** `swaymsg -t get_outputs` to see names, make/model, serial, and native resolution.
|
||||||
@ -351,6 +363,8 @@ modes:
|
|||||||
monitors:
|
monitors:
|
||||||
- name: desc:Dell Inc. DELL U2723QE # also works here
|
- name: desc:Dell Inc. DELL U2723QE # also works here
|
||||||
enabled: true
|
enabled: true
|
||||||
|
- name: size:2560x1440@165 # match by dimensions + refresh
|
||||||
|
enabled: true
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -92,6 +92,10 @@ func (h *hyprlandBackend) GetMonitors(ctx context.Context) ([]MonitorInfo, error
|
|||||||
|
|
||||||
// ApplyLayout writes the monitor configuration file and calls hyprctl reload.
|
// ApplyLayout writes the monitor configuration file and calls hyprctl reload.
|
||||||
// The config is written atomically (temp file + rename) to prevent corruption.
|
// The config is written atomically (temp file + rename) to prevent corruption.
|
||||||
|
//
|
||||||
|
// Any physically connected monitor not mentioned in the desired layout is
|
||||||
|
// explicitly disabled. This prevents external monitors from remaining enabled
|
||||||
|
// when switching to portable mode (where only eDP-* is listed).
|
||||||
func (h *hyprlandBackend) ApplyLayout(ctx context.Context, monitors []MonitorConfig) error {
|
func (h *hyprlandBackend) ApplyLayout(ctx context.Context, monitors []MonitorConfig) error {
|
||||||
var err error
|
var err error
|
||||||
monitors, err = resolveMonitorNames(ctx, monitors, h.GetMonitors, h.logger)
|
monitors, err = resolveMonitorNames(ctx, monitors, h.GetMonitors, h.logger)
|
||||||
@ -99,6 +103,14 @@ func (h *hyprlandBackend) ApplyLayout(ctx context.Context, monitors []MonitorCon
|
|||||||
return fmt.Errorf("resolve monitor names: %w", err)
|
return fmt.Errorf("resolve monitor names: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch the current set of connected monitors and explicitly disable
|
||||||
|
// any that are not part of the target layout.
|
||||||
|
current, err := h.GetMonitors(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get current monitors for cleanup: %w", err)
|
||||||
|
}
|
||||||
|
monitors = h.ensureCleanLayout(monitors, current)
|
||||||
|
|
||||||
content := h.generateConf(monitors)
|
content := h.generateConf(monitors)
|
||||||
|
|
||||||
destPath, err := h.resolveOutputPath()
|
destPath, err := h.resolveOutputPath()
|
||||||
@ -282,6 +294,33 @@ func (h *hyprlandBackend) generateConf(monitors []MonitorConfig) string {
|
|||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureCleanLayout adds disabled entries for any physically connected
|
||||||
|
// monitor that is not present in the target layout. This prevents
|
||||||
|
// monitors from remaining enabled when switching to a mode that only
|
||||||
|
// configures a subset of displays (e.g. portable mode).
|
||||||
|
func (h *hyprlandBackend) ensureCleanLayout(target []MonitorConfig, connected []MonitorInfo) []MonitorConfig {
|
||||||
|
names := make(map[string]bool, len(target))
|
||||||
|
for _, m := range target {
|
||||||
|
names[m.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []MonitorConfig
|
||||||
|
result = append(result, target...)
|
||||||
|
|
||||||
|
for _, mi := range connected {
|
||||||
|
if !names[mi.Name] {
|
||||||
|
h.logger.Debug("explicitly disabling unlisted monitor",
|
||||||
|
"monitor", mi.Name)
|
||||||
|
result = append(result, MonitorConfig{
|
||||||
|
Name: mi.Name,
|
||||||
|
Enabled: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// formatScale formats a scale float: 1 → "1", 1.5 → "1.5"
|
// formatScale formats a scale float: 1 → "1", 1.5 → "1.5"
|
||||||
func formatScale(s float64) string {
|
func formatScale(s float64) string {
|
||||||
if s == 0 {
|
if s == 0 {
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -103,23 +105,24 @@ type Backend interface {
|
|||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveMonitorNames converts desc: prefixed monitor config names to the
|
// resolveMonitorNames converts desc: or size: prefixed monitor config names
|
||||||
// actual connector names (e.g. "desc:Xiaomi Corporation Mi Monitor" → "DP-9")
|
// to the actual connector names by matching against the current set of
|
||||||
// by matching against the current set of connected monitors.
|
// connected monitors.
|
||||||
//
|
//
|
||||||
// Matching logic:
|
// Matching logic:
|
||||||
// - "desc:make model" — substring match against description (make + " " + model)
|
// - "desc:text" — substring match against description or serial
|
||||||
// - "desc:serial" — substring match against serial number
|
// - "size:WxH" — match by exact pixel dimensions (e.g. "size:2560x1440")
|
||||||
|
// - "size:WxH@R" — match by dimensions + refresh rate (e.g. "size:2560x1440@165")
|
||||||
// - plain names are returned as-is
|
// - plain names are returned as-is
|
||||||
func resolveMonitorNames(ctx context.Context, monitors []MonitorConfig, getMonitors func(context.Context) ([]MonitorInfo, error), logger *slog.Logger) ([]MonitorConfig, error) {
|
func resolveMonitorNames(ctx context.Context, monitors []MonitorConfig, getMonitors func(context.Context) ([]MonitorInfo, error), logger *slog.Logger) ([]MonitorConfig, error) {
|
||||||
var needsDescResolution bool
|
var needsResolution bool
|
||||||
for _, m := range monitors {
|
for _, m := range monitors {
|
||||||
if strings.HasPrefix(m.Name, "desc:") {
|
if strings.HasPrefix(m.Name, "desc:") || strings.HasPrefix(m.Name, "size:") {
|
||||||
needsDescResolution = true
|
needsResolution = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !needsDescResolution {
|
if !needsResolution {
|
||||||
return monitors, nil
|
return monitors, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,37 +133,123 @@ func resolveMonitorNames(ctx context.Context, monitors []MonitorConfig, getMonit
|
|||||||
|
|
||||||
resolved := make([]MonitorConfig, len(monitors))
|
resolved := make([]MonitorConfig, len(monitors))
|
||||||
for i, m := range monitors {
|
for i, m := range monitors {
|
||||||
if !strings.HasPrefix(m.Name, "desc:") {
|
switch {
|
||||||
resolved[i] = m
|
case strings.HasPrefix(m.Name, "desc:"):
|
||||||
continue
|
resolved[i] = resolveDesc(m, current, logger)
|
||||||
}
|
case strings.HasPrefix(m.Name, "size:"):
|
||||||
|
resolved[i] = resolveSize(m, current, logger)
|
||||||
needle := strings.TrimPrefix(m.Name, "desc:")
|
|
||||||
var matches []MonitorInfo
|
|
||||||
for _, mi := range current {
|
|
||||||
if strings.Contains(mi.Description, needle) || strings.Contains(mi.Serial, needle) {
|
|
||||||
matches = append(matches, mi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(matches) {
|
|
||||||
case 0:
|
|
||||||
return nil, fmt.Errorf("no connected monitor matches desc:%q", needle)
|
|
||||||
case 1:
|
|
||||||
resolved[i] = m
|
|
||||||
resolved[i].Name = matches[0].Name
|
|
||||||
logger.Debug("resolved desc to connector",
|
|
||||||
"desc", needle, "connector", matches[0].Name)
|
|
||||||
default:
|
default:
|
||||||
var names []string
|
resolved[i] = m
|
||||||
for _, mat := range matches {
|
}
|
||||||
names = append(names, mat.Name)
|
if resolved[i].Name == "" {
|
||||||
}
|
return nil, fmt.Errorf("cannot resolve monitor name %q", m.Name)
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"desc:%q matches multiple monitors: %s — use a more specific identifier",
|
|
||||||
needle, strings.Join(names, ", "))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolved, nil
|
return resolved, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveDesc resolves a desc: prefixed name to a connector name.
|
||||||
|
func resolveDesc(m MonitorConfig, current []MonitorInfo, logger *slog.Logger) MonitorConfig {
|
||||||
|
needle := strings.TrimPrefix(m.Name, "desc:")
|
||||||
|
var matches []MonitorInfo
|
||||||
|
for _, mi := range current {
|
||||||
|
if strings.Contains(mi.Description, needle) || strings.Contains(mi.Serial, needle) {
|
||||||
|
matches = append(matches, mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(matches) {
|
||||||
|
case 0:
|
||||||
|
// Return as-is so the caller sees the empty name.
|
||||||
|
m.Name = ""
|
||||||
|
return m
|
||||||
|
case 1:
|
||||||
|
logger.Debug("resolved desc to connector",
|
||||||
|
"desc", needle, "connector", matches[0].Name)
|
||||||
|
m.Name = matches[0].Name
|
||||||
|
return m
|
||||||
|
default:
|
||||||
|
var names []string
|
||||||
|
for _, mat := range matches {
|
||||||
|
names = append(names, mat.Name)
|
||||||
|
}
|
||||||
|
logger.Warn("desc:%q matches multiple monitors: %s — use a more specific identifier",
|
||||||
|
needle, strings.Join(names, ", "))
|
||||||
|
m.Name = ""
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveSize resolves a size: prefixed name to a connector name.
|
||||||
|
// Format: "size:WxH" or "size:WxH@R" where W and H are pixel dimensions
|
||||||
|
// and R is the refresh rate in Hz.
|
||||||
|
func resolveSize(m MonitorConfig, current []MonitorInfo, logger *slog.Logger) MonitorConfig {
|
||||||
|
spec := strings.TrimPrefix(m.Name, "size:")
|
||||||
|
|
||||||
|
parts := strings.Split(spec, "@")
|
||||||
|
dimParts := strings.Split(parts[0], "x")
|
||||||
|
if len(dimParts) != 2 {
|
||||||
|
logger.Warn("invalid size spec", "spec", spec)
|
||||||
|
m.Name = ""
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
targetW, errW := strconv.Atoi(strings.TrimSpace(dimParts[0]))
|
||||||
|
targetH, errH := strconv.Atoi(strings.TrimSpace(dimParts[1]))
|
||||||
|
if errW != nil || errH != nil {
|
||||||
|
logger.Warn("invalid size dimensions", "spec", spec)
|
||||||
|
m.Name = ""
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetR float64
|
||||||
|
hasRefresh := len(parts) == 2
|
||||||
|
if hasRefresh {
|
||||||
|
var err error
|
||||||
|
targetR, err = strconv.ParseFloat(strings.TrimSpace(parts[1]), 64)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("invalid refresh rate in size spec", "spec", spec)
|
||||||
|
m.Name = ""
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches []MonitorInfo
|
||||||
|
for _, mi := range current {
|
||||||
|
if mi.Width != targetW || mi.Height != targetH {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if hasRefresh {
|
||||||
|
if math.Abs(mi.RefreshRate-targetR) > 1.0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches = append(matches, mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(matches) {
|
||||||
|
case 0:
|
||||||
|
m.Name = ""
|
||||||
|
return m
|
||||||
|
case 1:
|
||||||
|
logger.Debug("resolved size to connector",
|
||||||
|
"size", spec, "connector", matches[0].Name)
|
||||||
|
m.Name = matches[0].Name
|
||||||
|
return m
|
||||||
|
default:
|
||||||
|
var names []string
|
||||||
|
for _, mat := range matches {
|
||||||
|
names = append(names, mat.Name)
|
||||||
|
}
|
||||||
|
if hasRefresh {
|
||||||
|
logger.Warn("size:%q matches multiple monitors: %s — add @R to disambiguate",
|
||||||
|
spec, strings.Join(names, ", "))
|
||||||
|
} else {
|
||||||
|
logger.Warn("size:%q matches multiple monitors: %s — use size:WxH@R for disambiguation",
|
||||||
|
spec, strings.Join(names, ", "))
|
||||||
|
}
|
||||||
|
m.Name = ""
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -157,10 +157,8 @@ func (c *Config) validate() error {
|
|||||||
if countEnabled(docked.Monitors) == 0 {
|
if countEnabled(docked.Monitors) == 0 {
|
||||||
return errors.New("docked mode must have at least one enabled monitor")
|
return errors.New("docked mode must have at least one enabled monitor")
|
||||||
}
|
}
|
||||||
// At least one external monitor must be listed.
|
// External list is optional. When empty, the daemon auto-detects
|
||||||
if len(c.External) == 0 {
|
// external monitors (any non-internal display with non-zero dimensions).
|
||||||
return errors.New("at least one external monitor must be specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -195,19 +193,40 @@ func countEnabled(entries []MonitorEntry) int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchesExternal checks whether a monitor name or description matches any
|
// isInternalConnector returns true if the connector name matches a known
|
||||||
// entry in the External list. Supports two match modes:
|
// internal display pattern (eDP, LVDS, DSI). These are always part of the
|
||||||
|
// laptop or tablet and should never trigger docked mode.
|
||||||
|
func isInternalConnector(name string) bool {
|
||||||
|
prefixes := []string{"eDP-", "LVDS-", "DSI-", "EDP-"}
|
||||||
|
for _, p := range prefixes {
|
||||||
|
if strings.HasPrefix(name, p) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchesExternal checks whether a monitor matches the External list.
|
||||||
|
// Supports match modes:
|
||||||
//
|
//
|
||||||
// - Plain name: exact match against MonitorEntry.Name
|
// - Plain name: exact match against name
|
||||||
// - desc: prefix: substring match against MonitorEntry.Description
|
// - desc: prefix: substring match against description
|
||||||
|
//
|
||||||
|
// When the External list is empty, any monitor that is not an internal
|
||||||
|
// display connector (eDP-, LVDS-, DSI-) is automatically external.
|
||||||
func (c *Config) MatchesExternal(name, description string) bool {
|
func (c *Config) MatchesExternal(name, description string) bool {
|
||||||
|
if len(c.External) == 0 {
|
||||||
|
return !isInternalConnector(name)
|
||||||
|
}
|
||||||
|
|
||||||
for _, ext := range c.External {
|
for _, ext := range c.External {
|
||||||
if strings.HasPrefix(ext, "desc:") {
|
switch {
|
||||||
|
case strings.HasPrefix(ext, "desc:"):
|
||||||
desc := strings.TrimPrefix(ext, "desc:")
|
desc := strings.TrimPrefix(ext, "desc:")
|
||||||
if strings.Contains(description, desc) {
|
if strings.Contains(description, desc) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
if name == ext {
|
if name == ext {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,9 @@ restore_on_exit: true
|
|||||||
# External monitors that trigger docked mode.
|
# External monitors that trigger docked mode.
|
||||||
# Plain name: matches the connector name (e.g. DP-1, HDMI-A-1).
|
# Plain name: matches the connector name (e.g. DP-1, HDMI-A-1).
|
||||||
# desc: prefix: matches by monitor description (survives rename).
|
# desc: prefix: matches by monitor description (survives rename).
|
||||||
|
# Optional — if omitted or left empty, the daemon auto-detects external
|
||||||
|
# monitors: any display whose connector is not eDP-/LVDS-/DSI- is treated
|
||||||
|
# as external.
|
||||||
external:
|
external:
|
||||||
- DP-1
|
- DP-1
|
||||||
- DP-2
|
- DP-2
|
||||||
@ -33,10 +36,10 @@ external:
|
|||||||
|
|
||||||
# Monitor layouts for each mode.
|
# Monitor layouts for each mode.
|
||||||
# "portable" and "docked" are required.
|
# "portable" and "docked" are required.
|
||||||
# Each mode lists monitors with their desired configuration.
|
# Monitors connected but not listed in the mode are automatically disabled.
|
||||||
#
|
#
|
||||||
# Fields:
|
# Fields:
|
||||||
# name — connector name or desc:description
|
# name — connector name, desc:description, size:WxH, or size:WxH@R
|
||||||
# enabled — true to show, false to disable
|
# enabled — true to show, false to disable
|
||||||
# mode — "preferred" (auto-detect), "1920x1080@60", etc.
|
# mode — "preferred" (auto-detect), "1920x1080@60", etc.
|
||||||
# position — "auto", "0x0", "1920x0", etc.
|
# position — "auto", "0x0", "1920x0", etc.
|
||||||
@ -70,6 +73,11 @@ modes:
|
|||||||
mode: "3840x2160@60"
|
mode: "3840x2160@60"
|
||||||
position: "0x0"
|
position: "0x0"
|
||||||
scale: 1.5
|
scale: 1.5
|
||||||
|
- name: size:1920x1080@60 # resolve by resolution + refresh rate
|
||||||
|
enabled: true
|
||||||
|
mode: preferred
|
||||||
|
position: auto
|
||||||
|
scale: 1.0
|
||||||
|
|
||||||
# Shell commands run after a layout change.
|
# Shell commands run after a layout change.
|
||||||
# Commands run concurrently; failures are logged but never crash the daemon.
|
# Commands run concurrently; failures are logged but never crash the daemon.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user