fix(screenshots): save to configured path with tilde expansion

Fixes #1 - Screenshots now properly save to disk when savePath is configured.

Changes:
- Changed config format from JSONC to JSON for reliable parsing
- Added tilde (~) expansion for all save paths
- Edit mode (swappy) now saves to configured savePath via -o flag
- Default paths: ~/Pictures/hypr-lens, ~/Videos/hypr-lens
- Added copyAlsoSaves option for users who want Copy to also save
- Copy mode (left-click) now clipboard-only by default
- Updated documentation and changelog

Closes #1
This commit is contained in:
DJ
2025-12-19 18:08:16 -05:00
parent 9c97ec1364
commit c616efa0cc
12 changed files with 168 additions and 107 deletions
+38
View File
@@ -53,6 +53,42 @@ Initial public release! 🎉
- **Installer Recovery** - Detect and recover missing shell integration during update mode
- **Invalid Input Handling** - Improved input validation in integration recovery flow
## 1.4.0 - 2025-12-19
### Fixed
- **Screenshot Save Path** - Screenshots and edited images now properly save to configured `savePath`
- **Tilde Expansion** - `~/` paths now correctly expand in all save path configurations
- **Swappy Integration** - Edit mode (swappy) now saves to configured path instead of default Desktop
- **Config Parsing** - Changed config format from JSONC to JSON for reliable parsing
### Added
- **Default Save Paths** - Screenshots default to `~/Pictures/hypr-lens`, recordings to `~/Videos/hypr-lens`
- **Copy Also Saves Option** - New `copyAlsoSaves` config option for users who want left-click to also save
### Changed
- **Copy Mode Behavior** - Left-click (Copy) now only copies to clipboard by default; use Edit mode (right-click) or enable `copyAlsoSaves` to save files
---
## 1.3.0 - 2025-12-16
### Added
- **Right-Click Edit on Monitor Buttons** - Right-click monitor buttons to capture and edit with swappy
### Fixed
- **MonitorButton Tooltip** - Fixed missing services import for tooltip functionality
### Changed
- **Documentation** - Clarified compatibility notes for non-Arch distributions
---
## 1.2.0 - 2025-12-08
### Added
@@ -70,6 +106,8 @@ Initial public release! 🎉
| Version | Date | Highlights |
|---------|------|------------|
| 1.4.0 | 2025-12-19 | Screenshot save fix, tilde expansion, swappy integration |
| 1.3.0 | 2025-12-16 | Right-click edit on monitor buttons |
| 1.2.0 | 2025-12-08 | Demo videos, security documentation |
| 1.1.0 | 2025-12-08 | Monitor capture buttons, installer recovery |
| 1.0.0 | 2025-12-08 | Initial release |
+2 -2
View File
@@ -109,7 +109,7 @@ cd hypr-lens
| QML modules | `~/.config/quickshell/hypr-lens/` |
| Scripts | `~/.local/share/hypr-lens/scripts/` |
| Python venv | `~/.local/share/hypr-lens/venv/` (optional) |
| Default config | `~/.config/hypr-lens/config.jsonc` |
| Default config | `~/.config/hypr-lens/config.json` |
## Updating
@@ -229,7 +229,7 @@ See [MANUAL.md](MANUAL.md#ipc-commands) for detailed IPC documentation.
## Configuration
Config file: `~/.config/hypr-lens/config.jsonc`
Config file: `~/.config/hypr-lens/config.json`
<details>
<summary><strong>Full Configuration Reference</strong></summary>
+1 -1
View File
@@ -23,7 +23,7 @@ The installer only touches files in your user directories:
| Path | Action | Purpose |
|------|--------|---------|
| `~/.config/quickshell/hypr-lens/` | Create | QML modules (UI components) |
| `~/.config/hypr-lens/config.jsonc` | Create | User configuration file |
| `~/.config/hypr-lens/config.json` | Create | User configuration file |
| `~/.local/share/hypr-lens/scripts/` | Create | Helper scripts (OpenCV, recording) |
| `~/.local/share/hypr-lens/venv/` | Create (optional) | Python venv for content detection |
| `~/.config/quickshell/shell.qml` | Modify (optional) | Integration (with timestamped backup) |
+9 -4
View File
@@ -44,9 +44,14 @@ Screenshot settings.
| Setting | Default | Description |
|---------|---------|-------------|
| `savePath` | `""` | Where to save screenshots. Empty = clipboard only |
| `savePath` | `""` | Where to save screenshots. Empty = `~/Pictures/hypr-lens` |
| `copyAlsoSaves` | `false` | If `true`, Copy mode (left-click) also saves to disk. If `false`, only Edit mode (swappy) saves. |
**Example:** `"/home/user/Pictures/Screenshots"`
**Behavior:**
- **Left-click (Copy)**: Copies to clipboard only (unless `copyAlsoSaves` is `true`)
- **Right-click (Edit)**: Opens swappy for annotation, then saves to `savePath`
**Example:** `"/home/user/Pictures/Screenshots"` or `"~/Pictures/Screenshots"`
---
@@ -56,7 +61,7 @@ Screen recording settings.
| Setting | Default | Description |
|---------|---------|-------------|
| `savePath` | `""` | Where to save recordings. Empty = `~/Videos` |
| `savePath` | `""` | Where to save recordings. Empty = `~/Videos/hypr-lens` |
**Example:** `"/home/user/Videos/Recordings"`
@@ -136,4 +141,4 @@ Circle selection tool settings.
- Empty string `""` for path settings = use default behavior
- Restart quickshell after changes: `killall quickshell; quickshell &`
- Use absolute paths (e.g., `/home/user/...` not `~/...`)
- Both absolute paths (`/home/user/...`) and tilde paths (`~/...`) are supported
+46
View File
@@ -0,0 +1,46 @@
{
"appearance": {
"matugenPath": "",
"ripple": {
"usePrimaryColor": true,
"colorMixRatio": 0.65,
"solidRadius": 0.45,
"fadeRadius": 0.7
}
},
"screenSnip": {
"savePath": "",
"copyAlsoSaves": false
},
"screenRecord": {
"savePath": ""
},
"ocr": {
"useCircleSelection": false
},
"search": {
"imageSearch": {
"imageSearchEngineBaseUrl": "https://lens.google.com/uploadbyurl?url=",
"useCircleSelection": false
}
},
"monitorOrder": [],
"regionSelector": {
"targetRegions": {
"windows": true,
"layers": false,
"content": true,
"showLabel": false,
"opacity": 0.3,
"contentRegionOpacity": 0.8,
"selectionPadding": 5
},
"rect": {
"showAimLines": true
},
"circle": {
"strokeWidth": 6,
"padding": 10
}
}
}
-73
View File
@@ -1,73 +0,0 @@
{
// ─── Appearance ─────────────────────────────────────────────
// Theme and visual settings
"appearance": {
// Path to matugen.json for dynamic theming (empty = ~/.config/quickshell/matugen.json)
"matugenPath": "",
// Button click ripple effect
"ripple": {
"usePrimaryColor": true, // true = accent color, false = subtle gray
"colorMixRatio": 0.65, // 0.0 = bold/visible, 1.0 = invisible
"solidRadius": 0.45, // How far solid color extends (0.0-1.0)
"fadeRadius": 0.7 // Where fade ends (0.0-1.0)
}
},
// ─── Screenshots ────────────────────────────────────────────
"screenSnip": {
// Save location (empty = clipboard only)
// Example: "/home/user/Pictures/Screenshots"
"savePath": ""
},
// ─── Screen Recording ───────────────────────────────────────
"screenRecord": {
// Save location (empty = ~/Videos)
"savePath": ""
},
// ─── OCR (Text Extraction) ──────────────────────────────────
"ocr": {
"useCircleSelection": false // true = circle tool, false = rectangle
},
// ─── Image Search ───────────────────────────────────────────
"search": {
"imageSearch": {
// Reverse image search URL (default: Google Lens)
// Alternatives: TinEye, Bing Visual Search
"imageSearchEngineBaseUrl": "https://lens.google.com/uploadbyurl?url=",
"useCircleSelection": false
}
},
// ─── Monitor Order ──────────────────────────────────────────
// Monitor button order for full-screen capture (empty = auto-detect)
// Example: ["DP-2", "DP-1"] to show DP-2 button first
"monitorOrder": [],
// ─── Region Selector ────────────────────────────────────────
// Controls the selection overlay behavior
"regionSelector": {
// Auto-detection of clickable regions
"targetRegions": {
"windows": true, // Detect window boundaries
"layers": false, // Detect Hyprland layers (panels, bars)
"content": true, // OpenCV content detection
"showLabel": false, // Show labels on regions
"opacity": 0.3, // Highlight opacity (0.0-1.0)
"contentRegionOpacity": 0.8,
"selectionPadding": 5 // Extra pixels around regions
},
// Rectangle selection tool
"rect": {
"showAimLines": true // Show crosshair when drawing
},
// Circle selection tool
"circle": {
"strokeWidth": 6, // Outline thickness
"padding": 10 // Extra space around selection
}
}
}
+23 -11
View File
@@ -26,7 +26,7 @@ Components installed:
1. QML modules → ~/.config/quickshell/hypr-lens/
2. Scripts → ~/.local/share/hypr-lens/scripts/
3. Python venv → ~/.local/share/hypr-lens/venv/
4. Default config → ~/.config/hypr-lens/config.jsonc
4. Default config → ~/.config/hypr-lens/config.json
Generated (you copy manually):
- keybinds.example.conf → Copy contents to your Hyprland keybinds config
@@ -402,7 +402,7 @@ install_config() {
if dry_run_preview \
"Would create: $CONFIG_DIR" \
"Would copy: $SCRIPT_DIR/defaults/config.jsonc$CONFIG_DIR/config.jsonc" \
"Would copy: $SCRIPT_DIR/defaults/config.json → $CONFIG_DIR/config.json" \
"Would copy: $SCRIPT_DIR/defaults/CONFIG_README.md → $CONFIG_DIR/CONFIG_README.md"; then
return
fi
@@ -412,16 +412,28 @@ install_config() {
# Always copy/update the README
cp "$SCRIPT_DIR/defaults/CONFIG_README.md" "$CONFIG_DIR/CONFIG_README.md"
if [[ -f "$CONFIG_DIR/config.jsonc" ]]; then
warn "Config already exists at $CONFIG_DIR/config.jsonc"
# Migrate old config.jsonc to config.json if needed
if [[ -f "$CONFIG_DIR/config.jsonc" && ! -f "$CONFIG_DIR/config.json" ]]; then
warn "Found old config.jsonc - migrating to config.json"
# Strip comments and trailing commas for valid JSON
if sed 's|//.*||g; s/,\s*}/}/g; s/,\s*]/]/g' "$CONFIG_DIR/config.jsonc" | jq . > "$CONFIG_DIR/config.json" 2>/dev/null; then
success "Config migrated to config.json"
info "Old config.jsonc preserved as backup"
else
warn "Migration failed - installing default config"
cp "$SCRIPT_DIR/defaults/config.json" "$CONFIG_DIR/config.json"
success "Default config installed"
fi
elif [[ -f "$CONFIG_DIR/config.json" ]]; then
warn "Config already exists at $CONFIG_DIR/config.json"
if ask "Overwrite with defaults?"; then
cp "$SCRIPT_DIR/defaults/config.jsonc" "$CONFIG_DIR/config.jsonc"
cp "$SCRIPT_DIR/defaults/config.json" "$CONFIG_DIR/config.json"
success "Config overwritten"
else
success "Keeping existing config"
fi
else
cp "$SCRIPT_DIR/defaults/config.jsonc" "$CONFIG_DIR/config.jsonc"
cp "$SCRIPT_DIR/defaults/config.json" "$CONFIG_DIR/config.json"
success "Default config installed"
fi
success "Config README installed"
@@ -802,8 +814,8 @@ discover_matugen() {
if [[ -n "$selected_path" ]]; then
if [[ -f "$selected_path" ]]; then
# Update config.jsonc with selected path
if [[ -f "$CONFIG_DIR/config.jsonc" ]]; then
# Update config.json with selected path
if [[ -f "$CONFIG_DIR/config.json" ]]; then
if dry_run_preview "Would set matugen path in config to: $selected_path"; then
return
fi
@@ -811,8 +823,8 @@ discover_matugen() {
# (jq doesn't support JSONC, so we use sed to strip // comments first)
local tmp_config
tmp_config=$(mktemp)
if sed 's|//.*||g' "$CONFIG_DIR/config.jsonc" | jq --arg path "$selected_path" '.appearance.matugenPath = $path' > "$tmp_config" 2>/dev/null; then
mv "$tmp_config" "$CONFIG_DIR/config.jsonc"
if sed 's|//.*||g' "$CONFIG_DIR/config.json" | jq --arg path "$selected_path" '.appearance.matugenPath = $path' > "$tmp_config" 2>/dev/null; then
mv "$tmp_config" "$CONFIG_DIR/config.json"
success "Matugen path set to: $selected_path"
warn "Note: Comments were stripped from config. See CONFIG_README.md for reference."
else
@@ -964,7 +976,7 @@ update() {
fi
# Config is preserved (don't overwrite user settings)
if [[ -f "$CONFIG_DIR/config.jsonc" ]]; then
if [[ -f "$CONFIG_DIR/config.json" ]]; then
success "Config preserved (not overwritten)"
else
install_config
+3 -2
View File
@@ -56,12 +56,13 @@ Singleton {
// ─── Screenshots ──────────────────────────────────────────────
property JsonObject screenSnip: JsonObject {
property string savePath: "" // Save location (empty = clipboard only)
property string savePath: "" // Save location (empty = ~/Pictures/hypr-lens)
property bool copyAlsoSaves: false // true = Copy mode also saves to disk, false = clipboard only
}
// ─── Screen Recording ─────────────────────────────────────────
property JsonObject screenRecord: JsonObject {
property string savePath: "" // Save location (empty = ~/Videos)
property string savePath: "" // Save location (empty = ~/Videos/hypr-lens)
}
// ─── OCR ──────────────────────────────────────────────────────
+1 -1
View File
@@ -17,7 +17,7 @@ Singleton {
property string screenshotTemp: "/tmp/hypr-lens/screenshot"
property string recordScriptPath: scriptPath + "/videos/record.sh"
property string shellConfig: home + "/.config/hypr-lens"
property string shellConfigPath: shellConfig + "/config.jsonc"
property string shellConfigPath: shellConfig + "/config.json"
// Create directories on init
Component.onCompleted: {
@@ -187,9 +187,9 @@ PanelWindow {
// Each builder takes (rx, ry, rw, rh, absX, absY) and returns a command array.
readonly property var commandBuilders: ({
[RegionSelection.SnipAction.Copy]: (rx, ry, rw, rh, absX, absY) =>
SnipCommands.buildCopyCommand(root.screenshotPath, rx, ry, rw, rh, root.saveScreenshotDir),
SnipCommands.buildCopyCommand(root.screenshotPath, rx, ry, rw, rh, root.saveScreenshotDir, Config.options.screenSnip.copyAlsoSaves),
[RegionSelection.SnipAction.Edit]: (rx, ry, rw, rh, absX, absY) =>
SnipCommands.buildEditCommand(root.screenshotPath, rx, ry, rw, rh),
SnipCommands.buildEditCommand(root.screenshotPath, rx, ry, rw, rh, root.saveScreenshotDir),
[RegionSelection.SnipAction.Search]: (rx, ry, rw, rh, absX, absY) =>
SnipCommands.buildSearchCommand(root.screenshotPath, rx, ry, rw, rh, "https://lens.google.com"),
[RegionSelection.SnipAction.CharRecognition]: (rx, ry, rw, rh, absX, absY) =>
@@ -9,6 +9,17 @@ import "../common/functions"
Singleton {
id: root
// Expand ~ to actual home directory (bash doesn't expand ~ in single quotes)
function expandTilde(path: string): string {
if (path.startsWith("~/")) {
return Directories.home + path.slice(1);
}
if (path === "~") {
return Directories.home;
}
return path;
}
// Build ImageMagick crop command base
function buildCropBase(screenshotPath: string, rx: int, ry: int, rw: int, rh: int): string {
return `magick ${StringUtils.shellSingleQuoteEscape(screenshotPath)} -crop ${rw}x${rh}+${rx}+${ry}`;
@@ -19,32 +30,50 @@ Singleton {
return `rm '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`;
}
// Copy to clipboard (with optional save to disk)
function buildCopyCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, saveDir: string): list<string> {
// Default save location when savePath is empty
readonly property string defaultSavePath: Directories.home + "/Pictures/hypr-lens"
// Copy to clipboard (optionally also saves to disk if copyAlsoSaves is true)
function buildCopyCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, saveDir: string, alsoSave: bool): list<string> {
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh);
const cropToStdout = `${cropBase} -`;
const cleanup = buildCleanup(screenshotPath);
if (saveDir === "") {
// If not saving, just copy to clipboard
if (!alsoSave) {
return ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`];
}
// Save + copy: use provided path or default
const targetDir = saveDir !== "" ? saveDir : defaultSavePath;
const expandedSaveDir = expandTilde(targetDir);
return [
"bash", "-c",
`mkdir -p '${StringUtils.shellSingleQuoteEscape(saveDir)}' && \
`mkdir -p '${StringUtils.shellSingleQuoteEscape(expandedSaveDir)}' && \
saveFileName="screenshot-$(date '+%Y-%m-%d_%H.%M.%S').png" && \
savePath="${saveDir}/$saveFileName" && \
savePath="${expandedSaveDir}/$saveFileName" && \
${cropToStdout} | tee >(wl-copy) > "$savePath" && \
${cleanup}`
];
}
// Edit with swappy
function buildEditCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int): list<string> {
// Edit with swappy (always saves to disk - uses default path if empty)
function buildEditCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, saveDir: string): list<string> {
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh);
const cropToStdout = `${cropBase} -`;
const cleanup = buildCleanup(screenshotPath);
return ["bash", "-c", `${cropToStdout} | swappy -f - && ${cleanup}`];
// Always save: use provided path or default
const targetDir = saveDir !== "" ? saveDir : defaultSavePath;
const expandedSaveDir = expandTilde(targetDir);
return [
"bash", "-c",
`mkdir -p '${StringUtils.shellSingleQuoteEscape(expandedSaveDir)}' && \
saveFileName="screenshot-$(date '+%Y-%m-%d_%H.%M.%S').png" && \
savePath="${expandedSaveDir}/$saveFileName" && \
${cropToStdout} | swappy -f - -o "$savePath" && \
${cleanup}`
];
}
// Image search (clipboard-based: copy to clipboard + open search page)
+6 -3
View File
@@ -25,14 +25,17 @@ get_active_monitor() {
hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name'
}
# Configuration
CUSTOM_PATH=$(jq -r "$JSON_PATH" "$CONFIG_FILE" 2>/dev/null)
# Configuration (strip // comments from JSONC before parsing)
CUSTOM_PATH=$(sed 's|//.*||g' "$CONFIG_FILE" 2>/dev/null | jq -r "$JSON_PATH" 2>/dev/null)
if [[ -n "$CUSTOM_PATH" && "$CUSTOM_PATH" != "null" ]]; then
RECORDING_DIR="$CUSTOM_PATH"
else
RECORDING_DIR="$HOME/Videos"
RECORDING_DIR="$HOME/Videos/hypr-lens"
fi
# Expand ~ to $HOME (tilde doesn't expand when read from config file)
RECORDING_DIR="${RECORDING_DIR/#\~/$HOME}"
mkdir -p "$RECORDING_DIR"
cd "$RECORDING_DIR" || exit