Skip to content

Add Sort As Display - #241

Open
kuusei wants to merge 4 commits into
SteamClientHomebrew:mainfrom
kuusei:add-sort-as-display
Open

Add Sort As Display#241
kuusei wants to merge 4 commits into
SteamClientHomebrew:mainfrom
kuusei:add-sort-as-display

Conversation

@kuusei

@kuusei kuusei commented Aug 30, 2026

Copy link
Copy Markdown

Sort As Display

Sort As Display copies Steam sort name onto the library sidebar display_name at runtime. Steam already uses sort name for ordering; this plugin also uses it as the visible list name.

Repository: https://github.com/kuusei/steam-sort-as-display

Pinned commit: dd71d51 (0.2.1)

Task Checklist

Developer

  • I am the original author or an authorized maintainer of this plugin.
  • I have complied with all license requirements for the libraries used, including providing appropriate notices where necessary.
  • My plugin is fully open source and does not depend on any external paid services, except for widely trusted and well-known platforms. Additionally, neither I nor anyone associated with me profits from any such services.

Plugin Functionality

  • I have tested the plugin on both the Stable and Beta Steam update channels.
  • My plugin is unique, or provides additional or alternative functionality to plugins already on the store.

Unlike Game Renamer, this plugin does not keep a custom rename table. It uses the sort name already set in Steam properties as the library list display name.

Backend Configuration

  • No: I use a standard Millennium Python backend in my plugin.
  • No: I use custom binaries or rely on external FOSS projects that are not part of Millennium's Starlight plugin environment.

The plugin uses Millennium's Lua backend and a TypeScript frontend. It does not bundle or execute custom native binaries.

Community Contribution

  • I have tested and left feedback on two other plugin pull requests.
  • I have added links to those feedback comments in this PR.

#243
#235

Testing Instructions

  1. Install and enable Sort As Display, then restart Steam.
  2. In the library sidebar, confirm titles match the Sort As field from Properties.
  3. Change Sort As on a game and confirm the sidebar name updates without clicking another game first.
  4. Restart Steam and confirm sort names still apply after library load.
  • Verified by a third party on Steam Client Stable.
  • Verified by a third party on Steam Client Beta.

@kuusei
kuusei marked this pull request as ready for review August 30, 2026 13:27
@ricewind012

Copy link
Copy Markdown
Collaborator

remove @steambrew/{api,webkit} from your dependencies

@kuusei

kuusei commented Aug 31, 2026

Copy link
Copy Markdown
Author

remove @steambrew/{api,webkit} from your dependencies

Thanks — I’ve removed @steambrew/api and @steambrew/webkit from the dependencies.
Based on feedback from testers, I’m also fixing two issues related to original-name search and restoring the display name after clearing a custom sort name. The changes are currently being tested on the dev branch. Once testing is complete, I’ll update the pinned commit.

@kuusei

kuusei commented Aug 31, 2026

Copy link
Copy Markdown
Author

remove @steambrew/{api,webkit} from your dependencies

Thanks — I’ve removed @steambrew/api and @steambrew/webkit from the dependencies. Based on feedback from testers, I’m also fixing two issues related to original-name search and restoring the display name after clearing a custom sort name. The changes are currently being tested on the dev branch. Once testing is complete, I’ll update the pinned commit.

The pinned commit is now updated. It includes the unused-dependency removal, plus the original-name search fix and restoring the display name after a custom sort name is cleared.

@kuusei kuusei mentioned this pull request Aug 31, 2026
9 tasks
@Jidos86

Jidos86 commented Aug 31, 2026

Copy link
Copy Markdown

