Compare commits

..

12 Commits
side ... main

Author SHA1 Message Date
jess bbd068e829 I concede on the Wayland display server always-on-top support. Not my battle. Not likely to be supported (by wayland). 2026-04-27 22:09:36 -07:00
jess 06c7ec0f4d wayland - ARE YOU IN THERE? (im not angry, im cupping my hands like im speaking into a cave) 2026-04-26 00:41:26 -07:00
jess af5f80da2c As kurt vile would say, "pretty cool" 2026-04-25 23:19:09 -07:00
jess 0cde65839c diagnose failure to stash or restore layer states across sessions on flatpak 2026-04-25 14:39:10 -07:00
jess 6f3461730e logs 2026-04-25 13:55:51 -07:00
jess 24054058ad Let's try something else (wayland) 2026-04-25 13:49:11 -07:00
jess 6f31cf779b Wayland 2026-04-25 13:38:48 -07:00
jess 1d4c9c96cb These might come in handy ;D whoops 2026-04-25 04:00:29 -07:00
jess 3cfaa7d370 Cleanup, reorganize scripts. 2026-04-25 00:34:35 -07:00
jess d96ce81cd7 Update README.md
Hol' up, im still working the kinks out of linux. I am realizing the real distinction these days has more to do with flatpak vs source vs appimage. Better linux instructions coming up
2026-04-25 06:47:27 +00:00
jess b6d37fd890 I might be fucking shit up right now, hold up
Merge remote-tracking branch 'refs/remotes/origin/main'
2026-04-24 13:02:41 -07:00
Jess 1498d78286 fixed linux build script (perms) 2026-04-23 14:26:01 -07:00
20 changed files with 1184 additions and 74 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[alias]
xtask = "run --release --package xtask --"

View File

@ -1,3 +1,8 @@
[workspace]
members = [".", "xtask"]
default-members = ["."]
resolver = "2"
[package]
name = "layers"
version = "0.1.0-dev"
@ -65,7 +70,12 @@ pretty_assertions = "1"
tempfile = "3"
[features]
default = ["x11"]
debug = []
# Linux front-end providers — exactly one selected at build time.
# build.sh detects the running WM and sets this for you.
x11 = ["dep:x11-dl"]
wayland = []
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.58", features = [
@ -75,4 +85,4 @@ windows = { version = "0.58", features = [
] }
[target.'cfg(target_os = "linux")'.dependencies]
x11-dl = "2"
x11-dl = { version = "2", optional = true }

View File

