initial commit
This commit is contained in:
commit
413cc87706
102
arrangement.go
Normal file
102
arrangement.go
Normal 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
33
config.go
Normal 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
17
config_test.go
Normal 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
28
current_arrangement.go
Normal 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
69
display_config.go
Normal 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
55
display_mode.go
Normal 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
12
go.mod
Normal 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
10
go.sum
Normal 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
29
position.go
Normal 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
52
reflection.go
Normal 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
29
resolution.go
Normal 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
31
rotation.go
Normal 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
23
utils.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user