Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 57 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,59 +133,87 @@ pip3 install --user .

Full command line documentation:
```
Usage: i3-resurrect save [OPTIONS]

Save an i3 workspace's layout and running programs to a file.
Usage: i3-resurrect save [OPTIONS] [WORKSPACES]...

Save i3 workspace(s) layout(s) or whole session and running programs to a file.

Use cases:

i3-resurrect save [--session]

i3-resurrect save [--workspace] WORKSPACES

WORKSPACES are the workspace(s) to save. [default: current workspace]

Options:
-w, --workspace TEXT The workspace to save.
[default: current workspace]
-w, --workspace Save workspace(s).
-n, --numeric Select workspace by number instead of name.
-d, --directory DIRECTORY The directory to save the workspace to.
[default: ~/.i3/i3-resurrect]
-p, --profile TEXT The profile to save the workspace to.
-s, --swallow TEXT The swallow criteria to use.
[options: class,instance,title,window_role]
-S, --session Save current session.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial thought is that I would prefer -a/--all

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it depends on your semantics, as an outsider that is more used to other wms restoring the session sounds logical.

-d, --directory DIRECTORY The directory to save the workspace to. [default: ~/.i3/i3-resurrect]
-p, --profile TEXT The profile to save.
-s, --swallow TEXT The swallow criteria to use. [options: class,instance,title,window_role]
[default: class,instance]
--layout-only Only save layout.
--programs-only Only save running programs.
-h, --help Show this message and exit.


Usage: i3-resurrect restore [OPTIONS]
Usage: i3-resurrect restore [OPTIONS] [WORKSPACES]...

Restore i3 workspace layout and programs.
Restore i3 workspace(s) layout(s) or whole session and programs.

Options:
-w, --workspace TEXT The workspace to restore.
[default: current workspace]
-n, --numeric Select workspace by number instead of name.
-d, --directory DIRECTORY The directory to restore the workspace from.
[default: ~/.i3/i3-resurrect]
-p, --profile TEXT The profile to restore the workspace from.
--layout-only Only restore layout.
--programs-only Only restore running programs.
Use cases:

i3-resurrect restore [--session]

Usage: i3-resurrect ls [OPTIONS] [[workspaces|profiles]]
i3-resurrect restore [--workspace] WORKSPACES

List saved workspaces or profiles.
i3-resurrect restore WORKSPACE_LAYOUT TARGET_WORKSPACE

WORKSPACES are the workspace(s) to restore. [default: current workspace]

WORKSPACE_LAYOUT is the workspace file to load. TARGET_WORKSPACE is the target workspace [default:
current workspace]

Options:
-d, --directory DIRECTORY The directory to search in.
[default: ~/.i3/i3-resurrect]
-w, --workspace Restore workspace(s).
-n, --numeric Select workspace(s) by number instead of name.
-S, --session Restore current session.
-d, --directory DIRECTORY The directory to restore the workspace from. [default: ~/.i3/i3-resurrect]
-p, --profile TEXT The profile to restore.
--layout-only Only restore layout.
--programs-only Only restore running programs.
-c, --clean Move program that are not part of the workspace layout to the
scratchpad.
-r, --reload Move program from the workspace to the scratchpad. before restore it.
-K, --kill Kill unwanted program insted of moving it the scratchpad.
-h, --help Show this message and exit.


Usage: i3-resurrect rm [OPTIONS]
Usage: i3-resurrect rm [OPTIONS] [WORKSPACES]...

Remove saved layout or programs.
Remove saved worspace(s) layout(s), whole session, or programs.

Options:
-w, --workspace TEXT The saved workspace to delete.
-d, --directory DIRECTORY The directory to delete from.
[default: ~/.i3/i3-resurrect]
-w, --workspace Delete workspace(s) files.
-S, --session Delete saved session layout.
-d, --directory DIRECTORY The directory to delete from. [default: ~/.i3/i3-resurrect]
-p, --profile TEXT The profile to delete.
--layout-only Only delete saved layout.
--programs-only Only delete saved programs.
-h, --help Show this message and exit.


Usage: i3-resurrect kill [OPTIONS] [WORKSPACES]...

Kill workspace(s) or whole session.

Options:
-w, --workspace Kill workspace(s) layout(s) and program(s).
-S, --session Kill all workspace(s) and program(s) in current session.
-F, --force Do not ask for confirmation.
-h, --help Show this message and exit.
```

Basic usage, matching only window class/instance:
Expand Down
3 changes: 3 additions & 0 deletions i3_resurrect/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ def create_default():
],
'window_swallow_criteria': {},
'terminals': ['Gnome-terminal', 'Alacritty'],
'map_timeout': 0,
'exec_timeout': 0,
'restore_timeout': 2,
}

# Make config directory if it doesn't exist.
Expand Down
118 changes: 90 additions & 28 deletions i3_resurrect/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,36 @@
import shlex
import subprocess
import sys
from time import sleep
import tempfile
from pathlib import Path

import i3ipc

from . import config
from . import programs
from . import treeutils
from . import util