@ -1,18 +1,21 @@
# Layers
A Photoshop-style logical-layer panel for KiCad 10. Group items into named logical layers,
hide them as a unit, rename/colour/lock/annotate, run set ops (merge/subtract/intersect),
snap to grid, snapshot and restore — all live against an open `pcbnew` session over KiCad's
IPC API.
An image maniplulation program-style logical-layer panel for KiCad 10.
You can add or subtract your selection to and from the layer, or to and from the board. You can hide and unhide each layer cleanly. You can rename/colour/lock/annotate, sort and organize the layers, a layer can also have childen.
There are also boolean operations (merge/subtract/intersect) between layers (logical), snap to grid, and more to come. Operations are all composed together as one commit and can easily be undone with Edit->Undo or equivalent.
Rendering of in-panel board previews is powered by [Siphon](https://git.else-if.org/jess/Siphon),
a pure-Rust KiCad-board-to-vectors crate. No shell-outs, no `kicad-cli`.
a pure-Rust KiCad-board-to-vectors crate. You would not technically need KiCad installed to produce the vectors, it is a completely isolated utility which makes no calls at all to kicad-cli.
## Install
Every platform installs into KiCad 10's 3rd-party plugin directory. After install, open
KiCad's **Plugin and Content Manager** and rescan, or relaunch KiCad; the Layers toolbar
button appears in `pcbnew`.
Every platform installs into KiCad 10's *10.0/plugins* directory (on Linux it'll also drop into 11.0, 12.0, etc. if you've already got those, native or flatpak, both). I'm 90% sure that's where they are supposed to go.
`cargo xtask install` picks the right script and runs it. `cargo xtask build` stages without installing. `install-linux` / `install-macos` / `install-windows` force a specific platform if you ever need that.
Close KiCad completely prior to running the install script or KiCad might crash, and you might lose work. Close KiCad completely when you update the plugin too. Turns out there's a lot of differences between python and Rust plugins, though maybe the better distiction is IPC plugins in general. I am yet to make a python IPC plugin though, so I can't say for certain.
### macOS
@ -22,16 +25,14 @@ to regenerate icons from `resources/Layers.svg`).
```sh
git clone https://git.else-if.org/jess/Layers
cd Layers
./scripts/install.sh
cargo xtask install
```
Installs to `~/Documents/KiCad/10.0/plugins/com.jesshunter.layers/`.
Installs to `~/Documents/KiCad/10.0/plugins/com.jesshunter.layers/`
### Windows (10 / 11, ARM64 or x86_64)
A single native Rust binary — no .NET, no WinUI, no Visual Studio. Mica backdrop
on Windows 11 22H2+ via `DwmSetWindowAttribute`, falls back gracefully on older
Windows. Both ARM64 and x86_64 hosts work the same way.
Grrr. It works. I'm still too tickled to talk about it. It did some real spiteful shit to me. I refused to install visual studio on my VM, and it never forgave me for that.
#### 1. Install MSYS2
@ -43,7 +44,7 @@ Download and run the installer from <https://www.msys2.org>. Accept the defaults
In the MSYS2 shell:
```sh
# ARM64 (Snapdragon X, Surface Pro 11, etc.)
# ARM64 (Apple M[x], Snapdragon X, Surface Pro 11, etc.)
pacman -Syu
pacman -S --needed \
mingw-w64-clang-aarch64-toolchain \
@ -108,14 +109,12 @@ Close and reopen every PowerShell / cmd window after this so they pick up the ch
```bat
git clone https://git.else-if.org/jess/Layers
cd Layers
install.bat
cargo xtask install
```
Installs to `%USERPROFILE%\Documents\KiCad\10.0\plugins\com.jesshunter.layers\`
(or the OneDrive-redirected equivalent).
No external runtime required — the exe is statically linked against the MSYS2
clangarm64 / ucrt64 toolchain. Just ship the folder.
### Linux — Debian / Ubuntu / Pop!_OS (apt)
@ -131,10 +130,10 @@ source "$HOME/.cargo/env"
git clone https://git.else-if.org/jess/Layers
cd Layers
./scripts/install-linux.sh
cargo xtask install
```
Installs to `~/.local/share/kicad/10.0/3rdparty/plugins/com.jesshunter.layers/`.
Installs to `~/.local/share/kicad/10.0/plugins/com.jesshunter.layers/`
### Linux — Arch / Manjaro (pacman)
@ -149,10 +148,14 @@ source "$HOME/.cargo/env"
git clone https://git.else-if.org/jess/Layers
cd Layers
./scripts/install-linux.sh
cargo xtask install
```
Installs to `~/.local/share/kicad/10.0/3rdparty/plugins/com.jesshunter.layers/`.
Installs to `~/.local/share/kicad/10.0/plugins/com.jesshunter.layers/`
In both cases, it checks both the standard directory and the Flatpak directory. If you for some reason have both, it places it in both. If for some reason you have both but only want it in one of them, you can just delete it from the one you don't want it in.
The Flatpak plugins dir is ` ~/.var/app/org.kicad.KiCad/data/kicad/10.0/plugins/com.jesshunter.layers/`
## Configuration

BIN
resources/icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
resources/icon-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,28 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
case "$(uname -s)" in
Linux) : ;;
*) echo "use scripts/install.sh on macOS or install.bat on Windows" >&2; exit 1;;
esac
bash "$ROOT/build-linux.sh"
INSTALL_ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/kicad/10.0/3rdparty/plugins"
INSTALL_DIR="$INSTALL_ROOT/com.jesshunter.layers"
mkdir -p "$INSTALL_ROOT"
# Preserve user state (state/, cache/, logs/, settings.json) across reinstall.
rm -rf "$INSTALL_DIR/bin" "$INSTALL_DIR/resources" "$INSTALL_DIR/plugin.json" "$INSTALL_DIR/LICENCE"
mkdir -p "$INSTALL_DIR/bin" "$INSTALL_DIR/resources"
STAGE="$ROOT/build/bin/com.jesshunter.layers"
cp -R "$STAGE/bin/." "$INSTALL_DIR/bin/"
[ -f "$STAGE/plugin.json" ] && cp "$STAGE/plugin.json" "$INSTALL_DIR/plugin.json"
[ -f "$STAGE/LICENCE" ] && cp "$STAGE/LICENCE" "$INSTALL_DIR/LICENCE"
cp -R "$STAGE/resources/." "$INSTALL_DIR/resources/"
echo "installed: $INSTALL_DIR"

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
render_plugin_json() {
@ -30,7 +30,26 @@ if command -v rsvg-convert >/dev/null 2>&1 && [ -f "$ROOT/resources/Layers.svg"
done
fi
cargo build --release --bin layers
# Detect the running window-manager / display protocol to pick the front-end
# provider that gets compiled into the binary. Override with LAYERS_FEATURES.
detect_wm_feature() {
if [ -n "${LAYERS_FEATURES:-}" ]; then
echo "$LAYERS_FEATURES"
return
fi
# Native Wayland session — use the wayland provider (winit xdg-toplevel,
# render transparency, user pins on top via compositor).
if [ -n "${WAYLAND_DISPLAY:-}" ]; then
echo "wayland"
return
fi
# X11 session or no WAYLAND_DISPLAY: use the X11 provider.
echo "x11"
}
FEATURE="$(detect_wm_feature)"
echo "build: front-end provider = $FEATURE (XDG_CURRENT_DESKTOP=${XDG_CURRENT_DESKTOP:-<unset>}, WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-<unset>})"
cargo build --release --bin layers --no-default-features --features "$FEATURE"
cp "$ROOT/target/release/layers" "$APPDIR/Layers"
chmod +x "$APPDIR/Layers"

66
scripts/linux/install.sh Executable file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
case "$(uname -s)" in
Linux) ;;
*) echo "wrong platform: $(uname -s) — use cargo xtask install" >&2; exit 1;;
esac
STAGE="$ROOT/build/bin/com.jesshunter.layers"
PLUGIN_ID="com.jesshunter.layers"
# Installs into every existing KiCad >=10 plugins dir — native and flatpak.
# Does not mkdir kicad/<ver>/plugins; KiCad creates it on first launch.
install_to_kicad_root() {
local kicad_root="$1"
[ -d "$kicad_root" ] || return 1
local installed=0
for ver_dir in "$kicad_root"/*/; do
local ver
ver="$(basename "$ver_dir")"
[[ "$ver" =~ ^([0-9]+)\.0$ ]] || continue
(( ${BASH_REMATCH[1]} >= 10 )) || continue
local plugins_dir="${ver_dir}plugins"
[ -d "$plugins_dir" ] || continue
local target="$plugins_dir/$PLUGIN_ID"
rm -rf "$target/bin" "$target/resources" "$target/plugin.json" "$target/LICENCE"
mkdir -p "$target/bin" "$target/resources"
cp -R "$STAGE/bin/." "$target/bin/"
[ -f "$STAGE/plugin.json" ] && cp "$STAGE/plugin.json" "$target/plugin.json"
[ -f "$STAGE/LICENCE" ] && cp "$STAGE/LICENCE" "$target/LICENCE"
cp -R "$STAGE/resources/." "$target/resources/"
echo "installed: $target"
installed=1
done
return $((1 - installed))
}
NATIVE_ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/kicad"
FLATPAK_ROOT="$HOME/.var/app/org.kicad.KiCad/data/kicad"
ok=0
# Native install: build with the front-end matching the host's WM (build.sh
# autodetects unless LAYERS_FEATURES is set).
if [ -d "$NATIVE_ROOT" ]; then
bash "$ROOT/scripts/linux/build.sh"
install_to_kicad_root "$NATIVE_ROOT" && ok=1 || true
fi
# Flatpak install: force the X11 front-end.
if [ -d "$FLATPAK_ROOT" ]; then
LAYERS_FEATURES=x11 bash "$ROOT/scripts/linux/build.sh"
install_to_kicad_root "$FLATPAK_ROOT" && ok=1 || true
fi
if [ "$ok" -eq 0 ]; then
echo "no KiCad 10+ install detected. checked:" >&2
echo " $NATIVE_ROOT/<ver>/plugins/" >&2
echo " $FLATPAK_ROOT/<ver>/plugins/" >&2
echo "launch KiCad once first so it creates its data dirs, then re-run." >&2
exit 1
fi

100
scripts/macos/build.sh Executable file
View File

@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
# Renders plugin.json.in -> $2 with @ENTRYPOINT@ replaced by $1.
render_plugin_json() {
local entrypoint="$1"
local out="$2"
local in="$ROOT/plugin.json.in"
if [ ! -f "$in" ]; then
echo "ERROR: $in not found" >&2
exit 1
fi
sed "s|@ENTRYPOINT@|$entrypoint|g" "$in" > "$out"
}
STAGE="$ROOT/build/bin/com.jesshunter.layers"
APP="$STAGE/bin/Layers.app"
CONTENTS="$APP/Contents"
MACOS_DIR="$CONTENTS/MacOS"
RESOURCES_DIR="$CONTENTS/Resources"
rm -rf "$STAGE"
mkdir -p "$STAGE/bin" "$STAGE/resources" "$MACOS_DIR" "$RESOURCES_DIR"
# Icon PNGs for the KiCad toolbar — rendered straight from Layers.svg.
if command -v rsvg-convert >/dev/null 2>&1 && [ -f "$ROOT/resources/Layers.svg" ]; then
for size in 24 48; do
rsvg-convert --width "$size" --height "$size" \
"$ROOT/resources/Layers.svg" -o "$ROOT/resources/icon-${size}.png"
done
fi
# Local path overrides for sibling checkouts (kicad-ipc-rs).
# Injected only while this script runs; Cargo.toml stays clean for commits.
CARGO_TOML="$ROOT/Cargo.toml"
CARGO_BAK=""
PATCH_APPLIED=0
if [ -d "$ROOT/../kicad-ipc-rs" ] || [ -d "$ROOT/../Siphon" ]; then
CARGO_BAK="$(mktemp "${TMPDIR:-/tmp}/Cargo.toml.orig.XXXXXX")"
cp "$CARGO_TOML" "$CARGO_BAK"
{
if [ -d "$ROOT/../kicad-ipc-rs" ]; then
printf '\n[patch."https://git.else-if.org/jess/kicad-ipc-rs.git"]\n'
printf 'kicad-ipc-rs = { path = "../kicad-ipc-rs" }\n'
echo "using local ../kicad-ipc-rs (patch injected transiently)" >&2
fi
if [ -d "$ROOT/../Siphon" ]; then
printf '\n[patch."https://git.else-if.org/jess/Siphon.git"]\n'
printf 'siphon = { path = "../Siphon" }\n'
echo "using local ../Siphon (patch injected transiently)" >&2
fi
} >> "$CARGO_TOML"
PATCH_APPLIED=1
trap 'if [ $PATCH_APPLIED -eq 1 ] && [ -f "$CARGO_BAK" ]; then cp "$CARGO_BAK" "$CARGO_TOML"; rm -f "$CARGO_BAK"; fi' EXIT
fi
# Rust staticlib.
cargo build --release
RUST_LIB="$ROOT/target/release"
if [ ! -f "$RUST_LIB/liblayers.a" ]; then
echo "ERROR: liblayers.a not found at $RUST_LIB" >&2
exit 1
fi
SDK="$(xcrun --show-sdk-path)"
RUST_FLAGS=(-import-objc-header "$ROOT/include/layers.h" -L "$RUST_LIB" -llayers)
# Swift shell → Layers.app/Contents/MacOS/Layers
swiftc \
-target arm64-apple-macosx14.0 \
-sdk "$SDK" \
"${RUST_FLAGS[@]}" \
-framework Cocoa \
-framework Metal \
-framework MetalKit \
-framework QuartzCore \
-framework CoreGraphics \
-framework CoreFoundation \
-framework CoreVideo \
-O \
-o "$MACOS_DIR/Layers" \
"$ROOT"/src/*.swift
cp "$ROOT/Info.plist" "$CONTENTS/Info.plist"
# Adhoc sign so Gatekeeper lets it launch.
codesign --force --sign - "$APP"
# KiCad plugin payload alongside the .app bundle.
render_plugin_json "bin/Layers.app/Contents/MacOS/Layers" "$STAGE/plugin.json"
cp -R "$ROOT/resources/." "$STAGE/resources/"
if [ -f "$ROOT/LICENCE" ]; then
cp "$ROOT/LICENCE" "$STAGE/LICENCE"
fi
echo "staged: $STAGE"
echo "app: $APP"

25
scripts/macos/install.sh Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
case "$(uname -s)" in
Darwin) ;;
*) echo "wrong platform: $(uname -s) — use cargo xtask install" >&2; exit 1;;
esac
INSTALL_DIR="$HOME/Documents/KiCad/10.0/plugins/com.jesshunter.layers"
bash "$ROOT/scripts/macos/build.sh"
rm -rf "$INSTALL_DIR/bin" "$INSTALL_DIR/resources" "$INSTALL_DIR/plugin.json" "$INSTALL_DIR/LICENCE"
mkdir -p "$INSTALL_DIR/bin" "$INSTALL_DIR/resources"
STAGE="$ROOT/build/bin/com.jesshunter.layers"
cp -R "$STAGE/bin/Layers.app" "$INSTALL_DIR/bin/Layers.app"
cp "$STAGE/plugin.json" "$INSTALL_DIR/plugin.json"
cp -R "$STAGE/resources/." "$INSTALL_DIR/resources/"
[ -f "$STAGE/LICENCE" ] && cp "$STAGE/LICENCE" "$INSTALL_DIR/LICENCE"
echo "installed: $INSTALL_DIR"

View File

@ -6,7 +6,7 @@ rem ARM64 -> aarch64-pc-windows-gnullvm (clangarm64 llvm-mingw toolchain)
rem x86_64 -> x86_64-pc-windows-gnu (ucrt64 gcc-mingw toolchain)
rem LAYERS_RUST_TARGET overrides.
pushd %~dp0
pushd %~dp0..\..
set "ROOT=%CD%"
if defined LAYERS_RUST_TARGET (

View File

@ -1,10 +1,10 @@
@echo off
setlocal enabledelayedexpansion
pushd %~dp0
pushd %~dp0..\..
set "ROOT=%CD%"
call "%ROOT%\build.bat"
call "%ROOT%\scripts\windows\build.bat"
if %ERRORLEVEL% neq 0 (
echo build failed
popd
@ -15,21 +15,16 @@ rem Resolve the real Documents folder (respects OneDrive / folder redirection).
for /f "usebackq delims=" %%i in (`powershell -NoProfile -Command "[Environment]::GetFolderPath('MyDocuments')"`) do set "DOCS=%%i"
if not defined DOCS set "DOCS=%USERPROFILE%\Documents"
set "INSTALL_ROOT=%DOCS%\KiCad\10.0\plugins"
set "INSTALL_DIR=%INSTALL_ROOT%\com.jesshunter.layers"
if not exist "%INSTALL_ROOT%" mkdir "%INSTALL_ROOT%" >nul 2>&1
set "INSTALL_DIR=%DOCS%\KiCad\10.0\plugins\com.jesshunter.layers"
rem Preserve user state (state/, cache/, logs/, settings.json) across reinstall.
if exist "%INSTALL_DIR%\bin" rmdir /s /q "%INSTALL_DIR%\bin"
if exist "%INSTALL_DIR%\resources" rmdir /s /q "%INSTALL_DIR%\resources"
if exist "%INSTALL_DIR%\bin" rmdir /s /q "%INSTALL_DIR%\bin"
if exist "%INSTALL_DIR%\resources" rmdir /s /q "%INSTALL_DIR%\resources"
if exist "%INSTALL_DIR%\plugin.json" del /q "%INSTALL_DIR%\plugin.json"
if exist "%INSTALL_DIR%\LICENCE" del /q "%INSTALL_DIR%\LICENCE"
set "STAGE=%ROOT%\build\bin\com.jesshunter.layers"
xcopy /e /i /y /q "%STAGE%" "%INSTALL_DIR%" >nul
xcopy /e /i /y /q "%STAGE%" "%INSTALL_DIR%" >nul
echo.
echo installed: %INSTALL_DIR%
echo.
popd
endlocal

View File

@ -29,7 +29,7 @@ struct LayersColors {
return result
}
/// Minimal key=value extractor for the flat subset we care about.
/// key=value extractor for the subset.
/// Returns dotted "section.key" numeric value.
private static func flatten(_ source: String) -> [String: Double] {
var out: [String: Double] = [:]

View File

@ -264,8 +264,21 @@ impl App {
}
crate::ipc::Event::OpenDocuments(paths) => {
if let Some(pcb) = paths.into_iter().next() {
let canonical = pcb
.canonicalize()
.ok()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "<canonicalize-failed>".to_string());
let hash = crate::paths::project_hash(&pcb);
let state_path = crate::paths::state_path(&hash);
tracing::info!(
pcb_raw = %pcb.display(),
pcb_canonical = %canonical,
project_hash = %hash,
state_path = %state_path.display(),
state_exists = state_path.exists(),
"open document: resolving project state"
);
self.current_project = Some(ProjectContext {
project_hash: hash,
kicad_pcb_path: pcb,
@ -274,6 +287,11 @@ impl App {
self.has_pruned_orphans = false;
match crate::layers::persistence::load(&state_path) {
Ok(tree) => {
tracing::info!(
state_path = %state_path.display(),
layer_count = tree.layers.len(),
"loaded layer tree from disk"
);
self.layer_tree = tree;
self.rebuild_panel_content();
}

View File

@ -27,15 +27,80 @@ fn main() {
tracing::info!(
plugin_root = ?plugin_root,
data_dir = ?layers::paths::data_dir(),
log_dir = ?layers::paths::log_dir(),
"layers shell: starting native event loop",
);
log_session_env();
tracing::info!(provider = PROVIDER_NAME, "shell route: provider chosen at build time");
#[cfg(all(target_os = "linux", feature = "wayland"))]
tracing::info!(provider = PROVIDER_NAME, "wayland: using winit xdg-toplevel (compositor provides pin-on-top via title bar)");
if let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)) {
tracing::error!("layers shell panicked: {e:?}");
std::process::exit(1);
}
}
#[cfg(all(target_os = "linux", feature = "wayland"))]
const PROVIDER_NAME: &str = "linux/wayland (winit + xdg-toplevel)";
#[cfg(all(target_os = "linux", feature = "x11", not(feature = "wayland")))]
const PROVIDER_NAME: &str = "linux/x11 (winit + xlib)";
#[cfg(all(target_os = "linux", not(feature = "x11"), not(feature = "wayland")))]
compile_error!("Linux build requires exactly one of: --features x11, --features wayland");
#[cfg(target_os = "macos")]
const PROVIDER_NAME: &str = "macos (winit + appkit)";
#[cfg(target_os = "windows")]
const PROVIDER_NAME: &str = "windows (winit + dwm)";
fn log_window_kind(handle: &raw_window_handle::RawWindowHandle) -> &'static str {
use raw_window_handle::RawWindowHandle::*;
match handle {
Wayland(_) => "wayland",
Xlib(_) => "xlib",
Xcb(_) => "xcb",
Win32(_) => "win32",
AppKit(_) => "appkit",
UiKit(_) => "uikit",
_ => "other",
}
}
fn log_display_kind(handle: &raw_window_handle::RawDisplayHandle) -> &'static str {
use raw_window_handle::RawDisplayHandle::*;
match handle {
Wayland(_) => "wayland",
Xlib(_) => "xlib",
Xcb(_) => "xcb",
Windows(_) => "windows",
AppKit(_) => "appkit",
UiKit(_) => "uikit",
_ => "other",
}
}
fn log_session_env() {
let keys = [
"WAYLAND_DISPLAY",
"DISPLAY",
"XDG_SESSION_TYPE",
"XDG_CURRENT_DESKTOP",
"XDG_SESSION_DESKTOP",
"DESKTOP_SESSION",
"WINIT_UNIX_BACKEND",
"GDK_BACKEND",
"QT_QPA_PLATFORM",
];
for k in keys {
match std::env::var(k) {
Ok(v) => tracing::info!(env = k, value = %v, "session env"),
Err(_) => tracing::info!(env = k, value = "<unset>", "session env"),
}
}
}
fn run() {
let event_loop = EventLoop::new().expect("winit: create event loop");
event_loop.set_control_flow(ControlFlow::Wait);
@ -87,8 +152,21 @@ impl ApplicationHandler for ShellApp {
.with_inner_size(LogicalSize::new(DEFAULT_LOGICAL_SIZE.0, DEFAULT_LOGICAL_SIZE.1))
.with_min_inner_size(LogicalSize::new(MIN_LOGICAL_SIZE.0, MIN_LOGICAL_SIZE.1))
.with_transparent(true)
.with_decorations(true)
.with_window_level(WindowLevel::AlwaysOnTop);
.with_decorations(true);
// On Wayland, always-on-top is the user's choice (only) I mulled it over. Why fight intent?
// Who am I to decide what's important? I have thought of a way to do it though, I think. So if any
// wayland users DO end up asking me for this feature, I'll add it.
// I'd add a cfg to the board change poll that only exists on wayland detected builds, it would use
// xdg activation to steal back top level. I think it would work, but it might be too
// invasive if it has to take focus as well as return to top. All around, wayland just isn't
// meant to do this. Sorry Ubuntu/Gnome/Pop_OS! users. If you use wayland and have any
// ideas,
// please do let me know.
// On other platforms it is set programmatically.
#[cfg(not(all(target_os = "linux", feature = "wayland")))]
let attrs = attrs.with_window_level(WindowLevel::AlwaysOnTop);
let window = event_loop
.create_window(attrs)
@ -110,6 +188,12 @@ impl ApplicationHandler for ShellApp {
.expect("winit: display handle")
.as_raw();
tracing::info!(
window_handle = log_window_kind(&raw_window),
display_handle = log_display_kind(&raw_display),
"winit window up"
);
tracing::info!(
"creating viewport handle: {}x{} @ {}",
inner.width,
@ -328,7 +412,7 @@ fn set_window_alpha(window: &Window, alpha: f32) {
}
}
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", feature = "x11"))]
fn set_window_alpha(window: &Window, alpha: f32) {
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
use std::ffi::CString;
@ -379,7 +463,12 @@ fn set_window_alpha(window: &Window, alpha: f32) {
}
}
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
///
#[cfg(any(
target_os = "macos",
target_os = "ios",
all(target_os = "linux", not(feature = "x11")),
))]
fn set_window_alpha(_window: &Window, _alpha: f32) {}
#[cfg(target_os = "windows")]

View File

@ -427,7 +427,6 @@ fn finalise_handle(
let caps = surface.get_capabilities(&adapter);
let format = caps.formats.first().copied()?;
// PostMultiplied so the Metal layer's alpha blends against the transparent NSWindow.
let alpha_mode = if caps
.alpha_modes
.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
@ -445,6 +444,27 @@ fn finalise_handle(
.unwrap_or(wgpu::CompositeAlphaMode::Auto)
};
let adapter_info = adapter.get_info();
tracing::info!(
backend = ?adapter_info.backend,
device_name = %adapter_info.name,
format = ?format,
all_formats = ?caps.formats,
all_alpha_modes = ?caps.alpha_modes,
chosen_alpha_mode = ?alpha_mode,
"wgpu surface configured"
);
if matches!(
alpha_mode,
wgpu::CompositeAlphaMode::Opaque | wgpu::CompositeAlphaMode::Auto
) {
tracing::warn!(
"wgpu chose {alpha_mode:?} — surface won't be transparent. \
X11 needs PreMultiplied/PostMultiplied. Try a different wgpu backend (set WGPU_BACKEND=gl or =vulkan) \
or check that the WM has a compositor running."
);
}
surface.configure(
&device,
&wgpu::SurfaceConfiguration {
@ -589,8 +609,12 @@ fn render(handle: &mut ViewportHandle) {
ui.draw(&mut handle.renderer, &theme, &style, handle.cursor);
handle.cache = ui.into_cache();
// NSWindow.alphaValue in Swift fades the composited surface; keep this opaque.
let background = crate::ui::theme::compositor_clear();
// macOS/Windows/X11: the OS fades the composited surface via window-level alpha.
// Wayland: no window-level opacity; clear with alpha=0 for transparent background.
// background; the UI content renders opaque on top.
let mut background = crate::ui::theme::compositor_clear();
#[cfg(all(target_os = "linux", feature = "wayland"))]
{ background.a = 0.0; }
handle.renderer.present(
Some(background),
handle.format,
@ -618,13 +642,23 @@ fn persist_if_due(app: &mut App) {
return;
}
let state_path = paths::state_path(&project.project_hash);
let layer_count = app.layer_tree.layers.len();
match crate::layers::persistence::save(&app.layer_tree, &state_path) {
Ok(()) => {
tracing::debug!(
state_path = %state_path.display(),
layer_count,
"saved layer tree"
);
app.dirty = false;
app.last_save = Some(std::time::Instant::now());
}
Err(e) => {
tracing::warn!("layer state save failed: {e}");
tracing::warn!(
state_path = %state_path.display(),
layer_count,
"layer state save failed: {e}"
);
app.last_save = Some(std::time::Instant::now());
}
}
@ -671,10 +705,28 @@ pub fn init_native_shell(plugin_root: Option<&std::path::Path>) -> anyhow::Resul
paths::create_dirs_if_missing()?;
let _ = init_logging_file_only();
dump_invocation_context_file_only();
probe_data_dir_writable();
crate::ui::colors::init(plugin_root);
Ok(())
}
fn probe_data_dir_writable() {
let dir = paths::data_dir();
let probe = dir.join(".write-probe");
match std::fs::write(&probe, b"ok") {
Ok(()) => {
let _ = std::fs::remove_file(&probe);
tracing::info!(data_dir = %dir.display(), "data dir is writable");
}
Err(e) => {
tracing::error!(
data_dir = %dir.display(),
"data dir NOT writable: {e} — state and logs won't persist; check flatpak --filesystem=home"
);
}
}
}
fn init_logging_file_only() -> anyhow::Result<()> {
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
let filter = EnvFilter::try_from_default_env()

View File

@ -5,6 +5,7 @@ use sha2::{Digest, Sha256};
const PLUGIN_ID: &str = "com.jesshunter.layers";
const KICAD_MAJOR_DIR: &str = "KiCad/10.0/plugins";
const USER_DATA_DOTDIR: &str = ".layers";
pub fn plugin_data_dir() -> PathBuf {
plugin_root()
@ -14,8 +15,18 @@ pub fn plugin_dir() -> PathBuf {
plugin_root()
}
/// User-writable runtime data root: logs, state, cache, settings, runtime sockets.
/// Decoupled from the plugin install location so a flatpak KiCad (which sandboxes
/// its own data dirs) still writes somewhere accessible from a normal shell.
pub fn data_dir() -> PathBuf {
plugin_root()
if let Ok(v) = std::env::var("LAYERS_DATA_DIR") {
if !v.is_empty() {
return PathBuf::from(v);
}
}
dirs::home_dir()
.map(|h| h.join(USER_DATA_DOTDIR))
.unwrap_or_else(|| PathBuf::from("/tmp").join(USER_DATA_DOTDIR))
}
pub fn state_dir() -> PathBuf {

655
src/wayland_shell.rs Normal file
View File

@ -0,0 +1,655 @@
use crate::ffi::ViewportHandle;
use raw_window_handle::{
RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle,
};
use smithay_client_toolkit::{
compositor::{CompositorHandler, CompositorState},
delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer,
delegate_registry, delegate_seat,
output::{OutputHandler, OutputState},
registry::{ProvidesRegistryState, RegistryState},
registry_handlers,
seat::{
keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers},
pointer::{PointerEvent, PointerEventKind, PointerHandler},
Capability, SeatHandler, SeatState,
},
shell::{
wlr_layer::{
Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
LayerSurfaceConfigure,
},
WaylandSurface,
},
};
use std::ptr::NonNull;
use wayland_client::{
globals::{registry_queue_init, GlobalList},
protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface},
Connection, Proxy, QueueHandle,
};
const DEFAULT_LOGICAL_SIZE: (u32, u32) = (480, 640);
const MIN_LOGICAL_SIZE: (u32, u32) = (380, 220);
const ANCHOR_MARGIN: i32 = 24;
/// Run the Wayland (sctk + wlr-layer-shell) front end. Returns `true` if it took
/// over the session, `false` if init failed (wayland socket unreachable, no
/// layer-shell, etc.) so the caller can fall through to the winit path. Hard
/// exiting here would silently kill the plugin process inside KiCad — which the
/// user sees as "didn't load."
pub fn try_run() -> bool {
tracing::info!("wayland: try_run entered");
let conn = match probe_connection() {
Some(c) => c,
None => {
tracing::warn!(
"wayland: no reachable compositor socket; falling through to winit"
);
return false;
}
};
let (globals, mut event_queue) = match registry_queue_init::<State>(&conn) {
Ok(pair) => {
tracing::info!("wayland: registry_queue_init OK");
pair
}
Err(e) => {
tracing::warn!("wayland: registry init failed ({e}); falling through to winit");
return false;
}
};
let qh = event_queue.handle();
log_globals(&globals);
let compositor = match CompositorState::bind(&globals, &qh) {
Ok(c) => {
tracing::info!("wayland: bound wl_compositor");
c
}
Err(e) => {
tracing::warn!("wayland: wl_compositor missing ({e}); falling through to winit");
return false;
}
};
let layer_shell = match LayerShell::bind(&globals, &qh) {
Ok(l) => {
tracing::info!("wayland: bound zwlr_layer_shell_v1");
l
}
Err(e) => {
tracing::warn!(
"wayland: zwlr_layer_shell_v1 not advertised ({e}); falling through to winit"
);
return false;
}
};
let surface = compositor.create_surface(&qh);
let layer = layer_shell.create_layer_surface(
&qh,
surface,
Layer::Overlay,
Some("layers"),
None,
);
layer.set_anchor(Anchor::TOP | Anchor::RIGHT);
layer.set_size(DEFAULT_LOGICAL_SIZE.0, DEFAULT_LOGICAL_SIZE.1);
layer.set_margin(ANCHOR_MARGIN, ANCHOR_MARGIN, 0, 0);
layer.set_keyboard_interactivity(KeyboardInteractivity::OnDemand);
layer.commit();
tracing::info!(
anchor = "TOP|RIGHT",
size = ?(DEFAULT_LOGICAL_SIZE.0, DEFAULT_LOGICAL_SIZE.1),
margin_top = ANCHOR_MARGIN,
margin_right = ANCHOR_MARGIN,
"wayland: layer surface committed (waiting for first configure)"
);
let raw_display = RawDisplayHandle::Wayland(WaylandDisplayHandle::new(
match NonNull::new(conn.backend().display_ptr() as *mut _) {
Some(p) => p,
None => {
tracing::warn!("wayland: null display ptr; falling through to winit");
return false;
}
},
));
let raw_window = RawWindowHandle::Wayland(WaylandWindowHandle::new(
match NonNull::new(layer.wl_surface().id().as_ptr() as *mut _) {
Some(p) => p,
None => {
tracing::warn!("wayland: null surface ptr; falling through to winit");
return false;
}
},
));
let mut state = State {
registry_state: RegistryState::new(&globals),
seat_state: SeatState::new(&globals, &qh),
output_state: OutputState::new(&globals, &qh),
viewport: None,
layer,
keyboard: None,
pointer: None,
modifiers: Modifiers::default(),
last_pointer: (0.0, 0.0),
width: DEFAULT_LOGICAL_SIZE.0,
height: DEFAULT_LOGICAL_SIZE.1,
scale: 1,
first_configure: true,
raw_display,
raw_window,
focused: false,
hovered: false,
exit: false,
};
tracing::info!("wayland: layer-shell up, entering dispatch loop");
while !state.exit {
if let Err(e) = event_queue.blocking_dispatch(&mut state) {
tracing::error!("wayland dispatch: {e}");
break;
}
}
true
}
struct State {
registry_state: RegistryState,
seat_state: SeatState,
output_state: OutputState,
/// Held above `layer` so the wgpu surface drops before the wl_surface.
viewport: Option<ViewportHandle>,
layer: LayerSurface,
keyboard: Option<wl_keyboard::WlKeyboard>,
pointer: Option<wl_pointer::WlPointer>,
modifiers: Modifiers,
last_pointer: (f32, f32),
width: u32,
height: u32,
scale: i32,
first_configure: bool,
raw_display: RawDisplayHandle,
raw_window: RawWindowHandle,
focused: bool,
hovered: bool,
exit: bool,
}
impl State {
fn ensure_viewport(&mut self) {
if self.viewport.is_some() {
return;
}
let scale = self.scale.max(1) as f32;
let logical_w = self.width.max(MIN_LOGICAL_SIZE.0) as f32;
let logical_h = self.height.max(MIN_LOGICAL_SIZE.1) as f32;
match ViewportHandle::new_from_raw(self.raw_window, self.raw_display, logical_w, logical_h, scale) {
Some(h) => {
tracing::info!(
"wayland: viewport ready ({}x{} @ {})",
logical_w,
logical_h,
scale
);
self.viewport = Some(h);
}
None => tracing::error!("wayland: ViewportHandle::new_from_raw returned None"),
}
}
fn render(&mut self, qh: &QueueHandle<Self>) {
if let Some(vp) = self.viewport.as_mut() {
vp.set_window_alpha(crate::ui::theme::window_alpha(self.focused, self.hovered));
vp.render_frame();
}
let surface = self.layer.wl_surface();
surface.frame(qh, surface.clone());
self.layer.commit();
}
}
impl CompositorHandler for State {
fn scale_factor_changed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
new_factor: i32,
) {
if self.scale == new_factor {
return;
}
self.scale = new_factor;
self.layer.wl_surface().set_buffer_scale(new_factor);
if let Some(vp) = self.viewport.as_mut() {
vp.resize_px(self.width as f32, self.height as f32, new_factor as f32);
}
}
fn transform_changed(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_surface::WlSurface,
_: wl_output::Transform,
) {
}
fn frame(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
_: &wl_surface::WlSurface,
_time: u32,
) {
self.render(qh);
}
fn surface_enter(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_surface::WlSurface,
_: &wl_output::WlOutput,
) {
}
fn surface_leave(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_surface::WlSurface,
_: &wl_output::WlOutput,
) {
}
}
impl OutputHandler for State {
fn output_state(&mut self) -> &mut OutputState {
&mut self.output_state
}
fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
}
impl LayerShellHandler for State {
fn closed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &LayerSurface) {
self.exit = true;
}
fn configure(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
_layer: &LayerSurface,
configure: LayerSurfaceConfigure,
serial: u32,
) {
let (w, h) = configure.new_size;
let new_w = if w == 0 { self.width } else { w };
let new_h = if h == 0 { self.height } else { h };
let resized = new_w != self.width || new_h != self.height;
tracing::info!(
serial,
requested_w = w,
requested_h = h,
applied_w = new_w,
applied_h = new_h,
first = self.first_configure,
"wayland: configure"
);
self.width = new_w;
self.height = new_h;
self.ensure_viewport();
if resized {
if let Some(vp) = self.viewport.as_mut() {
vp.resize_px(self.width as f32, self.height as f32, self.scale.max(1) as f32);
}
}
if self.first_configure {
self.first_configure = false;
tracing::info!("wayland: first configure → kicking initial render");
self.render(qh);
}
}
}
impl SeatHandler for State {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
fn new_capability(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
seat: wl_seat::WlSeat,
capability: Capability,
) {
if capability == Capability::Keyboard && self.keyboard.is_none() {
if let Ok(kb) = self.seat_state.get_keyboard(qh, &seat, None) {
self.keyboard = Some(kb);
}
}
if capability == Capability::Pointer && self.pointer.is_none() {
if let Ok(p) = self.seat_state.get_pointer(qh, &seat) {
self.pointer = Some(p);
}
}
}
fn remove_capability(
&mut self,
_conn: &Connection,
_: &QueueHandle<Self>,
_: wl_seat::WlSeat,
capability: Capability,
) {
if capability == Capability::Keyboard {
if let Some(k) = self.keyboard.take() {
k.release();
}
}
if capability == Capability::Pointer {
if let Some(p) = self.pointer.take() {
p.release();
}
}
}
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
}
impl KeyboardHandler for State {
fn enter(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_surface: &wl_surface::WlSurface,
_: u32,
_: &[u32],
_keysyms: &[Keysym],
) {
self.focused = true;
}
fn leave(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_surface: &wl_surface::WlSurface,
_: u32,
) {
self.focused = false;
}
fn press_key(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: u32,
event: KeyEvent,
) {
if let Some(vp) = self.viewport.as_mut() {
let (named, utf8) = map_key_event(&event);
vp.push_key_event(named, utf8, encode_modifiers(&self.modifiers), true);
}
}
fn release_key(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: u32,
event: KeyEvent,
) {
if let Some(vp) = self.viewport.as_mut() {
let (named, utf8) = map_key_event(&event);
vp.push_key_event(named, utf8, encode_modifiers(&self.modifiers), false);
}
}
fn repeat_key(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: u32,
_event: KeyEvent,
) {
}
fn update_modifiers(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_serial: u32,
modifiers: Modifiers,
_raw: RawModifiers,
_layout: u32,
) {
self.modifiers = modifiers;
}
}
impl PointerHandler for State {
fn pointer_frame(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_pointer::WlPointer,
events: &[PointerEvent],
) {
let Some(vp) = self.viewport.as_mut() else {
return;
};
for event in events {
if &event.surface != self.layer.wl_surface() {
continue;
}
let (px, py) = event.position;
let lx = px as f32;
let ly = py as f32;
self.last_pointer = (lx, ly);
match event.kind {
PointerEventKind::Enter { .. } => {
self.hovered = true;
vp.push_mouse_move(lx, ly);
}
PointerEventKind::Leave { .. } => {
self.hovered = false;
vp.push_mouse_left();
}
PointerEventKind::Motion { .. } => {
vp.push_mouse_move(lx, ly);
}
PointerEventKind::Press { button, .. } => {
if let Some(code) = map_button(button) {
vp.push_mouse_button(lx, ly, code, true);
}
}
PointerEventKind::Release { button, .. } => {
if let Some(code) = map_button(button) {
vp.push_mouse_button(lx, ly, code, false);
}
}
PointerEventKind::Axis {
horizontal,
vertical,
..
} => {
let dx = -(horizontal.absolute as f32);
let dy = -(vertical.absolute as f32);
vp.push_mouse_scroll(lx, ly, dx, dy);
}
}
}
}
}
/// Find a wayland socket and connect, regardless of whether `WAYLAND_DISPLAY` is set.
/// Inside flatpak the env var is often stripped even when the socket itself is bind-
/// mounted into the sandbox at `$XDG_RUNTIME_DIR/wayland-*`. Lists candidates
/// and tries each in order.
fn probe_connection() -> Option<Connection> {
let runtime_dir = std::env::var_os("XDG_RUNTIME_DIR").map(std::path::PathBuf::from);
tracing::info!(
xdg_runtime_dir = ?runtime_dir,
wayland_display_env = ?std::env::var_os("WAYLAND_DISPLAY"),
"wayland: probing for compositor socket"
);
if let Ok(c) = Connection::connect_to_env() {
tracing::info!("wayland: connected via $WAYLAND_DISPLAY");
return Some(c);
}
let Some(dir) = runtime_dir else {
tracing::warn!("wayland: $XDG_RUNTIME_DIR is unset; cannot scan for sockets");
return None;
};
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(e) => {
tracing::warn!(dir = %dir.display(), "wayland: cannot list runtime dir: {e}");
return None;
}
};
let mut candidates: Vec<String> = Vec::new();
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name_str) = name.to_str() else { continue; };
if !name_str.starts_with("wayland-") {
continue;
}
// Skip lock files; only the socket matters.
if name_str.ends_with(".lock") {
continue;
}
candidates.push(name_str.to_string());
}
candidates.sort();
tracing::info!(?candidates, "wayland: socket candidates in $XDG_RUNTIME_DIR");
for name in &candidates {
// Connection::connect_to_env reads $WAYLAND_DISPLAY; set it for one attempt.
std::env::set_var("WAYLAND_DISPLAY", name);
match Connection::connect_to_env() {
Ok(c) => {
tracing::info!(socket = name, "wayland: connected via probed socket");
return Some(c);
}
Err(e) => {
tracing::debug!(socket = name, "wayland: probe failed: {e}");
}
}
}
None
}
fn log_globals(globals: &GlobalList) {
let list = globals.contents().clone_list();
tracing::info!("wayland: registry advertises {} globals", list.len());
let mut seen_layer_shell = false;
let mut seen_compositor = false;
let mut seen_seat = false;
for g in &list {
tracing::debug!(
interface = %g.interface,
name = g.name,
version = g.version,
"wayland global"
);
match g.interface.as_str() {
"zwlr_layer_shell_v1" => seen_layer_shell = true,
"wl_compositor" => seen_compositor = true,
"wl_seat" => seen_seat = true,
_ => {}
}
}
tracing::info!(
layer_shell = seen_layer_shell,
compositor = seen_compositor,
seat = seen_seat,
"wayland: protocol summary"
);
}
fn map_button(button: u32) -> Option<u32> {
match button {
0x110 => Some(0), // BTN_LEFT
0x111 => Some(1), // BTN_RIGHT
0x112 => Some(2), // BTN_MIDDLE
_ => None,
}
}
fn map_key_event(event: &KeyEvent) -> (u32, Option<String>) {
let named = match event.keysym {
Keysym::Return => 1,
Keysym::Escape => 2,
Keysym::BackSpace => 3,
Keysym::Tab => 4,
Keysym::Left => 5,
Keysym::Right => 6,
Keysym::Up => 7,
Keysym::Down => 8,
Keysym::Delete => 9,
Keysym::Home => 10,
Keysym::End => 11,
_ => 0,
};
let utf8 = if named == 0 { event.utf8.clone() } else { None };
(named, utf8)
}
fn encode_modifiers(mods: &Modifiers) -> u32 {
let mut bits = 0u32;
if mods.shift {
bits |= 1;
}
if mods.ctrl {
bits |= 2;
}
if mods.alt {
bits |= 4;
}
if mods.logo {
bits |= 8;
}
bits
}
delegate_compositor!(State);
delegate_output!(State);
delegate_seat!(State);
delegate_keyboard!(State);
delegate_pointer!(State);
delegate_layer!(State);
delegate_registry!(State);
impl ProvidesRegistryState for State {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
registry_handlers![OutputState, SeatState];
}

9
xtask/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "xtask"
version = "0.0.0"
edition = "2021"
publish = false
[[bin]]
name = "xtask"
path = "src/main.rs"

84
xtask/src/main.rs Normal file
View File

@ -0,0 +1,84 @@
use std::env;
use std::path::PathBuf;
use std::process::{Command, ExitCode};
fn main() -> ExitCode {
let args: Vec<String> = env::args().skip(1).collect();
let cmd = args.first().map(String::as_str).unwrap_or("");
let (action, platform) = match cmd {
"build" => ("build", current_platform()),
"install" => ("install", current_platform()),
"build-linux" => ("build", "linux"),
"install-linux" => ("install", "linux"),
"build-macos" => ("build", "macos"),
"install-macos" => ("install", "macos"),
"build-windows" => ("build", "windows"),
"install-windows" => ("install", "windows"),
_ => {
eprintln!("usage: cargo xtask <command>");
eprintln!();
eprintln!("commands:");
eprintln!(" build build for the current platform");
eprintln!(" install build + install for the current platform");
eprintln!(" build-<platform> force a specific platform (linux | macos | windows)");
eprintln!(" install-<platform> force a specific platform");
return ExitCode::from(2);
}
};
let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask manifest must have a parent")
.to_path_buf();
let (script, runner) = match platform {
"windows" => (
repo_root.join(format!("scripts/windows/{action}.bat")),
vec!["cmd", "/c"],
),
"linux" | "macos" => (
repo_root.join(format!("scripts/{platform}/{action}.sh")),
vec!["bash"],
),
other => {
eprintln!("unknown platform: {other}");
return ExitCode::from(2);
}
};
if !script.exists() {
eprintln!("script not found: {}", script.display());
return ExitCode::from(1);
}
eprintln!("{} {}", runner.join(" "), script.display());
let mut command = Command::new(runner[0]);
for arg in &runner[1..] {
command.arg(arg);
}
command.arg(&script);
command.current_dir(&repo_root);
match command.status() {
Ok(status) if status.success() => ExitCode::SUCCESS,
Ok(status) => ExitCode::from(status.code().unwrap_or(1) as u8),
Err(e) => {
eprintln!("failed to run {}: {e}", script.display());
ExitCode::from(1)
}
}
}
fn current_platform() -> &'static str {
match env::consts::OS {
"linux" => "linux",
"macos" => "macos",
"windows" => "windows",
other => {
eprintln!("unsupported OS: {other}");
std::process::exit(2);
}
}
}