Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ node_modules
.venv
.idea/

/resources/demos/output/

.yarn/*
.pnp.*
!.yarn/cache
Expand Down
40 changes: 37 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,40 @@ If you use a tiling extension, switching the style trigger from **Maximized wind
## Screenshot
![gradient-bar-after](https://user-images.githubusercontent.com/3801306/236593253-bce6342f-67d4-4e68-9c1e-85db33074dfe.png)

## Demos

**Proximity blend:** the panel blends into the alternate style as a window approaches, then back as it moves away.

<video controls src="resources/demos/proximity.mp4" width="720">
<a href="resources/demos/proximity.mp4">Watch the proximity blend demo</a>
</video>

**Touch detection, 0 px:** an unmaximized window touches the panel to activate the alternate style.

<video controls src="resources/demos/touch.mp4" width="720">
<a href="resources/demos/touch.mp4">Watch the touch detection demo</a>
</video>

**Workspace transition:** swipe between workspaces with different panel styles for a smooth transition.

<video controls src="resources/demos/workspace.mp4" width="720">
<a href="resources/demos/workspace.mp4">Watch the workspace transition demo</a>
</video>

**Maximized mode:** maximize and restore a window to switch panel styles.

<video controls src="resources/demos/maximized.mp4" width="720">
<a href="resources/demos/maximized.mp4">Watch the maximized mode demo</a>
</video>

**Tiling, 0 px:** snap a window to the left half to activate the alternate style, then restore and move it away.

<video controls src="resources/demos/tiling.mp4" width="720">
<a href="resources/demos/tiling.mp4">Watch the tiling demo</a>
</video>

The clips use the included [wallpaper](resources/demos/wallpaper.jpg), a black-to-transparent normal style, and a gold-to-teal alternate style. See the [demo guide](resources/demos/README.md) to reproduce them.


# Compatibility

Expand Down Expand Up @@ -127,9 +161,9 @@ The project includes several scripts to help with development:

- [ ] Add box shadow settings
- [ ] Fix an issue where "Desktop icons" extension creates a weird window that messes up the dynamic toolbar colour effect.
- [ ] Add a demo video
- [ ] Figure out reliable testing methods or at least unit tests
- [ ] Make it work with something like tiling when the window occupies the whole top horizontally (to test with Tiling Assistant)
- [x] Add demo videos
- [x] Figure out reliable testing methods or at least unit tests - the [demo recording scripts](resources/demos/README.md) provide reproducible scenarios for visual testing.
- [x] Make it work with something like tiling when the window occupies the whole top horizontally - proximity mode detects tiled windows near or touching the panel, resolving the styling issue without relying on maximized-window detection.

# Credits
This extension is a fork of [the Original Gradient Top Bar extension](https://extensions.gnome.org/extension/1264/gradient-top-bar/) by [Julien/jpec](https://peclu.net/).
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"author": "Petar Vasilev <petar@pshow.org>",
"type": "module",
"scripts": {
"demo": "bash resources/demos/scripts/record.sh",
"demo:check": "bash resources/demos/scripts/check.sh",
"demo:export": "bash resources/demos/scripts/export.sh",
"nested-wayland": "dbus-run-session -- gnome-shell --devkit --wayland",
"pref-debug": "journalctl -f -o cat /usr/bin/gjs",
"open-prefs": "gnome-extensions prefs gradienttopbar@pshow.org",
Expand Down
70 changes: 70 additions & 0 deletions resources/demos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Demo recordings

The recorder in `scripts/` reproduces the extension demos in a disposable GNOME Shell 51 Wayland devkit session. It uses `wallpaper.jpg`, a vertical black-to-transparent normal panel, and a horizontal gold-to-teal alternate panel.

Final MP4s, the wallpaper, and scripts belong in Git. Generated masters and logs live under `output/`, which is ignored. Extension packaging includes only `src/`, so none of these demo assets enter the extension ZIP.

## Requirements

Recording needs GNOME Shell 51 with Mutter devkit, GJS, D-Bus tools, `glib-compile-schemas`, working PipeWire screencasting, and GStreamer's `x264enc` and `matroskamux` plugins. Run from an existing Wayland desktop. The preferences recorder uses the GNOME Shell libraries installed under `/usr/lib/gnome-shell`.

Exporting needs FFmpeg with `libx264`. Validation also needs Node.js, Yarn, `gst-launch-1.0`, and `ffprobe`. No additional Node or Python packages are needed for recording.

## Record

From the project root:

```sh
yarn demo all
```

`scripts/record.sh` records the six scenarios sequentially and stops on the first failure. Omitting the scenario also records all six. Each run creates `output/<timestamp>/masters/` and `output/<timestamp>/logs/`. To choose the run directory:

```sh
yarn demo all resources/demos/output/my-run
```

Run one scenario by name. The optional second argument is a run directory:

```sh
yarn demo proximity
yarn demo tiling resources/demos/output/tiling-run
yarn demo preferences resources/demos/output/preferences-run
```

| Scenario | Approximate length | What it records |
| --- | --- | --- |
| `proximity` | 6 seconds | A window approaches the panel, blends into the alternate style, then moves away. |
| `touch` | 4 seconds | With a 0 px distance, an unmaximized window touches the panel and moves away. |
| `workspace` | 6 seconds | A native workspace swipe moves between alternate and normal panel styles, then back. |
| `maximized` | 4 seconds | A window maximizes and restores. |
| `tiling` | 4–5 seconds | With proximity at 0 px, a floating window snaps to the left half, touches the panel and activates the alternate style, then restores. |
| `preferences` | 5 seconds | The actual Behaviour page switches to proximity, sets the distance to 0 and 100 px, and enables the gradual transition. |

The recorder checks the private Shell PID, D-Bus bus, extension path, and Wayland display before controlling the session. Settings use private XDG directories, and cleanup closes the private Shell. The printed `/tmp/gradienttopbar-demo.*` profile is retained for troubleshooting; session and Shell logs are also saved in the run's `logs/` folder.

The tiling demo uses GNOME's native Super+Left shortcut, sent through a virtual keyboard on the private Shell's seat. After untiling, it moves the window away from the panel so the normal style returns. It checks the snapped and restored window geometry and stops if either operation fails. No third-party tiling extension is needed. Watch the recorded [tiling clip](tiling.mp4).

Capture uses the built-in GNOME recorder at native 1024×640 resolution, requesting 30 fps. H.264 encoding uses zero quantization, 4:4:4 color, the ultrafast preset, and two encoder threads. Encoding is lossless after conversion to YUV color. The original recording is copied into `masters/` without resizing or re-encoding.

The preferences scenario is included, but its clip is still pending: the previous run stopped before capture because the private monitor reported 480×480 instead of 1024×640. The monitor check remains enforced.

## Export for the README

Export the available masters without starting a desktop session:

```sh
yarn demo:export resources/demos/output/my-run resources/demos/output/my-run/review
```

The exporter creates native-resolution H.264 MP4s at 30 fps with CRF 14 and 4:2:0 color for browser playback. The main README embeds each MP4 with a video element and a direct link for renderers that do not support it. Encoding uses two threads and a lower scheduling priority.

Review the exports, then replace the corresponding MP4 files directly under `resources/demos/`. If the export directory is omitted, that is the default destination. Existing exports and masters are never overwritten by the scripts. Only scenarios with a master are exported.

## Validate without recording

```sh
yarn demo:check
```

This checks Bash syntax and JavaScript lint, verifies the lossless encoder with synthetic frames, checks MP4 export metadata and output files, and verifies that the exporter refuses to overwrite an existing file. It does not launch GNOME or capture the desktop.
Binary file added resources/demos/maximized.mp4
Binary file not shown.
Binary file added resources/demos/proximity.mp4
Binary file not shown.
57 changes: 57 additions & 0 deletions resources/demos/scripts/check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Syntax, lint, and a synthetic encoder check: never start GNOME or capture the desktop.
set -euo pipefail
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
for script in "$script_dir/"*.sh; do
bash -n "$script"
done
for script in "$script_dir/"*.js; do
node --check "$script"
done
cd -- "$script_dir/../../.."
yarn exec eslint resources/demos/scripts/*.js

# Exercise the recorder's actual pipeline and compare its decoded pixels with the input.
pipeline=$(node --input-type=module - "$script_dir/record.js" <<'JS'
import { readFileSync } from 'node:fs';
const source = readFileSync(process.argv[2], 'utf8');
const match = source.match(/'pipeline': new GLib\.Variant\('s', '([^']+)'\)/);
if (!match) throw new Error('Recording pipeline not found');
process.stdout.write(match[1]);
JS
)
read -r -a pipeline_elements <<<"$pipeline"
test_dir=$(mktemp -d /tmp/gradienttopbar-quality.XXXXXX)
trap 'rm -rf -- "$test_dir"' EXIT
nice -n 10 timeout 20s gst-launch-1.0 -q \
videotestsrc num-buffers=8 pattern=snow \
! video/x-raw,format=Y444,width=320,height=200,framerate=30/1 ! tee name=t \
t. ! queue ! filesink location="$test_dir/source.yuv" \
t. ! queue ! "${pipeline_elements[@]}" ! filesink location="$test_dir/lossless.mkv"
nice -n 10 ffmpeg -hide_banner -loglevel error -n -threads 2 -i "$test_dir/lossless.mkv" \
-pix_fmt yuv444p -f rawvideo "$test_dir/decoded.yuv"
cmp -- "$test_dir/source.yuv" "$test_dir/decoded.yuv"

mkdir -- "$test_dir/masters"
# The new tiling scenario must export through the same pipeline as the other demos.
mv -- "$test_dir/lossless.mkv" "$test_dir/masters/tiling.mkv"
bash "$script_dir/export.sh" "$test_dir" "$test_dir/exports"
ffprobe -v error -select_streams v:0 \
-show_entries stream=codec_name,pix_fmt,width,height,nb_frames,r_frame_rate -of json \
"$test_dir/exports/tiling.mp4" >"$test_dir/export.json"
node --input-type=module - "$test_dir/export.json" "$test_dir/exports" <<'JS'
import assert from 'node:assert/strict';
import { readFileSync, readdirSync } from 'node:fs';
const [video] = JSON.parse(readFileSync(process.argv[2], 'utf8')).streams;
assert.deepEqual(video, {
codec_name: 'h264', width: 320, height: 200, pix_fmt: 'yuv420p',
r_frame_rate: '30/1', nb_frames: '8'
});
assert.deepEqual(readdirSync(process.argv[3]), ['tiling.mp4'], 'Export must produce only an MP4');
JS
cp -- "$test_dir/exports/tiling.mp4" "$test_dir/unchanged.mp4"
if bash "$script_dir/export.sh" "$test_dir" "$test_dir/exports" >"$test_dir/overwrite.log" 2>&1; then
printf 'Exporter unexpectedly accepted an existing output.\n' >&2; exit 1;
fi
cmp -- "$test_dir/unchanged.mp4" "$test_dir/exports/tiling.mp4"
printf 'Demo syntax, lint, lossless encoder, and MP4 export checks passed. No desktop sessions or captures started.\n'
34 changes: 34 additions & 0 deletions resources/demos/scripts/export.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Export available lossless masters as MP4s for the README.
set -euo pipefail

script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
[[ $# -ge 1 && $# -le 2 ]] || { printf 'Usage: %s RUN_DIRECTORY [export-directory]\n' "$0" >&2; exit 1; }
run_dir=$(realpath -e -- "$1")
output_dir=$(realpath -m -- "${2:-$script_dir/..}")
command -v ffmpeg >/dev/null || { printf 'Missing command: ffmpeg\n' >&2; exit 1; }
shopt -s nullglob
masters=("$run_dir/masters/"*.mkv)
[[ ${#masters[@]} -gt 0 ]] || { printf 'No masters found in %s/masters\n' "$run_dir" >&2; exit 1; }

for master in "${masters[@]}"; do
name=$(basename -- "$master" .mkv)
case $name in
proximity|touch|workspace|maximized|tiling|preferences) ;;
*) printf 'Unknown demo master: %s\n' "$master" >&2; exit 1 ;;
esac
[[ ! -e $output_dir/$name.mp4 && ! -L $output_dir/$name.mp4 ]] || {
printf 'Refusing to overwrite %s/%s.mp4\n' "$output_dir" "$name" >&2; exit 1;
}
done
mkdir -p -- "$output_dir"
staging_dir=$(mktemp -d "$run_dir/export.XXXXXX")
trap 'rm -rf -- "$staging_dir"' EXIT
for master in "${masters[@]}"; do
name=$(basename -- "$master" .mkv)
nice -n 10 ffmpeg -hide_banner -loglevel error -n -threads 2 -i "$master" \
-map 0:v:0 -vf fps=30 -c:v libx264 -preset slow -crf 14 -pix_fmt yuv420p \
-threads 2 -an -map_metadata -1 -movflags +faststart "$staging_dir/$name.mp4"
cp --update=none-fail -- "$staging_dir/$name.mp4" "$output_dir/$name.mp4"
printf 'Exported %s/%s.mp4\n' "$output_dir" "$name"
done
Loading
Loading