Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UX] Built in widget to install components and fonts from Winetricks #3102

Merged
merged 9 commits into from
Oct 18, 2023
9 changes: 9 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -718,5 +718,14 @@
},
"release": "Release Date",
"size": "Size"
},
"winetricks": {
"install": "Install",
"installed": "Installed components:",
"installing": "Installation in progress",
"loading": "Loading",
"nothingYet": "Nothing was installed by Winetricks yet",
"openGUI": "Open Winetricks GUI",
"search": "Search fonts or components"
}
}
31 changes: 30 additions & 1 deletion src/backend/api/wine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
ToolArgs,
WineVersionInfo,
ProgressInfo,
State
State,
Runner
} from 'common/types'

export const toggleDXVK = async (args: ToolArgs) =>
Expand Down Expand Up @@ -67,3 +68,31 @@ export const handleWineVersionsUpdated = (
ipcRenderer.removeListener('wineVersionsUpdated', callback)
}
}

export const winetricksListInstalled = async (
runner: Runner,
appName: string
): Promise<string[]> =>
ipcRenderer.invoke('winetricksInstalled', { runner, appName })

export const winetricksListAvailable = async (
runner: Runner,
appName: string
): Promise<string[]> =>
ipcRenderer.invoke('winetricksAvailable', { runner, appName })

export const winetricksInstall = async (
runner: Runner,
appName: string,
component: string
): Promise<void> =>
ipcRenderer.send('winetricksInstall', { runner, appName, component })

export const handleWinetricksInstalling = (
callback: (e: Electron.IpcRendererEvent, component: string) => void
): (() => void) => {
ipcRenderer.on('installing-winetricks-component', callback)
return () => {
ipcRenderer.removeListener('installing-winetricks-component', callback)
}
}
31 changes: 1 addition & 30 deletions src/backend/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ ipcMain.on('removeFolder', async (e, [path, folderName]) => {
removeFolder(path, folderName)
})

async function runWineCommandOnGame(
export async function runWineCommandOnGame(
runner: Runner,
appName: string,
{ commandParts, wait = false, protonVerb, startFolder }: WineCommandArgs
Expand All @@ -616,35 +616,6 @@ async function runWineCommandOnGame(
})
}

// Calls WineCFG or Winetricks. If is WineCFG, use the same binary as wine to launch it to dont update the prefix
ipcMain.handle('callTool', async (event, { tool, exe, appName, runner }) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this into tools.tsx to stop adding more callbacks in main.ts, it's becoming too big and it feels like a big bag of everything

const gameSettings = await gameManagerMap[runner].getSettings(appName)

switch (tool) {
case 'winetricks':
await Winetricks.run(runner, appName, event)
break
case 'winecfg':
runWineCommandOnGame(runner, appName, {
gameSettings,
commandParts: ['winecfg'],
wait: false
})
break
case 'runExe':
if (exe) {
const workingDir = path.parse(exe).dir
runWineCommandOnGame(runner, appName, {
gameSettings,
commandParts: [exe],
wait: false,
startFolder: workingDir
})
}
break
}
})

ipcMain.handle('runWineCommand', async (e, args) => runWineCommand(args))