def save(workspace, numeric, directory, profile, swallow_criteria):
def list(i3, numeric):
# List all active workspaces
workspaces_data = i3.get_workspaces()
workspaces = []
for n, workspace_data in enumerate(workspaces_data):
# Get all active workspaces from session
if numeric:
workspaces.append(str(workspace_data.num))
else:
workspaces.append(workspace_data.name)
return workspaces

def save(workspace, numeric, directory, swallow_criteria):
"""
Save an i3 workspace layout to a file.
"""
workspace_id = util.filename_filter(workspace)
filename = f'workspace_{workspace_id}_layout.json'
if profile is not None:
filename = f'{profile}_layout.json'
layout_file = Path(directory) / filename

workspace_tree = treeutils.get_workspace_tree(workspace, numeric)
Expand All @@ -34,59 +47,106 @@ def save(workspace, numeric, directory, profile, swallow_criteria):
)


def read(workspace, directory, profile):
def read(workspace, directory):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the profile removed?

"""
Read saved layout file.
"""
workspace_id = util.filename_filter(workspace)
filename = f'workspace_{workspace_id}_layout.json'
if profile is not None:
filename = f'{profile}_layout.json'
layout_file = Path(directory) / filename

layout = None
try:
layout = json.loads(layout_file.read_text())
except FileNotFoundError:
if profile is not None:
util.eprint(f'Could not find saved layout for profile "{profile}"')
else:
util.eprint('Could not find saved layout for workspace '
util.eprint('Could not find saved layout for workspace '
f'"{workspace}"')
sys.exit(1)
return layout


def restore(workspace_name, layout):
def remove_windows_from_workspace(normal_windows, kill=False):
for window in normal_windows:
# window and program instance that don't match to saved list have to be killed
if kill:
xdo_kill_window(window['window'])
else:
xdo_map_window(window['window'])
xdo_focus_window(window['window'])
i3.command(f'move scratchpad')


def clean_workspace(layout, saved_programs, normal_windows, target, kill=False):
""""
Move windows that don't match saved programs in layout to scatchpad or kill it.
"""
i3 = i3ipc.Connection()
preserved_windows = []
saved_windows = treeutils.get_leaves(layout)
for saved_program in saved_programs:
current_score = 0
best_match = None
# Get swallow criterias of saved program to restore
rule = saved_program['window_properties']

for window in normal_windows:
window_properties = window['window_properties']
if rule['class'] == window_properties['class']:
# The window is part of the saved layout
# calculate match score of window
score = programs.calc_rule_match_score(rule, window_properties)
if score > current_score:
current_score = score
# Bestmatched window
best_match = window

if current_score != 0:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you just don't want to relaunch programs that are already running, the code for that already exists so why not just use it? See line 72 onwards of programs.py on master branch, and the get_programs() function in there. You're also adding a requirement to store window properties in the saved programs files which I do not like.

# saved window already open
preserved_windows.append(best_match)
normal_windows.remove(best_match)

remove_windows_from_workspace(normal_windows, target)

return preserved_windows


def restore(workspace_name, layout, saved_programs, target, kill=False):
"""
Restore an i3 workspace layout.
"""
if layout == {}:
return
window_ids = []
placeholder_window_ids = []
normal_windows = []
placeholder_windows = []

# Get ids of all placeholder or normal windows in workspace.
ws = treeutils.get_workspace_tree(workspace_name, False)
windows = treeutils.get_leaves(ws)
for con in windows:
window_id = con['window']
if is_placeholder(con):

for window in windows:
if is_placeholder(window):
# If window is a placeholder, add it to list of placeholder
# windows.
placeholder_window_ids.append(window_id)
placeholder_windows.append(window)
else:
# Otherwise, add it to the list of regular windows.
window_ids.append(window_id)
normal_windows.append(window)

# Unmap all non-placeholder windows in workspace.
for window_id in window_ids:
xdo_unmap_window(window_id)
if target == 'clean':
preserved_windows = clean_workspace(layout, saved_programs, normal_windows, target, kill)
elif target == 'reload':
remove_windows_from_workspace(normal_windows, target, kill)
else:
preserved_windows = normal_windows

# Remove any remaining placeholder windows in workspace so that we don't
# have duplicates.
for window_id in placeholder_window_ids:
xdo_kill_window(window_id)
for window in placeholder_windows:
xdo_kill_window(window['window'])

if layout == {}:
return

# Unmap all non-placeholder windows in workspace.
for window in preserved_windows:
xdo_unmap_window(window['window'])

try:
i3 = i3ipc.Connection()
Expand Down Expand Up @@ -125,8 +185,10 @@ def restore(workspace_name, layout):
finally:
# Map all unmapped windows. We use finally because we don't want the
# user to lose their windows no matter what.
for window_id in window_ids:
xdo_map_window(window_id)
for window in preserved_windows:
xdo_map_window(window['window'])
map_timeout = config.get('map_timeout', 0)
sleep(map_timeout)


def build_layout(tree, swallow):
Expand Down
Loading