Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ game name, tier, confidence, OS list, and user score.
| `-c` / `--concurrency` | Limit concurrency for the search. |
| `--disable_cache` | Bypass the local cache. |
| `--clear_cache` | Wipe the local cache. |
| `--old-view` | Use the v1.15.0 blessed-based full-screen TUI instead of the default inline picker. |

```sh
protondb-cli "Half-Life" --detail
Expand Down
46 changes: 46 additions & 0 deletions lib/presenter/legacy/blessed.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
declare module 'blessed' {
export interface Screen {
append(child: Element): void
render(): void
key(keys: string[], listener: (ch?: unknown, key?: unknown) => void): void
children: Element[]
focused: Element | null
}

export interface Element {
focus(): void
show(): void
hide(): void
detach(): void
width?: number | string
setData(data: string[][]): void
key(keys: string[], listener: (ch?: unknown, key?: unknown) => void): void
up(offset?: number): void
down(offset?: number): void
selected: number
select(index: number): void
}

export interface ListTable extends Element {
on(event: string, listener: (...args: unknown[]) => void): void
}

export interface List extends Element {
on(event: string, listener: (...args: unknown[]) => void): void
items: string[]
ritems: string[]
}

export interface Box extends Element {
setContent(content: string): void
}

const blessed: {
screen(options?: Record<string, unknown>): Screen
listtable(options?: Record<string, unknown>): ListTable
list(options?: Record<string, unknown>): List
box(options?: Record<string, unknown>): Box
}

export default blessed
}
296 changes: 296 additions & 0 deletions lib/presenter/legacy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
import blessed from 'blessed'
import type {
ListTable as BlessedListTable,
Screen as BlessedScreen,
Element as BlessedElement
} from 'blessed'
import type { GameData } from '../formatter.js'
import {
formatGameName,
wrapCollection,
formatRequirements,
generateRequirementsEntries
} from '../formatter.js'

const TAG_TIERS: Record<string, string> = {
platinum: '{#E5E4E2-fg}platinum{/}',
gold: '{yellow-fg}gold{/}',
silver: '{#C0C0C0-fg}silver{/}',
bronze: '{#cd7f32-fg}bronze{/}',
pending: '{magenta-fg}pending{/}',
borked: '{red-fg}borked{/}'
}

const TAG_CONFIDENCE: Record<string, string> = {
strong: '{#AAFF00-fg}strong{/}',
good: '{green-fg}good{/}',
moderate: '{yellow-fg}moderate{/}',
low: '{red-fg}low{/}',
inadequate: '{#8B0000-fg}inadequate{/}'
}

const GAME_NA = '{gray-fg}N/A{/gray-fg}'
const DEFAULT_PADDING_DISPLAY = 15

function getTierValue(tier: string | undefined): number {
switch (tier) {
case 'platinum': return 6
case 'gold': return 5
case 'silver': return 4
case 'bronze': return 3
case 'borked': return 2
case 'pending': return 1
default: return 0
}
}

function sortGames(games: GameData[]): GameData[] {
return [...games].sort((a, b) => getTierValue(b.tier) - getTierValue(a.tier))
}

function formatTier(tier: string | undefined): string {
if (tier && TAG_TIERS[tier]) return TAG_TIERS[tier]
return tier ?? 'N/A'
}

function formatConfidence(confidence: string | undefined): string {
if (confidence && TAG_CONFIDENCE[confidence]) return TAG_CONFIDENCE[confidence]
return confidence ?? 'N/A'
}

function formatRow(game: GameData): string[] {
const name = formatGameName(game.name)
const tier = game.protondbNotFound ? GAME_NA : formatTier(game.tier)
const conf = game.protondbNotFound ? GAME_NA : formatConfidence(game.confidence)
return [name, tier, conf]
}

function generateWrappedEntries(
title: string,
data: string[] | undefined,
_width: number | string | undefined,
gameName: string
): string[][] {
if (data && Array.isArray(data) && data.length > 0) {
const wrappedData = wrapCollection(
data,
80 - gameName.length - DEFAULT_PADDING_DISPLAY
)
return wrappedData.map((line) => [title, line])
}
return []
}

function populateDetailTable(
name: string,
tier: string,
confidence: string,
gameData: GameData,
table: BlessedListTable
): void {
let data: string[][] = [
[`{bold}{red-fg}${name}{/red-fg}{/bold}`, '-'],
['{bold}Steam ObjectId{/bold}', `${gameData.objectID}`],
[
'{bold}Steam URL{/bold}',
`https://steamdb.info/app/${gameData.objectID}`
],
[
'{bold}Protondb URL{/bold}',
`https://www.protondb.com/app/${gameData.objectID}`
],
['{bold}Tier{/bold}', tier],
['{bold}Confidence{/bold}', confidence],
['{bold}OS List{/bold}', `${gameData.oslist?.join(',') ?? 'N/A'}`],
[
'{bold}SteamDB Rating{/bold}',
`${gameData.userScore != null ? `${gameData.userScore}%` : 'N/A'}`
],
['{bold}Release Date{/bold}', `${gameData?.release_date?.date ?? '-'}`],
['{bold}Developers{/bold}', `${gameData?.developers ?? '-'}`],
['{bold}Publishers{/bold}', `${gameData?.publishers ?? '-'}`]
]
.concat(
generateWrappedEntries(
'{bold}Tags{/bold}',
gameData.tags,
undefined,
name
)
)
.concat(
generateWrappedEntries(
'{bold}Technologies{/bold}',
gameData.technologies,
undefined,
name
)
)

