Repository: xonsh/xonsh

Issue:
Current job is not updated in terminal window's title
When I run `xonsh` is using its default settings, the `$TITLE` format string (responsible for setting the terminal window's title) is
```
{current_job:{} | }{user}@{hostname}: {cwd} | xonsh
```
The `current_job` variable in `$TITLE` means that when a foreground job is running, the terminal's title should be updated with the job's command.

For example, suppose my terminal's title is `yaxollum@fedora: ~ | xonsh` when no jobs are running.

When I launch the `cat` command, my terminal's title should be updated to `cat | yaxollum@fedora: ~ | xonsh`. However, under the current `main` version of xonsh, my terminal's title stays unchanged. `git bisect` shows that this was introduced by #4697.

Both this issue and #4034 appear to be related to setting the terminal's title, so I'll try to fix both of them in a PR.

## xonfig

<details>

```
+------------------+-------------------------+
| xonsh            | 0.13.0                  |
| Git SHA          | f2ca59a2                |
| Commit Date      | Aug 6 05:07:09 2022     |
| Python           | 3.9.13                  |
| PLY              | 3.11                    |
| have readline    | True                    |
| prompt toolkit   | 3.0.29                  |
| shell type       | prompt_toolkit          |
| history backend  | sqlite                  |
| pygments         | 2.7.4                   |
| on posix         | True                    |
| on linux         | True                    |
| distro           | fedora                  |
| on wsl           | False                   |
| on darwin        | False                   |
| on windows       | False                   |
| on cygwin        | False                   |
| on msys2         | False                   |
| is superuser     | False                   |
| default encoding | utf-8                   |
| xonsh encoding   | utf-8                   |
| encoding errors  | surrogateescape         |
| xontrib          | []                      |
| RC file 1        | /home/yaxollum/.xonshrc |
+------------------+-------------------------+
```

</details>

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**


Comments:

Patch:
diff --git a/news/fix-term-title-update.rst b/news/fix-term-title-update.rst
new file mode 100644
index 00000000..88cf5453
--- /dev/null
+++ b/news/fix-term-title-update.rst
@@ -0,0 +1,24 @@
+**Added:**
+
+* <news item>
+
+**Changed:**
+
+* The terminal's title is updated with the current command's name even if the command is a captured command or a callable alias
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* When using the sway window manager, ``swaymsg -t get_inputs`` no longer fails with the error "Unable to receive IPC response"
+* The ``current_job`` variable now works as expected when used in ``$TITLE``
+
+**Security:**
+
+* <news item>
diff --git a/xonsh/procs/pipelines.py b/xonsh/procs/pipelines.py
index f2c7dd5c..623b3a8f 100644
--- a/xonsh/procs/pipelines.py
+++ b/xonsh/procs/pipelines.py
@@ -757,17 +757,8 @@ class HiddenCommandPipeline(CommandPipeline):
         return ""
 
 
-def pause_call_resume(p, f, *args, **kwargs):
-    """For a process p, this will call a function f with the remaining args and
-    and kwargs. If the process cannot accept signals, the function will be called.
-
-    Parameters
-    ----------
-    p : Popen object or similar
-    f : callable
-    args : remaining arguments
-    kwargs : keyword arguments
-    """
+def resume_process(p):
+    """Sends SIGCONT to a process if possible."""
     can_send_signal = (
         hasattr(p, "send_signal")
         and xp.ON_POSIX
@@ -776,15 +767,9 @@ def pause_call_resume(p, f, *args, **kwargs):
     )
     if can_send_signal:
         try:
-            p.send_signal(signal.SIGSTOP)
+            p.send_signal(signal.SIGCONT)
         except PermissionError:
             pass
-    try:
-        f(*args, **kwargs)
-    except Exception:
-        pass
-    if can_send_signal:
-        p.send_signal(signal.SIGCONT)
 
 
 class PrevProcCloser(threading.Thread):
diff --git a/xonsh/procs/specs.py b/xonsh/procs/specs.py
index f1963690..dcd8be5e 100644
--- a/xonsh/procs/specs.py
+++ b/xonsh/procs/specs.py
@@ -22,7 +22,7 @@ from xonsh.procs.pipelines import (
     STDOUT_CAPTURE_KINDS,
     CommandPipeline,
     HiddenCommandPipeline,
-    pause_call_resume,
+    resume_process,
 )
 from xonsh.procs.posix import PopenThread
 from xonsh.procs.proxies import ProcProxy, ProcProxyThread
@@ -857,14 +857,8 @@ def cmds_to_specs(cmds, captured=False, envs=None):
     return specs
 
 
-def _should_set_title(captured=False):
-    env = XSH.env
-    return (
-        env.get("XONSH_INTERACTIVE")
-        and not env.get("XONSH_STORE_STDOUT")
-        and captured not in STDOUT_CAPTURE_KINDS
-        and XSH.shell is not None
-    )
+def _should_set_title():
+    return XSH.env.get("XONSH_INTERACTIVE") and XSH.shell is not None
 
 
 def run_subproc(cmds, captured=False, envs=None):
@@ -888,6 +882,23 @@ def run_subproc(cmds, captured=False, envs=None):
             print(f"TRACE SUBPROC: {cmds}, captured={captured}", file=sys.stderr)
 
     specs = cmds_to_specs(cmds, captured=captured, envs=envs)
+    if _should_set_title():
+        # context manager updates the command information that gets
+        # accessed by CurrentJobField when setting the terminal's title
+        with XSH.env["PROMPT_FIELDS"]["current_job"].update_current_cmds(cmds):
+            # remove current_job from prompt level cache
+            XSH.env["PROMPT_FIELDS"].reset_key("current_job")
+            # The terminal's title needs to be set before starting the
+            # subprocess to avoid accidentally answering interactive questions
+            # from commands such as `rm -i` (see #1436)
+            XSH.shell.settitle()
+            # run the subprocess
+            return _run_specs(specs, cmds)
+    else:
+        return _run_specs(specs, cmds)
+
+
+def _run_specs(specs, cmds):
     captured = specs[-1].captured
     if captured == "hiddenobject":
         command = HiddenCommandPipeline(specs)
@@ -906,15 +917,12 @@ def run_subproc(cmds, captured=False, envs=None):
                 "pgrp": command.term_pgid,
             }
         )
