From 413cc87706052de51336b880d57e4183410d6460 Mon Sep 17 00:00:00 2001 From: milarin Date: Mon, 18 Dec 2023 23:08:42 +0100 Subject: [PATCH] initial commit --- arrangement.go | 102 +++++++++++++++++++++++++++++++++++++++++ config.go | 33 +++++++++++++ config_test.go | 17 +++++++ current_arrangement.go | 28 +++++++++++ display_config.go | 69 ++++++++++++++++++++++++++++ display_mode.go | 55 ++++++++++++++++++++++ go.mod | 12 +++++ go.sum | 10 ++++ position.go | 29 ++++++++++++ reflection.go | 52 +++++++++++++++++++++ resolution.go | 29 ++++++++++++ rotation.go | 31 +++++++++++++ utils.go | 23 ++++++++++ 13 files changed, 490 insertions(+) create mode 100644 arrangement.go create mode 100644 config.go create mode 100644 config_test.go create mode 100644 current_arrangement.go create mode 100644 display_config.go create mode 100644 display_mode.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 position.go create mode 100644 reflection.go create mode 100644 resolution.go create mode 100644 rotation.go create mode 100644 utils.go diff --git a/arrangement.go b/arrangement.go new file mode 100644 index 0000000..e85db82 --- /dev/null +++ b/arrangement.go @@ -0,0 +1,102 @@ +package xrandr + +import ( + "encoding/json" + "errors" + "fmt" + "os/exec" + "strings" +) + +type Arrangement map[string]*DisplayArrangement + +type DisplayArrangement struct { + Off bool `json:"off,omitempty"` + Primary bool `json:"primary,omitempty"` + Position *Position `json:"position,omitempty"` + Resolution *Resolution `json:"resolution,omitempty"` + + Rate float64 `json:"rate,omitempty"` + Rotation Rotation `json:"rotation,omitempty"` + Reflection Reflection `json:"reflection,omitempty"` +} + +func (a *DisplayArrangement) Equivalent(b *DisplayArrangement) bool { + return a.Off == b.Off && + a.Primary == b.Primary && + a.Position.Equivalent(b.Position) && + a.Resolution.Equivalent(b.Resolution) && + a.Rate == b.Rate && + a.Rotation == b.Rotation && + a.Reflection == b.Reflection +} + +func (a *Arrangement) xrandr() string { + b := new(strings.Builder) + b.WriteString("xrandr") + + for port, display := range *a { + b.WriteString(fmt.Sprintf(" --output %s", port)) + + if display.Off { + b.WriteString(" --off") + continue + } + + if display.Primary { + b.WriteString(" --primary") + } + + b.WriteString(" --preferred") + + if display.Position != nil { + b.WriteString(fmt.Sprintf(" --pos %dx%d", display.Position.X, display.Position.Y)) + } + + if display.Resolution != nil { + b.WriteString(fmt.Sprintf(" --mode %dx%d", display.Resolution.Width, display.Resolution.Height)) + } + + if display.Rate > 0 { + b.WriteString(fmt.Sprintf(" --rate %g", display.Rate)) + } + + b.WriteString(fmt.Sprintf(" --rotate %s", display.Rotation)) + b.WriteString(fmt.Sprintf(" --reflect %s", display.Reflection.xrandr())) + } + + return b.String() +} + +func (a *Arrangement) Apply() error { + cmd := strings.Split(a.xrandr(), " ") + xrandr := exec.Command(cmd[0], cmd[1:]...) + + if err := xrandr.Start(); err != nil { + return err + } + + if err := xrandr.Wait(); err != nil { + return err + } + + if !xrandr.ProcessState.Success() { + return errors.New("xrandr could not apply display arrangement") + } + + return nil +} + +func (a *Arrangement) Display(port string) *DisplayArrangement { + return (*a)[port] +} + +func (a *Arrangement) String() string { + data, _ := json.MarshalIndent(a, "", "\t") + return string(data) +} + +func (a *DisplayArrangement) String() string { + data, _ := json.MarshalIndent(a, "", "\t") + return string(data) +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..f1b72d7 --- /dev/null +++ b/config.go @@ -0,0 +1,33 @@ +package xrandr + +import ( + "bytes" + "os/exec" + + "git.milar.in/milarin/bufr" +) + +type Config struct { + Displays []DisplayConfig +} + +func GetCurrentConfig() (*Config, error) { + output, err := exec.Command("xrandr").Output() + if err != nil { + return nil, err + } + + r := bufr.New(bytes.NewReader(output)) + r.StringUntil(isNewLine) + + configs := make([]DisplayConfig, 0) + for config, err := parseDisplayConfig(r); err == nil; config, err = parseDisplayConfig(r) { + configs = append(configs, *config) + } + + if err != nil { + return nil, err + } + + return &Config{configs}, nil +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..2adf268 --- /dev/null +++ b/config_test.go @@ -0,0 +1,17 @@ +package xrandr + +import ( + "fmt" + "testing" +) + +func TestConfig(t *testing.T) { + configs, err := GetCurrentConfig() + if err != nil { + panic(err) + } + + for _, config := range configs.Displays { + fmt.Println(config) + } +} diff --git a/current_arrangement.go b/current_arrangement.go new file mode 100644 index 0000000..6b37197 --- /dev/null +++ b/current_arrangement.go @@ -0,0 +1,28 @@ +package xrandr + +func (c *Config) Arrangement() *Arrangement { + a := map[string]*DisplayArrangement{} + + for _, config := range c.Displays { + a[config.Port] = config.Arrangement() + } + + return (*Arrangement)(&a) +} + +func (c *DisplayConfig) Arrangement() *DisplayArrangement { + off := c.Resolution == nil + if off { + return &DisplayArrangement{Off: off} + } + + return &DisplayArrangement{ + Off: off, + Primary: c.Primary, + Position: c.Position, + Resolution: c.Resolution, + Rate: c.ActiveMode.Rate, + Rotation: c.Rotation, + Reflection: c.Reflection, + } +} diff --git a/display_config.go b/display_config.go new file mode 100644 index 0000000..580a4aa --- /dev/null +++ b/display_config.go @@ -0,0 +1,69 @@ +package xrandr + +import ( + "encoding/json" + "errors" + "regexp" + "strings" + + "git.milar.in/milarin/bufr" +) + +var ( + displayConfig = regexp.MustCompile(`^(.*?-\d) (disconnected|connected)( primary)?(?: (\d*?)x(\d*?)\+(\d*?)\+(\d*?) )?(normal|inverted|left|right)?\s*?(X axis|Y axis|X and Y axis)?\s*?\(((?:normal |left |inverted |right |x axis |y axis )*(?:normal|left|inverted|right|x axis|y axis))\)(?: (\d*)mm x (\d*)mm)?$`) +) + +type DisplayConfig struct { + Port string `json:"port"` + Connected bool `json:"connected"` + Primary bool `json:"primary"` + Resolution *Resolution `json:"resolution"` + Position *Position `json:"position"` + Rotation Rotation `json:"rotation"` + Reflection Reflection `json:"reflection"` + + RecommendedMode *DisplayMode + ActiveMode *DisplayMode + Modes []DisplayMode `json:"modes"` + + DimensionsMillimeter *Resolution `json:"-"` +} + +func parseDisplayConfig(r *bufr.Reader) (*DisplayConfig, error) { + // bufcontent := reflect.ValueOf(r).Elem().FieldByName("buf").Elem().FieldByName("values") + // bufcontent = reflect.NewAt(bufcontent.Type(), unsafe.Pointer(bufcontent.UnsafeAddr())).Elem() + // fmt.Printf("%#v\n", string(bufcontent.Interface().([]rune))) + line, err := r.StringUntil(isNewLine) + if err != nil { + return nil, err + } + matches := displayConfig.FindStringSubmatch(line) + + if matches == nil { + return nil, errors.New("no data") + } + + active, recommended, modes, err := parseDisplayModes(r) + if err != nil { + return nil, err + } + + return &DisplayConfig{ + Port: matches[1], + Connected: strings.TrimSpace(matches[2]) == "connected", + Primary: strings.TrimSpace(matches[3]) == "primary", + Resolution: NewResolutionFromString(matches[4], matches[5]), + Position: NewPositionFromString(matches[6], matches[7]), + Rotation: MakeRotation(matches[8]), + Reflection: MakeReflection(matches[9]), + DimensionsMillimeter: NewResolutionFromString(matches[11], matches[12]), + ActiveMode: active, + RecommendedMode: recommended, + Modes: modes, + }, nil +} + +func (c DisplayConfig) String() string { + data, _ := json.MarshalIndent(c, "", "\t") + return string(data) +} diff --git a/display_mode.go b/display_mode.go new file mode 100644 index 0000000..1fb51ea --- /dev/null +++ b/display_mode.go @@ -0,0 +1,55 @@ +package xrandr + +import ( + "regexp" + + "git.milar.in/milarin/bufr" +) + +var modePattern = regexp.MustCompile(`^\s*(\d+?)x(\d+?)\s+(.*?)$`) +var ratePattern = regexp.MustCompile(`(\d+\.\d+)(\*)?(\+)?\s*`) + +type DisplayMode struct { + Resolution Resolution `json:"resolution"` + Rate float64 `json:"rate"` +} + +func parseDisplayModes(r *bufr.Reader) (active, recommended *DisplayMode, allModes []DisplayMode, err error) { + allModes = make([]DisplayMode, 0) + + var line string + + for line, err = r.StringUntil(isNewLine); err == nil; line, err = r.StringUntil(isNewLine) { + matches := modePattern.FindStringSubmatch(line) + + // line is not a DisplayMode + if matches == nil { + err = nil + break + } + + res := NewResolutionFromString(matches[1], matches[2]) + + rates := ratePattern.FindAllStringSubmatch(matches[3], -1) + for _, rate := range rates { + mode := DisplayMode{ + Resolution: *res, + Rate: atof(rate[1]), + } + if rate[2] == "*" { + active = &mode + } + if rate[3] == "+" { + recommended = &mode + } + allModes = append(allModes, mode) + } + } + + // last read line was not a DisplayMode. Unread line + if err == nil { + r.UnreadString(line) + } + + return active, recommended, allModes, err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..37d9725 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module git.milar.in/milarin/xrandr + +go 1.21.5 + +require git.milar.in/milarin/bufr v0.0.19 + +require ( + git.milar.in/milarin/adverr v1.1.1 // indirect + git.milar.in/milarin/ds v0.0.3 // indirect + git.milar.in/milarin/gmath v0.0.5 // indirect + git.milar.in/milarin/slices v0.0.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..04306e7 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +git.milar.in/milarin/adverr v1.1.1 h1:ENtBcqT7CncLsVfaLC3KzX8QSSGiSpsC7I7wDqladu8= +git.milar.in/milarin/adverr v1.1.1/go.mod h1:joU9sBb7ySyNv4SpTXB0Z4o1mjXsArBw4N27wjgzj9E= +git.milar.in/milarin/bufr v0.0.19 h1:qu3arsyePMw751HHb3ND67VbZrB6/pHvP04ZvCeY9Z0= +git.milar.in/milarin/bufr v0.0.19/go.mod h1:XVHI549bLeuR76l9wJDs8BKODdhlP8UxyE328NdVRb0= +git.milar.in/milarin/ds v0.0.3 h1:drC601kWpaFsKuQGU2BDTuOIoaV+awtZskVjKH+toOY= +git.milar.in/milarin/ds v0.0.3/go.mod h1:HJK7QERcRvV9j7xzEocrKUtW+1q4JB1Ly4Bj54chfwI= +git.milar.in/milarin/gmath v0.0.5 h1:qQQMUTbxEk5LriMMSRbElExDSouSJKYBo6zRcOYKVIU= +git.milar.in/milarin/gmath v0.0.5/go.mod h1:HDLftG5RLpiNGKiIWh+O2G1PYkNzyLDADO8Cd/1abiE= +git.milar.in/milarin/slices v0.0.8 h1:qN9TE3tkArdTixMKSnwvNPcApwAjxpLVwA5a9k1rm2s= +git.milar.in/milarin/slices v0.0.8/go.mod h1:qMhdtMnfWswc1rHpwgNw33lB84aNEkdBn5BDiYA+G3k= diff --git a/position.go b/position.go new file mode 100644 index 0000000..31ceaf3 --- /dev/null +++ b/position.go @@ -0,0 +1,29 @@ +package xrandr + +type Position struct { + X int `json:"x"` + Y int `json:"y"` +} + +func NewPositionFromString(x, y string) *Position { + if x == "" || y == "" { + return nil + } + return NewPosition(atoi(x), atoi(y)) +} + +func NewPosition(x, y int) *Position { + return &Position{x, y} +} + +func (p *Position) Equivalent(o *Position) bool { + if p == o { + return true + } + + if (p == nil) != (o == nil) { + return false + } + + return p.X == o.X && p.Y == o.Y +} diff --git a/reflection.go b/reflection.go new file mode 100644 index 0000000..591549c --- /dev/null +++ b/reflection.go @@ -0,0 +1,52 @@ +package xrandr + +type Reflection string + +const ( + ReflectionNormal Reflection = "normal" + ReflectionX Reflection = "X axis" + ReflectionY Reflection = "Y axis" + ReflectionXY Reflection = "X and Y axis" +) + +func MakeReflection(ref string) Reflection { + switch ref { + + case "normal": + return ReflectionNormal + + case "X axis": + return ReflectionX + + case "Y axis": + return ReflectionY + + case "X and Y axis": + return ReflectionXY + + default: + return ReflectionNormal + + } +} + +func (r Reflection) xrandr() string { + switch r { + + case ReflectionNormal: + return "normal" + + case ReflectionX: + return "x" + + case ReflectionY: + return "y" + + case ReflectionXY: + return "xy" + + default: + return "normal" + + } +} diff --git a/resolution.go b/resolution.go new file mode 100644 index 0000000..dab6181 --- /dev/null +++ b/resolution.go @@ -0,0 +1,29 @@ +package xrandr + +type Resolution struct { + Width int `json:"width"` + Height int `json:"height"` +} + +func NewResolutionFromString(width, height string) *Resolution { + if width == "" || height == "" { + return nil + } + return NewResolution(atoi(width), atoi(height)) +} + +func NewResolution(width, height int) *Resolution { + return &Resolution{width, height} +} + +func (r *Resolution) Equivalent(o *Resolution) bool { + if r == o { + return true + } + + if (r == nil) != (o == nil) { + return false + } + + return r.Width == o.Width && r.Height == o.Height +} diff --git a/rotation.go b/rotation.go new file mode 100644 index 0000000..b89a0f1 --- /dev/null +++ b/rotation.go @@ -0,0 +1,31 @@ +package xrandr + +type Rotation string + +const ( + RotationNormal Rotation = "normal" + RotationInverted Rotation = "inverted" + RotationLeft Rotation = "left" + RotationRight Rotation = "right" +) + +func MakeRotation(rot string) Rotation { + switch rot { + + case "normal": + return RotationNormal + + case "inverted": + return RotationInverted + + case "left": + return RotationLeft + + case "right": + return RotationRight + + default: + return RotationNormal + + } +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..a817552 --- /dev/null +++ b/utils.go @@ -0,0 +1,23 @@ +package xrandr + +import "strconv" + +func isNewLine(rn rune) bool { + return rn == '\n' +} + +func atoi(str string) int { + n, err := strconv.Atoi(str) + if err != nil { + panic(err) + } + return n +} + +func atof(str string) float64 { + f, err := strconv.ParseFloat(str, 64) + if err != nil { + panic(err) + } + return f +}