tmux sessions

I started to use tmux sessions a lot in the last months. Some of my learnings are in this post.

Creating a new session without opening a new terminal

From inside an existing tmux session, there is no need to open another terminal. Just create a new session directly:

# from inside tmux (prefix + :)
:new-session -s work

Or use the shortcut:

# from the shell inside a tmux pane
tmux new-session -d -s work

The -d flag creates the session in the background (detached), so the current session stays in focus. Switch to it afterwards (see below).

Listing sessions

# from the shell
tmux ls

From inside tmux, Ctrl-b s opens an interactive session picker that also shows the windows inside each session.

Switching between sessions

Ctrl-b s

Opens the session list. Use arrow keys to pick one, Enter to switch.

Ctrl-b (

Switch to the previous session.

Ctrl-b )

Switch to the next session.

From the shell:

tmux switch-client -t work

Tree view of sessions and windows

Ctrl-b w

Opens an interactive tree view of every session, with each session expanded to show its windows (and panes). Navigate with the arrow keys, press Enter to jump straight to that window, or x to kill the highlighted item.

Renaming a session

Ctrl-b $

Prompts to rename the current session.

From the shell:

tmux rename-session -t old-name new-name

Showing the session name in the status bar

By default the tmux status bar does not make it obvious which session you are in. Adding #S to status-right in ~/.tmux.conf displays the current session name. Note that status-right is a single string and setting it replaces the previous value entirely -- so include everything you want shown:

set-option -g status-right '#[fg=cyan]#S #[fg=green]| #[fg=blue]#H #[fg=green]|#[fg=yellow]#(uptime | rev | cut -d":" -f1 | rev | sed s/,//g ) #[fg=green]| #[fg=blue]%Y-%m-%d %H:%M'

The #(...) form runs a shell command -- here a small uptime | rev | cut | rev | sed pipeline grabs just the three load averages from the end of uptime's output and strips the commas. Rendered, this looks like:

blogging | dunkelsturmtaucher | 0.58 1.01 1.24 | 2026-05-03 14:36

This puts the session name in front of the hostname, current load and date/time, separated by |. Reload the config with Ctrl-b :source-file ~/.tmux.conf to see the change immediately.

See also

Claude Code tmux window names -- an earlier post on auto-renaming tmux windows (rather than sessions) when running long-lived commands like Claude Code.

Mapping my car's history with InfluxDB and VersaTiles

My Homeassistant instance tracks my car's GPS position via device_tracker.car_location and stores it in InfluxDB. Homeassistant uses the Renault integration which talks to the same API I used for downloading my charge history. I wanted to see all historic positions on a map -- not just the current one in Homeassistant.

img1

The positions are only updated when a journey ends, so the data shows parking locations, not routes. Still a lot of parking positions are stored in InfluxDB.

Querying InfluxDB with Flux

The InfluxDB query uses pivot to combine the separate latitude and longitude field rows into a single row per timestamp:

from(bucket: "home_assistant")
  |> range(start: 2025-10-01T00:00:00Z)
  |> filter(fn: (r) => r._measurement == "device_tracker.car_location")
  |> filter(fn: (r) => r._field == "latitude" or r._field == "longitude")
  |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")

After a few months this returns tens of thousands of rows. Server-side deduplication in Python groups them by rounded coordinates into a much smaller set of unique locations with a visit count.

I tried doing the aggregation in Flux directly (group + count), but it timed out on my Raspberry Pi 4. The Python-side dedup is fast enough -- the bottleneck is InfluxDB scanning the rows (~2 seconds).

VersaTiles as map provider

Trying VersaTiles was on my list after I attended a talk about it last summer at FrosCon. It's an open-source stack serving OpenStreetMap vector tiles at tiles.versatiles.org. So no need to selfhost anything. But I could if I want to.

On the frontend it works with MapLibre GL JS. The @versatiles/style npm package provides ready-made map styles. Both are loaded as ES modules from jsdelivr:

<script type="module">
  import maplibregl from 'https://cdn.jsdelivr.net/npm/maplibre-gl@5/+esm';
  import { colorful } from 'https://cdn.jsdelivr.net/npm/@versatiles/style@5/+esm';

  const style = colorful({ baseUrl: 'https://tiles.versatiles.org', language: 'de' });
  const map = new maplibregl.Map({ container: 'map', style });
</script>

Important: the baseUrl parameter is required, otherwise sprites and fonts resolve as relative URLs against my own server.

The backend

The whole backend is a single Flask file with one route for the API and has only two dependencies: flask and influxdb-client.

The positions are shown as red dots with a heatmap overlay -- the heatmap weight is based on how often the car was seen at each location. Hovering a dot shows the count.

Full Code (index.html + main.py) in a gist.

Using uv workspaces

I have a pet project that has a build folder to build the database with a cli and an app folder with everything needed to run a Datasette based readonly website. Both folders have a pyproject.toml and the app is deployed with a custom Dockerfile that uses uv.

To migrate them to use uv workspaces I added a toplevel pyproject.toml. The uv docs state that every workspace needs a root which is itself a member, so the root gets both a [project] table and the [tool.uv.workspace] one:

[project]
name = "k-workspace"
version = "0.1.0"
requires-python = ">=3.12"

[tool.uv.workspace]
members = ["app", "build"]

The sub folders have their own pyproject.toml -- unchanged. For reference, app/pyproject.toml looks roughly like this (and build/pyproject.toml is analogous with name = "k-build"):

[project]
name = "k-app"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "datasette>=1.0a",
    "datasette-cluster-map",
    # ...
]

The name field here is what you pass to uv run --package later. But we can remove the uv.lock files in both. And create a new lock file for in the toplevel:

rm app/uv.lock build/uv.lock
uv lock

This produces one uv.lock at the root that resolves dependencies across both packages. Running uv lock from a subdirectory still works — uv detects the workspace and uses the root lockfile.

To run a tool from a specific package, use --package:

uv run --package k-app datasette ...
uv run --package k-build build/cli.py ...

uv run auto-syncs the target package's dependencies into the root .venv before executing, so you don't need to run uv sync first.

One thing to watch out for: plain uv sync at the root does not install the members' dependencies -- it syncs to whatever the root pyproject.toml declares (nothing, since the root package has no dependencies of its own). It will also uninstall any packages in the .venv that don't match, so running uv sync after uv sync --all-packages strips the environment back down. Not a disaster -- the next uv run --package re-installs from uv's local cache in a second or two -- but surprising if you didn't expect it. If you want both members installed at once (for example to poke around interactively), use:

uv sync --all-packages

Since each Dockerfile only copies its own subdirectory, there's no parent pyproject.toml visible inside the container. Uv treats the sub-package as a standalone project and resolves independently. No Dockerfile changes needed.

For only two sub projects this seems like not worth it, but I still like having only one .venv folder and uv.lock file.

See also the uv workspaces documentation.