if (gameData.genres && Array.isArray(gameData.genres)) {
const genres = gameData.genres.map((g) => g.description).join(',')
data.push(['{bold}Genres{/bold}', genres])
}

if (gameData.recommendations) {
data.push([
'{bold}Recommendations{/bold}',
`${gameData?.recommendations?.total ?? '-'}`
])
}

const reqs = gameData.requirements
if (reqs) {
if (reqs.minimum && Object.keys(reqs.minimum).length > 0) {
data.push([
'{bold}{yellow-fg}Minimum Requirements{/yellow-fg}{/bold}',
'-'
])
data = generateRequirementsEntries(reqs.minimum, data)
}
if (reqs.recommended && Object.keys(reqs.recommended).length > 0) {
data.push([
'{bold}{green-fg}Recommended Requirements{/green-fg}{/bold}',
'-'
])
data = generateRequirementsEntries(reqs.recommended, data)
}
}

table.setData(data)
}

function createDetailView(
gameData: GameData,
table: BlessedListTable,
screen: BlessedScreen
): BlessedElement {
const [name, tier, confidence] = [
formatGameName(gameData.name),
gameData.protondbNotFound ? GAME_NA : formatTier(gameData.tier),
gameData.protondbNotFound ? GAME_NA : formatConfidence(gameData.confidence)
]
gameData.requirements = formatRequirements(gameData)

const detailTable = blessed.listtable({
tags: true,
mouse: false,
keys: false,
interactive: false,
data: [[]],
border: 'line',
width: '100%',
height: '100%',
scrollable: true,
noCellBorders: false,
style: {
fg: 'white',
bg: 'default',
border: {
fg: 'green',
bg: 'default'
},
header: {
fg: 'white',
bg: 'default',
bold: true
},
cell: {
selected: {
fg: 'white',
bg: 'blue'
}
}
}
})

table.hide()
screen.append(detailTable)
populateDetailTable(name, tier, confidence, gameData, detailTable)
screen.render()
return detailTable
}

export function presentLegacyData(data: GameData[]): void {
const games = sortGames(data)
const headers = ['name', 'tier', 'confidence']
const rows = games.map(formatRow)
const tableData = [headers, ...rows]

const screen = blessed.screen()

const table = blessed.listtable({
tags: true,
keys: false,
mouse: false,
interactive: true,
data: tableData,
border: 'line',
style: {
fg: 'white',
bg: 'default',
border: {
fg: 'green',
bg: 'default'
},
header: {
fg: 'white',
bg: 'default',
bold: true
},
cell: {
selected: {
fg: 'white',
bg: 'blue'
}
}
}
})

screen.append(table)

let displayGameView: BlessedElement | null = null

let lastKeyTime = 0
function handleNav(direction: number): void {
const now = Date.now()
if (now - lastKeyTime < 100) return
lastKeyTime = now
const idx = table.selected as number
const next = idx + direction
if (next >= 1 && next < rows.length + 1) {
table.select(next)
screen.render()
}
}

screen.key(['down'], () => handleNav(1))
screen.key(['j'], () => handleNav(1))
screen.key(['up'], () => handleNav(-1))
screen.key(['k'], () => handleNav(-1))

screen.key(['enter'], () => {
const idxVal = table.selected
const gameIndex = idxVal - 1
const game = games[gameIndex]
if (!game) return
displayGameView = createDetailView(game, table, screen)
})

table.focus()

screen.key(['escape', 'q', 'C-c'], () => {
if (displayGameView) {
displayGameView.detach()
table.focus()
table.show()
screen.render()
displayGameView = null
} else {
process.exit(0)
}
})

table.select(1)
screen.render()
}
7 changes: 7 additions & 0 deletions lib/process/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ProtondbCLIOptions {
clear_cache: boolean
detail?: boolean
json?: boolean
'old-view'?: boolean
}

const config = getConfig()
Expand Down Expand Up @@ -93,5 +94,11 @@ export default async function start(
await cache.write()
}

if (protondbCLI['old-view'] && isTty && mode !== 'json') {
const { presentLegacyData } = await import('../presenter/legacy/index.js')
presentLegacyData(result)
return
}

await render(result, { mode, isTty })
}
Loading
Loading