-    if _should_set_title(captured=captured):
-        # set title here to get currently executing command
-        pause_call_resume(proc, XSH.shell.settitle)
-    else:
-        # for some reason, some programs are in a stopped state when the flow
-        # reaches this point, hence a SIGCONT should be sent to `proc` to make
-        # sure that the shell doesn't hang. This `pause_call_resume` invocation
-        # does this
-        pause_call_resume(proc, int)
+    # For some reason, some programs are in a stopped state when the flow
+    # reaches this point, hence a SIGCONT should be sent to `proc` to make
+    # sure that the shell doesn't hang.
+    # See issue #2999 and the fix in PR #3000
+    resume_process(proc)
+
     # now figure out what we should return
     if captured == "object":
         return command  # object can be returned even if backgrounding
diff --git a/xonsh/prompt/base.py b/xonsh/prompt/base.py
index c0a44a59..2c6e379c 100644
--- a/xonsh/prompt/base.py
+++ b/xonsh/prompt/base.py
@@ -252,7 +252,7 @@ def _format_value(val, spec, conv) -> str:
     and 'current_job' returns 'sleep', the result is 'sleep | ', and if
     'current_job' returns None, the result is ''.
     """
-    if val is None:
+    if val is None or (isinstance(val, BasePromptField) and val.value is None):
         return ""
     val = xt.FORMATTER.convert_field(val, conv)
 
@@ -331,7 +331,7 @@ class PromptFields(tp.MutableMapping[str, "FieldType"]):
             _replace_home_cwd,
         )
         from xonsh.prompt.env import env_name, vte_new_tab_cwd
-        from xonsh.prompt.job import _current_job
+        from xonsh.prompt.job import CurrentJobField
         from xonsh.prompt.times import _localtime
         from xonsh.prompt.vc import branch_bg_color, branch_color, current_branch
 
@@ -349,7 +349,7 @@ class PromptFields(tp.MutableMapping[str, "FieldType"]):
                 curr_branch=current_branch,
                 branch_color=branch_color,
                 branch_bg_color=branch_bg_color,
-                current_job=_current_job,
+                current_job=CurrentJobField(name="current_job"),
                 env_name=env_name,
                 env_prefix="(",
                 env_postfix=") ",
@@ -403,6 +403,10 @@ class PromptFields(tp.MutableMapping[str, "FieldType"]):
         """the results are cached and need to be reset between prompts"""
         self._cache.clear()
 
+    def reset_key(self, key):
+        """remove a single key from the cache (if it exists)"""
+        self._cache.pop(key, None)
+
 
 class BasePromptField:
     value = ""
diff --git a/xonsh/prompt/job.py b/xonsh/prompt/job.py
index 97313e0a..ceeec1d9 100644
--- a/xonsh/prompt/job.py
+++ b/xonsh/prompt/job.py
@@ -1,14 +1,30 @@
 """Prompt formatter for current jobs"""
 
-import xonsh.jobs as xj
+import contextlib
+import typing as tp
 
+from xonsh.prompt.base import PromptField
 
-def _current_job():
-    j = xj.get_next_task()
-    if j is not None:
-        if not j["bg"]:
-            cmd = j["cmds"][-1]
+
+class CurrentJobField(PromptField):
+    _current_cmds: tp.Optional[list] = None
+
+    def update(self, ctx):
+        if self._current_cmds is not None:
+            cmd = self._current_cmds[-1]
             s = cmd[0]
             if s == "sudo" and len(cmd) > 1:
                 s = cmd[1]
-            return s
+            self.value = s
+        else:
+            self.value = None
+
+    @contextlib.contextmanager
+    def update_current_cmds(self, cmds):
+        """Context manager that updates the information used to update the job name"""
+        old_cmds = self._current_cmds
+        try:
+            self._current_cmds = cmds
+            yield
+        finally:
+            self._current_cmds = old_cmds
diff --git a/xonsh/readline_shell.py b/xonsh/readline_shell.py
index fce100b8..fb5fa48b 100644
--- a/xonsh/readline_shell.py
+++ b/xonsh/readline_shell.py
@@ -635,6 +635,8 @@ class ReadlineShell(BaseShell, cmd.Cmd):
             return self.mlprompt
         env = XSH.env  # pylint: disable=no-member
         p = env.get("PROMPT")
+        # clear prompt level cache
+        env["PROMPT_FIELDS"].reset()
         try:
             p = self.prompt_formatter(p)
         except Exception:  # pylint: disable=broad-except


Test patch:
diff --git a/tests/prompt/test_base.py b/tests/prompt/test_base.py
index 1f102ed1..9b633633 100644
--- a/tests/prompt/test_base.py
+++ b/tests/prompt/test_base.py
@@ -4,7 +4,7 @@ from unittest.mock import Mock
 import pytest
 
 from xonsh.prompt import env as prompt_env
-from xonsh.prompt.base import PromptFields, PromptFormatter
+from xonsh.prompt.base import PromptField, PromptFields, PromptFormatter
 
 
 @pytest.fixture
@@ -40,8 +40,10 @@ def test_format_prompt(inp, exp, fields, formatter, xession):
             "a_string": "cats",
             "a_number": 7,
             "empty": "",
-            "current_job": (lambda: "sleep"),
+            "a_function": (lambda: "hello"),
+            "current_job": PromptField(value="sleep"),
             "none": (lambda: None),
+            "none_pf": PromptField(value=None),
         }
     ],
 )
@@ -49,7 +51,9 @@ def test_format_prompt(inp, exp, fields, formatter, xession):
     "inp, exp",
     [
         ("{a_number:{0:^3}}cats", " 7 cats"),
+        ("{a_function:{} | }xonsh", "hello | xonsh"),
         ("{current_job:{} | }xonsh", "sleep | xonsh"),
+        ("{none_pf:{} | }xonsh", "xonsh"),
         ("{none:{} | }{a_string}{empty:!}", "cats!"),
         ("{none:{}}", ""),
         ("{{{a_string:{{{}}}}}}", "{{cats}}"),
diff --git a/tests/prompt/test_job.py b/tests/prompt/test_job.py
new file mode 100644
index 00000000..d8c286cb
--- /dev/null
+++ b/tests/prompt/test_job.py
@@ -0,0 +1,13 @@
+def test_current_job(xession):
+    prompts = xession.env["PROMPT_FIELDS"]
+    cmds = (["echo", "hello"], "|", ["grep", "h"])
+
+    prompts.reset()
+    assert format(prompts.pick("current_job")) == ""
+
+    with prompts["current_job"].update_current_cmds(cmds):
+        prompts.reset()
+        assert format(prompts.pick("current_job")) == "grep"
+
+    prompts.reset()
+    assert format(prompts.pick("current_job")) == ""
diff --git a/tests/test_aliases.py b/tests/test_aliases.py
index b1e92b0c..3662e2b9 100644
--- a/tests/test_aliases.py
+++ b/tests/test_aliases.py
@@ -2,6 +2,7 @@
 
 import inspect
 import os
+import sys
 
 import pytest
 
@@ -195,7 +196,7 @@ def test_exec_alias_args(xession):
 def test_exec_alias_return_value(exp_rtn, xonsh_session, monkeypatch):
     monkeypatch.setitem(xonsh_session.env, "RAISE_SUBPROC_ERROR", False)
     stack = inspect.stack()
-    rtn = ExecAlias(f"python -c 'exit({exp_rtn})'")([], stack=stack)
+    rtn = ExecAlias(f"{sys.executable} -c 'exit({exp_rtn})'")([], stack=stack)
     assert rtn == exp_rtn