mirror of
https://github.com/devmobasa/wayscriber.git
synced 2026-06-03 03:54:42 +02:00
Initial commit
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
||||
# Rust
|
||||
/target
|
||||
**/*.rs.bk
|
||||
|
||||
# Cargo
|
||||
# Keep Cargo.lock for binaries for reproducible builds (uncomment to ignore for libs)
|
||||
# Cargo.lock
|
||||
|
||||
# Editors/OS
|
||||
.idea/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# ZoomIt reference files (not part of our implementation)
|
||||
/ZoomIt/
|
||||
|
||||
# Test scripts
|
||||
test_run.sh
|
||||
run.sh
|
||||
|
||||
!/packaging/hyprmarker.service
|
||||
packaging/
|
||||
Generated
+1951
File diff suppressed because it is too large
Load Diff
+37
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "hyprmarker"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
# Wayland
|
||||
wayland-client = "0.31"
|
||||
wayland-protocols = { version = "0.32", features = ["client", "unstable"] }
|
||||
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
|
||||
smithay-client-toolkit = { version = "0.20", default-features = false, features = ["calloop", "xkbcommon"] }
|
||||
calloop = "0.14"
|
||||
|
||||
# Cairo for drawing
|
||||
cairo-rs = { version = "0.21", features = ["png"] }
|
||||
cairo-sys-rs = "0.21"
|
||||
|
||||
# Pango for advanced text rendering and font support
|
||||
pango = "0.21"
|
||||
pangocairo = "0.21"
|
||||
|
||||
# Utils
|
||||
nix = "0.30"
|
||||
memmap2 = "0.9"
|
||||
thiserror = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
anyhow = "1.0"
|
||||
toml = "0.9"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
dirs = "6.0"
|
||||
|
||||
# Daemon mode
|
||||
signal-hook = "0.3"
|
||||
ksni = "0.3"
|
||||
tokio = { version = "1.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,451 @@
|
||||
# hyprmarker
|
||||
|
||||
A ZoomIt-like screen annotation tool for Wayland compositors, written in Rust. Draw freehand, create shapes, add text, and annotate your screen during presentations or screen recordings.
|
||||
|
||||
## Demo
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## Quick Install (Arch Linux)
|
||||
|
||||
**Install from AUR:**
|
||||
```bash
|
||||
# Using yay
|
||||
yay -S hyprmarker
|
||||
|
||||
# Using paru
|
||||
paru -S hyprmarker
|
||||
```
|
||||
|
||||
**Setup daemon mode:**
|
||||
```bash
|
||||
# Enable systemd service (starts on login)
|
||||
systemctl --user enable --now hyprmarker.service
|
||||
|
||||
# Add keybind to ~/.config/hypr/hyprland.conf
|
||||
bind = SUPER, D, exec, pkill -SIGUSR1 hyprmarker
|
||||
```
|
||||
|
||||
**Done!** Press `Super+D` to toggle overlay, draw with your mouse, press `Escape` to exit.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
hyprmarker replicates all the drawing features of Microsoft's ZoomIt for Linux with native Wayland support:
|
||||
|
||||
- **Freehand Drawing**: Draw freely with your cursor (default mode)
|
||||
- **Straight Lines**: Hold `Shift` while dragging
|
||||
- **Rectangles**: Hold `Ctrl` while dragging
|
||||
- **Ellipses/Circles**: Hold `Tab` while dragging
|
||||
- **Arrows**: Hold `Ctrl+Shift` while dragging
|
||||
- **Text Annotations**: Press `T` to enter text mode, with multi-line support (`Shift+Enter`) and custom fonts
|
||||
- **Color Selection**: Press color keys (R, G, B, Y, O, P, W, K)
|
||||
- **Adjustable Line Thickness**: Use `+`/`-` keys or scroll wheel
|
||||
- **Undo**: `Ctrl+Z` to undo last shape
|
||||
- **Clear All**: Press `E` to clear all annotations
|
||||
- **Help Overlay**: Press `F10` to show/hide keybinding help
|
||||
- **Status Bar**: Shows current tool, color, and thickness
|
||||
- **Customizable**: TOML-based configuration file
|
||||
- **Exit**: Press `Escape` to exit drawing mode
|
||||
|
||||
## Installation (Other Distros)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**System Dependencies:**
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install libcairo2-dev libwayland-dev libpango1.0-dev
|
||||
|
||||
# Fedora
|
||||
sudo dnf install cairo-devel wayland-devel pango-devel
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
- Wayland compositor with wlr-layer-shell protocol support (Hyprland, Sway, etc.)
|
||||
- Rust 1.70+ for building from source
|
||||
|
||||
### Build from Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/devmobasa/hyprmarker.git
|
||||
cd hyprmarker
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The binary will be at `target/release/hyprmarker`
|
||||
|
||||
### Install Script (Manual Build)
|
||||
|
||||
```bash
|
||||
# Build and install locally
|
||||
cargo build --release
|
||||
./tools/install.sh
|
||||
```
|
||||
|
||||
The installer will:
|
||||
- Install binary to `~/.local/bin/hyprmarker`
|
||||
- Create config directory at `~/.config/hyprmarker/`
|
||||
- Optionally add Hyprland configuration
|
||||
|
||||
## Usage
|
||||
|
||||
### Daemon Mode (Recommended)
|
||||
|
||||
Run hyprmarker as a background daemon and toggle it with `Super+D`:
|
||||
|
||||
**Option 1: Systemd User Service (Recommended)**
|
||||
|
||||
Automatically starts on login and appears in system tray:
|
||||
|
||||
```bash
|
||||
# Run installer and choose option 1
|
||||
./tools/install.sh
|
||||
|
||||
# Or setup manually:
|
||||
mkdir -p ~/.config/systemd/user
|
||||
cp packaging/hyprmarker.service ~/.config/systemd/user/
|
||||
systemctl --user enable --now hyprmarker.service
|
||||
```
|
||||
|
||||
Then add keybind to Hyprland config:
|
||||
```conf
|
||||
bind = SUPER, D, exec, pkill -SIGUSR1 hyprmarker
|
||||
```
|
||||
|
||||
**Option 2: Hyprland exec-once**
|
||||
|
||||
Run installer and choose option 2, or add manually to `~/.config/hypr/hyprland.conf`:
|
||||
```conf
|
||||
# Autostart hyprmarker daemon
|
||||
exec-once = hyprmarker --daemon
|
||||
|
||||
# Toggle overlay with Super+D
|
||||
bind = SUPER, D, exec, pkill -SIGUSR1 hyprmarker
|
||||
```
|
||||
|
||||
Then reload: `hyprctl reload`
|
||||
|
||||
**Usage:**
|
||||
- Daemon starts automatically on login
|
||||
- System tray icon appears (may be in Waybar drawer/expander)
|
||||
- Press `Super+D` to show overlay and start drawing
|
||||
- Press `Ctrl+Q` or `Escape` to hide overlay (daemon keeps running)
|
||||
- Right-click tray icon for menu (Toggle/Quit)
|
||||
|
||||
### One-Shot Mode
|
||||
|
||||
For quick one-time annotations:
|
||||
|
||||
```bash
|
||||
hyprmarker --active
|
||||
```
|
||||
|
||||
Or bind to a key:
|
||||
```conf
|
||||
bind = $mainMod, D, exec, hyprmarker --active
|
||||
```
|
||||
|
||||
### Controls Reference
|
||||
|
||||
| Action | Key/Mouse |
|
||||
|--------|-----------|
|
||||
| **Drawing Tools** |
|
||||
| Freehand pen | Default (just drag with left mouse) |
|
||||
| Straight line | Hold `Shift` + drag |
|
||||
| Rectangle | Hold `Ctrl` + drag |
|
||||
| Ellipse/Circle | Hold `Tab` + drag |
|
||||
| Arrow | Hold `Ctrl+Shift` + drag |
|
||||
| Text mode | Press `T`, click to position, type, `Shift+Enter` for new line, `Enter` to finish |
|
||||
| **Colors** |
|
||||
| Red | `R` |
|
||||
| Green | `G` |
|
||||
| Blue | `B` |
|
||||
| Yellow | `Y` |
|
||||
| Orange | `O` |
|
||||
| Pink | `P` |
|
||||
| White | `W` |
|
||||
| Black | `K` |
|
||||
| **Line Thickness** |
|
||||
| Increase | `+` or `=` or scroll down |
|
||||
| Decrease | `-` or `_` or scroll up |
|
||||
| **Editing** |
|
||||
| Undo last shape | `Ctrl+Z` |
|
||||
| Clear all | `E` |
|
||||
| Cancel action | Right-click or `Escape` |
|
||||
| **Help** |
|
||||
| Toggle help overlay | `F10` |
|
||||
| Exit overlay | `Escape` or `Ctrl+Q` |
|
||||
|
||||
## Configuration
|
||||
|
||||
hyprmarker supports customization through a TOML configuration file.
|
||||
|
||||
### Configuration Location
|
||||
|
||||
`~/.config/hyprmarker/config.toml`
|
||||
|
||||
### Creating Configuration
|
||||
|
||||
1. Copy the example config:
|
||||
```bash
|
||||
mkdir -p ~/.config/hyprmarker
|
||||
cp config.example.toml ~/.config/hyprmarker/config.toml
|
||||
```
|
||||
|
||||
2. Edit to your preferences:
|
||||
```bash
|
||||
nano ~/.config/hyprmarker/config.toml
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
```toml
|
||||
[drawing]
|
||||
# Default pen color: "red", "green", "blue", etc. or RGB [255, 0, 0]
|
||||
default_color = "red"
|
||||
default_thickness = 3.0 # Pixels (1.0-20.0)
|
||||
default_font_size = 32.0 # Pixels (8.0-72.0)
|
||||
|
||||
# Custom font configuration (Pango-based)
|
||||
font_family = "Sans" # Any installed font: "Monospace", "JetBrains Mono", etc.
|
||||
font_weight = "bold" # "normal", "bold", "light", or numeric (100-900)
|
||||
font_style = "normal" # "normal", "italic", "oblique"
|
||||
text_background_enabled = false # Semi-transparent background behind text
|
||||
|
||||
[arrow]
|
||||
length = 20.0 # Arrowhead length in pixels
|
||||
angle_degrees = 30.0 # Arrowhead angle (15-60)
|
||||
|
||||
[performance]
|
||||
buffer_count = 3 # 2=double, 3=triple (recommended), 4=quad buffering
|
||||
enable_vsync = true # Prevent tearing
|
||||
|
||||
[ui]
|
||||
show_status_bar = true
|
||||
status_bar_position = "bottom-left" # top-left, top-right, bottom-left, bottom-right
|
||||
```
|
||||
|
||||
See **[docs/CONFIG.md](docs/CONFIG.md)** for detailed configuration documentation.
|
||||
|
||||
## Architecture
|
||||
|
||||
hyprmarker is built with a native Wayland backend using modern Rust libraries:
|
||||
|
||||
```
|
||||
hyprmarker/
|
||||
├── src/
|
||||
│ ├── main.rs # Entry point, CLI parsing
|
||||
│ ├── daemon.rs # Daemon mode with signal handling
|
||||
│ ├── ui.rs # Status bar and help overlay rendering
|
||||
│ ├── util.rs # Utility functions
|
||||
│ ├── backend/
|
||||
│ │ ├── mod.rs # Backend module
|
||||
│ │ └── wayland.rs # Wayland wlr-layer-shell implementation
|
||||
│ ├── config/
|
||||
│ │ ├── mod.rs # Configuration loader and validator
|
||||
│ │ ├── types.rs # Config structure definitions
|
||||
│ │ └── enums.rs # Color specs and enums
|
||||
│ ├── draw/
|
||||
│ │ ├── mod.rs # Drawing module
|
||||
│ │ ├── color.rs # Color definitions and constants
|
||||
│ │ ├── font.rs # Font descriptor for Pango
|
||||
│ │ ├── frame.rs # Frame container for shapes
|
||||
│ │ ├── shape.rs # Shape definitions (lines, text, etc.)
|
||||
│ │ └── render.rs # Cairo/Pango rendering functions
|
||||
│ └── input/
|
||||
│ ├── mod.rs # Input handling module
|
||||
│ ├── state.rs # Drawing state machine
|
||||
│ ├── events.rs # Keyboard/mouse event types
|
||||
│ ├── modifiers.rs # Modifier key tracking
|
||||
│ └── tool.rs # Drawing tool enum
|
||||
├── tools/ # Helper scripts (install, run, reload)
|
||||
├── packaging/ # Distribution files (service, PKGBUILD)
|
||||
├── docs/ # Documentation
|
||||
└── config.example.toml # Example configuration
|
||||
```
|
||||
|
||||
### Wayland Backend
|
||||
|
||||
The Wayland backend uses:
|
||||
- **wlr-layer-shell**: Overlay surface creation
|
||||
- **wl_shm**: Shared memory buffers with triple buffering
|
||||
- **Cairo**: Vector graphics rendering
|
||||
- **smithay-client-toolkit**: Wayland protocol handling
|
||||
|
||||
**Features:**
|
||||
- ✅ Full-screen transparent overlay
|
||||
- ✅ Native HiDPI/scaling support
|
||||
- ✅ Triple-buffered rendering for smooth drawing
|
||||
- ✅ Frame-synchronized updates (VSync)
|
||||
- ✅ Exclusive input capture
|
||||
- ✅ All drawing tools with live preview
|
||||
|
||||
## Platform Support
|
||||
|
||||
| Platform | Status | Notes |
|
||||
|----------|--------|-------|
|
||||
| Wayland (Hyprland, Sway, etc.) | ✅ **SUPPORTED** | Requires wlr-layer-shell protocol |
|
||||
|
||||
|
||||
## Performance
|
||||
|
||||
hyprmarker is optimized for high-resolution displays with smooth 60 FPS drawing:
|
||||
|
||||
- **Triple buffering**: Prevents flickering during fast drawing
|
||||
- **VSync synchronization**: Smooth frame pacing
|
||||
- **Dirty region optimization**: Minimal data transfer
|
||||
- **Efficient rendering**: Cairo vector graphics with buffer pooling
|
||||
|
||||
Performance characteristics:
|
||||
- **4K displays**: Smooth 60 FPS
|
||||
- **1080p/1440p**: Smooth 60 FPS
|
||||
- **CPU usage**: Low, event-driven architecture
|
||||
|
||||
Configure buffer count and VSync in `config.toml` for your setup.
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[docs/SETUP.md](docs/SETUP.md)** - Detailed installation and system setup
|
||||
- **[docs/CONFIG.md](docs/CONFIG.md)** - Configuration reference
|
||||
|
||||
## Comparison with ZoomIt
|
||||
|
||||
| Feature | ZoomIt (Windows) | hyprmarker (Linux) |
|
||||
|---------|-----------------|-------------------|
|
||||
| Freehand drawing | ✅ | ✅ |
|
||||
| Straight lines | ✅ | ✅ |
|
||||
| Rectangles | ✅ | ✅ |
|
||||
| Ellipses | ✅ | ✅ |
|
||||
| Arrows | ✅ | ✅ |
|
||||
| Text annotations | ✅ | ✅ |
|
||||
| Multi-line text | ❌ | ✅ (Shift+Enter) |
|
||||
| Custom fonts | ❌ | ✅ (Pango) |
|
||||
| Color selection | ✅ | ✅ (8 colors) |
|
||||
| Undo | ✅ | ✅ |
|
||||
| Clear all | ✅ | ✅ |
|
||||
| Help overlay | ❌ | ✅ |
|
||||
| Status bar | ❌ | ✅ |
|
||||
| Configuration file | ❌ | ✅ |
|
||||
| Scroll wheel thickness | ❌ | ✅ |
|
||||
| Zoom functionality | ✅ | ❌ (not planned) |
|
||||
| Break timer | ✅ | ❌ (not planned) |
|
||||
| Screen recording | ✅ | ❌ (not planned) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Overlay not appearing
|
||||
|
||||
1. **Check Wayland environment:**
|
||||
```bash
|
||||
echo $WAYLAND_DISPLAY # Should show wayland-0 or similar
|
||||
```
|
||||
|
||||
2. **Check compositor support:**
|
||||
```bash
|
||||
# Verify wlr-layer-shell protocol is available
|
||||
# Works on: Hyprland, Sway, river, etc.
|
||||
# Does NOT work on: GNOME Wayland, KDE Wayland (no wlr-layer-shell)
|
||||
```
|
||||
|
||||
3. **Check logs:**
|
||||
```bash
|
||||
RUST_LOG=info ./target/release/hyprmarker --active
|
||||
```
|
||||
|
||||
### Config not loading
|
||||
|
||||
```bash
|
||||
# Verify config file exists
|
||||
ls -la ~/.config/hyprmarker/config.toml
|
||||
|
||||
# Check for TOML syntax errors in logs
|
||||
RUST_LOG=info ./target/release/hyprmarker --active
|
||||
```
|
||||
|
||||
### Performance issues
|
||||
|
||||
Try adjusting performance settings in `config.toml`:
|
||||
```toml
|
||||
[performance]
|
||||
buffer_count = 2 # Reduce if memory-constrained
|
||||
enable_vsync = true # Keep enabled to prevent tearing
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Building for Development
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
cargo run -- --active
|
||||
```
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
```bash
|
||||
RUST_LOG=debug cargo run -- --active
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
cargo clippy # Linting
|
||||
cargo fmt # Code formatting
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Areas where help is needed:
|
||||
|
||||
1. **Additional compositors**: Test on more Wayland compositors
|
||||
2. **Multi-monitor support**: Per-monitor overlay surfaces
|
||||
3. **Additional tools**: Filled shapes, highlighter, eraser
|
||||
4. **Daemon mode**: Background process with hotkey toggle
|
||||
5. **Save/load**: Export annotations to image files
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) file for details
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Inspired by Microsoft's [ZoomIt](https://learn.microsoft.com/en-us/sysinternals/downloads/zoomit)
|
||||
- Similar to [Gromit-MPX](https://github.com/bk138/gromit-mpx)
|
||||
- Uses [Cairo](https://www.cairographics.org/) for rendering
|
||||
- Built with [Rust](https://www.rust-lang.org/) and [smithay-client-toolkit](https://github.com/Smithay/client-toolkit)
|
||||
|
||||
## Development
|
||||
|
||||
This tool was developed with AI assistance:
|
||||
- **Initial concept & planning**: ChatGPT
|
||||
- **Architecture review & design**: GitHub Copilot
|
||||
- **Implementation**: Claude Code (Anthropic)
|
||||
|
||||
Created as a native Wayland implementation of ZoomIt annotation features for Linux desktop environments.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] Native Wayland wlr-layer-shell implementation
|
||||
- [x] Configuration file support
|
||||
- [x] Status bar and help overlay
|
||||
- [x] Scroll wheel thickness adjustment
|
||||
- [x] Daemon mode with global hotkey toggle (Super+D)
|
||||
- [x] System tray integration
|
||||
- [x] Autostart with systemd user service
|
||||
- [x] Multi-line text support (Shift+Enter)
|
||||
- [x] Custom fonts with Pango rendering
|
||||
- [ ] Multi-monitor support with per-monitor surfaces
|
||||
- [ ] Additional shapes (filled shapes, highlighter)
|
||||
- [ ] Save annotations to image file
|
||||
- [ ] Eraser tool
|
||||
- [ ] Color picker
|
||||
|
||||
---
|
||||
|
||||
**Note**: This tool focuses solely on screen annotation. For screen magnification (zoom), consider using built-in accessibility tools or dedicated magnifiers like `wlr-randr` zoom or compositor-specific zoom features.
|
||||
@@ -0,0 +1,151 @@
|
||||
# hyprmarker configuration file
|
||||
# Location: ~/.config/hyprmarker/config.toml
|
||||
#
|
||||
# All settings are optional. If not specified, defaults will be used.
|
||||
# Delete this file to reset to defaults.
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# DRAWING SETTINGS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[drawing]
|
||||
# Default pen color
|
||||
# Options: "red", "green", "blue", "yellow", "orange", "pink", "white", "black"
|
||||
# Or RGB array: [255, 0, 0]
|
||||
default_color = "red"
|
||||
|
||||
# Default pen thickness in pixels (1.0 - 20.0)
|
||||
default_thickness = 3.0
|
||||
|
||||
# Default font size for text mode (8.0 - 72.0)
|
||||
default_font_size = 32.0
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
# Font Configuration (Pango-based text rendering)
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Font family name for text rendering
|
||||
# Install fonts system-wide and reference by family name
|
||||
# Examples to try:
|
||||
# font_family = "Sans" # Default sans-serif
|
||||
# font_family = "Monospace" # Fixed-width font
|
||||
# font_family = "Serif" # Serif font
|
||||
# font_family = "JetBrains Mono" # If installed
|
||||
# font_family = "Fira Code" # If installed
|
||||
# font_family = "Maple Mono NF" # If installed
|
||||
# font_family = "DejaVu Sans" # Common on Linux
|
||||
# font_family = "Liberation Sans" # Common on Linux
|
||||
# font_family = "Noto Sans" # Google Noto fonts
|
||||
# Falls back to "Sans" if the specified font is not available
|
||||
font_family = "Sans"
|
||||
|
||||
# Font weight: "normal", "bold", "light", "ultralight", "heavy", "ultrabold"
|
||||
# Or numeric: 100-900 (400=normal, 700=bold)
|
||||
# Try different weights:
|
||||
# font_weight = "normal" # Regular weight
|
||||
# font_weight = "bold" # Bold (default, best visibility)
|
||||
# font_weight = "light" # Light weight
|
||||
# font_weight = "600" # Semi-bold (numeric)
|
||||
font_weight = "bold"
|
||||
|
||||
# Font style: "normal", "italic", "oblique"
|
||||
# Try different styles:
|
||||
# font_style = "normal" # Standard upright text
|
||||
# font_style = "italic" # Italic/cursive
|
||||
# font_style = "oblique" # Slanted text
|
||||
font_style = "normal"
|
||||
|
||||
# Enable semi-transparent background box behind text for better contrast
|
||||
# Set to true if you find text hard to read against complex backgrounds
|
||||
# Default: false (no background, cleaner look with just stroke outline)
|
||||
text_background_enabled = false
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# ARROW SETTINGS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[arrow]
|
||||
# Arrowhead length in pixels
|
||||
length = 20.0
|
||||
|
||||
# Arrowhead angle in degrees (15-60)
|
||||
# 30 degrees gives a nice balanced arrow
|
||||
angle_degrees = 30.0
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# PERFORMANCE SETTINGS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[performance]
|
||||
# Number of buffers for rendering (2, 3, or 4)
|
||||
# 2 = double buffering (low memory)
|
||||
# 3 = triple buffering (recommended, smooth)
|
||||
# 4 = quad buffering (ultra-smooth on high refresh displays)
|
||||
buffer_count = 3
|
||||
|
||||
# Enable vsync frame synchronization
|
||||
# Prevents tearing and limits rendering to display refresh rate
|
||||
enable_vsync = true
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# UI SETTINGS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[ui]
|
||||
# Show status bar with current color/thickness/tool
|
||||
show_status_bar = true
|
||||
|
||||
# Status bar position
|
||||
# Options: "top-left", "top-right", "bottom-left", "bottom-right"
|
||||
status_bar_position = "bottom-left"
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
# Status Bar Styling
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[ui.status_bar_style]
|
||||
# Font size for status bar text
|
||||
font_size = 21.0
|
||||
|
||||
# Padding around status bar text
|
||||
padding = 15.0
|
||||
|
||||
# Background color [R, G, B, A] (0.0-1.0 range)
|
||||
# Default: semi-transparent black (85% opaque for visibility)
|
||||
bg_color = [0.0, 0.0, 0.0, 0.85]
|
||||
|
||||
# Text color [R, G, B, A] (0.0-1.0 range)
|
||||
# Default: white
|
||||
text_color = [1.0, 1.0, 1.0, 1.0]
|
||||
|
||||
# Color indicator dot radius
|
||||
dot_radius = 6.0
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
# Help Overlay Styling (Press F10 to toggle)
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[ui.help_overlay_style]
|
||||
# Font size for help overlay text
|
||||
font_size = 16.0
|
||||
|
||||
# Line height for help text
|
||||
line_height = 22.0
|
||||
|
||||
# Padding around help box
|
||||
padding = 20.0
|
||||
|
||||
# Background color [R, G, B, A] (0.0-1.0 range)
|
||||
# Default: semi-transparent black (darker than status bar)
|
||||
bg_color = [0.0, 0.0, 0.0, 0.85]
|
||||
|
||||
# Border color [R, G, B, A] (0.0-1.0 range)
|
||||
# Default: light blue
|
||||
border_color = [0.3, 0.6, 1.0, 0.9]
|
||||
|
||||
# Border line width
|
||||
border_width = 2.0
|
||||
|
||||
# Text color [R, G, B, A] (0.0-1.0 range)
|
||||
# Default: white
|
||||
text_color = [1.0, 1.0, 1.0, 1.0]
|
||||
@@ -0,0 +1,175 @@
|
||||
# AUR Update Workflow
|
||||
|
||||
## Quick Update Process
|
||||
|
||||
When you're ready to release a new version:
|
||||
|
||||
### 1. Update version in your project
|
||||
|
||||
```bash
|
||||
cd ~/code/hyprmarker
|
||||
|
||||
# Edit Cargo.toml - update version
|
||||
vim Cargo.toml
|
||||
|
||||
# Commit changes
|
||||
git add Cargo.toml
|
||||
git commit -m "Bump version to 0.2.0"
|
||||
git push origin master
|
||||
```
|
||||
|
||||
### 2. Run the automated AUR update script
|
||||
|
||||
```bash
|
||||
cd ~/code/hyprmarker
|
||||
./tools/update-aur.sh
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
1. ✅ Reads version from `Cargo.toml`
|
||||
2. ✅ Creates git tag (e.g., `v0.2.0`) and pushes to GitHub
|
||||
3. ✅ Copies `packaging/PKGBUILD.aur` to `~/aur-packages/PKGBUILD`
|
||||
4. ✅ Updates `pkgver` and resets `pkgrel` to 1
|
||||
5. ✅ Generates `.SRCINFO`
|
||||
6. ✅ Optionally tests build locally
|
||||
7. ✅ Commits and pushes to AUR
|
||||
|
||||
**Done!** Users can now update with `yay -Syu hyprmarker`
|
||||
|
||||
---
|
||||
|
||||
## Manual Update Process
|
||||
|
||||
If you prefer to do it manually:
|
||||
|
||||
```bash
|
||||
# 1. Update version in Cargo.toml
|
||||
cd ~/code/hyprmarker
|
||||
vim Cargo.toml # Change version to 0.2.0
|
||||
|
||||
# 2. Commit and tag
|
||||
git add Cargo.toml
|
||||
git commit -m "Bump version to 0.2.0"
|
||||
git tag -a v0.2.0 -m "Release v0.2.0"
|
||||
git push origin master --tags
|
||||
|
||||
# 3. Update AUR PKGBUILD
|
||||
cd ~/aur-packages
|
||||
cp ~/code/hyprmarker/packaging/PKGBUILD.aur ./PKGBUILD
|
||||
vim PKGBUILD # Update pkgver=0.2.0, pkgrel=1
|
||||
|
||||
# 4. Regenerate .SRCINFO
|
||||
makepkg --printsrcinfo > .SRCINFO
|
||||
|
||||
# 5. Test build (optional but recommended)
|
||||
makepkg -si
|
||||
|
||||
# 6. Commit and push to AUR
|
||||
git add PKGBUILD .SRCINFO
|
||||
git commit -m "Update to v0.2.0"
|
||||
git push origin master
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Version Numbering
|
||||
|
||||
### Major releases (breaking changes)
|
||||
```
|
||||
0.1.0 → 1.0.0
|
||||
pkgver=1.0.0
|
||||
pkgrel=1
|
||||
```
|
||||
|
||||
### Minor releases (new features)
|
||||
```
|
||||
0.1.0 → 0.2.0
|
||||
pkgver=0.2.0
|
||||
pkgrel=1
|
||||
```
|
||||
|
||||
### Patch releases (bug fixes)
|
||||
```
|
||||
0.1.0 → 0.1.1
|
||||
pkgver=0.1.1
|
||||
pkgrel=1
|
||||
```
|
||||
|
||||
### PKGBUILD-only updates (no code changes)
|
||||
```
|
||||
Same version: 0.1.0
|
||||
pkgver=0.1.0
|
||||
pkgrel=2 ← Increment this
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Tag already exists"
|
||||
```bash
|
||||
# Delete local tag
|
||||
git tag -d v0.1.0
|
||||
|
||||
# Delete remote tag
|
||||
git push --delete origin v0.1.0
|
||||
|
||||
# Recreate
|
||||
git tag -a v0.1.0 -m "Release v0.1.0"
|
||||
git push origin v0.1.0
|
||||
```
|
||||
|
||||
### "Build fails on AUR"
|
||||
```bash
|
||||
# Test locally first
|
||||
cd ~/aur-packages
|
||||
makepkg -si
|
||||
|
||||
# Common issues:
|
||||
# - Missing dependencies in PKGBUILD
|
||||
# - GitHub tag doesn't exist
|
||||
# - Wrong source URL
|
||||
```
|
||||
|
||||
### "Permission denied" when pushing to AUR
|
||||
```bash
|
||||
# Check SSH key is configured
|
||||
cat ~/.ssh/config | grep -A 3 "aur.archlinux.org"
|
||||
|
||||
# Should show:
|
||||
# Host aur.archlinux.org
|
||||
# IdentityFile ~/.ssh/aur_ed25519
|
||||
# User aur
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Locations
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `~/code/hyprmarker/Cargo.toml` | Source of truth for version |
|
||||
| `~/code/hyprmarker/packaging/PKGBUILD.aur` | Template for AUR PKGBUILD |
|
||||
| `~/aur-packages/PKGBUILD` | Actual AUR package file |
|
||||
| `~/aur-packages/.SRCINFO` | Generated metadata (auto-updated) |
|
||||
|
||||
---
|
||||
|
||||
## Quick Commands Reference
|
||||
|
||||
```bash
|
||||
# Update AUR (automated)
|
||||
./tools/update-aur.sh
|
||||
|
||||
# Check current version
|
||||
grep '^version = ' Cargo.toml
|
||||
|
||||
# List git tags
|
||||
git tag
|
||||
|
||||
# Check AUR package status
|
||||
cd ~/aur-packages && git log -1
|
||||
|
||||
# Rebuild local package
|
||||
cd ~/aur-packages && makepkg -si
|
||||
```
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
# Configuration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
hyprmarker supports customization through a TOML configuration file located at:
|
||||
```
|
||||
~/.config/hyprmarker/config.toml
|
||||
```
|
||||
|
||||
All settings are optional. If the configuration file doesn't exist or settings are missing, sensible defaults will be used.
|
||||
|
||||
## Configuration File Location
|
||||
|
||||
The configuration file should be placed at:
|
||||
- Linux: `~/.config/hyprmarker/config.toml`
|
||||
- The directory will be created automatically when you first create the config file
|
||||
|
||||
## Example Configuration
|
||||
|
||||
See `config.example.toml` in the repository root for a complete example with documentation.
|
||||
|
||||
## Configuration Sections
|
||||
|
||||
### `[drawing]` - Drawing Defaults
|
||||
|
||||
Controls the default appearance of annotations.
|
||||
|
||||
```toml
|
||||
[drawing]
|
||||
# Default pen color
|
||||
# Options: "red", "green", "blue", "yellow", "orange", "pink", "white", "black"
|
||||
# Or RGB array: [255, 0, 0]
|
||||
default_color = "red"
|
||||
|
||||
# Default pen thickness in pixels (1.0 - 20.0)
|
||||
default_thickness = 3.0
|
||||
|
||||
# Default font size for text mode (8.0 - 72.0)
|
||||
default_font_size = 32.0
|
||||
```
|
||||
|
||||
**Color Options:**
|
||||
- **Named colors**: `"red"`, `"green"`, `"blue"`, `"yellow"`, `"orange"`, `"pink"`, `"white"`, `"black"`
|
||||
- **RGB arrays**: `[255, 0, 0]` for red, `[0, 255, 0]` for green, etc.
|
||||
|
||||
**Defaults:**
|
||||
- Color: Red
|
||||
- Thickness: 3.0px
|
||||
- Font size: 32.0px
|
||||
|
||||
### `[arrow]` - Arrow Geometry
|
||||
|
||||
Controls the appearance of arrow annotations.
|
||||
|
||||
```toml
|
||||
[arrow]
|
||||
# Arrowhead length in pixels
|
||||
length = 20.0
|
||||
|
||||
# Arrowhead angle in degrees (15-60)
|
||||
# 30 degrees gives a nice balanced arrow
|
||||
angle_degrees = 30.0
|
||||
```
|
||||
|
||||
**Defaults:**
|
||||
- Length: 20.0px
|
||||
- Angle: 30.0°
|
||||
|
||||
### `[performance]` - Performance Tuning
|
||||
|
||||
Controls rendering performance and smoothness.
|
||||
|
||||
```toml
|
||||
[performance]
|
||||
# Number of buffers for rendering (2, 3, or 4)
|
||||
# 2 = double buffering (low memory)
|
||||
# 3 = triple buffering (recommended, smooth)
|
||||
# 4 = quad buffering (ultra-smooth on high refresh displays)
|
||||
buffer_count = 3
|
||||
|
||||
# Enable vsync frame synchronization
|
||||
# Prevents tearing and limits rendering to display refresh rate
|
||||
enable_vsync = true
|
||||
```
|
||||
|
||||
**Buffer Count:**
|
||||
- **2**: Double buffering - minimal memory usage, may flicker on fast drawing
|
||||
- **3**: Triple buffering - recommended default, smooth drawing
|
||||
- **4**: Quad buffering - for high-refresh displays (144Hz+), ultra-smooth
|
||||
|
||||
**VSync:**
|
||||
- **true** (default): Synchronizes with display refresh rate, no tearing
|
||||
- **false**: Uncapped rendering, may cause tearing but lower latency
|
||||
|
||||
**Defaults:**
|
||||
- Buffer count: 3 (triple buffering)
|
||||
- VSync: true
|
||||
|
||||
### `[ui]` - User Interface
|
||||
|
||||
Controls visual indicators, overlays, and UI styling.
|
||||
|
||||
```toml
|
||||
[ui]
|
||||
# Show status bar with current color/thickness/tool
|
||||
show_status_bar = true
|
||||
|
||||
# Status bar position
|
||||
# Options: "top-left", "top-right", "bottom-left", "bottom-right"
|
||||
status_bar_position = "bottom-left"
|
||||
|
||||
# Status bar styling
|
||||
[ui.status_bar_style]
|
||||
font_size = 14.0
|
||||
padding = 10.0
|
||||
bg_color = [0.0, 0.0, 0.0, 0.7] # Semi-transparent black [R, G, B, A]
|
||||
text_color = [1.0, 1.0, 1.0, 1.0] # White
|
||||
dot_radius = 4.0
|
||||
|
||||
# Help overlay styling
|
||||
[ui.help_overlay_style]
|
||||
font_size = 16.0
|
||||
line_height = 22.0
|
||||
padding = 20.0
|
||||
bg_color = [0.0, 0.0, 0.0, 0.85] # Darker background
|
||||
border_color = [0.3, 0.6, 1.0, 0.9] # Light blue
|
||||
border_width = 2.0
|
||||
text_color = [1.0, 1.0, 1.0, 1.0] # White
|
||||
```
|
||||
|
||||
**Status Bar:**
|
||||
- Shows current color, pen thickness, and active tool
|
||||
- Press `F10` to toggle help overlay
|
||||
- Fully customizable styling (fonts, colors, sizes)
|
||||
|
||||
**Position Options:**
|
||||
- `"top-left"`: Upper left corner
|
||||
- `"top-right"`: Upper right corner
|
||||
- `"bottom-left"`: Lower left corner (default)
|
||||
- `"bottom-right"`: Lower right corner
|
||||
|
||||
**UI Styling:**
|
||||
- **Font sizes**: Customize text size for status bar and help overlay
|
||||
- **Colors**: All RGBA values (0.0-1.0 range) with transparency control
|
||||
- **Layout**: Padding, line height, dot size, border width all configurable
|
||||
|
||||
**Defaults:**
|
||||
- Show status bar: true
|
||||
- Position: bottom-left
|
||||
- Status bar font: 14px
|
||||
- Help overlay font: 16px
|
||||
- Semi-transparent dark backgrounds
|
||||
- Light blue help overlay border
|
||||
|
||||
## Creating Your Configuration
|
||||
|
||||
1. Create the directory:
|
||||
```bash
|
||||
mkdir -p ~/.config/hyprmarker
|
||||
```
|
||||
|
||||
2. Copy the example config:
|
||||
```bash
|
||||
cp config.example.toml ~/.config/hyprmarker/config.toml
|
||||
```
|
||||
|
||||
3. Edit to your preferences:
|
||||
```bash
|
||||
nano ~/.config/hyprmarker/config.toml
|
||||
```
|
||||
|
||||
## Configuration Priority
|
||||
|
||||
Settings are loaded in this order:
|
||||
1. Built-in defaults (hardcoded)
|
||||
2. Configuration file values (override defaults)
|
||||
3. Runtime changes via keybindings (temporary, not saved)
|
||||
|
||||
**Note:** Changes to the config file require restarting hyprmarker daemon to take effect.
|
||||
|
||||
To reload config changes:
|
||||
```bash
|
||||
# Use the reload script
|
||||
./reload-daemon.sh
|
||||
|
||||
# Or manually
|
||||
pkill hyprmarker
|
||||
hyprmarker --daemon &
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Config File Not Loading
|
||||
|
||||
If your config file isn't being read:
|
||||
|
||||
1. Check the file path:
|
||||
```bash
|
||||
ls -la ~/.config/hyprmarker/config.toml
|
||||
```
|
||||
|
||||
2. Verify TOML syntax:
|
||||
```bash
|
||||
# Install a TOML validator if needed
|
||||
toml-validator ~/.config/hyprmarker/config.toml
|
||||
```
|
||||
|
||||
3. Check logs for errors:
|
||||
```bash
|
||||
RUST_LOG=info hyprmarker --active
|
||||
```
|
||||
|
||||
### Invalid Values
|
||||
|
||||
If you specify invalid values:
|
||||
- **Out of range**: Values will be clamped to valid ranges
|
||||
- **Invalid color name**: Falls back to default (red)
|
||||
- **Malformed RGB**: Falls back to default color
|
||||
- **Parse errors**: Entire config file ignored, defaults used
|
||||
|
||||
Check the application logs for warnings about config issues.
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Per-Project Configs
|
||||
|
||||
While hyprmarker uses a single global config, you can:
|
||||
1. Create different config files
|
||||
2. Symlink the active one to `~/.config/hyprmarker/config.toml`
|
||||
|
||||
Example:
|
||||
```bash
|
||||
# Create project-specific configs
|
||||
cp config.example.toml ~/configs/hyprmarker-presentation.toml
|
||||
cp config.example.toml ~/configs/hyprmarker-recording.toml
|
||||
|
||||
# Switch configs
|
||||
ln -sf ~/configs/hyprmarker-presentation.toml ~/.config/hyprmarker/config.toml
|
||||
```
|
||||
|
||||
### Configuration Examples
|
||||
|
||||
**High-contrast presentation mode:**
|
||||
```toml
|
||||
[drawing]
|
||||
default_color = "yellow"
|
||||
default_thickness = 5.0
|
||||
default_font_size = 48.0
|
||||
|
||||
[ui]
|
||||
status_bar_position = "top-right"
|
||||
```
|
||||
|
||||
**Screen recording mode (subtle annotations):**
|
||||
```toml
|
||||
[drawing]
|
||||
default_color = "blue"
|
||||
default_thickness = 2.0
|
||||
default_font_size = 24.0
|
||||
|
||||
[performance]
|
||||
buffer_count = 4
|
||||
enable_vsync = true
|
||||
|
||||
[ui]
|
||||
show_status_bar = false
|
||||
```
|
||||
|
||||
**High-refresh display optimization:**
|
||||
```toml
|
||||
[performance]
|
||||
buffer_count = 4
|
||||
enable_vsync = true
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `SETUP.md` - Installation and system requirements
|
||||
- `config.example.toml` - Annotated example configuration
|
||||
- `README.md` - Main documentation with usage guide
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
# Complete Setup Guide
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick Install
|
||||
|
||||
Run the install script:
|
||||
```bash
|
||||
./tools/install.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Build the release binary
|
||||
2. Copy it to `~/.local/bin/hyprmarker`
|
||||
3. Tell you how to add Hyprland keybind
|
||||
|
||||
### Manual Install
|
||||
|
||||
If you prefer manual installation:
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cargo build --release
|
||||
|
||||
# Copy to user bin
|
||||
mkdir -p ~/.local/bin
|
||||
cp target/release/hyprmarker ~/.local/bin/
|
||||
chmod +x ~/.local/bin/hyprmarker
|
||||
|
||||
# Make sure ~/.local/bin is in your PATH
|
||||
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
|
||||
```
|
||||
|
||||
## Hyprland Keybind Setup
|
||||
|
||||
### Method 1: Daemon Mode with Toggle (Recommended)
|
||||
|
||||
Add to `~/.config/hypr/hyprland.conf`:
|
||||
|
||||
```conf
|
||||
# hyprmarker - Screen annotation daemon (Super+D to toggle)
|
||||
exec-once = hyprmarker --daemon
|
||||
bind = SUPER, D, exec, pkill -SIGUSR1 hyprmarker
|
||||
```
|
||||
|
||||
Then reload:
|
||||
```bash
|
||||
hyprctl reload
|
||||
```
|
||||
|
||||
Now press **Super+D** to toggle the overlay on/off!
|
||||
|
||||
### Method 2: One-Shot Mode (Alternative)
|
||||
|
||||
For quick one-time annotations without daemon:
|
||||
|
||||
```bash
|
||||
# Run directly (not recommended - daemon mode is better)
|
||||
hyprmarker --active
|
||||
```
|
||||
|
||||
This starts a fresh overlay each time. Exit with Escape.
|
||||
|
||||
**Note:** We recommend using daemon mode with Super+D instead as it preserves your drawings.
|
||||
|
||||
## Usage Flow
|
||||
|
||||
### Daemon Mode Workflow (Recommended)
|
||||
|
||||
1. **Daemon starts automatically** → Runs in background with system tray icon
|
||||
2. **Press Super+D** → Drawing overlay appears
|
||||
3. **Draw your annotations** → All tools available
|
||||
4. **Press Escape or Ctrl+Q** → Overlay hides (daemon keeps running)
|
||||
5. **Press Super+D again** → Overlay reappears with previous drawings intact
|
||||
|
||||
### One-Shot Mode Workflow (Alternative)
|
||||
|
||||
1. **Run command** → Fresh drawing overlay appears
|
||||
2. **Draw your annotations** → All tools available
|
||||
3. **Press Escape** → Drawing overlay closes completely
|
||||
4. **Run command again** → New fresh overlay (previous drawings lost)
|
||||
|
||||
**Note:** Daemon mode with Super+D is recommended as it preserves your drawings when you toggle the overlay.
|
||||
|
||||
## Verification
|
||||
|
||||
Test the setup:
|
||||
|
||||
```bash
|
||||
# Test binary is accessible
|
||||
which hyprmarker
|
||||
|
||||
# Test daemon mode
|
||||
hyprmarker --daemon &
|
||||
|
||||
# Test keybind
|
||||
Press Super+D (should show overlay)
|
||||
Press Escape (should hide overlay)
|
||||
```
|
||||
|
||||
## Autostart
|
||||
|
||||
Daemon mode is already included in Method 1! The `exec-once` line will start hyprmarker automatically on login.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Keybind not working?**
|
||||
- Check `hyprctl reload` was run
|
||||
- Check for conflicts: `hyprctl binds | grep "SUPER, D"`
|
||||
- Try a different key combo
|
||||
|
||||
**Binary not found?**
|
||||
- Check PATH: `echo $PATH | grep .local/bin`
|
||||
- Add to PATH if missing (see Manual Install)
|
||||
- Restart terminal after PATH change
|
||||
|
||||
**Want different key?**
|
||||
- Edit hyprland.conf
|
||||
- Examples:
|
||||
- `SUPER, D` → Super+D
|
||||
- `ALT, D` → Alt+D
|
||||
- `CTRL SHIFT, 2` → Ctrl+Shift+2
|
||||
|
||||
## Uninstall
|
||||
|
||||
```bash
|
||||
rm ~/.local/bin/hyprmarker
|
||||
# Remove keybind from hyprland.conf
|
||||
```
|
||||
|
||||
## Recommended Setup
|
||||
|
||||
**Best setup (daemon mode):**
|
||||
|
||||
1. Install: `./tools/install.sh`
|
||||
2. Add to hyprland.conf:
|
||||
```conf
|
||||
exec-once = hyprmarker --daemon
|
||||
bind = SUPER, D, exec, pkill -SIGUSR1 hyprmarker
|
||||
```
|
||||
3. Reload: `hyprctl reload`
|
||||
4. Use: Press Super+D to toggle overlay
|
||||
|
||||
Done! Drawings persist, tray icon available. ✨
|
||||
@@ -0,0 +1,15 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub mod wayland;
|
||||
|
||||
// Removed: Backend trait - no longer needed with single backend
|
||||
// Removed: BackendChoice enum - Wayland is the only backend
|
||||
|
||||
/// Run Wayland backend with full event loop
|
||||
pub fn run_wayland() -> Result<()> {
|
||||
let mut backend = wayland::WaylandBackend::new()?;
|
||||
backend.init()?;
|
||||
backend.show()?; // show() calls run() internally
|
||||
backend.hide()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,938 @@
|
||||
// Wayland backend using wlr-layer-shell for overlay
|
||||
use anyhow::{Context, Result};
|
||||
use log::{debug, info, warn};
|
||||
use smithay_client_toolkit::{
|
||||
compositor::{CompositorHandler, CompositorState},
|
||||
delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer,
|
||||
delegate_registry, delegate_seat, delegate_shm,
|
||||
output::{OutputHandler, OutputState},
|
||||
registry::{ProvidesRegistryState, RegistryState},
|
||||
registry_handlers,
|
||||
seat::{
|
||||
Capability, SeatHandler, SeatState,
|
||||
keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers},
|
||||
pointer::{PointerEvent, PointerEventKind, PointerHandler},
|
||||
},
|
||||
shell::{
|
||||
WaylandSurface,
|
||||
wlr_layer::{
|
||||
Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
|
||||
LayerSurfaceConfigure,
|
||||
},
|
||||
},
|
||||
shm::{Shm, ShmHandler, slot::SlotPool},
|
||||
};
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, QueueHandle,
|
||||
globals::registry_queue_init,
|
||||
protocol::{wl_buffer, wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface},
|
||||
};
|
||||
// Removed: Arc, Mutex - not needed after removing WaylandBackend.inner
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::input::{InputState, Key, MouseButton};
|
||||
|
||||
/// Wayland backend state
|
||||
pub struct WaylandBackend {
|
||||
// Removed: inner Arc<Mutex> was unused - WaylandState is created and used directly in run()
|
||||
}
|
||||
|
||||
/// Internal Wayland state
|
||||
struct WaylandState {
|
||||
// Wayland protocol objects
|
||||
registry_state: RegistryState,
|
||||
compositor_state: CompositorState,
|
||||
layer_shell: LayerShell,
|
||||
shm: Shm,
|
||||
output_state: OutputState,
|
||||
seat_state: SeatState,
|
||||
|
||||
// Surface and buffer
|
||||
layer_surface: Option<LayerSurface>,
|
||||
pool: Option<SlotPool>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
configured: bool,
|
||||
|
||||
// Frame synchronization
|
||||
frame_callback_pending: bool,
|
||||
|
||||
// Configuration
|
||||
config: Config,
|
||||
|
||||
// Input state
|
||||
input_state: InputState,
|
||||
current_mouse_x: i32,
|
||||
current_mouse_y: i32,
|
||||
}
|
||||
|
||||
impl WaylandBackend {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> Result<()> {
|
||||
info!("Starting Wayland backend");
|
||||
|
||||
// Connect to Wayland compositor
|
||||
let conn =
|
||||
Connection::connect_to_env().context("Failed to connect to Wayland compositor")?;
|
||||
debug!("Connected to Wayland display");
|
||||
|
||||
// Initialize registry and event queue
|
||||
let (globals, mut event_queue) =
|
||||
registry_queue_init(&conn).context("Failed to initialize Wayland registry")?;
|
||||
let qh = event_queue.handle();
|
||||
|
||||
// Bind global interfaces
|
||||
let compositor_state =
|
||||
CompositorState::bind(&globals, &qh).context("wl_compositor not available")?;
|
||||
debug!("Bound compositor");
|
||||
|
||||
let layer_shell =
|
||||
LayerShell::bind(&globals, &qh).context("zwlr_layer_shell_v1 not available")?;
|
||||
debug!("Bound layer shell");
|
||||
|
||||
let shm = Shm::bind(&globals, &qh).context("wl_shm not available")?;
|
||||
debug!("Bound shared memory");
|
||||
|
||||
let output_state = OutputState::new(&globals, &qh);
|
||||
debug!("Initialized output state");
|
||||
|
||||
let seat_state = SeatState::new(&globals, &qh);
|
||||
debug!("Initialized seat state");
|
||||
|
||||
let registry_state = RegistryState::new(&globals);
|
||||
|
||||
// Load configuration
|
||||
let config = Config::load().unwrap_or_else(|e| {
|
||||
warn!("Failed to load config: {}. Using defaults.", e);
|
||||
Config::default()
|
||||
});
|
||||
info!("Configuration loaded");
|
||||
debug!(" Color: {:?}", config.drawing.default_color);
|
||||
debug!(" Thickness: {:.1}px", config.drawing.default_thickness);
|
||||
debug!(" Font size: {:.1}px", config.drawing.default_font_size);
|
||||
debug!(" Buffer count: {}", config.performance.buffer_count);
|
||||
debug!(" VSync: {}", config.performance.enable_vsync);
|
||||
debug!(
|
||||
" Status bar: {} @ {:?}",
|
||||
config.ui.show_status_bar, config.ui.status_bar_position
|
||||
);
|
||||
debug!(
|
||||
" Status bar font size: {}",
|
||||
config.ui.status_bar_style.font_size
|
||||
);
|
||||
debug!(
|
||||
" Help overlay font size: {}",
|
||||
config.ui.help_overlay_style.font_size
|
||||
);
|
||||
|
||||
// Create font descriptor from config
|
||||
let font_descriptor = crate::draw::FontDescriptor::new(
|
||||
config.drawing.font_family.clone(),
|
||||
config.drawing.font_weight.clone(),
|
||||
config.drawing.font_style.clone(),
|
||||
);
|
||||
|
||||
// Initialize input state with config defaults
|
||||
let input_state = InputState::with_defaults(
|
||||
config.drawing.default_color.to_color(),
|
||||
config.drawing.default_thickness,
|
||||
config.drawing.default_font_size,
|
||||
font_descriptor,
|
||||
config.drawing.text_background_enabled,
|
||||
config.arrow.length,
|
||||
config.arrow.angle_degrees,
|
||||
);
|
||||
|
||||
// Create application state
|
||||
let mut state = WaylandState {
|
||||
registry_state,
|
||||
compositor_state,
|
||||
layer_shell,
|
||||
shm,
|
||||
output_state,
|
||||
seat_state,
|
||||
layer_surface: None,
|
||||
pool: None,
|
||||
width: 0,
|
||||
height: 0,
|
||||
configured: false,
|
||||
frame_callback_pending: false,
|
||||
config,
|
||||
input_state,
|
||||
current_mouse_x: 0,
|
||||
current_mouse_y: 0,
|
||||
};
|
||||
|
||||
// Create layer shell surface
|
||||
info!("Creating layer shell surface");
|
||||
let wl_surface = state.compositor_state.create_surface(&qh);
|
||||
let layer_surface = state.layer_shell.create_layer_surface(
|
||||
&qh,
|
||||
wl_surface,
|
||||
Layer::Overlay,
|
||||
Some("hyprmarker"),
|
||||
None, // Default output
|
||||
);
|
||||
|
||||
// Configure the layer surface for fullscreen overlay
|
||||
layer_surface.set_anchor(Anchor::all());
|
||||
// NOTE: Using Exclusive keyboard interactivity for complete input capture
|
||||
// If clipboard operations are interrupted during overlay toggle, consider switching
|
||||
// to KeyboardInteractivity::OnDemand which cooperates better with other applications
|
||||
layer_surface.set_keyboard_interactivity(KeyboardInteractivity::Exclusive);
|
||||
layer_surface.set_size(0, 0); // Use full screen size
|
||||
layer_surface.set_exclusive_zone(-1);
|
||||
|
||||
// Commit the surface
|
||||
layer_surface.commit();
|
||||
|
||||
state.layer_surface = Some(layer_surface);
|
||||
info!("Layer shell surface created");
|
||||
|
||||
// Track consecutive render failures for error recovery
|
||||
let mut consecutive_render_failures = 0u32;
|
||||
const MAX_RENDER_FAILURES: u32 = 10;
|
||||
|
||||
// Main event loop
|
||||
let mut loop_error: Option<anyhow::Error> = None;
|
||||
loop {
|
||||
// Check if we should exit before blocking
|
||||
if state.input_state.should_exit {
|
||||
info!("Exit requested, breaking event loop");
|
||||
break;
|
||||
}
|
||||
|
||||
// Dispatch all pending events (blocking) but check should_exit after each batch
|
||||
match event_queue.blocking_dispatch(&mut state) {
|
||||
Ok(_) => {
|
||||
// Check immediately after dispatch returns
|
||||
if state.input_state.should_exit {
|
||||
info!("Exit requested after dispatch, breaking event loop");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Event queue error: {}", e);
|
||||
loop_error = Some(anyhow::anyhow!("Wayland event queue error: {}", e));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Render if configured and needs redraw, but only if no frame callback pending
|
||||
// This throttles rendering to display refresh rate (when vsync is enabled)
|
||||
let can_render = state.configured
|
||||
&& state.input_state.needs_redraw
|
||||
&& (!state.frame_callback_pending || !state.config.performance.enable_vsync);
|
||||
|
||||
if can_render {
|
||||
debug!(
|
||||
"Main loop: needs_redraw=true, frame_callback_pending={}, triggering render",
|
||||
state.frame_callback_pending
|
||||
);
|
||||
match state.render(&qh) {
|
||||
Ok(()) => {
|
||||
// Reset failure counter on successful render
|
||||
consecutive_render_failures = 0;
|
||||
state.input_state.needs_redraw = false;
|
||||
// Only set frame_callback_pending if vsync is enabled
|
||||
if state.config.performance.enable_vsync {
|
||||
state.frame_callback_pending = true;
|
||||
debug!(
|
||||
"Main loop: needs_redraw set to false, frame_callback_pending set to true (vsync enabled)"
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Main loop: needs_redraw set to false, frame_callback_pending unchanged (vsync disabled)"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
consecutive_render_failures += 1;
|
||||
warn!(
|
||||
"Rendering error (attempt {}/{}): {}",
|
||||
consecutive_render_failures, MAX_RENDER_FAILURES, e
|
||||
);
|
||||
|
||||
if consecutive_render_failures >= MAX_RENDER_FAILURES {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Too many consecutive render failures ({}), exiting: {}",
|
||||
consecutive_render_failures,
|
||||
e
|
||||
));
|
||||
}
|
||||
|
||||
// Clear redraw flag to avoid infinite error loop
|
||||
state.input_state.needs_redraw = false;
|
||||
}
|
||||
}
|
||||
} else if state.input_state.needs_redraw && state.frame_callback_pending {
|
||||
debug!("Main loop: Skipping render - frame callback already pending");
|
||||
}
|
||||
}
|
||||
|
||||
info!("Wayland backend exiting");
|
||||
|
||||
// Return error if loop exited due to error, otherwise success
|
||||
match loop_error {
|
||||
Some(e) => Err(e),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WaylandState {
|
||||
fn render(&mut self, _qh: &QueueHandle<Self>) -> Result<()> {
|
||||
debug!("=== RENDER START ===");
|
||||
let layer_surface = self
|
||||
.layer_surface
|
||||
.as_ref()
|
||||
.context("Layer surface not created")?;
|
||||
let wl_surface = layer_surface.wl_surface();
|
||||
|
||||
// Create pool if needed
|
||||
if self.pool.is_none() {
|
||||
// Create pool with configured number of buffers to prevent reuse during fast drawing
|
||||
// This prevents flickering when drawing quickly
|
||||
let buffer_size = (self.width * self.height * 4) as usize;
|
||||
let buffer_count = self.config.performance.buffer_count as usize;
|
||||
let pool_size = buffer_size * buffer_count;
|
||||
info!(
|
||||
"Creating new SlotPool ({}x{}, {} bytes, {} buffers)",
|
||||
self.width, self.height, pool_size, buffer_count
|
||||
);
|
||||
let pool = SlotPool::new(pool_size, &self.shm).context("Failed to create slot pool")?;
|
||||
self.pool = Some(pool);
|
||||
}
|
||||
|
||||
let pool = self
|
||||
.pool
|
||||
.as_mut()
|
||||
.context("Buffer pool not initialized despite check at line 215")?;
|
||||
|
||||
// Get a buffer from the pool
|
||||
debug!("Requesting buffer from pool");
|
||||
let (buffer, canvas) = pool
|
||||
.create_buffer(
|
||||
self.width as i32,
|
||||
self.height as i32,
|
||||
(self.width * 4) as i32,
|
||||
wl_shm::Format::Argb8888,
|
||||
)
|
||||
.context("Failed to create buffer")?;
|
||||
debug!("Buffer acquired from pool");
|
||||
|
||||
// Create Cairo surface from the buffer
|
||||
// SAFETY: This unsafe block creates a Cairo surface from raw memory buffer.
|
||||
// Safety invariants that must be maintained:
|
||||
// 1. `canvas` is a valid mutable slice from SlotPool with exactly (width * height * 4) bytes
|
||||
// 2. The buffer format ARgb32 matches the allocation (4 bytes per pixel: alpha, red, green, blue)
|
||||
// 3. The stride (width * 4) correctly represents the number of bytes per row
|
||||
// 4. `cairo_surface` and `ctx` are explicitly dropped (lines 315-316) before the buffer
|
||||
// is committed to Wayland, ensuring Cairo doesn't access memory after ownership transfers
|
||||
// 5. No other references to this memory exist during Cairo's usage
|
||||
// 6. The buffer remains valid throughout Cairo's usage (enforced by Rust's borrow checker
|
||||
// since `canvas` is borrowed until buffer.damage_buffer() call)
|
||||
let cairo_surface = unsafe {
|
||||
cairo::ImageSurface::create_for_data_unsafe(
|
||||
canvas.as_mut_ptr(),
|
||||
cairo::Format::ARgb32,
|
||||
self.width as i32,
|
||||
self.height as i32,
|
||||
(self.width * 4) as i32,
|
||||
)
|
||||
.context("Failed to create Cairo surface")?
|
||||
};
|
||||
|
||||
// Render using Cairo
|
||||
let ctx = cairo::Context::new(&cairo_surface).context("Failed to create Cairo context")?;
|
||||
|
||||
// Clear with fully transparent background
|
||||
debug!("Clearing background");
|
||||
ctx.set_operator(cairo::Operator::Clear);
|
||||
ctx.paint().context("Failed to clear background")?;
|
||||
ctx.set_operator(cairo::Operator::Over);
|
||||
|
||||
// Render all completed shapes
|
||||
debug!(
|
||||
"Rendering {} completed shapes",
|
||||
self.input_state.frame.shapes.len()
|
||||
);
|
||||
crate::draw::render_shapes(&ctx, &self.input_state.frame.shapes);
|
||||
|
||||
// Render provisional shape if actively drawing
|
||||
// Use optimized method that avoids cloning for freehand
|
||||
if self.input_state.render_provisional_shape(
|
||||
&ctx,
|
||||
self.current_mouse_x,
|
||||
self.current_mouse_y,
|
||||
) {
|
||||
debug!("Rendered provisional shape");
|
||||
}
|
||||
|
||||
// Render text cursor/buffer if in text mode
|
||||
if let crate::input::DrawingState::TextInput { x, y, buffer } = &self.input_state.state {
|
||||
let preview_text = if buffer.is_empty() {
|
||||
"_".to_string() // Show cursor when buffer is empty
|
||||
} else {
|
||||
// Show buffer with cursor at end (handles newlines naturally)
|
||||
format!("{}_", buffer)
|
||||
};
|
||||
crate::draw::render_text(
|
||||
&ctx,
|
||||
*x,
|
||||
*y,
|
||||
&preview_text,
|
||||
self.input_state.current_color,
|
||||
self.input_state.current_font_size,
|
||||
&self.input_state.font_descriptor,
|
||||
self.input_state.text_background_enabled,
|
||||
);
|
||||
}
|
||||
|
||||
// Render status bar if enabled
|
||||
if self.config.ui.show_status_bar {
|
||||
crate::ui::render_status_bar(
|
||||
&ctx,
|
||||
&self.input_state,
|
||||
self.config.ui.status_bar_position,
|
||||
&self.config.ui.status_bar_style,
|
||||
self.width,
|
||||
self.height,
|
||||
);
|
||||
}
|
||||
|
||||
// Render help overlay if toggled
|
||||
if self.input_state.show_help {
|
||||
crate::ui::render_help_overlay(
|
||||
&ctx,
|
||||
&self.config.ui.help_overlay_style,
|
||||
self.width,
|
||||
self.height,
|
||||
);
|
||||
}
|
||||
|
||||
// Flush Cairo
|
||||
debug!("Flushing Cairo surface");
|
||||
cairo_surface.flush();
|
||||
drop(ctx);
|
||||
drop(cairo_surface);
|
||||
|
||||
// Attach buffer and commit
|
||||
debug!("Attaching buffer and committing surface");
|
||||
wl_surface.attach(Some(buffer.wl_buffer()), 0, 0);
|
||||
wl_surface.damage_buffer(0, 0, self.width as i32, self.height as i32);
|
||||
|
||||
// Only request frame callback if vsync is enabled
|
||||
// This throttles rendering to display refresh rate
|
||||
if self.config.performance.enable_vsync {
|
||||
debug!("Requesting frame callback (vsync enabled)");
|
||||
wl_surface.frame(_qh, wl_surface.clone());
|
||||
} else {
|
||||
debug!("Skipping frame callback (vsync disabled - allows back-to-back renders)");
|
||||
}
|
||||
|
||||
wl_surface.commit();
|
||||
debug!("=== RENDER COMPLETE ===");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WaylandBackend {
|
||||
pub fn init(&mut self) -> Result<()> {
|
||||
info!("Initializing Wayland backend");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show(&mut self) -> Result<()> {
|
||||
info!("Showing Wayland overlay");
|
||||
self.run()
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) -> Result<()> {
|
||||
info!("Hiding Wayland overlay");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Implement required trait delegates
|
||||
delegate_compositor!(WaylandState);
|
||||
delegate_output!(WaylandState);
|
||||
delegate_shm!(WaylandState);
|
||||
delegate_layer!(WaylandState);
|
||||
delegate_seat!(WaylandState);
|
||||
delegate_keyboard!(WaylandState);
|
||||
delegate_pointer!(WaylandState);
|
||||
delegate_registry!(WaylandState);
|
||||
|
||||
// Implement CompositorHandler
|
||||
impl CompositorHandler for WaylandState {
|
||||
fn scale_factor_changed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_new_factor: i32,
|
||||
) {
|
||||
debug!("Scale factor changed");
|
||||
}
|
||||
|
||||
fn transform_changed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_new_transform: wl_output::Transform,
|
||||
) {
|
||||
debug!("Transform changed");
|
||||
}
|
||||
|
||||
fn frame(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_time: u32,
|
||||
) {
|
||||
// Frame callback - compositor is ready for next frame
|
||||
debug!(
|
||||
"Frame callback received (time: {}ms), clearing frame_callback_pending",
|
||||
_time
|
||||
);
|
||||
self.frame_callback_pending = false;
|
||||
|
||||
// If we're actively drawing, request another render
|
||||
// (input events may have set needs_redraw while we were waiting)
|
||||
if self.input_state.needs_redraw {
|
||||
debug!(
|
||||
"Frame callback: needs_redraw is still true, will render on next loop iteration"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn surface_enter(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_output: &wl_output::WlOutput,
|
||||
) {
|
||||
debug!("Surface entered output");
|
||||
}
|
||||
|
||||
fn surface_leave(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_output: &wl_output::WlOutput,
|
||||
) {
|
||||
debug!("Surface left output");
|
||||
}
|
||||
}
|
||||
|
||||
// Implement OutputHandler
|
||||
impl OutputHandler for WaylandState {
|
||||
fn output_state(&mut self) -> &mut OutputState {
|
||||
&mut self.output_state
|
||||
}
|
||||
|
||||
fn new_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
debug!("New output detected");
|
||||
}
|
||||
|
||||
fn update_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
debug!("Output updated");
|
||||
}
|
||||
|
||||
fn output_destroyed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
debug!("Output destroyed");
|
||||
}
|
||||
}
|
||||
|
||||
// Implement ShmHandler
|
||||
impl ShmHandler for WaylandState {
|
||||
fn shm_state(&mut self) -> &mut Shm {
|
||||
&mut self.shm
|
||||
}
|
||||
}
|
||||
|
||||
// Implement LayerShellHandler
|
||||
impl LayerShellHandler for WaylandState {
|
||||
fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {
|
||||
info!("Layer surface closed by compositor");
|
||||
self.input_state.should_exit = true;
|
||||
}
|
||||
|
||||
fn configure(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_layer: &LayerSurface,
|
||||
configure: LayerSurfaceConfigure,
|
||||
_serial: u32,
|
||||
) {
|
||||
info!(
|
||||
"Layer surface configured: {}x{}",
|
||||
configure.new_size.0, configure.new_size.1
|
||||
);
|
||||
|
||||
// Update dimensions
|
||||
if configure.new_size.0 > 0 && configure.new_size.1 > 0 {
|
||||
let size_changed =
|
||||
self.width != configure.new_size.0 || self.height != configure.new_size.1;
|
||||
|
||||
self.width = configure.new_size.0;
|
||||
self.height = configure.new_size.1;
|
||||
|
||||
// Recreate pool if dimensions changed
|
||||
if size_changed && self.pool.is_some() {
|
||||
info!("Surface size changed - recreating SlotPool");
|
||||
self.pool = None;
|
||||
}
|
||||
|
||||
// Update input state with actual screen dimensions
|
||||
self.input_state
|
||||
.update_screen_dimensions(self.width, self.height);
|
||||
}
|
||||
|
||||
// Mark as configured and request first draw
|
||||
self.configured = true;
|
||||
self.input_state.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Implement SeatHandler
|
||||
impl SeatHandler for WaylandState {
|
||||
fn seat_state(&mut self) -> &mut SeatState {
|
||||
&mut self.seat_state
|
||||
}
|
||||
|
||||
fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _seat: wl_seat::WlSeat) {
|
||||
debug!("New seat available");
|
||||
}
|
||||
|
||||
fn new_capability(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
seat: wl_seat::WlSeat,
|
||||
capability: Capability,
|
||||
) {
|
||||
if capability == Capability::Keyboard {
|
||||
info!("Keyboard capability available");
|
||||
if self.seat_state.get_keyboard(qh, &seat, None).is_ok() {
|
||||
debug!("Keyboard initialized");
|
||||
}
|
||||
}
|
||||
|
||||
if capability == Capability::Pointer {
|
||||
info!("Pointer capability available");
|
||||
if self.seat_state.get_pointer(qh, &seat).is_ok() {
|
||||
debug!("Pointer initialized");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_capability(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_seat: wl_seat::WlSeat,
|
||||
capability: Capability,
|
||||
) {
|
||||
if capability == Capability::Keyboard {
|
||||
info!("Keyboard capability removed");
|
||||
}
|
||||
if capability == Capability::Pointer {
|
||||
info!("Pointer capability removed");
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _seat: wl_seat::WlSeat) {
|
||||
debug!("Seat removed");
|
||||
}
|
||||
}
|
||||
|
||||
// Implement KeyboardHandler
|
||||
impl KeyboardHandler for WaylandState {
|
||||
fn enter(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_keyboard: &wl_keyboard::WlKeyboard,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_serial: u32,
|
||||
_raw: &[u32],
|
||||
_keysyms: &[Keysym],
|
||||
) {
|
||||
debug!("Keyboard focus entered");
|
||||
}
|
||||
|
||||
fn leave(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_keyboard: &wl_keyboard::WlKeyboard,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_serial: u32,
|
||||
) {
|
||||
debug!("Keyboard focus left");
|
||||
}
|
||||
|
||||
fn press_key(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_keyboard: &wl_keyboard::WlKeyboard,
|
||||
_serial: u32,
|
||||
event: KeyEvent,
|
||||
) {
|
||||
let key = keysym_to_key(event.keysym);
|
||||
debug!("Key pressed: {:?}", key);
|
||||
self.input_state.on_key_press(key);
|
||||
self.input_state.needs_redraw = true;
|
||||
}
|
||||
|
||||
fn release_key(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_keyboard: &wl_keyboard::WlKeyboard,
|
||||
_serial: u32,
|
||||
event: KeyEvent,
|
||||
) {
|
||||
let key = keysym_to_key(event.keysym);
|
||||
debug!("Key released: {:?}", key);
|
||||
self.input_state.on_key_release(key);
|
||||
}
|
||||
|
||||
fn update_modifiers(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_keyboard: &wl_keyboard::WlKeyboard,
|
||||
_serial: u32,
|
||||
modifiers: Modifiers,
|
||||
_layout: RawModifiers,
|
||||
_group: u32,
|
||||
) {
|
||||
debug!(
|
||||
"Modifiers: ctrl={} alt={} shift={}",
|
||||
modifiers.ctrl, modifiers.alt, modifiers.shift
|
||||
);
|
||||
}
|
||||
|
||||
fn repeat_key(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_keyboard: &wl_keyboard::WlKeyboard,
|
||||
_serial: u32,
|
||||
event: KeyEvent,
|
||||
) {
|
||||
// Handle key repeat - treat like a regular key press
|
||||
let key = keysym_to_key(event.keysym);
|
||||
debug!("Key repeated: {:?}", key);
|
||||
self.input_state.on_key_press(key);
|
||||
self.input_state.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Implement PointerHandler
|
||||
impl PointerHandler for WaylandState {
|
||||
fn pointer_frame(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_pointer: &wl_pointer::WlPointer,
|
||||
events: &[PointerEvent],
|
||||
) {
|
||||
use smithay_client_toolkit::seat::pointer::{BTN_LEFT, BTN_MIDDLE, BTN_RIGHT};
|
||||
|
||||
for event in events {
|
||||
match event.kind {
|
||||
PointerEventKind::Enter { .. } => {
|
||||
debug!(
|
||||
"Pointer entered at ({}, {})",
|
||||
event.position.0, event.position.1
|
||||
);
|
||||
self.current_mouse_x = event.position.0 as i32;
|
||||
self.current_mouse_y = event.position.1 as i32;
|
||||
}
|
||||
PointerEventKind::Leave { .. } => {
|
||||
debug!("Pointer left surface");
|
||||
}
|
||||
PointerEventKind::Motion { .. } => {
|
||||
self.current_mouse_x = event.position.0 as i32;
|
||||
self.current_mouse_y = event.position.1 as i32;
|
||||
self.input_state
|
||||
.on_mouse_motion(self.current_mouse_x, self.current_mouse_y);
|
||||
// Note: needs_redraw is set inside on_mouse_motion if actively drawing
|
||||
// Don't set it here unconditionally to avoid rendering on every mouse move
|
||||
}
|
||||
PointerEventKind::Press { button, .. } => {
|
||||
debug!(
|
||||
"Button {} pressed at ({}, {})",
|
||||
button, event.position.0, event.position.1
|
||||
);
|
||||
|
||||
let mb = match button {
|
||||
BTN_LEFT => MouseButton::Left,
|
||||
BTN_MIDDLE => MouseButton::Middle,
|
||||
BTN_RIGHT => MouseButton::Right,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
self.input_state.on_mouse_press(
|
||||
mb,
|
||||
event.position.0 as i32,
|
||||
event.position.1 as i32,
|
||||
);
|
||||
self.input_state.needs_redraw = true;
|
||||
}
|
||||
PointerEventKind::Release { button, .. } => {
|
||||
debug!("Button {} released", button);
|
||||
|
||||
let mb = match button {
|
||||
BTN_LEFT => MouseButton::Left,
|
||||
BTN_MIDDLE => MouseButton::Middle,
|
||||
BTN_RIGHT => MouseButton::Right,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
self.input_state.on_mouse_release(
|
||||
mb,
|
||||
event.position.0 as i32,
|
||||
event.position.1 as i32,
|
||||
);
|
||||
self.input_state.needs_redraw = true;
|
||||
}
|
||||
PointerEventKind::Axis { vertical, .. } => {
|
||||
// Use scroll wheel for pen width adjustment
|
||||
// Use discrete steps if available, otherwise fall back to absolute with threshold
|
||||
let scroll_direction = if vertical.discrete != 0 {
|
||||
vertical.discrete
|
||||
} else if vertical.absolute.abs() > 0.1 {
|
||||
// Threshold to ignore tiny movements
|
||||
if vertical.absolute > 0.0 { 1 } else { -1 }
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if scroll_direction > 0 {
|
||||
// Scroll up = decrease thickness
|
||||
self.input_state.current_thickness =
|
||||
(self.input_state.current_thickness - 1.0).max(1.0);
|
||||
debug!(
|
||||
"Thickness decreased: {:.0}px",
|
||||
self.input_state.current_thickness
|
||||
);
|
||||
self.input_state.needs_redraw = true;
|
||||
} else if scroll_direction < 0 {
|
||||
// Scroll down = increase thickness
|
||||
self.input_state.current_thickness =
|
||||
(self.input_state.current_thickness + 1.0).min(20.0);
|
||||
debug!(
|
||||
"Thickness increased: {:.0}px",
|
||||
self.input_state.current_thickness
|
||||
);
|
||||
self.input_state.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implement ProvidesRegistryState
|
||||
impl ProvidesRegistryState for WaylandState {
|
||||
fn registry(&mut self) -> &mut RegistryState {
|
||||
&mut self.registry_state
|
||||
}
|
||||
|
||||
registry_handlers![OutputState, SeatState];
|
||||
}
|
||||
|
||||
// Implement Dispatch for wl_buffer (required for buffer lifecycle)
|
||||
impl Dispatch<wl_buffer::WlBuffer, ()> for WaylandState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &wl_buffer::WlBuffer,
|
||||
event: wl_buffer::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if let wl_buffer::Event::Release = event {
|
||||
debug!("Buffer released by compositor");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Wayland keysym to our Key enum
|
||||
fn keysym_to_key(keysym: Keysym) -> Key {
|
||||
match keysym {
|
||||
Keysym::Escape => Key::Escape,
|
||||
Keysym::Return => Key::Return,
|
||||
Keysym::BackSpace => Key::Backspace,
|
||||
Keysym::Tab => Key::Tab,
|
||||
Keysym::space => Key::Space,
|
||||
Keysym::Shift_L | Keysym::Shift_R => Key::Shift,
|
||||
Keysym::Control_L | Keysym::Control_R => Key::Ctrl,
|
||||
Keysym::Alt_L | Keysym::Alt_R => Key::Alt,
|
||||
Keysym::plus | Keysym::equal => Key::Plus,
|
||||
Keysym::minus | Keysym::underscore => Key::Minus,
|
||||
Keysym::t => Key::Char('t'),
|
||||
Keysym::T => Key::Char('T'),
|
||||
Keysym::e => Key::Char('e'),
|
||||
Keysym::E => Key::Char('E'),
|
||||
Keysym::r => Key::Char('r'),
|
||||
Keysym::R => Key::Char('R'),
|
||||
Keysym::g => Key::Char('g'),
|
||||
Keysym::G => Key::Char('G'),
|
||||
Keysym::b => Key::Char('b'),
|
||||
Keysym::B => Key::Char('B'),
|
||||
Keysym::y => Key::Char('y'),
|
||||
Keysym::Y => Key::Char('Y'),
|
||||
Keysym::o => Key::Char('o'),
|
||||
Keysym::O => Key::Char('O'),
|
||||
Keysym::p => Key::Char('p'),
|
||||
Keysym::P => Key::Char('P'),
|
||||
Keysym::w => Key::Char('w'),
|
||||
Keysym::W => Key::Char('W'),
|
||||
Keysym::k => Key::Char('k'),
|
||||
Keysym::K => Key::Char('K'),
|
||||
Keysym::z => Key::Char('z'),
|
||||
Keysym::Z => Key::Char('Z'),
|
||||
Keysym::F10 => Key::F10,
|
||||
_ => {
|
||||
// For other printable characters, try to map them
|
||||
// Use the raw value to determine if it's ASCII printable
|
||||
let raw = keysym.raw();
|
||||
if (0x20..=0x7E).contains(&raw) {
|
||||
Key::Char(raw as u8 as char)
|
||||
} else {
|
||||
Key::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Configuration enum types.
|
||||
|
||||
use crate::draw::{Color, color::*};
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Status bar position on screen.
|
||||
///
|
||||
/// Controls where the status bar appears relative to screen edges.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum StatusPosition {
|
||||
/// Top-left corner
|
||||
TopLeft,
|
||||
/// Top-right corner
|
||||
TopRight,
|
||||
/// Bottom-left corner
|
||||
BottomLeft,
|
||||
/// Bottom-right corner
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
/// Color specification - either a named color or RGB values.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```toml
|
||||
/// # Named color
|
||||
/// default_color = "red"
|
||||
///
|
||||
/// # Custom RGB color (0-255 per component)
|
||||
/// default_color = [255, 128, 0] # Orange
|
||||
/// ```
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum ColorSpec {
|
||||
/// Named color: red, green, blue, yellow, orange, pink, white, black
|
||||
Name(String),
|
||||
/// RGB color as [red, green, blue] where each component is 0-255
|
||||
Rgb([u8; 3]),
|
||||
}
|
||||
|
||||
impl ColorSpec {
|
||||
/// Converts the color specification to a [`Color`] struct.
|
||||
///
|
||||
/// Named colors are mapped to predefined RGBA values using `util::name_to_color()`.
|
||||
/// Unknown color names default to red with a warning. RGB arrays are converted from
|
||||
/// 0-255 range to 0.0-1.0 range with full opacity.
|
||||
pub fn to_color(&self) -> Color {
|
||||
match self {
|
||||
ColorSpec::Name(name) => crate::util::name_to_color(name).unwrap_or_else(|| {
|
||||
warn!("Unknown color '{}', using red", name);
|
||||
RED
|
||||
}),
|
||||
ColorSpec::Rgb([r, g, b]) => Color {
|
||||
r: *r as f64 / 255.0,
|
||||
g: *g as f64 / 255.0,
|
||||
b: *b as f64 / 255.0,
|
||||
a: 1.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
//! Configuration file support for hyprmarker.
|
||||
//!
|
||||
//! This module handles loading and validating user settings from the configuration file
|
||||
//! located at `~/.config/hyprmarker/config.toml`. Settings include drawing defaults,
|
||||
//! arrow appearance, performance tuning, and UI preferences.
|
||||
//!
|
||||
//! If no config file exists, sensible defaults are used automatically.
|
||||
|
||||
pub mod enums;
|
||||
pub mod types;
|
||||
|
||||
// Re-export commonly used types at module level
|
||||
pub use enums::StatusPosition;
|
||||
pub use types::{
|
||||
ArrowConfig, DrawingConfig, HelpOverlayStyle, PerformanceConfig, StatusBarStyle, UiConfig,
|
||||
};
|
||||
|
||||
// Re-export for public API (unused internally but part of public interface)
|
||||
#[allow(unused_imports)]
|
||||
pub use enums::ColorSpec;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use log::{debug, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Main configuration structure containing all user settings.
|
||||
///
|
||||
/// This is the root configuration type that gets deserialized from the TOML file.
|
||||
/// All fields have sensible defaults and will use those if not specified in the config file.
|
||||
///
|
||||
/// # Example TOML
|
||||
/// ```toml
|
||||
/// [drawing]
|
||||
/// default_color = "red"
|
||||
/// default_thickness = 3.0
|
||||
/// default_font_size = 32.0
|
||||
///
|
||||
/// [arrow]
|
||||
/// length = 20.0
|
||||
/// angle_degrees = 30.0
|
||||
///
|
||||
/// [performance]
|
||||
/// buffer_count = 3
|
||||
/// enable_vsync = true
|
||||
///
|
||||
/// [ui]
|
||||
/// show_status_bar = true
|
||||
/// status_bar_position = "bottom-left"
|
||||
/// ```
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Config {
|
||||
/// Drawing tool defaults (color, thickness, font size)
|
||||
#[serde(default)]
|
||||
pub drawing: DrawingConfig,
|
||||
|
||||
/// Arrow appearance settings
|
||||
#[serde(default)]
|
||||
pub arrow: ArrowConfig,
|
||||
|
||||
/// Performance tuning options
|
||||
#[serde(default)]
|
||||
pub performance: PerformanceConfig,
|
||||
|
||||
/// UI display preferences
|
||||
#[serde(default)]
|
||||
pub ui: UiConfig,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Validates and clamps all configuration values to acceptable ranges.
|
||||
///
|
||||
/// This method ensures that user-provided config values won't cause undefined behavior
|
||||
/// or rendering issues. Invalid values are clamped to the nearest valid value and a
|
||||
/// warning is logged.
|
||||
///
|
||||
/// Validated ranges:
|
||||
/// - `default_thickness`: 1.0 - 20.0
|
||||
/// - `default_font_size`: 8.0 - 72.0
|
||||
/// - `arrow.length`: 5.0 - 50.0
|
||||
/// - `arrow.angle_degrees`: 15.0 - 60.0
|
||||
/// - `buffer_count`: 2 - 4
|
||||
fn validate_and_clamp(&mut self) {
|
||||
// Thickness: 1.0 - 20.0
|
||||
if !(1.0..=20.0).contains(&self.drawing.default_thickness) {
|
||||
log::warn!(
|
||||
"Invalid default_thickness {:.1}, clamping to 1.0-20.0 range",
|
||||
self.drawing.default_thickness
|
||||
);
|
||||
self.drawing.default_thickness = self.drawing.default_thickness.clamp(1.0, 20.0);
|
||||
}
|
||||
|
||||
// Font size: 8.0 - 72.0
|
||||
if !(8.0..=72.0).contains(&self.drawing.default_font_size) {
|
||||
log::warn!(
|
||||
"Invalid default_font_size {:.1}, clamping to 8.0-72.0 range",
|
||||
self.drawing.default_font_size
|
||||
);
|
||||
self.drawing.default_font_size = self.drawing.default_font_size.clamp(8.0, 72.0);
|
||||
}
|
||||
|
||||
// Arrow length: 5.0 - 50.0
|
||||
if !(5.0..=50.0).contains(&self.arrow.length) {
|
||||
log::warn!(
|
||||
"Invalid arrow length {:.1}, clamping to 5.0-50.0 range",
|
||||
self.arrow.length
|
||||
);
|
||||
self.arrow.length = self.arrow.length.clamp(5.0, 50.0);
|
||||
}
|
||||
|
||||
// Arrow angle: 15.0 - 60.0 degrees
|
||||
if !(15.0..=60.0).contains(&self.arrow.angle_degrees) {
|
||||
log::warn!(
|
||||
"Invalid arrow angle {:.1}°, clamping to 15.0-60.0° range",
|
||||
self.arrow.angle_degrees
|
||||
);
|
||||
self.arrow.angle_degrees = self.arrow.angle_degrees.clamp(15.0, 60.0);
|
||||
}
|
||||
|
||||
// Buffer count: 2 - 4
|
||||
if !(2..=4).contains(&self.performance.buffer_count) {
|
||||
log::warn!(
|
||||
"Invalid buffer_count {}, clamping to 2-4 range",
|
||||
self.performance.buffer_count
|
||||
);
|
||||
self.performance.buffer_count = self.performance.buffer_count.clamp(2, 4);
|
||||
}
|
||||
|
||||
// Validate font weight is reasonable
|
||||
let valid_weight = matches!(
|
||||
self.drawing.font_weight.to_lowercase().as_str(),
|
||||
"normal" | "bold" | "light" | "ultralight" | "heavy" | "ultrabold"
|
||||
) || self
|
||||
.drawing
|
||||
.font_weight
|
||||
.parse::<u32>()
|
||||
.is_ok_and(|w| (100..=900).contains(&w));
|
||||
|
||||
if !valid_weight {
|
||||
log::warn!(
|
||||
"Invalid font_weight '{}', falling back to 'bold'",
|
||||
self.drawing.font_weight
|
||||
);
|
||||
self.drawing.font_weight = "bold".to_string();
|
||||
}
|
||||
|
||||
// Validate font style
|
||||
if !matches!(
|
||||
self.drawing.font_style.to_lowercase().as_str(),
|
||||
"normal" | "italic" | "oblique"
|
||||
) {
|
||||
log::warn!(
|
||||
"Invalid font_style '{}', falling back to 'normal'",
|
||||
self.drawing.font_style
|
||||
);
|
||||
self.drawing.font_style = "normal".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to the configuration file.
|
||||
///
|
||||
/// The config file is located at `~/.config/hyprmarker/config.toml`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the config directory cannot be determined (e.g., HOME not set).
|
||||
pub fn get_config_path() -> Result<PathBuf> {
|
||||
let config_dir = dirs::config_dir()
|
||||
.context("Could not find config directory")?
|
||||
.join("hyprmarker");
|
||||
|
||||
Ok(config_dir.join("config.toml"))
|
||||
}
|
||||
|
||||
/// Loads configuration from file, or returns defaults if not found.
|
||||
///
|
||||
/// Attempts to read and parse the config file at `~/.config/hyprmarker/config.toml`.
|
||||
/// If the file doesn't exist, returns a Config with default values. All loaded values
|
||||
/// are validated and clamped to acceptable ranges.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if:
|
||||
/// - The config directory path cannot be determined
|
||||
/// - The file exists but cannot be read
|
||||
/// - The file exists but contains invalid TOML syntax
|
||||
pub fn load() -> Result<Self> {
|
||||
let config_path = Self::get_config_path()?;
|
||||
|
||||
if !config_path.exists() {
|
||||
info!("Config file not found, using defaults");
|
||||
debug!("Expected config at: {}", config_path.display());
|
||||
return Ok(Self::default());
|
||||
}
|
||||
|
||||
let config_str = fs::read_to_string(&config_path)
|
||||
.with_context(|| format!("Failed to read config from {}", config_path.display()))?;
|
||||
|
||||
let mut config: Config = toml::from_str(&config_str)
|
||||
.with_context(|| format!("Failed to parse config from {}", config_path.display()))?;
|
||||
|
||||
// Validate and clamp values to acceptable ranges
|
||||
config.validate_and_clamp();
|
||||
|
||||
info!("Loaded config from {}", config_path.display());
|
||||
debug!("Config: {:?}", config);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Saves the current configuration to file.
|
||||
///
|
||||
/// Serializes the config to TOML format and writes it to `~/.config/hyprmarker/config.toml`.
|
||||
/// Creates the parent directory if it doesn't exist. This method is kept for future use
|
||||
/// (e.g., runtime config editing).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if:
|
||||
/// - The config directory cannot be created
|
||||
/// - The config cannot be serialized to TOML
|
||||
/// - The file cannot be written
|
||||
#[allow(dead_code)]
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let config_path = Self::get_config_path()?;
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent).context("Failed to create config directory")?;
|
||||
}
|
||||
|
||||
let config_str = toml::to_string_pretty(self).context("Failed to serialize config")?;
|
||||
|
||||
fs::write(&config_path, config_str)
|
||||
.with_context(|| format!("Failed to write config to {}", config_path.display()))?;
|
||||
|
||||
info!("Saved config to {}", config_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a default configuration file with documentation comments.
|
||||
///
|
||||
/// Writes the example config from `config.example.toml` to the user's config directory.
|
||||
/// This method is kept for future use (e.g., `hyprmarker --init-config`).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if:
|
||||
/// - A config file already exists at the target path
|
||||
/// - The config directory cannot be created
|
||||
/// - The file cannot be written
|
||||
#[allow(dead_code)]
|
||||
pub fn create_default_file() -> Result<()> {
|
||||
let config_path = Self::get_config_path()?;
|
||||
|
||||
if config_path.exists() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Config file already exists at {}",
|
||||
config_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
// Create directory
|
||||
if let Some(parent) = config_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let default_config = include_str!("../../config.example.toml");
|
||||
fs::write(&config_path, default_config)?;
|
||||
|
||||
info!("Created default config at {}", config_path.display());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
//! Configuration type definitions.
|
||||
|
||||
use super::enums::{ColorSpec, StatusPosition};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Drawing-related settings.
|
||||
///
|
||||
/// Controls the default appearance of drawing tools when the overlay first opens.
|
||||
/// Users can change these values at runtime using keybindings.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DrawingConfig {
|
||||
/// Default pen color - either a named color (red, green, blue, yellow, orange, pink, white, black)
|
||||
/// or an RGB array like `[255, 0, 0]` for red
|
||||
#[serde(default = "default_color")]
|
||||
pub default_color: ColorSpec,
|
||||
|
||||
/// Default pen thickness in pixels (valid range: 1.0 - 20.0)
|
||||
#[serde(default = "default_thickness")]
|
||||
pub default_thickness: f64,
|
||||
|
||||
/// Default font size for text mode in points (valid range: 8.0 - 72.0)
|
||||
#[serde(default = "default_font_size")]
|
||||
pub default_font_size: f64,
|
||||
|
||||
/// Font family name for text rendering (e.g., "Sans", "Monospace", "JetBrains Mono")
|
||||
/// Falls back to "Sans" if the specified font is not available
|
||||
/// Note: Install fonts system-wide and reference by family name
|
||||
#[serde(default = "default_font_family")]
|
||||
pub font_family: String,
|
||||
|
||||
/// Font weight (e.g., "normal", "bold", "light", 400, 700)
|
||||
/// Can be a named weight or a numeric value (100-900)
|
||||
#[serde(default = "default_font_weight")]
|
||||
pub font_weight: String,
|
||||
|
||||
/// Font style (e.g., "normal", "italic", "oblique")
|
||||
#[serde(default = "default_font_style")]
|
||||
pub font_style: String,
|
||||
|
||||
/// Enable semi-transparent background box behind text for better contrast
|
||||
#[serde(default = "default_text_background")]
|
||||
pub text_background_enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for DrawingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_color: default_color(),
|
||||
default_thickness: default_thickness(),
|
||||
default_font_size: default_font_size(),
|
||||
font_family: default_font_family(),
|
||||
font_weight: default_font_weight(),
|
||||
font_style: default_font_style(),
|
||||
text_background_enabled: default_text_background(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Arrow drawing settings.
|
||||
///
|
||||
/// Controls the appearance of arrowheads when using the arrow tool (Ctrl+Shift+Drag).
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ArrowConfig {
|
||||
/// Arrowhead length in pixels (valid range: 5.0 - 50.0)
|
||||
#[serde(default = "default_arrow_length")]
|
||||
pub length: f64,
|
||||
|
||||
/// Arrowhead angle in degrees (valid range: 15.0 - 60.0)
|
||||
/// Smaller angles create narrower arrowheads, larger angles create wider ones
|
||||
#[serde(default = "default_arrow_angle")]
|
||||
pub angle_degrees: f64,
|
||||
}
|
||||
|
||||
impl Default for ArrowConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
length: default_arrow_length(),
|
||||
angle_degrees: default_arrow_angle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performance tuning options.
|
||||
///
|
||||
/// These settings control rendering performance and smoothness. Most users
|
||||
/// won't need to change these from their defaults.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PerformanceConfig {
|
||||
/// Number of buffers for buffering (valid range: 2 - 4)
|
||||
/// - 2 = double buffering (lower memory, potential tearing)
|
||||
/// - 3 = triple buffering (balanced, recommended)
|
||||
/// - 4 = quad buffering (highest memory, smoothest)
|
||||
#[serde(default = "default_buffer_count")]
|
||||
pub buffer_count: u32,
|
||||
|
||||
/// Enable vsync frame synchronization to prevent tearing
|
||||
/// Set to false for lower latency at the cost of potential screen tearing
|
||||
#[serde(default = "default_enable_vsync")]
|
||||
pub enable_vsync: bool,
|
||||
}
|
||||
|
||||
impl Default for PerformanceConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffer_count: default_buffer_count(),
|
||||
enable_vsync: default_enable_vsync(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// UI display preferences.
|
||||
///
|
||||
/// Controls the visibility and positioning of on-screen UI elements.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UiConfig {
|
||||
/// Show the status bar displaying current color, thickness, and tool
|
||||
#[serde(default = "default_show_status")]
|
||||
pub show_status_bar: bool,
|
||||
|
||||
/// Status bar screen position (top-left, top-right, bottom-left, bottom-right)
|
||||
#[serde(default = "default_status_position")]
|
||||
pub status_bar_position: StatusPosition,
|
||||
|
||||
/// Status bar styling options
|
||||
#[serde(default)]
|
||||
pub status_bar_style: StatusBarStyle,
|
||||
|
||||
/// Help overlay styling options
|
||||
#[serde(default)]
|
||||
pub help_overlay_style: HelpOverlayStyle,
|
||||
}
|
||||
|
||||
impl Default for UiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_status_bar: default_show_status(),
|
||||
status_bar_position: default_status_position(),
|
||||
status_bar_style: StatusBarStyle::default(),
|
||||
help_overlay_style: HelpOverlayStyle::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Status bar styling configuration.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct StatusBarStyle {
|
||||
/// Font size for status bar text
|
||||
#[serde(default = "default_status_font_size")]
|
||||
pub font_size: f64,
|
||||
|
||||
/// Padding around status bar text
|
||||
#[serde(default = "default_status_padding")]
|
||||
pub padding: f64,
|
||||
|
||||
/// Background color [R, G, B, A] (0.0-1.0 range)
|
||||
#[serde(default = "default_status_bg_color")]
|
||||
pub bg_color: [f64; 4],
|
||||
|
||||
/// Text color [R, G, B, A] (0.0-1.0 range)
|
||||
#[serde(default = "default_status_text_color")]
|
||||
pub text_color: [f64; 4],
|
||||
|
||||
/// Color indicator dot radius
|
||||
#[serde(default = "default_status_dot_radius")]
|
||||
pub dot_radius: f64,
|
||||
}
|
||||
|
||||
impl Default for StatusBarStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_size: default_status_font_size(),
|
||||
padding: default_status_padding(),
|
||||
bg_color: default_status_bg_color(),
|
||||
text_color: default_status_text_color(),
|
||||
dot_radius: default_status_dot_radius(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Help overlay styling configuration.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct HelpOverlayStyle {
|
||||
/// Font size for help overlay text
|
||||
#[serde(default = "default_help_font_size")]
|
||||
pub font_size: f64,
|
||||
|
||||
/// Line height for help text
|
||||
#[serde(default = "default_help_line_height")]
|
||||
pub line_height: f64,
|
||||
|
||||
/// Padding around help box
|
||||
#[serde(default = "default_help_padding")]
|
||||
pub padding: f64,
|
||||
|
||||
/// Background color [R, G, B, A] (0.0-1.0 range)
|
||||
#[serde(default = "default_help_bg_color")]
|
||||
pub bg_color: [f64; 4],
|
||||
|
||||
/// Border color [R, G, B, A] (0.0-1.0 range)
|
||||
#[serde(default = "default_help_border_color")]
|
||||
pub border_color: [f64; 4],
|
||||
|
||||
/// Border line width
|
||||
#[serde(default = "default_help_border_width")]
|
||||
pub border_width: f64,
|
||||
|
||||
/// Text color [R, G, B, A] (0.0-1.0 range)
|
||||
#[serde(default = "default_help_text_color")]
|
||||
pub text_color: [f64; 4],
|
||||
}
|
||||
|
||||
impl Default for HelpOverlayStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_size: default_help_font_size(),
|
||||
line_height: default_help_line_height(),
|
||||
padding: default_help_padding(),
|
||||
bg_color: default_help_bg_color(),
|
||||
border_color: default_help_border_color(),
|
||||
border_width: default_help_border_width(),
|
||||
text_color: default_help_text_color(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Default value functions
|
||||
// =============================================================================
|
||||
|
||||
fn default_color() -> ColorSpec {
|
||||
ColorSpec::Name("red".to_string())
|
||||
}
|
||||
|
||||
fn default_thickness() -> f64 {
|
||||
3.0
|
||||
}
|
||||
|
||||
fn default_font_size() -> f64 {
|
||||
32.0
|
||||
}
|
||||
|
||||
fn default_font_family() -> String {
|
||||
"Sans".to_string()
|
||||
}
|
||||
|
||||
fn default_font_weight() -> String {
|
||||
"bold".to_string()
|
||||
}
|
||||
|
||||
fn default_font_style() -> String {
|
||||
"normal".to_string()
|
||||
}
|
||||
|
||||
fn default_text_background() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn default_arrow_length() -> f64 {
|
||||
20.0
|
||||
}
|
||||
|
||||
fn default_arrow_angle() -> f64 {
|
||||
30.0
|
||||
}
|
||||
|
||||
fn default_buffer_count() -> u32 {
|
||||
3
|
||||
}
|
||||
|
||||
fn default_enable_vsync() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_show_status() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_status_position() -> StatusPosition {
|
||||
StatusPosition::BottomLeft
|
||||
}
|
||||
|
||||
// Status bar style defaults
|
||||
fn default_status_font_size() -> f64 {
|
||||
21.0 // 50% larger than previous 14.0
|
||||
}
|
||||
|
||||
fn default_status_padding() -> f64 {
|
||||
15.0 // 50% larger than previous 10.0
|
||||
}
|
||||
|
||||
fn default_status_bg_color() -> [f64; 4] {
|
||||
[0.0, 0.0, 0.0, 0.85] // More opaque (was 0.7) for better visibility
|
||||
}
|
||||
|
||||
fn default_status_text_color() -> [f64; 4] {
|
||||
[1.0, 1.0, 1.0, 1.0]
|
||||
}
|
||||
|
||||
fn default_status_dot_radius() -> f64 {
|
||||
6.0 // 50% larger than previous 4.0
|
||||
}
|
||||
|
||||
// Help overlay style defaults
|
||||
fn default_help_font_size() -> f64 {
|
||||
16.0
|
||||
}
|
||||
|
||||
fn default_help_line_height() -> f64 {
|
||||
22.0
|
||||
}
|
||||
|
||||
fn default_help_padding() -> f64 {
|
||||
20.0
|
||||
}
|
||||
|
||||
fn default_help_bg_color() -> [f64; 4] {
|
||||
[0.0, 0.0, 0.0, 0.85]
|
||||
}
|
||||
|
||||
fn default_help_border_color() -> [f64; 4] {
|
||||
[0.3, 0.6, 1.0, 0.9]
|
||||
}
|
||||
|
||||
fn default_help_border_width() -> f64 {
|
||||
2.0
|
||||
}
|
||||
|
||||
fn default_help_text_color() -> [f64; 4] {
|
||||
[1.0, 1.0, 1.0, 1.0]
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
/// Daemon mode implementation: background service with toggle activation
|
||||
use anyhow::{Context, Result};
|
||||
use ksni::TrayMethods;
|
||||
use log::{debug, info, warn};
|
||||
use signal_hook::consts::signal::{SIGINT, SIGTERM, SIGUSR1};
|
||||
use signal_hook::iterator::Signals;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::backend;
|
||||
|
||||
/// Overlay state for daemon mode
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OverlayState {
|
||||
Hidden, // Daemon running, overlay not visible
|
||||
Visible, // Overlay active, capturing input
|
||||
}
|
||||
|
||||
/// Daemon state manager
|
||||
pub struct Daemon {
|
||||
overlay_state: OverlayState,
|
||||
should_quit: Arc<AtomicBool>,
|
||||
toggle_requested: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Daemon {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
overlay_state: OverlayState::Hidden,
|
||||
should_quit: Arc::new(AtomicBool::new(false)),
|
||||
toggle_requested: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run daemon with signal handling
|
||||
pub fn run(&mut self) -> Result<()> {
|
||||
info!("Starting hyprmarker daemon");
|
||||
info!("Send SIGUSR1 to toggle overlay (e.g., pkill -SIGUSR1 hyprmarker)");
|
||||
info!("Configure Hyprland: bind = SUPER, D, exec, pkill -SIGUSR1 hyprmarker");
|
||||
|
||||
// Set up signal handling
|
||||
let mut signals = Signals::new([SIGUSR1, SIGTERM, SIGINT])
|
||||
.context("Failed to register signal handler")?;
|
||||
|
||||
let toggle_flag = self.toggle_requested.clone();
|
||||
let quit_flag = self.should_quit.clone();
|
||||
|
||||
// Spawn signal handler thread
|
||||
// Note: This thread will run until process termination. The signal_hook iterator
|
||||
// doesn't provide a clean shutdown mechanism with forever(), but this is acceptable
|
||||
// for a daemon process as the thread has no resources requiring explicit cleanup.
|
||||
// The thread will be terminated by the OS when the process exits.
|
||||
thread::spawn(move || {
|
||||
for sig in signals.forever() {
|
||||
match sig {
|
||||
SIGUSR1 => {
|
||||
info!("Received SIGUSR1 - toggling overlay");
|
||||
// Use Release ordering to ensure all prior memory operations
|
||||
// are visible to the thread that reads this flag
|
||||
toggle_flag.store(true, Ordering::Release);
|
||||
}
|
||||
SIGTERM | SIGINT => {
|
||||
info!("Received {} - initiating graceful shutdown",
|
||||
if sig == SIGTERM { "SIGTERM" } else { "SIGINT" });
|
||||
// Use Release ordering to ensure all prior memory operations
|
||||
// are visible to the thread that reads this flag
|
||||
quit_flag.store(true, Ordering::Release);
|
||||
}
|
||||
_ => {
|
||||
warn!("Received unexpected signal: {}", sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start system tray
|
||||
let tray_toggle = self.toggle_requested.clone();
|
||||
let tray_quit = self.should_quit.clone();
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = run_system_tray(tray_toggle, tray_quit) {
|
||||
warn!("System tray failed: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
info!("Daemon ready - waiting for toggle signal");
|
||||
|
||||
// Main daemon loop
|
||||
loop {
|
||||
// Check for quit signal
|
||||
// Use Acquire ordering to ensure we see all memory operations
|
||||
// that happened before the flag was set
|
||||
if self.should_quit.load(Ordering::Acquire) {
|
||||
info!("Quit signal received - exiting daemon");
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for toggle request
|
||||
// Use Acquire ordering to ensure we see all memory operations
|
||||
// that happened before the flag was set
|
||||
if self.toggle_requested.swap(false, Ordering::Acquire) {
|
||||
self.toggle_overlay()?;
|
||||
}
|
||||
|
||||
// Small sleep to avoid busy-waiting
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
info!("Daemon shutting down");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Toggle overlay visibility
|
||||
fn toggle_overlay(&mut self) -> Result<()> {
|
||||
match self.overlay_state {
|
||||
OverlayState::Hidden => {
|
||||
info!("Showing overlay");
|
||||
self.show_overlay()?;
|
||||
}
|
||||
OverlayState::Visible => {
|
||||
info!("Hiding overlay");
|
||||
self.hide_overlay()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show overlay (create layer surface and enter drawing mode)
|
||||
fn show_overlay(&mut self) -> Result<()> {
|
||||
if self.overlay_state == OverlayState::Visible {
|
||||
debug!("Overlay already visible");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Set state to visible before running
|
||||
self.overlay_state = OverlayState::Visible;
|
||||
info!("Overlay state set to Visible");
|
||||
|
||||
// Run the Wayland backend (this will block until overlay is closed)
|
||||
let result = backend::run_wayland();
|
||||
|
||||
// When run_wayland returns, the overlay was closed
|
||||
self.overlay_state = OverlayState::Hidden;
|
||||
info!("Overlay closed, back to daemon mode");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Hide overlay (destroy layer surface, return to hidden state)
|
||||
fn hide_overlay(&mut self) -> Result<()> {
|
||||
if self.overlay_state == OverlayState::Hidden {
|
||||
debug!("Overlay already hidden");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// NOTE: The overlay will be closed when user presses Escape
|
||||
// or when the backend exits naturally
|
||||
self.overlay_state = OverlayState::Hidden;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// System tray implementation
|
||||
fn run_system_tray(toggle_flag: Arc<AtomicBool>, quit_flag: Arc<AtomicBool>) -> Result<()> {
|
||||
use ksni;
|
||||
|
||||
struct HyprmarkerTray {
|
||||
toggle_flag: Arc<AtomicBool>,
|
||||
quit_flag: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl ksni::Tray for HyprmarkerTray {
|
||||
fn id(&self) -> String {
|
||||
"hyprmarker".into()
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
"Hyprmarker Screen Annotation".into()
|
||||
}
|
||||
|
||||
fn icon_name(&self) -> String {
|
||||
// Try common icon names - some systems might have these
|
||||
"applications-graphics".into()
|
||||
}
|
||||
|
||||
fn tool_tip(&self) -> ksni::ToolTip {
|
||||
ksni::ToolTip {
|
||||
icon_name: "applications-graphics".into(),
|
||||
icon_pixmap: vec![],
|
||||
title: "Hyprmarker".into(),
|
||||
description: "Press Super+D to toggle overlay".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_pixmap(&self) -> Vec<ksni::Icon> {
|
||||
// Create a simple, highly visible pencil icon
|
||||
// 22x22 pixels in ARGB32 format (4 bytes per pixel)
|
||||
let size = 22;
|
||||
let mut data = Vec::with_capacity(size * size * 4);
|
||||
|
||||
for y in 0..size {
|
||||
for x in 0..size {
|
||||
// Create a diagonal pencil shape pointing down-right
|
||||
let (a, r, g, b) = if (2..=4).contains(&x) && (2..=4).contains(&y) {
|
||||
// Pencil tip - dark gray/graphite
|
||||
(255, 60, 60, 60)
|
||||
} else if (3..=5).contains(&x) && (5..=7).contains(&y) {
|
||||
// Wood section - brown
|
||||
(255, 180, 120, 60)
|
||||
} else if (4..=8).contains(&x) && (6..=14).contains(&y) {
|
||||
// Main body - yellow pencil
|
||||
(255, 255, 220, 0)
|
||||
} else if (7..=9).contains(&x) && (13..=17).contains(&y) {
|
||||
// Metal ferrule - silver
|
||||
(255, 180, 180, 180)
|
||||
} else if (8..=11).contains(&x) && (16..=19).contains(&y) {
|
||||
// Eraser - pink
|
||||
(255, 255, 150, 180)
|
||||
} else {
|
||||
// Transparent background
|
||||
(0, 0, 0, 0)
|
||||
};
|
||||
|
||||
data.push(a);
|
||||
data.push(r);
|
||||
data.push(g);
|
||||
data.push(b);
|
||||
}
|
||||
}
|
||||
|
||||
vec![ksni::Icon {
|
||||
width: size as i32,
|
||||
height: size as i32,
|
||||
data,
|
||||
}]
|
||||
}
|
||||
|
||||
fn category(&self) -> ksni::Category {
|
||||
ksni::Category::ApplicationStatus
|
||||
}
|
||||
|
||||
fn status(&self) -> ksni::Status {
|
||||
ksni::Status::Active
|
||||
}
|
||||
|
||||
fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
|
||||
use ksni::menu::*;
|
||||
|
||||
vec![
|
||||
StandardItem {
|
||||
label: "Toggle Overlay (Super+D)".into(),
|
||||
icon_name: "tool-pointer".into(),
|
||||
activate: Box::new(|this: &mut Self| {
|
||||
// Use Release ordering to ensure memory operations are visible
|
||||
this.toggle_flag.store(true, Ordering::Release);
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
MenuItem::Separator,
|
||||
StandardItem {
|
||||
label: "Quit".into(),
|
||||
icon_name: "window-close".into(),
|
||||
activate: Box::new(|this: &mut Self| {
|
||||
// Use Release ordering to ensure memory operations are visible
|
||||
this.quit_flag.store(true, Ordering::Release);
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
let tray = HyprmarkerTray {
|
||||
toggle_flag,
|
||||
quit_flag: quit_flag.clone(),
|
||||
};
|
||||
|
||||
info!("Creating tray service...");
|
||||
|
||||
// ksni 0.3+ uses async API - spawn service in background tokio runtime
|
||||
// Note: This thread will be terminated when the main daemon loop exits.
|
||||
// The tray library handles its own cleanup when dropped.
|
||||
let _tray_thread = std::thread::spawn(move || {
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(runtime) => runtime,
|
||||
Err(e) => {
|
||||
warn!("Failed to create Tokio runtime for system tray: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
rt.block_on(async {
|
||||
match tray.spawn().await {
|
||||
Ok(handle) => {
|
||||
info!("System tray spawned successfully");
|
||||
|
||||
// Monitor quit flag and shutdown gracefully
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
if quit_flag.load(Ordering::Acquire) {
|
||||
info!("Quit signal received - shutting down system tray");
|
||||
// Shutdown awaiter will clean up the tray service
|
||||
let _ = handle.shutdown().await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("System tray error: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
info!("System tray thread started");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
//! RGBA color type and predefined color constants.
|
||||
|
||||
/// Represents an RGBA color with floating-point components.
|
||||
///
|
||||
/// All components are in the range 0.0 (minimum) to 1.0 (maximum).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use hyprmarker::draw::Color;
|
||||
/// let red = Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
|
||||
/// let semi_transparent_blue = Color { r: 0.0, g: 0.0, b: 1.0, a: 0.5 };
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Color {
|
||||
/// Red component (0.0 = no red, 1.0 = full red)
|
||||
pub r: f64,
|
||||
/// Green component (0.0 = no green, 1.0 = full green)
|
||||
pub g: f64,
|
||||
/// Blue component (0.0 = no blue, 1.0 = full blue)
|
||||
pub b: f64,
|
||||
/// Alpha/transparency (0.0 = fully transparent, 1.0 = fully opaque)
|
||||
pub a: f64,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// Creates a new color from RGBA components.
|
||||
///
|
||||
/// All values should be in the range 0.0 to 1.0.
|
||||
/// This method is kept for future extensibility (custom colors in config file).
|
||||
#[allow(dead_code)]
|
||||
pub fn new(r: f64, g: f64, b: f64, a: f64) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Predefined Color Constants (ZoomIt-inspired palette)
|
||||
// ============================================================================
|
||||
|
||||
/// Predefined red color (R=1.0, G=0.0, B=0.0)
|
||||
pub const RED: Color = Color {
|
||||
r: 1.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
/// Predefined green color (R=0.0, G=1.0, B=0.0)
|
||||
pub const GREEN: Color = Color {
|
||||
r: 0.0,
|
||||
g: 1.0,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
/// Predefined blue color (R=0.0, G=0.0, B=1.0)
|
||||
pub const BLUE: Color = Color {
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 1.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
/// Predefined yellow color (R=1.0, G=1.0, B=0.0)
|
||||
pub const YELLOW: Color = Color {
|
||||
r: 1.0,
|
||||
g: 1.0,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
/// Predefined orange color (R=1.0, G=0.5, B=0.0)
|
||||
pub const ORANGE: Color = Color {
|
||||
r: 1.0,
|
||||
g: 0.5,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
/// Predefined pink/magenta color (R=1.0, G=0.0, B=1.0)
|
||||
pub const PINK: Color = Color {
|
||||
r: 1.0,
|
||||
g: 0.0,
|
||||
b: 1.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
/// Predefined white color (R=1.0, G=1.0, B=1.0)
|
||||
pub const WHITE: Color = Color {
|
||||
r: 1.0,
|
||||
g: 1.0,
|
||||
b: 1.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
/// Predefined black color (R=0.0, G=0.0, B=0.0)
|
||||
pub const BLACK: Color = Color {
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
/// Fully transparent color - kept for future use (e.g., effects, config file)
|
||||
#[allow(dead_code)]
|
||||
pub const TRANSPARENT: Color = Color {
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 0.0,
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
//! Font descriptor for text rendering.
|
||||
|
||||
/// Font configuration for text rendering.
|
||||
///
|
||||
/// Describes which font to use, including family name, weight, and style.
|
||||
/// This descriptor is passed through the rendering pipeline to ensure
|
||||
/// consistent font usage across preview and finalized text.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FontDescriptor {
|
||||
/// Font family name (e.g., "Sans", "Monospace", "JetBrains Mono")
|
||||
/// Reference installed system fonts by name
|
||||
pub family: String,
|
||||
|
||||
/// Font weight (e.g., "normal", "bold", "light" or numeric 100-900)
|
||||
pub weight: String,
|
||||
|
||||
/// Font style (e.g., "normal", "italic", "oblique")
|
||||
pub style: String,
|
||||
}
|
||||
|
||||
impl FontDescriptor {
|
||||
/// Creates a new font descriptor with the specified parameters.
|
||||
pub fn new(family: String, weight: String, style: String) -> Self {
|
||||
Self {
|
||||
family,
|
||||
weight,
|
||||
style,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a default font descriptor matching the current hardcoded behavior.
|
||||
#[allow(dead_code)]
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
family: "Sans".to_string(),
|
||||
weight: "bold".to_string(),
|
||||
style: "normal".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this font descriptor to a Pango font description string.
|
||||
///
|
||||
/// Format: "Family Style Weight Size"
|
||||
/// Example: "Sans Bold 32" or "Monospace Italic 24"
|
||||
pub fn to_pango_string(&self, size: f64) -> String {
|
||||
let mut parts = vec![self.family.clone()];
|
||||
|
||||
// Add style if not normal
|
||||
if self.style.to_lowercase() != "normal" {
|
||||
parts.push(capitalize_first(&self.style));
|
||||
}
|
||||
|
||||
// Add weight if not normal
|
||||
if self.weight.to_lowercase() != "normal" {
|
||||
parts.push(capitalize_first(&self.weight));
|
||||
}
|
||||
|
||||
// Add size
|
||||
parts.push(format!("{}", size.round() as i32));
|
||||
|
||||
parts.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
/// Capitalizes the first letter of a string.
|
||||
fn capitalize_first(s: &str) -> String {
|
||||
let mut chars = s.chars();
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pango_string_default() {
|
||||
let font = FontDescriptor::default();
|
||||
assert_eq!(font.to_pango_string(32.0), "Sans Bold 32");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pango_string_italic() {
|
||||
let font = FontDescriptor::new(
|
||||
"Monospace".to_string(),
|
||||
None,
|
||||
"normal".to_string(),
|
||||
"italic".to_string(),
|
||||
);
|
||||
assert_eq!(font.to_pango_string(24.0), "Monospace Italic 24");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pango_string_custom() {
|
||||
let font = FontDescriptor::new(
|
||||
"JetBrains Mono".to_string(),
|
||||
None,
|
||||
"light".to_string(),
|
||||
"normal".to_string(),
|
||||
);
|
||||
assert_eq!(font.to_pango_string(16.0), "JetBrains Mono Light 16");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//! Frame container for managing collections of shapes.
|
||||
|
||||
use super::shape::Shape;
|
||||
|
||||
/// Container for all shapes in the current drawing session.
|
||||
///
|
||||
/// Manages a collection of [`Shape`]s and provides operations like adding,
|
||||
/// clearing, and undoing shapes. Acts as the drawing canvas state.
|
||||
pub struct Frame {
|
||||
/// Vector of all shapes in draw order (first = bottom layer, last = top layer)
|
||||
pub shapes: Vec<Shape>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// Creates a new empty frame with no shapes.
|
||||
pub fn new() -> Self {
|
||||
Self { shapes: Vec::new() }
|
||||
}
|
||||
|
||||
/// Removes all shapes from the frame, clearing the canvas.
|
||||
pub fn clear(&mut self) {
|
||||
self.shapes.clear();
|
||||
}
|
||||
|
||||
/// Adds a new shape to the frame (drawn on top of existing shapes).
|
||||
pub fn add_shape(&mut self, shape: Shape) {
|
||||
self.shapes.push(shape);
|
||||
}
|
||||
|
||||
/// Removes the most recently added shape.
|
||||
///
|
||||
/// Returns `true` if a shape was removed, `false` if the frame was already empty.
|
||||
pub fn undo(&mut self) -> bool {
|
||||
if self.shapes.is_empty() {
|
||||
false
|
||||
} else {
|
||||
self.shapes.pop();
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//! Rendering primitives and shape definitions (Cairo-based).
|
||||
//!
|
||||
//! This module defines the core drawing types used for screen annotation:
|
||||
//! - [`Color`]: RGBA color representation with predefined color constants
|
||||
//! - [`Shape`]: Different annotation types (lines, rectangles, text, etc.)
|
||||
//! - [`Frame`]: Container for all shapes in the current drawing
|
||||
//! - Rendering functions for Cairo-based output
|
||||
|
||||
pub mod color;
|
||||
pub mod font;
|
||||
pub mod frame;
|
||||
pub mod render;
|
||||
pub mod shape;
|
||||
|
||||
// Re-export commonly used types at module level
|
||||
pub use color::Color;
|
||||
pub use font::FontDescriptor;
|
||||
pub use frame::Frame;
|
||||
pub use render::{render_freehand_borrowed, render_shape, render_shapes, render_text};
|
||||
pub use shape::Shape;
|
||||
|
||||
// Re-export color constants for public API (unused internally but part of public interface)
|
||||
#[allow(unused_imports)]
|
||||
pub use color::{BLACK, BLUE, GREEN, ORANGE, PINK, RED, TRANSPARENT, WHITE, YELLOW};
|
||||
|
||||
// Re-export utility functions for public API (unused internally but part of public interface)
|
||||
#[allow(unused_imports)]
|
||||
pub use render::fill_transparent;
|
||||
@@ -0,0 +1,363 @@
|
||||
//! Cairo-based rendering functions for shapes.
|
||||
|
||||
use super::color::Color;
|
||||
use super::shape::Shape;
|
||||
use crate::util;
|
||||
|
||||
/// Renders all shapes in a collection to a Cairo context.
|
||||
///
|
||||
/// Iterates through the shapes slice and renders each one in order.
|
||||
/// Shapes are drawn in the order they appear (first shape = bottom layer).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ctx` - Cairo drawing context to render to
|
||||
/// * `shapes` - Slice of shapes to render
|
||||
pub fn render_shapes(ctx: &cairo::Context, shapes: &[Shape]) {
|
||||
for shape in shapes {
|
||||
render_shape(ctx, shape);
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a single shape to a Cairo context.
|
||||
///
|
||||
/// Dispatches to the appropriate internal rendering function based on shape type.
|
||||
/// Handles all shape variants: Freehand, Line, Rect, Ellipse, Arrow, and Text.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ctx` - Cairo drawing context to render to
|
||||
/// * `shape` - The shape to render
|
||||
pub fn render_shape(ctx: &cairo::Context, shape: &Shape) {
|
||||
match shape {
|
||||
Shape::Freehand {
|
||||
points,
|
||||
color,
|
||||
thick,
|
||||
} => {
|
||||
render_freehand_borrowed(ctx, points, *color, *thick);
|
||||
}
|
||||
Shape::Line {
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
color,
|
||||
thick,
|
||||
} => {
|
||||
render_line(ctx, *x1, *y1, *x2, *y2, *color, *thick);
|
||||
}
|
||||
Shape::Rect {
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
color,
|
||||
thick,
|
||||
} => {
|
||||
render_rect(ctx, *x, *y, *w, *h, *color, *thick);
|
||||
}
|
||||
Shape::Ellipse {
|
||||
cx,
|
||||
cy,
|
||||
rx,
|
||||
ry,
|
||||
color,
|
||||
thick,
|
||||
} => {
|
||||
render_ellipse(ctx, *cx, *cy, *rx, *ry, *color, *thick);
|
||||
}
|
||||
Shape::Arrow {
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
color,
|
||||
thick,
|
||||
arrow_length,
|
||||
arrow_angle,
|
||||
} => {
|
||||
render_arrow(
|
||||
ctx,
|
||||
*x1,
|
||||
*y1,
|
||||
*x2,
|
||||
*y2,
|
||||
*color,
|
||||
*thick,
|
||||
*arrow_length,
|
||||
*arrow_angle,
|
||||
);
|
||||
}
|
||||
Shape::Text {
|
||||
x,
|
||||
y,
|
||||
text,
|
||||
color,
|
||||
size,
|
||||
font_descriptor,
|
||||
background_enabled,
|
||||
} => {
|
||||
render_text(
|
||||
ctx,
|
||||
*x,
|
||||
*y,
|
||||
text,
|
||||
*color,
|
||||
*size,
|
||||
font_descriptor,
|
||||
*background_enabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render freehand stroke (polyline through points)
|
||||
///
|
||||
/// This function accepts a borrowed slice, avoiding clones for better performance.
|
||||
/// Use this for rendering provisional shapes during drawing to prevent quadratic behavior.
|
||||
pub fn render_freehand_borrowed(
|
||||
ctx: &cairo::Context,
|
||||
points: &[(i32, i32)],
|
||||
color: Color,
|
||||
thick: f64,
|
||||
) {
|
||||
if points.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.set_source_rgba(color.r, color.g, color.b, color.a);
|
||||
ctx.set_line_width(thick);
|
||||
ctx.set_line_cap(cairo::LineCap::Round);
|
||||
ctx.set_line_join(cairo::LineJoin::Round);
|
||||
|
||||
// Start at first point
|
||||
let (x0, y0) = points[0];
|
||||
ctx.move_to(x0 as f64, y0 as f64);
|
||||
|
||||
// Draw lines through all points
|
||||
for &(x, y) in &points[1..] {
|
||||
ctx.line_to(x as f64, y as f64);
|
||||
}
|
||||
|
||||
let _ = ctx.stroke();
|
||||
}
|
||||
|
||||
/// Render a straight line
|
||||
fn render_line(ctx: &cairo::Context, x1: i32, y1: i32, x2: i32, y2: i32, color: Color, thick: f64) {
|
||||
ctx.set_source_rgba(color.r, color.g, color.b, color.a);
|
||||
ctx.set_line_width(thick);
|
||||
ctx.set_line_cap(cairo::LineCap::Round);
|
||||
|
||||
ctx.move_to(x1 as f64, y1 as f64);
|
||||
ctx.line_to(x2 as f64, y2 as f64);
|
||||
let _ = ctx.stroke();
|
||||
}
|
||||
|
||||
/// Render a rectangle (outline)
|
||||
fn render_rect(ctx: &cairo::Context, x: i32, y: i32, w: i32, h: i32, color: Color, thick: f64) {
|
||||
ctx.set_source_rgba(color.r, color.g, color.b, color.a);
|
||||
ctx.set_line_width(thick);
|
||||
ctx.set_line_join(cairo::LineJoin::Miter);
|
||||
|
||||
// Normalize rectangle to handle any legacy data with negative dimensions
|
||||
// (InputState already normalizes, but this ensures consistent rendering)
|
||||
let (norm_x, norm_w) = if w >= 0 {
|
||||
(x as f64, w as f64)
|
||||
} else {
|
||||
((x + w) as f64, (-w) as f64)
|
||||
};
|
||||
let (norm_y, norm_h) = if h >= 0 {
|
||||
(y as f64, h as f64)
|
||||
} else {
|
||||
((y + h) as f64, (-h) as f64)
|
||||
};
|
||||
|
||||
ctx.rectangle(norm_x, norm_y, norm_w, norm_h);
|
||||
let _ = ctx.stroke();
|
||||
}
|
||||
|
||||
/// Render an ellipse using Cairo's arc with scaling
|
||||
fn render_ellipse(
|
||||
ctx: &cairo::Context,
|
||||
cx: i32,
|
||||
cy: i32,
|
||||
rx: i32,
|
||||
ry: i32,
|
||||
color: Color,
|
||||
thick: f64,
|
||||
) {
|
||||
if rx == 0 || ry == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.set_source_rgba(color.r, color.g, color.b, color.a);
|
||||
ctx.set_line_width(thick);
|
||||
|
||||
ctx.save().ok();
|
||||
ctx.translate(cx as f64, cy as f64);
|
||||
ctx.scale(rx as f64, ry as f64);
|
||||
ctx.arc(0.0, 0.0, 1.0, 0.0, 2.0 * std::f64::consts::PI);
|
||||
ctx.restore().ok();
|
||||
|
||||
let _ = ctx.stroke();
|
||||
}
|
||||
|
||||
/// Render an arrow (line with arrowhead pointing towards start)
|
||||
fn render_arrow(
|
||||
ctx: &cairo::Context,
|
||||
x1: i32,
|
||||
y1: i32,
|
||||
x2: i32,
|
||||
y2: i32,
|
||||
color: Color,
|
||||
thick: f64,
|
||||
arrow_length: f64,
|
||||
arrow_angle: f64,
|
||||
) {
|
||||
// Draw the main line
|
||||
render_line(ctx, x1, y1, x2, y2, color, thick);
|
||||
|
||||
// Draw arrowhead at (x1, y1) pointing towards start
|
||||
// Returns [left_point, right_point]
|
||||
let arrow_points = util::calculate_arrowhead_custom(x1, y1, x2, y2, arrow_length, arrow_angle);
|
||||
|
||||
ctx.set_source_rgba(color.r, color.g, color.b, color.a);
|
||||
ctx.set_line_width(thick);
|
||||
ctx.set_line_cap(cairo::LineCap::Round);
|
||||
|
||||
// Draw left line of arrowhead (from start to left point)
|
||||
ctx.move_to(x1 as f64, y1 as f64);
|
||||
ctx.line_to(arrow_points[0].0, arrow_points[0].1);
|
||||
let _ = ctx.stroke();
|
||||
|
||||
// Draw right line of arrowhead (from start to right point)
|
||||
ctx.move_to(x1 as f64, y1 as f64);
|
||||
ctx.line_to(arrow_points[1].0, arrow_points[1].1);
|
||||
let _ = ctx.stroke();
|
||||
}
|
||||
|
||||
/// Renders text at a specified position with multi-line support using Pango.
|
||||
///
|
||||
/// Uses Pango for advanced font rendering with custom font support. The position (x, y)
|
||||
/// represents the text baseline starting point for the first line.
|
||||
/// Text containing newline characters ('\n') will be rendered across multiple lines
|
||||
/// with proper line spacing determined by the font metrics.
|
||||
///
|
||||
/// Text is rendered with a contrasting stroke outline for better visibility
|
||||
/// against any background color.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ctx` - Cairo drawing context to render to
|
||||
/// * `x` - X coordinate of text baseline start
|
||||
/// * `y` - Y coordinate of text baseline (first line)
|
||||
/// * `text` - Text content to render (may contain '\n' for line breaks)
|
||||
/// * `color` - Text color
|
||||
/// * `size` - Font size in points
|
||||
/// * `font_descriptor` - Font configuration (family, weight, style)
|
||||
/// * `background_enabled` - Whether to draw background box behind text
|
||||
pub fn render_text(
|
||||
ctx: &cairo::Context,
|
||||
x: i32,
|
||||
y: i32,
|
||||
text: &str,
|
||||
color: Color,
|
||||
size: f64,
|
||||
font_descriptor: &super::FontDescriptor,
|
||||
background_enabled: bool,
|
||||
) {
|
||||
// Save context state to prevent settings from leaking to other drawing operations
|
||||
ctx.save().ok();
|
||||
|
||||
// Use Best antialiasing (gray) instead of Subpixel for ARGB overlay
|
||||
// Subpixel can cause color fringing on transparent/composited surfaces
|
||||
ctx.set_antialias(cairo::Antialias::Best);
|
||||
|
||||
// Create Pango layout for text rendering
|
||||
let layout = pangocairo::functions::create_layout(ctx);
|
||||
|
||||
// Set font description from config
|
||||
let font_desc_str = font_descriptor.to_pango_string(size);
|
||||
let font_desc = pango::FontDescription::from_string(&font_desc_str);
|
||||
layout.set_font_description(Some(&font_desc));
|
||||
|
||||
// Set the text (Pango handles newlines automatically)
|
||||
layout.set_text(text);
|
||||
|
||||
// Get layout extents for background and effects
|
||||
let (ink_rect, _logical_rect) = layout.extents();
|
||||
|
||||
// Include ink rect offsets for italic/stroked glyphs with negative bearings
|
||||
let ink_x = ink_rect.x() as f64 / pango::SCALE as f64;
|
||||
let ink_y = ink_rect.y() as f64 / pango::SCALE as f64;
|
||||
let ink_width = ink_rect.width() as f64 / pango::SCALE as f64;
|
||||
let ink_height = ink_rect.height() as f64 / pango::SCALE as f64;
|
||||
|
||||
// Calculate brightness to determine background/stroke color
|
||||
let brightness = color.r * 0.299 + color.g * 0.587 + color.b * 0.114;
|
||||
let (bg_r, bg_g, bg_b) = if brightness > 0.5 {
|
||||
(0.0, 0.0, 0.0) // Dark background/stroke for light text colors
|
||||
} else {
|
||||
(1.0, 1.0, 1.0) // Light background/stroke for dark text colors
|
||||
};
|
||||
|
||||
// Adjust y position (Pango measures from top-left, we want baseline)
|
||||
let baseline = layout.baseline() as f64 / pango::SCALE as f64;
|
||||
let adjusted_y = y as f64 - baseline;
|
||||
|
||||
// First pass: draw semi-transparent background rectangle (if enabled)
|
||||
if background_enabled && ink_width > 0.0 && ink_height > 0.0 {
|
||||
let padding = size * 0.15;
|
||||
// Use ink rect offsets to properly align background for italic/stroked glyphs
|
||||
ctx.rectangle(
|
||||
x as f64 + ink_x - padding,
|
||||
adjusted_y + ink_y - padding,
|
||||
ink_width + padding * 2.0,
|
||||
ink_height + padding * 2.0,
|
||||
);
|
||||
ctx.set_source_rgba(bg_r, bg_g, bg_b, 0.3);
|
||||
let _ = ctx.fill();
|
||||
}
|
||||
|
||||
// Second pass: draw drop shadow for depth
|
||||
let shadow_offset = size * 0.04;
|
||||
ctx.move_to(x as f64 + shadow_offset, adjusted_y + shadow_offset);
|
||||
ctx.set_source_rgba(0.0, 0.0, 0.0, 0.4);
|
||||
pangocairo::functions::show_layout(ctx, &layout);
|
||||
|
||||
// Third pass: render text with contrasting stroke outline
|
||||
ctx.move_to(x as f64, adjusted_y);
|
||||
|
||||
// Create path from layout for stroking
|
||||
pangocairo::functions::layout_path(ctx, &layout);
|
||||
|
||||
// Fully opaque stroke for maximum contrast and crispness
|
||||
ctx.set_source_rgba(bg_r, bg_g, bg_b, 1.0);
|
||||
ctx.set_line_width(size * 0.06);
|
||||
ctx.set_line_join(cairo::LineJoin::Round);
|
||||
let _ = ctx.stroke_preserve();
|
||||
|
||||
// Fill with bright, full-intensity color
|
||||
ctx.set_source_rgba(color.r, color.g, color.b, color.a);
|
||||
let _ = ctx.fill();
|
||||
|
||||
// Restore context state
|
||||
ctx.restore().ok();
|
||||
}
|
||||
|
||||
/// Fills the entire surface with a semi-transparent tinted background.
|
||||
///
|
||||
/// Creates a barely visible dark tint (0.05 alpha) to confirm the overlay is active
|
||||
/// without obscuring the screen content. This function is kept for potential future use.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ctx` - Cairo drawing context to fill
|
||||
/// * `width` - Surface width in pixels
|
||||
/// * `height` - Surface height in pixels
|
||||
#[allow(dead_code)]
|
||||
pub fn fill_transparent(ctx: &cairo::Context, width: i32, height: i32) {
|
||||
// Use a very slight tint so we can see the overlay is there
|
||||
// 0.05 alpha = barely visible, just enough to confirm it's working
|
||||
ctx.set_source_rgba(0.1, 0.1, 0.1, 0.05);
|
||||
ctx.set_operator(cairo::Operator::Source);
|
||||
ctx.rectangle(0.0, 0.0, width as f64, height as f64);
|
||||
let _ = ctx.fill();
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//! Shape definitions for screen annotations.
|
||||
|
||||
use super::color::Color;
|
||||
use super::font::FontDescriptor;
|
||||
|
||||
/// Represents a drawable shape or annotation on screen.
|
||||
///
|
||||
/// Each variant represents a different drawing tool/primitive with its specific parameters.
|
||||
/// All shapes store their own color and size information for independent rendering.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Shape {
|
||||
/// Freehand drawing - polyline connecting mouse drag points
|
||||
Freehand {
|
||||
/// Sequence of (x, y) coordinates traced by the mouse
|
||||
points: Vec<(i32, i32)>,
|
||||
/// Stroke color
|
||||
color: Color,
|
||||
/// Line thickness in pixels
|
||||
thick: f64,
|
||||
},
|
||||
/// Straight line between two points (drawn with Shift modifier)
|
||||
Line {
|
||||
/// Starting X coordinate
|
||||
x1: i32,
|
||||
/// Starting Y coordinate
|
||||
y1: i32,
|
||||
/// Ending X coordinate
|
||||
x2: i32,
|
||||
/// Ending Y coordinate
|
||||
y2: i32,
|
||||
/// Line color
|
||||
color: Color,
|
||||
/// Line thickness in pixels
|
||||
thick: f64,
|
||||
},
|
||||
/// Rectangle outline (drawn with Ctrl modifier)
|
||||
Rect {
|
||||
/// Top-left X coordinate
|
||||
x: i32,
|
||||
/// Top-left Y coordinate
|
||||
y: i32,
|
||||
/// Width in pixels
|
||||
w: i32,
|
||||
/// Height in pixels
|
||||
h: i32,
|
||||
/// Border color
|
||||
color: Color,
|
||||
/// Border thickness in pixels
|
||||
thick: f64,
|
||||
},
|
||||
/// Ellipse/circle outline (drawn with Tab modifier)
|
||||
Ellipse {
|
||||
/// Center X coordinate
|
||||
cx: i32,
|
||||
/// Center Y coordinate
|
||||
cy: i32,
|
||||
/// Horizontal radius
|
||||
rx: i32,
|
||||
/// Vertical radius
|
||||
ry: i32,
|
||||
/// Border color
|
||||
color: Color,
|
||||
/// Border thickness in pixels
|
||||
thick: f64,
|
||||
},
|
||||
/// Arrow with directional head (drawn with Ctrl+Shift modifiers)
|
||||
Arrow {
|
||||
/// Starting X coordinate (arrowhead location)
|
||||
x1: i32,
|
||||
/// Starting Y coordinate (arrowhead location)
|
||||
y1: i32,
|
||||
/// Ending X coordinate (arrow tail)
|
||||
x2: i32,
|
||||
/// Ending Y coordinate (arrow tail)
|
||||
y2: i32,
|
||||
/// Arrow color
|
||||
color: Color,
|
||||
/// Line thickness in pixels
|
||||
thick: f64,
|
||||
/// Arrowhead length in pixels
|
||||
arrow_length: f64,
|
||||
/// Arrowhead angle in degrees
|
||||
arrow_angle: f64,
|
||||
},
|
||||
/// Text annotation (activated with 'T' key)
|
||||
Text {
|
||||
/// Baseline X coordinate
|
||||
x: i32,
|
||||
/// Baseline Y coordinate
|
||||
y: i32,
|
||||
/// Text content to display
|
||||
text: String,
|
||||
/// Text color
|
||||
color: Color,
|
||||
/// Font size in points
|
||||
size: f64,
|
||||
/// Font descriptor (family, weight, style)
|
||||
font_descriptor: FontDescriptor,
|
||||
/// Whether to draw background box behind text
|
||||
background_enabled: bool,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//! Generic input event types for cross-backend compatibility.
|
||||
|
||||
/// Generic key representation for cross-backend compatibility.
|
||||
///
|
||||
/// Backend implementations map their native key codes to these generic
|
||||
/// key values for unified input handling.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[allow(dead_code)] // Some variants used only in specific contexts
|
||||
pub enum Key {
|
||||
/// Regular character key (a-z, 0-9, symbols)
|
||||
Char(char),
|
||||
/// Escape key
|
||||
Escape,
|
||||
/// Return/Enter key
|
||||
Return,
|
||||
/// Backspace key
|
||||
Backspace,
|
||||
/// Tab key
|
||||
Tab,
|
||||
/// Space bar
|
||||
Space,
|
||||
/// Shift modifier
|
||||
Shift,
|
||||
/// Ctrl modifier
|
||||
Ctrl,
|
||||
/// Alt modifier
|
||||
Alt,
|
||||
/// Plus key (increase thickness)
|
||||
Plus,
|
||||
/// Minus key (decrease thickness)
|
||||
Minus,
|
||||
/// Equals key (alternate for plus)
|
||||
Equals,
|
||||
/// Underscore key (alternate for minus)
|
||||
Underscore,
|
||||
/// F10 function key (toggle help)
|
||||
F10,
|
||||
/// Unmapped or unrecognized key
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Mouse button identification.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MouseButton {
|
||||
/// Left mouse button (primary drawing button)
|
||||
Left,
|
||||
/// Right mouse button (cancel action)
|
||||
Right,
|
||||
/// Middle mouse button (currently unused)
|
||||
Middle,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//! Input handling and tool state machine.
|
||||
//!
|
||||
//! This module translates backend keyboard and mouse events into drawing actions.
|
||||
//! It maintains the current tool state, drawing parameters (color, thickness),
|
||||
//! and manages the state machine for different drawing modes (idle, drawing, text input).
|
||||
|
||||
pub mod events;
|
||||
pub mod modifiers;
|
||||
pub mod state;
|
||||
pub mod tool;
|
||||
|
||||
// Re-export commonly used types at module level
|
||||
pub use events::{Key, MouseButton};
|
||||
pub use state::{DrawingState, InputState};
|
||||
pub use tool::Tool;
|
||||
|
||||
// Re-export for public API (unused internally but part of public interface)
|
||||
#[allow(unused_imports)]
|
||||
pub use modifiers::Modifiers;
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Keyboard modifier state tracking.
|
||||
|
||||
use super::tool::Tool;
|
||||
|
||||
/// Keyboard modifier state.
|
||||
///
|
||||
/// Tracks which modifier keys (Shift, Ctrl, Alt, Tab) are currently pressed.
|
||||
/// Used to determine the active drawing tool and handle keyboard shortcuts.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Modifiers {
|
||||
/// Shift key pressed
|
||||
pub shift: bool,
|
||||
/// Ctrl key pressed
|
||||
pub ctrl: bool,
|
||||
/// Alt key pressed
|
||||
pub alt: bool,
|
||||
/// Tab key pressed
|
||||
pub tab: bool,
|
||||
}
|
||||
|
||||
impl Modifiers {
|
||||
/// Creates a new Modifiers instance with all keys released.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
tab: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines which drawing tool is active based on current modifier state.
|
||||
///
|
||||
/// # Tool Selection Priority
|
||||
/// 1. Ctrl+Shift → Arrow
|
||||
/// 2. Ctrl → Rectangle
|
||||
/// 3. Shift → Line
|
||||
/// 4. Tab → Ellipse
|
||||
/// 5. None → Pen (default)
|
||||
pub fn current_tool(&self) -> Tool {
|
||||
if self.ctrl && self.shift {
|
||||
Tool::Arrow
|
||||
} else if self.ctrl {
|
||||
Tool::Rect
|
||||
} else if self.shift {
|
||||
Tool::Line
|
||||
} else if self.tab {
|
||||
Tool::Ellipse
|
||||
} else {
|
||||
Tool::Pen
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
//! Drawing state machine and input state management.
|
||||
|
||||
use super::events::{Key, MouseButton};
|
||||
use super::modifiers::Modifiers;
|
||||
use super::tool::Tool;
|
||||
use crate::draw::{Color, FontDescriptor, Frame, Shape};
|
||||
use crate::util;
|
||||
|
||||
/// Current drawing mode state machine.
|
||||
///
|
||||
/// Tracks whether the user is idle, actively drawing a shape, or entering text.
|
||||
/// State transitions occur based on mouse and keyboard events.
|
||||
#[derive(Debug)]
|
||||
pub enum DrawingState {
|
||||
/// Not actively drawing - waiting for user input
|
||||
Idle,
|
||||
/// Actively drawing a shape (mouse button held down)
|
||||
Drawing {
|
||||
/// Which tool is being used for this shape
|
||||
tool: Tool,
|
||||
/// Starting X coordinate (where mouse was pressed)
|
||||
start_x: i32,
|
||||
/// Starting Y coordinate (where mouse was pressed)
|
||||
start_y: i32,
|
||||
/// Accumulated points for freehand drawing
|
||||
points: Vec<(i32, i32)>,
|
||||
},
|
||||
/// Text input mode - user is typing text to place on screen
|
||||
TextInput {
|
||||
/// X coordinate where text will be placed
|
||||
x: i32,
|
||||
/// Y coordinate where text will be placed
|
||||
y: i32,
|
||||
/// Accumulated text buffer
|
||||
buffer: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Main input state containing all drawing session state.
|
||||
///
|
||||
/// This struct holds the current frame (all drawn shapes), drawing parameters,
|
||||
/// modifier keys, drawing mode, and UI flags. It processes all keyboard and
|
||||
/// mouse events to update the drawing state and determine when redraws are needed.
|
||||
pub struct InputState {
|
||||
/// Container for all shapes drawn in this session
|
||||
pub frame: Frame,
|
||||
/// Current drawing color (changed with color keys: R, G, B, etc.)
|
||||
pub current_color: Color,
|
||||
/// Current pen/line thickness in pixels (changed with +/- keys)
|
||||
pub current_thickness: f64,
|
||||
/// Current font size for text mode (from config)
|
||||
pub current_font_size: f64,
|
||||
/// Font descriptor for text rendering (family, weight, style)
|
||||
pub font_descriptor: FontDescriptor,
|
||||
/// Whether to draw background behind text
|
||||
pub text_background_enabled: bool,
|
||||
/// Arrowhead length in pixels (from config)
|
||||
pub arrow_length: f64,
|
||||
/// Arrowhead angle in degrees (from config)
|
||||
pub arrow_angle: f64,
|
||||
/// Current modifier key state
|
||||
pub modifiers: Modifiers,
|
||||
/// Current drawing mode state machine
|
||||
pub state: DrawingState,
|
||||
/// Whether user requested to exit the overlay
|
||||
pub should_exit: bool,
|
||||
/// Whether the display needs to be redrawn
|
||||
pub needs_redraw: bool,
|
||||
/// Whether the help overlay is currently visible (toggled with F10)
|
||||
pub show_help: bool,
|
||||
/// Screen width in pixels (set by backend after configuration)
|
||||
pub screen_width: u32,
|
||||
/// Screen height in pixels (set by backend after configuration)
|
||||
pub screen_height: u32,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
/// Creates a new InputState with specified defaults.
|
||||
///
|
||||
/// Screen dimensions default to 0 and should be updated by the backend
|
||||
/// after surface configuration (see `update_screen_dimensions`).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `color` - Initial drawing color
|
||||
/// * `thickness` - Initial pen thickness in pixels
|
||||
/// * `font_size` - Font size for text mode in points
|
||||
/// * `font_descriptor` - Font configuration for text rendering
|
||||
/// * `text_background_enabled` - Whether to draw background behind text
|
||||
/// * `arrow_length` - Arrowhead length in pixels
|
||||
/// * `arrow_angle` - Arrowhead angle in degrees
|
||||
pub fn with_defaults(
|
||||
color: Color,
|
||||
thickness: f64,
|
||||
font_size: f64,
|
||||
font_descriptor: FontDescriptor,
|
||||
text_background_enabled: bool,
|
||||
arrow_length: f64,
|
||||
arrow_angle: f64,
|
||||
) -> Self {
|
||||
Self {
|
||||
frame: Frame::new(),
|
||||
current_color: color,
|
||||
current_thickness: thickness,
|
||||
current_font_size: font_size,
|
||||
font_descriptor,
|
||||
text_background_enabled,
|
||||
arrow_length,
|
||||
arrow_angle,
|
||||
modifiers: Modifiers::new(),
|
||||
state: DrawingState::Idle,
|
||||
should_exit: false,
|
||||
needs_redraw: true,
|
||||
show_help: false,
|
||||
screen_width: 0,
|
||||
screen_height: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates screen dimensions after backend configuration.
|
||||
///
|
||||
/// This should be called by the backend when it receives the actual
|
||||
/// screen dimensions from the display server.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `width` - Screen width in pixels
|
||||
/// * `height` - Screen height in pixels
|
||||
pub fn update_screen_dimensions(&mut self, width: u32, height: u32) {
|
||||
self.screen_width = width;
|
||||
self.screen_height = height;
|
||||
}
|
||||
|
||||
/// Processes a key press event.
|
||||
///
|
||||
/// Handles all keyboard input including:
|
||||
/// - Drawing color selection (R, G, B, Y, O, P, W, K)
|
||||
/// - Tool actions (T for text mode, E for clear, Ctrl+Z for undo)
|
||||
/// - Text input (when in TextInput state)
|
||||
/// - Exit commands (Escape, Ctrl+Q)
|
||||
/// - Thickness adjustment (+/-, mouse scroll handled separately)
|
||||
/// - Help toggle (F10)
|
||||
/// - Modifier key tracking
|
||||
pub fn on_key_press(&mut self, key: Key) {
|
||||
match key {
|
||||
Key::Escape => {
|
||||
// Exit drawing mode or cancel current action
|
||||
match &self.state {
|
||||
DrawingState::TextInput { .. } => {
|
||||
// Cancel text input
|
||||
self.state = DrawingState::Idle;
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
DrawingState::Drawing { .. } => {
|
||||
// Cancel current drawing
|
||||
self.state = DrawingState::Idle;
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
DrawingState::Idle => {
|
||||
// Exit application
|
||||
self.should_exit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Key::Char(c) => {
|
||||
// Handle character input - check if we're in text mode first
|
||||
if let DrawingState::TextInput { buffer, .. } = &mut self.state {
|
||||
// In text mode, ALL characters go to the buffer
|
||||
buffer.push(c);
|
||||
self.needs_redraw = true;
|
||||
} else {
|
||||
// Not in text mode, handle special keys
|
||||
match c {
|
||||
't' | 'T' => {
|
||||
// Enter text mode
|
||||
if matches!(self.state, DrawingState::Idle) {
|
||||
self.state = DrawingState::TextInput {
|
||||
x: (self.screen_width / 2) as i32,
|
||||
y: (self.screen_height / 2) as i32,
|
||||
buffer: String::new(),
|
||||
};
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
'e' | 'E' => {
|
||||
// Clear all annotations
|
||||
self.frame.clear();
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
'z' | 'Z' if self.modifiers.ctrl => {
|
||||
// Undo last shape
|
||||
if self.frame.undo() {
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
'q' | 'Q' if self.modifiers.ctrl => {
|
||||
// Exit overlay (return to daemon mode)
|
||||
log::info!("Ctrl+Q pressed - setting should_exit=true");
|
||||
self.should_exit = true;
|
||||
// Trigger redraw to force event loop to check should_exit
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
_ => {
|
||||
// Check if it's a color key
|
||||
if let Some(color) = util::key_to_color(c) {
|
||||
self.current_color = color;
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Key::Backspace => {
|
||||
if let DrawingState::TextInput { buffer, .. } = &mut self.state {
|
||||
buffer.pop();
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
Key::Return => {
|
||||
// Finalize text input or insert newline if Shift is held
|
||||
if let DrawingState::TextInput { x, y, buffer } = &mut self.state {
|
||||
if self.modifiers.shift {
|
||||
// Shift+Enter: insert newline
|
||||
buffer.push('\n');
|
||||
self.needs_redraw = true;
|
||||
} else {
|
||||
// Plain Enter: finalize text input
|
||||
if !buffer.is_empty() {
|
||||
self.frame.add_shape(Shape::Text {
|
||||
x: *x,
|
||||
y: *y,
|
||||
text: buffer.clone(),
|
||||
color: self.current_color,
|
||||
size: self.current_font_size,
|
||||
font_descriptor: self.font_descriptor.clone(),
|
||||
background_enabled: self.text_background_enabled,
|
||||
});
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
self.state = DrawingState::Idle;
|
||||
}
|
||||
}
|
||||
}
|
||||
Key::Space => {
|
||||
if let DrawingState::TextInput { buffer, .. } = &mut self.state {
|
||||
buffer.push(' ');
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
Key::Shift => self.modifiers.shift = true,
|
||||
Key::Ctrl => self.modifiers.ctrl = true,
|
||||
Key::Alt => self.modifiers.alt = true,
|
||||
Key::Tab => self.modifiers.tab = true,
|
||||
Key::Plus | Key::Equals => {
|
||||
// Increase thickness
|
||||
self.current_thickness = (self.current_thickness + 1.0).min(20.0);
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
Key::Minus | Key::Underscore => {
|
||||
// Decrease thickness
|
||||
self.current_thickness = (self.current_thickness - 1.0).max(1.0);
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
Key::F10 => {
|
||||
// Toggle help overlay
|
||||
self.show_help = !self.show_help;
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a key release event.
|
||||
///
|
||||
/// Currently only tracks modifier key releases to update the modifier state.
|
||||
pub fn on_key_release(&mut self, key: Key) {
|
||||
match key {
|
||||
Key::Shift => self.modifiers.shift = false,
|
||||
Key::Ctrl => self.modifiers.ctrl = false,
|
||||
Key::Alt => self.modifiers.alt = false,
|
||||
Key::Tab => self.modifiers.tab = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a mouse button press event.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `button` - Which mouse button was pressed
|
||||
/// * `x` - Mouse X coordinate
|
||||
/// * `y` - Mouse Y coordinate
|
||||
///
|
||||
/// # Behavior
|
||||
/// - Left click while Idle: Starts drawing with the current tool (based on modifiers)
|
||||
/// - Left click during TextInput: Updates text position
|
||||
/// - Right click: Cancels current action
|
||||
pub fn on_mouse_press(&mut self, button: MouseButton, x: i32, y: i32) {
|
||||
match button {
|
||||
MouseButton::Left => {
|
||||
// Start drawing with current tool
|
||||
if matches!(self.state, DrawingState::Idle) {
|
||||
let tool = self.modifiers.current_tool();
|
||||
self.state = DrawingState::Drawing {
|
||||
tool,
|
||||
start_x: x,
|
||||
start_y: y,
|
||||
points: vec![(x, y)],
|
||||
};
|
||||
self.needs_redraw = true;
|
||||
} else if let DrawingState::TextInput { x: tx, y: ty, .. } = &mut self.state {
|
||||
// Update text position if in text mode
|
||||
*tx = x;
|
||||
*ty = y;
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
MouseButton::Right => {
|
||||
// Right-click could cancel or exit
|
||||
if !matches!(self.state, DrawingState::Idle) {
|
||||
self.state = DrawingState::Idle;
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes mouse motion (dragging) events.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `x` - Current mouse X coordinate
|
||||
/// * `y` - Current mouse Y coordinate
|
||||
///
|
||||
/// # Behavior
|
||||
/// - When drawing with Pen tool: Adds points to the freehand stroke
|
||||
/// - When drawing with other tools: Triggers redraw for live preview
|
||||
pub fn on_mouse_motion(&mut self, x: i32, y: i32) {
|
||||
if let DrawingState::Drawing { tool, points, .. } = &mut self.state {
|
||||
if *tool == Tool::Pen {
|
||||
// Add point to freehand stroke
|
||||
points.push((x, y));
|
||||
}
|
||||
// For other tools, we'll update the end point in release
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes mouse button release events.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `button` - Which mouse button was released
|
||||
/// * `x` - Mouse X coordinate at release
|
||||
/// * `y` - Mouse Y coordinate at release
|
||||
///
|
||||
/// # Behavior
|
||||
/// When left button is released during drawing:
|
||||
/// - Finalizes the shape using start position and current position
|
||||
/// - Adds the completed shape to the frame
|
||||
/// - Returns to Idle state
|
||||
pub fn on_mouse_release(&mut self, button: MouseButton, x: i32, y: i32) {
|
||||
if button != MouseButton::Left {
|
||||
return;
|
||||
}
|
||||
|
||||
if let DrawingState::Drawing {
|
||||
tool,
|
||||
start_x,
|
||||
start_y,
|
||||
points,
|
||||
} = &self.state
|
||||
{
|
||||
let shape = match tool {
|
||||
Tool::Pen => Shape::Freehand {
|
||||
points: points.clone(),
|
||||
color: self.current_color,
|
||||
thick: self.current_thickness,
|
||||
},
|
||||
Tool::Line => Shape::Line {
|
||||
x1: *start_x,
|
||||
y1: *start_y,
|
||||
x2: x,
|
||||
y2: y,
|
||||
color: self.current_color,
|
||||
thick: self.current_thickness,
|
||||
},
|
||||
Tool::Rect => {
|
||||
// Normalize rectangle to handle dragging in any direction
|
||||
let (x, w) = if x >= *start_x {
|
||||
(*start_x, x - start_x)
|
||||
} else {
|
||||
(x, start_x - x)
|
||||
};
|
||||
let (y, h) = if y >= *start_y {
|
||||
(*start_y, y - start_y)
|
||||
} else {
|
||||
(y, start_y - y)
|
||||
};
|
||||
Shape::Rect {
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
color: self.current_color,
|
||||
thick: self.current_thickness,
|
||||
}
|
||||
}
|
||||
Tool::Ellipse => {
|
||||
let (cx, cy, rx, ry) = util::ellipse_bounds(*start_x, *start_y, x, y);
|
||||
Shape::Ellipse {
|
||||
cx,
|
||||
cy,
|
||||
rx,
|
||||
ry,
|
||||
color: self.current_color,
|
||||
thick: self.current_thickness,
|
||||
}
|
||||
}
|
||||
Tool::Arrow => Shape::Arrow {
|
||||
x1: *start_x,
|
||||
y1: *start_y,
|
||||
x2: x,
|
||||
y2: y,
|
||||
color: self.current_color,
|
||||
thick: self.current_thickness,
|
||||
arrow_length: self.arrow_length,
|
||||
arrow_angle: self.arrow_angle,
|
||||
},
|
||||
};
|
||||
|
||||
self.frame.add_shape(shape);
|
||||
self.state = DrawingState::Idle;
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the shape currently being drawn for live preview.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `current_x` - Current mouse X coordinate
|
||||
/// * `current_y` - Current mouse Y coordinate
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some(Shape)` if actively drawing (for preview rendering)
|
||||
/// - `None` if idle or in text input mode
|
||||
///
|
||||
/// # Note
|
||||
/// For Pen tool (freehand), this clones the points vector. For better performance
|
||||
/// with long strokes, consider using `render_provisional_shape` directly with a
|
||||
/// borrow instead of calling this method and rendering separately.
|
||||
///
|
||||
/// This allows the backend to render a preview of the shape being drawn
|
||||
/// before the mouse button is released.
|
||||
pub fn get_provisional_shape(&self, current_x: i32, current_y: i32) -> Option<Shape> {
|
||||
if let DrawingState::Drawing {
|
||||
tool,
|
||||
start_x,
|
||||
start_y,
|
||||
points,
|
||||
} = &self.state
|
||||
{
|
||||
match tool {
|
||||
Tool::Pen => Some(Shape::Freehand {
|
||||
points: points.clone(), // TODO: Consider using Cow or separate borrow API
|
||||
color: self.current_color,
|
||||
thick: self.current_thickness,
|
||||
}),
|
||||
Tool::Line => Some(Shape::Line {
|
||||
x1: *start_x,
|
||||
y1: *start_y,
|
||||
x2: current_x,
|
||||
y2: current_y,
|
||||
color: self.current_color,
|
||||
thick: self.current_thickness,
|
||||
}),
|
||||
Tool::Rect => {
|
||||
// Normalize rectangle to handle dragging in any direction
|
||||
let (x, w) = if current_x >= *start_x {
|
||||
(*start_x, current_x - start_x)
|
||||
} else {
|
||||
(current_x, start_x - current_x)
|
||||
};
|
||||
let (y, h) = if current_y >= *start_y {
|
||||
(*start_y, current_y - start_y)
|
||||
} else {
|
||||
(current_y, start_y - current_y)
|
||||
};
|
||||
Some(Shape::Rect {
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
color: self.current_color,
|
||||
thick: self.current_thickness,
|
||||
})
|
||||
}
|
||||
Tool::Ellipse => {
|
||||
let (cx, cy, rx, ry) =
|
||||
util::ellipse_bounds(*start_x, *start_y, current_x, current_y);
|
||||
Some(Shape::Ellipse {
|
||||
cx,
|
||||
cy,
|
||||
rx,
|
||||
ry,
|
||||
color: self.current_color,
|
||||
thick: self.current_thickness,
|
||||
})
|
||||
}
|
||||
Tool::Arrow => Some(Shape::Arrow {
|
||||
x1: *start_x,
|
||||
y1: *start_y,
|
||||
x2: current_x,
|
||||
y2: current_y,
|
||||
color: self.current_color,
|
||||
thick: self.current_thickness,
|
||||
arrow_length: self.arrow_length,
|
||||
arrow_angle: self.arrow_angle,
|
||||
}),
|
||||
// No provisional shape for other tools
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the provisional shape directly to a Cairo context without cloning.
|
||||
///
|
||||
/// This is an optimized version for freehand drawing that avoids cloning
|
||||
/// the points vector on every render, preventing quadratic performance.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ctx` - Cairo context to render to
|
||||
/// * `current_x` - Current mouse X coordinate
|
||||
/// * `current_y` - Current mouse Y coordinate
|
||||
///
|
||||
/// # Returns
|
||||
/// `true` if a provisional shape was rendered, `false` otherwise
|
||||
pub fn render_provisional_shape(
|
||||
&self,
|
||||
ctx: &cairo::Context,
|
||||
current_x: i32,
|
||||
current_y: i32,
|
||||
) -> bool {
|
||||
if let DrawingState::Drawing {
|
||||
tool,
|
||||
start_x: _,
|
||||
start_y: _,
|
||||
points,
|
||||
} = &self.state
|
||||
{
|
||||
match tool {
|
||||
Tool::Pen => {
|
||||
// Render freehand without cloning - just borrow the points
|
||||
crate::draw::render_freehand_borrowed(
|
||||
ctx,
|
||||
points,
|
||||
self.current_color,
|
||||
self.current_thickness,
|
||||
);
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
// For other tools, use the normal path (no clone needed)
|
||||
if let Some(shape) = self.get_provisional_shape(current_x, current_y) {
|
||||
crate::draw::render_shape(ctx, &shape);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//! Drawing tool selection.
|
||||
|
||||
/// Drawing tool selection.
|
||||
///
|
||||
/// The active tool determines what shape is created when the user drags the mouse.
|
||||
/// Tools are selected by holding modifier keys (Shift, Ctrl, Tab) while dragging.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Tool {
|
||||
/// Freehand drawing - follows mouse path (default, no modifiers)
|
||||
Pen,
|
||||
/// Straight line - between start and end points (Shift)
|
||||
Line,
|
||||
/// Rectangle outline - from corner to corner (Ctrl)
|
||||
Rect,
|
||||
/// Ellipse/circle outline - from center outward (Tab)
|
||||
Ellipse,
|
||||
/// Arrow with directional head (Ctrl+Shift)
|
||||
Arrow,
|
||||
// Note: Text mode uses DrawingState::TextInput instead of Tool::Text
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
use clap::{ArgAction, Parser};
|
||||
|
||||
mod backend;
|
||||
mod config;
|
||||
mod daemon;
|
||||
mod draw;
|
||||
mod input;
|
||||
mod ui;
|
||||
mod util;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "hyprmarker")]
|
||||
#[command(version, about = "Screen annotation tool for Wayland compositors")]
|
||||
struct Cli {
|
||||
/// Run as daemon (background, toggle with Super+D)
|
||||
#[arg(long, short = 'd', action = ArgAction::SetTrue)]
|
||||
daemon: bool,
|
||||
|
||||
/// Start active (show overlay immediately, one-shot mode)
|
||||
#[arg(long, short = 'a', action = ArgAction::SetTrue)]
|
||||
active: bool,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Check for Wayland environment
|
||||
if std::env::var("WAYLAND_DISPLAY").is_err() && (cli.daemon || cli.active) {
|
||||
log::error!("WAYLAND_DISPLAY not set - this application requires Wayland.");
|
||||
log::error!("Please run on a Wayland compositor (Hyprland, Sway, etc.).");
|
||||
return Err(anyhow::anyhow!("Wayland environment required"));
|
||||
}
|
||||
|
||||
if cli.daemon {
|
||||
// Daemon mode: background service with toggle activation
|
||||
log::info!("Starting in daemon mode");
|
||||
let mut daemon = daemon::Daemon::new();
|
||||
daemon.run()?;
|
||||
} else if cli.active {
|
||||
// One-shot mode: show overlay immediately and exit when done
|
||||
log::info!("Starting Wayland overlay...");
|
||||
log::info!("Starting annotation overlay...");
|
||||
log::info!("Controls:");
|
||||
log::info!(" - Freehand: Just drag");
|
||||
log::info!(" - Line: Hold Shift + drag");
|
||||
log::info!(" - Rectangle: Hold Ctrl + drag");
|
||||
log::info!(" - Ellipse: Hold Tab + drag");
|
||||
log::info!(" - Arrow: Hold Ctrl+Shift + drag");
|
||||
log::info!(" - Text: Press T, click to position, type, press Enter");
|
||||
log::info!(
|
||||
" - Colors: R (red), G (green), B (blue), Y (yellow), O (orange), P (pink), W (white), K (black)"
|
||||
);
|
||||
log::info!(" - Undo: Ctrl+Z");
|
||||
log::info!(" - Clear all: E");
|
||||
log::info!(" - Increase thickness: + or = or scroll down");
|
||||
log::info!(" - Decrease thickness: - or _ or scroll up");
|
||||
log::info!(" - Help: F10");
|
||||
log::info!(" - Exit: Escape");
|
||||
log::info!("");
|
||||
|
||||
// Run Wayland backend
|
||||
backend::run_wayland()?;
|
||||
|
||||
log::info!("Annotation overlay closed.");
|
||||
} else {
|
||||
// No flags: show usage
|
||||
println!("hyprmarker: Screen annotation tool for Wayland compositors");
|
||||
println!();
|
||||
println!("Usage:");
|
||||
println!(" hyprmarker --daemon Run as background daemon (toggle with Super+D)");
|
||||
println!(" hyprmarker --active Show overlay immediately (one-shot mode)");
|
||||
println!(" hyprmarker --help Show help");
|
||||
println!();
|
||||
println!("Daemon mode (recommended):");
|
||||
println!(" 1. Run: hyprmarker --daemon");
|
||||
println!(" 2. Add to Hyprland config:");
|
||||
println!(" exec-once = hyprmarker --daemon");
|
||||
println!(" bind = SUPER, D, exec, pkill -SIGUSR1 hyprmarker");
|
||||
println!(" 3. Press Super+D to toggle overlay on/off");
|
||||
println!();
|
||||
println!("Requirements:");
|
||||
println!(" - Wayland compositor (Hyprland, Sway, etc.)");
|
||||
println!(" - wlr-layer-shell protocol support");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/// UI rendering: status bar, help overlay, visual indicators
|
||||
use crate::config::StatusPosition;
|
||||
use crate::input::{DrawingState, InputState, Tool};
|
||||
|
||||
// ============================================================================
|
||||
// UI Layout Constants (not configurable)
|
||||
// ============================================================================
|
||||
|
||||
/// Background rectangle X offset
|
||||
const STATUS_BG_OFFSET_X: f64 = 5.0;
|
||||
/// Background rectangle Y offset
|
||||
const STATUS_BG_OFFSET_Y: f64 = 3.0;
|
||||
/// Background rectangle width padding
|
||||
const STATUS_BG_WIDTH_PAD: f64 = 10.0;
|
||||
/// Background rectangle height padding
|
||||
const STATUS_BG_HEIGHT_PAD: f64 = 8.0;
|
||||
/// Color indicator dot X offset
|
||||
const STATUS_DOT_OFFSET_X: f64 = 3.0;
|
||||
|
||||
/// Fallback character width for monospace font estimation
|
||||
const HELP_CHAR_WIDTH_ESTIMATE: f64 = 9.0;
|
||||
|
||||
/// Render status bar showing current color, thickness, and tool
|
||||
pub fn render_status_bar(
|
||||
ctx: &cairo::Context,
|
||||
input_state: &InputState,
|
||||
position: StatusPosition,
|
||||
style: &crate::config::StatusBarStyle,
|
||||
screen_width: u32,
|
||||
screen_height: u32,
|
||||
) {
|
||||
let color = &input_state.current_color;
|
||||
let thickness = input_state.current_thickness;
|
||||
let tool = input_state.modifiers.current_tool();
|
||||
|
||||
// Determine tool name
|
||||
let tool_name = match &input_state.state {
|
||||
DrawingState::TextInput { .. } => "Text",
|
||||
DrawingState::Drawing { tool, .. } => match tool {
|
||||
Tool::Pen => "Pen",
|
||||
Tool::Line => "Line",
|
||||
Tool::Rect => "Rectangle",
|
||||
Tool::Ellipse => "Circle",
|
||||
Tool::Arrow => "Arrow",
|
||||
},
|
||||
DrawingState::Idle => match tool {
|
||||
Tool::Pen => "Pen",
|
||||
Tool::Line => "Line",
|
||||
Tool::Rect => "Rectangle",
|
||||
Tool::Ellipse => "Circle",
|
||||
Tool::Arrow => "Arrow",
|
||||
},
|
||||
};
|
||||
|
||||
// Determine color name
|
||||
let color_name = crate::util::color_to_name(color);
|
||||
|
||||
// Build status text
|
||||
let status_text = format!(
|
||||
"[{}] [{}px] [{}] F10=Help",
|
||||
color_name, thickness as i32, tool_name
|
||||
);
|
||||
|
||||
// Set font
|
||||
log::debug!("Status bar font_size from config: {}", style.font_size);
|
||||
ctx.set_font_size(style.font_size);
|
||||
ctx.select_font_face("Sans", cairo::FontSlant::Normal, cairo::FontWeight::Bold);
|
||||
|
||||
// Measure text
|
||||
let extents = match ctx.text_extents(&status_text) {
|
||||
Ok(ext) => ext,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to measure status bar text: {}, skipping status bar",
|
||||
e
|
||||
);
|
||||
return; // Gracefully skip rendering if font measurement fails
|
||||
}
|
||||
};
|
||||
let text_width = extents.width();
|
||||
let text_height = extents.height();
|
||||
|
||||
// Calculate position using configurable padding
|
||||
let padding = style.padding;
|
||||
let (x, y) = match position {
|
||||
StatusPosition::TopLeft => (padding, padding + text_height),
|
||||
StatusPosition::TopRight => (
|
||||
screen_width as f64 - text_width - padding,
|
||||
padding + text_height,
|
||||
),
|
||||
StatusPosition::BottomLeft => (padding, screen_height as f64 - padding),
|
||||
StatusPosition::BottomRight => (
|
||||
screen_width as f64 - text_width - padding,
|
||||
screen_height as f64 - padding,
|
||||
),
|
||||
};
|
||||
|
||||
// Draw semi-transparent background
|
||||
let [r, g, b, a] = style.bg_color;
|
||||
ctx.set_source_rgba(r, g, b, a);
|
||||
ctx.rectangle(
|
||||
x - STATUS_BG_OFFSET_X,
|
||||
y - text_height - STATUS_BG_OFFSET_Y,
|
||||
text_width + STATUS_BG_WIDTH_PAD,
|
||||
text_height + STATUS_BG_HEIGHT_PAD,
|
||||
);
|
||||
let _ = ctx.fill();
|
||||
|
||||
// Draw color indicator dot
|
||||
let dot_x = x + STATUS_DOT_OFFSET_X;
|
||||
let dot_y = y - text_height / 2.0;
|
||||
ctx.set_source_rgba(color.r, color.g, color.b, color.a);
|
||||
ctx.arc(
|
||||
dot_x,
|
||||
dot_y,
|
||||
style.dot_radius,
|
||||
0.0,
|
||||
2.0 * std::f64::consts::PI,
|
||||
);
|
||||
let _ = ctx.fill();
|
||||
|
||||
// Draw text
|
||||
let [r, g, b, a] = style.text_color;
|
||||
ctx.set_source_rgba(r, g, b, a);
|
||||
ctx.move_to(x, y);
|
||||
let _ = ctx.show_text(&status_text);
|
||||
}
|
||||
|
||||
/// Render help overlay showing all keybindings
|
||||
pub fn render_help_overlay(
|
||||
ctx: &cairo::Context,
|
||||
style: &crate::config::HelpOverlayStyle,
|
||||
screen_width: u32,
|
||||
screen_height: u32,
|
||||
) {
|
||||
let help_text = vec![
|
||||
"━━━━━━━━━━━ CONTROLS ━━━━━━━━━━━",
|
||||
"",
|
||||
"Drawing Tools:",
|
||||
" Drag = Freehand pen",
|
||||
" Shift + Drag = Straight line",
|
||||
" Ctrl + Drag = Rectangle",
|
||||
" Tab + Drag = Circle",
|
||||
" Ctrl+Shift + Drag = Arrow",
|
||||
" T = Text mode",
|
||||
"",
|
||||
"Text Mode:",
|
||||
" Type = Enter text",
|
||||
" Shift + Enter = New line",
|
||||
" Enter = Finish text",
|
||||
" Backspace = Delete char",
|
||||
"",
|
||||
"Colors:",
|
||||
" R = Red G = Green B = Blue",
|
||||
" Y = Yellow O = Orange P = Pink",
|
||||
" W = White K = Black",
|
||||
"",
|
||||
"Pen Thickness:",
|
||||
" + or = = Increase",
|
||||
" - or _ = Decrease",
|
||||
" Scroll Up = Increase",
|
||||
" Scroll Down = Decrease",
|
||||
"",
|
||||
"Actions:",
|
||||
" E = Clear all",
|
||||
" Ctrl+Z = Undo last",
|
||||
" Escape or Ctrl+Q = Exit",
|
||||
"",
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
||||
"",
|
||||
"Press F10 to hide help",
|
||||
];
|
||||
|
||||
// Set font
|
||||
ctx.set_font_size(style.font_size);
|
||||
ctx.select_font_face(
|
||||
"Monospace",
|
||||
cairo::FontSlant::Normal,
|
||||
cairo::FontWeight::Normal,
|
||||
);
|
||||
|
||||
// Find longest line for width
|
||||
let mut max_width: f64 = 0.0;
|
||||
for line in &help_text {
|
||||
let extents = match ctx.text_extents(line) {
|
||||
Ok(ext) => ext,
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to measure help text line '{}': {}, using fallback width",
|
||||
line,
|
||||
e
|
||||
);
|
||||
// Use a fallback width estimate based on character count
|
||||
let fallback_width = line.len() as f64 * HELP_CHAR_WIDTH_ESTIMATE;
|
||||
max_width = max_width.max(fallback_width);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if extents.width() > max_width {
|
||||
max_width = extents.width();
|
||||
}
|
||||
}
|
||||
|
||||
let box_width = max_width + style.padding * 2.0;
|
||||
let box_height = (help_text.len() as f64) * style.line_height + style.padding * 2.0;
|
||||
|
||||
// Center the box
|
||||
let box_x = (screen_width as f64 - box_width) / 2.0;
|
||||
let box_y = (screen_height as f64 - box_height) / 2.0;
|
||||
|
||||
// Draw semi-transparent background
|
||||
let [r, g, b, a] = style.bg_color;
|
||||
ctx.set_source_rgba(r, g, b, a);
|
||||
ctx.rectangle(box_x, box_y, box_width, box_height);
|
||||
let _ = ctx.fill();
|
||||
|
||||
// Draw border
|
||||
let [r, g, b, a] = style.border_color;
|
||||
ctx.set_source_rgba(r, g, b, a);
|
||||
ctx.set_line_width(style.border_width);
|
||||
ctx.rectangle(box_x, box_y, box_width, box_height);
|
||||
let _ = ctx.stroke();
|
||||
|
||||
// Draw text
|
||||
let [r, g, b, a] = style.text_color;
|
||||
ctx.set_source_rgba(r, g, b, a);
|
||||
for (i, line) in help_text.iter().enumerate() {
|
||||
let text_x = box_x + style.padding;
|
||||
let text_y = box_y + style.padding + (i as f64 + 1.0) * style.line_height;
|
||||
|
||||
ctx.move_to(text_x, text_y);
|
||||
let _ = ctx.show_text(line);
|
||||
}
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
//! Utility functions for colors, geometry, and arrowhead calculations.
|
||||
//!
|
||||
//! This module provides:
|
||||
//! - Key-to-color mapping for keyboard shortcuts (constants moved to draw::color)
|
||||
//! - Arrowhead geometry calculations
|
||||
//! - Ellipse bounding box calculations
|
||||
|
||||
use crate::draw::{Color, color::*};
|
||||
|
||||
// ============================================================================
|
||||
// Arrowhead Geometry
|
||||
// ============================================================================
|
||||
|
||||
/// Calculates arrowhead points with custom length and angle.
|
||||
///
|
||||
/// Creates a V-shaped arrowhead at position (x1, y1) pointing in the direction
|
||||
/// from (x2, y2) to (x1, y1). The arrowhead length is automatically capped at
|
||||
/// 30% of the line length to prevent weird-looking arrows on short lines.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `x1` - Arrowhead tip X coordinate
|
||||
/// * `y1` - Arrowhead tip Y coordinate
|
||||
/// * `x2` - Arrow tail X coordinate
|
||||
/// * `y2` - Arrow tail Y coordinate
|
||||
/// * `length` - Desired arrowhead length in pixels (will be capped at 30% of line length)
|
||||
/// * `angle_degrees` - Arrowhead angle in degrees (angle between arrowhead lines and main line)
|
||||
///
|
||||
/// # Returns
|
||||
/// Array of two points `[(left_x, left_y), (right_x, right_y)]` for the arrowhead lines.
|
||||
/// If the line is too short (< 1 pixel), both points equal (x1, y1).
|
||||
pub fn calculate_arrowhead_custom(
|
||||
x1: i32,
|
||||
y1: i32,
|
||||
x2: i32,
|
||||
y2: i32,
|
||||
length: f64,
|
||||
angle_degrees: f64,
|
||||
) -> [(f64, f64); 2] {
|
||||
let dx = (x1 - x2) as f64; // Direction from END to START (reversed)
|
||||
let dy = (y1 - y2) as f64;
|
||||
let line_length = (dx * dx + dy * dy).sqrt();
|
||||
|
||||
if line_length < 1.0 {
|
||||
// Line too short for arrowhead
|
||||
return [(x1 as f64, y1 as f64), (x1 as f64, y1 as f64)];
|
||||
}
|
||||
|
||||
// Normalize direction vector (pointing from end to start)
|
||||
let ux = dx / line_length;
|
||||
let uy = dy / line_length;
|
||||
|
||||
// Arrowhead length (max 30% of line length to avoid weird-looking arrows on short lines)
|
||||
let arrow_length = length.min(line_length * 0.3);
|
||||
|
||||
// Convert angle to radians
|
||||
let angle = angle_degrees.to_radians();
|
||||
let cos_a = angle.cos();
|
||||
let sin_a = angle.sin();
|
||||
|
||||
// Left side of arrowhead (at START point)
|
||||
let left_x = x1 as f64 - arrow_length * (ux * cos_a - uy * sin_a);
|
||||
let left_y = y1 as f64 - arrow_length * (uy * cos_a + ux * sin_a);
|
||||
|
||||
// Right side of arrowhead (at START point)
|
||||
let right_x = x1 as f64 - arrow_length * (ux * cos_a + uy * sin_a);
|
||||
let right_y = y1 as f64 - arrow_length * (uy * cos_a - ux * sin_a);
|
||||
|
||||
[(left_x, left_y), (right_x, right_y)]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Color Mapping
|
||||
// ============================================================================
|
||||
|
||||
/// Maps keyboard characters to colors for quick color switching.
|
||||
///
|
||||
/// # Supported Keys (case-insensitive)
|
||||
/// - `R` → Red
|
||||
/// - `G` → Green
|
||||
/// - `B` → Blue
|
||||
/// - `Y` → Yellow
|
||||
/// - `O` → Orange
|
||||
/// - `P` → Pink
|
||||
/// - `W` → White
|
||||
/// - `K` → Black (K for blacK, since B is blue)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `c` - Character key pressed by user
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some(Color)` if the character maps to a predefined color
|
||||
/// - `None` if the character doesn't correspond to any color
|
||||
pub fn key_to_color(c: char) -> Option<Color> {
|
||||
match c.to_ascii_uppercase() {
|
||||
'R' => Some(RED),
|
||||
'G' => Some(GREEN),
|
||||
'B' => Some(BLUE),
|
||||
'Y' => Some(YELLOW),
|
||||
'O' => Some(ORANGE),
|
||||
'P' => Some(PINK),
|
||||
'W' => Some(WHITE),
|
||||
'K' => Some(BLACK), // K for blacK
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps color name strings to Color values.
|
||||
///
|
||||
/// Used by the configuration system to parse color names from the config file.
|
||||
///
|
||||
/// # Supported Names (case-insensitive)
|
||||
/// - "red", "green", "blue", "yellow", "orange", "pink", "white", "black"
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `name` - Color name string
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some(Color)` if the name matches a predefined color
|
||||
/// - `None` if the name is not recognized
|
||||
pub fn name_to_color(name: &str) -> Option<Color> {
|
||||
match name.to_lowercase().as_str() {
|
||||
"red" => Some(RED),
|
||||
"green" => Some(GREEN),
|
||||
"blue" => Some(BLUE),
|
||||
"yellow" => Some(YELLOW),
|
||||
"orange" => Some(ORANGE),
|
||||
"pink" => Some(PINK),
|
||||
"white" => Some(WHITE),
|
||||
"black" => Some(BLACK),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a Color value to its human-readable name.
|
||||
///
|
||||
/// Uses approximate matching (threshold-based) to identify colors.
|
||||
/// Used by the UI status bar to display the current color name.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `color` - The color to identify
|
||||
///
|
||||
/// # Returns
|
||||
/// A static string with the color name, or "Custom" if the color doesn't
|
||||
/// match any predefined color.
|
||||
pub fn color_to_name(color: &Color) -> &'static str {
|
||||
// Match colors approximately with 0.1 tolerance
|
||||
if color.r > 0.9 && color.g < 0.1 && color.b < 0.1 {
|
||||
"Red"
|
||||
} else if color.r < 0.1 && color.g > 0.9 && color.b < 0.1 {
|
||||
"Green"
|
||||
} else if color.r < 0.1 && color.g < 0.1 && color.b > 0.9 {
|
||||
"Blue"
|
||||
} else if color.r > 0.9 && color.g > 0.9 && color.b < 0.1 {
|
||||
"Yellow"
|
||||
} else if color.r > 0.9 && (0.4..=0.6).contains(&color.g) && color.b < 0.1 {
|
||||
"Orange"
|
||||
} else if color.r > 0.9 && color.g < 0.1 && color.b > 0.9 {
|
||||
"Pink"
|
||||
} else if color.r > 0.9 && color.g > 0.9 && color.b > 0.9 {
|
||||
"White"
|
||||
} else if color.r < 0.1 && color.g < 0.1 && color.b < 0.1 {
|
||||
"Black"
|
||||
} else {
|
||||
"Custom"
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Geometry Utilities
|
||||
// ============================================================================
|
||||
|
||||
/// Clamps a value to a specified range.
|
||||
///
|
||||
/// Kept for future use (e.g., dirty region optimization, bounds checking).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `val` - Value to clamp
|
||||
/// * `min` - Minimum allowed value
|
||||
/// * `max` - Maximum allowed value
|
||||
///
|
||||
/// # Returns
|
||||
/// The clamped value: `min` if `val < min`, `max` if `val > max`, otherwise `val`.
|
||||
#[allow(dead_code)]
|
||||
pub fn clamp(val: i32, min: i32, max: i32) -> i32 {
|
||||
if val < min {
|
||||
min
|
||||
} else if val > max {
|
||||
max
|
||||
} else {
|
||||
val
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates ellipse parameters from two corner points.
|
||||
///
|
||||
/// Converts a drag rectangle (from corner to corner) into ellipse parameters
|
||||
/// (center point and radii) suitable for Cairo's ellipse rendering.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `x1` - First corner X coordinate
|
||||
/// * `y1` - First corner Y coordinate
|
||||
/// * `x2` - Opposite corner X coordinate
|
||||
/// * `y2` - Opposite corner Y coordinate
|
||||
///
|
||||
/// # Returns
|
||||
/// Tuple `(cx, cy, rx, ry)` where:
|
||||
/// - `cx`, `cy` = center point coordinates
|
||||
/// - `rx` = horizontal radius (half width)
|
||||
/// - `ry` = vertical radius (half height)
|
||||
pub fn ellipse_bounds(x1: i32, y1: i32, x2: i32, y2: i32) -> (i32, i32, i32, i32) {
|
||||
let cx = (x1 + x2) / 2;
|
||||
let cy = (y1 + y2) / 2;
|
||||
let rx = ((x2 - x1).abs()) / 2;
|
||||
let ry = ((y2 - y1).abs()) / 2;
|
||||
(cx, cy, rx, ry)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Tools
|
||||
|
||||
Helper scripts for development and installation.
|
||||
|
||||
## Scripts
|
||||
|
||||
- **install.sh** - Installation script for hyprmarker
|
||||
- Builds and installs binary to `~/.local/bin`
|
||||
- Sets up config directory
|
||||
- Optionally configures systemd or Hyprland autostart
|
||||
- Usage: `./tools/install.sh`
|
||||
|
||||
- **run.sh** - Quick run script for development
|
||||
- Runs hyprmarker in daemon mode with debug logging
|
||||
- Usage: `./tools/run.sh`
|
||||
|
||||
- **reload-daemon.sh** - Reload running daemon
|
||||
- Kills and restarts the daemon (picks up config changes)
|
||||
- Usage: `./tools/reload-daemon.sh`
|
||||
|
||||
All scripts work from any location in the project.
|
||||
Executable
+215
@@ -0,0 +1,215 @@
|
||||
#!/bin/bash
|
||||
# Installation script for hyprmarker
|
||||
|
||||
set -e
|
||||
|
||||
# Get the directory where the script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# Get the project root (parent of tools/)
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
INSTALL_DIR="$HOME/.local/bin"
|
||||
BINARY_NAME="hyprmarker"
|
||||
CONFIG_DIR="$HOME/.config/hyprmarker"
|
||||
HYPR_CONFIG="$HOME/.config/hypr/hyprland.conf"
|
||||
|
||||
echo "================================"
|
||||
echo " Hyprmarker Installation"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
# Check if binary exists
|
||||
if [ ! -f "$PROJECT_ROOT/target/release/$BINARY_NAME" ]; then
|
||||
echo "Error: Binary not found at $PROJECT_ROOT/target/release/$BINARY_NAME"
|
||||
echo "Please run 'cargo build --release' from the project root first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create install directory if needed
|
||||
echo "Creating installation directory: $INSTALL_DIR"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Copy binary
|
||||
echo "Installing binary to $INSTALL_DIR/$BINARY_NAME"
|
||||
cp "$PROJECT_ROOT/target/release/$BINARY_NAME" "$INSTALL_DIR/"
|
||||
chmod +x "$INSTALL_DIR/$BINARY_NAME"
|
||||
|
||||
# Check if install directory is in PATH
|
||||
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
||||
echo ""
|
||||
echo "⚠️ Warning: $INSTALL_DIR is not in your PATH"
|
||||
echo " Add this line to your ~/.bashrc or ~/.zshrc:"
|
||||
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Create config directory
|
||||
echo "Creating config directory: $CONFIG_DIR"
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
# Copy example config if config doesn't exist
|
||||
if [ ! -f "$CONFIG_DIR/config.toml" ]; then
|
||||
if [ -f "$PROJECT_ROOT/config.example.toml" ]; then
|
||||
echo "Installing example config to $CONFIG_DIR/config.toml"
|
||||
cp "$PROJECT_ROOT/config.example.toml" "$CONFIG_DIR/config.toml"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Installation complete!"
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " Setup Instructions"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "1. Test the installation:"
|
||||
echo " $BINARY_NAME --help"
|
||||
echo ""
|
||||
echo "2. Run in daemon mode (recommended):"
|
||||
echo " $BINARY_NAME --daemon &"
|
||||
echo ""
|
||||
echo "3. For Hyprland integration, add to $HYPR_CONFIG:"
|
||||
echo ""
|
||||
echo " # Autostart hyprmarker daemon"
|
||||
echo " exec-once = $INSTALL_DIR/$BINARY_NAME --daemon"
|
||||
echo ""
|
||||
echo " # Toggle overlay with Super+D"
|
||||
echo " bind = SUPER, D, exec, pkill -SIGUSR1 $BINARY_NAME"
|
||||
echo ""
|
||||
|
||||
# Setup autostart options
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " Autostart Setup"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "Choose autostart method:"
|
||||
echo " 1) Systemd user service (recommended - runs on login)"
|
||||
echo " 2) Hyprland exec-once (Hyprland only)"
|
||||
echo " 3) Skip autostart setup"
|
||||
echo ""
|
||||
read -p "Enter choice [1-3]: " -n 1 -r
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
case $REPLY in
|
||||
1)
|
||||
# Systemd user service
|
||||
SYSTEMD_DIR="$HOME/.config/systemd/user"
|
||||
SERVICE_FILE="$SYSTEMD_DIR/hyprmarker.service"
|
||||
|
||||
echo "Setting up systemd user service..."
|
||||
mkdir -p "$SYSTEMD_DIR"
|
||||
|
||||
if [ -f "$PROJECT_ROOT/packaging/hyprmarker.service" ]; then
|
||||
cp "$PROJECT_ROOT/packaging/hyprmarker.service" "$SERVICE_FILE"
|
||||
echo "✅ Service file installed to $SERVICE_FILE"
|
||||
|
||||
# Enable and start the service
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable hyprmarker.service
|
||||
systemctl --user start hyprmarker.service
|
||||
|
||||
echo "✅ Service enabled and started"
|
||||
echo ""
|
||||
echo "Service status:"
|
||||
systemctl --user status hyprmarker.service --no-pager -l
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " Start: systemctl --user start hyprmarker"
|
||||
echo " Stop: systemctl --user stop hyprmarker"
|
||||
echo " Status: systemctl --user status hyprmarker"
|
||||
echo " Logs: journalctl --user -u hyprmarker -f"
|
||||
else
|
||||
echo "⚠️ Service file not found. Please run installer from repository root."
|
||||
fi
|
||||
|
||||
# Still add Hyprland keybind if config exists
|
||||
if [ -f "$HYPR_CONFIG" ]; then
|
||||
echo ""
|
||||
read -p "Add Super+D keybind to Hyprland config? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
if grep -q "pkill -SIGUSR1 $BINARY_NAME" "$HYPR_CONFIG"; then
|
||||
echo "⚠️ Keybind already configured"
|
||||
else
|
||||
echo "" >> "$HYPR_CONFIG"
|
||||
echo "# hyprmarker toggle keybind" >> "$HYPR_CONFIG"
|
||||
echo "bind = SUPER, D, exec, pkill -SIGUSR1 $BINARY_NAME" >> "$HYPR_CONFIG"
|
||||
echo "✅ Keybind added to Hyprland config"
|
||||
echo ""
|
||||
echo "Reload Hyprland: hyprctl reload"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
2)
|
||||
# Hyprland exec-once
|
||||
if [ -f "$HYPR_CONFIG" ]; then
|
||||
echo "Adding to Hyprland config..."
|
||||
if grep -q "hyprmarker --daemon" "$HYPR_CONFIG"; then
|
||||
echo "⚠️ hyprmarker already configured in Hyprland config"
|
||||
else
|
||||
echo "" >> "$HYPR_CONFIG"
|
||||
echo "# hyprmarker - Screen annotation tool" >> "$HYPR_CONFIG"
|
||||
echo "exec-once = $INSTALL_DIR/$BINARY_NAME --daemon" >> "$HYPR_CONFIG"
|
||||
echo "bind = SUPER, D, exec, pkill -SIGUSR1 $BINARY_NAME" >> "$HYPR_CONFIG"
|
||||
echo "✅ Added to Hyprland config"
|
||||
fi
|
||||
echo ""
|
||||
echo "Reload Hyprland to activate:"
|
||||
echo " hyprctl reload"
|
||||
else
|
||||
echo "⚠️ Hyprland config not found at $HYPR_CONFIG"
|
||||
echo "Add these lines manually to your Hyprland config:"
|
||||
echo " exec-once = $INSTALL_DIR/$BINARY_NAME --daemon"
|
||||
echo " bind = SUPER, D, exec, pkill -SIGUSR1 $BINARY_NAME"
|
||||
fi
|
||||
;;
|
||||
|
||||
3)
|
||||
echo "Skipping autostart setup."
|
||||
echo "To start manually: $BINARY_NAME --daemon &"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Invalid choice. Skipping autostart setup."
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " Usage"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "Daemon mode (background, toggle with Super+D):"
|
||||
echo " $BINARY_NAME --daemon"
|
||||
echo ""
|
||||
echo "One-shot mode (overlay shows immediately):"
|
||||
echo " $BINARY_NAME --active"
|
||||
echo ""
|
||||
echo "Controls:"
|
||||
echo " - Freehand: Drag mouse"
|
||||
echo " - Line: Shift + drag"
|
||||
echo " - Rectangle: Ctrl + drag"
|
||||
echo " - Ellipse: Tab + drag"
|
||||
echo " - Arrow: Ctrl+Shift + drag"
|
||||
echo " - Text: Press T"
|
||||
echo " - Colors: R/G/B/Y/O/P/W/K"
|
||||
echo " - Thickness: +/- or scroll wheel"
|
||||
echo " - Help: F10"
|
||||
echo " - Undo: Ctrl+Z"
|
||||
echo " - Clear: E"
|
||||
echo " - Exit: Escape"
|
||||
echo ""
|
||||
echo "Configuration:"
|
||||
echo " Edit: $CONFIG_DIR/config.toml"
|
||||
echo ""
|
||||
echo "Documentation:"
|
||||
echo " docs/SETUP.md"
|
||||
echo " docs/CONFIG.md"
|
||||
echo " docs/QUICKSTART.md"
|
||||
echo ""
|
||||
echo "Happy annotating! 🎨"
|
||||
echo ""
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# Reload hyprmarker daemon
|
||||
# This will kill the old daemon and start a new one with updated config
|
||||
|
||||
echo "Stopping hyprmarker daemon..."
|
||||
pkill hyprmarker
|
||||
|
||||
# Wait a moment for clean shutdown
|
||||
sleep 0.5
|
||||
|
||||
echo "Starting hyprmarker daemon..."
|
||||
hyprmarker --daemon &
|
||||
|
||||
# Wait to verify it started
|
||||
sleep 0.5
|
||||
|
||||
if pgrep -x hyprmarker > /dev/null; then
|
||||
echo "✓ Daemon restarted successfully (PID: $(pgrep -x hyprmarker))"
|
||||
echo "Press Super+D to toggle overlay"
|
||||
else
|
||||
echo "✗ Failed to start daemon"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+174
@@ -0,0 +1,174 @@
|
||||
#!/bin/bash
|
||||
# Automated AUR package update script
|
||||
# Updates PKGBUILD version, builds, tests, and pushes to AUR
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
AUR_DIR="$HOME/aur-packages"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo " HYPRMARKER - AUR UPDATE AUTOMATION"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
# Check we're in the right directory
|
||||
if [ ! -f "$PROJECT_ROOT/Cargo.toml" ]; then
|
||||
echo -e "${RED}❌ Error: Not in hyprmarker project root${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get version from Cargo.toml
|
||||
CARGO_VERSION=$(grep '^version = ' "$PROJECT_ROOT/Cargo.toml" | head -1 | sed 's/version = "\(.*\)"/\1/')
|
||||
echo -e "${GREEN}📦 Current version in Cargo.toml: $CARGO_VERSION${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if version tag exists on GitHub
|
||||
cd "$PROJECT_ROOT"
|
||||
if ! git tag | grep -q "^v$CARGO_VERSION\$"; then
|
||||
echo -e "${YELLOW}⚠️ Git tag v$CARGO_VERSION does not exist${NC}"
|
||||
echo ""
|
||||
read -p "Create and push tag v$CARGO_VERSION? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
git tag -a "v$CARGO_VERSION" -m "Release v$CARGO_VERSION"
|
||||
git push origin "v$CARGO_VERSION"
|
||||
echo -e "${GREEN}✅ Tag created and pushed${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Aborted - tag required for AUR${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check AUR directory exists
|
||||
if [ ! -d "$AUR_DIR" ]; then
|
||||
echo -e "${RED}❌ Error: AUR directory not found: $AUR_DIR${NC}"
|
||||
echo ""
|
||||
echo "Initialize it first:"
|
||||
echo " mkdir -p $AUR_DIR"
|
||||
echo " cd $AUR_DIR"
|
||||
echo " git init"
|
||||
echo " git remote add origin ssh://aur@aur.archlinux.org/hyprmarker.git"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "STEP 1: Update PKGBUILD"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
# Copy PKGBUILD.aur to AUR directory
|
||||
if [ ! -f "$PROJECT_ROOT/packaging/PKGBUILD.aur" ]; then
|
||||
echo -e "${RED}❌ Error: PKGBUILD.aur not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$PROJECT_ROOT/packaging/PKGBUILD.aur" "$AUR_DIR/PKGBUILD"
|
||||
echo -e "${GREEN}✅ Copied PKGBUILD.aur to $AUR_DIR/PKGBUILD${NC}"
|
||||
|
||||
# Update version in PKGBUILD
|
||||
cd "$AUR_DIR"
|
||||
sed -i "s/^pkgver=.*/pkgver=$CARGO_VERSION/" PKGBUILD
|
||||
sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD
|
||||
|
||||
echo -e "${GREEN}✅ Updated PKGBUILD: pkgver=$CARGO_VERSION, pkgrel=1${NC}"
|
||||
echo ""
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "STEP 2: Generate .SRCINFO"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
makepkg --printsrcinfo > .SRCINFO
|
||||
echo -e "${GREEN}✅ Generated .SRCINFO${NC}"
|
||||
echo ""
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "STEP 3: Test build locally"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
read -p "Test build locally? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Building package..."
|
||||
if makepkg -f; then
|
||||
echo -e "${GREEN}✅ Build successful${NC}"
|
||||
echo ""
|
||||
read -p "Install locally to test? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
makepkg -i
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}❌ Build failed - fix errors before pushing to AUR${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Skipping local build test${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "STEP 4: Commit and push to AUR"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
# Show git status
|
||||
echo "Files to be committed:"
|
||||
git status --short
|
||||
echo ""
|
||||
|
||||
read -p "Push to AUR? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
# Check if git is initialized
|
||||
if [ ! -d "$AUR_DIR/.git" ]; then
|
||||
echo "Initializing git repository..."
|
||||
git init
|
||||
git remote add origin ssh://aur@aur.archlinux.org/hyprmarker.git
|
||||
fi
|
||||
|
||||
# Add and commit
|
||||
git add PKGBUILD .SRCINFO .gitignore 2>/dev/null || git add PKGBUILD .SRCINFO
|
||||
git commit -m "Update to v$CARGO_VERSION"
|
||||
|
||||
# Push
|
||||
if git push origin master 2>/dev/null; then
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Successfully pushed to AUR!${NC}"
|
||||
else
|
||||
# If master doesn't exist, try pushing with -u
|
||||
git push -u origin master
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Successfully pushed to AUR!${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo -e "${GREEN}✅ AUR PACKAGE UPDATED SUCCESSFULLY${NC}"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "Package URL: https://aur.archlinux.org/packages/hyprmarker"
|
||||
echo "Version: $CARGO_VERSION"
|
||||
echo ""
|
||||
echo "Users can update with:"
|
||||
echo " yay -Syu hyprmarker"
|
||||
echo " paru -Syu hyprmarker"
|
||||
echo ""
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Push to AUR cancelled${NC}"
|
||||
echo ""
|
||||
echo "To push manually later:"
|
||||
echo " cd $AUR_DIR"
|
||||
echo " git add PKGBUILD .SRCINFO"
|
||||
echo " git commit -m 'Update to v$CARGO_VERSION'"
|
||||
echo " git push origin master"
|
||||
fi
|
||||
Reference in New Issue
Block a user