optexity-browser-use 0.9.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. browser_use/__init__.py +157 -0
  2. browser_use/actor/__init__.py +11 -0
  3. browser_use/actor/element.py +1175 -0
  4. browser_use/actor/mouse.py +134 -0
  5. browser_use/actor/page.py +561 -0
  6. browser_use/actor/playground/flights.py +41 -0
  7. browser_use/actor/playground/mixed_automation.py +54 -0
  8. browser_use/actor/playground/playground.py +236 -0
  9. browser_use/actor/utils.py +176 -0
  10. browser_use/agent/cloud_events.py +282 -0
  11. browser_use/agent/gif.py +424 -0
  12. browser_use/agent/judge.py +170 -0
  13. browser_use/agent/message_manager/service.py +473 -0
  14. browser_use/agent/message_manager/utils.py +52 -0
  15. browser_use/agent/message_manager/views.py +98 -0
  16. browser_use/agent/prompts.py +413 -0
  17. browser_use/agent/service.py +2316 -0
  18. browser_use/agent/system_prompt.md +185 -0
  19. browser_use/agent/system_prompt_flash.md +10 -0
  20. browser_use/agent/system_prompt_no_thinking.md +183 -0
  21. browser_use/agent/views.py +743 -0
  22. browser_use/browser/__init__.py +41 -0
  23. browser_use/browser/cloud/cloud.py +203 -0
  24. browser_use/browser/cloud/views.py +89 -0
  25. browser_use/browser/events.py +578 -0
  26. browser_use/browser/profile.py +1158 -0
  27. browser_use/browser/python_highlights.py +548 -0
  28. browser_use/browser/session.py +3225 -0
  29. browser_use/browser/session_manager.py +399 -0
  30. browser_use/browser/video_recorder.py +162 -0
  31. browser_use/browser/views.py +200 -0
  32. browser_use/browser/watchdog_base.py +260 -0
  33. browser_use/browser/watchdogs/__init__.py +0 -0
  34. browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
  35. browser_use/browser/watchdogs/crash_watchdog.py +335 -0
  36. browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
  37. browser_use/browser/watchdogs/dom_watchdog.py +817 -0
  38. browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
  39. browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
  40. browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
  41. browser_use/browser/watchdogs/popups_watchdog.py +143 -0
  42. browser_use/browser/watchdogs/recording_watchdog.py +126 -0
  43. browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
  44. browser_use/browser/watchdogs/security_watchdog.py +280 -0
  45. browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
  46. browser_use/cli.py +2359 -0
  47. browser_use/code_use/__init__.py +16 -0
  48. browser_use/code_use/formatting.py +192 -0
  49. browser_use/code_use/namespace.py +665 -0
  50. browser_use/code_use/notebook_export.py +276 -0
  51. browser_use/code_use/service.py +1340 -0
  52. browser_use/code_use/system_prompt.md +574 -0
  53. browser_use/code_use/utils.py +150 -0
  54. browser_use/code_use/views.py +171 -0
  55. browser_use/config.py +505 -0
  56. browser_use/controller/__init__.py +3 -0
  57. browser_use/dom/enhanced_snapshot.py +161 -0
  58. browser_use/dom/markdown_extractor.py +169 -0
  59. browser_use/dom/playground/extraction.py +312 -0
  60. browser_use/dom/playground/multi_act.py +32 -0
  61. browser_use/dom/serializer/clickable_elements.py +200 -0
  62. browser_use/dom/serializer/code_use_serializer.py +287 -0
  63. browser_use/dom/serializer/eval_serializer.py +478 -0
  64. browser_use/dom/serializer/html_serializer.py +212 -0
  65. browser_use/dom/serializer/paint_order.py +197 -0
  66. browser_use/dom/serializer/serializer.py +1170 -0
  67. browser_use/dom/service.py +825 -0
  68. browser_use/dom/utils.py +129 -0
  69. browser_use/dom/views.py +906 -0
  70. browser_use/exceptions.py +5 -0
  71. browser_use/filesystem/__init__.py +0 -0
  72. browser_use/filesystem/file_system.py +619 -0
  73. browser_use/init_cmd.py +376 -0
  74. browser_use/integrations/gmail/__init__.py +24 -0
  75. browser_use/integrations/gmail/actions.py +115 -0
  76. browser_use/integrations/gmail/service.py +225 -0
  77. browser_use/llm/__init__.py +155 -0
  78. browser_use/llm/anthropic/chat.py +242 -0
  79. browser_use/llm/anthropic/serializer.py +312 -0
  80. browser_use/llm/aws/__init__.py +36 -0
  81. browser_use/llm/aws/chat_anthropic.py +242 -0
  82. browser_use/llm/aws/chat_bedrock.py +289 -0
  83. browser_use/llm/aws/serializer.py +257 -0
  84. browser_use/llm/azure/chat.py +91 -0
  85. browser_use/llm/base.py +57 -0
  86. browser_use/llm/browser_use/__init__.py +3 -0
  87. browser_use/llm/browser_use/chat.py +201 -0
  88. browser_use/llm/cerebras/chat.py +193 -0
  89. browser_use/llm/cerebras/serializer.py +109 -0
  90. browser_use/llm/deepseek/chat.py +212 -0
  91. browser_use/llm/deepseek/serializer.py +109 -0
  92. browser_use/llm/exceptions.py +29 -0
  93. browser_use/llm/google/__init__.py +3 -0
  94. browser_use/llm/google/chat.py +542 -0
  95. browser_use/llm/google/serializer.py +120 -0
  96. browser_use/llm/groq/chat.py +229 -0
  97. browser_use/llm/groq/parser.py +158 -0
  98. browser_use/llm/groq/serializer.py +159 -0
  99. browser_use/llm/messages.py +238 -0
  100. browser_use/llm/models.py +271 -0
  101. browser_use/llm/oci_raw/__init__.py +10 -0
  102. browser_use/llm/oci_raw/chat.py +443 -0
  103. browser_use/llm/oci_raw/serializer.py +229 -0
  104. browser_use/llm/ollama/chat.py +97 -0
  105. browser_use/llm/ollama/serializer.py +143 -0
  106. browser_use/llm/openai/chat.py +264 -0
  107. browser_use/llm/openai/like.py +15 -0
  108. browser_use/llm/openai/serializer.py +165 -0
  109. browser_use/llm/openrouter/chat.py +211 -0
  110. browser_use/llm/openrouter/serializer.py +26 -0
  111. browser_use/llm/schema.py +176 -0
  112. browser_use/llm/views.py +48 -0
  113. browser_use/logging_config.py +330 -0
  114. browser_use/mcp/__init__.py +18 -0
  115. browser_use/mcp/__main__.py +12 -0
  116. browser_use/mcp/client.py +544 -0
  117. browser_use/mcp/controller.py +264 -0
  118. browser_use/mcp/server.py +1114 -0
  119. browser_use/observability.py +204 -0
  120. browser_use/py.typed +0 -0
  121. browser_use/sandbox/__init__.py +41 -0
  122. browser_use/sandbox/sandbox.py +637 -0
  123. browser_use/sandbox/views.py +132 -0
  124. browser_use/screenshots/__init__.py +1 -0
  125. browser_use/screenshots/service.py +52 -0
  126. browser_use/sync/__init__.py +6 -0
  127. browser_use/sync/auth.py +357 -0
  128. browser_use/sync/service.py +161 -0
  129. browser_use/telemetry/__init__.py +51 -0
  130. browser_use/telemetry/service.py +112 -0
  131. browser_use/telemetry/views.py +101 -0
  132. browser_use/tokens/__init__.py +0 -0
  133. browser_use/tokens/custom_pricing.py +24 -0
  134. browser_use/tokens/mappings.py +4 -0
  135. browser_use/tokens/service.py +580 -0
  136. browser_use/tokens/views.py +108 -0
  137. browser_use/tools/registry/service.py +572 -0
  138. browser_use/tools/registry/views.py +174 -0
  139. browser_use/tools/service.py +1675 -0
  140. browser_use/tools/utils.py +82 -0
  141. browser_use/tools/views.py +100 -0
  142. browser_use/utils.py +670 -0
  143. optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
  144. optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
  145. optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
  146. optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
  147. optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,41 @@
