aegis-framework 0.1.0 → 0.1.1

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.
package/aegis/cli/cli.py CHANGED
@@ -434,48 +434,266 @@ console.log('✅ Preload configured');
434
434
  # README
435
435
  readme_content = f'''# {project_name}
436
436
 
437
- An application built with [Aegis Framework](https://github.com/your-repo/aegis).
437
+ An application built with [Aegis Framework](https://github.com/Diegopam/aegis-framework) - the lightweight alternative to Electron!
438
438
 
439
- ## Getting Started
439
+ ## 🚀 Quick Start
440
440
 
441
441
  ```bash
442
- # Development mode
442
+ # Development mode (with hot-reload)
443
443
  aegis dev
444
444
 
445
- # Build AppImage
445
+ # Run in production mode
446
+ aegis run
447
+
448
+ # Build AppImage (~200KB!)
446
449
  aegis build
447
450
  ```
448
451
 
449
- ## Project Structure
452
+ ## 📁 Project Structure
450
453
 
451
454
  ```
452
455
  {project_name}/
453
- ├── aegis.config.json # Project configuration
456
+ ├── aegis.config.json # App configuration (size, title, etc.)
454
457
  ├── src/
455
- │ ├── index.html # Main HTML
456
- │ ├── styles.css # Styles
457
- │ ├── app.js # JavaScript
458
- │ └── preload.js # Security configuration
458
+ │ ├── index.html # Main HTML entry point
459
+ │ ├── styles.css # Your styles
460
+ │ ├── app.js # Your JavaScript code
461
+ │ └── preload.js # Security: control which APIs are exposed
459
462
  └── assets/
460
- └── icon.png # App icon
463
+ └── icon.png # App icon (256x256 recommended)
461
464
  ```
462
465
 
463
- ## Aegis API
466
+ ## 🔌 Aegis API Reference
467
+
468
+ ### File Operations
464
469
 
465
470
  ```javascript
466
- // Read files
467
- const content = await Aegis.read({{ path: '.', file: 'data.txt' }});
471
+ // Read directory contents
472
+ const dir = await Aegis.read({{ path: '/home/user' }});
473
+ console.log(dir.entries); // [{{ name: 'file.txt', isFile: true, size: 1234 }}, ...]
474
+
475
+ // Read file content
476
+ const file = await Aegis.read({{ path: '/home/user', file: 'data.txt' }});
477
+ console.log(file.content);
478
+
479
+ // Write file
480
+ await Aegis.write({{
481
+ path: '/home/user',
482
+ file: 'output.txt',
483
+ content: 'Hello, Aegis!'
484
+ }});
485
+
486
+ // Check if file/directory exists
487
+ const info = await Aegis.exists({{ path: '/home/user/file.txt' }});
488
+ if (info.exists && info.isFile) {{
489
+ console.log('File exists!');
490
+ }}
491
+
492
+ // Create directory
493
+ await Aegis.mkdir({{ path: '/home/user/new-folder' }});
494
+
495
+ // Delete file or directory
496
+ await Aegis.remove({{ path: '/home/user/old-file.txt' }});
497
+ await Aegis.remove({{ path: '/home/user/old-folder', recursive: true }});
498
+
499
+ // Copy file or directory
500
+ await Aegis.copy({{
501
+ src: '/home/user/file.txt',
502
+ dest: '/home/user/backup/file.txt'
503
+ }});
504
+
505
+ // Move/rename file or directory
506
+ await Aegis.move({{
507
+ src: '/home/user/old-name.txt',
508
+ dest: '/home/user/new-name.txt'
509
+ }});
510
+ ```
468
511
 
469
- // Write files
470
- await Aegis.write({{ path: '.', file: 'output.txt', content: 'Hello!' }});
512
+ ### Execute Commands
471
513
 
472
- // Run commands
514
+ ```javascript
515
+ // Run shell command
473
516
  const result = await Aegis.run({{ sh: 'ls -la' }});
517
+ console.log(result.output);
518
+ console.log(result.exitCode);
519
+
520
+ // Run Python code
521
+ const pyResult = await Aegis.run({{ py: 'print(2 + 2)' }});
522
+ console.log(pyResult.output); // "4"
523
+
524
+ // Run async command with streaming output (no UI freeze!)
525
+ await Aegis.runAsync(
526
+ {{ sh: 'apt update' }},
527
+ (progress) => {{
528
+ console.log(progress.line); // Each line as it comes
529
+ }}
530
+ );
531
+ ```
532
+
533
+ ### Dialogs
474
534
 
475
- // Dialogs
476
- await Aegis.dialog.message({{ type: 'info', title: 'Hello', message: 'World!' }});
535
+ ```javascript
536
+ // Info dialog
537
+ await Aegis.dialog.message({{
538
+ type: 'info',
539
+ title: 'Success',
540
+ message: 'Operation completed!'
541
+ }});
542
+
543
+ // Confirmation dialog
544
+ const confirm = await Aegis.dialog.message({{
545
+ type: 'question',
546
+ title: 'Confirm',
547
+ message: 'Are you sure?',
548
+ buttons: 'yesno'
549
+ }});
550
+ if (confirm.response) {{
551
+ // User clicked Yes
552
+ }}
553
+
554
+ // Open file dialog
555
+ const file = await Aegis.dialog.open({{
556
+ title: 'Select a file',
557
+ filters: [{{ name: 'Images', extensions: ['png', 'jpg', 'gif'] }}]
558
+ }});
559
+ console.log(file.path);
560
+
561
+ // Save file dialog
562
+ const savePath = await Aegis.dialog.save({{
563
+ title: 'Save as',
564
+ defaultName: 'document.txt'
565
+ }});
566
+ ```
567
+
568
+ ### Download with Progress
569
+
570
+ ```javascript
571
+ // Download file with progress bar
572
+ await Aegis.download(
573
+ {{
574
+ url: 'https://example.com/file.zip',
575
+ dest: '/home/user/downloads/file.zip'
576
+ }},
577
+ (progress) => {{
578
+ const percent = progress.percent.toFixed(1);
579
+ progressBar.style.width = percent + '%';
580
+ statusText.textContent = `${{progress.downloaded}} / ${{progress.total}} bytes`;
581
+ }}
582
+ );
477
583
  ```
478
584
 
585
+ ### App Control
586
+
587
+ ```javascript
588
+ // Window controls
589
+ Aegis.app.minimize();
590
+ Aegis.app.maximize();
591
+ Aegis.app.quit();
592
+
593
+ // Get system paths (localized for your language!)
594
+ const home = await Aegis.app.getPath({{ name: 'home' }});
595
+ const docs = await Aegis.app.getPath({{ name: 'documents' }}); // Returns "Documentos" on pt-BR
596
+ const downloads = await Aegis.app.getPath({{ name: 'downloads' }});
597
+ // Also: desktop, music, pictures, videos
598
+ ```
599
+
600
+ ### Window Control (Frameless Windows)
601
+
602
+ ```javascript
603
+ // Make element draggable for window movement
604
+ Aegis.window.moveBar('#titlebar', {{ exclude: '.btn-close' }});
605
+
606
+ // Setup resize handles
607
+ Aegis.window.resizeHandles({{
608
+ '.resize-n': 'n',
609
+ '.resize-s': 's',
610
+ '.resize-se': 'se',
611
+ // Options: n, s, e, w, ne, nw, se, sw
612
+ }});
613
+
614
+ // Get/set window size
615
+ const size = await Aegis.window.getSize();
616
+ await Aegis.window.setSize({{ width: 1024, height: 768 }});
617
+
618
+ // Get/set window position
619
+ const pos = await Aegis.window.getPosition();
620
+ await Aegis.window.setPosition({{ x: 100, y: 100 }});
621
+ ```
622
+
623
+ ## 🔒 Security (preload.js)
624
+
625
+ Control which APIs your app can access:
626
+
627
+ ```javascript
628
+ // src/preload.js
629
+ Aegis.expose([
630
+ 'read', // File reading
631
+ 'write', // File writing
632
+ 'run', // Command execution
633
+ 'dialog', // Native dialogs
634
+ 'app', // App control
635
+ 'window', // Window control
636
+ 'download', // Download with progress
637
+ 'exists', // File existence
638
+ 'mkdir', // Create directories
639
+ 'remove', // Delete files
640
+ 'copy', // Copy files
641
+ 'move' // Move/rename files
642
+ ]);
643
+
644
+ // For maximum security, only expose what you need!
645
+ // If you omit 'run', the app cannot execute shell commands
646
+ ```
647
+
648
+ ## ⚙️ Configuration (aegis.config.json)
649
+
650
+ ```json
651
+ {{
652
+ "name": "{project_name}",
653
+ "title": "My Awesome App",
654
+ "version": "1.0.0",
655
+ "main": "src/index.html",
656
+ "preload": "src/preload.js",
657
+ "width": 1200,
658
+ "height": 800,
659
+ "resizable": true,
660
+ "frame": true,
661
+ "icon": "assets/icon.png"
662
+ }}
663
+ ```
664
+
665
+ | Option | Description |
666
+ |--------|-------------|
667
+ | `frame` | Set to `false` for frameless window (custom titlebar) |
668
+ | `resizable` | Allow window resizing |
669
+ | `width/height` | Initial window size |
670
+ | `devTools` | Enable right-click → Inspect Element |
671
+
672
+ ## 📦 Building AppImage
673
+
674
+ ```bash
675
+ aegis build
676
+ ```
677
+
678
+ This creates a portable AppImage (~200KB!) in the `dist/` folder.
679
+
680
+ **Note:** The AppImage requires `python3-gi` and `gir1.2-webkit2-4.1` on the target system.
681
+
682
+ ## 🆚 Why Aegis over Electron?
683
+
684
+ | Aspect | Electron | Aegis |
685
+ |--------|----------|-------|
686
+ | App Size | ~150 MB | **~200 KB** |
687
+ | Backend | Node.js | Python |
688
+ | Renderer | Chromium (bundled) | WebKit2GTK (system) |
689
+ | RAM Usage | High (~100MB+) | Low (~30MB) |
690
+ | Platform | Cross-platform | Linux |
691
+
692
+ ## 📚 Learn More
693
+
694
+ - [Aegis GitHub](https://github.com/Diegopam/aegis-framework)
695
+ - [npm Package](https://www.npmjs.com/package/aegis-framework)
696
+
479
697
  ## License
480
698
 
481
699
  MIT
@@ -10,6 +10,9 @@ gi.require_version('WebKit2', '4.1')
10
10
  from gi.repository import Gtk, WebKit2, Gdk, GLib
11
11
  import json
12
12
  import os
13
+ import threading
14
+ import urllib.request
15
+ import shutil
13
16
 
14
17
 
15
18
  class AegisWindow(Gtk.Window):
@@ -114,12 +117,17 @@ class AegisWindow(Gtk.Window):
114
117
 
115
118
  print(f"[Aegis] Action: {action}, Payload: {payload}")
116
119
 
117
- # Process action and get result
118
- result = self._process_action(action, payload)
120
+ # Check if this is an async action
121
+ async_actions = {'run.async', 'download', 'copy.async'}
119
122
 
120
- # Send response back to JavaScript
121
- if callback_id:
122
- self._send_response(callback_id, result)
123
+ if action in async_actions:
124
+ # Handle in background thread - response sent via callback
125
+ self._process_async_action(action, payload, callback_id)
126
+ else:
127
+ # Sync action - process and respond immediately
128
+ result = self._process_action(action, payload)
129
+ if callback_id:
130
+ self._send_response(callback_id, result)
123
131
 
124
132
  except Exception as e:
125
133
  print(f"[Aegis Bridge] Error: {e}")
@@ -597,6 +605,180 @@ class AegisWindow(Gtk.Window):
597
605
  )
598
606
  self.content_manager.add_script(user_script)
599
607
 
608
+ # ==================== Async Action Handlers ====================
609
+
610
+ def _process_async_action(self, action, payload, callback_id):
611
+ """Process async actions in background threads"""
612
+ handlers = {
613
+ 'run.async': self._handle_run_async,
614
+ 'download': self._handle_download,
615
+ 'copy.async': self._handle_copy_async,
616
+ }
617
+
618
+ handler = handlers.get(action)
619
+ if handler:
620
+ # Start background thread
621
+ thread = threading.Thread(
622
+ target=handler,
623
+ args=(payload, callback_id),
624
+ daemon=True
625
+ )
626
+ thread.start()
627
+ else:
628
+ GLib.idle_add(self._send_error, callback_id, f"Unknown async action: {action}")
629
+
630
+ def _send_progress(self, callback_id, progress_data):
631
+ """Send progress update to JavaScript (must be called via GLib.idle_add)"""
632
+ response = json.dumps({
633
+ 'callbackId': callback_id,
634
+ 'type': 'progress',
635
+ 'data': progress_data
636
+ })
637
+ script = f"window.__aegisProgress({response})"
638
+ self.webview.evaluate_javascript(script, -1, None, None, None, None, None)
639
+ return False # Don't repeat
640
+
641
+ def _handle_run_async(self, payload, callback_id):
642
+ """Execute command asynchronously with streaming output"""
643
+ import subprocess
644
+
645
+ try:
646
+ cmd = payload.get('sh', '')
647
+
648
+ process = subprocess.Popen(
649
+ cmd,
650
+ shell=True,
651
+ stdout=subprocess.PIPE,
652
+ stderr=subprocess.STDOUT,
653
+ text=True,
654
+ bufsize=1
655
+ )
656
+
657
+ output_lines = []
658
+
659
+ # Stream output line by line
660
+ for line in process.stdout:
661
+ output_lines.append(line)
662
+ GLib.idle_add(self._send_progress, callback_id, {
663
+ 'type': 'output',
664
+ 'line': line.rstrip('\n')
665
+ })
666
+
667
+ process.wait()
668
+
669
+ # Send final result
670
+ result = {
671
+ 'output': ''.join(output_lines),
672
+ 'exitCode': process.returncode
673
+ }
674
+ GLib.idle_add(self._send_response, callback_id, result)
675
+
676
+ except Exception as e:
677
+ GLib.idle_add(self._send_error, callback_id, str(e))
678
+
679
+ def _handle_download(self, payload, callback_id):
680
+ """Download file with progress updates"""
681
+ try:
682
+ url = payload.get('url')
683
+ dest = payload.get('dest')
684
+
685
+ # Create request
686
+ req = urllib.request.Request(url, headers={
687
+ 'User-Agent': 'Aegis/0.1.0'
688
+ })
689
+
690
+ response = urllib.request.urlopen(req, timeout=30)
691
+ total_size = int(response.headers.get('Content-Length', 0))
692
+ downloaded = 0
693
+
694
+ # Ensure destination directory exists
695
+ os.makedirs(os.path.dirname(dest) or '.', exist_ok=True)
696
+
697
+ with open(dest, 'wb') as f:
698
+ while True:
699
+ chunk = response.read(8192)
700
+ if not chunk:
701
+ break
702
+
703
+ f.write(chunk)
704
+ downloaded += len(chunk)
705
+
706
+ # Send progress update
707
+ progress = {
708
+ 'downloaded': downloaded,
709
+ 'total': total_size,
710
+ 'percent': (downloaded / total_size * 100) if total_size else 0
711
+ }
712
+ GLib.idle_add(self._send_progress, callback_id, progress)
713
+
714
+ # Send completion
715
+ result = {
716
+ 'success': True,
717
+ 'path': dest,
718
+ 'size': downloaded
719
+ }
720
+ GLib.idle_add(self._send_response, callback_id, result)
721
+
722
+ except Exception as e:
723
+ GLib.idle_add(self._send_error, callback_id, str(e))
724
+
725
+ def _handle_copy_async(self, payload, callback_id):
726
+ """Copy files/directories with progress (for large files)"""
727
+ try:
728
+ src = payload.get('src')
729
+ dest = payload.get('dest')
730
+
731
+ if os.path.isdir(src):
732
+ # Copy directory
733
+ def copy_with_progress(src_dir, dest_dir):
734
+ total_files = sum([len(files) for _, _, files in os.walk(src_dir)])
735
+ copied = 0
736
+
737
+ for root, dirs, files in os.walk(src_dir):
738
+ rel_path = os.path.relpath(root, src_dir)
739
+ dest_path = os.path.join(dest_dir, rel_path)
740
+ os.makedirs(dest_path, exist_ok=True)
741
+
742
+ for file in files:
743
+ src_file = os.path.join(root, file)
744
+ dest_file = os.path.join(dest_path, file)
745
+ shutil.copy2(src_file, dest_file)
746
+ copied += 1
747
+
748
+ GLib.idle_add(self._send_progress, callback_id, {
749
+ 'copied': copied,
750
+ 'total': total_files,
751
+ 'percent': (copied / total_files * 100) if total_files else 100,
752
+ 'current': file
753
+ })
754
+
755
+ copy_with_progress(src, dest)
756
+ else:
757
+ # Copy single file with progress
758
+ file_size = os.path.getsize(src)
759
+ copied = 0
760
+
761
+ with open(src, 'rb') as fsrc:
762
+ with open(dest, 'wb') as fdest:
763
+ while True:
764
+ chunk = fsrc.read(8192)
765
+ if not chunk:
766
+ break
767
+ fdest.write(chunk)
768
+ copied += len(chunk)
769
+
770
+ GLib.idle_add(self._send_progress, callback_id, {
771
+ 'copied': copied,
772
+ 'total': file_size,
773
+ 'percent': (copied / file_size * 100) if file_size else 100
774
+ })
775
+
776
+ result = {'success': True, 'src': src, 'dest': dest}
777
+ GLib.idle_add(self._send_response, callback_id, result)
778
+
779
+ except Exception as e:
780
+ GLib.idle_add(self._send_error, callback_id, str(e))
781
+
600
782
  def run(self):
601
783
  """Show window and start main loop"""
602
784
  self.show_all()
@@ -14,6 +14,7 @@
14
14
 
15
15
  let callbackId = 0;
16
16
  const pendingCallbacks = new Map();
17
+ const progressCallbacks = new Map(); // For async progress updates
17
18
 
18
19
  /**
19
20
  * Resolve a callback from Python
@@ -22,6 +23,7 @@
22
23
  const callback = pendingCallbacks.get(response.callbackId);
23
24
  if (callback) {
24
25
  pendingCallbacks.delete(response.callbackId);
26
+ progressCallbacks.delete(response.callbackId); // Cleanup progress callback
25
27
  if (response.success) {
26
28
  callback.resolve(response.data);
27
29
  } else {
@@ -30,6 +32,16 @@
30
32
  }
31
33
  };
32
34
 
35
+ /**
36
+ * Handle progress updates from Python (for async operations)
37
+ */
38
+ window.__aegisProgress = function (response) {
39
+ const callback = progressCallbacks.get(response.callbackId);
40
+ if (callback) {
41
+ callback(response.data);
42
+ }
43
+ };
44
+
33
45
  /**
34
46
  * Send a message to Python backend
35
47
  */
@@ -78,6 +90,41 @@
78
90
  };
79
91
  }
80
92
 
93
+ /**
94
+ * Invoke an async action with progress callback
95
+ * @param {string} action - The action name
96
+ * @param {object} payload - The payload
97
+ * @param {function} onProgress - Progress callback function
98
+ */
99
+ function invokeWithProgress(action, payload, onProgress) {
100
+ return new Promise((resolve, reject) => {
101
+ if (!isAllowed(action)) {
102
+ reject(new Error(`API '${action}' is not allowed by preload`));
103
+ return;
104
+ }
105
+
106
+ const id = ++callbackId;
107
+ pendingCallbacks.set(id, { resolve, reject });
108
+
109
+ // Register progress callback if provided
110
+ if (onProgress && typeof onProgress === 'function') {
111
+ progressCallbacks.set(id, onProgress);
112
+ }
113
+
114
+ const message = JSON.stringify({
115
+ action: action,
116
+ payload: payload,
117
+ callbackId: id
118
+ });
119
+
120
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.aegis) {
121
+ window.webkit.messageHandlers.aegis.postMessage(message);
122
+ } else {
123
+ reject(new Error('Aegis bridge not available'));
124
+ }
125
+ });
126
+ }
127
+
81
128
  // ==================== Public Aegis API ====================
82
129
 
83
130
  const Aegis = {
@@ -170,6 +217,74 @@
170
217
  */
171
218
  copy: guardedInvoke('copy'),
172
219
 
220
+ // ==================== Async Operations with Progress ====================
221
+
222
+ /**
223
+ * Run command asynchronously with streaming output
224
+ *
225
+ * @param {object} options - Command options
226
+ * @param {string} options.sh - Shell command to execute
227
+ * @param {function} onProgress - Progress callback, called with each output line
228
+ * @returns {Promise<{output: string, exitCode: number}>}
229
+ *
230
+ * @example
231
+ * const result = await Aegis.runAsync(
232
+ * { sh: 'apt install htop' },
233
+ * (progress) => {
234
+ * console.log(progress.line); // Each line of output
235
+ * }
236
+ * );
237
+ */
238
+ runAsync: function (options, onProgress) {
239
+ return invokeWithProgress('run.async', options, onProgress);
240
+ },
241
+
242
+ /**
243
+ * Download file with progress updates
244
+ *
245
+ * @param {object} options - Download options
246
+ * @param {string} options.url - URL to download from
247
+ * @param {string} options.dest - Destination file path
248
+ * @param {function} onProgress - Progress callback
249
+ * @returns {Promise<{success: boolean, path: string, size: number}>}
250
+ *
251
+ * @example
252
+ * const result = await Aegis.download(
253
+ * {
254
+ * url: 'https://example.com/file.zip',
255
+ * dest: '/home/user/downloads/file.zip'
256
+ * },
257
+ * (progress) => {
258
+ * progressBar.style.width = progress.percent + '%';
259
+ * statusText.textContent = `${progress.downloaded} / ${progress.total}`;
260
+ * }
261
+ * );
262
+ */
263
+ download: function (options, onProgress) {
264
+ return invokeWithProgress('download', options, onProgress);
265
+ },
266
+
267
+ /**
268
+ * Copy file or directory with progress updates (for large files)
269
+ *
270
+ * @param {object} options - Copy options
271
+ * @param {string} options.src - Source path
272
+ * @param {string} options.dest - Destination path
273
+ * @param {function} onProgress - Progress callback
274
+ * @returns {Promise<{success: boolean, src: string, dest: string}>}
275
+ *
276
+ * @example
277
+ * await Aegis.copyAsync(
278
+ * { src: '/path/to/large-folder', dest: '/path/to/backup' },
279
+ * (progress) => {
280
+ * console.log(`${progress.percent.toFixed(1)}% - ${progress.current}`);
281
+ * }
282
+ * );
283
+ */
284
+ copyAsync: function (options, onProgress) {
285
+ return invokeWithProgress('copy.async', options, onProgress);
286
+ },
287
+
173
288
  /**
174
289
  * Move/rename file or directory
175
290
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegis-framework",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Lightweight AppImage framework using WebKit2GTK and Python - An alternative to Electron that creates ~200KB apps instead of 150MB!",
5
5
  "keywords": [
6
6
  "appimage",
@@ -48,4 +48,4 @@
48
48
  "engines": {
49
49
  "node": ">=14"
50
50
  }
51
- }
51
+ }