From 15e338095e792972dc888cd0335b047df1f0f7c1 Mon Sep 17 00:00:00 2001 From: Nico Rittstieg Date: Fri, 21 Dec 2018 14:51:40 +0100 Subject: [PATCH] Initial commit --- .gitignore | 4 ++ README.md | 90 ++++++++++++++++++++++- read.py | 24 +++++++ save.py | 24 +++++++ savedesktop/__init__.py | 19 +++++ savedesktop/rd.py | 81 +++++++++++++++++++++ savedesktop/sd.py | 75 +++++++++++++++++++ savedesktop/wmctrl.py | 156 ++++++++++++++++++++++++++++++++++++++++ setup.py | 46 ++++++++++++ 9 files changed, 517 insertions(+), 2 deletions(-) create mode 100644 read.py create mode 100644 save.py create mode 100644 savedesktop/__init__.py create mode 100755 savedesktop/rd.py create mode 100755 savedesktop/sd.py create mode 100644 savedesktop/wmctrl.py create mode 100755 setup.py diff --git a/.gitignore b/.gitignore index 894a44c..b9574f3 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,7 @@ venv.bak/ # mypy .mypy_cache/ + +# IntelliJ +.idea +*.iml diff --git a/README.md b/README.md index eefb36d..55495e3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,88 @@ -# savedesktop -CLI tool for saving and restoring virtual linux desktops +# SaveDesktop + +A CLI tool for saving and restoring virtual linux desktops. + +Currently in proof of concept. + +Main features: +------------------- + - Dumping window geometry and hints to human readable JSON files + - Apply the saved layout to any virtual desktop + - Command Line Interface + +Dependencies: +---------------------- + + - X Window Manager that implement the EWMH specification + - wmctrl (https://sites.google.com/site/tstyblo/wmctrl/) + - xwininfo + - Python 3 + - python-setuptools + +Installation: +-------------------------- + +Arch Linux package: + +``` +$ pacman -U savedesktop-*.pkg.tar.xz +``` + +For manual installation use the following command: + +``` +$ sudo python setup.py install --optimize=1 +``` + +Usage: +-------------------------- + +dump a desktop: + +``` +$ sd -d 0 -p profile1 -o +``` +options: + +`-d 0` dump first desktop + +`-p profile1` save to ~/.config/savedesktop/profile1.json + +`-o` open in default editor + +restore a desktop: + +``` +$ rd -d 1 -p profile1 +``` + +`-d 1` restore to second desktop + +`-p profile1` load ~/.config/savedesktop/profile1.json + +Project Web site : +-------------------- + +https://github.com/nrittsti/savedesktop/ + +-------------------------------------------------------------------------------- +Licence: +-------------------------------------------------------------------------------- + + +This program is free software: +you can redistribute it and/or modify it under the terms of the GNU General Public License +as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with this program. +If not, see . + +Copyright (C) 2018 Nico Rittstieg + +-------------------------------------------------------------------------------- +End of document \ No newline at end of file diff --git a/read.py b/read.py new file mode 100644 index 0000000..6ce6c7d --- /dev/null +++ b/read.py @@ -0,0 +1,24 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +# +# This file is part of savedesktop. +# A CLI tool for saving and restoring virtual linux desktops. +# +# Copyright (C) 2018 Nico Rittstieg +# +# This program is free software: +# you can redistribute it and/or modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. +# If not, see . + +import savedesktop.rd as rd + +if __name__ == '__main__': + rd.main() diff --git a/save.py b/save.py new file mode 100644 index 0000000..22b0c15 --- /dev/null +++ b/save.py @@ -0,0 +1,24 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +# +# This file is part of savedesktop. +# A CLI tool for saving and restoring virtual linux desktops. +# +# Copyright (C) 2018 Nico Rittstieg +# +# This program is free software: +# you can redistribute it and/or modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. +# If not, see . + +import savedesktop.sd as sd + +if __name__ == '__main__': + sd.main() diff --git a/savedesktop/__init__.py b/savedesktop/__init__.py new file mode 100644 index 0000000..9ee381e --- /dev/null +++ b/savedesktop/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +# +# This file is part of savedesktop. +# A CLI tool for saving and restoring virtual linux desktops. +# +# Copyright (C) 2018 Nico Rittstieg +# +# This program is free software: +# you can redistribute it and/or modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. +# If not, see . diff --git a/savedesktop/rd.py b/savedesktop/rd.py new file mode 100755 index 0000000..7bd5ec4 --- /dev/null +++ b/savedesktop/rd.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +# +# This file is part of savedesktop. +# A CLI tool for saving and restoring virtual linux desktops. +# +# Copyright (C) 2018 Nico Rittstieg +# +# This program is free software: +# you can redistribute it and/or modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. +# If not, see . + +import argparse +import json +import subprocess +import sys +import time +from pathlib import Path +from typing import List + +import savedesktop.wmctrl as wmctrl + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-p", "--profile", default="default", help="profile name") + parser.add_argument("-d", "--desktop", type=int, default=0, help="target desktop number from 0 to n") + parser.add_argument("--version", action="version", version="0.01") + args = parser.parse_args() + window_list = list() + try: + window_list = read_profile(args.profile) + except FileNotFoundError as e: + print("profile '{0}' not found".format(e.filename), file=sys.stderr) + exit(-1) + try: + if not wmctrl.check_wmctrl_installation(): + print("wmctrl is not installed", file=sys.stderr) + exit(-1) + wmctrl.switch_desktop(args.desktop) + for props in window_list: + open_window(args.desktop, props) + except subprocess.CalledProcessError as e: + print("wmctrl did not work properly: {0}".format(str(e)), file=sys.stderr) + exit(-1) + + +def read_profile(profile: str) -> List[dict]: + json_path = Path.home().joinpath(".config/savedesktop/" + profile + ".json") + text = json_path.read_text("UTF-8") + result = json.loads(text) + return result + + +def open_window(desktop: int, props: dict) -> bool: + window_ids = wmctrl.list_window_id(desktop) + print(window_ids) + subprocess.Popen(props["cmd"], shell=False) + for x in range(10): + time.sleep(.50) + new_ids = wmctrl.list_window_id(desktop) - window_ids + if len(new_ids) == 0: + continue + win_id = new_ids.pop() + wmctrl.resize_move(win_id, props["x"], props["y"], props["width"], props["height"]) + if len(props["state"]) > 0: + wmctrl.set_state(win_id, props["state"]) + return True + return False + + +if __name__ == "__main__": + main() diff --git a/savedesktop/sd.py b/savedesktop/sd.py new file mode 100755 index 0000000..12551b0 --- /dev/null +++ b/savedesktop/sd.py @@ -0,0 +1,75 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +# +# This file is part of savedesktop. +# A CLI tool for saving and restoring virtual linux desktops. +# +# Copyright (C) 2018 Nico Rittstieg +# +# This program is free software: +# you can redistribute it and/or modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. +# If not, see . + +import argparse +import json +import subprocess +import sys +from pathlib import Path +from typing import List + +import savedesktop.wmctrl as wmctrl + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-o", "--open", action="store_true", help="open saved profile") + parser.add_argument("-p", "--profile", default="default", help="custom profile name") + parser.add_argument("-d", "--desktop", type=int, default=None, help="desktop number from 0 to n") + parser.add_argument("--version", action="version", version="0.01") + args = parser.parse_args() + try: + if not wmctrl.check_wmctrl_installation(): + print("wmctrl is not installed", file=sys.stderr) + exit(-1) + if not wmctrl.check_xwininfo_installation(): + print("xwininfo is not installed", file=sys.stderr) + exit(-1) + if args.desktop is not None: + desktop = args.desktop + else: + desktop = wmctrl.current_desktop() + window_list = wmctrl.list_window_details(desktop) + json_path = write_profile(window_list, args.profile) + if args.open: + subprocess.call(["xdg-open", str(json_path)]) + + except subprocess.CalledProcessError as e: + print("wmctrl did not work properly: {0}".format(str(e)), file=sys.stderr) + exit(-1) + + +def write_profile(window_list: List[dict], profile: str) -> Path: + # remove unnecessary window properties + for props in window_list: + del props["id"] + del props["desktop"] + del props["pid"] + + text = json.dumps(window_list, indent=2) + conf_dir = Path.home().joinpath(".config/savedesktop") + conf_dir.mkdir(exist_ok=True) + json_path = conf_dir.joinpath(profile + ".json") + json_path.write_text(text, "UTF-8") + return json_path + + +if __name__ == "__main__": + main() diff --git a/savedesktop/wmctrl.py b/savedesktop/wmctrl.py new file mode 100644 index 0000000..9e6f64b --- /dev/null +++ b/savedesktop/wmctrl.py @@ -0,0 +1,156 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +# +# This file is part of savedesktop. +# A CLI tool for saving and restoring virtual linux desktops. +# +# Copyright (C) 2018 Nico Rittstieg +# +# This program is free software: +# you can redistribute it and/or modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. +# If not, see . + +import subprocess +import sys +from typing import List +from typing import Set + + +def check_wmctrl_installation(): + try: + subprocess.call(["wmctrl", "--version"]) + return True + except FileNotFoundError as e: + return False + + +def check_xwininfo_installation(): + try: + subprocess.call(["xwininfo", "-version"]) + return True + except FileNotFoundError as e: + return False + + +def run_wmctrl(*args: str) -> str: + arglist = ["wmctrl"] + arglist.extend(args) + output = subprocess.check_output(arglist, stderr=subprocess.STDOUT) + if output is not None: + output = output.decode(sys.stdout.encoding).strip() + return output + + +def list_window_details(desktop: int = None) -> List[dict]: + result = list() + out = run_wmctrl("-p", "-G", "-l") + for line in out.split('\n'): + tokens = line.split(maxsplit=7) + if desktop is not None and desktop != int(tokens[1]): + continue + props = dict() + props["id"] = tokens[0] + props["desktop"] = int(tokens[1]) + props["pid"] = int(tokens[2]) + props["x"] = int(tokens[3]) + props["y"] = int(tokens[4]) + props["width"] = int(tokens[5]) + props["height"] = int(tokens[6]) + file = open("/proc/{0}/cmdline".format(props["pid"]), "r") + try: + props["cmd"] = file.read().replace('\0', ' ').strip() + finally: + file.close() + xwininfo(props) + if "gnome-terminal-server" in props["cmd"]: + props["cmd"] = "gnome-terminal" + result.append(props) + return result + + +def list_window_id(desktop: int = None) -> Set[int]: + window_list = list_window_details(desktop) + id_set = set() + for props in window_list: + id_set.add(props["id"]) + return id_set + + +def xwininfo(props: dict): + out = subprocess.check_output(["xwininfo", "-id", props["id"], "-stats", "-wm"], stderr=subprocess.STDOUT) + if out is not None: + out = out.decode(sys.stdout.encoding).strip() + lines = out.split('\n') + x = 0 + y = 0 + top_decoration = 0 + left_border = 0 + right_border = 0 + bottom_border = 0 + state = list() + for line in lines: + line = line.strip() + if line.startswith("Absolute upper-left X:"): + # the partition method returns a 3-tuple containing left text, separator, right text + x = int(line.partition(":")[2].strip()) + continue + if line.startswith("Absolute upper-left Y:"): + y = int(line.partition(":")[2].strip()) + continue + if line == "Maximized Horz": + state.append("maximized_horz") + continue + if line == "Maximized Vert": + state.append("maximized_vert") + continue + if line == "Hidden": + state.append("hidden") + continue + if line.startswith("Frame extents:"): + extents = line.partition(":")[2].strip().split(", ") + left_border = int(extents[0]) + left_border = int(extents[1]) + top_decoration = int(extents[2]) + bottom_border = int(extents[3]) + break + props["x"] = x - left_border + props["y"] = y - top_decoration + props["state"] = ",".join(state) + + +def switch_desktop(desktop: int): + run_wmctrl("-s", str(desktop)) + + +def get_winid_by_pid(pid: int) -> str: + out = run_wmctrl("-p", "-l") + print(pid) + print(out) + for line in out.split('\n'): + tokens = line.split(maxsplit=4) + if int(tokens[2]) == pid: + return tokens[0] + + +def resize_move(winid: str, x: int, y: int, width: int, height: int): + run_wmctrl("-i", "-r", winid, "-e", "0,{0},{1},{2},{3}".format(x, y, width, height)) + + +def set_state(winid: str, props: str): + run_wmctrl("-i", "-r", winid, "-b", "add," + props) + + +def current_desktop() -> int: + out = run_wmctrl("-d") + for line in out.split('\n'): + if '*' in line: + return int(line[0]) + return 0 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..28a274b --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +# +# This file is part of savedesktop. +# A CLI tool for saving and restoring virtual linux desktops. +# +# Copyright (C) 2018 Nico Rittstieg +# +# This program is free software: +# you can redistribute it and/or modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. +# If not, see . + +import setuptools + +with open('README.md', 'r') as fh: + long_description = fh.read() + +setuptools.setup( + name='savedesktop', + version='0.0.1', + entry_points={ + 'console_scripts': ['sd=savedesktop.sd:main', 'rd=savedesktop.rd:main'] + }, + author='Nico Rittstieg', + description='cli script to save and restore virtual desktops', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://gitlab.com/nrittsti/sd', + packages=setuptools.find_packages(), + license="GPL", + keywords=['window manager', 'wmctrl', 'desktop'], + classifiers=( + 'Programming Language :: Python :: 3', + 'License :: OSI Approved :: GPL License', + 'Operating System :: Linux', + 'Intended Audience :: End Users/Desktop', + ), +)