Vitrine is an open-source, touch-optimized system monitoring dashboard purpose-built for the HYTE Y70 Touch Infinite case display panel running on Linux. It replaces the proprietary Windows-only HYTE Nexus software with a native Linux application that provides real-time hardware telemetry, animated visualizations, and full touch interaction on the Y70's integrated 14.9" vertical touchscreen.
The name "Vitrine" is French for "glass display case" or "showcase window" — a direct reference to the Y70's glass panel construction that showcases both hardware and data.
Repository: GitHub (public, open source) License: GPL v3 Target Platform: Linux (primary target: Bazzite / Fedora Atomic, but should work on any modern Linux distribution with Wayland or X11 support) Target Display: HYTE Y70 Touch Infinite — 682 x 2560 pixels (2.5K), 60Hz, 14.9" IPS, 10-point multi-touch, connected via DisplayPort + USB 2.0
| Layer | Technology | Rationale |
|---|---|---|
| Application Shell | Tauri v2 | Lightweight native webview shell (~5-10MB binary), minimal RAM footprint for 24/7 operation alongside games, single-binary distribution |
| Backend | Rust (integrated into Tauri) | Zero-overhead sensor reading via sysfs, native Tauri IPC bridge to frontend, TOML config parsing with serde, no separate process to manage |
| Frontend | React 18 + TypeScript | Rich component ecosystem, excellent touch event handling, strong Claude Code output quality |
| Styling | shadcn/ui + Tailwind CSS | Owned component files (not external dependency), Radix UI primitives for accessibility, Tailwind for rapid custom styling |
| Data Visualization | D3.js | Full creative control over every SVG element and animation, required for the animated gauge/graph aesthetic, no pre-built component limitations |
| Package Manager | pnpm | Fast, strict dependency resolution, disk-efficient |
| Configuration | TOML | Rust-native serde support, human-readable, clean syntax for end users |
Vitrine is a single Tauri application consisting of:
-
Rust Backend (src-tauri/): Reads hardware sensors from Linux sysfs, /proc, and lm-sensors interfaces. Exposes data to the frontend via Tauri's IPC command/event system. Manages configuration persistence. Handles auto-start and display targeting.
-
React Frontend (src/): Renders the touch-optimized dashboard UI. Receives sensor data via Tauri event listeners. Manages widget layout state, page navigation, touch gestures, and D3 visualizations.
-
Configuration Layer: TOML file stored at
~/.config/vitrine/config.tomldefining widget layout, theme settings, polling intervals, and page configurations. Editable via the in-app settings UI or manually by the user.
Linux sysfs/proc/lm-sensors
│
▼
Rust Sensor Module (polls at configurable interval)
│
▼
Tauri IPC Event ("sensor-update")
│
▼
React Frontend State (via useEffect listener)
│
▼
D3.js Visualizations (animated gauge/graph updates)
The Rust backend must read the following sensor data from Linux system interfaces. All sensor reading must be non-blocking and async. Failed sensor reads should return null/None rather than crashing — graceful degradation is required since not all hardware exposes all sensors.
CPU Telemetry:
- Overall CPU utilization percentage (from /proc/stat)
- Per-core utilization percentages (from /proc/stat, calculated per-core)
- CPU temperature (from /sys/class/hwmon/ or lm-sensors, typically k10temp for AMD Ryzen)
- Per-core temperatures if available
- Current CPU frequency per-core (from /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq)
- CPU package power draw if available (from hwmon RAPL interface)
GPU Telemetry:
- GPU utilization percentage (AMDGPU: /sys/class/drm/card*/device/gpu_busy_percent; NVIDIA: nvidia-smi or NVML)
- GPU temperature (AMDGPU: hwmon temp1_input; NVIDIA: nvidia-smi)
- GPU clock frequency (AMDGPU: /sys/class/drm/card*/device/pp_dpm_sclk; NVIDIA: nvidia-smi)
- VRAM usage — used and total (AMDGPU: /sys/class/drm/card*/device/mem_info_vram_used and mem_info_vram_total; NVIDIA: nvidia-smi)
- GPU power draw if available
Note on GPU support: The primary user will transition from an NVIDIA GTX 1070 (nvidia-smi/NVML) to an AMD RX 9070 XT (AMDGPU sysfs). The backend must support both NVIDIA (via nvidia-smi CLI parsing or NVML bindings) and AMD (via sysfs direct reads). Detection should be automatic based on which GPU driver is loaded.
Memory:
- Total RAM, used RAM, available RAM (from /proc/meminfo)
- Usage percentage
Storage (NVMe):
- NVMe drive temperature (from /sys/class/nvme/nvme*/hwmon*/temp1_input)
- Read/write throughput (from /proc/diskstats, calculated as delta between polls)
- Drive identification (model name from /sys/class/nvme/nvme*/model)
Network:
- Upload speed in bytes/sec (from /proc/net/dev, calculated as delta)
- Download speed in bytes/sec (from /proc/net/dev, calculated as delta)
- Active interface detection (skip loopback)
Fan Speeds:
- All detected fan RPM values from hwmon (from /sys/class/hwmon/hwmon*/fan*_input)
- Fan label identification where available (from /sys/class/hwmon/hwmon*/fan*_label)
The sensor polling interval must be user-configurable via both the TOML config file and the in-app settings UI. The allowed range is 250ms to 5000ms, with a default of 1000ms (1 second). The frontend should interpolate/animate between data points for smooth visual transitions regardless of the polling interval.
The Y70 Touch Infinite panel is 682 pixels wide and 2560 pixels tall at native resolution. The panel is physically oriented vertically (portrait) in the case chassis. All UI design must be optimized for this extreme portrait aspect ratio (approximately 1:3.75).
Theme: "Frosted Glass" — Light/White Aesthetic
The design must complement the user's white PC build. The visual language is:
- Background: Subtle gradient or soft solid in off-white/light gray (#F8F9FA to #EBEDF0 range), with optional translucent frosted-glass effect on widget cards
- Widget Cards: Semi-transparent white cards with soft shadows and subtle border (frosted glass effect using backdrop-blur where supported by the webview). Rounded corners (12-16px radius).
- Typography: Clean sans-serif font (Inter or system-ui). Large, bold numbers for primary metrics. Lighter weight for labels and secondary info.
- Accent Colors: Configurable accent color stored in config. Default: soft blue (#60A5FA) for primary indicators and highlights. Accent color is used for gauge fills, graph lines, active states, and interactive element highlights.
- Data Colors: Temperature uses a gradient from blue (cool) through green (normal) to orange/red (hot) based on configurable threshold values. Utilization percentages use the accent color with opacity mapping to percentage.
- Animations: All gauge and graph transitions should be smooth (200-400ms easing). D3 transitions must feel fluid, not jarring. Particle effects and ambient animations should be subtle — enhance the aesthetic without being distracting during gaming.
- Glow Effects: Primary metric values and gauge fills should have a subtle glow/bloom effect using CSS box-shadow or SVG filters, giving the dashboard a modern, luminous quality against the white background.
The dashboard uses a swipeable page carousel with a draggable widget grid within each page.
Page Navigation:
- Horizontal swipe (left/right) navigates between pages
- Page indicator dots at the bottom of the screen show current position and total pages
- Swipe gesture must feel native — use momentum-based physics with configurable deceleration
- Minimum 2 pages in default configuration, user can add/remove pages via settings
- Swipe threshold: 50px horizontal movement with velocity > 0.3px/ms triggers page transition
Widget Grid (within each page):
- CSS Grid-based layout, 2 columns wide (each column ~321px accounting for gap and padding)
- Widgets can span 1 or 2 columns
- Widgets have a fixed set of height options: small (1 row, ~200px), medium (2 rows, ~420px), large (3 rows, ~640px)
- Vertical scrolling within a page if widgets exceed viewport height
- Page padding: 16px on all sides. Grid gap: 12px.
Drag and Drop:
- Long-press (500ms) on a widget activates drag mode — widget lifts with scale animation (1.05x) and shadow increase
- Other widgets animate to show available drop zones
- Drop snaps to grid position
- Layout changes persist to the TOML config file automatically
- Drag handle is the entire widget card surface (long-press distinguishes from tap and swipe)
| Gesture | Action |
|---|---|
| Swipe left/right | Navigate between pages |
| Tap on widget | Expand widget to detail view (full-width modal overlay with expanded data, historical graph, and additional metrics) |
| Long-press on widget (500ms) | Activate drag mode for rearranging |
| Tap outside expanded widget | Collapse back to grid view |
| Swipe down from top edge | Open settings panel (slide-down drawer) |
| Tap page indicator dot | Jump to that page |
All touch interactions must use passive event listeners where possible for performance. Touch feedback should include subtle haptic-style visual responses (brief scale pulse on tap, etc.).
Each widget is a self-contained React component that receives its data via props from the parent page's sensor state. All widgets must implement a compact (grid) view and an expanded (detail) view.
Compact View (2-column span, medium height):
- Large primary number: CPU temperature in °C with color-coded glow
- Circular arc gauge (D3) showing overall CPU utilization (0-100%) with animated fill
- Current frequency displayed below the gauge
- Small sparkline (last 60 data points) showing temperature history
Expanded View:
- Per-core utilization bar chart (D3 animated horizontal bars)
- Per-core frequency readout
- Per-core temperature if available
- Temperature history graph (last 5 minutes)
- CPU power draw if available
Compact View (2-column span, medium height):
- Large primary number: GPU temperature in °C with color-coded glow
- Circular arc gauge (D3) showing GPU utilization
- VRAM usage bar below gauge (used / total with percentage)
- GPU clock frequency display
Expanded View:
- Temperature history graph
- Utilization history graph
- VRAM usage over time
- Power draw if available
- Clock frequency over time
Compact View (1-column span, small height):
- Radial progress ring (D3) showing usage percentage
- "XX.X / YY.Y GB" text in center
- Color transitions from accent (low usage) to orange to red as usage increases
Expanded View:
- Memory usage over time graph
- Breakdown if available (cached, buffers, swap)
Compact View (1-column span, small height):
- Drive name/model truncated
- Temperature with color coding
- Read/Write throughput as small real-time counters (MB/s)
Expanded View:
- Per-drive breakdown if multiple NVMe drives detected
- Throughput history graph
- Temperature history graph
- Drive health info if available (from smartctl)
Compact View (1-column span, small height):
- Upload speed (▲ arrow + value in appropriate unit: KB/s, MB/s)
- Download speed (▼ arrow + value)
- Small dual-line sparkline showing recent history
Expanded View:
- Full throughput history graph (upload and download overlaid)
- Interface name and IP address
- Total session data transferred
Compact View (2-column span, medium height):
- Album art (large, fills most of the widget with rounded corners)
- Song title (bold, truncated with ellipsis if needed)
- Artist name (lighter weight)
- Playback progress bar (thin, accent-colored)
- Touch controls: previous, play/pause, next (tap targets minimum 44px)
Expanded View:
- Larger album art
- Full song title and artist (no truncation)
- Album name
- Playback progress with elapsed/remaining time
- Volume control slider (if supported by MPRIS2)
MPRIS2 Integration:
The Rust backend listens to the D-Bus MPRIS2 interface (org.mpris.MediaPlayer2.*) for media player state. It should auto-detect running media players (Tidal desktop app, Spotify, Firefox playing media, etc.) and expose the currently active player's metadata to the frontend. This includes: track title, artist, album, album art URL/path, playback status (playing/paused/stopped), track position, and track duration. Playback control commands (play, pause, next, previous) are sent back through Tauri IPC to the Rust backend, which issues D-Bus method calls to the active player.
The settings panel slides down from the top of the screen when the user swipes down from the top edge. It provides touch-accessible configuration for:
- Polling Interval: Slider from 250ms to 5000ms with labeled presets (Fast: 250ms, Normal: 1000ms, Relaxed: 3000ms)
- Accent Color: Color picker with preset swatches (blue, purple, cyan, green, pink, orange, white) plus a custom hex input
- Theme: Toggle between light (default, white frosted glass) and dark mode (dark frosted glass for those who want it)
- Pages: Add/remove pages, rename pages
- Widget Library: Grid of available widgets that can be dragged onto a page or tapped to add to the current page
- About: Version info, links to GitHub repo
Settings changes apply immediately (live preview) and persist to the TOML config file on close.
Location: ~/.config/vitrine/config.toml
The configuration file is the source of truth for all user preferences and layout state. It must be human-readable and manually editable. The application watches this file for changes and hot-reloads when modified externally.
[general]
polling_interval_ms = 1000
auto_start = true
target_display = "auto" # "auto" detects Y70 panel, or specify output name
[theme]
mode = "light" # "light" or "dark"
accent_color = "#FF6A13"
background_opacity = 0.85
enable_glow_effects = true
enable_animations = true
[temperature_thresholds]
cpu_warn = 75
cpu_critical = 90
gpu_warn = 80
gpu_critical = 95
[[pages]]
name = "System"
widgets = [
{ type = "cpu_monitor", position = { col = 0, row = 0 }, size = { cols = 2, rows = 2 } },
{ type = "gpu_monitor", position = { col = 0, row = 2 }, size = { cols = 2, rows = 2 } },
{ type = "ram_usage", position = { col = 0, row = 4 }, size = { cols = 1, rows = 1 } },
{ type = "nvme_storage", position = { col = 1, row = 4 }, size = { cols = 1, rows = 1 } },
{ type = "network_monitor", position = { col = 0, row = 5 }, size = { cols = 1, rows = 1 } },
]
[[pages]]
name = "Media"
widgets = [
{ type = "media_player", position = { col = 0, row = 0 }, size = { cols = 2, rows = 2 } },
]Tauri produces a single binary (AppImage or native package). The recommended distribution format for Bazzite and immutable Linux distros is AppImage, since it runs without installation and doesn't require modifying the immutable filesystem.
Vitrine must start automatically on boot and display on the Y70 touch panel without user interaction. The implementation:
-
Systemd User Service: A systemd user unit file (
~/.config/systemd/user/vitrine.service) launches the Vitrine binary on login. -
Display Targeting: Vitrine must open its window on the Y70 panel specifically, not on the primary gaming monitor. On Wayland/KDE, this is achieved via KDE Window Rules that match the Vitrine window class and assign it to the Y70 output. On X11, the
--displayflag or window manager rules achieve the same. The config file'starget_displaysetting specifies which output to use, with "auto" detecting the Y70 panel by its EDID or resolution signature (682x2560 is unique enough to auto-detect reliably). -
Window Behavior: The Tauri window must be configured as:
- Fullscreen on the target display (682x2560)
- No window decorations (frameless)
- Always on the assigned display (does not follow focus)
- Below all other windows (does not steal focus from games)
- Excluded from taskbar and window switcher
- Not affected by desktop virtual workspace changes
An install script (install.sh) in the repo root must:
- Copy the Vitrine binary to
~/.local/bin/vitrine - Create the default config at
~/.config/vitrine/config.tomlif it doesn't exist - Install the systemd user service file
- Enable and start the service
- Configure KDE Window Rules for display targeting (via
kwriteconfig5or kcm script)
vitrine/
├── README.md
├── LICENSE # GPL v3
├── install.sh # Installation and auto-start setup script
├── src-tauri/
│ ├── Cargo.toml
│ ├── tauri.conf.json # Tauri window config (frameless, size, etc.)
│ ├── src/
│ │ ├── main.rs # Tauri entry point, app setup, event loop
│ │ ├── sensors/
│ │ │ ├── mod.rs # Sensor module exports
│ │ │ ├── cpu.rs # CPU telemetry (temp, freq, utilization)
│ │ │ ├── gpu.rs # GPU telemetry (AMD sysfs + NVIDIA nvidia-smi)
│ │ │ ├── memory.rs # RAM usage from /proc/meminfo
│ │ │ ├── nvme.rs # NVMe temps and throughput
│ │ │ ├── network.rs # Network interface speeds
│ │ │ └── fans.rs # Fan RPM from hwmon
│ │ ├── media/
│ │ │ └── mpris.rs # MPRIS2 D-Bus integration for media player
│ │ ├── config/
│ │ │ ├── mod.rs # Config module exports
│ │ │ └── schema.rs # TOML config structs with serde
│ │ └── commands.rs # Tauri IPC command handlers
│ └── icons/ # App icons
├── src/
│ ├── main.tsx # React entry point
│ ├── App.tsx # Root component — page carousel + settings drawer
│ ├── components/
│ │ ├── ui/ # shadcn/ui components (button, slider, etc.)
│ │ ├── layout/
│ │ │ ├── PageCarousel.tsx # Swipeable page container with gesture handling
│ │ │ ├── WidgetGrid.tsx # CSS Grid + drag-and-drop within a page
│ │ │ ├── WidgetCard.tsx # Individual widget wrapper (card styling, tap/long-press)
│ │ │ └── PageIndicator.tsx # Dot indicators for page position
│ │ ├── widgets/
│ │ │ ├── CpuMonitor.tsx # CPU widget — compact and expanded views
│ │ │ ├── GpuMonitor.tsx # GPU widget — compact and expanded views
│ │ │ ├── RamUsage.tsx # RAM widget
│ │ │ ├── NvmeStorage.tsx # NVMe widget
│ │ │ ├── NetworkMonitor.tsx # Network widget
│ │ │ └── MediaPlayer.tsx # Tidal/MPRIS2 media widget
│ │ ├── visualizations/
│ │ │ ├── ArcGauge.tsx # D3 circular arc gauge (reusable)
│ │ │ ├── RadialProgress.tsx # D3 radial progress ring (reusable)
│ │ │ ├── Sparkline.tsx # D3 mini line graph (reusable)
│ │ │ ├── HistoryGraph.tsx # D3 full-width time series graph (reusable)
│ │ │ └── AnimatedBar.tsx # D3 animated horizontal bar (reusable)
│ │ └── settings/
│ │ ├── SettingsDrawer.tsx # Slide-down settings panel
│ │ ├── PollingControl.tsx # Polling interval slider
│ │ ├── ThemeControl.tsx # Theme and accent color picker
│ │ └── PageManager.tsx # Add/remove/rename pages, widget library
│ ├── hooks/
│ │ ├── useSensorData.ts # Hook to subscribe to Tauri sensor events
│ │ ├── useMediaPlayer.ts # Hook for MPRIS2 media state
│ │ ├── useConfig.ts # Hook to read/write config via Tauri commands
│ │ ├── useTouchGestures.ts # Hook for swipe, tap, long-press gesture detection
│ │ └── useWidgetLayout.ts # Hook for drag-and-drop layout state management
│ ├── lib/
│ │ ├── types.ts # TypeScript interfaces for all sensor data and config
│ │ ├── constants.ts # Default values, thresholds, animation durations
│ │ └── utils.ts # Formatting helpers (bytes, frequencies, temperatures)
│ └── styles/
│ └── globals.css # Tailwind directives and global styles
├── package.json
├── pnpm-lock.yaml
├── tsconfig.json
├── tailwind.config.ts
├── postcss.config.js
├── vite.config.ts # Vite config for Tauri frontend
└── components.json # shadcn/ui configuration
Goal: Tauri app launches, reads CPU temperature, displays a single animated D3 gauge on a 682x2560 window.
Tasks:
- Initialize Tauri v2 project with React + TypeScript + Vite frontend
- Configure pnpm, Tailwind CSS, shadcn/ui
- Set Tauri window to 682x2560, frameless, with transparent background
- Implement
cpu.rssensor module — read CPU temp and overall utilization from sysfs - Create Tauri IPC command to return sensor data on demand
- Set up polling loop in Rust that emits "sensor-update" events at 1-second intervals
- Build
useSensorDatahook in React to listen for Tauri events - Build
ArcGauge.tsxD3 component — circular arc gauge with animated transitions and glow effect - Render a single CPU gauge centered on screen to verify the full data pipeline works
- Verify the window opens at the correct resolution and can be manually moved to the Y70 panel
Goal: All sensor modules implemented and emitting data.
Tasks:
- Implement
gpu.rs— AMD sysfs + NVIDIA nvidia-smi support with auto-detection - Implement
memory.rs— /proc/meminfo parsing - Implement
nvme.rs— hwmon temperature + /proc/diskstats throughput - Implement
network.rs— /proc/net/dev interface speed calculation - Implement
fans.rs— hwmon fan RPM enumeration - Implement
mpris.rs— D-Bus MPRIS2 listener for media player metadata and controls - Unify all sensors into a single
SensorDatastruct emitted as one JSON payload per tick - Implement
config/schema.rs— TOML config deserialization with serde - Make polling interval configurable via config
Goal: All widgets rendered in a draggable grid with swipeable pages.
Tasks:
- Build all D3 visualization components:
ArcGauge,RadialProgress,Sparkline,HistoryGraph,AnimatedBar - Build all widget components with compact views:
CpuMonitor,GpuMonitor,RamUsage,NvmeStorage,NetworkMonitor,MediaPlayer - Build
WidgetCard.tsx— card wrapper with frosted glass styling, tap handler, long-press handler - Build
WidgetGrid.tsx— CSS Grid layout with drag-and-drop (use @dnd-kit/core for drag-and-drop) - Build
PageCarousel.tsx— horizontal swipe navigation with momentum physics (use framer-motion for gesture handling and page transitions) - Build
PageIndicator.tsx— dot navigation at bottom of screen - Implement widget expanded/detail view — full-width modal overlay triggered by tap
- Wire layout state to config — persist widget positions and page order to TOML
Goal: In-app settings panel, theme customization, visual polish, and auto-start.
Tasks:
- Build
SettingsDrawer.tsx— slide-down panel triggered by swipe from top edge - Implement polling interval control with live preview
- Implement accent color picker with preset swatches and custom hex input
- Implement light/dark theme toggle
- Implement page management (add, remove, rename)
- Implement widget library browser (available widgets to add to pages)
- Implement config hot-reload — watch
config.tomlfor external changes - Add glow effects and ambient animations to all widgets
- Add smooth page transition animations
- Build
install.sh— binary installation, systemd service, KDE window rules - Write README.md with screenshots, installation instructions, configuration guide, and contributing guidelines
tauriv2 — application shell and IPCserde+serde_json— JSON serialization for IPC payloadstoml+serde— TOML config deserializationtokio— async runtime for sensor polling loopnotify— filesystem watcher for config hot-reloadzbus— D-Bus client for MPRIS2 media player integrationsysinfo— cross-platform system information (supplement to direct sysfs reads)
react+react-domv18typescript@tauri-apps/apiv2 — Tauri frontend bindingsd3+@types/d3— data visualizationframer-motion— gesture handling (swipe, drag) and page transition animations@dnd-kit/core+@dnd-kit/sortable— drag-and-drop widget rearrangementtailwindcss+postcss+autoprefixer— stylingclass-variance-authority+clsx+tailwind-merge— shadcn/ui utilitieslucide-react— icons
Performance: The application must use less than 150MB RAM and less than 3% CPU at the default 1-second polling interval. D3 animations must maintain 60fps on the Y70's 60Hz panel. The application runs 24/7 alongside games and must not cause perceptible performance impact.
Reliability: The application must not crash if a sensor becomes unavailable (e.g., GPU removed, NVMe drive disconnected). All sensor reads must be wrapped in error handling that returns null/None for unavailable data, and widgets must display a graceful "N/A" or "—" state.
Startup Time: The application must be ready and displaying data within 3 seconds of launch.
Touch Responsiveness: All touch interactions must respond within 100ms. Gesture recognition must not have perceptible lag.
Graceful Degradation: On systems without a Y70 panel, the application should still run in a resizable window for development and testing purposes. The 682x2560 layout should be the default but not hard-coded as the only option.