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',
+ ),
+)