10 KiB
deste
English | Türkçe
A Hearthstone deck tracker for Linux (KDE Wayland). It reads the game's own log files: no screen capture, no image recognition, no memory reading. When a new set is released only the card data is updated, not the code.
It exists because the one open source tracker on Linux, Arena Tracker, is unmaintained and its Qt5 + OpenCV design breaks on every patch. "Deste" is Turkish for "deck".
What it does today
- Your deck: remaining cards sorted by mana cost, with counts. A card you drew does not disappear from the list, it fades in place, so "I already played this" stays visible.
- Draw chance: the odds of each card coming up on the next draw.
- Opponent: every card they played or revealed, plus how many cards are in their hand and deck.
- No deck codes needed: decks are read from the game's own offline cache and the one matching the match is picked by class and by the cards you draw. You can also pick one by hand from the menu.
- Card art as the background of each row, and the full card image (rules text included) when you hover over it.
- Adjustable transparency, an overlay mode (frameless, always on top) and a normal window mode, a system tray icon and a desktop shortcut.
- English and Turkish interface, switchable from the menu. It follows your system language by default.
How it works
Every session the game writes a few log files under
Logs/Hearthstone_YYYY_MM_DD_HH_MM_SS/. Power.log contains the whole match:
entities being created, zone transitions (deck, hand, play, graveyard), tag
changes, turn boundaries and the result.
The flow:
a line of Power.log
-> core/parser_power.py turns the line into an Event (one regex table)
-> core/state.py applies it to game state (entities, zones, deck)
-> data/decks.py matches a known deck using class and drawn cards
-> ui/window.py remaining list, draw chances, opponent panel
core/logtail.py follows the file by offset and switches over when the game
starts a new session directory. core/watcher.py drives all of this without
knowing about Qt, which is why the same engine runs from the terminal
(tools/live.py) or over recorded logs (tools/replay.py).
Card names, costs and rarities are downloaded once from HearthstoneJSON into
~/.cache/deste/. Card images go to the same place, lazily and in the
background. With no network the app keeps running off the cache.
Install
Requires Python 3.11+ and PyQt6. Everything else is the standard library.
./install.sh # shortcut, icon, KWin rules
./install.sh --no-kwin # shortcut only, do not touch the window manager
./install.sh --no-hs-rule # without the Hearthstone rule
./install.sh --uninstall # undo all of it
install.sh installs the desktop shortcut and the icon, writes a KWin rule that
keeps wmclass=deste above other windows, and asks KWin to reload its
configuration. All of it is reversible with --uninstall.
If you use a tiling script (Krohnkite and friends), add the panel to that script's floating list yourself. The installer does not edit other people's settings.
The game needs two files in place before it writes usable logs. If they are already set up, nothing is touched:
<prefix>/users/<user>/AppData/Local/Blizzard/Hearthstone/log.configwith[Power] Verbose=1,[Zone],[LoadingScreen],[Arena]<game dir>/client.configwith[Log] FileSizeLimit.Int=-1
The app never edits them on its own, it warns if something is missing.
Usage
./run.sh # the interface (the desktop shortcut calls this)
python main.py --full # start by replaying the current session log
python -m tools.live # live tracking in the terminal
python -m tools.replay <log_dir> --deck # replay a recorded session
python -m tests.test_replay <log_dir> # consistency tests
Closing the window drops the app into the tray, clicking the tray icon brings it
back. Transparency, window mode, language and deck selection live in the ⋮
menu.
Wayland notes
Staying above the game. In KWin a fullscreen window is raised into the
active layer, which sits above the keep-above layer. No overlay can stay on top
of a fullscreen game, whatever rule you write. The fix is to stop the game from
going fullscreen: install.sh writes a rule with fullscreen=false and
noborder=true for Hearthstone, so the game still covers the screen as a
borderless window, looks identical, and the overlay can stay above it. Games
launched through umu/Proton use the window class steam_app_default, which is
shared with other games, so the rule matches on the title (Hearthstone) too.
Position. On Wayland an application cannot know where its own window is on
screen, and cannot move itself. Dragging the panel goes through
startSystemMove() and lets the window manager do it. Calling move() does not
move the window, it only corrupts Qt's idea of where the window is, and after
that menus and the card preview end up at the bottom of the screen. For the same
reason only the size is persisted, never the position. Menu and preview
positions are computed relative to the window itself rather than to the screen
edges, so the unknown offset cancels out.
Window size. The layout does not impose its minimum size on the window
(SetNoConstraint), the header text is elided and the turn label has a fixed
width. Otherwise the match result made the panel grow on its own and you had to
resize it after every game.
Icon. The tray icon is deliberately not built with QIcon.fromTheme. Doing
that makes Qt send the icon's name instead of the icon itself, and Plasma
draws an empty square when a freshly installed icon is not in its cache yet.
ui/icon.py always builds the icon from files and ready-made PNGs.
Architecture
core/ pure stdlib, knows nothing about UI or network, testable headless
logdir.py find the installation and the log directory, check log.config
logtail.py offset based file following
parser_power.py a line of Power.log -> event
state.py event -> game state (entities, zones, deck)
watcher.py live tracking loop (no Qt dependency)
deckstring.py deck code encode/decode
config.py user settings
data/ network and disk cache
cards.py HearthstoneJSON card data
localdecks.py decks from the game's offline cache
decks.py deck library and match-to-deck matching
images.py card images (tile and full render), downloaded in background
ui/ PyQt6 interface
window.py panel, tray icon, transparency, window modes
widgets.py card row (art strip) and hover preview
i18n.py interface strings, English and Turkish
theme.py colors and style
icon.py application icon
tools/ replay and terminal live tracking
tests/ consistency tests over a real log corpus
Design decisions
Power.log is the single source of truth. Zone.log is easier to read, but merging two files by timestamp is complexity for nothing. Power.log already has the match boundaries, the metadata and every zone transition. Zone.log is used only in the tests, as an independent source to check against.
Only GameState.* lines are processed. PowerTaskList.* lines are the
client side copy of the same content; process both and every event is counted
twice.
State is tracked, not events. Log lines can repeat, so "decrement the counter on every DECK -> HAND line" gives wrong results. Each entity's zone is stored and only real transitions are processed.
Deck tracking is independent from deck selection. The tracker only records what left your deck and what got shuffled in. The remaining list is computed against a deck list when asked, so you can still pick the deck mid-match.
Generated cards do not come out of your deck. Discovered, copied or randomly generated cards never touch the deck counter. Cards shuffled into your deck during the game are added to the list instead.
Log files are never deleted. They are only read.
Verification
tests/test_replay.py uses no synthetic data, it reads real session logs from
the machine. The two checks that matter most:
- Cross-check against an independent source: the set of "cards I drew"
derived from Power.log must match the
FRIENDLY DECK -> FRIENDLY HANDtransitions in Zone.log, entity id by entity id. - The remaining list must agree with the game's own counter: the size of the computed remaining deck must equal the number of cards in the game's DECK zone. Miss a draw, or fail to count a shuffled-in card, and this breaks immediately.
Run over 6 sessions and 33 matches, all passing.
Window manager behaviour (menu and preview placement, staying out of tiling) is
exercised in a virtual KWin session: kwin_wayland --virtual --width 1800 --height 1125. That way the test never opens a window on your screen.
Roadmap
Next:
- Match history and winrate (sqlite, stdlib): mode, classes, result and turn count per match; winrate by deck and by class.
- Opponent archetype prediction: match revealed cards against HSReplay signature cards and show something like "Zee Shaman, 4/8 signature cards". If the match is weak the panel hides itself instead of inventing a guess.
- Opponent secret tracking: candidates for a played secret get eliminated by
game events. The elimination rules live in
rules/secrets.jsonas data, so a new set means updating that file, not the code.
Later:
- Arena draft assistant. The only feature that needs screen capture: recognise cards by a perceptual hash (pHash) of the art, score them from published arena statistics.
- Battlegrounds panel
- Mulligan statistics, per-deck analysis
- Turn/rope timer, Twitch overlay, HSReplay upload
Notes
Card data comes from HearthstoneJSON, card art belongs to Blizzard. This project is not affiliated with Blizzard. The app only reads the log files the game itself writes; it does not touch the game's memory or network traffic.