diff --git a/CHANGES b/CHANGES index be938951cb5..08fd505258f 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,14 @@ current ------- - *Insert changes/features/fixes for next release here* +tmuxp 1.7.0a1 (2020-11-07) +-------------------------- +- :issue:`530` Add plugin system for user customization of tmuxp +- :issue:`530` Add tests for the plugin system +- :issue:`530` Update existing tests for the plugin system +- :issue:`530` Add the plugin interface to the tmuxp package +- :issue:`530` Add in depth documentation for the plugin system + tmuxp 1.6.1 (2020-11-07) ------------------------ - :issue:`641` Improvements to ``shell`` diff --git a/README.rst b/README.rst index dc9f150d305..99a2bf25a46 100644 --- a/README.rst +++ b/README.rst @@ -155,7 +155,6 @@ Snapshot your tmux layout, pane paths, and window/session names. See more about `freezing tmux`_ sessions. - Convert a session file ---------------------- @@ -176,6 +175,11 @@ You can auto confirm the prompt. In this case no preview will be shown. $ tmuxp convert -y filename $ tmuxp convert --yes filename +Plugin System +------------- + +tmuxp has a plugin system to allow for custom behavior. See more about the +`Plugin System`_. Docs / Reading material ----------------------- @@ -200,6 +204,7 @@ Want to learn more about tmux itself? `Read The Tao of Tmux online`_. .. _teamocil: https://github.com/remiprev/teamocil .. _Examples: http://tmuxp.git-pull.com/examples.html .. _freezing tmux: http://tmuxp.git-pull.com/cli.html#freeze-sessions +.. _Plugin System: http://tmuxp.git-pull.com/plugin_system.html .. _bootstrap_env.py: https://github.com/tmux-python/tmuxp/blob/master/bootstrap_env.py .. _testing: http://tmuxp.git-pull.com/developing.html#test-runner .. _python objects: http://tmuxp.git-pull.com/api.html#api diff --git a/docs/index.rst b/docs/index.rst index fc6d6779547..6f1a1919c3b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,6 +28,7 @@ Table of Contents quickstart examples cli + plugin_system developing api history diff --git a/docs/plugin_system.rst b/docs/plugin_system.rst new file mode 100644 index 00000000000..8cf50b44eda --- /dev/null +++ b/docs/plugin_system.rst @@ -0,0 +1,130 @@ +.. _plugin_system: + +============= +Plugin System +============= + +The plugin system allows users to customize and extend different aspects of +tmuxp without the need to change tmuxp itself. + + +Using a Plugin +-------------- + +To use a plugin, install it in your local python environment and add it to +your tmuxp configuration file. + +Example Configurations +^^^^^^^^^^^^^^^^^^^^^^ +YAML +~~~~ + +.. literalinclude:: ../examples/plugin-system.yaml + :language: yaml + +JSON +~~~~ + +.. literalinclude:: ../examples/plugin-system.json + :language: json + +.. _poetry: https://python-poetry.org/ + + +Developing a Plugin +------------------- + +tmuxp expects all plugins to be class within a python submodule named +``plugin`` that is within a python module that is installed in the local +python environment. A plugin interface is provided by tmuxp to inherit. + +`poetry`_ is the chosen python package manager for tmuxp. It is highly +suggested to use it when developing plugins; however, ``pip`` will work +just as well. Only one of the configuration files is needed for the packaging +tool that the package developer desides to use. + +.. code-block:: bash + + python_module + ├── tmuxp_plugin_my_plugin_module + │   ├── __init__.py + │   └── plugin.py + ├── pyproject.toml # Poetry's module configuration file + └── setup.py # pip's module configuration file + + +When publishing plugins to pypi, tmuxp advocates for standardized naming: +``tmuxp-plugin-{your-plugin-name}`` to allow for easier searching. To create a +module configuration file with poetry, run ``poetry init`` in the module +directory. The resulting file looks something like this: + +.. code-block:: toml + + [tool.poetry] + name = "tmuxp-plugin-my-tmuxp-plugin" + version = "0.0.2" + description = "An example tmuxp plugin." + authors = ["Author Name .com>"] + + [tool.poetry.dependencies] + python = "~2.7 || ^3.5" + tmuxp = "^1.6.0" + + [tool.poetry.dev-dependencies] + + [build-system] + requires = ["poetry>=0.12"] + build-backend = "poetry.masonry.api" + + +The `plugin.py` file could contain something like the following: + +.. code-block:: python + + from tmuxp.plugin import TmuxpPlugin + import datetime + + class MyTmuxpPlugin(TmuxpPlugin): + def __init__(self): + """ + Initialize my custom plugin. + """ + # Optional version dependency configuration. See Plugin API docs + # for all supported config parameters + config = { + 'tmuxp_min_version' = '1.6.2' + } + + TmuxpPlugin.__init__( + self, + plugin_name='tmuxp-plugin-my-tmuxp-plugin', + **config + ) + + def before_workspace_builder(self, session): + session.rename_session('my-new-session-name') + + def reattach(self, session): + now = datetime.datetime.now().strftime('%Y-%m-%d') + session.rename_session('session_{}'.format(now)) + + +Once this plugin is installed in the local python environment, it can be used +in a configuration file like the following: + +.. code-block:: yaml + + session_name: plugin example + plugins: + - my_plugin_module.plugin.MyTmuxpPlugin + # ... the rest of your config + + +Plugin API +---------- + +.. automethod:: tmuxp.plugin.TmuxpPlugin.before_workspace_builder +.. automethod:: tmuxp.plugin.TmuxpPlugin.on_window_create +.. automethod:: tmuxp.plugin.TmuxpPlugin.after_window_finished +.. automethod:: tmuxp.plugin.TmuxpPlugin.before_script +.. automethod:: tmuxp.plugin.TmuxpPlugin.reattach \ No newline at end of file diff --git a/examples/plugin-system.json b/examples/plugin-system.json new file mode 100644 index 00000000000..4309676b309 --- /dev/null +++ b/examples/plugin-system.json @@ -0,0 +1,24 @@ +{ + "session_name": "plugin-system", + "plugins": [ + "tmuxp_plugin_extended_build.plugin.PluginExtendedBuild" + ], + "windows": [ + { + "window_name": "editor", + "layout": "tiled", + "shell_command_before": [ + "cd ~/" + ], + "panes": [ + { + "shell_command": [ + "cd /var/log", + "ls -al | grep *.log" + ] + }, + "echo \"hello world\"" + ] + } + ] +} \ No newline at end of file diff --git a/examples/plugin-system.yaml b/examples/plugin-system.yaml new file mode 100644 index 00000000000..13fa6278cee --- /dev/null +++ b/examples/plugin-system.yaml @@ -0,0 +1,13 @@ +session_name: plugin-system +plugins: +- 'tmuxp_plugin_extended_build.plugin.PluginExtendedBuild' +windows: +- window_name: editor + layout: tiled + shell_command_before: + - cd ~/ + panes: + - shell_command: + - cd /var/log + - ls -al | grep *.log + - echo "hello world" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1338e43e524..4c5a1345a88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ skip-string-normalization = true [tool.poetry] name = "tmuxp" -version = "1.6.1" +version = "1.7.0a1" description = "tmux session manager" license = "MIT" authors = ["Tony Narlock "] @@ -69,6 +69,12 @@ pytest-mock = [ {version="<3.0.0", python="<3"}, {version="*", python=">=3"} ] +tmuxp-test-plugin-bwb = { path = "tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/"} +tmuxp-test-plugin-bs = { path = "tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/"} +tmuxp-test-plugin-r = { path = "tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/"} +tmuxp-test-plugin-owc = { path = "tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/"} +tmuxp-test-plugin-awf = { path = "tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/"} +tmuxp-test-plugin-fail = { path = "tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/"} ### Coverage ### codecov = "*" diff --git a/tests/fixtures/pluginsystem/__init__.py b/tests/fixtures/pluginsystem/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/pluginsystem/partials/__init__.py b/tests/fixtures/pluginsystem/partials/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/pluginsystem/partials/all_pass.py b/tests/fixtures/pluginsystem/partials/all_pass.py new file mode 100644 index 00000000000..a19ade914ab --- /dev/null +++ b/tests/fixtures/pluginsystem/partials/all_pass.py @@ -0,0 +1,20 @@ +from .test_plugin_helpers import MyTestTmuxpPlugin + + +class AllVersionPassPlugin(MyTestTmuxpPlugin): + def __init__(self): + config = { + 'plugin_name': 'tmuxp-plugin-my-tmuxp-plugin', + 'tmux_min_version': '1.8', + 'tmux_max_version': '100.0', + 'tmux_version_incompatible': ['2.3'], + 'libtmux_min_version': '0.8.3', + 'libtmux_max_version': '100.0', + 'libtmux_version_incompatible': ['0.7.1'], + 'tmuxp_min_version': '1.6.0', + 'tmuxp_max_version': '100.0.0', + 'tmuxp_version_incompatible': ['1.5.6'], + 'tmux_version': '3.0', + 'tmuxp_version': '1.6.0', + } + MyTestTmuxpPlugin.__init__(self, config) diff --git a/tests/fixtures/pluginsystem/partials/libtmux_version_fail.py b/tests/fixtures/pluginsystem/partials/libtmux_version_fail.py new file mode 100644 index 00000000000..8e7535cab1d --- /dev/null +++ b/tests/fixtures/pluginsystem/partials/libtmux_version_fail.py @@ -0,0 +1,31 @@ +from .test_plugin_helpers import MyTestTmuxpPlugin + + +class LibtmuxVersionFailMinPlugin(MyTestTmuxpPlugin): + def __init__(self): + config = { + 'plugin_name': 'libtmux-min-version-fail', + 'libtmux_min_version': '0.8.3', + 'libtmux_version': '0.7.0', + } + MyTestTmuxpPlugin.__init__(self, config) + + +class LibtmuxVersionFailMaxPlugin(MyTestTmuxpPlugin): + def __init__(self): + config = { + 'plugin_name': 'libtmux-max-version-fail', + 'libtmux_max_version': '3.0', + 'libtmux_version': '3.5', + } + MyTestTmuxpPlugin.__init__(self, config) + + +class LibtmuxVersionFailIncompatiblePlugin(MyTestTmuxpPlugin): + def __init__(self): + config = { + 'plugin_name': 'libtmux-incompatible-version-fail', + 'libtmux_version_incompatible': ['0.7.1'], + 'libtmux_version': '0.7.1', + } + MyTestTmuxpPlugin.__init__(self, config) diff --git a/tests/fixtures/pluginsystem/partials/test_plugin_helpers.py b/tests/fixtures/pluginsystem/partials/test_plugin_helpers.py new file mode 100644 index 00000000000..d58eca8dd85 --- /dev/null +++ b/tests/fixtures/pluginsystem/partials/test_plugin_helpers.py @@ -0,0 +1,20 @@ +from tmuxp.plugin import TmuxpPlugin + + +class MyTestTmuxpPlugin(TmuxpPlugin): + def __init__(self, config): + tmux_version = config.pop('tmux_version', None) + libtmux_version = config.pop('libtmux_version', None) + tmuxp_version = config.pop('tmuxp_version', None) + + TmuxpPlugin.__init__(self, **config) + + # WARNING! This should not be done in anything but a test + if tmux_version: + self.version_constraints['tmux']['version'] = tmux_version + if libtmux_version: + self.version_constraints['libtmux']['version'] = libtmux_version + if tmuxp_version: + self.version_constraints['tmuxp']['version'] = tmuxp_version + + self._version_check() diff --git a/tests/fixtures/pluginsystem/partials/tmux_version_fail.py b/tests/fixtures/pluginsystem/partials/tmux_version_fail.py new file mode 100644 index 00000000000..19ffbb1e086 --- /dev/null +++ b/tests/fixtures/pluginsystem/partials/tmux_version_fail.py @@ -0,0 +1,32 @@ +from .test_plugin_helpers import MyTestTmuxpPlugin + + +class TmuxVersionFailMinPlugin(MyTestTmuxpPlugin): + def __init__(self): + config = { + 'plugin_name': 'tmux-min-version-fail', + 'tmux_min_version': '1.8', + 'tmux_version': '1.7', + } + MyTestTmuxpPlugin.__init__(self, config) + + +class TmuxVersionFailMaxPlugin(MyTestTmuxpPlugin): + def __init__(self): + config = { + 'plugin_name': 'tmux-max-version-fail', + 'tmux_max_version': '3.0', + 'tmux_version': '3.5', + } + MyTestTmuxpPlugin.__init__(self, config) + + +class TmuxVersionFailIncompatiblePlugin(MyTestTmuxpPlugin): + def __init__(self): + config = { + 'plugin_name': 'tmux-incompatible-version-fail', + 'tmux_version_incompatible': ['2.3'], + 'tmux_version': '2.3', + } + + MyTestTmuxpPlugin.__init__(self, config) diff --git a/tests/fixtures/pluginsystem/partials/tmuxp_version_fail.py b/tests/fixtures/pluginsystem/partials/tmuxp_version_fail.py new file mode 100644 index 00000000000..05cbd4ce059 --- /dev/null +++ b/tests/fixtures/pluginsystem/partials/tmuxp_version_fail.py @@ -0,0 +1,31 @@ +from .test_plugin_helpers import MyTestTmuxpPlugin + + +class TmuxpVersionFailMinPlugin(MyTestTmuxpPlugin): + def __init__(self): + config = { + 'plugin_name': 'tmuxp-min-version-fail', + 'tmuxp_min_version': '1.6.0', + 'tmuxp_version': '1.5.6', + } + MyTestTmuxpPlugin.__init__(self, config) + + +class TmuxpVersionFailMaxPlugin(MyTestTmuxpPlugin): + def __init__(self): + config = { + 'plugin_name': 'tmuxp-max-version-fail', + 'tmuxp_max_version': '2.0.0', + 'tmuxp_version': '2.5', + } + MyTestTmuxpPlugin.__init__(self, config) + + +class TmuxpVersionFailIncompatiblePlugin(MyTestTmuxpPlugin): + def __init__(self): + config = { + 'plugin_name': 'tmuxp-incompatible-version-fail', + 'tmuxp_version_incompatible': ['1.5.0'], + 'tmuxp_version': '1.5.0', + } + MyTestTmuxpPlugin.__init__(self, config) diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/pyproject.toml b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/pyproject.toml new file mode 100644 index 00000000000..198d239d631 --- /dev/null +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "tmuxp_test_plugin_awf" +version = "0.0.2" +description = "A tmuxp plugin to test after_window_finished part of the tmuxp plugin system" +authors = ["Joseph Flinn "] + +[tool.poetry.dependencies] +python = "~2.7 || ^3.5" +tmuxp = "^1.6.0" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/tmuxp_test_plugin_awf/__init__.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/tmuxp_test_plugin_awf/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/tmuxp_test_plugin_awf/plugin.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/tmuxp_test_plugin_awf/plugin.py new file mode 100644 index 00000000000..442f86e2677 --- /dev/null +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/tmuxp_test_plugin_awf/plugin.py @@ -0,0 +1,18 @@ +from tmuxp.plugin import TmuxpPlugin + + +class PluginAfterWindowFinished(TmuxpPlugin): + def __init__(self): + self.message = '[+] This is the Tmuxp Test Plugin' + + def after_window_finished(self, window): + if window.name == 'editor': + window.rename_window('plugin_test_awf') + elif window.name == 'awf_mw_test': + window.rename_window('plugin_test_awf_mw') + elif window.name == 'awf_mw_test_2': + window.rename_window('plugin_test_awf_mw_2') + elif window.name == 'mp_test_owc': + window.rename_window('mp_test_awf') + else: + pass diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/pyproject.toml b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/pyproject.toml new file mode 100644 index 00000000000..15cd0f2ff67 --- /dev/null +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "tmuxp_test_plugin_bs" +version = "0.0.2" +description = "A tmuxp plugin to test before_script part of the tmuxp plugin system" +authors = ["Joseph Flinn "] + +[tool.poetry.dependencies] +python = "~2.7 || ^3.5" +tmuxp = "^1.6.0." + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/tmuxp_test_plugin_bs/__init__.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/tmuxp_test_plugin_bs/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/tmuxp_test_plugin_bs/plugin.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/tmuxp_test_plugin_bs/plugin.py new file mode 100644 index 00000000000..98692ac9f30 --- /dev/null +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/tmuxp_test_plugin_bs/plugin.py @@ -0,0 +1,9 @@ +from tmuxp.plugin import TmuxpPlugin + + +class PluginBeforeScript(TmuxpPlugin): + def __init__(self): + self.message = '[+] This is the Tmuxp Test Plugin' + + def before_script(self, session): + session.rename_session('plugin_test_bs') diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/pyproject.toml b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/pyproject.toml new file mode 100644 index 00000000000..e34023ad100 --- /dev/null +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "tmuxp_test_plugin_bwb" +version = "0.0.2" +description = "A tmuxp plugin to test before_workspace_build part of the tmuxp plugin system" +authors = ["Joseph Flinn "] + +[tool.poetry.dependencies] +python = "~2.7 || ^3.5" +tmuxp = "^1.6.0" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/tmuxp_test_plugin_bwb/__init__.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/tmuxp_test_plugin_bwb/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/tmuxp_test_plugin_bwb/plugin.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/tmuxp_test_plugin_bwb/plugin.py new file mode 100644 index 00000000000..78fd68f28b0 --- /dev/null +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/tmuxp_test_plugin_bwb/plugin.py @@ -0,0 +1,9 @@ +from tmuxp.plugin import TmuxpPlugin + + +class PluginBeforeWorkspaceBuilder(TmuxpPlugin): + def __init__(self): + self.message = '[+] This is the Tmuxp Test Plugin' + + def before_workspace_builder(self, session): + session.rename_session('plugin_test_bwb') diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/pyproject.toml b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/pyproject.toml new file mode 100644 index 00000000000..977cabeb3ea --- /dev/null +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "tmuxp_test_plugin_fail" +version = "0.1.0" +description = "A test plugin designed to fail to test the cli" +authors = ["Joseph Flinn "] + +[tool.poetry.dependencies] +python = "~2.7 || ^3.5" +tmuxp = "^1.6.0" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/tmuxp_test_plugin_fail/__init__.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/tmuxp_test_plugin_fail/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/tmuxp_test_plugin_fail/plugin.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/tmuxp_test_plugin_fail/plugin.py new file mode 100644 index 00000000000..abe56e46c75 --- /dev/null +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/tmuxp_test_plugin_fail/plugin.py @@ -0,0 +1,10 @@ +from tmuxp.plugin import TmuxpPlugin + + +class PluginFailVersion(TmuxpPlugin): + def __init__(self): + config = { + 'plugin_name': 'tmuxp-plugin-fail-version', + 'tmuxp_max_version': '0.0.0', + } + TmuxpPlugin.__init__(self, **config) diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/pyproject.toml b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/pyproject.toml new file mode 100644 index 00000000000..c6a94e5db1f --- /dev/null +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "tmuxp_test_plugin_owc" +version = "0.0.2" +description = "A tmuxp plugin to test on_window_create part of the tmuxp plugin system" +authors = ["Joseph Flinn "] + +[tool.poetry.dependencies] +python = "~2.7 || ^3.5" +tmuxp = "^1.6.0" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/tmuxp_test_plugin_owc/__init__.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/tmuxp_test_plugin_owc/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/tmuxp_test_plugin_owc/plugin.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/tmuxp_test_plugin_owc/plugin.py new file mode 100644 index 00000000000..2ba059cd4d8 --- /dev/null +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/tmuxp_test_plugin_owc/plugin.py @@ -0,0 +1,18 @@ +from tmuxp.plugin import TmuxpPlugin + + +class PluginOnWindowCreate(TmuxpPlugin): + def __init__(self): + self.message = '[+] This is the Tmuxp Test Plugin' + + def on_window_create(self, window): + if window.name == 'editor': + window.rename_window('plugin_test_owc') + elif window.name == 'owc_mw_test': + window.rename_window('plugin_test_owc_mw') + elif window.name == 'owc_mw_test_2': + window.rename_window('plugin_test_owc_mw_2') + elif window.name == 'mp_test': + window.rename_window('mp_test_owc') + else: + pass diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/pyproject.toml b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/pyproject.toml new file mode 100644 index 00000000000..ae0b438dba2 --- /dev/null +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "tmuxp_test_plugin_r" +version = "0.0.2" +description = "A tmuxp plugin to test reattach part of the tmuxp plugin system" +authors = ["Joseph Flinn "] + +[tool.poetry.dependencies] +python = "~2.7 || ^3.5" +tmuxp = "^1.6.0" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/tmuxp_test_plugin_r/__init__.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/tmuxp_test_plugin_r/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/tmuxp_test_plugin_r/plugin.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/tmuxp_test_plugin_r/plugin.py new file mode 100644 index 00000000000..ccf251142a0 --- /dev/null +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/tmuxp_test_plugin_r/plugin.py @@ -0,0 +1,9 @@ +from tmuxp.plugin import TmuxpPlugin + + +class PluginReattach(TmuxpPlugin): + def __init__(self): + self.message = '[+] This is the Tmuxp Test Plugin' + + def reattach(self, session): + session.rename_session('plugin_test_r') diff --git a/tests/fixtures/workspacebuilder/plugin_awf.yaml b/tests/fixtures/workspacebuilder/plugin_awf.yaml new file mode 100644 index 00000000000..5111833e14a --- /dev/null +++ b/tests/fixtures/workspacebuilder/plugin_awf.yaml @@ -0,0 +1,15 @@ +session_name: plugin-test-awf +plugins: +- 'tmuxp_test_plugin_awf.plugin.PluginAfterWindowFinished' +windows: +- window_name: editor + layout: tiled + shell_command_before: + - cd ~/ + panes: + - shell_command: + - cd /var/log + - ls -al | grep \.log + - echo hello + - echo hello + - echo hello diff --git a/tests/fixtures/workspacebuilder/plugin_awf_multiple_windows.yaml b/tests/fixtures/workspacebuilder/plugin_awf_multiple_windows.yaml new file mode 100644 index 00000000000..994c2a7c4db --- /dev/null +++ b/tests/fixtures/workspacebuilder/plugin_awf_multiple_windows.yaml @@ -0,0 +1,18 @@ +session_name: plugin-test-awf-mw +plugins: +- 'tmuxp_test_plugin_awf.plugin.PluginAfterWindowFinished' +windows: +- window_name: awf_mw_test + layout: tiled + shell_command_before: + - cd ~/ + panes: + - shell_command: + - cd /var/log + - ls -al | grep \.log +- window_name: awf_mw_test_2 + layout: tiled + shell_command_before: + - cd ~/ + panes: + - echo hello \ No newline at end of file diff --git a/tests/fixtures/workspacebuilder/plugin_bs.yaml b/tests/fixtures/workspacebuilder/plugin_bs.yaml new file mode 100644 index 00000000000..a04d6eb3f30 --- /dev/null +++ b/tests/fixtures/workspacebuilder/plugin_bs.yaml @@ -0,0 +1,15 @@ +session_name: plugin-test-bs +plugins: +- 'tmuxp_test_plugin_bs.plugin.PluginBeforeScript' +windows: +- window_name: editor + layout: tiled + shell_command_before: + - cd ~/ + panes: + - shell_command: + - cd /var/log + - ls -al | grep \.log + - echo hello + - echo hello + - echo hello diff --git a/tests/fixtures/workspacebuilder/plugin_bwb.yaml b/tests/fixtures/workspacebuilder/plugin_bwb.yaml new file mode 100644 index 00000000000..8241f279462 --- /dev/null +++ b/tests/fixtures/workspacebuilder/plugin_bwb.yaml @@ -0,0 +1,15 @@ +session_name: plugin-test-bwb +plugins: +- 'tmuxp_test_plugin_bwb.plugin.PluginBeforeWorkspaceBuilder' +windows: +- window_name: editor + layout: tiled + shell_command_before: + - cd ~/ + panes: + - shell_command: + - cd /var/log + - ls -al | grep \.log + - echo hello + - echo hello + - echo hello diff --git a/tests/fixtures/workspacebuilder/plugin_missing_fail.yaml b/tests/fixtures/workspacebuilder/plugin_missing_fail.yaml new file mode 100644 index 00000000000..4e1097debd5 --- /dev/null +++ b/tests/fixtures/workspacebuilder/plugin_missing_fail.yaml @@ -0,0 +1,6 @@ +session_name: plugin-test-missing-fail +plugins: +- 'tmuxp_test_plugin_fail.plugin.PluginFailMissing' +windows: +- panes: + - echo "hey" \ No newline at end of file diff --git a/tests/fixtures/workspacebuilder/plugin_multiple_plugins.yaml b/tests/fixtures/workspacebuilder/plugin_multiple_plugins.yaml new file mode 100644 index 00000000000..113347db6e9 --- /dev/null +++ b/tests/fixtures/workspacebuilder/plugin_multiple_plugins.yaml @@ -0,0 +1,14 @@ +session_name: plugin-test-multiple-plugins +plugins: +- 'tmuxp_test_plugin_bwb.plugin.PluginBeforeWorkspaceBuilder' +- 'tmuxp_test_plugin_owc.plugin.PluginOnWindowCreate' +- 'tmuxp_test_plugin_awf.plugin.PluginAfterWindowFinished' +windows: +- window_name: mp_test + layout: tiled + shell_command_before: + - cd ~/ + panes: + - shell_command: + - cd /var/log + - ls -al | grep \.log \ No newline at end of file diff --git a/tests/fixtures/workspacebuilder/plugin_owc.yaml b/tests/fixtures/workspacebuilder/plugin_owc.yaml new file mode 100644 index 00000000000..abc81d9192f --- /dev/null +++ b/tests/fixtures/workspacebuilder/plugin_owc.yaml @@ -0,0 +1,15 @@ +session_name: plugin-test-owc +plugins: +- 'tmuxp_test_plugin_owc.plugin.PluginOnWindowCreate' +windows: +- window_name: editor + layout: tiled + shell_command_before: + - cd ~/ + panes: + - shell_command: + - cd /var/log + - ls -al | grep \.log + - echo hello + - echo hello + - echo hello diff --git a/tests/fixtures/workspacebuilder/plugin_owc_multiple_windows.yaml b/tests/fixtures/workspacebuilder/plugin_owc_multiple_windows.yaml new file mode 100644 index 00000000000..c7d437e5847 --- /dev/null +++ b/tests/fixtures/workspacebuilder/plugin_owc_multiple_windows.yaml @@ -0,0 +1,17 @@ +session_name: plugin-test-owc-mw +plugins: +- 'tmuxp_test_plugin_owc.plugin.PluginOnWindowCreate' +windows: +- window_name: owc_mw_test + shell_command_before: + - cd ~/ + panes: + - shell_command: + - cd /var/log + - ls -al | grep \.log +- window_name: owc_mw_test_2 + layout: tiled + shell_command_before: + - cd ~/ + panes: + - echo hello \ No newline at end of file diff --git a/tests/fixtures/workspacebuilder/plugin_r.yaml b/tests/fixtures/workspacebuilder/plugin_r.yaml new file mode 100644 index 00000000000..b220aab6b83 --- /dev/null +++ b/tests/fixtures/workspacebuilder/plugin_r.yaml @@ -0,0 +1,15 @@ +session_name: plugin-test-r +plugins: +- 'tmuxp_test_plugin_r.plugin.PluginReattach' +windows: +- window_name: editor + layout: tiled + shell_command_before: + - cd ~/ + panes: + - shell_command: + - cd /var/log + - ls -al | grep \.log + - echo hello + - echo hello + - echo hello diff --git a/tests/fixtures/workspacebuilder/plugin_versions_fail.yaml b/tests/fixtures/workspacebuilder/plugin_versions_fail.yaml new file mode 100644 index 00000000000..e4e7a4910bf --- /dev/null +++ b/tests/fixtures/workspacebuilder/plugin_versions_fail.yaml @@ -0,0 +1,6 @@ +session_name: plugin-test-version-fail +plugins: +- 'tmuxp_test_plugin_fail.plugin.PluginFailVersion' +windows: +- panes: + - echo "hey" \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 2741a30a2f7..627ab21e48a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -7,6 +7,8 @@ import pytest +import kaptan + import click from click.testing import CliRunner @@ -20,7 +22,11 @@ is_pure_name, load_workspace, scan_config, + _reattach, + load_plugins, ) +from tmuxp.workspacebuilder import WorkspaceBuilder +from tmuxp_test_plugin_bwb.plugin import PluginBeforeWorkspaceBuilder from .fixtures._util import curjoin, loadfixture @@ -953,3 +959,103 @@ def test_ls_cli(monkeypatch, tmpdir): runner = CliRunner() cli_output = runner.invoke(command_ls).output assert cli_output == '\n'.join(stems) + '\n' + + +def test_load_plugins(): + plugins_config = loadfixture("workspacebuilder/plugin_bwb.yaml") + + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(plugins_config).get() + sconfig = config.expand(sconfig) + + plugins = load_plugins(sconfig) + + assert len(plugins) == 1 + + test_plugin_class_types = [ + PluginBeforeWorkspaceBuilder().__class__, + ] + for plugin in plugins: + assert plugin.__class__ in test_plugin_class_types + + +@pytest.mark.skip('Not sure how to clean up the tmux session this makes') +@pytest.mark.parametrize( + "cli_args,inputs", + [ + ( + ['load', 'tests/fixtures/workspacebuilder/plugin_versions_fail.yaml'], + ['y\n'], + ) + ], +) +def test_load_plugins_version_fail_skip(cli_args, inputs): + runner = CliRunner() + + results = runner.invoke(cli.cli, cli_args, input=''.join(inputs)) + assert '[Loading]' in results.output + + +@pytest.mark.parametrize( + "cli_args,inputs", + [ + ( + ['load', 'tests/fixtures/workspacebuilder/plugin_versions_fail.yaml'], + ['n\n'], + ) + ], +) +def test_load_plugins_version_fail_no_skip(cli_args, inputs): + runner = CliRunner() + + results = runner.invoke(cli.cli, cli_args, input=''.join(inputs)) + assert '[Not Skipping]' in results.output + + +@pytest.mark.parametrize( + "cli_args", [(['load', 'tests/fixtures/workspacebuilder/plugin_missing_fail.yaml'])] +) +def test_load_plugins_plugin_missing(cli_args): + runner = CliRunner() + + results = runner.invoke(cli.cli, cli_args) + assert '[Plugin Error]' in results.output + + +def test_plugin_system_before_script(server, monkeypatch): + # this is an implementation test. Since this testsuite may be ran within + # a tmux session by the developer himself, delete the TMUX variable + # temporarily. + monkeypatch.delenv('TMUX', raising=False) + session_file = curjoin("workspacebuilder/plugin_bs.yaml") + + # open it detached + session = load_workspace( + session_file, socket_name=server.socket_name, detached=True + ) + + assert isinstance(session, libtmux.Session) + assert session.name == 'plugin_test_bs' + + +def test_reattach_plugins(server): + config_plugins = loadfixture("workspacebuilder/plugin_r.yaml") + + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(config_plugins).get() + sconfig = config.expand(sconfig) + + # open it detached + builder = WorkspaceBuilder( + sconf=sconfig, plugins=load_plugins(sconfig), server=server + ) + builder.build() + + try: + _reattach(builder) + except libtmux.exc.LibTmuxException: + pass + + proc = builder.session.cmd('display-message', '-p', "'#S'") + + assert proc.stdout[0] == "'plugin_test_r'" diff --git a/tests/test_config.py b/tests/test_config.py index f8f027e25cd..e1085cf47b6 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -383,3 +383,23 @@ def test_replaces_env_variables(monkeypatch): assert "%s/moo" % env_val == sconfig['global_options']['default-shell'] assert "%s/lol" % env_val == sconfig['options']['default-command'] assert "logging @ %s" % env_val == sconfig['windows'][1]['window_name'] + + +def test_plugins(): + yaml_config = """ + session_name: test session + plugins: tmuxp-plugin-one.plugin.TestPluginOne + windows: + - window_name: editor + panes: + shell_command: + - tail -F /var/log/syslog + start_directory: /var/log + """ + + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(yaml_config).get() + + with pytest.raises(exc.ConfigError) as excinfo: + config.validate_schema(sconfig) + assert excinfo.matches('only supports list type') diff --git a/tests/test_plugin.py b/tests/test_plugin.py new file mode 100644 index 00000000000..488465e0bd1 --- /dev/null +++ b/tests/test_plugin.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +"""Test for tmuxp plugin api.""" +from __future__ import absolute_import + +import pytest + +from tmuxp.exc import TmuxpPluginException + +from .fixtures.pluginsystem.partials.all_pass import AllVersionPassPlugin +from .fixtures.pluginsystem.partials.libtmux_version_fail import ( + LibtmuxVersionFailIncompatiblePlugin, + LibtmuxVersionFailMaxPlugin, + LibtmuxVersionFailMinPlugin, +) +from .fixtures.pluginsystem.partials.tmux_version_fail import ( + TmuxVersionFailIncompatiblePlugin, + TmuxVersionFailMaxPlugin, + TmuxVersionFailMinPlugin, +) +from .fixtures.pluginsystem.partials.tmuxp_version_fail import ( + TmuxpVersionFailIncompatiblePlugin, + TmuxpVersionFailMaxPlugin, + TmuxpVersionFailMinPlugin, +) + + +def test_all_pass(): + AllVersionPassPlugin() + + +def test_tmux_version_fail_min(): + with pytest.raises(TmuxpPluginException, match=r'Incompatible.*') as exc_info: + TmuxVersionFailMinPlugin() + assert 'tmux-min-version-fail' in str(exc_info.value) + + +def test_tmux_version_fail_max(): + with pytest.raises(TmuxpPluginException, match=r'Incompatible.*') as exc_info: + TmuxVersionFailMaxPlugin() + assert 'tmux-max-version-fail' in str(exc_info.value) + + +def test_tmux_version_fail_incompatible(): + with pytest.raises(TmuxpPluginException, match=r'Incompatible.*') as exc_info: + TmuxVersionFailIncompatiblePlugin() + assert 'tmux-incompatible-version-fail' in str(exc_info.value) + + +def test_tmuxp_version_fail_min(): + with pytest.raises(TmuxpPluginException, match=r'Incompatible.*') as exc_info: + TmuxpVersionFailMinPlugin() + assert 'tmuxp-min-version-fail' in str(exc_info.value) + + +def test_tmuxp_version_fail_max(): + with pytest.raises(TmuxpPluginException, match=r'Incompatible.*') as exc_info: + TmuxpVersionFailMaxPlugin() + assert 'tmuxp-max-version-fail' in str(exc_info.value) + + +def test_tmuxp_version_fail_incompatible(): + with pytest.raises(TmuxpPluginException, match=r'Incompatible.*') as exc_info: + TmuxpVersionFailIncompatiblePlugin() + assert 'tmuxp-incompatible-version-fail' in str(exc_info.value) + + +def test_libtmux_version_fail_min(): + with pytest.raises(TmuxpPluginException, match=r'Incompatible.*') as exc_info: + LibtmuxVersionFailMinPlugin() + assert 'libtmux-min-version-fail' in str(exc_info.value) + + +def test_libtmux_version_fail_max(): + with pytest.raises(TmuxpPluginException, match=r'Incompatible.*') as exc_info: + LibtmuxVersionFailMaxPlugin() + assert 'libtmux-max-version-fail' in str(exc_info.value) + + +def test_libtmux_version_fail_incompatible(): + with pytest.raises(TmuxpPluginException, match=r'Incompatible.*') as exc_info: + LibtmuxVersionFailIncompatiblePlugin() + assert 'libtmux-incompatible-version-fail' in str(exc_info.value) diff --git a/tests/test_workspacebuilder.py b/tests/test_workspacebuilder.py index 4aab73dafe8..3783d0d8e4c 100644 --- a/tests/test_workspacebuilder.py +++ b/tests/test_workspacebuilder.py @@ -15,6 +15,7 @@ from tmuxp import config, exc from tmuxp._compat import text_type from tmuxp.workspacebuilder import WorkspaceBuilder +from tmuxp.cli import load_plugins from . import example_dir, fixtures_dir from .fixtures._util import loadfixture @@ -673,3 +674,108 @@ def test_before_load_true_if_test_passes_with_args(server): with temp_session(server) as session: builder.build(session=session) + + +def test_plugin_system_before_workspace_builder(session): + config_plugins = loadfixture("workspacebuilder/plugin_bwb.yaml") + + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(config_plugins).get() + sconfig = config.expand(sconfig) + + builder = WorkspaceBuilder(sconf=sconfig, plugins=load_plugins(sconfig)) + assert len(builder.plugins) > 0 + + builder.build(session=session) + + proc = session.cmd('display-message', '-p', "'#S'") + assert proc.stdout[0] == "'plugin_test_bwb'" + + +def test_plugin_system_on_window_create(session): + config_plugins = loadfixture("workspacebuilder/plugin_owc.yaml") + + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(config_plugins).get() + sconfig = config.expand(sconfig) + + builder = WorkspaceBuilder(sconf=sconfig, plugins=load_plugins(sconfig)) + assert len(builder.plugins) > 0 + + builder.build(session=session) + + proc = session.cmd('display-message', '-p', "'#W'") + assert proc.stdout[0] == "'plugin_test_owc'" + + +def test_plugin_system_after_window_finished(session): + config_plugins = loadfixture("workspacebuilder/plugin_awf.yaml") + + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(config_plugins).get() + sconfig = config.expand(sconfig) + + builder = WorkspaceBuilder(sconf=sconfig, plugins=load_plugins(sconfig)) + assert len(builder.plugins) > 0 + + builder.build(session=session) + + proc = session.cmd('display-message', '-p', "'#W'") + assert proc.stdout[0] == "'plugin_test_awf'" + + +def test_plugin_system_on_window_create_multiple_windows(session): + config_plugins = loadfixture("workspacebuilder/plugin_owc_multiple_windows.yaml") + + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(config_plugins).get() + sconfig = config.expand(sconfig) + + builder = WorkspaceBuilder(sconf=sconfig, plugins=load_plugins(sconfig)) + assert len(builder.plugins) > 0 + + builder.build(session=session) + + proc = session.cmd('list-windows', '-F', "'#W'") + assert "'plugin_test_owc_mw'" in proc.stdout + assert "'plugin_test_owc_mw_2'" in proc.stdout + + +def test_plugin_system_after_window_finished_multiple_windows(session): + config_plugins = loadfixture("workspacebuilder/plugin_awf_multiple_windows.yaml") + + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(config_plugins).get() + sconfig = config.expand(sconfig) + + builder = WorkspaceBuilder(sconf=sconfig, plugins=load_plugins(sconfig)) + assert len(builder.plugins) > 0 + + builder.build(session=session) + + proc = session.cmd('list-windows', '-F', "'#W'") + assert "'plugin_test_awf_mw'" in proc.stdout + assert "'plugin_test_awf_mw_2'" in proc.stdout + + +def test_plugin_system_multiple_plugins(session): + config_plugins = loadfixture("workspacebuilder/plugin_multiple_plugins.yaml") + + sconfig = kaptan.Kaptan(handler='yaml') + sconfig = sconfig.import_config(config_plugins).get() + sconfig = config.expand(sconfig) + + builder = WorkspaceBuilder(sconf=sconfig, plugins=load_plugins(sconfig)) + assert len(builder.plugins) > 0 + + builder.build(session=session) + + # Drop through to the before_script plugin hook + proc = session.cmd('display-message', '-p', "'#S'") + assert proc.stdout[0] == "'plugin_test_bwb'" + + # Drop through to the after_window_finished. This won't succeed + # unless on_window_create succeeds because of how the test plugin + # override methods are currently written + proc = session.cmd('display-message', '-p', "'#W'") + assert proc.stdout[0] == "'mp_test_awf'" diff --git a/tmuxp/__about__.py b/tmuxp/__about__.py index 7fe395812e9..7242004d0b6 100644 --- a/tmuxp/__about__.py +++ b/tmuxp/__about__.py @@ -1,6 +1,6 @@ __title__ = 'tmuxp' __package_name__ = 'tmuxp' -__version__ = '1.6.1' +__version__ = '1.7.0a1' __description__ = 'tmux session manager' __email__ = 'tony@git-pull.com' __author__ = 'Tony Narlock' diff --git a/tmuxp/cli.py b/tmuxp/cli.py index 6b1ac0cf5df..dc0b93571f7 100644 --- a/tmuxp/cli.py +++ b/tmuxp/cli.py @@ -7,6 +7,7 @@ """ from __future__ import absolute_import +import importlib import logging import os import sys @@ -372,13 +373,48 @@ def scan_config(config, config_dir=None): return config -def _reattach(session): +def load_plugins(sconf): + """ + Load and return plugins in config + """ + plugins = [] + if 'plugins' in sconf: + for plugin in sconf['plugins']: + try: + module_name = plugin.split('.') + module_name = '.'.join(module_name[:-1]) + plugin_name = plugin.split('.')[-1] + plugin = getattr(importlib.import_module(module_name), plugin_name) + plugins.append(plugin()) + except exc.TmuxpPluginException as error: + if not click.confirm( + '%sSkip loading %s?' + % (click.style(str(error), fg='yellow'), plugin_name), + default=True, + ): + click.echo( + click.style('[Not Skipping] ', fg='yellow') + + 'Plugin versions constraint not met. Exiting...' + ) + sys.exit(1) + except Exception as error: + click.echo( + click.style('[Plugin Error] ', fg='red') + + "Couldn\'t load {0}\n".format(plugin) + + click.style('{0}'.format(error), fg='yellow') + ) + sys.exit(1) + + return plugins + + +def _reattach(builder): """ Reattach session (depending on env being inside tmux already or not) Parameters ---------- - session : :class:`libtmux.Session` + builder: :class:`workspacebuilder.WorkspaceBuilder` Notes ----- @@ -388,11 +424,18 @@ def _reattach(session): If not, ``tmux attach-session`` loads the client to the target session. """ + + for plugin in builder.plugins: + plugin.reattach(builder.session) + proc = builder.session.cmd('display-message', '-p', "'#S'") + for line in proc.stdout: + print(line) + if 'TMUX' in os.environ: - session.switch_client() + builder.session.switch_client() else: - session.attach_session() + builder.session.attach_session() def load_workspace( @@ -514,7 +557,9 @@ def load_workspace( which('tmux') # raise exception if tmux not found try: # load WorkspaceBuilder object for tmuxp config / tmux server - builder = WorkspaceBuilder(sconf=sconfig, server=t) + builder = WorkspaceBuilder( + sconf=sconfig, plugins=load_plugins(sconfig), server=t + ) except exc.EmptyConfigException: click.echo('%s is empty or parsed no config data' % config_file, err=True) return @@ -532,7 +577,7 @@ def load_workspace( default=True, ) ): - _reattach(builder.session) + _reattach(builder) return try: @@ -590,13 +635,14 @@ def load_workspace( builder.session.kill_session() click.echo('Session killed.') elif choice == 'a': - if 'TMUX' in os.environ: - builder.session.switch_client() - else: - builder.session.attach_session() + _reattach(builder) else: sys.exit() + # Runs after before_script + for plugin in builder.plugins: + plugin.before_script(builder.session) + return builder.session diff --git a/tmuxp/config.py b/tmuxp/config.py index bea1df00811..eee39ce45c4 100644 --- a/tmuxp/config.py +++ b/tmuxp/config.py @@ -42,6 +42,10 @@ def validate_schema(sconf): if 'window_name' not in window: raise exc.ConfigError('config window is missing "window_name"') + if 'plugins' in sconf: + if not isinstance(sconf['plugins'], list): + raise exc.ConfigError('"plugins" only supports list type') + return True diff --git a/tmuxp/exc.py b/tmuxp/exc.py index fcf567796ff..fb847f82c1d 100644 --- a/tmuxp/exc.py +++ b/tmuxp/exc.py @@ -15,6 +15,8 @@ class TmuxpException(Exception): """Base Exception for Tmuxp Errors.""" + pass + class ConfigError(TmuxpException): @@ -30,6 +32,13 @@ class EmptyConfigException(ConfigError): pass +class TmuxpPluginException(TmuxpException): + + """Base Exception for Tmuxp Errors.""" + + pass + + class BeforeLoadScriptNotExists(OSError): def __init__(self, *args, **kwargs): super(BeforeLoadScriptNotExists, self).__init__(*args, **kwargs) diff --git a/tmuxp/plugin.py b/tmuxp/plugin.py new file mode 100644 index 00000000000..759e0bb4714 --- /dev/null +++ b/tmuxp/plugin.py @@ -0,0 +1,227 @@ +from distutils.version import LooseVersion + +import libtmux +from libtmux.common import get_version + +from .__about__ import __version__ +from .exc import TmuxpPluginException + +#: Minimum version of tmux required to run libtmux +TMUX_MIN_VERSION = '1.8' + +#: Most recent version of tmux supported +TMUX_MAX_VERSION = None + +#: Minimum version of libtmux required to run libtmux +LIBTMUX_MIN_VERSION = '0.8.3' + +#: Most recent version of libtmux supported +LIBTMUX_MAX_VERSION = None + +#: Minimum version of tmuxp required to use plugins +TMUXP_MIN_VERSION = '1.6.0' + +#: Most recent version of tmuxp +TMUXP_MAX_VERSION = None + + +class TmuxpPlugin: + def __init__( + self, + plugin_name='tmuxp-plugin', + tmux_min_version=TMUX_MIN_VERSION, + tmux_max_version=TMUX_MAX_VERSION, + tmux_version_incompatible=None, + libtmux_min_version=LIBTMUX_MIN_VERSION, + libtmux_max_version=LIBTMUX_MAX_VERSION, + libtmux_version_incompatible=None, + tmuxp_min_version=TMUXP_MIN_VERSION, + tmuxp_max_version=TMUXP_MAX_VERSION, + tmuxp_version_incompatible=None, + ): + """ + Initialize plugin. + + The default version values are set to the versions that the plugin + system requires. + + Parameters + ---------- + plugin_name : str + Name of the child plugin. Used in error message plugin fails to + load + + tmux_min_version : str + Min version of tmux that the plugin supports + + tmux_max_version : str + Min version of tmux that the plugin supports + + tmux_version_incompatible : list + Versions of tmux that are incompatible with the plugin + + libtmux_min_version : str + Min version of libtmux that the plugin supports + + libtmux_max_version : str + Max version of libtmux that the plugin supports + + libtmux_version_incompatible : list + Versions of libtmux that are incompatible with the plugin + + tmuxp_min_version : str + Min version of tmuxp that the plugin supports + + tmuxp_max_version : str + Max version of tmuxp that the plugin supports + + tmuxp_version_incompatible : list + Versions of tmuxp that are incompatible with the plugin + + """ + self.plugin_name = plugin_name + + # Dependency versions + self.tmux_version = get_version() + self.libtmux_version = libtmux.__version__ + self.tmuxp_version = LooseVersion(__version__) + + self.version_constraints = { + 'tmux': { + 'version': self.tmux_version, + 'vmin': tmux_min_version, + 'vmax': tmux_max_version, + 'incompatible': tmux_version_incompatible + if tmux_version_incompatible + else [], + }, + 'libtmux': { + 'version': self.libtmux_version, + 'vmin': libtmux_min_version, + 'vmax': libtmux_max_version, + 'incompatible': libtmux_version_incompatible + if libtmux_version_incompatible + else [], + }, + 'tmuxp': { + 'version': self.tmuxp_version, + 'vmin': tmuxp_min_version, + 'vmax': tmuxp_max_version, + 'incompatible': tmuxp_version_incompatible + if tmuxp_version_incompatible + else [], + }, + } + + self._version_check() + + def _version_check(self): + """ + Check all dependency versions for compatibility. + """ + for dep, constraints in self.version_constraints.items(): + try: + assert self._pass_version_check(**constraints) + except AssertionError: + raise TmuxpPluginException( + 'Incompatible {dep} version: {version}\n{plugin_name} ' + 'requirements:\nmin: {vmin} | max: {vmax} | ' + 'incompatible: {incompatible}\n'.format( + dep=dep, plugin_name=self.plugin_name, **constraints + ) + ) + + def _pass_version_check(self, version, vmin, vmax, incompatible): + """ + Provide affirmative if version compatibility is correct. + """ + if vmin and version < LooseVersion(vmin): + return False + if vmax and version > LooseVersion(vmax): + return False + if version in incompatible: + return False + + return True + + def before_workspace_builder(self, session): + """ + Provide a session hook previous to creating the workspace. + + This runs after the session has been created but before any of + the windows/panes/commands are entered. + + Parameters + ---------- + session : :class:`libtmux.Session` + session to hook into + """ + pass + + def on_window_create(self, window): + """ + Provide a window hook previous to doing anything with a window. + + This runs runs before anything is created in the windows, like panes. + + Parameters + ---------- + window: :class:`libtmux.Window` + window to hook into + """ + pass + + def after_window_finished(self, window): + """ + Provide a window hook after creating the window. + + This runs after everything has been created in the window, including + the panes and all of the commands for the panes. It also runs after the + ``options_after`` has been applied to the window. + + Parameters + ---------- + window: :class:`libtmux.Window` + window to hook into + """ + pass + + def before_script(self, session): + """ + Provide a session hook after the workspace has been built. + + This runs after the workspace has been loaded with ``tmuxp load``. It + augments instead of replaces the ``before_script`` section of the + configuration. + + This hook provides access to the LibTmux.session object for any + behavior that would be used in the ``before_script`` section of the + configuration file that needs access directly to the session object. + This runs after the workspace has been loaded with ``tmuxp load``. + + The hook augments, rather than replaces, the ``before_script`` section + of the configuration. While it is possible to do all of the + ``before_script`` configuration in this function, if a shell script + is currently being used for the configuration, it would be cleaner to + continue using the script in the ``before_section``. + + If changes to the session need to be made prior to + anything being built, please use ``before_workspace_builder`` instead. + + Parameters + ---------- + session : :class:`libtmux.Session` + session to hook into + """ + pass + + def reattach(self, session): + """ + Provide a session hook before reattaching to the session. + + Parameters + ---------- + session : :class:`libtmux.Session` + session to hook into + """ + pass diff --git a/tmuxp/workspacebuilder.py b/tmuxp/workspacebuilder.py index d24164f5f9e..d313438d83e 100644 --- a/tmuxp/workspacebuilder.py +++ b/tmuxp/workspacebuilder.py @@ -68,7 +68,7 @@ class WorkspaceBuilder(object): a session inside tmux (when `$TMUX` is in the env variables). """ - def __init__(self, sconf, server=None): + def __init__(self, sconf, plugins=[], server=None): """ Initialize workspace loading. @@ -77,6 +77,9 @@ def __init__(self, sconf, server=None): sconf : dict session config, includes a :py:obj:`list` of ``windows``. + plugins : list + plugins to be used for this session + server : :class:`libtmux.Server` tmux server to build session in @@ -98,6 +101,8 @@ def __init__(self, sconf, server=None): self.sconf = sconf + self.plugins = plugins + def session_exists(self, session_name=None): exists = self.server.has_session(session_name) if not exists: @@ -159,6 +164,9 @@ def build(self, session=None): assert isinstance(session, Session) + for plugin in self.plugins: + plugin.before_workspace_builder(self.session) + focus = None if 'before_script' in self.sconf: @@ -186,6 +194,9 @@ def build(self, session=None): for w, wconf in self.iter_create_windows(session): assert isinstance(w, Window) + for plugin in self.plugins: + plugin.on_window_create(w) + focus_pane = None for p, pconf in self.iter_create_panes(w, wconf): assert isinstance(p, Pane) @@ -202,6 +213,9 @@ def build(self, session=None): self.config_after_window(w, wconf) + for plugin in self.plugins: + plugin.after_window_finished(w) + if focus_pane: focus_pane.select_pane()