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
10 changes: 7 additions & 3 deletions .claude/skills/run-overlay/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ launch it, photograph it, and look. `PPC_DEV_ITEM` is what makes that possible w
| --- | --- |
| `PPC_DEV_OVERLAY=1` | Opens Settings and disables dismiss-on-blur. Required for every dev run. |
| `PPC_DEV_ITEM=<file>` | Opens the price-check panel on a captured clipboard instead. |
| `PPC_DEV_PASTE=1` | Opens the QuickPaste popup at wherever the pointer is, which is the only way to see it without the game. |
| `PPC_DEV_IDLE=1` | Keeps the idle status marker up (it otherwise shows only while the game is in front). |
| `PPC_MANAGED=1` | Lets the window manager manage the window — needed if you want it stackable/movable on a real session. |
| `PPC_DEBUG_COPY=1` | Traces the copy timeline to stderr. |
Expand All @@ -23,9 +24,12 @@ launch it, photograph it, and look. `PPC_DEV_ITEM` is what makes that possible w

```sh
cmake --build build -j
pkill -f PathOfPriceCheck # one instance per user — a second launch refuses, loudly
(PPC_DEV_OVERLAY=1 PPC_DEV_ITEM=tests/data/items/<file>.txt \
./build/PathOfPriceCheck > "$SCRATCH/run.log" 2>&1 &)
pkill -x -f ./build/PathOfPriceCheck # ALWAYS, first — one instance per user, and a second
# launch refuses. -x -f matches that exact command line
# and so cannot match the shell running this script.
setsid nohup env PPC_DEV_OVERLAY=1 PPC_DEV_ITEM=tests/data/items/<file>.txt \
./build/PathOfPriceCheck > "$SCRATCH/run.log" 2>&1 < /dev/null & # setsid: a plain `&`
# dies with the shell
sleep 10 # data bundle, poe.ninja overview, exchange digest, CDN icons
spectacle -b -n -f -o "$SCRATCH/shot.png"
magick "$SCRATCH/shot.png" -crop 480x620+1220+0 +repage -resize 220% "$SCRATCH/panel.png"
Expand Down
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ Pipeline: **hotkey → auto-copy → clipboard → parse → identify → price

The overlay, Settings, the league list, the static game-data layer, the item layer (parse →
resolve → price-relevant numbers → search plan, plus the game-styled tooltip), the trade search,
poe.ninja reference pricing, the in-game currency exchange feed and the binary updater (with the
Windows installer it depends on) are all **built and tested**.
poe.ninja reference pricing, the in-game currency exchange feed, QuickPaste and the binary updater
(with the Windows installer it depends on) are all **built and tested**.
What is not built is [docs/roadmap.md](docs/roadmap.md) — including the fact that a language other
than English cannot yet be selected, because the data build emits only English.

Expand All @@ -40,6 +40,7 @@ read whole; each is one layer.
| [docs/updater.md](docs/updater.md) | `src/update/` and `packaging/` — how a copy arrives and how it replaces itself: the install flavours, the swap, `latest.json`, and the Windows installer. |
| [docs/item-layer.md](docs/item-layer.md) | `src/item/` — parse, resolve, derive, range matching, and the plan rules every strategy shares. Where most pricing judgement lives. |
| [docs/strategy-unique.md](docs/strategy-unique.md), [strategy-map.md](docs/strategy-map.md), [strategy-gem.md](docs/strategy-gem.md), [strategy-logbook.md](docs/strategy-logbook.md) | One per search strategy that has more to say than the shared rules: uniques (including unidentified), maps (with charts and Valdo maps), gems, expedition logbooks (the one item that is up to three items at once). |
| [docs/quickpaste.md](docs/quickpaste.md) | The paste list — the popup at the cursor, the nine number-key slots, and the clipboard *write*, which is a seam of its own. |
| [docs/trade-layer.md](docs/trade-layer.md) | `src/trade/` — query building, the two-step client, the rate limiter, and how results and the filter list are drawn. |
| [docs/ninja.md](docs/ninja.md) | `src/ninja/` — the poe.ninja reference price. |
| [docs/exchange.md](docs/exchange.md) | `src/exchange/` — GGG's hourly in-game currency exchange digests. |
Expand Down Expand Up @@ -90,6 +91,9 @@ violate one of these on the strength of not having read it.
- **Never issue a GGG request outside `trade::request`.** The shared rate limiter is a hard
requirement, not a courtesy. poe.ninja and the currency-exchange CDN are *different hosts with
different rules* and deliberately do not go through it. → trade-layer, external-apis
- **The clipboard is ours to read *and* to write.** `clipboard_set_text` owns the X selection from
a thread of its own, because a write on X11 is a promise to answer for the text later. →
quickpaste, platform
- **Do not go back to `SDL_GetClipboardText()`**, do not clear the clipboard before a copy, and do
not build a purely passive clipboard watcher. All three were tried and measured; each fails in a
way that reads as a hang. → platform, architecture
Expand Down
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ target_compile_definitions(imgui PUBLIC SDL_MAIN_HANDLED)
add_library(ppc_core STATIC
src/paths.cpp
src/config.cpp
src/quickpaste.cpp
src/leagues.cpp
src/util/sha256.cpp
src/util/base64.cpp
Expand Down Expand Up @@ -177,6 +178,7 @@ set(APP_SOURCES
src/ui/theme.cpp
src/ui/range_slider.cpp
src/screens/settings_screen.cpp
src/screens/quickpaste_screen.cpp
src/screens/pricecheck_screen.cpp
src/screens/item_view.cpp)
if(WIN32)
Expand Down Expand Up @@ -280,3 +282,4 @@ ppc_add_test(ninja_test)
ppc_add_test(exchange_test)
ppc_add_test(ratelimit_test)
ppc_add_test(track_test)
ppc_add_test(quickpaste_test)
10 changes: 7 additions & 3 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,18 @@ The whole tool works by reading the clipboard, so this is worth being precise ab
- If you press the hotkey while something other than an item is on your clipboard, that text is
what gets parsed. It fails to parse, nothing opens, and it is discarded. But it *was* read — so
the ordinary caution applies: this is a global hotkey and the clipboard is a global thing.
- Nothing is ever written to your clipboard except when you click the diagnostic check id in the
panel footer, which copies that four-character id and nothing else.
- **Two things write to your clipboard, both because you asked**: picking an entry from QuickPaste,
which puts that entry's own text there and nothing else, and clicking the diagnostic check id in
the panel footer, which copies that four-character id. On Linux the text is then served from a
window this application owns for as long as it runs — which is how the X11 clipboard works for
every program — so closing the tool takes it with it unless your desktop's clipboard manager has
kept a copy.

## What is stored on your machine

| path | what |
|---|---|
| `<config>/config.json` | your settings: league, hotkeys, panel geometry, listing status, result count, filter ranges, client and interface language, panel opacity, whether to update automatically |
| `<config>/config.json` | your settings: league, hotkeys, panel geometry, listing status, result count, filter ranges, client and interface language, panel opacity, whether to update automatically — **and your QuickPaste entries, in full**, since they are text you typed for this tool to hold |
| `<config>/cookies.txt` | the cookie jar above |
| `<cache>/data/<version>/` | the downloaded game-data bundle, plus a `current` pointer |
| `<cache>/update/` | a downloaded release of the application, waiting for the restart that applies it. One file, consumed as it is applied; absent whenever no update is pending |
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ the game in the game's own typeface.
volume-weighted average, the band around it, and the volume on both sides. Items that trade
there get no Search button, and one that had no trade in the last hour says so rather than
showing you nothing.
- **QuickPaste.** Alt+V opens your saved snippets at the cursor — a map regex, a vendor search, the
whisper you send twenty times an evening. Pick one by clicking it or by pressing its number, and
it is on your clipboard. **It does not press Ctrl+V for you**: you paste it yourself, in the
field you meant, at your own time. Nine at a time, because that is how many number keys there
are; keep as many as you like and switch them in and out in Settings.
- **Listings you can read.** Account, listing age and price, with the seller's own item drawn
beside the list when you hover a row — through the same renderer as the item in your hand, so the
comparison is like-for-like.
Expand Down Expand Up @@ -117,11 +122,15 @@ for Arch, Debian/Ubuntu/Mint/Pop!\_OS and Fedora, plus the Windows toolchain.
| | |
|---|---|
| **Ctrl+D** | price-check the item under the cursor |
| **Alt+V** | open QuickPaste at the cursor; **1**-**9** picks one |
| **Shift+Space** | Settings |
| **Escape**, click away, or the hotkey again | dismiss the panel |

Both hotkeys are rebindable in Settings, and both are ignored unless Path of Exile is the window in
front — they are grabbed system-wide, so they must not go off in your browser.
All three hotkeys are rebindable in Settings, and all three are ignored unless Path of Exile is the
window in front — they are grabbed system-wide, so they must not go off in your browser.

QuickPaste's number keys go by the key's **position**, not by what your layout prints on it, so the
top row works whatever keyboard you have.

The panel docks beside the frame the item came from: right of the stash, or left of the inventory,
depending on which half of the screen your cursor was in. If it lands wrong, the stash and
Expand Down
23 changes: 15 additions & 8 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,25 @@ could only tick it or leave it.

**Does not** — carry an edit onto the next item. Bounds belong to the item in hand.

## 0.5 — The paste list
## 0.5 — QuickPaste — **shipped**

*Working name.* A hotkey opens a small window at the cursor listing saved snippets — a map regex,
a vendor search, a whisper you send twenty times an evening.
A hotkey opens a small window at the cursor listing saved snippets — a map regex, a vendor
search, a whisper you send twenty times an evening.

**Will:**
**Does:**

- Hold entries of a heading and a body, multi-line, edited in Settings.
- Hold entries of a heading and a body, multi-line, written and arranged in Settings' own
**QuickPaste** tab: add, edit, delete, and drag into the order you want them offered in.
- Put the one you pick on the clipboard and give the game back the foreground.
- Pick by number key, so the mouse never has to travel.

**Will not** — press Ctrl+V for you. You paste, in your own field, at your own time.
- **Pick by number key**, so the mouse never has to travel — and by the key's *position*, so it
works on a keyboard layout that does not print digits on that row.
- Offer nine at a time, which is how many number keys there are. Keep as many as you like: the
ones beyond nine are simply switched off until you switch something else off. Nothing to read,
nothing refused — the tenth tick is just not available.
- Open at your cursor, growing down, up, or from somewhere between the two, whichever fits the
screen.

**Does not** — press Ctrl+V for you. You paste, in your own field, at your own time.

**Might** — grow that keystroke as an option later if it turns out to be wanted.

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4
0.5
46 changes: 34 additions & 12 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ it drives have docs of their own and are read separately: [data-layer.md](data-l
[platform.md](platform.md).

Pipeline: **hotkey → auto-copy → clipboard → parse → identify → price → render**. `App` (`src/app.cpp`)
owns the SDL event loop and a `Screen` state machine `{ Hidden, PriceCheck, Settings }`. Price-check
owns the SDL event loop and a `Screen` state machine `{ Hidden, PriceCheck, Settings, QuickPaste }`
— the last of which is the paste list and is [quickpaste.md](quickpaste.md), the only screen that
does not involve the copy path at all. Price-check
hotkey → `simulate_copy()` → wait for the clipboard to be written → parse → show if it's an item.
**Four steps, and they are meant to stay four.** An earlier version grew a pre-copy snapshot, a
byte comparison against it, a latching write detector, `SDL_EVENT_CLIPBOARD_UPDATE` as a third
Expand Down Expand Up @@ -129,15 +131,29 @@ the copy path used to call `focus_game_window()` on a window it had just confirm
and `XSetInputFocus` on the toplevel can land somewhere Wine didn't put it. Focus is handed back to
the game on close **only** if `overlay_.has_focus()` — i.e. only focus we took ourselves.

**Claiming the keyboard is a smaller thing than claiming the foreground**, and two places do it:
Settings, for its text fields, and the filter list's range editor, for its two boxes. Both call
`overlay_take_keyboard_focus`, which is `XSetInputFocus` on our own override-redirect window — it
**Claiming the keyboard is a smaller thing than claiming the foreground**, and three places do it:
Settings, for its text fields; the filter list's range editor, for its two boxes; and the paste
popup, for its number keys. All three go through `App::take_keyboard`, which is
`overlay_take_keyboard_focus` — `XSetInputFocus` on our own override-redirect window — it
moves `input=` and leaves `active=` on the game, which is why it is useless for prising the
clipboard out of Wine (below) and exactly right here. Without it a text field on a price check
looks live and receives nothing, because the WM will not focus the window it is drawn on. Neither
hands the focus back on closing the widget: for a price check the game regaining focus *is* the
dismiss, so returning it would close the panel out from under the edit. `set_screen` hands it back
when the screen closes, and only if `overlay_.has_focus()`.
looks live and receives nothing, because the WM will not focus the window it is drawn on. None of
them hands the focus back on closing the widget: for a price check the game regaining focus *is*
the dismiss, so returning it would close the panel out from under the edit. `set_screen` hands it
back when the screen closes, through `give_keyboard_back`.

**Claiming it once is not enough, and `overlay_.has_focus()` is not the record of having claimed
it.** Two reported bugs came out of that pair. A screen that lives on the keyboard has to
*re-*claim it whenever the game comes back to the front, because the window manager will not hand
the focus to a window it does not manage — without that, alt-tabbing to a browser and back left
Settings on screen, apparently live, receiving nothing (`App::reclaim_keyboard`, run from the
placement poll and, up to 400ms sooner, from a click into the panel). And handing it back cannot
be gated on SDL's `has_focus()` alone, which lags the `XSetInputFocus` that caused it: a paste
popup dismissed briskly reached the hand-back before SDL had registered the focus we had taken
ourselves, so the game never got a focus change — and that focus change is what makes Wine re-read
the clipboard, which is why the first paste served the *previous* one. `took_keyboard_` is our own
record of the call, and `give_keyboard_back` still checks the foreground first, so it can never
become a focus steal from a third application.

**A drag that leaves the window is reconciled against the physical mouse** (`Overlay::sync_held_mouse`,
run between the backend's `NewFrame` and ImGui's). The overlay is never wider than it needs to be
Expand Down Expand Up @@ -178,7 +194,8 @@ releases even when it finds no game window — an unmatched pair leaks the helpe
`deactivate_game_window` then refuses.

`App::place_overlay()` gives each screen its own geometry: Settings is a 640×720 dialog centered over
the game, price-check is a **full-height panel docked beside the item's own frame** — right of the
the game, the paste popup is sized to its own list and placed at the cursor sampled when its hotkey
fired (see [quickpaste.md](quickpaste.md)), price-check is a **full-height panel docked beside the item's own frame** — right of the
stash if the cursor was in the left half of the game window at hotkey time, left of the inventory if
in the right half (`App::cursor_side()`, sampled before the copy; the user has moved on by the time
the clipboard lands). Panels straddling the middle — vendor, quest rewards — have no correct answer,
Expand Down Expand Up @@ -222,7 +239,7 @@ globe's lower half, so that the third line — the one an available update adds
glass instead of on the frame. `place_overlay` sizes the window to the text for that screen, so the idle
overlay is a 200×48 rectangle rather than a dialog-sized one nothing is drawn into.

**Settings is three tabs** — General, Price check, Application — between a fixed header (the title
**Settings is four tabs** — General, Price check, QuickPaste, Application — between a fixed header (the title
and the close disc) and a fixed footer (Save). `kTabs` in `settings_screen.cpp` pairs each name with
the function that draws it; `App::settings_tab()` holds which one is open, because the screen is a
free function rebuilt every frame. The strip is buttons, not `ImGui::BeginTabBar`: the game marks
Expand Down Expand Up @@ -311,13 +328,18 @@ holds no SDL/X11/curl and every layer can log into it.
`clipboard_poke`). Suspect it first whenever turning the log on changes the behaviour being logged.

A **system-tray icon** (SDL3 `SDL_Tray`, cross-platform) provides Exit. `Overlay` wraps
the SDL3+GL+ImGui window; `Config` persists to JSON. **`SDL_HINT_VIDEO_ALLOW_SCREENSAVER` is set
the SDL3+GL+ImGui window; `Config` persists to JSON. **`Config::load` reads every field inside
one `try`**, not just the parse: `config.json` is hand-editable and nlohmann throws as readily on
a field of the wrong type as on a truncated file, so an object where a string belongs would
otherwise be an uncaught exception before the first window — a config the user can only fix by
deleting it. What was read before the throw stands and everything after it keeps its default,
which is the same posture as the clamping the numeric fields already do. **`SDL_HINT_VIDEO_ALLOW_SCREENSAVER` is set
back on**: SDL disables the screensaver at video init on the assumption that it is running a game,
and on Linux that is an `org.freedesktop.ScreenSaver` inhibit — reason "Playing a game" — held for
the life of the process, so an application that sits in the tray all day stopped the machine from
sleeping. The game does its own inhibiting; we are a desktop app. `PPC_DEV_OVERLAY=1` opens Settings and disables
dismiss-on-blur for local dev; add `PPC_DEV_ITEM=<file>` to open the price-check panel on a captured
clipboard instead, or `PPC_DEV_IDLE=1` to keep the idle status marker up (it otherwise only ever
clipboard instead, `PPC_DEV_PASTE=1` to open the paste popup at the pointer, or `PPC_DEV_IDLE=1` to keep the idle status marker up (it otherwise only ever
appears while the game is the window in front). `PPC_DEV_UPDATE_URL=<url>` points the update check
at a `latest.json` of your own, which is the only way to see its three notice surfaces before a
release publishes one — see [updater.md](updater.md).
Expand Down
Loading