Refactor screen rotation setup and GTK theme configuration in first-boot process
Update the one-shot script to set screen rotation using kanshi based on kernel command line parameters, replacing the previous wlr-randr method. The script now writes the configuration to ~/.config/kanshi/config and sets the GTK dark theme (PiXnoir or Adwaita-dark) at first login. Additionally, enhance documentation to reflect these changes and clarify the role of the new script in the first-boot process.
This commit is contained in:
5
TODO.MD
Normal file
5
TODO.MD
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
- change icon on taskbar.
|
||||
- fix dark theme.
|
||||
- check for duplicates commands in all scripts and cloud init during deployment.
|
||||
@@ -1,30 +1,44 @@
|
||||
#!/bin/bash
|
||||
# One-shot: set reTerminal DM (labwc/Wayland) rotation to Left via wlr-randr, then remove self.
|
||||
# One-shot: set screen rotation via kanshi (same as Control Center), then remove self.
|
||||
# Reads video=DSI-1:rotate=N from kernel cmdline and writes ~/.config/kanshi/config.
|
||||
# Runs once as user pi at first login; deletes its autostart and this script so it never runs again.
|
||||
# Logs to /var/log/first-boot.log.
|
||||
FIRST_BOOT_LOG="/var/log/first-boot.log"
|
||||
BASE="$(basename "$0" .sh)"
|
||||
log() { echo "[$(date -Iseconds)] [$BASE] $*" >> "$FIRST_BOOT_LOG" 2>/dev/null || true; }
|
||||
|
||||
log "started (labwc/wlr-randr)"
|
||||
log "waiting 5s for compositor ..."
|
||||
sleep 5
|
||||
|
||||
OUTPUT=""
|
||||
if command -v wlr-randr &>/dev/null; then
|
||||
OUTPUT=$(wlr-randr 2>/dev/null | awk '/^[A-Za-z0-9_-]+ /{print $1; exit}')
|
||||
fi
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
OUTPUT="DSI-1"
|
||||
log "using default output: $OUTPUT"
|
||||
ROTATE="270"
|
||||
for f in /boot/firmware/cmdline.txt /boot/cmdline.txt; do
|
||||
if [[ -f "$f" ]]; then
|
||||
val=$(grep -o 'video=DSI-1:rotate=[0-9]*' "$f" 2>/dev/null | head -1)
|
||||
val="${val#*rotate=}"
|
||||
if [[ "$val" =~ ^(90|180|270)$ ]]; then ROTATE="$val"; break; fi
|
||||
fi
|
||||
done
|
||||
log "writing kanshi config with transform $ROTATE (from cmdline)"
|
||||
|
||||
if [[ -n "$OUTPUT" ]] && command -v wlr-randr &>/dev/null; then
|
||||
log "applying rotation left (transform 270) on $OUTPUT"
|
||||
wlr-randr --output "$OUTPUT" --transform 270 2>&1 | while read -r line; do log "$line"; done
|
||||
KANSHI_DIR="$HOME/.config/kanshi"
|
||||
KANSHI_CONFIG="$KANSHI_DIR/config"
|
||||
mkdir -p "$KANSHI_DIR"
|
||||
cat > "$KANSHI_CONFIG" << EOF
|
||||
profile {
|
||||
output DSI-1 enable scale 1.000000 mode 800x1280@60.000 position 0,0 transform $ROTATE
|
||||
}
|
||||
EOF
|
||||
log "kanshi config written to $KANSHI_CONFIG"
|
||||
|
||||
# Set GTK dark theme (same as first-boot step 08)
|
||||
GTK_THEME_NAME="PiXnoir"
|
||||
[[ -d /usr/share/themes/Adwaita-dark ]] && ! [[ -d /usr/share/themes/PiXnoir ]] && GTK_THEME_NAME="Adwaita-dark"
|
||||
GTK_SETTINGS="$HOME/.config/gtk-3.0/settings.ini"
|
||||
mkdir -p "$(dirname "$GTK_SETTINGS")"
|
||||
if [[ ! -f "$GTK_SETTINGS" ]]; then
|
||||
printf '%s\n' '[Settings]' 'gtk-application-prefer-dark-theme=1' "gtk-theme-name=$GTK_THEME_NAME" > "$GTK_SETTINGS"
|
||||
else
|
||||
log "WARNING: wlr-randr not found or no output"
|
||||
grep -q '^gtk-application-prefer-dark-theme=' "$GTK_SETTINGS" && sed -i 's/^gtk-application-prefer-dark-theme=.*/gtk-application-prefer-dark-theme=1/' "$GTK_SETTINGS" || echo 'gtk-application-prefer-dark-theme=1' >> "$GTK_SETTINGS"
|
||||
grep -q '^gtk-theme-name=' "$GTK_SETTINGS" && sed -i "s/^gtk-theme-name=.*/gtk-theme-name=$GTK_THEME_NAME/" "$GTK_SETTINGS" || echo "gtk-theme-name=$GTK_THEME_NAME" >> "$GTK_SETTINGS"
|
||||
fi
|
||||
log "Set dark theme ($GTK_THEME_NAME) in gtk-3.0/settings.ini"
|
||||
|
||||
log "removing one-shot desktop and script"
|
||||
rm -f "$HOME/.config/autostart/${BASE}.desktop" "$HOME/${BASE}.sh"
|
||||
|
||||
73
emmc-provisioning/cloud-init/TASKBAR-ICON.md
Normal file
73
emmc-provisioning/cloud-init/TASKBAR-ICON.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Taskbar (start menu) icon
|
||||
|
||||
The icon you see in the taskbar that opens the application menu is the **start-here** icon from the current icon theme. On Raspberry Pi OS with rpd-labwc (wf-panel-pi + wfplug-menu), that icon is resolved by name: **`start-here`** in the **places** context.
|
||||
|
||||
On the default PiXtrix theme, `start-here` is a symlink to the Raspberry Pi logo (`rpi.png`). To show a different icon, replace **start-here** with your own image in the same theme (or in a custom theme that overrides it).
|
||||
|
||||
---
|
||||
|
||||
## How to change it
|
||||
|
||||
### Option 1: Replace on the device (one-off)
|
||||
|
||||
After first-boot, SSH in and replace the icon files. The panel uses **PiXtrix** by default; start-here there is under **places**:
|
||||
|
||||
- Paths: `/usr/share/icons/PiXtrix/<size>/places/start-here.png`
|
||||
- Sizes present on Raspberry Pi OS: **16, 24, 32, 48, 64, 96** (and optionally 22 for legacy).
|
||||
|
||||
**Steps (as root):**
|
||||
|
||||
```bash
|
||||
# Backup originals (currently symlinks to rpi.png)
|
||||
for s in 16 24 32 48 64 96; do
|
||||
[ -L /usr/share/icons/PiXtrix/${s}x${s}/places/start-here.png ] && \
|
||||
cp -P /usr/share/icons/PiXtrix/${s}x${s}/places/start-here.png /usr/share/icons/PiXtrix/${s}x${s}/places/start-here.png.bak
|
||||
done
|
||||
|
||||
# Replace with your PNG (one size – e.g. 32x32 – then scale for others, or use a scalable SVG and generate PNGs)
|
||||
# Example: copy your icon to 32x32 (panel default icon_size is 32)
|
||||
cp /path/to/your-icon.png /usr/share/icons/PiXtrix/32x32/places/start-here.png
|
||||
# Remove symlink first if it was a link:
|
||||
rm -f /usr/share/icons/PiXtrix/32x32/places/start-here.png
|
||||
cp /path/to/your-icon.png /usr/share/icons/PiXtrix/32x32/places/start-here.png
|
||||
|
||||
# Repeat for other sizes, or use ImageMagick to scale:
|
||||
for s in 16 24 48 64 96; do
|
||||
convert /path/to/your-icon.png -resize ${s}x${s} /usr/share/icons/PiXtrix/${s}x${s}/places/start-here.png
|
||||
done
|
||||
```
|
||||
|
||||
Then restart the panel (or log out and back in):
|
||||
|
||||
```bash
|
||||
pkill wf-panel-pi
|
||||
# Panel is usually restarted by the session; if not, log out and in again.
|
||||
```
|
||||
|
||||
### Option 2: Deploy via first-boot (file server)
|
||||
|
||||
**first-boot.sh** (step 05) already does this: if **`start-here.png`** is present in the first-boot folder on the file server (same URL as splash.png, e.g. `http://10.20.50.1:5000/files/first-boot/start-here.png`), it is downloaded and installed as the taskbar start button icon in PiXtrix (all sizes 16, 24, 32, 48, 64, 96). If ImageMagick (`convert`) is installed, the icon is resized per size; otherwise the same image is copied to each size. Place **start-here.png** in your portal first-boot folder (e.g. next to splash.png) and re-provision or run first-boot; no extra config needed.
|
||||
|
||||
---
|
||||
|
||||
## Specs for the new icon
|
||||
|
||||
So that it looks correct in the taskbar and in the menu, use these specs:
|
||||
|
||||
| Property | Value |
|
||||
|----------------|--------|
|
||||
| **Icon name** | Must be installed as **start-here** (in **places** context) so the panel finds it. |
|
||||
| **Format** | PNG (preferred for raster), or SVG if you generate PNGs per size. |
|
||||
| **Sizes** | 16×16, 24×24, 32×32, 48×48, 64×64, 96×96 px (panel default is 32px). |
|
||||
| **Aspect** | Square (1∶1). The panel shows it in a square cell. |
|
||||
| **Background** | Transparent (PNG with alpha) so it fits any panel background. |
|
||||
| **Content** | Simple, recognizable at 16–32 px (logo or symbol). Avoid fine detail. |
|
||||
| **Theme path** | `PiXtrix/<size>x<size>/places/start-here.png` (or override via a custom theme). |
|
||||
|
||||
### Recommendation
|
||||
|
||||
- **Source asset**: One PNG at **32×32** or **64×64** (or one SVG), then generate the other sizes to avoid scaling artifacts.
|
||||
- **Design**: High contrast, clear at 24–32 px; works on both light and dark panel (or match your panel colour).
|
||||
- **File server name**: e.g. `taskbar-icon.png` or `start-here.png`; first-boot script would download it and install to the paths above.
|
||||
|
||||
If you want, the next step is to add a small script (and optional first-boot wiring) that downloads `taskbar-icon.png` from the file server and installs it as **start-here** in all required sizes.
|
||||
155
emmc-provisioning/cloud-init/TASKBAR-THEME.md
Normal file
155
emmc-provisioning/cloud-init/TASKBAR-THEME.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Taskbar (wf-panel-pi) editing and custom theme
|
||||
|
||||
You can edit the taskbar to your liking and create a custom theme. The panel is **wf-panel-pi** (GTK3); configuration is in **`~/.config/wf-panel-pi/wf-panel-pi.ini`** and optional **custom CSS** via the **css_path** option.
|
||||
|
||||
---
|
||||
|
||||
## 1. Editing the taskbar (INI config)
|
||||
|
||||
Config file: **`~/.config/wf-panel-pi/wf-panel-pi.ini`** (user `pi`). If the file is empty or missing, the panel uses built-in defaults. Options (from `panel-pi.xml`):
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| **css_path** | string | (empty) | Path to a custom CSS file for the panel (GTK CSS). |
|
||||
| **widgets_left** | string | `smenu spacing0 spacing4 launchers spacing8 window-list` | Widget names for the left section (space-separated). |
|
||||
| **widgets_center** | string | `none` | Widgets in the center. |
|
||||
| **widgets_right** | string | `tray power ejecter updater ... clock ... batt squeek` | Widgets on the right. |
|
||||
| **icon_size** | int | 32 | Icon size in pixels. |
|
||||
| **minimal_height** | int | 24 | Panel height (min). |
|
||||
| **autohide** | bool | false | Hide panel until cursor touches edge. |
|
||||
| **autohide_duration** | int | 300 | Animation duration (ms) when showing/hiding. |
|
||||
| **position** | string | top | `top` or `bottom`. |
|
||||
| **background_color** | string | #EDECEBFF | Panel background (ARGB hex). |
|
||||
| **layer** | string | bottom | `top`, `bottom`, `overlay`, `background`. |
|
||||
| **monitor** | string | 0 | Monitor index. |
|
||||
| **edge_offset** | int | 20 | Pixels from edge to trigger autohide. |
|
||||
| **gestures_touch_only** | bool | false | Restrict gestures to touch. |
|
||||
|
||||
### Widget names (for widgets_left / _center / _right)
|
||||
|
||||
- **Left:** `smenu` (start menu), `spacing0` … `spacing8` (spacers), `launchers`, `window-list`
|
||||
- **Right:** `tray`, `power`, `ejecter`, `updater`, `connect`, `bluetooth`, `netman`, `volumepulse`, `clock`, `batt`, `squeek`, plus `spacing2` etc.
|
||||
|
||||
Example: minimal left (only menu + window list), right (clock + tray only):
|
||||
|
||||
```ini
|
||||
[panel]
|
||||
widgets_left = smenu spacing4 window-list
|
||||
widgets_center = none
|
||||
widgets_right = spacing2 clock spacing2 tray
|
||||
icon_size = 28
|
||||
minimal_height = 28
|
||||
position = bottom
|
||||
background_color = #2D2D2DFF
|
||||
```
|
||||
|
||||
Save as `~/.config/wf-panel-pi/wf-panel-pi.ini` for user `pi`, then restart the panel: `pkill wf-panel-pi` (session will respawn it, or log out/in).
|
||||
|
||||
---
|
||||
|
||||
## 2. Custom theme (CSS + INI)
|
||||
|
||||
The panel supports **GTK 3 CSS** via **css_path**. You can:
|
||||
|
||||
1. Set **css_path** in `wf-panel-pi.ini` to a full path to your `.css` file (e.g. `~/.config/wf-panel-pi/panel-theme.css`).
|
||||
2. Use GTK CSS to style the panel window and its children (colors, borders, font, padding, etc.).
|
||||
|
||||
### INI
|
||||
|
||||
```ini
|
||||
[panel]
|
||||
css_path = /home/pi/.config/wf-panel-pi/panel-theme.css
|
||||
background_color = #1E1E1EFF
|
||||
icon_size = 28
|
||||
minimal_height = 28
|
||||
```
|
||||
|
||||
### Example CSS (panel-theme.css)
|
||||
|
||||
GTK panel often uses a single top-level window; you can target it and inner boxes. Example (dark, rounded, with padding):
|
||||
|
||||
```css
|
||||
/* Custom taskbar theme – dark, subtle border */
|
||||
window {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Main panel box */
|
||||
window box {
|
||||
background-color: rgba(30, 30, 30, 0.95);
|
||||
border-radius: 12px;
|
||||
margin: 4px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Buttons / plugin containers */
|
||||
button,
|
||||
button:hover,
|
||||
button:active {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* Labels (e.g. clock) */
|
||||
label {
|
||||
color: #e0e0e0;
|
||||
font-size: 11pt;
|
||||
}
|
||||
```
|
||||
|
||||
Paths: use an **absolute path** in **css_path** (e.g. `/home/pi/.config/wf-panel-pi/panel-theme.css`). Expand `~` yourself when writing the INI (the panel may not expand it).
|
||||
|
||||
---
|
||||
|
||||
## 3. Deploying a custom theme via first-boot
|
||||
|
||||
To apply a custom taskbar theme on every provisioned device:
|
||||
|
||||
1. **On the file server** (e.g. first-boot folder): add:
|
||||
- **wf-panel-pi.ini** – INI with your `[panel]` options and `css_path` pointing to the CSS file path on the device.
|
||||
- **panel-theme.css** – your GTK CSS.
|
||||
|
||||
2. **In first-boot** (or a one-shot script): for user `pi`:
|
||||
- Create `~/.config/wf-panel-pi/` if needed.
|
||||
- Copy or download **wf-panel-pi.ini** to `~/.config/wf-panel-pi/wf-panel-pi.ini`.
|
||||
- Copy or download **panel-theme.css** to e.g. `~/.config/wf-panel-pi/panel-theme.css`.
|
||||
- In the INI, set `css_path = /home/pi/.config/wf-panel-pi/panel-theme.css` (or use `$PI_HOME` in the script).
|
||||
- `chown -R pi:pi ~/.config/wf-panel-pi`.
|
||||
|
||||
3. **Restart panel** after first login (or rely on next login): `pkill wf-panel-pi` (optional in script; session often restarts it).
|
||||
|
||||
Example first-boot snippet (run as root, after `$PI_HOME` is set):
|
||||
|
||||
```bash
|
||||
PANEL_CONF="$PI_HOME/.config/wf-panel-pi"
|
||||
mkdir -p "$PANEL_CONF"
|
||||
curl -fsSL "${FILE_SERVER}/wf-panel-pi.ini" -o "$PANEL_CONF/wf-panel-pi.ini"
|
||||
curl -fsSL "${FILE_SERVER}/panel-theme.css" -o "$PANEL_CONF/panel-theme.css"
|
||||
sed -i "s|/home/pi|$PI_HOME|g" "$PANEL_CONF/wf-panel-pi.ini"
|
||||
chown -R "$PI_USER:$PI_USER" "$PANEL_CONF"
|
||||
```
|
||||
|
||||
Ensure the INI uses the same path as where you save the CSS (e.g. `css_path = /home/pi/.config/wf-panel-pi/panel-theme.css` or `$PI_HOME/...` if you substitute in the script).
|
||||
|
||||
**Example theme files** in this repo: `config-files/wf-panel-pi.ini` and `config-files/panel-theme.css` (dark, rounded panel). Host them on the file server and deploy with the snippet above.
|
||||
|
||||
---
|
||||
|
||||
## 4. Summary
|
||||
|
||||
| Goal | Where | How |
|
||||
|------|--------|-----|
|
||||
| Change layout (widgets, position, size) | `~/.config/wf-panel-pi/wf-panel-pi.ini` | Edit `[panel]` options. |
|
||||
| Change colors / style | Same INI + custom CSS | Set **css_path** and write GTK CSS. |
|
||||
| Custom theme on all devices | File server + first-boot | Deploy **wf-panel-pi.ini** and **panel-theme.css** and copy to `~/.config/wf-panel-pi/` for `pi`. |
|
||||
|
||||
After any change, restart the panel (`pkill wf-panel-pi`) or log out and back in so the new theme is applied.
|
||||
@@ -8,5 +8,7 @@ first-boot.sh downloads these from `FILE_SERVER` (e.g. `http://10.20.50.1:5000/f
|
||||
| 99-wallpaper.conf | /etc/lightdm/lightdm.conf.d/99-wallpaper.conf |
|
||||
| 99-default-session.conf | /etc/lightdm/lightdm.conf.d/99-default-session.conf (rpd-labwc) |
|
||||
| maliit-keyboard.desktop | /home/pi/.config/autostart/maliit-keyboard.desktop |
|
||||
| 01-set-rotation-once.desktop | /home/pi/.config/autostart/01-set-rotation-once.desktop (with 01-set-rotation-once.sh) |
|
||||
| 01-set-rotation-once.desktop | /home/pi/.config/autostart/01-set-rotation-once.desktop (with 01-set-rotation-once.sh; writes kanshi config + dark theme) |
|
||||
| 02-set-wallpaper-once.desktop | /home/pi/.config/autostart/02-set-wallpaper-once.desktop (with 02-set-wallpaper-once.sh). Wallpaper is also set during first-boot via pcmanfm. |
|
||||
| wf-panel-pi.ini | Optional: /home/pi/.config/wf-panel-pi/wf-panel-pi.ini (custom taskbar layout and css_path). See TASKBAR-THEME.md. |
|
||||
| panel-theme.css | Optional: /home/pi/.config/wf-panel-pi/panel-theme.css (custom taskbar CSS). Deploy with wf-panel-pi.ini; see TASKBAR-THEME.md. |
|
||||
|
||||
44
emmc-provisioning/cloud-init/config-files/panel-theme.css
Normal file
44
emmc-provisioning/cloud-init/config-files/panel-theme.css
Normal file
@@ -0,0 +1,44 @@
|
||||
/* Custom wf-panel-pi theme – dark, rounded panel
|
||||
* Place at ~/.config/wf-panel-pi/panel-theme.css
|
||||
* Set css_path in wf-panel-pi.ini to this file (absolute path).
|
||||
*/
|
||||
|
||||
/* Panel window */
|
||||
window {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Main panel box – dark with slight rounding and shadow */
|
||||
window box {
|
||||
background-color: rgba(45, 45, 45, 0.96);
|
||||
border-radius: 10px;
|
||||
margin: 4px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
/* Plugin / launcher buttons */
|
||||
button,
|
||||
button:hover,
|
||||
button:active {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
/* Labels (e.g. clock text) */
|
||||
label {
|
||||
color: #e0e0e0;
|
||||
font-size: 11pt;
|
||||
}
|
||||
21
emmc-provisioning/cloud-init/config-files/wf-panel-pi.ini
Normal file
21
emmc-provisioning/cloud-init/config-files/wf-panel-pi.ini
Normal file
@@ -0,0 +1,21 @@
|
||||
# wf-panel-pi custom theme example
|
||||
# Copy to ~/.config/wf-panel-pi/wf-panel-pi.ini (user pi)
|
||||
# Optional: copy panel-theme.css to ~/.config/wf-panel-pi/panel-theme.css
|
||||
# and ensure css_path below points to it. Restart panel: pkill wf-panel-pi
|
||||
|
||||
[panel]
|
||||
# Custom CSS (use absolute path; replace /home/pi with actual $PI_HOME if different)
|
||||
css_path = /home/pi/.config/wf-panel-pi/panel-theme.css
|
||||
# Layout
|
||||
widgets_left = smenu spacing0 spacing4 launchers spacing8 window-list
|
||||
widgets_center = none
|
||||
widgets_right = tray power ejecter updater spacing2 connect spacing2 bluetooth spacing2 netman spacing2 volumepulse spacing2 clock spacing2 batt spacing2 squeek
|
||||
# Size and position
|
||||
icon_size = 28
|
||||
minimal_height = 28
|
||||
position = top
|
||||
# Dark background (ARGB)
|
||||
background_color = #2D2D2DFF
|
||||
# Optional: autohide
|
||||
autohide = false
|
||||
autohide_duration = 300
|
||||
@@ -14,7 +14,7 @@ first-boot.sh downloads from **`.../files/first-boot/`** (e.g. `http://10.20.50.
|
||||
| **99-wallpaper.conf** | LightDM greeter wallpaper (from `config-files/`). |
|
||||
| **99-default-session.conf** | LightDM default session rpd-labwc (from `config-files/`). |
|
||||
| **maliit-keyboard.desktop** | Maliit on-screen keyboard autostart (from `config-files/`). |
|
||||
| **01-set-rotation-once.sh** + **.desktop** | One-shot: wlr-randr rotation (Left) at first login. |
|
||||
| **01-set-rotation-once.sh** + **.desktop** | One-shot: writes ~/.config/kanshi/config with rotation from cmdline and sets GTK dark theme at first login. |
|
||||
|
||||
Desktop wallpaper is set once during first-boot via pcmanfm config (first-boot.sh). Optional one-shot: **02-set-wallpaper-once.sh**.
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
# WALLPAPER_MODE="crop"
|
||||
|
||||
# --- Display (reTerminal DM) ---
|
||||
# Kernel cmdline: DSI rotation. 90 = 90° clockwise; use 180 or 270 for other orientations.
|
||||
# Kernel cmdline: DSI rotation. 90 = 90° clockwise; 180 or 270 for other orientations.
|
||||
# At login, ~/.config/kanshi/config is written with this transform (same as Control Center).
|
||||
# DSI_ROTATE="270"
|
||||
|
||||
# Kernel cmdline: swiotlb size (for vc4-drm/DSI). Leave empty to skip.
|
||||
|
||||
@@ -62,7 +62,7 @@ Installs the software needed for the rest of the script and for the kiosk:
|
||||
| **wmctrl** | Window control; used to force Chromium into fullscreen. |
|
||||
| **openssh-server** | SSH access (often also enabled in user-data). |
|
||||
| **swaybg** | Wallpaper for labwc (Wayland); used by one-shot and labwc autostart. |
|
||||
| **wlr-randr** | Display rotation for wlroots/labwc; one-shot sets “Left” (transform 270). |
|
||||
| **kanshi** | Display rotation: ~/.config/kanshi/config is written at login with transform from cmdline (same as Control Center). The same login scripts also set GTK dark theme (~/.config/gtk-3.0/settings.ini). |
|
||||
| **maliit-keyboard** | On-screen keyboard for touch input. |
|
||||
| **xinput-calibrator** | Touchscreen calibration (optional; run manually if needed). |
|
||||
|
||||
@@ -84,7 +84,7 @@ Downloads from `FILE_SERVER` (no local creation):
|
||||
|
||||
Ensure the `.desktop` file on the server has `Exec=/home/pi/start-chromium.sh` (or the path you use on the device).
|
||||
|
||||
**Touch in Chromium:** Long-press on the touchscreen to open the context menu (right-click). This works when Chromium runs as a Wayland client (default under rpd-labwc). If you ever run under pure X11, long-press may not trigger the context menu; in that case you can use **evdev-right-click-emulation** (see e.g. [evdev-right-click-emulation](https://github.com/PeterCxy/evdev-right-click-emulation)) to inject right-click on long-press at the input layer.
|
||||
**Touch long-press → right-click in Chromium:** Under Wayland (rpd-labwc), long-press may work for right-click elsewhere (e.g. desktop) but **not inside Chromium**—Chromium gets touch events directly and does not implement long-press-as-right-click. To get context menu on long-press in the browser (and everywhere), use **evdev-right-click-emulation** at the input layer so long-press is converted to right-click before any app sees it: [evdev-right-click-emulation](https://github.com/PeterCxy/evdev-right-click-emulation). Build, install the binary (e.g. to `/usr/local/bin/evdev-rce`), and run it as a systemd service at boot (works on both X11 and Wayland).
|
||||
|
||||
---
|
||||
|
||||
@@ -134,7 +134,7 @@ The reTerminal DM default is portrait. Rotation is set **persistently** via the
|
||||
|
||||
- **Kernel cmdline** — First-boot appends **`video=DSI-1:rotate=90`** to **`/boot/firmware/cmdline.txt`** (or `/boot/cmdline.txt`). The file must remain **one single line** with no line breaks.
|
||||
- **90° clockwise** — The value `90` gives 90° clockwise rotation. Other valid values: `180`, `270` (90° counter-clockwise).
|
||||
- **Effect** — Rotation is applied by the kernel at boot; no one-shot script or wlr-randr needed.
|
||||
- **Effect** — Kernel cmdline rotates at boot; at login ~/.config/kanshi/config is written with the same transform (kanshi applies it; same as Control Center).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -134,8 +134,26 @@ step_02_packages() {
|
||||
log "Packages installed successfully"
|
||||
}
|
||||
|
||||
# --- Step 03: Kiosk files from file server ---
|
||||
step_03_kiosk_files() {
|
||||
# --- Step 03: reTerminal DM drivers (Seeed) ---
|
||||
step_03_reterminal_drivers() {
|
||||
if [[ -z "$RETERMINAL_REPO_URL" ]]; then
|
||||
log "Skipping reTerminal drivers (RETERMINAL_REPO_URL not set)"
|
||||
return 0
|
||||
fi
|
||||
REPO_DIR="/tmp/seeed-linux-dtoverlays"
|
||||
log "Cloning seeed-linux-dtoverlays to $REPO_DIR ..."
|
||||
git clone --depth 1 "$RETERMINAL_REPO_URL" "$REPO_DIR"
|
||||
log "Running reTerminal.sh --device $RETERMINAL_DEVICE from $REPO_DIR ..."
|
||||
if ( cd "$REPO_DIR" && "$REPO_DIR/scripts/reTerminal.sh" --device "$RETERMINAL_DEVICE" ); then
|
||||
log "reTerminal DM drivers installed (reboot will apply)"
|
||||
else
|
||||
log "WARNING: reTerminal.sh failed (see log above). Display/touch may still work; you can retry later with: cd $REPO_DIR && sudo ./scripts/reTerminal.sh --device $RETERMINAL_DEVICE"
|
||||
fi
|
||||
rm -rf "$REPO_DIR"
|
||||
}
|
||||
|
||||
# --- Step 04: Kiosk files from file server ---
|
||||
step_04_kiosk_files() {
|
||||
log "Creating $AUTOSTART"
|
||||
mkdir -p "$AUTOSTART"
|
||||
log "Downloading start-chromium.sh from ${FILE_SERVER}/start-chromium.sh"
|
||||
@@ -147,8 +165,8 @@ step_03_kiosk_files() {
|
||||
log "Kiosk files installed under $PI_HOME and $AUTOSTART"
|
||||
}
|
||||
|
||||
# --- Step 04: Boot splash and wallpaper ---
|
||||
step_04_splash_wallpaper() {
|
||||
# --- Step 05: Boot splash and wallpaper ---
|
||||
step_05_splash_wallpaper() {
|
||||
log "Creating $PLYMOUTH_DIR and /usr/share/rpd-wallpaper"
|
||||
mkdir -p "$PLYMOUTH_DIR" /usr/share/rpd-wallpaper
|
||||
if curl -fsSL "${FILE_SERVER}/splash.png" -o "$PLYMOUTH_DIR/splash.png"; then
|
||||
@@ -183,10 +201,32 @@ step_04_splash_wallpaper() {
|
||||
else
|
||||
log "WARNING: Could not download splash.png"
|
||||
fi
|
||||
# Optional: taskbar start button icon from file server (start-here.png)
|
||||
if curl -fsSL "${FILE_SERVER}/start-here.png" -o /tmp/start-here.png; then
|
||||
PIXTRIX_PLACES="/usr/share/icons/PiXtrix"
|
||||
if [[ -d "$PIXTRIX_PLACES/32x32/places" ]]; then
|
||||
for s in 16 24 32 48 64 96; do
|
||||
DEST="$PIXTRIX_PLACES/${s}x${s}/places/start-here.png"
|
||||
if [[ -d "$(dirname "$DEST")" ]]; then
|
||||
rm -f "$DEST"
|
||||
if command -v convert >/dev/null 2>&1; then
|
||||
convert /tmp/start-here.png -resize "${s}x${s}" "$DEST" 2>/dev/null && true
|
||||
else
|
||||
cp /tmp/start-here.png "$DEST"
|
||||
fi
|
||||
chmod 644 "$DEST"
|
||||
fi
|
||||
done
|
||||
log "Taskbar start button icon (start-here.png) installed from file server"
|
||||
else
|
||||
log "WARNING: PiXtrix theme places dir not found; skipped taskbar icon"
|
||||
fi
|
||||
rm -f /tmp/start-here.png
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Step 05: LightDM session ---
|
||||
step_05_lightdm() {
|
||||
# --- Step 06: LightDM session ---
|
||||
step_06_lightdm() {
|
||||
mkdir -p /etc/lightdm/lightdm.conf.d
|
||||
if curl -fsSL "${FILE_SERVER}/99-default-session.conf" -o /etc/lightdm/lightdm.conf.d/99-default-session.conf 2>/dev/null; then
|
||||
log "99-default-session.conf installed"
|
||||
@@ -198,16 +238,36 @@ step_05_lightdm() {
|
||||
sed -i "s/^autologin-session=.*/autologin-session=$LIGHTDM_SESSION/" /etc/lightdm/lightdm.conf
|
||||
log "Patched /etc/lightdm/lightdm.conf to use $LIGHTDM_SESSION"
|
||||
fi
|
||||
# Delay LightDM on first boot after provisioning so reTerminal DM DSI panel has time to init (avoids black screen on first reboot)
|
||||
mkdir -p /etc/systemd/system/lightdm.service.d
|
||||
cat > /etc/systemd/system/cm4-await-display.service << 'AWAITSVC'
|
||||
[Unit]
|
||||
Description=Wait for reTerminal DM display on first boot after provisioning
|
||||
Before=lightdm.service
|
||||
DefaultDependencies=no
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/bin/sh -c 'if [ -f /var/lib/cm4-provisioning/await-display ]; then echo "Waiting 18s for DSI panel..."; sleep 18; rm -f /var/lib/cm4-provisioning/await-display; fi'
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
||||
AWAITSVC
|
||||
printf '%s\n' '[Unit]' 'After=cm4-await-display.service' > /etc/systemd/system/lightdm.service.d/99-await-display.conf
|
||||
systemctl daemon-reload
|
||||
systemctl enable cm4-await-display.service 2>/dev/null || true
|
||||
log "Installed cm4-await-display.service (delays LightDM on first reboot after provisioning)"
|
||||
}
|
||||
|
||||
# --- Step 06: Maliit on-screen keyboard ---
|
||||
step_06_maliit() {
|
||||
# --- Step 07: Maliit on-screen keyboard ---
|
||||
step_07_maliit() {
|
||||
mkdir -p "$AUTOSTART" "$PI_HOME/.config"
|
||||
curl -fsSL "${FILE_SERVER}/maliit-keyboard.desktop" -o "$AUTOSTART/maliit-keyboard.desktop" 2>/dev/null && log "maliit-keyboard.desktop installed" || log "WARNING: Could not download maliit-keyboard.desktop"
|
||||
}
|
||||
|
||||
# --- Step 07: Dark theme (GTK) ---
|
||||
step_07_dark_theme() {
|
||||
# --- Step 08: Dark theme (GTK) ---
|
||||
step_08_dark_theme() {
|
||||
GTK_SETTINGS="$PI_HOME/.config/gtk-3.0/settings.ini"
|
||||
mkdir -p "$(dirname "$GTK_SETTINGS")"
|
||||
if [[ ! -f "$GTK_SETTINGS" ]]; then
|
||||
@@ -220,24 +280,6 @@ step_07_dark_theme() {
|
||||
chown -R "$PI_USER:$PI_USER" "$PI_HOME/.config"
|
||||
}
|
||||
|
||||
# --- Step 08: reTerminal DM drivers (Seeed) ---
|
||||
step_08_reterminal_drivers() {
|
||||
if [[ -z "$RETERMINAL_REPO_URL" ]]; then
|
||||
log "Skipping reTerminal drivers (RETERMINAL_REPO_URL not set)"
|
||||
return 0
|
||||
fi
|
||||
REPO_DIR="/tmp/seeed-linux-dtoverlays"
|
||||
log "Cloning seeed-linux-dtoverlays to $REPO_DIR ..."
|
||||
git clone --depth 1 "$RETERMINAL_REPO_URL" "$REPO_DIR"
|
||||
log "Running reTerminal.sh --device $RETERMINAL_DEVICE from $REPO_DIR ..."
|
||||
if ( cd "$REPO_DIR" && "$REPO_DIR/scripts/reTerminal.sh" --device "$RETERMINAL_DEVICE" ); then
|
||||
log "reTerminal DM drivers installed (reboot will apply)"
|
||||
else
|
||||
log "WARNING: reTerminal.sh failed (see log above). Display/touch may still work; you can retry later with: cd $REPO_DIR && sudo ./scripts/reTerminal.sh --device $RETERMINAL_DEVICE"
|
||||
fi
|
||||
rm -rf "$REPO_DIR"
|
||||
}
|
||||
|
||||
# --- Step 09: Re-apply splash and Plymouth theme ---
|
||||
step_09_reapply_splash() {
|
||||
CFG_PATH="/boot/firmware/config.txt"
|
||||
@@ -277,6 +319,17 @@ step_10_cmdline() {
|
||||
step_11_oneshots() {
|
||||
if [[ -n "$DSI_ROTATE" ]]; then
|
||||
log "Rotation is set via kernel cmdline (video=DSI-1:rotate=$DSI_ROTATE)"
|
||||
# Install set-rotation-at-login to write ~/.config/kanshi/config at every login (same as Control Center)
|
||||
if curl -fsSL "${FILE_SERVER}/set-rotation-at-login.sh" -o "$PI_HOME/set-rotation-at-login.sh" 2>/dev/null; then
|
||||
chmod 755 "$PI_HOME/set-rotation-at-login.sh"
|
||||
chown "$PI_USER:$PI_USER" "$PI_HOME/set-rotation-at-login.sh"
|
||||
if curl -fsSL "${FILE_SERVER}/set-rotation-at-login.desktop" -o /tmp/set-rotation-at-login.desktop 2>/dev/null; then
|
||||
sed "s|/home/pi|$PI_HOME|g" /tmp/set-rotation-at-login.desktop > "$AUTOSTART/set-rotation-at-login.desktop"
|
||||
chown "$PI_USER:$PI_USER" "$AUTOSTART/set-rotation-at-login.desktop"
|
||||
log "Installed set-rotation-at-login (re-applies rotation from cmdline every login)"
|
||||
fi
|
||||
rm -f /tmp/set-rotation-at-login.desktop
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$ONESHOT_SCRIPTS" ]]; then
|
||||
for _name in $ONESHOT_SCRIPTS; do
|
||||
@@ -298,6 +351,9 @@ step_13_reboot() {
|
||||
DEVICE_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||||
report_status "done" "First-boot complete" "13" "reboot" "$DEVICE_IP"
|
||||
log "Device IP: ${DEVICE_IP:-unknown}"
|
||||
# Flag for cm4-await-display.service: on next boot delay LightDM so DSI panel can init (avoids black screen)
|
||||
mkdir -p /var/lib/cm4-provisioning
|
||||
touch /var/lib/cm4-provisioning/await-display
|
||||
log "=== first-boot.sh finished, rebooting ==="
|
||||
reboot
|
||||
}
|
||||
@@ -305,12 +361,12 @@ step_13_reboot() {
|
||||
# --- Main: run steps in order ---
|
||||
run_step 01 hostname
|
||||
run_step 02 packages
|
||||
run_step 03 kiosk_files
|
||||
run_step 04 splash_wallpaper
|
||||
run_step 05 lightdm
|
||||
run_step 06 maliit
|
||||
run_step 07 dark_theme
|
||||
run_step 08 reterminal_drivers
|
||||
run_step 03 reterminal_drivers
|
||||
run_step 04 kiosk_files
|
||||
run_step 05 splash_wallpaper
|
||||
run_step 06 lightdm
|
||||
run_step 07 maliit
|
||||
run_step 08 dark_theme
|
||||
run_step 09 reapply_splash
|
||||
run_step 10 cmdline
|
||||
run_step 11 oneshots
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Set reTerminal DM (labwc/Wayland) rotation to Left at every login.
|
||||
# Runs from autostart when user pi logs in; does not remove itself.
|
||||
# Use this when wlr-randr transform does not persist across reboots.
|
||||
sleep 5
|
||||
OUTPUT=""
|
||||
if command -v wlr-randr &>/dev/null; then
|
||||
OUTPUT=$(wlr-randr 2>/dev/null | awk '/^[A-Za-z0-9_-]+ /{print $1; exit}')
|
||||
fi
|
||||
[[ -z "$OUTPUT" ]] && OUTPUT="DSI-1"
|
||||
if [[ -n "$OUTPUT" ]] && command -v wlr-randr &>/dev/null; then
|
||||
wlr-randr --output "$OUTPUT" --transform 270
|
||||
# Set screen rotation via kanshi (same as Control Center). Reads video=DSI-1:rotate=N
|
||||
# from kernel cmdline and writes ~/.config/kanshi/config. Kanshi auto-reloads when the file changes.
|
||||
# Also sets GTK dark theme (PiXnoir / Adwaita-dark). Runs from autostart when user pi logs in; does not remove itself.
|
||||
ROTATE="270"
|
||||
for f in /boot/firmware/cmdline.txt /boot/cmdline.txt; do
|
||||
[[ -f "$f" ]] || continue
|
||||
val=$(grep -o 'video=DSI-1:rotate=[0-9]*' "$f" 2>/dev/null | head -1)
|
||||
val="${val#*rotate=}"
|
||||
[[ "$val" =~ ^(90|180|270)$ ]] && ROTATE="$val" && break
|
||||
done
|
||||
KANSHI_DIR="$HOME/.config/kanshi"
|
||||
KANSHI_CONFIG="$KANSHI_DIR/config"
|
||||
mkdir -p "$KANSHI_DIR"
|
||||
cat > "$KANSHI_CONFIG" << EOF
|
||||
profile {
|
||||
output DSI-1 enable scale 1.000000 mode 800x1280@60.000 position 0,0 transform $ROTATE
|
||||
}
|
||||
EOF
|
||||
|
||||
# Set GTK dark theme (same as first-boot step 08)
|
||||
GTK_THEME_NAME="PiXnoir"
|
||||
[[ -d /usr/share/themes/Adwaita-dark ]] && ! [[ -d /usr/share/themes/PiXnoir ]] && GTK_THEME_NAME="Adwaita-dark"
|
||||
GTK_SETTINGS="$HOME/.config/gtk-3.0/settings.ini"
|
||||
mkdir -p "$(dirname "$GTK_SETTINGS")"
|
||||
if [[ ! -f "$GTK_SETTINGS" ]]; then
|
||||
printf '%s\n' '[Settings]' 'gtk-application-prefer-dark-theme=1' "gtk-theme-name=$GTK_THEME_NAME" > "$GTK_SETTINGS"
|
||||
else
|
||||
grep -q '^gtk-application-prefer-dark-theme=' "$GTK_SETTINGS" && sed -i 's/^gtk-application-prefer-dark-theme=.*/gtk-application-prefer-dark-theme=1/' "$GTK_SETTINGS" || echo 'gtk-application-prefer-dark-theme=1' >> "$GTK_SETTINGS"
|
||||
grep -q '^gtk-theme-name=' "$GTK_SETTINGS" && sed -i "s/^gtk-theme-name=.*/gtk-theme-name=$GTK_THEME_NAME/" "$GTK_SETTINGS" || echo "gtk-theme-name=$GTK_THEME_NAME" >> "$GTK_SETTINGS"
|
||||
fi
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
#!/bin/bash
|
||||
# Start Chromium in app mode. Optional env vars:
|
||||
# CHROMIUM_APP_URL - URL to open (default: http://127.0.0.1:8080)
|
||||
# CHROMIUM_MODE - "fullscreen" or "kiosk" (default: fullscreen)
|
||||
#
|
||||
# Touch long-press → right-click: In Chromium on Wayland, long-press often does *not*
|
||||
# open the context menu (Chromium handles touch itself). Elsewhere (e.g. desktop) it may
|
||||
# work. For long-press right-click *inside* Chromium, use evdev-right-click-emulation
|
||||
# at the input layer so all apps (including Chromium) receive real right-click events:
|
||||
# https://github.com/PeterCxy/evdev-right-click-emulation
|
||||
#
|
||||
# Disable keyring prompts
|
||||
export GNOME_KEYRING_CONTROL=""
|
||||
|
||||
# Prefer Wayland when available so touch long-press produces right-click (context menu)
|
||||
# like the rest of the desktop. X11/XWayland does not get that behavior for Chromium.
|
||||
USE_WAYLAND=0
|
||||
if [ -n "$WAYLAND_DISPLAY" ] && [ -S "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$WAYLAND_DISPLAY" ]; then
|
||||
USE_WAYLAND=1
|
||||
fi
|
||||
|
||||
if [ "$USE_WAYLAND" -eq 1 ]; then
|
||||
# Native Wayland: fullscreen + touch-friendly (long-press = right-click)
|
||||
# Wayland + labwc compositor.
|
||||
export GDK_BACKEND=wayland
|
||||
# Wait for compositor
|
||||
for i in {1..60}; do
|
||||
@@ -19,38 +21,22 @@ if [ "$USE_WAYLAND" -eq 1 ]; then
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
sleep 3
|
||||
/usr/bin/chromium --start-fullscreen --noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=wayland --enable-features=WaylandWindowDecorations --disable-features=UseChromeOSDirectVideoDecoder --app=http://127.0.0.1:8080 &
|
||||
else
|
||||
# Fallback: X11 (e.g. no Wayland session)
|
||||
export DISPLAY=:0
|
||||
export GDK_BACKEND=x11
|
||||
unset WAYLAND_DISPLAY
|
||||
for i in {1..60}; do
|
||||
if xset q >/dev/null 2>&1 || [ -n "$DISPLAY" ]; then
|
||||
if pgrep -x pcmanfm >/dev/null 2>&1 || pgrep -x lxsession >/dev/null 2>&1 || pgrep -x xfdesktop >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
sleep 5
|
||||
/usr/bin/chromium --start-fullscreen --noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=x11 --disable-features=UseChromeOSDirectVideoDecoder --app=http://127.0.0.1:8080 &
|
||||
sleep 3
|
||||
for i in {1..10}; do
|
||||
WINDOW_ID=$(wmctrl -l 2>/dev/null | grep -i chromium | head -1 | awk '{print $1}')
|
||||
if [ -n "$WINDOW_ID" ]; then
|
||||
wmctrl -i -r "$WINDOW_ID" -b add,fullscreen 2>/dev/null
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
fi
|
||||
# URL to open in Chromium (app mode)
|
||||
#CHROMIUM_APP_URL="${CHROMIUM_APP_URL:-http://127.0.0.1:8080}"
|
||||
CHROMIUM_APP_URL="${CHROMIUM_APP_URL:-https://tototheo.com}"
|
||||
|
||||
# Mode: "fullscreen" or "kiosk"
|
||||
CHROMIUM_MODE="${CHROMIUM_MODE:-fullscreen}"
|
||||
|
||||
CHROMIUM_OPTS="--noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=wayland --enable-features=WaylandWindowDecorations --disable-features=UseChromeOSDirectVideoDecoder --app=${CHROMIUM_APP_URL}"
|
||||
|
||||
case "${CHROMIUM_MODE}" in
|
||||
kiosk) CHROMIUM_OPTS="--kiosk ${CHROMIUM_OPTS}" ;;
|
||||
*) CHROMIUM_OPTS="--start-fullscreen ${CHROMIUM_OPTS}" ;;
|
||||
esac
|
||||
|
||||
sleep 3
|
||||
/usr/bin/chromium $CHROMIUM_OPTS &
|
||||
|
||||
# Keep script running
|
||||
wait
|
||||
|
||||
# Kiosk mode (commented out - uncomment to use instead of fullscreen)
|
||||
# /usr/bin/chromium --kiosk --noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=x11 --app=http://127.0.0.1:8080
|
||||
|
||||
Reference in New Issue
Block a user