Skip to content

Commit d26ccdd

Browse files
author
Jonas Falck
committed
Add support for rate
Also fixed a few other things: - bugs in active mode detection with my display (144hz and 60hz) - Turn off changed displays in a smarter way. Im using usb-c daisychain and had trouble with the current approach. it disabled my screen as soon as the screen went into hibernate because xrandr stopped detecting the display. - some autoformatting of comments and code according to efficient go guidelines - if xrand (ApplyRule) fails, try 3 times with a little more sleep each time. This also solved issues with my daisychained displays. Fixes fd0#29
1 parent 176988a commit d26ccdd

File tree

8 files changed

+167
-99
lines changed

8 files changed

+167
-99
lines changed

cmd_apply.go

+26-9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/exec"
88
"strings"
9+
"time"
910
)
1011

1112
type CmdApply struct{}
@@ -37,23 +38,39 @@ func ApplyRule(outputs Outputs, rule Rule) error {
3738
return fmt.Errorf("no output configuration for rule %v", rule.Name)
3839
}
3940

40-
after := append(globalOpts.cfg.ExecuteAfter, rule.ExecuteAfter...)
41-
if len(after) > 0 {
42-
for _, cmd := range after {
43-
cmds = append(cmds, exec.Command("sh", "-c", cmd))
44-
}
45-
}
46-
4741
if err != nil {
4842
return err
4943
}
44+
45+
foundError := false
5046
for _, cmd := range cmds {
51-
err = RunCommand(cmd)
52-
if err != nil {
47+
for i := 0; i < 4; i++ {
48+
err = RunCommand(cmd)
49+
if err == nil {
50+
break
51+
}
5352
fmt.Fprintf(os.Stderr, "executing command for rule %v failed: %v\n", rule.Name, err)
53+
54+
dur := time.Millisecond * 500 * time.Duration(i)
55+
fmt.Fprintf(os.Stderr, "trying again in %s", dur)
56+
time.Sleep(dur)
5457
}
58+
if err != nil {
59+
fmt.Fprint(os.Stderr, "failed after 3 retries")
60+
foundError = true
61+
}
62+
}
63+
if foundError {
64+
return nil // Dont run ExecuteAfter if xrandr commands failed
5565
}
5666

67+
after := append(globalOpts.cfg.ExecuteAfter, rule.ExecuteAfter...)
68+
for _, cmd := range after {
69+
err = RunCommand(exec.Command("sh", "-c", cmd))
70+
if err != nil {
71+
fmt.Fprintf(os.Stderr, "executing command for rule %v failed: %v\n", rule.Name, err)
72+
}
73+
}
5774
return nil
5875
}
5976

cmd_watch.go