/// IPC handlers begin here.
Expand Down
167 changes: 143 additions & 24 deletions src/backend/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from './constants'
import { logError, logInfo, LogPrefix, logWarning } from './logger/logger'
import i18next from 'i18next'
import { dirname, join } from 'path'
import path, { dirname, join } from 'path'
import { isOnline } from './online_monitor'
import { showDialogBoxModalAuto } from './dialog/dialog'
import {
Expand All @@ -38,6 +38,9 @@ import {
} from './utils/graphics/vulkan'
import { lt as semverLt } from 'semver'
import { gameManagerMap } from './storeManagers'
import { ipcMain } from 'electron'
import { runWineCommandOnGame } from './main'
import { sendFrontendMessage } from './main_window'

export const DXVK = {
getLatest: async () => {
Expand Down Expand Up @@ -429,7 +432,7 @@ export const Winetricks = {
runner: Runner,
appName: string,
args: string[],
event?: Electron.IpcMainInvokeEvent
returnOutput = false
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we didn't need the event for the messages to work so I removed that

I also added a new parameters to make the method return the output instead of sending messages, so we can capture the results of some commands

) => {
const gameSettings = await gameManagerMap[runner].getSettings(appName)

Expand All @@ -445,7 +448,7 @@ export const Winetricks = {
await Winetricks.download()
}

return new Promise<void>((resolve) => {
return new Promise<string[] | null>((resolve) => {
const { winePrefix, wineBin } = getWineFromProton(
wineVersion,
baseWinePrefix
Expand Down Expand Up @@ -487,14 +490,12 @@ export const Winetricks = {
executeMessages.push(message)
progressUpdated = true
}
const sendProgress =
event &&
setInterval(() => {
if (progressUpdated) {
event.sender.send('progressOfWinetricks', executeMessages)
progressUpdated = false
}
}, 1000)
const sendProgress = setInterval(() => {
if (progressUpdated) {
sendFrontendMessage('progressOfWinetricks', executeMessages)
progressUpdated = false
}
}, 1000)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just removing the event, the formatter is changed too many lines


// check if winetricks dependencies are installed
const dependencies = ['7z', 'cabextract', 'zenity', 'unzip', 'curl']
Expand All @@ -515,16 +516,24 @@ export const Winetricks = {
})

logInfo(
`Running WINEPREFIX='${winePrefix}' PATH='${winepath}':$PATH ${winetricks} --force -q`,
`Running WINEPREFIX='${winePrefix}' PATH='${winepath}':$PATH ${winetricks} ${args.join(
' '
)}`,
LogPrefix.WineTricks
)

const child = spawn(winetricks, args, { env: envs })

const output: string[] = []

child.stdout.setEncoding('utf8')
child.stdout.on('data', (data: string) => {
logInfo(data, LogPrefix.WineTricks)
appendMessage(data)
if (returnOutput) {
output.push(data)
} else {
appendMessage(data)
logInfo(data, LogPrefix.WineTricks)
}
})

child.stderr.setEncoding('utf8')
Expand All @@ -536,7 +545,6 @@ export const Winetricks = {
child.on('error', (error) => {
logError(['Winetricks threw Error:', error], LogPrefix.WineTricks)
showDialogBoxModalAuto({
event,
title: i18next.t('box.error.winetricks.title', 'Winetricks error'),
message: i18next.t('box.error.winetricks.message', {
defaultValue:
Expand All @@ -547,26 +555,78 @@ export const Winetricks = {
type: 'ERROR'
})
clearInterval(sendProgress)
resolve()
resolve(returnOutput ? output : null)
})

child.on('exit', () => {
sendFrontendMessage('progressOfWinetricks', ['Done'])
clearInterval(sendProgress)
resolve()
resolve(returnOutput ? output : null)
})

child.on('close', () => {
clearInterval(sendProgress)
resolve()
resolve(returnOutput ? output : null)
})
})
},
run: async (
runner: Runner,
appName: string,
event: Electron.IpcMainInvokeEvent
) => {
await Winetricks.runWithArgs(runner, appName, ['--force', '-q'], event)
run: async (runner: Runner, appName: string) => {
await Winetricks.runWithArgs(runner, appName, ['--force', '-q'])
},
listAvailable: async (runner: Runner, appName: string) => {
try {
const dlls: string[] = []
const outputDlls = await Winetricks.runWithArgs(
runner,
appName,
['dlls', 'list'],
true
)
if (outputDlls) {
outputDlls.forEach((component: string) =>
dlls.push(component.split(' ', 1)[0])
)
}

const fonts: string[] = []
const outputFonts = await Winetricks.runWithArgs(
runner,
appName,
['fonts', 'list'],
true
)
if (outputFonts) {
outputFonts.forEach((font: string) => fonts.push(font.split(' ', 1)[0]))
}
return [...dlls, ...fonts]
} catch {
return []
}
},
listInstalled: async (runner: Runner, appName: string) => {
try {
const output = await Winetricks.runWithArgs(
runner,
appName,
['list-installed'],
true
)
if (!output) {
return []
} else {
const last = output.pop() || ''
if (
last === '' ||
last.match('winetricks has not installed anything')
) {
return []
} else {
return last.split('\n').filter((component) => component.trim() !== '')
}
}
} catch {
return []
}
}
}

Expand Down Expand Up @@ -641,3 +701,62 @@ function getVkd3dUrl(): string {
// that would also need bigger changes in the frontend
return 'https://api.github.com/repos/Heroic-Games-Launcher/vkd3d-proton/releases/latest'
}

// Calls WineCFG or Winetricks. If is WineCFG, use the same binary as wine to launch it to dont update the prefix
ipcMain.handle('callTool', async (event, { tool, exe, appName, runner }) => {
const gameSettings = await gameManagerMap[runner].getSettings(appName)

switch (tool) {
case 'winetricks':
await Winetricks.run(runner, appName)
break
case 'winecfg':
runWineCommandOnGame(runner, appName, {
gameSettings,
commandParts: ['winecfg'],
wait: false
})
break
case 'runExe':
if (exe) {
const workingDir = path.parse(exe).dir
runWineCommandOnGame(runner, appName, {
gameSettings,
commandParts: [exe],
wait: false,
startFolder: workingDir
})
}
break
}
})
arielj marked this conversation as resolved.
Show resolved Hide resolved

ipcMain.on(
'winetricksInstall',
async (event, { runner, appName, component }) => {
sendFrontendMessage('installing-winetricks-component', component)
try {
await Winetricks.runWithArgs(runner, appName, ['-q', component])
} finally {
sendFrontendMessage('installing-winetricks-component', '')
}
}
)

ipcMain.handle('winetricksAvailable', async (event, { runner, appName }) => {
try {
const x = await Winetricks.listAvailable(runner, appName)
return x || []
} catch {
return []
}
})

ipcMain.handle('winetricksInstalled', async (event, { runner, appName }) => {
try {
const x = await Winetricks.listInstalled(runner, appName)
return x || []
} catch {
return []
}
})
13 changes: 13 additions & 0 deletions src/common/typedefs/ipcBridge.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ interface SyncIPCFunctions {
pauseCurrentDownload: () => void
cancelDownload: (removeDownloaded: boolean) => void
copySystemInfoToClipboard: () => void
winetricksInstall: ({
runner: Runner,
appName: string,
component: string
}) => void
}

// ts-prune-ignore-next
Expand All @@ -115,6 +120,14 @@ interface AsyncIPCFunctions {
runWineCommand: (
args: WineCommandArgs
) => Promise<{ stdout: string; stderr: string }>
winetricksInstalled: ({
runner: Runner,
appName: string
}) => Promise<string[]>
winetricksAvailable: ({
runner: Runner,
appName: string
}) => Promise<string[]>
checkGameUpdates: () => Promise<string[]>
getEpicGamesStatus: () => Promise<boolean>
updateAll: () => Promise<({ status: 'done' | 'error' | 'abort' } | null)[]>
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/components/UI/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React from 'react'
import { SearchBar } from 'frontend/components/UI'
import StoreFilter from 'frontend/components/UI/StoreFilter'
import PlatformFilter from '../PlatformFilter'

import './index.css'
import LibrarySearchBar from '../LibrarySearchBar'

export default function Header() {
return (
<>
<div className="Header">
<div className="Header__search">
<SearchBar />
<LibrarySearchBar />
</div>
<span className="Header__filters">
<StoreFilter />
Expand Down
Loading