├── .gitignore ├── NOTICE ├── .ccls ├── .editorconfig ├── 99-xlayoutdisplay.rules ├── .xlayoutdisplay ├── config.mk ├── src ├── layout.h ├── xrdbutil.h ├── xutil.h ├── Mode.cpp ├── xrdbutil.cpp ├── Pos.h ├── xutil.cpp ├── Settings.h ├── Mode.h ├── Edid.h ├── Monitors.h ├── xrandrrutil.h ├── util.h ├── Edid.cpp ├── Monitors.cpp ├── Output.h ├── Output.cpp ├── calculations.h ├── layout.cpp ├── xrandrrutil.cpp └── calculations.cpp ├── test ├── test-xrdbutil.cpp ├── test-MockMonitors.h ├── test-Mode.cpp ├── test-MockEdid.h ├── gtest_main.cpp ├── test-Monitors.cpp ├── test-Edid.cpp ├── test-Output.cpp ├── test-xrandrutil.cpp └── test-calculations.cpp ├── Makefile ├── README.md ├── main.cpp └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | xlayoutdisplay 2 | gtest 3 | **/*.o 4 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | xlayoutdisplay 2 | Copyright 2018 Alexander Courtis alex@courtis.org 3 | -------------------------------------------------------------------------------- /.ccls: -------------------------------------------------------------------------------- 1 | clang 2 | 3 | -DVERSION="DUMMY" 4 | 5 | -pedantic 6 | -Wall 7 | -Wextra 8 | 9 | %h -std=gnu++14 10 | %cpp -std=gnu++14 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | trim_trailing_whitespace = false 9 | 10 | -------------------------------------------------------------------------------- /99-xlayoutdisplay.rules: -------------------------------------------------------------------------------- 1 | ACTION=="change", SUBSYSTEM=="drm", ENV{HOME}="/home/username", ENV{DISPLAY}=":0", ENV{XAUTHORITY}="$env{HOME}/.Xauthority", RUN+="/bin/sh -c 'pidof -q Xorg && xlayoutdisplay -w 5 >> /tmp/xlayoutdisplay.udev.log 2>&1 || true'" 2 | -------------------------------------------------------------------------------- /.xlayoutdisplay: -------------------------------------------------------------------------------- 1 | # all options here will be overridden by command line options 2 | 3 | # mirror outputs using the lowest common resolution 4 | #mirror=true 5 | 6 | # order of outputs 7 | #order=DP-1 8 | #order=HDMI-0 9 | 10 | # primary output 11 | #primary=eDP-0 12 | 13 | # suppress output 14 | #quiet=true 15 | 16 | # override calculated DPI 17 | #dpi=192 18 | 19 | # wait seconds before acting 20 | #wait=10 21 | -------------------------------------------------------------------------------- /config.mk: -------------------------------------------------------------------------------- 1 | VERSION ?= 1.5.2-SNAPSHOT 2 | 3 | PREFIX ?= /usr/local 4 | 5 | INCS += 6 | 7 | CPPFLAGS += $(INCS) -DVERSION=\"$(VERSION)\" 8 | 9 | OFLAGS = -O3 10 | WFLAGS = -pedantic -Wall -Wextra -Werror 11 | COMPFLAGS = $(WFLAGS) $(OFLAGS) 12 | 13 | CXXFLAGS += $(COMPFLAGS) -std=c++17 14 | 15 | LDFLAGS += 16 | 17 | PKGS = x11 xcursor xrandr 18 | CXXFLAGS += $(foreach p,$(PKGS),$(shell pkg-config --cflags $(p))) 19 | LDLIBS += $(foreach p,$(PKGS),$(shell pkg-config --libs $(p))) 20 | 21 | PKGS_TEST = gmock 22 | CXXFLAGS_TEST += $(CXXFLAGS) $(foreach p,$(PKGS_TEST),$(shell pkg-config --cflags $(p))) 23 | LDLIBS_TEST += $(LDLIBS) $(foreach p,$(PKGS_TEST),$(shell pkg-config --libs $(p))) 24 | 25 | CXX = g++ 26 | -------------------------------------------------------------------------------- /src/layout.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_LAYOUT_H 17 | #define XLAYOUTDISPLAY_LAYOUT_H 18 | 19 | #include "Settings.h" 20 | 21 | int layout(const Settings &settings); 22 | 23 | #endif //XLAYOUTDISPLAY_LAYOUT_H 24 | -------------------------------------------------------------------------------- /test/test-xrdbutil.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include 17 | 18 | #include "../src/xrdbutil.h" 19 | 20 | using namespace std; 21 | 22 | TEST(xrdbutil_renderXrdbCmd, render) { 23 | EXPECT_EQ("echo \"Xft.dpi: 234\" | xrdb -merge", renderXrdbCmd(234)); 24 | } 25 | -------------------------------------------------------------------------------- /src/xrdbutil.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_XRDBUTIL_H 17 | #define XLAYOUTDISPLAY_XRDBUTIL_H 18 | 19 | #include 20 | 21 | // render an xrdb command to set "Xft.dpi" 22 | const std::string renderXrdbCmd(const long &dpi); 23 | 24 | #endif //XLAYOUTDISPLAY_XRDBUTIL_H 25 | -------------------------------------------------------------------------------- /src/xutil.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_XUTIL_H 17 | #define XLAYOUTDISPLAY_XUTIL_H 18 | 19 | // reset the cursor to "left_ptr" cursor on the root window 20 | // takes into account new Xft.dpi as well as user Xcursor theme/size settings 21 | void resetRootCursor(); 22 | 23 | #endif //XLAYOUTDISPLAY_XUTIL_H 24 | -------------------------------------------------------------------------------- /src/Mode.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include "Mode.h" 17 | 18 | bool Mode::operator<(const Mode &o) const { 19 | if (width == o.width) 20 | if (height == o.height) 21 | return refresh < o.refresh; 22 | else 23 | return height < o.height; 24 | else 25 | return width < o.width; 26 | } 27 | -------------------------------------------------------------------------------- /src/xrdbutil.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include "xrdbutil.h" 17 | 18 | #include "Output.h" 19 | 20 | #include 21 | 22 | using namespace std; 23 | 24 | const std::string renderXrdbCmd(const long &dpi) { 25 | stringstream ss; 26 | ss << "echo \"Xft.dpi: " 27 | << dpi 28 | << "\" | xrdb -merge"; 29 | return ss.str(); 30 | } 31 | -------------------------------------------------------------------------------- /src/Pos.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_POS_H 17 | #define XLAYOUTDISPLAY_POS_H 18 | 19 | #include 20 | 21 | // position of an output 22 | class Pos { 23 | public: 24 | Pos(const int &x, const int &y) : 25 | x(x), y(y) { 26 | } 27 | 28 | const int x = 0; 29 | const int y = 0; 30 | }; 31 | 32 | #endif //XLAYOUTDISPLAY_POS_H 33 | -------------------------------------------------------------------------------- /test/test-MockMonitors.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_TEST_MOCKMONITORS_H 17 | #define XLAYOUTDISPLAY_TEST_MOCKMONITORS_H 18 | 19 | #include 20 | 21 | #include "../src/Monitors.h" 22 | 23 | class MockMonitors : public Monitors { 24 | public: 25 | MOCK_CONST_METHOD1(shouldDisableOutput, bool(const std::string &name)); 26 | }; 27 | 28 | #endif //XLAYOUTDISPLAY_TEST_MOCKMONITORS_H 29 | -------------------------------------------------------------------------------- /test/test-Mode.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include 17 | 18 | #include "../src/Mode.h" 19 | 20 | using namespace std; 21 | 22 | TEST(Mode_order, width) { 23 | EXPECT_TRUE(Mode(0, 1, 2, 2) < Mode(0, 2, 1, 1)); 24 | } 25 | 26 | TEST(Mode_order, height) { 27 | EXPECT_TRUE(Mode(0, 1, 1, 2) < Mode(0, 1, 2, 1)); 28 | } 29 | 30 | TEST(Mode_order, refresh) { 31 | EXPECT_TRUE(Mode(0, 1, 1, 1) < Mode(0, 1, 1, 2)); 32 | } 33 | -------------------------------------------------------------------------------- /src/xutil.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include 17 | #include 18 | 19 | void resetRootCursor() { 20 | Display* dpy = XOpenDisplay(nullptr); 21 | int screen = DefaultScreen(dpy); 22 | Window root = RootWindow(dpy, screen); 23 | Cursor cursor = XcursorLibraryLoadCursor(dpy, "left_ptr"); 24 | XDefineCursor(dpy, root, cursor); 25 | XFreeCursor(dpy, cursor); 26 | XCloseDisplay(dpy); 27 | } 28 | -------------------------------------------------------------------------------- /src/Settings.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_SETTINGS_H 17 | #define XLAYOUTDISPLAY_SETTINGS_H 18 | 19 | #include 20 | #include 21 | 22 | // user provided settings for this utility 23 | class Settings { 24 | public: 25 | long dpi = 0; 26 | long rate = 0; 27 | bool info = false; 28 | bool noop = false; 29 | bool mirror = false; 30 | std::vector order; 31 | std::string primary; 32 | bool quiet = false; 33 | long wait = 0; 34 | }; 35 | 36 | #endif //XLAYOUTDISPLAY_SETTINGS_H 37 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include config.mk 2 | 3 | HDR = $(wildcard src/*.h) 4 | SRC = $(wildcard src/*.cpp) 5 | SRC_TEST = $(wildcard test/*.cpp) 6 | 7 | OBJ = $(SRC:.cpp=.o) 8 | OBJ_TEST = $(SRC_TEST:.cpp=.o) 9 | 10 | all: xlayoutdisplay 11 | 12 | $(OBJ): $(HDR) config.mk Makefile 13 | main.o: $(HDR) config.mk Makefile 14 | 15 | xlayoutdisplay: $(OBJ) main.o 16 | $(CXX) $(CXXFLAGS) $(CPPFLAGS) -o $(@) $(^) $(LDLIBS) 17 | 18 | $(OBJ_TEST): $(HDR) config.mk Makefile 19 | $(CXX) $(CXXFLAGS_TEST) $(CPPFLAGS) -c -o $(@) $(@:.o=.cpp) 20 | 21 | gtest: $(OBJ) $(OBJ_TEST) 22 | $(CXX) $(CXXFLAGS_TEST) $(CPPFLAGS) -o $(@) $(^) $(LDLIBS) $(LDLIBS_TEST) 23 | ./gtest 24 | 25 | clean: 26 | rm -f xlayoutdisplay main.o $(OBJ) $(OBJ_TEST) 27 | 28 | install: xlayoutdisplay 29 | mkdir -p $(DESTDIR)$(PREFIX)/bin 30 | cp -f xlayoutdisplay $(DESTDIR)$(PREFIX)/bin 31 | chmod 755 $(DESTDIR)$(PREFIX)/bin/xlayoutdisplay 32 | 33 | uninstall: 34 | rm -f $(DESTDIR)$(PREFIX)/bin/xlayoutdisplay 35 | 36 | # https://github.com/alex-courtis/arch/blob/b530f331dacaaba27484593a87ca20a9f53ab73f/home/bin/ctags-something 37 | ctags: 38 | ctags-c++ $(CPPFLAGS) --project-src $(HDR) $(SRC) $(SRC_TEST) main.cpp 39 | 40 | .PHONY: all clean test install uninstall ctags 41 | 42 | -------------------------------------------------------------------------------- /test/test-MockEdid.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_TEST_MOCKEDID_H 17 | #define XLAYOUTDISPLAY_TEST_MOCKEDID_H 18 | 19 | #include 20 | 21 | #include "../src/Edid.h" 22 | 23 | class MockEdid : public Edid { 24 | public: 25 | MockEdid() : Edid( 26 | reinterpret_cast("01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF"), 27 | EDID_MIN_LENGTH, "MockEdid") {}; 28 | 29 | MOCK_CONST_METHOD0(maxCmHoriz, unsigned int()); 30 | 31 | MOCK_CONST_METHOD0(maxCmVert, unsigned int()); 32 | 33 | MOCK_CONST_METHOD1(dpiForMode, long(const std::shared_ptr &mode)); 34 | }; 35 | 36 | #endif //XLAYOUTDISPLAY_TEST_MOCKEDID_H 37 | -------------------------------------------------------------------------------- /src/Mode.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_MODE_H 17 | #define XLAYOUTDISPLAY_MODE_H 18 | 19 | #include 20 | 21 | // a mode that may be used by an Xrandr display 22 | class Mode { 23 | public: 24 | Mode(const RRMode &rrMode, 25 | const unsigned int &width, 26 | const unsigned int &height, 27 | const unsigned int &refresh) : 28 | rrMode(rrMode), 29 | width(width), 30 | height(height), 31 | refresh(refresh) { 32 | } 33 | 34 | // order by width, height, refresh 35 | bool operator<(const Mode &o) const; 36 | 37 | const RRMode rrMode; 38 | const unsigned int width; 39 | const unsigned int height; 40 | const unsigned int refresh; 41 | }; 42 | 43 | #endif //XLAYOUTDISPLAY_MODE_H 44 | -------------------------------------------------------------------------------- /src/Edid.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_EDID_H 17 | #define XLAYOUTDISPLAY_EDID_H 18 | 19 | #include 20 | #include "Mode.h" 21 | 22 | #define EDID_MIN_LENGTH 128 23 | #define EDID_BYTE_MAX_CM_HORIZ 0x15 24 | #define EDID_BYTE_MAX_CM_VERT 0x16 25 | 26 | class Edid { 27 | public: 28 | // we only need the first 128 bytes of EDID - the basic structure 29 | // throws invalid_argument: 30 | // when length < EDID_MIN_LENGTH 31 | Edid(const unsigned char *edid, size_t length, const char *name); 32 | 33 | virtual ~Edid(); 34 | 35 | virtual unsigned int maxCmHoriz() const; 36 | 37 | virtual unsigned int maxCmVert() const; 38 | 39 | // nearest 12 40 | virtual long dpiForMode(const std::shared_ptr &mode) const; 41 | 42 | private: 43 | unsigned char *edid = nullptr; 44 | }; 45 | 46 | #endif //XLAYOUTDISPLAY_EDID_H 47 | -------------------------------------------------------------------------------- /src/Monitors.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_MONITORS_H 17 | #define XLAYOUTDISPLAY_MONITORS_H 18 | 19 | #include 20 | 21 | #define LAPTOP_OUTPUT_PREFIX "eDP" 22 | #define LAPTOP_LID_ROOT_PATH "/proc/acpi/button/lid" 23 | 24 | 25 | // return true if we have a "closed" status under laptopLidRootPath 26 | bool calculateLaptopLidClosed(const char *laptopLidRootPath); 27 | 28 | 29 | // calculates and holds state about attached monitors 30 | class Monitors { 31 | public: 32 | Monitors() : laptopLidClosed(calculateLaptopLidClosed(LAPTOP_LID_ROOT_PATH)) {} 33 | 34 | // return true if the output should be disabled i.e. lid closed and name begins with LAPTOP_OUPUT_PREFIX 35 | virtual bool shouldDisableOutput(const std::string &name) const; 36 | 37 | // true if the laptop lid is closed 38 | const bool laptopLidClosed; 39 | }; 40 | 41 | #endif //XLAYOUTDISPLAY_MONITORS_H 42 | -------------------------------------------------------------------------------- /src/xrandrrutil.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_XRANDRUTIL_H 17 | #define XLAYOUTDISPLAY_XRANDRUTIL_H 18 | 19 | #include "Output.h" 20 | 21 | // v refresh frequency in even Hz, zero if modeInfo is NULL 22 | unsigned int refreshFromModeInfo(const XRRModeInfo &modeInfo); 23 | 24 | // render xrandr cmd to layout outputs 25 | // will activate only if desiredActive, desiredMode, desiredPos are set 26 | // desiredPrimary is only set if activated 27 | const std::string renderXrandrCmd(const std::list> &outputs, const std::shared_ptr &primary, const long &dpi, const long &rate); 28 | 29 | // throws invalid_argument: 30 | // null resources 31 | // id not found in resources 32 | Mode *modeFromXRR(RRMode id, const XRRScreenResources *resources); 33 | 34 | // build a list of Output based on the current and possible state of the world 35 | const std::list> discoverOutputs(); 36 | 37 | #endif //XLAYOUTDISPLAY_XRANDRUTIL_H 38 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_STDUTIL_H 17 | #define XLAYOUTDISPLAY_STDUTIL_H 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | // sorting function for shared pointers... this must be in STL somewhere... 24 | template 25 | inline bool sortSharedPtr(const std::shared_ptr &l, const std::shared_ptr &r) { 26 | return (*l) < (*r); 27 | } 28 | 29 | // copy list of shared_ptr, reverse sort it, return it as const 30 | template 31 | inline const std::list> reverseSort(const std::list> &list) { 32 | std::list> sorted = list; 33 | sorted.sort(sortSharedPtr); 34 | sorted.reverse(); 35 | return sorted; 36 | } 37 | 38 | // return an absolute UNIX path for a relative path under env 39 | inline const std::string resolveEnvPath(const char *env, const char *homeRelativePath) { 40 | char settingsFilePath[PATH_MAX]; 41 | snprintf(settingsFilePath, PATH_MAX, "%s/%s", getenv(env), homeRelativePath); 42 | return std::string(settingsFilePath); 43 | } 44 | 45 | #endif //XLAYOUTDISPLAY_STDUTIL_H 46 | -------------------------------------------------------------------------------- /test/gtest_main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2006, Google Inc. 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are 6 | // met: 7 | // 8 | // * Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer. 10 | // * Redistributions in binary form must reproduce the above 11 | // copyright notice, this list of conditions and the following disclaimer 12 | // in the documentation and/or other materials provided with the 13 | // distribution. 14 | // * Neither the name of Google Inc. nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | #include 31 | #include "gtest/gtest.h" 32 | 33 | GTEST_API_ int main(int argc, char **argv) { 34 | printf("Running main() from %s\n", __FILE__); 35 | testing::InitGoogleTest(&argc, argv); 36 | return RUN_ALL_TESTS(); 37 | } 38 | -------------------------------------------------------------------------------- /src/Edid.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include "Edid.h" 17 | 18 | #include 19 | #include 20 | #include 21 | 22 | using namespace std; 23 | 24 | #define INCHES_PER_CM 2.54 // apparently this is exact 25 | 26 | Edid::Edid(const unsigned char *edid, const size_t length, const char *name) { 27 | if (length < EDID_MIN_LENGTH) 28 | throw invalid_argument(string(name) + " has Edid size " + to_string(length) + ", expected at least " + 29 | to_string(EDID_MIN_LENGTH)); 30 | 31 | this->edid = (unsigned char *) malloc(length); 32 | memcpy(this->edid, edid, length); 33 | } 34 | 35 | Edid::~Edid() { 36 | free(edid); 37 | } 38 | 39 | unsigned int Edid::maxCmHoriz() const { 40 | return edid[EDID_BYTE_MAX_CM_HORIZ]; 41 | } 42 | 43 | unsigned int Edid::maxCmVert() const { 44 | return edid[EDID_BYTE_MAX_CM_VERT]; 45 | } 46 | 47 | long Edid::dpiForMode(const std::shared_ptr &mode) const { 48 | if (maxCmVert() == 0 || maxCmHoriz() == 0) { 49 | return 0; 50 | } 51 | double dpiHoriz = mode->width * INCHES_PER_CM / maxCmHoriz(); 52 | double dpiVert = mode->height * INCHES_PER_CM / maxCmVert(); 53 | 54 | // nearest 12 dpi 55 | return lround((dpiHoriz + dpiVert) / 24) * 12; 56 | } 57 | 58 | -------------------------------------------------------------------------------- /test/test-Monitors.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include 17 | 18 | #include "../src/Monitors.h" 19 | 20 | using namespace std; 21 | 22 | class Monitors_calculateLaptopLidClosed : public ::testing::Test { 23 | protected: 24 | void TearDown() override { 25 | // always try and remove anything from createStateFile 26 | remove("./lid/LIDX/state"); 27 | rmdir("./lid/LIDX"); 28 | rmdir("./lid"); 29 | } 30 | 31 | void createStateFile(const char *contents) { 32 | ASSERT_EQ(0, mkdir("./lid", 0755)); 33 | ASSERT_EQ(0, mkdir("./lid/LIDX", 0755)); 34 | FILE *lidStateFile = fopen("./lid/LIDX/state", "w"); 35 | ASSERT_TRUE(lidStateFile != nullptr); 36 | fputs(contents, lidStateFile); 37 | ASSERT_EQ(0, fclose(lidStateFile)); 38 | }; 39 | }; 40 | 41 | TEST_F(Monitors_calculateLaptopLidClosed, notClosedMissingFile) { 42 | EXPECT_FALSE(calculateLaptopLidClosed("./nonexistent")); 43 | } 44 | 45 | TEST_F(Monitors_calculateLaptopLidClosed, open) { 46 | createStateFile("something OpEn something something\n"); 47 | EXPECT_FALSE(calculateLaptopLidClosed("./lid")); 48 | } 49 | 50 | TEST_F(Monitors_calculateLaptopLidClosed, closed) { 51 | createStateFile("something ClOsEd something something\n"); 52 | EXPECT_TRUE(calculateLaptopLidClosed("./lid")); 53 | } 54 | -------------------------------------------------------------------------------- /src/Monitors.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include "Monitors.h" 17 | 18 | #include 19 | #include 20 | 21 | bool calculateLaptopLidClosed(const char *laptopLidRootPath) { 22 | static char lidFileName[PATH_MAX]; 23 | static char line[512]; 24 | 25 | // find the lid state directory 26 | DIR *dir = opendir(laptopLidRootPath); 27 | if (dir) { 28 | struct dirent *dirent; 29 | while ((dirent = readdir(dir)) != nullptr) { 30 | if (dirent->d_type == DT_DIR && strcmp(dirent->d_name, ".") != 0 && strcmp(dirent->d_name, "..") != 0) { 31 | 32 | // read the lid state file 33 | snprintf(lidFileName, PATH_MAX, "%s/%s/%s", laptopLidRootPath, dirent->d_name, "state"); 34 | FILE *lidFile = fopen(lidFileName, "r"); 35 | if (lidFile != nullptr) { 36 | if (fgets(line, 512, lidFile)) 37 | if (strcasestr(line, "closed")) 38 | return true; 39 | fclose(lidFile); 40 | } 41 | 42 | // drivers/acpi/button.c acpi_button_add_fs seems to indicate there will be only one file 43 | break; 44 | } 45 | } 46 | closedir(dir); 47 | } 48 | return false; 49 | } 50 | 51 | bool Monitors::shouldDisableOutput(const std::string &name) const { 52 | return laptopLidClosed && strncasecmp(LAPTOP_OUTPUT_PREFIX, name.c_str(), strlen(LAPTOP_OUTPUT_PREFIX)) == 0; 53 | } 54 | -------------------------------------------------------------------------------- /src/Output.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_DISPL_H 17 | #define XLAYOUTDISPLAY_DISPL_H 18 | 19 | #include "Mode.h" 20 | #include "Pos.h" 21 | #include "Edid.h" 22 | #include "Monitors.h" 23 | 24 | #include 25 | #include 26 | 27 | // a single Xrandr output 28 | class Output { 29 | public: 30 | enum State { 31 | active, connected, disconnected 32 | }; 33 | 34 | // throws invalid_argument: 35 | // active must have: currentMode, currentPos, modes 36 | // connected must have: modes 37 | // active/connected must have: empty or currentMode/preferredMode in modes 38 | // modes will be ordered descending 39 | // optimalMode will be set to highest refresh preferredMode, then highest mode, then empty 40 | Output(const std::string &name, 41 | const State &state, 42 | const std::list> &modes, 43 | const std::shared_ptr ¤tMode, 44 | const std::shared_ptr &preferredMode, 45 | const std::shared_ptr ¤tPos, 46 | const std::shared_ptr &edid); 47 | 48 | const std::string name; 49 | const State state; 50 | const std::list> modes; 51 | const std::shared_ptr currentMode; 52 | const std::shared_ptr preferredMode; 53 | const std::shared_ptr optimalMode; 54 | const std::shared_ptr currentPos; 55 | const std::shared_ptr edid; 56 | 57 | bool desiredActive = false; 58 | std::shared_ptr desiredMode; 59 | std::shared_ptr desiredPos; 60 | }; 61 | 62 | 63 | #endif //XLAYOUTDISPLAY_DISPL_H 64 | -------------------------------------------------------------------------------- /src/Output.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include "Output.h" 17 | #include "calculations.h" 18 | 19 | #include 20 | #include 21 | 22 | using namespace std; 23 | 24 | Output::Output(const string &name, 25 | const State &state, 26 | const list> &modes, 27 | const shared_ptr ¤tMode, 28 | const shared_ptr &preferredMode, 29 | const shared_ptr ¤tPos, 30 | const shared_ptr &edid) : 31 | name(name), 32 | state(state), 33 | modes(modes), 34 | currentMode(currentMode), 35 | preferredMode(preferredMode), 36 | optimalMode(calculateOptimalMode(modes, preferredMode)), 37 | currentPos(currentPos), 38 | edid(edid) { 39 | switch (state) { 40 | case active: 41 | if (!currentMode) throw invalid_argument("active Output '" + name + "' has no currentMode"); 42 | if (!currentPos) throw invalid_argument("active Output '" + name + "' has no currentPos"); 43 | if (modes.empty()) throw invalid_argument("active Output '" + name + "' has no modes"); 44 | break; 45 | case connected: 46 | if (modes.empty()) throw invalid_argument("connected Output '" + name + "' has no modes"); 47 | break; 48 | default: 49 | break; 50 | } 51 | 52 | // active / connected must have NULL or valid preferred mode 53 | if (state == active || state == connected) { 54 | if (preferredMode && find(this->modes.begin(), this->modes.end(), preferredMode) == this->modes.end()) 55 | throw invalid_argument("Output '" + name + "' has preferredMode not present in modes"); 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /test/test-Edid.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include 17 | 18 | #include "../src/Edid.h" 19 | 20 | using namespace std; 21 | 22 | TEST(Edid_constructor, gteEdid) { 23 | const size_t len = EDID_MIN_LENGTH * 3 / 2; 24 | unsigned char val[len]; 25 | memset(val, 1, len); 26 | 27 | Edid(val, len, "blargh"); 28 | } 29 | 30 | TEST(Edid_constructor, ltEdid) { 31 | const size_t len = EDID_MIN_LENGTH - 1; 32 | unsigned char val[len]; 33 | memset(val, 1, len); 34 | 35 | EXPECT_THROW(Edid(val, len, "blargh"), invalid_argument); 36 | } 37 | 38 | class Edid_measurements : public ::testing::Test { 39 | protected: 40 | Edid_measurements() { 41 | memset(val, 1, EDID_MIN_LENGTH); 42 | } 43 | 44 | unsigned char val[EDID_MIN_LENGTH]{}; 45 | }; 46 | 47 | TEST_F(Edid_measurements, valid) { 48 | val[EDID_BYTE_MAX_CM_HORIZ] = 2; 49 | val[EDID_BYTE_MAX_CM_VERT] = 3; 50 | Edid edid = Edid(val, EDID_MIN_LENGTH, "valid"); 51 | 52 | EXPECT_EQ(2, edid.maxCmHoriz()); 53 | EXPECT_EQ(3, edid.maxCmVert()); 54 | EXPECT_EQ(180, edid.dpiForMode(make_shared(0, 123, 234, 0))); 55 | } 56 | 57 | TEST_F(Edid_measurements, zeroHoriz) { 58 | val[EDID_BYTE_MAX_CM_HORIZ] = 0; 59 | val[EDID_BYTE_MAX_CM_VERT] = 3; 60 | Edid edid = Edid(val, EDID_MIN_LENGTH, "zeroHoriz"); 61 | 62 | EXPECT_EQ(0, edid.maxCmHoriz()); 63 | EXPECT_EQ(3, edid.maxCmVert()); 64 | EXPECT_EQ(0, edid.dpiForMode(make_shared(0, 123, 234, 0))); 65 | } 66 | 67 | TEST_F(Edid_measurements, zeroVert) { 68 | val[EDID_BYTE_MAX_CM_HORIZ] = 2; 69 | val[EDID_BYTE_MAX_CM_VERT] = 0; 70 | Edid edid = Edid(val, EDID_MIN_LENGTH, "zeroVert"); 71 | 72 | EXPECT_EQ(2, edid.maxCmHoriz()); 73 | EXPECT_EQ(0, edid.maxCmVert()); 74 | EXPECT_EQ(0, edid.dpiForMode(make_shared(0, 123, 234, 0))); 75 | } 76 | -------------------------------------------------------------------------------- /src/calculations.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #ifndef XLAYOUTDISPLAY_CALCULATIONS_H 17 | #define XLAYOUTDISPLAY_CALCULATIONS_H 18 | 19 | #include 20 | #include "Output.h" 21 | 22 | #define DEFAULT_DPI 96 23 | 24 | // reorder outputs putting those whose names match order at the front, case insensitive 25 | const std::list> orderOutputs(const std::list> &outputs, const std::vector &order); 26 | 27 | // mark outputs that should be activated and return the nonempty primary 28 | // throws invalid_argument: 29 | // when outputs is empty 30 | const std::shared_ptr activateOutputs(const std::list> &outputs, 31 | const std::string &desiredPrimary, const Monitors &monitors); 32 | 33 | // arrange outputs left to right at optimal mode; will mutate contents 34 | void ltrOutputs(const std::list> &outputs); 35 | 36 | // arrange outputs so that they all mirror at highest common mode; will mutate contents 37 | // throws runtime_error: 38 | // no common mode found 39 | void mirrorOutputs(const std::list> &outputs); 40 | 41 | // render a user readable string explaining the current state of outputs 42 | const std::string renderUserInfo(const std::list> &outputs); 43 | 44 | // calculate the DPI for the output given 45 | // use only the horiz/vert cm values from EDID - they are intentionally zero for projectors and some tvs 46 | // if horiz/vert values are unavailable or zero, return DEFAULT_DPI 47 | // throws invalid_argument: 48 | // when output is empty 49 | long calculateDpi(const std::shared_ptr &output, std::string *explaination); 50 | 51 | // retrieve the highest resolution/refresh mode from a list of modes, using the highest refresh rate of preferredMode, if available 52 | const std::shared_ptr calculateOptimalMode(const std::list> &modes, 53 | const std::shared_ptr &preferredMode); 54 | 55 | #endif //XLAYOUTDISPLAY_CALCULATIONS_H 56 | -------------------------------------------------------------------------------- /test/test-Output.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include 17 | 18 | #include "../src/Output.h" 19 | 20 | using namespace std; 21 | 22 | class Output_test : public ::testing::Test { 23 | protected: 24 | shared_ptr mode1 = make_shared(0, 1, 2, 3); 25 | shared_ptr mode2 = make_shared(4, 5, 6, 7); 26 | shared_ptr modeInexistent = make_shared(5, 4, 3, 2); 27 | 28 | shared_ptr pos = make_shared(0, 0); 29 | shared_ptr edid; 30 | list> modes = {mode1, mode2}; 31 | }; 32 | 33 | TEST_F(Output_test, validActive) { 34 | Output("validActive", Output::active, modes, mode1, nullptr, pos, edid); 35 | } 36 | 37 | TEST_F(Output_test, validConnected) { 38 | Output("validConnected", Output::connected, modes, nullptr, nullptr, nullptr, edid); 39 | } 40 | 41 | TEST_F(Output_test, validDisconnected) { 42 | Output("validDisconnected", Output::disconnected, list>(), nullptr, nullptr, nullptr, edid); 43 | } 44 | 45 | TEST_F(Output_test, activeMissingCurrentMode) { 46 | EXPECT_THROW(Output("activeMissingCurrentMode", Output::active, modes, nullptr, nullptr, pos, edid), invalid_argument); 47 | } 48 | 49 | TEST_F(Output_test, activeMissingCurrentPos) { 50 | EXPECT_THROW(Output("activeMissingCurrentPos", Output::active, modes, mode1, nullptr, nullptr, edid), invalid_argument); 51 | } 52 | 53 | TEST_F(Output_test, activeEmptyModes) { 54 | EXPECT_THROW(Output("activeEmptyModes", Output::active, list>(), mode1, nullptr, pos, edid), invalid_argument); 55 | } 56 | 57 | TEST_F(Output_test, connectedEmptyModes) { 58 | EXPECT_THROW(Output("connectedEmptyModes", Output::connected, list>(), nullptr, nullptr, nullptr, edid), invalid_argument); 59 | } 60 | 61 | TEST_F(Output_test, activePreferredNotInModes) { 62 | EXPECT_THROW(Output("activePreferredNotInModes", Output::active, modes, nullptr, modeInexistent, nullptr, edid), invalid_argument); 63 | } 64 | 65 | TEST_F(Output_test, connectedPreferredNotInModes) { 66 | EXPECT_THROW(Output("connectedPreferredNotInModes", Output::connected, modes, nullptr, modeInexistent, nullptr, edid), invalid_argument); 67 | } 68 | 69 | TEST_F(Output_test, disconnectedPreferredNotInModes) { 70 | Output("disconnectedPreferredNotInModes", Output::disconnected, modes, nullptr, modeInexistent, nullptr, edid); 71 | } 72 | -------------------------------------------------------------------------------- /src/layout.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include "layout.h" 17 | 18 | #include "xrandrrutil.h" 19 | #include "xrdbutil.h" 20 | #include "xutil.h" 21 | #include "calculations.h" 22 | #include 23 | #include 24 | 25 | using namespace std; 26 | 27 | int layout(const Settings& settings) { 28 | // optional wait 29 | if (settings.wait) { 30 | if (!settings.quiet) { 31 | cout << "Waiting " << settings.wait << " seconds..." << endl; 32 | } 33 | sleep(settings.wait); 34 | } 35 | 36 | // discover monitors 37 | const Monitors monitors = Monitors(); 38 | 39 | // discover outputs 40 | const list> currentOutputs = discoverOutputs(); 41 | if (currentOutputs.empty()) { 42 | throw runtime_error("no outputs found"); 43 | } 44 | 45 | // output verbose information 46 | if (!settings.quiet || settings.info) { 47 | cout << renderUserInfo(currentOutputs) << "\n\n"; 48 | cout << "laptop lid "; 49 | if (monitors.laptopLidClosed) { 50 | cout << "closed"; 51 | } 52 | else { 53 | cout << "open or not present"; 54 | } 55 | cout << "\n"; 56 | } 57 | 58 | // current info is all output, we're done 59 | if (settings.info) { 60 | return EXIT_SUCCESS; 61 | } 62 | 63 | // order the outputs if the user wishes 64 | const list> outputs = orderOutputs(currentOutputs, settings.order); 65 | 66 | // activate ouputs and determine primary 67 | const shared_ptr primary = activateOutputs(outputs, settings.primary, monitors); 68 | 69 | // arrange mirrored or left to right 70 | if (settings.mirror) { 71 | mirrorOutputs(outputs); 72 | } 73 | else { 74 | ltrOutputs(outputs); 75 | } 76 | 77 | // determine DPI from the primary 78 | string dpiExplaination; 79 | long dpi = calculateDpi(primary, &dpiExplaination); 80 | if (!settings.quiet) { 81 | cout << "\n" << dpiExplaination << "\n"; 82 | } 83 | 84 | // user overrides DPI 85 | if (settings.dpi) { 86 | dpi = settings.dpi; 87 | cout << "overriding with provided DPI " << to_string(dpi) << "\n"; 88 | } 89 | 90 | // user overrides refresh rate 91 | long rate = 0; 92 | if (settings.rate) { 93 | rate = settings.rate; 94 | cout << "overriding with provided refresh rate " << to_string(rate) << "\n"; 95 | } 96 | 97 | // render desired commands 98 | const string xrandrCmd = renderXrandrCmd(outputs, primary, dpi, rate); 99 | const string xrdbCmd = renderXrdbCmd(dpi); 100 | if (!settings.quiet || settings.noop) { 101 | cout << "\n" << xrandrCmd << "\n\n" << xrdbCmd << "\n"; 102 | } 103 | 104 | // execute 105 | if (!settings.noop) { 106 | // xrandr 107 | int rc = system(xrandrCmd.c_str()); 108 | if (rc != 0) { 109 | return rc; 110 | } 111 | 112 | // xrdb 113 | rc = system(xrdbCmd.c_str()); 114 | if (rc != 0) { 115 | return rc; 116 | } 117 | 118 | // update root window's cursor 119 | resetRootCursor(); 120 | } 121 | return EXIT_SUCCESS; 122 | } 123 | -------------------------------------------------------------------------------- /test/test-xrandrutil.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include 17 | #include 18 | 19 | #include "../src/xrandrrutil.h" 20 | 21 | #include "test-MockEdid.h" 22 | 23 | using namespace std; 24 | using ::testing::Eq; 25 | using ::testing::Return; 26 | 27 | TEST(xrandrutil_renderXrandrCmd, renderAll) { 28 | list> outputs; 29 | list> modes = {make_shared(0, 0, 0, 0)}; 30 | 31 | shared_ptr output1 = make_shared("One", Output::disconnected, modes, shared_ptr(), 32 | shared_ptr(), shared_ptr(), shared_ptr()); 33 | outputs.push_back(output1); 34 | 35 | shared_ptr edid2 = make_shared(); 36 | shared_ptr mode2 = make_shared(0, 1, 2, 3); 37 | shared_ptr output2 = make_shared("Two", Output::disconnected, list>({mode2}), 38 | shared_ptr(), shared_ptr(), shared_ptr(), edid2); 39 | output2->desiredActive = true; 40 | output2->desiredMode = mode2; 41 | output2->desiredPos = make_shared(5, 6); 42 | outputs.push_back(output2); 43 | 44 | shared_ptr output3 = make_shared("Three", Output::disconnected, modes, shared_ptr(), 45 | shared_ptr(), shared_ptr(), shared_ptr()); 46 | output3->desiredActive = true; 47 | output3->desiredPos = make_shared(13, 14); 48 | outputs.push_back(output3); 49 | 50 | shared_ptr mode4 = make_shared(15, 16, 17, 18); 51 | shared_ptr output4 = make_shared("Four", Output::disconnected, list>({mode4}), 52 | shared_ptr(), shared_ptr(), shared_ptr(), 53 | shared_ptr()); 54 | output4->desiredActive = true; 55 | output4->desiredMode = mode4; 56 | outputs.push_back(output4); 57 | 58 | shared_ptr mode5 = make_shared(7, 8, 9, 10); 59 | shared_ptr output5 = make_shared("Five", Output::disconnected, list>({mode5}), 60 | shared_ptr(), shared_ptr(), shared_ptr(), 61 | shared_ptr()); 62 | output5->desiredActive = true; 63 | output5->desiredMode = mode5; 64 | output5->desiredPos = make_shared(11, 12); 65 | outputs.push_back(output5); 66 | 67 | stringstream expected; 68 | expected << "xrandr \\\n"; 69 | expected << " --dpi 123 \\\n"; 70 | expected << " --output One --off \\\n"; 71 | expected << " --output Two --mode 1x2 --rate 60 --pos 5x6 --primary \\\n"; 72 | expected << " --output Three --off \\\n"; 73 | expected << " --output Four --off \\\n"; 74 | expected << " --output Five --mode 8x9 --rate 60 --pos 11x12"; 75 | 76 | EXPECT_EQ(expected.str(), renderXrandrCmd(outputs, output2, 123, 60)); 77 | } 78 | 79 | class xrandrutil_modeFromXRR : public ::testing::Test { 80 | protected: 81 | virtual void SetUp() { 82 | resources.nmode = 3; 83 | resources.modes = &modeInfos[0]; 84 | 85 | modeInfos[0].id = 10; 86 | modeInfos[1].id = 11; 87 | modeInfos[1].width = 111; 88 | modeInfos[1].height = 112; 89 | modeInfos[2].id = 12; 90 | } 91 | 92 | XRRScreenResources resources {}; 93 | XRRModeInfo modeInfos[3] {}; 94 | }; 95 | 96 | TEST_F(xrandrutil_modeFromXRR, valid) { 97 | Mode *mode = modeFromXRR(11, &resources); 98 | 99 | ASSERT_THAT(mode->rrMode, Eq(11)); 100 | ASSERT_THAT(mode->width, Eq(111)); 101 | ASSERT_THAT(mode->height, Eq(112)); 102 | 103 | delete(mode); 104 | } 105 | 106 | TEST_F(xrandrutil_modeFromXRR, modeNotPresent) { 107 | EXPECT_THROW(modeFromXRR(13, &resources), invalid_argument); 108 | } 109 | 110 | TEST_F(xrandrutil_modeFromXRR, resourcesNotPresent) { 111 | EXPECT_THROW(modeFromXRR(11, nullptr), invalid_argument); 112 | } 113 | 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xlayoutdisplay 2 | 3 | Detects and arranges outputs for an X display, using [XRandR](https://www.x.org/wiki/Projects/XRandR/) for detection and [xrandr](https://wiki.archlinux.org/index.php/xrandr) for arrangement. 4 | 5 | Highest refresh rate of the output's preferred resolution are used. 6 | 7 | Left-to-right ordering is used, unless the user specifies mirrorred outputs. 8 | 9 | Laptop displays `eDP.*` are disabled when the lid is closed. 10 | 11 | Wayland equivalent: [way-displays](https://github.com/alex-courtis/way-displays). 12 | 13 | ## Usage 14 | 15 | ``` 16 | Arranges outputs in a left to right manner, using highest resolution and refresh. 17 | DPI is calculated based on the first or primary output's EDID information and rounded to the nearest 12. 18 | Laptop outputs are turned off when the lid is closed. 19 | 20 | e.g. xlayoutdisplay -p DP-4 -o HDMI-0 -o DP-4 21 | 22 | CLI: 23 | -h [ --help ] print this help text and exit 24 | -i [ --info ] print information about current outputs and exit 25 | -n [ --noop ] perform a trial run and exit 26 | -v [ --version ] print version string 27 | 28 | CLI, $XDG_CONFIG_HOME/.xlayoutdisplay, $HOME/.xlayoutdisplay and /etc/xlayoutdisplay: 29 | -d [ --dpi ] arg DPI override 30 | -r [ --rate ] arg Refresh rate override 31 | -m [ --mirror ] mirror outputs using the lowest common resolution 32 | -o [ --order ] arg order of outputs, repeat as needed 33 | -p [ --primary ] arg primary output 34 | -q [ --quiet ] suppress feedback 35 | -w [ --wait ] arg wait seconds before running 36 | ``` 37 | 38 | ## Installation 39 | 40 | Package Manager: 41 | 42 | [![Packaging status](https://repology.org/badge/vertical-allrepos/xlayoutdisplay.svg)](https://repology.org/project/xlayoutdisplay/versions) 43 | 44 | or 45 | 46 | [Build From Source](#build) 47 | 48 | ``` 49 | git clone https://github.com/alex-courtis/xlayoutdisplay.git 50 | cd xlayoutdisplay 51 | make 52 | sudo make install 53 | ``` 54 | 55 | ## Configuration File 56 | 57 | `$XDG_CONFIG_HOME/.xlayoutdisplay` then `$HOME/.xlayoutdisplay` then `/etc/xlayoutdisplay` may be used to provide defaults, which will be overwritten by CLI options. 58 | 59 | See [xlayoutdisplay](.xlayoutdisplay) 60 | 61 | ## Automatically detect new display 62 | 63 | To automatically run `xlayoutdisplay` whenever a new display is plugged in or unplugged, `udev` can be used. 64 | 65 | A sample rule can be found at [99-xlayoutdisplay.rules](99-xlayoutdisplay.rules). 66 | Simply update the value of `ENV{HOME}`, and copy the customized file to `/etc/udev/rules.d/`. 67 | `pidof` from `procps-ng` is required. 68 | 69 | The wait time is necessary to allow Xorg time to enumerate new monitors. 5 seconds is a conservative value; experiment with smaller values for better response time. 70 | 71 | Additional informations can be found at the [`udev` Arch Wiki article](https://wiki.archlinux.org/title/Udev#Execute_when_HDMI_cable_is_plugged_in_or_unplugged). 72 | 73 | ## Sample Output 74 | 75 | DP-4 is the only ouput, then HDMI-0 is plugged in. 76 | 77 | `xlayoutdisplay -p DP-4 -o HDMI-0 -o DP-4` 78 | 79 | HDMI-0 is enabled to the left of DP-4, however DP-4 is still the primary output that determines DPI. 80 | 81 | ``` 82 | DVI-D-0 disconnected 83 | HDMI-0 connected 0cm/0cm 84 | + 1920x1080 50Hz 85 | 2880x576 50Hz 86 | 2880x576 50Hz 87 | 2880x480 60Hz 88 | 2880x480 60Hz 89 | 1920x1080 60Hz 90 | 1920x1080 60Hz 91 | 1920x1080 24Hz 92 | 1920x1080 60Hz 93 | !1920x1080 60Hz 94 | 1920x1080 50Hz 95 | 1440x576 50Hz 96 | 1440x480 60Hz 97 | 1280x720 60Hz 98 | 1280x720 50Hz 99 | 720x576 50Hz 100 | 720x480 60Hz 101 | 640x480 60Hz 102 | DP-0 disconnected 103 | DP-1 disconnected 104 | DP-2 disconnected 105 | DP-3 disconnected 106 | DP-4 active 60cm/34cm 2560x1440+0+0 165Hz 107 | + 2560x1440 60Hz 108 | * !2560x1440 165Hz 109 | 2560x1440 144Hz 110 | 2560x1440 120Hz 111 | 2560x1440 100Hz 112 | 2560x1440 85Hz 113 | 2560x1440 24Hz 114 | 1024x768 60Hz 115 | 800x600 60Hz 116 | 640x480 60Hz 117 | *current +preferred !optimal 118 | 119 | laptop lid open or not present 120 | 121 | calculated DPI 108 for output DP-4 122 | 123 | xrandr \ 124 | --dpi 108 \ 125 | --output HDMI-0 --mode 1920x1080 --rate 60 --pos 0x0 \ 126 | --output DP-4 --mode 2560x1440 --rate 165 --pos 1920x0 --primary \ 127 | --output DVI-D-0 --off \ 128 | --output DP-0 --off \ 129 | --output DP-1 --off \ 130 | --output DP-2 --off \ 131 | --output DP-3 --off 132 | 133 | echo "Xft.dpi: 108" | xrdb -merge 134 | ``` 135 | 136 | End state: 137 | 138 | ``` 139 | /--------------\/----------------------\ 140 | | || | 141 | | HDMI-0 || | 142 | | || DP-4 | 143 | | 1920x1080 || | 144 | | || 2560x1440 | 145 | \--------------/| | 146 | | | 147 | | | 148 | \----------------------/ 149 | ``` 150 | 151 | ## Problems 152 | 153 | ### Freezing When Using NVIDIA Closed Source Drivers 154 | 155 | `xlayoutdisplay` may sometimes freeze the display, when applying a (full) composition pipeline to the whole X server. 156 | 157 | This may be avoided by running `xlayoutdisplay` first. You'll need to remove this bit of configuration from your Xorg conf and explicitly invoke via `nvidia-settings`. e.g. 158 | 159 | From `/etc/X11/xorg.conf.d/20-nvidia.conf`: 160 | 161 | ``` 162 | Section "Screen" 163 | Identifier "nvidiaSpecific" 164 | # Option "metamodes" "nvidia-auto-select +0+0 {ForceFullCompositionPipeline=On, AllowGSYNCCompatible=On}" 165 | EndSection 166 | ``` 167 | 168 | To `.xinitrc`: 169 | 170 | ``` 171 | xlayoutdisplay 172 | nvidia-settings --assign CurrentMetaMode="nvidia-auto-select +0+0 {ForceFullCompositionPipeline=On, AllowGSYNCCompatible=On}" 173 | ``` 174 | 175 | ## Developing 176 | 177 | ### Build 178 | 179 | ``` 180 | git clone https://github.com/alex-courtis/xlayoutdisplay.git 181 | cd xlayoutdisplay 182 | make 183 | ``` 184 | 185 | ### Build using Docker 186 | 187 | Prepare build directory 188 | 189 | ```bash 190 | mkdir -p build 191 | ``` 192 | 193 | Use one of: 194 | 195 | Ubuntu 196 | 197 | ```sh 198 | docker run --rm -it -v $(pwd)/build:/src --name ubuntu ubuntu:22.04 bash 199 | apt-get update 200 | apt-get install -y build-essential libxrandr-dev libxcursor-dev git-core pkg-config libgtest-dev libgmock-dev 201 | ``` 202 | 203 | OR Arch 204 | 205 | ```sh 206 | docker run --rm -it -v $(pwd)/build:/src --name archlinux archlinux:base bash 207 | pacman -Sy git gcc make pkgconfig libxcursor xorg-xrandr gtest gmock 208 | ``` 209 | 210 | Being inside the container run 211 | 212 | ```sh 213 | cd /src 214 | git clone --depth 1 https://github.com/alex-courtis/xlayoutdisplay . 215 | make 216 | exit 217 | ``` 218 | 219 | You may find compiled binary inside build directory 220 | 221 | ```bash 222 | ls -la build/xlayoutdisplay 223 | ``` 224 | 225 | ### Test 226 | 227 | Install [Google Test](https://github.com/google/googletest) and [Google Mock](https://github.com/google/googlemock). 228 | 229 | ``` 230 | make gtest 231 | ``` 232 | 233 | ## Contributing 234 | 235 | PRs very welcome: fork this repo and submit a PR. 236 | 237 | ## Bugs / Features 238 | 239 | Please raise an issue. 240 | 241 | Note again that PRs are very welcome ;) 242 | -------------------------------------------------------------------------------- /src/xrandrrutil.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include "xrandrrutil.h" 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | using namespace std; 24 | 25 | // stolen from xrandr.c; assuming this works, as we're reliant on xrandr anyway 26 | unsigned int refreshFromModeInfo(const XRRModeInfo &modeInfo) { 27 | double rate; 28 | double vTotal = modeInfo.vTotal; 29 | 30 | if (modeInfo.modeFlags & RR_DoubleScan) 31 | /* doublescan doubles the number of lines */ 32 | vTotal *= 2; 33 | 34 | if (modeInfo.modeFlags & RR_Interlace) 35 | /* interlace splits the frame into two fields */ 36 | /* the field rate is what is typically reported by monitors */ 37 | vTotal /= 2; 38 | 39 | if (modeInfo.hTotal && vTotal) 40 | rate = ((double) modeInfo.dotClock / ((double) modeInfo.hTotal * vTotal)); 41 | else 42 | rate = 0; 43 | 44 | // round up 45 | return static_cast(round(rate)); 46 | } 47 | 48 | const string renderXrandrCmd(const list> &outputs, const shared_ptr &primary, const long &dpi, const long &rate) { 49 | stringstream ss; 50 | ss << "xrandr \\\n --dpi " << dpi; 51 | for (const auto &output : outputs) { 52 | ss << " \\\n --output " << output->name; 53 | if (output->desiredActive && output->desiredMode && output->desiredPos) { 54 | ss << " --mode " << output->desiredMode->width << "x" << output->desiredMode->height; 55 | if (rate == 0) { 56 | ss << " --rate " << output->desiredMode->refresh; 57 | } else { 58 | ss << " --rate " << rate; 59 | } 60 | ss << " --pos "; 61 | ss << output->desiredPos->x << "x" << output->desiredPos->y; 62 | if (output == primary) { 63 | ss << " --primary"; 64 | } 65 | } else { 66 | ss << " --off"; 67 | } 68 | } 69 | return ss.str(); 70 | } 71 | 72 | Mode *modeFromXRR(RRMode id, const XRRScreenResources *resources) { 73 | if (resources == nullptr) 74 | throw invalid_argument("cannot construct Mode: NULL XRRScreenResources"); 75 | 76 | XRRModeInfo *modeInfo = nullptr; 77 | for (int i = 0; i < resources->nmode; i++) { 78 | if (id == resources->modes[i].id) { 79 | modeInfo = &(resources->modes[i]); 80 | break; 81 | } 82 | } 83 | 84 | if (modeInfo == nullptr) 85 | throw invalid_argument("cannot construct Mode: cannot retrieve RRMode '" + to_string(id) + "'"); 86 | 87 | return new Mode(id, modeInfo->width, modeInfo->height, refreshFromModeInfo(*modeInfo)); 88 | } 89 | 90 | // build a list of Output based on the current and possible state of the world 91 | const list> discoverOutputs() { 92 | list> outputs; 93 | 94 | // get the display 95 | Display *dpy = XOpenDisplay(nullptr); 96 | if (!dpy) 97 | throw domain_error(string("unable to open display '") + XDisplayName(nullptr) + "'"); 98 | 99 | // get the root window 100 | int screen = DefaultScreen(dpy); 101 | Window rootWindow = RootWindow(dpy, screen); 102 | 103 | // get RandR resources 104 | XRRScreenResources *screenResources = XRRGetScreenResources(dpy, rootWindow); 105 | 106 | // iterate outputs 107 | for (int i = 0; i < screenResources->noutput; i++) { 108 | Output::State state; 109 | list> modes; 110 | std::shared_ptr currentMode, preferredMode; 111 | shared_ptr currentPos; 112 | shared_ptr edid; 113 | 114 | // current state 115 | const RROutput rrOutput = screenResources->outputs[i]; 116 | const XRROutputInfo *outputInfo = XRRGetOutputInfo(dpy, screenResources, rrOutput); 117 | const char *name = outputInfo->name; 118 | RRMode rrMode = 0; 119 | if (outputInfo->crtc != 0) { 120 | // active outputs have CRTC info 121 | state = Output::active; 122 | 123 | // current position and mode 124 | XRRCrtcInfo *crtcInfo = XRRGetCrtcInfo(dpy, screenResources, outputInfo->crtc); 125 | currentPos = make_shared(crtcInfo->x, crtcInfo->y); 126 | rrMode = crtcInfo->mode; 127 | currentMode = shared_ptr(modeFromXRR(rrMode, screenResources)); 128 | 129 | if (outputInfo->nmode == 0) { 130 | // output is active but has been disconnected 131 | state = Output::disconnected; 132 | } 133 | } else if (outputInfo->nmode != 0) { 134 | // inactive connected outputs have modes available 135 | state = Output::connected; 136 | } else { 137 | state = Output::disconnected; 138 | } 139 | 140 | // iterate all properties to find EDID; XRRQueryOutputProperty fails when queried with XInternAtom 141 | int nprop; 142 | Atom *atoms = XRRListOutputProperties(dpy, rrOutput, &nprop); 143 | for (int j = 0; j < nprop; j++) { 144 | Atom atom = atoms[j]; 145 | char *atomName = XGetAtomName(dpy, atom); 146 | 147 | // drill down on Edid 148 | if (strcmp(atomName, RR_PROPERTY_RANDR_EDID) == 0) { 149 | 150 | // retrieve property specifics 151 | Atom actualType; 152 | int actualFormat; 153 | unsigned long nitems, bytesAfter; 154 | unsigned char *prop; 155 | XRRGetOutputProperty(dpy, rrOutput, atom, 156 | 0, // offset 157 | 64, // length in CARD32 - EDID 2.0 max length is 256 bytes 158 | false, // delete 159 | false, // pending 160 | AnyPropertyType, &actualType, &actualFormat, &nitems, &bytesAfter, &prop 161 | ); 162 | 163 | // record Edid 164 | edid = make_shared(prop, nitems, name); 165 | } 166 | } 167 | 168 | // add available modes 169 | for (int j = 0; j < outputInfo->nmode; j++) { 170 | 171 | // add to modes 172 | const auto &mode = shared_ptr(modeFromXRR(outputInfo->modes[j], screenResources)); 173 | modes.push_back(mode); 174 | 175 | // (optional) preferred mode based on outputInfo->modes indexed by 1 176 | if (outputInfo->npreferred == j + 1) 177 | preferredMode = mode; 178 | 179 | // replace currentMode with the one from the list 180 | if (mode->rrMode == rrMode) 181 | currentMode = mode; 182 | } 183 | 184 | // add the output 185 | outputs.push_back(make_shared(name, state, modes, currentMode, preferredMode, currentPos, edid)); 186 | } 187 | 188 | return outputs; 189 | } 190 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "src/layout.h" 25 | #include "src/util.h" 26 | 27 | using namespace std; 28 | 29 | bool parseBool(const string &str) { 30 | return regex_match(str, regex("true|on|1", regex::icase)); 31 | } 32 | 33 | long parseLong(const string &name, const string &val) { 34 | try { 35 | return stoi(val); 36 | } catch (invalid_argument &e) { 37 | throw invalid_argument("invalid " + name + ": " + val); 38 | } 39 | } 40 | 41 | void usage(std::ostream &os) { 42 | os << 43 | "Arranges outputs in a left to right manner, using highest resolution and refresh.\n" 44 | "DPI is calculated based on the first or primary output's EDID information and rounded to the nearest 12.\n" 45 | "Laptop outputs are turned off when the lid is closed.\n" 46 | "\n" 47 | "e.g. xlayoutdisplay -p DP-4 -o HDMI-0 -o DP-4\n" 48 | "\n" 49 | "CLI:\n" 50 | " -h [ --help ] print this help text and exit\n" 51 | " -i [ --info ] print information about current outputs and exit\n" 52 | " -n [ --noop ] perform a trial run and exit\n" 53 | " -v [ --version ] print version string\n" 54 | "\n" 55 | "CLI, $XDG_CONFIG_HOME/.xlayoutdisplay, $HOME/.xlayoutdisplay and /etc/xlayoutdisplay:\n" 56 | " -d [ --dpi ] arg DPI override\n" 57 | " -r [ --rate ] arg Refresh rate override\n" 58 | " -m [ --mirror ] mirror outputs using the lowest common resolution\n" 59 | " -o [ --order ] arg order of outputs, repeat as needed\n" 60 | " -p [ --primary ] arg primary output\n" 61 | " -q [ --quiet ] suppress feedback\n" 62 | " -w [ --wait ] arg wait seconds before running\n"; 63 | } 64 | 65 | void parseCfgFile(ifstream &ifs, Settings &settings) { 66 | string line; 67 | while (getline(ifs, line)) { 68 | 69 | // comments and empties 70 | if (regex_match(line, regex("\\s*#.*")) || regex_match(line, regex("\\s*"))) { 71 | continue; 72 | } 73 | 74 | // name=val 75 | smatch match; 76 | if (!regex_match(line, match, regex("\\s*(\\S+)\\s*=\\s*(\\S+)\\s*")) || match.size() != 3) { 77 | continue; 78 | } 79 | 80 | if (match[1] == "dpi") { 81 | settings.dpi = parseLong(match[1], match[2]); 82 | } else if (match[1] == "rate") { 83 | settings.rate = parseLong(match[1], match[2]); 84 | } else if (match[1] == "mirror") { 85 | settings.mirror = parseBool(match[2]); 86 | } else if (match[1] == "order") { 87 | settings.order.push_back(match[2]); 88 | } else if (match[1] == "primary") { 89 | settings.primary = match[2]; 90 | } else if (match[1] == "quiet") { 91 | settings.quiet = parseBool(match[2]); 92 | } else if (match[1] == "wait") { 93 | settings.wait = parseLong(match[1], match[2]); 94 | } else { 95 | throw invalid_argument("unrecognised file option '" + match[0].str() + "'"); 96 | } 97 | } 98 | 99 | } 100 | 101 | void parseArgs(int argc, char **argv, Settings &settings) { 102 | static struct option long_options[] = { 103 | { "help", no_argument, 0, 'h' }, 104 | { "info", no_argument, 0, 'i' }, 105 | { "noop", no_argument, 0, 'n' }, 106 | { "version", no_argument, 0, 'v' }, 107 | { "dpi", required_argument, 0, 'd' }, 108 | { "rate", required_argument, 0, 'r' }, 109 | { "mirror", no_argument, 0, 'm' }, 110 | { "order", required_argument, 0, 'o' }, 111 | { "primary", required_argument, 0, 'p' }, 112 | { "quiet", no_argument, 0, 'q' }, 113 | { "wait", required_argument, 0, 'w' }, 114 | { 0, 0, 0, 0 } 115 | }; 116 | static const char *short_options = "hinvd:r:mo:p:qw:"; 117 | 118 | bool orderFromFile = !settings.order.empty(); 119 | 120 | int c; 121 | while (1) { 122 | int long_index = 0; 123 | c = getopt_long(argc, argv, short_options, long_options, &long_index); 124 | if (c == -1) 125 | break; 126 | switch (c) { 127 | case 'h': 128 | usage(cout); 129 | exit(EXIT_SUCCESS); 130 | case 'i': 131 | settings.info = true; 132 | break; 133 | case 'n': 134 | settings.noop = true; 135 | break; 136 | case 'v': 137 | cout << argv[0] << " " << VERSION << endl; 138 | exit(EXIT_SUCCESS); 139 | case 'd': 140 | settings.dpi = parseLong("dpi", optarg); 141 | break; 142 | case 'r': 143 | settings.rate = parseLong("rate", optarg); 144 | break; 145 | case 'm': 146 | settings.mirror = true; 147 | break; 148 | case 'o': 149 | if (orderFromFile) { 150 | settings.order.clear(); 151 | orderFromFile = false; 152 | } 153 | settings.order.push_back(optarg); 154 | break; 155 | case 'p': 156 | settings.primary = optarg; 157 | break; 158 | case 'q': 159 | settings.quiet = true; 160 | break; 161 | case 'w': 162 | settings.wait = parseLong("wait", optarg);; 163 | break; 164 | case '?': 165 | default: 166 | usage(cerr); 167 | exit(EXIT_FAILURE); 168 | } 169 | } 170 | } 171 | 172 | int main(int argc, char **argv) { 173 | try { 174 | Settings settings; 175 | try { 176 | // file options 177 | ifstream ifs(resolveEnvPath("XDG_CONFIG_HOME", ".xlayoutdisplay")); 178 | if (!ifs) 179 | ifs = ifstream(resolveEnvPath("HOME", ".xlayoutdisplay")); 180 | if (!ifs) 181 | ifs = ifstream("/etc/xlayoutdisplay"); 182 | if (ifs) { 183 | parseCfgFile(ifs, settings); 184 | } 185 | 186 | // command line options override 187 | parseArgs(argc, argv, settings); 188 | 189 | } catch (invalid_argument &e) { 190 | cerr << argv[0] << ": " << e.what() << endl << endl; 191 | usage(cerr); 192 | return EXIT_FAILURE; 193 | } 194 | 195 | // execute 196 | return WEXITSTATUS(layout(settings)); 197 | } catch (const exception &e) { 198 | cerr << argv[0] << ": " << e.what() << ", exiting\n"; 199 | return EXIT_FAILURE; 200 | } catch (...) { 201 | cerr << argv[0] << ": unknown exception, exiting\n"; 202 | return EXIT_FAILURE; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/calculations.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include "calculations.h" 17 | #include "util.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | using namespace std; 25 | 26 | const std::list> orderOutputs(const list> &outputs, const vector &order) { 27 | list> orderedOutputs(outputs); 28 | 29 | // stack all the preferred, available outputs 30 | stack> preferredOutputs; 31 | for (const auto &name : order) { 32 | for (const auto &output : outputs) { 33 | if (strcasecmp(name.c_str(), output->name.c_str()) == 0) { 34 | preferredOutputs.push(output); 35 | } 36 | } 37 | } 38 | 39 | // move preferred to the front 40 | while (!preferredOutputs.empty()) { 41 | orderedOutputs.remove(preferredOutputs.top()); 42 | orderedOutputs.push_front(preferredOutputs.top()); 43 | preferredOutputs.pop(); 44 | } 45 | 46 | return orderedOutputs; 47 | } 48 | 49 | const shared_ptr activateOutputs(const list> &outputs, const string &desiredPrimary, 50 | const Monitors &monitors) { 51 | if (outputs.empty()) throw invalid_argument("activateOutputs received empty outputs"); 52 | 53 | shared_ptr primary; 54 | 55 | for (const auto &output : outputs) { 56 | 57 | // don't activate any monitors that shouldn't 58 | if (monitors.shouldDisableOutput(output->name)) 59 | continue; 60 | 61 | // only activate currently active or connected ouputs 62 | if (output->state != Output::active && output->state != Output::connected) 63 | continue; 64 | 65 | // mark active 66 | output->desiredActive = true; 67 | 68 | // default first to primary 69 | if (!primary) 70 | primary = output; 71 | 72 | // user selected primary 73 | if (!desiredPrimary.empty() && strcasecmp(desiredPrimary.c_str(), output->name.c_str()) == 0) 74 | primary = output; 75 | } 76 | 77 | if (!primary) throw runtime_error("no active or connected outputs found"); 78 | 79 | return primary; 80 | } 81 | 82 | void ltrOutputs(const list> &outputs) { 83 | int xpos = 0; 84 | int ypos = 0; 85 | for (const auto &output : outputs) { 86 | 87 | if (output->desiredActive) { 88 | 89 | // set the desired mode to optimal 90 | output->desiredMode = output->optimalMode; 91 | 92 | // position the screen 93 | output->desiredPos = make_shared(xpos, ypos); 94 | 95 | // next position 96 | xpos += output->desiredMode->width; 97 | } 98 | } 99 | } 100 | 101 | void mirrorOutputs(const list> &outputs) { 102 | 103 | // find the first active output 104 | shared_ptr firstOutput; 105 | for (const auto &output : outputs) { 106 | if (output->desiredActive) { 107 | firstOutput = output; 108 | break; 109 | } 110 | } 111 | if (!firstOutput) 112 | return; 113 | 114 | // iterate through first active output's modes 115 | for (const auto &possibleMode : reverseSort(firstOutput->modes)) { 116 | bool matched = true; 117 | 118 | // attempt to match mode to each active output 119 | for (const auto &output : outputs) { 120 | if (!output->desiredActive) 121 | continue; 122 | 123 | // reset failed matches 124 | shared_ptr desiredMode; 125 | 126 | // match height and width 127 | for (const auto &mode : reverseSort(output->modes)) { 128 | if (mode->width == possibleMode->width && mode->height == possibleMode->height) { 129 | 130 | // select best refresh 131 | desiredMode = mode; 132 | break; 133 | } 134 | } 135 | 136 | // match a mode for every output; root it at 0, 0 137 | matched = matched && desiredMode; 138 | if (matched) { 139 | output->desiredMode = desiredMode; 140 | output->desiredPos = make_shared(0, 0); 141 | continue; 142 | } 143 | } 144 | 145 | // we've set desiredMode and desiredPos (zero) for all outputs, all done 146 | if (matched) 147 | return; 148 | } 149 | 150 | // couldn't find a common mode, exit 151 | throw runtime_error("unable to find common width/height for mirror"); 152 | } 153 | 154 | const string renderUserInfo(const list> &outputs) { 155 | stringstream ss; 156 | for (const auto &output : outputs) { 157 | ss << output->name; 158 | switch (output->state) { 159 | case Output::active: 160 | ss << " active"; 161 | break; 162 | case Output::connected: 163 | ss << " connected"; 164 | break; 165 | case Output::disconnected: 166 | ss << " disconnected"; 167 | break; 168 | } 169 | if (output->edid) { 170 | ss << ' ' << output->edid->maxCmHoriz() << "cm/" << output->edid->maxCmVert() << "cm"; 171 | } 172 | if (output->currentMode && output->currentPos) { 173 | ss << ' ' << output->currentMode->width << 'x' << output->currentMode->height; 174 | ss << '+' << output->currentPos->x << '+' << output->currentPos->y; 175 | ss << ' ' << output->currentMode->refresh << "Hz"; 176 | } 177 | ss << endl; 178 | for (const auto &mode : output->modes) { 179 | ss << (mode == output->currentMode ? '*' : ' '); 180 | ss << (mode == output->preferredMode ? '+' : ' '); 181 | ss << (mode == output->optimalMode ? '!' : ' '); 182 | ss << mode->width << 'x' << mode->height << ' ' << mode->refresh << "Hz"; 183 | ss << endl; 184 | } 185 | } 186 | ss << "*current +preferred !optimal"; 187 | return ss.str(); 188 | } 189 | 190 | long calculateDpi(const shared_ptr &output, string *explaination) { 191 | if (!output) throw invalid_argument("calculateDpi received empty output"); 192 | 193 | long dpi = DEFAULT_DPI; 194 | stringstream verbose; 195 | if (!output->edid) { 196 | verbose << "DPI defaulting to " 197 | << dpi 198 | << "; EDID information not available for output " 199 | << output->name; 200 | } else if (!output->desiredMode) { 201 | verbose << "DPI defaulting to " 202 | << dpi 203 | << "; no desired mode for output " 204 | << output->name; 205 | } else { 206 | const long caldulatedDpi = output->edid->dpiForMode(output->desiredMode); 207 | if (caldulatedDpi == 0) { 208 | verbose << "DPI defaulting to " 209 | << dpi 210 | << " as no EDID horiz/vert cm information available for " 211 | << output->name 212 | << "; this is normal for projectors and some TVs"; 213 | } else { 214 | dpi = caldulatedDpi; 215 | verbose << "calculated DPI " 216 | << dpi 217 | << " for output " 218 | << output->name; 219 | } 220 | } 221 | 222 | *explaination = verbose.str(); 223 | return dpi; 224 | } 225 | 226 | const shared_ptr calculateOptimalMode(const list> &modes, const shared_ptr &preferredMode) { 227 | shared_ptr optimalMode; 228 | 229 | // default optimal mode is empty 230 | if (!modes.empty()) { 231 | 232 | // use highest resolution/refresh for optimal 233 | const list> reverseOrderedModes = reverseSort(modes); 234 | optimalMode = reverseOrderedModes.front(); 235 | 236 | // override with highest refresh of preferred resolution, if available 237 | if (preferredMode) 238 | for (const auto &mode : reverseOrderedModes) 239 | if (mode->width == preferredMode->width && mode->height == preferredMode->height) { 240 | optimalMode = mode; 241 | break; 242 | } 243 | } 244 | 245 | return optimalMode; 246 | } 247 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /test/test-calculations.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Alexander Courtis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #include 17 | #include 18 | 19 | #include "../src/calculations.h" 20 | 21 | #include "test-MockEdid.h" 22 | #include "test-MockMonitors.h" 23 | 24 | using namespace std; 25 | using ::testing::_; 26 | using ::testing::Return; 27 | using ::testing::NiceMock; 28 | 29 | TEST(calculations_orderOutputs, reposition) { 30 | 31 | list> outputs; 32 | shared_ptr output1 = make_shared("One", Output::disconnected, list>(), 33 | shared_ptr(), shared_ptr(), shared_ptr(), 34 | shared_ptr()); 35 | outputs.push_back(output1); 36 | shared_ptr output2 = make_shared("Two", Output::disconnected, list>(), 37 | shared_ptr(), shared_ptr(), shared_ptr(), 38 | shared_ptr()); 39 | outputs.push_back(output2); 40 | shared_ptr output3 = make_shared("Three", Output::disconnected, list>(), 41 | shared_ptr(), shared_ptr(), shared_ptr(), 42 | shared_ptr()); 43 | outputs.push_back(output3); 44 | shared_ptr output4 = make_shared("Four", Output::disconnected, list>(), 45 | shared_ptr(), shared_ptr(), shared_ptr(), 46 | shared_ptr()); 47 | outputs.push_back(output4); 48 | shared_ptr output5 = make_shared("Five", Output::disconnected, list>(), 49 | shared_ptr(), shared_ptr(), shared_ptr(), 50 | shared_ptr()); 51 | outputs.push_back(output5); 52 | 53 | list> orderedOutputs = orderOutputs(outputs, {"FOUR", "THREE", "TWO"}); 54 | 55 | EXPECT_EQ(output4, orderedOutputs.front()); 56 | orderedOutputs.pop_front(); 57 | EXPECT_EQ(output3, orderedOutputs.front()); 58 | orderedOutputs.pop_front(); 59 | EXPECT_EQ(output2, orderedOutputs.front()); 60 | orderedOutputs.pop_front(); 61 | EXPECT_EQ(output1, orderedOutputs.front()); 62 | orderedOutputs.pop_front(); 63 | EXPECT_EQ(output5, orderedOutputs.front()); 64 | } 65 | 66 | 67 | class calculations_activateOutputs : public ::testing::Test { 68 | protected: 69 | shared_ptr mode = make_shared(0, 0, 0, 0); 70 | shared_ptr pos = make_shared(0, 0); 71 | list> modes = {mode}; 72 | NiceMock mockMonitors; 73 | }; 74 | 75 | TEST_F(calculations_activateOutputs, primarySpecifiedAndLaptop) { 76 | list> outputs; 77 | 78 | shared_ptr output1 = make_shared("One", Output::active, modes, mode, mode, pos, shared_ptr()); 79 | outputs.push_back(output1); 80 | 81 | shared_ptr output2 = make_shared("Two", Output::disconnected, modes, mode, mode, pos, 82 | shared_ptr()); 83 | outputs.push_back(output2); 84 | 85 | shared_ptr output3 = make_shared("Three", Output::connected, modes, mode, mode, pos, 86 | shared_ptr()); 87 | outputs.push_back(output3); 88 | 89 | shared_ptr output4 = make_shared(LAPTOP_OUTPUT_PREFIX + string("Four"), Output::active, modes, mode, 90 | mode, pos, shared_ptr()); 91 | outputs.push_back(output4); 92 | 93 | EXPECT_CALL(mockMonitors, shouldDisableOutput(_)).WillRepeatedly(Return(false)); 94 | EXPECT_CALL(mockMonitors, shouldDisableOutput(output4->name)).WillOnce(Return(true)); 95 | 96 | const shared_ptr primary = activateOutputs(outputs, "three", mockMonitors); 97 | 98 | EXPECT_TRUE(output1->desiredActive); 99 | EXPECT_FALSE(output2->desiredActive); 100 | EXPECT_TRUE(output3->desiredActive); 101 | EXPECT_FALSE(output4->desiredActive); 102 | 103 | EXPECT_EQ(primary, output3); 104 | } 105 | 106 | TEST_F(calculations_activateOutputs, defaultPrimary) { 107 | 108 | list> outputs; 109 | 110 | shared_ptr output1 = make_shared("One", Output::disconnected, modes, mode, mode, pos, 111 | shared_ptr()); 112 | outputs.push_back(output1); 113 | 114 | shared_ptr output2 = make_shared("Two", Output::active, modes, mode, mode, pos, 115 | shared_ptr()); 116 | outputs.push_back(output2); 117 | 118 | shared_ptr output3 = make_shared("Three", Output::active, modes, mode, mode, pos, 119 | shared_ptr()); 120 | outputs.push_back(output3); 121 | 122 | const shared_ptr primary = activateOutputs(outputs, "nouserprimary", mockMonitors); 123 | 124 | EXPECT_FALSE(output1->desiredActive); 125 | EXPECT_TRUE(output2->desiredActive); 126 | EXPECT_TRUE(output3->desiredActive); 127 | 128 | EXPECT_EQ(primary, output2); 129 | } 130 | 131 | TEST_F(calculations_activateOutputs, noOutputs) { 132 | EXPECT_THROW(activateOutputs({}, "ouch", mockMonitors), invalid_argument); 133 | } 134 | 135 | TEST_F(calculations_activateOutputs, noActiveOrConnected) { 136 | EXPECT_THROW( 137 | activateOutputs( 138 | {make_shared("Two", Output::disconnected, modes, mode, mode, pos, shared_ptr())}, 139 | "ouch", mockMonitors), 140 | runtime_error); 141 | } 142 | 143 | 144 | TEST(calculations_ltrOutputs, arrange) { 145 | 146 | list> outputs; 147 | list> modes; 148 | 149 | modes = {make_shared(0, 10, 20, 30)}; 150 | shared_ptr output1 = make_shared("One", Output::connected, modes, shared_ptr(), modes.front(), 151 | shared_ptr(), shared_ptr()); 152 | output1->desiredActive = true; 153 | outputs.push_back(output1); 154 | 155 | modes = {}; 156 | shared_ptr output2 = make_shared("Two", Output::disconnected, modes, shared_ptr(), 157 | shared_ptr(), shared_ptr(), shared_ptr()); 158 | outputs.push_back(output2); 159 | 160 | modes = {make_shared(0, 50, 60, 70)}; 161 | shared_ptr output3 = make_shared("Three", Output::connected, modes, shared_ptr(), 162 | modes.front(), shared_ptr(), shared_ptr()); 163 | output3->desiredActive = true; 164 | outputs.push_back(output3); 165 | 166 | ltrOutputs(outputs); 167 | 168 | EXPECT_TRUE(output1->desiredActive); 169 | EXPECT_TRUE(output1->desiredMode); 170 | EXPECT_EQ(10, output1->desiredMode->width); 171 | EXPECT_EQ(20, output1->desiredMode->height); 172 | EXPECT_EQ(30, output1->desiredMode->refresh); 173 | EXPECT_TRUE(output1->desiredPos); 174 | EXPECT_EQ(0, output1->desiredPos->x); 175 | EXPECT_EQ(0, output1->desiredPos->y); 176 | 177 | EXPECT_FALSE(output2->desiredActive); 178 | EXPECT_FALSE(output2->desiredMode); 179 | EXPECT_FALSE(output2->desiredPos); 180 | 181 | EXPECT_TRUE(output3->desiredActive); 182 | EXPECT_TRUE(output3->desiredMode); 183 | EXPECT_EQ(50, output3->desiredMode->width); 184 | EXPECT_EQ(60, output3->desiredMode->height); 185 | EXPECT_EQ(70, output3->desiredMode->refresh); 186 | EXPECT_TRUE(output3->desiredPos); 187 | EXPECT_EQ(10, output3->desiredPos->x); 188 | EXPECT_EQ(0, output3->desiredPos->y); 189 | } 190 | 191 | 192 | TEST(calculations_mirrorOutputs, noneActive) { 193 | 194 | list> outputs; 195 | 196 | shared_ptr output1 = make_shared("One", Output::disconnected, list>(), 197 | shared_ptr(), shared_ptr(), shared_ptr(), 198 | shared_ptr()); 199 | outputs.push_back(output1); 200 | 201 | shared_ptr output2 = make_shared("Two", Output::disconnected, list>(), 202 | shared_ptr(), shared_ptr(), shared_ptr(), 203 | shared_ptr()); 204 | outputs.push_back(output2); 205 | 206 | mirrorOutputs(outputs); 207 | 208 | EXPECT_FALSE(output1->desiredMode); 209 | EXPECT_FALSE(output1->desiredPos); 210 | 211 | EXPECT_FALSE(output2->desiredMode); 212 | EXPECT_FALSE(output2->desiredPos); 213 | } 214 | 215 | TEST(calculations_mirrorOutputs, oneActive) { 216 | 217 | list> outputs; 218 | 219 | const shared_ptr mode2 = make_shared(0, 5, 6, 0); 220 | const shared_ptr mode1 = make_shared(0, 7, 8, 0); 221 | 222 | shared_ptr output1 = make_shared("One", Output::disconnected, 223 | list>({mode1, mode2}), shared_ptr(), 224 | shared_ptr(), shared_ptr(), shared_ptr()); 225 | output1->desiredActive = true; 226 | outputs.push_back(output1); 227 | 228 | shared_ptr output2 = make_shared("Two", Output::disconnected, list>(), 229 | shared_ptr(), shared_ptr(), shared_ptr(), 230 | shared_ptr()); 231 | outputs.push_back(output2); 232 | 233 | mirrorOutputs(outputs); 234 | 235 | EXPECT_EQ(mode1, output1->desiredMode); 236 | EXPECT_EQ(0, output1->desiredPos->x); 237 | EXPECT_EQ(0, output1->desiredPos->y); 238 | 239 | EXPECT_FALSE(output2->desiredMode); 240 | EXPECT_FALSE(output2->desiredPos); 241 | } 242 | 243 | TEST(calculations_mirrorOutputs, someActive) { 244 | 245 | list> outputs; 246 | 247 | shared_ptr mode5 = make_shared(0, 1, 2, 0); 248 | shared_ptr mode4 = make_shared(0, 1, 2, 1); 249 | shared_ptr mode3 = make_shared(0, 3, 4, 0); 250 | shared_ptr mode2 = make_shared(0, 5, 6, 0); 251 | shared_ptr mode1 = make_shared(0, 7, 8, 0); 252 | 253 | shared_ptr output1 = make_shared("One", Output::disconnected, 254 | list>({mode1, mode2}), shared_ptr(), 255 | shared_ptr(), shared_ptr(), shared_ptr()); 256 | outputs.push_back(output1); 257 | 258 | shared_ptr output2 = make_shared("Two", Output::disconnected, 259 | list>({mode3, mode5, mode4}), shared_ptr(), 260 | shared_ptr(), shared_ptr(), shared_ptr()); 261 | output2->desiredActive = true; 262 | outputs.push_back(output2); 263 | 264 | shared_ptr output3 = make_shared("Three", Output::disconnected, 265 | list>({mode2, mode5, mode4}), shared_ptr(), 266 | shared_ptr(), shared_ptr(), shared_ptr()); 267 | output3->desiredActive = true; 268 | outputs.push_back(output3); 269 | 270 | mirrorOutputs(outputs); 271 | 272 | EXPECT_FALSE(output1->desiredMode); 273 | EXPECT_FALSE(output1->desiredPos); 274 | 275 | EXPECT_EQ(mode4, output2->desiredMode); 276 | EXPECT_EQ(0, output2->desiredPos->x); 277 | EXPECT_EQ(0, output2->desiredPos->y); 278 | 279 | EXPECT_EQ(mode4, output3->desiredMode); 280 | EXPECT_EQ(0, output3->desiredPos->x); 281 | EXPECT_EQ(0, output3->desiredPos->y); 282 | } 283 | 284 | TEST(calculations_mirrorOutputs, manyActive) { 285 | 286 | list> outputs; 287 | 288 | shared_ptr mode4 = make_shared(0, 1, 2, 0); 289 | shared_ptr mode3 = make_shared(0, 3, 4, 0); 290 | shared_ptr mode2 = make_shared(0, 5, 6, 0); 291 | shared_ptr mode1 = make_shared(0, 7, 8, 0); 292 | 293 | shared_ptr output1 = make_shared("One", Output::disconnected, 294 | list>({mode1, mode2, mode3}), shared_ptr(), 295 | shared_ptr(), shared_ptr(), shared_ptr()); 296 | output1->desiredActive = true; 297 | outputs.push_back(output1); 298 | 299 | shared_ptr output2 = make_shared("Two", Output::disconnected, 300 | list>({mode3, mode4}), shared_ptr(), 301 | shared_ptr(), shared_ptr(), shared_ptr()); 302 | output2->desiredActive = true; 303 | outputs.push_back(output2); 304 | 305 | shared_ptr output3 = make_shared("Three", Output::disconnected, 306 | list>({mode2, mode3}), shared_ptr(), 307 | shared_ptr(), shared_ptr(), shared_ptr()); 308 | output3->desiredActive = true; 309 | outputs.push_back(output3); 310 | 311 | mirrorOutputs(outputs); 312 | 313 | EXPECT_EQ(mode3, output1->desiredMode); 314 | EXPECT_EQ(0, output1->desiredPos->x); 315 | EXPECT_EQ(0, output1->desiredPos->y); 316 | 317 | EXPECT_EQ(mode3, output2->desiredMode); 318 | EXPECT_EQ(0, output2->desiredPos->x); 319 | EXPECT_EQ(0, output2->desiredPos->y); 320 | 321 | EXPECT_EQ(mode3, output3->desiredMode); 322 | EXPECT_EQ(0, output3->desiredPos->x); 323 | EXPECT_EQ(0, output3->desiredPos->y); 324 | } 325 | 326 | TEST(calculations_mirrorOutputs, noCommon) { 327 | 328 | list> outputs; 329 | 330 | shared_ptr mode4 = make_shared(0, 1, 2, 0); 331 | shared_ptr mode3 = make_shared(0, 3, 4, 0); 332 | shared_ptr mode2 = make_shared(0, 5, 6, 0); 333 | shared_ptr mode1 = make_shared(0, 7, 8, 0); 334 | 335 | shared_ptr output1 = make_shared("One", Output::disconnected, 336 | list>({mode1, mode2}), shared_ptr(), 337 | shared_ptr(), shared_ptr(), shared_ptr()); 338 | output1->desiredActive = true; 339 | outputs.push_back(output1); 340 | 341 | shared_ptr output2 = make_shared("Two", Output::disconnected, 342 | list>({mode3, mode4}), shared_ptr(), 343 | shared_ptr(), shared_ptr(), shared_ptr()); 344 | output2->desiredActive = true; 345 | outputs.push_back(output2); 346 | 347 | shared_ptr output3 = make_shared("Three", Output::disconnected, 348 | list>({mode1, mode4}), shared_ptr(), 349 | shared_ptr(), shared_ptr(), shared_ptr()); 350 | output3->desiredActive = true; 351 | outputs.push_back(output3); 352 | 353 | EXPECT_THROW(mirrorOutputs(outputs), runtime_error); 354 | } 355 | 356 | 357 | TEST(calculations_renderUserInfo, renderAll) { 358 | shared_ptr mode1 = make_shared(1, 2, 3, 4); 359 | shared_ptr mode2 = make_shared(5, 6, 7, 8); 360 | shared_ptr mode3 = make_shared(9, 10, 11, 12); 361 | shared_ptr pos = make_shared(13, 14); 362 | shared_ptr edid1 = make_shared(); 363 | EXPECT_CALL(*edid1, maxCmHoriz()).WillOnce(Return(15)); 364 | EXPECT_CALL(*edid1, maxCmVert()).WillOnce(Return(16)); 365 | shared_ptr edid3 = make_shared(); 366 | EXPECT_CALL(*edid3, maxCmHoriz()).WillOnce(Return(17)); 367 | EXPECT_CALL(*edid3, maxCmVert()).WillOnce(Return(18)); 368 | 369 | list> outputs; 370 | 371 | shared_ptr dis = make_shared("dis", Output::disconnected, list>(), 372 | shared_ptr(), shared_ptr(), shared_ptr(), edid1); 373 | outputs.push_back(dis); 374 | 375 | shared_ptr con = make_shared("con", Output::connected, list>({mode1, mode2}), 376 | shared_ptr(), shared_ptr(), shared_ptr(), 377 | shared_ptr()); 378 | outputs.push_back(con); 379 | 380 | shared_ptr act = make_shared("act", Output::active, list>({mode3, mode2, mode1}), 381 | mode2, mode3, pos, edid3); 382 | outputs.push_back(act); 383 | 384 | const string expected = "" 385 | "dis disconnected 15cm/16cm\n" 386 | "con connected\n" 387 | " 2x3 4Hz\n" 388 | " !6x7 8Hz\n" 389 | "act active 17cm/18cm 6x7+13+14 8Hz\n" 390 | " +!10x11 12Hz\n" 391 | "* 6x7 8Hz\n" 392 | " 2x3 4Hz\n" 393 | "*current +preferred !optimal"; 394 | 395 | EXPECT_EQ(expected, renderUserInfo(outputs)); 396 | 397 | } 398 | 399 | class calculations_calculateOptimalMode : public ::testing::Test { 400 | protected: 401 | shared_ptr mode1 = make_shared(1, 2, 3, 4); 402 | shared_ptr mode21 = make_shared(5, 6, 7, 8); 403 | shared_ptr mode22 = make_shared(5, 6, 7, 9); 404 | shared_ptr mode3 = make_shared(9, 10, 11, 12); 405 | list> modes = {mode1, mode21, mode22, mode3}; 406 | }; 407 | 408 | TEST_F(calculations_calculateOptimalMode, highestRes) { 409 | EXPECT_EQ(mode3, calculateOptimalMode(modes, nullptr)); 410 | } 411 | 412 | TEST_F(calculations_calculateOptimalMode, highestPreferredRefresh) { 413 | EXPECT_EQ(mode22, calculateOptimalMode(modes, mode21)); 414 | } 415 | 416 | TEST_F(calculations_calculateOptimalMode, noModes) { 417 | EXPECT_EQ(nullptr, calculateOptimalMode({}, mode21)); 418 | } 419 | 420 | 421 | class calculations_calculateDpi : public ::testing::Test { 422 | protected: 423 | const shared_ptr mockEdid = make_shared(); 424 | const shared_ptr output = make_shared("someoutput", Output::disconnected, list>(), 425 | shared_ptr(), shared_ptr(), shared_ptr(), 426 | mockEdid); 427 | }; 428 | 429 | TEST_F(calculations_calculateDpi, noOutput) { 430 | string explaination; 431 | EXPECT_THROW(calculateDpi(shared_ptr(), &explaination), invalid_argument); 432 | } 433 | 434 | TEST_F(calculations_calculateDpi, noEdid) { 435 | const shared_ptr output = make_shared("noedidoutput", Output::disconnected, 436 | list>(), shared_ptr(), 437 | shared_ptr(), shared_ptr(), shared_ptr()); 438 | 439 | string explaination; 440 | const long calculated = calculateDpi(output, &explaination); 441 | 442 | stringstream expectedExplaination; 443 | expectedExplaination << "DPI defaulting to " 444 | << DEFAULT_DPI 445 | << "; EDID information not available for output " 446 | << output->name; 447 | 448 | EXPECT_EQ(DEFAULT_DPI, calculated); 449 | EXPECT_EQ(expectedExplaination.str(), explaination); 450 | } 451 | 452 | TEST_F(calculations_calculateDpi, noDesiredMode) { 453 | 454 | string explaination; 455 | const long calculated = calculateDpi(output, &explaination); 456 | 457 | stringstream expectedExplaination; 458 | expectedExplaination << "DPI defaulting to " 459 | << DEFAULT_DPI 460 | << "; no desired mode for output " 461 | << output->name; 462 | 463 | EXPECT_EQ(DEFAULT_DPI, calculated); 464 | EXPECT_EQ(expectedExplaination.str(), explaination); 465 | } 466 | 467 | TEST_F(calculations_calculateDpi, zeroEdid) { 468 | output->desiredMode = make_shared(1, 2, 3, 4); 469 | EXPECT_CALL(*mockEdid, dpiForMode(output->desiredMode)).WillOnce(Return(0)); 470 | 471 | string explaination; 472 | const long calculated = calculateDpi(output, &explaination); 473 | 474 | stringstream expectedExplaination; 475 | expectedExplaination << "DPI defaulting to " 476 | << DEFAULT_DPI 477 | << " as no EDID horiz/vert cm information available for " 478 | << output->name 479 | << "; this is normal for projectors and some TVs"; 480 | 481 | EXPECT_EQ(DEFAULT_DPI, calculated); 482 | EXPECT_EQ(expectedExplaination.str(), explaination); 483 | } 484 | 485 | TEST_F(calculations_calculateDpi, valid) { 486 | output->desiredMode = make_shared(1, 2, 3, 4); 487 | EXPECT_CALL(*mockEdid, dpiForMode(output->desiredMode)).WillOnce(Return(1)); 488 | 489 | string explaination; 490 | const long calculated = calculateDpi(output, &explaination); 491 | 492 | stringstream expectedExplaination; 493 | expectedExplaination << "calculated DPI " 494 | << 1 495 | << " for output " 496 | << output->name; 497 | 498 | EXPECT_EQ(1, calculated); 499 | EXPECT_EQ(expectedExplaination.str(), explaination); 500 | } --------------------------------------------------------------------------------