libtmux 0.1.0-alpha.16

Async typed tmux client and object model (alpha)
Documentation
# Migrating between alphas

Each section is one release: what a caller has to change to build against it.
Start at the section for the version you are on and work up. Changes that
need no edit on your side are in the changelog, not here.

## From 0.1.0-alpha.12 to 0.1.0-alpha.13

### `ServerBuilder::colors` refuses 88

No supported tmux release accepts `-8`, so `colors(88)` makes `build` fail
with `ServerConfigurationErrorKind::InvalidColorMode` instead of starting tmux
with it. Ask for 256 colors, which tmux spells `-2`, or leave the override
out:

```
// was: libtmux::Server::builder().colors(88).build()?
let server = libtmux::Server::builder().colors(256).build()?;
assert_eq!(server.colors(), Some(256));
# Ok::<(), libtmux::Error>(())
```

### A classic layout string is checked before tmux sees it

`Window::select_layout` refuses a classic layout string whose checksum does
not match or whose tree is empty, and `Server::validate_layouts` answers the
same question before anything changes. tmux 3.3 and 3.3a exit the whole daemon
on a layout string they cannot parse, so the refusal now comes from libtmux
rather than from a lost server. A layout copied from `#{window_layout}` is
unaffected.

## From 0.1.0-alpha.11 to 0.1.0-alpha.12

### Timestamps are `SystemTime`

`Session::created`, `Session::last_attached`, `Window::last_activity`,
`Client::created` and `ServerGeneration::start_time` return
`std::time::SystemTime` in place of an `i64` of Unix seconds:

```no_run
# fn age(session: &libtmux::Session) -> Result<(), std::time::SystemTimeError> {
use std::time::{SystemTime, UNIX_EPOCH};

// was: let created: i64 = session.created();
let created: SystemTime = session.created();
let age = SystemTime::now().duration_since(created)?;

// The seconds are one conversion away.
let seconds = created.duration_since(UNIX_EPOCH)?.as_secs();
# let _ = (age, seconds);
# Ok(())
# }
```

The field handles are unchanged: `session.get(fields.session_created)` and a
filter on it still see the `i64` tmux reports.

### Buffer names are `TmuxText`

`Server::buffer_names` returns `Vec<TmuxText>` in place of `Vec<String>`.
`Server::buffer` and `Server::delete_buffer` take `impl AsRef<[u8]>`, so a
listed name passes back as it is, and a `&str` needs no change:

```no_run
# async fn buffers(server: &libtmux::Server) -> Result<(), libtmux::Error> {
for name in server.buffer_names().await? {
    // was: `name` was a `String`; `to_string_lossy` is the old reading.
    println!("{}", name.to_string_lossy());
    server.delete_buffer(&name).await?;
}
# Ok(())
# }
```

### `respawn` and `display_menu` take types, not literals

```no_run
# async fn respawn(pane: &mut libtmux::Pane) -> Result<(), libtmux::Error> {
// was: pane.respawn(Some("sh"), true)
pane.respawn(Some("sh"), libtmux::Respawn::Replacing).await?;
# Ok(())
# }
```

`Respawn::OnlyIfDead` is the `false` case, and it is the one worth checking
for: tmux refuses the respawn while the old command is alive.

`Server::display_menu` takes `MenuItem::new(label, key, command)` in place of
a `(String, String, String)` triple.

### ID filter handles carry their ID type

`PaneFields::pane_id`, `WindowFields::window_id` and
`SessionFields::session_id` are `TextField<Target, PaneId>`,
`TextField<Target, WindowId>` and `TextField<Target, SessionId>`. Filtering
through them is unchanged. Only code that spells the handle's type changes:

```
use libtmux::query::{Filterable as _, TextField};
use libtmux::{Pane, PaneId};

// was: let handle: TextField<Pane> = Pane::filter_fields().pane_id;
let handle: TextField<Pane, PaneId> = Pane::filter_fields().pane_id;
let _ = handle.eq("%1");
```

### One option reader: `typed_option`

`get_option` on `Server`, `Session`, `Window` and `Pane`, and
`Server::{get_global_option, get_global_window_option}`, are gone. Read with
`typed_option`, `Server::typed_global_option` or
`Server::typed_global_window_option`. A flag now arrives as a `bool` and a
number as an `i64`; where the bytes are wanted, `TmuxText::from` gives back
exactly what the removed reader returned:

```no_run
# async fn read(session: &libtmux::Session) -> Result<(), libtmux::Error> {
use libtmux::TmuxText;

// was: session.get_option("status-left").await?
let left = session.typed_option("status-left").await?.map(TmuxText::from);
# let _ = left;
# Ok(())
# }
```

`set_typed_option` writes the same types back, checked against tmux's option
table; `set_option` is unchanged.

### The `_or_empty` listing twins are gone

Replace `x_or_empty().await` with `x().await.unwrap_or_default()`:

```no_run
# async fn listing(server: &libtmux::Server) -> Result<(), libtmux::Error> {
let sessions = server.sessions().await.unwrap_or_default();
# let _ = sessions;
# Ok(())
# }
```

Worth a moment's thought rather than a blind rewrite: the twins collapsed an
unreachable tmux into an empty list, and anything that reconciles state should
take the `?` instead.

### Names, titles and start directories are text

