diff --git a/README.md b/README.md
index 55495e3..eb9e562 100644
--- a/README.md
+++ b/README.md
@@ -2,22 +2,45 @@
A CLI tool for saving and restoring virtual linux desktops.
-Currently in proof of concept.
+Stage of development: alpha version
+
Main features:
-------------------
- Dumping window geometry and hints to human readable JSON files
- Apply the saved layout to any virtual desktop
- Command Line Interface
+ - GTK Interface
Dependencies:
----------------------
- X Window Manager that implement the EWMH specification
- wmctrl (https://sites.google.com/site/tstyblo/wmctrl/)
- - xwininfo
+ - xwininfo (for getting window geometry and extents)
+ - xdotool (for hiding windows)
- Python 3
- python-setuptools
+ - zenity, yad (for gtk dialogs)
+
+Compatible:
+--------------------------
+
+Tested with Cinnamon Window Manager (Muffin)
+
+- Chromium
+- Firefox
+- Galculator
+- Gedit
+- Libre Office
+- Nemo
+- Wine Apps
+- Xed
+
+Incompatible:
+--------------------------
+
+- Gimp, setting gemometry failed
Installation:
--------------------------
@@ -25,7 +48,7 @@ Installation:
Arch Linux package:
```
-$ pacman -U savedesktop-*.pkg.tar.xz
+$ sudo pacman -U savedesktop-*.pkg.tar.xz
```
For manual installation use the following command:
@@ -40,7 +63,7 @@ Usage:
dump a desktop:
```
-$ sd -d 0 -p profile1 -o
+$ svd -d 0 -p profile1 -o
```
options:
@@ -50,17 +73,56 @@ options:
`-o` open in default editor
+save with gui:
+
+```
+$ svd --gui
+```
+
restore a desktop:
```
-$ rd -d 1 -p profile1
+$ rvd -d 1 -p profile1
```
`-d 1` restore to second desktop
-`-p profile1` load ~/.config/savedesktop/profile1.json
+`-p profile1` load ~/.config/savedesktop/profile1.json
+
+restore with gui dialog:
+
+```
+$ rvd --gui
+```
+
+Profiles:
+--------------------
+
+Profile files are stored in '~/.config/savedesktop'
+
+```
+[
+ {
+ "x": 403,
+ "y": 219,
+ "width": 1094,
+ "height": 599,
+ "cmd": [
+ "nemo"
+ ],
+ "state": ""
+ }
+]
+```
+
+The following state properties are supported:
+- maximized_vert
+- maximized_horz
+- shaded
+- hidden
+- fullscreen.
-Project Web site :
+Project Web site:
--------------------
https://github.com/nrittsti/savedesktop/
diff --git a/data/restoredesktop.desktop b/data/restoredesktop.desktop
new file mode 100644
index 0000000..e5a9e59
--- /dev/null
+++ b/data/restoredesktop.desktop
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Name=Restore Desktop
+Comment=Restore virtual desktop
+Exec=rvd --gui
+Icon=desktop
+Terminal=false
+Type=Application
+StartupNotify=true
+Categories=Utility;
+Keywords=desktop;workspace;wmctrl;
\ No newline at end of file
diff --git a/data/rvd.1 b/data/rvd.1
new file mode 100644
index 0000000..266069d
--- /dev/null
+++ b/data/rvd.1
@@ -0,0 +1,56 @@
+.TH rvd 1 "23 Dezember 2018" "0.1" "rvd man page"
+.\"---------------------------------------------------------------
+.SH NAME
+.\"---------------------------------------------------------------
+rvd \- restore virtual desktop
+.\"---------------------------------------------------------------
+.SH SYNOPSIS
+.\"---------------------------------------------------------------
+.B rvd
+.RI [ " options " ] ...
+.\"---------------------------------------------------------------
+.SH DESCRIPTION
+.\"---------------------------------------------------------------
+.B rvd
+loads stored desktop profiles.
+The profiles must be stored in the directory ~/.config/savedesktop.
+
+.B OPTIONS
+
+.TP
+\fB\-d\fR, \fB\-\-desktop\fR
+desktop number from 0 to n (default is 0)
+
+.TP
+\fB\-g\fR, \fB\-\-gui\fR
+gui mode, gtk interface based on yad and zenity
+
+.TP
+\fB\-p\fR, \fB\-\-profile\fR
+profile name with default ~/.config/savedesktop/default.json
+
+.\"---------------------------------------------------------------
+.SH EXAMPLES
+.\"---------------------------------------------------------------
+
+Restore second desktop with profile 'demo'
+
+ rvd -d 1 -p demo
+
+Show user interface
+
+ rvd --gui
+
+.\"---------------------------------------------------------------
+.SH AUTHOR
+.\"---------------------------------------------------------------
+SD was written by Nico Rittstieg.
+
+https://github.com/nrittsti/savedesktop
+.\"---------------------------------------------------------------
+.SH SEE ALSO
+.\"---------------------------------------------------------------
+.BR svd (1)
+.BR wmctrl (1)
+.BR xdotool (1)
+.BR xwininfo (1)
\ No newline at end of file
diff --git a/data/savedesktop.desktop b/data/savedesktop.desktop
new file mode 100644
index 0000000..ed15e5c
--- /dev/null
+++ b/data/savedesktop.desktop
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Name=Save Desktop
+Comment=Save virtual desktop
+Exec=svd --gui
+Icon=desktop
+Terminal=false
+Type=Application
+StartupNotify=true
+Categories=Utility;
+Keywords=desktop;workspace;wmctrl;
\ No newline at end of file
diff --git a/data/svd.1 b/data/svd.1
new file mode 100644
index 0000000..15893a0
--- /dev/null
+++ b/data/svd.1
@@ -0,0 +1,77 @@
+.TH svd 1 "23 Dezember 2018" "0.1" "svd man page"
+.\"---------------------------------------------------------------
+.SH NAME
+.\"---------------------------------------------------------------
+svd \- save virtual desktop
+.\"---------------------------------------------------------------
+.SH SYNOPSIS
+.\"---------------------------------------------------------------
+.B svd
+.RI [ " options " ] ...
+.\"---------------------------------------------------------------
+.SH DESCRIPTION
+.\"---------------------------------------------------------------
+.B svd
+reads geometry information and properties from all running desktop applications
+and saves the collected data to disk. Profile files are stored in '~/.config/savedesktop'.
+The profiles are saved in json format and can be easily customized.
+
+[
+ {
+ "x": 403,
+ "y": 219,
+ "width": 1094,
+ "height": 599,
+ "cmd": [
+ "nemo"
+ ],
+ "state": ""
+ }
+]
+
+The following state properties are supported:
+maximized_vert, maximized_horz, shaded, hidden, fullscreen.
+
+.B OPTIONS
+
+.TP
+\fB\-d\fR, \fB\-\-desktop\fR
+desktop number from 0 to n (default is 0)
+
+.TP
+\fB\-g\fR, \fB\-\-gui\fR
+gui mode, gtk interface based on yad and zenity
+
+.TP
+\fB\-o\fR, \fB\-\-open\fR
+open saved profile with xdg-open
+
+.TP
+\fB\-p\fR, \fB\-\-profile\fR
+profile name with default ~/.config/savedesktop/default.json
+
+.\"---------------------------------------------------------------
+.SH EXAMPLES
+.\"---------------------------------------------------------------
+
+Save second desktop to profile 'demo'
+
+ svd -d 1 -p demo
+
+Show user interface
+
+ svd --gui
+
+.\"---------------------------------------------------------------
+.SH AUTHOR
+.\"---------------------------------------------------------------
+svd was written by Nico Rittstieg.
+
+https://github.com/nrittsti/savedesktop
+.\"---------------------------------------------------------------
+.SH SEE ALSO
+.\"---------------------------------------------------------------
+.BR rd (1)
+.BR wmctrl (1)
+.BR xdotool (1)
+.BR xwininfo (1)
\ No newline at end of file
diff --git a/read.py b/read.py
index 6ce6c7d..fbc808f 100644
--- a/read.py
+++ b/read.py
@@ -18,7 +18,7 @@
# You should have received a copy of the GNU General Public License along with this program.
# If not, see .
-import savedesktop.rd as rd
+import savedesktop.rvd as rvd
if __name__ == '__main__':
- rd.main()
+ rvd.main()
diff --git a/save.py b/save.py
index 22b0c15..7b4f2ed 100644
--- a/save.py
+++ b/save.py
@@ -18,7 +18,7 @@
# You should have received a copy of the GNU General Public License along with this program.
# If not, see .
-import savedesktop.sd as sd
+import savedesktop.svd as svd
if __name__ == '__main__':
- sd.main()
+ svd.main()
diff --git a/savedesktop/check.py b/savedesktop/check.py
new file mode 100644
index 0000000..f09fe99
--- /dev/null
+++ b/savedesktop/check.py
@@ -0,0 +1,106 @@
+#!/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 subprocess
+import sys
+
+import savedesktop.gui as gui
+import savedesktop.wmctrl as wmctrl
+
+
+def check_dependencies(args: argparse.Namespace):
+ if not check_wmctrl_installation():
+ print("wmctrl is not installed", file=sys.stderr)
+ if args.gui:
+ gui.show_error("wmctrl is not installed")
+ exit(-1)
+ if not check_xwininfo_installation():
+ print("xwininfo is not installed", file=sys.stderr)
+ if args.gui:
+ gui.show_error("xwininfo is not installed")
+ exit(-1)
+ if not check_xdotool_installation():
+ print("xdotool is not installed", file=sys.stderr)
+ if args.gui:
+ gui.show_error("xdotool is not installed")
+ exit(-1)
+ if args.gui and not check_yad_installation():
+ print("yad is not installed", file=sys.stderr)
+ if args.gui:
+ gui.show_error("yad is not installed")
+ exit(-1)
+ if args.gui and not check_zenity_installation():
+ print("zenity is not installed", file=sys.stderr)
+ if args.gui:
+ gui.show_error("zenity is not installed")
+ exit(-1)
+
+
+def check_wmctrl_installation():
+ try:
+ subprocess.call(["wmctrl", "--version"], stdout=subprocess.DEVNULL)
+ return True
+ except FileNotFoundError as e:
+ return False
+
+
+def check_xwininfo_installation():
+ try:
+ subprocess.call(["xwininfo", "-version"], stdout=subprocess.DEVNULL)
+ return True
+ except FileNotFoundError as e:
+ return False
+
+
+def check_xdotool_installation():
+ try:
+ subprocess.call(["xdotool", "--version"], stdout=subprocess.DEVNULL)
+ return True
+ except FileNotFoundError as e:
+ return False
+
+
+def check_zenity_installation():
+ try:
+ subprocess.call(["zenity", "--version"], stdout=subprocess.DEVNULL)
+ return True
+ except FileNotFoundError as e:
+ return False
+
+
+def check_yad_installation():
+ try:
+ subprocess.call(["yad", "--version"], stdout=subprocess.DEVNULL)
+ return True
+ except FileNotFoundError as e:
+ return False
+
+
+def check_desktop(args: argparse.Namespace, desktop_count: int):
+ if args.desktop is not None:
+ if args.desktop < 0 or args.desktop >= desktop_count:
+ msg = "Desktop '{}' is invalid! Choose between 0 and {}".format(args.desktop, desktop_count - 1)
+ print(msg, file=sys.stderr)
+ if args.gui:
+ gui.show_error(msg)
+ exit(-1)
+ else:
+ args.desktop = wmctrl.current_desktop()
diff --git a/savedesktop/gui.py b/savedesktop/gui.py
new file mode 100644
index 0000000..53c9368
--- /dev/null
+++ b/savedesktop/gui.py
@@ -0,0 +1,99 @@
+#!/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 subprocess
+import time
+from typing import List
+
+
+def show_error(msg: str):
+ subprocess.check_output(["zenity",
+ "--error",
+ "--title=Save Virtual Desktop",
+ "--width=600",
+ "--text={}".format(msg)])
+
+
+def show_save_desktop(args: argparse.Namespace, desktop_list: List[str], profile_list: List[str]):
+ if len(profile_list) == 0:
+ profile_list.append("default")
+ if args.profile not in profile_list:
+ profile_list.insert(0, args.profile)
+ else:
+ for i in range(len(profile_list)):
+ if profile_list[i] == args.profile:
+ profile_list[i] = "^{}".format(args.profile)
+ break
+ if args.desktop is not None:
+ desktop_list[args.desktop] = "^{}".format(desktop_list[args.desktop])
+ try:
+ output = subprocess.check_output(["yad",
+ "--width=300",
+ "--form",
+ "--title=Save Virtual Desktop",
+ "--field=Desktop:CB",
+ "!".join(desktop_list),
+ "--field=Profile:CBE",
+ "!".join(profile_list),
+ "--field=Open JSON file:CHK",
+ "TRUE" if args.open else "FALSE",
+ ],
+ stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError:
+ # Return Code 1 -> The user has pressed Cancel button
+ exit(0)
+ # parse yad output : b'0 - Workspace 1|default|TRUE|\n'
+ values = str(output)[2:-4].split("|")
+ args.desktop = int(values[0][0])
+ args.profile = values[1].strip()
+ if len(args.profile) == 0:
+ args.profile = "default"
+ args.open = "TRUE" == values[2]
+ # avoid wmctrl timing probs
+ time.sleep(.100)
+
+
+def show_restore_desktop(args: argparse.Namespace, desktop_list: List[str], profile_list: List[str]):
+ for i in range(len(profile_list)):
+ if profile_list[i] == args.profile:
+ profile_list[i] = "^{}".format(args.profile)
+ break
+ desktop_list[args.desktop] = "^{}".format(desktop_list[args.desktop])
+ try:
+ output = subprocess.check_output(["yad",
+ "--width=300",
+ "--form",
+ "--title=Restore Virtual Desktop",
+ "--field=Desktop:CB",
+ "!".join(desktop_list),
+ "--field=Profile:CB",
+ "!".join(profile_list),
+ ],
+ stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError:
+ # Return Code 1 -> The user has pressed Cancel button
+ exit(0)
+ # parse yad output : b'0 - Workspace 1|default|\n'
+ values = str(output)[2:-4].split("|")
+ args.desktop = int(values[0][0])
+ args.profile = values[1].strip()
+ # avoid wmctrl timing probs
+ time.sleep(.100)
diff --git a/savedesktop/sd.py b/savedesktop/profile.py
old mode 100755
new mode 100644
similarity index 50%
rename from savedesktop/sd.py
rename to savedesktop/profile.py
index 12551b0..50be1f0
--- a/savedesktop/sd.py
+++ b/savedesktop/profile.py
@@ -18,42 +18,29 @@
# 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 Dict
from typing import List
-import savedesktop.wmctrl as wmctrl
+def list_profiles() -> List[str]:
+ pdir = Path.home().joinpath(".config/savedesktop")
+ result = list()
+ if pdir.exists():
+ files = pdir.glob("*.json")
+ for file in files:
+ result.append(file.stem)
+ return result
-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 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)
+ for profile in result:
+ set_default_values(profile)
+ return result
def write_profile(window_list: List[dict], profile: str) -> Path:
@@ -62,6 +49,7 @@ def write_profile(window_list: List[dict], profile: str) -> Path:
del props["id"]
del props["desktop"]
del props["pid"]
+ del props["subtract_extents"]
text = json.dumps(window_list, indent=2)
conf_dir = Path.home().joinpath(".config/savedesktop")
@@ -71,5 +59,6 @@ def write_profile(window_list: List[dict], profile: str) -> Path:
return json_path
-if __name__ == "__main__":
- main()
+def set_default_values(profile: Dict):
+ profile.setdefault("timeout", 5)
+ profile.setdefault("subtract_extents", True)
diff --git a/savedesktop/rd.py b/savedesktop/rd.py
deleted file mode 100755
index 7bd5ec4..0000000
--- a/savedesktop/rd.py
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/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/rvd.py b/savedesktop/rvd.py
new file mode 100755
index 0000000..6d73dd0
--- /dev/null
+++ b/savedesktop/rvd.py
@@ -0,0 +1,91 @@
+#!/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 subprocess
+import sys
+import time
+import traceback
+from typing import Dict
+
+import savedesktop.check as c
+import savedesktop.gui as gui
+import savedesktop.profile as p
+import savedesktop.wmctrl as wmctrl
+
+
+def main():
+ parser = argparse.ArgumentParser(description='Restore virtual desktops (workspaces)')
+ parser.add_argument("-g", "--gui", action="store_true", help="gui mode")
+ 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.1.0")
+ args = parser.parse_args()
+ c.check_dependencies(args)
+ try:
+ restore(args)
+ except Exception as e:
+ traceback.print_exc()
+ if args.gui:
+ gui.show_error("{}: {}".format(type(e).__name__, str(e)))
+ exit(-1)
+
+
+def restore(args: argparse.Namespace):
+ desktop_list = wmctrl.list_desktop()
+ c.check_desktop(args, len(desktop_list))
+ if args.gui:
+ gui.show_restore_desktop(args, desktop_list, p.list_profiles())
+ print("Restoring profile: {}".format(args.profile))
+ try:
+ window_list = p.read_profile(args.profile)
+ except FileNotFoundError as e:
+ print("profile '{0}' not found".format(e.filename), file=sys.stderr)
+ exit(-1)
+ wmctrl.switch_desktop(args.desktop)
+ for props in window_list:
+ open_window(args.desktop, props)
+
+
+def open_window(desktop: int, profile: Dict) -> bool:
+ window_ids = wmctrl.list_window_id(desktop)
+ print("Popen: {}".format(" ".join(profile["cmd"])))
+ profile["cmd"].insert(0, "nohup")
+ subprocess.Popen(profile["cmd"], shell=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ timeout = 5 if (profile["timeout"] is None) else profile["timeout"]
+ for x in range(timeout * 10):
+ time.sleep(.10)
+ new_ids = wmctrl.list_window_id(desktop) - window_ids
+ if len(new_ids) == 0:
+ continue
+ win_id = new_ids.pop()
+ wmctrl.reset_maximized_state(win_id)
+ print("Apply: x={} y={} w={} h={} state={}".format(profile["x"], profile["y"],
+ profile["width"], profile["height"], profile["state"]))
+ wmctrl.resize_move(win_id, profile["x"], profile["y"], profile["width"], profile["height"])
+ if len(profile["state"]) > 0:
+ wmctrl.set_state(win_id, profile["state"])
+ return True
+ print("timeout reached")
+ return False
+
+
+if __name__ == "__main__":
+ main()
diff --git a/savedesktop/svd.py b/savedesktop/svd.py
new file mode 100755
index 0000000..577085b
--- /dev/null
+++ b/savedesktop/svd.py
@@ -0,0 +1,63 @@
+#!/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 subprocess
+import traceback
+
+import savedesktop.check as c
+import savedesktop.gui as gui
+import savedesktop.profile as p
+import savedesktop.wmctrl as wmctrl
+
+
+def main():
+ parser = argparse.ArgumentParser(description='Save virtual desktops (workspaces)')
+ parser.add_argument("-g", "--gui", action="store_true", help="gui mode")
+ parser.add_argument("-o", "--open", action="store_true", help="show saved profile")
+ parser.add_argument("-p", "--profile", default="default", help="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.1.0")
+ args = parser.parse_args()
+ c.check_dependencies(args)
+ try:
+ save(args)
+ except Exception as e:
+ traceback.print_exc()
+ if args.gui:
+ gui.show_error("{}: {}".format(type(e).__name__, str(e)))
+ exit(-1)
+
+
+def save(args: argparse.Namespace):
+ desktop_list = wmctrl.list_desktop()
+ c.check_desktop(args, len(desktop_list))
+ if args.gui:
+ gui.show_save_desktop(args, desktop_list, p.list_profiles())
+ window_list = wmctrl.list_window_details(args.desktop)
+ json_path = p.write_profile(window_list, args.profile)
+ print("profile saved to {}".format(str(json_path)))
+ if args.open:
+ subprocess.call(["nohup", "xdg-open", str(json_path)], shell=False, stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/savedesktop/wmctrl.py b/savedesktop/wmctrl.py
index 9e6f64b..33d2736 100644
--- a/savedesktop/wmctrl.py
+++ b/savedesktop/wmctrl.py
@@ -20,24 +20,11 @@
import subprocess
import sys
+from typing import Dict
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
+import savedesktop.profile as p
def run_wmctrl(*args: str) -> str:
@@ -56,27 +43,38 @@ def list_window_details(desktop: int = None) -> List[dict]:
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")
+ profile = dict()
+ profile["id"] = tokens[0]
+ profile["desktop"] = int(tokens[1])
+ profile["pid"] = int(tokens[2])
+ profile["x"] = int(tokens[3])
+ profile["y"] = int(tokens[4])
+ profile["width"] = int(tokens[5])
+ profile["height"] = int(tokens[6])
+ p.set_default_values(profile)
+ file = open("/proc/{0}/cmdline".format(profile["pid"]), "r")
try:
- props["cmd"] = file.read().replace('\0', ' ').strip()
+ profile["cmd"] = list(filter(str.strip, file.read().split('\0')))
+ for i in range(len(profile["cmd"])):
+ profile["cmd"][i] = profile["cmd"][i].strip()
finally:
file.close()
- xwininfo(props)
- if "gnome-terminal-server" in props["cmd"]:
- props["cmd"] = "gnome-terminal"
- result.append(props)
+ fix_window_props(profile)
+ xwininfo(profile)
+ result.append(profile)
return result
-def list_window_id(desktop: int = None) -> Set[int]:
+def fix_window_props(profile: dict):
+ if "gnome-terminal-server" in profile["cmd"][0]:
+ profile["cmd"] = ["gnome-terminal"]
+ if "soffice.bin" in profile["cmd"][0]:
+ profile["subtract_extents"] = False
+ if profile["cmd"][0].endswith(".exe"):
+ profile["subtract_extents"] = False
+
+
+def list_window_id(desktop: int = None) -> Set[str]:
window_list = list_window_details(desktop)
id_set = set()
for props in window_list:
@@ -84,8 +82,8 @@ def list_window_id(desktop: int = None) -> Set[int]:
return id_set
-def xwininfo(props: dict):
- out = subprocess.check_output(["xwininfo", "-id", props["id"], "-stats", "-wm"], stderr=subprocess.STDOUT)
+def xwininfo(profile: Dict):
+ out = subprocess.check_output(["xwininfo", "-id", profile["id"], "-stats", "-wm"], stderr=subprocess.STDOUT)
if out is not None:
out = out.decode(sys.stdout.encoding).strip()
lines = out.split('\n')
@@ -121,9 +119,14 @@ def xwininfo(props: dict):
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)
+ if profile["subtract_extents"]:
+ profile["x"] = x - left_border
+ profile["y"] = y - top_decoration
+ else:
+ profile["x"] = x
+ profile["y"] = y
+
+ profile["state"] = ",".join(state)
def switch_desktop(desktop: int):
@@ -144,8 +147,15 @@ 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 set_state(winid: str, hint: str):
+ if hint == "hidden":
+ subprocess.call(["xdotool", "windowminimize", winid])
+ else:
+ run_wmctrl("-i", "-r", winid, "-b", "add," + hint)
+
+
+def reset_maximized_state(winid: str):
+ run_wmctrl("-i", "-r", winid, "-b", "remove,maximized_vert,maximized_horz")
def current_desktop() -> int:
@@ -154,3 +164,12 @@ def current_desktop() -> int:
if '*' in line:
return int(line[0])
return 0
+
+
+def list_desktop() -> List[str]:
+ out = run_wmctrl("-d")
+ result = list()
+ for line in out.split('\n'):
+ items = line.split(" ")
+ result.append("{} - {}".format(items[0], items[-1]))
+ return result
diff --git a/setup.py b/setup.py
index 28a274b..2451eda 100755
--- a/setup.py
+++ b/setup.py
@@ -20,14 +20,21 @@
import setuptools
+data_files = [
+ ("share/applications", ["data/restoredesktop.desktop", "data/savedesktop.desktop"]),
+ ("share/man/man1", ["data/svd.1"]),
+ ("share/man/man1", ["data/rvd.1"])
+]
+
with open('README.md', 'r') as fh:
long_description = fh.read()
setuptools.setup(
name='savedesktop',
- version='0.0.1',
+ version='0.1.0',
+ data_files=data_files,
entry_points={
- 'console_scripts': ['sd=savedesktop.sd:main', 'rd=savedesktop.rd:main']
+ 'console_scripts': ['svd=savedesktop.svd:main', 'rvd=savedesktop.rvd:main']
},
author='Nico Rittstieg',
description='cli script to save and restore virtual desktops',