logo Error on Launch!
{#if launchError}
{JSON.stringify(
            launchError,
            Object.getOwnPropertyNames(launchError),
            2
          )}
{:else} Unexpected error {/if}
logo Update available!
Current Version {$launcherVersion}
New Version {$newVersion?.tag_name}
{#if $platform === 'windows'} {#if downloadingUpdate}
{launchInfo}
{:else} {/if} {:else} {/if}
logo App Tracking Consent

We value your privacy. To enhance your experience and improve our services, we would like to collect anonymous usage data. This data helps us understand how the application is used and identify areas for improvement.

No personal information is collected, and all data is anonymized. You can choose to enable or disable this tracking at any time in the application settings.

Do you consent to the collection of anonymous usage data to help us improve the application?

Update check failed

Failed to check for updates.

Do you want to launch anyway?

Import Theme

You are about to import the theme "{droppedTheme?.themeInfo.name}" by {droppedTheme?.themeInfo.author}.

Warning
Themes from external sources may contain malicious code or assets.
Only import themes from creators you trust.

Are you sure you want to proceed with the installation?

{trustDialogEditor ? 'Edit this theme?' : "Run this theme's code?"}
{#if trustDialogCode === 'v2-svelte'} {trustDialogTheme?.name} contains a custom Svelte layer, so the launcher will execute JavaScript authored by the theme. Only continue if you trust {trustDialogTheme?.author} and got the theme from a source you trust. Other API 2.0 themes are data-only and do not need this prompt. {:else} {trustDialogTheme?.name} is an API 1.0 theme, so the launcher will execute its bundled JavaScript. Only continue if you trust {trustDialogTheme?.author} and got the theme from a source you trust. {/if} {#if trustDialogEditor}

The editor renders custom layers immediately, so this code starts running as soon as the editor opens.

{/if}
Restore previous version?
This replaces the current files for {restoreCandidate?.theme.name} with the backup from {restoreCandidate ? new Date(restoreCandidate.backup.createdAt).toLocaleString() : 'the previous version'} ({restoreCandidate ? formatBytes(restoreCandidate.backup.size) : 'unknown size'}). The active theme will be reloaded if it was in use.
Open external website?

{$active_custom_theme?.name ?? 'The active theme'} wants to open a website in your browser. Check the full address before continuing.

Full URL

{pendingExternalUrl}

Only continue if you recognize this address. Theme links never open without this confirmation.

{#if dragAndDrop.isDraggingOverApp && !droppedTheme}

Drag and Drop a .ezpplauncher-theme file here to import

{/if}
logo
{$launcherVersion || 'dev'}
{#if $currentUser} { await Promise.all([ config.value('username').set(''), config.value('password').set(''), ]); sileo.success({ title: 'Logout successful!', description: 'See you soon!', fill: '#181825', }); currentUser.set(undefined); currentUserInfo.set(undefined); selectedView = 'HOME'; }} > Logout {:else} (selectedView = 'LOGIN')}> Login {/if}
{#if selectedView === 'HOME'}
{#if activeChrome.header !== false}
EZPPLauncher
{timeGreeting}, {$currentUser?.name ?? 'Guest'}!
{:else}
{/if} {#if activeChrome.layoutMode === 'dashboard'} {#if activeChrome.playerCard !== false || activeChrome.recentActivity !== false}
{#if activeChrome.playerCard !== false} {#if $currentUserInfo}
{#if $currentUserInfo.info.country} {countryCodeToEmoji($currentUserInfo.info.country)} {/if}
{$currentUser?.name ?? 'Player'}
Lv.{currentStats?.level ?? 1}
{currentStats?.level_progress ?? 0}%
{#if selectedMode === 0} {:else if selectedMode === 1} {:else if selectedMode === 2} {:else if selectedMode === 3} {/if} {getGamemodeName( modeIntToStr(selectedMode), typeIntToStr(selectedType) )}
{#each validModeTypeCombinationsSorted as gamemode (gamemode)} {@const gamemod = getModeAndTypeFromGamemode(gamemode)}
{#if gamemod.mode === 0} {:else if gamemod.mode === 1} {:else if gamemod.mode === 2} {:else if gamemod.mode === 3} {/if} {getGamemodeName( modeIntToStr(gamemod.mode), typeIntToStr(gamemod.type) )}
{/each}
Rank {#if currentStats?.country_rank} #{currentStats.country_rank} {countryCodeToEmoji($currentUserInfo.info.country)} {/if}
{#if $reduceAnimations} #{numberHumanReadable(currentStats?.rank ?? 0)} {:else} {/if}
Performance
{#if $reduceAnimations} {numberHumanReadable(currentStats?.pp ?? 0)}pp {:else} {/if}
Accuracy
{#if $reduceAnimations} {(currentStats?.acc ?? 0).toFixed(2)}% {:else} {/if}
Plays
{#if $reduceAnimations} {numberHumanReadable(currentStats?.plays ?? 0)} {:else} {/if}
{:else}

Welcome to EZPPFarm

Sign in to view your live stats and progress

Log in to track your global and country rank, PP, accuracy, level progression, and recent play scores directly on your dashboard.

{/if} {/if} {#if activeChrome.recentActivity !== false && $currentUserInfo} {#if activeEvent} {@const gradeInfo = gradeBadgeStyle(activeEvent.grade)}
{#if activeEvent.setId} {activeEvent.title} { (e.currentTarget as HTMLElement).style.display = 'none'; }} /> {/if}
Recent Play {#if userEvents.length > 1}
{safeEventIndex + 1}/{userEvents.length}
{/if}
{gradeInfo.label}
{activeEvent.title} {activeEvent.artist}
[{activeEvent.version}]
{#if activeEvent.mode === 0} Standard {:else if activeEvent.mode === 1} Taiko {:else if activeEvent.mode === 2} Catch {:else if activeEvent.mode === 3} Mania {/if}
{#if activeEvent.rank === 1}
Top Rank #1 Global
{:else if activeEvent.rank && activeEvent.rank <= 3}
Podium Rank #{activeEvent.rank}
{:else if activeEvent.rank}
#
Placement Rank #{activeEvent.rank}
{:else}
Score Completed
{/if}
{#if activeEvent.event === 'GAINED'} Gained {:else if activeEvent.event === 'LOST'} Lost {:else} Play Achieved {/if}
{formatRelativeTime(activeEvent.time)}
{#if userEvents.length > 1}
{#each userEvents.slice(0, 4) as event, idx (`${event.time}-${idx}`)} {@const eventGrade = gradeBadgeStyle(event.grade)} {/each}
{/if}
b/{activeEvent.mapId}
{:else}
No Recent Plays

Launch osu! and play a beatmap to see your latest performance highlighted here.

{/if} {/if}
{/if} {:else if activeChrome.layoutMode === 'classic'}
{#if activeChrome.playerCard !== false} {#if $currentUserInfo}
{#if selectedMode === 0} {:else if selectedMode === 1} {:else if selectedMode === 2} {:else if selectedMode === 3} {/if} {getGamemodeName(modeIntToStr(selectedMode), typeIntToStr(selectedType))}
{#each validModeTypeCombinationsSorted as gamemode (gamemode)} {@const gamemod = getModeAndTypeFromGamemode(gamemode)}
{#if gamemod.mode === 0} {:else if gamemod.mode === 1} {:else if gamemod.mode === 2} {:else if gamemod.mode === 3} {/if} {getGamemodeName( modeIntToStr(gamemod.mode), typeIntToStr(gamemod.type) )}
{/each}
Rank
#{numberHumanReadable(currentStats?.rank ?? 0)}
PP
{numberHumanReadable(currentStats?.pp ?? 0)}pp
Accuracy
{(currentStats?.acc ?? 0).toFixed(2)}%
Playcount
{numberHumanReadable(currentStats?.plays ?? 0)}
{:else}
See your stats here Log in with your EZPPFarm account to track your rank.
{/if} {/if}
{/if} {#if activeChrome.dock !== false}
{#if activeChrome.dockTelemetry !== false}
{#if $osuInstallationPath !== ''}
{#if !$beatmapSets && $beatmapSets !== 0} {:else} {numberHumanReadable($beatmapSets ?? 0)} {/if}
{#if !$skinsCount && $skinsCount !== 0} {:else} {numberHumanReadable($skinsCount ?? 0)} {/if}
{#if $currentSkin} {$currentSkin} {/if} {/if}
{#if $serverConnectionFails > 1} {:else} {/if} {#if !$serverPing || $serverConnectionFails > 1} {:else} {#if $reduceAnimations} {$serverPing}ms {:else} {/if} {/if}
{#if $osuInstallationPath !== ''} {#if $osuStream} {releaseStreamToReadable($osuStream)} {:else} {/if} {#if $osuBuild} {$osuBuild} {:else} {/if} {/if}
{:else}
{/if} 1 ? 'No connection' : $osuInstallationPath === '' ? 'Hmmm..' : 'Launch'} subtext={$launching && !cleanup ? launchInfo : $osuInstallationPath === '' ? 'osu! path is not set!' : $serverConnectionFails > 1 ? 'No connection...' : undefined} disabled={$osuInstallationPath === '' || $serverConnectionFails > 1} {iconSet} onClick={async () => await launch()} />
{:else if !hasThemeLaunchButton}
1 ? 'No connection' : $osuInstallationPath === '' ? 'Hmmm..' : 'Launch'} subtext={$launching && !cleanup ? launchInfo : $osuInstallationPath === '' ? 'osu! path is not set!' : $serverConnectionFails > 1 ? 'No connection...' : undefined} disabled={$osuInstallationPath === '' || $serverConnectionFails > 1} {iconSet} onClick={async () => await launch()} />
{/if}
{:else if selectedView === 'SETTINGS'}

Settings

{#if activeSettingsTab === 'logging'} {$loggingEnabled ? 'Enabled' : 'Disabled'} {/if}

{activeSettings.description}

Saved automatically
{#key activeSettingsTab}
{#if activeSettingsTab === 'gameplay'}

Shows misses in Relax and Autopilot. {#if $platform === 'macos'} Runs through your osu! Wine prefix. Only the experimental patcher stream needs .NET 8 installed there. {/if}

{ patch.set(e); }} class="mt-0.5" />
{#if $platform === 'windows'}

Test different versions of the patcher. The experimental stream requires the .NET 8 Desktop Runtime; stable does not.

{ // Only the experimental patcher build targets .NET 8. if (newStream === 'experimental') { const isNet8Installed = await hasNet8(); if (!isNet8Installed) { launcherStream.set('stable'); sileo.error({ title: 'Hmm...', description: '.NET 8.0 Desktop Runtime is required for the experimental patcher stream.', button: { title: 'Download .NET 8.0', onClick: async () => await openURL( 'https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.22-windows-x64-installer' ), }, }); return; } } await config.value('patcherStream').set(newStream); launcherStream.set(newStream); }} >
{$launcherStream}
{#each $launcherStreams as stream (stream)}
{stream}
{/each}
{/if} {#if $platform !== 'windows'}

The experimental stream is Windows-only, so {$platform === 'macos' ? 'macOS' : 'Linux'} uses stable.

stable
{/if}
{:else if activeSettingsTab === 'appearance'}

Enable a custom cursor in the launcher, like the lazer build of osu!.

{ if (!e) { cursorSmoothening.set(false); } customCursor.set(e); }} class="mt-0.5" />

Makes the custom cursor movement smoother.

{ if (!$customCursor) return; cursorSmoothening.set(e); }} disabled={!$customCursor} class="mt-0.5" />

Disables some launcher animations to improve performance on low-end devices.

{ reduceAnimations.set(e); }} class="mt-0.5" />

Boosts osu! process priority to High and requests maximum sub-millisecond Windows timer resolution (1ms) during gameplay to reduce input latency.

{ gameOptimization.set(e); }} class="mt-0.5" />
{:else if activeSettingsTab === 'integrations'}

Let other Discord users see what you are doing right now 👀

{#if $presenceLoading} {/if}

Customize details, actions, templates, interactive buttons, and preview live Discord cards.

Allow anonymous usage data to be collected to help improve the application.

{ trackingEnabled.set(e); await config.value('tracking_consent').set(e); }} class="mt-0.5" />
{:else if activeSettingsTab === 'installation'}

The path to your osu! installation. {#if $platform === 'macos'} Select your osu!.app bundle directly. The launcher will locate osu! inside the Wineskin wrapper for you. {/if}

{#if $platform === 'windows'}

Automatically start OpenTabletDriver when launching osu!.

{/if}
{:else if activeSettingsTab === 'diagnostics'}
{:else if activeSettingsTab === 'logging'}

Capture launcher, osu! and patcher output. Rotated logs are bundled into one ZIP per session, and the last five bundles are kept.

loggingEnabled.set(enabled)} class="mt-0.5" />

Stored in {$platform === 'windows' ? '%USERPROFILE%\\.ezpplauncher\\logs' : '~/.ezpplauncher/logs'}.

{/if}
{#if activeSettingsTab !== 'diagnostics'} {/if}
{/key}
{:else if selectedView === 'LOGIN'}
Login to EZPPFarm Use your EZPPFarm account to login to EZPPLauncher
{:else if selectedView === 'THEMES'}

Themes

Make it feel like yours.

{#if $active_custom_theme} {/if}
{#if activeThemesTab === 'library'}
{/if}
{#if activeThemesTab === 'workshop'} requestThemeTrust(theme, false)} /> {:else} {#if filteredThemes.length > 0} {#key themeFilter}
{#each filteredThemes as theme, index (theme.folder_name)} {@const isActive = $active_custom_theme?.folder_name === theme.folder_name}
{#if theme.preview} Preview of {theme.name} {:else if themeBackgroundVideo(theme)} {:else if theme.definition}
{:else} Preview of {theme.name} {/if}
{#if isActive} In use {/if} {#if theme.name !== 'Default' && theme.folder_name !== 'default' && theme.apiVersion} API {theme.apiVersion.startsWith('2') ? '2.0' : '1.0'} {/if} {#if theme.nsfw && !revealedNsfw.includes(theme.name)} {:else if theme.nsfw} {/if} {#if theme.nsfw} 18+ {/if}
{theme.name} by {theme.author}
{#if theme.updateAvailable && theme.availableVersion} v{theme.version} → v{theme.availableVersion} {:else} v{theme.version} {/if}
{#if theme.description}

{theme.description}

{/if}
{#if theme.status === 'installed' && theme.definition} {/if} {#if theme.status === 'installed' && (theme.backups?.length ?? 0) > 0} {/if} {#if theme.status === 'installed' && theme.updateAvailable} {/if} {#if (theme.status === 'installed' || theme.status === 'deleting') && !isActive && theme.name !== 'Default'} {/if}
{/each}
{/key} {:else}

{#if themeSearch} No themes match "{themeSearch}" {:else if themeFilter === 'updates'} Everything is up to date {:else} No themes found {/if}

{#if themeSearch} Try a different name, author or API version. {:else if themeFilter === 'updates'} You are running the latest version of every installed theme. {:else} Discover themes in the Community Workshop or create a new API 2.0 theme. {/if}

{#if themeSearch || themeFilter !== 'all'} {/if}
{/if}
{/if}
{/if}
{#if selectedView === 'HOME' && $active_custom_theme?.definition?.elements?.length}
{#key $theme_reload_token} 1 ? 'No connection' : $osuInstallationPath === '' ? 'No osu! path' : 'Launch'} launchSubtext={$launching && !cleanup ? launchInfo : $osuInstallationPath === '' ? 'osu! path is not set!' : $serverConnectionFails > 1 ? 'No connection...' : undefined} launchDisabled={$osuInstallationPath === '' || $serverConnectionFails > 1} oninteraction={triggerThemeScriptElement} onbuttonaction={handleThemeButtonAction} /> {/key}
{/if} {#if logViewerOpen} (logViewerOpen = false)} /> {/if}