pg-sui 0.2.3__py3-none-any.whl → 1.6.16a3__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 (128) hide show
  1. pg_sui-1.6.16a3.dist-info/METADATA +292 -0
  2. pg_sui-1.6.16a3.dist-info/RECORD +81 -0
  3. {pg_sui-0.2.3.dist-info → pg_sui-1.6.16a3.dist-info}/WHEEL +1 -1
  4. pg_sui-1.6.16a3.dist-info/entry_points.txt +4 -0
  5. {pg_sui-0.2.3.dist-info → pg_sui-1.6.16a3.dist-info/licenses}/LICENSE +0 -0
  6. pg_sui-1.6.16a3.dist-info/top_level.txt +1 -0
  7. pgsui/__init__.py +35 -54
  8. pgsui/_version.py +34 -0
  9. pgsui/cli.py +922 -0
  10. pgsui/data_processing/__init__.py +0 -0
  11. pgsui/data_processing/config.py +565 -0
  12. pgsui/data_processing/containers.py +1436 -0
  13. pgsui/data_processing/transformers.py +557 -907
  14. pgsui/{example_data/trees → electron/app}/__init__.py +0 -0
  15. pgsui/electron/app/__main__.py +5 -0
  16. pgsui/electron/app/extra-resources/.gitkeep +1 -0
  17. pgsui/electron/app/icons/icons/1024x1024.png +0 -0
  18. pgsui/electron/app/icons/icons/128x128.png +0 -0
  19. pgsui/electron/app/icons/icons/16x16.png +0 -0
  20. pgsui/electron/app/icons/icons/24x24.png +0 -0
  21. pgsui/electron/app/icons/icons/256x256.png +0 -0
  22. pgsui/electron/app/icons/icons/32x32.png +0 -0
  23. pgsui/electron/app/icons/icons/48x48.png +0 -0
  24. pgsui/electron/app/icons/icons/512x512.png +0 -0
  25. pgsui/electron/app/icons/icons/64x64.png +0 -0
  26. pgsui/electron/app/icons/icons/icon.icns +0 -0
  27. pgsui/electron/app/icons/icons/icon.ico +0 -0
  28. pgsui/electron/app/main.js +227 -0
  29. pgsui/electron/app/package-lock.json +6894 -0
  30. pgsui/electron/app/package.json +51 -0
  31. pgsui/electron/app/preload.js +15 -0
  32. pgsui/electron/app/server.py +157 -0
  33. pgsui/electron/app/ui/logo.png +0 -0
  34. pgsui/electron/app/ui/renderer.js +131 -0
  35. pgsui/electron/app/ui/styles.css +59 -0
  36. pgsui/electron/app/ui/ui_shim.js +72 -0
  37. pgsui/electron/bootstrap.py +43 -0
  38. pgsui/electron/launch.py +57 -0
  39. pgsui/electron/package.json +14 -0
  40. pgsui/example_data/__init__.py +0 -0
  41. pgsui/example_data/phylip_files/__init__.py +0 -0
  42. pgsui/example_data/phylip_files/test.phy +0 -0
  43. pgsui/example_data/popmaps/__init__.py +0 -0
  44. pgsui/example_data/popmaps/{test.popmap → phylogen_nomx.popmap} +185 -99
  45. pgsui/example_data/structure_files/__init__.py +0 -0
  46. pgsui/example_data/structure_files/test.pops.2row.allsites.str +0 -0
  47. pgsui/example_data/vcf_files/phylogen_subset14K.vcf.gz +0 -0
  48. pgsui/example_data/vcf_files/phylogen_subset14K.vcf.gz.tbi +0 -0
  49. pgsui/impute/__init__.py +0 -0
  50. pgsui/impute/deterministic/imputers/allele_freq.py +725 -0
  51. pgsui/impute/deterministic/imputers/mode.py +844 -0
  52. pgsui/impute/deterministic/imputers/nmf.py +221 -0
  53. pgsui/impute/deterministic/imputers/phylo.py +973 -0
  54. pgsui/impute/deterministic/imputers/ref_allele.py +669 -0
  55. pgsui/impute/supervised/__init__.py +0 -0
  56. pgsui/impute/supervised/base.py +343 -0
  57. pgsui/impute/{unsupervised/models/in_development → supervised/imputers}/__init__.py +0 -0
  58. pgsui/impute/supervised/imputers/hist_gradient_boosting.py +317 -0
  59. pgsui/impute/supervised/imputers/random_forest.py +291 -0
  60. pgsui/impute/unsupervised/__init__.py +0 -0
  61. pgsui/impute/unsupervised/base.py +1121 -0
  62. pgsui/impute/unsupervised/callbacks.py +92 -262
  63. {simulation → pgsui/impute/unsupervised/imputers}/__init__.py +0 -0
  64. pgsui/impute/unsupervised/imputers/autoencoder.py +1361 -0
  65. pgsui/impute/unsupervised/imputers/nlpca.py +1666 -0
  66. pgsui/impute/unsupervised/imputers/ubp.py +1660 -0
  67. pgsui/impute/unsupervised/imputers/vae.py +1316 -0
  68. pgsui/impute/unsupervised/loss_functions.py +261 -0
  69. pgsui/impute/unsupervised/models/__init__.py +0 -0
  70. pgsui/impute/unsupervised/models/autoencoder_model.py +215 -567
  71. pgsui/impute/unsupervised/models/nlpca_model.py +155 -394
  72. pgsui/impute/unsupervised/models/ubp_model.py +180 -1106
  73. pgsui/impute/unsupervised/models/vae_model.py +269 -630
  74. pgsui/impute/unsupervised/nn_scorers.py +255 -0
  75. pgsui/utils/__init__.py +0 -0
  76. pgsui/utils/classification_viz.py +608 -0
  77. pgsui/utils/logging_utils.py +22 -0
  78. pgsui/utils/misc.py +35 -480
  79. pgsui/utils/plotting.py +996 -829
  80. pgsui/utils/pretty_metrics.py +290 -0
  81. pgsui/utils/scorers.py +213 -666
  82. pg_sui-0.2.3.dist-info/METADATA +0 -322
  83. pg_sui-0.2.3.dist-info/RECORD +0 -75
  84. pg_sui-0.2.3.dist-info/top_level.txt +0 -3
  85. pgsui/example_data/phylip_files/test_n10.phy +0 -118
  86. pgsui/example_data/phylip_files/test_n100.phy +0 -118
  87. pgsui/example_data/phylip_files/test_n2.phy +0 -118
  88. pgsui/example_data/phylip_files/test_n500.phy +0 -118
  89. pgsui/example_data/structure_files/test.nopops.1row.10sites.str +0 -117
  90. pgsui/example_data/structure_files/test.nopops.2row.100sites.str +0 -234
  91. pgsui/example_data/structure_files/test.nopops.2row.10sites.str +0 -234
  92. pgsui/example_data/structure_files/test.nopops.2row.30sites.str +0 -234
  93. pgsui/example_data/structure_files/test.nopops.2row.allsites.str +0 -234
  94. pgsui/example_data/structure_files/test.pops.1row.10sites.str +0 -117
  95. pgsui/example_data/structure_files/test.pops.2row.10sites.str +0 -234
  96. pgsui/example_data/trees/test.iqtree +0 -376
  97. pgsui/example_data/trees/test.qmat +0 -5
  98. pgsui/example_data/trees/test.rate +0 -2033
  99. pgsui/example_data/trees/test.tre +0 -1
  100. pgsui/example_data/trees/test_n10.rate +0 -19
  101. pgsui/example_data/trees/test_n100.rate +0 -109
  102. pgsui/example_data/trees/test_n500.rate +0 -509
  103. pgsui/example_data/trees/test_siterates.txt +0 -2024
  104. pgsui/example_data/trees/test_siterates_n10.txt +0 -10
  105. pgsui/example_data/trees/test_siterates_n100.txt +0 -100
  106. pgsui/example_data/trees/test_siterates_n500.txt +0 -500
  107. pgsui/example_data/vcf_files/test.vcf +0 -244
  108. pgsui/example_data/vcf_files/test.vcf.gz +0 -0
  109. pgsui/example_data/vcf_files/test.vcf.gz.tbi +0 -0
  110. pgsui/impute/estimators.py +0 -1268
  111. pgsui/impute/impute.py +0 -1463
  112. pgsui/impute/simple_imputers.py +0 -1431
  113. pgsui/impute/supervised/iterative_imputer_fixedparams.py +0 -782
  114. pgsui/impute/supervised/iterative_imputer_gridsearch.py +0 -1024
  115. pgsui/impute/unsupervised/keras_classifiers.py +0 -697
  116. pgsui/impute/unsupervised/models/in_development/cnn_model.py +0 -486
  117. pgsui/impute/unsupervised/neural_network_imputers.py +0 -1440
  118. pgsui/impute/unsupervised/neural_network_methods.py +0 -1395
  119. pgsui/pg_sui.py +0 -261
  120. pgsui/utils/sequence_tools.py +0 -407
  121. simulation/sim_benchmarks.py +0 -333
  122. simulation/sim_treeparams.py +0 -475
  123. test/__init__.py +0 -0
  124. test/pg_sui_simtest.py +0 -215
  125. test/pg_sui_testing.py +0 -523
  126. test/test.py +0 -151
  127. test/test_pgsui.py +0 -374
  128. test/test_tkc.py +0 -185
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "pgsui-gui",
3
+ "version": "1.0.0",
4
+ "description": "PG-SUI GUI wrapper for the Python CLI",
5
+ "author": { "name": "Bradley T. Martin", "email": "evobio721@gmail.com" },
6
+ "homepage": "https://github.com/btmartin721/PG-SUI",
7
+ "main": "main.js",
8
+ "type": "commonjs",
9
+ "scripts": {
10
+ "start": "electron .",
11
+ "icons": "electron-icon-builder --input=ui/logo.png --output=icons --flatten",
12
+ "dist": "electron-builder -mwl"
13
+ },
14
+ "devDependencies": {
15
+ "electron": "^38.4.0",
16
+ "electron-builder": "^24.13.3",
17
+ "electron-icon-builder": "^2.0.1"
18
+ },
19
+ "build": {
20
+ "appId": "io.pgsui.gui",
21
+ "directories": {
22
+ "buildResources": "icons",
23
+ "output": "dist"
24
+ },
25
+ "asar": true,
26
+ "extraResources": [
27
+ { "from": "../../cli.py", "to": "pgsui/cli.py" },
28
+ { "from": "extra-resources", "to": "extras" }
29
+ ],
30
+ "files": [
31
+ "main.js",
32
+ "preload.js",
33
+ "ui/**/*",
34
+ "icons/**/*"
35
+ ],
36
+ "mac": {
37
+ "target": ["dmg", "zip"],
38
+ "category": "public.app-category.science",
39
+ "icon": "icons/icon.icns"
40
+ },
41
+ "linux": {
42
+ "target": ["AppImage", "deb"],
43
+ "category": "Science",
44
+ "icon": "icons"
45
+ },
46
+ "win": {
47
+ "target": ["nsis"],
48
+ "icon": "icons/icon.ico"
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,15 @@
1
+ const { contextBridge, ipcRenderer } = require('electron');
2
+
3
+ contextBridge.exposeInMainWorld('pgsui', {
4
+ detectPgSui: () => ipcRenderer.invoke('detect:pgsui'),
5
+ start: (payload) => ipcRenderer.invoke('pgsui:start', payload),
6
+ stop: () => ipcRenderer.invoke('pgsui:stop'),
7
+ pickFile: (filters) => ipcRenderer.invoke('pick:file', { filters }),
8
+ pickDir: () => ipcRenderer.invoke('pick:dir'),
9
+ pickSave: (defaultPath) => ipcRenderer.invoke('pick:save', { defaultPath }),
10
+ onLog: (fn) => ipcRenderer.on('pgsui:log', (_e, d) => fn(d)),
11
+ onError: (fn) => ipcRenderer.on('pgsui:error', (_e, d) => fn(d)),
12
+ onExit: (fn) => ipcRenderer.on('pgsui:exit', (_e, d) => fn(d)),
13
+ onStarted: (fn) => ipcRenderer.on('pgsui:started', (_e, d) => fn(d)),
14
+ defaultCli: () => ipcRenderer.invoke('default:cli'),
15
+ });
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import signal
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
10
+ from fastapi.responses import HTMLResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+
13
+ ROOT = Path(__file__).resolve().parent / "ui"
14
+ WORK = Path("/work")
15
+ app = FastAPI()
16
+ _proc = None
17
+ _log_queue: asyncio.Queue[str] = asyncio.Queue()
18
+
19
+
20
+ def _build_args(p: dict[str, str | int | bool | list[str] | None]) -> list[str]:
21
+ a: list[str] = []
22
+
23
+ def kv(flag, v):
24
+ if v not in (None, "", []):
25
+ a.extend([flag, str(v)])
26
+
27
+ def fl(flag, cond):
28
+ if cond:
29
+ a.append(flag)
30
+
31
+ kv("--input", p.get("inputPath"))
32
+ kv("--format", p.get("format"))
33
+ kv("--popmap", p.get("popmapPath"))
34
+ kv("--prefix", p.get("prefix"))
35
+ kv("--config", p.get("yamlPath"))
36
+ kv("--dump-config", p.get("dumpConfigPath"))
37
+ kv("--preset", p.get("preset") or "fast")
38
+ kv("--sim-strategy", p.get("simStrategy"))
39
+ m = p.get("models") or []
40
+ if m:
41
+ a.extend(["--models", *m])
42
+ inc = p.get("includePops") or []
43
+ if inc:
44
+ a.extend(["--include-pops", *inc])
45
+ fl("--tune", bool(p.get("tune")))
46
+ if p.get("tuneNTrials"):
47
+ kv("--tune-n-trials", p["tuneNTrials"])
48
+ kv("--batch-size", p.get("batchSize") or 64)
49
+ dev = p.get("device")
50
+ if dev in ("cpu", "cuda", "mps"):
51
+ kv("--device", dev)
52
+ if p.get("nJobs"):
53
+ kv("--n-jobs", p["nJobs"])
54
+ pf = p.get("plotFormat")
55
+ if pf in ("png", "pdf", "svg"):
56
+ kv("--plot-format", pf)
57
+ if p.get("seed"):
58
+ kv("--seed", p["seed"])
59
+ fl("--verbose", bool(p.get("verbose")))
60
+ if p.get("logFile"):
61
+ kv("--log-file", p["logFile"])
62
+ fl("--force-popmap", bool(p.get("forcePopmap")))
63
+ fl("--dry-run", bool(p.get("dryRun")))
64
+ for kvp in p.get("setPairs") or []:
65
+ if isinstance(kvp, str) and "=" in kvp:
66
+ a.extend(["--set", kvp])
67
+ return a
68
+
69
+
70
+ @app.get("/api/status")
71
+ def status():
72
+ return {"running": _proc is not None}
73
+
74
+
75
+ @app.get("/api/ls")
76
+ def ls(path: str = Query("/work")):
77
+ p = Path(path).resolve()
78
+ if not str(p).startswith(str(WORK)):
79
+ return {"ok": False, "error": "out_of_root"}
80
+ items = [
81
+ {"name": e.name, "path": str(e), "dir": e.is_dir()} for e in sorted(p.iterdir())
82
+ ]
83
+ return {"ok": True, "cwd": str(p), "items": items}
84
+
85
+
86
+ @app.post("/api/start")
87
+ async def start(payload: dict):
88
+ global _proc
89
+ if _proc:
90
+ return {"ok": False, "error": "already_running"}
91
+ cwd = Path(payload.get("cwd") or "/work").resolve()
92
+ if not str(cwd).startswith(str(WORK)):
93
+ return {"ok": False, "error": "cwd_must_be_under_/work"}
94
+ args = _build_args(payload)
95
+ use_pg = bool(payload.get("usePgSui", True))
96
+ if use_pg:
97
+ cmd = ["pg-sui", *args]
98
+ else:
99
+ py_candidates = [
100
+ payload.get("pythonPath"),
101
+ os.environ.get("PGSUI_PYTHON"),
102
+ "/opt/homebrew/bin/python3.12",
103
+ "/usr/local/bin/python3.12",
104
+ "python3.12",
105
+ "python3",
106
+ "python",
107
+ ]
108
+ py = next((c for c in py_candidates if c and shutil.which(c)), "python3")
109
+ cli = payload.get("cliPath")
110
+ if not cli:
111
+ return {"ok": False, "error": "cli_path_required"}
112
+ cmd = [py, cli, *args]
113
+ _proc = await asyncio.create_subprocess_exec(
114
+ *cmd,
115
+ cwd=str(cwd),
116
+ stdout=asyncio.subprocess.PIPE,
117
+ stderr=asyncio.subprocess.PIPE,
118
+ env=os.environ.copy(),
119
+ )
120
+
121
+ async def pump(stream, tag):
122
+ async for line in stream:
123
+ await _log_queue.put(f"{tag}|{line.decode(errors='ignore').rstrip()}")
124
+
125
+ asyncio.create_task(pump(_proc.stdout, "stdout"))
126
+ asyncio.create_task(pump(_proc.stderr, "stderr"))
127
+ return {"ok": True, "argv": cmd, "cwd": str(cwd)}
128
+
129
+
130
+ @app.post("/api/stop")
131
+ async def stop():
132
+ global _proc
133
+ if not _proc:
134
+ return {"ok": False, "error": "not_running"}
135
+ try:
136
+ _proc.send_signal(signal.SIGTERM)
137
+ try:
138
+ await asyncio.wait_for(_proc.wait(), timeout=5)
139
+ except asyncio.TimeoutError:
140
+ _proc.kill()
141
+ finally:
142
+ _proc = None
143
+ return {"ok": True}
144
+
145
+
146
+ @app.websocket("/api/logs")
147
+ async def logs(ws: WebSocket):
148
+ await ws.accept()
149
+ try:
150
+ while True:
151
+ msg = await _log_queue.get()
152
+ await ws.send_text(msg)
153
+ except WebSocketDisconnect:
154
+ return
155
+
156
+
157
+ app.mount("/", StaticFiles(directory=ROOT, html=True), name="ui")
Binary file
@@ -0,0 +1,131 @@
1
+ /* ---- helpers (define first) ---- */
2
+ const $ = (id) => document.getElementById(id);
3
+ const logEl = $('log');
4
+
5
+ function appendLog({ stream, line }) {
6
+ const span = document.createElement('span');
7
+ if (stream === 'stderr') span.className = 'err';
8
+ span.textContent = line + '\n';
9
+ logEl.appendChild(span);
10
+ if (logEl.textContent.length > 2_000_000) logEl.textContent = logEl.textContent.slice(-1_000_000);
11
+ logEl.scrollTop = logEl.scrollHeight;
12
+ }
13
+
14
+ /* ---- payload ---- */
15
+ function collectPayload() {
16
+ const modelsSel = Array.from($('models').selectedOptions).map(o => o.value);
17
+ const includePops = $('includePops').value.trim().split(/\s+/).filter(Boolean);
18
+ const setPairs = $('setPairs').value.split('\n').map(s => s.trim()).filter(Boolean);
19
+ const bs = $('batchSize').value ? Number($('batchSize').value) : 64;
20
+ return {
21
+ pythonPath: undefined,
22
+ cliPath: undefined,
23
+ cwd: $('cwd').value.trim(),
24
+ inputPath: $('inputPath').value.trim(),
25
+ format: $('format').value,
26
+ popmapPath: $('popmapPath').value.trim() || undefined,
27
+ prefix: $('prefix').value.trim() || undefined,
28
+ yamlPath: $('yamlPath').value.trim() || undefined,
29
+ dumpConfigPath: $('dumpConfigPath').value.trim() || undefined,
30
+ preset: $('preset').value || 'fast',
31
+ simStrategy: $('simStrategy').value || 'random',
32
+ models: modelsSel,
33
+ includePops,
34
+ device: $('device').value || undefined,
35
+ batchSize: bs,
36
+ nJobs: $('nJobs').value ? Number($('nJobs').value) : undefined,
37
+ plotFormat: $('plotFormat').value || undefined,
38
+ seed: $('seed').value.trim() || undefined,
39
+ verbose: $('verbose').checked,
40
+ forcePopmap: $('forcePopmap').checked,
41
+ dryRun: $('dryRun').checked,
42
+ setPairs,
43
+ logFile: $('logFile').value.trim() || undefined,
44
+ tune: $('tune').checked,
45
+ tuneNTrials: $('tune').checked ? (Number($('tuneNTrials').value) || 50) : undefined
46
+ };
47
+ }
48
+
49
+ function setRunningUI(isRunning) {
50
+ $('start').disabled = isRunning;
51
+ $('stop').disabled = !isRunning;
52
+ document.querySelectorAll('button').forEach(b => { if (b.id !== 'stop') b.disabled = isRunning; });
53
+ }
54
+
55
+ /* ---- init small UI bits ---- */
56
+ (() => {
57
+ const tune = $('tune'), tuneOpts = $('tuneOpts');
58
+ if (tune && tuneOpts) tune.addEventListener('change', () => { tuneOpts.style.display = tune.checked ? '' : 'none'; });
59
+ const logoEl = $('logo');
60
+ if (logoEl) logoEl.addEventListener('error', () => { logoEl.style.display = 'none'; });
61
+ })();
62
+
63
+ /* ---- events (wire exactly once) ---- */
64
+ const on = (id, ev, fn) => { const el = $(id); if (el) el.addEventListener(ev, fn); };
65
+
66
+ on('pickCwd', 'click', async () => {
67
+ const d = await window.pgsui.pickDir();
68
+ if (d) $('cwd').value = d;
69
+ });
70
+
71
+ on('start', 'click', async () => {
72
+ try {
73
+ if (!window.pgsui) { appendLog({ stream:'stderr', line:'Bridge missing (preload).' }); return; }
74
+ logEl.textContent = '';
75
+ const payload = collectPayload();
76
+ if (!payload.cwd) { appendLog({ stream:'stderr', line:'Working directory is required.' }); return; }
77
+ if (!payload.inputPath) { appendLog({ stream:'stderr', line:'Input file required.' }); return; }
78
+ if (!payload.cliPath) {
79
+ const r = await window.pgsui.defaultCli?.();
80
+ if (r?.ok && r.path) payload.cliPath = r.path;
81
+ }
82
+ if (!payload.cliPath) { appendLog({ stream:'stderr', line:'Could not find <project_root>/pgsui/cli.py. Set PGSUI_CLI_DEFAULT or adjust repo layout.' }); return; }
83
+ const res = await window.pgsui.start(payload);
84
+ if (!res?.ok) appendLog({ stream:'stderr', line:`Start failed: ${res?.error || 'unknown error'}` });
85
+ } catch (e) {
86
+ appendLog({ stream:'stderr', line:`Start exception: ${e?.message || String(e)}` });
87
+ }
88
+ });
89
+
90
+ on('stop', 'click', async () => {
91
+ const res = await window.pgsui.stop();
92
+ if (!res.ok) appendLog({ stream:'stderr', line: res.error });
93
+ });
94
+
95
+ on('pickInput', 'click', async () => {
96
+ const fmt = $('format').value;
97
+ const filters = {
98
+ vcf: [{ name: 'VCF', extensions: ['vcf','gz'] }],
99
+ phylip: [{ name: 'PHYLIP', extensions: ['phy','phylip'] }],
100
+ structure: [{ name: 'STRUCTURE', extensions: ['str','stru'] }],
101
+ genepop: [{ name: 'GENEPOP', extensions: ['gen','genepop'] }]
102
+ }[fmt] || [{ name: 'All', extensions: ['*'] }];
103
+ const f = await window.pgsui.pickFile(filters);
104
+ if (f) $('inputPath').value = f;
105
+ });
106
+ on('pickPopmap', 'click', async () => {
107
+ const f = await window.pgsui.pickFile();
108
+ if (f) $('popmapPath').value = f;
109
+ });
110
+ on('pickYaml', 'click', async () => {
111
+ const f = await window.pgsui.pickFile([{ name: 'YAML', extensions: ['yml','yaml'] }]);
112
+ if (f) $('yamlPath').value = f;
113
+ });
114
+ on('pickDump', 'click', async () => {
115
+ const f = await window.pgsui.pickSave('effective.yaml');
116
+ if (f) $('dumpConfigPath').value = f;
117
+ });
118
+ on('pickLogFile', 'click', async () => {
119
+ const f = await window.pgsui.pickSave('pgsui-run.log');
120
+ if (f) $('logFile').value = f;
121
+ });
122
+
123
+ /* ---- streams ---- */
124
+ window.pgsui.onLog(appendLog);
125
+ window.pgsui.onError((e) => appendLog({ stream:'stderr', line: e.message || String(e) }));
126
+ window.pgsui.onExit(({ code }) => { appendLog({ stream:'stdout', line:`Process exited with code ${code}` }); setRunningUI(false); });
127
+ window.pgsui.onStarted(({ argv, cwd }) => {
128
+ appendLog({ stream:'stdout', line:`Started: ${argv?.join(' ') || ''}` });
129
+ appendLog({ stream:'stdout', line:`CWD: ${cwd}` });
130
+ setRunningUI(true);
131
+ });
@@ -0,0 +1,59 @@
1
+ :root{
2
+ --bg:#0b0f14; --panel:#0f1621; --muted:#8aa0b4; --text:#e7eef7;
3
+ --accent:#3aa0ff; --accent-2:#8b5cf6; --danger:#ef4444; --ok:#22c55e;
4
+ --border:#1b2433; --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
5
+ --sans: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
6
+ }
7
+ *{box-sizing:border-box}
8
+ html,body{height:100%}
9
+ body{
10
+ margin:0; background:
11
+ radial-gradient(1200px 600px at 10% -10%, rgba(58,160,255,.12), transparent 60%),
12
+ radial-gradient(900px 500px at 120% -20%, rgba(139,92,246,.10), transparent 60%),
13
+ var(--bg);
14
+ color:var(--text); font-family:var(--sans);
15
+ }
16
+ .titlebar{height:64px; display:flex; align-items:center; padding:12px 20px 0}
17
+ .logo-row{display:flex; gap:12px; align-items:center}
18
+ .logo{width:40px; height:40px; border-radius:10px; box-shadow:0 6px 18px rgba(0,0,0,.4)}
19
+ h1{margin:0; font-weight:700; letter-spacing:.2px}
20
+ .subtitle{margin:2px 0 0; color:var(--muted); font-size:12px}
21
+
22
+ main{padding:12px 20px; max-width:1200px; margin:0 auto}
23
+ .card{
24
+ background:linear-gradient(180deg, rgba(255,255,255,.02), rgba(255,255,255,.00));
25
+ border:1px solid var(--border); border-radius:16px; padding:16px; margin:12px 0;
26
+ box-shadow:0 8px 30px rgba(0,0,0,.25);
27
+ }
28
+ h2{margin:0 0 12px 0; font-size:16px; color:#cfe6ff}
29
+ .muted{color:var(--muted); font-size:12px}
30
+
31
+ .grid{
32
+ display:grid; gap:12px;
33
+ grid-template-columns: repeat(3, minmax(260px, 1fr));
34
+ }
35
+ .grid .colspan{grid-column:1 / -1}
36
+ label{display:block; font-size:12px; color:var(--muted); margin:0 0 6px}
37
+ input,select,textarea{
38
+ width:100%; padding:10px 12px; border-radius:10px; border:1px solid var(--border);
39
+ background:#0d1420; color:var(--text); outline:none;
40
+ }
41
+ input:focus,select:focus,textarea:focus{border-color:var(--accent)}
42
+ textarea{resize:vertical}
43
+ .hstack{display:flex; gap:8px}
44
+ button{
45
+ border:none; border-radius:10px; padding:10px 14px; cursor:pointer; color:#fff;
46
+ background:#1c2433; border:1px solid var(--border);
47
+ }
48
+ button.primary{background:linear-gradient(90deg, var(--accent), var(--accent-2))}
49
+ button.danger{background:linear-gradient(90deg, var(--danger), #f97316)}
50
+ button.ghost{background:transparent; border:1px solid var(--border)}
51
+ button:disabled{opacity:.5; cursor:not-allowed}
52
+ .controls{display:flex; gap:10px; margin:12px 0}
53
+
54
+ .log{
55
+ background:#06090f; color:#e0e8f5; border:1px solid var(--border); border-radius:12px;
56
+ padding:12px; height:340px; overflow:auto; font-family:var(--mono); font-size:12px; line-height:1.35;
57
+ white-space:pre-wrap; word-break:break-word;
58
+ }
59
+ .log .err{color:#ff8a8a}
@@ -0,0 +1,72 @@
1
+ // ui-shim.js: Browser shim for Electron's window.pgsui API
2
+
3
+ (function () {
4
+ function wsConnect(onLog) {
5
+ const ws = new WebSocket(`ws://${location.host}/api/logs`);
6
+ ws.onmessage = (e) => {
7
+ const s = e.data.indexOf('|');
8
+ if (s > 0) {
9
+ const stream = e.data.slice(0, s);
10
+ const line = e.data.slice(s + 1);
11
+ onLog({ stream, line });
12
+ }
13
+ };
14
+ ws.onerror = () => {/* no-op */};
15
+ return ws;
16
+ }
17
+
18
+ const listeners = { log: [], error: [], exit: [], started: [] };
19
+ function emit(type, payload) { listeners[type].forEach(fn => fn(payload)); }
20
+
21
+ let ws = null;
22
+
23
+ const api = {
24
+ // Electron feature parity stubs
25
+ pickFile: async () => null, // Browser cannot provide host filesystem paths
26
+ pickDir: async () => null,
27
+ pickSave: async () => null,
28
+
29
+ // Detect pg-sui presence by calling the API once we start; return optimistic true
30
+ detectPgSui: async () => ({ ok: true }),
31
+
32
+ // Start/stop map to REST
33
+ start: async (payload) => {
34
+ try {
35
+ const r = await fetch('/api/start', {
36
+ method: 'POST', headers: { 'content-type': 'application/json' },
37
+ body: JSON.stringify(payload)
38
+ });
39
+ const j = await r.json();
40
+ if (!j.ok) return j;
41
+ emit('started', { argv: j.argv || [], cwd: j.cwd || '' });
42
+ if (!ws) ws = wsConnect((m) => emit('log', m));
43
+ return { ok: true };
44
+ } catch (e) {
45
+ emit('error', { message: String(e) });
46
+ return { ok: false, error: String(e) };
47
+ }
48
+ },
49
+ stop: async () => {
50
+ try {
51
+ const r = await fetch('/api/stop', { method: 'POST' });
52
+ const j = await r.json();
53
+ if (!j.ok) return j;
54
+ emit('exit', { code: 0 });
55
+ return { ok: true };
56
+ } catch (e) {
57
+ emit('error', { message: String(e) });
58
+ return { ok: false, error: String(e) };
59
+ }
60
+ },
61
+
62
+ // Event wiring
63
+ onLog: (fn) => { listeners.log.push(fn); },
64
+ onError: (fn) => { listeners.error.push(fn); },
65
+ onExit: (fn) => { listeners.exit.push(fn); },
66
+ onStarted: (fn) => { listeners.started.push(fn); },
67
+ };
68
+
69
+ // Expose only if Electron preload didn't define it
70
+ if (!window.pgsui) window.pgsui = api;
71
+
72
+ })();
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ APP_DIR = Path(__file__).resolve().parent / "app"
9
+
10
+
11
+ def _which(cmd: str) -> str | None:
12
+ return shutil.which(cmd)
13
+
14
+
15
+ def main() -> int:
16
+ if not APP_DIR.exists():
17
+ print(f"[pgsui-gui-setup] Missing Electron app at {APP_DIR}", file=sys.stderr)
18
+ return 2
19
+
20
+ node = _which("node")
21
+ npm = _which("npm")
22
+ if not node or not npm:
23
+ print(
24
+ "[pgsui-gui-setup] Node.js and npm are required. Install from https://nodejs.org/",
25
+ file=sys.stderr,
26
+ )
27
+ return 2
28
+
29
+ # Prefer deterministic installs if package-lock.json exists
30
+ lock = APP_DIR / "package-lock.json"
31
+ cmd = ["npm", "ci"] if lock.exists() else ["npm", "install"]
32
+ try:
33
+ print(f"[pgsui-gui-setup] Running: {' '.join(cmd)} in {APP_DIR}")
34
+ subprocess.check_call(cmd, cwd=str(APP_DIR))
35
+ print("[pgsui-gui-setup] Done.")
36
+ return 0
37
+ except subprocess.CalledProcessError as e:
38
+ print(f"[pgsui-gui-setup] Failed: {e}", file=sys.stderr)
39
+ return e.returncode
40
+
41
+
42
+ if __name__ == "__main__":
43
+ raise SystemExit(main())
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ APP_DIR = Path(__file__).resolve().parent / "app"
10
+
11
+
12
+ def _bin(path: Path, name: str) -> Path:
13
+ # node_modules/.bin on POSIX, node_modules\\.bin on Windows
14
+ d = path / ("node_modules/.bin" if os.name != "nt" else r"node_modules\.bin")
15
+ exe = name + (".cmd" if os.name == "nt" else "")
16
+ return d / exe
17
+
18
+
19
+ def main() -> int:
20
+ if not APP_DIR.exists():
21
+ print(f"[pgsui-gui] Missing Electron app at {APP_DIR}", file=sys.stderr)
22
+ return 2
23
+
24
+ # Resolve electron binary: local install first, else global npx fallback
25
+ local_electron = _bin(APP_DIR, "electron")
26
+ npx = shutil.which("npx")
27
+
28
+ env = os.environ.copy()
29
+ env.setdefault("PGSUI_PYTHON", sys.executable)
30
+ env.setdefault("PGSUI_CLI_DEFAULT", str(Path(__file__).resolve().parents[1] / "cli.py"))
31
+
32
+ try:
33
+ if local_electron.exists():
34
+ cmd = [str(local_electron), "."]
35
+ proc = subprocess.Popen(cmd, cwd=str(APP_DIR), env=env)
36
+ elif npx:
37
+ # Uses Electron from registry on demand
38
+ cmd = ["npx", "electron", "."]
39
+ proc = subprocess.Popen(cmd, cwd=str(APP_DIR), env=env)
40
+ else:
41
+ print(
42
+ "[pgsui-gui] Electron is not installed. Run: pgsui-gui-setup",
43
+ file=sys.stderr,
44
+ )
45
+ return 2
46
+
47
+ proc.wait()
48
+ return proc.returncode or 0
49
+ except KeyboardInterrupt:
50
+ return 130
51
+ except FileNotFoundError as e:
52
+ print(f"[pgsui-gui] Failed to start Electron: {e}", file=sys.stderr)
53
+ return 2
54
+
55
+
56
+ if __name__ == "__main__":
57
+ raise SystemExit(main())
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "pgsui-gui-root",
3
+ "private": true,
4
+ "scripts": {
5
+ "start": "electron app",
6
+ "dist": "electron-builder -mwl --projectDir app",
7
+ "icons": "electron-icon-builder --input=app/ui/logo.png --output=app/icons --flatten"
8
+ },
9
+ "devDependencies": {
10
+ "electron": "^38.4.0",
11
+ "electron-builder": "^24.13.3",
12
+ "electron-icon-builder": "^2.0.1"
13
+ }
14
+ }
File without changes
File without changes
File without changes
File without changes