├── readme.md └── i3session /readme.md: -------------------------------------------------------------------------------- 1 | i3session 2 | ========= 3 | 4 | `i3session` remembers what's running in your i3 workspaces by saving a session file (in ~/.i3/session). 5 | It is then able to restore the running processes (and their simple orientation) to one or more workspaces. 6 | 7 | Since `i3session` executes i3 commands sequentially (tree traversal), changing focus during restore will affect where clients open. By default, the `i3-nagbar` will appear during restore with a message to remind you of this. 8 | 9 | **Note: The restore command does not remove the current contents of a workspace, so you'll want to (manually) clear a workspace before restoring to it.** 10 | 11 | Saving a session 12 | ---------------- 13 | 14 | % i3session save 15 | Saving... 16 | Session saved to ~/.i3/session 17 | 18 | 19 | Restoring an individual workspace 20 | --------------------------------- 21 | 22 | % i3session restore 1 23 | Restoring... 24 | Session restored from ~/.i3/session 25 | 26 | 27 | Restoring all workspaces 28 | ------------------------ 29 | 30 | % i3session restore 31 | Restoring... 32 | Session restored from ~/.i3/session 33 | 34 | Dependencies 35 | ------------ 36 | 37 | * Python 38 | * i3-py (https://github.com/ziberna/i3-py) 39 | * i3-nagbar 40 | * xprop 41 | * PyXDG 42 | -------------------------------------------------------------------------------- /i3session: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import os 4 | import i3 5 | import sys 6 | import pickle 7 | import subprocess 8 | import logging 9 | from time import sleep 10 | from xdg.BaseDirectory import * 11 | 12 | class Node: 13 | def __init__(self, data, parent = None): 14 | self.data = data 15 | self.parent = parent 16 | 17 | def __str__(self): 18 | dictionary = { 19 | 'id': self.data['id'], 20 | 'name': self.data['name'], 21 | 'orientation': self.data['orientation'] 22 | } 23 | 24 | if 'process' in self.data: 25 | dictionary['process'] = self.data['process'] 26 | if self.parent: 27 | dictionary['parent'] = self.parent.data['id'] 28 | 29 | return str(self.__class__) + str(dictionary) 30 | 31 | @property 32 | def data(self): 33 | return self.data 34 | 35 | @property 36 | def parent(self): 37 | return self.parent 38 | 39 | @property 40 | def children(self): 41 | return self.data['nodes'] 42 | 43 | def has_children(self): 44 | return 'nodes' in self.data 45 | 46 | # xprop WM_CLASS -id ID 47 | def get_wm_class(self): 48 | output = subprocess.check_output(["xprop", "WM_CLASS", "-id", str(self.data['window'])]) 49 | return output.split()[3].strip('"').lower() 50 | 51 | def restore(self): 52 | # Use orientation of parent container 53 | if self.parent and self.parent.data['orientation']!='none': 54 | logging.debug("orientation is now %s", self.parent.data['orientation']) 55 | i3.command('split', self.parent.data['orientation']) 56 | 57 | # This is a workspace 58 | if isinstance(self, Workspace): 59 | # Switch to workspace 60 | logging.debug("switching to workspace %s", str(self.data['num'])) 61 | i3.command('workspace', str(self.data['num'])) 62 | Tree.sleep_until_changed() 63 | # This is a client 64 | elif isinstance(self, Client): 65 | # Start this process 66 | logging.debug("exec %s", self.data['process']) 67 | i3.command('exec', self.data['process']) 68 | Tree.sleep_until_changed() 69 | i3.command('focus', 'parent') 70 | Tree.sleep_until_changed() 71 | elif isinstance(self, Container): 72 | pass 73 | 74 | class Workspace(Node): pass 75 | class Client(Node): pass 76 | class Container(Node): pass 77 | 78 | class Tree(): 79 | CHANGE_TIMEOUT = 0.2 80 | CHANGE_RETRY = 5 81 | 82 | # for each node that has a window, get WM_CLASS 83 | @classmethod 84 | def assign_processes(self, nodes): 85 | for n in nodes: 86 | # Recurse subtree 87 | if n['nodes']: 88 | n['nodes'] = Tree.assign_processes(n['nodes']) 89 | # Window ID is set for this client 90 | elif n['window']: 91 | # get process from xprop 92 | node = Node(n) 93 | n[u'process'] = node.get_wm_class() 94 | 95 | return nodes 96 | 97 | # set up workspaces, exec clients 98 | @classmethod 99 | def restore(self, nodes, parent = None, only_workspace = None): 100 | for n in nodes: 101 | if 'num' in n: 102 | node = Workspace(n, parent) 103 | elif 'process' in n: 104 | node = Client(n, parent) 105 | else: 106 | node = Container(n, parent) 107 | 108 | logging.debug(node) 109 | node.restore() 110 | 111 | # Recurse subtree 112 | if node.has_children() == True: 113 | if only_workspace and isinstance(node, Workspace) and str(node.data['num']) != only_workspace: 114 | break 115 | else: 116 | self.restore(node.children, node, only_workspace) 117 | 118 | # TODO: a subscription should be able to pick this up 119 | @classmethod 120 | def sleep_until_changed(self): 121 | i = 0 122 | original_tree = i3.get_tree() 123 | 124 | while i < Tree.CHANGE_RETRY: 125 | i += 1 126 | sleep(Tree.CHANGE_TIMEOUT) 127 | if original_tree != i3.get_tree(): 128 | break 129 | 130 | # use i3-nagbar to show a message while restoring 131 | def nag_bar_process(): 132 | return subprocess.Popen(["i3-nagbar", "-m", "Currently restoring session. Don't change workspace focus!"]) 133 | 134 | # print usage instructions 135 | def show_help(): 136 | print(sys.argv[0] + " [workspace]") 137 | 138 | if __name__ == '__main__': 139 | # logging.basicConfig(level=logging.DEBUG) 140 | 141 | # If ~/.i3 doesn't exist use XDG_CONFIG_DIR 142 | home = os.getenv("HOME") 143 | config_dir = os.path.join(home, '.i3') 144 | 145 | if not os.path.isdir(config_dir): 146 | config_dir = os.path.join(xdg_config_home, 'i3') 147 | 148 | config_file = os.path.join(config_dir, 'session') 149 | 150 | if len(sys.argv) < 2: 151 | show_help() 152 | sys.exit(1) 153 | 154 | if sys.argv[1] == 'save': 155 | print "Saving..." 156 | session = i3.get_tree() 157 | 158 | # traverse tree and assign node processes before storing 159 | if session['nodes']: 160 | session['nodes'] = Tree.assign_processes(session['nodes']) 161 | 162 | pickle.dump(session, open(config_file, "wb")) 163 | print "Session saved to " + config_file 164 | elif sys.argv[1] == 'restore': 165 | nag_bar = nag_bar_process() 166 | print "Restoring..." 167 | 168 | # load session from file 169 | try: 170 | session = pickle.load(open(config_file, "rb")) 171 | except Exception: 172 | print "Can't restore saved session..." 173 | sys.exit(1) 174 | 175 | # traverse tree and send commands to i3 based on what was saved 176 | if 'nodes' in session: 177 | if len(sys.argv) > 2: 178 | Tree.restore(session['nodes'], None, sys.argv[2]) 179 | else: 180 | Tree.restore(session['nodes']) 181 | 182 | nag_bar.terminate() 183 | print "Session restored from " + config_file 184 | else: 185 | show_help() 186 | sys.exit(1) 187 | --------------------------------------------------------------------------------