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