diff --git a/CHANGELOG.md b/CHANGELOG.md
index b479c5c..f7837de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
+## 3.19.0
+
+### Added
+
+- Tmux strategy - Runs test commands in a tmux pane [#119](https://github.com/gerardroche/sublime-phpunit/issues/119)
+
## 3.18.3 - Unreleased
### Fixed
diff --git a/Preferences.sublime-settings b/Preferences.sublime-settings
index 2bcc8c1..de804a5 100644
--- a/Preferences.sublime-settings
+++ b/Preferences.sublime-settings
@@ -94,5 +94,23 @@
// paths and the values are the replacement remote paths. Environment
// variables and user home directory ~ placeholder are expanded.
// Example: {"~/code/project1": "~/project1"}
- "phpunit.docker_paths": {}
+ "phpunit.docker_paths": {},
+
+ // Clear the terminal screen before running tests.
+ "phpunit.tmux_clear": true,
+
+ // Clear the terminal's scrollback buffer using the extended "E3" capability.
+ "phpunit.tmux_clear_scrollback": true,
+
+ // Specify the session, window, and pane which should be used to run tests.
+ //
+ // Format: `{session}:{window}.{pane}`
+ //
+ // The default means the current pane.
+ //
+ // For example, ":{start}.{top}", would mean the current session,
+ // lowest-numbered window, top pane.
+ //
+ // See http://man.openbsd.org/OpenBSD-current/man1/tmux.1#COMMANDS
+ "phpunit.tmux_target": ":."
}
diff --git a/README.md b/README.md
index 759e073..564d559 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ Enhance your coding experience with seamless PHPUnit integration for [Sublime Te
- [xterm] - A terminal emulator for the X Window System.
- [cmd] - A command-line interpreter for Windows.
- [PowerShell] - A cross-platform command-line shell.
+ - [Tmux] - A terminal multiplexer. :new:
* Zero configuration required
Read [Running PHPUnit Tests from Sublime Text](https://blog.gerardroche.com/2023/05/05/running-phpunit-tests-within-sublime-text/) for a quick introduction.
@@ -58,6 +59,7 @@ Read [Running PHPUnit Tests from Sublime Text](https://blog.gerardroche.com/2023
- [PHP Executable](#php-executable)
- [SSH](#ssh)
- [Docker](#docker)
+ - [Tmux](#tmux)
- [Auto Commands](#auto-commands)
- [Toggle Commands](#toggle-commands)
- [Custom Toggle Commands](#custom-toggle-commands)
@@ -150,17 +152,13 @@ Enhance your testing workflow with these commands for efficient testing directly
PHPUnitKit can run tests using different execution environments known as "strategies".
-**Example:** Using the Kitty Terminal Strategy
-
-To set this strategy:
+**Example:** Using the Tmux Strategy
1. Open the Command Palette: `Preferences: PHPUnit Settings`
2. Add the following to your settings:
-```json
-{
- "phpunit.strategy": "kitty"
-}
+```
+"phpunit.strategy": "tmux"
```
Available strategies and their identifiers:
@@ -173,6 +171,7 @@ Available strategies and their identifiers:
| **[xterm]** | `xterm` | Sends test commands to the xterm terminal.
| **[cmd]** | `cmd` | Sends test commands to the cmd.exe terminal.
| **[PowerShell]** | `powershell` | Sends test commands to the PowerShell command shell.
+| **[Tmux]** | `tmux` | Sends test commands to the Tmux terminal multiplexer. :new:
## Configuration
@@ -184,9 +183,9 @@ Available settings and their details:
| Setting | Type | Default | Description
| :------------------------ | :----------------- | :------------------- | :----------
-| `phpunit.executable` | `string` or `list` | Auto-discovery | Path to the PHPUnit executable for running tests. Environment variables and user home directory ~ placeholder are expanded. The executable can be a string or a list of parameters. Example: `vendor/bin/phpunit`
+| `phpunit.executable` | `string` or `list` | Auto-discovery | Path to the PHPUnit executable for running tests. Environment variables and user home directory ~ placeholder are expanded. The executable can be a string or a list of parameters. Example: `vendor/bin/phpunit`
| `phpunit.options` | `dict` | `{}` | Command-line options to pass to PHPUnit. Example: `{"no-coverage": true}`
-| `phpunit.php_executable` | `string` | Auto-discovery | Path to the PHP executable for running tests. Environment variables and user home directory ~ placeholder are expanded. Example: `~/.phpenv/versions/8.2/bin/php`
+| `phpunit.php_executable` | `string` | Auto-discovery | Path to the PHP executable for running tests. Environment variables and user home directory ~ placeholder are expanded. Example: `~/.phpenv/versions/8.2/bin/php`
| `phpunit.save_all_on_run` | `boolean` | `true` | Automatically saves all unsaved buffers before running tests.
| `phpunit.on_post_save` | `list` | `[]` | Auto commands to execute when views are saved. Example: `["phpunit_test_file"]`
| `phpunit.debug` | `boolean` | `false` | Prints debug information about the test runner.
@@ -198,9 +197,7 @@ Available settings and their details:
| `phpunit.paratest` | `boolean` | `false` | Uses ParaTest to run tests.
| `phpunit.pest` | `boolean` | `false` | Uses Pest to run tests.
-These settings allow you to customize PHPUnitKit according to your preferences and requirements.
-
-**SSH Settings** :rocket:
+**SSH Settings**
Configure SSH settings for running tests remotely:
@@ -212,9 +209,7 @@ Configure SSH settings for running tests remotely:
| `phpunit.ssh_host` | `string` | `null` | Host for running tests via SSH. Example: `homestead.test`
| `phpunit.ssh_paths` | `dict` | `{}` | Path mapping for running tests via SSH. Keys: local paths, Values: corresponding remote paths. Environment variables and user home directory ~ placeholder are expanded. Example: `{"~/code/project1": "~/project1"}`
-Use these settings to configure PHPUnitKit's SSH options for seamless remote testing.
-
-**Docker Settings** :rocket:
+**Docker Settings**
Configure Docker settings for running tests within containers:
@@ -224,7 +219,15 @@ Configure Docker settings for running tests within containers:
| `phpunit.docker_command` | `list` | `[]` | Command to use when running tests via Docker. Example: `["docker", "exec", "-it", "my-container"]`
| `phpunit.docker_paths` | `dict` | `{}` | Path mapping for running tests via Docker. Keys: local paths, Values: corresponding remote paths. Environment variables and user home directory ~ placeholder are expanded. Example: `{"~/code/project1": "~/project1"}`
-Utilize these settings to configure PHPUnitKit for streamlined testing within Docker containers.
+**Tmux Settings** :new:
+
+Configure Tmux settings for running tests in a tmux pane:
+
+| Setting | Type | Default | Description
+| :-------------------------------- | :------------ | :-------- | :----------
+| `phpunit.tmux_clear` | `bool` | `true` | Clear the terminal screen before running tests.
+| `phpunit.tmux_clear_scrollback` | `bool` | `false` | Clear the terminal's scrollback buffer using the extended "E3" capability.
+| `phpunit.tmux_target` | `string` | `:.` | Specify the session, window, and pane which should be used to run tests.
Format: `{session}:{window}.{pane}`
The default means the current pane.
For example, `:{start}.{top}` would mean the current session, lowest-numbered window, top pane.
See [Tmux documentation](http://man.openbsd.org/OpenBSD-current/man1/tmux.1#COMMANDS) for target usage.
### CLI Options
@@ -367,6 +370,32 @@ Command Palette → Preferences: PHPUnit Settings
}
```
+### Tmux :new:
+
+**Example:** Run tests in a Tmux pane.
+
+```json
+{
+ "phpunit.strategy": "tmux",
+ "phpunit.options": { "colors": true, "no-coverage": true }
+}
+```
+
+Tip: Use the **`no-coverage`** option with the Command Palette **PHPUnit: Toggle --no-coverage** to turn code coverage on and off for quicker test runs when you just don't need the code coverage report.
+
+**Example:** Run tests in current session, lowest-numbered window, and top pane
+
+```json
+{
+ "phpunit.strategy": "tmux",
+ "phpunit.tmux_target": ":{start}.{top}"
+}
+```
+
+The target accepts the format `{target-session}:{target-window}.{target-pane}`. The default is `:.`, which means the current pane.
+
+See [Tmux documentation](http://man.openbsd.org/OpenBSD-current/man1/tmux.1#COMMANDS) for more details on the target usage.
+
### Auto Commands
You can configure the `on_post_save` event to run the "Test File" command when views are saved. This will instruct the runner to automatically run a test every time it is saved.
@@ -488,6 +517,7 @@ Released under the [GPL-3.0-or-later License](LICENSE).
[ParaTest]: https://github.com/paratestphp/paratest
[Pest]: https://pestphp.com
[PowerShell]: https://learn.microsoft.com/en-us/powershell/
+[Tmux]: https://github.com/tmux/tmux/wiki
[cmd]: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd
[iTerm2]: https://iterm2.com
[xterm]: https://invisible-island.net/xterm/
diff --git a/lib/runner.py b/lib/runner.py
index da57633..722a147 100644
--- a/lib/runner.py
+++ b/lib/runner.py
@@ -138,13 +138,10 @@ def run(self, working_dir=None, file=None, options=None) -> None:
'options': options
})
- strategy.execute(
- self.window,
- self.view,
- env,
- cmd,
- working_dir
- )
+ if get_setting(self.view, 'strategy') == 'tmux':
+ cmd = strategy.build_tmux_cmd(self.view, working_dir, cmd)
+
+ strategy.execute(self.window, self.view, env, cmd, working_dir)
def run_last(self) -> None:
last_test_args = get_last_run()
diff --git a/lib/strategy.py b/lib/strategy.py
index 26ad8e0..ab1afc0 100644
--- a/lib/strategy.py
+++ b/lib/strategy.py
@@ -17,6 +17,7 @@
import os
import re
+import shlex
from sublime import cache_path
from sublime import load_resource
@@ -28,7 +29,7 @@
def execute(window, view, env: dict, cmd: list, working_dir: str) -> None:
- if get_setting(view, 'strategy') in ('cmd', 'external', 'iterm', 'kitty', 'powershell', 'xterm'):
+ if get_setting(view, 'strategy') in ('cmd', 'external', 'iterm', 'kitty', 'powershell', 'tmux', 'xterm'):
window.run_command('exec', {
'env': env,
'cmd': cmd,
@@ -138,3 +139,31 @@ def _get_color_scheme(view):
' scheme with PHPUnit test results colors: {}'.format(str(e)))
return color_scheme
+
+
+def build_tmux_cmd(view, working_dir: str, cmd: list) -> list:
+ tmux_target = get_setting(view, 'tmux_target')
+
+ # Try make initial cmd relative to working directory to reduce length.
+ if cmd[0].startswith(working_dir):
+ cmd = [os.path.relpath(cmd[0], working_dir)] + cmd[1:]
+
+ key_cmds = []
+
+ # Clear the terminal screen.
+ if get_setting(view, 'tmux_clear'):
+ clear_cmd = ['clear']
+ if get_setting(view, 'tmux_clear_scrollback'):
+ clear_cmd.append('-x')
+ key_cmds.append(shlex.join(clear_cmd))
+
+ # Switch to the working directory.
+ key_cmds.append(shlex.join(['cd', working_dir]))
+
+ # The test command.
+ key_cmds.append(shlex.join(cmd))
+
+ # Run inside a subshell to avoid changing the current working directory.
+ keys = '({})\n'.format(' && '.join(key_cmds))
+
+ return ['tmux', 'send-keys', '-t', tmux_target, keys]
diff --git a/lib/utils.py b/lib/utils.py
index 9ae7733..bfae24e 100644
--- a/lib/utils.py
+++ b/lib/utils.py
@@ -18,6 +18,9 @@
import os
import re
import shutil
+import subprocess
+import sys
+import traceback
from sublime import active_window
from sublime import platform
@@ -750,3 +753,76 @@ def toggle_on_post_save(view, item: str) -> None:
view.settings().erase('phpunit.on_post_save')
if on_post_save != view.settings().get('phpunit.on_post_save'):
view.settings().set('phpunit.on_post_save', on_post_save)
+
+
+def _get_default_shell() -> str:
+ if sys.platform.startswith('linux') or sys.platform.startswith('darwin'):
+ return os.environ.get('SHELL', 'sh')
+ elif sys.platform.startswith('win'):
+ return 'cmd.exe'
+ else:
+ return ''
+
+
+if sys.platform.startswith('win'):
+ try:
+ import ctypes
+ except ImportError:
+ traceback.print_exc()
+ ctypes = None
+
+ def _get_startup_info():
+ # Hide the child process window.
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+
+ return startupinfo
+
+ def _get_encoding() -> str:
+ return str(ctypes.windll.kernel32.GetOEMCP())
+
+ def _shell_filter_newlines(text: str):
+ return text.replace('\r\n', '\n')
+
+ def _shell_decode(res):
+ return _shell_filter_newlines(res.decode(_get_encoding()))
+
+ def _shell_read(view, cmd: str) -> str:
+ p = subprocess.Popen([_get_default_shell(), '/c', cmd],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ startupinfo=_get_startup_info())
+ out, err = p.communicate()
+
+ if out:
+ return _shell_decode(out)
+
+ if err:
+ return _shell_decode(err)
+
+ return ''
+
+else:
+ def _shell_read(view, cmd: str) -> str:
+ p = subprocess.Popen([_get_default_shell(), '-c', cmd],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+
+ out, err = p.communicate()
+
+ if out:
+ return out.decode('utf-8').strip()
+
+ if err:
+ return err.decode('utf-8').strip()
+
+ return ''
+
+
+def shell_read(view, cmd: str) -> str:
+ try:
+ return _shell_read(view, cmd)
+ except Exception:
+ traceback.print_exc()
+
+ return ''