diff --git a/Makefile b/Makefile
index 3e56a282102..c1af01d7a4b 100644
--- a/Makefile
+++ b/Makefile
@@ -68,6 +68,10 @@ bump-lazycore:
record-demo:
demo/record_demo.sh $(filter-out $@,$(MAKECMDGOALS))
+.PHONY: rerecord-demos
+rerecord-demos:
+ demo/rerecord_demos.sh $(filter-out $@,$(MAKECMDGOALS))
+
.PHONY: vendor
vendor:
go mod tidy && go mod vendor
diff --git a/README.md b/README.md
index aafd2768d65..3440c7b52c8 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,8 @@ A simple terminal UI for git commands
[](https://github.com/jesseduffield/lazygit/releases) [](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [](https://app.codacy.com/gh/jesseduffield/lazygit/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [](https://app.codacy.com/gh/jesseduffield/lazygit/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage) [](https://golangci-lint.run/) [](https://github.com/jesseduffield/lazygit/releases/latest) [](https://formulae.brew.sh/formula/lazygit)
-
+
+
@@ -73,7 +74,7 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
- [Elevator Pitch](#elevator-pitch)
- [Table of contents](#table-of-contents)
- [Features](#features)
- - [Stage individual lines](#stage-individual-lines)
+ - [Stage hunks or individual lines](#stage-hunks-or-individual-lines)
- [Interactive Rebase](#interactive-rebase)
- [Cherry-pick](#cherry-pick)
- [Bisect](#bisect)
@@ -133,11 +134,12 @@ Lazygit is not my fulltime job but it is a hefty part time job so if you want to
## Features
-### Stage individual lines
+### Stage hunks or individual lines
-Press `` on a changed file to focus its diff in the main view. Press `` on the selected line to stage it, or press `v` to start selecting a range of lines. You can also press `a` to switch to hunk selection mode. When a file has both staged and unstaged changes, use `` to move between the two diff panes; the same actions stage or unstage the selection depending on the pane.
+Press `0` on a changed file to focus its diff in the main view. The selection covers a whole hunk to begin with, so `` stages that hunk and moves on to the next one. When you want only part of a hunk, press `a` for line-by-line selection and `v` to select a range of lines. What you staged shows up in the pane below. Press `` to move between the two panes; `` unstages in the lower one. Press `c` to commit without leaving the diff.
-
+
+
### Interactive Rebase
@@ -147,65 +149,80 @@ You can also perform any of these actions as a once-off (e.g. pressing `s` on a
This demo also uses shift+down to select a range of commits to move and fixup.
-
+
+
### Cherry-pick
Press `shift+c` on a commit to copy it and press `shift+v` to paste (cherry-pick) it.
-
+
+
### Bisect
Press `b` in the commits view to mark a commit as good/bad in order to begin a git bisect.
-
+
+
### Nuke the working tree
For when you really want to just get rid of anything that shows up when you run `git status` (and yes that includes dirty submodules) [kidpix style](https://www.youtube.com/watch?v=N4E2B_k2Bss), press `shift+d` to bring up the reset options menu and then select the 'nuke' option.
-
+
+
### Amend an old commit
Pressing `shift+a` on any commit will amend that commit with the currently staged changes (running an interactive rebase in the background).
-
+
+
### Filter
You can filter a view with `/`. Here we filter down our branches view and then hit `enter` to view its commits.
-
+
+
### Invoke a custom command
Lazygit has a very flexible [custom command system](docs/Custom_Command_Keybindings.md). In this example a custom command is defined which emulates the built-in branch checkout action.
-
+
+
### Worktrees
You can create worktrees to have multiple branches going at once without the need for stashing or creating WIP commits when switching between them. Press `w` in the branches view to create a worktree from the selected branch and switch to it.
-
+
+
### Rebase magic (custom patches)
You can build a custom patch from an old commit and then remove the patch from the commit, split out a new commit, apply the patch in reverse to the index, and more.
-In this example we have a redundant comment that we want to remove from an old commit. We hit `` on the commit to view its files, then `` on a file to focus its diff. From there, `` adds the selected comment line to the custom patch and `ctrl+p` opens the custom patch options, where we choose to remove the patch from the original commit.
+In this example an old commit contains a change that belongs in a commit of its own. We hit `0` on the commit to focus its diff. `` on the hunk we want to move adds it to the custom patch, and `ctrl+p` opens the custom patch options, where we choose to move the patch into a new commit.
Learn more in the [Rebase magic Youtube tutorial](https://youtu.be/4XaToVut_hs).
-
+
+
+
+If you only want to remove a hunk from an old commit, you don't need a custom patch for that. Select the hunk in the commit's diff and press `d`. Lazygit rewrites the commit without it, running an interactive rebase in the background.
+
+
+
### Rebase from marked base commit
Say you're on a feature branch that was itself branched off of the develop branch, and you've decided you'd rather be branching off the master branch. You need a way to rebase only the commits from your feature branch. In this demo we check to see which was the last commit on the develop branch, then press `shift+b` to mark that commit as our base commit, then press `r` on the master branch to rebase onto it, only bringing across the commits from our feature branch. Then we push our changes with `shift+p`.
-
+
+
### Undo
@@ -214,19 +231,22 @@ Undo uses the reflog which is specific to commits and branches so we can't undo
[More info](/docs/Undoing.md)
-
+
+
### Commit graph
When viewing the commit graph in an enlarged window (use `+` and `_` to cycle screen modes), the commit graph is shown. Colours correspond to the commit authors, and as you navigate down the graph, the parent commits of the selected commit are highlighted.
-
+
+
### Compare two commits
If you press `shift+w` on a commit (or branch/ref) a menu will open that allows you to mark that commit so that any other commit you select will be diffed against it. Once you've selected the second commit, you'll see the diff in the main view and if you press `` you'll see the files of the diff. You can press `shift+w` to view the diff menu again to see options like reversing the diff direction or exiting diff mode. You can also exit diff mode by pressing ``.
-
+
+
### Show GitHub pull requests
diff --git a/demo/config.yml b/demo/config.yml
deleted file mode 100644
index defe50a5bc1..00000000000
--- a/demo/config.yml
+++ /dev/null
@@ -1,112 +0,0 @@
-# Specify a command to be executed
-# like `/bin/bash -l`, `ls`, or any other commands
-# the default is bash for Linux
-# or powershell.exe for Windows
-command: echo "YOU NEED TO SPECIFY YOUR OWN COMMAND WITH THE -d ARG"
-
-# Specify the current working directory path
-# the default is the current working directory path
-cwd: null
-
-# Export additional ENV variables
-env:
- recording: true
-
-# Explicitly set the number of columns
-# or use `auto` to take the current
-# number of columns of your shell
-cols: 120 # 100
-
-# Explicitly set the number of rows
-# or use `auto` to take the current
-# number of rows of your shell
-rows: 35 # 30
-
-# Amount of times to repeat GIF
-# If value is -1, play once
-# If value is 0, loop indefinitely
-# If value is a positive number, loop n times
-repeat: 0
-
-# Quality
-# 1 - 100
-# Higher quality seems to make no difference, but running it through
-# gifsicle ends up with a much better compressed version.
-quality: 100
-
-# Delay between frames in ms
-# If the value is `auto` use the actual recording delays
-frameDelay: auto
-
-# Maximum delay between frames in ms
-# Ignored if the `frameDelay` isn't set to `auto`
-# Set to `auto` to prevent limiting the max idle time
-maxIdleTime: 2000
-
-# The surrounding frame box
-# The `type` can be null, window, floating, or solid`
-# To hide the title use the value null
-# Don't forget to add a backgroundColor style with a null as type
-frameBox:
- type: floating
- title: Lazygit
- style:
- border: 0px black solid
- backgroundColor: "#1d1d1d"
- margin: -5px
-
-# Add a watermark image to the rendered gif
-# You need to specify an absolute path for
-# the image on your machine or a URL, and you can also
-# add your own CSS styles
-watermark:
- imagePath: null
- style:
- position: absolute
- right: 15px
- bottom: 15px
- width: 100px
- opacity: 0.9
-
-# Cursor style can be one of
-# `block`, `underline`, or `bar`
-cursorStyle: block
-
-# Font family
-# You can use any font that is installed on your machine
-# in CSS-like syntax
-# Download from:
-# https://github.com/ryanoasis/nerd-fonts/releases/download/v3.0.2/DejaVuSansMono.zip
-# Not using the mono font because it makes icons too small.
-fontFamily: "DejaVuSansM Nerd Font"
-
-# The size of the font
-fontSize: 8
-
-# The height of lines
-lineHeight: 1
-
-# The spacing between letters
-letterSpacing: 0
-
-# Theme
-theme:
- background: "transparent"
- foreground: "#dddad6"
- cursor: "#c7c7c7"
- black: "#7a7a7a"
- red: "#fc4384"
- green: "#b3e33b"
- yellow: "#ffa727"
- blue: "#102895"
- magenta: "#c930c7"
- cyan: "#00c5c7"
- white: "#c7c7c7"
- brightBlack: "#676767"
- brightRed: "#ff7fac"
- brightGreen: "#c8ed71"
- brightYellow: "#ebdf86"
- brightBlue: "#6871ff"
- brightMagenta: "#ff76ff"
- brightCyan: "#5ffdff"
- brightWhite: "#fffefe"
diff --git a/demo/fonts/FlogSymbolsDemo-Bold.ttf b/demo/fonts/FlogSymbolsDemo-Bold.ttf
new file mode 100644
index 00000000000..864b6449599
Binary files /dev/null and b/demo/fonts/FlogSymbolsDemo-Bold.ttf differ
diff --git a/demo/fonts/FlogSymbolsDemo-Regular.ttf b/demo/fonts/FlogSymbolsDemo-Regular.ttf
new file mode 100644
index 00000000000..0946f38ab7c
Binary files /dev/null and b/demo/fonts/FlogSymbolsDemo-Regular.ttf differ
diff --git a/demo/fonts/LICENSE-FlogSymbols b/demo/fonts/LICENSE-FlogSymbols
new file mode 100644
index 00000000000..af6af6c9c7a
--- /dev/null
+++ b/demo/fonts/LICENSE-FlogSymbols
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 rbong
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/demo/fonts/fit_flog_symbols.py b/demo/fonts/fit_flog_symbols.py
new file mode 100644
index 00000000000..048aaa1e941
--- /dev/null
+++ b/demo/fonts/fit_flog_symbols.py
@@ -0,0 +1,116 @@
+"""Make a copy of the Flog Symbols font whose lines fill a terminal cell.
+
+The demo recordings draw the commit graph with the branch drawing symbols, and
+the terminal that vhs records takes them from the Flog Symbols Demo font in
+demo/fonts. This script made its regular and bold faces from FlogSymbols.ttf of
+https://github.com/rbong/flog-symbols (see LICENSE-FlogSymbols):
+
+ pip install fonttools
+ python3 demo/fonts/fit_flog_symbols.py FlogSymbols.ttf "Flog Symbols Demo" \\
+ demo/fonts
+
+Flog Symbols draws its lines for a cell that is 620 units wide and reaches from
+-206 to 1006 units. A terminal that takes the symbols from a fallback font
+draws them in the cells of its main font, and if those are larger, the lines
+stop short of the cell edges and leave gaps between neighbouring cells.
+
+So move the ends of the strokes that run to an edge of the cell out to the
+terminal's cell edges, and centre everything else in the cell. The circles and
+bends in the middle of the cell keep their shape.
+"""
+
+import os
+import sys
+
+from fontTools.ttLib import TTFont
+
+# The cell of the symbols, in font units
+LEFT, RIGHT, BOTTOM, TOP = -6, 626, -206, 1006
+
+# The width of the terminal cells in the recordings, in font units: at the font
+# size in demo/settings.tape they are 16 pixels wide (SauceCodePro's 14.4
+# pixels, plus the pixel of letter spacing that vhs adds, rounded up), which is
+# 16/24 of an em. Regenerate the font if the font size changes.
+CELL_WIDTH = 667
+
+# How far the horizontal strokes reach into the neighbouring cells, in font
+# units. xterm.js doesn't clip a character to its cell horizontally, so this
+# has to stay short of where the bends in a neighbouring cell begin. In the
+# recordings, less than 30 leaves a dim line where two cells meet, and 35 or
+# more opens a dark gap there.
+HORIZONTAL_OVERLAP = 30
+
+# How far past the top and bottom of the symbols' cell the vertical strokes
+# reach, in font units. xterm.js clips a character to its row, so this only
+# has to be more than the terminal's row sticks out beyond the symbols' cell.
+VERTICAL_REACH = 400
+
+# Points this close to an edge of the symbols' cell belong to the end of a
+# stroke that runs to that edge
+EDGE_TOLERANCE = 30
+
+
+def main():
+ flog_path, family, output_dir = sys.argv[1:4]
+ font = TTFont(flog_path)
+ fit_to_cell(font)
+
+ # The bold face has the same outlines. Without one, the browser makes the
+ # symbols of bold text bold itself by thickening them, and that leaves gaps
+ # where they meet.
+ for style in ("Regular", "Bold"):
+ set_style(font, family, style)
+ font.save(os.path.join(output_dir, f"{family.replace(' ', '')}-{style}.ttf"))
+
+
+def fit_to_cell(font):
+ glyf = font["glyf"]
+
+ # Centre the symbols in the terminal's cell
+ dx = round((CELL_WIDTH - (RIGHT + LEFT)) / 2)
+
+ for name in font.getGlyphOrder():
+ glyph = glyf[name]
+ if glyph.numberOfContours <= 0:
+ continue
+ coordinates = glyph.coordinates
+ for i, (x, y) in enumerate(coordinates):
+ if x <= LEFT + EDGE_TOLERANCE:
+ x = -HORIZONTAL_OVERLAP
+ elif x >= RIGHT - EDGE_TOLERANCE:
+ x = CELL_WIDTH + HORIZONTAL_OVERLAP
+ else:
+ x += dx
+ if y <= BOTTOM + EDGE_TOLERANCE:
+ y -= VERTICAL_REACH
+ elif y >= TOP - EDGE_TOLERANCE:
+ y += VERTICAL_REACH
+ coordinates[i] = (x, y)
+ glyph.recalcBounds(glyf)
+ font["hmtx"][name] = (CELL_WIDTH, glyph.xMin)
+
+
+def set_style(font, family, style):
+ bold = style == "Bold"
+ postscript_name = f"{family.replace(' ', '')}-{style}"
+ full_name = family if not bold else f"{family} {style}"
+ for record in font["name"].names:
+ if record.nameID == 1:
+ record.string = family
+ elif record.nameID == 2:
+ record.string = style
+ elif record.nameID == 4:
+ record.string = full_name
+ elif record.nameID in (3, 6):
+ record.string = postscript_name
+
+ os2 = font["OS/2"]
+ os2.usWeightClass = 700 if bold else 400
+ fs_bold, fs_regular = 1 << 5, 1 << 6
+ os2.fsSelection &= ~(fs_bold | fs_regular)
+ os2.fsSelection |= fs_bold if bold else fs_regular
+ font["head"].macStyle = 1 if bold else 0
+
+
+if __name__ == "__main__":
+ main()
diff --git a/demo/record_demo.sh b/demo/record_demo.sh
index 97d5c2f3631..88b43cbead3 100755
--- a/demo/record_demo.sh
+++ b/demo/record_demo.sh
@@ -2,55 +2,72 @@
set -e
-TYPE=$1
-TEST=$2
+# The repository the demo is uploaded to. GitHub only plays videos that live in
+# its own attachment store, and an attachment is tied to one repository.
+REPO=jesseduffield/lazygit
+
+# The issue that collects the demo recordings. Posting a comment there is what
+# makes an uploaded video readable by people who are not signed in to GitHub.
+# It can stay closed; commenting on a closed issue publishes the video just as
+# well, and does not reopen it.
+PUBLISH_ISSUE=6051
usage() {
- echo "Usage: $0 [gif|mp4] "
- echo "e.g. using full path: $0 gif pkg/integration/tests/demo/nuke_working_tree.go"
+ echo "Usage: $0 [--no-upload] "
+ echo "e.g. $0 pkg/integration/tests/demo/nuke_working_tree.go"
+ echo
+ echo "--no-upload leaves the video in demo/output and stops there, for"
+ echo "checking how a change to demo/settings.tape turns out."
exit 1
}
-if [ "$#" -ne 2 ]
-then
- usage
-fi
+UPLOAD=true
-if [ "$TYPE" != "gif" ] && [ "$TYPE" != "mp4" ]
+if [ "$1" = "--no-upload" ]
then
- usage
- exit 1
+ UPLOAD=false
+ shift
fi
-if [ -z "$TEST" ]
+TEST=$1
+
+if [ "$#" -ne 1 ]
then
usage
fi
-WORKTREE_PATH=$(git worktree list | grep assets | awk '{print $1}')
+TOOLS="vhs ttyd ffmpeg"
-if [ -z "$WORKTREE_PATH" ]
+if [ "$UPLOAD" = true ]
then
- echo "Could not find assets worktree. You'll need to create a worktree for the assets branch using the following command:"
- echo "git worktree add .worktrees/assets assets"
- echo "The assets branch has no shared history with the main branch: it exists to store assets which are too large to store in the main branch."
- exit 1
+ TOOLS="$TOOLS gh"
fi
-OUTPUT_DIR="$WORKTREE_PATH/demo"
+for TOOL in $TOOLS
+do
+ if ! command -v "$TOOL" > /dev/null 2>&1
+ then
+ echo "$TOOL could not be found"
+ echo "Install it with: brew install $TOOL"
+ exit 1
+ fi
+done
-if ! command -v terminalizer &> /dev/null
+if [ "$UPLOAD" = true ]
then
- echo "terminalizer could not be found"
- echo "Install it with: npm install -g terminalizer"
- exit 1
-fi
+ WORKTREE_PATH=$(git worktree list | grep assets | awk '{print $1}')
-if ! command -v "gifsicle" &> /dev/null
-then
- echo "gifsicle could not be found"
- echo "Install it with: npm install -g gifsicle"
- exit 1
+ if [ -z "$WORKTREE_PATH" ]
+ then
+ echo "Could not find assets worktree. You'll need to create a worktree for the assets branch using the following command:"
+ echo "git worktree add .worktrees/assets assets"
+ echo "The assets branch has no shared history with the main branch: it exists to store assets which are too large to store in the main branch."
+ exit 1
+ fi
+
+ OUTPUT_DIR="$WORKTREE_PATH/demo"
+else
+ OUTPUT_DIR=demo/output
fi
# Get last part of the test path and set that as the output name
@@ -63,19 +80,179 @@ go generate pkg/integration/tests/tests.go
mkdir -p "$OUTPUT_DIR"
-# First we record the demo into a yaml representation
-terminalizer -c demo/config.yml record --skip-sharing -d "go run cmd/integration_test/main.go cli --slow $TEST" "$OUTPUT_DIR/$NAME"
-# Then we render it into a gif
-terminalizer render "$OUTPUT_DIR/$NAME" -o "$OUTPUT_DIR/$NAME.gif"
+SCRATCH=$(mktemp -d)
+trap 'rm -rf "$SCRATCH"' EXIT
+
+TAPE="$SCRATCH/$NAME.tape"
+RECORDING="$SCRATCH/$NAME.mp4"
+OUTPUT="$OUTPUT_DIR/$NAME.mp4"
+
+# Start recording once lazygit has drawn the top left corner of a view frame.
+# Demos run in whichever screen mode they ask for, so no particular panel is
+# on screen for all of them, but every view is drawn with a frame. This is the
+# corner that `border: rounded` draws; a demo config that turns borders off
+# would need a different signal.
+#
+# The two quotes in the end marker keep the literal VHSDONE out of the command
+# line, so that waiting for the marker cannot match the command that prints it.
+#
+# The screen is cleared before lazygit is started, so that the screen the
+# terminal restores when lazygit exits is a blank one rather than the typed
+# command line. The frames vhs records after lazygit has exited are then blank
+# apart from the marker, which is what the trimming below looks for.
+cat > "$TAPE" </dev/null |
+ # ffmpeg writes these numbers with a decimal point, so keep awk in a
+ # locale that reads them back that way. The + 0 turns them from strings
+ # into numbers, without which awk compares them as text.
+ LC_ALL=C awk -v blank="$BLANK_INK" '
+ /pts_time:/ { split($3, a, ":"); time = a[2] }
+ /YAVG=/ { split($0, b, "="); n++; at[n] = time + 0; ink[n] = b[2] + 0 }
+ END {
+ # Walk back over the blank frames at the end. Starting from the
+ # end leaves a blank frame that the demo painted over again where
+ # it is, so only the tail after lazygit is cut.
+ k = n + 1
+ while (k > 1 && ink[k - 1] < blank) {
+ k--
+ }
+ if (k <= n) {
+ printf "%.3f\n", at[k]
+ }
+ }')
+
+if [ -n "$CUT" ]
then
- COMPRESSED_PATH="$OUTPUT_DIR/$NAME.mp4"
- ffmpeg -y -i "$OUTPUT_DIR/$NAME.gif" -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "$COMPRESSED_PATH"
+ TRIM="-t $CUT"
else
- COMPRESSED_PATH="$OUTPUT_DIR/$NAME-compressed.gif"
- gifsicle --colors 256 --use-col=web -O3 < "$OUTPUT_DIR/$NAME.gif" > "$COMPRESSED_PATH"
+ TRIM=
+fi
+
+# The video ends on the last frame of the demo. A browser goes on showing that
+# frame once it has played to the end, so it needs no padding out. Move the
+# moov atom to the front so that playback can start before the whole file has
+# downloaded.
+# shellcheck disable=SC2086
+ffmpeg -y -loglevel error -i "$RECORDING" $TRIM \
+ -vf "pad=iw:ih+$CAPTION_CLEARANCE:0:0:color=$BACKGROUND" \
+ -c:v libx264 -crf 23 -preset slow -pix_fmt yuv420p \
+ -movflags +faststart -an "$OUTPUT"
+
+if [ "$UPLOAD" = false ]
+then
+ echo "Demo recorded to $OUTPUT"
+ exit 0
+fi
+
+# GitHub's web editor posts to this endpoint when you drag a file into a
+# comment box. It is undocumented, but it accepts an ordinary token, so we can
+# upload from here. You need push access to $REPO for it to work. If the
+# endpoint ever goes away, drag the video into a comment box on github.com
+# instead and copy the URL that GitHub inserts.
+REPOSITORY_ID=$(gh api "repos/$REPO" --jq .id)
+
+RESPONSE=$(curl --silent --show-error --fail \
+ --request POST \
+ --header "Authorization: Bearer $(gh auth token)" \
+ --header "Accept: application/json" \
+ --header "Content-Type: video/mp4" \
+ --data-binary "@$OUTPUT" \
+ "https://uploads.github.com/user-attachments/assets?name=$NAME.mp4&content_type=video%2Fmp4&repository_id=$REPOSITORY_ID")
+
+URL=$(echo "$RESPONSE" | sed -e 's/.*"url":"//' -e 's/".*//')
+
+if [ -z "$URL" ]
+then
+ echo "Could not read an attachment URL out of GitHub's response:"
+ echo "$RESPONSE"
+ exit 1
+fi
+
+# An attachment stays private until a posted comment somewhere in the
+# repository refers to it. Until that happens the video is a 404 for anyone who
+# is not signed in, and the README shows a broken player. Referring to it once
+# makes it public for good, even if the comment is deleted afterwards, so we
+# collect the recordings in one issue and leave the comments in place.
+gh api "repos/$REPO/issues/$PUBLISH_ISSUE/comments" \
+ --raw-field "body=$NAME
+
+$URL" > /dev/null
+
+# Make sure that worked before handing over a URL, because the person recording
+# the demo is signed in and will not see the failure.
+ATTEMPT=0
+while [ "$ATTEMPT" -lt 30 ]
+do
+ if curl --silent --fail --output /dev/null --max-time 20 --range 0-1 "$URL"
+ then
+ break
+ fi
+ ATTEMPT=$((ATTEMPT + 1))
+ sleep 2
+done
+
+if [ "$ATTEMPT" -eq 30 ]
+then
+ echo "$URL is still not readable without signing in to GitHub."
+ echo "Embedding it now would give logged-out readers a broken player."
+ exit 1
fi
-echo "Demo recorded to $COMPRESSED_PATH"
+echo "Demo recorded to $OUTPUT"
+echo
+echo "Embed it with:"
+echo ""
diff --git a/demo/rerecord_demos.sh b/demo/rerecord_demos.sh
new file mode 100755
index 00000000000..db5c7140cf9
--- /dev/null
+++ b/demo/rerecord_demos.sh
@@ -0,0 +1,113 @@
+#!/bin/sh
+
+set -e
+
+# Re-records the demos that a page embeds and points the page at the new
+# videos. Use it when a change to demo/settings.tape, or to lazygit's
+# appearance, leaves the existing recordings looking out of date.
+#
+# A page names the demo behind each video in a comment above it:
+#
+#
+#
+#
+# GitHub drops that comment when it renders the page, so it costs the reader
+# nothing. This script re-records the demo the comment names and rewrites the
+# URL on the line below it.
+
+usage() {
+ echo "Usage: $0 [--no-upload] [page ...]"
+ echo "e.g. $0 README.md"
+ echo
+ echo "Re-records every demo the given pages embed. With no page given,"
+ echo "that means README.md."
+ echo
+ echo "--no-upload leaves the videos in demo/output and the pages untouched,"
+ echo "which is what you want for reviewing a change to demo/settings.tape."
+ exit 1
+}
+
+NO_UPLOAD=
+
+if [ "$1" = "--no-upload" ]
+then
+ NO_UPLOAD=--no-upload
+ shift
+fi
+
+if [ "$1" = "-h" ] || [ "$1" = "--help" ]
+then
+ usage
+fi
+
+if [ ! -x demo/record_demo.sh ]
+then
+ echo "Run this from the root of the repository."
+ exit 1
+fi
+
+if [ "$#" -eq 0 ]
+then
+ set -- README.md
+fi
+
+for PAGE in "$@"
+do
+ if [ ! -f "$PAGE" ]
+ then
+ echo "$PAGE does not exist"
+ exit 1
+ fi
+
+ NAMES=$(sed -n 's/^$/\1/p' "$PAGE")
+
+ if [ -z "$NAMES" ]
+ then
+ echo "$PAGE embeds no demos. Each video needs a "
+ echo "comment on the line above it to say which demo it came from."
+ exit 1
+ fi
+
+ for NAME in $NAMES
+ do
+ TEST="pkg/integration/tests/demo/$NAME.go"
+
+ if [ ! -f "$TEST" ]
+ then
+ echo "$PAGE asks for a demo called $NAME, but $TEST does not exist"
+ exit 1
+ fi
+
+ echo
+ echo "=== $NAME ==="
+
+ if [ -n "$NO_UPLOAD" ]
+ then
+ demo/record_demo.sh --no-upload "$TEST"
+ continue
+ fi
+
+ # Keep the recording chatter on screen, since a full run takes a while,
+ # and read the new URL back out of it afterwards.
+ LOG=$(mktemp)
+ demo/record_demo.sh "$TEST" | tee "$LOG"
+ URL=$(sed -n 's/.*