+9-56
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ func (cmd CmdWatch) Execute(args []string) (err error) {
100100
var eventReceived bool
101101

102102
var lastRule Rule
103-
var lastOutputs Outputs
104103
for {
105104
if !disablePoll {
106105
var outputs Outputs
@@ -117,53 +116,6 @@ func (cmd CmdWatch) Execute(args []string) (err error) {
117116
return fmt.Errorf("detecting outputs: %w", err)
118117
}
119118

120-
// disable outputs which have a changed display
121-
var off Outputs
122-
for _, o := range outputs {
123-
for _, last := range lastOutputs {
124-
if o.Name != last.Name {
125-
continue
126-
}
127-
128-
if last.Active() && !o.Active() {
129-
V(" output %v: monitor not active any more, disabling", o.Name)
130-
off = append(off, o)
131-
continue
132-
}
133-
134-
if o.Active() && o.MonitorId != last.MonitorId {
135-
V(" output %v: monitor has changed, disabling", o.Name)
136-
off = append(off, o)
137-
continue
138-
}
139-
}
140-
}
141-
142-
if len(off) > 0 {
143-
V("disable %d outputs", len(off))
144-
145-
cmd, err := DisableOutputs(off)
146-
if err != nil {
147-
return fmt.Errorf("disabling outputs: %w", err)
148-
}
149-
150-
// forget the last rule set, something has changed for sure
151-
lastRule = Rule{}
152-
153-
err = RunCommand(cmd)
154-
if err != nil {
155-
fmt.Fprintf(os.Stderr, "error disabling: %v", err)
156-
}
157-
158-
// refresh outputs again
159-
outputs, err = GetOutputs()
160-
if err != nil {
161-
return fmt.Errorf("detecting outputs after disabling: %w", err)
162-
}
163-
164-
V("new outputs after disable: %v", outputs)
165-
}
166-
167119
rule, err := MatchRules(globalOpts.cfg.Rules, outputs)
168120
if err != nil {
169121
return fmt.Errorf("matching rules: %w", err)
@@ -173,6 +125,15 @@ func (cmd CmdWatch) Execute(args []string) (err error) {
173125
V("outputs: %v", outputs)
174126
V("new rule found: %v", rule.Name)
175127

128+
// Disable old rules outputs if they are not in current active rules outputs.
129+
diff := rule.OutputsDiff(lastRule)
130+
if len(diff) > 0 {
131+
err = RunCommand(DisableOutputs(diff))
132+
if err != nil {
133+
fmt.Fprintf(os.Stderr, "error disabling: %v", err)
134+
}
135+
}
136+
176137
err = ApplyRule(outputs, rule)
177138
if err != nil {
178139
return fmt.Errorf("applying rules: %w", err)
@@ -185,15 +146,7 @@ func (cmd CmdWatch) Execute(args []string) (err error) {
185146
disablePoll = true
186147
backoffCh = time.After(time.Duration(globalOpts.Pause) * time.Second)
187148
}
188-
189-
// refresh outputs for next cycle
190-
outputs, err = GetOutputs()
191-
if err != nil {
192-
return fmt.Errorf("refreshing outputs: %w", err)
193-
}
194149
}
195-
196-
lastOutputs = outputs
197150
}
198151

199152
select {

config.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type Config struct {
2020
OnFailure []string `yaml:"on_failure"`
2121
}
2222

23-
// xdgConfigDir returns the config directory according to the xdg standard, see
23+
// xdgConfigDir returns the config directory according to the xdg standard, see.
2424
// http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html.
2525
func xdgConfigDir() string {
2626
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
@@ -37,7 +37,8 @@ func openConfigFile(name string) (io.ReadCloser, error) {
3737
os.Getenv("GROBI_CONFIG"),
3838
filepath.Join(xdgConfigDir(), "grobi.conf"),
3939
filepath.Join(os.Getenv("HOME"), ".grobi.conf"),
40-
"/etc/xdg/grobi.conf"} {
40+
"/etc/xdg/grobi.conf",
41+
} {
4142
if filename != "" {
4243
if f, err := os.Open(filename); err == nil {
4344
V("reading config from %v\n", filename)
@@ -81,7 +82,6 @@ func readConfig(name string) (Config, error) {
8182

8283
// Valid returns an error if the config is invalid, ie a pattern is malformed.
8384
func (cfg Config) Valid() error {
84-
8585
for _, rule := range cfg.Rules {
8686
for _, list := range [][]string{rule.OutputsPresent, rule.OutputsAbsent, rule.OutputsConnected, rule.OutputsDisconnected} {
8787
for _, pat := range list {

main.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ func RunCommand(cmd *exec.Cmd) error {
5656
return cmd.Run()
5757
}
5858

59-
var globalOpts = GlobalOptions{}
60-
var parser = flags.NewParser(&globalOpts, flags.Default)
59+
var (
60+
globalOpts = GlobalOptions{}
61+
parser = flags.NewParser(&globalOpts, flags.Default)
62+
)
6163

6264
func V(s string, data ...interface{}) {
6365
if globalOpts.Verbose && globalOpts.log == nil {

randr.go

+39-21
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,9 @@ func (m Modes) String() string {
192192
return strings.Join(str, " ")
193193
}
194194

195-
// Generates the monitor id from the edid
195+
// Generates the monitor id from the edid.
196196
func GenerateMonitorId(s string) (string, error) {
197-
var errEdidCorrupted = errors.New("corrupt EDID: " + s)
197+
errEdidCorrupted := errors.New("corrupt EDID: " + s)
198198
if len(s) < 32 || s[:16] != "00ffffffffffff00" {
199199
return "", errEdidCorrupted
200200
}
@@ -318,30 +318,40 @@ func parseModeLine(line string) (mode Mode, err error) {
318318
}
319319
mode.Name = ws.Text()
320320

321-
if !ws.Scan() {
322-
return Mode{}, fmt.Errorf("line too short, no refresh rate found: %s", line)
323-
}
324-
rate := ws.Text()
321+
i := 0
322+
for ws.Scan() {
323+
i++
324+
rate := ws.Text()
325+
if len(rate) == 0 {
326+
break
327+
}
328+
if rate[len(rate)-1] == '+' {
329+
mode.Default = true
330+
}
325331

326-
if rate[len(rate)-1] == '+' {
327-
mode.Default = true
328-
}
332+
if len(rate) > 1 && rate[len(rate)-2] == '*' {
333+
mode.Active = true
334+
}
329335

330-
if rate[len(rate)-2] == '*' {
331-
mode.Active = true
332-
}
336+
if rate[len(rate)-1] == '*' {
337+
mode.Active = true
338+
}
333339

334-
// handle single-word "+", which happens when a mode is default but not active
335-
if ws.Scan() && ws.Text() == "+" {
336-
mode.Default = true
340+
// handle single-word "+", which happens when a mode is default but not active
341+
if ws.Text() == "+" {
342+
mode.Default = true
343+
}
344+
}
345+
if i == 0 {
346+
return Mode{}, fmt.Errorf("line too short, no refresh rate found: %s", line)
337347
}
338348

339349
return mode, nil
340350
}
341351

342352
var errNotEdidLine = errors.New("not an edid line")
343353

344-
// parseEdidLine returns the partial EDID on that line
354+
// parseEdidLine returns the partial EDID on that line.
345355
func parseEdidLine(line string) (edid string, err error) {
346356
if !strings.HasPrefix(line, " ") {
347357
return "", errNotEdidLine
@@ -513,14 +523,18 @@ func BuildCommandOutputRow(rule Rule, current Outputs) ([]*exec.Cmd, error) {
513523
enableOutputArgs := [][]string{}
514524

515525
active := make(map[string]struct{})
516-
var lastOutput = ""
526+
lastOutput := ""
517527
for i, output := range outputs {
518-
data := strings.SplitN(output, "@", 2)
528+
data := strings.SplitN(output, "@", 3)
519529
name := data[0]
520530
mode := ""
531+
rate := ""
521532
if len(data) > 1 {
522533
mode = data[1]
523534
}
535+
if len(data) > 2 {
536+
rate = data[2]
537+
}
524538

525539
active[name] = struct{}{}
526540

@@ -532,6 +546,10 @@ func BuildCommandOutputRow(rule Rule, current Outputs) ([]*exec.Cmd, error) {
532546
args = append(args, "--mode", mode)
533547
}
534548

549+
if rate != "" {
550+
args = append(args, "--rate", rate)
551+
}
552+
535553
if i > 0 {
536554
if row {
537555
args = append(args, "--right-of", lastOutput)
@@ -622,9 +640,9 @@ func BuildCommandOutputRow(rule Rule, current Outputs) ([]*exec.Cmd, error) {
622640
}
623641

624642
// DisableOutputs returns a call to `xrandr` to switch off the specified outputs.
625-
func DisableOutputs(off Outputs) (*exec.Cmd, error) {
643+
func DisableOutputs(off Outputs) *exec.Cmd {
626644
if len(off) == 0 {
627-
return nil, nil
645+
return nil
628646
}
629647

630648
command := "xrandr"
@@ -638,5 +656,5 @@ func DisableOutputs(off Outputs) (*exec.Cmd, error) {
638656

639657
V("disable outputs: %v\n", outputs)
640658

641-
return exec.Command(command, args...), nil
659+
return exec.Command(command, args...)
642660
}

0 commit comments

Comments
 (0)