Display VIRTUAL_ENV in a custom status bar component

289 views
Skip to first unread message

doerw...@googlemail.com

unread,
Sep 14, 2021, 3:57:31 PM9/14/21
to iterm2-discuss
Hi all!

I've written my first custom status bar component. It is used to display the
currently active Python virtual environment. Here is the code:

```
#!/usr/bin/env python3

import iterm2


async def main(connection):
    python_venv_component = iterm2.StatusBarComponent(
        short_description="Python virtual environment",
        detailed_description="Show the currently active Python virtual environment",
        knobs=[
            iterm2.CheckboxKnob("Show even if unset?", False, "force"),
            iterm2.CheckboxKnob("Shorten to trailing directory?", True, "shorten"),
        ],
        exemplar="🐍\N{THIN SPACE}~/pyvenvs/default",
        update_cadence=None,
        identifier="com.livinglogic.walter.iterm.status.python-venv")

    @iterm2.StatusBarRPC
    async def python_venv_callback(
        knobs,
        python_venv=iterm2.Reference("user.python_venv?")
    ):
        if python_venv:
            if "shorten" in knobs and knobs["shorten"]:
                python_venv = python_venv.rsplit("/")[-1]
            return f"🐍\N{THIN SPACE}{python_venv}"
        elif "force" in knobs and knobs["force"]:
            return f"🐍\N{THIN SPACE}\N{TWO-EM DASH}"
        else:
            return ""

    await python_venv_component.async_register(connection, python_venv_callback)


iterm2.run_forever(main)
```

For this to work `iterm2_print_user_vars` must set `python_venv` like this:

```
iterm2_print_user_vars()
{
    iterm2_set_user_var python_venv $VIRTUAL_ENV
}
```

Is there a place where examples like this can be collected?

Also it would be really great if we could merge status bar components and tab titles. I.e. the tab title could consist of multiple components just like the status bar does. Such a component would be usable both in the tab title and the status bar.

Then I could configure my tab title simply by assembling components. By using spacers I could make some of them right aligned (and using "Remove components with empty values" like in the status bar, I could make empty ones disappear).

And if the HTML functionality that the tab title provides was available in the status bar components, this would allow enhanced styling for the status bar component.

