Personal Modifications & Fixes for Pegasus Game Launcher + gameOS Fire

From notfire's wiki
Jump to navigation Jump to search

sKye: Hi-hi! If alls you're wanting is to nab some download links, scroll no further. Wouldn't want you getting lost. My macOS builds of Pegasus can be found at DEES BLURB, & my customized Pegasus theme can be found at DEES BLURB. And you make sure to take care of yourself out there.

Choosing a Game Launcher

sKye: The current state of the digital world gives us access to so much art to experience. And obviously, that's a net-positive for everyone. With a little searching, we can uncover treasure that will change our life-- and I'm sure that you have found at least some of those things for yourself, and understand that this search is oh, so worth it. But you know what the problem is? The PACKAGING of the digital world. Like, the library IMMEDIATELY makes me want to read books, and makes me make the Time to read them. But then owning an eBook, for example, doesn't feel like much. To repurpose a thought from No Country for Old Men, no matter the incredible contents of any file on my drive, it just gets lumped in with all of the others and becomes just a file. Which it is.

So this terribly true joke of the modern era of being excited by Steam sales and then buying a whole bunch of games when you haven't played ANY of the other games you've already bought... I want to remedy that for myself. And I realized the answer was to get myself a game launcher that reminded me as little of the ugly, artless modern Internet as possible. Something that feels deeply responsive, where the whole experience is meant to engage, immerse and aesthetize. (Which I just learned isn't technically a word, but boyhowdy, should it be.) A personal cabinet of wonders (please read Wonderstruck) that brings that wonder back... without having to hoard anything. Other than drives.

What could get me to play my games? (What a stupid thing to ask, but wow, if there was ever a generation in need of catharsis through the arts, it's ours.)

Steam & Steam ROM Manager

No offense, Steam-- it created the modern indie gaming scene, leading to many of my favourite games, after all... but when I log into Steam, I almost immediately don't want to play any games. The whole UI feels sluggish and cumbersome to use. Skins can make it a little nicer, but that's just adding more weight on-top. It all feels like playing with a Jacob Marley ball-and-chain around me. Plus, despite it having an obvious KIND of design sense, I'd call it devoid of personality.

Let's say some nice stuff, though; Steam really DOES deserve praise. Five nice things about you, like we're in a little group activity circle:

  • Steam Input can make a HUGE difference, especially on a Mac, where many controllers, USB and Bluetooth alike, are not well-supported by games. I have had to add quite a few non-Steam games to my Steam library, then add Steam Input, and that's the only genuine way to get my controller working. There IS a big difference between controller support & keyboard mapping, okay? 'Specially with those analog sticks. Can't be scrimpin' on those analog sticks.
  • Steam Link & Remote Play are both the best tools available for playing many games non-locally. I know for a fact they'll be useful for me. So all that extra weight, I get it.
  • The metrics that it collects are really fun. However, the public nature of always, always being in a community where all of those metrics are collected together against everyone else's... I'm sure that it actually inspires many to play, but it almost... dissuades me? It's not just about if I don't succeed in the game I'm playing-- it's a weird fear that if other people notice I'm playing, they'll judge me for not spending my time doing something more traditionally important. I know, I know, EVERYONE'S ON STEAM to play, so ABSOLUTELY NO ONE is judging me for PLAYING games. But I can't be the only person to feel simultaneously bad for not playing games AND for playing games. Just gotta strike that balance is all!
  • It IS the largest game asset resource compendium I personally know of, and it's all laid out for you from the word go. (From the word "Steam Guard code".) And it tends to be slick-looking stuff, too. Just not presented as well as it could be. So it's still going to be a primary reference for me, regardless of what launcher I end up picking. (The best way to access all official Steam gameart, as well as user-submitted material, is through SteamGridDB.)
  • It supports animated GIFs for custom backgrounds! My brothers and I found that out together; I didn't believe it 'til I saw it. It was, like, a 26MB GIF, too.

Steam also has a great deal of support for launching emulators through the community-developer tool, Steam ROM Manager. Really useful stuff, since not every launcher has it this easy!

GOG Galaxy

This one was new to me! It has a fair amount of built-in game art and game logos for presentation, but part of the experience is kind of soured for me by the fact that, to add a game, you have to grab an ID of a game that's already in the server. Then you specify that you own the game, and then you select its location on your drive(s). The fact that I have to start by typing in a game that's in a database I didn't make... it really had a bad aftertaste. Even though the database is pretty well-kept, full of unusual things from itch.io, etc., there's still plenty of things I play that weren't in it. An easy way around that is to add some game you know you'll never play out of the millions already in the database, then change the title and all its related information. But just the process alone makes me feel like the game I'm selecting isn't actually the game that I own. It's being conformed into the shape of what someone else put INTO GOG Galaxy. It'd be different if it helpfully AutoFilled in things it knew about a game, or if it checked the database first, then allowed me to put in whatever game I wanted (the Plex way!), but... telling the computer I OWN THIS GAME in specific-- the one SOMEONE ELSE CATALOGUED-- felt like taking away my ownership.

Besides that, I would say it runs a lot nicer than Steam, and felt more like the developers were making use of some of the beautiful things about a macOS-specific UI. It looks FRIENDLIER, you know? On design alone, this is a huge step-up from Steam, and it's the only launcher in the more professional scene that I think looks nice.

Phoenix

I'm putting this up here primarily because I just want to sing this dude's praises -- he made a game launcher from scratch specifically FOR MAC. And here it is; please take a look at it regardless of if you have any desire to use it, or even method by which to! Because I sure don't have any such method, considering it (at the moment) is only for macOS Ventura, and I ree-lee don't want to have that up-date up-heaval. It can leave me re-adapting for months.

However, for the sake of documentation (I mean, YOU see the contents table... if I'm being this comprehensive...), I installed it on my brother's computer, and it really runs as lightweight and easy as you pleasy! I DO think the buttons themselves leave something to be desired, in shape and response to the click. (2024 EDIT: Already looking a lot better now!) I can't click on the parts of the Play button that don't have text over them, for example. It also requires typing in Terminal prompts, even for things that should be as simple as just opening the app. This is extremely smart, but it DOES make it so a new user of the app will have to look into documentation. Since it's as simple as "open [insert path of app here]", in that case, a short note about that should just be built into the configuration window as the example. Then you'll empower people who use it! I also love how readable the JSONs that the launcher generates are. That's also majorly empowering for coder-posers like me.

Another launcher very similar to Phoenix with GPTK built-in will probably release in this upcoming year-- it has a very similar aesthetic!

EmulationStation

I think that this desktop port of a RetroPie project isn't even developed by the original team, because the website for the initial project doesn't mention it at all. Doesn't even mention THEMES, for that matter, which is definitely EmulationStation's most alluring attribute. (I'm quite fond of Art Book Next!) This desktop port makes access to those themes majorly convenient -- just download those themes from an online database INSIDE the program itself. However, in-spite of the customization features, it feels like it's also fighting against you, should you ever want to customize anything outside of what is already set for you. (It's like an Apple product!)

For starters, it saves all of the most important stuff in a big ol' invisible folder in your user directory called ".emulationstation". That's where your settings, your metadata entries, and your themes are all saved. Instead of going into that folder, EmulationStation wants you to manage all of this in the launcher GUI. Which isn't catered to desktop, from what I can see. In-spite of my computer having a fully functional keyboard, if I want to enter in metadata, I have to painstakingly navigate between letters on an on-screen keyboard. Ark and Kerrigan was right, that's the TRUE challenge of any game-- and as THE way you're presenting to me to make my games actually look pretty, you want me to do THAT? If that was a core gameplay loop in a game, it'd sure be for a niche audience.

Speaking of the keyboard, on default, only the arrow keys, Return, and Escape are properly mapped. The B button's mapped to Delete, which makes you have to put your hand in a funny little claw shape while continually accepting and going back... and then as far as I can tell, all the other necessary navigation buttons are just not mapped when you start, making a lot of the non-barebones aspects of EmulationStation inaccessible. You have to go into the settings, then go into the Controller and Keyboard configuration, THEN hold down a button on the keyboard to remap. So, from the get-go, it doesn't seem... SO desktop-friendly. Like, c'mon. A lot of the games I play, I don't want to use a controller at all.

There's also zero mouse input-- they don't even allow me to look at my mouse while in the program. Which is hilarious since one of the engines they support is point-and-click bastion ScummVM. Like, I realize this was envisioned for DIY handhelds, so you probably can't use a mouse without essentially re-developing the whole system, but at least let me SEE it. I feel a keen sense of separation! EmulationStation won't even let me see my DESKTOP if I'm playing a windowed game (which is one of those things that one often has to do if they're participating in the world). Instead of giving me freedom, it decides to fill the whole screen with black. Stop taking my computer away from me, you're not my OS! I'm not one of those doofuses who thinks it's cool to have a separate computer with a deliberately hidden OS so it can perform just ONE task.

Even once I found where the metadata is, it can't be edited without having already fully filled out each piece of relevant metadata successfully already. The fields I'd like to edit don't exist yet, and I don't know what they're called yet. On-top of that, you can't actually add your own personal images for logo, game art, screenshots, etc. That's all supposed to be scraped from a collection. So it leans heavily towards a hands-off experience of dropping ROMs in a folder and having it be instantly populated by a preset database. That doesn't fill me with joy. And I can't speak for everyone, but I have a feeling that nearly all the people who've downloaded full no-intro ROMsets and set up some kind of Raspberry Pi RetroArch rigamarole haven't done much more than set-up it up, test it, give it a beautiful home, and forget about it.

Nostlan

Now, Nostlan wouldn't have been suited to my case to begin with, due to it being not only specifically for emulation but also only for specifically compatible emulators... but I really want to laud its scrappy, norm-deviating ways. It starts out feeling a little too similar to the startup of EmulationStation, where it dumps a whole bunch of folders in an empty directory, which you're then supposed to fill up. However, I immediately noticed that I could use my mouse on EVERYTHING, HA HA. Take that, EmulationStation!

NostalgaiaA.png

And the mouse really feels important here, it's got a nearly point-and-click-adventure feeling to it. While Nostlan searches through your ROMs, it has all kinds of lovingly rendered graphics meant just to pass that first-time loading. The Switch logo in the picture above had a real accurate animated loop, and I could click EVERY button on the Switch console itself with a proper animated response: A way to bring tangibility to a very non-tangible experience!

NostalgaiaB.png

Heheh, there's only one part where the mouse doesn't do quite what I expected it to. I was trying to drag through the game library by pulling one of the games down, and...

NostalgaiaC.png

... WwwwOOP! Ghost Trick now has a lil' image overlay grabby-ghost image. It actually stores quite a few of its assets like it's a website that you can drag images off of! Don't believe me? Here I go!

NostalgaiaD.png

[gameThumb.jpg] has been saved to my Desktop!!

NostalgaiaE.png

Now THIS part REALLY reminds me of playing a point-and-click game. When you click on a game, it switches to a close-up for further inspection. And if you click on that:

NostalgaiaF.png

... Then, you can actually click on the manual for another similar reorienting close-up, and, of course, by clicking on the cartridge itself, you get:

NostalgaiaG.png

... Now is that not The Exact Same as inspecting incremental levels-worth of hotspots in a 2000s Nancy Drew game? And isn't that more than enough to REALLY get you mentally prepared to play Ghost Trick? (Play Ghost Trick, play Ghost Trick!)

I had to add in the cartridge image myself, since it was just a blank cartridge, but it was INSANELY easy to do so for each game placed in the library. And I gotta say, it's refreshing that this experience, unlike every other, doesn't bother displaying metadata. I don't think comprehensive, detailed metadata (especially metadata that I didn't fill out myself) has ever made me want to play a game, but these curators seem to be obsessed with what is essentially paperwork. No offense to paperwork-- I mean, I'm the one writing some paperwork here. But writing paperwork is not even remotely related to riffling through game cases and letting possibilities rush over you. That's pure, and so is this.

This is also the only launcher thus far that's been separated by console/system and hasn't felt so regimented, you know? Maybe this is really odd, but since all of these games are being played on my computer, and the experience of playing them all is linked to my memories interacting with this beautiful device, I feel like it's more delectable and, uh, Honey Bunches of Oats-y to just throw them all into one collection. However, in this case, especially because it's emphasizing the game cases (Each system collection even varies UI elements to give it a touch of console specificity!!), I feel it adds to this catered experience. And it really IS the most catered of the experiences I've tried.

I'm also surprised that it found where my emulators were and launched them-- I really thought it was going to foolishly re-download every emulator all at once, or at least ask me to provide some file paths. But it went for it, and got each right! Some didn't launch correctly, I assume due to some Terminal trickery (I also assume that the Windowed mode not giving me any way to position said window is also macOS malarkey.), but overall, so impressed. Of all the launchers, this one feels the closest to the SPIRIT of my search.

EmuHub

I'm actually adding in EmuHub retrospectively in early 2024, because I want anyone who happens upon this to have the clearest view of their options. EmuHub is currently in beta, and while it might-- on first blush-- look user-friendly as can be... well, I can't get a single game running in it, due to complicated configurations that have to be done manually on macOS. Also, similar to Nostlan, it's only for emulators to begin with, so-- were everything else as I'd like it, it's still not what I set out to do. (Speaking of "set", it was surprisingly difficult for me to know how to access the Settings, as I wasn't on a controller. To those in the not-know: On keyboard, press the Delete key. Navigation menu is Tab.)

Perhaps most disappointing of all is its dogged desire to display scraped game assets only, much like EmulationStation. Unlike SteamGridDB or LaunchBox, it doesn't seem like the databases EmuHub is linked to are robust-- or pretty, for that matter.

As an example, this is the screenshot it provided for Super Mario 64 DS:

E-Reader.png

Look'it that GIF-esque rainbow mosaic of compression! PLUS an unreadable watermark betwixt the screenes? And it's made even worse when, just by Reverse Google Image searching, there's a million vastly improved copies of this very same screeneshote.

Here's one I took from a random Nintendo Fandom Wiki entry:

Fandomdotcom.png

To make matters worse, the scraper also randomly generates composites of these assets, meaning you get this lovely showcase of the dead-center of a dual-screen screenshot.

ThisismyNFTakamyNotFireThing.png

... But hey, once you've got at least one successful scrape under your belt (ow!), you can use that same syntax to give everything custom artwork (and trade out any downloaded images the database may spew at yew). As I said, though, I can't even get it to boot a game. Really, the best thing this has currently got going for it is its responsiveness and smart Wii Homebrew Menu-esque design sensibilities.

Should someone want to nab the basic structure of a fully scrape'd metadata for their games.yml, here's the template the system generated for Super Mario 64 DS:

- id: px8Yxq
  romname: Super Mario 64 DS.nds
  system: nds
  name: Super Mario 64 DS
  added: Thu, 22 Feb 2024 05:29:23 GMT
  lastViewed: Thu, 22 Feb 2024 05:33:17 GMT
  hero: /Users/[INSERT USERNAME HERE]/Documents/EmuHub/assets/games/nds/Super Mario 64 DS.nds/hero.jpg
  poster: /Users/[INSERT USERNAME HERE]/Documents/EmuHub/assets/games/nds/Super Mario 64
    DS.nds/poster.jpg
  logo: /Users/[INSERT USERNAME HERE]/Documents/EmuHub/assets/games/nds/Super Mario 64 DS.nds/logo.png
  screenshot: /Users/[INSERT USERNAME HERE]/Documents/EmuHub/assets/games/nds/Super Mario 64
    DS.nds/screenshot.png
  gameTileDisplayType: fanart
  description: >-
    Mario, Luigi, and Wario have all gone to the Mushroom Palace for some of
    Princess Peach's famous home made cake. However, when they don't return its
    up to the heroic dino Yoshi to find out what's wrong.


    In this portable remake of the original Super Mario 64 You' are able to play as different characters from the Mario franchise. Wario, Yoshi, Luigi, and of course Mario. Each character has different abilities to be made use of during the games expanded quest. 


    The game now features 150 Stars to collect up from the previous 120, new stages with new bosses, changes to old stages and bosses, and a multiplayer in which player compete to get the most stars.


    The game also includes over two dozen mini games that make use of both screens and the touch pad.


    The DS touch screen can be used to mimic analog control.
  players: "1"
  genre: Puzzle-Game
  publisher: Nintendo
  developer: Nintendo
  crc: e6321562
  romsize: "16777216"
  gamePageDisplayType: fanart
  showcaseDisplayType: fanart
  timesPlayed: 1
  lastPlayed: Thu, 22 Feb 2024 05:31:12 GMT

Pegasus

I really think Pegasus Frontend is the most evenly developed of all of these options. Maybe a lot of people would call it less professional than things like Steam or GOG Galaxy, but that already makes it better to me. It doesn't withhold anything from you to tinker with. It's already a GitHub project, so all of the code is publicly available. But then, on-top of that, every single aspect of every theme developed for the launcher can be easily edited in any text editor, and all it takes is a reboot of the app to see any changes you made immediately.

Pegasus COMES with a metadata editor app (or, if you can swing it, a matadeti atotar epp)-- whereas some of these other apps (like EmulationStation) have independently developed metadata editors, hahaha. (Or, if you can swing it, endipindintly dovoleped...) Every line of the metadata is easy to replicate the format and syntax of on your own, once you've generated a few in the app. I just use TextEdit now to make each entry exactly the way I want it. Unlike all of these other launchers, there's, like, a blending of approaches for how to launch. They give a bit of options for Terminal-esque commands, as well as just... putting in a file pathname and saying COME ON OPEN THAT FILE. And yes, I can pick ANY APP ON MY COMPUTER THAT I WANT. And IT ALL CAN GO IN ONE COLLECTION TOGETHER. Now, was that so hard? (I'm looking at all the other launchers here, hahaha.)

When I boot a game, Pegasus shifts away and waits for the game to finish to pop back. When I saw that it stops responding in the Activity Monitor during the waiting, I feared the worst-- that it was sucking up needless energy like most stalled apps would-- but instead, I found out from the very thoughtful and patient dev that it's just trying to make as many resources available again as it can, so the game can run well. That's some good-guy-dev behaviour right there-- whoever heard of a developer caring about conserving energy expenditure on my computer? They all behave like teachers, piling an amount of homework that would only make sense if the only thing we had going on in our lives was their class.

In the standard theme, keyboard, mouse and controller are all equally supported. (I don't think I can say that about ANY of the other launchers, actually. They all prefer one over the other.) Every action feels snappy and responsive. Fullscreen and Windowed mode both look gorgeous, and properly resize all elements. And while the standard theme is honestly way more bold than any of the other standard themes I've seen thus far (except for Nostlan, obviously), it doesn't incite feelings of... "Let's play!", you know?

What I want, essentially, is Plex for games. I gotta say, Plex changed the way I share video, audio, or images with anyone in my house. To me, it re-injects vitality into the home theatre experience... so much so that even when I have the means to watch a movie or a show on one of the other streaming platforms, I tend to put it on Plex, instead. That might sound ridiculous-- they both just PLAY THE THING. But it's the only app on my Roku that I think looks and feels nice. That's what this whole launcher deal is about-- giving it all a beautiful world to live in that just begs to be explored.

... So, maybe if I just... look for a more fitting Pegasus theme?

Choosing a Pegasus Theme

Minimis

After going through all the themes currently available as quickly as you please, Minimis was the first theme to really stand out to me. I think it makes the best first impression, without having to do any fiddling. You boot it, and it's already going to have great game[launcher]-feel. It's the first theme I was able to successfully get the logo overlay feature working in. (That's honestly my favourite thing about Pegasus in general-- you give it background art and a transparent PNG of the logo, then you put the two together, and upon selection, the logo goes BWOOOMP like the Dimension Films logo at the beginning of Spy Kids 3-D.) Some people might look at it as a cheap gimmick, but I'm all about the gimmicks.

Here's how my setup in Minimis looked:

Pegasuscollectiondec92022small.png

(click here for full-size image)

When setting up the logo zoom-in, I was impressed to find that built into the settings is a little milliseconds slider, which really allowed me to let the impact of the bwoomp settle into my soul. Now, if only it were possible to make those kinds of tiny adjustments in the OTHER themes that don't explicitly allow it! (Little did I know... to be continued~)

The navigation in Minimis is super-smooth. It even includes a nifty sidebar I haven't seen in any other theme, allowing you to navigate by letter, which becomes a bit of a necessity for larger collections. It's an interesting balance to strike, of quick, convenient travel (which I suppose would just be the sidebar library in Steam-- just a big ol' vertical list of names and tiny icons that barely register to the eye...) and giving each game the space they deserve to explain why they should be played.

Another point in Minimis' favour is that it was the only theme I could more easily figure out how to write my metadata so as to have my boxart and my background as two different things. I'unno if I'm just not understanding something really simple, but I feel like the standard settings of all themes make it difficult to tell which arbitrary switches-of-setting to juggle together to achieve the desired result. But if it wasn't for Minimis being my introduction to Pegasus... giving me a wealth of settings, letting my carefully chosen artwork speak for itself, separating the logo from my boxart... I would've just moved on to a different launcher. I used it with (befittingly) minimal complaints for over half-a-year!

But I amassed an itty-bitty pile of case-usage issues. First, you can't click ANYTHING in Minimis. The dev made it for their Nvidia Shield, so there'd be very little reason to put so much effort into making it mouse-compatible when all the computer users will have a keyboard or controller, anyway. (And as I will later find out, code for navigation with only controllers & keyboards really simplifies operations in the code. You can't really move to elements out of order with a controller. So fair enough.) The only thing that HAS mouse support is really more of a quirk of all themes in Pegasus -- unless specified, every list element in a Pegasus theme can be dragged around or scrolled over, and they'll swish about. (I believe the proper terminology in the Qt language they're using is that it's Flickable.) So I can scroll or pull my list around, but I can't click on any game I pass. Effectively, it's only for window-shopping, as it were.

And while a letter-navigation sidebar IS majorly useful, it actually takes just about as long to get through my 4-columned list of about 200 games by tapping through the alphabet as it would just tapping through the list. After all, 26 x 4 = 104, so I'm really only saving about half the tapping-time. And the sidebar doesn't allow wrapping around from the top of the alphabet to the bottom, which tended to be where I wanted to be. The beginning and the end of a list both constantly need to be inspected!

Probably the thing that finally made me trudge back into no-theme's-land (which, MAN, after getting comfortable in ANYTHING, going back to the purgatory in-between is highly undesirable) was when I decided to add another collection into Minimis. Before this, I only had one collection, and it had all the games I wanted to play in it. And I really figured it would stay that way-- as I said, games should be a delicious bowl of Honey Bunches of Oats-- tastier together. (Me and my dad are currently enjoying adding more slivered almonds from a separate bag into our Honey Bunches of Oats. And I also put banana slices. The more the merrier, right?) BUT, after that half-a-year, I happened upon the one scenario where I would feel more joy if I could separate two things, to let each shine from the added definition. I, uh... decided I wanted to put BOOKS into my gaming launcher. C'mon, we ALL KNOW all eBook readers have the WORST, MOST CUMBERSOME-feeling programs-- even worse than gaming launchers. I'd honestly be overjoyed just to have an eBook reader that feels like a mediocre gaming launcher. And now, since I have a dream-come-true type'a gaming launcher... well, I at least had to try. And I immediately loved it and deleted every book I had in Apple Books, migrating them over to Pegasus.

Only trouble is, since Minimis wanted to present us with as few navigational screens as possible, on every boot up, it just plops you down in your first collection, as alphabetically listed. It doesn't matter what collection you're looking in when you quit out for the day-- you'll be booted right back into the tippity-toppest collection. Which I bet must be a MAJOR tap-tap revenge of a workout for anyone with a NORMAL kind of collection situation in Minimis. Imagine, you have to start off in your... Amstrad CPC collection every time you boot, then you hit Escape to back out into the navigation level with all the dropdown menus, then go on over to the Collections dropdown menu, select that, then dive all the way down through 20 letters' worth of collections just to get to your Sega Dreamcast collection. And the more completionist you are, the worse that issue becomes. And for me, it's really just two collections, and it was still getting to me, especially since it would've just been two clicks away, if clicking were an option. It had become more convenient for me to force-quit Pegasus instead, since that would save the screen I was on when I reopened. And that just seemed far too desperate a measure to continue on that way.

[NOTE: I had no clue 'til later that you could navigate between collections by pressing Q & E (which really would've been easy with my TWO collections, although I can still see this being majorly cumbersome for all the console-separatists), but I, uh, really like using my mouse? It IS a computer, after all. Still, had I known about the quick navigation, it might've been enough to keep me in my comfort zone rather than setting off on this second expedition. So I'm grateful.]

FlixNet_Plus

A quicker detour I made on my way to the theme I'd settle on was this fork of Flixnet, a lightweight Netflix-themed... theme. I don't like how current Netflix looks and behaves, so it didn't really seem like my kind of thing. I was just making sure I tried everything available to me before making my next (hopefully ultimate) decision. This fork has one feature that absolutely blew me away-- one I don't think I happened upon in any of the other themes... at least nowhere it deserves to be. Pegasus actually TRACKS INDIVIDUAL GAMEPLAY TIME. It does that automatically, even if the theme itself doesn't feel like doing anything with this unique and personally investing metric! FlixNet_Plus is the only theme I saw that decided to take full advantage of this, and even format the time spent in an hrs-mins funjob. Once again, maybe it's right in-front of me (everything on my computer is right in-front of me), but I'm just really surprised this isn't one of the first things devs have thought to add to their custom Pegasus themes. We're all trying to not let... split infinitives happen, hahaha... not to let these amazing experiences pass us by-- so this would let us know we've done good... without my paranoia of the information being broadcasted online all the time. Or with it being in association with a list of unreceived achievements that make me fear I'm not playing the game well enough.

gameOS

Things that go in gameOS' favour-- it's one of the few themes I've found that actually has a great deal of mouse support. Nearly everything can be clicked on-- most importantly, you can click on THE GAMES, PRAISE BE. Just like Minimis, the art is the focus, and with the right metadata and in-theme settings, you can get that ESSENTIAL logo overlay experience, and also make each background separate from your boxart. And here's a major quality-of-life up-res: a SEARCH BAR. A SEARCH BAR. Oh, sure, there's no letter navigation, but it's honestly even more convenient for a computer user to just click on a text field and type IN some letters!

Of most importance, though, was finding a theme that could easily move me between collections... and MAN, is it simple here. You start each session on a main menu with a super-accessible horizontal list of your collections right there at the top. All you gotta do is click on one of them. And for me, it's one of two, so I can be where I wanna be within ONE click from opening my application. I'm sold, if not a little apprehensive!

Because, of course, there were things I didn't like so much. While everything is clickable, it didn't take into account that I'd want to drag or scroll the game list and select a game that way. Any click is liable to fully activate any of the games I'm dragging upwards or downwards, if I'm not careful about where I lift my mouse-click. And even if I ensure I let go of my click where it won't register on top of any game, it still will select games, which makes it want to center the selection on the screen. And since the theme doesn't really keep track of where I am vertically or horizontally on each screen, unless I'm using a keyboard/controller, it ends up creating really yucky select transitions where it has no clue where the starting point should be. It especially goes crazy if I'm continuing to drag as the transition occurs, the two motions arguing with each other. Sometimes, it isn't sure what I selected, and just boots me back to the start of the list. A common problem is that I'll search for something on the other half of the alphabet, and then after deleting the search result, it'll accidentally place the game I'd successfully located out of alphabetical order.

Like so:

Youonlygotoneshotsmall.png

(click here for full-size image)

It stays that way until you try to navigate with the mouse or arrow keys, at which point, it'll typically send you back to the top of the list.

You also can't really see any of those sweet, sweet logo zooms unless you're navigating without your mouse. It's set so that the moment you click anything is also the moment you've fully activated that thing, rather than simply highlighting it. And if the zooms are an essential feature of my theme (really, they are non-negotiable for me), and I'd be skipping that feature by using a mouse, then I just... wouldn't use the mouse. Which would be a bit of a shame, wouldn't it?

Another thing you can see in that screenshot that doesn't jibe with me-- the settings allow you to turn off and turn on something labeled, "Always show titles", but even if you set it to "No", it'll ALWAYS put up that ugly text-bubble footer on every game selected. The logo clearly states the name of the game already, so, that's just a redundance. Besides, I think anyone who's using this launcher, even if they're not using a logo overlay, will still have the name of the game displayed in the game-select art itself. Why give us double-title and interrupt the grid?

Lastly, and this might seem like a REAL nitpick, but... the gradient colour of the game-highlight isn't appealing to me within the context of the rest of the UI colours. It gives me this kind of strange corporate next-gen feel. Maybe most of all, it reminds me too much of those... Gamer Room(™) LED-type deals? I don't enjoy using my computer or playing video games in the dark, and I definitely wouldn't enjoy it any more if I was having to drown in a dim, oversaturated neon. (Reading books in the dark by flashlight is probably the only similar activity I can understand!) To give you an idea of how I am about gamer colours, I even had to change my keyboard to be less reactive and crazy with its light show under the caps because it genuinely made me not want to so much as strike it. This highlight colour is infused into the trim on everything in the theme, and it just leaves a bit of a bad taste in my eyes.

I might not have even bothered to bring that up, but it's kind of the reason I ended up where I did...

gameOS - Fire

In spite of my resolve to try out every theme before making my decision, I nearly didn't bother with gameOS - Fire. The theme gallery entry has no differentiating details in its entry for it, and there's not even a screenshot of what it might look like. But then again, Minimis' "screenshot" is just a still from an anime called Black Rock Shooter, so I figured it might surprise me in a similar way!

AND BOY, DID IT. The background is now a much warmer gray, which I think just feels better, in comparison. Too much technology is all calm and blue-feeling, you know? Then, all the highlight trim has shifted from the murky magenta to a really stand-out orange-yellow that I happen to think is MAJORLY appealing. So all the colour problems are solved, to start. Like-- it might not LOOK different, but it FEELS different:

Mrblandingsbuildshisthemehousesmall.png

(click here for full-size image)

Since Fire was forked (fork that Fire!) from an earlier version of gameOS, it also leaves out a kinda-extraneous-n'-ugly "Return to Launcher" screen, which feels much better to me.

Next up-- and this was an accidental find, but-- there's LETTER NAVIGATION IN GAMEOS FIRE, but NOT in gameOS.

[NOTE: Actually, this is untrue! The current source code of gameOS that I downloaded is just missing some stuff. The ZIP on the Pegasus website is missing even more currently. If you download v1.10 from the RELEASES tab, you get letter navigation. I just hadn't thought to look there, so I'll credit Fire for this illumination.]

You activate it by pressing PageUp & PageDown. Going back to Minimis, I found out that PageUp & PageDown work there, too, but gameOS - Fire's letter navigation goes the extra mile: It wraps around. You can start at # or A, then press PageUp and go straight to Z. WEE! This integration of letter navigation was even made by the dude behind Minimis. I don't know if the feature is on the chopping block (due, I assume, to the problems I'll be discussing), but it's so worth it. My computer is full of choices, and I feel like the software I use should share that same philosophy. Rather than just having one way of interfacing with something, I find that I'm most in-the-zone when I can take advantage of different methods of interaction spontaneously in any configuration I choose. It's like the multifaceted moveset of Mario-- especially in Super Mario 64, you know? That freedom of expression allows for a deeper, more personal user connection. I just want options!

But I was still left with at LEAST one more major gripe about both gameOS - Fire and the... what's the opposite of a fork? A spoon?... The spoon from whence the fork originated: gameOS. That leftover double-title just wouldn't be acceptable for me. Up until this point, I hadn't even considered trying to mess with the code that made up the themes. I've been trying for many years to sort of... fall into programming. The things I do are really just archaeology of other people's code, then essentially scrambling bits of code until it does what I'd like it to do, regardless of how graceless and suboptimal my solutions are. They work and all; they're just nothing but duct tape. So I hadn't considered it-- besides, who knows how complicated it would be to dissect?

Turns out, really not-at-all! QMLs are just plain text files written in what is essentially a relative of JavaScript-- which works super-well with where I'm at on my coding journey, having learnt serviceable smatterings of basic HTML, AS2, AS3, Lingo, AppleScript & the aforementioned JabbaScript. So I took the elimination of the double-title as my first coding challenge to overcome, then continued to stumble all the way down to the bottom of this long-long document!

But before we get to that part, there were quite a few other related problems I encountered and solved personally in my duct-tape sorta way. Since the floor is mine (フロアイズマイン), I feel that I may as well say what I wanna say. And let the code fall out.

Adding to Launcher

Adding Games (General Methods)

So, although launching a game in a launcher is supposed to be, most ideally, around as simple as drag-and-drop, Pegasus hasn't been so kind to all the different types of scenarios I've presented it with. First off, Pegasus is probably way more comfortable opening things on a Windows computer, and I don't blame it. But also, what I'm throwing at the launcher is, more often than not, a little off the beaten path. All we gotta do is just beat it! (*tuneful echoes*)

I'm about to give every specific specific I've had reason to specify myself, but here's what most of the methods have in common: Script Editor, formerly known as AppleScript Editor. I've already mentioned I know the itty-bittiest amount of AppleScript, and when I realized that all that Pegasus needed was some kind of .app it could reliably detect (it doesn't know what to do with most other filetypes), it gave me a winding workaround of an idea. Script Editor can create tiny .apps that perform certain functions, and can keep those apps open until certain parameters are met. And Script Editor can also execute shell script, which is important, since a great deal of the best launching practices require Terminal commands.

Side-note: Yes, Pegasus has a built-in place to run Terminal-type commands-- that IS what the launch variable is for, after all. However, I've found that, at least on my system, even if it halfway-works and will open up the right program with the right file, it tends not to focus the screen, or sometimes, seems to be unable to access all the resources it needs correctly. So. We do this.

To Pegasus, it's JUST like launching the actual thing we want to launch, but pinned on a little lightweight alias in-lockstep, which seems to be far more liable to work across the board. Automator could also do everything Script Editor's doing, but Script Editor apps tend to be just hundreds of kilobytes, while Automator apps are all a little over 3MB. So, since I'm all about shhaving shhpace (3MB might not seem like a lot, but boy-oh, 3MB files sure can add up when weighed against a buncha 180kb-somethings.), I personally use Script Editor .apps for this purpose exclusively, so all my examples will be Script Editor .apps.

As mentioned, none of this launching information is made for Windows-- besides... don't s'pose you'd have any such problems on a Windows computer. I'd assume if you wanted to do anything that requires more complicated launching, you might be able to make a simple .bat script in a very similar fashion to my own.

Adding Games (Specific Methods)

This section is far from comprehensive (you'd probably contest that statement) and is sure to be added to when I find out some new addition to my collection doesn't open properly with a standard launch: {file.path}. Enjoy being confused by the wealth of weird.

Security Prerequisites

macOS has continued to increase security measures from version to version, and it becomes VERY cautious nowadays with a lot of the normal prompts you can run in Script Editor that control computer functions. It's all very counter-intuitive, since the whole point of Script Editor, Automator, and even Shortcuts is to control your computer. So, as it stands, Apple gives us the power and immediately takes it away. (So Apple.) Besides what I had to write IN the script, I also had to add privileges to Security & Privacy in System Preferences. I'm not entirely sure HOW many of these are necessary for the scripts to work, but these will probably all benefit you, should you use these scripts. (I'm on Monterey, so the settings may be slightly different beyond this point.) Go to Security & Privacy, then under the Privacy tab, select Accessibility. Click the + button and add the following programs:

  • AEServer (System > Library > Frameworks > CoreServices.framework > Versions > A > Frameworks > AE.framework > Versions > A > Support > AEServer)
  • bash (Go to Macintosh HD, and select Go > Go To Folder from the menubar. Type in "bin". Find "bash" in this directory. Drag this file into the prompt window that comes up when you click +, then select Open.)
  • osascript (Go to Macintosh HD, and select Go > Go To Folder from the menubar. Type in "usr/bin". Find "osascript" in this directory. Drag this file into the prompt window that comes up when you click +, then select Open.)
  • Pegasus (Wherever you've placed your personal download of Pegasus-- you'd know better than me. Also, if you install a new version of Pegasus, you're going to have to remove it from the list and re-add it to make it work again.)
  • Script Editor (Applications > Utilities > Script Editor)
  • System Events (System > Library > CoreServices > System Events)
  • Terminal (Applications > Utilities > Terminal)

There may be some other one-off prompts that will come up, asking you to grant other kinds of access-- just accept them as needed. Lemme know about them, if it turns out I've missed any crucial preparation! Hard to know what you've done when a lot of it wasn't even done for this specific reason. You're not the only project in my life, I'll have you know.

Compatible macOS Apps

Alright, maybe it's best if I start by explaining how a game entry looks in [metadata.pegasus.txt]. When you boot Pegasus for the first time and hit Esc, you can head to the Settings screen and "Set game directories". It'll ask for you to select .txts in the directories you'll be launching your games from. So each of the .txts will start like this...

[metadata.pegasus.txt] <-- (Whenever I refer to text that belongs in a certain file, I'll do this headline thing.)

collection: [insert name of collection here]

My Games collection is called JAMES. You know, like how Homsar sez'. So mine looks like this at the top:

[metadata.pegasus.txt]

collection: JAMES

Then afterwards, each game/jame is listed in neat little infoblocks. Like this:

game: Bean's Quest
file: ./BeansQuest.app
developer: Kumobius
publisher: Kumobius
genre: Platformer
release: 2011-07-16
players: 1
rating: 100%
launch: {file.path}
assets.screenshots: ./pegasus_assets/beansquest_box.png
assets.logo: ./pegasus_assets/beansquest_logo.png
assets.background: ./pegasus_assets/beansquest_background.png

If you're following all the stuff that I personally do in my Pegasus setup, this is how you're gonna want to do it. Explanations and elaborations (as in, extra metadata fields!) shall come later. Depending on your own setup, you might be placing certain assets under different classnames, or you may have less or more variables to fill. For example, I really dislike having any information involving summaries, short or long, so I've taken them out of my infoblock entirely. I'm only putting games I know I want to play into Pegasus, so I don't need to be told what the game's about. Besides, I like flavour in my flavour text; I'm quite critical of copy. So I wouldn't just accept anyone else's back-of-the-box without scrutiny.

In the example I just gave, the game I chose (Bean's Quest, with music by the indomitable flashygoodness) runs perfectly fine from its actual filepath! No extra precautions necessary! Easily opened from your Desktop, AND from Pegasus!

(Specifically, Pegasus searches for the "game" value when you use a search bar. If you have an accent or diacritical mark (ö, ø, é, etc.), you will have to type in that mark for it to come up. So, if you don't want to do that, don't type in that accent. Das' the price you pay for being fancy.)

Non-compatible macOS Apps

But some apps, while they'll open fine with a lil' double-click from a Finder window or a single selection from your Dock, Spotlight, etc... Pegasus will open the game, but then have no clue it's open, so it never gives its reins over to the app. If Pegasus is launching something correctly, the screen will slide over to the booted game. If it's not, it'll just stay at the booting screen. Most of the time, it seems to be because the game is booting in windowed mode, but some games boot just fine, regardless of if they're windowed or fullscreen. My working theory is that certain games are more aggressive at ensuring the screen is focused on them when they boot up. So, while I could just slide my screen over manually to get to the game, that's pretty close to something flat-out not working.

So instead of just letting that be-- just accepting that this launcher is primarily for display and not for function-- instead, this became my starting point with Script Editor. This is the first and simplest of the applications I created, so it was a good place to start for me, and I hope it'll be for you, too.

Open up Script Editor, make a new document (with the scripting language set to AppleScript, which it should be automatically), and type in the following.

set filePath to "[insert UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application filePath to activate
	delay 1
	if application filePath is running then
		repeat until application filePath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

If your game has one of those Unity-style resolution-and-graphics settings panel-types to start off the process, or something similar, you might need to activate the Finder first before activating the game, to achieve full-focus:

set filePath to "[insert UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	delay 1
	tell application "Finder"
		activate
	end tell
	tell application filePath to activate
	delay 1
	if application filePath is running then
		repeat until application filePath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

As you can see, for both of these, there is only ONE blank that you personally need to fill. That one part in the []s-- You need to get a UNIX path for whatever it is you want to launch. To do so is VERY easy: Click-n'-drag the file in question right on-top of the script's window. The text of that UNIX path will appear wherever you dropped it.

To finish the process, go File > Export, change the File Format to Application, and Code Sign it to Run Locally. Then, put this as your game's filepath in the metadata. Relevant information in metadata.pegasus.txt:

[metadata.pegasus.txt]

file: [insert path to Script Editor .app]

[...]

launch: {file.path}

Out of the 120 games I've currently put into Pegasus, 103 use some form of an AppleScript launcher, while 17 can be launched with the standard Pegasus launch command. Maybe another 17 or so of the games in the 103 would've worked fine, had I always had them in fullscreen mode, but I... typically fluctuate between fullscreen and windowed, so unless the game forces fullscreen at startup every time, I can't rely on that to focus the screen. Really, there's a case to be made for having all of them calling on these secondary launchers. This way, you can keep these lightweight aliases on your computer, remove the actual games when you're not going to play them any time soon, but still get to appreciate the beauty of the game-art you provided. PLUS, if you happen to launch it and the game isn't currently on your HD (or perhaps has just been relocated or renamed, etc.), all the scripts I'm providing will toss up a really neato-completo message explaining what's missing, what's it called, and where it's supposed to go. It's all a LOT more useful than it just... disappearing from your collection, which is what it typically does when you remove a game and you're not rerouting through these AppleScript alias assistants.

What's written here will only work for native macOS .app files-- hence the part where the script, y'know, activates an application and all. (Gotta love the plain English of AppleScript. In-case you're wondering, the magic word in the AppleScript that places correct focus onto the game is, in-fact, "activate".) But stick around, there's lots ahead for emulation, and other interesting outliers!

Steam Games Running in Steam

As someone who's trying to pick up as little Steam as possible (teehee), I like to open my Steam games like normal applications instead of through Steam services. Quite a few games don't bother implementing DRM-protection to begin with, so when booting outside of Steam, they won't simultaneously open up Steam to verify if it's a legitimate copy. But even if it DOES do that, I'd say about 90% of games will, after the security checkpoint, work perfectly.

You might be wondering why I'm even talking about running games from your Steam library when Pegasus outrightly has an option FOR adding Steam games automatically. Besides the fact that I want to add my own artwork to begin with, I've noticed some other downsides. Pegasus doesn't automatically detect that you've finished a game this way. It just waits for you to tap on Pegasus in your dock, then click on the launching screen to return. I checked-- you don't even have to close anything. You could continue to keep everything running, go back to Pegasus and just tap the screen... and it'll think you closed the game. (This approach to launching also means that Pegasus is using more resources than usual, since it doesn't go into that hibernation I've become accustomed to.) And you also need to completely close Steam every time you want to launch another game this way, at least nowadays. Steam's been getting loads of updates, so it's possible that a recent modification is impeding the process. But as it stands, if you keep Steam open after launching a game, or have it open BEFORE trying to launch any games, nothing will launch until you close Steam. And I've already mentioned how bulky Steam is-- I feel like it has to boot up three separate times whenever I open it. 'Sides, what did I say about wanting all my games to be TOGETHER? Why would I want to segregate Steam? Part of the fambly, I say!

With my methods, you can have your own custom art, actually keep track of genuine application run-time, AND you can keep Steam open all the while. You can give the methods for Compatible macOS Apps & Non-compatible macOS Apps a try, and just use the game's filepath found in your User Library under Application Support > Steam > steamapps > common. (If the Library's not visible in your User folder, while in your User folder, select View > Show View Options and check Show Library Folder. There are other ways, but this is the Method of Modern Mac. M-E-T-H-O-D-@-M-1-M-A-C.) Either of those ways, should Pegasus do its job right, will keep perfect track of the game being open.

Or, instead of launching the game without Steam, you can create the following Script Editor .app to launch the game through Steam-proper with Terminal prompts:

set filePath to "[insert UNIX path of game here]"
set processPath to "Steam"
set terminalScript to "open steam://rungameid/[insert gameid here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	if application processPath is running then
		repeat until application filePath is running
			delay 1
		end repeat
	end if
	if application filePath is running then
		repeat until application filePath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Once again, locate the Steam game inside your User Library, and place it under filePath. Then, for the gameid, the easiest way to figure it out is just by looking the game up in the Steam store. The URL for the game will include the necessary ID directly before the name of the game. The way this is set-up, Pegasus will re-activate whenever you close the game rather than when you close Steam, which I think is much more useful if you're not just going to play one Steam game in a session.

Non-Steam Games Running in Steam (Steam Input)

Now for the real magic that comes from letting Steam stay in your party. (... It casts Magic Missile at the darkness!) As previously mentioned, Steam is pretty exceptional at taking controllers that otherwise would not be supported well by a game and converting its inputs into something more universally recognized. Connect the controller you want to use, boot Steam, go into its Controller settings, and just check and see that it's recognized. You might need to map it initially, or mess with the preset, but after that, it's set from the moment it's connected again.

Depending on the game, in order to add this controller compatibility layer, you'll have to go into the game's info in Steam, press the gears button, select Controller settings and either Enable Steam Input or use default settings. ... Well, actually, there might even be many times, weirdly enough, when Disabling Steam Input allows you to USE Steam Input? I think I've disabled Steam Input more than I've enabled, come to think of it. Just try all three settings and see which gives you controller support.)

But this isn't just for games you bought on Steam -- you can add non-Steam games (or even plain ol' non-Steam APPLICATIONS) to your Steam library (click the Add a Game button in the bottom-left corner of your Steam window), then allow Steam Input support for them. Only question is, how to boot a game that doesn't have a gameid?

Turns out, once you add a non-Steam game and boot it once, it's given a unique local gameid. Following the instructions on this Steam Community post, I looked for a folder in my Steam userdata numbered "760", then opened up the file in there in TextEdit. All non-Steam games and their newly generated gameids are in there. With that information, the exact same format of the Script Editor app fits the circumstance yet again:

set filePath to "[insert UNIX path of game here]"
set processPath to "Steam"
set terminalScript to "open steam://rungameid/[insert gameid here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	if application processPath is running then
		repeat until application filePath is running
			delay 1
		end repeat
	end if
	if application filePath is running then
		repeat until application filePath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

I found this incredibly useful for running Celeste and Celeste mods with controller support. (Unlike my incredible brother, I have the most difficult Time with holds and ordinal directions without a controller.) Just have Steam boot Olympus, and the controller support passes through. That shouldn't work. But it works.

Various Games Running in ScummVM

Point-and-click adventures will always be my favourite type'a game, so ScummVM is truly my favourite of all cross-platform game-playing tools. So I'm-a putting a spotlight on it here at the top of the list of supported systems. It's also a nice, clean introduction to using Terminal commands to properly launch a game! Before doing this, you're going to have to add the game(s) in question TO ScummVM, which is a very simple process. Then, in Script Editor:

set folderPath to "[insert UNIX path of game folder here]"
set processPath to "[insert UNIX path of ScummVM here]"
set terminalScript to "[insert Terminal-style UNIX path of ScummVM.app/Contents/MacOS/scummvm here] -f [insert ScummVM gameid here]"

tell application "System Events"
	if exists folder folderPath then
		set folderExists to true
	else if not (exists folder folderPath) then
		set folderExists to false
	end if
end tell

if folderExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if folderExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & folderPath buttons {"OK"} default button "OK"
	end tell
end if

Instead of dragging a specific program into Script Editor like the last few times, you'll be dragging in a specific folder containing all the game files. ScummVM doesn't work by locating a file, but rather, a whole directory, so it only feels right that I do the same here. Hence I replaced all mentions of "file" in the code with "folder". One thing to keep in mind when adding Terminal code is that if UNIX filepaths are involved, you'll be having to write them in a more cumbersome format. To ensure you get it right, open up Terminal (found in Applications > Utilities), and then begin to write in whatever's necessary for that terminalScript value. Find your copy of ScummVM, right-click it and Show Package Contents. Look in Contents > MacOS, then drag ScummVM into the open Terminal window. The path, as it has been written, might include backslashes.

These unusual buggers. --> \

Any time these are part of a UNIX filepath, when you include them in your Script Editor script, you're going to have to double them up.

Like this. --> \\

So they're not lonely.

For example, my Terminal script to boot The Longest Journey looks like this:

/Applications/\*\ JAMES/ScummVM.app/Contents/MacOS/scummvm -f tlj-win

However, to include it in Script Editor, in the terminalScript value, I'm going to have to write it as:

/Applications/\\*\\ JAMES/ScummVM.app/Contents/MacOS/scummvm -f tlj-win

And let's not forget your gameid! (As you can see, mine was tlj-win.) Boot ScummVM, highlight the game you're running, then press Game Options. Under the Game tab, ID should be the first value listed. You could even modify that preset, should you desire to specify such to your own liking!

Playdate Games Running in Playdate Simulator

I feel like this is the proper follow-up to ScummVM-- if there's one engine currently unsupported by ScummVM that belongs in that party, it's HyperCard. And Playdate is just neo-HyperCard (Neo Hyper Card sounds like an anime.), c'mon. In-fact, the one PDX I currently have on my computer is a remake of a classic HyperCard stack. (Shout-out to both Thoru Yamamoto & Matt Sephton.) And what's so great is that, while it's not very advertised, you can actually play any PDX files you might happen upon on itch.io with an officially licensed emulator! It's included in the double-digit-MB-occupying and fully cross-platform Playdate SDK.

Instead of the normal fullscreen command I've used, I'm firing off a keystroke. The other more sophisticated technique just centers the app, without making any aspect of the UI larger. Just... puts the Simulator as-is on a black background.

set filePath to "[insert UNIX path of game here]"
set filePOSIX to POSIX file filePath
set appPath to "[insert UNIX path of Playdate Simulator.app here]"
set processPath to "Playdate Simulator"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application appPath to open filePath
	tell application appPath
		activate
	end tell
	delay 1
	tell application "System Events"
		keystroke "f" using {command down, control down}
	end tell
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

iOS Games Running in PlayCover

Here's a real fun macOS-only dealio-- PlayCover's a surprisingly robust way to play iOS games on your computer. Which is great for me, because I find playing games on mobile devices to be a far less engrossing way to play games, in spite of how terrific many of the games exclusively on that platform are.

In Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert name of game process here]"
set terminalScript to "osascript -e 'tell application \"System Events\" to set value of attribute \"AXFullScreen\" of front window of process \"[insert name of game process here]\" to true' > /dev/null 2>&1 &"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application filePath to activate
	delay 1
	do shell script terminalScript
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

This is a fitting second example of Terminal use, since it includes non-paired backslashes. This is because the code it's in does not involve breaking up spaces in a UNIX filepath. Those are there to allow me to write quotation marks without setting off the scripting syntax. To find a game you've added to PlayCover, you boot PlayCover, right-click on your chosen game and Show in Finder. For myself, I placed a symlink to the PlayCover Games folder in my metadata directory, which I then use as my filepath. I've done likewise for my Steam Games, etc. (Symlinks are like a much stronger version of an alias-- they don't break when referred to by databases, etc.)

MOST of the time, there's a good chance the name of the game process is just the name of the PlayCover app itself, but not always! It's almost certainly going to be the name of the window when you boot the game. If you're in doubt, you can also check for the process name in Activity Monitor (found in Applications > Utilities).

One current issue with PlayCover emulation is, for certain games that would normally have online connectivity, it'll get stuck in this prompt loop about your Game Center ID-- every time you click in and out of the game window, in-fact. For games that are like this, just to save me the stress of selecting it the first time (and more importantly, still getting me into fullscreen-- this is really important since none of these are running in a MacBook-style resolution...), I run a little code in it that cancels out of that prompt for me.

Here's that modified AppleScript:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert name of game process here]"
set terminalScript to "osascript -e 'delay 2' -e 'tell application \"System Events\" to set value of attribute \"AXFullScreen\" of front window of process \"[insert name of game process here]\" to true' > /dev/null 2>&1 &"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application filePath to activate
	delay 2
	tell application "System Events"
		keystroke return
		delay 1
	end tell
	tell application filePath to activate
	do shell script terminalScript
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

A lot of delays in there-- any game that flashes this prompt can be a little finicky about when it gives the user the ability to toggle fullscreen, so it's all there to give a lil' buffer. It's silly to say, but it feels luxurious to have my computer hit two buttons for me.

EXEs Running in Wineskin

I DO have Parallels, and MOST Windows games will run better in Parallels... but actually, not always! Certain games I've encountered will refuse to run on Parallels for me, but work like a dream in Wineskin. It can also just feel less cumbersome to launch a program rather than a whole other OS. When you can.

Right-click on Wineskin, Show Package Contents, then open the Wineskin app INSIDE the Wineskin app. Press Advanced on the first menu, then go under Tools > Custom EXE Creator. Type in a name for your Custom EXE, then Browse for the .exe you're wanting to launch. Hit Save, and then right inside the Package Contents will be a newly generated .app that will directly launch the .exe you chose. This .app will be now be sourced in the AppleScript you make:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of wine64-preloader here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application filePath to activate
	delay 3
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Under filePath will be that personalized EXE Launcher you made, then under processPath, you'll put wine64-preloader. wine64-preloader's whereabouts are within Wineskin's Package Contents: Contents > SharedSupport > wine > bin. I've noticed that, even if the game is a 32-bit version, wine64-preloader will still open and stay open until the game has been closed.

(This is probably something I'll document later, but I currently don't have access to a computer that runs GPTK, but any kind of launching method for GPTK-- Wineskin or otherwise-- should be MAJORLY easy to cue.)

EXEs Running in Parallels

In order to launch a specific EXE correctly through Parallels, I had to have these virtual machine settings properly configured: (Click on the Gears icon to access these settings.)

  • Under Options > Sharing > Share Mac, check "Share custom Mac folders with Windows" & "Share Mac volumes with Windows".
  • Under Options > Sharing > Share Windows, check "Access Windows folders from Mac"
  • Under Options > Applications, check "Share Windows applications with Mac"

I'm not sure if all of these are necessary, but they seem to make Parallels happy when I'm trying to do cross-OS stuff. (Whoa, that'd be a nifty open-source OS name. crossOS. You could even spiff it up, like, +OS, or xOS.)

One frustrating thing about setting things up this way is that ALL of the programs on the Windows side get shown as normal .apps on my Mac side. This is only frustrating when you happen to use the Spotlight to search for programs, like I do. Submit it for your .app-roval: You might be typing in "Calculator", because you just wanna use your Calculator-- but the Spotlight will always favour the Windows Calculator. So you're bound to hit Return before registering why the icon looked like a Discord emoticon and, whaddya know, now you're booting up Parallels! (Now you're playing with power!) Thankfully, an easy workaround for this Spotlight issue is to remove the "Applications (Parallels)" folder-- found in your User folder-- from being indexed at all. In System Preferences > Spotlight > Privacy, just add that outlying Applications folder, and you'll never get Terminal and Terminal mixed up ever again.

Now that Windows apps behave like macOS apps, we'll be able to specifically launch programs in Parallels rather than just launching Parallels itself.

I like to keep most of my Windows stuff OUT of the virtual machine itself as much as possible-- it's already bulky enough as it is, and I don't know if I believe that it's always clearing out space when I remove a double-digit-gigabyte dealio from that virtual disk space. So the best way I've found for running games through Parallels that reside in my macOS directory is to place these games inside a folder inside my user folder... inside my Users folder. I have called that folder "JAMESPALARINOS". You just KNOW that Homsar would call "Parallels" "Palarinos", right? Then, in Parallels, I go into the Mac Files shared folder that should be on the Windows desktop. Otherwise, it should be one of the Network locations under This PC (probably called "Home"). Then, I find the game I want to play, right-click it, and create a shortcut. (On the Parallels side, that is, not the macOS side. A shortcut, not an alias.) For some reason, games being played out of the Mac directory tend to run better when booted through a shortcut. Must fool it a bit, since it created the shortcut itself, thereby making it a recognized part of the Windows VM.

When you boot the shortcut, you'll notice that what you opened will actually be IN your dock. Right-click that Dock icon and go Options > Show in Finder. If this app isn't in a folder labeled "Windows 11 Applications", or something similar (y'know, in "Applications (Parallels)"), this newly generated app is probably stuck in a temporary location. Take it and place it in Applications (Parallels) > Windows 11 Applications (or whatever that second folder level might be called for you). Now you'll be able to launch and monitor the Windows app through this macOS alias of a Windows alias!

In Script Editor:

set filePath to "[insert UNIX path of game here]"
set filePOSIX to POSIX file filePath
set processPath to "[insert UNIX path of prl_client_app here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application "Finder"
		activate
		open file filePOSIX
	end tell
	delay 3
	if application processPath is running then
		repeat until application filePath is running
			delay 1
		end repeat
	end if
	if application filePath is running then
		repeat until application filePath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

filePath this time is obviously what you just found or placed in Applcations (Parallels). Make sure you're continuing to drag-and-drop your file into the Script Editor window, rather than trying to guess the UNIX path yourself. See, the name of the folder is secretly different than the name it displays. For me, instead of "Windows 11 Applications", it turned out to be "{9d1c9520-32ec-4bf0-9fac-ee0ee8a639c9} Applications.localized". Then, prl_client_app will be found in the Package Contents of Parallels Desktop.app > Contents > MacOS.

Sometimes, certain games will still refuse to run unless they're really added to the Windows side. Just do all the same steps after copying the files over -- create a shortcut, boot the shortcut, find/relocate the newly generated macOS shortcut, and add that to the AppleScript. There will also be a few games that might be technically running a different program after the initial launch. You have to ensure you're following the launch process through to its final destination, or Pegasus will think you're finished on the Parallels side. (Yeah, that's the nice thing about what I did-- just like with Steam, you can keep Parallels open between games, rather than Pegasus waiting for Parallels to close before welcoming you back. It's a little trickier on the coding side, but ultimately has a clear reward in boot time. If I thought waiting for Steam to boot was hard, just think about booting a whole other operating system... No, actually, Steam still takes longer.)

For any game that does a two-step launching process, we'll want to do something like this:

set filePath to "[insert UNIX path of game here]"
set filePOSIX to POSIX file filePath
set processPathA to "[insert UNIX path of prl_client_app here]"
set processPathB to "[insert UNIX path of post-launch app here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application "Finder"
		activate
		open file filePOSIX
	end tell
	delay 3
	if application processPathA is running then
		repeat until application processPathB is running
			delay 1
		end repeat
	end if
	if application processPathB is running then
		repeat until application processPathB is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

I haven't yet run into a game on the Mac side that would require me to check for a different app after launch, but I'm sure that they exist! The logic of this example would fully transfer over.

EXEs Running in Boxer

Certain EXEs are so old that they don't run on Wine OR Parallels! Those are MS-DOS games. Although, MS-DOS really was the first major Microsoft product, so, shouldn't it be MS-UNO? Mm?

The most user-friendly way to boot DOS stuff (although perhaps less powerful than DOSBox-X) is through an Apple Silicon build of Boxer. Launch Boxer, and generate your game package (it's essentially a folder with a simulated C drive and some preset preferences), then with that information prepared, fill in this AppleScript:

set filePath to "[insert UNIX path of game here]"
set filePOSIX to POSIX file filePath
set processPath to "[insert UNIX path of Boxer here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application processPath
		activate
		open filePOSIX
	end tell
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

In this case, the filePath value will be the package that Boxer will generate. (By default, it'll place anything it generates into a folder called DOS Games in your User directory.) Boxer packages are set to hide their filetype, which is .boxer. (I mean, it's made BY Boxer, so shouldn't it just be .box? Or, er... .punchintheface?) Another good reason to use my technique of grabbing the UNIX path by drag-n'-drop. Give 'um the old one-two, as it were!

RPG Maker Games Running in EasyRPG Player

EasyRPG Player, much like ScummVM, acts as a central hub for picking RPG Maker games. Funnily enough, it doesn't even allow you to use some kind of hotkey to return BACK to the game-chooser menu after choosing a game. You just gotta reboot to pick again. So breaking it free from this non-effective launcher situation and into the loving encircling embrace of the wings of Pegasus will be a fine move.

The good news is that, if you remove the Unix executable from inside EasyRPG Player's Package Contents and just place that IN the directory of any RPG Maker 2000/2003 game (meaning right alongside its respective RPG_RT.exe), it'll boot RIGHT into the game. But as someone who's trying to save space, I don't want to duplicate even this dinky 16meg file into the folder of every game I've got. Instead, I've created Unix executables of my own that just look inside the copy of EasyRPG Player I have, then boot that executable like it's sittin' in all of the different spots this significantly smaller file is now placed in.

Er-- well, first, one last bit of my personal druthers that will influence the rest of the scripting here: I personally have EasyRPG Player to always initially boot in Windowed Mode. This is because, on its default Fullscreen boot, it doesn't allow you to switch to Windowed Mode. You're just kind of stuck. Meanwhile, if you specify the Windowed parameter, you can toggle back-and-forth whenever you please. (It's like a conversation of resolutions!) So, the first step, should you choose to do these steps the very way I do them:

  • Right-click EasyRPG Player & Show Package Contents.
  • In Contents > MacOS, rename "EasyRPG Player" to "EasyRPG Player_App".
  • Open TextEdit (found in Applications) and create a new document.
  • On this document, select Format > Make Plain Text.
  • Enter this script:
#!/bin/bash
[insert Terminal path of EasyRPG Player_App here] --window

(Like before, to ensure correct formatting for the EasyRPG Player_App path, just open Terminal and drag-and-drop that executable. It may or may not have those backslashes we talked about. Since this isn't AppleScript, don't double them up. And, of course, moving forward, if you're not bothering with any of my windowed malarkey, you just do what makes sense depending on the steps you've skipped.)

  • Save this TextEdit document inside EasyRPG Player.app/Contents/MacOS. Keep encoding on Unicode, but uncheck "If no extension is provided, use '.txt'." Then entitle this file, "EasyRPG Player", no filetype at the end.
  • Back in Terminal, enter "chmod +x " (sans quotes, with space at the end), then drag-n'-drop this new "EasyRPG Player" to fill in the text after that space. Hit Enter, and the text file will have turned into its own Unix executable. Now, when you open EasyRPG Player, it'll boot in windowed mode! So this'll be good groundwork for any outside-of-Pegasus interaction.

Using the same techniques for making Unix executables, we'll now make another text file prepped to become a Unix executable. In this Plain Text document, enter:

#!/bin/bash
cd "$(dirname "$0")"
[insert Terminal path of EasyRPG Player_App here] --window

That new "cd" line just ensures that, wherever we place this Unix executable, it'll be looking inside its own directory for game data. I believe "cd" means... "compact disc"? ;) No-no-no, "change directory".

Once again, save that file out correctly (I called mines "EasyRPG Player Passthru") and "chmod" it. ("chmod" stands for "depeCHe MODe." ...) Then, place a copy of this in each RPG Maker game directory you want to launch. When in the right place, it'll boot right into each individual game, which means we're ready to get that Script Editor out:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of EasyRPG Player here]"
set terminalScript to "osascript -e 'tell application \"System Events\" to set value of attribute \"AXFullScreen\" of front window of process \"EasyRPG Player_App\" to true' > /dev/null 2>&1 &"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application filePath to activate
	delay 1
	do shell script terminalScript
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

filePath will be whichever of our brand-new "Passthru"s we're wanting to boot, and processPath will be plain-ol' "EasyRPG Player.app", NOT its contents that we tampered with. Also, the script will take the initial windowed mode and toggle fullscreen to start out with. Might sound like a roundabout way to fullscreen, but getting to keep my window buttons are worth the extra distance.

One last RPG Maker-related osa-capade I had to go down: the original freeware version of OneShot can only be run properly on a modern Mac through a FORK of EasyRPG Player found here. So instead of our "Passthru", we'll be using the fork. So more of a "Stabthru". Place it in your OneShot directory-- I personally renamed mine to "EasyRPG Player OneShot Hack" so I wouldn't get confused. (The following actions will correspond with that name, thusly.) And here's the altered AppleScript:

set filePath to "[insert UNIX path of game here]"
set filePOSIX to POSIX file filePath
set processPath to filePath
set terminalScript to "osascript -e 'tell application \"System Events\" to set value of attribute \"AXFullScreen\" of front window of process \"EasyRPG Player OneShot Hack\" to true' > /dev/null 2>&1 &"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application "Finder"
		activate
		open file filePOSIX
	end tell
	delay 1
	do shell script terminalScript
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

In this case, it IS important to have Finder search out and open this file. If I had the UNIX executable activate itself in the script, it won't have any clue where the game directory is. So the Finder aspect allows it to have a sense of place. (Don't want the game to feel lost, now do we?) Another benefit of launching this through Pegasus is that you won't have to deal with the initial glitch-y boot into EasyRPG. At least on current Macs, it'll render the screen resolution incorrectly until you toggle between windowed/fullscreen once-times.

Various Games Running in OpenEmu

Honestly, there's very little reason for this, if we've got everything in Pegasus. (These cores are top-notch, though!) But this was one of those "What if... ?" ideas that was instantly hilarious to me, so I had to follow it through. Go ahead. Launch a launcher. With this code (which I have rewritten, to handle how bad OpenEmu is at being opened this way):

set filePath to "[insert UNIX path of game here]"
set filePOSIX to POSIX file filePath
set appPath to "[insert UNIX path of OpenEmu.app here]"
set processPath to "OpenEmu"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	activate application appPath
	delay 1
	tell application appPath to open filePath
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

If you'd like the game to open up in fullscreen, enable that checkmark in OpenEmu's Gameplay preferences.

The filePath in this case would be the ROM, which is PROBABLY going to be in your OpenEmu Application Support folder, I'd assume. Also, OpenEmu has a difficult time closing right if you do Apple-Q? If you go through all three Quit prompts that will pop up, you'll get stalled without a proper way to Quit. Cancel the quit on the last one and do Apple-Q again. What I've found to be the easiest way to click that red Quit button in the menu bar, then to tell it not to always prompt me on that (same with not to always prompt about restarting/resuming the game), THEN go Apple-Q after that.

Super Nintendo Entertainment System Games Running in SNES9x

The long-standing SNES9x emulator just recently got a huge M1-compatible overhaul-- however, it has just about zero Terminal launching ability. Most likely due to its oldie-but-goodie status in the emulator world. (Speaking of oldies-but-goodies, SNES9x.) So the following AppleScript has no schmancy sch-ell sch-cript:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of SNES9x here]"
set filePOSIX to POSIX file filePath
set terminalScript to "osascript -e 'tell application \"System Events\" to set value of attribute \"AXFullScreen\" of front window of process \"Snes9x\" to true' > /dev/null 2>&1 &"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application processPath
		activate
		open filePOSIX
	end tell
	delay 0.25
	do shell script terminalScript
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

It currently takes a good 5 seconds before SNES9x feels willing to let the script toggle the fullscreen, and there doesn't seem to be any option to make it boot games in fullscreen, so this is the best I can do with these limited options. Come to think of it, I might have to rescind my comment that running OpenEmu is a silly idea. It's an easy-breezy way to run SNES games on a Mac-- s'got an SNES9x and a BSNES core, it runs any game perfectly with lots of easily accessible features... Can't rightly ask for a better user-experience right out of the gate. But this HAS been my emulator since the Mac OS 9 days, so there was no way I WASN'T including this as part of my button-mashing family.

Super Nintendo Entertainment System Games Running in bsnes-hd

As I got to the ending of that last entry, I decided to take one last look-see through the SNES emulation scene on macOS, and remembered that the recent widescreen fork of BSNES was super-duper cross-platform. And so, I just gave it a shot, and... it allows Terminal prompts. And lest you forget, I said it's a widescreen SNES emulator. It allows you to see just about twice the amount of information on either side of your screen!! Unless a certain Vitor Vilela changes the bounds of each object, you'll end up with LOTS of things sort of popping in and out of existence in the non-4:3 boundaries of the screen, but it's still enough to make my heart soar out of my chest. AND it can be turned off (under Enhancements), should you want a classic experience. (I'll admit, games all benefit from different aspect ratios, just like movies.)

One last note about the program-- you'll definitely want to set hotkeys for Pause, Toggle Fullscreen, Fast Forward, and Save/Load State. All useful, all unmapped, and all strangely difficult to access, at least on a Mac.

The new script, IN HD (or hd? Caps makes a difference.):

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of bsnes_hd.app here]"
set terminalScript to "[insert Terminal-style UNIX path of bsnes_hd.app/Contents/MacOS/bsnes here] [insert Terminal-style UNIX path of game here] --fullscreen"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Again, all paths in Terminal scripts are most easily generated in Terminal, and if there are backslashes IN the paths, double them up.

Nintendo DS Games Running in melonDS

Thinking about it, all the rest of the examples I'm currently typing will have those beautiful Terminal prompts! (More commonly referred to as command line switches. As convenient as a light switch-- but it tends to feel like trying to look for a light switch in the dark! I feel like I often just type in "-fullscreen" or "--fullscreen" and just hope for the best, and more often than not, it works.)

melonDS' real showstopper is its full emulation of the console experience for both DS and DSi-- however, I don't think there's currently any way for me to specifically boot into one DSi game over another, since they're installed INSIDE the system itself. Quite the non-problem, though, when navigating on a DSi is one of the finest feelings I know.

In Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of melonDS.app here]"
set terminalScript to "[insert Terminal-style UNIX path of melonDS.app/Contents/MacOS/melonDS] [insert Terminal-style UNIX path of game here] --fullscreen"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

There's almost no chances your Terminal-style UNIX path ain't gonna have at least TWO backslashes this time. And those two will be turning into four. Double up the backslashes after getting the paths from Terminal!

Nintendo GameCube/Wii Games Running in Dolphin

Here's an added wrinkly obfuscation to our process. Certain emulators seem to have issues being launched through my Script Editor alias, if that alias is being sourced by Pegasus. Some of the resources that give the emulator considerable power are deprived from them, when Pegasus directly launches the alias. And so, I found that if I, instead, had Pegasus hold onto an alias that, in turn, launches and watches for ANOTHER alias... ... then the original alias could then proceed to do all of its processes unimpeded.

What I'm saying is this one takes TWO Script Editor apps. I call the first one "[insert name of game here]_pre" and the second one "[insert name of game here]_run". You'll be adding the "_pre" app as your filepath in [metadata.pegasus.txt].

"_pre" will look like this, for Dolphin:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

"_run" will look like that, for Dolphin:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of Dolphin.app here]"
set terminalScript to "[insert Terminal-style UNIX path of Dolphin.app/Contents/MacOS/Dolphin here] [insert Terminal-style UNIX path of game here] --config \"Dolphin.Display.Fullscreen=True\""

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Dolphin's truly a marvel among emulators. Have you HEARD about its ability to run a whole bunch of instances of Game Boy Advance natively?! As someone who actually played Four Swords Adventures on an actual Gamecube and enjoyed it immensely, I can tell you this is a far superior way to get that asynchronous multiplayer goin'.

Nintendo 3DS Games Running in Citra

Citra needs a _pre app, as well. Again:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

The _run app:

set filePath to "[insert UNIX path of game here]"
set processPath to "citra-qt"
set terminalScript to "[insert Terminal-style UNIX path of citra-qt.app/Contents/MacOS/citra-qt here] [insert Terminal-style UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Double-up any backslashes you get from Terminal-style UNIX paths. Just in-case anyone else ends up confused like me, if you're on Monterey, there's this flashing-screen issue that apparently goes away if you just bump yourself up to Ventura. But that's not a "just" for me. I can't a "just", if you get my GIF-t.

Nintendo Switch Games Running in Ryujinx

Ryujinx REALLY needs the _pre app-- otherwise it completely takes away the Hypervisor that makes. Time travel. Possib-- I mean, makes Ryujinx run at framerates beyond a cool 5fps. Which feels like just as much of an accomplishment as time travel, really.

_pre, Script Editor:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

_run, Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of Ryujinx.app here]"
set terminalScript to "[insert Terminal-style UNIX path of Ryujinx.app/Contents/MacOS/Ryujinx] --fullscreen [insert Terminal-style UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

For the terminalScript, make sure you're doubling up the backslashes! You won't even be able to save the script until you learn to heed this advice!

There's a few feasible command-line flags to add for customization (including booting in docked/windowed mode), but there are still plenty of important settings that can't be initially set through the command-line. Instead, I've created multiple JSONs in the Ryujinx preferences folder, and I have a version of this Script Editor launcher that simply juggles a few names around so that another JSON ends up as the in-use [Config.json], then switches everything right back afterwards.

In this case, _pre will remain the same...

_pre, Script Editor:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

And _run will be as such...

_run, Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of Ryujinx.app here]"
set terminalScript to "[insert Terminal-style UNIX path of Ryujinx.app/Contents/MacOS/Ryujinx] --fullscreen [insert Terminal-style UNIX path of game here]"

set configDirectory to "[insert UNIX path of folder Ryujinx preferences folder containing JSONs]"

set configInUseName to "Config.json"
set configCustomName to "[insert name of other JSON w/ custom game-specific settings]"
set configNotInUseName to "Config_notinuse.json"

set configOldPathA to configDirectory & configInUseName
set configOldPOSIXA to POSIX file configOldPathA
set configOldPathB to configDirectory & configNotInUseName
set configOldPOSIXB to POSIX file configOldPathB

set configNewPathA to configDirectory & configCustomName
set configNewPOSIXA to POSIX file configNewPathA
set configNewPathB to configDirectory & configInUseName
set configNewPOSIXB to POSIX file configNewPathB

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application "Finder"
		set the name of file configOldPOSIXA to configNotInUseName
		set the name of file configNewPOSIXA to configInUseName
	end tell
	try
		do shell script terminalScript
	on error
		tell application "Finder"
			set the name of file configNewPOSIXB to configCustomName
			set the name of file configOldPOSIXB to configInUseName
		end tell
	end try
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	else
		tell application "System Events"
			if exists file configOldPathB then
				tell application "Finder"
					set the name of file configNewPOSIXB to configCustomName
					set the name of file configOldPOSIXB to configInUseName
				end tell
			end if
		end tell
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Since Ryujinx doesn't like my SDL2 configurations, it tends to force-quit on launch, so to ensure that the JSONs will always set themselves right-ways-round, I added a try & on error catch. If there is an error, it won't schuffle again.

Some final words of tread-lightliness: I found that all IDs for controllers come up slightly differently when booted through the Terminal (the 0 at the front of becomes a 1, in my case [EDIT OCT 2023: This looks to have been fixed in the latest update!]), so if you plan on using Controller Profiles, you'll need to make configurations specifically for Terminal play. If you've already got profiles made, I highly suggest just duplicating those JSONs in your settings and editing the IDs in a text editor.

SEGA Dreamcast Games Running in redream

I'm unsure of whether I like Flycast more than redream yet. I have to say, though, that launching redream through the Terminal felt like quite the revelation, since it completely bypasses that otherwise unskippable animated logo. I might revisit this issue, but I get a really special movie-theatre dim-the-lights excited-whispers feeling from redream. Just feels too good to pass up.

_pre, Script Editor:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

_run, Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of redream.app here]"
set terminalScript to "[insert Terminal-style UNIX path of redream.app/Contents/MacOS/redream here] [insert Terminal-style UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Keep those double-backslashes in-stock for terminalScript! \ before \, except within 's! Or after \-- definitely not AND after, that... that would be an infinite number of \s.

PlayStation 1 Games Running in DuckStation

Does, uh, anyone remember when Steve Jobs announced that Mac OS 9 computers could run PS1 games perfectly with Connectix Game Station? That's how I played PlayStation games growing up. It was VERY strange to then end up in a Mac OS X world that barely ran PlayStation 1 games. Seemed like a huge step backwards, hahaha. Really, it took all the way up until 2018 for Mac OS X to be able to run PS1 games anywhere near as well as my Mac OS 9 did, hahaha. A little embarrassing, but I couldn't be more pleased. That crisp dither on the polygonal shading has finally returned to me!

_pre, Script Editor:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

_run, Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of DuckStation.app here]"
set terminalScript to "[insert Terminal-style UNIX path of DuckStation.app/Contents/MacOS/DuckStation here] -fullscreen [insert Terminal-style UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Keep those \s a'comin'! I doubt your paths are space-less. Special characters also tend to require \s. So many opportunities for a \, I know I'm not the only one in need of a \ or two. (And two.)

PlayStation 2 Games Running in AetherSX2

It's really only been in the last year that PlayStation 2 on M1 Macs has really gotten to a playable state. I've personally noticed that it tends to fare better playing PAL versions of certain games, due to them requiring at LEAST 10 less frames-per-second. I'm not entirely sure if there was a difference when I didn't double-app this one, but PS2 games have felt like, by far, the most demanding to my system. And yeah, I know I'm about to talk about PS3 games, and in-spite of the very long wait-times, I still feel like it's less of a burden on this workhorse. (I also happen to think that PS2 games, on the whole, tend to look more beautiful than PS3 games. It's hard to explain, but PS2 just has a sheen to it still, where everything looks like it came from a console that couldn't truly exist outside of one's dreams. Like, c'mon. Team ICO. O_O) So we're double-appin' it.

_pre, Script Editor:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

_run, Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of AetherSX2.app here]"
set terminalScript to "[insert Terminal-style UNIX path of AetherSX2.app/Contents/MacOS/AetherSX2 here] -fullscreen [insert Terminal-style UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

We clear about those backslashes yet? Do you have them in your terminalScript value? Are they DOUBLEDDOUBLED so they don't TOIL and become TROUBLED?

PlayStation Portable (PSP) Games Running in PPSSPPSDL

Unlike its core in OpenEmu, standalone PPSSPPSDL (kind of looks like the sound someone makes when trying to call a cat) can handle quite a bit of EBOOT homebrew. Right in its built-in Homebrew Store, you can nab a pristine copy of Cave Story. Plus, the playful face-button background is genuine fun. Software should be fun more often. ESPECIALLY when it comes to games, like, chee-hee. You'd think we were making... password security apps or something...

No need for a buffer app, back to one:

set filePath to "[insert UNIX path of game here]"
set processPath to "[insert UNIX path of PPSSPPSDL.app here]"
set terminalScript to "[insert Terminal-style UNIX path of PPSSPPSDL.app/Contents/MacOS/PPSSPPSDL here] [insert Terminal-style UNIX path of game here] --fullscreen"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Let's just be grateful you only have to repeat backslashes in terminalScript. Imagine if it was every letter. You'd be out there trying to prompt PPPPSSSSPPPPSSDDLL... it all feels like I'm replicating printing errors I got when trying to build a cipher with Nova...

PlayStation 3 Games Running in RPCS3

RPCS3 seems to be the most demanding and cumbersome of all the emulators I've handled thus far in this M1 generation. But it's what we've got, so... I'll take it. ... And eat it. Currently, I'm running the newest version of RPCS3 -- which is supposedly SLIGHTLY too high to run on my current version, but it has no issues if I run it through the command-line. To give it that advantage it desperately needs, let's _preface it.

_pre, Script Editor:

set aliasPath to "[insert UNIX path of _run app here]"
set aliasPOSIX to POSIX file aliasPath

tell application "Finder"
	open file aliasPOSIX
end tell
delay 1
if application aliasPath is running then
	repeat until application aliasPath is not running
		delay 1
	end repeat
end if

_run, Script Editor:

set filePath to "[insert UNIX path of game here]"
set processPath to "RPCS3"
set terminalScript to "[insert Terminal-style UNIX path of RPCS3.app/Contents/MacOS/rpcs3 here] [insert Terminal-style UNIX path of game here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Wherever Terminal gives you a \ in your Terminal script, add another \, why-won't-you? : )

...

... Oh! I completely forgot-- RPCS3 has scant controller support right now, so the best way I've found to get my less-compatible controllers is through Steam Input, like ro'ight back up at the top of this section! (2024 EDIT: Things are improving in this department now! You can now add a "gamecontrollerdb.txt" file with SDL2 configurations. Put it in [your RPCS3 preferences folder] > input_configs > gamecontrollerdb.txt.)

To boot RPCS3 through Steam, you'd go through the same process of adding it as a non-Steam game, then finding its gameid in 760 > screenshots.vdf.

We'd make the script a Steam Input dupe:

set filePath to "[insert UNIX path of RPCS3.app here]"
set processPath to "Steam"
set terminalScript to "open steam://rungameid/[insert gameid here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	if application processPath is running then
		repeat until application filePath is running
			delay 1
		end repeat
	end if
	if application filePath is running then
		repeat until application filePath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

... As you can see, none of that would boot a SPECIFIC PS3 game, but I can't think of a way to boot an emulator from Steam while also booting a game from the Terminal in the same instance. Unlike other emulators, this one doesn't have an "open" function, at least not written in the same way, thereby making the AppleScript command "open" null. There's going to be a lot of things I haven't figured out as it gets further down here, so really, if you can illuminate those possibilities, please do. This is only as far as I can go, and the distance I've taken has all been taken in a truly strange direction. It's all that I've ever known.

PlayStation Vita Games Running in Vita3K

I wasn't even aware that the Vita had actually released (or fully what it was [and why it was]) until around its discontinuation in late 2018. I think there were more people happily modding away at their PSPs than there were Vita users during its lifespan.

... I just decided to see what Sony's up to right now. What's a PlayStation Portal? (Is that just a PlayStation Portable without the B and E? For, uh... BEing? Always be-sing and do-sing, Sony.)

But much like how the Ouya had TowerFall, and the Stadia had GYLT, and the PS5 had Astro's Playroom, I sure am enjoying going having myself a bit of a Vita retrospective! (It's so recent an ordeal, I wanna call it a yesterspective instead...) That's the best part about always looking into consoles two+ generations later...

set filePath to "[insert UNIX path of installed eboot.bin]"
set processPath to "[insert UNIX path to Vita3K.app]"
set terminalScript to "[insert Terminal-style UNIX path of Vita3K.app/Contents/MacOS/Vita3K here] -r [insert gameid here] --fullscreen"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	do shell script terminalScript
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Adding Books

I was very eager to be able to abandon the Apple Books application in favour of Pegasus... it seemed like a no-brainer (ah, yes, a no-brainer has many no-brainers), even though I'm fairly certain I'm the only person who's ever tried to launch books in a game launcher. (If you have, in-fact, come to the same conclusion yourself, we really must become friends -- simultaneous invention has gotta be solid grounds for building true, bonkers friendship.) If you happen to be trying to liberate your library, you gotta know that Apple Books slightly modifies the format of anything it imports. So they won't open up in anything else after they've been touched by the program. Thankfully, there's an easy script a kind soul posted here. I hadn't bothered to have two copies of any of my books, so if not for this, I would've been rebuilding a good-sized chunk of my collection.

I had meant to try to read books in Calibre, but it really does feel like the Steam of book-readers. Highly powerful (It IS the best way to convert between all kinds of readable formats.), but clunky with a rather garish UI if left on standard settings. Man, speaking of clunky, Calibre has gotta be one of the largest independently developed Mac apps I've ever seen -- the current version is hoverin' (more like plummetin') around 800MB. Since I genuinely can't feel a discernible difference between Calibre's Universal 6.x build and the Intel 5.44 build (which is half the size of the other), I'm sticking with 5.44. And should I ever need to launch things directly through Calibre instead of having Pegasus open Calibre, I've made some adjustments to the Feng Shui of the space. I've completely turned off icons in the toolbar, and changed all the other UI icons to Setenove Mono Dark, which I think is the most pleasant and cohesive, overall.

Perhaps my greatest find about Calibre is that I don't even need to open Calibre at all, or bother importing books into its database. There's a much tinier, more manageable program INSIDE the Package Contents of Calibre that's meant only for the reading part of things. Calibre is actually referring to that tinier app every time you open an ePub, MOBI, FB2, etc. So I can completely skip the middleman here, and open ebook-viewer instead. I have my ebook-viewer set to a white background, paged mode, 2 pages in landscape, progress footer removed (they draw too much focus away), Iowan Old Style as the font, and both "Remember last used window size and position" + "Remember current page when quitting" check'd.

Calibre really only runs ePubs and the like natively (actually, I just found out this is wrong -- ebook-viewer's just really slow at opening PDFs, and tends not to organize more complicated contents correctly.). Otherwise, when I launch a PDF from inside Calibre, it opens up macOS' Preview app by default. Which excels with PDFs, admittedly. All you gotta do is change the display style from Continuous Scroll to Two Pages, then flip between each set of two pages with your left+right/up+down arrows. It even remembers your place in each document! However, there's a setting in the preferences of Preview that will open every file in Two Pages format from the get-go, and really, most of the stuff I open in Preview, I'd really prefer to have open in Continuous Scroll to begin with. I felt I needed a different program that would only open book PDFs in this format. I personally went with a program called Skim. It's beautifully lightweight (30MB, less than 10% the size of Calibre) and has a layout I, in many ways, prefer over Preview. And these experiences I'm making here are all Matters of the Art-- where feel is the utmost priority.

Two quick asides: It proved more difficult than expected to fully change Calibre's two application icons, post-boot; info on how to fix that is here. (Another very similar icon debacle is in store here pretty soon.) And, should Calibre's load times be a bit too much on anything you load, Murasaki's great at ePubs, as well. Really, I just wish I could use up+down arrows on navigation; for some reason, they feel... better to me than left+right.

Adding ePubs

I'm really excited to be able to tell you that you can actually open documents in Calibre's ebook-viewer JUST using the launch command in [metadata.pegasus.txt] without any extra assistance from Script Editor. Not a single hiccough, which must be my reward for sticking with this bulksome app.

The proper launch command to put in your [metadata.pegasus.txt] entry:

launch: [insert path of calibre.app/Contents/ebook-viewer.app/Contents/MacOS/ebook-viewer-placeholder-for-codesigning here] {file.path}

... with your "file" value, of course, being the .epub itself. After everything else I did, I honestly expected getting books to work in Pegasus to be more work than, you know, getting games to. But no, a game launcher runs books better on a Mac than it runs games. And that makes my eyes smile!

Adding PDFs

A simultaneous pro-&-con of most digital book formats is that they allow you to modify your font face and your font size. This means that there's no proper, regimented page numbers. And I DO feel that can be a big part of the reading journey. Authors can really use pages the same way a graphic novelist might use panel composition. I mean, taking away pages can feel a bit like taking away chapter headings. Story framing aside, it can also, more importantly, really make reading an ePub with a friend a bit of a challenge. This is why I either convert to PDF through Calibre, or just outrightly find a PDF to begin with. For books that really put a lot of emphasis on how they use a page, or that frame their text around illustrations, this tends to be a non-negotiable. Please do not read Wonderstruck in an ePub.

Here's the (so far) only Script I've had to write for da büks:

set filePath to "[insert UNIX path of da bük here]"
set filePOSIX to POSIX file filePath
set processPath to "[insert UNIX path of Skim.app here]"

tell application "System Events"
	if exists file filePath then
		set fileExists to true
	else if not (exists file filePath) then
		set fileExists to false
	end if
end tell

if fileExists is true then
	tell application processPath
		activate
		open filePOSIX
	end tell
	delay 1
	if application processPath is running then
		repeat until application processPath is not running
			delay 1
		end repeat
	end if
else if fileExists is false then
	tell me
		activate
		display dialog "The application has been moved.
Please replace it in the following directory:

" & filePath buttons {"OK"} default button "OK"
	end tell
end if

Customizing gameOS - Fire

If the fact that we're barely halfway down this page isn't clear enough, we're about to get both nitty, AND gritty. But before we do, just like the UI settings in Calibre, there's a few incredibly easy ways that I started making gameOS - Fire more to my liking, with some simple switcheroos in the settings.

Settings > General

I have "Animate highlight" to Yes and "Hide button help" to Yes. I don't think the color moving around in the highlighted outline is too strobe-y-- it's just the right amount of life and emphasis added to the scene. I guess I'd think of it like a marquee, or the way one backlights a movie poster. And the button help feels deeply unnecessary, since they're all pretty intuitive, and they never go away-way. Imagine if "Press A to jump!" stayed for your entire playthrough of a game! Plus, I'm on keyboard n' mouse. Controller help isn't applicable. Regardless, I don't think I'll need to peep at any crib sheets, should I find myself on a controller.

Settings > Home page

Fire normally sets "Number of games showcased" to 15, which I just thought was a bit of an arbitrary amount. Top 10 of anything seems more appropriate, so I just set mine to 10. (Those top-15 lists, y'know, they're all the rage.) Having "Recently Played" be the top collection makes total sense to me-- there's a huge possibility you'll want to get right back into the things you just spent Time on. Collection 2 is set to... "Most Played", which at first glance sounds like a perfect metric to gauge what I'm going to want to continue playing in the future. Except this doesn't measure how LONG you've been playing a game. It measures how many times you OPEN the game. Even if it's just been for one millisecond. Since a lot of what I do when adding a new game involves testing out if my launch method works, this is a highly inaccurate way to go about this. But this will be addressed later.

For Collection 3, I changed mine to "Recommended", which I found out is not even some kind of system where they try to predict what you want to play based on the genre or the developer of other games you've been playing -- it's just a randomizer, which I think is a real fun way of being able to, by chance, notice something in your collection in a new light. Then I disabled the fourth collection and kept the fifth one disabled, as well. I think three lists of ten games on the main page is more than enough information to sift through. One thing I really dislike about many streaming services is how you can just keep descending through far too many middlin'-sized lists that the service just seem to be generating at random. So let's give some good information and get out of here.

I've also changed all the aspect ratios to Tall, rather than the alternating between Wide and Tall. The way I'm going about it, the layouts I've personally created do NOT look just as good in Wide and Tall view. Come to think of it, Netflix's wide and tall thumbnails are VASTLY different, so the solution to use both ratios would BE to employ two different layouts. Which just seems like a misallocation of my energy, since it's not like having two different preview images will hype me up more than one good one.

Settings > Platform page

When looking through your entire collections, gameOS - Fire is set by default to a grid of wide thumbnails arranged in three columns. My Minimis set-up was four columns, but really, as the collection expanded, I felt it makes more sense to have at least five columns to navigate through. That seems like the right balance between showing off the art and not making it too arduous a journey. I also changed the Grid Thumbnail's aspect ratio to Tall, just like the others. There IS one stray collection on the Game View list that's set to Wide that cannot be modified in the settings. I'll get to that.

Settings > Game details

Here's the settings I had to flip to get the all the image classes working right together -- I set "Game Background" to "Fanart". I also set "Show scanlines" to No. I worked so hard to get crisp high-quality art, and I don't think the scanlines really do it any favours. I'm not really displaying any of the pixel art at a resolution that would allow this particular scanline overlay to divide the pixels in the way they would on a CRT. It also dulls it considerably, since it does nothing to boost the colours that are there. That might be the idea, to keep the background in the background, but I want to see what I've picked, if you don't mind.

Settings > Advanced

... and I didn't have to make any changes on this page! Those preset ratios for Wide & Tall are golden. Well, no. Not golden, by definition. But maybe they're other elemental ratios...

Modifying Pegasus, Pre-Built App (Download)

Alright, here we are at the other big icon debacle! Like Calibre, Pegasus changes where it's sourcing its icon from starting the moment you boot it-- and the icon was baked right into it, to my dismay. Thankfully, the dev let me know where in the code I could remove this forced boot icon, and then proceeded to give me a working compile of qmake to put it back together, myself. I took this opportunity to make some new assets, now that I actually could bundle them up inside the app.

Should you just want a macOS build the way I did it without having to fumble around with the Terminal yourself, here's a build of Pegasus that doesn't force its boot icon:

[Pegasus w/o permanent icon; click here to download.]

And if you'd like your copy with my custom assets built-in on top o' that, here's a build of Penelope:

[Penelope w/o permanent icon; click here to download.]

Das' what I call it. If you get the joke in any way, I am impressed.

I haven't been able to build it with SDL2 correctly, which is the one thing I'm lacking in comparison to the official builds by the dev. But the controller support just on Qt still is functional! Just a forewarning, should that be an issue for your own dream-setup. You might have to do this yourself. Or help me figure out how in the world to get SDL2 working with the rest of my M1 setup (which also appears to be the reason I couldn't compile qmake myself.)

Modifying Pegasus, Step-by-Step Instructions

The first step to modifying the assets of Pegasus, as well as rebuilding the app afterward, is cloning the Git repository.

In Terminal, enter:

git clone --recursive https://github.com/mmatyas/pegasus-frontend

After all the necessary resources finish downloading into your User directory, you're set! (And maybe you're ready, you'll have to be the judge of that.)

Icon

So here's the personal icon I doodled:

Penelopeicon.png

I have no strong feelings toward pegasuseseses, but this seemed like a similar kind of mascot I could go for. There's a lot of layers to this, but essentially, this is a character my sister played when she was six, and I consider it one of my favourite bits of fAmily mythos. I think it represents a kind of effervescent childlike joy and imagination that perfectly matches the whole reason I'm making this-- or why I make anything.

PeneloplexB.png

Here it is, docked and loaded, next to all of my other custom Skycons. (This whole project is essentially just a very complicated icon theme, really.)

As explained in the GitHub issue by the dev himself, the way to allow permanent icon changes is to remove a line of code found in pegasus-frontend > src > app > main.cpp.

app.setWindowIcon(QIcon(QStringLiteral(":/icon.png")));

Just excise that whole line, and any icon you place on Pegasus will stay post-boot. While it doesn't really matter, since you can change your icon whenever, if you'd like your icon of choice to be added during the initial build, replace a file in pegasus-frontend > src > app > platform > macos called "pegasus-fe.icns".

Here's Nova's incredible art of my warrior-princess sister Sara, riding atop Penelope:

Penelopebannerwgray.png

I replaced the normal Pegasus logo with this one-- its whereabouts are at pegasus-frontend > src > frontend > assets > logo.png. So just rename your logo to the same name, replace it, and see how it turns out! ... there's a good chance you won't be able to see it for longer than a second at a time, but if it loads fast, all the better. This is just for the impression it leaves. Like, a momentary afterimage.

Boot Progress Bar

THIS was great fun to draw & manipulate until it lined up just so-- I made a horizontally-tiling progress bar texture:

Pbar.png

Here's how those last two assets look like, werkeeng toogedda:

PeneloplexA.png

... And screenshotting this took quite a few tries, I'll have you know! That one second has me scrambling, since within that one second, there's gotta only be, like, a five-frame window where the loading bar is actually doing anything.

This texture is neighbouring with the logo at pegasus-frontend > src > frontend > assets > pbar.png. Go ahead and replace it, those candy-cane-type stripes are so played out! Don't you want to load some scales, instead? *ascending notes on a marimba are heard from some far-off place*

Building

Then, after everything's been changed as you like it, you do summa dis' action. As mentioned, I've never gotten SDL2 working, and I couldn't even compile a static build of qmake myself. (I had successfully made non-static qmake. A non-static version of an app comes pre-packaged with none of its dependent libraries. It's linked to wherever you have those pieces on your own computer. Basically useless, if you tried to run it on anyone else's computer.) So, instead, Pegasus' dev, Mátyás, supplied me with his own personal static build here. Then, I did the following Terminal prompts:

  • [insert path to qmake here] [insert path to pegasus-frontend Git folder here]
  • make
  • sudo make install

... At which point, it'll create a distributable version of your app in the hidden folder directory, usr > local. To get there, just open up your Macintosh HD directory, then select Go > Go to Folder. Type in "usr/local". There should be a new pegasus-frontend folder in there, with a new build of Pegasus, Just For You! That's the great part about going through this process -- you'll always know, wherever it may go, that it's your very own. : ) You can feel proud, as it takes wing.

Modifying gameOS - Fire, Fully Modified Theme (Download)

I'd only planned on touching on all of this in a deeply truncated newsblurb on my personal website... up until THIS part of the quest: the part where I finally started digging through the QML. I figured I'd come this far, so... I might as well see what else I could tweak to my liking. And-- while I really can't take credit for that much, what I've done still feels like a major step in my aspirations to be good at programming. Quite a few reasons to call this Personal Progress-- not the least of which is that I launched Nova... into space, おやすみなさい〜 -- No, Nova is the name of a web design application Nova uses. Using Nova, I cloned the GitHub repository for the gameOS - Fire fork (Fire Fork is sounding more and more like a legendary RPG weapon), then successfully forged a second fork from it. We might just call them twinflames, mm?! (Hi-hi, Lottiebug! ^_^)

This fork brings-- *double-checks the table of contents for the article*-- at least 35 unique features to gameOS - Fire. Nothing drastically different in terms of how things LOOK, but I truly believe that if you use both themes, you'd feel a great deal of functional improvement that make this worthwhile or, at the very least, unique among all its other theme counterparts. Like Fire before it, Fire & sKye primarily FEELS different. (Sounds like an alternate Apple slogan. Feel Different™.)

Here's the reason for all of the hubbub I'm hubbubbling over with: My own personal adventures in tinkering with gameOS - Fire, which I've dubb[ub]ed gameOS - Fire & sKye.

[gameOS - Fire & sKye; click here to download.]

Ged'dit? Because a dragon epitomizes both Fire & Sky-- and, of course, as previously noted at the very beginning of this very long bit of... dialogue?... (If it's all preceded by my name followed by a colon, it DOES seem to indicate that.) ... my name is Skye! (Hi.)

At the repository linked, you can download the gameOS - Fire theme, but now complete with every change I could make that appealed to my sensibilities. But that's just not enough for me. It was a massive minefield of trial-and-error that led me here, and I want to at least explain some of my solutions & workarounds, so that people looking to make their own themes can learn faster than me. Even if it's from my mistakes-- I'm proud of my... singular thought process. I wouldn't even bother doing anything if I didn't have it, after all, and I want others to feel they can do likewise and be supported.

First, we'll just show off a few more asset switches (which are even more simple this time, since themes are just in loose folders), but after that, I'm gonna be going line-by-line in certain parts. Some fine, fine line-by-line.

Modifying gameOS - Fire (Assets), Step-by-Step Instructions

gameOS - Fire's assets are almost identical to gameOS'-- just with some colour palette swaps. (Original Theme, do not steal!) And they're made to be pretty character-less. The gameOS logo is just the words "gameOS" in a round-y modern sans-serif. The background video that frames it is, like... some royalty-free background that one might do some very in-depth narration over. And the collection logo presets are just nice, clean copies of each of the system's logos. Which doesn't even matter for me, since I'm not planning on separating by console. Let's give everything summa'dat secwet-secwet sauce!

This is going to look identical to the copy of the Penelope logo used during the boot sequence, but the glow emanating around Penelope & Princess Sara (Sara means Princess, so I'm saying Princess Princess) actually feels pretty different on both of them. They blend differently against the background-- the boot screen needed something a little more saturated and dark. Meanwhile, against those slate-greyish-blue clouds, I felt it needed to be lighter-- give it more shimmer.

GameOS-logo.png

So don't get the two mixed up, for the most beautimous result! In your theme folder, replace assets > images > gameOS-logo.png. It might look too small, if you're doing this from scratch, but this can (and will) be changed! For now, I'm just happy that I can see the logo for more than a second at a time, because I worked far too hard on making those edges on the glow nice and smoof.

gameOS Logo Background Video

Now that we have our mighty flying steed (of a different feather), the background video doesn't quite match, thematically. When thinking of something that would loop, my first thought was the Outer Wall background from Cave Story. The good news for me is that someone made a PIXEL-perfect GIF of it here. No yucky compression artefacting in sight, just-- what-- 13 colours, tops, all brilliantly preserved as they would be in-game.

I know now that I could actually embed an animated GIF in lieu of an MP4 here, but I felt like animated GIFs and WebPs, somehow, took more of a toll on the performance of Pegasus. So I cropped the GIF and halved its framerate in Photoshop, then converted the result to MP4 through a code discussed in this StackExchange thread:

ffmpeg -i [insert path of GIF here] -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" [insert destination path & desired filename here]

The resulting filesize compression, with (to my eye) no loss in quality, was incredible. This >5MB GIF shrunkled down into a <500KB MP4. So from both a performance & filesize perspective, MP4s are top-marks all the way.

Here's the MP4, so you can see for yourself. I was anxious to use the video component in QML because, up until this month, Pegasus on macOS hadn't been properly rendering video. So I just had to employ it somewhere for users to experience. There is an issue on the "looping" half of "looping video": There tends to be this flickering black screen that persists every time the video restarts. But I "has the solution," as a terrific athlete once said. Even if you don't plan on reading any of what follows, or even doing any of it, I'd suggest reading about that part-- I think the solution I has was funny, and functional.

So my setup requires just one collection for all my games. What could possibly represent all my games, though? To me, it was a pretty easy answer. The Super Nintendo Gamepad, of course! Well, specifically the Super Famicom controller I bought, which is just nicer in every conceivable way, I think. (Not that into the convex on the other buttons. I think Smarties are delicious, but I don't think I'd use them as buttons.)

In order to HAVE a "James" collection, as I call it, just entitle your collection "James"! Right at the top of your corresponding [metadata.pegasus.txt]. Then, to give it a custom logo, you just take an image, call it "james.png", then place it in your theme's folder under assets > images > logospng.

Here's my own personal picture I took and gave a good polish:

James.png

I erased the notch where the wire normally would be, then carefully cut out slightly illuminated outlines around the main elements of the logo. I did it this way because, after being selected, the logo turns to white, and I wanted it to read nicely as a silhouette. However, I think it has extra benefits during the initial selection -- when the logo is selected in the collections grid, it gives it a very pleasingly playful attitude. Most of the controller IS grey, after all-- it feels like that golden glow is coming to validate the buttons, who must wonder if they belong on this otherwise colourless controller. Yes, buttons, you belong!! ... Unless you're in the US, boooo...

... ks! (Get it?) I paid extra-special attention to my custom logo for the Books collection, since I knew this was something pretty novel. (Don't get it. It's more impressive if you don't get that one; it was too simple! FIGHT YOUR PUN COMPREHENSION!) Gaze upon its perfect silhouette:

Borks.png

Obviously, this is far more abstracted than an actual picture of a book. So maybe it seems odd to put this next to an actual picture of a controller. But... I mean, don't you agree that most books don't photograph anyway near as beautifully as they should? Like, so as to reflect the feeling you get from reading them? Then you look at all those BYOOTIFUL books in those BYOOTIFUL labyrinths-- er, LIBRARIES in cartoons... I can't deny that books in cartoons are superior in their graphic design. Something made of flat pages looks best when represented in a flat medium. I 'unno. So I took the shape someone drew on a custom Apple Books icon, and then added a nice shadow and mapped it with textures I photographed. I also used the shadow work to create the appearance of more of a full shape to the book. Honestly, from the distance that both icons are normally displayed, I think they play nicely together. (You can get that joke.)

(The book I used as the page texture is one of my top five favourite books ever written. I photographed the copy I first read it from-- I think it's pretty special to still have those memories to hold! And the memories go back before I even had it in my care -- it was a gift from my cousin Ben, who took it out of his own collection. Thank you so-so much, Ben!

Just like the "James" collection, to make a "Borks" collection, just go ahead and call it "Borks" in one of your [metadata.pegasus.txt]s. Then, go into this theme's folder. Under assets > images > logospng, place "borks.png". If you don't, all you're gonna get is the word "Borks" in that selection square. Which is also acceptable. Maybe more fun, to you.

Modifying gameOS - Fire (Code), Step-by-Step Instructions

Ah, here we are. I'm going to be talking about how I slapped my grubby little hands on the modeling clay of this theme code and made something funny-- and how you can, too! My hopes coming into this section is, if someone is just looking for one solution to one problem, that this can be quickly searched through for any number (35 numbers) of highly specific and unusual quandaries. Documentation on the actual QML website can be pretty vague and the examples threadbare. Stuff like this is the cure -- I have a hard time with thinking of anything outside of an applied example. The realer the scenario, the more it makes sense to me. So, maybe it all comes down to nonsense, but it's sensemake nonsense, con'sarnit.

Some quick naming terminology for this theme -- I've tried to define the names initially given within the gameOS code, and here's the conclusions I've come to:

  • Showcase View is the main menu-- you choose a collection or enter the in-theme Settings menu from here.
  • Grid View is the game lists, displaying your chosen collection.
  • Game View is the individual game selection screens, where you can add games to your Favorites, view Game Details, and yes, even press a button that will allow you to play your games. (You can play games? I just thought you collect them, you know, like Pokémon cards. Wait-- you can PLAY Pokémon cards?!)

I made an effort to alphabetically organize these changes-- I couldn't really think of better ways to do so. That might sound like things would get out-of-order, but... just about every change affects each other, and not in a very linear way. Any line I follow just loops back on itself. I've used a couple loose categories of Adding, Removing, Fixing, etc... but it still requires a good deal of disentangling. Wish me luck! *dives in*

Add/Modify Click Functionality

gameOS already comes with FAR more mouse support than pretty much any other theme I've come across. However, there were a few things that didn't interact quite like I expected them to.

Add Click Functionality to Sorting Options

I assume this is to prevent confusion with your selected index, but you cannot click on any of the sorting options on the game Grid View. Well, okay, you CAN click there, but clicking there (or anywhere else on that whole header bar area) will take you right back out into the previous menu. And my mouse constantly finds its way over there -- I've heard in point-and-click games, during beta testing, they'll generate a kind of heat map that shows where people click on any given screen? Yeah, those sorting buttons would be all aglow for me.

Let's put down some new variables at the beginning-- they will be used to gauge if our sorting options are being clicked or not. Without this variable, you'd click on the button, and there'd be no visual response back.

[HeaderBar.qml] - Before

FocusScope {
id: root

    property bool searchActive

[HeaderBar.qml] - After

FocusScope {
id: root

    property bool searchActive
    property bool onDownDirectionButton: false;
    property bool onDownTimeButton: false;
    property bool onDownFilterButton: false;

Using our new click-variables, we'll apply them to each of the orange rounded rectangles that light up the sorting options when you're navigating with your arrows. The "visible" parameter is in charge of that-- so let's add an "or" to the three rectangles, followed by the respective click-variable...

[HeaderBar.qml] - Before

            // Ascending/descending
            Item {
            id: directionbutton

                [...]

                Rectangle
                { 

                    [...]

                    visible: directionbutton.selected
                }

[HeaderBar.qml] - After

            // Ascending/descending
            Item {
            id: directionbutton

                [...]

                Rectangle
                { 

                    [...]

                    visible: directionbutton.selected || onDownDirectionButton == true
                }

[HeaderBar.qml] - Before

            // Order by title
            Item {
            id: titlebutton

                [...]

                Rectangle
                { 

                    [...]

                    visible: titlebutton.selected
                }

[HeaderBar.qml] - After

            // Order by title
            Item {
            id: titlebutton

                [...]

                Rectangle
                { 

                    [...]

                    visible: titlebutton.selected || onDownTimeButton == true
                }

[HeaderBar.qml] - Before

            // Filters menu
            Item {
            id: filterbutton

                [...]

                Rectangle
                { 

                    [...]

                    visible: filterbutton.selected
                }

[HeaderBar.qml] - After

            // Filters menu
            Item {
            id: filterbutton

                [...]

                Rectangle
                { 

                    [...]

                    visible: filterbutton.selected || onDownFilterButton == true
                }

Then, we'll add MouseAreas to each of these sorting options, and ask it to flip on our click-variable whenever we have our mouse down on it, and to flip it off once we let go. And on a full click, we'll have it toggle the option.

[HeaderBar.qml] - Before

            // Ascending/descending
            Item {
            id: directionbutton

                [...]

                Text {
                id: directiontitle
                    
                    [...]

                }

[HeaderBar.qml] - After

            // Ascending/descending
            Item {
            id: directionbutton

                [...]

                Text {
                id: directiontitle
                    
                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: {onDownDirectionButton = true;}
                    onExited: {onDownDirectionButton = false;}
                    onClicked: {
                        toggleOrderBy();
                    }
                }

[HeaderBar.qml] - Before

            // Order by title
            Item {
            id: titlebutton

                [...]

                Text {
                id: ordertitle
                    
                    [...]

                }

[HeaderBar.qml] - After

            // Order by title
            Item {
            id: titlebutton

                [...]

                Text {
                id: ordertitle
                    
                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: {onDownTimeButton = true;}
                    onExited: {onDownTimeButton = false;}
                    onClicked: {
                        cycleSort();
                    }
                }

[HeaderBar.qml] - Before

            // Filters menu
            Item {
            id: filterbutton

                [...]
                
                // Filter title
                Text {
                id: filtertitle
                    
                    [...]

                }

[HeaderBar.qml] - After

            // Filters menu
            Item {
            id: filterbutton

                [...]
                
                // Filter title
                Text {
                id: filtertitle
                    
                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: {onDownFilterButton = true;}
                    onExited: {onDownFilterButton = false;}
                    onClicked: {
                        toggleFavs();
                    }
                }

Fix Click-and-Drag Detection/Modify Click Functionality on Showcase & Grid View

The way that clicking on gameOS is set up, if you click on something that's supposed to take you to a new screen, you go there immediately. As mentioned previously, this makes it so I wouldn't be able to see things like the selection animation, which features my beloved logo overlay zoom. Plus, it feels just a bit too quick of a switch in this case. This double-click helps me get around with a lot more intention, making my cursory clicks function more like arrow keys, rather than the Return key.

First, we'll remove all the MouseEnter/MouseExit actions, since those would be the ones getting me in trouble while clicking. Instead, I'm just gonna let both the highlighting & activating be handled by the click event.

[DynamicGridItem.qml] - Before

    // Mouse/touch functionality
    MouseArea {

        [...]

        onEntered: { sfxNav.play(); highlighted(); }
        onExited: { unhighlighted(); }
        onClicked: {
            sfxNav.play();
            activated();
        }
    }
}

[DynamicGridItem.qml] - After

    // Mouse/touch functionality
    MouseArea {

        [...]

	onEntered: {}
	onExited: {}
        onClicked: { if (selected) { sfxNav.play(); activated(); } else { sfxNav.play(); highlighted(); } }
    }
}

After fixing that, that instantly made it a good deal easier to hold down on a section of the game grid and pull it around to navigate. However, I noticed there was a little area of the screen space near the top of the grid and near the bottom that I couldn't grab. It turned out to be some margin malarkey. I just zeroed out some of the top & bottom values, and it gave me back mouse interaction in those dead zones.

[GridViewMenu.qml] - Before

    Item {
    id: gridContainer

        anchors {
            top: header.bottom; topMargin: globalMargin
            left: parent.left; leftMargin: globalMargin
            right: parent.right; rightMargin: globalMargin
            bottom: parent.bottom; bottomMargin: globalMargin
        }

[GridViewMenu.qml] - After

    Item {
    id: gridContainer

        anchors {
            top: header.bottom; topMargin: 0
            left: parent.left; leftMargin: globalMargin
            right: parent.right; rightMargin: globalMargin
            bottom: parent.bottom; bottomMargin: 0
        }

[GridViewMenu.qml] - Before

        GridView {
        id: gamegrid

            [...]

            anchors {
                top: parent.top; left: parent.left; right: parent.right;
                bottom: parent.bottom; bottomMargin: helpMargin + vpx(40)
            }

[GridViewMenu.qml] - After

        GridView {
        id: gamegrid

            [...]

            anchors {
                top: parent.top; left: parent.left; right: parent.right;
                bottom: parent.bottom; bottomMargin: 0
            }

Under the grid, I added an invisible rectangle with a MouseArea that helps catch blank areas in the grid that would otherwise not be interactable (whh-- how is that NOT a word?).

[GridViewMenu.qml] - Before

            Component {
            id: highlightcomponent

            [...]

            Keys.onUpPressed: {

                [...]

            }

[GridViewMenu.qml] - After

            Component {
            id: highlightcomponent

            [...]

            Keys.onUpPressed: {

                [...]

            }
            
            Rectangle {
                width: parent.width;
                height: parent.height;
                opacity: 0;
                z:-1;
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: gamegrid.focus = true;
                    onExited: gamegrid.focus = true;
                    onClicked: gamegrid.focus = true;
                }
            }

Then, to ensure that the blank space in the header bar around the buttons doesn't send me to the previous menu screen on accident, I added another MouseArea to fill the cracks, set to do nothing when it's clicked. I've also noticed I tend to find myself clicking around that blank area when I want to escape the search bar, so I also use it as an opportunity to defocus anything that might still be in there.

[HeaderBar.qml] - Before

        // Buttons
        ListView {
        id: buttonbar

            [...]
            
        }

[HeaderBar.qml] - After

        // Buttons
        ListView {
        id: buttonbar

            [...]
            
        }

        // Mouse/touch functionality
        MouseArea {
                    anchors.fill: parent
                    onEntered: {}
                    onExited: {}
                    onClicked: {gamegrid.focus = true;}
                    z: -100
        }

Whenever that failed, I'd try to click on one of the games in that ol' game grid, but that also did nothing at the time. So to remedy that, I added another gamegrid.focus = true; where it'd be able to have the desired result without confusing the grid elements on the Showcase Menu. (If you try to get it to focus in, say, [DynamicGridItem.qml], then try to click any of 'em on the Showcase Menu, they'll keep objecting, "WHAT'S A GAMEGRID?!")

[GridViewMenu.qml] - Before

            Component {
            id: dynamicDelegate

                    [...]

                    onHighlighted: {
                        gamegrid.currentIndex = index;
                    }

[GridViewMenu.qml] - After

            Component {
            id: dynamicDelegate

                    [...]

                    onHighlighted: {
                        gamegrid.currentIndex = index;
                        gamegrid.focus = true;
                    }

Speaking of Showcase Menu, clicking a collection on that mainéd menu is set to immediately switch screens by default, just like selecting games. So I just removed all those default actions besides the clicking sound effect in "onEntered". Then, to fix the "onClicked", I did something a little odd -- maybe if I based it more on the original "onEntered" stuff, it'd be cleaner, but the values I chose made sense to me.

[ShowcaseViewMenu.qml] - Before

                Text {
                id: platformname

                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {

                    [...]

                    onEntered: { sfxNav.play(); mainList.currentIndex = platformlist.ObjectModel.index; platformlist.savedIndex = index; platformlist.currentIndex = index; }
                    onExited: {}
                    onClicked: {
                        if (selected)
                        {
                            currentCollectionIndex = index;
                            softwareScreen();
                        } else {
                            mainList.currentIndex = platformlist.ObjectModel.index;
                            platformlist.currentIndex = index;
                        }
                        
                    }

[ShowcaseViewMenu.qml] - After

                Text {
                id: platformname

                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {

                    [...]

                    onEntered: {sfxNav.play();}
                    onExited: {}
                    onClicked: {
			if (mainList.currentIndex != 1 || platformlist.currentIndex != index) {
			                mainList.focus = true;
                            mainList.currentIndex = -1;
                            mainList.currentIndex = 1;
                            mainList.currentIndex = platformlist.ObjectModel.index;  
		            platformlist.currentIndex = index;
                        } else {
                            currentGame = null;
                            currentCollectionIndex = index;
                            softwareScreen();
                        }             
                    }

So when it clicks one of the collection boxes, it checks if it either hasn't navigated into this area of the menu yet, or hasn't selected the specific box I just picked. If it hasn't, it focuses on the collection list, then -- well, this is part of a solution to a later problem, but I do this funny (and by funny, I mean terrible-coding-practices) index-overshoot where it tries to go out of the index of selectable things, then comes back to where I want it to go. It helps cancel any unusual Y-coordinate transitions that might occur. Then it selects the proper secondary index within this list.

But if I'm clicking the collection box for the second time (i.e., you HAVE navigated to this area of the menu already, and you HAVE selected this box already), it clears whatever you might've selected in the menu you're going to (just a safety measure), remembers your current spot on the last menu, then switches the screen.

And then, while I'm at it, to get rid of multiple click sound effects firing at the same time, I removed each instance of "sfxNav.play();" from the five potential game collection lists on the menu:

[ShowcaseViewMenu.qml] - Before

        HorizontalCollection {
        id: list1

            [...]

            onListHighlighted: { sfxNav.play(); mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - After

        HorizontalCollection {
        id: list1

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - Before

        HorizontalCollection {
        id: list2

            [...]

            onListHighlighted: { sfxNav.play(); mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - After

        HorizontalCollection {
        id: list2

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - Before

        HorizontalCollection {
        id: list3

            [...]

            onListHighlighted: { sfxNav.play(); mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - After

        HorizontalCollection {
        id: list3

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - Before

        HorizontalCollection {
        id: list4

            [...]

            onListHighlighted: { sfxNav.play(); mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - After

        HorizontalCollection {
        id: list4

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - Before

        HorizontalCollection {
        id: list5

            [...]

            onListHighlighted: { sfxNav.play(); mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - After

        HorizontalCollection {
        id: list5

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

Which is part of a type of click behaviour, so it's not nearly as tangential as it may first appear.

Modify Click Functionality of Settings Button

The settings button also suffers from the ramifications of the hasty effects of a single click. It feels like a big enough choice, right alongside collections & games, that I'd like to settle upon its highlighted form before truly making a decision. Just completely delete "onEntered" & "onExited", and check if your mouse is currently focused on the button to decide if it's a full click or a half click, as it were.

[ShowcaseViewMenu.qml] - Before

    Item {
    id: ftueContainer

        [...]

            // Mouse/touch functionality
            MouseArea {

                [...]

                onEntered: settingsbutton.focus = true;
                onExited: settingsbutton.focus = false;
                onClicked: settingsScreen();
            }
        }

[ShowcaseViewMenu.qml] - After

    Item {
    id: ftueContainer

        [...]

            // Mouse/touch functionality
            MouseArea {

                [...]

                onClicked: { if (settingsbutton.focus == true) { settingsScreen(); } else { settingsbutton.focus = true; } }
            }
        }

Add Line Divider under Grid View Header Bar

This was actually a complete accident near the beginning of my QML exhuming of those who have come before me. I just placed down a white rectangle to see where it was going to go, and realized it looked amazing-- both as a counter-balance to the background, AND as a place for the grid overflow to properly hide behind... when it's trailin' its way up under the header.

[GridViewMenu.qml] - Before

    Item {
    id: gridContainer

        anchors {
            [...]
        }

[GridViewMenu.qml] - After

    Item {
    id: gridContainer

        anchors {
            [...]
        }
        
        Rectangle {
            width: parent.width
            height: vpx(1)
            z: 100
        }

Add "More Games by Developer" on Game View & Change from Wide to Tall Ratio

Personally, I think that developers are far more important to a game's quality than publishers.

... Developers developers developers developers.

So I thought it was a little funny that the Game View shows off a "More by Publisher", but not a "More by Developer". In-fact, gameOS doesn't even have it as an option to display Developer information anywhere! But it's basically as easy as duplicating & renaming all the information used for displaying Publisher information.

[GameView.qml] - Before

    ListPublisher { id: publisherCollection; publisher: game && game.publisher ? game.publisher : ""; max: 10 }

[GameView.qml] - After

    ListDeveloper { id: developerCollection; developer: game && game.developer ? game.developer : ""; max: 10 }
    ListPublisher { id: publisherCollection; publisher: game && game.publisher ? game.publisher : ""; max: 10 }

(And come to think of it, you can take out the ListGenre brackets right underneath that', but I didn't bother. Maybe someone'll want to put it back! Three is a magic number.)

Since I want to prioritize my developers (as anyone should), I'm going to be replacing the top "Publisher" collection with developers, then replacing the bottom "Genre" collection with publishers. I don't imagine I'll be REALLY using the genre list. Like, with movies, I rarely am ever trying to look up another "Action" film after watching a good Action film. Like, those kinds of lists tend to be a little useless for me. It's not the genre that impressed me as much as the people who made this one-- I look up everybody who MADE the thang ding. Similar situation. It's a platformer? Sure, that's nice, so is a million other games, but is it made by ASKIISOFT?!

[GameView.qml] - Before

        // More by publisher
        HorizontalCollection {
        id: list1

            [...]

            itemWidth: (root.width - globalMargin * 2) / 4.0
            itemHeight: itemWidth * settings.WideRatio

            title: game ? "More games by " + game.publisher : ""
            search: publisherCollection

            [...]

        }

        // More in genre
        HorizontalCollection {
        id: list2

            [...]

            title: game ? "More " + game.genreList[0].toLowerCase() + " games" : ""
            search: genreCollection

            [...]

        }

[GameView.qml] - After

        // More by developer
        HorizontalCollection {
        id: list1

            [...]

            itemWidth: (root.width - globalMargin * 2) / 8.0
            itemHeight: itemWidth / settings.TallRatio

            title: game ? "By " + game.developer : ""
            search: developerCollection

            [...]

        }

        // More by publisher
        HorizontalCollection {
        id: list2

            [...]

            title: game ? "By " + game.publisher : ""
            search: publisherCollection

            [...]

        }

I also used the opportunity to just switch the ratio of the first collection, which happens to be the only forced-wide collection in the theme. ... Which I suppose makes the SECOND the only forced-tall collection. Huh.

Next thing to do is to create a [ListDeveloper.qml] by duplicating [ListPublisher.qml] (found in the "Lists" folder) and modifying the criteria. The majority of it just boils down to replacing every instance of the word "publisher" with "developer"; it's that simple.

[ListPublisher.qml]

    function currentGame(index) { return api.allGames.get(publisherGames.mapToSource(index)) }
    property int max: publisherGames.count

[ListDeveloper.qml]

    function currentGame(index) { return api.allGames.get(developerGames.mapToSource(index)) }
    property int max: developerGames.count

[ListPublisher.qml]

    SortFilterProxyModel {
    id: publisherGames

        sourceModel: api.allGames
        filters: RegExpFilter { roleName: "publisher"; pattern: publisher; caseSensitivity: Qt.CaseInsensitive; }

[ListDeveloper.qml]

    SortFilterProxyModel {
    id: developerGames

        sourceModel: api.allGames
        filters: RegExpFilter { roleName: "developer"; pattern: developer; caseSensitivity: Qt.CaseInsensitive; }

[ListPublisher.qml]

    SortFilterProxyModel {
    id: gamesFiltered

        sourceModel: publisherGames

[ListDeveloper.qml]

    SortFilterProxyModel {
    id: gamesFiltered

        sourceModel: developerGames

[ListPublisher.qml]

    property var collection: {
        return {
            name:       "Top Games by " + publisher,
            shortName:  "publisher",
            games:      gamesFiltered
        }
    }

[ListDeveloper.qml]

    property var collection: {
        return {
            name:       "Top Games by " + developer,
            shortName:  "developer",
            games:      gamesFiltered
        }
    }

There's one fun outlier, though it's just a placeholder. I had fun choosing an appropriate comparison.

[ListPublisher.qml]

    property string publisher: "Nintendo"

[ListDeveloper.qml]

    property string developer: "HAL Laboratory"

Add playTime to Game Details

I think this is my favourite thing I implemented into the theme: playTime!! (I can't stop hearing that in Bone Saw's voice.) Actually, in the theme's external JavaScript library, there was already a way to roughly calculate the playTime into minutes/hours built-in. So, when first trying things out, I just called on that function. However, the calculation seemed a little clumsy to read-- it would return anything longer than an hour with a decimal place (instead of 3-and-a-half hours, you get 3.5 hours) -- what I wanted was movie runtime readout. So first, I'll demonstrate my revamped playTime format function, but below it, I'll go into how to use the original function. Someone took the Time to make it, after all; it deserves to be appreciated.

New Method

I don't really see any reason to have a "players" counter on the Game View screen. There's no good way to sort by player number currently, anyway, and I'm not about to select every game just to find the right number of players. So I just decided to replace that space with "playTime".

[GameInfo.qml] - Before

        // Players box
        Text {
        id: playerstitle

            [...]

            text: "Players: "

            [...]

        }

[GameInfo.qml] - After

        // Players box
        Text {
        id: playerstitle

            [...]

            text: "Time Spent: "

            [...]

        }

And here we go, here's my new function, which gives both hours and minutes with a fancy comma separating them! (v1.5.0 Me butting in to say I'd planned for hours without minutes... but not the much rarer minutes without hours, which left behind my previously-stated fancy comma. Just caught that comma in the code-- put in a lil' COMMA CODE, one [(1) me] might say. Did some renovation while I was at it.) It even knows whether to say "hour" or "hours". Isn't it really funny that zero denotes pluralizing the word it's appended to? Like... the only time we DON'T pluralize is when we say "one". But zero, which is less than one, is many.

[GameInfo.qml] - Before

        Text {
        id: playerstext

            [...]

            text: gameData ? gameData.players : ""

            [...]

        }

[GameInfo.qml] - After

        Text {
        id: playerstext

            [...]

            text: {
            function formatplayTime(timeSecs) {
                var hours = Math.floor(timeSecs / (60 * 60));
                var minutes = Math.floor((timeSecs % (60 * 60)) / 60);
                var hoursWord = "";
                var minutesWord = "";
                var timecompositeWord = "";

                switch (minutes) {
                    case 1:
                        minutesWord = " minute";
                        break;
                    default:
                        minutesWord = " minutes";
                        break;
                }

                switch (true) {
                    case (hours == 0):
                        timecompositeWord = minutes + minutesWord;
                        break;
                    case (hours == 1 && minutes == 0):
                        hoursWord = " hour";
                        timecompositeWord = hours + hoursWord;
                        break;
                    case (hours >= 1 && minutes == 0):
                        hoursWord = " hours";
                        timecompositeWord = hours + hoursWord;
                        break;
                    case (hours == 1 && minutes != 0):
                        hoursWord = " hour, ";
                        timecompositeWord = hours + hoursWord + minutes + minutesWord;
                        break;
                    case (hours >= 1 && minutes != 0):
                        hoursWord = " hours, ";
                        timecompositeWord = hours + hoursWord + minutes + minutesWord;
                        break;
                    default:
                        break;
                }
                
                return timecompositeWord;
            }
            return "" + formatplayTime(game ? game.playTime : 0);
            }

            [...]

        }

Old Method

For the old methods, we start by ensuring that the GameInfo blurb sources from "utils.js", which is where the original playTime format function was placed. (With a cute dev-comment saying it should probably be placed into the actual API at a later Time.)

[GameInfo.qml] - Before

import QtQuick 2.0
import QtQuick.Layouts 1.11
import "qrc:/qmlutils" as PegasusUtils

[GameInfo.qml] - After

import QtQuick 2.0
import QtQuick.Layouts 1.11
import "qrc:/qmlutils" as PegasusUtils
import "../utils.js" as Utils

Then change the accompanying text again...

[GameInfo.qml] - Before

        // Players box
        Text {
        id: playerstitle

            [...]

            text: "Players: "

            [...]

        }

[GameInfo.qml] - After

        // Players box
        Text {
        id: playerstitle

            [...]

            text: "Time Spent: "

            [...]

        }

... and source Utils.formatPlayTime! Go ahead. You've earned it.

[GameInfo.qml] - Before

        Text {
        id: playerstext

            [...]

            text: gameData ? gameData.players : ""

            [...]

        }

[GameInfo.qml] - After

        Text {
        id: playerstext

            [...]

            text: gameData ? Utils.formatPlayTime(gameData.playTime) : ""

            [...]

        }

Change Button Text

Chances are this one isn't going to matter to most people, but since I'm using Pegasus for books, too, I wanted to come up with more neutral words for the buttons that would match both case-uses. At the very least, like, come on, capitalize your entire button like a proper title-- that always bugs me.

Change Text of "All Games" Button

I changed "All games" to "Show All". I think it's a little more clear when worded that way, anyway. Are the favorites not games? Toggling makes a lot more functional sense now.

[HeaderBar.qml] - Before

                    text: (showFavs) ? "Favorites" : "All games"

[HeaderBar.qml] - After

                    text: (showFavs) ? "Favorites" : "Show All"

Change Text of "Play Game" Button

"Play game" becomes "Launch". If this was just for games, I probably would've just made it "Play"-- playin an' simple. A Play button is one of the most beautiful buttons I can imagine! But Launch has a kind of official, powerful feeling to it that I like, as well.

[GameView.qml] - Before

        Button { 
        id: button1 

            text: "Play game"

[GameView.qml] - After

        Button { 
        id: button1 

            text: "Launch"

Change Collection Text

Here's some more of me being picky about the phrasing. I've already mentioned that the "Recommended" collection is really a "Random" tab. It's not like there's anything the system did to quantify if any particular game should be recommended to you-- it just picked them out of a hat[xt]. And I don't enjoy the phrasing on these new streaming platforms that this theme has adopted of "Continue Playing". That sounds like a command rather than a suggestion, which-- let's be real, it is a command from the almighty mandate of retention above all else. So I changed that to "Recently Launched". Then it's just a friendly reminder! AaAaAaAaA.

Change Collections Names in Settings (Mandatory when Renaming Collections)

As the heading here says, should you change any of the names of the collections, you have GOT to change them in the Settings file, too. If you don't, the settings won't respond to the application, or vice-versa. Here's my own changes, following in-line with my desired name changes:

[SettingsScreen.qml] - Before

        ListElement {
            settingName: "Collection 1"
            setting: "Recently Played,Most Played,Recommended,Top by Publisher,Top by Genre,None,Favorites"
        }

        [...]

        ListElement {
            settingName: "Collection 2"
            setting: "Most Played,Recommended,Top by Publisher,Top by Genre,None,Favorites,Recently Played"
        }

        [...]

        ListElement {
            settingName: "Collection 3"
            setting: "Top by Publisher,Top by Genre,None,Favorites,Recently Played,Most Played,Recommended"
        }

        [...]

        ListElement {
            settingName: "Collection 4"
            setting: "Top by Genre,None,Favorites,Recently Played,Most Played,Recommended,Top by Publisher"
        }

        [...]

        ListElement {
            settingName: "Collection 5"
            setting: "None,Favorites,Recently Played,Most Played,Recommended,Top by Publisher,Top by Genre"
        }

[SettingsScreen.qml] - After

        ListElement {
            settingName: "Collection 1"
            setting: "Recently Launched,Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre,None,Favorites"
        }

        [...]

        ListElement {
            settingName: "Collection 2"
            setting: "Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre,None,Favorites,Recently Launched"
        }

        [...]

        ListElement {
            settingName: "Collection 3"
            setting: "Top by Publisher,Top by Genre,None,Favorites,Recently Launched,Most Time Spent,Randomly Picked"
        }

        [...]

        ListElement {
            settingName: "Collection 4"
            setting: "Top by Genre,None,Favorites,Recently Launched,Most Time Spent,Randomly Picked,Top by Publisher"
        }

        [...]

        ListElement {
            settingName: "Collection 5"
            setting: "None,Favorites,Recently Launched,Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre"
        }

Change Text of "All Games" Collection (Mandatory when Changing Text of "All Games" Button)

Ah-- yes-- if you don't change the name of the collection here, I think the button will just... well, I actually think it WILL show all, which might give you the wrong idea. It's only doing so because it doesn't know how else to filter it, with your supplied information.

[ListAllGames.qml] - Before

    property var collection: {
        return {
            name:       "All games",
            shortName:  "allgames",
            games:      gamesFiltered
        }
    }

[ListAllGames.qml] - After

    property var collection: {
        return {
            name:       "Show All",
            shortName:  "allgames",
            games:      gamesFiltered
        }
    }

Change Text of "Continue Playing" / "Recently Played" Collection

Actually, it's interesting that it's only ever referred to as "Continue Playing" on the menu, since it's called the "Recently Played" collection IN your settings... so I'm just making it consistent. Change it in three different files (besides Settings, in the first section): the List class, the Menu class, and the overall Theme class.

[ListLastPlayed.qml] - Before

    property var collection: {
        return {
            name:       "Continue Playing",
            shortName:  "lastplayed",
            games:      gamesFiltered
        }
    }

[ListLastPlayed.qml] - After

    property var collection: {
        return {
            name:       "Recently Launched",
            shortName:  "lastplayed",
            games:      gamesFiltered
        }
    }

[ShowcaseViewMenu.qml] - Before

            case "Recently Played":
                collection.search = listLastPlayed;

[ShowcaseViewMenu.qml] - After

            case "Recently Launched":
                collection.search = listLastPlayed;

[theme.qml] - Before

            ShowcaseCollection1:           api.memory.has("Collection 1") ? api.memory.get("Collection 1") : "Recently Played",

[theme.qml] - After

            ShowcaseCollection1:           api.memory.has("Collection 1") ? api.memory.get("Collection 1") : "Recently Launched",

Change Text of "Most Played" Collection

Switching out "play", because most people would say you can't play a book. (I'd contest that. I play my books, and I'm not just talking about audiobooks. Or that stageplays have books! Reading books is a deeply active experience. One time, I straight-up headbutted A Wrinkle in Time [also given to me by Ben] due to how worried I was about Charles Wallace. It ripped in half. If that's not playing a book, I dunno what is. [Congratulations, I played myself.])

[ListMostPlayed.qml] - Before

    property var collection: {
        return {
            name:       "Most Played Games",
            shortName:  "mostplayed",
            games:      gamesFiltered
        }
    }

[ListMostPlayed.qml] - After

    property var collection: {
        return {
            name:       "Most Time Spent",
            shortName:  "mostplayed",
            games:      gamesFiltered
        }
    }

[ShowcaseViewMenu.qml] - Before

            case "Most Played":
                collection.search = listMostPlayed;

[ShowcaseViewMenu.qml] - After

            case "Most Time Spent":
                collection.search = listMostPlayed;

[theme.qml] - Before

            ShowcaseCollection2:           api.memory.has("Collection 2") ? api.memory.get("Collection 2") : "Most Played",

[theme.qml] - After

            ShowcaseCollection2:           api.memory.has("Collection 2") ? api.memory.get("Collection 2") : "Most Time Spent",

Change Text of "Recommended Games" Collection

Recommended to Random, which exposes rather than disguises the beauty of the system.

[ListRecommended.qml] - Before

    property var collection: {
        return {
            name:       "Recommended Games",
            shortName:  "recommended",
            games:      gamesFiltered
        }
    }

[ListRecommended.qml] - After

    property var collection: {
        return {
            name:       "Randomly Picked",
            shortName:  "recommended",
            games:      gamesFiltered
        }
    }

[ShowcaseViewMenu.qml] - Before

            case "Recommended":
                collection.search = listRecommended;

[ShowcaseViewMenu.qml] - After

            case "Randomly Picked":
                collection.search = listRecommended;

Change Text of "Top Games" Collection

I, er, I don't know why this is called the "Top Games" collection. (Top Games: Mavericks.) I don't even know if it's even being utilized anywhere on the theme. It says it's shuffling the games... doesn't that just make it another randomizer? Just in-case it shows up, I made the terminology consistent.

[ListTopGames.qml] - Before

    property var collection: {
        return {
            name:       "All games",
            shortName:  "allgames",
            games:      shuffle(gamesWithLimit)
        }
    }

[ListTopGames.qml] - After

    property var collection: {
        return {
            name:       "Show All",
            shortName:  "allgames",
            games:      shuffle(gamesWithLimit)
        }
    }

Change Game Title, Developer & Publisher between Sorting and Display

There's a good chance you don't want your sorting of different values to look at "the", "a" or "an" at the beginning of titles. The three values I'm using in [metadata.pegasus.txt] that can potentially include those articles are the game title, the game developer, and the game publisher. Pegasus already supports a separate field for writing a game title with the leading articles shifted to the end (sortBy), but Developer & Publisher don't have their own space. Pegasus does support custom fields, though. (More good-guy-dev stuff. Following some very tech-oriented version of the golden rule.) So, specifying it's a custom field with the prefix "x-", I place a separate field for displaying the text correctly, and use the normal "developer" and "publisher" field for the sorting order version. Really, there's no place in my own theme where one can sort by these values, but knowing I'm ready for that makes this feel more of a sturdy way to put my metadata together for all possible scenarios.

So, if I the game title has a "the", "a" or "an" I want it to ignore, I put in a proper sortable version of the title in the "sortBy" field. If the title doesn't have that in it, I just don't put the value in at all. If I did and left it blank, it's going to throw up errors in the log. I do similar for Developer and Publisher. If it needs to have a separate sortable version of the name, add "x-developernosort" or "x-publishernosort"-- otherwise, don't.

Here's an example of part of an entry in my metadata that uses all of the separate classes:

[metadata.pegasus.txt]

game: The Last Door: Season 1
sortBy: Last Door, The: Season 1
file: ./pegasus_assets/thelastdoorseason1_run.app
developer: Game Kitchen, The
x-developernosort: The Game Kitchen
publisher: Game Kitchen, The
x-publishernosort: The Game Kitchen

[...]

In the Theme class, change sorting to "sortBy". (By doing so, this will automatically fall back on title, should sortBy not exist.)

[theme.qml] - Before

    property var sortByFilter: ["title" [...] ]

[theme.qml] - After

    property var sortByFilter: ["sortBy" [...] ]

Then, going along with me putting Developer & Publisher collections on Game View, let's make sure what's displayed there is non-sorted information. Just like sortBy+title automatically do, have it fall back on the primary value, should the other not exist.

[GameView.qml] - Before

        // More by developer
        HorizontalCollection {
        id: list1

            [...]

            title: game ? "By " + game.developer : ""

            [...]

        }

[GameView.qml] - After

        // More by developer
        HorizontalCollection {
        id: list1

            [...]

            title: if (game) {
                if (game.extra.developernosort != null) {
                    "By " + game.extra.developernosort;
                } else {
                    "By " + game.developer;
                }
            } else {
                ""
            }

            [...]

        }

[GameView.qml] - Before

        // More in genre
        HorizontalCollection {
        id: list2

            title: game ? "By " + game.publisher : ""

        }

[GameView.qml] - After

        // More by publisher
        HorizontalCollection {
        id: list2

            [...]

            title: if (game) {
                if (game.extra.publishernosort != null) {
                    "By " + game.extra.publishernosort;
                } else {
                    "By " + game.publisher;
                }
            } else {
                ""
            }

            [...]

        }
        
    }

Change Game Details Graphic to Different Asset

The way that gameOS handles assets, it only allows me to use the boxFront asset field in [metadata.pegasus.txt] if I'm not planning on putting a logo overlay over it. But when toggling Game Details, it's actually set to display the boxFront asset, should it exist. After cycling through a couple other assets that I don't personally have filled in, it settles on display the logo again. Which I don't really want it to do-- when you open Game Details, it displays the title written out, so to have both the logo & the title just seems redundant. Since I've spent a great deal of Time editing my boxart choices to be separate from its logo, I figured it'd be nice to just have the boxart here, sans title, to be admired as key-art. For me, I have my boxart under "assets.screenshots". I know, kind of a messy solution... but I need that logo overlay zoom. And I don't want the boxart to be the same as the background; that's no fun. Three images: one good logo, one good box, and one good background. That's all I need. So I don't feel bad about sacrificing a list of screenshots; I didn't want it to begin with. So I just take out that long list of possible replacements for boxArt and ask it to display the first screenshot listed in each metadata entry-- which, as I've mentioned, is where I put my boxart.

[utils.js] - Before

function boxArt(data) {
  if (data != null) {
    if (data.assets.boxFront.includes("/header.jpg")) 
      return steamBoxArt(data);
    else {
      if (data.assets.boxFront != "")
        return data.assets.boxFront;
      else if (data.assets.poster != "")
        return data.assets.poster;
      else if (data.assets.banner != "")
        return data.assets.banner;
      else if (data.assets.tile != "")
        return data.assets.tile;
      else if (data.assets.cartridge != "")
        return data.assets.cartridge;
      else if (data.assets.logo != "")
        return data.assets.logo;
    }
  }
  return "";
}

[utils.js] - After

function boxArt(data) {
  if (data != null) {
    if (data.assets.boxFront.includes("/header.jpg")) 
      return steamBoxArt(data);
    else {
      if (data.assets.screenshots[0] != "")
        return data.assets.screenshots[0];
    }
  }
  return "";
}

You could, of course, feel free to change this to any asset you'd wish, then change its position and scale on-screen! The text information that displays is all in Global > GameInfo.qml, and all the image asset stuff is managed in GameDetails > GameView.qml.

Change Game Details Title Wrap

A lot of my favourite things have titanic titles that make me teehee. And I'd like to see the whole title, if you please. Game Details is normally set to have anything that can't fit in one line cut off by ellipses. Unacceptable. Instead, just add a wrapMode, to give it multi-line support!

[GameInfo.qml] - Before

    // Game title
    Text {
    id: gametitle
        
        [...]

        elide: Text.ElideRight
    }

[GameInfo.qml] - After

    // Game title
    Text {
    id: gametitle
        
        [...]

        elide: Text.ElideRight
        wrapMode: Text.WordWrap
    }

Change Image Asset Size / Placement

All visual stuffs is really only as good as its organization. I'm reminded of this every time I open up my current metadata in a theme it wasn't prepared for. In-spite of my efforts to pick only the most quality of assets, those very same high-quality rips sure can look majorly ugly in the wrong configuration. Let's make sure we see things only in a context where they look their best!

Change Game Logo Placement between Grid View & Game View

This change really pleased me. See, I didn't just want to place my logo in the center of each "boxart". I wanted to place each exactly where it needed to go. So my trick to get around the centering was to simply add a whole bunch of blank space to the height of my logo asset-- sometimes making the height-width equivalent to the 2:3 ratio that the full boxarts are. Then, within all of that blank space, I just place it where it looks best. Problem solved, but with a catch. When any non-centered logos show up on the Game View screen, they will, of course, show off all that extra empty space, wherever there is any. And every time, I've placed it a little differently, so I can't just add some new X/Y position or scale to make them all look right. The easiest solution on-top of the previous solution was to have two classes of logo.

I initially thought of using that custom "x-" thing (which you might've thought yourself about the screenshots-as-boxArt dealio-- "Why didn't he use 'x-'?!") ... but it seems to only work with text values, not filepaths. So I had to steal another assets field-- one that I was certainly never going to use-- to act as logo #2. I chose "assets.titlescreen"! Couldn't ever imagine myself wanting to see the title screen that bad. I'm GOING to be seeing it the very moment I open the game, aren't I? A little too on-the-nose.

As an example of these two logo classes in use, here's my metadata entry for Jean Ferris' romance-on-the-high-seas novel, Into the Wind:

[metadata.pegasus.txt]

game: Into the Wind
file: ./borks_assets/intothewind_run.app
developer: Jean Ferris
publisher: Jean Ferris
genre: Fiction
release: 1996-06-01
players: 1
rating: 100%
launch: {file.path}
assets.screenshot: ./borks_assets/intothewind_box.jpg
assets.logo: ./borks_assets/intothewind_logo.png
assets.titlescreen: ./borks_assets/intothewind_logocentered.png
assets.background: ./borks_assets/intothewind_background.jpg

"intothewind_logo.png" is placed in the bottom-right hand corner of the boxart, whereas "intothewind_logocentered.png" is just as it sez': it's centered, with minimal border-space around it.

Let's take a quick look at the function called when displaying a logo:

[utils.js] - Before

function logo(data) {
  if (data != null) {
    if (data.assets.boxFront.includes("/header.jpg")) 
      return steamLogo(data);
    else {
      if (data.assets.logo != "")
        return data.assets.logo;
    }
  }
  return "";
}

Then, right underneath that function, let's make a duplicate function that calls "titlescreen"-- or if "titlescreen" doesn't exist, it falls back on "logo".

[utils.js] - After

function logo(data) {
  if (data != null) {
    if (data.assets.boxFront.includes("/header.jpg")) 
      return steamLogo(data);
    else {
      if (data.assets.logo != "")
        return data.assets.logo;
    }
  }
  return "";
}

function logocentered(data) {
  if (data != null) {
    if (data.assets.boxFront.includes("/header.jpg")) 
      return steamLogo(data);
    else {
      if (data.assets.titlescreen != "")
        return data.assets.titlescreen;
      else if (data.assets.logo != "")
        return data.assets.logo;
    }
  }
  return "";
}

Then, in [GameView.qml], we'll substitute the get-logo function for this new get-logocentered function, so we can see our function in action! (What's your function? [It's Utils.logocentered(game).])

[GameView.qml] - Before

    // Clear logo
    Image {
    id: logo

        [...]

        width: vpx(500)
        height: vpx(450) + header.height
        source: game ? Utils.logo(game) : ""

        [...]

    }

[GameView.qml] - After

    // Clear logo
    Image {
    id: logo

        [...]

        width: vpx(400)
        height: vpx(480) + header.height
        source: game ? Utils.logocentered(game) : ""

        [...]

    }

Change Size of Collection Logo on Showcase View, Game View, & Grid View

Since a lot of the collection logos are super-short and skinny, my new collection logos look a little small in those selection boxes. But hey, I worked hard on those! Don't just want them to be the size of a solitary piece'a glitter.

On GameView & HeaderBar, I increased the size of its white silhouette by deleting some of the bounding-box constraints.

[GameView.qml] - Before

        Image {
        id: platformlogo

            anchors {
                top: parent.top; topMargin: vpx(20)
                bottom: parent.bottom; bottomMargin: vpx(20)
                left: parent.left; leftMargin: globalMargin
            }

[GameView.qml] - After

        Image {
        id: platformlogo

            anchors {
                top: parent.top; topMargin: vpx(10)
                left: parent.left; leftMargin: vpx(20)
            }
            height: vpx(60)

[HeaderBar.qml] - Before

        Image {
        id: platformlogo

            anchors {
                top: parent.top; topMargin: vpx(20)
                bottom: parent.bottom; bottomMargin: vpx(20)
                left: parent.left; leftMargin: globalMargin
            }

[HeaderBar.qml] - After

        Image {
        id: platformlogo
            anchors {
                top: parent.top; topMargin: vpx(10)
                left: parent.left; leftMargin: vpx(20)
            }
            height: vpx(60)

Then, on the main Showcase View menu, I completely removed the left and right margin, and decreased the squeezing between the top and bottom margin... However, I noticed that if I didn't keep those as-is for all the original logos, they'd pop straight off the box. So I just added a case-switcher that will only make the logo beeg if they're the ones I made.

[ShowcaseViewMenu.qml] - Before

                Image {
                id: collectionlogo

                    anchors.fill: parent
                    anchors.centerIn: parent
                    /*anchors.margins: vpx(15)*/
                    anchors.topMargin: vpx(35)
                    anchors.bottomMargin: vpx(35)
                    anchors.leftMargin: vpx(22)
                    anchors.rightMargin: vpx(22)

[ShowcaseViewMenu.qml] - After

                Image {
                id: collectionlogo

                    anchors.fill: parent
                    anchors.centerIn: parent
                    /*anchors.margins: vpx(15)*/
                    anchors.topMargin: { switch (Utils.processPlatformName(modelData.shortName)) {
                case "borks":
                case "james":  
                    return vpx(15)
                default:
                    return vpx(35)
            } }
                    anchors.bottomMargin: { switch (Utils.processPlatformName(modelData.shortName)) {
                case "borks":
                case "james": 
                    return vpx(15)
                default:
                    return vpx(35)
            } }

Change Size of gameOS Logo on Showcase View

Similarly, now that I added a WHOLE DRAGON to the menu logo, it was squeezing the information into far too small a space. So I just doubled the width. Ahh, the dragon can breathe now. (And spit fire. [IT'SFUNNYHOWWORDSLIKECONSCIOUSNESSANDPOSITIVEMUSICCANSOMEHOWSTARTTOFEELHOLLOWIT'SBECOMETOOSYNONYMOUSWITHPOLISHINSOFTCOLLAGENLIPSONTHEFACEOFRACEPOLITICS.])

[ShowcaseViewMenu.qml] - Before

        Image {
        id: ftueLogo

            width: vpx(350)

[ShowcaseViewMenu.qml] - After

        Image {
        id: ftueLogo

            width: vpx(700)

Change "Most Played" Collection to Measure by playTime

It makes no sense for the calculation of my "Most Played" game to end up being some unruly entry that I had to open over and over again, for only seconds at a time, just to see if things were working correctly. Thankfully, this part's another quick fix of replacing one predefined variable for another! Substitute "playCount" for "playTime" in Lists > ListMostPlayed.qml.

[ListMostPlayed.qml] - Before

        sourceModel: api.allGames
        sorters: RoleSorter { roleName: "playCount"; sortOrder: Qt.DescendingOrder }

[ListMostPlayed.qml] - After

        sourceModel: api.allGames
        sorters: RoleSorter { roleName: "playTime"; sortOrder: Qt.DescendingOrder }

Change Scale Animation Speed on Game Highlight

Unlike in Minimis, gameOS doesn't let their logo zoom really have its chance to shine. I think it thinks a slow transition makes the OS feel less responsive. No-no, it's just less theatrical, gameOS! Give it some pomp! It's as simple as increasing two numbers, one for the background, and one for the logo. I found that it looks best, to my eyes, to have the logo end its zooming before the background does. I have the logo set to 300 (milliseconds, I believe) and the background art set to 400. I don't know how easy it would be to integrate some actual ease-in or ease-out, but I feel like these two elements working in-tandem creates a similar feeling.

[DynamicGrid.qml] - Before

    property bool playVideo: gameData ? gameData.assets.videoList.length && (settings.AllowThumbVideo == "Yes") : ""
    scale: selected ? 1 : 0.95
    Behavior on scale { NumberAnimation { duration: 100 } }

[DynamicGrid.qml] - After

    property bool playVideo: gameData ? gameData.assets.videoList.length && (settings.AllowThumbVideo == "Yes") : ""
    scale: selected ? 1 : 0.95
    Behavior on scale { NumberAnimation { duration: 400 } }

[DynamicGrid.qml] - Before

        Image {
        id: favelogo

            [...]

            Behavior on scale { NumberAnimation { duration: 100 } }
            z: 10
        }

[DynamicGrid.qml] - After

        Image {
        id: favelogo

            [...]

            Behavior on scale { NumberAnimation { duration: 300 } }
            z: 10
        }

Change Sorting Text & Variables on Grid View

Now that the collection on the Showcase Menu knows what to do, let's make sure Grid View knows to do the same. In the main theme class, the four metrics it sorts by are "title", "lastPlayed", "playCount" (which, again, only counts number of launches), and "rating". We've already changed "title" to "sortBy" as our primary, so do the same here. And I just removed "rating" altogether. Useless, when the collection won't have any games I don't want to play in them. And last, switch out "playCount" for "playTime" again.

[theme.qml] - Before

    property var sortByFilter: ["title", "lastPlayed", "playCount", "rating"]

[theme.qml] - After

    property var sortByFilter: ["sortBy", "lastPlayed", "playTime"] 

The "Sort by" button was made as a convenient one line of code, where all it did was spit out the metrics, just as written. Meaning nothing had proper phrasing or capitalization. So I expanded the text value to manage each toggled variable differently. Most of the values of the before-time would make a decent amount sense in the provided context, but having a button say "By sortBy"... aʻole readable.

[HeaderBar.qml] - Before

                    text: "By " + sortByFilter[sortByIndex]

[HeaderBar.qml] - After

                    text: if (sortByFilter[sortByIndex] == "sortBy") {
                            "Sort by Title";
                        } else if (sortByFilter[sortByIndex] == "lastPlayed") {
                            "Sort by Last Opened";
                        } else if (sortByFilter[sortByIndex] == "playTime") {
                            "Sort by Time Spent";
                        }

Change Values of Default Settings

I already went over all those changes I made from the Settings menu, before deciding to go code-diving. But now that I'm making the theme exactly the way I want it, what's stopping me from just making the settings I like the settings that would be applied from first launch? So I just took all my preferred settings from earlier (as well as some of these more recent changes of collection names, etc.) and plopped them down in theme.qml & Settings > SettingsScreen.qml. Together, these two decide what happens when you open a clean install of this theme and when you try to modify the settings later.

[theme.qml] - Before

FocusScope {
id: root

    [...]

    // Load settings
    property var settings: {
        return {

            [...]

            GridThumbnail:                 api.memory.has("Grid Thumbnail") ? api.memory.get("Grid Thumbnail") : "Dynamic Wide",
            GridColumns:                   api.memory.has("Number of columns") ? api.memory.get("Number of columns") : "3",
            GameBackground:                api.memory.has("Game Background") ? api.memory.get("Game Background") : "Screenshot",

            [...]

            HideButtonHelp:                api.memory.has("Hide button help") ? api.memory.get("Hide button help") : "No",

            [...]

            AnimateHighlight:              api.memory.has("Animate highlight") ? api.memory.get("Animate highlight") : "No",

            [...]

            ShowScanlines:                 api.memory.has("Show scanlines") ? api.memory.get("Show scanlines") : "Yes",

            [...]

            ShowcaseColumns:               api.memory.has("Number of games showcased") ? api.memory.get("Number of games showcased") : "15",

            [...]

            ShowcaseCollection1:           api.memory.has("Collection 1") ? api.memory.get("Collection 1") : "Recently Played",
            ShowcaseCollection1_Thumbnail: api.memory.has("Collection 1 - Thumbnail") ? api.memory.get("Collection 1 - Thumbnail") : "Wide",
            ShowcaseCollection2:           api.memory.has("Collection 2") ? api.memory.get("Collection 2") : "Most Played",

            [...]

            ShowcaseCollection3:           api.memory.has("Collection 3") ? api.memory.get("Collection 3") : "Top by Publisher",
            ShowcaseCollection3_Thumbnail: api.memory.has("Collection 3 - Thumbnail") ? api.memory.get("Collection 3 - Thumbnail") : "Wide",
            ShowcaseCollection4:           api.memory.has("Collection 4") ? api.memory.get("Collection 4") : "Top by Genre",

            [...]

            ShowcaseCollection5_Thumbnail: api.memory.has("Collection 5 - Thumbnail") ? api.memory.get("Collection 5 - Thumbnail") : "Wide",

[theme.qml] - After

FocusScope {
id: root

    [...]

    // Load settings
    property var settings: {
        return {

            [...]

            GridThumbnail:                 api.memory.has("Grid Thumbnail") ? api.memory.get("Grid Thumbnail") : "Tall",
            GridColumns:                   api.memory.has("Number of columns") ? api.memory.get("Number of columns") : "5",
            GameBackground:                api.memory.has("Game Background") ? api.memory.get("Game Background") : "Fanart",

            [...]

            HideButtonHelp:                api.memory.has("Hide button help") ? api.memory.get("Hide button help") : "Yes",

            [...]

            AnimateHighlight:              api.memory.has("Animate highlight") ? api.memory.get("Animate highlight") : "Yes",

            [...]

            ShowScanlines:                 api.memory.has("Show scanlines") ? api.memory.get("Show scanlines") : "No",

            [...]

            ShowcaseColumns:               api.memory.has("Number of games showcased") ? api.memory.get("Number of games showcased") : "10",

            [...]

            ShowcaseCollection1:           api.memory.has("Collection 1") ? api.memory.get("Collection 1") : "Recently Launched",
            ShowcaseCollection1_Thumbnail: api.memory.has("Collection 1 - Thumbnail") ? api.memory.get("Collection 1 - Thumbnail") : "Tall",
            ShowcaseCollection2:           api.memory.has("Collection 2") ? api.memory.get("Collection 2") : "Most Time Spent",

            [...]

            ShowcaseCollection3:           api.memory.has("Collection 3") ? api.memory.get("Collection 3") : "Randomly Picked",
            ShowcaseCollection3_Thumbnail: api.memory.has("Collection 3 - Thumbnail") ? api.memory.get("Collection 3 - Thumbnail") : "Tall",
            ShowcaseCollection4:           api.memory.has("Collection 4") ? api.memory.get("Collection 4") : "None",

            [...]

            ShowcaseCollection5_Thumbnail: api.memory.has("Collection 5 - Thumbnail") ? api.memory.get("Collection 5 - Thumbnail") : "Tall",

[SettingsScreen.qml] - Before

FocusScope {
id: root

    ListModel {
    id: settingsModel

        [...]

        ListElement {
            settingName: "Animate highlight"
            setting: "No,Yes"
        }

        [...]

        ListElement {
            settingName: "Hide button help"
            setting: "No,Yes"
        }
    }

    [...]

    ListModel {
    id: showcaseSettingsModel
        ListElement {
            settingName: "Number of games showcased"
            setting: "15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,1,2,3,4,5,6,7,8,9,10,11,12,13,14"
        }
        ListElement {
            settingName: "Collection 1"
            setting: "Recently Played,Most Played,Recommended,Top by Publisher,Top by Genre,None,Favorites"
        }
        ListElement {
            settingName: "Collection 1 - Thumbnail"
            setting: "Wide,Tall,Square"
        }
        ListElement {
            settingName: "Collection 2"
            setting: "Most Played,Recommended,Top by Publisher,Top by Genre,None,Favorites,Recently Played"
        }

        [...]

        ListElement {
            settingName: "Collection 3"
            setting: "Top by Publisher,Top by Genre,None,Favorites,Recently Played,Most Played,Recommended"
        }
        ListElement {
            settingName: "Collection 3 - Thumbnail"
            setting: "Wide,Tall,Square"
        }
        ListElement {
            settingName: "Collection 4"
            setting: "Top by Genre,None,Favorites,Recently Played,Most Played,Recommended,Top by Publisher"
        }

        [...]

        ListElement {
            settingName: "Collection 5"
            setting: "None,Favorites,Recently Played,Most Played,Recommended,Top by Publisher,Top by Genre"
        }
        ListElement {
            settingName: "Collection 5 - Thumbnail"
            setting: "Wide,Tall,Square"
        }

    }

    [...]

    ListModel {
    id: gridSettingsModel

        ListElement {
            settingName: "Grid Thumbnail"
            setting: "Wide,Tall,Square,Box Art"
        }
        ListElement {
            settingName: "Number of columns"
            setting: "3,4,5,6,7,8"
        }
    }

    [...]

    ListModel {
    id: gameSettingsModel

        ListElement {
            settingName: "Game Background"
            setting: "Screenshot,Fanart"
        }

        [...]

        ListElement {
            settingName: "Show scanlines"
            setting: "Yes,No"
        }
    }

[SettingsScreen.qml] - After

FocusScope {
id: root

    ListModel {
    id: settingsModel

        [...]

        ListElement {
            settingName: "Animate highlight"
            setting: "Yes,No"
        }

        [...]

        ListElement {
            settingName: "Hide button help"
            setting: "Yes,No"
        }
    }

    [...]

    ListModel {
    id: showcaseSettingsModel
        ListElement {
            settingName: "Number of games showcased"
            setting: "10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,1,2,3,4,5,6,7,8,9"
        }
        ListElement {
            settingName: "Collection 1"
            setting: "Recently Launched,Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre,None,Favorites"
        }
        ListElement {
            settingName: "Collection 1 - Thumbnail"
            setting: "Tall,Square,Wide"
        }
        ListElement {
            settingName: "Collection 2"
            setting: "Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre,None,Favorites,Recently Launched"
        }

        [...]

        ListElement {
            settingName: "Collection 3"
            setting: "Randomly Picked,Top by Publisher,Top by Genre,None,Favorites,Recently Launched,Most Time Spent"
        }
        ListElement {
            settingName: "Collection 3 - Thumbnail"
            setting: "Tall,Square,Wide"
        }
        ListElement {
            settingName: "Collection 4"
            setting: "None,Favorites,Recently Launched,Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre"
        }

        [...]

        ListElement {
            settingName: "Collection 5"
            setting: "None,Favorites,Recently Launched,Most Time Spent,Randomly Picked,Top by Publisher,Top by Genre"
        }
        ListElement {
            settingName: "Collection 5 - Thumbnail"
            setting: "Tall,Square,Wide"
        }

    }

    [...]

    ListModel {
    id: gridSettingsModel

        ListElement {
            settingName: "Grid Thumbnail"
            setting: "Tall,Square,Box Art,Wide"
        }
        ListElement {
            settingName: "Number of columns"
            setting: "5,6,7,8,3,4"
        }
    }

    [...]

    ListModel {
    id: gameSettingsModel

        ListElement {
            settingName: "Game Background"
            setting: "Fanart,Screenshot"
        }

        [...]

        ListElement {
            settingName: "Show scanlines"
            setting: "No,Yes"
        }
    }

Fix Adding External Game Collections

I'm not personally planning on using any of the additional supported game sources, like Steam. (I already mentioned its unreliable behaviour, at least on Mac.) But I still want it to be possible! The newest version of gameOS fixed what was wrong in the version that gameOS Fire diverged from, so I just copied over this extra source for the model to take from. Otherwise, all the collections disappear when you try to add another data source-- I assume because the external JavaScript function doesn't read external collections correctly.

[ShowcaseViewMenu.qml] - Before

            model: Utils.reorderCollection(api.collections);

[ShowcaseViewMenu.qml] - After

            model: api.collections//Utils.reorderCollection(api.collections);

Fix Background Blinking White When Opening Game View

In gameOS Fire, this isn't an issue, but this popped up amidst some new choices I made for how to load assets. Those methods make opening up a new screen WAY more responsive, but it causes the screen to light up for a second while the assets populate the screen-- and while it's loading, each asset is set by default to just be WHITE until it loads. Which really causes a nasty flicker, since this whole theme is dark-mode-oriented. All I needed to do was remove the value "asynchronous" from the two background elements, thereby saving my eyes and yours.

[GameView.qml] - Before

    // Background
    Image {
    id: screenshot

        anchors.fill: parent
        asynchronous: true

[GameView.qml] - After

    // Background
    Image {
    id: screenshot

        anchors.fill: parent

[GameView.qml] - Before

    // Dark Hero
    Image {
    id: darkhero

        anchors.fill: parent
        source: "../assets/images/background.jpg"
        asynchronous: true
        opacity: 0.5 /*darken hero*/
    }

[GameView.qml] - After

    // Dark Hero
    Image {
    id: darkhero

        anchors.fill: parent
        source: "../assets/images/background.jpg"
        opacity: 0.5 /*darken hero*/
    }

Fix Blinking Looping Video & Turn Off Header Video

Thinking back to that custom background video I put behind the Penelope logo, let's fix that one-frame flicker! While tending to that, I'm also going to make it so that I can turn off the video. I think there's plenty of people who would rather there be no videos at all on their theme, and there's no way to turn it off in the Settings along with the other video settings. So I'm gonna lump it in with the "Video Preview" switch-- I think anyone who doesn't want to see the full-screen video playback in Game View are likely to not want it on their main page, either.

I personally use IINA as my primary video player, so I can take perfect screenshots wherever I want to in a video with minimal stress. Jus' take a screenshot of the first frame of the background video, name it "ftueBG01.png", and place it in assets > images.

Then, within the container element for this logo header, I completely reorganize things, so there's an image element calling "ftueBG01.png". It's placed right behind the video component, so for that one frame that would normally flicker, you'll see "ftueBG01.png" instead, thereby maintaining that Persistence of Vision.

In-front of "ftueBG01.png" is two rectangles. One of them is a black rectangle that fills the same area the video inhabits. It will only appear if the video background is turned off. The other rectangle also fills the video area and is the colour of the main page's background. When the screen is focused on the logo header, the rectangle is entirely invisible, and as one navigates away from it, it slowly becomes more opaque, until the logo header fades out. I found this to be a nicer-looking solution than adding an opacity slider to two separate overlayed elements.

[ShowcaseViewMenu.qml] - Before

    Item {
    id: ftueContainer

        width: parent.width
        height: vpx(360)
        visible: ftue
        opacity: {
            switch (mainList.currentIndex) {
                [...]
            }
        }
        Behavior on opacity { PropertyAnimation { duration: 1000; easing.type: Easing.OutQuart; easing.amplitude: 2.0; easing.period: 1.5 } }

        Component.onCompleted: { [...] }

        /*Image {
            [...]
        }*/

        Rectangle {
            [...]
        }

[ShowcaseViewMenu.qml] - After

    Item {
    id: ftueContainer

        width: parent.width
        height: vpx(360)
        visible: ftue
        
        Component.onCompleted: { [...] }

        Image {
            anchors.fill: parent
            source: if (settings.VideoPreview === "Yes") { "../assets/images/ftueBG01.png" } else { "" }
            sourceSize { width: root.width; height: root.height}
            fillMode: Image.PreserveAspectCrop
            smooth: true
            asynchronous: true
        }
        
        Rectangle {
            anchors.fill: parent
            color: "black"
            opacity: if (settings.VideoPreview === "Yes") { 0 } else { 0.5 }
        }

        Rectangle {
            anchors.fill: parent
            color: "#12151a"
            z: 1;
            
        opacity: {
            switch (mainList.currentIndex) {
                case 0:
                    return 0;
                case 1:
                    return 0.7;
                case 2:
                    return 0.9;
                case -1:
                    break;
                default:
                    return 1
            }
        }
        Behavior on opacity { PropertyAnimation { duration: 1000; easing.type: Easing.OutQuart; easing.amplitude: 2.0; easing.period: 1.5 } }
        }

Over the embedded video itself, I put an if-else that blanks out the source when Video Preview is disabled.

[ShowcaseViewMenu.qml] - Before

        Video {
        id: videocomponent

            anchors.fill: parent
            source: "../assets/video/ftue.mp4"

[ShowcaseViewMenu.qml] - After

        Video {
        id: videocomponent

            anchors.fill: parent
            source: if (settings.VideoPreview === "Yes") { "../assets/video/ftue.mp4" } else { "" }

In similar fashion to the looping video in this header, you can also embed looping video previews that will play either on each game's individual Game View screen, or when highlighting a game on Grid View. As mentioned, the "Video Preview" setting controls video playback on Game View, and "Allow Video Thumbnails" controls playback on Grid View. And they're subject to the same flicker at the start of each loop. So I go into IINA and take a first-frame screenshot for these preview videos, and... unfortunately, I'm gonna need to steal another asset field for this. I took assets.boxFull, since I can't use the boxArt spaces for boxArt without sacrificing the logo overlay, anyway. Plus, fixing the video thumbnail preview does, in effect, make my boxart full, or complete. Here's an example of a metadata entry that employs the use of the assets.video & assets.boxFull values:

[metadata.pegasus.txt]

game: INSIDE
file: ./james_assets/inside_run.app
developer: Playdead
publisher: Playdead
genre: Puzzle-Platformer
release: 2020-06-23
players: 1
rating: 100%
launch: {file.path}
assets.screenshots: ./james_assets/inside_box.jpg
assets.logo: ./james_assets/inside_logo.png
assets.background: ./james_assets/inside_background.jpg
assets.video: ./james_assets/inside_video.mov
assets.boxFull: ./james_assets/inside_video.png

I've found both MOVs and MP4s work for video-- anything H.264, essentially. Then, just like on the main page, I add an image element behind the video wrapper, in both GameView.qml (obviously, for the Game View "Preview video") & ItemHighlight.qml (for "Video thumbnails"). They both source "assets.boxFull" and are only visible if the video preview loader notices there's a video cued up. (My brain just started shivering trying to figure out if it's "queued up" or "cued up", and my subsequent research is inconclusive.) The code for both is identical and placed in-between identical bits of code-- just in two separate QML classes.

[GameView.qml] - Before

    Timer {
    id: stopvideo

        [...]

    }

    // NOTE: Video Preview
    Component {
    id: videoPreviewWrapper

[GameView.qml] - After

    Timer {
    id: stopvideo

        [...]

    }

        Image {
            anchors.fill: parent
            source: { if (game.assets.boxFull) { game.assets.boxFull; } else { "" } }
            sourceSize { width: root.width; height: root.height}
            fillMode: Image.PreserveAspectCrop
            smooth: true
            asynchronous: true
            visible: videoPreviewLoader.sourceComponent != undefined
        }

    // NOTE: Video Preview
    Component {
    id: videoPreviewWrapper

[ItemHighlight.qml] - Before

    Timer {
    id: stopvideo

        [...]

    }

    // NOTE: Video Preview
    Component {
    id: videoPreviewWrapper

[ItemHighlight.qml] - After

    Timer {
    id: stopvideo

        [...]

    }
    
    Image {
            anchors.fill: parent
            source: game.assets.boxFull ? game.assets.boxFull : ""
            sourceSize { width: root.width; height: root.height}
            fillMode: Image.PreserveAspectCrop
            smooth: true
            asynchronous: true
            visible: videoPreviewLoader.sourceComponent != undefined
        }

    // NOTE: Video Preview
    Component {
    id: videoPreviewWrapper

Fix Favorites Header on Showcase View

Now, I personally haven't bothered using the ability to add Favorites on gameOS (I've only added things that I believe will become favourites to my list)... however, I found out that doing so removes the logo on Showcase View and, in that logo header's place is a similarly sized sliver of your chosen game's background, complete with logo zoom overlay. It displays up to ten favorites at a time, in order of when you've last played them.

I quite liked the idea! Then, if all your games are favourites (as mine well should be), you can just use the Favorites button as a way to show off some particularly visually appealing game art. However, I thought the rectangular banner shape of the header didn't do any favours to the full screen-sized art I was picking. And why would I pick banner-sized art, anyway? So whenever this header's been enabled (I have it detect that by when the launcher's logo disappears), the header height expands to fill roughly 80% of the screen. This leaves just enough space for the first row of buttons, so you can still easily navigate to a full Collection.

[ShowcaseViewMenu.qml] - Before

        ListView {
        id: featuredlist

            [...]

            height: vpx(360)

            [...]

            preferredHighlightBegin: vpx(0)
            preferredHighlightEnd: parent.width
            highlightRangeMode: ListView.StrictlyEnforceRange
            //highlightMoveDuration: 200
            highlightMoveVelocity: -1
            snapMode: ListView.SnapOneItem
            keyNavigationWraps: true
            currentIndex: (storedHomePrimaryIndex == 0) ? storedHomeSecondaryIndex : 0

[ShowcaseViewMenu.qml] - After

        ListView {
        id: featuredlist

            [...]

            height: if (!ftue) { parent.width * 0.50 } else { vpx(360);

            [...]

            highlightRangeMode: ListView.StrictlyEnforceRange
            highlightMoveDuration: 200
            snapMode: ListView.SnapOneItem
            keyNavigationWraps: true

Another thing happening here is I'm fixing the transition animation on this list, so the background image scrolls as you navigate. These changes also make it possible to drag the list around and still have it select the next game correctly.

I decided, however, that all my custom logos tended to look messy placed dead-center over the background. I hadn't created them to look good that way. Very few of them would. So, instead of sourcing the logo image, I decided that I'd just have it display the game title in the theme font, instead. Cleaner, I thought. Besides, it's a nice place to utilize my text metadata, rather than my asset metadata.

So delete this entire bit about starting the logo zoom when you select the header:

[ShowcaseViewMenu.qml]

                    onSelectedChanged: {
                        if (selected)
                            logoAnim.start()
                    }

As well as the image component the logo would otherwise belong to:

[ShowcaseViewMenu.qml]

                    Image {
                    id: specialLogo

                        [...]

                    }


Right where that component would go, I placed a text box instead:

[ShowcaseViewMenu.qml]

    // Game title
    Text {
    id: gametitle
        
        text: modelData ? modelData.title : ""
        
        anchors {
            top:    parent.top;
            left:   parent.left;
            leftMargin: vpx(200);
            right:  parent.right;
            rightMargin: vpx(200);
            bottom: parent.bottom;
        }
        
        color: theme.text
        font.family: titleFont.name
        font.pixelSize: vpx(60)
        font.bold: true
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
        elide: Text.ElideRight
        wrapMode: Text.WordWrap
        opacity: if (mainList.currentIndex == 0) { 1; } else { 0.2; }
        scale: if (mainList.currentIndex == 0) { 1.1; } else { 1; }
        Behavior on opacity { NumberAnimation { duration: 200 } }
        Behavior on scale { NumberAnimation { duration: 200 } }
    }

Finally, to ensure that the screen moves back up to fully display the Favorites header, should you be further down the page, I make the selected index overshoot and return again, if your selection wasn't on the header's index. This also makes it so you can CLICK on the header without immediately going into the game it's displaying. I think that takes care of all the Times I'd accidentally end up on a new page when all I was trying to do was navigate the page I was on.

[ShowcaseViewMenu.qml] - Before

    // Using an object model to build the list
    ObjectModel {
    id: mainModel

        [...]

                    // Mouse/touch functionality
                    MouseArea {
                        anchors.fill: parent
                        hoverEnabled: settings.MouseHover == "Yes"
                        onEntered: { sfxNav.play(); mainList.currentIndex = 0; }
                        onClicked: {
                            if (selected)
                                gameDetails(modelData);  
                            else
                                mainList.currentIndex = 0;
                        }
                    }

[ShowcaseViewMenu.qml] - After

    // Using an object model to build the list
    ObjectModel {
    id: mainModel

        [...]

                    // Mouse/touch functionality
                    MouseArea {
                        anchors.fill: parent
                        hoverEnabled: settings.MouseHover == "Yes"
                        onEntered: { sfxNav.play(); }
                        onClicked: {
                                if (mainList.currentIndex != 0) {
                                    mainList.currentIndex = -1;
                                    mainList.currentIndex = 0;
                                } else {
                                    gameDetails(modelData);
                                }
                        }

Fix Game Select Indexing on Showcase View

Due to the system trying to transition gracefully between areas of the screen, there's a lot of problems with proper game select indexing on the main Showcase View menu-- primarily if you click around with the mouse, which wasn't what was envisioned, I'm sure. (Is this me sequence-breaking?) So just about everything I'm doing here involves me trying to keep any of the on-screen elements from freaking out too bad. Most of this is just me giving it less stringent boundaries to work with, and using that overshoot method, of taking the index to -1, then bringing it to its destination. And in the last section, I fixed a WHOLE bunch of indexing issues involving the Favorites Header, which feels more like its own thing. So it's not here. Don't worry, though, it's nearby, and you can visit it whenever you want to. : )

I found that some of the more detailed "highlight" parameters were causing things to behave strangely after being clicked, so I've left them out whenever possible. highlightRangeMode, in particular, would make a collection completely clip out-of-bounds sometimes.

[HorizontalCollection.qml] - Before

        preferredHighlightBegin: vpx(0)
        preferredHighlightEnd: parent.width - vpx(60)
        highlightRangeMode: ListView.ApplyRange
        snapMode: ListView.SnapOneItem 
        highlightMoveDuration: 100
        highlight: highlightcomponent

[HorizontalCollection.qml] - After

        snapMode: ListView.SnapOneItem 
        highlightMoveDuration: 100
        highlight: highlightcomponent

Depending on what steps you've already taken, you might already've added "break" to the Logo Header opacity case-switcher. "Break" just means that the value won't change from whatever it was, previous to reaching this point. What we're saying is that, should you go out-of-order when clicking through the index, clicking on the Settings button will not drastically change the opacity of the logo header. Because now that its click behaviour's been modified, this could become a problem. You could be at the very bottom of the page-- no logo header-- then you click on that Settings button, and WHOOPS, there's the header again in full-effect, behind all the other menu elements.

[ShowcaseViewMenu.qml] - Before

    Item {
    id: ftueContainer

        [...]

                case -1:
                    [...];

[ShowcaseViewMenu.qml] - After

    Item {
    id: ftueContainer

        [...]

                case -1:
                    break;

The "onEntered" and "onExited" events on the Settings Button have this meddlesome focus toggle. If it defocuses the moment I let go of my mouse, I can't very well select it, can I? So I just completely deleted both of those focuses. Best of both worlds. In its place, I cued the select sound effect, since I'm going to be removing it somewhere else shortly after this block o' code.

[ShowcaseViewMenu.qml] - Before

        Rectangle {
        id: settingsbutton

            [...]

            // Mouse/touch functionality
            MouseArea {

                [...]

                onEntered: settingsbutton.focus = true;
                onExited: settingsbutton.focus = false;
                onClicked: [...]
            }

[ShowcaseViewMenu.qml] - After

        Rectangle {
        id: settingsbutton

            [...]

            // Mouse/touch functionality
            MouseArea {

                [...]

                onEntered: { sfxNav.play(); }
                onClicked: [...]
            }

Yes, since there will be no focus disorienting, let's just remove it from "onFocusChanged".

[ShowcaseViewMenu.qml] - Before

        Rectangle {
        id: settingsbutton

            [...]

            onFocusChanged: {
                sfxNav.play()

[ShowcaseViewMenu.qml] - After

        Rectangle {
        id: settingsbutton

            [...]

            onFocusChanged: {

On launch, the right index (the logo header or favorites header) must be on-screen & focused. To ensure this, I put the -1 overshoot in a "Component.onCompleted" event.

[ShowcaseViewMenu.qml] - Before

    Item {
    id: ftueContainer

        width: parent.width
        height: vpx(360)
        visible: ftue

[ShowcaseViewMenu.qml] - After

    Item {
    id: ftueContainer

        width: parent.width
        height: vpx(360)
        visible: ftue
        
        Component.onCompleted: { mainList.currentIndex = -1; mainList.currentIndex = 0; }

More trimming of confusing highlight parameters from the Collection list...

[ShowcaseViewMenu.qml] - Before

        ListView {
        id: platformlist

            [...]

            preferredHighlightBegin: vpx(0)
            preferredHighlightEnd: parent.width - vpx(60)

[ShowcaseViewMenu.qml] - After

        ListView {
        id: platformlist

            [...]

For all five potential game lists that could be displayed on the main menu, I've added a focus parameter -- just to make sure I take away focus from the Settings button. (Its gravitational pull is purty strong, even after taking out a direct order to focus. I'll admit, maybe it's just the steampunk in me, but, like... You see a gear, and you're stuck on that gear.) And also place down the -1 overshoot. This will ensure that we don't get any strange, jerky transitions when index-hopping with the mouse.

[ShowcaseViewMenu.qml] - Before

        HorizontalCollection {
        id: list1

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - After

        HorizontalCollection {
        id: list1

            [...]

            onListHighlighted: { mainList.focus = true; mainList.currentIndex = -1; mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - Before

        HorizontalCollection {
        id: list2

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - After

        HorizontalCollection {
        id: list2

            [...]

            onListHighlighted: { mainList.focus = true; mainList.currentIndex = -1; mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - Before

        HorizontalCollection {
        id: list3

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - After

        HorizontalCollection {
        id: list3

            [...]

            onListHighlighted: { mainList.focus = true; mainList.currentIndex = -1; mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - Before

        HorizontalCollection {
        id: list4

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - After

        HorizontalCollection {
        id: list4

            [...]

            onListHighlighted: { mainList.focus = true; mainList.currentIndex = -1; mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - Before

        HorizontalCollection {
        id: list5

            [...]

            onListHighlighted: { mainList.currentIndex = currentList.ObjectModel.index; }
        }

[ShowcaseViewMenu.qml] - After

        HorizontalCollection {
        id: list5

            [...]

            onListHighlighted: { mainList.focus = true; mainList.currentIndex = -1; mainList.currentIndex = currentList.ObjectModel.index; }
        }

I've kept this particular "preferredHighlightBegin" and "preferredHightlightEnd", and just changed the value! This one helps set vertical screen position when navigating. I felt like, post-select, the screen elements weren't shifting over quite enough. It's important that, after you navigate up or down, there will always be room above and room below where you currently are, showing you where you can go next. I also noticed that, due to me making the Favorites header so luxuriously large, toggling between Windowed and Fullscreen would cause the screen to defocus in increments of about half the screen height at a time. Deleting the snapMode (Ie wrote snapeMode one accidente ate firste. ... I'm a little trigger happy with the "e", I think. In my defense, it IS the most commonly used letter in the English language.) parameter caused the screen to stay put.

[ShowcaseViewMenu.qml] - Before

    ListView {
    id: mainList

        [...]

        preferredHighlightBegin: header.height
        preferredHighlightEnd: parent.height - (helpMargin * 2)
        snapMode: ListView.SnapOneItem

[ShowcaseViewMenu.qml] - After

    ListView {
    id: mainList

        [...]

        preferredHighlightBegin: parent.height*0.5
        preferredHighlightEnd: parent.height*0.5

Fix Game Select Indexing on Grid View

This was definitely the challenge I took on that gave me the most grief. (In-fact, in the middle of writing this, I had to go back n' smoosh some more bugs. Like I'm Coraline, moving into her new house or summat.) Compared to Showcase View, Grid View gives you even more opportunities & diverse ways to experience disorientation and layout corruption when trying to properly select a game. I've tried to neatly separate the issues, but there's going to be some overlap, regardless.

Part of the issue can even stem from outside circumstances. A big one is proper naming conventions. Pegasus can be pretty picky about what can be part of your title, and will not allow you to sort, search for, or perhaps even list your game if you use any of the following symbols: (, ), [, ], {, }, $, ^, *, +, and \.

?s seem to only work at the end of titles. I've never run into a game that has multiple question marks peppered throughout in their title, but I bet wherever that game is, it's a good game.

Highlight Transition

Similar to Showcase View, I've taken out some highlightBegin & highlightEnd values. This one wants the selected game to go straight to the top of the screen when selected, and also to clear the space where the "helpMargin" is displayed (that "Here's what your controls are, on a controller!" overlay I disabled). It all made a little dizzy-- and if I didn't select games in order, the entire grid would jitter to compensate for where it thinks everything should be on-screen, due to the large gap I crossed all at once.

[GridViewMenu.qml] - Before

            preferredHighlightBegin: vpx(0)
            preferredHighlightEnd: gamegrid.height - helpMargin - vpx(40)
            highlightRangeMode: GridView.ApplyRange
            highlightMoveDuration: 200
            highlight: highlightcomponent

[GridViewMenu.qml] - After

           highlightMoveDuration: 200
            highlight: highlightcomponent

Navigate by Letter (Ascending & Descending)

After finding out I could PageUp & PageDown to navigate by Letter, I decided to try flipping the alphabetical order from Z-A instead of from A-Z, only to find that the navigation continued to behave identically, thinking the A was at the top and Z was at the bottom. That simply will not do.

Near the top of the GridViewMenu script, where all the variables are being declared, I added three more variables, to help me catch important values while increasing the scope of Letter navigation script. (charCode was actually already declared later, but I need that variable available to all functions.)

[GridViewMenu.qml] - Before

   property var sortedGames;

[GridViewMenu.qml] - After

    property var sortedGames;
    property var sortedGamesRedo;
    property var charReverse;
    property var charCode;

Above the nextChar function, I added a complementary "prevChar" function-- which is just a quick stop during the nextChar function. Mainly I did it that way just so there'd BE a function with that name. A "next" indicates the existence of a "prev", right? I also added one more parameter to the information nextChar is given from the start -- "order"-- since I want it to detect if I'm sorted in Ascending/Descending order. The new "charReverse" variable is used here to turn the numerical character code into an actual symbol to display.

[GridViewMenu.qml] - Before

   function nextChar(c, modifier) {

[GridViewMenu.qml] - After

   function prevChar(c) {
        charReverse = String.fromCharCode(c);
    }

   function nextChar(c, modifier, order) {

Currently, everything in the nextChar function is only meant for sorting in Ascending order, so I wrap it all in an if-else -- if the order is Ascending, it'll do what it already knows to do. If the order is Descending... well, then it does something else! I take out the "var" declaration in-front of charCode, because otherwise, this is going to be a completely different variable named "charCode". Gotta be wary of double-declaration! (National Treasure 2 was subtitled Double Declaration, wasn't it?) I declare another new variable called "charCodeReverse" in here, but it doesn't need to be declared at the top; it's all insular information. If you're reading into the text, you might notice that most of the code is identical. I actually tried to flip more operators, but it really didn't like that! So I more just... tiptoed around it.

[GridViewMenu.qml] - Before

       var charCode = c.charCodeAt(0) + modifier;

        if (modifier > 0) { // Scroll down

            [...]

        }

[GridViewMenu.qml] - After

       if (order == "ascending") {
            charCode = c.charCodeAt(0) + modifier;

        if (modifier > 0) { // Scroll down

            [...]

        }
        } else if (order == "descending") {
            charCode = c.charCodeAt(0) - modifier;
            var charCodeReverse = charCode - modifier;
            prevChar(charCodeReverse);
        if (modifier < 0) { // Scroll up
            if (charCode < firstAlpha || isNaN(charCode)) {
                return 'a';
            }
            if (charCode > lastAlpha) {
                return '';
            }
        } else { // Scroll down
            if (charCode == firstAlpha - 1) {
                return '';
            }
            if (charCode < firstAlpha || charCode > lastAlpha || isNaN(charCode)) {
                return 'z';
            }
        }
        }

When you navigate, a little message flashes on-screen to display which letter you've navigated to. However, the limitations of the present code (and my present understanding of said code) means I can't navigate correctly if I've added any extra Sorting factors. gameOS already is smart enough to know to cancel navigation if you try it on, like, Sort by Rating, but it didn't respond at all to the keyboard command. If the feature exists, it needs to be responsive, even when it can't be used. So I just copied the same component for the text overlay and placed a duplicate underneath, with modified font-size, pause duration... text rendering, too. (I also fix text rendering in the original overlay, but that bit's discussed in its own sub-section.)

[GridViewMenu.qml] - Before

    Rectangle {
    id: navigationOverlay

        [...]

    }

[GridViewMenu.qml] - After

    Rectangle {
    id: navigationOverlay

        [...]

    }

   Rectangle {
    id: navigationOverlay2
        anchors.fill: parent;
        color: theme.main
        opacity: 0
        z: 10

        Text {
        id: navigationError
            antialiasing: true
            renderType: Text.QtRendering
            font.hintingPreference: Font.PreferNoHinting
            font.family: titleFont.name
            font.pixelSize: vpx(25)
            color: "white"
            anchors.centerIn: parent
        }

        SequentialAnimation {
        id: navigationErrorOpacityAnimator
            PauseAnimation { duration: 1250 }
            OpacityAnimator {

                target: navigationOverlay2
                from: navigationOverlay2.opacity
                to: 0;
                duration: 500
            }
        }
    }

I trade out "title" for "sortby" -- even if I wasn't trying to do all this other madcap stuff, it would be important to switch out, due to my other adjustments of variable! Otherwise, the letter navigation ain't gonna work under any circumstances. Then, I add commands under various no-no circumstances to throw the user an error message. You catchin' what I'm throwin' down, user? If not, submit an Issue on my GitHub. Call it "Bug: Persistent error when catching what is thrown."

[GridViewMenu.qml] - Before

       if (sortByFilter[sortByIndex].toLowerCase() != "title") {
            return false;
        }

[GridViewMenu.qml] - After

        if (sortByFilter[sortByIndex].toLowerCase() != "sortby") {
            navigationErrorOpacityAnimator.running = false
            navigationError.text = "Navigating by letter is only compatible on<br>Show All + Sort by Title without Search Terms.";
            navigationOverlay2.opacity = 0.8;
            navigationErrorOpacityAnimator.running = true
            return false;
        }
        
        if (showFavs == true) {
            navigationErrorOpacityAnimator.running = false
            navigationError.text = "Navigating by letter is only compatible on<br>Show All + Sort by Title without Search Terms.";
            navigationOverlay2.opacity = 0.8;
            navigationErrorOpacityAnimator.running = true
            return false;
        }
        
        if (searchTerm != "") {
            navigationErrorOpacityAnimator.running = false
            navigationError.text = "Navigating by letter is only compatible on<br>Show All + Sort by Title without Search Terms.";
            navigationOverlay2.opacity = 0.8;
            navigationErrorOpacityAnimator.running = true
            return false;
        }

This is all inside of a function called "navigateToNextLetter", by the way. After ensuring there's nothing unsupported by the function going on, the real calculation begins. I keep two newly-declared variables, "currentGameTitle" and "currentLetter", but I can't issue them a value yet, due to these new complications I'm adding. (That's what we should call features from now on: complications.) I also can't do any defining of sortedGames yet, which is a listing of the game collection printed as an array.

[GridViewMenu.qml] - Before

       if (currentIndex == -1) {
            gamegrid.currentIndex = 0;
        }
        else {
            // NOTE: We should be using the scroll proxy here, but this is significantly faster.
            if (sortedGames == null) {
                sortedGames = list.collection.games.toVarArray().map(g => g.title.toLowerCase()).sort((a, b) => a.localeCompare(b));
            }

            var currentGameTitle = sortedGames[currentIndex];
            var currentLetter = currentGameTitle.toLowerCase().charAt(0);

[GridViewMenu.qml] - After

       if (currentIndex == -1) {
            gamegrid.currentIndex = 0;
        }
        else {
            
            var currentGameTitle;
            var currentLetter;

Before the function checks if the charCode is out of the range of [A-Z], I insert a more complete set of definitions for "sortedGames", as well as the newly instated "sortedGamesRedo". These two crucially give us the array in Ascending and Descending order.

[GridViewMenu.qml] - Before

           const firstAlpha = 97;
            const lastAlpha = 122;

            if (currentLetter.charCodeAt(0) < firstAlpha || currentLetter.charCodeAt(0) > lastAlpha) {
                currentLetter = '';
            }

[GridViewMenu.qml] - After

           const firstAlpha = 97;
            const lastAlpha = 122;
            
            if (orderBy === Qt.AscendingOrder) {
                sortedGames = list.collection.games.toVarArray().map(g => g.sortBy.toLowerCase()).sort((a, b) => a.localeCompare(b));
            } else if (orderBy === Qt.DescendingOrder) {
                sortedGames = list.collection.games.toVarArray().map(g => g.sortBy.toLowerCase()).sort((a, b) => b.localeCompare(a));
                sortedGamesRedo = list.collection.games.toVarArray().map(g => g.sortBy.toLowerCase()).sort((a, b) => a.localeCompare(b));
            }

            currentGameTitle = sortedGames[currentIndex];
            currentLetter = currentGameTitle.toLowerCase().charAt(0);

            if (currentLetter.charCodeAt(0) < firstAlpha || currentLetter.charCodeAt(0) > lastAlpha) {
                currentLetter = '';
            }

And now we see the nextChar is used here to call the correct character to display on-screen! Gotta add a new third parameter. And make sure to separate the two! If you ascend and descend at the same time, you might end up Halfway Down/Up the Stairs.

[GridViewMenu.qml] - Before

           do {
                do {
                    nextLetter = nextChar(nextLetter, modifier);

[GridViewMenu.qml] - After

           do {
                do {
                    if (orderBy === Qt.AscendingOrder) {
                        nextLetter = nextChar(nextLetter, modifier, "ascending");
                    } else if (orderBy === Qt.DescendingOrder) {
                        nextLetter = nextChar(nextLetter, modifier, "descending");
                    }

Here, we wrap the first localeCompare command (which figures out where the next letter starts in our full sortedGames/sortedGamesRedo list) in an if-Ascending. Descending's a little trickier-- at least I couldn't figure out how to express it in a single command. Instead, I have to check if we're navigating back or forth in the alphabet, then in order to get the opposite index order, I just subtract the upcoming index point from the full number of games currently being displayed, then multiply that by -1 to get a positive result. A bit of hilarious Algebra for me, and I assume there's ex-probably a less silly way to go about it, but I take ownership of my silliness. It works both ways, even-- you could take the result and feed it through the same equation to retrieve the first number, so neato!

[GridViewMenu.qml] - Before

                   else if (sortedGames.some(g => g.charAt(0) == nextLetter)) {
                        break;
                    }
                } while (true)

                nextIndex = sortedGames.findIndex(g => g.toLowerCase().localeCompare(nextLetter) >= 0);

[GridViewMenu.qml] - After

                   else if (sortedGames.some(g => g.charAt(0) == nextLetter)) {
                        break;
                    }
                } while (true)
                if (orderBy === Qt.AscendingOrder) {
                    nextIndex = sortedGames.findIndex(g => g.toLowerCase().localeCompare(nextLetter) >= 0);
                } else if (orderBy === Qt.DescendingOrder) {
                    if (modifier == +1) {
                        nextIndex = sortedGamesRedo.findIndex(g => g.toLowerCase().localeCompare(currentLetter) >= 0);
                        nextIndex = (nextIndex - gamegrid.count)*-1;
                    } else if (modifier == -1) {
                        nextIndex = sortedGamesRedo.findIndex(g => g.toLowerCase().localeCompare(charReverse) >= 0);
                        nextIndex = (nextIndex - gamegrid.count)*-1;
                    }
                }

Even moreso than what I just described, the next set of Descending order instructions are A HEAP in comparison to the one line of text in the if-Ascending slot. The overarching if-elses are:

  • If the nextIndex takes you one farther than the amount of games there are (remember that index counting starts at zero), and you're navigating backward.
  • If the nextIndex takes you one farther than the amount of games there are, and you're navigating forward.
  • If the nextIndex somehow is ONE MORE than that, while navigating backward. (It happened, and I sure hope it doesn't go any further than that.)
  • And then just the typical case-- what it should do every other time.

As you can see, most of this has to do with not knowing how to wrap around the collection correctly. It's probably some underlying comprehension issue on my part. But at least it seems to be catchin' what I'm throwin' down. (It catches more than I can. Bad hand-eye coordination.)

[GridViewMenu.qml] - Before

           } while(nextIndex === -1)

            gamegrid.currentIndex = nextIndex;

            nextLetter = sortedGames[nextIndex].toLowerCase().charAt(0);
            var nextLetterCharCode = nextLetter.charCodeAt(0);
            if (nextLetterCharCode < firstAlpha || nextLetterCharCode > lastAlpha) {
                nextLetter = '#';
            }

[GridViewMenu.qml] - After

            } while(nextIndex === -1)
            
            if (orderBy === Qt.AscendingOrder) {
                gamegrid.currentIndex = nextIndex;
            } else if (orderBy === Qt.DescendingOrder) {
                if (nextIndex == gamegrid.count && modifier == -1) {
                    if (charCode > lastAlpha) {
                        nextIndex = sortedGamesRedo.findIndex(g => g.toLowerCase().localeCompare("a") >= 0);
                        nextIndex = (nextIndex - gamegrid.count)*-1;
                        gamegrid.currentIndex = nextIndex;
                    } else if (currentLetter == "") {
                        nextIndex = sortedGamesRedo.findIndex(g => g.toLowerCase().localeCompare("b") >= 0);
                        nextIndex = (nextIndex - gamegrid.count)*-1;
                        gamegrid.currentIndex = nextIndex;
                    } else if (nextLetter == "a" && modifier == -1) {
                        nextIndex = sortedGamesRedo.findIndex(g => g.toLowerCase().localeCompare("a") >= 0);
                        nextIndex = (nextIndex - gamegrid.count + 1)*-1;
                        gamegrid.currentIndex = nextIndex;
                    } else {
                        gamegrid.currentIndex = 0;
                        nextIndex = 0;
                    }
                } else if (nextIndex == gamegrid.count && modifier == +1) {
                    gamegrid.currentIndex = 0;
                    nextIndex = 0;
                } else if (nextIndex == gamegrid.count + 1 && modifier == -1) {
                    gamegrid.currentIndex = 0;
                    nextIndex = 0;
                } else {
                    gamegrid.currentIndex = nextIndex;
                }
            }
            
            nextLetter = sortedGames[nextIndex].toLowerCase().charAt(0);
            var nextLetterCharCode = nextLetter.charCodeAt(0);
            if (nextLetterCharCode < firstAlpha || nextLetterCharCode > lastAlpha) {
                nextLetter = '#';
            }

Search Bar Usage & Changing Screens

This one's gonna seem like it should be split up-- it IS the largest of these sub-sub-sections, after all. However, these two fixes really need each other to work. During this, I finally get to address the issue where you search for a game, then clear the search bar, and the game you found ends up completely out of index. I've seen even funkier things where the entire list ends up collapsed, so when you reach the end, the remaining games just pop up on the same grid space. Or-- this was a really interesting search bar problem I wasn't expecting to have: If you search up a game, and it's positioned at the topmost row of the grid, it tends to forget how to display video previews.

Another big problem being fixed here arises from another Complication Feature added later on in this article: removing consecutive game selections from your back history. So when you hit Back from Game View, no matter how many other games you've looked through in Game View, you'll go right back to the previous menu you were on. I wanted to ensure if you selected a different game than the one you started on, under as many circumstances as possible, when heading back out into Grid View, you'd now be selecting the current game, instead of the old game.

Working on this also helped me with the more common case: You're playing a game, then when you return to Pegasus afterwards and head out in Grid View, you're no longer selecting that game and have to start searching at the top of the list again. My solution is primarily just that same overshoot logic I've been employing on a lot of index-selection stuff. If the video preview doesn't play on a video, you have to navigate off it and back onto it. If your game gets stuck out of order, it tends to realign after you navigate off it, as well. So I just had the script do the action for me. The stuck-out-of-order one requires this action to be repeated multiple times over the period of a few hundred milliseconds. Which is just... a non-solution in so many ways, but I just feel so good that I got that Timer working right. I couldn't figure that part out for the first month of this project, which led to the system firing off this infinite navigate back-and-forth wiggle forever... ))<>((... impossible to detect until things go REALLY wrong, hahaha.

Here's a first thing to get right while navigating between pages -- if I exit the Grid View, I don't want it to remember where I was in the order. That's my way of saying I'm done, and I'd like to start anew next Time I'm there. So in the Back command, zero out the currentIndex/currentGame and clear the "sortedGames" list. I'd also forgotten until now to clear the Search Bar before exiting. Without clearing that, it'll think the first item in the search-filtered list is where you want to begin when you re-enter the page.

[GridViewMenu.qml] - Before

        // Back
        if (api.keys.isCancel(event) && !event.isAutoRepeat) {

            [...]

            if (gamegrid.focus) {
                previousScreen();

            [...]

        }

[GridViewMenu.qml] - After

        // Back
        if (api.keys.isCancel(event) && !event.isAutoRepeat) {

            [...]

            if (gamegrid.focus) {
                searchTerm = "";
                previousScreen();
                gamegrid.currentIndex = 0;
                sortedGames = null;
                currentGame = null;
            
            [...]

        }

Since you can also exit Grid View by clicking on the logo icon in the upper-left hand corner, it's also in need of a zero. (Get with the hero!) Same is true for the fallback text that appears, should there be no specified logo. And clear the searchTerm againymore. (But NOT searchInput.text -- it doesn't like that. Clear the variable that's sourcing it, instead. Because the text is always in a state of flux where it's sourcing the variable of itself, methinks.)

[HeaderBar.qml] - Before

       OpacityMask {
            anchors.fill: logobg
            source: logobg

            [...]

                onClicked: previousScreen();

[HeaderBar.qml] - After

       OpacityMask {
            anchors.fill: logobg
            source: logobg

            [...]

                onClicked: { searchTerm = ""; gamegrid.currentIndex = 0; previousScreen(); }

[HeaderBar.qml] - Before

       // Platform title
        Text {
        id: softwareplatformtitle
            
            [...]

                onClicked: previousScreen();
            }
        }

[HeaderBar.qml] - After

       // Platform title
        Text {
        id: softwareplatformtitle
            
            [...]

                onClicked: { searchTerm = ""; gamegrid.currentIndex = 0; previousScreen(); }
            }
        }

For something a little different, instead of introducing variables on a specific page, we're going to be adding global variables in the main theme QML. This is important, because when you return to Pegasus after playing a game, it forgets just about every variable... but if the variables are globally accessible, you can save them directly before launching a game, and refer back to them later. The three new variables I'm adding here are "storedSortIndex" -- this ensures I know if the game was on a different sorting option than "sortBy". "reselecting" lets the index know if it should be wiggling still or not. sortDifferent goes along with storedSortIndex, but it also takes into account if I've filtered my selection to show Favorites only, as well.

[theme.qml] - Before

    // Stored variables for page navigation
    property int storedHomePrimaryIndex: 0
    property int storedHomeSecondaryIndex: 0
    property int storedCollectionIndex: 0
    property int storedCollectionGameIndex: 0

[theme.qml] - After

    // Stored variables for page navigation
    property int storedHomePrimaryIndex: 0
    property int storedHomeSecondaryIndex: 0
    property int storedCollectionIndex: 0
    property int storedCollectionGameIndex: 0
    property int storedSortIndex: 0
    property bool reselecting: true;
    property bool sortDifferent: false;

Then, in the function that saves this information, I add both "storedSortIndex" and "sortDifferent" to be set in the API's memory. Trying to set "reselecting" actually wouldn't do much, I've found. I think it's because it will immediately reset itself, anyway, due to the timer, etc. So, instead, we use "sortDifferent" to track "reselecting"'s current state. Which couldn't be done unless reselecting was declared at the beginning here! There were a few more scenarios sortDifferent had to catch that I hadn't thought of til' my v1.2.0 update -- if you've used the search bar at all, or if you're in Order Descending.

... And remember: don't declare a variable more than once in your project. If you declare it again, dassa completely different variable.

[theme.qml] - Before

    // Save current states for returning from game
    function saveCurrentState(game) {

        [...]

        api.memory.set('storedCollectionGameIndex', storedCollectionGameIndex);

[theme.qml] - After

    // Save current states for returning from game
    function saveCurrentState(game) {

        [...]

        api.memory.set('storedCollectionGameIndex', storedCollectionGameIndex);
        api.memory.set('storedSortIndex', sortByIndex);
        if (sortByIndex != 0 || showFavs == true || reselecting == true || searchTerm != "" || orderBy === Qt.DescendingOrder) {
            sortDifferent = true;
        } else {
            sortDifferent = false;
        }
        api.memory.set('sortDifferent', sortDifferent);

It is a truth universally acknowledged, that after you save... you must load. (What you don't know is that I'm secretly pronouncing "save" and "load" as "sah-vé" and "lo-add", a la SBCG4AP.) So retrieve the variables you set into api.memory in the returnedFromGame function.

[theme.qml] - Before

    // Handle loading settings when returning from a game
    property bool fromGame: api.memory.has('To Game');
    function returnedFromGame() {

        [...]

        storedCollectionGameIndex   = api.memory.get('storedCollectionGameIndex');

[theme.qml] - After

    // Handle loading settings when returning from a game
    property bool fromGame: api.memory.has('To Game');
    function returnedFromGame() {

        [...]

        storedCollectionGameIndex   = api.memory.get('storedCollectionGameIndex');
        storedSortIndex = api.memory.get('storedSortIndex');
        sortDifferent = api.memory.get('sortDifferent');

As the grid populates itself (which could be on an initial load, or after re-sorting the list), I add a Component.onCompleted function that checks to see if "reselecting" is true or not, as well as if there's more than one game currently visible in the grid. If both of these criteria are met, it wiggles the index and begins a timer to set "reselecting" to "false" in 300 milliseconds.

[GridViewMenu.qml] - Before

           Component {
            id: dynamicDelegate

                [...]

                    Keys.onPressed: {

                        [...]

                    }
                }
            }

[GridViewMenu.qml] - After

           Component {
            id: dynamicDelegate

                [...]

                    Keys.onPressed: {

                        [...]

                    }
                    
                    Component.onCompleted: {
                        if (reselecting == true && gamegrid.count > 1) {
                            gamegrid.currentIndex = gamegrid.currentIndex + 1;
                            gamegrid.currentIndex = gamegrid.currentIndex - 1;
                            reindexing.start();
                        }
                    }
                }
            }
            
            Timer {
    id: reindexing

        interval: 300
        onTriggered: { reselecting = false; reindexing.stop(); }
    }

Just as a safety net, I have it zero the index position out again if the application is refocusing on the Grid View (which could happen while exiting another page, or entering Grid View after it's already been opened previously), and a current game isn't specified. It also checks to see if the game that you came from is the same as the game that the Grid View is situated on, through comparing their titles. (If only I could figure out how to jump TO a game with a specific title, when the list is in non-alphabetical order, or has been significantly shortened by sorting filters.) It's a little silly, but since I've recently been experiencing issues where the index wiggle isn't enough, instead of simply turning OFF the index-wiggle conditional, I've also put in a single index-wiggle here as a fail-safe. It makes a measurable difference, as ungraceful as it is. ... I mean, it's still turning OFF the conditional. I call that a success.

Other possible situations handled here, in many different configurations: Is the order Ascending or Descending? Is the currentIndex the same as the index of the game you just returned from? Is your sorting filter on "sortBy", or one of the non-index-selectable options? And, of course, is "reselecting" true, so we can have a wiggle break?

The actions performed for each boils down to re-setting the sorting filter to "sortBy", showing All Games, and deleting anything that might've been left behind in the search bar, thereby allowing the system to find and select the right game in the right place. People who HAVE been navigating in Favorites or Sort by Last Opened / Sort by Time Spent might feel like they lost their place, but since I don't have any way to keep track of the index in any of those places (and for those other sorting metrics, they change order EVERY TIME you launch something), this seems like the most useful thing to do post-game.

[GridViewMenu.qml] - Before

   onFocusChanged: {
        if (focus) {
            currentHelpbarModel = gridviewHelpModel;
            gamegrid.focus = true;
        }
    }

[GridViewMenu.qml] - After

    onFocusChanged: {
        if (focus) {
            currentHelpbarModel = gridviewHelpModel;
            gamegrid.focus = true;
            if (currentGame.title == list.currentGame(gamegrid.currentIndex).title) {
                gamegrid.currentIndex = gamegrid.currentIndex + 1;
                gamegrid.currentIndex = gamegrid.currentIndex - 1;
                reselecting = false;
            }
            if (currentGame == null) {
                gamegrid.currentIndex = 0;
                sortedGames = null;
            } else if (orderBy === Qt.AscendingOrder && list.collection.games.toVarArray().findIndex(g => g === currentGame) != gamegrid.currentIndex && sortByFilter[sortByIndex] == "sortBy" && reselecting == true) {
                sortByIndex = 0;
                showFavs = false;
                searchTerm = "";
                gamegrid.currentIndex = list.collection.games.toVarArray().findIndex(g => g === currentGame);
            } else if (orderBy === Qt.DescendingOrder && list.collection.games.toVarArray().reverse().findIndex(g => g === currentGame) != gamegrid.currentIndex && sortByFilter[sortByIndex] == "sortBy" && reselecting == true) {
                sortByIndex = 0;
                showFavs = false;
                searchTerm = "";
                gamegrid.currentIndex = list.collection.games.toVarArray().reverse().findIndex(g => g === currentGame);
            } else if (sortByFilter[sortByIndex] != "sortBy" && reselecting == true && orderBy === Qt.AscendingOrder) {
                sortByIndex = 0;
                showFavs = false;
                searchTerm = "";
                gamegrid.currentIndex = list.collection.games.toVarArray().findIndex(g => g === currentGame);
            } else if (sortByFilter[sortByIndex] != "sortBy" && reselecting == true && orderBy === Qt.DescendingOrder) {
                sortByIndex = 0;
                showFavs = false;
                searchTerm = "";
                gamegrid.currentIndex = list.collection.games.toVarArray().reverse().findIndex(g => g === currentGame);
            } else if (sortDifferent == true && orderBy === Qt.AscendingOrder) {
                sortDifferent = false;
                reselecting = true;
                sortByIndex = 0;
                showFavs = false;
                searchTerm = "";
                gamegrid.currentIndex = list.collection.games.toVarArray().findIndex(g => g === currentGame);
            } else if (sortDifferent == true && orderBy === Qt.DescendingOrder) {
                sortDifferent = false;
                reselecting = true;
                sortByIndex = 0;
                showFavs = false;
                searchTerm = "";
                gamegrid.currentIndex = list.collection.games.toVarArray().reverse().findIndex(g => g === currentGame);
            }
        }
    }

When moving between the Grid View header back to the games grid, I just have it do one more wiggle back and forth, should there be at least two items in the grid. (Two grid items is the minimum-required space to do the wiggle.)

[GridViewMenu.qml] - Before

   Rectangle {
    id: header

        [...]

        Keys.onDownPressed: {
            sfxNav.play();
            gamegrid.focus = true;
            gamegrid.currentIndex = 0;
        }
    }

[GridViewMenu.qml] - After

    Rectangle {
    id: header

        [...]

        Keys.onDownPressed: {
            sfxNav.play();
            gamegrid.focus = true;
            if (gamegrid.currentIndex > -1 && gamegrid.count > 1) {
                gamegrid.currentIndex = gamegrid.currentIndex + 1;
                gamegrid.currentIndex = gamegrid.currentIndex - 1;
            } else {
                gamegrid.currentIndex = 0;
            }
        }
    }

In the search bar's TextInput component, I saved the currentIndex in a temporary variable, then zeroed it out. I used the "onTextEdited" event to do overshooting, as well as setting "reselecting" to "true". This really helps keep your selected game from getting stuck while the list repopulates. Really, this is the most important instance of "reselecting".

It goes on to check if the game grid's current number of games displayed is greater than zero but less than or equal to the amount of games currently set as a complete row... if so, it overshoots the index to -1, then re-sets to the temporary variable's value. This action wasn't firing at the right time, leading to video previews not being activated when selected through a search query, so I off-loaded this particular action to a timer. (I know, I know, if I wiggle anymore, this Jenga tower of code will be FAR too wobbly. I always chance 'um when I play Jenga.)

If it's not within the set range (aka, not in the topmost row), it overshoots to 0 instead, then back. It also checks to see if the currentIndex has glitched out of the range of [0-how many things you got]. If so, it sets the index back to its default. AFTER all that has been decided, it then sends the search bar information into a variable that handles the actual sorting. If this is done earlier, the stability of the grid WILL suffer.

Placed after the TextInput's MouseArea, I put another Component.onCompleted event. This actually is used again, adding another code into the event. That's just a little later. But I don't know if someone would think to do any of this out of order, so instead of assuming you haven't or have done anything involving this event, I'm just adding a little [...] in the event to indicate you MIGHT have already put something here, and that it should stay there, should you want to hold onto those changes. For now, it's a good place to tell "sortByIndex" to set itself to that save-state'd version of itself, "storedSortIndex".

[HeaderBar.qml] - Before

               TextInput {
                id: searchInput
                    
                    [...]

                    onTextEdited: {
                        searchTerm = searchInput.text
                    }
                }

                // Mouse/touch functionality
                MouseArea {

                    [...]

                }

[HeaderBar.qml] - After

               TextInput {
                id: searchInput
                    
                    [...]

                    onTextEdited: {
                        reselecting = true;
                        var tempIndex = gamegrid.currentIndex;
                        if (gamegrid.count <= settings.GridColumns && gamegrid.count > 0) {
                            firstcolumntimer.start();
                        } else {
                            gamegrid.currentIndex = 0;
                            gamegrid.currentIndex = tempIndex;
                        }
                        if (gamegrid.currentIndex > gamegrid.count || gamegrid.currentIndex < 0) {
                            gamegrid.currentIndex = 0;
                        }
                        searchTerm = searchInput.text
                    }
                }

                Timer {
                id: firstcolumntimer

                    interval: 100
                    onTriggered: {
                        tempIndex = gamegrid.currentIndex;
                        gamegrid.currentIndex = -1;
                        gamegrid.currentIndex = tempIndex;
                        firstcolumntimer.stop();
                    }
                }

                // Mouse/touch functionality
                MouseArea {

                    [...]

                }

                Component.onCompleted: {

                [...]

                sortByIndex = storedSortIndex; }

Here's another safety net to ensure that, when you go from one collection to another, it doesn't hold onto previous saved indexes. This is especially important when navigating between different collections. On some previous Pegasus sessions, I've had, say, game #5 selected in my James collection, exited, gone into my Borks collection, and it's got book #5 selected! That gets especially messy when you don't have the same number of things in each collection, so you could very well select something totally out of the range of the other collection. (Here's hoping that would be caught by one of the other overly cautious codes I w'rodes. Oh, no, my code tripped over its own feet! Who could've predicted?)

[ShowcaseViewMenu.qml] - Before

               if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                    event.accepted = true;
                    currentCollectionIndex = platformlist.currentIndex;

[ShowcaseViewMenu.qml] - After

               if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                    event.accepted = true;
                    currentGame = null;
                    currentCollectionIndex = platformlist.currentIndex;

Sorting Select

This is a problem that wouldn't even arise in pre-modified gameOS-- but I just HAD TO GO and make the sorting options clickable, now didn't I? So, adding to the brand-new MouseAreas, y'gotta do some funky stuff to ensure that you maintain game index when flipping alphabetical order. I found it best to do a type of overshoot where I saved the currentIndex in a temporary variable, zeroed it out, then toggled the order switch (it didn't like when I put this before the zero-out). After that, I placed the currentIndex back on the value of my saved temporary variable, then set "reselecting" to true. If you're not sorting on "sortBy", I just have it pop back up to the top every time. Seemed like the thing to do, especially since it tends not to sort items in the same order when flipped, if the values between some of them are the same.

Somehow, I forgot about this, but I forgot an additional toggleOrderBy in the onClicked action, so for months, I would've been unable to click the Ascending/Descending if I wasn't sorting by Title. What I'm telling you is I haven't used those buttons at all. Just for show. Literally, it is just for showing.

[HeaderBar.qml] - Before

            // Ascending/descending
            Item {
            id: directionbutton

                [...]

                Text {
                id: directiontitle
                    
                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: {onDownDirectionButton = true;}
                    onExited: {onDownDirectionButton = false;}
                    onClicked: {
                        toggleOrderBy();
                    }
                }

[HeaderBar.qml] - After

            // Ascending/descending
            Item {
            id: directionbutton

                [...]

                Text {
                id: directiontitle
                    
                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: {onDownDirectionButton = true;}
                    onExited: {onDownDirectionButton = false;}
                    onClicked: {
                        if (sortByFilter[sortByIndex] == "sortBy") {
                            var tempIndex = (gamegrid.currentIndex - (gamegrid.count-1))*-1;
                                gamegrid.currentIndex = 0;
                                toggleOrderBy();
                                gamegrid.currentIndex = tempIndex;
                                reselecting = true;
                        } else {
                            toggleOrderBy();
                            gamegrid.currentIndex = 0;
                        }
                    }
                }

We do a kind of modified "reselecting" wiggle for the other two sorting toggles, setting the currentIndex to 0, calling the respective sort-cycle function, then setting the currentIndex to 0 again for good measure.

[HeaderBar.qml] - Before

            // Order by title
            Item {
            id: titlebutton

                [...]

                Text {
                id: ordertitle
                    
                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: {onDownTimeButton = true;}
                    onExited: {onDownTimeButton = false;}
                    onClicked: {
                        cycleSort();
                    }
                }

[HeaderBar.qml] - After

            // Order by title
            Item {
            id: titlebutton

                [...]

                Text {
                id: ordertitle
                    
                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: {onDownTimeButton = true;}
                    onExited: {onDownTimeButton = false;}
                    onClicked: {
                        gamegrid.currentIndex = 0;
                        cycleSort();
                        gamegrid.currentIndex = 0;
                    }
                }

[HeaderBar.qml] - Before

            // Filters menu
            Item {
            id: filterbutton

                [...]
                
                // Filter title
                Text {
                id: filtertitle
                    
                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: {onDownFilterButton = true;}
                    onExited: {onDownFilterButton = false;}
                    onClicked: {
                        toggleFavs();
                    }
                }

[HeaderBar.qml] - After

            // Filters menu
            Item {
            id: filterbutton

                [...]
                
                // Filter title
                Text {
                id: filtertitle
                    
                    [...]

                }
                
                // Mouse/touch functionality
                MouseArea {
                    anchors.fill: parent
                    onEntered: {onDownFilterButton = true;}
                    onExited: {onDownFilterButton = false;}
                    onClicked: {
                        gamegrid.currentIndex = 0;
                        toggleFavs();
                        gamegrid.currentIndex = 0;
                    }
                }

If you're selecting with the keyboard only, there's no chance that any index within the grid will still be selected (you WOULD have to travel all the way back up there, after all). So all that need be done this time is to highlight the first index in the grid after cycle-sort.

[HeaderBar.qml] - Before

                Text {
                id: directiontitle
                    
                    [...]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        toggleOrderBy();
                    }
                }
            }

[HeaderBar.qml] - After

                Text {
                id: directiontitle
                    
                    [...]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        toggleOrderBy();
                        gamegrid.currentIndex = 0;
                    }
                }
            }


[HeaderBar.qml] - Before

            // Order by title
            Item {
            id: titlebutton

                [...]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        cycleSort();
                    }
                }
            }

[HeaderBar.qml] - After

            // Order by title
            Item {
            id: titlebutton

                [...]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        cycleSort();
                        gamegrid.currentIndex = 0;
                    }
                }
            }

[HeaderBar.qml] - Before

            // Filters menu
            Item {
            id: filterbutton

                [...]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        toggleFavs();
                    }
                }
            }

[HeaderBar.qml] - After

            // Filters menu
            Item {
            id: filterbutton

                [...]

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        toggleFavs();
                        gamegrid.currentIndex = 0;
                    }
                }
            }

Switching Games in Game View

When you select a game in Grid View, it takes you into the game's individual Game View. Below the Launch button, Details button, Favorite button (it adds to Favorites, it is not my favourite button -- that is obviously the Launch button), and Back button, there are [potentially] two lists of related games to peruse. If you select a new game, and then head back out into Grid View (with my impending adjustment to the Back History), it normally wouldn't switch to your most recent game selection. But that just seemed like a new bit of acknowledgement, to any cool folk (is folk still plural?) who's doing that kind of active exploration.

We add a new event to both lists called onActivateSelected, where it will pose the question: "Does the game you just clicked on have the same title as the game that's selected on the game grid?" If not, you just get it prepped to do the Reselect Wiggle.

[GameView.qml] - Before

        HorizontalCollection {
        id: list1

            [...]

            onListHighlighted: { sfxNav.play(); content.currentIndex = list1.ObjectModel.index; }
        }

[GameView.qml] - After

        HorizontalCollection {
        id: list1

            [...]

            onListHighlighted: { sfxNav.play(); content.currentIndex = list1.ObjectModel.index; }
            onActivateSelected: { if (search.currentGame(currentIndex).title != currentGame.title) { reselecting = true; } }
        }

[GameView.qml] - Before

        HorizontalCollection {
        id: list2

            [...]

            onListHighlighted: { sfxNav.play(); content.currentIndex = list2.ObjectModel.index; }
        }

[GameView.qml] - After

        HorizontalCollection {
        id: list2

            [...]

            onListHighlighted: { sfxNav.play(); content.currentIndex = list2.ObjectModel.index; }
            onActivateSelected: { if (search.currentGame(currentIndex).title != currentGame.title) { reselecting = true; } }
        }

Y-Coordinate Reset on Selecting Header Bar

When testing something, you often find yourself having to get to a specific place in the program/game in order to properly demonstrate something that takes place there. And there's no way you're getting that right the first time, so you keep doing a mad, increasingly speedrun-strat-esque dash back into place, should there be no faster way. So it was that, during one of those mad dashes, I came to find out that if I was closer to the bottom of the list of games, then held down the Up key on the game grid, once focus jumped over to the header, half of that first row of games was still stuck off-screen.

Since our ultimate destination is -1 this time, instead of doing a -1 overshoot, I do a 0 overshoot, then back to -1, then that first row at index 0 will realign itself to the bottom of the header. How's that for a satisfying ending to the overshoot saga? (Woah, Overshoot Saga. ... I'm making a note of that.)

[GridViewMenu.qml] - Before

            // Manually set the navigation this way so audio can play without performance hits
            Keys.onUpPressed: {
                sfxNav.play();
                if (currentIndex < numColumns) {
                    headercontainer.focus = true;
                    gamegrid.currentIndex = -1;
                } else {
                    moveCurrentIndexUp();
                }
            }

[GridViewMenu.qml] - After

            // Manually set the navigation this way so audio can play without performance hits
            Keys.onUpPressed: {
                sfxNav.play();
                if (currentIndex < numColumns) {
                    headercontainer.focus = true;
                    gamegrid.currentIndex = -1;
                    gamegrid.currentIndex = 0;
                    gamegrid.currentIndex = -1;
                } else {
                    moveCurrentIndexUp();
                }
            }

Fix Lowercase Formatting on Genres

I'm not entirely sure why this is, but if you have a Genre collection on Showcase View, it displays the genre name in lower-case! It doesn't appear like it's necessary to call the data correctly or anything-- plus, it just looks ugly with everything else. Although I suppose the unedited theme does have a consistent inconsistent lower-case formatting. Just delete ".toLowerCase()" from this line in the Showcase View class. See-- someone had to DECIDE to make it lower-case, that's what I'm having a difficult Time accepting.

[ShowcaseViewMenu.qml] - Before

    property string randoGenre: (Utils.returnRandom(Utils.uniqueValuesArray('genreList'))[0] || '').toLowerCase()

[ShowcaseViewMenu.qml] - After

    property string randoGenre: (Utils.returnRandom(Utils.uniqueValuesArray('genreList'))[0] || '')

Fix Metadata Loading on Game View

In comparing the code of gameOS - Fire and later builds of standard gameOS, I noticed more asynchronous loading added to different components. I don't entirely understand what asynchronous loading entails, but I've noticed it ensures that data doesn't display incorrectly or incompletely. Oftentimes, I was noticing that assets weren't appearing on the Game View screen, etc., if I went about things in a more casual, meandering way. So I copied the scripts from standard gameOS, wholesale. The properties of everything remain nearly untouched from the reorganization. You just wrap all the main code you were using in a Loader, tell it to asynchronously load, to be sourced into a separate Component. The newly separated component is pretty much empty, but it's gonna be the receptacle for all that asynchronous goodness.

Les'see now, "listview" and "listviewloader" are responsible, I believe, for loading Grid View.

[theme.qml] - Before

    SoftwareListMenu {
    id: listviewloader

        focus: (root.state === "softwarescreen")
        visible: opacity !== 0
        opacity: focus ? 1 : 0
        Behavior on opacity { PropertyAnimation { duration: transitionTime } }

        anchors.fill: parent
    }

[theme.qml] - After

    Loader  {
    id: listviewloader

        focus: (root.state === "softwarescreen")
        active: opacity !== 0
        opacity: focus ? 1 : 0
        Behavior on opacity { PropertyAnimation { duration: transitionTime } }

        anchors.fill: parent
        sourceComponent: listview
        asynchronous: true
    }
    
    Component {
    id: listview

        SoftwareListMenu { focus: true }
    }

"gameview" and "gameviewloader" handle loading assets & other metadata related to your chosen game. The component calls on the currentGame variable-- twice? I'm not sure about if a // makes your code a comment. I would've thought it'd source that info inside the LOADER, instead of after (asynchronous, remember), but I wager it needs to be situated there to ensure it switches all the game info more than the initial time you enter this View.

[theme.qml] - Before

    GameView {
    id: gameviewloader

        focus: (root.state === "gameviewscreen")
        visible: opacity !== 0
        onVisibleChanged: if (!active) popLastGame();
        opacity: focus ? 1 : 0
        Behavior on opacity { PropertyAnimation { duration: transitionTime } }

        anchors.fill: parent
        game: currentGame
    }

[theme.qml] - After

    Loader  {
    id: gameviewloader

        focus: (root.state === "gameviewscreen")
        active: opacity !== 0
        onActiveChanged: if (!active) popLastGame();
        opacity: focus ? 1 : 0
        Behavior on opacity { PropertyAnimation { duration: transitionTime } }

        anchors.fill: parent
        sourceComponent: gameview
        asynchronous: true
        //game: currentGame
    }

    Component {
    id: gameview

        GameView {
            focus: true
            game: currentGame
        }
    }

"launchgame" and "launchgameloader" -- I think a specific launching screen like this only shows up if you're accessing games from an external source like Steam. So I haven't encountered this particular screen a whole lot, personally. But I hope, should it be needful in your own plight, that the added asynchrony will serve you well.

[theme.qml] - Before

    LaunchGame {
    id: launchgameloader

        focus: (root.state === "launchgamescreen")
        visible: opacity !== 0
        opacity: focus ? 1 : 0
        Behavior on opacity { PropertyAnimation { duration: transitionTime } }

        anchors.fill: parent
    }

[theme.qml] - After

    Loader  {
    id: launchgameloader

        focus: (root.state === "launchgamescreen")
        active: opacity !== 0
        opacity: focus ? 1 : 0
        Behavior on opacity { PropertyAnimation { duration: transitionTime } }

        anchors.fill: parent
        sourceComponent: launchgameview
        asynchronous: true
    }
    
    Component {
    id: launchgameview

        LaunchGame { focus: true }
    }

Fix Mouse & Keyboard Interactions with Search Bar

The search bar in gameOS is collapsed by default, and you have to click the bar to open it. If you use your mouse, after opening it, you still have to click it a second time to begin typing in it, which juked me out a good deal. I couldn't find any way to leverage the focus of the search bar into also focusing the TextInput component-- none of the commands I tried seemed to be able to reach it. Seemed to kinda just be doing its own thing. After all, what's being entered into the TextInput component only gets fed into the variables at a later point. (Yes, I refer to that later point I spoke of earlier.)

Other things I didn't find useful:

  • The defocusing from the grid made it so, if you navigate back to the grid, it restarts at index 0, assuming you were, rightfully so, on index -1.
  • The placement of this collapsed search-bar circle is exactly the same as the gear circle for gameOS settings. Out of my peripheral, I can completely forget the search bar exists this way.
  • Whenever I navigate to the search bar with my keyboard, I feel that I should be able to just start typing without hitting Enter on it, that the search bar should begin taking input and expand.
  • But perhaps the worst problem-- and this is an actual glitch instead of a functionality nitpick: After typing in the search bar and exiting it once, you actually can't get back into it with the keyboard or the controller. Attempting to activate it will send you into the grid, instead. This issue is still present in the newest build of gameOS, although I think an effort was made to mitigate it.

And... my solution to all of this was to just... never collapse the search bar. Doing this makes it so I can start typing the moment I've selected the textbox with either my mouse or my keyboard, it doesn't defocus from the currentIndex, the search bar just looks better all the time (Sorry, the UI animation was really nice, but it DOES leave the header looking a little bare when collapsed.), and utmostly important, it doesn't lock me out after first use.

I set the toggleSearchBar function to always set searchActive to true, rather than to... toggle.

[HeaderBar.qml] - Before

    function toggleSearch() {
        searchActive = !searchActive;
    }

[HeaderBar.qml] - After

    function toggleSearch() {
        searchActive = true;
    }

I'm also taking the opportunity to take out any instances of "searchInput.selectAll();". The way things were set up, if there's any text in the search bar, the next time you select the search bar (if you can even do that), it assumes you want to select everything you've written. To delete it, I suppose. I'd say most Times, I just wanna add more to the terms that are already there.

Ah-- and this is what I was talking about, if you came from Search Bar Usage fixes for Game Select Indexing. After the search bar's MouseArea, I added a "Component.onCompleted" event. But if you already added "Component.onCompleted" in the aforementioned indexing exploits, don't delete the line of code already in the event. Both are needed for their respective fixes, so just add both into ONE "onCompleted" event. All this new line does is open the search bar whenever it's finished loading. Always Be Opening!

[HeaderBar.qml] - Before

                // Mouse/touch functionality
                MouseArea {

                    [...]

                    onClicked: {
                        if (!searchActive)
                        {
                            toggleSearch();
                            searchInput.selectAll();
                        }
                    }
                }

[HeaderBar.qml] - After

                // Mouse/touch functionality
                MouseArea {

                    [...]

                    onClicked: {
                        if (!searchActive)
                        {
                            toggleSearch();
                        }
                    }
                }
                
                Component.onCompleted: { toggleSearch();

                [...]

}

Again, deletingAll() the selectAll()s. I'm just realizing why this was implemented: for non-keyboard users. But non-keyboard users have a much bigger problem than selectingAll(). Really, no one should be typing anything longer than their own name without a keyboard. :|

[HeaderBar.qml] - Before

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        if (!searchActive) {
                            toggleSearch();
                            searchInput.selectAll();
                        } else {
                            searchInput.selectAll();
                        }
                    }
                }

[HeaderBar.qml] - After

                Keys.onPressed: {
                    // Accept
                    if (api.keys.isAccept(event) && !event.isAutoRepeat) {
                        event.accepted = true;
                        if (!searchActive) {
                            toggleSearch();
                        }
                    }
                }

Fix Scrolling Glitches on Showcase View & Game View

Quick scrolling-related notice: if you're a macOS user, and you're experiencing jittery scrolling on your trackpad or the like, download the newest release of Pegasus, with its updated Qt Framework! Or you can use my own build with the same update, if icon modification is big on your priorities. Any other scrolling problems that may crop up are probably due to scrolling inertia-- a problem that doesn't seem to be in QT's [scroll]wheelhouse to tend to. I've noticed the energy of my bigger scrolls can linger in areas, or build up a glitch-ridden amount of momentum on the collection header in the standard Pegasus Grid theme. But overall, I'd say leaving your scroll inertia on won't give you too much of an issue, once you're on this new build.

On Showcase View, when selecting the rightmost box in my Collection list, as one returns to that list, it would push the leftmost box off-screen. That seemed to be connected to the "highlightRangeMode" value. "ListView.StrictlyEnforceRange" can be really hasty to move any selected item to be placed where the first item in a list would be. I ree-lee don't want that, especially because I only have two collections... So really, both of them have no reason to shove the other off-stage. This town's big enough. Just change "StrictlyEnforceRange" to "ApplyRange" -- it doesn't do any unnecessary shoving.

[ShowcaseViewMenu.qml] - Before

        // Collections list
        ListView {
        id: platformlist

            [...]

            highlightRangeMode: ListView.StrictlyEnforceRange

[ShowcaseViewMenu.qml] - After

        // Collections list
        ListView {
        id: platformlist

            [...]

            highlightRangeMode: ListView.ApplyRange

If I positioned my cursor in certain cracks between elements, I noticed I could scroll up and down a bit, which would make the page elements move around all jumbly on subsequent navigation, due to not being aware of this covert scroll change. This happens both on Showcase View, as well as Game View. The problem can be exacerbated on Game View if one of the two lists happens to be hidden, a la an upcoming Complication Feature. Since the hidden list still exists down there, you can scroll twice as far into the QML abyss. By putting down an "interactive: false" on ListView components in [ShowcaseViewMenu.qml] and [GameView.qml], it disables any accidental scrolling on little corners and cracks-- and it does so without disabling, say, horizontal scrolling on the collections, like I thought it would. Neither page actually allows one to properly scroll vertically to begin with, so I wasn't really taking any functional away. Would love to implement it, as long as it's not at odds with the rest of the UI interactions! Getting everything to play nice together seems to be the real challenge. Could you please stop shoving your siblings?!

[ShowcaseViewMenu.qml] - Before

    ListView {
    id: mainList

        [...]

        currentIndex: storedHomePrimaryIndex

[ShowcaseViewMenu.qml] - After

    ListView {
    id: mainList

        [...]

        currentIndex: storedHomePrimaryIndex
        interactive: false

[GameView.qml] - Before

    ListView {
    id: content

        [...]

        header: Item { height: vpx(450) }

[GameView.qml] - After

    ListView {
    id: content

        [...]

        header: Item { height: vpx(450) }
        interactive: false

Fix Text Rendering on Navigate by Letter

I mentioned this was coming up later. At the start of this journey, when I accidentally ran across the Letter Navigation keyboard shortcut, I actually wondered if, for a second, I had broken the whole theme. See, the letters weren't displaying correctly. To stress-test it, I added more letters to the overlay, and it came out looking like this, with only the letter "X" somehow un-besmirched.

Xperiencingheadachessmall.png

(click here for full-size image)

Looks like the language of an ancient civilization with accelerated technology, Stargate style. And the X is, like, the key to deciphering it-- the fulcrum upon which this language has been slanted on.

To fix the problem, I changed the rendering type from "NativeRendering" to "QtRendering", and my text was no longer being scrambled like a pre-HD television signal. However, as I said, the developer recently updated to a recent Qt Framework for macOS, and I decided to check if NativeRendering no longer topped my turvies. Turns out that in the most recent build, NativeRendering & QtRendering are now equally supported. But-- I'm going to stick with QtRendering here... simply because it makes the font prettier. No more perfectly sharp edges -- everything's been rounded slightly. Which I think is more unique and personable. Text can get too... obelisk-y for my liking. Meet me on my level, Text.

[GridViewMenu.qml] - Before

        Text {
        id: navigationLetter
            antialiasing: true
            renderType: Text.NativeRendering

            [...]

        }

[GridViewMenu.qml] - After

        Text {
        id: navigationLetter
            antialiasing: true
            renderType: Text.QtRendering

            [...]

        }

Let's just call it a design choice born out of a limitation. Like all good design choices are.

Hide "More Games from Publisher" on Game View if Developer is the Same

As mentioned, my two displayed lists on Game View are "More from Developer" and "More from Publisher". Quite a few times in the indiesphere (which is my whole orbit), Developer & Publisher are sure to be exactly the same. (Not to mention, I'm also using this space for author's names. -- Ooh, that means I could have Developer as Author, then Publisher as Illustrator, should there be one! Ooh. Ooh. Illustrators.) In this case, I just want it to only display both lists if the names in both those fields are not the same.

Under the second list (which is Publisher, for me), I dictate that it only be visible if this game's developer is not the same as its publisher.

[GameView.qml] - Before

        HorizontalCollection {
        id: list2

            property bool selected: ListView.isCurrentItem
            focus: selected

[GameView.qml] - After

        HorizontalCollection {
        id: list2

            property bool selected: ListView.isCurrentItem
            focus: selected
            visible: game.developer != game.publisher

Even when it's not visible, it can still be navigated to. There are three vertical indexes that are being used on the Game View page. Index 0 is where all the UI is. Index 1 is the first related game list. Index 2 is the second related game list. (Increase the index ranges here by 1, if you opt not to remove the Media Viewer, as I later suggest.) A manageable amount of stuff-- so I added an if-else that just asks, "Is this game's Publisher and Developer the same? Are you on index 0, if you're navigating up-- or on index 1, if you're navigating down? If you answered yes to any or all of these questions, skip index 2!"

[GameView.qml] - Before

        onCurrentIndexChanged: { 

            [...]

        Keys.onUpPressed: { sfxNav.play(); decrementCurrentIndex() }
        Keys.onDownPressed: { sfxNav.play(); incrementCurrentIndex() }
    }

[GameView.qml] - After

        onCurrentIndexChanged: { 

            [...]

        Keys.onUpPressed: { sfxNav.play(); if (game.publisher == game.developer && content.currentIndex == 0) { content.currentIndex = 1; } else { decrementCurrentIndex() } }
        Keys.onDownPressed: { sfxNav.play(); if (game.publisher == game.developer && content.currentIndex == 1) { content.currentIndex = 0; } else { incrementCurrentIndex() } }

Improve Image Resolution of Game Art & Collection Logos

If you're gonna use this on a screen with normal pixel density, no reason to increase these proportions. I happen to be on a Retina display, so the more pixels, the merrier. Especially because I worked pretty painstakingly hard editing some of the art being showcased, it would be a shame to see it looking like a 480p instead of, at the very least, a solid 720p. For the assets on the grid items, I doubled the size of both image assets:

[DynamicGridItem.qml] - Before

        Image {
        id: screenshot

            [...]

            sourceSize { width: 256; height: 256 }
            smooth: false

            [...]

        }

[DynamicGridItem.qml] - After

        Image {
        id: screenshot

            [...]

            sourceSize { width: 512; height: 512 }
            smooth: true

            [...]

        }

[DynamicGridItem.qml] - Before

        Image {
        id: favelogo

            [...]

            sourceSize { width: 200; height: 150 }

[DynamicGridItem.qml] - After

        Image {
        id: favelogo

            [...]

            sourceSize { width: 400; height: 300 }

For the gameOS logo, since it's really just one image that'll be there, I went ahead and changed the sourceSize to the actual source image's size. I don't think it'd suddenly gain more pixels if I push those boundaries even a hairline further.

[ShowcaseViewMenu.qml] - Before

        Image {
        id: ftueLogo

            [...]

            sourceSize { width: 350; height: 250}

[ShowcaseViewMenu.qml] - After

        Image {
        id: ftueLogo

            [...]

            sourceSize { width: 725; height: 227}

And since my new logos for my collections are both of unusual size (Two Collections of Unusual Size. ... COUSCOUS.), I doubled the sourceSize's width and had the height come right up to meet the width. SQUARE.

[ShowcaseViewMenu.qml] - Before

                Image {
                id: collectionlogo

                    [...]

                    sourceSize { width: 128; height: 64 }

[ShowcaseViewMenu.qml] - After

                Image {
                id: collectionlogo

                    [...]

                    sourceSize { width: 256; height: 256 }

Remove Consecutive Game Selection from Back History

This is probably the one thing I did to the theme that requires less of your system than the original. (I truly don't know what my adamant mandate of GOOD GRAPHICS will do to other people.)

I couldn't think of any reason I'd want to slog back through a whole bunch of subsequent game selections I might make while in Game View. If you were being picky, and also maybe a little choosy, you might be stuck hitting the Escape key for a good long while. So I just wrapped the save-state action from [theme.qml] in an if statement. ONLY save things that AREN'T occurring on the Game View screen. Then you won't be punished for exploring by having to backtrack. No one likes getting back out of the labyrinth; exploring the labyrinth and obtaining its secrets were the Thing. Now gimme a back-entrance or an Escape Rope!

[theme.qml] - Before

        // Save the state before pushing the new one
        lastState.push(state);
        root.state = "gameviewscreen";
    }

[theme.qml] - After

        // Save the state before pushing the new one
        if (root.state != "gameviewscreen") {
            lastState.push(state);
            root.state = "gameviewscreen";
        }
    }

... *ties an Escape Rope firmly to a stalactite*

I don't know how the Escape Rope works, come to think of it.

Remove Description from Game Details

I don't think I need to bother with handling game descriptions in Pegasus. I DO, however, need to bother with writing this college-thesis-length'd article about loosely-defined QML "development".

I just deleted the entire section shown below in [GameInfo.qml]. Nothing else seemed to source the description, which is pretty unusual. Normally I pay, should I unravel the simplest thread in this loom. So I took a lot of glee in this simple, and also clean, cut.

[GameInfo.qml]

    // Description
    PegasusUtils.AutoScroll
    {
    id: gameDescription
    
        [...]

    }

Remove Game Rating from Game Details

I've never been very good at measuring value of one piece of art against another, if I love it. Just about everything I love, I'd bestow all 5 stars. (Legend of the 5 Stars~) I wasn't going to get much out of rating things in Pegasus. In [metadata.pegasus.txt], I've just defaulted everything to a rating of 100%; if it's IN my database-- if it's STAYING in my database, it's a 100.

You trim out three components related to the rating (relatedtotheratingrelatedtotheratingrelatedtotherating), then modify the margin of the element that would normally display right after it.

[GameInfo.qml] - Before

        // Rating box
        Text {
        id: ratingtitle

            [...]

        }

        Text {
        id: ratingtext
            
            [...]

        }

        Rectangle {

        [...]

        }

        // Players box
        Text {
        id: playerstitle

            width: contentWidth
            height: parent.height
            anchors { left: divider1.right; leftMargin: vpx(25) }

[GameInfo.qml] - After

        // Players box
        Text {
        id: playerstitle

            width: contentWidth
            height: parent.height
            anchors { leftMargin: vpx(25) }

See? This is the norm when deleting something; the description truly wasn't tied to anything. A lone wolf! A maverick! A Steve!

Remove Game Title Bubble on Game Highlight

Finally-- this was the most egregious visual nitpick to begin with, and it's high time I pick that nit. (*audience chants along*)

I decided to still leave some functionality here, for if someone wants to turn on the "Always show titles" setting. Honestly, that looks so much cleaner and nicer than that intrusive bubble-- it just shouldn't have been an option at all. So the code over here checks on the status of AlwaysShowTitles. If it's on, the title container's fully opaque and the title text is set. If it's off, the title container (which would otherwise be bubble-ized) is fully transparent and the title text is blank.

[ItemBorder.qml] - Before

    Rectangle {
    id: titlecontainer

        [...]

        opacity: selected ? 1 : 0

[ItemBorder.qml] - After

    Rectangle {
    id: titlecontainer

        [...]

        opacity: { if (settings.AlwaysShowTitles === "Yes") { 1 } else { 0 } }

[ItemBorder.qml] - Before

        Text {
        id: bubbletitle

            text: modelData.title

[ItemBorder.qml] - After

        Text {
        id: bubbletitle

            text: { if (settings.AlwaysShowTitles === "Yes") { modelData.title } else { "" } }

Remove Media Viewer from Game View

Most of these high-end Pegasus themes allows you to look at the media assets you've added to each individual game. ... I do not see the point in this. I have put in all of these assets so that they can be displayed in-theme, in the places I have specified. I do not want to see them out of that context, in civilian clothes. Take it away, take it away!

Delete the entirety of these functions and components, all found in [GameView.qml]:

[GameView.qml]

    // Combine the video and the screenshot arrays into one
    function mediaArray() {

        [...]

        return mediaList;
    }

[GameView.qml]

    // Show/hide the media view
    function showMedia(index) {

        [...]

    }

    function closeMedia() {

        [...]

    }

[GameView.qml]

        HorizontalCollection {
        id: media

            [...]
            
        }

[GameView.qml]

    MediaView {
    id: mediaScreen
        
        [...]

    }

Due to no longer having "media" or a "mediaScreen", remove the related variables from the screen-reset function:

[GameView.qml] - Before

    // Reset the screen to default state
    function reset() {
        content.currentIndex = 0;
        menu.currentIndex = 0;
        media.savedIndex = 0;
        list1.savedIndex = 0;
        list2.savedIndex = 0;
        screenshot.opacity = 1;
        mediaScreen.opacity = 0;
        toggleVideo(true);
    }

[GameView.qml] - After

    // Reset the screen to default state
    function reset() {
        content.currentIndex = 0;
        menu.currentIndex = 0;
        list1.savedIndex = 0;
        list2.savedIndex = 0;
        screenshot.opacity = 1;
        toggleVideo(true);
    }

Back navigation on the keyboard would normally check to see if you're another level deeper in the Media Viewer, so just remove that bracket-less if-else. (Those really tripped me up in editing the code that was already present, since I didn't realize you could only add single-line commands in bracket-less if-elses.)

[GameView.qml] - Before

            if (mediaScreen.visible)
                closeMedia();
            else
                previousScreen();

[GameView.qml] - After

            previousScreen();

Remove "More from Genre" from Game View

In my gameOS - Fire & sKye build, I DO have two lists on the Game View screen, so I don't actually delete a list-- rather, I just changed what it does. But if you wanted to fully delete the second list for cleanliness' sake, you'd just grab everything in-between these brackets...

[GameView.qml]

        // More in genre
        HorizontalCollection {
        id: list2

            [...]

        }

... And snip it out. Then, at the top of the page, you'd get rid of the list's saved index, since the corresponding ID cannot be corresponded with no mores.

[GameView.qml]

        list2.savedIndex = 0;

I could see someone wanting just ONE list. Or no lists! But it IS really nice to be able to cross-reference AT LEAST one aspect of your game with others in your collection. Unifies things. Give us the ol' conspiracy board. *jazz hands*

Having Fun

This might not seem true from the outside, but all of this -- all of the creative stuff I do -- is done in the name of Having Fun.

Pegasuscollectionjul312023Asmall.png

(click here for full-size image)

Pegasuscollectionjul312023Bsmall.png

(click here for full-size image)

When putting all of this together, my main question was: "What would make me happy?" And so, as you can imagine, looking at the result makes me very, very happy. Not just because it looks nice, but because of all the limitless potential it holds inside it now, and how the inside of the box finally reflects the outside of the box. It wouldn't be worth much if I didn't open those boxes. It's like Kevin McAllister and his skates!

Neil Cicierega speaks of keeping in touch with what sparked your fire as a child. Temmie Chang speaks of making play a priority, and making Time for experiencing things that enrich our lives. I know some people may question the importance of the Arts, especially if approached in a non-casual way, but I strongly insist they are not simply ways to distract, or to fill Time. Obviously, try your best to balance things (as Temmie writes in her comic), but, like... ... ENJOY THINGS. Make the Time to ENJOY THINGS, because ENJOYING THINGS really does take putting aside some Time and devoting some Energy.

I promise that, if gone about the right way, making Time for Fun actually improves all other more crucial aspects of your life. At least personally, productivity tends to come if I allow myself to play, too. All the Times in my life when I have accomplished a lot, I've also been actively playing a game alongside those accomplishments. I know that without funtime, I can't find it in me to do much of anything at all, except collapse. Runnin' on empty. Games tend to be made up of tiny accomplishments, and I think it can be tremendously encouraging, especially when more practical accomplishments can be hard to truly visualize getting done. When I complete something in a game (or finish reading a book, etc.), it helps me know that if I just break down my tasks in real life, I can complete them, too. Crucially, it helps me put a spin on the situation, keeping me from feeling overwhelmed and allowing me to Have far more Fun.

So I followed the Fun all the way through this journey, and I've ended up learning a considerable amount -- all in my own wiggly way, of course. As is always the case when I let my younger self take the lead. Together, we can remind ourselves of what an amazing privilege it is to be able to actively experience a carefully crafted piece of someone's imagination and take it with us forever.

And now, if you don't mind, I'm gonna go read a book.

I believe that You can make Time for something just as joyful in your own life.