shellac-webview 1.0.1__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.
- shellac/__init__.py +5 -0
- shellac/enums.py +11 -0
- shellac/launcher.py +110 -0
- shellac/models.py +25 -0
- shellac/window.py +297 -0
- shellac_webview-1.0.1.dist-info/METADATA +147 -0
- shellac_webview-1.0.1.dist-info/RECORD +9 -0
- shellac_webview-1.0.1.dist-info/WHEEL +5 -0
- shellac_webview-1.0.1.dist-info/top_level.txt +1 -0
shellac/__init__.py
ADDED
shellac/enums.py
ADDED
shellac/launcher.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import shutil
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from selenium import webdriver
|
|
8
|
+
from selenium.webdriver.chrome.service import Service as ChromeService
|
|
9
|
+
from selenium.webdriver.firefox.service import Service as FirefoxService
|
|
10
|
+
from selenium.webdriver.edge.service import Service as EdgeService
|
|
11
|
+
|
|
12
|
+
from .enums import Browser
|
|
13
|
+
from .models import WindowConfig
|
|
14
|
+
|
|
15
|
+
class BrowserLauncher:
|
|
16
|
+
@staticmethod
|
|
17
|
+
def get_path(browser: Browser) -> Optional[str]:
|
|
18
|
+
system = platform.system()
|
|
19
|
+
if system == "Windows":
|
|
20
|
+
import winreg
|
|
21
|
+
reg_paths = {
|
|
22
|
+
Browser.Chrome: r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe",
|
|
23
|
+
Browser.Edge: r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe",
|
|
24
|
+
Browser.Firefox: r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\firefox.exe",
|
|
25
|
+
}
|
|
26
|
+
if browser in reg_paths:
|
|
27
|
+
for hive in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]:
|
|
28
|
+
try:
|
|
29
|
+
with winreg.OpenKey(hive, reg_paths[browser]) as key:
|
|
30
|
+
return winreg.QueryValue(key, None)
|
|
31
|
+
except FileNotFoundError: continue
|
|
32
|
+
|
|
33
|
+
names = {Browser.Chrome: "google-chrome", Browser.Firefox: "firefox", Browser.Edge: "microsoft-edge"}
|
|
34
|
+
suffix = ".exe" if system == "Windows" else ""
|
|
35
|
+
return shutil.which(names.get(browser, "") + suffix) or shutil.which(browser.name.lower() + suffix)
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def prepare_firefox_profile(config: WindowConfig) -> str:
|
|
39
|
+
temp_dir = tempfile.mkdtemp(prefix="webui_ff_")
|
|
40
|
+
chrome_dir = Path(temp_dir) / "chrome"
|
|
41
|
+
chrome_dir.mkdir()
|
|
42
|
+
|
|
43
|
+
prefs = {
|
|
44
|
+
"toolkit.legacyUserProfileCustomizations.stylesheets": "true",
|
|
45
|
+
"browser.tabs.inTitlebar": "0",
|
|
46
|
+
"browser.shell.checkDefaultBrowser": "false",
|
|
47
|
+
"browser.startup.page": "0",
|
|
48
|
+
"browser.tabs.warnOnClose": "false",
|
|
49
|
+
"security.csp.enable": "false",
|
|
50
|
+
"security.mixed_content.block_active_content": "false",
|
|
51
|
+
"network.websocket.allowInsecureFromHttp": "true",
|
|
52
|
+
"devtools.chrome.enabled": "true"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
with open(Path(temp_dir) / "prefs.js", "w") as f:
|
|
56
|
+
for k, v in prefs.items():
|
|
57
|
+
f.write(f'user_pref("{k}", {v});\n')
|
|
58
|
+
|
|
59
|
+
if config.hide_controls:
|
|
60
|
+
with open(chrome_dir / "userChrome.css", "w") as f:
|
|
61
|
+
f.write("""
|
|
62
|
+
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
|
63
|
+
#nav-bar, #TabsToolbar, #PersonalToolbar, #sidebar-box, #urlbar-container {
|
|
64
|
+
visibility: collapse !important;
|
|
65
|
+
}
|
|
66
|
+
""")
|
|
67
|
+
return temp_dir
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def create_driver(cls, browser: Browser, url: str, config: WindowConfig) -> webdriver.Remote:
|
|
71
|
+
path = cls.get_path(browser)
|
|
72
|
+
|
|
73
|
+
if browser in [Browser.Chrome, Browser.Edge, Browser.Chromium, Browser.Brave, Browser.Vivaldi]:
|
|
74
|
+
is_edge = browser == Browser.Edge
|
|
75
|
+
options = webdriver.EdgeOptions() if is_edge else webdriver.ChromeOptions()
|
|
76
|
+
if path: options.binary_location = path
|
|
77
|
+
|
|
78
|
+
if config.hide_controls:
|
|
79
|
+
options.add_argument(f"--app={url}")
|
|
80
|
+
|
|
81
|
+
options.add_argument("--disable-web-security")
|
|
82
|
+
options.add_argument("--allow-running-insecure-content")
|
|
83
|
+
options.add_argument("--disable-site-isolation-trials")
|
|
84
|
+
options.add_argument(f"--user-data-dir={tempfile.mkdtemp()}")
|
|
85
|
+
|
|
86
|
+
options.add_argument(f"--window-size={config.width},{config.height}")
|
|
87
|
+
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
|
88
|
+
|
|
89
|
+
return (webdriver.Edge(options=options) if is_edge else webdriver.Chrome(options=options))
|
|
90
|
+
|
|
91
|
+
elif browser == Browser.Firefox:
|
|
92
|
+
profile_path = cls.prepare_firefox_profile(config)
|
|
93
|
+
|
|
94
|
+
options = webdriver.FirefoxOptions()
|
|
95
|
+
if path: options.binary_location = path
|
|
96
|
+
|
|
97
|
+
options.add_argument("-profile")
|
|
98
|
+
options.add_argument(profile_path)
|
|
99
|
+
|
|
100
|
+
if config.kiosk:
|
|
101
|
+
options.add_argument("--kiosk")
|
|
102
|
+
|
|
103
|
+
options.set_preference("security.csp.enable", False)
|
|
104
|
+
|
|
105
|
+
service = FirefoxService()
|
|
106
|
+
driver = webdriver.Firefox(service=service, options=options)
|
|
107
|
+
driver.set_window_size(config.width, config.height)
|
|
108
|
+
return driver
|
|
109
|
+
|
|
110
|
+
raise ValueError(f"Unsupported browser: {browser}")
|
shellac/models.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any, List
|
|
2
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
3
|
+
|
|
4
|
+
class WindowConfig(BaseModel):
|
|
5
|
+
width: int = 1000
|
|
6
|
+
height: int = 800
|
|
7
|
+
hide_controls: bool = True
|
|
8
|
+
kiosk: bool = False
|
|
9
|
+
|
|
10
|
+
class Event(BaseModel):
|
|
11
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
12
|
+
window: Any
|
|
13
|
+
element: str = ""
|
|
14
|
+
data: List[Any] = Field(default_factory=list)
|
|
15
|
+
|
|
16
|
+
def get_string(self, index: int = 0) -> str:
|
|
17
|
+
return str(self.data[index]) if index < len(self.data) else ""
|
|
18
|
+
|
|
19
|
+
def get_int(self, index: int = 0) -> int:
|
|
20
|
+
try: return int(self.data[index])
|
|
21
|
+
except (IndexError, ValueError): return 0
|
|
22
|
+
|
|
23
|
+
def get_dict(self, index: int = 0) -> dict:
|
|
24
|
+
try: return dict(self.data[index])
|
|
25
|
+
except (IndexError, ValueError, TypeError): return {}
|
shellac/window.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import socket
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Callable, Dict, Optional, Union
|
|
8
|
+
|
|
9
|
+
import uvicorn
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
from fastapi.responses import HTMLResponse
|
|
12
|
+
from selenium import webdriver
|
|
13
|
+
from selenium.common.exceptions import WebDriverException, NoSuchWindowException
|
|
14
|
+
|
|
15
|
+
# Note: These imports assume the structure of your package
|
|
16
|
+
from .enums import Browser
|
|
17
|
+
from .models import WindowConfig, Event
|
|
18
|
+
from .launcher import BrowserLauncher
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Window:
|
|
22
|
+
"""
|
|
23
|
+
Represents a WebUI window instance that manages a FastAPI backend
|
|
24
|
+
and a Selenium-controlled browser frontend.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
"""Initializes a new Window instance with default configurations."""
|
|
29
|
+
self.config = WindowConfig()
|
|
30
|
+
self.port = self._get_free_port()
|
|
31
|
+
self.app = FastAPI()
|
|
32
|
+
self.bindings: Dict[str, Callable] = {}
|
|
33
|
+
self._html_content: Optional[str] = None
|
|
34
|
+
self.driver: Optional[webdriver.Remote] = None
|
|
35
|
+
self._running = False
|
|
36
|
+
self._setup_routes()
|
|
37
|
+
|
|
38
|
+
def _get_free_port(self) -> int:
|
|
39
|
+
"""Finds an available TCP port on the localhost."""
|
|
40
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
41
|
+
s.bind(('', 0))
|
|
42
|
+
return s.getsockname()[1]
|
|
43
|
+
|
|
44
|
+
def _get_bridge_js(self) -> str:
|
|
45
|
+
"""Returns the JavaScript bridge code for Python-JS communication."""
|
|
46
|
+
return """
|
|
47
|
+
if (typeof window.webui === 'undefined') {
|
|
48
|
+
window._webui_queue = [];
|
|
49
|
+
window._webui_promises = {};
|
|
50
|
+
window._webui_call_id = 0;
|
|
51
|
+
window.webui = {
|
|
52
|
+
call: (fn, ...args) => {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const id = window._webui_call_id++;
|
|
55
|
+
window._webui_promises[id] = resolve;
|
|
56
|
+
window._webui_queue.push({fn: fn, args: args, id: id});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
window._webui_resolve = (id, result) => {
|
|
61
|
+
if (window._webui_promises[id]) {
|
|
62
|
+
window._webui_promises[id](result);
|
|
63
|
+
delete window._webui_promises[id];
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def _setup_routes(self):
|
|
70
|
+
"""Configures the internal FastAPI routes."""
|
|
71
|
+
@self.app.get("/")
|
|
72
|
+
async def index():
|
|
73
|
+
content = self._html_content or "<html><body>No Content</body></html>"
|
|
74
|
+
bridge = f"<script>{self._get_bridge_js()}</script>"
|
|
75
|
+
if "<head>" in content:
|
|
76
|
+
return HTMLResponse(content.replace("<head>", f"<head>{bridge}"))
|
|
77
|
+
return HTMLResponse(bridge + content)
|
|
78
|
+
|
|
79
|
+
def _bind_target(self, target: Any, prefix: str = "", exact_name: bool = False):
|
|
80
|
+
"""Internal helper to map functions or class methods to the registry."""
|
|
81
|
+
if inspect.isfunction(target) or inspect.ismethod(target):
|
|
82
|
+
name = prefix if (exact_name and prefix) else (f"{prefix}.{target.__name__}" if prefix else target.__name__)
|
|
83
|
+
self.bindings[name] = target
|
|
84
|
+
else:
|
|
85
|
+
if inspect.isclass(target):
|
|
86
|
+
try:
|
|
87
|
+
obj = target()
|
|
88
|
+
except TypeError as e:
|
|
89
|
+
raise ValueError(f"Cannot auto-instantiate class '{target.__name__}'.") from e
|
|
90
|
+
else:
|
|
91
|
+
obj = target
|
|
92
|
+
|
|
93
|
+
for name in dir(obj):
|
|
94
|
+
if not name.startswith('_'):
|
|
95
|
+
attr = getattr(obj, name)
|
|
96
|
+
if callable(attr):
|
|
97
|
+
bind_name = f"{prefix}.{name}" if prefix else name
|
|
98
|
+
self.bindings[bind_name] = attr
|
|
99
|
+
|
|
100
|
+
def bind(self, name_or_target: Union[str, Any] = None, target: Optional[Any] = None):
|
|
101
|
+
"""
|
|
102
|
+
Binds a Python function, class, or instance to be callable from JavaScript.
|
|
103
|
+
|
|
104
|
+
This method can be used as a decorator or as a direct function call.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
name_or_target (Union[str, Any], optional): The name/prefix for the binding
|
|
108
|
+
or the target itself if no name is provided.
|
|
109
|
+
target (Any, optional): The function or class instance to bind (only used
|
|
110
|
+
if name_or_target is a string).
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
The original target or a decorator function.
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
>>> win.bind("say_hello", lambda e: "Hello!")
|
|
117
|
+
>>> @win.bind("math")
|
|
118
|
+
... class Math:
|
|
119
|
+
... def add(self, e): return e.data[0] + e.data[1]
|
|
120
|
+
"""
|
|
121
|
+
if isinstance(name_or_target, str) and target is not None:
|
|
122
|
+
is_func = inspect.isfunction(target) or inspect.ismethod(target)
|
|
123
|
+
self._bind_target(target, prefix=name_or_target, exact_name=is_func)
|
|
124
|
+
return target
|
|
125
|
+
|
|
126
|
+
if isinstance(name_or_target, str) and target is None:
|
|
127
|
+
def decorator(decor_target: Any):
|
|
128
|
+
is_func = inspect.isfunction(decor_target) or inspect.ismethod(decor_target)
|
|
129
|
+
self._bind_target(decor_target, prefix=name_or_target, exact_name=is_func)
|
|
130
|
+
return decor_target
|
|
131
|
+
return decorator
|
|
132
|
+
|
|
133
|
+
if name_or_target is not None:
|
|
134
|
+
self._bind_target(name_or_target, exact_name=False)
|
|
135
|
+
return name_or_target
|
|
136
|
+
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def navigate(self, url_or_html: str):
|
|
140
|
+
"""
|
|
141
|
+
Navigates the current window to a new URL, a local file, or raw HTML content.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
url_or_html (str): A web URL (http://...), a path to a .html file,
|
|
145
|
+
or a raw HTML string.
|
|
146
|
+
"""
|
|
147
|
+
if not self.driver:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
is_url = url_or_html.startswith(('http://', 'https://', 'file://'))
|
|
151
|
+
if is_url:
|
|
152
|
+
self.driver.get(url_or_html)
|
|
153
|
+
else:
|
|
154
|
+
try:
|
|
155
|
+
if len(url_or_html) < 1000 and os.path.exists(url_or_html):
|
|
156
|
+
with open(url_or_html, 'r', encoding='utf-8') as f:
|
|
157
|
+
self._html_content = f.read()
|
|
158
|
+
else:
|
|
159
|
+
self._html_content = url_or_html
|
|
160
|
+
except OSError:
|
|
161
|
+
self._html_content = url_or_html
|
|
162
|
+
|
|
163
|
+
self.driver.get(f"http://127.0.0.1:{self.port}/")
|
|
164
|
+
|
|
165
|
+
def run_js(self, script: str) -> Any:
|
|
166
|
+
"""
|
|
167
|
+
Executes synchronous JavaScript code in the browser.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
script (str): The JavaScript code to execute.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Any: The result returned by the JavaScript execution.
|
|
174
|
+
"""
|
|
175
|
+
if self.driver:
|
|
176
|
+
try: return self.driver.execute_script(script)
|
|
177
|
+
except Exception as e: print(f"[WebUI] JS Execution Error: {e}")
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
def close(self):
|
|
181
|
+
"""Closes the browser window and shuts down the backend server."""
|
|
182
|
+
self._running = False
|
|
183
|
+
if self.driver:
|
|
184
|
+
try: self.driver.quit()
|
|
185
|
+
except Exception: pass
|
|
186
|
+
self.driver = None
|
|
187
|
+
|
|
188
|
+
def set_size(self, width: int, height: int):
|
|
189
|
+
"""
|
|
190
|
+
Resizes the browser window.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
width (int): Target width in pixels.
|
|
194
|
+
height (int): Target height in pixels.
|
|
195
|
+
"""
|
|
196
|
+
self.config.width = width
|
|
197
|
+
self.config.height = height
|
|
198
|
+
if self.driver: self.driver.set_window_size(width, height)
|
|
199
|
+
|
|
200
|
+
def set_title(self, title: str):
|
|
201
|
+
"""
|
|
202
|
+
Updates the browser window title.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
title (str): The new title string.
|
|
206
|
+
"""
|
|
207
|
+
self.run_js(f"document.title = {json.dumps(title)};")
|
|
208
|
+
|
|
209
|
+
def is_running(self) -> bool:
|
|
210
|
+
"""
|
|
211
|
+
Checks if the window and backend are currently running.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
bool: True if running, False otherwise.
|
|
215
|
+
"""
|
|
216
|
+
return self._running
|
|
217
|
+
|
|
218
|
+
def _bridge_monitor(self):
|
|
219
|
+
"""Internal background thread that polls the JS bridge for incoming calls."""
|
|
220
|
+
while self._running:
|
|
221
|
+
if self.driver:
|
|
222
|
+
try:
|
|
223
|
+
exists = self.driver.execute_script("return (typeof window.webui !== 'undefined' && typeof window._webui_resolve !== 'undefined');")
|
|
224
|
+
if not exists:
|
|
225
|
+
self.driver.execute_script(self._get_bridge_js())
|
|
226
|
+
|
|
227
|
+
events = self.driver.execute_script("""
|
|
228
|
+
var e = window._webui_queue;
|
|
229
|
+
window._webui_queue = [];
|
|
230
|
+
return e;
|
|
231
|
+
""")
|
|
232
|
+
|
|
233
|
+
for event_data in (events or []):
|
|
234
|
+
fn = event_data.get('fn')
|
|
235
|
+
call_id = event_data.get('id')
|
|
236
|
+
if fn in self.bindings:
|
|
237
|
+
event = Event(window=self, element=fn, data=event_data.get('args', []))
|
|
238
|
+
cb = self.bindings[fn]
|
|
239
|
+
result = cb(event)
|
|
240
|
+
result_json = json.dumps(result)
|
|
241
|
+
self.driver.execute_script(f"window._webui_resolve({call_id}, {result_json});")
|
|
242
|
+
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
time.sleep(0.05)
|
|
246
|
+
|
|
247
|
+
def show(self, content: str, browser: Browser = Browser.AnyBrowser):
|
|
248
|
+
"""
|
|
249
|
+
Starts the backend server and launches the browser window.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
content (str): The URL, HTML file path, or HTML string to display.
|
|
253
|
+
browser (Browser): The browser engine to use (defaults to AnyBrowser).
|
|
254
|
+
"""
|
|
255
|
+
self._running = True
|
|
256
|
+
is_url = content.startswith(('http://', 'https://', 'file://'))
|
|
257
|
+
if not is_url:
|
|
258
|
+
try:
|
|
259
|
+
if len(content) < 1000 and os.path.exists(content):
|
|
260
|
+
with open(content, 'r', encoding='utf-8') as f: self._html_content = f.read()
|
|
261
|
+
else: self._html_content = content
|
|
262
|
+
except OSError:
|
|
263
|
+
self._html_content = content
|
|
264
|
+
url = f"http://127.0.0.1:{self.port}/"
|
|
265
|
+
else:
|
|
266
|
+
url = content
|
|
267
|
+
|
|
268
|
+
threading.Thread(target=uvicorn.run, args=(self.app,),
|
|
269
|
+
kwargs={"host": "127.0.0.1", "port": self.port, "log_level": "error"},
|
|
270
|
+
daemon=True).start()
|
|
271
|
+
|
|
272
|
+
target = browser
|
|
273
|
+
if target == Browser.AnyBrowser:
|
|
274
|
+
for b in [Browser.Chrome, Browser.Edge, Browser.Firefox]:
|
|
275
|
+
if BrowserLauncher.get_path(b):
|
|
276
|
+
target = b
|
|
277
|
+
break
|
|
278
|
+
|
|
279
|
+
self.driver = BrowserLauncher.create_driver(target, url, self.config)
|
|
280
|
+
self.driver.get(url)
|
|
281
|
+
threading.Thread(target=self._bridge_monitor, daemon=True).start()
|
|
282
|
+
|
|
283
|
+
def wait(self):
|
|
284
|
+
"""
|
|
285
|
+
Blocks the main thread until the browser window is closed.
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
while self._running:
|
|
289
|
+
if self.driver:
|
|
290
|
+
try:
|
|
291
|
+
_ = self.driver.window_handles
|
|
292
|
+
except (NoSuchWindowException, WebDriverException):
|
|
293
|
+
break
|
|
294
|
+
time.sleep(1)
|
|
295
|
+
except KeyboardInterrupt: pass
|
|
296
|
+
finally:
|
|
297
|
+
self.close()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shellac-webview
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Author-email: Sharkow1743 <Sharkow1743@users.noreply.github.com>
|
|
5
|
+
Project-URL: Homepage, https://github.com/Sharkow1743/shellac-webview
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/Sharkow1743/shellac-webview/issues
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: fastapi
|
|
10
|
+
Requires-Dist: uvicorn
|
|
11
|
+
Requires-Dist: pydantic
|
|
12
|
+
Requires-Dist: selenium
|
|
13
|
+
|
|
14
|
+
# Shellac Webview
|
|
15
|
+
|
|
16
|
+
A Selenium wrapper for building desktop applications using web technologies. It uses FastAPI for backend communication and Selenium for browser automation.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install shellac-webview
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Basic Usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from shellac import Window
|
|
28
|
+
|
|
29
|
+
win = Window()
|
|
30
|
+
|
|
31
|
+
@win.bind
|
|
32
|
+
def my_function(event):
|
|
33
|
+
return "Data from Python"
|
|
34
|
+
|
|
35
|
+
html = """
|
|
36
|
+
<button onclick="run()">Run</button>
|
|
37
|
+
<script>
|
|
38
|
+
async function run() {
|
|
39
|
+
const result = await webui.call("my_function");
|
|
40
|
+
console.log(result);
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
win.show(html)
|
|
46
|
+
win.wait()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Binding
|
|
50
|
+
|
|
51
|
+
You can bind functions in python to be called from js using `webui.call("func_name")`
|
|
52
|
+
|
|
53
|
+
### Function Binding
|
|
54
|
+
You can bind a specific function to a name.
|
|
55
|
+
```python
|
|
56
|
+
def my_function(event):
|
|
57
|
+
return f"Hello, {event.data[0]}!"
|
|
58
|
+
|
|
59
|
+
win.bind("greet", my_function)
|
|
60
|
+
```
|
|
61
|
+
**JS Usage:**
|
|
62
|
+
```javascript
|
|
63
|
+
const response = await webui.call("greet", "Alice");
|
|
64
|
+
console.log(response); // "Hello, Alice!"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Decorator Binding
|
|
68
|
+
Use the `@bind` decorator for a cleaner syntax.
|
|
69
|
+
```python
|
|
70
|
+
@win.bind
|
|
71
|
+
def calculate(event):
|
|
72
|
+
return event.data[0] * 2
|
|
73
|
+
```
|
|
74
|
+
**JS Usage:** `webui.call("calculate", 21);`
|
|
75
|
+
|
|
76
|
+
### Class/Namespace Binding
|
|
77
|
+
You can bind an entire class or instance. All public methods (not starting with `_`) will be available in JavaScript.
|
|
78
|
+
```python
|
|
79
|
+
@win.bind("db")
|
|
80
|
+
class Database:
|
|
81
|
+
def get_user(self, event):
|
|
82
|
+
return {"id": event.data[0], "name": "John Doe"}
|
|
83
|
+
```
|
|
84
|
+
or with
|
|
85
|
+
```python
|
|
86
|
+
win.bind("db", Database())
|
|
87
|
+
```
|
|
88
|
+
**JS Usage:**
|
|
89
|
+
```javascript
|
|
90
|
+
const user = await webui.call("db.get_user", 1);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Class Mapping
|
|
94
|
+
If you bind a class without a prefix, its methods are mapped to the top-level.
|
|
95
|
+
```python
|
|
96
|
+
class API:
|
|
97
|
+
def status(self, event):
|
|
98
|
+
return "Online"
|
|
99
|
+
|
|
100
|
+
win.bind(API)
|
|
101
|
+
```
|
|
102
|
+
**JS Usage:** `webui.call("status");`
|
|
103
|
+
|
|
104
|
+
### The `Event` Object
|
|
105
|
+
Every bound Python function receives an `Event` object as its first argument.
|
|
106
|
+
- `event.window`: The `Window` instance.
|
|
107
|
+
- `event.data`: A list of arguments passed from JavaScript.
|
|
108
|
+
- `event.element`: The name of the function called.
|
|
109
|
+
|
|
110
|
+
## Window Configuration
|
|
111
|
+
|
|
112
|
+
You can configure the window size and behavior before calling `show()`.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
win = Window()
|
|
116
|
+
win.config.width = 1200
|
|
117
|
+
win.config.height = 900
|
|
118
|
+
win.config.hide_controls = True # Hides address bar/tabs (App Mode)
|
|
119
|
+
win.config.kiosk = False # Fullscreen mode
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Browser Selection
|
|
123
|
+
|
|
124
|
+
By default, the library looks for Chrome, Edge, or Firefox. You can force a specific browser:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from webui import Browser
|
|
128
|
+
|
|
129
|
+
win.show("index.html", browser=Browser.Firefox)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Available options: `Chrome`, `Firefox`, `Edge`, `Chromium`, `Brave`, `Vivaldi`.
|
|
133
|
+
|
|
134
|
+
## Other functions
|
|
135
|
+
|
|
136
|
+
Once the window is running, you can navigate to new pages or execute JavaScript from Python.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# Navigate to a new URL
|
|
140
|
+
win.navigate("https://google.com")
|
|
141
|
+
|
|
142
|
+
# Execute JS directly
|
|
143
|
+
win.run_js("alert('Hello from Python!')")
|
|
144
|
+
|
|
145
|
+
# Change title
|
|
146
|
+
win.set_title("New Title")
|
|
147
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
shellac/__init__.py,sha256=T_0tUvsKypt1skVvX3Nu17VTqSjAKdZZdVOdhhsOR4g,151
|
|
2
|
+
shellac/enums.py,sha256=ak5X3bh5N3T0tF2uUFwJN7PZ7aRkjGPcXM_0M2_Xg44,177
|
|
3
|
+
shellac/launcher.py,sha256=p_bktZ8b9bbNxq4kcM_HGkTgSLwj4DPyWjjdS8itSr0,4747
|
|
4
|
+
shellac/models.py,sha256=LVPbDokc1VHphN47WXFQsErSsji6ZYaXZdaTPwQkWkY,815
|
|
5
|
+
shellac/window.py,sha256=KJ8m-GLYR9A2wwbY-Xkt1Ne89oQKdCBsBO7NdwrZuuc,11264
|
|
6
|
+
shellac_webview-1.0.1.dist-info/METADATA,sha256=904PPg72YZEjEWhQRSwApJYq60f4J7pPVwcbVj01iXg,3385
|
|
7
|
+
shellac_webview-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
shellac_webview-1.0.1.dist-info/top_level.txt,sha256=XufVWfhTQ-kO571Rqhkmxx5nMdJnCPuU_UiJLHVF2DU,8
|
|
9
|
+
shellac_webview-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shellac
|