From 3a37f236a7ec8140b8624df2356dfaccf0646bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20F=2E=20R=C3=B8dseth?= Date: Thu, 7 Feb 2019 17:00:02 +0100 Subject: [PATCH] Initial commit --- .gitignore | 18 +- .travis.yml | 13 ++ LICENSE | 24 +++ README.md | 3 +- cinnamon.go | 56 ++++++ closest.go | 43 ++++ closest_test.go | 29 +++ cmd/getdpi/README.md | 31 +++ cmd/getdpi/main.go | 39 ++++ cmd/lsmon/README.md | 3 + cmd/lsmon/main.go | 25 +++ cmd/setrandom/README.md | 3 + cmd/setrandom/main.go | 53 +++++ cmd/setwallpaper/README.md | 3 + cmd/setwallpaper/main.go | 38 ++++ cmd/wayinfo/main.go | 20 ++ cmd/xinfo/main.go | 19 ++ deepin.go | 23 +++ feh.go | 34 ++++ gnome2.go | 25 +++ gnome3.go | 24 +++ go.mod | 1 + mate.go | 56 ++++++ monitors.go | 54 +++++ plasma.go | 32 +++ res.go | 101 ++++++++++ res_test.go | 16 ++ sway.go | 23 +++ utils.go | 83 ++++++++ wallpaper.go | 62 ++++++ wallpaperpack.md | 104 ++++++++++ wayinfo.go | 105 ++++++++++ wayinfo.h | 403 +++++++++++++++++++++++++++++++++++++ wayinfo_test.go | 31 +++ weston.go | 37 ++++ x11.go | 41 ++++ xfce4.go | 40 ++++ xinfo.go | 72 +++++++ xinfo.h | 83 ++++++++ xinfo_test.go | 27 +++ xwallpaper.h | 64 ++++++ 41 files changed, 1948 insertions(+), 13 deletions(-) create mode 100644 .travis.yml create mode 100644 cinnamon.go create mode 100644 closest.go create mode 100644 closest_test.go create mode 100644 cmd/getdpi/README.md create mode 100644 cmd/getdpi/main.go create mode 100644 cmd/lsmon/README.md create mode 100644 cmd/lsmon/main.go create mode 100644 cmd/setrandom/README.md create mode 100644 cmd/setrandom/main.go create mode 100644 cmd/setwallpaper/README.md create mode 100644 cmd/setwallpaper/main.go create mode 100644 cmd/wayinfo/main.go create mode 100644 cmd/xinfo/main.go create mode 100644 deepin.go create mode 100644 feh.go create mode 100644 gnome2.go create mode 100644 gnome3.go create mode 100644 go.mod create mode 100644 mate.go create mode 100644 monitors.go create mode 100644 plasma.go create mode 100644 res.go create mode 100644 res_test.go create mode 100644 sway.go create mode 100644 utils.go create mode 100644 wallpaper.go create mode 100644 wallpaperpack.md create mode 100644 wayinfo.go create mode 100644 wayinfo.h create mode 100644 wayinfo_test.go create mode 100644 weston.go create mode 100644 x11.go create mode 100644 xfce4.go create mode 100644 xinfo.go create mode 100644 xinfo.h create mode 100644 xinfo_test.go create mode 100644 xwallpaper.h diff --git a/.gitignore b/.gitignore index f1c181e..73e7413 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,6 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, build with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out +cmd/setrandom/setrandom +cmd/setwallpaper/setwallpaper +cmd/lsmon/lsmon +cmd/wayinfo/wayinfo +cmd/xinfo/xinfo +cmd/getdpi/getdpi diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..72ebb73 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: go + +addons: + apt: + packages: + - build-essential + - libx11-dev + - libxcursor-dev + - libwayland-dev + +go: + - "1.11" + - tip diff --git a/LICENSE b/LICENSE index 0d6d9e7..9c43c2e 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,27 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +Copyright for portions of the Wayland-related parts of Wallpaper2 are held by +Philipp Brüschweiler, 2012 as part of weston. All other copyright for +Wallpaper2 are held by Alexander F. Rødseth, 2019. + +Permission to use, copy, modify, distribute, and sell this software and +its documentation for any purpose is hereby granted without fee, provided +that the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of the copyright holders not be used in +advertising or publicity pertaining to distribution of the software +without specific, written prior permission. The copyright holders make +no representations about the suitability of this software for any +purpose. It is provided "as is" without express or implied warranty. + +THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY +SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md index 0909407..5bcb306 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # monitor -Go package for dealing with monitors, resolutions and setting wallpapers, under both X and Wayland + +Go package for dealing with monitors, resolutions and setting wallpapers, under both X and Wayland. diff --git a/cinnamon.go b/cinnamon.go new file mode 100644 index 0000000..3f18aea --- /dev/null +++ b/cinnamon.go @@ -0,0 +1,56 @@ +package monitor + +// Cinnamon windowmanager detector +type Cinnamon struct { + mode string // none | wallpaper | centered | scaled | stretched | zoom | spanned, scaled is the default + hasDconf bool + hasGsettings bool + hasChecked bool +} + +func (s *Cinnamon) Name() string { + return "Cinnamon" +} + +func (s *Cinnamon) ExecutablesExists() bool { + s.hasDconf = which("dconf") != "" + s.hasGsettings = which("gsettings") != "" + s.hasChecked = true + return s.hasDconf || s.hasGsettings +} + +func (s *Cinnamon) Running() bool { + // TODO: To test + return (containsE("GDMSESSION", "cinnamon") || containsE("XDG_SESSION_DESKTOP", "cinnamon") || containsE("XDG_CURRENT_DESKTOP", "cinnamon")) +} + +func (s *Cinnamon) SetMode(mode string) { + s.mode = mode +} + +// SetWallpaper sets the desktop wallpaper, given an image filename. +// The image must exist and be readable. +func (s *Cinnamon) SetWallpaper(imageFilename string) error { + // Check if dconf or gsettings are there, if we haven't already checked + if !s.hasChecked { + s.ExecutablesExists() + } + // Set the desktop wallpaper picture mode + mode := defaultMode + if s.mode != "" { + mode = s.mode + } + // Change the background with either dconf or gsettings + if s.hasDconf { + // use dconf + if err := run("dconf write /org/cinnamon/desktop/background/picture-filename \"'" + imageFilename + "'\""); err != nil { + return err + } + return run("dconf write /org/cinnamon/desktop/background/picture-options \"'" + mode + "'\"") + } + // use gsettings + if err := run("gsettings set org.cinnamon.background picture-filename '" + imageFilename + "'"); err != nil { + return err + } + return run("gsettings set org.cinnamon.background picture-options '" + mode + "'") +} diff --git a/closest.go b/closest.go new file mode 100644 index 0000000..9feaec9 --- /dev/null +++ b/closest.go @@ -0,0 +1,43 @@ +package monitor + +import ( + "os" +) + +// Exists checks if the given filename exists in the current directory +// (or if an absolute path exists) +func Exists(filename string) bool { + _, err := os.Stat(filename) + return !os.IsNotExist(err) +} + +// Closest takes a list of filenames on the form "*_WIDTHxHEIGHT.ext". +// WIDTH and HEIGHT are numbers. Closest returns the filename that is closest +// to the average monitor resolution. Any filenames not following the pattern +// will result in an error being returned. +func Closest(filenames []string) (string, error) { + avgRes, err := AverageResolution() + if err != nil { + return "", err + } + // map: (distance to average resolution) => (filename) + d := make(map[int]string) + var dist int + var minDist int + var minDistSet bool + for _, filename := range filenames { + res, err := FilenameToRes(filename) + if err != nil { + return "", err + } + dist = Distance(avgRes, res) + if dist < minDist || !minDistSet { + minDist = dist + minDistSet = true + } + //fmt.Printf("FILENAME %s HAS DISTANCE %d TO AVERAGE RESOLUTION %s\n", filename, dist, avgRes) + d[dist] = filename + } + // ok, have a map, now find the filename of the smallest distance + return d[minDist], nil +} diff --git a/closest_test.go b/closest_test.go new file mode 100644 index 0000000..41a38f3 --- /dev/null +++ b/closest_test.go @@ -0,0 +1,29 @@ +package monitor + +import ( + "fmt" + "testing" +) + +func TestClosest(t *testing.T) { + filenames := []string{"hello_1024x768.jpg", "hello_1600x1200.jpg", "hello_320x200.jpg"} + fmt.Println(filenames) + + resolutions, err := ExtractResolutions(filenames) + if err != nil { + t.Error(err) + } + fmt.Println("Resolutions extracted from slice of filenames:", resolutions) + + avgRes, err := AverageResolution() + if err != nil { + t.Error(err) + } + fmt.Println("Average resolution for all connected monitors:", avgRes) + + filename, err := Closest(filenames) + if err != nil { + t.Error(err) + } + fmt.Println("Filename closest to the average resolution:", filename) +} diff --git a/cmd/getdpi/README.md b/cmd/getdpi/README.md new file mode 100644 index 0000000..f2794ef --- /dev/null +++ b/cmd/getdpi/README.md @@ -0,0 +1,31 @@ +# getdpi + +Tool for retrieving the average DPI across all monitors, regardless of if X or Wayland is in use. + +## Building getdpi + + go build + +## Usage + +Retreive the average DPI as a pair of numbers (ie. `96x96`): + + getdpi -b + +Retreive the average DPI as a single number (ie. `96`): + + getdpi + +Version information: + + getdpi --version + +## Listing DPI per monitor + +The `lsmon` utility in the [monitor](https://github.com/xyproto/monitor) package supports listing monitor resolutions and DPI with the `-dpi` flag. + +## General Info + +* Version: 1.2.0 +* License: MIT +* Author: Alexander F. Rødseth <xyproto@archlinux.org> diff --git a/cmd/getdpi/main.go b/cmd/getdpi/main.go new file mode 100644 index 0000000..1c44b8b --- /dev/null +++ b/cmd/getdpi/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "os" + + "github.com/xyproto/monitor" +) + +const versionString = "1.2.0" + +func main() { + // Retrieve a slice of Monitor structs, or exit with an error + monitors, err := monitor.Detect() + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + // Output the average DPI + DPIw, DPIh := uint(0), uint(0) + for _, monitor := range monitors { + DPIw += monitor.DPIw + DPIh += monitor.DPIh + } + DPIw /= uint(len(monitors)) + DPIh /= uint(len(monitors)) + + // Check if -b is given (for outputting both numbers) + if len(os.Args) > 1 && os.Args[1] == "-b" { + fmt.Printf("%dx%d\n", DPIw, DPIh) + return + } else if len(os.Args) > 1 && os.Args[1] == "--version" { + fmt.Println(versionString) + return + } + + // Output a single number + fmt.Println(DPIw) +} diff --git a/cmd/lsmon/README.md b/cmd/lsmon/README.md new file mode 100644 index 0000000..a4759e2 --- /dev/null +++ b/cmd/lsmon/README.md @@ -0,0 +1,3 @@ +List all monitors, with resolutions + +Use the `-dpi` flag to also list the DPI per monitor. diff --git a/cmd/lsmon/main.go b/cmd/lsmon/main.go new file mode 100644 index 0000000..e67a6b9 --- /dev/null +++ b/cmd/lsmon/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "os" + + "github.com/xyproto/monitor" +) + +func main() { + // Retrieve a slice of Monitor structs, or exit with an error + monitors, err := monitor.Detect() + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + // For every monitor, output the ID, width and height + for _, monitor := range monitors { + if len(os.Args) > 1 && os.Args[1] == "-dpi" { + fmt.Printf("%d: %dx%d (DPI: %dx%d)\n", monitor.ID, monitor.Width, monitor.Height, monitor.DPIw, monitor.DPIh) + } else { + fmt.Printf("%d: %dx%d\n", monitor.ID, monitor.Width, monitor.Height) + } + } +} diff --git a/cmd/setrandom/README.md b/cmd/setrandom/README.md new file mode 100644 index 0000000..3f085a1 --- /dev/null +++ b/cmd/setrandom/README.md @@ -0,0 +1,3 @@ +This executable will change the desktop wallpaper to a random .png image from `/usr/share/pixmaps`. + +Don't run the executable if you don't want to change the desktop wallpaper! diff --git a/cmd/setrandom/main.go b/cmd/setrandom/main.go new file mode 100644 index 0000000..de96505 --- /dev/null +++ b/cmd/setrandom/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "bufio" + "fmt" + "math/rand" + "os" + "path/filepath" + "strings" + "time" + + "github.com/xyproto/monitor" +) + +const versionString = "Random Wallpaper Changer 1.0.0" + +func init() { + rand.Seed(time.Now().UTC().UnixNano()) +} + +func main() { + fmt.Println(versionString) + reader := bufio.NewReader(os.Stdin) + fmt.Print("Please type \"yes\" if you want to change the desktop wallpaper to a random image: ") + text, err := reader.ReadString('\n') + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if strings.TrimSpace(text) != "yes" { + fmt.Println("OK, made no changes.") + os.Exit(0) + } + + matches, err := filepath.Glob("/usr/share/pixmaps/*.png") + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + imageFilename := matches[rand.Int()%len(matches)] + if absImageFilename, err := filepath.Abs(imageFilename); err == nil { + imageFilename = absImageFilename + } + if _, err := os.Stat(imageFilename); os.IsNotExist(err) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Println("Setting background image to: " + imageFilename) + if err := monitor.SetWallpaper(imageFilename); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cmd/setwallpaper/README.md b/cmd/setwallpaper/README.md new file mode 100644 index 0000000..8a54c71 --- /dev/null +++ b/cmd/setwallpaper/README.md @@ -0,0 +1,3 @@ +`setwallpaper` aims to be able to change the desktop wallpaper, for any windowmanager. + +The first argument is the image to use. diff --git a/cmd/setwallpaper/main.go b/cmd/setwallpaper/main.go new file mode 100644 index 0000000..fafe7b6 --- /dev/null +++ b/cmd/setwallpaper/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/xyproto/monitor" +) + +const versionString = "setwallpaper" + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, versionString+"\n\nNeeds an image file as the first argument.") + os.Exit(1) + } + imageFilename := os.Args[1] + + // Find the absolute path + absImageFilename, err := filepath.Abs(imageFilename) + if err == nil { + imageFilename = absImageFilename + } + + // Check that the file exists + if _, err := os.Stat(imageFilename); os.IsNotExist(err) { + // File does not exist + fmt.Fprintf(os.Stderr, "File does not exist: %s\n", imageFilename) + os.Exit(1) + } + + // Set the desktop wallpaper + if err := monitor.SetWallpaper(imageFilename); err != nil { + fmt.Fprintf(os.Stderr, "Could not set wallpaper: %s\n", err) + os.Exit(1) + } +} diff --git a/cmd/wayinfo/main.go b/cmd/wayinfo/main.go new file mode 100644 index 0000000..d90bc1f --- /dev/null +++ b/cmd/wayinfo/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "os" + + "github.com/xyproto/monitor" +) + +func main() { + // Fetch the info string + info, err := monitor.WaylandInfo() + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + + // Output the info + fmt.Println(info) +} diff --git a/cmd/xinfo/main.go b/cmd/xinfo/main.go new file mode 100644 index 0000000..febf322 --- /dev/null +++ b/cmd/xinfo/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "github.com/xyproto/monitor" + "os" +) + +func main() { + // Fetch the info string + info, err := monitor.XInfo() + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + + // Output the info + fmt.Println(info) +} diff --git a/deepin.go b/deepin.go new file mode 100644 index 0000000..050066c --- /dev/null +++ b/deepin.go @@ -0,0 +1,23 @@ +package monitor + +// Deepin windowmanager detector +type Deepin struct { +} + +func (s *Deepin) Name() string { + return "Deepin" +} + +func (s *Deepin) ExecutablesExists() bool { + return which("deepin-session") != "" && which("dconf") != "" +} + +func (s *Deepin) Running() bool { + return containsE("GDMSESSION", "deepin") || containsE("XDG_SESSION_DESKTOP", "deepin") || containsE("XDG_CURRENT_DESKTOP", "deepin") +} + +// SetWallpaper sets the desktop wallpaper, given an image filename. +// The image must exist and be readable. +func (s *Deepin) SetWallpaper(imageFilename string) error { + return run("dconf write /com/deepin/wrap/gnome/desktop/background/picture-uri \"'" + imageFilename + "'\"") +} diff --git a/feh.go b/feh.go new file mode 100644 index 0000000..ce36d47 --- /dev/null +++ b/feh.go @@ -0,0 +1,34 @@ +package monitor + +// This is the fallback if no specific windowmanager has been detected + +import ( + "errors" +) + +type Feh struct { +} + +func (g *Feh) Name() string { + return "Feh" +} + +func (g *Feh) ExecutablesExists() bool { + return which("feh") != "" +} + +func (g *Feh) Running() bool { + return true +} + +// SetWallpaper sets the desktop wallpaper, given an image filename. +// The image must exist and be readable. +// `feh` is used for setting the desktop background, and must be in the PATH. +func (g *Feh) SetWallpaper(imageFilename string) error { + // bg-fill | bg-center | bg-max | bg-scale | bg-tile + command := "feh --bg-fill " + imageFilename + if err := run(command); err != nil { + return errors.New(command + " failed to run") + } + return nil +} diff --git a/gnome2.go b/gnome2.go new file mode 100644 index 0000000..51393e9 --- /dev/null +++ b/gnome2.go @@ -0,0 +1,25 @@ +package monitor + +// Gnome2 windowmanager detector +type Gnome2 struct { +} + +func (s *Gnome2) Name() string { + return "Gnome2" +} + +func (s *Gnome2) ExecutablesExists() bool { + return which("gconftool-2") != "" +} + +func (s *Gnome2) Running() bool { + // TODO: To implement + //return (containsE("GDMSESSION", "gnome2") || containsE("XDG_SESSION_DESKTOP", "gnome2") || containsE("XDG_CURRENT_DESKTOP", "gnome2")) + return false +} + +// SetWallpaper sets the desktop wallpaper, given an image filename. +// The image must exist and be readable. +func (s *Gnome2) SetWallpaper(imageFilename string) error { + return run("gconftool-2 –type string –set /desktop/gnome/background/picture_filename \"" + imageFilename + "\"") +} diff --git a/gnome3.go b/gnome3.go new file mode 100644 index 0000000..559ce89 --- /dev/null +++ b/gnome3.go @@ -0,0 +1,24 @@ +package monitor + +// Gnome3 windowmanager detector +type Gnome3 struct { +} + +func (s *Gnome3) Name() string { + return "Gnome3" +} + +func (s *Gnome3) ExecutablesExists() bool { + return which("gsettings") != "" +} + +func (s *Gnome3) Running() bool { + // TODO: Needs testing + return (containsE("GDMSESSION", "gnome") || containsE("XDG_SESSION_DESKTOP", "gnome") || containsE("XDG_CURRENT_DESKTOP", "gnome") || containsE("XDG_CURRENT_DESKTOP", "GNOME")) +} + +// SetWallpaper sets the desktop wallpaper, given an image filename. +// The image must exist and be readable. +func (s *Gnome3) SetWallpaper(imageFilename string) error { + return run("gsettings set org.gnome.desktop.background picture-uri \"file://" + imageFilename + "\"") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..faf0974 --- /dev/null +++ b/go.mod @@ -0,0 +1 @@ +module github.com/xyproto/monitor diff --git a/mate.go b/mate.go new file mode 100644 index 0000000..89d06d0 --- /dev/null +++ b/mate.go @@ -0,0 +1,56 @@ +package monitor + +// Mate windowmanager detector +type Mate struct { + mode string // none | wallpaper | centered | scaled | stretched | zoom | spanned, scaled is the default + hasDconf bool + hasGsettings bool + hasChecked bool +} + +func (s *Mate) Name() string { + return "Mate" +} + +func (s *Mate) ExecutablesExists() bool { + s.hasDconf = which("dconf") != "" + s.hasGsettings = which("gsettings") != "" + s.hasChecked = true + return s.hasDconf || s.hasGsettings +} + +func (s *Mate) Running() bool { + return (containsE("GDMSESSION", "mate") || containsE("XDG_SESSION_DESKTOP", "mate") || containsE("XDG_CURRENT_DESKTOP", "mate")) +} + +func (s *Mate) SetMode(mode string) { + s.mode = mode +} + +// SetWallpaper sets the desktop wallpaper, given an image filename. +// The image must exist and be readable. +func (s *Mate) SetWallpaper(imageFilename string) error { + // Check if dconf or gsettings are there, if we haven't already checked + if !s.hasChecked { + s.ExecutablesExists() + } + // Set the desktop wallpaper picture mode + mode := defaultMode + if s.mode != "" { + mode = s.mode + } + // Change the background with either dconf or gsettings + if s.hasDconf { + // use dconf + if err := run("dconf write /org/mate/desktop/background/picture-filename \"'" + imageFilename + "'\""); err != nil { + return err + } + return run("dconf write /org/mate/desktop/background/picture-options \"'" + mode + "'\"") + } + // use gsettings + if err := run("gsettings set org.mate.background picture-filename '" + imageFilename + "'"); err != nil { + return err + } + return run("gsettings set org.mate.background picture-options '" + mode + "'") + +} diff --git a/monitors.go b/monitors.go new file mode 100644 index 0000000..f77b92f --- /dev/null +++ b/monitors.go @@ -0,0 +1,54 @@ +package monitor + +import ( + "errors" + "fmt" +) + +// Monitor contains an ID, the width in pixels and the height in pixels +type Monitor struct { + ID uint // monitor number, from 0 and up + Width uint // width, in pixels + Height uint // height, in pixels + DPIw uint // DPI, if available (width) + DPIh uint // DPI, if available (height) +} + +var errNoWaylandNoX = errors.New("could not detect either Wayland or X") + +func (m Monitor) String() string { + return fmt.Sprintf("[%d] %dx%d", m.ID, m.Width, m.Height) +} + +func Info() (string, error) { + if WaylandCanConnect() { + return WaylandInfo() + } else if XCanConnect() { + return XInfo() + } + return "", errNoWaylandNoX +} + +// Monitors returns information about all monitors, regardless of if it's under +// Wayland or X11. Will use additional plugins, if available. +func Detect() ([]Monitor, error) { + IDs, widths, heights, wDPIs, hDPIs := []uint{}, []uint{}, []uint{}, []uint{}, []uint{} + if WaylandCanConnect() { + if err := WaylandMonitors(&IDs, &widths, &heights, &wDPIs, &hDPIs); err != nil { + return []Monitor{}, err + } + } else if XCanConnect() { + if err := XMonitors(&IDs, &widths, &heights, &wDPIs, &hDPIs); err != nil { + return []Monitor{}, err + } + } + if len(IDs) == 0 { + return []Monitor{}, errNoWaylandNoX + } + // Build and return a []Monitor slice + var monitors = []Monitor{} + for i, ID := range IDs { + monitors = append(monitors, Monitor{ID, widths[i], heights[i], wDPIs[i], hDPIs[i]}) + } + return monitors, nil +} diff --git a/plasma.go b/plasma.go new file mode 100644 index 0000000..f526d90 --- /dev/null +++ b/plasma.go @@ -0,0 +1,32 @@ +package monitor + +// Plasma windowmanager detector +type Plasma struct { +} + +func (s *Plasma) Name() string { + return "Plasma" +} + +func (s *Plasma) ExecutablesExists() bool { + return (which("dbus-send") != "") && ((which("kwin_x11") != "") || (which("kwin_wayland") != "") || (which("kwin") != "")) +} + +func (s *Plasma) Running() bool { + return containsE("GDMSESSION", "plasma") || containsE("XDG_SESSION_DESKTOP", "plasma") || containsE("XDG_CURRENT_DESKTOP", "plasma") +} + +// SetWallpaper sets the desktop wallpaper, given an image filename. +// The image must exist and be readable. +func (s *Plasma) SetWallpaper(imageFilename string) error { + return run(`dbus-send --session --dest=org.kde.plasmashell --type=method_call /PlasmaShell org.kde.PlasmaShell.evaluateScript 'string: + var Desktops = desktops(); + for (i=0;i 4 { + w, err := strconv.Atoi(fields[1]) + if err != nil { + return err + } + h, err := strconv.Atoi(fields[4]) + if err != nil { + return err + } + *IDs = append(*IDs, counter) + *widths = append(*widths, uint(w)) + *heights = append(*heights, uint(h)) + if physW == 0 || physH == 0 { + wDPI = 96 // default DPI value, if no physical size is given + hDPI = 96 // default DPI value, if no physical size is given + log.Println("WARN: No physical monitor size detected!") + //return errors.New("no physical monitor size detected") + } + if physW > 0 && physH > 0 { + // Calculate DPI, from the monitor size (in mm) and the pixel size + wDPI = uint(float64(w) / (float64(physW) / 25.4)) + hDPI = uint(float64(h) / (float64(physH) / 25.4)) + } + *wDPIs = append(*wDPIs, wDPI) + *hDPIs = append(*hDPIs, hDPI) + counter++ + } + } else if strings.Contains(line, "physical_width:") { + // Example output from Wayland Info: + // physical_width: 518 mm, physical_height: 324 mm, + fields := strings.Fields(line) + if len(fields) > 5 { + w, err := strconv.Atoi(fields[1]) + if err != nil { + return err + } + h, err := strconv.Atoi(fields[4]) + if err != nil { + return err + } + physW = uint(w) + physH = uint(h) + physCounter++ + } + } + } + if 0 < physCounter && physCounter < counter { + log.Println("WARN: Some monitors contains a physical size, but not all of them") + //return errors.New("some monitors contains a physical size, but not all of them") + } + return nil +} diff --git a/wayinfo.h b/wayinfo.h new file mode 100644 index 0000000..220b7b8 --- /dev/null +++ b/wayinfo.h @@ -0,0 +1,403 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#define BUF buf + strlen(buf) +#define RETBUF \ + char* str = calloc(strlen(buf) + 1, sizeof(char)); \ + strcpy(str, buf); \ + return str + +static char buf[10240]; + +typedef void (*sprint_info_t)(void* info); + +struct global_info { + struct wl_list link; + + uint32_t id; + uint32_t version; + char* interface; + + sprint_info_t sprint; +}; + +struct output_mode { + struct wl_list link; + + uint32_t flags; + int32_t width, height; + int32_t refresh; +}; + +struct output_info { + struct global_info global; + + struct wl_output* output; + + struct { + int32_t x, y; + int32_t physical_width, physical_height; + enum wl_output_subpixel subpixel; + enum wl_output_transform output_transform; + char* make; + char* model; + } geometry; + + struct wl_list modes; +}; + +struct shm_format { + struct wl_list link; + + uint32_t format; +}; + +struct shm_info { + struct global_info global; + struct wl_shm* shm; + + struct wl_list formats; +}; + +struct seat_info { + struct global_info global; + struct wl_seat* seat; + + uint32_t capabilities; +}; + +struct weston_info { + struct wl_display* display; + struct wl_registry* registry; + + struct wl_list infos; + bool roundtrip_needed; +}; + +void sprint_global_info(void* data) +{ + struct global_info* global = data; + + sprintf(BUF, "interface: '%s', version: %u, name: %u\n", global->interface, global->version, + global->id); +} + +static void init_global_info(struct weston_info* info, struct global_info* global, uint32_t id, + const char* interface, uint32_t version) +{ + global->id = id; + global->version = version; + global->interface = strdup(interface); + + wl_list_insert(info->infos.prev, &global->link); +} + +void sprint_output_info(void* data) +{ + struct output_info* output = data; + struct output_mode* mode; + const char* subpixel_orientation; + const char* transform; + + sprint_global_info(data); + + switch (output->geometry.subpixel) { + case WL_OUTPUT_SUBPIXEL_UNKNOWN: + subpixel_orientation = "unknown"; + break; + case WL_OUTPUT_SUBPIXEL_NONE: + subpixel_orientation = "none"; + break; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB: + subpixel_orientation = "horizontal rgb"; + break; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR: + subpixel_orientation = "horizontal bgr"; + break; + case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB: + subpixel_orientation = "vertical rgb"; + break; + case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR: + subpixel_orientation = "vertical bgr"; + break; + default: + fprintf(stderr, "unknown subpixel orientation %u\n", output->geometry.subpixel); + subpixel_orientation = "unexpected value"; + break; + } + + switch (output->geometry.output_transform) { + case WL_OUTPUT_TRANSFORM_NORMAL: + transform = "normal"; + break; + case WL_OUTPUT_TRANSFORM_90: + transform = "90°"; + break; + case WL_OUTPUT_TRANSFORM_180: + transform = "180°"; + break; + case WL_OUTPUT_TRANSFORM_270: + transform = "270°"; + break; + case WL_OUTPUT_TRANSFORM_FLIPPED: + transform = "flipped"; + break; + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + transform = "flipped 90°"; + break; + case WL_OUTPUT_TRANSFORM_FLIPPED_180: + transform = "flipped 180°"; + break; + case WL_OUTPUT_TRANSFORM_FLIPPED_270: + transform = "flipped 270°"; + break; + default: + fprintf(stderr, "unknown output transform %u\n", output->geometry.output_transform); + transform = "unexpected value"; + break; + } + + sprintf(BUF, "\tx: %d, y: %d,\n", output->geometry.x, output->geometry.y); + sprintf(BUF, "\tphysical_width: %d mm, physical_height: %d mm,\n", + output->geometry.physical_width, output->geometry.physical_height); + sprintf(BUF, "\tmake: '%s', model: '%s',\n", output->geometry.make, output->geometry.model); + sprintf(BUF, "\tsubpixel_orientation: %s, output_tranform: %s,\n", subpixel_orientation, + transform); + + wl_list_for_each(mode, &output->modes, link) + { + sprintf(BUF, "\tmode:\n"); + sprintf(BUF, "\t\twidth: %d px, height: %d px, refresh: %.f Hz,\n", mode->width, + mode->height, (float)mode->refresh / 1000); + sprintf(BUF, "\t\tflags:"); + if (mode->flags & WL_OUTPUT_MODE_CURRENT) { + sprintf(BUF, " current"); + } + if (mode->flags & WL_OUTPUT_MODE_PREFERRED) { + sprintf(BUF, " preferred"); + } + sprintf(BUF, "\n"); + } +} + +void sprint_shm_info(void* data) +{ + struct shm_info* shm = data; + struct shm_format* format; + + sprint_global_info(data); + sprintf(BUF, "%s", "\tformats:"); + + wl_list_for_each(format, &shm->formats, link) + sprintf(BUF, " %s", (format->format == WL_SHM_FORMAT_ARGB8888) ? "ARGB8888" : "XRGB8888"); + + sprintf(BUF, "\n"); +} + +void sprint_seat_info(void* data) +{ + struct seat_info* seat = data; + + sprint_global_info(data); + sprintf(BUF, "%s", "\tcapabilities:"); + + if (seat->capabilities & WL_SEAT_CAPABILITY_POINTER) { + sprintf(BUF, " pointer"); + } + if (seat->capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { + sprintf(BUF, " keyboard"); + } + if (seat->capabilities & WL_SEAT_CAPABILITY_TOUCH) { + sprintf(BUF, " touch"); + } + + sprintf(BUF, "\n"); +} + +static void seat_handle_capabilities( + void* data, struct wl_seat* wl_seat, enum wl_seat_capability caps) +{ + struct seat_info* seat = data; + seat->capabilities = caps; +} + +static const struct wl_seat_listener seat_listener = { + seat_handle_capabilities, +}; + +static void add_seat_info(struct weston_info* info, uint32_t id, uint32_t version) +{ + struct seat_info* seat = malloc(sizeof *seat); + + init_global_info(info, &seat->global, id, "wl_seat", version); + seat->global.sprint = sprint_seat_info; + + seat->seat = wl_registry_bind(info->registry, id, &wl_seat_interface, 1); + wl_seat_add_listener(seat->seat, &seat_listener, seat); + + info->roundtrip_needed = true; +} + +static void shm_handle_format(void* data, struct wl_shm* wl_shm, uint32_t format) +{ + struct shm_info* shm = data; + struct shm_format* shm_format = malloc(sizeof *shm_format); + + wl_list_insert(&shm->formats, &shm_format->link); + shm_format->format = format; +} + +static const struct wl_shm_listener shm_listener = { + shm_handle_format, +}; + +static void add_shm_info(struct weston_info* info, uint32_t id, uint32_t version) +{ + struct shm_info* shm = malloc(sizeof *shm); + + init_global_info(info, &shm->global, id, "wl_shm", version); + shm->global.sprint = sprint_shm_info; + wl_list_init(&shm->formats); + + shm->shm = wl_registry_bind(info->registry, id, &wl_shm_interface, 1); + wl_shm_add_listener(shm->shm, &shm_listener, shm); + + info->roundtrip_needed = true; +} + +static void output_handle_geometry(void* data, struct wl_output* wl_output, int32_t x, int32_t y, + int32_t physical_width, int32_t physical_height, int32_t subpixel, const char* make, + const char* model, int32_t output_transform) +{ + struct output_info* output = data; + + output->geometry.x = x; + output->geometry.y = y; + output->geometry.physical_width = physical_width; + output->geometry.physical_height = physical_height; + output->geometry.subpixel = subpixel; + output->geometry.make = strdup(make); + output->geometry.model = strdup(model); + output->geometry.output_transform = output_transform; +} + +static void output_handle_mode(void* data, struct wl_output* wl_output, uint32_t flags, + int32_t width, int32_t height, int32_t refresh) +{ + struct output_info* output = data; + struct output_mode* mode = malloc(sizeof *mode); + + mode->flags = flags; + mode->width = width; + mode->height = height; + mode->refresh = refresh; + + wl_list_insert(output->modes.prev, &mode->link); +} + +static const struct wl_output_listener output_listener = { + output_handle_geometry, + output_handle_mode, +}; + +static void add_output_info(struct weston_info* info, uint32_t id, uint32_t version) +{ + struct output_info* output = malloc(sizeof *output); + + init_global_info(info, &output->global, id, "wl_output", version); + output->global.sprint = sprint_output_info; + + wl_list_init(&output->modes); + + output->output = wl_registry_bind(info->registry, id, &wl_output_interface, 1); + wl_output_add_listener(output->output, &output_listener, output); + + info->roundtrip_needed = true; +} + +static void add_global_info( + struct weston_info* info, uint32_t id, const char* interface, uint32_t version) +{ + struct global_info* global = malloc(sizeof *global); + + init_global_info(info, global, id, interface, version); + global->sprint = sprint_global_info; +} + +static void global_handler( + void* data, struct wl_registry* registry, uint32_t id, const char* interface, uint32_t version) +{ + struct weston_info* info = data; + + if (!strcmp(interface, "wl_seat")) + add_seat_info(info, id, version); + else if (!strcmp(interface, "wl_shm")) + add_shm_info(info, id, version); + else if (!strcmp(interface, "wl_output")) + add_output_info(info, id, version); + else + add_global_info(info, id, interface, version); +} + +static void global_remove_handler(void* data, struct wl_registry* registry, uint32_t name) {} + +static const struct wl_registry_listener registry_listener + = { global_handler, global_remove_handler }; + +void sprint_infos(struct wl_list* infos) +{ + struct global_info* info; + wl_list_for_each(info, infos, link) info->sprint(info); +} + +bool WaylandRunning() +{ + struct wl_display* dpy = wl_display_connect(NULL); + bool canConnect = (bool)dpy; + if (canConnect) { + wl_display_disconnect(dpy); + } + return canConnect; +} + +char* WaylandInfoString() +{ + buf[0] = 0; + buf[1] = 0; + + struct weston_info info; + + info.display = wl_display_connect(NULL); + if (!info.display) { + sprintf(BUF, "wayland plugin info string: wayland is not in use right now\n"); + RETBUF; + } + + wl_list_init(&info.infos); + + info.registry = wl_display_get_registry(info.display); + wl_registry_add_listener(info.registry, ®istry_listener, &info); + + do { + info.roundtrip_needed = false; + wl_display_roundtrip(info.display); + } while (info.roundtrip_needed); + + sprint_infos(&info.infos); + + if (info.display) { + wl_display_disconnect(info.display); + } + + char* str = calloc(strlen(buf) + 1, sizeof(char)); + strcpy(str, buf); + buf[0] = 0; + buf[1] = 0; + return str; +} diff --git a/wayinfo_test.go b/wayinfo_test.go new file mode 100644 index 0000000..1d57412 --- /dev/null +++ b/wayinfo_test.go @@ -0,0 +1,31 @@ +package monitor + +import ( + "fmt" + "log" + "testing" +) + +func TestWaylandMonitors(t *testing.T) { + if !WaylandCanConnect() { + log.Println("Could not connect over Wayland, skipping test") + return + } + + info, err := WaylandInfo() + if err != nil { + t.Error(err) + } + fmt.Println(info) + IDs, widths, heights, wDPIs, hDPIs := []uint{}, []uint{}, []uint{}, []uint{}, []uint{} + err = WaylandMonitors(&IDs, &widths, &heights, &wDPIs, &hDPIs) + if err != nil { + t.Error(err) + } + if len(IDs) == 0 { + t.Fatal("No monitors?") + } + for i, ID := range IDs { + fmt.Printf("monitor %d: %dx%d (DPI: %dx%d)\n", ID, widths[i], heights[i], wDPIs[i], hDPIs[i]) + } +} diff --git a/weston.go b/weston.go new file mode 100644 index 0000000..17bc3a8 --- /dev/null +++ b/weston.go @@ -0,0 +1,37 @@ +package monitor + +import ( + "errors" + "fmt" + "os" +) + +// Weston windowmanager detector +type Weston struct { +} + +func (s *Weston) Name() string { + return "Weston" +} + +func (s *Weston) ExecutablesExists() bool { + return which("weston") != "" +} + +func (s *Weston) Running() bool { + return containsE("GDMSESSION", "weston") || containsE("XDG_SESSION_DESKTOP", "weston") || containsE("XDG_CURRENT_DESKTOP", "weston") +} + +// SetWallpaper sets the desktop wallpaper, given an image filename. +// The image must exist and be readable. +func (s *Weston) SetWallpaper(imageFilename string) error { + + // TODO: Add the following to ~/.config/weston.ini (or whichever configuration file Weston uses) + fmt.Println("WESTON CONFIG FILE: ", os.Getenv("WESTON_CONFIG_FILE")) + + // [shell] + // background-image=/home/user/somewhere/image.jpg + // background-type=scale + + return errors.New("Weston currently does not support changing the desktop wallpaper at runtime") +} diff --git a/x11.go b/x11.go new file mode 100644 index 0000000..f4e273d --- /dev/null +++ b/x11.go @@ -0,0 +1,41 @@ +package monitor + +// #cgo LDFLAGS: -lX11 +// #include "xwallpaper.h" +import "C" +import ( + "errors" + "unsafe" +) + +// X11 or Xorg windowmanager detector +type X11 struct { +} + +func (s *X11) Name() string { + return "X11" +} + +func (s *X11) ExecutablesExists() bool { + return which("X") != "" +} + +func (s *X11) Running() bool { + i3 := containsE("DESKTOP_SESSION", "i3") || containsE("XDG_CURRENT_DESKTOP", "i3") || containsE("XDG_SESSION_DESKTOP", "i3") + // This method does not seem to work with i3 + return hasE("DISPLAY") && !i3 +} + +// SetWallpaper sets the desktop wallpaper, given an image filename. +// The image must exist and be readable. +// NOTE: The C counterpart to this function may exit(1) if it's out of memory +func (s *X11) SetWallpaper(imageFilename string) error { + imageFilenameC := C.CString(imageFilename) + retval := C.SetBackground(imageFilenameC) + C.free(unsafe.Pointer(imageFilenameC)) + switch retval { + case -1: + return errors.New("could not open X11 display with XOpenDisplay") + } + return nil +} diff --git a/xfce4.go b/xfce4.go new file mode 100644 index 0000000..fe8be72 --- /dev/null +++ b/xfce4.go @@ -0,0 +1,40 @@ +package monitor + +import ( + "errors" + "fmt" + "strings" +) + +// Xfce4 windowmanager detector +type Xfce4 struct { +} + +func (s *Xfce4) Name() string { + return "Xfce4" +} + +func (s *Xfce4) ExecutablesExists() bool { + return (which("xfconf-query") != "") && (which("xfce4-session") != "") +} + +func (s *Xfce4) Running() bool { + return (containsE("GDMSESSION", "xfce") || containsE("XDG_SESSION_DESKTOP", "xfce") || containsE("DESKTOP_SESSION", "xfce")) +} + +// SetWallpaper sets the desktop wallpaper, given an image filename. +// The image must exist and be readable. +func (s *Xfce4) SetWallpaper(imageFilename string) error { + properties := strings.Split(output("xfconf-query --channel xfce4-desktop --list"), "\n") + if len(properties) == 0 { + return errors.New("Could not list any properties for Xfce4") + } + for _, prop := range properties { + if strings.HasSuffix(prop, "/last-image") { + if err := run(fmt.Sprintf("xfconf-query --channel xfce4-desktop --property %s --set %q", prop, imageFilename)); err != nil { + return err + } + } + } + return nil +} diff --git a/xinfo.go b/xinfo.go new file mode 100644 index 0000000..da7875c --- /dev/null +++ b/xinfo.go @@ -0,0 +1,72 @@ +package monitor + +// #cgo LDFLAGS: -lX11 +// #include "xinfo.h" +import "C" +import ( + "errors" + "strconv" + "strings" +) + +func XCanConnect() bool { + return bool(C.X11Running()) +} + +func XInfo() (string, error) { + if !XCanConnect() { + return "", errors.New("XInfo(): not connected over X11") + } + infoString := C.GoString(C.X11InfoString()) + return infoString, nil +} + +// Monitor enumerates the monitors and returns a slice of structs, +// including the resolution. +func XMonitors(IDs, widths, heights, wDPIs, hDPIs *[]uint) error { + if !XCanConnect() { + return errors.New("XMonitors(): not connected over X11") + } + info, err := XInfo() + if err != nil { + return err + } + var counter uint + // TODO: Write a C implementation instead of parsing the info string + for _, line := range strings.Split(info, "\n") { + if strings.Contains(line, "dimensions:") { + fields := strings.Fields(line) + if len(fields) > 2 && strings.Contains(fields[1], "x") { + resFields := strings.SplitN(fields[1], "x", 2) + w, err := strconv.Atoi(resFields[0]) + if err != nil { + return err + } + h, err := strconv.Atoi(resFields[1]) + if err != nil { + return err + } + *IDs = append(*IDs, counter) + *widths = append(*widths, uint(w)) + *heights = append(*heights, uint(h)) + counter++ + } + } else if strings.Contains(line, "resolution:") { + fields := strings.Fields(line) + if len(fields) > 2 && strings.Contains(fields[1], "x") { + dpiFields := strings.SplitN(fields[1], "x", 2) + wDPI, err := strconv.Atoi(dpiFields[0]) + if err != nil { + return err + } + hDPI, err := strconv.Atoi(dpiFields[1]) + if err != nil { + return err + } + *wDPIs = append(*wDPIs, uint(wDPI)) + *hDPIs = append(*hDPIs, uint(hDPI)) + } + } + } + return nil +} diff --git a/xinfo.h b/xinfo.h new file mode 100644 index 0000000..1c99c49 --- /dev/null +++ b/xinfo.h @@ -0,0 +1,83 @@ +/* Thanks: https://stackoverflow.com/a/48843306/131264 */ +#ifdef WIN32 +#include +#endif +#include +#include +#include +#include +#include + +#define BUF buf + strlen(buf) +#define RETBUF \ + char* str = calloc(strlen(buf) + 1, sizeof(char)); \ + strcpy(str, buf); \ + return str + +static char buf[10240]; + +static void sprint_screen_info(Display* dpy, int scr) +{ + /* + * there are 2.54 centimeters to an inch; so there are 25.4 millimeters. + * + * dpi = N pixels / (M millimeters / (25.4 millimeters / 1 inch)) + * = N pixels / (M inch / 25.4) + * = N * 25.4 pixels / M inch + */ + + double xres, yres; + + xres = ((((double)DisplayWidth(dpy, scr)) * 25.4) / ((double)DisplayWidthMM(dpy, scr))); + yres = ((((double)DisplayHeight(dpy, scr)) * 25.4) / ((double)DisplayHeightMM(dpy, scr))); + + sprintf(BUF, "\n"); + sprintf(BUF, "screen #%d:\n", scr); + sprintf(BUF, " dimensions: %dx%d pixels (%dx%d millimeters)\n", XDisplayWidth(dpy, scr), + XDisplayHeight(dpy, scr), XDisplayWidthMM(dpy, scr), XDisplayHeightMM(dpy, scr)); + sprintf(BUF, " resolution: %dx%d dots per inch\n", (int)(xres + 0.5), (int)(yres + 0.5)); +} + +bool X11Running() +{ + Display* dpy = XOpenDisplay(NULL); + bool canConnect = (bool)dpy; + if (canConnect) { + XCloseDisplay(dpy); + } + return canConnect; +} + +char* X11InfoString() +{ + buf[0] = 0; + buf[1] = 0; + + Display* dpy; /* X connection */ + char* displayname = NULL; /* server to contact */ + int i; + + dpy = XOpenDisplay(displayname); + if (!dpy) { + sprintf(BUF, "unable to open display \"%s\".\n", XDisplayName(displayname)); + RETBUF; + } + + sprintf(BUF, "name of display: %s\n", DisplayString(dpy)); + sprintf(BUF, "default screen number: %d\n", DefaultScreen(dpy)); + sprintf(BUF, "number of screens: %d\n", ScreenCount(dpy)); + + for (i = 0; i < ScreenCount(dpy); i++) { + sprintf(BUF, "SCREEN %d\n", i); + sprint_screen_info(dpy, i); + } + + if (dpy) { + XCloseDisplay(dpy); + } + char* str = calloc(strlen(buf) + 1, sizeof(char)); + strcpy(str, buf); + buf[0] = 0; + buf[1] = 0; + return str; +} diff --git a/xinfo_test.go b/xinfo_test.go new file mode 100644 index 0000000..0d1413b --- /dev/null +++ b/xinfo_test.go @@ -0,0 +1,27 @@ +package monitor + +import ( + "fmt" + "log" + "testing" +) + +func TestXMonitors(t *testing.T) { + if !XCanConnect() { + log.Println("Could not connect over X, skipping test") + return + } + info, err := XInfo() + if err != nil { + t.Error(err) + } + fmt.Println(info) + IDs, widths, heights, wDPIs, hDPIs := []uint{}, []uint{}, []uint{}, []uint{}, []uint{} + err = XMonitors(&IDs, &widths, &heights, &wDPIs, &hDPIs) + if err != nil { + t.Error(err) + } + for i, ID := range IDs { + fmt.Printf("monitor %d: %dx%d (DPI: %dx%d)\n", ID, widths[i], heights[i], wDPIs[i], hDPIs[i]) + } +} diff --git a/xwallpaper.h b/xwallpaper.h new file mode 100644 index 0000000..ce7b30a --- /dev/null +++ b/xwallpaper.h @@ -0,0 +1,64 @@ +#include "X11/bitmaps/gray" +#include +#include +#include +#include +#include +#include +#include +#include + +static Display* dpy; +static int screen; +static Window root; + +static Pixmap ReadBitmapFile( + char* filename, unsigned int* width, unsigned int* height, int* x_hot, int* y_hot) +{ + Pixmap bitmap; + int status = XReadBitmapFile(dpy, root, filename, width, height, &bitmap, x_hot, y_hot); + if (status == BitmapSuccess) { + return (bitmap); + } else if (status == BitmapOpenFailed) { + fprintf(stderr, "can't open file: %s\n", filename); + } else if (status == BitmapFileInvalid) { + fprintf(stderr, "bad bitmap format file: %s\n", filename); + } + fprintf(stderr, "insufficient memory for bitmap: %s", filename); + exit(1); +} + +static void SetBackgroundToBitmap(Pixmap bitmap, unsigned int width, unsigned int height) +{ + XGCValues gc_init; + GC gc = XCreateGC(dpy, root, GCForeground | GCBackground, &gc_init); + Pixmap pix = XCreatePixmap(dpy, root, width, height, (unsigned int)DefaultDepth(dpy, screen)); + XCopyPlane(dpy, bitmap, pix, gc, 0, 0, width, height, 0, 0, (unsigned long)1); + XSetWindowBackgroundPixmap(dpy, root, pix); + XFreeGC(dpy, gc); + XFreePixmap(dpy, bitmap); + XFreePixmap(dpy, pix); + XClearWindow(dpy, root); +} + +int SetBackground(char* filename) +{ + unsigned int ww; + unsigned int hh; + + dpy = XOpenDisplay(""); + if (!dpy) { + return -1; + } + screen = DefaultScreen(dpy); + root = RootWindow(dpy, screen); + + Pixmap bitmap = ReadBitmapFile(filename, &ww, &hh, (int*)NULL, (int*)NULL); + SetBackgroundToBitmap(bitmap, ww, hh); + + if (dpy) { + XCloseDisplay(dpy); + } + + return 0; +}