1
+ import asyncio
2
+
3
+ from browser_use import Agent, Browser, ChatOpenAI
4
+
5
+ llm = ChatOpenAI('gpt-4.1-mini')
6
+
7
+
8
+ async def main():
9
+ """
10
+ Main function demonstrating mixed automation with Browser-Use and Playwright.
11
+ """
12
+ print('🚀 Mixed Automation with Browser-Use and Actor API')
13
+
14
+ browser = Browser(keep_alive=True)
15
+ await browser.start()
16
+
17
+ page = await browser.get_current_page() or await browser.new_page()
18
+
19
+ # Go to apple wikipedia page
20
+ await page.goto('https://www.google.com/travel/flights')
21
+
22
+ await asyncio.sleep(1)
23
+
24
+ round_trip_button = await page.must_get_element_by_prompt('round trip button', llm)
25
+ await round_trip_button.click()
26
+
27
+ one_way_button = await page.must_get_element_by_prompt('one way button', llm)
28
+ await one_way_button.click()
29
+
30
+ await asyncio.sleep(1)
31
+
32
+ agent = Agent(task='Find the cheapest flight from London to Paris on 2025-10-15', llm=llm, browser_session=browser)
33
+ await agent.run()
34
+
35
+ input('Press Enter to continue...')
36
+
37
+ await browser.stop()
38
+
39
+
40
+ if __name__ == '__main__':
41
+ asyncio.run(main())
@@ -0,0 +1,54 @@
1
+ import asyncio
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from browser_use import Browser, ChatOpenAI
6
+
7
+ TASK = """
8
+ On the current wikipedia page, find the latest huge edit and tell me what is was about.
9
+ """
10
+
11
+
12
+ class LatestEditFinder(BaseModel):
13
+ """Find the latest huge edit on the current wikipedia page."""
14
+
15
+ latest_edit: str
16
+ edit_time: str
17
+ edit_author: str
18
+ edit_summary: str
19
+ edit_url: str
20
+
21
+
22
+ llm = ChatOpenAI('gpt-4.1-mini')
23
+
24
+
25
+ async def main():
26
+ """
27
+ Main function demonstrating mixed automation with Browser-Use and Playwright.
28
+ """
29
+ print('🚀 Mixed Automation with Browser-Use and Actor API')
30
+
31
+ browser = Browser(keep_alive=True)
32
+ await browser.start()
33
+
34
+ page = await browser.get_current_page() or await browser.new_page()
35
+
36
+ # Go to apple wikipedia page
37
+ await page.goto('https://browser-use.github.io/stress-tests/challenges/angularjs-form.html')
38
+
39
+ await asyncio.sleep(1)
40
+
41
+ element = await page.get_element_by_prompt('zip code input', llm)
42
+
43
+ print('Element found', element)
44
+
45
+ if element:
46
+ await element.click()
47
+ else:
48
+ print('No element found')
49
+
50
+ await browser.stop()
51
+
52
+
53
+ if __name__ == '__main__':
54
+ asyncio.run(main())
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Playground script to test the browser-use actor API.
4
+
5
+ This script demonstrates:
6
+ - Starting a browser session
7
+ - Using the actor API to navigate and interact
8
+ - Finding elements, clicking, scrolling, JavaScript evaluation
9
+ - Testing most of the available methods
10
+ """
11
+
12
+ import asyncio
13
+ import json
14
+ import logging
15
+
16
+ from browser_use import Browser
17
+
18
+ # Configure logging to see what's happening
19
+ logging.basicConfig(level=logging.INFO)
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ async def main():
24
+ """Main playground function."""
25
+ logger.info('🚀 Starting browser actor playground')
26
+
27
+ # Create browser session
28
+ browser = Browser()
29
+
30
+ try:
31
+ # Start the browser
32
+ await browser.start()
33
+ logger.info('✅ Browser session started')
34
+
35
+ # Navigate to Wikipedia using integrated methods
36
+ logger.info('📖 Navigating to Wikipedia...')
37
+ page = await browser.new_page('https://en.wikipedia.org')
38
+
39
+ # Get basic page info
40
+ url = await page.get_url()
41
+ title = await page.get_title()
42
+ logger.info(f'📄 Page loaded: {title} ({url})')
43
+
44
+ # Take a screenshot
45
+ logger.info('📸 Taking initial screenshot...')
46
+ screenshot_b64 = await page.screenshot()
47
+ logger.info(f'📸 Screenshot captured: {len(screenshot_b64)} bytes')
48
+
49
+ # Set viewport size
50
+ logger.info('🖥️ Setting viewport to 1920x1080...')
51
+ await page.set_viewport_size(1920, 1080)
52
+
53
+ # Execute some JavaScript to count links
54
+ logger.info('🔍 Counting article links using JavaScript...')
55
+ js_code = """() => {
56
+ // Find all article links on the page
57
+ const links = Array.from(document.querySelectorAll('a[href*="/wiki/"]:not([href*=":"])'))
58
+ .filter(link => !link.href.includes('Main_Page') && !link.href.includes('Special:'));
59
+
60
+ return {
61
+ total: links.length,
62
+ sample: links.slice(0, 3).map(link => ({
63
+ href: link.href,
64
+ text: link.textContent.trim()
65
+ }))
66
+ };
67
+ }"""
68
+
69
+ link_info = json.loads(await page.evaluate(js_code))
70
+ logger.info(f'🔗 Found {link_info["total"]} article links')
71
+ # Try to find and interact with links using CSS selector
72
+ try:
73
+ # Find article links on the page
74
+ links = await page.get_elements_by_css_selector('a[href*="/wiki/"]:not([href*=":"])')
75
+
76
+ if links:
77
+ logger.info(f'📋 Found {len(links)} wiki links via CSS selector')
78
+
79
+ # Pick the first link
80
+ link_element = links[0]
81
+
82
+ # Get link info using available methods
83
+ basic_info = await link_element.get_basic_info()
84
+ link_href = await link_element.get_attribute('href')
85
+
86
+ logger.info(f'🎯 Selected element: <{basic_info["nodeName"]}>')
87
+ logger.info(f'🔗 Link href: {link_href}')
88
+
89
+ if basic_info['boundingBox']:
90
+ bbox = basic_info['boundingBox']
91
+ logger.info(f'📏 Position: ({bbox["x"]}, {bbox["y"]}) Size: {bbox["width"]}x{bbox["height"]}')
92
+
93
+ # Test element interactions with robust implementations
94
+ logger.info('👆 Hovering over the element...')
95
+ await link_element.hover()
96
+ await asyncio.sleep(1)
97
+
98
+ logger.info('🔍 Focusing the element...')
99
+ await link_element.focus()
100
+ await asyncio.sleep(0.5)
101
+
102
+ # Click the link using robust click method
103
+ logger.info('🖱️ Clicking the link with robust fallbacks...')
104
+ await link_element.click()
105
+
106
+ # Wait for navigation
107
+ await asyncio.sleep(3)
108
+
109
+ # Get new page info
110
+ new_url = await page.get_url()
111
+ new_title = await page.get_title()
112
+ logger.info(f'📄 Navigated to: {new_title}')
113
+ logger.info(f'🌐 New URL: {new_url}')
114
+ else:
115
+ logger.warning('❌ No links found to interact with')
116
+
117
+ except Exception as e:
118
+ logger.warning(f'⚠️ Link interaction failed: {e}')
119
+
120
+ # Scroll down the page
121
+ logger.info('📜 Scrolling down the page...')
122
+ mouse = await page.mouse
123
+ await mouse.scroll(x=0, y=100, delta_y=500)
124
+ await asyncio.sleep(1)
125
+
126
+ # Test mouse operations
127
+ logger.info('🖱️ Testing mouse operations...')
128
+ await mouse.move(x=100, y=200)
129
+ await mouse.click(x=150, y=250)
130
+
131
+ # Execute more JavaScript examples
132
+ logger.info('🧪 Testing JavaScript evaluation...')
133
+
134
+ # Simple expressions
135
+ page_height = await page.evaluate('() => document.body.scrollHeight')
136
+ current_scroll = await page.evaluate('() => window.pageYOffset')
137
+ logger.info(f'📏 Page height: {page_height}px, current scroll: {current_scroll}px')
138
+
139
+ # JavaScript with arguments
140
+ result = await page.evaluate('(x) => x * 2', 21)
141
+ logger.info(f'🧮 JavaScript with args: 21 * 2 = {result}')
142
+
143
+ # More complex JavaScript
144
+ page_stats = json.loads(
145
+ await page.evaluate("""() => {
146
+ return {
147
+ url: window.location.href,
148
+ title: document.title,
149
+ links: document.querySelectorAll('a').length,
150
+ images: document.querySelectorAll('img').length,
151
+ scrollTop: window.pageYOffset,
152
+ viewportHeight: window.innerHeight
153
+ };
154
+ }""")
155
+ )
156
+ logger.info(f'📊 Page stats: {page_stats}')
157
+
158
+ # Get page title using different methods
159
+ title_via_js = await page.evaluate('() => document.title')
160
+ title_via_api = await page.get_title()
161
+ logger.info(f'📝 Title via JS: "{title_via_js}"')
162
+ logger.info(f'📝 Title via API: "{title_via_api}"')
163
+
164
+ # Take a final screenshot
165
+ logger.info('📸 Taking final screenshot...')
166
+ final_screenshot = await page.screenshot()
167
+ logger.info(f'📸 Final screenshot: {len(final_screenshot)} bytes')
168
+
169
+ # Test browser navigation with error handling
170
+ logger.info('⬅️ Testing browser back navigation...')
171
+ try:
172
+ await page.go_back()
173
+ await asyncio.sleep(2)
174
+
175
+ back_url = await page.get_url()
176
+ back_title = await page.get_title()
177
+ logger.info(f'📄 After going back: {back_title}')
178
+ logger.info(f'🌐 Back URL: {back_url}')
179
+ except RuntimeError as e:
180
+ logger.info(f'ℹ️ Navigation back failed as expected: {e}')
181
+
182
+ # Test creating new page
183
+ logger.info('🆕 Creating new blank page...')
184
+ new_page = await browser.new_page()
185
+ new_page_url = await new_page.get_url()
186
+ logger.info(f'🆕 New page created with URL: {new_page_url}')
187
+
188
+ # Get all pages
189
+ all_pages = await browser.get_pages()
190
+ logger.info(f'📑 Total pages: {len(all_pages)}')
191
+
192
+ # Test form interaction if we can find a form
193
+ try:
194
+ # Look for search input on the page
195
+ search_inputs = await page.get_elements_by_css_selector('input[type="search"], input[name*="search"]')
196
+
197
+ if search_inputs:
198
+ search_input = search_inputs[0]
199
+ logger.info('🔍 Found search input, testing form interaction...')
200
+
201
+ await search_input.focus()
202
+ await search_input.fill('test search query')
203
+ await page.press('Enter')
204
+
205
+ logger.info('✅ Form interaction test completed')
206
+ else:
207
+ logger.info('ℹ️ No search inputs found for form testing')
208
+
209
+ except Exception as e:
210
+ logger.info(f'ℹ️ Form interaction test skipped: {e}')
211
+
212
+ # wait 2 seconds before closing the new page
213
+ logger.info('🕒 Waiting 2 seconds before closing the new page...')
214
+ await asyncio.sleep(2)
215
+ logger.info('🗑️ Closing new page...')
216
+ await browser.close_page(new_page)
217
+
218
+ logger.info('✅ Playground completed successfully!')
219
+
220
+ input('Press Enter to continue...')
221
+
222
+ except Exception as e:
223
+ logger.error(f'❌ Error in playground: {e}', exc_info=True)
224
+
225
+ finally:
226
+ # Clean up
227
+ logger.info('🧹 Cleaning up...')
228
+ try:
229
+ await browser.stop()
230
+ logger.info('✅ Browser session stopped')
231
+ except Exception as e:
232
+ logger.error(f'❌ Error stopping browser: {e}')
233
+
234
+
235
+ if __name__ == '__main__':
236
+ asyncio.run(main())
@@ -0,0 +1,176 @@
1
+ """Utility functions for actor operations."""
2
+
3
+
4
+ class Utils:
5
+ """Utility functions for actor operations."""
6
+
7
+ @staticmethod
8
+ def get_key_info(key: str) -> tuple[str, int | None]:
9
+ """Get the code and windowsVirtualKeyCode for a key.
10
+
11
+ Args:
12
+ key: Key name (e.g., 'Enter', 'ArrowUp', 'a', 'A')
13
+
14
+ Returns:
15
+ Tuple of (code, windowsVirtualKeyCode)
16
+
17
+ Reference: Windows Virtual Key Codes
18
+ https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
19
+ """
20
+ # Complete mapping of key names to (code, virtualKeyCode)
21
+ # Based on standard Windows Virtual Key Codes
22
+ key_map = {
23
+ # Navigation keys
24
+ 'Backspace': ('Backspace', 8),
25
+ 'Tab': ('Tab', 9),
26
+ 'Enter': ('Enter', 13),
27
+ 'Escape': ('Escape', 27),
28
+ 'Space': ('Space', 32),
29
+ ' ': ('Space', 32),
30
+ 'PageUp': ('PageUp', 33),
31
+ 'PageDown': ('PageDown', 34),
32
+ 'End': ('End', 35),
33
+ 'Home': ('Home', 36),
34
+ 'ArrowLeft': ('ArrowLeft', 37),
35
+ 'ArrowUp': ('ArrowUp', 38),
36
+ 'ArrowRight': ('ArrowRight', 39),
37
+ 'ArrowDown': ('ArrowDown', 40),
38
+ 'Insert': ('Insert', 45),
39
+ 'Delete': ('Delete', 46),
40
+ # Modifier keys
41
+ 'Shift': ('ShiftLeft', 16),
42
+ 'ShiftLeft': ('ShiftLeft', 16),
43
+ 'ShiftRight': ('ShiftRight', 16),
44
+ 'Control': ('ControlLeft', 17),
45
+ 'ControlLeft': ('ControlLeft', 17),
46
+ 'ControlRight': ('ControlRight', 17),
47
+ 'Alt': ('AltLeft', 18),
48
+ 'AltLeft': ('AltLeft', 18),
49
+ 'AltRight': ('AltRight', 18),
50
+ 'Meta': ('MetaLeft', 91),
51
+ 'MetaLeft': ('MetaLeft', 91),
52
+ 'MetaRight': ('MetaRight', 92),
53
+ # Function keys F1-F24
54
+ 'F1': ('F1', 112),
55
+ 'F2': ('F2', 113),
56
+ 'F3': ('F3', 114),
57
+ 'F4': ('F4', 115),
58
+ 'F5': ('F5', 116),
59
+ 'F6': ('F6', 117),
60
+ 'F7': ('F7', 118),
61
+ 'F8': ('F8', 119),
62
+ 'F9': ('F9', 120),
63
+ 'F10': ('F10', 121),
64
+ 'F11': ('F11', 122),
65
+ 'F12': ('F12', 123),
66
+ 'F13': ('F13', 124),
67
+ 'F14': ('F14', 125),
68
+ 'F15': ('F15', 126),
69
+ 'F16': ('F16', 127),
70
+ 'F17': ('F17', 128),
71
+ 'F18': ('F18', 129),
72
+ 'F19': ('F19', 130),
73
+ 'F20': ('F20', 131),
74
+ 'F21': ('F21', 132),
75
+ 'F22': ('F22', 133),
76
+ 'F23': ('F23', 134),
77
+ 'F24': ('F24', 135),
78
+ # Numpad keys
79
+ 'NumLock': ('NumLock', 144),
80
+ 'Numpad0': ('Numpad0', 96),
81
+ 'Numpad1': ('Numpad1', 97),
82
+ 'Numpad2': ('Numpad2', 98),
83
+ 'Numpad3': ('Numpad3', 99),
84
+ 'Numpad4': ('Numpad4', 100),
85
+ 'Numpad5': ('Numpad5', 101),
86
+ 'Numpad6': ('Numpad6', 102),
87
+ 'Numpad7': ('Numpad7', 103),
88
+ 'Numpad8': ('Numpad8', 104),
89
+ 'Numpad9': ('Numpad9', 105),
90
+ 'NumpadMultiply': ('NumpadMultiply', 106),
91
+ 'NumpadAdd': ('NumpadAdd', 107),
92
+ 'NumpadSubtract': ('NumpadSubtract', 109),
93
+ 'NumpadDecimal': ('NumpadDecimal', 110),
94
+ 'NumpadDivide': ('NumpadDivide', 111),
95
+ # Lock keys
96
+ 'CapsLock': ('CapsLock', 20),
97
+ 'ScrollLock': ('ScrollLock', 145),
98
+ # OEM/Punctuation keys (US keyboard layout)
99
+ 'Semicolon': ('Semicolon', 186),
100
+ ';': ('Semicolon', 186),
101
+ 'Equal': ('Equal', 187),
102
+ '=': ('Equal', 187),
103
+ 'Comma': ('Comma', 188),
104
+ ',': ('Comma', 188),
105
+ 'Minus': ('Minus', 189),
106
+ '-': ('Minus', 189),
107
+ 'Period': ('Period', 190),
108
+ '.': ('Period', 190),
109
+ 'Slash': ('Slash', 191),
110
+ '/': ('Slash', 191),
111
+ 'Backquote': ('Backquote', 192),
112
+ '`': ('Backquote', 192),
113
+ 'BracketLeft': ('BracketLeft', 219),
114
+ '[': ('BracketLeft', 219),
115
+ 'Backslash': ('Backslash', 220),
116
+ '\\': ('Backslash', 220),
117
+ 'BracketRight': ('BracketRight', 221),
118
+ ']': ('BracketRight', 221),
119
+ 'Quote': ('Quote', 222),
120
+ "'": ('Quote', 222),
121
+ # Media/Browser keys
122
+ 'AudioVolumeMute': ('AudioVolumeMute', 173),
123
+ 'AudioVolumeDown': ('AudioVolumeDown', 174),
124
+ 'AudioVolumeUp': ('AudioVolumeUp', 175),
125
+ 'MediaTrackNext': ('MediaTrackNext', 176),
126
+ 'MediaTrackPrevious': ('MediaTrackPrevious', 177),
127
+ 'MediaStop': ('MediaStop', 178),
128
+ 'MediaPlayPause': ('MediaPlayPause', 179),
129
+ 'BrowserBack': ('BrowserBack', 166),
130
+ 'BrowserForward': ('BrowserForward', 167),
131
+ 'BrowserRefresh': ('BrowserRefresh', 168),
132
+ 'BrowserStop': ('BrowserStop', 169),
133
+ 'BrowserSearch': ('BrowserSearch', 170),
134
+ 'BrowserFavorites': ('BrowserFavorites', 171),
135
+ 'BrowserHome': ('BrowserHome', 172),
136
+ # Additional common keys
137
+ 'Clear': ('Clear', 12),
138
+ 'Pause': ('Pause', 19),
139
+ 'Select': ('Select', 41),
140
+ 'Print': ('Print', 42),
141
+ 'Execute': ('Execute', 43),
142
+ 'PrintScreen': ('PrintScreen', 44),
143
+ 'Help': ('Help', 47),
144
+ 'ContextMenu': ('ContextMenu', 93),
145
+ }
146
+
147
+ if key in key_map:
148
+ return key_map[key]
149
+
150
+ # Handle alphanumeric keys dynamically
151
+ if len(key) == 1:
152
+ if key.isalpha():
153
+ # Letter keys: A-Z have VK codes 65-90
154
+ return (f'Key{key.upper()}', ord(key.upper()))
155
+ elif key.isdigit():
156
+ # Digit keys: 0-9 have VK codes 48-57 (same as ASCII)
157
+ return (f'Digit{key}', ord(key))
158
+
159
+ # Fallback: use the key name as code, no virtual key code
160
+ return (key, None)
161
+
162
+
163
+ # Backward compatibility: provide standalone function
164
+ def get_key_info(key: str) -> tuple[str, int | None]:
165
+ """Get the code and windowsVirtualKeyCode for a key.
166
+
167
+ Args:
168
+ key: Key name (e.g., 'Enter', 'ArrowUp', 'a', 'A')
169
+
170
+ Returns:
171
+ Tuple of (code, windowsVirtualKeyCode)
172
+
173
+ Reference: Windows Virtual Key Codes
174
+ https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
175
+ """
176
+ return Utils.get_key_info(key)