(And while we're at it, I would want that new component based functionality for the window title and the badge too ;))

Servus,
   Walter

George Nachman

unread,
Sep 26, 2021, 6:04:50 PM9/26/21
to iterm2-...@googlegroups.com
> Is there a place where examples like this can be collected?

I’ve been adding them here: https://iterm2.com/python-api/examples/index.html. I’ve added yours and it’ll appear next time I update the website.

> Also it would be really great if we could merge status bar components and tab titles. I.e. the tab title could consist of multiple components just like the status bar does. Such a component would be usable both in the tab title and the status bar.

I think that doing this is harder for tab titles than for status bar components because the space is so much smaller. That’s why custom tab titles have to be implemented by an API script. The existing components (job name, pwd, etc.) are combined by a really complicated built-in algorithm that tries to do something sane for all combinations of components.

> Then I could configure my tab title simply by assembling components. By using spacers I could make some of them right aligned (and using "Remove components with empty values" like in the status bar, I could make empty ones disappear).

It would be neat if custom title providers were configurable like status bar components. I think that’s the best way to do this.

> And if the HTML functionality that the tab title provides was available in the status bar components, this would allow enhanced styling for the status bar component.

Done in commit e089ffbfe3e024a94218084cef1e9413ffafd736. You’ll need version 1.29 of the iterm2 module. The initializer for the StatusBarComponent class now takes a format argument. It defaults to iterm2.StatusBarComponent.Format.PLAIN_TEXT. You should use HTML instead of PLAIN_TEXT. It only accepts <u>, <I>, and <b> for now.

> (And while we're at it, I would want that new component based functionality for the window title and the badge too ;))

This is a bit more work but is quite reasonable.

--
You received this message because you are subscribed to the Google Groups "iterm2-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to iterm2-discus...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/iterm2-discuss/88722886-145b-419d-9cf0-00e67f60c813n%40googlegroups.com.

Walter Dörwald

unread,
Sep 28, 2021, 7:35:03 AM9/28/21
to iterm2-...@googlegroups.com

On 27 Sep 2021, at 0:04, George Nachman wrote:

Is there a place where examples like this can be collected?

I’ve been adding them here:
https://iterm2.com/python-api/examples/index.html. I’ve added yours and
it’ll appear next time I update the website.

OK. Thanks!

I'm using another one that displays the value of DOCKER_HOST, but that is more or less the same as the one for VIRTUAL_ENV.

Also it would be really great if we could merge status bar components and
tab titles. I.e. the tab title could consist of multiple components just
like the status bar does. Such a component would be usable both in the tab
title and the status bar.

I think that doing this is harder for tab titles than for status bar
components because the space is so much smaller.

But status bar components seem to be already equipped with features that help reduce space: Empty components can be completely skipped, the component can return a list with output of varying lengths so that iTerm can pick the appropriate one and the user can configure various preferences for compression and minimum/maximum width. This seems to me to be much better that what we have for tab titles, which is basically one string returned by the tab title component. Or am I misunderstanding something here?

That’s why custom tab
titles have to be implemented by an API script. The existing components
(job name, pwd, etc.) are combined by a really complicated built-in
algorithm that tries to do something sane for all combinations of
components.

Then I could configure my tab title simply by assembling components. By
using spacers I could make some of them right aligned (and using "Remove
components with empty values" like in the status bar, I could make empty
ones disappear).

It would be neat if custom title providers were configurable like status
bar components. I think that’s the best way to do this.

Indeed that would be great. I've looked at your title bar example at https://iterm2.com/python-api/examples/georges_title.html and the title bar seems to consist of "components" anyway:

parts = [make_title(auto_name, profile_name),
         make_hostname(hostname, localhost),
         make_pwd(user_home, localhome, pwd),
         make_branch(branch)]
return " ".join(list(filter(lambda x: x, parts)))

i.e. make_title(...) returns a "component", make_hostname(...) returns one etc.

return " ".join(list(filter(lambda x: x, parts))) combines the components and skips empty ones.

So a new version could turn these pseudo components (which are just strings) into real components that could be assembled and configured by the user.

And if the HTML functionality that the tab title provides was available
in the status bar components, this would allow enhanced styling for the
status bar component.

Done in commit e089ffbfe3e024a94218084cef1e9413ffafd736. You’ll need
version 1.29 of the iterm2 module. The initializer for
the StatusBarComponent class now takes a format argument. It defaults to
iterm2.StatusBarComponent.Format.PLAIN_TEXT. You should use HTML instead of
PLAIN_TEXT. It only accepts <u>, <I>, and <b> for now.

Great! I look forward to the next iTerm version to try that out.

(And while we're at it, I would want that new component based
functionality for the window title and the badge too ;))

This is a bit more work but is quite reasonable.

BTW, I wondered why status bar components implement their functionality the way they do. I would have expected that I can subclass iterm2.StatusBarComponent and implement formatting by overwriting the appropriate method, i.e. something like this:

class StatusBarPythonVEnv(iterm2.StatusBarComponent):
    def __init__(self):
        super().__init__(
            short_description="Python virtual environment",
            detailed_description="Show the currently active Python virtual environment",
            knobs=[
                iterm2.CheckboxKnob("Show even if unset?", False, "force"),
                iterm2.CheckboxKnob("Shorten to trailing directory?", True, "shorten"),
            ],
            exemplar="🐍\N{THIN SPACE}~/pyvenvs/default",
            update_cadence=None,
            identifier="com.livinglogic.walter.iterm.status.python-venv"
        )

    async def execute(
        knobs,
        python_venv=iterm2.Reference("user.python_venv?"),
        user_home=iterm2.Reference("user.home?"),
    ):
        if python_venv:
            if knobs.get("shorten", True):
                python_venv = python_venv.rsplit("/")[-1]
            
return f"🐍\N{THIN SPACE}{python_venv}"

        elif knobs.get("force", False):
            
return f"🐍\N{THIN SPACE}\N{TWO-EM DASH}"
        else:
            return ""


component = StatusBarPythonVEnv()

await component.async_register(connection)

Instead the component object and the function that formats the output are totally unrelated. That makes it hard to give the component state which is independent of the user configurable knobs.

Servus,
Walter

George Nachman

unread,
Oct 1, 2021, 1:02:15 PM10/1/21
to iterm2-...@googlegroups.com
It certainly could have been designed that way. If you want to have encapsulation, you can still do it, just without subclassing.

class MyStatusBar:
  def __init__(self):
    …

  async def async_register(self, connection):
    await self.execute.async_register(connection)

  @iterm2.StatusBarRPC
  async def execute(…):
    …
component = MyStatusBar()
await component.async_register()

Disclaimer: I haven't actually tried it, but it's at least close to correct :)

