Some games ship an `account_name.txt` file somewhere under the unpacked
`local/` tree (location varies per game). After install or update, write
the configured username into the first such file we find so the game
launches under the user's account instead of whatever default the
archive contains.
The search is a deterministic alphabetical DFS rooted at the install
staging dir (`.local.installing/`, which becomes `local/` on rename),
stopping at the first regular-file match. Symlinks named
`account_name.txt` are skipped (`is_file()` is false for symlinks on
Linux), so a hostile archive can't redirect the write outside the game
tree. If no `account_name.txt` exists anywhere in the install, the step
is a no-op. If the write fails, the existing install rollback (cleanup
of staging on fresh installs, restore from backup on updates) handles
it — no partial state is left behind.
The username flows from the Tauri layer, where it is already sanitized
by `sanitize_username`, down through `PeerCommand` variants
(`InstallGame`, `DownloadGameFiles`, `DownloadGameFilesWithOptions`)
into `install`/`update`, which now take an `Option<&str> account_name`.
For the "install game that isn't downloaded yet" path the username has
to bridge the async gap between the `GetGame` / `FetchLatestFromPeers`
request and the eventual `GotGameFiles` event; we park it in a
per-game-id map on `LanSpreadState` and pop it when forwarding the
download command. The map is also cleared defensively on
`cancel_download`, `DownloadGameFilesFailed`, and
`DownloadGameFilesAllPeersGone` so a stale entry can't bleed into a
subsequent install with a different username.
`PeerCommand` is the in-process command channel, not the wire protocol;
no on-wire types changed, so the "one wire version" policy is
preserved. The peer-cli harness keeps passing `account_name: None`
since it tests peer interop, not user-facing settings.
# Test Plan
Unit tests in `crates/lanspread-peer/src/install/transaction.rs`:
- `install_overwrites_first_account_name_file` — unpacker creates
`a/account_name.txt` and `z/account_name.txt`; after install with
username "Alice", `a/` is overwritten and `z/` is left untouched,
pinning the sorted-DFS "first match wins" behavior.
- `install_account_name_missing_file_is_noop` — install with a
username but no `account_name.txt` anywhere in the archive
succeeds and creates no spurious file.
Manual GUI check: in Settings, set a username; install a game whose
archive contains `account_name.txt`; open `local/` and confirm the
file now holds the configured username. Repeat for the update flow
(install, change username, click update).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The design update moved game-folder configuration out of launcher chrome and
into Settings > Library. Follow that contract in the runtime UI without
changing the existing storage or Tauri directory commands.
The top bar now leaves its right edge for the kebab menu. Settings owns a new
Game folder row that shows a valid selected path with a neutral Change button,
or the red Not set state with a stronger Choose button when no accessible
directory is configured. Both the empty-library state and the Settings row
still use the existing native directory picker, so existing saved paths and
rescans keep their current behavior.
Keep useGameDirectory as the directory-state owner and expose the shared
hasGameDirectory boolean from that hook so the grid and Settings field agree on
what counts as configured.
Test Plan:
- git diff --cached --check
- just frontend-test
- just build
Refs: 62b409f4bfc4995c25461776107d28f52b24f30e
Unpack logs lived only in memory, so closing the app dropped history.
Unrar progress also flooded stdout with carriage-return redraws, which
made the log viewer noisy and hard to search.
Persist the last twenty entries to unpack-logs.json under the app data
directory, load them on startup, and rewrite stdout/stderr through a
small terminal-sequence cleaner (CR/LF, backspace, control chars) before
storage and display. Sort the unpack-logs window newest-first by finish
or start time.
Test plan:
- cargo test -p lanspread-tauri-deno-ts -- terminal_log unpack_log
- Run an unpack, restart the app, open unpack logs: prior entries remain
- Confirm progress lines collapse to final text instead of spam
Co-authored-by: Cursor <cursoragent@cursor.com>
Move the design contract for choosing the library game folder out of the top
bar and into Settings > Library. The launcher chrome now reserves the right
edge for the kebab/menu controls, while Settings owns the required folder path
and its set/unset states.
The reference mock now persists `gameFolder: string | null` instead of a
boolean, adds an exercisable GameFolderField, and includes matching CSS so the
prototype reflects the documented interaction. This is still design/reference
only; no runtime Tauri settings code changes here.
Test Plan:
- git diff --cached --check
Refs: none
Typing the first character into the launcher search field used to insert the
clear button into the flex row, which widened the field after focus. The clear
button now stays mounted in the row so its slot is part of the empty field's
layout, while the empty state hides and disables it.
Users now get the wider steady-state search width immediately, and the control
does not jump when the clear affordance becomes visible.
Test Plan:
- git diff --check
- just frontend-test
- just build
Refs: user-reported search field jump
Ctrl+F is a common search shortcut and should open the launcher search just
like the existing slash shortcut. Handle it in the same SearchField keydown
listener so the behavior stays scoped to the topbar search component.
The shortcut is ignored while the user is already typing in another input or
textarea, matching the existing slash behavior. When handled, it prevents the
webview's browser find UI and focuses the app search field instead.
Test Plan:
- `just frontend-test`
Refs: none
Follow the latest top-bar spec for the Game Folder control. The button stays
as only the folder icon and short label, with the full path in tooltip and aria
text, and now sits in the app-level control group with the kebab menu on the
far-right edge of the wide top bar.
This keeps Sort as the right-zone lead control next to the centered search
cluster while treating Game Folder and the kebab menu as a tight trailing pair.
The narrow fallback still flows in source order: sort, game folder, kebab.
Test Plan:
- git diff --check
- rg -n "dirbtn-status-dot|status dot|green status|red status" crates/lanspread-tauri-deno-ts/src
- just frontend-test
- just build
Validate the persisted game directory before sending it to the backend or
showing library content for it. When the saved path no longer exists, the
launcher keeps the top bar visible but shows the folder picker empty state
and labels the Game Folder button as an unset folder.
This keeps stale local data from being presented as the active library when
an old path is deleted or disconnected.
Test Plan:
- git diff --check
- just frontend-test
- just build
Implements the v2 design's game-folder button in the Tauri launcher.
The previous control squeezed the full game-directory path into the
top-bar button as truncated monospace (e.g.
`…s/Desktop/eti_games_AFTER_LAN_2025`). In practice the leading
ellipsis rarely showed the meaningful part of the path, ate
horizontal space the new 3-zone top bar needs for its primary
controls, and competed with the filter / search / sort cluster for
attention.
The button now communicates the *state* of the configuration at a
glance — an icon + short label + colored status dot — while the full
path moves into the native tooltip and `aria-label`, where it stays
one mouseover (and screen-reader friendly) away.
Two visual states, both 36 px tall and sharing the surface of the
other top-bar controls:
- **Set & valid** (`.dirbtn-set`) — label `Game folder`, green dot
(`--ok`) with a soft glow, default border, tooltip = full path.
- **Not set / invalid** (`.dirbtn-unset`) — label `Set game folder`,
red dot (`--danger`) with a soft glow, a red-tinted border, and a
faint red wash on hover so the bad state reads as "this is what
you need to fix". Tooltip = `Please select a game folder`.
`DirectoryButton` now takes `path: string | null` and picks the
state from `!!(path && path.trim())`. The `aria-label` carries the
full state in words (`Game folder: <path>` / `Set game folder`) so
screen readers don't have to interpret the colored dot.
Note: in the current `MainWindow`, `gameDir` is gated upstream — if
no directory is selected, `NoDirectoryState` is shown instead of the
top bar, so the unset state will only surface here if we later
validate disk existence and clear `gameDir`. The button accepts a
nullable path anyway, so it's ready when that check lands.
`truncatePath` in `lib/format.ts` was the only caller-less helper
left behind and is removed.
Test Plan
- `npx tsc --noEmit` from the frontend crate — clean.
- `just frontend-test` — passes.
- Manual: `just run`, pick a valid game directory, and confirm the
top-bar button reads `Game folder` with a green dot; hovering it
reveals the full path in the OS tooltip. Inspect the DOM and
confirm `aria-label` reads `Game folder: <path>`. (The unset
variant currently isn't surfaced by `MainWindow`; eyeball it via
DevTools by toggling the `dirbtn-set` class to `dirbtn-unset`.)
Implements the v2 design's top-bar reorganization in the Tauri
launcher. The bar was previously a flat flex row that let the search
field drift left or right depending on filter / sort widths; now it's
a 3-column CSS grid with the search field pinned to the geometric
center of the window.
- `.topbar` becomes `display: grid` with
`grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr)` and a
16 px column gap. The middle (auto) column holds only the search,
capped at `flex: 0 1 360px` so it cannot push into the side columns.
- The left zone is `flex; justify-content: space-between`: brand
pins far-left, filter pills hug the search. The filter pills are
now grouped with the search semantically (they scope it) instead of
floating next to the brand.
- The right zone mirrors that: sort hugs the search, kebab pins
far-right, with the directory button between them.
- A `@container launcher (max-width: 1100px)` rule collapses the
layout back to a single nowrap flex row at narrow widths — the
geometric centering doesn't read at small widths and would force
awkward truncation, so we abandon it rather than fight it. The
launcher root opts into container queries via
`container-type: inline-size; container-name: launcher`.
`TopBar.tsx` now wraps the existing children in `.topbar-left`,
`.topbar-center`, `.topbar-right` (plus `.topbar-left-trail` /
`.topbar-right-lead` for the inner space-between alignment), but each
control component is otherwise untouched.
Test Plan
- `just frontend-test` — passes.
- `npx tsc --noEmit` from the frontend crate — clean.
- Manual: run `just run`, confirm the search input's horizontal center
matches the window's horizontal center across the standard launcher
width. Shrink the window below 1100 px and confirm the row collapses
to a single left-to-right strip with no overlap or wrapping.
The previous design squeezed the full game-directory path into the
top-bar button as truncated `ui-monospace` (e.g.
`…s/Desktop/eti_games_AFTER_LAN_2025`). In practice the leading-ellipsis
truncation rarely showed the meaningful part of the path on real-world
configurations, ate horizontal space the new 3-zone top bar needs for
its actual primary controls, and competed with the filter / search /
sort cluster for attention.
Replace the inline path with an icon + short label + colored status
dot. The full path moves into the tooltip and `aria-label`, where it's
still one mouseover (and screen-reader friendly) away. The button now
communicates the *state* of the configuration at a glance — which is
what users actually need.
Two visual states, both 36 px tall with the same surface as the other
top-bar controls:
- **Set & valid** — label `Game folder`, green dot (`--ok`) with a
soft glow, default border, tooltip = full path.
- **Not set / invalid** — label `Set game folder`, red dot (`--danger`)
with a soft glow, a red-tinted border, and a faint red wash on
hover so the bad state reads as "this is what you need to fix".
Tooltip = `Please select a game folder`.
"Invalid" (a path is stored but doesn't exist on disk) is collapsed
into the same visual state as "not set" — the user's required action
is identical (open the picker, pick a folder), so a third state
isn't worth the visual budget yet. If we later want to surface a
*last-known* path so the user can re-attach an external drive,
introduce a distinct missing state then.
Implementation notes:
- `DirectoryButton` now takes a single `path: string | null` prop and
picks state from `!!(path && path.trim())`. Children are
`Icon.folder`, the label, and an 8 px `.dirbtn-status-dot` sibling
— the dot is an inline flex sibling, not a corner badge, because
the button is now wider than tall and a corner pin would feel
misplaced.
- `.dirbtn` is `inline-flex` with `padding: 0 14px 0 12px`, gap 8 px,
`white-space: nowrap`, and `flex-shrink: 0`. The `max-width: 360px`
cap from the path-truncation era is gone — the button is now
intrinsically sized.
- Dot glow uses `box-shadow: 0 0 6px color-mix(...)` so it still
reads through the launcher's translucent top-bar background.
- The Tweaks panel grows a dev-only `Game folder set` toggle (under
the new *Library* section) that flips a `gameFolderSet` flag wired
into the Launcher, so reviewers can see both states without
fiddling with real filesystem state. The README explicitly calls
this out as **dev-only** — production state comes from the
settings store, not a user-facing toggle.
The README gains a new *Game-folder button* section with the full
spec, a state table, and a rationale paragraph; the "Changes since v2"
list and the interactions list are updated to reflect the new label
and behavior.
Test Plan
- Open `design_reference/SoftLAN Launcher.html` and locate the Tweaks
panel's *Library → Game folder set* toggle.
- With the toggle **on**: the top-bar button shows `Game folder`
with a green dot; hovering the button reveals the full mock path
in the native tooltip.
- With the toggle **off**: the label switches to `Set game folder`,
the dot turns red, the border picks up a red tint, and hovering
the button reveals `Please select a game folder`.
- Inspect the button with a screen reader / DevTools accessibility
pane: the `aria-label` should read `Game folder: <path>` when set
and `Set game folder` when unset.
The single-row top bar was a flat flex row, which made the search field
drift left or right depending on how wide the surrounding clusters were.
Rework it as a 3-column CSS grid (left zone, search, right zone) so the
search input lands at the geometric center of the window regardless of
the side-zone widths.
Mechanics:
- `.topbar-single` becomes `display: grid` with
`grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr)` and a 16 px
column gap. The middle (auto) column holds only the search, which is
capped at `flex: 0 1 360px` so it cannot push into the side columns.
- The left and right zones are flex containers with
`justify-content: space-between`, so brand pins far-left while filter
pills hug the search, and sort hugs the search while the kebab pins
far-right. The filter pills are now grouped semantically with the
search (they scope it) instead of floating next to the brand.
- A `@container launcher (max-width: 1100px)` rule collapses the layout
back to a single nowrap flex row at narrow widths — the geometric
centering stops reading at narrow widths and would otherwise force
awkward truncation, so we abandon it rather than fight it.
- The `.launcher` root opts into container queries via
`container-type: inline-size; container-name: launcher`.
`launcher.jsx` now wraps the existing children in `.topbar-left`,
`.topbar-center`, `.topbar-right` (plus a `.topbar-left-trail` /
`.topbar-right-lead` for the inner space-between alignment), but each
control component is otherwise untouched.
The README's "Top bar" section is rewritten to spec the new layout, and
a new "Changes since v2" section calls it out at the top of the
handoff. The game-directory button line is left as-is in this commit
and addressed separately.
Test Plan
- Open `design_reference/SoftLAN Launcher.html` in a static server and
inspect variant A at full width: the search input's horizontal center
should match the window's horizontal center, regardless of accent /
density choices in the Tweaks panel.
- Shrink the launcher artboard below 1100 px and confirm the row
collapses to a single left-to-right strip with no overlap.
The detail view already exposes View Files while a game is downloaded,
installed, or actively downloading. During the download-to-install handoff,
the backend can report Installing before the local downloaded/installed flags
have settled in the next snapshot, which briefly hid the file viewer button.
Treat Installing as another state where the game root should remain reachable.
This keeps the detail view stable during the handoff without changing backend
file-opening behavior.
Test Plan:
- just frontend-test
The launcher settings dialog now shows a Build-Nr value in its footer so users
can identify the exact application build without manually updating a number
before release.
The value is generated by Vite when the frontend bundle is built and injected
as an import.meta.env constant. Using the current millisecond timestamp keeps
the number numeric and monotonic for normal builds without a checked-in counter
file or any extra build-state mutation. The tradeoff is that the value follows
the build machine clock rather than a central sequence service.
The footer layout now keeps the build number on the lower left and the Done
button on the lower right.
Test Plan:
- just frontend-test
- just build
- git diff --cached --check
Refs: local user request
Manual download cancellation uses the same internal error path as transfer
failures after the terminal-event ordering fix. That made the Tauri UI receive
DownloadGameFilesFailed and show a red failure state even though the user had
asked for the cancellation.
Keep a clone of the cancellation token in the download task and check it after
the transfer task returns an error. Cancelled downloads still refresh local
state and clear active operation tracking, but they no longer emit the failed
event. Real, uncancelled errors continue to send DownloadGameFilesFailed.
Add unit coverage for both branches so the UI-facing event contract stays
explicit.
Test Plan:
- just fmt
- just test
- just clippy
- git diff --check
Refs: manual cancel regression from app-state follow-up
Move launcher-owned metadata from game roots into the configured peer state
area. Peer identity, the local library index, install intent logs, and setup
markers now live under app/CLI state instead of being written beside games.
The Tauri shell passes its app data directory into the peer, and the peer CLI
runs the same path through its explicit --state-dir.
Add a dedicated pre-start migration phase for legacy files. It migrates the
old global library index, per-game install intents, and the old first-start
marker into app state, then deletes legacy files only after the replacement
write succeeds. Normal scan, install, recovery, and transfer paths no longer
read legacy state files.
Rename the old first-start meaning to setup_done and only set it after
launching game_setup.cmd. Start/setup scripts keep the shared argument shape,
while server_start.cmd now uses cmd /k and a visible window so server logs stay
open for inspection.
While validating the Docker scenario matrix, make download terminal events
come from the handler after local state refresh and operation cleanup. This
makes download-finished/download-failed safe points for immediate follow-up CLI
commands. Also update the multi-peer chunking scenario to use a sparse archive
large enough to actually span multiple production chunks.
Test Plan:
- just fmt
- just test
- just frontend-test
- just build
- just clippy
- git diff --check
- python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py
Refs: local app-state migration discussion
Add launcher profile settings for username and language, then thread those
values into the Windows script launch path. The game setup, game start, and
server start scripts now share the same argument shape:
- game path: local
- game id
- language: en or de
- player name
Expose a local can_host_server flag in the games payload by checking for
server_start.cmd in an installed game's root directory. The detail modal uses
that flag to show Start Server only for installed games with the script, and
the new start_server command invokes server_start.cmd with the same sanitized
settings used by game_setup.cmd and game_start.cmd.
Test Plan:
- just fmt
- just test
- just frontend-test
- just build
- just clippy
- git diff --check
Refs: design/README.md
Document the profile settings added to the launcher design and the new
Start Server detail action. The settings contract now includes a persisted
username and language choice, and the game detail overlay shows Start Server
only for installed games that can host a dedicated server.
The reference mock now includes the matching Profile controls, a server icon,
server-capable sample catalog entries, and the updated detail/settings
artboards so implementation can follow the selected design direction.
Test Plan:
- git diff --cached --check
Refs: design/README.md