From 3855e1aafcb62c7bf98a72b2a303aa37b0c2be45 Mon Sep 17 00:00:00 2001 From: Cedric Leporcq Date: Mon, 30 Dec 2019 00:21:46 +0100 Subject: [PATCH 1/7] Save workspaces profiles in dirs --- i3_resurrect/layout.py | 13 ++------ i3_resurrect/main.py | 68 ++++++++++++++++++---------------------- i3_resurrect/programs.py | 14 ++------- i3_resurrect/util.py | 2 +- 4 files changed, 37 insertions(+), 60 deletions(-) diff --git a/i3_resurrect/layout.py b/i3_resurrect/layout.py index 1d39188..3b912dd 100644 --- a/i3_resurrect/layout.py +++ b/i3_resurrect/layout.py @@ -11,14 +11,12 @@ from . import util -def save(workspace, numeric, directory, profile, swallow_criteria): +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) @@ -34,24 +32,19 @@ def save(workspace, numeric, directory, profile, swallow_criteria): ) -def read(workspace, directory, profile): +def read(workspace, directory): """ 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 diff --git a/i3_resurrect/main.py b/i3_resurrect/main.py index 33c864f..aeb9c0a 100644 --- a/i3_resurrect/main.py +++ b/i3_resurrect/main.py @@ -33,7 +33,7 @@ def main(): '[default: ~/.i3/i3-resurrect]')) @click.option('--profile', '-p', default=None, - help=('The profile to save the workspace to.')) + help=('The profile to save.')) @click.option('--swallow', '-s', default='class,instance', help=('The swallow criteria to use.\n' @@ -81,7 +81,7 @@ def save_workspace(workspace, numeric, directory, profile, swallow, target): '[default: ~/.i3/i3-resurrect]')) @click.option('--profile', '-p', default=None, - help=('The profile to restore the workspace from.')) + help=('The profile to restore.')) @click.option('--layout-only', 'target', flag_value='layout_only', help='Only restore layout.') @@ -104,7 +104,7 @@ def restore_workspace(workspace, numeric, directory, profile, target): sys.exit(1) # Get layout name from file. - workspace_layout = layout.read(workspace, directory, profile) + workspace_layout = layout.read(workspace, directory) if 'name' in workspace_layout and profile is None: workspace_name = workspace_layout['name'] else: @@ -119,7 +119,7 @@ def restore_workspace(workspace, numeric, directory, profile, target): if target != 'layout_only': # Restore programs. - saved_programs = programs.read(workspace, directory, profile) + saved_programs = programs.read(workspace, directory) programs.restore(workspace_name, saved_programs) @@ -129,42 +129,37 @@ def restore_workspace(workspace, numeric, directory, profile, target): default=DEFAULT_DIRECTORY, help=('The directory to search in.\n' '[default: ~/.i3/i3-resurrect]')) -@click.argument('item', - type=click.Choice(['workspaces', 'profiles']), - default='workspaces') -def list_workspaces(directory, item): +@click.option('--profile', '-p', + default=None, + help=('list saved workspaces from given profile.')) +@click.option('--profiles', '-P', + is_flag=True, + help=('list saved profiles names.')) +def list_workspaces(directory, profile, profiles): """ List saved workspaces or profiles. """ - directory = util.resolve_directory(directory) + directory = util.resolve_directory(directory, profile) - if item == 'workspaces': - workspaces = [] - for entry in directory.iterdir(): - if entry.is_file(): - name = entry.name + directory = Path(directory) + items = [] + # import ipdb; ipdb.set_trace() + for entry in directory.iterdir(): + if not profiles and entry.is_file(): + # List workspaces + name = entry.name + if name.rfind('workspace_') != -1: name = name[name.index('_') + 1:] workspace = name[:name.rfind('_')] file_type = name[name.rfind('_') + 1:name.index('.json')] - workspaces.append(f'Workspace {workspace} {file_type}') - workspaces = natsorted(workspaces) - for workspace in workspaces: - print(workspace) - else: - directory = directory / 'profiles' - profiles = [] - try: - for entry in directory.iterdir(): - if entry.is_file(): - name = entry.name - profile = name[:name.rfind('_')] - file_type = name[name.rfind('_') + 1:name.index('.json')] - profiles.append(f'Profile {profile} {file_type}') - profiles = natsorted(profiles) - for profile in profiles: - print(profile) - except FileNotFoundError: - print('No profiles found') + items.append(f'Workspace {workspace} {file_type}') + elif profiles and entry.is_dir(): + # List profiles names + profile = entry.name + items.append(f'Profile {profile}') + items = natsorted(items) + for item in items: + print(item) @main.command('rm') @@ -189,15 +184,12 @@ def remove(workspace, directory, profile, target): """ directory = util.resolve_directory(directory, profile) - if profile is not None: - programs_filename = f'{profile}_programs.json' - layout_filename = f'{profile}_layout.json' - elif workspace is not None: + if workspace is not None: workspace_id = util.filename_filter(workspace) programs_filename = f'workspace_{workspace_id}_programs.json' layout_filename = f'workspace_{workspace_id}_layout.json' else: - util.eprint('Either --profile or --workspace must be specified.') + util.eprint('--workspace must be specified.') sys.exit(1) programs_file = Path(directory) / programs_filename layout_file = Path(directory) / layout_filename diff --git a/i3_resurrect/programs.py b/i3_resurrect/programs.py index d7ea638..f5db078 100644 --- a/i3_resurrect/programs.py +++ b/i3_resurrect/programs.py @@ -13,15 +13,13 @@ from . import util -def save(workspace, numeric, directory, profile): +def save(workspace, numeric, directory): """ Save the commands to launch the programs open in the specified workspace to a file. """ workspace_id = util.filename_filter(workspace) filename = f'workspace_{workspace_id}_programs.json' - if profile is not None: - filename = f'{profile}_programs.json' programs_file = Path(directory) / filename # Print deprecation warning if using old dictionary method of writing @@ -40,25 +38,19 @@ def save(workspace, numeric, directory, profile): f.write(json.dumps(programs, indent=2)) -def read(workspace, directory, profile): +def read(workspace, directory): """ Read saved programs file. """ workspace_id = util.filename_filter(workspace) filename = f'workspace_{workspace_id}_programs.json' - if profile is not None: - filename = f'{profile}_programs.json' programs_file = Path(directory) / filename programs = None try: programs = json.loads(programs_file.read_text()) except FileNotFoundError: - if profile is not None: - util.eprint('Could not find saved programs for profile ' - f'"{profile}"') - else: - util.eprint('Could not find saved programs for workspace ' + util.eprint('Could not find saved programs for workspace ' f'"{workspace}"') sys.exit(1) return programs diff --git a/i3_resurrect/util.py b/i3_resurrect/util.py index 1b5cc4f..bcde9ad 100644 --- a/i3_resurrect/util.py +++ b/i3_resurrect/util.py @@ -28,5 +28,5 @@ def filename_filter(filename): def resolve_directory(directory, profile=None): directory = Path(expandvars(directory)).expanduser() if profile is not None: - directory = directory / 'profiles' + directory = directory / profile return directory From 7e11a3aadee5d66224b0823e3c58271e5e527dd2 Mon Sep 17 00:00:00 2001 From: Cedric Leporcq Date: Mon, 4 May 2020 23:03:56 +0200 Subject: [PATCH 2/7] Allow saving multiples workspaces in once --- i3_resurrect/layout.py | 1 - i3_resurrect/main.py | 186 +++++++++++++++++++++++++++++------------ 2 files changed, 134 insertions(+), 53 deletions(-) diff --git a/i3_resurrect/layout.py b/i3_resurrect/layout.py index 3b912dd..101789a 100644 --- a/i3_resurrect/layout.py +++ b/i3_resurrect/layout.py @@ -46,7 +46,6 @@ def read(workspace, directory): except FileNotFoundError: util.eprint('Could not find saved layout for workspace ' f'"{workspace}"') - sys.exit(1) return layout diff --git a/i3_resurrect/main.py b/i3_resurrect/main.py index aeb9c0a..d7a5d6b 100644 --- a/i3_resurrect/main.py +++ b/i3_resurrect/main.py @@ -22,7 +22,8 @@ def main(): @main.command('save') @click.option('--workspace', '-w', - help='The workspace to save.\n[default: current workspace]') + is_flag=True, + help='Save workspace(s).') @click.option('--numeric', '-n', is_flag=True, help='Select workspace by number instead of name.') @@ -45,13 +46,21 @@ def main(): @click.option('--programs-only', 'target', flag_value='programs_only', help='Only save running programs.') -def save_workspace(workspace, numeric, directory, profile, swallow, target): +@click.argument('workspaces', nargs=-1, default=None) +def save_workspace(workspace, numeric, directory, profile, swallow, target, workspaces): """ - Save an i3 workspace's layout and running programs to a file. + Save i3 workspace(s) layout(s) or whole session and running programs to a file. + + Use cases: + + i3-resurrect save [--workspace] WORKSPACES + + WORKSPACES are the workspace(s) to save. [default: current workspace] """ - if workspace is None: - i3 = i3ipc.Connection() - workspace = i3.get_tree().find_focused().workspace().name + i3 = i3ipc.Connection() + if not workspaces: + # set default value + workspaces = (i3.get_tree().find_focused().workspace().name, ) directory = util.resolve_directory(directory, profile) @@ -59,21 +68,47 @@ def save_workspace(workspace, numeric, directory, profile, swallow, target): Path(directory).mkdir(parents=True, exist_ok=True) if target != 'programs_only': - # Save workspace layout to file. swallow_criteria = swallow.split(',') - layout.save(workspace, numeric, directory, profile, swallow_criteria) + + for workspace_id in workspaces: + if target != 'programs_only': + # Save workspace layout to file. + layout.save(workspace_id, numeric, directory, swallow_criteria) + + if target != 'layout_only': + # Save running programs to file. + programs.save(workspace_id, numeric, directory) + + +def restore_workspace(i3, saved_layout, saved_programs, target): + if saved_layout == None: + return + + # Get layout name from file. + if 'name' in saved_layout: + workspace_name = saved_layout['name'] + else: + util.eprint('Workspace name not found.') + sys.exit(1) + + i3.command(f'workspace --no-auto-back-and-forth {workspace_name}') + + if target != 'programs_only': + # Load workspace layout. + layout.restore(workspace_name, saved_layout) if target != 'layout_only': - # Save running programs to file. - programs.save(workspace, numeric, directory, profile) + # Restore programs. + programs.restore(workspace_name, saved_programs) @main.command('restore') @click.option('--workspace', '-w', - help='The workspace to restore.\n[default: current workspace]') + is_flag=True, + help='Restore workspace(s).') @click.option('--numeric', '-n', is_flag=True, - help='Select workspace by number instead of name.') + help='Select workspace(s) by number instead of name.') @click.option('--directory', '-d', type=click.Path(file_okay=False), default=DEFAULT_DIRECTORY, @@ -88,39 +123,81 @@ def save_workspace(workspace, numeric, directory, profile, swallow, target): @click.option('--programs-only', 'target', flag_value='programs_only', help='Only restore running programs.') -def restore_workspace(workspace, numeric, directory, profile, target): +@click.argument('workspaces', nargs=-1) +def restore_workspaces(workspace, numeric, directory, profile, target, + workspaces): """ - Restore i3 workspace layout and programs. + Restore i3 workspace(s) layout(s) and programs. + + Use cases: + + i3-resurrect restore [--workspace] WORKSPACES + + 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] """ i3 = i3ipc.Connection() - if workspace is None: - workspace = i3.get_tree().find_focused().workspace().name + focused_workspace = i3.get_tree().find_focused().workspace().name - directory = util.resolve_directory(directory, profile) + if not workspaces: + if numeric: + workspaces = (str(i3.get_tree().find_focused().workspace().num), ) + else: + workspaces = (focused_workspace, ) - if numeric and not workspace.isdigit(): - util.eprint('Invalid workspace number.') - sys.exit(1) + directory = util.resolve_directory(directory, profile) - # Get layout name from file. - workspace_layout = layout.read(workspace, directory) - if 'name' in workspace_layout and profile is None: - workspace_name = workspace_layout['name'] + if workspace: + for workspace_id in workspaces: + if numeric and not workspace_id.isdigit(): + util.eprint('Invalid workspace number.') + sys.exit(1) + saved_layout = layout.read(workspace_id, directory) + saved_programs = programs.read(workspace_id, directory) + restore_workspace(i3, saved_layout, saved_programs, target) else: - workspace_name = workspace + workspace_layout = workspaces[0] + # Get layout from file. + saved_layout = layout.read(workspace_layout, directory) + saved_programs = programs.read(workspace_layout, directory) - # Switch to the workspace which we are loading. - i3.command(f'workspace --no-auto-back-and-forth "{workspace_name}"') + for target_workspace in workspaces[1:]: + # Make eventualy possible to restore layout in multiples workspaces + if numeric: + if not workspace_layout.isdigit(): + util.eprint('Invalid workspace number.') + sys.exit(1) - if target != 'programs_only': - # Load workspace layout. - layout.restore(workspace_name, workspace_layout) + if not target_workspace: + target_workspace = str(i3.get_tree().find_focused().workspace().num) + elif not target_workspace.isdigit(): + util.eprint('Invalid workspace number.') + sys.exit(1) + else: + if not target_workspace: + target_workspace = i3.get_tree().find_focused().workspace().name - if target != 'layout_only': - # Restore programs. - saved_programs = programs.read(workspace, directory) - programs.restore(workspace_name, saved_programs) + if numeric: + i3.command(f'workspace --no-auto-back-and-forth number \ + {target_workspace}') + else: + i3.command(f'workspace --no-auto-back-and-forth {target_workspace}') + + if target != 'programs_only': + # Load workspace layout. + layout.restore(target_workspace, saved_layout, + saved_programs) + + if target != 'layout_only': + # Restore programs. + programs.restore(target_workspace, saved_programs) @main.command('ls') @@ -162,10 +239,20 @@ def list_workspaces(directory, profile, profiles): print(item) +def delete(layout_file, programs_file, target): + if target != 'programs_only': + # Delete programs file. + programs_file.unlink() + + if target != 'layout_only': + # Delete layout file. + layout_file.unlink() + + @main.command('rm') @click.option('--workspace', '-w', - default=None, - help='The saved workspace to delete.') + is_flag=True, + help='Delete workspace(s) files.') @click.option('--directory', '-d', type=click.Path(file_okay=False), default=DEFAULT_DIRECTORY, @@ -178,29 +265,24 @@ def list_workspaces(directory, profile, profiles): @click.option('--programs-only', 'target', flag_value='programs_only', help='Only delete saved programs.') -def remove(workspace, directory, profile, target): +@click.argument('workspaces', nargs=-1) +def remove(workspace, directory, profile, target, workspaces): """ - Remove saved layout or programs. + Remove saved worspace(s) layout(s), or programs. """ directory = util.resolve_directory(directory, profile) - if workspace is not None: - workspace_id = util.filename_filter(workspace) - programs_filename = f'workspace_{workspace_id}_programs.json' - layout_filename = f'workspace_{workspace_id}_layout.json' + if workspace: + for workspace_id in workspaces: + workspace_id = util.filename_filter(workspace_id) + programs_filename = f'workspace_{workspace_id}_programs.json' + layout_filename = f'workspace_{workspace_id}_layout.json' + programs_file = Path(directory) / programs_filename + layout_file = Path(directory) / layout_filename + delete(layout_file, programs_file, target) else: util.eprint('--workspace must be specified.') sys.exit(1) - programs_file = Path(directory) / programs_filename - layout_file = Path(directory) / layout_filename - - if target != 'programs_only': - # Delete programs file. - programs_file.unlink() - - if target != 'layout_only': - # Delete layout file. - layout_file.unlink() if __name__ == '__main__': From 840bf08b0fc4e2a39efba9ec2c95aa19991b822f Mon Sep 17 00:00:00 2001 From: Cedric Leporcq Date: Mon, 4 May 2020 23:11:07 +0200 Subject: [PATCH 3/7] Allow saving whole workspaces session --- i3_resurrect/layout.py | 12 +++++++++ i3_resurrect/main.py | 61 ++++++++++++++++++++++++++++++++++++------ i3_resurrect/util.py | 17 ++++++++++++ 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/i3_resurrect/layout.py b/i3_resurrect/layout.py index 101789a..d58330a 100644 --- a/i3_resurrect/layout.py +++ b/i3_resurrect/layout.py @@ -11,6 +11,18 @@ from . import util +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. diff --git a/i3_resurrect/main.py b/i3_resurrect/main.py index d7a5d6b..8cf20fe 100644 --- a/i3_resurrect/main.py +++ b/i3_resurrect/main.py @@ -1,4 +1,6 @@ +import json import sys +import os from pathlib import Path import click @@ -27,6 +29,9 @@ def main(): @click.option('--numeric', '-n', is_flag=True, help='Select workspace by number instead of name.') +@click.option('--session', '-S', + is_flag=True, + help='Save current session.') @click.option('--directory', '-d', type=click.Path(file_okay=False, writable=True), default=DEFAULT_DIRECTORY, @@ -47,12 +52,14 @@ def main(): flag_value='programs_only', help='Only save running programs.') @click.argument('workspaces', nargs=-1, default=None) -def save_workspace(workspace, numeric, directory, profile, swallow, target, workspaces): +def save_workspace(workspace, numeric, session, directory, profile, swallow, target, 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] @@ -70,6 +77,16 @@ def save_workspace(workspace, numeric, directory, profile, swallow, target, work if target != 'programs_only': swallow_criteria = swallow.split(',') + if session and os.path.isdir(directory): + # Purge previous layout files before saving in current profile + purge_directory(directory, target) + + if session: + workspaces = layout.list(i3, numeric) + elif not workspace: + util.eprint('Either --workspace or --session should be specified.') + sys.exit(1) + for workspace_id in workspaces: if target != 'programs_only': # Save workspace layout to file. @@ -109,6 +126,9 @@ def restore_workspace(i3, saved_layout, saved_programs, target): @click.option('--numeric', '-n', is_flag=True, help='Select workspace(s) by number instead of name.') +@click.option('--session', '-S', + is_flag=True, + help='Restore current session.') @click.option('--directory', '-d', type=click.Path(file_okay=False), default=DEFAULT_DIRECTORY, @@ -124,13 +144,15 @@ def restore_workspace(i3, saved_layout, saved_programs, target): flag_value='programs_only', help='Only restore running programs.') @click.argument('workspaces', nargs=-1) -def restore_workspaces(workspace, numeric, directory, profile, target, +def restore_workspaces(workspace, numeric, session, directory, profile, target, workspaces): """ - Restore i3 workspace(s) layout(s) and programs. + Restore i3 workspace(s) layout(s) or whole session and programs. Use cases: + i3-resurrect restore [--session] + i3-resurrect restore [--workspace] WORKSPACES i3-resurrect restore WORKSPACE_LAYOUT TARGET_WORKSPACE @@ -154,7 +176,14 @@ def restore_workspaces(workspace, numeric, directory, profile, target, directory = util.resolve_directory(directory, profile) - if workspace: + if session: + # Restore all workspaces from dir + files = util.list_filenames(directory) + for layout_file, programs_file in files: + saved_layout = json.loads(layout_file.read_text()) + saved_programs = json.loads(programs_file.read_text()) + restore_workspace(i3, saved_layout, saved_programs, target) + elif workspace: for workspace_id in workspaces: if numeric and not workspace_id.isdigit(): util.eprint('Invalid workspace number.') @@ -249,10 +278,22 @@ def delete(layout_file, programs_file, target): layout_file.unlink() +def purge_directory(directory, target): + ''' + purge saved layout session + ''' + files = util.list_filenames(directory) + for layout_file, programs_file in files: + delete(layout_file, programs_file, target) + + @main.command('rm') @click.option('--workspace', '-w', is_flag=True, help='Delete workspace(s) files.') +@click.option('--session', '-S', + is_flag=True, + help='Delete saved session layout.') @click.option('--directory', '-d', type=click.Path(file_okay=False), default=DEFAULT_DIRECTORY, @@ -266,13 +307,17 @@ def delete(layout_file, programs_file, target): flag_value='programs_only', help='Only delete saved programs.') @click.argument('workspaces', nargs=-1) -def remove(workspace, directory, profile, target, workspaces): +def remove(workspace, session, directory, profile, target, workspaces): """ - Remove saved worspace(s) layout(s), or programs. + Remove saved worspace(s) layout(s), whole session, or programs. """ directory = util.resolve_directory(directory, profile) - if workspace: + if session and os.path.isdir(directory): + purge_directory(directory, target) + if profile is not None: + os.rmdir(directory) + elif workspace: for workspace_id in workspaces: workspace_id = util.filename_filter(workspace_id) programs_filename = f'workspace_{workspace_id}_programs.json' @@ -281,7 +326,7 @@ def remove(workspace, directory, profile, target, workspaces): layout_file = Path(directory) / layout_filename delete(layout_file, programs_file, target) else: - util.eprint('--workspace must be specified.') + util.eprint('either --workspace or --session option should be specified.') sys.exit(1) diff --git a/i3_resurrect/util.py b/i3_resurrect/util.py index bcde9ad..4e2ea18 100644 --- a/i3_resurrect/util.py +++ b/i3_resurrect/util.py @@ -1,3 +1,5 @@ +import os +import re import sys from os.path import expandvars from pathlib import Path @@ -30,3 +32,18 @@ def resolve_directory(directory, profile=None): if profile is not None: directory = directory / profile return directory + + +def list_filenames(directory): + (_, _, filenames) = next(os.walk(directory)) + layout_regex = re.compile(r'.*_layout.json') + layout_filenames = list(filter(layout_regex.search, filenames)) + programs_regex = re.compile(r'.*_programs.json') + programs_filenames = list(filter(programs_regex.search, filenames)) + # Create a list of tuples to save workspace files + files = [] + for n, layout_filename in enumerate(layout_filenames): + layout_file = Path(directory) / layout_filename + programs_file = Path(directory) / programs_filenames[n] + files.append((layout_file, programs_file)) + return files From e6f7b2ee5f2d651019fbf120773af61dd0cf7cdb Mon Sep 17 00:00:00 2001 From: Cedric Leporcq Date: Mon, 4 May 2020 23:32:20 +0200 Subject: [PATCH 4/7] Add --clean, --reload and --kill options in restore command --- i3_resurrect/layout.py | 88 ++++++++++++++++++++++++++++++++-------- i3_resurrect/main.py | 26 ++++++++---- i3_resurrect/programs.py | 1 + 3 files changed, 91 insertions(+), 24 deletions(-) diff --git a/i3_resurrect/layout.py b/i3_resurrect/layout.py index d58330a..74d8c08 100644 --- a/i3_resurrect/layout.py +++ b/i3_resurrect/layout.py @@ -7,6 +7,7 @@ import i3ipc +from . import programs from . import treeutils from . import util @@ -61,36 +62,89 @@ def read(workspace, directory): 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: + # 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() @@ -129,8 +183,8 @@ 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']) def build_layout(tree, swallow): diff --git a/i3_resurrect/main.py b/i3_resurrect/main.py index 8cf20fe..f19030d 100644 --- a/i3_resurrect/main.py +++ b/i3_resurrect/main.py @@ -97,8 +97,8 @@ def save_workspace(workspace, numeric, session, directory, profile, swallow, tar programs.save(workspace_id, numeric, directory) -def restore_workspace(i3, saved_layout, saved_programs, target): - if saved_layout == None: +def restore_workspace(i3, saved_layout, saved_programs, target, kill): + if not saved_layout: return # Get layout name from file. @@ -112,7 +112,9 @@ def restore_workspace(i3, saved_layout, saved_programs, target): if target != 'programs_only': # Load workspace layout. - layout.restore(workspace_name, saved_layout) + layout.restore( + workspace_name, saved_layout, saved_programs, target, kill + ) if target != 'layout_only': # Restore programs. @@ -143,9 +145,19 @@ def restore_workspace(i3, saved_layout, saved_programs, target): @click.option('--programs-only', 'target', flag_value='programs_only', help='Only restore running programs.') +@click.option('--clean', '-c', 'target', + flag_value='clean', + help='Move program that are not part of the workspace layout to \ + the scratchpad.') +@click.option('--reload', '-r', 'target', + flag_value='reload', + help='Move program from the workspace to the scratchpad. before restore it.') +@click.option('--kill', '-K', + is_flag=True, + help='Kill unwanted program insted of moving it the scratchpad.') @click.argument('workspaces', nargs=-1) def restore_workspaces(workspace, numeric, session, directory, profile, target, - workspaces): + kill, workspaces): """ Restore i3 workspace(s) layout(s) or whole session and programs. @@ -182,7 +194,7 @@ def restore_workspaces(workspace, numeric, session, directory, profile, target, for layout_file, programs_file in files: saved_layout = json.loads(layout_file.read_text()) saved_programs = json.loads(programs_file.read_text()) - restore_workspace(i3, saved_layout, saved_programs, target) + restore_workspace(i3, saved_layout, saved_programs, target, kill) elif workspace: for workspace_id in workspaces: if numeric and not workspace_id.isdigit(): @@ -190,7 +202,7 @@ def restore_workspaces(workspace, numeric, session, directory, profile, target, sys.exit(1) saved_layout = layout.read(workspace_id, directory) saved_programs = programs.read(workspace_id, directory) - restore_workspace(i3, saved_layout, saved_programs, target) + restore_workspace(i3, saved_layout, saved_programs, target, kill) else: workspace_layout = workspaces[0] # Get layout from file. @@ -222,7 +234,7 @@ def restore_workspaces(workspace, numeric, session, directory, profile, target, if target != 'programs_only': # Load workspace layout. layout.restore(target_workspace, saved_layout, - saved_programs) + saved_programs, target) if target != 'layout_only': # Restore programs. diff --git a/i3_resurrect/programs.py b/i3_resurrect/programs.py index f5db078..1db4e0d 100644 --- a/i3_resurrect/programs.py +++ b/i3_resurrect/programs.py @@ -147,6 +147,7 @@ def get_programs(workspace, numeric): # Add the command to the list. programs.append({ + 'window_properties': con['window_properties'], 'command': command, 'working_directory': working_directory }) From da103b70967d2bd6f67b07b2fd880a9f449adee2 Mon Sep 17 00:00:00 2001 From: Cedric Leporcq Date: Mon, 4 May 2020 23:55:32 +0200 Subject: [PATCH 5/7] Add Kill command to delete workspaces or whole session. --- i3_resurrect/main.py | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/i3_resurrect/main.py b/i3_resurrect/main.py index f19030d..5e2815d 100644 --- a/i3_resurrect/main.py +++ b/i3_resurrect/main.py @@ -342,5 +342,51 @@ def remove(workspace, session, directory, profile, target, workspaces): sys.exit(1) +@main.command('kill') +@click.option('--workspace', '-w', + is_flag=True, + help='Kill workspace(s) layout(s) and program(s).') +@click.option('--session', '-S', + is_flag=True, + help='Kill all workspace(s) and program(s) in current session.\n') +@click.option('--force', '-F', + is_flag=True, + help='Do not ask for confirmation.') +@click.argument('workspaces', nargs=-1) +def kill(workspace, session, force, workspaces): + """ + Kill workspace(s) or whole session. + """ + i3 = i3ipc.Connection() + + if not workspaces: + workspaces = ( i3.get_tree().find_focused().workspace().name, ) + + answer = "n" + if session: + workspaces = layout.list(i3, False) + if not force: + answer = input("Kill all workspaces in current session y/N ? ") + elif workspace: + if not force: + import ipdb; ipdb.set_trace() + w_list = workspaces[0] + for workspace in workspaces[1:]: + w_list = w_list + "," + "'" + workspace + "'" + if len(workspaces) == 1: + answer = input(f"Kill workspace {w_list} y/N ? ") + else: + answer = input(f"Kill workspaces {w_list} y/N ? ") + else: + util.eprint('either --workspace or --session option should be specified.') + sys.exit(1) + + if not force and answer not in ("y", "Y"): + sys.exit(1) + + for workspace_id in workspaces: + i3.command(f'[workspace="{workspace_id}"] kill') + + if __name__ == '__main__': main() From e80033038f593166357cd8b9adc0fb1a248f6a21 Mon Sep 17 00:00:00 2001 From: Cedric Leporcq Date: Sat, 18 Jun 2022 07:06:42 +0200 Subject: [PATCH 6/7] Update README --- README.md | 86 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 4d4ab1f..e78af1e 100644 --- a/README.md +++ b/README.md @@ -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. + -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: From 02e568f8ec26f86c3c08d92f823382ccb06638c2 Mon Sep 17 00:00:00 2001 From: Cedric Leporcq Date: Sun, 6 Jun 2021 20:22:06 +0200 Subject: [PATCH 7/7] Add TIMEOUT variable in config to restore command --- i3_resurrect/config.py | 3 +++ i3_resurrect/layout.py | 4 ++++ i3_resurrect/main.py | 9 ++++++++- i3_resurrect/programs.py | 3 +++ i3_resurrect/util.py | 5 +++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/i3_resurrect/config.py b/i3_resurrect/config.py index 0dbb322..5c5d21b 100644 --- a/i3_resurrect/config.py +++ b/i3_resurrect/config.py @@ -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. diff --git a/i3_resurrect/layout.py b/i3_resurrect/layout.py index 74d8c08..310f065 100644 --- a/i3_resurrect/layout.py +++ b/i3_resurrect/layout.py @@ -2,11 +2,13 @@ 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 @@ -185,6 +187,8 @@ def restore(workspace_name, layout, saved_programs, target, kill=False): # user to lose their windows no matter what. 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): diff --git a/i3_resurrect/main.py b/i3_resurrect/main.py index 5e2815d..a1e3385 100644 --- a/i3_resurrect/main.py +++ b/i3_resurrect/main.py @@ -1,6 +1,7 @@ import json import sys import os +from time import sleep from pathlib import Path import click @@ -178,6 +179,9 @@ def restore_workspaces(workspace, numeric, session, directory, profile, target, """ i3 = i3ipc.Connection() + # Display warning message + nag_bar = util.nag_bar_process() + focused_workspace = i3.get_tree().find_focused().workspace().name if not workspaces: @@ -240,6 +244,10 @@ def restore_workspaces(workspace, numeric, session, directory, profile, target, # Restore programs. programs.restore(target_workspace, saved_programs) + restore_timeout = config.get('restore_timeout', 2) + sleep(restore_timeout) + nag_bar.terminate() + @main.command('ls') @click.option('--directory', '-d', @@ -369,7 +377,6 @@ def kill(workspace, session, force, workspaces): answer = input("Kill all workspaces in current session y/N ? ") elif workspace: if not force: - import ipdb; ipdb.set_trace() w_list = workspaces[0] for workspace in workspaces[1:]: w_list = w_list + "," + "'" + workspace + "'" diff --git a/i3_resurrect/programs.py b/i3_resurrect/programs.py index 1db4e0d..fe23418 100644 --- a/i3_resurrect/programs.py +++ b/i3_resurrect/programs.py @@ -3,6 +3,7 @@ import shutil import subprocess import sys +from time import sleep from pathlib import Path import i3ipc @@ -93,6 +94,8 @@ def restore(workspace_name, saved_programs): # Execute command via i3 exec. i3.command(f'exec "cd \\"{working_directory}\\" && {command}"') + exec_timeout = config.get('exec_timeout', 0.1) + sleep(exec_timeout) def get_programs(workspace, numeric): diff --git a/i3_resurrect/util.py b/i3_resurrect/util.py index 4e2ea18..c11aba7 100644 --- a/i3_resurrect/util.py +++ b/i3_resurrect/util.py @@ -1,6 +1,7 @@ import os import re import sys +import subprocess from os.path import expandvars from pathlib import Path @@ -47,3 +48,7 @@ def list_filenames(directory): programs_file = Path(directory) / programs_filenames[n] files.append((layout_file, programs_file)) return files + + +def nag_bar_process(): + return subprocess.Popen(["i3-nagbar", "--type", "warning", "-m", "Currently restoring session. Don't change workspace focus!"])