I don't particularly like OO inheritance so my preference is to not force people to use it if.
 

Walter Dörwald

unread,
Oct 5, 2021, 2:13:00 PM10/5/21
to iterm2-...@googlegroups.com

On 1 Oct 2021, at 19:01, George Nachman wrote:

[...]


It certainly could have been designed that way. If you want to have
encapsulation, you can still do it, just without subclassing.

class MyStatusBar:
def __init__(self):


async def async_register(self, connection):
await self.execute.async_register(connection)

@iterm2.StatusBarRPC
async def execute(…):

component = MyStatusBar()
await component.async_register()

Disclaimer: I haven't actually tried it, but it's at least close to correct
:)

But where does the actual component get created?

I tried the following:

class PythonVenvStatusBar(iterm2.StatusBarComponent):
    
def __init__(self):
        super().__init__(
            short_description="Python virtual environment",
            detailed_description="Show the currently active Python virtual environment",
            knobs=[
                iterm2.CheckboxKnob("Show even if unset?", False, "force"),
                iterm2.CheckboxKnob("Shorten to trailing directory?", True, "shorten"),
            ],
            exemplar="🐍\N{THIN SPACE}~/pyvenvs/default",
            update_cadence=None,
            identifier="com.livinglogic.walter.iterm.status.python-venv"

        )

    async def async_register(self, connection):
        await self.execute.async_register(connection)

    @iterm2.StatusBarRPC
    async def execute(
        self,
        knobs,
        python_venv=iterm2.Reference("user.python_venv?"),
        user_home=iterm2.Reference("user.home?"),
    ):
        
if python_venv:
            if knobs.get("shorten", True):
                python_venv = python_venv.rsplit("/")[-1
]
            else:
                python_venv = fancy_directory_name(user_home, python_venv)
            
return f"🐍\N{THIN SPACE}{python_venv}"
        elif knobs.get("force", False):
            return f"🐍\N{THIN SPACE}\N{TWO-EM DASH}"
        else:
            return ""


comp = PythonVenvStatusBar()
await comp.async_register(connection)

This gives me

Traceback (most recent call last):
  File "/Users/walter/.config/iterm2/AppSupport/iterm2env/versions/3.8.6/lib/python3.8/site-packages/iterm2/connection.py", line 412, in async_connect
    return await coro(self)
  File "/Users/walter/.config/iterm2/AppSupport/iterm2env/versions/3.8.6/lib/python3.8/site-packages/iterm2/connection.py", line 219, in async_main
    result = await coro(connection)
  File "/Users/walter/.config/iterm2/AppSupport/Scripts/AutoLaunch/walters_title.py", line 354, in main
    await comp.async_register(connection)
  File "/Users/walter/.config/iterm2/AppSupport/Scripts/AutoLaunch/walters_title.py", line 333, in async_register
    await self.execute.async_register(connection)
TypeError: async_register() missing 1 required positional argument: 'component'

And if I replace

async def async_register(self, connection):
        await self.execute.async_register(connection)

with

async def register(self, connection):
            await self.async_register(connection, self.execute)

(and then call register() instead of async_register()), I get:

❗️ Status bar component “Python virtual environment” (com.livinglogic.walter.iterm.status.python-venv) failed.

This function call had an error:

statusbar.com.livinglogic.walter.iterm.status.python_venv.execute(knobs:__knobs,python_venv:user.python_venv?,user_home:user.home?)

The error was:

No function registered for invocation “statusbar.com.livinglogic.walter.iterm.status.python_venv.execute()”. Ensure the script is running and the function name and argument names are correct. There is a similarly named function available with a different signature: statusbar.com.livinglogic.walter.iterm.status.python_venv.execute(knobs,python_venv,self,user_home)

I don't particularly like OO inheritance so my preference is to not force
people to use it if.

But in this case I think it's the right thing to done: Combine state and behaviour in one package.

Servus,
Walter

George Nachman

unread,
Oct 12, 2021, 3:04:17 PM10/12/21
to iterm2-...@googlegroups.com
You can’t put an `iterm2.StatusBarRPC` inside a class because it gets an implicit first argument of self that it isn’t expecting. I acknowledge this is not ideal; I hadn’t considered the possibility of people subclassing StatusBarComponent.

You can make it a static method, which gets you the encapsulation you want:

import iterm2

class PythonVenvStatusBar(iterm2.StatusBarComponent):
    def __init__(self):
        super().__init__(
            short_description="Python virtual environment",
            detailed_description="Show the currently active Python virtual environment",
            knobs=[
                iterm2.CheckboxKnob("Show even if unset?", False, "force"),
                iterm2.CheckboxKnob("Shorten to trailing directory?", True, "shorten"),
            ],
            exemplar="🐍\N{THIN SPACE}~/pyvenvs/default",
            update_cadence=None,
            identifier="com.livinglogic.walter.iterm.status.python-venv"
        )

    async def async_register(self, connection):
        await self.execute.async_register(connection)

    @staticmethod
    @iterm2.StatusBarRPC
    async def execute(
        knobs,
        python_venv=iterm2.Reference("user.python_venv?"),
        user_home=iterm2.Reference("user.home?"),
    ):
        if python_venv:
            if knobs.get("shorten", True):
                python_venv = python_venv.rsplit("/")[-1]
            else:
                python_venv = fancy_directory_name(user_home, python_venv)
            return f"🐍\N{THIN SPACE}{python_venv}"
        elif knobs.get("force", False):
            return f"🐍\N{THIN SPACE}\N{TWO-EM DASH}"
        else:
            return ""

async def main(connection):
    comp = PythonVenvStatusBar()
    await PythonVenvStatusBar.execute.async_register(connection, comp)

iterm2.run_forever(main)


--
You received this message because you are subscribed to the Google Groups "iterm2-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to iterm2-discus...@googlegroups.com.

Walter Dörwald

unread,
Oct 22, 2021, 12:22:57 PM10/22/21
to iterm2-...@googlegroups.com

OK, but the problem with a static method is that a static method can't access the instance attributes of the component either.

I tried decorating the bound method like this:

class StatusBarPythonVEnv(iterm2.StatusBarComponent):
    
def __init__(self):
        super().__init__(
            short_description="Python virtual environment",
            detailed_description="Show the currently active Python virtual environment",
            knobs=[
                iterm2.CheckboxKnob("Show even if unset?", False, "force"),
                iterm2.CheckboxKnob("Shorten to trailing directory?", True, "shorten"),
            ],
            exemplar="🐍\N{THIN SPACE}~/pyvenvs/default",
            update_cadence=None
,
            identifier="com.livinglogic.walter.iterm.status.python-venv2"
        )

    async def register(self, connection):
        execute = iterm2.StatusBarRPC(self.execute)
        await self.register(connection, execute)

    
async def execute(
        knobs,
        python_venv=iterm2.Reference("user.python_venv?"),
        user_home=iterm2.Reference("user.home?"),
    ):
        if python_venv:
            if knobs.get("shorten", True):
                python_venv = python_venv.rsplit("/")[-1]
            else:
                python_venv = fancy_directory_name(user_home, python_venv)
            return f"🐍\N{THIN SPACE}{python_venv}"
        elif knobs.get("force", False):
            return f"🐍\N{THIN SPACE}\N{TWO-EM DASH}"
        else:
            return ""