Every argument tmux expands as a format now takes `TmuxArg`, and every
conversion into it escapes. `&str`, `String`, `OsString`, `Path` and
`&TmuxText` all convert, so ordinary calls are unchanged:

```no_run
# async fn names(server: &libtmux::Server) -> Result<(), libtmux::Error> {
// Unchanged, and now stored as written rather than expanded.
server.new_session("release#1").await?;
# Ok(())
# }
```

Two changes to make:

- Drop any `escape_format` you applied before calling one of these. The sink
  escapes now, so passing pre-escaped text stores the escape characters.
- Where you meant a format, say so with `TmuxArg::format`. This is the one way
  to reach `#{pane_current_path}` in a start directory, and it belongs only
  around a template the program itself wrote.

```no_run
# async fn split(pane: &libtmux::Pane) -> Result<(), libtmux::Error> {
use libtmux::{SplitDirection, SplitOptions, TmuxArg};

pane.split(
    SplitOptions::new(SplitDirection::Below)
        .start_directory(TmuxArg::format("#{pane_current_path}")),
)
.await?;
# Ok(())
# }
```

The sinks: `NewSessionOptions::{new, window_name, start_directory}`,
`NewWindowOptions::{new, start_directory}`, `SplitOptions::start_directory`,
`Session::rename`, `Window::rename`, `Pane::set_title`. Option names and the
`plan` operations escape internally and need no change at the call site.

### Control events

`ControlEvents` yields `Result<Event, Error>`. `ControlEvents::next_event`
and `ControlMode::next_event` return `Option<Result<Event, Error>>`. Change
`match event` to `match event?` in a fallible consumer:

```no_run
# async fn watch(mut events: libtmux::control::ControlEvents) -> Result<(), libtmux::Error> {
use libtmux::control::Event;

while let Some(event) = events.next_event().await {
    match event? {
        Event::Output { pane, bytes } => println!("{pane}: {} bytes", bytes.len()),
        Event::Exit { .. } => break,
        _ => {}
    }
}
events.shutdown().await?;
# Ok(())
# }
```

A terminal failure arrives once, after buffered notifications. Later polls
return `None`. Normal `%exit` produces `Event::Exit`; EOF without it produces
`ControlModeErrorKind::Closed`. Exhaustion waits for cleanup. Explicit
`shutdown` discards unread notifications, closes the connection and returns
any terminal error not already delivered by iteration. After an error has
been delivered, `shutdown` succeeds. `Server::shutdown` may discard pending
notifications so an unread stream cannot block executor shutdown.

`PaneOutput` keeps its infallible byte-stream contract. Its `None` combines
normal completion and failure; call `shutdown` to observe a connection error.

### Scoped errors

`with_session`, `with_window` and `with_pane` return
`Result<T, ScopeError<T, E>>` instead of `Result<T, E>`. Remove
`E: From<libtmux::Error>` from caller error types when it was needed only for
these helpers. Functions propagating a scope's result can return
`ScopeError<T, E>` or wrap it in their application error. Nested scopes
retain nested error types.

Match `ScopeError::Creation`, `Operation`, `Cleanup` or
`OperationAndCleanup`. The combined variant retains both original values, and
`Cleanup` retains the operation's own successful result, since cleanup
failing is the only way that result would otherwise be lost:

```
use libtmux::{Error, ScopeError};

fn both<T, E>(error: &ScopeError<T, E>) -> Option<(&E, &Error)> {
    match error {
        ScopeError::OperationAndCleanup { operation, cleanup } => Some((operation, cleanup)),
        _ => None,
    }
}

let error: ScopeError<(), _> = ScopeError::Operation("application error");
assert!(both(&error).is_none());
```

Cleanup errors carry `Error::AfterEffect` because resource creation succeeded.
`Debug` and `Display` show the operation and `Cleanup` values when `T` and
`E` implement the matching trait, and `std::error::Error` needs both, since
it requires them as supertraits; a caller whose types implement neither still
gets a working scope, with both values reachable by matching the variant.
The standard error source is the creation or cleanup `Error`, never the
operation value: `E` need not implement `std::error::Error` at all, so match
the operation variant to reach its value or its own source chain. This keeps
`ScopeError<T, E>` usable with boxed errors and values that do not implement
`std::error::Error`.

### Owned queries and names

Borrowed `.iter().matching(...)` calls retain their result and inference
behaviour. Use `.into_iter().matching_owned(...)` to move selected items
without cloning. `exactly_one` and `one_or_none` return the iterator's item
type, including owned values:

```
use libtmux::query::QueryIteratorExt;

let names = vec![String::from("build"), String::from("test")];
let selected = names.into_iter()
    .matching_owned(|name: &String| name.starts_with('b'))
    .exactly_one()?;
assert_eq!(selected, "build");
# Ok::<(), libtmux::query::ExactlyOneError>(())
```

Replace generic bounds such as `I: QueryIteratorExt<'a, T>` with
`I: Iterator<Item = &'a T> + QueryIteratorExt`. The trait no longer has type
or lifetime parameters. Ordinary `Iterator::filter` remains available for
closures with inferred arguments.

`TmuxText` implements `AsRef<[u8]>`. Use `server.session(session.name())`
and `session.window(window.name())` directly. The conversion borrows the
original bytes and never decodes them lossily.