Skip to content

Commit 4fb9b30

Browse files
ogomez92claude
andcommitted
Add Steam games launcher mode
Add new UI mode triggered by comma (,) that shows installed Steam games with fuzzy search. Games are launched via steam://rungameid/ protocol. Features: - SteamScanner service parses appmanifest_*.acf files - New -l/--steam-library flag for custom Steam library path - Spanish localization for new strings Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 6e7b6b6 commit 4fb9b30

9 files changed

Lines changed: 139 additions & 5 deletions

File tree

CLAUDE.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Launchtype is a Windows application launcher inspired by macOS's Launchbar. It p
1010
- Command launcher with fuzzy search
1111
- Text snippets system
1212
- Clipboard history (up to 50 items)
13+
- Steam games launcher (scans installed games)
1314
- Keyboard-driven interface designed for screen reader accessibility
1415
- Audio feedback for UI interactions
1516

@@ -49,6 +50,7 @@ uv run python src/main.py
4950
# -m, --start-minimized: Start application minimized
5051
# -s, --snippets-on-invoke: Start in snippets mode instead of commands
5152
# -c, --commands [file]: Specify custom commands file (default: commands.json)
53+
# -l, --steam-library [path]: Specify custom Steam library path (default: C:\Program Files (x86)\Steam\steamapps)
5254
```
5355

5456
## Architecture
@@ -75,10 +77,11 @@ The entry point is `src/main.py` which:
7577

7678
**UIManager** (`src/managers/ui_manager.py`)
7779
- Manages the wxPython UI frame and controls
78-
- Handles three UI modes (UIMode enum):
80+
- Handles four UI modes (UIMode enum):
7981
- COMMANDS (default): Shows saved commands
8082
- SNIPPETS (activated by "-"): Shows text snippets
8183
- CLIPBOARD (activated by "?"): Shows clipboard history
84+
- STEAM (activated by ","): Shows installed Steam games
8285
- Toggle back to COMMANDS mode with "."
8386
- Provides dialogs for adding/editing commands and snippets
8487

@@ -95,6 +98,10 @@ The entry point is `src/main.py` which:
9598
- `clipboard_history.py`: Background thread monitoring clipboard with pyperclip
9699
- Polls every 0.1s, maintains up to 50 items
97100
- Persists to `clipboard_history.json`
101+
- `steam_scanner.py`: Scans Steam library for installed games
102+
- Parses `appmanifest_*.acf` files in steamapps folder
103+
- Extracts game names and appids
104+
- Launches games via `steam://rungameid/APPID` URL
98105

99106
### Data Model
100107

@@ -129,6 +136,17 @@ The entry point is `src/main.py` which:
129136
}
130137
```
131138

139+
**Steam game structure** (in-memory):
140+
```python
141+
{
142+
"name": "game name", # lowercase for matching
143+
"shortcut": "", # not used for Steam games
144+
"id": "uuid",
145+
"appid": "620", # Steam app ID
146+
"type": "steam"
147+
}
148+
```
149+
132150
### Search Algorithm
133151
Commands and clipboard items use difflib.get_close_matches with 0.6 cutoff. Shortcuts provide exact match priority (triggers "match" sound vs "type" sound).
134152

@@ -138,7 +156,7 @@ SoundPlayer (`src/helpers/sound_player.py`) provides audio cues:
138156
- "show"/"hide": Window visibility toggle
139157
- "match": Exact shortcut match
140158
- "type": Search results update
141-
- "run": Command execution
159+
- "run": Command execution or Steam game launch
142160
- "copy": Snippet/clipboard item copied
143161

144162
### Accessibility
163 Bytes
Binary file not shown.

locale/es/LC_MESSAGES/launchtype.po

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ msgstr "Arranca la aplicación minimizada"
3030
msgid "specify commands file to use, default is commands.json"
3131
msgstr "especifica el archivo de comandos a usar (por defecto commands.json)"
3232

33+
#: src/managers/command_line_parameters.py:32
34+
msgid "specify custom Steam library path"
35+
msgstr "especifica la ruta personalizada de la biblioteca de Steam"
36+
3337
#: src/managers/ui_manager.py:24
3438
msgid "Input Field"
3539
msgstr "Consola"
@@ -82,6 +86,10 @@ msgstr "Modo historial de portapapeles"
8286
msgid "commands mode"
8387
msgstr "modo comandos"
8488

89+
#: src/managers/ui_manager.py:195
90+
msgid "Steam games mode"
91+
msgstr "modo juegos de Steam"
92+
8593
#: src/managers/ui_manager.py:221
8694
msgid "{}, {} search results shown, use tab and down arrow to access more results"
8795
msgstr "{}, {} resultados de búsqueda mostrados, usa tab y flecha abajo para acceder a más resultados"

src/enums/ui_mode.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
class UIMode(Enum):
44
COMMANDS = 0
55
SNIPPETS = 1
6-
CLIPBOARD = 2
6+
CLIPBOARD = 2
7+
STEAM = 3

src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
language_handler.initialize()
1111

1212
command_line = get_command_line_parameters()
13-
dataManager = DataManager(command_line.commands)
13+
dataManager = DataManager(command_line.commands, command_line.steam_library)
1414

1515
SpeechService().initialize()
1616

src/managers/command_line_parameters.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ def get_command_line_parameters():
2626
default="commands.json",
2727
)
2828

29+
parser.add_argument(
30+
"-l",
31+
"--steam-library",
32+
help=_("specify custom Steam library path"),
33+
action="store",
34+
default=None,
35+
)
36+
2937
args = parser.parse_args()
3038

3139
return args

src/managers/data_manager.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
from os.path import exists
33
from services.clipboard_history import ClipboardHistory
4+
from services.steam_scanner import SteamScanner
45
from helpers.plist_helper import parse_apple_snippets
56
from helpers.search_utility import fuzzy_search, check_exact_shortcut_match
67
from enums.ui_mode import UIMode
@@ -15,8 +16,9 @@ class DataManager:
1516
clipboard_history = ClipboardHistory()
1617
snippets = []
1718

18-
def __init__(self, commands_file):
19+
def __init__(self, commands_file, steam_library_path=None):
1920
self.commands_file = commands_file
21+
self.steam_scanner = SteamScanner(steam_library_path)
2022

2123
if not exists("snippets"):
2224
import os
@@ -66,6 +68,9 @@ def get_data_list_items(self, search_string="", mode=UIMode.COMMANDS):
6668
if mode == UIMode.CLIPBOARD:
6769
return self.get_history_items(search_string)
6870

71+
if mode == UIMode.STEAM:
72+
return self.get_steam_games(search_string)
73+
6974
def get_commands_with_path(self, path):
7075
commands_to_return = []
7176
print(self)
@@ -196,6 +201,29 @@ def get_history_items(self, search_string):
196201

197202
return results
198203

204+
def scan_steam_games(self):
205+
"""Trigger a scan of the Steam library."""
206+
self.steam_scanner.scan_games()
207+
208+
def get_steam_games(self, search_string):
209+
"""Get Steam games, optionally filtered by search string."""
210+
steam_games = self.steam_scanner.get_games_as_items()
211+
212+
if search_string == "":
213+
return steam_games
214+
215+
# Use fuzzy subsequence search on game names
216+
results = fuzzy_search(
217+
search_string, steam_games, lambda game: game["name"]
218+
)
219+
220+
if results:
221+
SoundPlayer.play("type")
222+
else:
223+
SoundPlayer.play("type")
224+
225+
return results
226+
199227
def add_snippet(self, name, contents):
200228
with open("snippets/" + name + ".txt", "w", encoding="utf-8") as outputFile:
201229
outputFile.write(contents)

src/managers/ui_manager.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ def update_list(self, event=None):
191191
self.mode = UIMode.COMMANDS
192192
self.edit.Value = ""
193193

194+
if self.edit.Value == ",":
195+
SpeechService.speak(_("Steam games mode"))
196+
self.dataManager.scan_steam_games()
197+
self.mode = UIMode.STEAM
198+
self.edit.Value = ""
199+
194200
self.commands_in_ui = []
195201
self.list.Clear()
196202

@@ -250,6 +256,11 @@ def run_button_clicked(self, event):
250256
SoundPlayer.play("copy")
251257
copy_to_clipboard(str(selected_option["name"]))
252258

259+
if selected_option["type"] == "steam":
260+
appid = str(selected_option["appid"])
261+
webbrowser.open(f"steam://rungameid/{appid}")
262+
SoundPlayer.play("run")
263+
253264
except Exception as e:
254265
import traceback
255266

src/services/steam_scanner.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
import re
3+
import uuid
4+
5+
6+
class SteamScanner:
7+
def __init__(self, library_path=None):
8+
self.library_path = library_path or r"C:\Program Files (x86)\Steam\steamapps"
9+
self.games = []
10+
11+
def scan_games(self):
12+
"""Scan Steam library for installed games by parsing appmanifest files."""
13+
self.games = []
14+
15+
if not os.path.exists(self.library_path):
16+
return self.games
17+
18+
try:
19+
for filename in os.listdir(self.library_path):
20+
if filename.startswith("appmanifest_") and filename.endswith(".acf"):
21+
filepath = os.path.join(self.library_path, filename)
22+
game = self._parse_appmanifest(filepath)
23+
if game:
24+
self.games.append(game)
25+
except OSError:
26+
pass
27+
28+
# Sort games alphabetically by name
29+
self.games.sort(key=lambda g: g["name"].lower())
30+
return self.games
31+
32+
def _parse_appmanifest(self, filepath):
33+
"""Parse a single appmanifest ACF file to extract game info."""
34+
try:
35+
with open(filepath, "r", encoding="utf-8") as f:
36+
content = f.read()
37+
38+
appid_match = re.search(r'"appid"\s+"(\d+)"', content)
39+
name_match = re.search(r'"name"\s+"([^"]+)"', content)
40+
41+
if appid_match and name_match:
42+
appid = appid_match.group(1)
43+
name = name_match.group(1)
44+
return {
45+
"name": name.lower(),
46+
"shortcut": "",
47+
"id": str(uuid.uuid4()),
48+
"appid": appid,
49+
"type": "steam",
50+
}
51+
except (OSError, UnicodeDecodeError):
52+
pass
53+
54+
return None
55+
56+
def get_games_as_items(self):
57+
"""Return games in UI format."""
58+
if not self.games:
59+
self.scan_games()
60+
return self.games

0 commit comments

Comments
 (0)