async def main
(connection):
    component = StatusBarPythonVEnv()

    await component.register(connection)

But this gives me:

22.10., 18:16:03,676: /bin/zsh -c /Applications/iTerm.app/Contents/Resources/it2_api_wrapper.sh /Users/walter/.config/iterm2/AppSupport/iterm2env/versions/3.8.6/bin/python3 /Users/walter/.config/iterm2/AppSupport/Scripts/AutoLaunch/walters_title.py
22.10., 18:16:03,692: + unset PYTHONPATH
22.10., 18:16:03,692: + export PYTHONUNBUFFERED=1
22.10., 18:16:03,692: + PYTHONUNBUFFERED=1
22.10., 18:16:03,692: + /Users/walter/.config/iterm2/AppSupport/iterm2env/versions/3.8.6/bin/python3 /Users/walter/.config/iterm2/AppSupport/Scripts/AutoLaunch/walters_title.py
22.10., 18:16:04,016: Connection accepted: Script launched by user action
22.10., 18:16:04,232: Traceback (most recent call last):
22.10., 18:16:04,232:   File "/Users/walter/.config/iterm2/AppSupport/iterm2env/versions/3.8.6/lib/python3.8/site-packages/iterm2/connection.py", line 412, in async_connect
22.10., 18:16:04,232:     return await coro(self)
22.10., 18:16:04,232:   File "/Users/walter/.config/iterm2/AppSupport/iterm2env/versions/3.8.6/lib/python3.8/site-packages/iterm2/connection.py", line 219, in async_main
22.10., 18:16:04,232:     result = await coro(connection)
22.10., 18:16:04,232:   File "/Users/walter/.config/iterm2/AppSupport/Scripts/AutoLaunch/walters_title.py", line 386, in main
22.10., 18:16:04,232:     await component.register(connection)
22.10., 18:16:04,232:   File "/Users/walter/.config/iterm2/AppSupport/Scripts/AutoLaunch/walters_title.py", line 253, in register
22.10., 18:16:04,232:     execute = iterm2.StatusBarRPC(self.execute)
22.10., 18:16:04,232:   File "/Users/walter/.config/iterm2/AppSupport/iterm2env/versions/3.8.6/lib/python3.8/site-packages/iterm2/registration.py", line 359, in StatusBarRPC
22.10., 18:16:04,232:     func.async_register = async_register
22.10., 18:16:04,232: AttributeError: 'method' object has no attribute 'async_register'
22.10., 18:16:04,242:
22.10., 18:16:04,242: Connection closed.
22.10., 18:16:04,268:
22.10., 18:16:04,268: ** Script exited with status 1 **

Servus,
Walter

George Nachman

unread,
Oct 28, 2021, 3:15:16 PM10/28/21
to iterm2-...@googlegroups.com
I think your best bet is to define a function that captures the connection in a closure and use composition instead of inheritance on StatusBarComponent. Here’s a working example:


import iterm2

class C:
  @staticmethod
  async def create(connection):
    instance = C()
    vl = "variable_length_demo"
    knobs = [iterm2.CheckboxKnob("Variable-Length Demo", False, vl)]
    instance._component = iterm2.StatusBarComponent(
        short_description="Status Bar Demo",
        detailed_description="Tests script-provided status bar components",
        knobs=knobs,
        exemplar="row x cols",
        update_cadence=None,
        identifier="com.iterm2.example.status-bar-demo")

    @iterm2.StatusBarRPC
    async def coro(knobs, rows=iterm2.Reference("rows"), cols=iterm2.Reference("columns")):
      value = await instance.coro(knobs, rows, cols)
      return value
    await instance._component.async_register(connection, coro)

    return instance

  def __init__(self):
    self._state = 1

  async def coro(self, knobs, rows, cols):
    self._state += 1
    return f'{self._state} {rows}x{cols}'

async def main(connection):
    # Define the configuration knobs:
    component = await C.create(connection)

iterm2.run_forever(main)

--
You received this message because you are subscribed to the Google Groups "iterm2-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to iterm2-discus...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages