Cross-platform window switcher for playing Dofus with several accounts.
It maps every running Dofus client to a numbered tab and lets you jump between them instantly. Unlike the existing tools it targets Linux, Windows and macOS from a single codebase.
Status: v0. Linux and macOS work end to end and have been used on real hardware. The Windows backend is complete and covered by CI on a Windows runner, but nobody has run it yet. On macOS the hotkeys live in the overlay bar only, not the daemon. See Roadmap.
Dofus-tab only switches focus between windows and lets you name and order them. It deliberately does not broadcast input to several clients, automate gameplay, or read game memory. Multi-accounting is allowed by Ankama; automation and input broadcasting are not.
Nothing here touches the game. It talks to the window manager and reads the Ankama Launcher's own files to work out which client belongs to which account — the same information the launcher already shows you.
Dofus-tab is an unofficial, independent project, developed by players and not connected to Ankama in any way. It is not affiliated with, endorsed by, sponsored by or approved by Ankama Games.
Dofus, Ankama and the Ankama Launcher are trademarks of Ankama, used here only to say what this program works with. All artwork in this repository — icons, banners — is original to the project; no game asset is redistributed.
Ankama's terms are the authority on what is allowed with your accounts, not this README. If they ever say otherwise, they are right and this is wrong.
The releases page has an archive per platform, each holding the binaries and the installer below, so no Rust toolchain is needed. Building from a checkout does need one: Rust 1.89 or newer, which is where the standard library gained the file locking used to stop two processes registering the hotkeys at once.
The binaries are not code-signed. macOS will refuse a downloaded archive until
you clear the quarantine flag, xattr -dr com.apple.quarantine, and Windows
SmartScreen will warn once. Building from source avoids both.
The instructions below work either way: run the installer from the extracted archive, or from a checkout, where it builds first.
./packaging/install.shThat builds, then installs the binaries into ~/.local/bin, the icons into
~/.local/share/icons and the desktop entry into ~/.local/share/applications.
Installing is not optional on Linux, at least if you want global hotkeys.
The XDG desktop portal derives the application identity from the systemd scope
the launcher puts the process in, and it only accepts that identity if a
matching desktop entry exists and its Exec resolves to the binary that is
running. Start the bar from your application menu, not from a terminal; a
terminal-launched process inherits the terminal's identity, so the shortcuts end
up filed under it.
Two details that cost an afternoon to find, in case you fork this:
- The application id must not contain a hyphen. The portal parses the scope name
as
app-<id>-<random>, sodofus-tabis ambiguous with theapp-<launcher>-<id>-<random>form and resolves to an empty id, which the portal rejects withNotAllowed: An app id is required. Hence the reverse-DNSio.github.gambl3r08.DofusTab. - The desktop entry's
Execmust resolve to the running binary, either as an absolute path or as a bare name that is onPATH.
To run from a checkout without installing:
cargo build --release
./target/release/dofus-tab list # command line
./target/release/dofus-tab-bar # overlay bar (hotkeys will not register)./packaging/install-macos.shThat builds and assembles ~/Applications/DofusTab.app, generating the icon set
with iconutil along the way. Launch it from Spotlight or Launchpad.
The bundle matters for more than tidiness: its Info.plist sets LSUIElement,
which is what makes macOS treat Dofus-tab as a menu bar application rather than
putting it in the Dock. A bare binary works for testing but behaves like an
ordinary app.
None of this needs permissions. NSRunningApplication and Carbon hotkeys were
chosen precisely to avoid the Accessibility and Screen Recording prompts that a
window-level approach would have required.
No installer yet. Extract the release archive and run bin\dofus-tab-bar.exe,
or build from a checkout with cargo build --release and run
target\release\dofus-tab-bar.exe. Nothing has to be installed for the global
hotkeys to work — that requirement is specific to the Linux desktop portal.
dofus-tab list # show the detected clients as numbered tabs
dofus-tab focus 2 # focus a tab by slot
dofus-tab focus enutrof # ...or by part of its label
dofus-tab label 1 "Iop" # rename a tab (empty string restores the nickname)
dofus-tab save # persist the current order across restarts
dofus-tab daemon # register the global hotkeys and listen
dofus-tab doctor # report what the platform layer detectsExample:
$ dofus-tab list
1 someone#1546 account=62833850 pid=11825 window=0x220000a
2 Iop account=62845763 pid=12904 window=0x2400012
cargo run --release --bin dofus-tab-bardofus-tab-bar is a thin always-on-top strip with one button per detected
client. The tab of the focused client is highlighted, clicking a tab focuses it,
and the dotted grip on the left drags the bar around.
The position is remembered: it is written out once the bar has sat still for a moment after a move, not only when the application quits, so it survives a crash or a killed session as well as a clean exit. The same process also registers the global hotkeys, so one binary covers both ways of switching.
The bar re-scans every 750 ms on a worker thread, so the UI never blocks on an X11 round trip, and clients launched or closed behind its back appear and disappear on their own.
The bar has no title bar and no taskbar entry, so the system tray is where it lives. Its menu offers Settings, Show/hide bar and Quit.
Settings opens a second window with three sections:
- Tabs — rename a tab, or move it up and down. Both are stored under the account id, so they survive restarts; an empty name restores the launcher nickname. Reordering only touches the accounts that are running, and keeps the remembered position of any that are not.
- Layout — direction and wrap point, applied to the bar as you change them, with a preview of the resulting grid.
- Startup — start Dofus-tab with the desktop session. Applied immediately rather than on Save, because it writes a file outside our own config.
- Hotkeys — read-only, showing the trigger the desktop actually assigned to each slot. Read-only on purpose: once registered they belong to the desktop, and on Linux the portal will not let an application reassign them, so an editable field would silently do nothing.
Edits reach the background worker immediately, so a rename or a reorder shows up in the bar without a restart.
Windows and macOS use tray-icon, which wraps the native APIs. Linux does not:
tray-icon's Linux path needs libayatana-appindicator, which is not installed
everywhere and pulls in GTK plus a second main loop, so the StatusNotifierItem
D-Bus specification is spoken directly instead. Both funnel into the same
channel, so the bar sees one interface. The tray is optional — if the desktop
offers none, the bar and the hotkeys carry on working.
Tabs are laid out in a grid that wraps, so eight accounts do not become a thousand-pixel-wide strip. By default the bar fills downwards and wraps after four, which keeps it narrow:
| Accounts | Default grid |
|---|---|
| 2 | 1 column x 2 rows |
| 4 | 1 column x 4 rows |
| 5 | 2 columns x 4 rows |
| 8 | 2 columns x 4 rows |
Both the direction and the wrap point are configurable:
{
"overlay_orientation": "horizontal",
"overlay_wrap_at": 4
}vertical (the default) fills a column top to bottom before starting the next
one. horizontal fills a row left to right before starting the next one, so
eight accounts become 4 columns x 2 rows. Setting overlay_wrap_at above the
account count gives the old single-line bar back.
The window resizes itself to the grid, so it never reserves space it is not using.
Two window-manager details worth knowing:
- The bar forces itself onto X11 even in a Wayland session. Wayland has no
standard way for an ordinary toplevel to stay above other windows, so a
native-Wayland bar would sink behind the game. Going through XWayland lets the
window manager honour
_NET_WM_STATE_ABOVE, and costs nothing because the Dofus client is an XWayland client too. - The always-on-top hints are applied by our own EWMH backend rather than by the
GUI toolkit. winit maps
WindowLevel::AlwaysOnTopto something KWin does not act on, somake_overlaysets_NET_WM_STATE_ABOVE,SKIP_TASKBARandSKIP_PAGERdirectly. It both writes the property and sends the client message, because EWMH requires the former before the window is mapped and the latter after, and a caller cannot tell which side of the mapping it is on.
dofus-tab daemon registers Alt+1 … Alt+8 and focuses the matching tab,
whichever window currently has focus.
Hotkeys are bound to slots, not accounts: Alt+1 always focuses whatever is
in the first tab. That keeps muscle memory stable when an account is not logged
in, and it is what the portal requires — bindings are registered once, up front,
and cannot follow a changing window count. A fixed pool of eight is therefore
registered regardless of how many clients run; pressing a hotkey for an empty
slot does nothing.
Customise them in the config file (~/.config/dofus-tab/config.json on Linux):
{
"max_slots": 4,
"hotkeys": {
"1": "CTRL+F1",
"2": "CTRL+F2"
}
}Triggers use the freedesktop shortcuts syntax (ALT, CTRL, SHIFT, LOGO,
joined with +). They are preferred triggers: the desktop may reassign one
that clashes with an existing shortcut, and the user can always change it
afterwards from their system settings.
The obvious shape is a blocking listen(callback) on a worker thread, and that
is what X11 and Win32 want. macOS cannot do it, for the reason above. The trait
therefore exposes a non-blocking poll, and each backend hides its own
machinery behind it: the portal and Win32 run an internal thread and feed a
channel, macOS registers on the caller's thread. The bar polls once per frame,
the daemon in a small loop.
Win32 has the same constraint in miniature — RegisterHotKey and the message
pump must share a thread — so bind starts that thread and waits for it to
report what it managed to register.
An X11 key grab only fires while focus sits in an X11 or XWayland client. Dofus
is one, but a browser or chat client on a Wayland session is not, so a grab
would silently stop working depending on which window is focused. The XDG
GlobalShortcuts portal has no such blind spot.
The trade-off is desktop support: KDE Plasma implements the portal, GNOME's
support has lagged, and xdg-desktop-portal-wlr ships no implementation at all.
Hyprland and Sway will need their own IPC-based fallback.
The desktop asks for confirmation the first time shortcuts are registered. Declining that prompt puts KDE into a state worth knowing about:
BindShortcutsfails immediately with portal response code 1.- The shortcuts are nevertheless recorded, with the preferred trigger as their
default and no active key (
focus-slot-1=none,Alt+1,…inkglobalshortcutsrc). - Later runs succeed without prompting again, because the entries already exist — so registration reports success while nothing ever fires.
dofus-tab doctor exists to make that visible: it calls ListShortcuts and
prints the trigger the desktop actually assigned to each action, flagging any
that read NOT ASSIGNED. dofus-tab daemon prints the same warning at startup.
The fix is to assign the keys once from the desktop's keyboard shortcut
settings. Deleting the focus-slot-* lines from kglobalshortcutsrc is not
enough on its own, and kglobalaccel's setShortcut D-Bus method refuses to
assign keys to portal-owned actions.
The Dofus 3 window title is a constant "Dofus" — it contains neither the
account nor the character name, so titles are useless for identification.
Dofus-tab goes through the Ankama Launcher instead. The launcher starts every client with a per-account log file, and stores the account nicknames in its own settings file:
X11 window
-> _NET_WM_PID -> 11825
-> /proc/<pid>/cmdline, "-logFile" argument -> dofus.62833850.log
-> account id -> 62833850
-> <config>/zaap/Settings, USER_ACCOUNTS -> someone#1546
The account id is the only identifier that survives a restart, so labels and tab order are stored under it.
This relies on undocumented launcher behaviour and may break when Ankama ships an update, so identification degrades gracefully: a user-assigned label wins over the launcher nickname, and a window whose account cannot be resolved still gets a tab.
Privacy. The launcher settings file also contains the login e-mail and
session material. Dofus-tab reads only id, nickname and tag, and never
writes to that file.
A hexagonal split, so the hard part — the operating system integration — stays isolated and unit-testable:
crates/
dofus-core/ domain model and ports (traits). No OS-specific code.
account.rs Ankama account value objects
identity.rs command line -> account id
launcher.rs Ankama Launcher settings -> nicknames
config.rs persisted labels and order
registry.rs windows + identity + config -> ordered tabs
dofus-platform/ OS adapters implementing the ports
linux/x11.rs EWMH backend (X11 sessions and XWayland clients)
linux/portal.rs global hotkeys via the XDG GlobalShortcuts portal
windows/window.rs Win32 backend
windows/hotkeys.rs global hotkeys via RegisterHotKey
macos/window.rs AppKit backend (applications, not windows)
macos/hotkeys.rs stub; see the macOS notes
process.rs reading another process' command line
dofus-cli/ the command line interface
dofus-gui/ the always-on-top bar (egui), plus the hotkey listener
engine.rs worker thread owning every platform handle
app.rs the bar itself
tray.rs system tray icon and menu
settings.rs settings window
dofus-core defines two ports: WindowManager (enumerate and focus) and
ProcessInspector (read a command line). Every backend implements the same
traits, which keeps the ordering and naming logic testable without a display
server, and keeps the future GUI independent of the backend in use.
The Dofus 3 client is a Unity build whose SDL layer selects the X11 video driver, so the game runs through XWayland even inside a Wayland session (verified on KDE Plasma 6). The EWMH backend therefore covers both X11 and Wayland desktops today.
That is a default, not a guarantee. A client started with -force-wayland or a
future Unity release would need a compositor-specific backend: KWin exposes
workspace.windowList() and workspace.activeWindow over D-Bus, Hyprland has
its IPC socket, and wlroots compositors implement
wlr-foreign-toplevel-management. GNOME is the hard case, as it offers no
window-control interface to third-party applications without a Shell extension.
Global hotkeys should go through the XDG GlobalShortcuts portal rather than
XGrabKey: an X11 grab only fires while focus sits in an X11 or XWayland
client, so it would miss keystrokes typed in native Wayland applications.
Implemented but not yet run on a Windows machine; it is verified only by
cross-compilation (cargo check --target x86_64-pc-windows-msvc) and by the
unit tests that cover its pure logic. Treat it as untested until someone
launches it there.
Two differences from Linux are worth knowing:
- The window class is useless. Every Unity game reports
UnityWndClass, so the backend fillsGameWindow::app_classwith the process image name (Dofus.exe) instead. That matches what X11 exposes throughWM_CLASS, which is itself derived from the executable, so the shared filtering indofus-coreneeds no platform branch. - Focus needs a handover. Windows only lets the foreground process give
focus away, so
focusattaches its input queue to the current foreground thread withAttachThreadInputfor the duration of the call. Minimised windows are restored first, since they cannot take focus otherwise.
Global hotkeys use RegisterHotKey, which needs no permission prompt but does
require registration and the message loop to run on the same thread — so
bind only records the bindings and listen performs the registration. A
combination already owned by another application fails to register and is
reported by doctor as having no trigger, the same way an inert portal shortcut
is on Linux.
Command lines go through sysinfo rather than hand-rolled
NtQueryInformationProcess plus ReadProcessMemory on the PEB. That is the
part of this platform most likely to be subtly wrong — WOW64 mismatches, access
rights — and the least pleasant to debug remotely.
Window switching is implemented but not yet run on a Mac; like Windows, it
is verified only by cross-compilation (cargo check --target aarch64-apple-darwin). Global hotkeys are not implemented — see below.
The backend enumerates applications, not windows, and uses the process id as the window handle. That is not a shortcut; it is what the platform rewards:
- Every Dofus client is its own process, so one application is one account, and focusing the application is exactly what the user wants.
NSRunningApplication::activateneeds no permission, whereas per-window control means the Accessibility API, which the user must grant by hand and which silently returns nothing until they do.- Reading window titles through
CGWindowListCopyWindowInfoneeds the Screen Recording permission, and titles are useless for identification anyway.
The limitation is that a client showing several windows would collapse into one tab. Dofus shows one main window per client, so this holds in practice.
make_overlay is a no-op on macOS: unlike X11, winit's always-on-top window
level does reach AppKit, so there is nothing to patch up.
Hotkeys work in the bar, not in the daemon. Carbon's RegisterEventHotKey
needs no permission, but it delivers through the application's event target,
which only a CFRunLoop on the main thread pumps. The overlay bar has one —
winit runs it — so it registers and polls from there. dofus-tab daemon has no
such loop, so on macOS it refuses up front and points at the bar rather than
waiting for keypresses that can never arrive.
This is why [HotkeyManager] polls rather than blocking; see below.
The account identification chain is expected to carry over unchanged, because
the launcher is the same Electron application on all three platforms and stores
its settings at ~/Library/Application Support/zaap/Settings.
cargo test
cargo clippy --all-targets -- -D warnings
cargo fmt --allCI runs the tests and clippy natively on Linux, Windows and macOS. That matters more here than in most projects: the Windows and macOS backends are developed on Linux, so without native jobs they would only ever be type-checked.
Platform code is exercised through fakes rather than the real system wherever
possible: Worker takes its window backend and process inspector as trait
objects so the tests can drive it without an X server, and the settings that
touch the filesystem are split so their writing half can be pointed at a
throwaway directory.
dofus-tab daemon and dofus-tab-bar both register the same global shortcuts.
Run together they fight over them: on Linux the portal hands the same slots to
two sessions and every keypress switches twice, on Windows the second
RegisterHotKey simply fails. Neither reports anything useful on its own.
Whichever starts second therefore refuses to register, and says so. The guard is an advisory file lock rather than a pid file, because the operating system releases it however the holder dies — a pid file left by a crash would lock the user out until they deleted it by hand.
The bar does everything the daemon does and adds the tray, so prefer it.
Platform code that cannot be run locally is still type-checked and linted, which catches most of what compiling would:
rustup target add x86_64-pc-windows-msvc aarch64-apple-darwin
cargo clippy --workspace --all-targets --target x86_64-pc-windows-msvc -- -D warnings
cargo clippy --workspace --all-targets --target aarch64-apple-darwin -- -D warningsAll code, comments, documentation and tests are written in English.
See CONTRIBUTING.md for the house style and what is in scope. Two things are wanted more than patches right now: someone running the Windows backend on real hardware, and native Wayland backends for the compositors where the game does not go through XWayland.
Security reports go through SECURITY.md, privately, rather than the issue tracker.
-
dofus-coredomain model with account identity and stable ordering - Linux EWMH backend (X11 and XWayland)
-
dofus-tabcommand line interface - Global hotkeys via the XDG
GlobalShortcutsportal -
.desktopfile and installer, so the portal sees our own app id - Tray icon with a settings window
- Always-on-top overlay bar
- Windows backend (Win32) — written and cross-compiling, not yet run
- Run and fix the Windows backend on real hardware
- macOS window backend (AppKit)
- Run the macOS backend on real hardware
- macOS global hotkeys (bar only; the daemon has no run loop)
- macOS
.appbundle installer - CI building and testing natively on all three platforms
- Rename and reorder tabs from the settings window
- Native Wayland backends (KWin, Hyprland, wlroots)
- Tray icon on Windows and macOS
- Tests for the overlay bar and the command line
- Embed the
.icoin the Windows executable - Autostart entry (Linux; Windows and macOS still to do)
- Refuse to register hotkeys twice at once
Copyright (C) 2026 Roberto Lozada.
Dofus-tab is free software under the GNU General Public License, version 3 or later. See LICENSE for the full text.
You may use it, study it, change it and share it. What the licence asks in return is reciprocity: if you distribute a modified version, it has to be free software too, under the same terms, with its source available. That is a deliberate choice — this project exists because the tools that came before it were closed, and it should not be possible to take this work, close it and sell it back to the same players.
Using it, including in a commercial setting, costs nothing and requires nothing. The obligation only ever applies to whoever distributes a derivative.
That licence covers this project's own code and artwork. It says nothing about Ankama's trademarks, which are not ours to license — see Not affiliated with Ankama.