# fastmux’s source


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## Setup

## Running tmux

Everything fastmux does happens by shelling out to the
[`tmux`](./core.html#tmux) binary, which is the one true API to a tmux
server. The foundation is a small runner that executes one tmux command
and either returns its stdout or raises.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L16"
target="_blank" style="float:right; font-size:smaller">source</a>

### TmuxError

``` python
def TmuxError(
    *args, **kwargs
):
```

*Raised when a tmux command fails or a target cannot be resolved.*

[`_tmux`](./core.html#_tmux) takes the command as separate arguments (no
shell involved), passes optional `input` on stdin (used later for
loading paste buffers), and strips the trailing newline tmux prints. Any
failure becomes a [`TmuxError`](./core.html#tmuxerror) carrying tmux’s
own message. `tmux -V` needs no running server, so it makes a good first
check:

``` python
ver = _tmux('-V')
ver
```

    'tmux 3.7b'

``` python
test_eq(ver.startswith('tmux'), True)
with expect_fail(TmuxError, contains='fastmux-empty'): _tmux('-L','fastmux-empty','ls')
```

## Querying tmux

tmux answers questions through [format
strings](https://man.openbsd.org/tmux#FORMATS): every `list-*` and
`display-message` command takes `-F` with `#{variable}` placeholders. So
fastmux describes each concept once, as a mapping of field name to
format variable, and parses the tab-separated answer into a typed
`dict`.

Sessions, windows, and panes all have server-unique ids (`$1`, `@1`,
`%1`) which never change or get reused. Names and indexes are ambiguous
as `-t` targets (a session auto-named `5` can resolve as a window
index!), so fastmux keys every object on its id and only ever targets
ids internally.

A field spec is used twice: [`_fmt`](./core.html#_fmt) renders it as the
`-F` argument, and [`_parse`](./core.html#_parse) types the reply. The
pane title comes last since it is the one field that may itself contain
tabs. `scroll_position` is blank except in copy-mode, so `scroll_pos`
types as int-or-None rather than int. Some tmux versions report an
unknown `-t` target as success with every field empty rather than as an
error, so [`_get`](./core.html#_get) turns that reply into the
[`TmuxError`](./core.html#tmuxerror) it should have been.

``` python
test_eq(_fmt(dict(id='pane_id', w='pane_width')), '#{pane_id}\t#{pane_width}')
test_eq(_parse(dict(id='pane_id', width='pane_width', active='pane_active'), '%3\t80\t1'),
        dict(id='%3', width=80, active=True))
```

## Sessions

A *session* is tmux’s top-level unit: a named group of windows with its
own lifecycle, which clients attach to. All fastmux objects follow the
same pattern: an `AttrDict` holding the fields queried from tmux, a
`fetch` classmethod that builds one from any target, and `refresh` to
re-query in place. The repr matches what `tmux ls` prints.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L63"
target="_blank" style="float:right; font-size:smaller">source</a>

### new_session

``` python
def new_session(
    cmd:NoneType=None, # Command to run (str or argv list); default: the user's shell
    name:NoneType=None, # Session name; default: tmux auto-numbers
    width:NoneType=None, # Terminal width in columns
    height:NoneType=None, # Terminal height in rows
    cwd:NoneType=None, # Working directory
    env:NoneType=None, # Extra environment vars as a dict
    remain:bool=False, # Keep dead panes around (inspectable, with exit status) instead of destroying them
):
```

*Start a detached tmux session and return its
[`Session`](./core.html#session)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L53"
target="_blank" style="float:right; font-size:smaller">source</a>

### Session

``` python
def Session(
    *args, **kwargs
):
```

*A tmux session; fields from `_sess_f`, keyed on the immutable session
id*

Sessions are created detached (`-d`), so making one never steals the
terminal you are working in. We’ll create one now running a small script
that prints numbered lines then waits. It will be the playground for the
rest of this notebook.

``` python
printer = [sys.executable,'-u','-c','import time\n'
                                    'for i in range(30): print(f"ln {i}")\n'
                                    'time.sleep(600)']
s = new_session(printer, width=80, height=10)
s
```

<div class="prose" data-markdown="1">

``` python
0: 1 windows
```

</div>

``` python
assert s.id.startswith('$') and s.n_wins==1
test_eq(s.refresh().id, s.id)
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L98"
target="_blank" style="float:right; font-size:smaller">source</a>

### Session.attach_command

``` python
def attach_command():
```

*Shell command that attaches a terminal to this session*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L92"
target="_blank" style="float:right; font-size:smaller">source</a>

### Session.rename

``` python
def rename(
    name
):
```

*Rename this session, returning it*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L86"
target="_blank" style="float:right; font-size:smaller">source</a>

### Session.kill

``` python
def kill():
```

*Kill this session and everything in it*

Destructive operations raise a `sys.audit` event, so audit-hook based
sandboxes can gate them. In-place operations return `self` for chaining.

``` python
test_eq(s.rename('fastmux-demo').name, 'fastmux-demo')
test_eq(s.attach_command, 'tmux attach -t fastmux-demo')
```

## Panes

A *pane* is one terminal: a pty running a command, with a visible screen
and a scrollback transcript. Everything you can read or type happens in
a pane. Because a pane knows its geometry and cursor, we can treat its
whole transcript (history plus visible screen) as a list of lines,
addressed absolutely from line 0.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L103"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane

``` python
def Pane(
    *args, **kwargs
):
```

*A tmux pane; fields from `_pane_f`, keyed on the immutable pane id*

``` python
p = Pane.fetch(s.id)
test_eq((p.width, p.height), (80, 10))
assert p.id.startswith('%') and p.running and p.exit_code is None
with expect_fail(TmuxError, contains="can't find"): Pane.fetch('%9999')
p.target
```

    'fastmux-demo:1.1'

`target` is the pane’s address in standard tmux syntax
(`session:window.pane`), so it can be pasted into any `tmux -t` command.

`in_mode` and `scroll_pos` report copy-mode state — the one thing
`capture` and [`screen`](./bg.html#screen) cannot show, since tmux
always captures the underlying pane, never the copy-mode overlay. A
driver that sends a wheel gesture asserts arrival with `in_mode`, and
reads how far up the view sits with `scroll_pos` (lines above the live
position; `None` outside copy-mode). For queries not worth a curated
field, `fmt` evaluates any tmux format string on the pane.

``` python
assert not p.in_mode and p.scroll_pos is None
_tmux('copy-mode','-t',p.id)
assert p.refresh().in_mode and p.scroll_pos == 0
_tmux('send-keys','-t',p.id,'-X','scroll-up')
test_eq(p.refresh().scroll_pos, 1)
_tmux('send-keys','-t',p.id,'-X','cancel')
assert not p.refresh().in_mode
test_eq(p.fmt('#{pane_id}'), p.id)
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L136"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.capture

``` python
def capture(
    start:int=0, end:NoneType=None, ansi:bool=False
):
```

*A [`Capture`](./core.html#capture) of transcript lines `start:end`
(absolute; `end=None` means up to the cursor)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L127"
target="_blank" style="float:right; font-size:smaller">source</a>

### Capture

``` python
def Capture(
    *args, **kwargs
):
```

*Captured transcript lines plus their absolute range and source pane*

tmux numbers capture lines relative to the top of the visible screen
(scrollback is negative), so `capture` converts from absolute transcript
lines. The [`Capture`](./core.html#capture) repr is the text itself with
a source footer:

``` python
time.sleep(0.5)
c = p.capture(5, 8)
c
```

<div class="prose" data-markdown="1">

``` python
ln 5
ln 6
ln 7
── fastmux-demo:1.1 %0 · lines 5-8 of 30
```

</div>

``` python
test_eq(c.lines, ('ln 5','ln 6','ln 7'))
test_eq((c.start, c.end, c.n), (5, 8, 30))
test_eq(p.capture(29, 99).lines, ('ln 29',))  # ranges are clamped
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L160"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.\_\_repr\_\_

``` python
def __repr__():
```

*Return repr(self).*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L155"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.ansi

``` python
def ansi():
```

*The visible screen with ANSI escape sequences*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L150"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.text

``` python
def text():
```

*The visible screen as plain text*

A pane’s repr is its current screen, so displaying one is like glancing
at that terminal:

``` python
p
```

<div class="prose" data-markdown="1">

``` python
ln 21
ln 22
ln 23
ln 24
ln 25
ln 26
ln 27
ln 28
ln 29
```

</div>

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L187"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.display

``` python
def display(
    lines:int=80, ansi:bool=False
):
```

*Capture the last `lines` transcript lines, recording the pane’s
last-seen state*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L172"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.\_\_getitem\_\_

``` python
def __getitem__(
    i
):
```

*Transcript line `i` as a str, a [`Capture`](./core.html#capture) for a
slice; str keys keep normal dict access*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L169"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.\_\_len\_\_

``` python
def __len__():
```

*Return len(self).*

Indexing and slicing read the transcript like a list of lines, in the
same coordinates tmux itself uses, so `p[-200:]` is the last 200 lines
including scrollback. Since [`Pane`](./core.html#pane) is a dict
underneath, string keys still do dict lookup (`p['id']`), while ints and
slices read the transcript.

``` python
test_eq(len(p), 30)
test_eq((p[0], p[-1]), ('ln 0', 'ln 29'))
test_eq(p[5:7].lines, ('ln 5','ln 6'))
test_eq(p[-3:].lines, ('ln 27','ln 28','ln 29'))
test_eq(p['id'], p.id)
with expect_fail(IndexError): p[99]
```

### The visible screen

`capture` and [`display`](./bg.html#display) read the *transcript*,
which ends at the cursor — right for shell work, wrong for full-screen
apps, which park the cursor mid-screen and paint below it (completion
menus, status rows). [`screen`](./bg.html#screen) captures the visible
viewport regardless of cursor position, with geometry and cursor in its
footer, and the cursor cell overlaid as ▮ in the repr (the
`text`/`lines` fields stay unmarked). `styles=True` keeps the SGR text
attributes that carry UI meaning — dim ghost text, reversed selections —
while stripping color codes, which in a syntax-highlighting app
outnumber everything else.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L232"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.screen

``` python
def screen(
    styles:bool=False
):
```

*A [`Screen`](./core.html#screen) of the visible viewport (rows below
the cursor included), recording the pane’s last-seen state*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L220"
target="_blank" style="float:right; font-size:smaller">source</a>

### Screen

``` python
def Screen(
    *args, **kwargs
):
```

*The visible screen plus geometry, cursor position, and source pane*

``` python
tui = new_session([sys.executable,'-u','-c',
       "import time\nprint('\x1b[32mdoc\x1b[0m')\nprint('\x1b[2mmenu\x1b[0m\x1b[F', end='', flush=True)\ntime.sleep(60)"],
       width=30, height=6)
time.sleep(0.5)
tp = Pane.fetch(tui.id)
assert 'menu' not in tp.display().text  # the transcript stops at the cursor: the menu row is below it
tp.screen(styles=True)
```

``` python
sc = tp.screen(styles=True)
test_eq((sc.lines, sc.cx, sc.cy), (('doc', '\x1b[2mmenu\x1b[0m'), 0, 0))  # color stripped, attrs kept, cursor parked up
test_eq(_quiet('\x1b[1;32mdoc\x1b[0m \x1b[38;5;208mx\x1b[39m'), '\x1b[1mdoc\x1b[0m x')
tui.kill()
```

### Sending input

fastmux writes to a pane two ways: [`send`](./bg.html#send) pastes
literal text through a tmux buffer (no key-name interpretation, so any
characters are safe), and [`send_keys`](./bg.html#send_keys) sends [tmux
key names](https://man.openbsd.org/tmux#KEY_BINDINGS) like `Enter` or
`C-c`. Both then [`poll`](./bg.html#poll), waiting up to `wait_ms` for
the pane to differ from its last-seen state and returning the latest
[`Capture`](./core.html#capture), so the common send-then-read round
trip is one call.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L250"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.poll

``` python
def poll(
    wait_ms:int=0, interval_ms:int=50, lines:int=80, screen:bool=False, styles:bool=False, until:NoneType=None,
    settle_ms:int=0
):
```

*Wait up to `wait_ms` for the pane to differ from its last-seen state
(returning at once if it* already does) – or, with `until`, for the
capture to match that regex instead. With `settle_ms`, then keep
sampling until the pane has stopped changing for that long (running at
most `settle_ms` past the deadline). On timeout the latest capture is
returned regardless. Capture is the last `lines` of transcript, or the
viewport with `screen=True`.

Let’s drive a stdin-echo process end to end, sending text and getting
the acknowledgement back in the same call:

``` python
echoer = new_session([sys.executable,'-u','-c',
                      "import sys\n"
                      "print('ready')\n"
                      "for l in sys.stdin: print(f'ACK:{l.rstrip()}')"],
                      width=60, height=8, remain=True)
ep = Pane.fetch(echoer.id)
ep.poll(wait_ms=1500, lines=3)
```

<div class="prose" data-markdown="1">

``` python
ready
── 1:1.1 %1 · lines 0-1 of 1
```

</div>

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L299"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.wait

``` python
def wait(
    timeout_ms:NoneType=None, interval_ms:int=50
):
```

*Wait for this pane’s command to exit, returning its status (`None` on
timeout)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L293"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.interrupt

``` python
def interrupt(
    wait_ms:int=0, interval_ms:int=50, lines:int=80, screen:bool=False, styles:bool=False, until:NoneType=None,
    settle_ms:int=0
):
```

*Send `Ctrl-C` to this pane, then [`poll`](./bg.html#poll)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L287"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.send_keys

``` python
def send_keys(
    *keys, wait_ms:int=0, interval_ms:int=50, lines:int=80, screen:bool=False, styles:bool=False,
    until:NoneType=None, settle_ms:int=0
):
```

*Send tmux key names to this pane, then [`poll`](./bg.html#poll)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L274"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.send

``` python
def send(
    chars:str='', wait_ms:int=0, interval_ms:int=50, lines:int=80, screen:bool=False, styles:bool=False,
    until:NoneType=None, settle_ms:int=0
):
```

*Paste `chars` into this pane literally, then [`poll`](./bg.html#poll)*

``` python
r = ep.send('hello\n', wait_ms=1500, lines=3)
assert 'ACK:hello' in r.text
test_eq(ep.wait(timeout_ms=1), None)  # still running
```

When the process exits, the pane dies but (with `remain-on-exit` set)
stays inspectable, and `wait` returns the exit status:

``` python
ep.send_keys('C-d')  # EOF ends the loop
test_eq(ep.wait(timeout_ms=3000), 0)
echoer.kill()
```

[`poll`](./bg.html#poll) doesn’t just wait for the pane to change after
the call: it waits for the pane to differ from the *last state a capture
showed you*. Each [`display`](./bg.html#display) (and so each
[`send`](./bg.html#send), [`send_keys`](./bg.html#send_keys),
[`interrupt`](./bg.html#interrupt), and [`poll`](./bg.html#poll), which
all return one) records the pane’s last-seen state, and
[`poll`](./bg.html#poll) returns as soon as the pane differs from it. So
output that arrived while you weren’t watching satisfies the next
[`poll`](./bg.html#poll) immediately, rather than making it wait for yet
another change:

``` python
late = new_session([sys.executable,'-u','-c','import time\n'
                                          'print("early")\n'
                                          'time.sleep(2)\n'
                                          'print("late")\n'
                                          'time.sleep(600)'], width=60, height=8)
lp = Pane.fetch(late.id)
c = lp.poll(wait_ms=500)
assert 'late' not in c.text
c
```

``` python
time.sleep(2.5)  # "late" arrives while we aren't watching
t0 = time.monotonic(); c = lp.poll(wait_ms=5000); elapsed = time.monotonic()-t0
late.kill()
assert 'late' in c.text
assert elapsed < 1
```

Two refinements cover the patterns that plain change-detection gets
wrong. `until=` waits for the capture to *match a regex* rather than
merely differ — the way to wait out a program’s startup before sending
it input. `settle_ms=` waits for the pane to *stop* changing:
[`poll`](./bg.html#poll) returns at the first observed change, so a
capture during a burst of output shows a half-painted frame; settling
returns the frame the burst ended on.

``` python
staged = new_session([sys.executable,'-u','-c','import time\n'
                     'time.sleep(1)\n'
                     'print("READY")\n'
                     'for i in range(5): print(f"burst {i}"); time.sleep(0.15)\n'
                     'time.sleep(600)'], width=60, height=12)
sp = Pane.fetch(staged.id)
t0 = time.monotonic(); c = sp.poll(wait_ms=5000, until='READY'); dt = time.monotonic()-t0
assert 'READY' in c.text and dt < 3          # returned on the match, not the deadline
c = sp.poll(wait_ms=5000, settle_ms=500)     # the burst is mid-flight now
staged.kill()
assert 'burst 4' in c.text                   # settled past the whole burst, not the first change
```

### Mouse input

Terminal apps that enable SGR mouse reporting receive clicks as escape
sequences on stdin, so a synthetic click is just bytes — but composing
them by hand means counting screen rows and remembering that a real
click is a press *and* a release (apps see the release; sending only the
press masks bugs). [`click`](./bg.html#click) does both halves
correctly, and can find its own coordinates: pass a 1-based `(col,row)`,
or a string (literal) or compiled regex located on the current screen.
[`wheel`](./bg.html#wheel) sends scroll events (SGR buttons 64/65).

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L326"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.wheel

``` python
def wheel(
    n:int=3, at:tuple=(1, 1), **poll_kw
):
```

*Send `n` SGR wheel events (up for positive `n`, down for negative),
then [`poll`](./bg.html#poll) with `poll_kw`*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L310"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.click

``` python
def click(
    target, btn:int=0, **poll_kw
):
```

*Send an SGR mouse click (press+release) to this pane’s application,
then [`poll`](./bg.html#poll) with `poll_kw`.* `target` is a 1-based
`(col,row)`, or a str (literal) or compiled regex located on the current
screen – the click lands on the bottom-most match’s first cell (screens
grow downward: the newest occurrence is the live one). Raises
`ValueError` if it is not on screen.

``` python
peek = new_session([sys.executable,'-u','-c',
                    "import sys, tty; tty.setcbreak(0)\n"
                    "print('alpha [beta] gamma')\n"
                    "while True: print(repr(sys.stdin.buffer.read1(64)))"],
                   width=60, height=12, remain=True)
pk = Pane.fetch(peek.id)
r = pk.click((5, 2), wait_ms=2000)
assert r"\x1b[<0;5;2M\x1b[<0;5;2m" in r.text        # press and release, at the given cell
r = pk.click('[beta]', wait_ms=2000)                # a literal str: regex chars are no trap
assert r"\x1b[<0;7;1M" in r.text                    # found on row 1, col 7
r = pk.click(re.compile(r'gam+a'), wait_ms=2000)
assert r"\x1b[<0;14;1M" in r.text
r = pk.wheel(2, wait_ms=2000)
assert r.text.count(r"\x1b[<64;1;1M") >= 1 and '65;' not in r.text
with expect_fail(ValueError, contains='not on screen'): pk.click('nope')
peek.kill()
```

### Listing panes

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L348"
target="_blank" style="float:right; font-size:smaller">source</a>

### Session.pane

``` python
def pane():
```

*This session’s active pane*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L343"
target="_blank" style="float:right; font-size:smaller">source</a>

### Session.panes

``` python
def panes():
```

*All panes in this session*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L337"
target="_blank" style="float:right; font-size:smaller">source</a>

### Panes

``` python
def Panes(
    items:NoneType=None, *rest, use_list:bool=False, match:NoneType=None
):
```

*Panes shown as `tmux list-panes`-style summary lines*

A [`Panes`](./core.html#panes) collection reprs as summary lines rather
than screens (a dozen full screens would be unreadable). The line format
mirrors `tmux list-panes`, with the pane title appended when it carries
information (tmux defaults it to the hostname, which doesn’t):

``` python
s.panes
```

    1.1: [80x10] %0 python (active)

``` python
test_eq(len(s.panes), 1)
test_eq(s.pane.id, p.id)
```

## Windows

A *window* groups panes into one screenful, like tabs in a terminal
emulator. Its repr is its `tmux list-windows`-style summary line
followed by its panes.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L378"
target="_blank" style="float:right; font-size:smaller">source</a>

### Session.new_window

``` python
def new_window(
    cmd:NoneType=None, # Command to run (str or argv list); default: the user's shell
    name:NoneType=None, # Window name; default: tmux auto-names from the command
    cwd:NoneType=None, # Working directory
    env:NoneType=None, # Extra environment vars as a dict
    remain:bool=False, # Keep dead panes around instead of destroying them
    focus:bool=False, # Make it the session's current window
):
```

*Create a window in this session, returning the new
[`Window`](./core.html#window)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L373"
target="_blank" style="float:right; font-size:smaller">source</a>

### Session.windows

``` python
def windows():
```

*This session’s windows*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L367"
target="_blank" style="float:right; font-size:smaller">source</a>

### Windows

``` python
def Windows(
    items:NoneType=None, *rest, use_list:bool=False, match:NoneType=None
):
```

*Windows shown with their pane listings*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L353"
target="_blank" style="float:right; font-size:smaller">source</a>

### Window

``` python
def Window(
    *args, **kwargs
):
```

*A tmux window; fields from `_win_f`, keyed on the immutable window id*

Like sessions, windows are created without stealing focus unless you
pass `focus=True`, since a script composing a layout shouldn’t yank the
user’s cursor around. (`remain` is a per-window setting; panes made by
splitting inherit it from their window.)

``` python
w = s.new_window(printer, name='work')
test_eq((w.name, w.n_panes), ('work', 1))
test_eq(s.refresh().n_wins, 2)
assert not w.active  # focus stayed put
w
```

<div class="prose" data-markdown="1">

``` python
2: work (1 panes) @2
  2.1: [80x10] %2 tmux (active)
```

</div>

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L427"
target="_blank" style="float:right; font-size:smaller">source</a>

### Window.layout

``` python
def layout(
    name
):
```

*Apply a tmux layout (`even-horizontal`, `tiled`, …), returning this
window*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L421"
target="_blank" style="float:right; font-size:smaller">source</a>

### Window.select

``` python
def select():
```

*Make this the session’s current window, returning it*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L415"
target="_blank" style="float:right; font-size:smaller">source</a>

### Window.rename

``` python
def rename(
    name
):
```

*Rename this window, returning it*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L406"
target="_blank" style="float:right; font-size:smaller">source</a>

### Window.resize

``` python
def resize(
    width:NoneType=None, height:NoneType=None
):
```

*Resize this window (and so its panes) to absolute `width`/`height`,
returning it (tmux \>= 2.9)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L400"
target="_blank" style="float:right; font-size:smaller">source</a>

### Window.kill

``` python
def kill():
```

*Kill this window and its panes*

``` python
test_eq(w.rename('jobs').name, 'jobs')
assert w.select().active
```

[`Window.resize`](./core.html#window.resize) is the resize that works in
a detached single-pane session. [`Pane.resize`](./core.html#pane.resize)
is clamped by the window geometry there, so it silently no-ops; resizing
the window reaches the pane and delivers its SIGWINCH, which is what you
want when driving a full-screen app headlessly and testing its resize
behavior. Panes also carry their process `pid`, the handle for
inspecting a stuck session’s process from outside.

``` python
ow, oh = w.panes[0].width, w.panes[0].height
test_eq(w.resize(70, 20).panes[0].width, 70)
assert w.panes[0].pid > 0
w.resize(ow, oh)
```

## Splitting panes

Splitting divides a pane in two, and is how layouts get built. tmux’s
own flags are famously backwards (`-h` puts panes side by side; `-v`
stacks them), so fastmux speaks in directions instead: where the *new*
pane goes, `right`/`below`/`left`/`above`, with a one-word verb for
each. Like window creation, splits never steal focus unless asked.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L465"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.asplit

``` python
def asplit(
    cmd:NoneType=None, # Command to run (str or argv list); default: the user's shell
    size:NoneType=None, # Size of the new pane: rows/columns (int) or `'30%'`
    cwd:NoneType=None, # Working directory
    env:NoneType=None, # Extra environment vars as a dict
    focus:bool=False, # Make the new pane active
):
```

*Split this pane, returning the new [`Pane`](./core.html#pane)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L462"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.lsplit

``` python
def lsplit(
    cmd:NoneType=None, # Command to run (str or argv list); default: the user's shell
    size:NoneType=None, # Size of the new pane: rows/columns (int) or `'30%'`
    cwd:NoneType=None, # Working directory
    env:NoneType=None, # Extra environment vars as a dict
    focus:bool=False, # Make the new pane active
):
```

*Split this pane, returning the new [`Pane`](./core.html#pane)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L459"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.bsplit

``` python
def bsplit(
    cmd:NoneType=None, # Command to run (str or argv list); default: the user's shell
    size:NoneType=None, # Size of the new pane: rows/columns (int) or `'30%'`
    cwd:NoneType=None, # Working directory
    env:NoneType=None, # Extra environment vars as a dict
    focus:bool=False, # Make the new pane active
):
```

*Split this pane, returning the new [`Pane`](./core.html#pane)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L456"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.rsplit

``` python
def rsplit(
    cmd:NoneType=None, # Command to run (str or argv list); default: the user's shell
    size:NoneType=None, # Size of the new pane: rows/columns (int) or `'30%'`
    cwd:NoneType=None, # Working directory
    env:NoneType=None, # Extra environment vars as a dict
    focus:bool=False, # Make the new pane active
):
```

*Split this pane, returning the new [`Pane`](./core.html#pane)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L436"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.split

``` python
def split(
    where:str='right', # Where the new pane goes: `right`, `below`, `left`, or `above`
    cmd:NoneType=None, # Command to run (str or argv list); default: the user's shell
    size:NoneType=None, # Size of the new pane: rows/columns (int) or `'30%'`
    cwd:NoneType=None, # Working directory
    env:NoneType=None, # Extra environment vars as a dict
    focus:bool=False, # Make the new pane active
):
```

*Split this pane, returning the new [`Pane`](./core.html#pane)*

``` python
right = p.rsplit()
above = p.asplit(size=3)
test_eq(len(Window.fetch(p.window_id).panes), 3)
assert above.height==3 and right.id != p.id
assert Pane.fetch(p.window_id).id == p.id  # focus in this window untouched
Window.fetch(p.window_id)
```

<div class="prose" data-markdown="1">

``` python
1: nbs (3 panes) @0
  1.1: [40x3] %4 bash
  1.2: [40x6] %0 python (active)
  1.3: [39x10] %3 bash
```

</div>

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L490"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.select

``` python
def select():
```

*Make this the active pane, returning it*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L484"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.zoom

``` python
def zoom():
```

*Toggle this pane fullscreen within its window, returning it*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L475"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.resize

``` python
def resize(
    width:NoneType=None, height:NoneType=None
):
```

*Resize this pane to absolute `width`/`height`, returning it*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L469"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.kill

``` python
def kill():
```

*Kill this pane (killing its window/session if it is the last one)*

``` python
test_eq(above.resize(height=5).height, 5)
assert right.select().active
above.kill(); right.kill()
test_eq(len(Window.fetch(p.window_id).panes), 1)
```

## Searching

Any scope can be grepped: one pane’s transcript, every pane in a window
or session, or every pane on the server. Matches display rg-style, and
each match’s `target` is a real tmux target, so you can paste it back
into [`tmux()`](./core.html#tmux) to get the pane that said it.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L531"
target="_blank" style="float:right; font-size:smaller">source</a>

### Session.search

``` python
def search(
    pattern, lines:int=2000, regex:bool=False, ignore_case:bool=True
):
```

*Search every pane in this session, rg-style*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L526"
target="_blank" style="float:right; font-size:smaller">source</a>

### Window.search

``` python
def search(
    pattern, lines:int=2000, regex:bool=False, ignore_case:bool=True
):
```

*Search every pane in this window, rg-style*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L521"
target="_blank" style="float:right; font-size:smaller">source</a>

### Pane.search

``` python
def search(
    pattern, lines:int=2000, regex:bool=False, ignore_case:bool=True
):
```

*Search this pane’s recent transcript, rg-style*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L503"
target="_blank" style="float:right; font-size:smaller">source</a>

### SearchResults

``` python
def SearchResults(
    items:NoneType=None, *rest, use_list:bool=False, match:NoneType=None
):
```

*Search matches, one rg-style row per line*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L498"
target="_blank" style="float:right; font-size:smaller">source</a>

### SearchMatch

``` python
def SearchMatch(
    *args, **kwargs
):
```

*One matching transcript line, shown rg-style*

``` python
hits = p.search('ln 2', lines=100)
test_eq(hits[0].line, 'ln 2')
test_eq(len(hits), 11)  # ln 2 and ln 20..29
hits
```

    fastmux-demo:1.1:2: ln 2
    fastmux-demo:1.1:20: ln 20
    fastmux-demo:1.1:21: ln 21
    fastmux-demo:1.1:22: ln 22
    fastmux-demo:1.1:23: ln 23
    fastmux-demo:1.1:24: ln 24
    fastmux-demo:1.1:25: ln 25
    fastmux-demo:1.1:26: ln 26
    fastmux-demo:1.1:27: ln 27
    fastmux-demo:1.1:28: ln 28
    fastmux-demo:1.1:29: ln 29

``` python
m = hits[0]
test_eq(m.target, p.target)
test_eq(p[m.line_no], m.line)  # line numbers are absolute transcript coordinates
```

## tmux()

[`tmux()`](./core.html#tmux) with no arguments returns every session as
an indented session/window/pane tree, empty if no server is running at
all. With a target it returns the right live handle: a session name or
`$id` gives a [`Session`](./core.html#session), `sess:win` or `@id` a
[`Window`](./core.html#window), `sess:win.pane` or `%id` a
[`Pane`](./core.html#pane).

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L554"
target="_blank" style="float:right; font-size:smaller">source</a>

### tmux

``` python
def tmux(
    target:NoneType=None
):
```

*All sessions as a tree, or a live handle for `target` (session name,
`sess:win`, `sess:win.pane`, or a `$`/`@`/`%` id)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L543"
target="_blank" style="float:right; font-size:smaller">source</a>

### Sessions

``` python
def Sessions(
    items:NoneType=None, *rest, use_list:bool=False, match:NoneType=None
):
```

*Sessions shown as an indented session/window/pane tree*

``` python
tmux()
```

    fastmux-demo: 2 windows
      1: nbs (1 panes) @0
        1.1: [80x10] %0 python (active)
      2: jobs* (1 panes) @2
        2.1: [80x10] %2 python (active)

``` python
test_eq(tmux(s.name).id, s.id)
test_eq(tmux(s.id).name, s.name)
test_eq(tmux(p.id).target, p.target)
test_eq(tmux(p.target).id, p.id)          # target strings round-trip
test_eq(tmux(p.window_id).idx, p.win)
assert any(o.id==s.id for o in tmux())
with expect_fail(TmuxError): tmux('no-such-session')
```

Search for a line, then jump to the pane that printed it:

``` python
hit = tmux().search('ln 29')[0]
test_eq(tmux(hit.target).id, hit.id)
```

One more query: [`current_pane`](./core.html#current_pane) answers
“which pane is *this*?”. Inside tmux it is the caller’s own pane;
outside, tmux resolves it to the active pane of the most recently used
session. `fastmux.bg` uses it to give `sid=None` its meaning.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastmux/blob/main/fastmux/core.py#L565"
target="_blank" style="float:right; font-size:smaller">source</a>

### current_pane

``` python
def current_pane():
```

*The pane tmux considers current: the caller’s own inside tmux, else the
active pane of the most recent session*

``` python
assert current_pane().id.startswith('%')
```

Finally, clean up the playground:

``` python
s.kill()
with expect_fail(TmuxError, contains="can't find"): tmux('fastmux-demo')
```
