Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,20 @@ change where screenshots land or what they are called, create
`~/.config/omasnap/omasnap.conf` (INI format); every key is optional:

```ini
[editor]
# overlay (default): the editor fills the screen as a fullscreen overlay.
# window: the editor opens as a normal compositor window, tiled or floated
# by the compositor, so a capture can be annotated next to another window.
# W switches a live editor between the two either way, and --editor
# window|overlay overrides this per invocation.
mode = overlay
# floating (default): a windowed editor asks the compositor to float it at
# the capture's natural size. tiled: it joins the tiling layout instead.
window = floating
# opaque (default): a windowed editor paints a solid backdrop.
# translucent: it keeps the overlay's see-through dim.
backdrop = opaque

[output]
# Where saved screenshots go. Default: ~/Pictures/Screenshots
directory = ~/Pictures/Captures
Expand Down Expand Up @@ -372,6 +386,7 @@ without reaching for the pointer.
| `B` | Cycle shadowed colors, window gray (shadowed and flat), and Off |
| `Shift+B` | Toggle the screenshot card's drop shadow; on by default |
| `G` / `Shift+G` | Cycle canvas boundaries forward/backward: Framed, Overflow, Image. Framed auto-grows with the normal frame; Overflow grows only the sides needed by annotations with no frame; Image clips at the original screenshot edge |
| `W` | Re-present the editor as a normal compositor window, or back as the fullscreen overlay; selection, layers, and undo history carry over |
| `1`–`8` | Set annotation color; `7` is black and `8` is white |
| Wheel | Scale selected layer, magnify the spotlight under the cursor, or change active tool size (`Alt`+wheel: rectangle corner radius or spotlight border); while just viewing a zoomed capture, scroll it like a document |
| `Shift`+wheel | Scroll a zoomed capture sideways (a wide stitch); never changes the zoom |
Expand Down
53 changes: 53 additions & 0 deletions src/capture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <QPainterPath>
#include <QProcess>
#include <QRandomGenerator>
#include <QRegularExpression>
#include <QSaveFile>
#include <QStandardPaths>

Expand Down Expand Up @@ -1442,6 +1443,58 @@ void prunePinnedSnapshots() {
}
}

QString editorHandoffPath() {
return runtimePath(QStringLiteral("edit-%1-%2.png")
.arg(QCoreApplication::applicationPid())
.arg(QRandomGenerator::global()->generate64(), 16, 16,
QChar('0')));
}

bool removeEditorHandoff(const QString &path) {
const QString runtime = secureRuntimeDirectory();
const QFileInfo file(path);
static const QRegularExpression name(QStringLiteral("^edit-[0-9]+-[0-9a-f]{16}\\.png$"));
if (runtime.isEmpty() || file.absolutePath() != runtime ||
!name.match(file.fileName()).hasMatch())
Comment on lines +1456 to +1458
return false;
const QString log = operationLogPath(path);
const bool sourceRemoved = !QFile::exists(path) || QFile::remove(path);
const bool logRemoved = !QFile::exists(log) || QFile::remove(log);
return sourceRemoved && logRemoved;
}

void pruneEditorHandoffs() {
const QString runtime = secureRuntimeDirectory();
if (runtime.isEmpty())
return;
const QDateTime cutoff = QDateTime::currentDateTime().addDays(-1);
const QFileInfoList stale =
QDir(runtime).entryInfoList({QStringLiteral("edit-*.png"),
QStringLiteral("edit-*.json")},
QDir::Files);
for (const QFileInfo &entry : stale) {
if (entry.lastModified() < cutoff)
QFile::remove(entry.absoluteFilePath());
}
}

QSize editorWindowSize(const QSize &preview, const QSize &available,
int legendHeight) {
// The capture at its natural size plus the editor's chrome: the key
// guide band as measured, the toolbar and handle clearance, the status
// band below, and the mat margins, so the image reads at 100% in a
// window that hugs it and the guide never covers anything. Clamped to
// the screen for captures too large to hug.
QSize size(preview.width() + 128, preview.height() + legendHeight + 210);
const QSize room = available.isEmpty()
? QSize(1728, 1080)
: QSize(qRound(available.width() * 0.9),
qRound(available.height() * 0.9));
if (size.width() > room.width() || size.height() > room.height())
size.scale(room, Qt::KeepAspectRatio);
return {std::max(size.width(), 640), std::max(size.height(), 420)};
}

bool savePinnedSnapshot(const QImage &image, const QString &path,
const QSize &logicalSize, QString &error) {
if (!saveTemporarySnapshot(image, path, error))
Expand Down
13 changes: 13 additions & 0 deletions src/capture.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,19 @@ QImage applyRedactionsScaled(QImage image, const QVector<Annotation> &redactions
[[nodiscard]] QString temporarySnapshotPath();
[[nodiscard]] QString pinnedSnapshotPath(int index);
void prunePinnedSnapshots();
/** Private runtime path for handing a live edit to the other editor
* presentation (overlay to window or back). */
[[nodiscard]] QString editorHandoffPath();
void pruneEditorHandoffs();
/** Removes a consumed private handoff pair; never removes ordinary image files. */
bool removeEditorHandoff(const QString &path);
/** Window size for a windowed editor: the capture at its logical size
* plus the chrome, where `legendHeight` is the measured key guide band,
* scaled down to fit inside `available` with a little margin, never
* smaller than a usable floor. */
[[nodiscard]] QSize editorWindowSize(const QSize &preview,
const QSize &available,
int legendHeight);
/**
* Writes `image` into the private runtime directory. `quality` is the Qt PNG
* quality knob, which maps inversely onto zlib levels: -1 keeps the default
Expand Down
110 changes: 110 additions & 0 deletions src/cli-path.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <QCommandLineParser>
#include <QCommandLineOption>
/** @fileoverview Resolves local image targets accepted by the command line. */
#include "cli-path.hpp"

Expand All @@ -22,3 +24,111 @@ QString resolveLocalImagePath(const QString &target) {
const QFileInfo file(path);
return file.isFile() ? file.absoluteFilePath() : QString{};
}

void configureCaptureCommandLine(QCommandLineParser &parser, bool beforeQt) {
// QApplication consumes these before the normal parse. Recognize them in
// the early shell-role parse too, without exposing or applying them here.
if (beforeQt) {
parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
for (const char *name : {"platform", "platformpluginpath", "platformtheme",
"plugin", "qmljsdebugger", "qwindowgeometry",
"qwindowicon", "qwindowtitle", "session",
"style", "stylesheet"}) {
QCommandLineOption option(QString::fromLatin1(name), QString(),
QStringLiteral("value"));
option.setFlags(QCommandLineOption::HiddenFromHelp);
parser.addOption(option);
}
for (const char *name : {"reverse", "widgetcount"}) {
QCommandLineOption option(QString::fromLatin1(name));
option.setFlags(QCommandLineOption::HiddenFromHelp);
parser.addOption(option);
}
}
parser.setApplicationDescription(QStringLiteral(
"Native Wayland screenshot and annotation overlay for Hyprland and "
"Omarchy.\n"
"\n"
"Only one capture overlay runs at a time. Starting omasnap again while "
"an\noverlay is open dismisses it: the running instance is asked to "
"quit and the\nnew process exits without capturing, so the same hotkey "
"opens and closes the\noverlay. Quick output (--copy, --save) dismisses "
"it the same way instead of\nscreenshotting the overlay. With --file (or "
"an image path) or --clipboard, the running\ninstance is stopped and "
"the editor opens on that image instead.\n"
"\n"
"Exit codes: 0 success, including dismissing a running overlay; 1 "
"capture,\nimage, or single-instance lock failure; 2 usage error."));
parser.addHelpOption();
parser.addVersionOption();
const QCommandLineOption fullscreenOption(
QStringLiteral("capture-fullscreen"),
QStringLiteral("Start with the entire focused monitor selected."));
const QCommandLineOption windowOption(
{QStringLiteral("capture-window"), QStringLiteral("capture-windows")},
QStringLiteral("Start in window selection mode."));
const QCommandLineOption regionOption(
QStringLiteral("capture-region"),
QStringLiteral("Start in freeform region selection mode (default)."));
parser.addOption(fullscreenOption);
parser.addOption(windowOption);
parser.addOption(regionOption);
const QCommandLineOption copyOption(
QStringLiteral("copy"),
QStringLiteral("Copy the capture directly without opening the editor."));
const QCommandLineOption saveOption(
QStringLiteral("save"),
QStringLiteral("Save the capture directly without opening the editor."));
parser.addOption(copyOption);
parser.addOption(saveOption);
const QCommandLineOption fileOption(
QStringLiteral("file"),
QStringLiteral("Open an existing image file in the annotation editor "
"instead of capturing the screen."),
QStringLiteral("path"));
parser.addOption(fileOption);
const QCommandLineOption clipboardOption(
QStringLiteral("clipboard"),
QStringLiteral("Open the current clipboard image in the annotation "
"editor instead of capturing the screen."));
parser.addOption(clipboardOption);
const QCommandLineOption pinOption(
QStringLiteral("pin"),
QStringLiteral("Show an image as a pinned always-visible layer."),
QStringLiteral("path"));
parser.addOption(pinOption);
const QCommandLineOption editorOption(
QStringLiteral("editor"),
QStringLiteral("Editor presentation: overlay (fullscreen, default) or "
"window (a normal compositor window). Also configurable "
"as [editor] mode in omasnap.conf; W switches a live "
"editor between the two."),
QStringLiteral("mode"));
parser.addOption(editorOption);
QCommandLineOption handoffMonitor(QStringLiteral("handoff-monitor"), QString(),
QStringLiteral("name"));
handoffMonitor.setFlags(QCommandLineOption::HiddenFromHelp);
parser.addOption(handoffMonitor);
const QCommandLineOption scrollOption(
QStringLiteral("scroll"),
QStringLiteral("Capture a scrolling region and stitch it into one tall "
"image, then open it in the editor."));
parser.addOption(scrollOption);
parser.addPositionalArgument(
QStringLiteral("target"),
QStringLiteral("Capture mode (smart, region, windows, fullscreen) or the "
"path of an image file to edit."),
QStringLiteral("[target]"));
}

bool windowedEditorRequested(const QCommandLineParser &parser, bool defaultWindow) {
const QString mode = parser.value(QStringLiteral("editor")).trimmed().toLower();
const bool window = mode.isEmpty() ? defaultWindow : mode == QStringLiteral("window");
if (!window || parser.isSet(QStringLiteral("pin")))
return false;
if (!parser.value(QStringLiteral("file")).isEmpty() ||
parser.isSet(QStringLiteral("clipboard")))
return true;
const QStringList positional = parser.positionalArguments();
return positional.size() == 1 && !resolveLocalImagePath(positional.first()).isEmpty();
}
5 changes: 5 additions & 0 deletions src/cli-path.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@

/** Returns an existing local path for a raw path or local file URL. */
QString resolveLocalImagePath(const QString &target);

class QCommandLineParser;
/** Shared by the pre-QApplication shell choice and normal CLI validation. */
void configureCaptureCommandLine(QCommandLineParser &parser, bool beforeQt = false);
bool windowedEditorRequested(const QCommandLineParser &parser, bool defaultWindow);
Loading
Loading