aegis-framework 0.1.0

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.
@@ -0,0 +1,603 @@
1
+ """
2
+ Aegis Window Manager
3
+ Manages WebKit2GTK windows with full customization support
4
+ """
5
+
6
+ import gi
7
+ gi.require_version('Gtk', '3.0')
8
+ gi.require_version('WebKit2', '4.1')
9
+
10
+ from gi.repository import Gtk, WebKit2, Gdk, GLib
11
+ import json
12
+ import os
13
+
14
+
15
+ class AegisWindow(Gtk.Window):
16
+ """
17
+ A WebKit2GTK-based window with Aegis bridge integration
18
+ """
19
+
20
+ def __init__(self, config=None):
21
+ super().__init__()
22
+
23
+ self.config = config or {}
24
+ self._setup_window()
25
+ self._setup_webview()
26
+ self._setup_bridge()
27
+
28
+ def _setup_window(self):
29
+ """Configure the main window"""
30
+ # Window properties from config
31
+ title = self.config.get('title', 'Aegis App')
32
+ width = self.config.get('width', 1200)
33
+ height = self.config.get('height', 800)
34
+ resizable = self.config.get('resizable', True)
35
+ decorated = self.config.get('frame', True)
36
+
37
+ self.set_title(title)
38
+ self.set_default_size(width, height)
39
+ self.set_resizable(resizable)
40
+ self.set_decorated(decorated)
41
+ self.set_position(Gtk.WindowPosition.CENTER)
42
+
43
+ # Handle close
44
+ self.connect('destroy', Gtk.main_quit)
45
+
46
+ # Frameless window support
47
+ if not decorated:
48
+ self._setup_frameless()
49
+
50
+ def _setup_frameless(self):
51
+ """Setup frameless window with drag support"""
52
+ self.set_app_paintable(True)
53
+
54
+ # Enable dragging from anywhere
55
+ self.drag_start_x = 0
56
+ self.drag_start_y = 0
57
+
58
+ def _setup_webview(self):
59
+ """Setup WebKit2GTK webview"""
60
+ # Create webview with settings
61
+ self.webview = WebKit2.WebView()
62
+ settings = self.webview.get_settings()
63
+
64
+ # Enable developer tools
65
+ settings.set_enable_developer_extras(True)
66
+ settings.set_enable_javascript(True)
67
+ settings.set_javascript_can_access_clipboard(True)
68
+ settings.set_enable_write_console_messages_to_stdout(True)
69
+
70
+ # Allow file access
71
+ settings.set_allow_file_access_from_file_urls(True)
72
+ settings.set_allow_universal_access_from_file_urls(True)
73
+
74
+ # Hardware acceleration - disabled by default for compatibility
75
+ # nouveau and some other drivers have issues with GPU acceleration
76
+ settings.set_hardware_acceleration_policy(
77
+ WebKit2.HardwareAccelerationPolicy.NEVER
78
+ )
79
+
80
+ # Disable context menu if configured
81
+ if not self.config.get('contextMenu', True):
82
+ self.webview.connect('context-menu', lambda *args: True)
83
+
84
+ # Add to window
85
+ self.add(self.webview)
86
+
87
+ def _setup_bridge(self):
88
+ """Setup JavaScript bridge for IPC"""
89
+ # Get user content manager
90
+ self.content_manager = self.webview.get_user_content_manager()
91
+
92
+ # Register message handler for Aegis calls
93
+ self.content_manager.register_script_message_handler('aegis')
94
+ self.content_manager.connect(
95
+ 'script-message-received::aegis',
96
+ self._on_message_received
97
+ )
98
+
99
+ # Pending callbacks for async responses
100
+ self._pending_callbacks = {}
101
+ self._callback_id = 0
102
+
103
+ def _on_message_received(self, content_manager, js_result):
104
+ """Handle messages from JavaScript"""
105
+ callback_id = None
106
+ try:
107
+ # WebKit2 4.1 uses get_js_value()
108
+ js_value = js_result.get_js_value()
109
+ data = json.loads(js_value.to_string())
110
+
111
+ action = data.get('action')
112
+ payload = data.get('payload', {})
113
+ callback_id = data.get('callbackId')
114
+
115
+ print(f"[Aegis] Action: {action}, Payload: {payload}")
116
+
117
+ # Process action and get result
118
+ result = self._process_action(action, payload)
119
+
120
+ # Send response back to JavaScript
121
+ if callback_id:
122
+ self._send_response(callback_id, result)
123
+
124
+ except Exception as e:
125
+ print(f"[Aegis Bridge] Error: {e}")
126
+ import traceback
127
+ traceback.print_exc()
128
+ if callback_id:
129
+ self._send_error(callback_id, str(e))
130
+
131
+ def _process_action(self, action, payload):
132
+ """Process an Aegis action and return result"""
133
+ handlers = {
134
+ 'read': self._handle_read,
135
+ 'write': self._handle_write,
136
+ 'run': self._handle_run,
137
+ 'exists': self._handle_exists,
138
+ 'mkdir': self._handle_mkdir,
139
+ 'remove': self._handle_remove,
140
+ 'copy': self._handle_copy,
141
+ 'move': self._handle_move,
142
+ 'dialog.open': self._handle_dialog_open,
143
+ 'dialog.save': self._handle_dialog_save,
144
+ 'dialog.message': self._handle_dialog_message,
145
+ 'app.quit': self._handle_app_quit,
146
+ 'app.minimize': self._handle_app_minimize,
147
+ 'app.maximize': self._handle_app_maximize,
148
+ 'app.getPath': self._handle_app_get_path,
149
+ 'window.startDrag': self._handle_window_start_drag,
150
+ 'window.resize': self._handle_window_resize,
151
+ 'window.setSize': self._handle_window_set_size,
152
+ 'window.getSize': self._handle_window_get_size,
153
+ 'window.setPosition': self._handle_window_set_position,
154
+ 'window.getPosition': self._handle_window_get_position,
155
+ }
156
+
157
+ handler = handlers.get(action)
158
+ if handler:
159
+ return handler(payload)
160
+ else:
161
+ raise ValueError(f"Unknown action: {action}")
162
+
163
+ def _send_response(self, callback_id, result):
164
+ """Send successful response to JavaScript"""
165
+ response = json.dumps({
166
+ 'callbackId': callback_id,
167
+ 'success': True,
168
+ 'data': result
169
+ })
170
+ script = f"window.__aegisResolve({response})"
171
+ self.webview.evaluate_javascript(script, -1, None, None, None, None, None)
172
+
173
+ def _send_error(self, callback_id, error):
174
+ """Send error response to JavaScript"""
175
+ response = json.dumps({
176
+ 'callbackId': callback_id,
177
+ 'success': False,
178
+ 'error': error
179
+ })
180
+ script = f"window.__aegisResolve({response})"
181
+ self.webview.evaluate_javascript(script, -1, None, None, None, None, None)
182
+
183
+ # ==================== Action Handlers ====================
184
+
185
+ def _handle_read(self, payload):
186
+ """Read file or directory contents"""
187
+ path = payload.get('path', '.')
188
+ file = payload.get('file')
189
+
190
+ if file:
191
+ # Read single file
192
+ full_path = os.path.join(path, file)
193
+ with open(full_path, 'r', encoding='utf-8') as f:
194
+ return {'content': f.read(), 'path': full_path}
195
+ else:
196
+ # List directory
197
+ entries = []
198
+ for entry in os.listdir(path):
199
+ full_path = os.path.join(path, entry)
200
+ try:
201
+ stat = os.stat(full_path)
202
+ entries.append({
203
+ 'name': entry,
204
+ 'isDirectory': os.path.isdir(full_path),
205
+ 'isFile': os.path.isfile(full_path),
206
+ 'size': stat.st_size if os.path.isfile(full_path) else 0,
207
+ 'modified': stat.st_mtime
208
+ })
209
+ except (PermissionError, OSError):
210
+ # Skip files we can't access
211
+ entries.append({
212
+ 'name': entry,
213
+ 'isDirectory': False,
214
+ 'isFile': True,
215
+ 'size': 0,
216
+ 'modified': 0
217
+ })
218
+ return {'entries': entries, 'path': path}
219
+
220
+ def _handle_write(self, payload):
221
+ """Write content to file"""
222
+ path = payload.get('path', '.')
223
+ file = payload.get('file')
224
+ content = payload.get('content', '')
225
+
226
+ full_path = os.path.join(path, file)
227
+
228
+ # Create directories if needed
229
+ os.makedirs(os.path.dirname(full_path) or '.', exist_ok=True)
230
+
231
+ with open(full_path, 'w', encoding='utf-8') as f:
232
+ f.write(content)
233
+
234
+ return {'success': True, 'path': full_path}
235
+
236
+ def _handle_run(self, payload):
237
+ """Execute Python or shell commands"""
238
+ import subprocess
239
+
240
+ if 'py' in payload:
241
+ # Execute Python code
242
+ try:
243
+ result = eval(payload['py'])
244
+ return {'output': str(result), 'exitCode': 0}
245
+ except:
246
+ exec_globals = {}
247
+ exec(payload['py'], exec_globals)
248
+ return {'output': '', 'exitCode': 0}
249
+
250
+ elif 'sh' in payload:
251
+ # Execute shell command
252
+ result = subprocess.run(
253
+ payload['sh'],
254
+ shell=True,
255
+ capture_output=True,
256
+ text=True
257
+ )
258
+ return {
259
+ 'output': result.stdout,
260
+ 'error': result.stderr,
261
+ 'exitCode': result.returncode
262
+ }
263
+
264
+ return {'error': 'No command specified'}
265
+
266
+ def _handle_exists(self, payload):
267
+ """Check if path exists"""
268
+ path = payload.get('path')
269
+ return {
270
+ 'exists': os.path.exists(path),
271
+ 'isFile': os.path.isfile(path),
272
+ 'isDirectory': os.path.isdir(path)
273
+ }
274
+
275
+ def _handle_mkdir(self, payload):
276
+ """Create directory"""
277
+ path = payload.get('path')
278
+ recursive = payload.get('recursive', True)
279
+
280
+ if recursive:
281
+ os.makedirs(path, exist_ok=True)
282
+ else:
283
+ os.mkdir(path)
284
+
285
+ return {'success': True, 'path': path}
286
+
287
+ def _handle_remove(self, payload):
288
+ """Remove file or directory"""
289
+ import shutil
290
+ path = payload.get('path')
291
+ recursive = payload.get('recursive', False)
292
+
293
+ if os.path.isdir(path):
294
+ if recursive:
295
+ shutil.rmtree(path)
296
+ else:
297
+ os.rmdir(path)
298
+ else:
299
+ os.remove(path)
300
+
301
+ return {'success': True}
302
+
303
+ def _handle_copy(self, payload):
304
+ """Copy file or directory"""
305
+ import shutil
306
+ src = payload.get('src')
307
+ dest = payload.get('dest')
308
+
309
+ if os.path.isdir(src):
310
+ shutil.copytree(src, dest)
311
+ else:
312
+ shutil.copy2(src, dest)
313
+
314
+ return {'success': True, 'dest': dest}
315
+
316
+ def _handle_move(self, payload):
317
+ """Move/rename file or directory"""
318
+ import shutil
319
+ src = payload.get('src')
320
+ dest = payload.get('dest')
321
+
322
+ shutil.move(src, dest)
323
+ return {'success': True, 'dest': dest}
324
+
325
+ def _handle_dialog_open(self, payload):
326
+ """Open file dialog"""
327
+ dialog = Gtk.FileChooserDialog(
328
+ title=payload.get('title', 'Open File'),
329
+ parent=self,
330
+ action=Gtk.FileChooserAction.OPEN
331
+ )
332
+ dialog.add_buttons(
333
+ Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
334
+ Gtk.STOCK_OPEN, Gtk.ResponseType.OK
335
+ )
336
+
337
+ # Add filters
338
+ filters = payload.get('filters', [])
339
+ for f in filters:
340
+ file_filter = Gtk.FileFilter()
341
+ file_filter.set_name(f.get('name', 'Files'))
342
+ for ext in f.get('extensions', ['*']):
343
+ file_filter.add_pattern(f'*.{ext}')
344
+ dialog.add_filter(file_filter)
345
+
346
+ response = dialog.run()
347
+ result = None
348
+
349
+ if response == Gtk.ResponseType.OK:
350
+ if payload.get('multiple'):
351
+ result = dialog.get_filenames()
352
+ else:
353
+ result = dialog.get_filename()
354
+
355
+ dialog.destroy()
356
+ return {'path': result}
357
+
358
+ def _handle_dialog_save(self, payload):
359
+ """Save file dialog"""
360
+ dialog = Gtk.FileChooserDialog(
361
+ title=payload.get('title', 'Save File'),
362
+ parent=self,
363
+ action=Gtk.FileChooserAction.SAVE
364
+ )
365
+ dialog.add_buttons(
366
+ Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
367
+ Gtk.STOCK_SAVE, Gtk.ResponseType.OK
368
+ )
369
+
370
+ dialog.set_do_overwrite_confirmation(True)
371
+
372
+ if payload.get('defaultName'):
373
+ dialog.set_current_name(payload['defaultName'])
374
+
375
+ response = dialog.run()
376
+ result = dialog.get_filename() if response == Gtk.ResponseType.OK else None
377
+ dialog.destroy()
378
+
379
+ return {'path': result}
380
+
381
+ def _handle_dialog_message(self, payload):
382
+ """Show message dialog"""
383
+ msg_type = {
384
+ 'info': Gtk.MessageType.INFO,
385
+ 'warning': Gtk.MessageType.WARNING,
386
+ 'error': Gtk.MessageType.ERROR,
387
+ 'question': Gtk.MessageType.QUESTION
388
+ }.get(payload.get('type', 'info'), Gtk.MessageType.INFO)
389
+
390
+ buttons = Gtk.ButtonsType.OK
391
+ if payload.get('buttons') == 'yesno':
392
+ buttons = Gtk.ButtonsType.YES_NO
393
+ elif payload.get('buttons') == 'okcancel':
394
+ buttons = Gtk.ButtonsType.OK_CANCEL
395
+
396
+ dialog = Gtk.MessageDialog(
397
+ parent=self,
398
+ flags=Gtk.DialogFlags.MODAL,
399
+ message_type=msg_type,
400
+ buttons=buttons,
401
+ text=payload.get('title', ''),
402
+ )
403
+ dialog.format_secondary_text(payload.get('message', ''))
404
+
405
+ response = dialog.run()
406
+ dialog.destroy()
407
+
408
+ return {'response': response == Gtk.ResponseType.OK or response == Gtk.ResponseType.YES}
409
+
410
+ def _handle_app_quit(self, payload):
411
+ """Quit the application"""
412
+ Gtk.main_quit()
413
+ return {'success': True}
414
+
415
+ def _handle_app_minimize(self, payload):
416
+ """Minimize the window"""
417
+ self.iconify()
418
+ return {'success': True}
419
+
420
+ def _handle_app_maximize(self, payload):
421
+ """Toggle maximize"""
422
+ if self.is_maximized():
423
+ self.unmaximize()
424
+ else:
425
+ self.maximize()
426
+ return {'success': True}
427
+
428
+ def _handle_app_get_path(self, payload):
429
+ """Get system paths with proper localization"""
430
+ import subprocess
431
+ name = payload.get('name', 'home')
432
+
433
+ # Use xdg-user-dir for localized paths
434
+ xdg_mapping = {
435
+ 'desktop': 'DESKTOP',
436
+ 'documents': 'DOCUMENTS',
437
+ 'downloads': 'DOWNLOAD',
438
+ 'music': 'MUSIC',
439
+ 'pictures': 'PICTURES',
440
+ 'videos': 'VIDEOS',
441
+ 'templates': 'TEMPLATES',
442
+ 'publicshare': 'PUBLICSHARE'
443
+ }
444
+
445
+ if name in xdg_mapping:
446
+ try:
447
+ result = subprocess.run(
448
+ ['xdg-user-dir', xdg_mapping[name]],
449
+ capture_output=True,
450
+ text=True
451
+ )
452
+ if result.returncode == 0 and result.stdout.strip():
453
+ return {'path': result.stdout.strip()}
454
+ except:
455
+ pass
456
+
457
+ # Fallback paths
458
+ home = os.path.expanduser('~')
459
+ paths = {
460
+ 'home': home,
461
+ 'temp': '/tmp',
462
+ 'app': os.getcwd(),
463
+ 'root': '/'
464
+ }
465
+
466
+ return {'path': paths.get(name, home)}
467
+
468
+ # ==================== Window Control Handlers ====================
469
+
470
+ def _handle_window_start_drag(self, payload):
471
+ """Start window drag operation - coordinates from JS"""
472
+ x = payload.get('x', 0)
473
+ y = payload.get('y', 0)
474
+ button = payload.get('button', 1)
475
+
476
+ # Use GLib.idle_add for thread safety
477
+ def do_move():
478
+ try:
479
+ self.begin_move_drag(
480
+ button,
481
+ int(x),
482
+ int(y),
483
+ Gdk.CURRENT_TIME
484
+ )
485
+ except Exception as e:
486
+ print(f"[Aegis] Move drag error: {e}")
487
+ return False
488
+
489
+ GLib.idle_add(do_move)
490
+ return {'success': True}
491
+
492
+ def _handle_window_resize(self, payload):
493
+ """Start window resize operation"""
494
+ edge = payload.get('edge', 'se')
495
+ x = payload.get('x', 0)
496
+ y = payload.get('y', 0)
497
+ button = payload.get('button', 1)
498
+
499
+ # Map edge name to Gdk.WindowEdge
500
+ edges = {
501
+ 'n': Gdk.WindowEdge.NORTH,
502
+ 's': Gdk.WindowEdge.SOUTH,
503
+ 'e': Gdk.WindowEdge.EAST,
504
+ 'w': Gdk.WindowEdge.WEST,
505
+ 'ne': Gdk.WindowEdge.NORTH_EAST,
506
+ 'nw': Gdk.WindowEdge.NORTH_WEST,
507
+ 'se': Gdk.WindowEdge.SOUTH_EAST,
508
+ 'sw': Gdk.WindowEdge.SOUTH_WEST
509
+ }
510
+
511
+ gdk_edge = edges.get(edge, Gdk.WindowEdge.SOUTH_EAST)
512
+
513
+ def do_resize():
514
+ try:
515
+ self.begin_resize_drag(
516
+ gdk_edge,
517
+ button,
518
+ int(x),
519
+ int(y),
520
+ Gdk.CURRENT_TIME
521
+ )
522
+ except Exception as e:
523
+ print(f"[Aegis] Resize drag error: {e}")
524
+ return False
525
+
526
+ GLib.idle_add(do_resize)
527
+ return {'success': True}
528
+
529
+ def _handle_window_set_size(self, payload):
530
+ """Set window size"""
531
+ width = payload.get('width')
532
+ height = payload.get('height')
533
+
534
+ if width and height:
535
+ self.resize(width, height)
536
+
537
+ return {'success': True}
538
+
539
+ def _handle_window_get_size(self, payload):
540
+ """Get current window size"""
541
+ width, height = self.get_size()
542
+ return {'width': width, 'height': height}
543
+
544
+ def _handle_window_set_position(self, payload):
545
+ """Set window position"""
546
+ x = payload.get('x')
547
+ y = payload.get('y')
548
+
549
+ if x is not None and y is not None:
550
+ self.move(x, y)
551
+
552
+ return {'success': True}
553
+
554
+ def _handle_window_get_position(self, payload):
555
+ """Get current window position"""
556
+ x, y = self.get_position()
557
+ return {'x': x, 'y': y}
558
+
559
+ # ==================== Public API ====================
560
+
561
+ def load_file(self, path):
562
+ """Load HTML file into webview"""
563
+ if not os.path.isabs(path):
564
+ path = os.path.abspath(path)
565
+ self.webview.load_uri(f'file://{path}')
566
+
567
+ def load_url(self, url):
568
+ """Load URL into webview"""
569
+ self.webview.load_uri(url)
570
+
571
+ def inject_aegis_api(self, preload_path=None):
572
+ """Inject the Aegis JavaScript API"""
573
+ # Load the Aegis API script
574
+ api_path = os.path.join(
575
+ os.path.dirname(__file__),
576
+ '..', 'runtime', 'aegis-api.js'
577
+ )
578
+
579
+ with open(api_path, 'r') as f:
580
+ api_script = f.read()
581
+
582
+ # Load preload script if provided
583
+ preload_script = ''
584
+ if preload_path and os.path.exists(preload_path):
585
+ with open(preload_path, 'r') as f:
586
+ preload_script = f.read()
587
+
588
+ # Combine scripts
589
+ full_script = api_script + '\n' + preload_script
590
+
591
+ # Inject at document start
592
+ user_script = WebKit2.UserScript(
593
+ full_script,
594
+ WebKit2.UserContentInjectedFrames.ALL_FRAMES,
595
+ WebKit2.UserScriptInjectionTime.START,
596
+ None, None
597
+ )
598
+ self.content_manager.add_script(user_script)
599
+
600
+ def run(self):
601
+ """Show window and start main loop"""
602
+ self.show_all()
603
+ Gtk.main()
@@ -0,0 +1 @@
1
+ """Aegis Runtime - JavaScript API injection"""