feat: support named switchable layouts
This commit is contained in:
@@ -231,8 +231,10 @@ to bypass that boundary or request elevation through a manifest.
|
|||||||
### Configuration reload
|
### Configuration reload
|
||||||
|
|
||||||
The YAML decoder rejects unknown fields. Semantic validation checks the schema
|
The YAML decoder rejects unknown fields. Semantic validation checks the schema
|
||||||
version, gap range, unique monitor entries, zone bounds, hotkey action syntax,
|
version, gap range, active-layout existence, unique layout-name/monitor pairs,
|
||||||
and required values.
|
zone bounds, hotkey action syntax, and required values. Legacy files in which
|
||||||
|
all layouts are unnamed and `active_layout` is absent are normalized to a
|
||||||
|
single layout name of `default` before validation.
|
||||||
|
|
||||||
The message-loop timer checks the configuration modification time. A valid new
|
The message-loop timer checks the configuration modification time. A valid new
|
||||||
configuration replaces the active one and re-registers hotkeys. If parsing,
|
configuration replaces the active one and re-registers hotkeys. If parsing,
|
||||||
|
|||||||
@@ -79,15 +79,17 @@ Here is a complete two-column configuration:
|
|||||||
version: 1
|
version: 1
|
||||||
gap: 8
|
gap: 8
|
||||||
shift_drag: true
|
shift_drag: true
|
||||||
|
active_layout: columns
|
||||||
|
|
||||||
overlay:
|
overlay:
|
||||||
enabled: true
|
enabled: true
|
||||||
color: "#00AEEF"
|
color: "#00AEEF"
|
||||||
opacity: 90
|
opacity: 90
|
||||||
border_width: 3
|
border_width: 10
|
||||||
|
|
||||||
layouts:
|
layouts:
|
||||||
- monitor: "*"
|
- name: columns
|
||||||
|
monitor: "*"
|
||||||
zones:
|
zones:
|
||||||
- name: left
|
- name: left
|
||||||
x: 0
|
x: 0
|
||||||
@@ -100,6 +102,15 @@ layouts:
|
|||||||
width: 50
|
width: 50
|
||||||
height: 100
|
height: 100
|
||||||
|
|
||||||
|
- name: fullscreen
|
||||||
|
monitor: "*"
|
||||||
|
zones:
|
||||||
|
- name: full
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: 100
|
||||||
|
height: 100
|
||||||
|
|
||||||
excluded_apps:
|
excluded_apps:
|
||||||
- mstsc.exe
|
- mstsc.exe
|
||||||
|
|
||||||
@@ -126,6 +137,7 @@ use.
|
|||||||
| `version` | Configuration format. This must currently be `1`. |
|
| `version` | Configuration format. This must currently be `1`. |
|
||||||
| `gap` | Inward spacing, in pixels, applied to every edge of every zone. Valid range: `0`–`500`. |
|
| `gap` | Inward spacing, in pixels, applied to every edge of every zone. Valid range: `0`–`500`. |
|
||||||
| `shift_drag` | Enables or disables mouse snapping when Shift is held at the end of a drag. |
|
| `shift_drag` | Enables or disables mouse snapping when Shift is held at the end of a drag. |
|
||||||
|
| `active_layout` | Name of the layout currently used for snapping and the overlay. |
|
||||||
| `overlay` | Controls the transparent zone guide shown during Shift-dragging. |
|
| `overlay` | Controls the transparent zone guide shown during Shift-dragging. |
|
||||||
| `layouts` | One or more monitor layouts containing zones. |
|
| `layouts` | One or more monitor layouts containing zones. |
|
||||||
| `excluded_apps` | Applications FancyWin must not move. May be empty. |
|
| `excluded_apps` | Applications FancyWin must not move. May be empty. |
|
||||||
@@ -144,7 +156,7 @@ overlay:
|
|||||||
enabled: true
|
enabled: true
|
||||||
color: "#00AEEF"
|
color: "#00AEEF"
|
||||||
opacity: 90
|
opacity: 90
|
||||||
border_width: 3
|
border_width: 10
|
||||||
```
|
```
|
||||||
|
|
||||||
- `enabled` turns the guide on or off.
|
- `enabled` turns the guide on or off.
|
||||||
@@ -182,7 +194,32 @@ For example, the bottom-right quarter is:
|
|||||||
Zones may overlap. If the pointer is inside more than one zone, the first
|
Zones may overlap. If the pointer is inside more than one zone, the first
|
||||||
matching zone listed in the file is selected.
|
matching zone listed in the file is selected.
|
||||||
|
|
||||||
### Monitor-specific layouts
|
### Named layouts and monitor-specific variants
|
||||||
|
|
||||||
|
Every entry under `layouts` has a `name`. Set `active_layout` to that name to
|
||||||
|
choose which layout FancyWin uses:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
active_layout: focus
|
||||||
|
|
||||||
|
layouts:
|
||||||
|
- name: focus
|
||||||
|
monitor: "*"
|
||||||
|
zones:
|
||||||
|
- { name: left, x: 0, y: 0, width: 25, height: 100 }
|
||||||
|
- { name: middle, x: 25, y: 0, width: 50, height: 100 }
|
||||||
|
- { name: right, x: 75, y: 0, width: 25, height: 100 }
|
||||||
|
|
||||||
|
- name: equal-thirds
|
||||||
|
monitor: "*"
|
||||||
|
zones:
|
||||||
|
- { name: left, x: 0, y: 0, width: 33.333, height: 100 }
|
||||||
|
- { name: middle, x: 33.333, y: 0, width: 33.334, height: 100 }
|
||||||
|
- { name: right, x: 66.667, y: 0, width: 33.333, height: 100 }
|
||||||
|
```
|
||||||
|
|
||||||
|
Changing `active_layout` to `equal-thirds` is applied automatically within
|
||||||
|
about two seconds; FancyWin does not need to be restarted.
|
||||||
|
|
||||||
Use `monitor: "*"` as the fallback layout for every display that does not have
|
Use `monitor: "*"` as the fallback layout for every display that does not have
|
||||||
a specific entry. At startup, FancyWin prints detected device names such as
|
a specific entry. At startup, FancyWin prints detected device names such as
|
||||||
@@ -192,18 +229,25 @@ A specific monitor layout can override the fallback:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
layouts:
|
layouts:
|
||||||
- monitor: "*"
|
- name: focus
|
||||||
|
monitor: "*"
|
||||||
zones:
|
zones:
|
||||||
- { name: left, x: 0, y: 0, width: 50, height: 100 }
|
- { name: left, x: 0, y: 0, width: 50, height: 100 }
|
||||||
- { name: right, x: 50, y: 0, width: 50, height: 100 }
|
- { name: right, x: 50, y: 0, width: 50, height: 100 }
|
||||||
|
|
||||||
- monitor: '\\.\DISPLAY2'
|
- name: focus
|
||||||
|
monitor: '\\.\DISPLAY2'
|
||||||
zones:
|
zones:
|
||||||
- { name: main, x: 0, y: 0, width: 70, height: 100 }
|
- { name: main, x: 0, y: 0, width: 70, height: 100 }
|
||||||
- { name: side, x: 70, y: 0, width: 30, height: 100 }
|
- { name: side, x: 70, y: 0, width: 30, height: 100 }
|
||||||
```
|
```
|
||||||
|
|
||||||
Only one layout may be declared for each monitor name.
|
The monitor-specific entry must use the same layout name as its fallback. Only
|
||||||
|
one entry may be declared for each combination of layout name and monitor name.
|
||||||
|
Names and monitor matching are not case-sensitive.
|
||||||
|
|
||||||
|
Older configurations containing no `active_layout` and no layout names are
|
||||||
|
loaded as one backwards-compatible layout named `default`.
|
||||||
|
|
||||||
### Hotkeys
|
### Hotkeys
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "0.2.1"
|
const version = "0.3.0"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
exe, err := os.Executable()
|
exe, err := os.Executable()
|
||||||
|
|||||||
+15
-3
@@ -3,16 +3,18 @@
|
|||||||
version: 1
|
version: 1
|
||||||
gap: 8
|
gap: 8
|
||||||
shift_drag: true
|
shift_drag: true
|
||||||
|
active_layout: focus
|
||||||
|
|
||||||
overlay:
|
overlay:
|
||||||
enabled: true
|
enabled: true
|
||||||
color: "#00AEEF"
|
color: "#00AEEF"
|
||||||
opacity: 90
|
opacity: 90
|
||||||
border_width: 3
|
border_width: 10
|
||||||
|
|
||||||
layouts:
|
layouts:
|
||||||
# The wildcard is used for any monitor without a more specific entry.
|
# The wildcard is used for any monitor without a more specific entry.
|
||||||
- monitor: "*"
|
- name: focus
|
||||||
|
monitor: "*"
|
||||||
zones:
|
zones:
|
||||||
- name: left
|
- name: left
|
||||||
x: 0
|
x: 0
|
||||||
@@ -30,9 +32,19 @@ layouts:
|
|||||||
width: 25
|
width: 25
|
||||||
height: 100
|
height: 100
|
||||||
|
|
||||||
|
# Other layouts remain available by name. Change active_layout above to
|
||||||
|
# switch layouts; a running FancyWin instance reloads the change.
|
||||||
|
- name: equal-thirds
|
||||||
|
monitor: "*"
|
||||||
|
zones:
|
||||||
|
- { name: left, x: 0, y: 0, width: 33.333, height: 100 }
|
||||||
|
- { name: middle, x: 33.333, y: 0, width: 33.334, height: 100 }
|
||||||
|
- { name: right, x: 66.667, y: 0, width: 33.333, height: 100 }
|
||||||
|
|
||||||
# Optional per-display override. Get the name from Windows Display Settings
|
# Optional per-display override. Get the name from Windows Display Settings
|
||||||
# or the startup diagnostics, then uncomment and edit this block.
|
# or the startup diagnostics, then uncomment and edit this block.
|
||||||
# - monitor: '\\.\DISPLAY2'
|
# - name: focus
|
||||||
|
# monitor: '\\.\DISPLAY2'
|
||||||
# zones:
|
# zones:
|
||||||
# - { name: main, x: 0, y: 0, width: 70, height: 100 }
|
# - { name: main, x: 0, y: 0, width: 70, height: 100 }
|
||||||
# - { name: side, x: 70, y: 0, width: 30, height: 100 }
|
# - { name: side, x: 70, y: 0, width: 30, height: 100 }
|
||||||
|
|||||||
+48
-13
@@ -12,13 +12,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Version int `yaml:"version"`
|
Version int `yaml:"version"`
|
||||||
Gap int `yaml:"gap"`
|
Gap int `yaml:"gap"`
|
||||||
ShiftDrag bool `yaml:"shift_drag"`
|
ShiftDrag bool `yaml:"shift_drag"`
|
||||||
Overlay Overlay `yaml:"overlay"`
|
ActiveLayout string `yaml:"active_layout"`
|
||||||
Layouts []Layout `yaml:"layouts"`
|
Overlay Overlay `yaml:"overlay"`
|
||||||
Excluded []string `yaml:"excluded_apps"`
|
Layouts []Layout `yaml:"layouts"`
|
||||||
Hotkeys []Hotkey `yaml:"hotkeys"`
|
Excluded []string `yaml:"excluded_apps"`
|
||||||
|
Hotkeys []Hotkey `yaml:"hotkeys"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Overlay struct {
|
type Overlay struct {
|
||||||
@@ -29,6 +30,7 @@ type Overlay struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Layout struct {
|
type Layout struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
Monitor string `yaml:"monitor"`
|
Monitor string `yaml:"monitor"`
|
||||||
Zones []Zone `yaml:"zones"`
|
Zones []Zone `yaml:"zones"`
|
||||||
}
|
}
|
||||||
@@ -49,8 +51,9 @@ type Hotkey struct {
|
|||||||
func Default() Config {
|
func Default() Config {
|
||||||
return Config{
|
return Config{
|
||||||
Version: 1, Gap: 8, ShiftDrag: true,
|
Version: 1, Gap: 8, ShiftDrag: true,
|
||||||
Overlay: Overlay{Enabled: true, Color: "#00AEEF", Opacity: 90, BorderWidth: 3},
|
ActiveLayout: "columns",
|
||||||
Layouts: []Layout{{Monitor: "*", Zones: []Zone{
|
Overlay: Overlay{Enabled: true, Color: "#00AEEF", Opacity: 90, BorderWidth: 10},
|
||||||
|
Layouts: []Layout{{Name: "columns", Monitor: "*", Zones: []Zone{
|
||||||
{Name: "left", X: 0, Y: 0, Width: 50, Height: 100},
|
{Name: "left", X: 0, Y: 0, Width: 50, Height: 100},
|
||||||
{Name: "right", X: 50, Y: 0, Width: 50, Height: 100},
|
{Name: "right", X: 50, Y: 0, Width: 50, Height: 100},
|
||||||
}}},
|
}}},
|
||||||
@@ -73,6 +76,7 @@ func Load(path string) (Config, error) {
|
|||||||
if err := d.Decode(&c); err != nil {
|
if err := d.Decode(&c); err != nil {
|
||||||
return Config{}, fmt.Errorf("parse YAML: %w", err)
|
return Config{}, fmt.Errorf("parse YAML: %w", err)
|
||||||
}
|
}
|
||||||
|
c.upgradeLegacyLayouts()
|
||||||
if err := c.Validate(); err != nil {
|
if err := c.Validate(); err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
@@ -100,15 +104,27 @@ func (c Config) Validate() error {
|
|||||||
if len(c.Layouts) == 0 {
|
if len(c.Layouts) == 0 {
|
||||||
return errors.New("at least one layout is required")
|
return errors.New("at least one layout is required")
|
||||||
}
|
}
|
||||||
|
active := strings.TrimSpace(c.ActiveLayout)
|
||||||
|
if active == "" {
|
||||||
|
return errors.New("active_layout is required")
|
||||||
|
}
|
||||||
seenMonitors := map[string]bool{}
|
seenMonitors := map[string]bool{}
|
||||||
|
activeExists := false
|
||||||
for li, l := range c.Layouts {
|
for li, l := range c.Layouts {
|
||||||
|
layoutName := strings.TrimSpace(l.Name)
|
||||||
|
if layoutName == "" {
|
||||||
|
return fmt.Errorf("layouts[%d].name is required", li)
|
||||||
|
}
|
||||||
|
if strings.EqualFold(layoutName, active) {
|
||||||
|
activeExists = true
|
||||||
|
}
|
||||||
name := strings.TrimSpace(l.Monitor)
|
name := strings.TrimSpace(l.Monitor)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("layouts[%d].monitor is required", li)
|
return fmt.Errorf("layouts[%d].monitor is required", li)
|
||||||
}
|
}
|
||||||
key := strings.ToLower(name)
|
key := strings.ToLower(layoutName) + "\x00" + strings.ToLower(name)
|
||||||
if seenMonitors[key] {
|
if seenMonitors[key] {
|
||||||
return fmt.Errorf("duplicate layout for monitor %q", name)
|
return fmt.Errorf("duplicate layout named %q for monitor %q", layoutName, name)
|
||||||
}
|
}
|
||||||
seenMonitors[key] = true
|
seenMonitors[key] = true
|
||||||
if len(l.Zones) == 0 {
|
if len(l.Zones) == 0 {
|
||||||
@@ -120,6 +136,9 @@ func (c Config) Validate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !activeExists {
|
||||||
|
return fmt.Errorf("active_layout %q does not match any layout name", c.ActiveLayout)
|
||||||
|
}
|
||||||
for i, h := range c.Hotkeys {
|
for i, h := range c.Hotkeys {
|
||||||
a := strings.ToLower(strings.TrimSpace(h.Action))
|
a := strings.ToLower(strings.TrimSpace(h.Action))
|
||||||
if a != "next_zone" && a != "previous_zone" && !strings.HasPrefix(a, "zone_") {
|
if a != "next_zone" && a != "previous_zone" && !strings.HasPrefix(a, "zone_") {
|
||||||
@@ -138,6 +157,21 @@ func (c Config) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) upgradeLegacyLayouts() {
|
||||||
|
if strings.TrimSpace(c.ActiveLayout) != "" || len(c.Layouts) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, l := range c.Layouts {
|
||||||
|
if strings.TrimSpace(l.Name) != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.ActiveLayout = "default"
|
||||||
|
for i := range c.Layouts {
|
||||||
|
c.Layouts[i].Name = "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validHexColor(s string) bool {
|
func validHexColor(s string) bool {
|
||||||
if len(s) != 7 || s[0] != '#' {
|
if len(s) != 7 || s[0] != '#' {
|
||||||
return false
|
return false
|
||||||
@@ -151,14 +185,15 @@ func DefaultPath(exePath string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) LayoutFor(device string) (Layout, bool) {
|
func (c Config) LayoutFor(device string) (Layout, bool) {
|
||||||
|
active := strings.TrimSpace(c.ActiveLayout)
|
||||||
for _, l := range c.Layouts {
|
for _, l := range c.Layouts {
|
||||||
monitor := strings.TrimSpace(l.Monitor)
|
monitor := strings.TrimSpace(l.Monitor)
|
||||||
if monitor != "*" && strings.EqualFold(monitor, strings.TrimSpace(device)) {
|
if strings.EqualFold(strings.TrimSpace(l.Name), active) && monitor != "*" && strings.EqualFold(monitor, strings.TrimSpace(device)) {
|
||||||
return l, true
|
return l, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, l := range c.Layouts {
|
for _, l := range c.Layouts {
|
||||||
if strings.TrimSpace(l.Monitor) == "*" {
|
if strings.EqualFold(strings.TrimSpace(l.Name), active) && strings.TrimSpace(l.Monitor) == "*" {
|
||||||
return l, true
|
return l, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestDefaultValid(t *testing.T) {
|
func TestDefaultValid(t *testing.T) {
|
||||||
if err := Default().Validate(); err != nil {
|
if err := Default().Validate(); err != nil {
|
||||||
@@ -15,6 +19,51 @@ func TestLayoutFallback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestActiveLayoutSelection(t *testing.T) {
|
||||||
|
c := Default()
|
||||||
|
c.Layouts = append(c.Layouts, Layout{Name: "wide", Monitor: "*", Zones: []Zone{{Name: "wide", Width: 100, Height: 100}}})
|
||||||
|
c.ActiveLayout = "wide"
|
||||||
|
got, ok := c.LayoutFor(`\\.\DISPLAY1`)
|
||||||
|
if !ok || got.Name != "wide" || len(got.Zones) != 1 {
|
||||||
|
t.Fatalf("wrong active layout: %+v, %v", got, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLegacyLayoutUpgrade(t *testing.T) {
|
||||||
|
c := Default()
|
||||||
|
c.ActiveLayout = ""
|
||||||
|
for i := range c.Layouts {
|
||||||
|
c.Layouts[i].Name = ""
|
||||||
|
}
|
||||||
|
c.upgradeLegacyLayouts()
|
||||||
|
if c.ActiveLayout != "default" || c.Layouts[0].Name != "default" {
|
||||||
|
t.Fatalf("legacy layout was not upgraded: %+v", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadLegacyUnnamedLayout(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "legacy.yaml")
|
||||||
|
data := []byte("version: 1\ngap: 8\nshift_drag: true\nlayouts:\n - monitor: '*'\n zones:\n - {x: 0, y: 0, width: 100, height: 100}\n")
|
||||||
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
c, err := Load(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c.ActiveLayout != "default" || c.Layouts[0].Name != "default" {
|
||||||
|
t.Fatalf("legacy file was not upgraded: %+v", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMissingActiveLayoutFails(t *testing.T) {
|
||||||
|
c := Default()
|
||||||
|
c.ActiveLayout = "missing"
|
||||||
|
if err := c.Validate(); err == nil {
|
||||||
|
t.Fatal("expected missing active layout to fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExcluded(t *testing.T) {
|
func TestExcluded(t *testing.T) {
|
||||||
c := Default()
|
c := Default()
|
||||||
c.Excluded = []string{"notepad", "exact.exe"}
|
c.Excluded = []string{"notepad", "exact.exe"}
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ func Run(configPath string, cfg config.Config, debug bool) error {
|
|||||||
}
|
}
|
||||||
defer procKillTimer.Call(0, shiftTimer)
|
defer procKillTimer.Call(0, shiftTimer)
|
||||||
|
|
||||||
log.Printf("ready: Shift+drag snapping=%v, overlay=%v, %d hotkeys", cfg.ShiftDrag, cfg.Overlay.Enabled, len(e.hotkeys))
|
log.Printf("ready: layout=%q, Shift+drag snapping=%v, overlay=%v, %d hotkeys", cfg.ActiveLayout, cfg.ShiftDrag, cfg.Overlay.Enabled, len(e.hotkeys))
|
||||||
logMonitors()
|
logMonitors()
|
||||||
var msg message
|
var msg message
|
||||||
for {
|
for {
|
||||||
@@ -419,7 +419,7 @@ func (e *engine) snapAtCursor(hwnd windows.Handle) (bool, string) {
|
|||||||
e.mu.RUnlock()
|
e.mu.RUnlock()
|
||||||
l, ok := cfg.LayoutFor(mi.device())
|
l, ok := cfg.LayoutFor(mi.device())
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, fmt.Sprintf("no layout matches monitor %s and no wildcard layout exists", mi.device())
|
return false, fmt.Sprintf("active layout %q has no entry for monitor %s and no wildcard fallback", cfg.ActiveLayout, mi.device())
|
||||||
}
|
}
|
||||||
work := toLayoutRect(mi.Work)
|
work := toLayoutRect(mi.Work)
|
||||||
zi := layout.ZoneAt(work, l.Zones, 0, layout.Point{X: p.X, Y: p.Y})
|
zi := layout.ZoneAt(work, l.Zones, 0, layout.Point{X: p.X, Y: p.Y})
|
||||||
@@ -690,5 +690,5 @@ func (e *engine) reloadIfChanged() {
|
|||||||
_ = e.registerHotkeys()
|
_ = e.registerHotkeys()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("configuration reloaded")
|
log.Printf("configuration reloaded: active layout=%q", cfg.ActiveLayout)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user