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