From c12025f9464bb8ddd1a8549f47f1737340983d6e Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Wed, 13 May 2026 14:05:13 -0700 Subject: [PATCH 1/8] docs(setup): show how to run the workshop in one window (VSCode terminals or tmux) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workshop needs ~4 shells inside the container at once (Gazebo, PX4, the common launch, the example), and attendees have been opening ~4 separate OS terminals + 4 `docker exec -it` each time. Document the two friendlier alternatives. * Option A: VSCode DevContainer integrated terminal — every tab is already inside the container; click the `+` to add a new one. Zero setup required. * Option B: `tmux` inside the container — adds `tmux` to the apt install list so the image ships with it pre-installed, then a newcomer-friendly walkthrough of the prefix-key model and the ~10 most useful shortcuts (split, switch panes, detach/reattach, exit). Covers the case where someone is on the plain `./docker/docker_run.sh` flow without VSCode. --- docker/scripts/install_deps.sh | 3 +- docs/setup.md | 77 ++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/docker/scripts/install_deps.sh b/docker/scripts/install_deps.sh index 1daaf37..b6f78d7 100755 --- a/docker/scripts/install_deps.sh +++ b/docker/scripts/install_deps.sh @@ -39,7 +39,8 @@ ${SUDO} apt-get install -y --no-install-recommends \ ros-humble-gps-msgs \ ros-humble-vision-msgs \ libgflags-dev \ - python3-rospkg + python3-rospkg \ + tmux ${SUDO} rm -rf /var/lib/apt/lists/* ${SUDO} apt-get clean \ No newline at end of file diff --git a/docs/setup.md b/docs/setup.md index e55ceee..d9cd84e 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -107,6 +107,83 @@ To use the DevContainers, simply open the workshop repo in VSCode, then type `CT Finally, select the devcontainer of your choice. VSCode will automatically reopen inside the running container. +### Running several workshop commands in one window + +The workshop's setup steps each occupy a terminal of their own. A typical run needs at least four shells inside the container at the same time: + +1. Gazebo (`simulation-gazebo`) — runs in the foreground, prints physics + plugin messages. +2. PX4 SITL (`/home/ubuntu/px4_sitl/bin/px4 …`) — runs in the foreground, prints its uORB / mavlink startup log. +3. The workshop's _common launch_ (`ros2 launch px4_ossna_26 common.launch.py`) — starts the MicroXRCEAgent, clock bridge, foxglove bridge, etc. +4. The example you are running for that exercise (`ros2 launch offboard_demo …`, `ros2 run precision_land …`, and so on). + +You _can_ open a fresh OS terminal for each of those and `docker exec -it px4-ossna-26 bash` four times, but that gets cumbersome. Two friendlier ways to do it in one window are below — pick whichever matches how you started the container. + +#### Option A — VSCode DevContainer integrated terminal (no extra setup) + +If you started the workshop with `Dev Containers: Reopen in container`, VSCode is already _attached to the running container_ and every integrated terminal it opens is a shell inside that container. You do not need `docker exec` again. + +1. Open the terminal panel with **Ctrl+`** (the backtick key) — or **View → Terminal**. +2. The first tab is already inside the container, at `/home/ubuntu/ossna-26-workshop_ws`. Run your first command there (for example, start Gazebo). +3. Click the **`+` icon** in the top-right of the terminal panel (next to the trash bin) to open a second tab. It is also inside the container. +4. Repeat for as many shells as you need. Each tab is independent: closing one doesn't kill the others. +5. To see two tabs side by side, click the **split icon** (the rectangle with a vertical line through it) next to `+`, or right-click a tab and pick **Split Terminal**. +6. Switch between tabs with **`Ctrl+PageDown` / `Ctrl+PageUp`**, or click the tab list on the right. + +Tip: the workspace is bind-mounted, so when you edit a source file in VSCode and rebuild with `colcon build`, the new binary is immediately picked up by `ros2 run` / `ros2 launch` in the integrated terminal. + +#### Option B — `tmux` inside the container (works with plain `docker run`) + +If you started the container with `./docker/docker_run.sh` (i.e. you are not in VSCode), use `tmux` to multiplex one OS terminal into many shells. The workshop image ships with `tmux` pre-installed, so there is nothing to install. + +1. Attach the first terminal to the container as usual: + + ```sh + docker exec -it px4-ossna-26 bash + ``` + +2. Inside the container, start a `tmux` session: + + ```sh + tmux + ``` + + You'll see a green status bar at the bottom — that means you are inside a `tmux` session. Every shortcut starts with the **prefix key**, which is `Ctrl+b` by default. Press the prefix, _release_ it, then press the next key. +3. Common shortcuts (each preceded by `Ctrl+b`, then released): + + | Shortcut | What it does | + | --- | --- | + | `"` | Split the current pane **horizontally** (new pane below) | + | `%` | Split the current pane **vertically** (new pane to the right) | + | `arrow keys` | Move focus between panes | + | `c` | Create a new full-screen **window** (different from a pane) | + | `n` / `p` | Next / previous window | + | `0`-`9` | Jump to window N | + | `z` | Zoom the current pane to full-screen / unzoom | + | `x` | Close the current pane (asks for confirmation) | + | `d` | **Detach** the session (it keeps running in the background) | + | `?` | Show the full key reference | + +4. A workshop-shaped layout, by hand, from a fresh `tmux`: + + ```text + Ctrl+b " # split horizontally ─────────────── + Ctrl+b % # split the new bottom pane vertically + Ctrl+b arrow # move focus + ``` + + gives you three panes — one for Gazebo on top, two stacked below for PX4 and the ROS 2 launches. Paste the relevant command into each. + +5. To detach and free your terminal without killing anything, press `Ctrl+b` then `d`. The simulation keeps running. To reattach later (from a new `docker exec -it px4-ossna-26 bash`): + + ```sh + tmux attach # or: tmux a + ``` + + If you have more than one session, `tmux ls` lists them and `tmux attach -t ` picks one. +6. To end everything cleanly: exit every shell in the session (`Ctrl+d` in each pane) or kill the whole session with `tmux kill-session`. + +For people who are new to `tmux`: think of it as "screen-sharing for shells" — your single terminal window becomes a tiled layout of multiple independent bash sessions, and the session survives even if you accidentally close your terminal. + ### Starting the PX4-GZ simulation PX4 can directly connect to GZ using the `gz-transport` libraries. From 0a46ed0ba6645c811b18a012f865d847602fd742 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Wed, 13 May 2026 14:12:26 -0700 Subject: [PATCH 2/8] docs(setup): add preconfigured workshop-tmux layout (Option C) Adds a small script, /usr/local/bin/workshop-tmux, that builds a ready-to-use tmux layout for the workshop: * one window "sim" with a 2x2 tiled grid of panes labelled in their borders as 'gazebo', 'px4', 'ros2', 'qgc' * each pane pre-seeded with comment-only hint lines showing the exact command to paste there (Gazebo invocation, PX4 SITL command, the common.launch.py + per-example launches, QGC) * a second window "scratch" for ad-hoc ros2 topic/node commands * friendlier defaults: pane title in border, mouse on, 20000-line scrollback, vi-style copy mode Re-running `workshop-tmux` reattaches to the existing session instead of building a new one, so it doubles as the way back in after `Ctrl+b d` or after closing the OS terminal. Wired up in Dockerfile's dev stage (COPY before the USER switch so the file lands in /usr/local/bin owned by root and exec'able by everyone). docs/setup.md gets a new "Option C" subsection under the "Running several workshop commands in one window" section documenting the layout, the launch command, reattach, and how to nuke and recreate a session. --- docker/Dockerfile | 6 ++ docker/scripts/workshop-tmux.sh | 97 +++++++++++++++++++++++++++++++++ docs/setup.md | 46 ++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100755 docker/scripts/workshop-tmux.sh diff --git a/docker/Dockerfile b/docker/Dockerfile index 1cefb98..beb40aa 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -82,6 +82,12 @@ COPY --from=dep-ros2 --chown=${USER}:${USER} /root/px4_ros_ws/install /home/${US # retrieve QGC COPY --from=qgc --chown=${USER}:${USER} /home/${USER}/QGroundControl /home/${USER}/QGroundControl +# Workshop tmux launcher (lays out a labelled 2x2 pane grid for gazebo / px4 / +# qgc / ros2 in a single window). Copied into /usr/local/bin before switching +# to the unprivileged user, so it ends up owned by root and exec'able by all. +COPY ./docker/scripts/workshop-tmux.sh /usr/local/bin/workshop-tmux +RUN chmod +x /usr/local/bin/workshop-tmux + # Switch to user USER ${USER} diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh new file mode 100755 index 0000000..374880b --- /dev/null +++ b/docker/scripts/workshop-tmux.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# workshop-tmux: launch a preconfigured tmux layout for the OSSNA 2026 +# PX4 + ROS 2 workshop. +# +# Creates one named session ('ossna') with two windows: +# +# sim - 2x2 grid of panes labelled 'gazebo', 'px4', 'qgc', 'ros2'. +# Each pane gets a comment-only hint showing the command you +# would paste there. The script does not start anything for +# you — you still copy-paste the actual commands from the +# workshop docs, but now into ONE terminal window. +# +# scratch - empty pane for ad-hoc `ros2 topic echo`, `ros2 node list`, +# editing files with vim/nano, etc. +# +# If the session already exists this just reattaches to it. + +set -eu + +SESSION="ossna" + +if tmux has-session -t "${SESSION}" 2>/dev/null; then + exec tmux attach -t "${SESSION}" +fi + +# Start the 'sim' window with the first pane. Creating the session also +# starts the tmux server, so subsequent `set -g` (which target the +# server/session) work. +tmux new-session -d -s "${SESSION}" -n sim + +# Friendlier defaults: per-pane titles in the border, mouse on, +# bigger scrollback, vi-style copy mode. +tmux set -g pane-border-status top +tmux set -g pane-border-format " #[bold]#{pane_index}: #{pane_title}#[default] " +tmux set -g mouse on +tmux set -g history-limit 20000 +tmux setw -g mode-keys vi +tmux set -g status-right "ossna-26-workshop | prefix=C-b | ?=help" + +# Create 3 more panes (4 total), then ask tmux to arrange them as an even +# 2x2 grid. Using `tiled` avoids hand-managing pane indices through a +# sequence of horizontal/vertical splits. +tmux split-window -t "${SESSION}:sim" +tmux split-window -t "${SESSION}:sim" +tmux split-window -t "${SESSION}:sim" +tmux select-layout -t "${SESSION}:sim" tiled + +# Title each pane by index. With `tiled` on 4 panes the layout is: +# 0 = top-left 1 = top-right +# 2 = bottom-left 3 = bottom-right +tmux select-pane -t "${SESSION}:sim.0" -T "gazebo" +tmux select-pane -t "${SESSION}:sim.1" -T "px4" +tmux select-pane -t "${SESSION}:sim.2" -T "ros2 (common.launch.py + example launches)" +tmux select-pane -t "${SESSION}:sim.3" -T "qgc" + +# Seed each pane with hint comments. The shell sees these as no-op comments, +# they just remind the attendee what to paste where. +tmux send-keys -t "${SESSION}:sim.0" \ + "# === pane 0: Gazebo ===" Enter \ + "# Paste, then Enter:" Enter \ + "# python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo \\" Enter \ + "# --model_store /home/ubuntu/PX4-gazebo-models/ --world default" Enter + +tmux send-keys -t "${SESSION}:sim.1" \ + "# === pane 1: PX4 SITL ===" Enter \ + "# Wait until Gazebo is up, then paste:" Enter \ + "# PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 \\" Enter \ + "# PX4_PARAM_UXRCE_DDS_SYNCT=0 \\" Enter \ + "# /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs" Enter + +tmux send-keys -t "${SESSION}:sim.2" \ + "# === pane 2: ROS 2 ===" Enter \ + "# First the workshop's common launch (XRCE-DDS agent, clock+foxglove bridges):" Enter \ + "# ros2 launch px4_ossna_26 common.launch.py" Enter \ + "# Then in the SAME pane (split with Ctrl+b \" if you want to keep both running):" Enter \ + "# ros2 launch offboard_demo offboard_demo.launch.py" Enter \ + "# ros2 launch aruco_tracker aruco_tracker.launch.py world_name:=aruco model_name:=x500_mono_cam_down_0" Enter \ + "# ros2 launch custom_mode_demo custom_mode_demo.launch.py" Enter \ + "# ros2 launch teleop teleop.launch.py" Enter \ + "# ros2 run precision_land precision_land --ros-args -p use_sim_time:=true" Enter \ + "# ros2 launch precision_land_executor precision_land_executor.launch.py" Enter + +tmux send-keys -t "${SESSION}:sim.3" \ + "# === pane 3: QGroundControl ===" Enter \ + "# Needs an X11-enabled container (default for ./docker/docker_run.sh)." Enter \ + "# /home/ubuntu/QGroundControl/qgroundcontrol" Enter + +# Scratch window for inspection commands. +tmux new-window -t "${SESSION}" -n scratch +tmux send-keys -t "${SESSION}:scratch" \ + "# === scratch ===" Enter \ + "# ros2 node list ros2 topic list ros2 topic echo /fmu/out/vehicle_status_v1" Enter + +# Focus the first pane and attach. +tmux select-window -t "${SESSION}:sim" +tmux select-pane -t "${SESSION}:sim.0" +exec tmux attach -t "${SESSION}" diff --git a/docs/setup.md b/docs/setup.md index d9cd84e..d0698c0 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -184,6 +184,52 @@ If you started the container with `./docker/docker_run.sh` (i.e. you are not in For people who are new to `tmux`: think of it as "screen-sharing for shells" — your single terminal window becomes a tiled layout of multiple independent bash sessions, and the session survives even if you accidentally close your terminal. +#### Option C — the preconfigured workshop layout (`workshop-tmux`) + +If you do not want to remember tmux's split commands at all, the image ships with a small launcher script that builds the layout for you. Run it instead of plain `tmux`: + +```sh +docker exec -it px4-ossna-26 workshop-tmux +``` + +It creates a tmux session named `ossna` with two windows: + +1. **`sim`** — a 2×2 grid of panes, each one labelled in its border with the role it plays: + + ```text + ┌───────────────────────────────┬───────────────────────────────┐ + │ 0: gazebo │ 1: px4 │ + │ │ │ + │ (paste simulation-gazebo │ (paste the PX4 SITL │ + │ here) │ command here) │ + │ │ │ + ├───────────────────────────────┼───────────────────────────────┤ + │ 2: ros2 (common.launch.py │ 3: qgc │ + │ + example launches) │ │ + │ │ (paste │ + │ (paste `ros2 launch` │ /home/ubuntu/QGroundControl│ + │ commands here) │ /qgroundcontrol here) │ + └───────────────────────────────┴───────────────────────────────┘ + ``` + + Each pane is also pre-seeded with comment lines (`# ...`) showing the actual commands to paste. The shell treats those as comments and does nothing, so you can read the hint and either paste the suggested command verbatim or edit it (different `--world`, different airframe, different launchfile, etc.). +2. **`scratch`** — a single empty pane for `ros2 topic echo`, `ros2 node list`, editing files with `nano` / `vim`, and anything else ad-hoc. Switch to it with `Ctrl+b n` (next window) or `Ctrl+b 1`. + +The launcher also enables a couple of friendlier defaults on top of stock tmux: pane titles in the border, mouse mode (click to focus, scroll wheel works), 20 000 lines of scrollback, and vi keys in copy mode. All the keybindings from Option B still work, so once you are comfortable you can split / merge panes further. + +Re-running `workshop-tmux` reattaches to the existing session instead of building a new one, so it is also a convenient way back in after `Ctrl+b d` (detach) or after closing your OS terminal: + +```sh +docker exec -it px4-ossna-26 workshop-tmux # builds session OR reattaches +``` + +To start fresh with a clean layout, kill the session first: + +```sh +docker exec -it px4-ossna-26 tmux kill-session -t ossna +docker exec -it px4-ossna-26 workshop-tmux +``` + ### Starting the PX4-GZ simulation PX4 can directly connect to GZ using the `gz-transport` libraries. From 6da47e59c7af0d57ed450bdc82c0b7eeb3c4494a Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Wed, 13 May 2026 14:22:43 -0700 Subject: [PATCH 3/8] =?UTF-8?q?workshop-tmux:=205-pane=20layout=20?= =?UTF-8?q?=E2=80=94=20common=20and=20example=20each=20get=20their=20own?= =?UTF-8?q?=20pane?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original 4-pane layout suggested running common.launch.py and the example launch in the same pane, but both are foreground `ros2 launch` processes so that's not actually possible. Restructure to a 5-pane layout where each long-running foreground process owns its own pane: ┌─────────────────────┬─────────────────────┐ │ gazebo │ px4 │ ├─────────────────────┤ │ │ ros2 common │ qgc (tall pane) │ ├─────────────────────┤ │ │ ros2 example │ │ └─────────────────────┴─────────────────────┘ QGC gets the bulk of the right column since its UI is what you watch most. Also switch the script from numeric pane_index targeting to stable pane_id (#{pane_id}, %N) targeting because tmux re-numbers pane_index in reading order whenever the layout changes, which silently scrambles titles applied after all splits. docs/setup.md's Option C diagram updated to match. --- docker/scripts/workshop-tmux.sh | 105 ++++++++++++++++++++------------ docs/setup.md | 23 +++---- 2 files changed, 79 insertions(+), 49 deletions(-) diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh index 374880b..cdfa196 100755 --- a/docker/scripts/workshop-tmux.sh +++ b/docker/scripts/workshop-tmux.sh @@ -4,11 +4,24 @@ # # Creates one named session ('ossna') with two windows: # -# sim - 2x2 grid of panes labelled 'gazebo', 'px4', 'qgc', 'ros2'. -# Each pane gets a comment-only hint showing the command you -# would paste there. The script does not start anything for -# you — you still copy-paste the actual commands from the -# workshop docs, but now into ONE terminal window. +# sim - a 5-pane layout where each of the four long-running +# foreground processes (gazebo / px4 / common.launch.py / +# example launch) gets its own pane, plus a tall pane on +# the right for QGroundControl. +# +# ┌─────────────┬─────────────┐ +# │ 0: gazebo │ 1: px4 │ +# ├─────────────┤ │ +# │ 3: common │ 2: qgc │ +# ├─────────────┤ │ +# │ 4: example │ │ +# └─────────────┴─────────────┘ +# +# Each pane is pre-seeded with comment-only hint lines +# showing the command you would paste there. The script +# does not start anything for you — you still copy-paste +# the actual commands from the workshop docs, but now into +# ONE terminal window. # # scratch - empty pane for ad-hoc `ros2 topic echo`, `ros2 node list`, # editing files with vim/nano, etc. @@ -37,54 +50,70 @@ tmux set -g history-limit 20000 tmux setw -g mode-keys vi tmux set -g status-right "ossna-26-workshop | prefix=C-b | ?=help" -# Create 3 more panes (4 total), then ask tmux to arrange them as an even -# 2x2 grid. Using `tiled` avoids hand-managing pane indices through a -# sequence of horizontal/vertical splits. -tmux split-window -t "${SESSION}:sim" -tmux split-window -t "${SESSION}:sim" -tmux split-window -t "${SESSION}:sim" -tmux select-layout -t "${SESSION}:sim" tiled - -# Title each pane by index. With `tiled` on 4 panes the layout is: -# 0 = top-left 1 = top-right -# 2 = bottom-left 3 = bottom-right -tmux select-pane -t "${SESSION}:sim.0" -T "gazebo" -tmux select-pane -t "${SESSION}:sim.1" -T "px4" -tmux select-pane -t "${SESSION}:sim.2" -T "ros2 (common.launch.py + example launches)" -tmux select-pane -t "${SESSION}:sim.3" -T "qgc" - -# Seed each pane with hint comments. The shell sees these as no-op comments, -# they just remind the attendee what to paste where. -tmux send-keys -t "${SESSION}:sim.0" \ - "# === pane 0: Gazebo ===" Enter \ +# Build the 5-pane layout described in the header comment. Use stable +# pane IDs (#{pane_id}, %0/%1/...) instead of numeric pane_index because +# tmux re-numbers pane_index in reading order whenever the layout +# changes, which would scramble titles applied after all splits. + +# Pane 0 is the existing pane we got from new-session. +GZ_PANE="$(tmux display-message -p -t "${SESSION}:sim" '#{pane_id}')" + +# Split horizontally → new pane on the right = px4 +PX4_PANE="$(tmux split-window -h -p 50 -t "${GZ_PANE}" -PF '#{pane_id}')" + +# Split the right column vertically → new pane below = qgc (taking the +# bottom ~67% so QGC has more room than its tiny pane 1 sibling). +QGC_PANE="$(tmux split-window -v -p 67 -t "${PX4_PANE}" -PF '#{pane_id}')" + +# Split the left column (gazebo) vertically → new pane below for common. +COMMON_PANE="$(tmux split-window -v -p 67 -t "${GZ_PANE}" -PF '#{pane_id}')" + +# Split the common pane vertically → new pane below = example launch. +EXAMPLE_PANE="$(tmux split-window -v -p 50 -t "${COMMON_PANE}" -PF '#{pane_id}')" + +# Title every pane by stable ID (titles render in the pane border +# thanks to the `pane-border-status top` option set above). +tmux select-pane -t "${GZ_PANE}" -T "gazebo" +tmux select-pane -t "${PX4_PANE}" -T "px4" +tmux select-pane -t "${QGC_PANE}" -T "qgc" +tmux select-pane -t "${COMMON_PANE}" -T "ros2 common.launch.py" +tmux select-pane -t "${EXAMPLE_PANE}" -T "ros2 example launch" + +# Seed each pane with hint comments. The shell sees these as no-op +# comments, they just remind the attendee what to paste where. +tmux send-keys -t "${GZ_PANE}" \ + "# === Gazebo ===" Enter \ "# Paste, then Enter:" Enter \ "# python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo \\" Enter \ "# --model_store /home/ubuntu/PX4-gazebo-models/ --world default" Enter -tmux send-keys -t "${SESSION}:sim.1" \ - "# === pane 1: PX4 SITL ===" Enter \ +tmux send-keys -t "${PX4_PANE}" \ + "# === PX4 SITL ===" Enter \ "# Wait until Gazebo is up, then paste:" Enter \ "# PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 \\" Enter \ "# PX4_PARAM_UXRCE_DDS_SYNCT=0 \\" Enter \ "# /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs" Enter -tmux send-keys -t "${SESSION}:sim.2" \ - "# === pane 2: ROS 2 ===" Enter \ - "# First the workshop's common launch (XRCE-DDS agent, clock+foxglove bridges):" Enter \ - "# ros2 launch px4_ossna_26 common.launch.py" Enter \ - "# Then in the SAME pane (split with Ctrl+b \" if you want to keep both running):" Enter \ +tmux send-keys -t "${QGC_PANE}" \ + "# === QGroundControl ===" Enter \ + "# Needs an X11-enabled container (default for ./docker/docker_run.sh)." Enter \ + "# /home/ubuntu/QGroundControl/qgroundcontrol" Enter + +tmux send-keys -t "${COMMON_PANE}" \ + "# === common.launch.py ===" Enter \ + "# XRCE-DDS agent, clock + foxglove bridges, robot_state_publisher, px4_tf, static TF:" Enter \ + "# ros2 launch px4_ossna_26 common.launch.py" Enter + +tmux send-keys -t "${EXAMPLE_PANE}" \ + "# === example launch ===" Enter \ + "# Pick ONE of these once common.launch.py is up:" Enter \ "# ros2 launch offboard_demo offboard_demo.launch.py" Enter \ - "# ros2 launch aruco_tracker aruco_tracker.launch.py world_name:=aruco model_name:=x500_mono_cam_down_0" Enter \ "# ros2 launch custom_mode_demo custom_mode_demo.launch.py" Enter \ + "# ros2 launch aruco_tracker aruco_tracker.launch.py world_name:=aruco model_name:=x500_mono_cam_down_0" Enter \ "# ros2 launch teleop teleop.launch.py" Enter \ "# ros2 run precision_land precision_land --ros-args -p use_sim_time:=true" Enter \ "# ros2 launch precision_land_executor precision_land_executor.launch.py" Enter -tmux send-keys -t "${SESSION}:sim.3" \ - "# === pane 3: QGroundControl ===" Enter \ - "# Needs an X11-enabled container (default for ./docker/docker_run.sh)." Enter \ - "# /home/ubuntu/QGroundControl/qgroundcontrol" Enter - # Scratch window for inspection commands. tmux new-window -t "${SESSION}" -n scratch tmux send-keys -t "${SESSION}:scratch" \ diff --git a/docs/setup.md b/docs/setup.md index d0698c0..9e2b21e 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -194,25 +194,26 @@ docker exec -it px4-ossna-26 workshop-tmux It creates a tmux session named `ossna` with two windows: -1. **`sim`** — a 2×2 grid of panes, each one labelled in its border with the role it plays: +1. **`sim`** — five panes, each labelled in its border with the role it plays. Every long-running foreground process gets its own pane (`ros2 launch …` is foreground, so common and the example can't share one): ```text ┌───────────────────────────────┬───────────────────────────────┐ - │ 0: gazebo │ 1: px4 │ - │ │ │ + │ gazebo │ px4 │ │ (paste simulation-gazebo │ (paste the PX4 SITL │ │ here) │ command here) │ - │ │ │ - ├───────────────────────────────┼───────────────────────────────┤ - │ 2: ros2 (common.launch.py │ 3: qgc │ - │ + example launches) │ │ - │ │ (paste │ - │ (paste `ros2 launch` │ /home/ubuntu/QGroundControl│ - │ commands here) │ /qgroundcontrol here) │ + ├───────────────────────────────┤ │ + │ ros2 common.launch.py │ qgc │ + │ (paste │ (paste │ + │ `ros2 launch px4_ossna_26 │ /home/ubuntu/QGroundControl│ + │ common.launch.py`) │ /qgroundcontrol here) │ + ├───────────────────────────────┤ │ + │ ros2 example launch │ │ + │ (paste `ros2 launch │ │ + │ offboard_demo …` etc.) │ │ └───────────────────────────────┴───────────────────────────────┘ ``` - Each pane is also pre-seeded with comment lines (`# ...`) showing the actual commands to paste. The shell treats those as comments and does nothing, so you can read the hint and either paste the suggested command verbatim or edit it (different `--world`, different airframe, different launchfile, etc.). + Each pane is also pre-seeded with comment lines (`# ...`) showing the actual commands to paste. The shell treats those as comments and does nothing, so you can read the hint and either paste the suggested command verbatim or edit it (different `--world`, different airframe, different launchfile, etc.). The QGC pane is given the bulk of the right column because its UI is what you'll be looking at most. 2. **`scratch`** — a single empty pane for `ros2 topic echo`, `ros2 node list`, editing files with `nano` / `vim`, and anything else ad-hoc. Switch to it with `Ctrl+b n` (next window) or `Ctrl+b 1`. The launcher also enables a couple of friendlier defaults on top of stock tmux: pane titles in the border, mouse mode (click to focus, scroll wheel works), 20 000 lines of scrollback, and vi keys in copy mode. All the keybindings from Option B still work, so once you are comfortable you can split / merge panes further. From 429941a24cc3262df42cae06cb5ec7390ea5e152 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Wed, 13 May 2026 14:30:30 -0700 Subject: [PATCH 4/8] workshop-tmux: add color theme + status-bar animations Dress up the workshop layout. Two small helper scripts run from tmux's status bar (status-interval 1) to drive cheap animations: /usr/local/bin/workshop-banner Prints a tmux format string with a rainbow "OSSNA 2026" banner where one letter at a time is inverted (white text on its own colour). Cycles one letter per second. /usr/local/bin/workshop-spinner Prints one frame of a 10-frame braille spinner based on the wall-clock second. The launcher applies a cohesive theme: dark navy status background, bright cyan active pane border, magenta inactive pane title dots, yellow flash on inactive-window activity, and a purple-on-yellow message popup. The two helpers feed status-left (banner) and status-right (spinner + ros2-coloured "workshop" label + clock). Dockerfile copies all three scripts into /usr/local/bin in the dev stage, owned by root, executable by everyone. --- docker/Dockerfile | 10 +++++---- docker/scripts/workshop-banner | 28 +++++++++++++++++++++++++ docker/scripts/workshop-spinner | 8 ++++++++ docker/scripts/workshop-tmux.sh | 36 +++++++++++++++++++++++++++++---- 4 files changed, 74 insertions(+), 8 deletions(-) create mode 100755 docker/scripts/workshop-banner create mode 100755 docker/scripts/workshop-spinner diff --git a/docker/Dockerfile b/docker/Dockerfile index beb40aa..9beaf5c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -82,11 +82,13 @@ COPY --from=dep-ros2 --chown=${USER}:${USER} /root/px4_ros_ws/install /home/${US # retrieve QGC COPY --from=qgc --chown=${USER}:${USER} /home/${USER}/QGroundControl /home/${USER}/QGroundControl -# Workshop tmux launcher (lays out a labelled 2x2 pane grid for gazebo / px4 / -# qgc / ros2 in a single window). Copied into /usr/local/bin before switching -# to the unprivileged user, so it ends up owned by root and exec'able by all. +# Workshop tmux launcher + its animation helpers (rainbow banner + spinner). +# Copied into /usr/local/bin before switching to the unprivileged user so +# they end up owned by root and exec'able by all. COPY ./docker/scripts/workshop-tmux.sh /usr/local/bin/workshop-tmux -RUN chmod +x /usr/local/bin/workshop-tmux +COPY ./docker/scripts/workshop-banner /usr/local/bin/workshop-banner +COPY ./docker/scripts/workshop-spinner /usr/local/bin/workshop-spinner +RUN chmod +x /usr/local/bin/workshop-tmux /usr/local/bin/workshop-banner /usr/local/bin/workshop-spinner # Switch to user USER ${USER} diff --git a/docker/scripts/workshop-banner b/docker/scripts/workshop-banner new file mode 100755 index 0000000..c0fc65e --- /dev/null +++ b/docker/scripts/workshop-banner @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# workshop-banner: prints a rainbow "OSSNA 2026" with a roving white-highlight +# cell that cycles one letter per second. Designed to be called from tmux's +# status-left with `status-interval 1`. + +import time + +text = "OSSNA 2026" +palette = [ + "#ff5577", "#ff9955", "#ffdd55", "#55ff77", "#55ddff", + "#bb55ff", "#ff55dd", "#ff5577", "#ff9955", "#ffdd55", +] +n = len(text) +highlight = int(time.time()) % n + +parts = [] +for i, ch in enumerate(text): + if ch == " ": + parts.append(" ") + continue + color = palette[i % len(palette)] + if i == highlight: + # Inverse cell: white text on the slot's own color + parts.append(f"#[fg=#ffffff,bg={color},bold]{ch}#[default]") + else: + parts.append(f"#[fg={color},bold]{ch}#[default]") + +print("".join(parts), end="") diff --git a/docker/scripts/workshop-spinner b/docker/scripts/workshop-spinner new file mode 100755 index 0000000..42fa3bf --- /dev/null +++ b/docker/scripts/workshop-spinner @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +# workshop-spinner: prints one frame of a 10-frame braille spinner based on +# the current wall-clock second. Called from tmux's status-right with +# `status-interval 1`. +import time + +FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" +print(FRAMES[int(time.time()) % len(FRAMES)], end="") diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh index cdfa196..94e3556 100755 --- a/docker/scripts/workshop-tmux.sh +++ b/docker/scripts/workshop-tmux.sh @@ -41,14 +41,42 @@ fi # server/session) work. tmux new-session -d -s "${SESSION}" -n sim -# Friendlier defaults: per-pane titles in the border, mouse on, -# bigger scrollback, vi-style copy mode. +# --- Friendlier defaults --- tmux set -g pane-border-status top -tmux set -g pane-border-format " #[bold]#{pane_index}: #{pane_title}#[default] " tmux set -g mouse on tmux set -g history-limit 20000 tmux setw -g mode-keys vi -tmux set -g status-right "ossna-26-workshop | prefix=C-b | ?=help" +tmux set -g status-interval 1 # refresh status bar (and animations) every second + +# --- Colors: a punchy "OSSNA 26" palette on a dark background --- +# tmux 3.2+ honours hex colours; the image ships tmux 3.2a. +tmux set -g status-style "bg=#1a1a2e,fg=#e0e0e0" +tmux set -g message-style "bg=#ffdd55,fg=#1a1a2e,bold" +tmux set -g pane-border-style "fg=#444466" # dim purple-gray +tmux set -g pane-active-border-style "fg=#55ddff,bold" # bright cyan for the active pane + +# Pane title in the border: pink dot + bold white name (visible against #1a1a2e) +tmux set -g pane-border-format " #[fg=#ff55dd,bold]●#[default] #[fg=#ffffff,bold]#{pane_title}#[default] " + +# Window list in the status bar +tmux setw -g window-status-style "fg=#999999" +tmux setw -g window-status-current-style "fg=#ffdd55,bold,bg=#440044" +tmux setw -g window-status-format " #I:#W " +tmux setw -g window-status-current-format " #I:#W " + +# Flash the window name yellow when an inactive window has new output +tmux set -g monitor-activity on +tmux set -g visual-activity off +tmux setw -g window-status-activity-style "fg=#ffdd55,bold,blink" + +# --- Animations --- +# status-left: rainbow "OSSNA 2026" with a single highlighted letter that +# rotates every second (workshop-banner generates the tmux format string). +# status-right: a braille spinner that advances every second, plus a clock. +tmux set -g status-left-length 40 +tmux set -g status-right-length 80 +tmux set -g status-left "#(workshop-banner) " +tmux set -g status-right "#[fg=#55ddff,bold]#(workshop-spinner)#[default] #[fg=#999999]workshop #[fg=#bb55ff,bold]%H:%M:%S " # Build the 5-pane layout described in the header comment. Use stable # pane IDs (#{pane_id}, %0/%1/...) instead of numeric pane_index because From 242ec28e574142b789eb402046bb9ca801f7f911 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Thu, 14 May 2026 12:01:15 -0700 Subject: [PATCH 5/8] =?UTF-8?q?workshop-tmux:=20synthwave=20glow-up=20?= =?UTF-8?q?=E2=80=94=20Dracula=20theme,=20EQ=20spinner,=20Tux=20welcome?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the workshop session look like something built for an Open Source Summit room and not a 90s vt100. Upgrades: * Theme switched from the previous five-color hack to a coherent Dracula-inspired palette (pink/purple/cyan/green/yellow on #13111c). Heavy pane borders on tmux 3.2+; bright pink active border with hex-bullet ⬢ ... ⬢ pane titles; cyan-on-dark window list. * status-left animation reworked: instead of a single rotating letter highlight, a traveling "scanner" sweeps across the title string `░▒▓ OSS NA ◆ 26 ▓▒░`. Letters within 1, 2, 3 columns of the scanner fade from white-hot pink → purple → cyan → dim grey. Advances two columns per tmux tick so the motion is visible at status-interval 1. * status-right animation reworked: replaces the braille spinner with a 5-bar audio-EQ visualisation. Each bar samples a sine wave at a different phase so the heights snapshot to different positions each tick — looks like a small bouncing equaliser. * Pane hint comments dressed up with ▙▟ glyph headers; per-pane blurbs name the tech stack (Gazebo Harmonic, PX4 v1.16 SITL, ROS 2 common.launch.py, QGroundControl v5.0.8). * New helper /usr/local/bin/workshop-welcome prints a colored Tux ASCII + tech-stack badges + a six-line tmux cheat sheet. Wired into the scratch window so attendees see it the moment they switch with Ctrl-b 1. * Dockerfile copies all four helpers (workshop-tmux, banner, spinner, welcome) into /usr/local/bin in the dev stage, owned by root, world executable. --- docker/Dockerfile | 12 ++-- docker/scripts/workshop-banner | 51 +++++++++----- docker/scripts/workshop-spinner | 29 ++++++-- docker/scripts/workshop-tmux.sh | 117 +++++++++++++++++--------------- docker/scripts/workshop-welcome | 42 ++++++++++++ 5 files changed, 171 insertions(+), 80 deletions(-) create mode 100755 docker/scripts/workshop-welcome diff --git a/docker/Dockerfile b/docker/Dockerfile index 9beaf5c..90056ee 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -82,13 +82,17 @@ COPY --from=dep-ros2 --chown=${USER}:${USER} /root/px4_ros_ws/install /home/${US # retrieve QGC COPY --from=qgc --chown=${USER}:${USER} /home/${USER}/QGroundControl /home/${USER}/QGroundControl -# Workshop tmux launcher + its animation helpers (rainbow banner + spinner). -# Copied into /usr/local/bin before switching to the unprivileged user so -# they end up owned by root and exec'able by all. +# Workshop tmux launcher + its animation / welcome helpers. Copied into +# /usr/local/bin before switching to the unprivileged user so they end up +# owned by root and exec'able by all. COPY ./docker/scripts/workshop-tmux.sh /usr/local/bin/workshop-tmux COPY ./docker/scripts/workshop-banner /usr/local/bin/workshop-banner COPY ./docker/scripts/workshop-spinner /usr/local/bin/workshop-spinner -RUN chmod +x /usr/local/bin/workshop-tmux /usr/local/bin/workshop-banner /usr/local/bin/workshop-spinner +COPY ./docker/scripts/workshop-welcome /usr/local/bin/workshop-welcome +RUN chmod +x /usr/local/bin/workshop-tmux \ + /usr/local/bin/workshop-banner \ + /usr/local/bin/workshop-spinner \ + /usr/local/bin/workshop-welcome # Switch to user USER ${USER} diff --git a/docker/scripts/workshop-banner b/docker/scripts/workshop-banner index c0fc65e..afb4183 100755 --- a/docker/scripts/workshop-banner +++ b/docker/scripts/workshop-banner @@ -1,28 +1,43 @@ #!/usr/bin/env python3 -# workshop-banner: prints a rainbow "OSSNA 2026" with a roving white-highlight -# cell that cycles one letter per second. Designed to be called from tmux's -# status-left with `status-interval 1`. +# workshop-banner: prints a synthwave-y status-left banner with a traveling +# white-hot "scanner" that sweeps through the title every half second. Called +# from tmux's status-left with `status-interval 1`. The scanner advances two +# columns per tick so it visibly moves at the 1 Hz refresh rate. import time -text = "OSSNA 2026" -palette = [ - "#ff5577", "#ff9955", "#ffdd55", "#55ff77", "#55ddff", - "#bb55ff", "#ff55dd", "#ff5577", "#ff9955", "#ffdd55", -] -n = len(text) -highlight = int(time.time()) % n +# The visible title: chunky shaded gradient on either side of the words. +# All characters render fine in a stock terminal; no Nerd Font required. +TITLE = "░▒▓ OSS NA ◆ 26 ▓▒░" -parts = [] -for i, ch in enumerate(text): +# Dracula-ish palette, biased toward pink/purple/cyan for the OSS vibe. +PEAK = "#f8f8f2" # white-hot +HOT = "#ff79c6" # pink +WARM = "#bd93f9" # purple +COOL = "#8be9fd" # cyan +DIM = "#6272a4" # muted comment-grey + +n = len(TITLE) +# Advance by 2 columns every tmux tick (status-interval 1 -> 2 cols/sec). +pos = (int(time.time()) * 2) % n + +def style_for(distance: int) -> str: + if distance == 0: + return f"#[fg={PEAK},bg={HOT},bold]" + if distance == 1: + return f"#[fg={HOT},bold]" + if distance == 2: + return f"#[fg={WARM},bold]" + if distance == 3: + return f"#[fg={COOL}]" + return f"#[fg={DIM}]" + +parts: list[str] = [] +for i, ch in enumerate(TITLE): if ch == " ": parts.append(" ") continue - color = palette[i % len(palette)] - if i == highlight: - # Inverse cell: white text on the slot's own color - parts.append(f"#[fg=#ffffff,bg={color},bold]{ch}#[default]") - else: - parts.append(f"#[fg={color},bold]{ch}#[default]") + d = min((i - pos) % n, (pos - i) % n) + parts.append(style_for(d) + ch + "#[default]") print("".join(parts), end="") diff --git a/docker/scripts/workshop-spinner b/docker/scripts/workshop-spinner index 42fa3bf..19d8346 100755 --- a/docker/scripts/workshop-spinner +++ b/docker/scripts/workshop-spinner @@ -1,8 +1,27 @@ #!/usr/bin/env python3 -# workshop-spinner: prints one frame of a 10-frame braille spinner based on -# the current wall-clock second. Called from tmux's status-right with -# `status-interval 1`. +# workshop-spinner: prints a 5-bar audio-EQ visualisation that snapshots +# itself every second. Each bar is a different phase of the same sine +# wave, so on each tmux refresh the column heights re-arrange and it +# looks like a small equaliser is bouncing. Designed for tmux's +# status-right with `status-interval 1`. +# +# Output: 5 colored block characters, e.g. `▃▆█▄▂`. + import time +import math + +# Lower → taller block-fill glyphs (8 levels). +LEVELS = " ▁▂▃▄▅▆▇█" + +# Bar colors, fading from cool to hot — gives the bars a gradient look. +BAR_COLORS = ["#8be9fd", "#bd93f9", "#ff79c6", "#bd93f9", "#8be9fd"] + +t = time.time() * 3.0 # speed +bars: list[str] = [] +for i in range(5): + phase = i * 0.7 + val = (math.sin(t + phase) + 1.0) / 2.0 # normalise to [0, 1] + idx = int(val * (len(LEVELS) - 1)) + bars.append(f"#[fg={BAR_COLORS[i]},bold]{LEVELS[idx]}#[default]") -FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" -print(FRAMES[int(time.time()) % len(FRAMES)], end="") +print("".join(bars), end="") diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh index 94e3556..06c0c6f 100755 --- a/docker/scripts/workshop-tmux.sh +++ b/docker/scripts/workshop-tmux.sh @@ -46,37 +46,50 @@ tmux set -g pane-border-status top tmux set -g mouse on tmux set -g history-limit 20000 tmux setw -g mode-keys vi -tmux set -g status-interval 1 # refresh status bar (and animations) every second - -# --- Colors: a punchy "OSSNA 26" palette on a dark background --- -# tmux 3.2+ honours hex colours; the image ships tmux 3.2a. -tmux set -g status-style "bg=#1a1a2e,fg=#e0e0e0" -tmux set -g message-style "bg=#ffdd55,fg=#1a1a2e,bold" -tmux set -g pane-border-style "fg=#444466" # dim purple-gray -tmux set -g pane-active-border-style "fg=#55ddff,bold" # bright cyan for the active pane - -# Pane title in the border: pink dot + bold white name (visible against #1a1a2e) -tmux set -g pane-border-format " #[fg=#ff55dd,bold]●#[default] #[fg=#ffffff,bold]#{pane_title}#[default] " - -# Window list in the status bar -tmux setw -g window-status-style "fg=#999999" -tmux setw -g window-status-current-style "fg=#ffdd55,bold,bg=#440044" -tmux setw -g window-status-format " #I:#W " -tmux setw -g window-status-current-format " #I:#W " - -# Flash the window name yellow when an inactive window has new output +tmux set -g status-interval 1 # refresh status bar (and animations) every second +tmux set -g default-terminal "tmux-256color" # opt into 256/truecolor where supported +tmux set -ga terminal-overrides ",xterm-256color:Tc" # tell tmux the outer terminal is true-color +tmux set -g pane-border-lines heavy # thicker borders on tmux 3.2+ + +# --- Dracula-inspired palette (synthwave-y, dev-friendly) --- +# bg #282a36 bg-dark #13111c +# pink #ff79c6 purple #bd93f9 cyan #8be9fd +# green #50fa7b yellow #f1fa8c orange #ffb86c +# red #ff5555 fg #f8f8f2 comment #6272a4 +tmux set -g status-style "bg=#13111c,fg=#f8f8f2" +tmux set -g message-style "bg=#ff79c6,fg=#13111c,bold" +tmux set -g pane-border-style "fg=#3a3a5a" # dim border for inactive +tmux set -g pane-active-border-style "fg=#ff79c6,bold" # hot pink for the active pane + +# Pane title in the border: hex-bullets in cyan + bold pink name. +# Active pane gets a brighter title via pane-active-border-style colour, the +# format string itself is the same for all panes. +tmux set -g pane-border-format " #[fg=#8be9fd,bold]⬢#[default] #[fg=#ff79c6,bold]#{pane_title}#[default] #[fg=#8be9fd,bold]⬢#[default] " + +# Window list in the status bar (powerline-ish flat segments) +tmux setw -g window-status-style "fg=#6272a4" +tmux setw -g window-status-current-style "fg=#13111c,bold,bg=#8be9fd" +tmux setw -g window-status-format " #I⋅#W " +tmux setw -g window-status-current-format " #I⋅#W " +tmux setw -g window-status-separator "" + +# Flash window name yellow when an inactive window has new output tmux set -g monitor-activity on tmux set -g visual-activity off -tmux setw -g window-status-activity-style "fg=#ffdd55,bold,blink" +tmux setw -g window-status-activity-style "fg=#f1fa8c,bold,blink" # --- Animations --- -# status-left: rainbow "OSSNA 2026" with a single highlighted letter that -# rotates every second (workshop-banner generates the tmux format string). -# status-right: a braille spinner that advances every second, plus a clock. -tmux set -g status-left-length 40 +# status-left: synthwave title with a traveling "scanner" highlight that +# sweeps across the text twice per second (workshop-banner +# emits the tmux format string). +# status-right: 5-bar EQ visualiser + cyan workshop label + magenta clock. +tmux set -g status-left-length 60 tmux set -g status-right-length 80 tmux set -g status-left "#(workshop-banner) " -tmux set -g status-right "#[fg=#55ddff,bold]#(workshop-spinner)#[default] #[fg=#999999]workshop #[fg=#bb55ff,bold]%H:%M:%S " +tmux set -g status-right "#[fg=#6272a4]┤ #(workshop-spinner) #[fg=#8be9fd,bold]workshop #[fg=#6272a4]│ #[fg=#bd93f9,bold]%H:%M:%S #[fg=#6272a4]├" + +# Make the prefix indicator obvious when prefix is held +tmux set -g status-keys vi # Build the 5-pane layout described in the header comment. Use stable # pane IDs (#{pane_id}, %0/%1/...) instead of numeric pane_index because @@ -110,43 +123,41 @@ tmux select-pane -t "${EXAMPLE_PANE}" -T "ros2 example launch" # Seed each pane with hint comments. The shell sees these as no-op # comments, they just remind the attendee what to paste where. tmux send-keys -t "${GZ_PANE}" \ - "# === Gazebo ===" Enter \ - "# Paste, then Enter:" Enter \ - "# python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo \\" Enter \ - "# --model_store /home/ubuntu/PX4-gazebo-models/ --world default" Enter + "# ▙▟ Gazebo Harmonic ▙▟" Enter \ + "# Spawn the physics world. Paste:" Enter \ + "# python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo \\" Enter \ + "# --model_store /home/ubuntu/PX4-gazebo-models/ --world default" Enter tmux send-keys -t "${PX4_PANE}" \ - "# === PX4 SITL ===" Enter \ - "# Wait until Gazebo is up, then paste:" Enter \ - "# PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 \\" Enter \ - "# PX4_PARAM_UXRCE_DDS_SYNCT=0 \\" Enter \ - "# /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs" Enter + "# ▙▟ PX4 v1.16 SITL ▙▟" Enter \ + "# Once Gazebo is up, paste (4001 = x500):" Enter \ + "# PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 \\" Enter \ + "# PX4_PARAM_UXRCE_DDS_SYNCT=0 \\" Enter \ + "# /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs" Enter tmux send-keys -t "${QGC_PANE}" \ - "# === QGroundControl ===" Enter \ - "# Needs an X11-enabled container (default for ./docker/docker_run.sh)." Enter \ - "# /home/ubuntu/QGroundControl/qgroundcontrol" Enter + "# ▙▟ QGroundControl v5.0.8 ▙▟" Enter \ + "# Needs an X11-enabled container (default for ./docker/docker_run.sh)." Enter \ + "# /home/ubuntu/QGroundControl/qgroundcontrol" Enter tmux send-keys -t "${COMMON_PANE}" \ - "# === common.launch.py ===" Enter \ - "# XRCE-DDS agent, clock + foxglove bridges, robot_state_publisher, px4_tf, static TF:" Enter \ - "# ros2 launch px4_ossna_26 common.launch.py" Enter + "# ▙▟ ROS 2 :: common.launch.py ▙▟" Enter \ + "# XRCE-DDS agent + clock/foxglove bridges + robot_state_publisher + px4_tf + static TF:" Enter \ + "# ros2 launch px4_ossna_26 common.launch.py" Enter tmux send-keys -t "${EXAMPLE_PANE}" \ - "# === example launch ===" Enter \ - "# Pick ONE of these once common.launch.py is up:" Enter \ - "# ros2 launch offboard_demo offboard_demo.launch.py" Enter \ - "# ros2 launch custom_mode_demo custom_mode_demo.launch.py" Enter \ - "# ros2 launch aruco_tracker aruco_tracker.launch.py world_name:=aruco model_name:=x500_mono_cam_down_0" Enter \ - "# ros2 launch teleop teleop.launch.py" Enter \ - "# ros2 run precision_land precision_land --ros-args -p use_sim_time:=true" Enter \ - "# ros2 launch precision_land_executor precision_land_executor.launch.py" Enter - -# Scratch window for inspection commands. + "# ▙▟ ROS 2 :: example launch ▙▟" Enter \ + "# Pick ONE once common.launch.py is up:" Enter \ + "# ros2 launch offboard_demo offboard_demo.launch.py" Enter \ + "# ros2 launch custom_mode_demo custom_mode_demo.launch.py" Enter \ + "# ros2 launch aruco_tracker aruco_tracker.launch.py world_name:=aruco model_name:=x500_mono_cam_down_0" Enter \ + "# ros2 launch teleop teleop.launch.py" Enter \ + "# ros2 run precision_land precision_land --ros-args -p use_sim_time:=true" Enter \ + "# ros2 launch precision_land_executor precision_land_executor.launch.py" Enter + +# Scratch window: small welcome banner (Tux + tech stack + tmux cheat sheet). tmux new-window -t "${SESSION}" -n scratch -tmux send-keys -t "${SESSION}:scratch" \ - "# === scratch ===" Enter \ - "# ros2 node list ros2 topic list ros2 topic echo /fmu/out/vehicle_status_v1" Enter +tmux send-keys -t "${SESSION}:scratch" "clear; workshop-welcome 2>/dev/null || true" Enter # Focus the first pane and attach. tmux select-window -t "${SESSION}:sim" diff --git a/docker/scripts/workshop-welcome b/docker/scripts/workshop-welcome new file mode 100755 index 0000000..9c77d4c --- /dev/null +++ b/docker/scripts/workshop-welcome @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# workshop-welcome: prints a colored Tux + tech-stack badge for the OSSNA +# 2026 workshop. Run inside the scratch tmux window so attendees see it on +# arrival. + +import sys + +# Dracula-inspired ANSI 256 colour indices (close matches to the hex +# palette workshop-tmux.sh uses for tmux itself): +PINK = "\x1b[38;5;213m" # ~#ff79c6 +PURPLE = "\x1b[38;5;141m" # ~#bd93f9 +CYAN = "\x1b[38;5;87m" # ~#8be9fd +GREEN = "\x1b[38;5;156m" # ~#50fa7b +YELLOW = "\x1b[38;5;228m" # ~#f1fa8c +DIM = "\x1b[38;5;245m" +BOLD = "\x1b[1m" +RESET = "\x1b[0m" +SEP = f"{DIM}::{RESET}" + + +def line(s: str) -> None: + sys.stdout.write(s + "\n") + + +line("") +line(f"{PINK} .--.{RESET}") +line(f"{PINK} |o_o | {CYAN}{BOLD}░▒▓ OSS NA 2026 · Hands-On Aerial Robotics ▓▒░{RESET}") +line(f"{PINK} |:_/ | {PURPLE}tmux session{RESET} {SEP} {PURPLE}ossna{RESET} {SEP} {PURPLE}prefix = Ctrl-b{RESET} {SEP} {PURPLE}? for help{RESET}") +line(f"{PINK} // \\ \\ {GREEN}stack{RESET} {SEP} {GREEN}PX4 v1.16{RESET} {SEP} {GREEN}ROS 2 Humble{RESET} {SEP} {GREEN}Gazebo Harmonic{RESET} {SEP} {GREEN}QGC v5.0.8{RESET}") +line(f"{PINK} (| | ) {YELLOW}scratch{RESET} {SEP} {YELLOW}use this window for ros2 topic echo / node list / vim / nano{RESET}") +line(f"{PINK} /'\\_ _/`\\{RESET}") +line(f"{PINK} \\___)=(___/{RESET}") +line("") +line(f" {DIM}┌─ tmux quick reference ─────────────────────────────────────────────────────{RESET}") +line(f" {DIM}│{RESET} {CYAN}Ctrl-b 0{RESET} / {CYAN}Ctrl-b 1{RESET} switch to window 0 (sim) / 1 (scratch)") +line(f" {DIM}│{RESET} {CYAN}Ctrl-b n{RESET} / {CYAN}Ctrl-b p{RESET} next / previous window") +line(f" {DIM}│{RESET} {CYAN}Ctrl-b arrow{RESET} move focus between panes") +line(f" {DIM}│{RESET} {CYAN}Ctrl-b z{RESET} zoom the current pane to full screen (toggle)") +line(f" {DIM}│{RESET} {CYAN}Ctrl-b \"{RESET} / {CYAN}Ctrl-b %{RESET} split horizontally / vertically") +line(f" {DIM}│{RESET} {CYAN}Ctrl-b d{RESET} detach (sim keeps running); reattach with `workshop-tmux`") +line(f" {DIM}└─────────────────────────────────────────────────────────────────────────────{RESET}") +line("") From 5947d7db6a7a959b8f4ffd948a4f4694ee7531c0 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Fri, 15 May 2026 14:41:30 -0700 Subject: [PATCH 6/8] workshop-tmux: title the scratch pane, drop the $ spam, restore mouse copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small UX fixes spotted on first interactive use: 1. The scratch pane had no explicit title, so the global pane-border-format `⬢ #{pane_title} ⬢` fell back to the container hostname. Set its title to "scratch" right after creating the window. 2. Each pane was seeded by a chain of `send-keys '#hint' Enter '#hint' Enter ...`, which prints one shell prompt per line. With the longer ROS 2 example pane that was six prompts before the user typed anything. Replace the chain with a single `clear; printf 'colored\nmulti\nline\n'` invocation so each pane shows the hint once, then exactly one trailing prompt. 3. `set -g mouse on` was capturing click-drag, so attendees couldn't use their terminal's native selection to copy command output. Turn mouse mode off and add a comment explaining the trade-off (no click-to-focus on panes; use Ctrl-b arrow instead). Anyone who prefers mouse mode can re-enable per session with `tmux set mouse on`. --- docker/scripts/workshop-tmux.sh | 80 +++++++++++++++++---------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh index 06c0c6f..91f06ce 100755 --- a/docker/scripts/workshop-tmux.sh +++ b/docker/scripts/workshop-tmux.sh @@ -43,7 +43,6 @@ tmux new-session -d -s "${SESSION}" -n sim # --- Friendlier defaults --- tmux set -g pane-border-status top -tmux set -g mouse on tmux set -g history-limit 20000 tmux setw -g mode-keys vi tmux set -g status-interval 1 # refresh status bar (and animations) every second @@ -51,6 +50,14 @@ tmux set -g default-terminal "tmux-256color" # opt into 256/truecolor where supp tmux set -ga terminal-overrides ",xterm-256color:Tc" # tell tmux the outer terminal is true-color tmux set -g pane-border-lines heavy # thicker borders on tmux 3.2+ +# NOTE: mouse mode is deliberately OFF. +# With `mouse on`, tmux captures click-drag and you cannot select text with +# the mouse in the usual terminal way. Leaving it off lets attendees use +# their terminal's native click-drag → copy. The trade-off is no click-to- +# focus on panes — use Ctrl-b arrow / `Ctrl-b q ` to switch panes +# instead. Enable per-session with `tmux set mouse on` if you prefer. +tmux set -g mouse off + # --- Dracula-inspired palette (synthwave-y, dev-friendly) --- # bg #282a36 bg-dark #13111c # pink #ff79c6 purple #bd93f9 cyan #8be9fd @@ -120,44 +127,41 @@ tmux select-pane -t "${QGC_PANE}" -T "qgc" tmux select-pane -t "${COMMON_PANE}" -T "ros2 common.launch.py" tmux select-pane -t "${EXAMPLE_PANE}" -T "ros2 example launch" -# Seed each pane with hint comments. The shell sees these as no-op -# comments, they just remind the attendee what to paste where. -tmux send-keys -t "${GZ_PANE}" \ - "# ▙▟ Gazebo Harmonic ▙▟" Enter \ - "# Spawn the physics world. Paste:" Enter \ - "# python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo \\" Enter \ - "# --model_store /home/ubuntu/PX4-gazebo-models/ --world default" Enter - -tmux send-keys -t "${PX4_PANE}" \ - "# ▙▟ PX4 v1.16 SITL ▙▟" Enter \ - "# Once Gazebo is up, paste (4001 = x500):" Enter \ - "# PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 \\" Enter \ - "# PX4_PARAM_UXRCE_DDS_SYNCT=0 \\" Enter \ - "# /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs" Enter - -tmux send-keys -t "${QGC_PANE}" \ - "# ▙▟ QGroundControl v5.0.8 ▙▟" Enter \ - "# Needs an X11-enabled container (default for ./docker/docker_run.sh)." Enter \ - "# /home/ubuntu/QGroundControl/qgroundcontrol" Enter - -tmux send-keys -t "${COMMON_PANE}" \ - "# ▙▟ ROS 2 :: common.launch.py ▙▟" Enter \ - "# XRCE-DDS agent + clock/foxglove bridges + robot_state_publisher + px4_tf + static TF:" Enter \ - "# ros2 launch px4_ossna_26 common.launch.py" Enter - -tmux send-keys -t "${EXAMPLE_PANE}" \ - "# ▙▟ ROS 2 :: example launch ▙▟" Enter \ - "# Pick ONE once common.launch.py is up:" Enter \ - "# ros2 launch offboard_demo offboard_demo.launch.py" Enter \ - "# ros2 launch custom_mode_demo custom_mode_demo.launch.py" Enter \ - "# ros2 launch aruco_tracker aruco_tracker.launch.py world_name:=aruco model_name:=x500_mono_cam_down_0" Enter \ - "# ros2 launch teleop teleop.launch.py" Enter \ - "# ros2 run precision_land precision_land --ros-args -p use_sim_time:=true" Enter \ - "# ros2 launch precision_land_executor precision_land_executor.launch.py" Enter - -# Scratch window: small welcome banner (Tux + tech stack + tmux cheat sheet). +# Seed each pane with a single hint. We use `clear; printf` (one command) +# rather than a sequence of `send-keys ... Enter ... Enter ...` so the +# pane shows ONE shell prompt at the end instead of one prompt per line. +# The pink/cyan ANSI escapes pick up the same Dracula palette as the rest +# of the session. +seed() { + local target="$1" + local payload="$2" + tmux send-keys -t "${target}" "clear; printf '${payload}'" Enter +} + +# %s escapes: \\033 (ESC), \\n (newline). The whole string is single-quoted +# inside the seed() call so bash does not interpret anything. +seed "${GZ_PANE}" \ +'\033[38;5;213m▙▟ Gazebo Harmonic ▙▟\033[0m\n\033[38;5;245mSpawn the physics world. Paste:\033[0m\n\033[38;5;87m python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo \\\n --model_store /home/ubuntu/PX4-gazebo-models/ --world default\033[0m\n' + +seed "${PX4_PANE}" \ +'\033[38;5;213m▙▟ PX4 v1.16 SITL ▙▟\033[0m\n\033[38;5;245mOnce Gazebo is up, paste (4001 = x500):\033[0m\n\033[38;5;87m PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 \\\n PX4_PARAM_UXRCE_DDS_SYNCT=0 \\\n /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs\033[0m\n' + +seed "${QGC_PANE}" \ +'\033[38;5;213m▙▟ QGroundControl v5.0.8 ▙▟\033[0m\n\033[38;5;245mNeeds an X11-enabled container (default for ./docker/docker_run.sh).\033[0m\n\033[38;5;87m /home/ubuntu/QGroundControl/qgroundcontrol\033[0m\n' + +seed "${COMMON_PANE}" \ +'\033[38;5;213m▙▟ ROS 2 :: common.launch.py ▙▟\033[0m\n\033[38;5;245mXRCE-DDS agent + clock/foxglove bridges + robot_state_publisher + px4_tf + static TF:\033[0m\n\033[38;5;87m ros2 launch px4_ossna_26 common.launch.py\033[0m\n' + +seed "${EXAMPLE_PANE}" \ +'\033[38;5;213m▙▟ ROS 2 :: example launch ▙▟\033[0m\n\033[38;5;245mPick ONE once common.launch.py is up:\033[0m\n\033[38;5;87m ros2 launch offboard_demo offboard_demo.launch.py\n ros2 launch custom_mode_demo custom_mode_demo.launch.py\n ros2 launch aruco_tracker aruco_tracker.launch.py world_name:=aruco model_name:=x500_mono_cam_down_0\n ros2 launch teleop teleop.launch.py\n ros2 run precision_land precision_land --ros-args -p use_sim_time:=true\n ros2 launch precision_land_executor precision_land_executor.launch.py\033[0m\n' + +# Scratch window: title it so the pane-border-format does not render the +# default (container hostname); the welcome banner is the first thing +# attendees see when they switch to this window with Ctrl-b 1. tmux new-window -t "${SESSION}" -n scratch -tmux send-keys -t "${SESSION}:scratch" "clear; workshop-welcome 2>/dev/null || true" Enter +SCRATCH_PANE="$(tmux display-message -p -t "${SESSION}:scratch" '#{pane_id}')" +tmux select-pane -t "${SCRATCH_PANE}" -T "scratch" +tmux send-keys -t "${SCRATCH_PANE}" "clear; workshop-welcome 2>/dev/null || true" Enter # Focus the first pane and attach. tmux select-window -t "${SESSION}:sim" From 7e678aa20366aae034d840b31d5a3d727d1bd749 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Fri, 15 May 2026 14:58:16 -0700 Subject: [PATCH 7/8] workshop-tmux: fix stuck Gazebo pane and re-enable mouse mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues reported on the previous iteration: 1. First (Gazebo) pane stuck showing the literal `clear; printf '\033[38;5;213m▙▟ Gazebo Harmonic ▙▟\033[0m\n...` and never reaching a fresh prompt — typing into the pane just extended that broken command line. Cause: tmux send-keys with a long quoted-printf payload races with the freshly-spawned shell on the first pane (the one already attached to the new-session), so the payload can be torn mid-stream and Enter fires on a partial line. Fix: ship a tiny external helper `workshop-hint TOPIC` that prints the colored card from a Python script. The pane seed is now one short, quote-free command (`clear; workshop-hint gazebo`) instead of a long printf, which sidesteps the entire race. 2. With `mouse off` (set in the previous push to make copy/paste work) attendees could not click panes to focus them either, which they expected to be able to do. Switch back to `mouse on` and tell the welcome banner to advise Shift+drag for copy — Shift+drag bypasses tmux's mouse capture on every common terminal emulator, so attendees still get native selection. Wired the new `workshop-hint` helper into the dev stage of the Dockerfile alongside the existing animation/welcome helpers. --- docker/Dockerfile | 8 ++-- docker/scripts/workshop-hint | 77 +++++++++++++++++++++++++++++++++ docker/scripts/workshop-tmux.sh | 50 +++++++-------------- docker/scripts/workshop-welcome | 1 + 4 files changed, 99 insertions(+), 37 deletions(-) create mode 100755 docker/scripts/workshop-hint diff --git a/docker/Dockerfile b/docker/Dockerfile index 90056ee..7c7bffa 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -82,16 +82,18 @@ COPY --from=dep-ros2 --chown=${USER}:${USER} /root/px4_ros_ws/install /home/${US # retrieve QGC COPY --from=qgc --chown=${USER}:${USER} /home/${USER}/QGroundControl /home/${USER}/QGroundControl -# Workshop tmux launcher + its animation / welcome helpers. Copied into -# /usr/local/bin before switching to the unprivileged user so they end up -# owned by root and exec'able by all. +# Workshop tmux launcher + its animation / hint / welcome helpers. Copied +# into /usr/local/bin before switching to the unprivileged user so they end +# up owned by root and exec'able by all. COPY ./docker/scripts/workshop-tmux.sh /usr/local/bin/workshop-tmux COPY ./docker/scripts/workshop-banner /usr/local/bin/workshop-banner COPY ./docker/scripts/workshop-spinner /usr/local/bin/workshop-spinner +COPY ./docker/scripts/workshop-hint /usr/local/bin/workshop-hint COPY ./docker/scripts/workshop-welcome /usr/local/bin/workshop-welcome RUN chmod +x /usr/local/bin/workshop-tmux \ /usr/local/bin/workshop-banner \ /usr/local/bin/workshop-spinner \ + /usr/local/bin/workshop-hint \ /usr/local/bin/workshop-welcome # Switch to user diff --git a/docker/scripts/workshop-hint b/docker/scripts/workshop-hint new file mode 100755 index 0000000..8b322f6 --- /dev/null +++ b/docker/scripts/workshop-hint @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# workshop-hint TOPIC — print a short colored hint card for one of the +# workshop tmux panes. Used from workshop-tmux.sh so each pane's seed is +# one short, quote-safe command (`workshop-hint gazebo`, etc.) instead of +# a long multi-line printf that races with the freshly-spawned shell. +# +# TOPIC is one of: gazebo, px4, qgc, common, example. + +import sys + +# Dracula-ish ANSI 256-colour indices, matched to the tmux theme. +PINK = "\x1b[38;5;213m" +CYAN = "\x1b[38;5;87m" +GREEN = "\x1b[38;5;156m" +DIM = "\x1b[38;5;245m" +BOLD = "\x1b[1m" +RESET = "\x1b[0m" + + +def card(title: str, blurb: str, commands: list[str]) -> str: + out = [] + out.append(f"{PINK}{BOLD}▙▟ {title} ▙▟{RESET}") + out.append(f"{DIM}{blurb}{RESET}") + for c in commands: + out.append(f"{CYAN} {c}{RESET}") + return "\n".join(out) + + +HINTS = { + "gazebo": card( + "Gazebo Harmonic", + "Spawn the physics world. Paste:", + [ + "python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo \\", + " --model_store /home/ubuntu/PX4-gazebo-models/ --world default", + ], + ), + "px4": card( + "PX4 v1.16 SITL", + "Once Gazebo is up, paste (4001 = x500):", + [ + "PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 \\", + " PX4_PARAM_UXRCE_DDS_SYNCT=0 \\", + " /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs", + ], + ), + "qgc": card( + "QGroundControl v5.0.8", + "Needs an X11-enabled container (default for ./docker/docker_run.sh).", + ["/home/ubuntu/QGroundControl/qgroundcontrol"], + ), + "common": card( + "ROS 2 :: common.launch.py", + "XRCE-DDS agent + clock/foxglove bridges + robot_state_publisher + px4_tf + static TF:", + ["ros2 launch px4_ossna_26 common.launch.py"], + ), + "example": card( + "ROS 2 :: example launch", + "Pick ONE once common.launch.py is up:", + [ + "ros2 launch offboard_demo offboard_demo.launch.py", + "ros2 launch custom_mode_demo custom_mode_demo.launch.py", + "ros2 launch aruco_tracker aruco_tracker.launch.py world_name:=aruco model_name:=x500_mono_cam_down_0", + "ros2 launch teleop teleop.launch.py", + "ros2 run precision_land precision_land --ros-args -p use_sim_time:=true", + "ros2 launch precision_land_executor precision_land_executor.launch.py", + ], + ), +} + +topic = sys.argv[1] if len(sys.argv) > 1 else "" +text = HINTS.get(topic) +if text is None: + sys.stderr.write(f"workshop-hint: unknown topic {topic!r}; " + f"expected one of {', '.join(HINTS)}\n") + sys.exit(2) +print(text) diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh index 91f06ce..0679ebf 100755 --- a/docker/scripts/workshop-tmux.sh +++ b/docker/scripts/workshop-tmux.sh @@ -50,13 +50,12 @@ tmux set -g default-terminal "tmux-256color" # opt into 256/truecolor where supp tmux set -ga terminal-overrides ",xterm-256color:Tc" # tell tmux the outer terminal is true-color tmux set -g pane-border-lines heavy # thicker borders on tmux 3.2+ -# NOTE: mouse mode is deliberately OFF. -# With `mouse on`, tmux captures click-drag and you cannot select text with -# the mouse in the usual terminal way. Leaving it off lets attendees use -# their terminal's native click-drag → copy. The trade-off is no click-to- -# focus on panes — use Ctrl-b arrow / `Ctrl-b q ` to switch panes -# instead. Enable per-session with `tmux set mouse on` if you prefer. -tmux set -g mouse off +# Mouse mode ON so attendees can click panes to focus them and scroll +# back through long-running command output with the wheel. To copy text +# while mouse mode is on, HOLD SHIFT while dragging — every common +# terminal (GNOME, Konsole, Alacritty, Kitty, iTerm2, Terminal.app) +# treats Shift+drag as a native terminal selection that bypasses tmux. +tmux set -g mouse on # --- Dracula-inspired palette (synthwave-y, dev-friendly) --- # bg #282a36 bg-dark #13111c @@ -127,33 +126,16 @@ tmux select-pane -t "${QGC_PANE}" -T "qgc" tmux select-pane -t "${COMMON_PANE}" -T "ros2 common.launch.py" tmux select-pane -t "${EXAMPLE_PANE}" -T "ros2 example launch" -# Seed each pane with a single hint. We use `clear; printf` (one command) -# rather than a sequence of `send-keys ... Enter ... Enter ...` so the -# pane shows ONE shell prompt at the end instead of one prompt per line. -# The pink/cyan ANSI escapes pick up the same Dracula palette as the rest -# of the session. -seed() { - local target="$1" - local payload="$2" - tmux send-keys -t "${target}" "clear; printf '${payload}'" Enter -} - -# %s escapes: \\033 (ESC), \\n (newline). The whole string is single-quoted -# inside the seed() call so bash does not interpret anything. -seed "${GZ_PANE}" \ -'\033[38;5;213m▙▟ Gazebo Harmonic ▙▟\033[0m\n\033[38;5;245mSpawn the physics world. Paste:\033[0m\n\033[38;5;87m python3 /home/ubuntu/PX4-gazebo-models/simulation-gazebo \\\n --model_store /home/ubuntu/PX4-gazebo-models/ --world default\033[0m\n' - -seed "${PX4_PANE}" \ -'\033[38;5;213m▙▟ PX4 v1.16 SITL ▙▟\033[0m\n\033[38;5;245mOnce Gazebo is up, paste (4001 = x500):\033[0m\n\033[38;5;87m PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 \\\n PX4_PARAM_UXRCE_DDS_SYNCT=0 \\\n /home/ubuntu/px4_sitl/bin/px4 -w /home/ubuntu/px4_sitl/romfs\033[0m\n' - -seed "${QGC_PANE}" \ -'\033[38;5;213m▙▟ QGroundControl v5.0.8 ▙▟\033[0m\n\033[38;5;245mNeeds an X11-enabled container (default for ./docker/docker_run.sh).\033[0m\n\033[38;5;87m /home/ubuntu/QGroundControl/qgroundcontrol\033[0m\n' - -seed "${COMMON_PANE}" \ -'\033[38;5;213m▙▟ ROS 2 :: common.launch.py ▙▟\033[0m\n\033[38;5;245mXRCE-DDS agent + clock/foxglove bridges + robot_state_publisher + px4_tf + static TF:\033[0m\n\033[38;5;87m ros2 launch px4_ossna_26 common.launch.py\033[0m\n' - -seed "${EXAMPLE_PANE}" \ -'\033[38;5;213m▙▟ ROS 2 :: example launch ▙▟\033[0m\n\033[38;5;245mPick ONE once common.launch.py is up:\033[0m\n\033[38;5;87m ros2 launch offboard_demo offboard_demo.launch.py\n ros2 launch custom_mode_demo custom_mode_demo.launch.py\n ros2 launch aruco_tracker aruco_tracker.launch.py world_name:=aruco model_name:=x500_mono_cam_down_0\n ros2 launch teleop teleop.launch.py\n ros2 run precision_land precision_land --ros-args -p use_sim_time:=true\n ros2 launch precision_land_executor precision_land_executor.launch.py\033[0m\n' +# Seed each pane with one short, quote-free command: `clear; workshop-hint +# TOPIC`. Earlier attempts at sending a long `printf '...long escape string'` +# via send-keys raced with the freshly-spawned shell and could leave the +# first pane stuck in an unterminated command line. A single tiny invocation +# of an external helper script avoids that entire class of bug. +tmux send-keys -t "${GZ_PANE}" "clear; workshop-hint gazebo" Enter +tmux send-keys -t "${PX4_PANE}" "clear; workshop-hint px4" Enter +tmux send-keys -t "${QGC_PANE}" "clear; workshop-hint qgc" Enter +tmux send-keys -t "${COMMON_PANE}" "clear; workshop-hint common" Enter +tmux send-keys -t "${EXAMPLE_PANE}" "clear; workshop-hint example" Enter # Scratch window: title it so the pane-border-format does not render the # default (container hostname); the welcome banner is the first thing diff --git a/docker/scripts/workshop-welcome b/docker/scripts/workshop-welcome index 9c77d4c..93258fb 100755 --- a/docker/scripts/workshop-welcome +++ b/docker/scripts/workshop-welcome @@ -38,5 +38,6 @@ line(f" {DIM}│{RESET} {CYAN}Ctrl-b arrow{RESET} move focus between line(f" {DIM}│{RESET} {CYAN}Ctrl-b z{RESET} zoom the current pane to full screen (toggle)") line(f" {DIM}│{RESET} {CYAN}Ctrl-b \"{RESET} / {CYAN}Ctrl-b %{RESET} split horizontally / vertically") line(f" {DIM}│{RESET} {CYAN}Ctrl-b d{RESET} detach (sim keeps running); reattach with `workshop-tmux`") +line(f" {DIM}│{RESET} {YELLOW}Shift+drag{RESET} copy text out of any pane (bypasses tmux mouse capture)") line(f" {DIM}└─────────────────────────────────────────────────────────────────────────────{RESET}") line("") From 065c67ebbfb69d284b1f846cf03ff553663ba4a9 Mon Sep 17 00:00:00 2001 From: Nuno Marques Date: Fri, 15 May 2026 15:20:36 -0700 Subject: [PATCH 8/8] workshop-tmux: no-prefix Alt shortcuts for switching windows and panes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reports of "can't select other tabs" — likely a host terminal (often VSCode's built-in one) that eats tmux mouse events or blocks Ctrl-b chords. Add a small set of bare-Alt root-table bindings so attendees always have a working way to navigate, regardless of mouse capture or prefix interception: Alt+1 / Alt+2 switch to window 1 (sim) / 2 (scratch) Alt+Left / Alt+Right previous / next window Alt+h Alt+j Alt+k Alt+l move focus between panes (vim arrows) Alt+z zoom current pane to full screen All bound on the `root` key table (no prefix). The welcome banner's cheat sheet is rewritten to put the no-prefix shortcuts first and the Ctrl-b alternatives below for users who prefer them. --- docker/scripts/workshop-tmux.sh | 14 ++++++++++++++ docker/scripts/workshop-welcome | 15 ++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/docker/scripts/workshop-tmux.sh b/docker/scripts/workshop-tmux.sh index 0679ebf..40c6047 100755 --- a/docker/scripts/workshop-tmux.sh +++ b/docker/scripts/workshop-tmux.sh @@ -97,6 +97,20 @@ tmux set -g status-right "#[fg=#6272a4]┤ #(workshop-spinner) #[fg=#8be9fd,bold # Make the prefix indicator obvious when prefix is held tmux set -g status-keys vi +# --- Easy-to-reach shortcuts (NO PREFIX needed) ----------------------------- +# Some terminals (notably VSCode's built-in one) eat tmux's mouse events or +# block Ctrl-b. Bind a few common navigations to bare Alt-something so they +# work everywhere. +tmux bind -n M-1 select-window -t 0 # Alt+1 -> sim window +tmux bind -n M-2 select-window -t 1 # Alt+2 -> scratch window +tmux bind -n M-Left previous-window # Alt+Left -> prev window +tmux bind -n M-Right next-window # Alt+Right -> next window +tmux bind -n M-h select-pane -L # Alt+h/j/k/l -> move pane +tmux bind -n M-j select-pane -D +tmux bind -n M-k select-pane -U +tmux bind -n M-l select-pane -R +tmux bind -n M-z resize-pane -Z # Alt+z toggles pane zoom + # Build the 5-pane layout described in the header comment. Use stable # pane IDs (#{pane_id}, %0/%1/...) instead of numeric pane_index because # tmux re-numbers pane_index in reading order whenever the layout diff --git a/docker/scripts/workshop-welcome b/docker/scripts/workshop-welcome index 93258fb..fcc78ad 100755 --- a/docker/scripts/workshop-welcome +++ b/docker/scripts/workshop-welcome @@ -32,12 +32,13 @@ line(f"{PINK} /'\\_ _/`\\{RESET}") line(f"{PINK} \\___)=(___/{RESET}") line("") line(f" {DIM}┌─ tmux quick reference ─────────────────────────────────────────────────────{RESET}") -line(f" {DIM}│{RESET} {CYAN}Ctrl-b 0{RESET} / {CYAN}Ctrl-b 1{RESET} switch to window 0 (sim) / 1 (scratch)") -line(f" {DIM}│{RESET} {CYAN}Ctrl-b n{RESET} / {CYAN}Ctrl-b p{RESET} next / previous window") -line(f" {DIM}│{RESET} {CYAN}Ctrl-b arrow{RESET} move focus between panes") -line(f" {DIM}│{RESET} {CYAN}Ctrl-b z{RESET} zoom the current pane to full screen (toggle)") -line(f" {DIM}│{RESET} {CYAN}Ctrl-b \"{RESET} / {CYAN}Ctrl-b %{RESET} split horizontally / vertically") -line(f" {DIM}│{RESET} {CYAN}Ctrl-b d{RESET} detach (sim keeps running); reattach with `workshop-tmux`") -line(f" {DIM}│{RESET} {YELLOW}Shift+drag{RESET} copy text out of any pane (bypasses tmux mouse capture)") +line(f" {DIM}│{RESET} {GREEN}Alt+1{RESET} / {GREEN}Alt+2{RESET} switch to window 1 (sim) / 2 (scratch) {DIM}— no prefix{RESET}") +line(f" {DIM}│{RESET} {GREEN}Alt+←{RESET} / {GREEN}Alt+→{RESET} previous / next window {DIM}— no prefix{RESET}") +line(f" {DIM}│{RESET} {GREEN}Alt+h{RESET} {GREEN}Alt+j{RESET} {GREEN}Alt+k{RESET} {GREEN}Alt+l{RESET} move focus between panes (vim arrows) {DIM}— no prefix{RESET}") +line(f" {DIM}│{RESET} {GREEN}Alt+z{RESET} zoom current pane to full screen {DIM}— no prefix{RESET}") +line(f" {DIM}│{RESET} {CYAN}Ctrl-b 0{RESET} / {CYAN}Ctrl-b 1{RESET} same as Alt+1 / Alt+2 (tmux defaults)") +line(f" {DIM}│{RESET} {CYAN}Ctrl-b \"{RESET} / {CYAN}Ctrl-b %{RESET} split current pane horizontally / vertically") +line(f" {DIM}│{RESET} {CYAN}Ctrl-b d{RESET} detach (sim keeps running); reattach with `workshop-tmux`") +line(f" {DIM}│{RESET} {YELLOW}Shift+drag{RESET} copy text out of any pane (bypasses tmux mouse capture)") line(f" {DIM}└─────────────────────────────────────────────────────────────────────────────{RESET}") line("")