Reviewed at ce876f2 (reciprocal — @kuusei reviewed #243, and toward the Community Contribution requirement). Read the frontend end to end; didn't run it on a real client, so the runtime notes are "please verify".

Bugs

1. Hooks are never removed — disabling the plugin doesn't stop it.
onDismount calls runtime.stop(), which only clears the poll interval. OverviewSync replaces m_mapApps.set and SetCustomSortAs with closures over the originals (replaceProperty / hookAppMap / hookCustomSortName) and nothing ever restores them. After the user disables the plugin, Steam keeps calling the wrappers for the rest of the session: this.sync() still runs on every map write and display names stay overridden until a full Steam restart. replaceProperty should return an unpatch function, OverviewSync a dispose(), and PluginRuntime.stop() should call it.

2. hookCustomSortName gives up after one failed attempt.

const replaced = replaceProperty(store, "SetCustomSortAs", ...);
console[replaced ? "log" : "warn"](...);
this.setCustomSortAsHooked = true;   // set even when replaced === false

hookAppMap does if (!replaced) return; before setting its flag, so it retries on the next 200 ms tick. hookCustomSortName sets the flag unconditionally. If that property isn't writable yet on the first tick that wins the race, the SetCustomSortAs hook is skipped for the whole session and editing a sort name in properties won't live-update the sidebar (only a restart will). Mirror the hookAppMap guard.

3. Poll interval has no cap.
start() runs tick() every 200 ms until mapHooked && appliedOnce. If m_mapApps.set ever stops being hookable (Steam internals change), mapHooked stays false and ensureHooks() + apply() run 5×/s for the entire session. A max-attempts / timeout that logs and stops would bound the failure.

Please verify on a real client

4. cloneOverview may drop fields.
Object.assign(new Constructor(), app) copies only own-enumerable properties. Steam overview objects are MobX-ish and often keep state in non-enumerable / accessor form; if any of that is lost, the cloned sidebar entry could render with missing data (icon, metadata). Worth diffing a cloned vs. original overview for a game that has a custom sort name.

5. m_mapApps.set is a hot path.
The hook does a synchronous GetCustomSortAs(appid) on every m_mapApps.set, and Steam writes that map constantly (metadata refreshes, download progress, …). For most apps it's one call + early return, but on a large library or during a big download it's worth measuring.

Minor

  • backend/main.luacall_frontend_method("apply_now"), but export const apply_now has no /** @ffi */ annotation — if it isn't registered the kick is a silent no-op (the poll loop covers it, so low impact, but the pcall warn will fire every launch).
  • icon: <></> — an empty fragment; most plugins pass a real IconsModule icon.
  • readCustomSortNames calls GetByPrefix(""), scanning the whole cloud namespace and JSON.parse-ing every value. If sort names have a known key prefix, use it.

Overall the design (in-memory only, append original name to sort_as for search, restore on clear) is sound and the tests cover the tricky restore-after-clear path well. #1 and #2 are the ones I'd fix before merge.

@Jidos86 Jidos86 mentioned this pull request Aug 31, 2026
@kuusei

kuusei commented Aug 31, 2026

Copy link
Copy Markdown
Author

Reviewed at ce876f2 (reciprocal — @kuusei reviewed #243, and toward the Community Contribution requirement). Read the frontend end to end; didn't run it on a real client, so the runtime notes are "please verify".

Bugs

1. Hooks are never removed — disabling the plugin doesn't stop it. onDismount calls runtime.stop(), which only clears the poll interval. OverviewSync replaces m_mapApps.set and SetCustomSortAs with closures over the originals (replaceProperty / hookAppMap / hookCustomSortName) and nothing ever restores them. After the user disables the plugin, Steam keeps calling the wrappers for the rest of the session: this.sync() still runs on every map write and display names stay overridden until a full Steam restart. replaceProperty should return an unpatch function, OverviewSync a dispose(), and PluginRuntime.stop() should call it.

2. hookCustomSortName gives up after one failed attempt.

const replaced = replaceProperty(store, "SetCustomSortAs", ...);
console[replaced ? "log" : "warn"](...);
this.setCustomSortAsHooked = true;   // set even when replaced === false

hookAppMap does if (!replaced) return; before setting its flag, so it retries on the next 200 ms tick. hookCustomSortName sets the flag unconditionally. If that property isn't writable yet on the first tick that wins the race, the SetCustomSortAs hook is skipped for the whole session and editing a sort name in properties won't live-update the sidebar (only a restart will). Mirror the hookAppMap guard.

3. Poll interval has no cap. start() runs tick() every 200 ms until mapHooked && appliedOnce. If m_mapApps.set ever stops being hookable (Steam internals change), mapHooked stays false and ensureHooks() + apply() run 5×/s for the entire session. A max-attempts / timeout that logs and stops would bound the failure.

Please verify on a real client

4. cloneOverview may drop fields. Object.assign(new Constructor(), app) copies only own-enumerable properties. Steam overview objects are MobX-ish and often keep state in non-enumerable / accessor form; if any of that is lost, the cloned sidebar entry could render with missing data (icon, metadata). Worth diffing a cloned vs. original overview for a game that has a custom sort name.

5. m_mapApps.set is a hot path. The hook does a synchronous GetCustomSortAs(appid) on every m_mapApps.set, and Steam writes that map constantly (metadata refreshes, download progress, …). For most apps it's one call + early return, but on a large library or during a big download it's worth measuring.

Minor

  • backend/main.luacall_frontend_method("apply_now"), but export const apply_now has no /** @ffi */ annotation — if it isn't registered the kick is a silent no-op (the poll loop covers it, so low impact, but the pcall warn will fire every launch).
  • icon: <></> — an empty fragment; most plugins pass a real IconsModule icon.
  • readCustomSortNames calls GetByPrefix(""), scanning the whole cloud namespace and JSON.parse-ing every value. If sort names have a known key prefix, use it.

Overall the design (in-memory only, append original name to sort_as for search, restore on clear) is sound and the tests cover the tricky restore-after-clear path well. #1 and #2 are the ones I'd fix before merge.

Thanks for the review — I’ve pinned dd71d51 (0.2.1) with the three runtime fixes:

Hooks are removed on disable (onDismount → dispose()), and official display_name / sort_as are restored so the plugin actually stops without a Steam restart.
hookCustomSortName now matches hookAppMap: if the first replace fails, it retries on the next tick instead of giving up for the session.
Startup polling stops after 150 attempts (~30s) if the library map never becomes hookable, so it can’t spin at 5×/s for the rest of the session.
On 4 and 5 I haven’t seen a real issue so far. Sidebar entries with a custom sort name still show their icon/metadata after cloneOverview, and m_mapApps.set hasn’t been a problem on a large library. I’ll keep an eye on both; happy to revisit if something shows up in testing.

@Jidos86

Jidos86 commented Aug 31, 2026

Copy link
Copy Markdown

Checked dd71d51 — all three look good:

  • replaceProperty now returns an unpatch closure, OverviewSync.dispose() calls it and restores official names, PluginRuntime.stop() wires it up — disabling the plugin now takes effect without a restart.
  • hookCustomSortName retries.
  • 150-tick poll cap.

(cloneOverview's own-enumerable copy is still there, but that was a "verify on a real client" note and you've tested it, so no objection.) LGTM.

@kuusei kuusei mentioned this pull request Aug 31, 2026
9 tasks
@ricewind012

Copy link
Copy Markdown
Collaborator

is this ai talking to ai bruh

@kuusei

kuusei commented Sep 1, 2026

Copy link
Copy Markdown
Author

is this ai talking to ai bruh

I don’t know why you’d think I’m AI. I used AI to translate from Chinese to English, but I wrote and reviewed the substance myself. That accusation honestly hurts a bit.

不知道为什么你会觉得我是ai, 我使用了ai翻译我的语言从中文到English,但内容都是我自己审核和构思的, 我很难过.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants