initial commit

This commit is contained in:
milarin 2023-12-18 23:08:42 +01:00
commit 413cc87706
13 changed files with 490 additions and 0 deletions

102
arrangement.go Normal file
View File

@ -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)
}

33
config.go Normal file
View File

@ -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
}

17
config_test.go Normal file
View File

@ -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)
}
}

28
current_arrangement.go Normal file
View File

@ -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,
}
}

69
display_config.go Normal file
View File

@ -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)
}

55
display_mode.go Normal file
View File

@ -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
}

12
go.mod Normal file
View File

@ -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
)

10
go.sum Normal file
View File

@ -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=

29
position.go Normal file
View File

@ -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
}

52
reflection.go Normal file
View File

@ -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"
}
}

29
resolution.go Normal file
View File

@ -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
}

31
rotation.go Normal file
View File

@ -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
}
}

23
utils.go Normal file
View File

@ -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
}