pg-sui 0.2.3__py3-none-any.whl → 1.6.14.dev9__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 (127) hide show
  1. {pg_sui-0.2.3.dist-info → pg_sui-1.6.14.dev9.dist-info}/METADATA +99 -77
  2. pg_sui-1.6.14.dev9.dist-info/RECORD +81 -0
  3. {pg_sui-0.2.3.dist-info → pg_sui-1.6.14.dev9.dist-info}/WHEEL +1 -1
  4. pg_sui-1.6.14.dev9.dist-info/entry_points.txt +4 -0
  5. {pg_sui-0.2.3.dist-info → pg_sui-1.6.14.dev9.dist-info/licenses}/LICENSE +0 -0
  6. pg_sui-1.6.14.dev9.dist-info/top_level.txt +1 -0
  7. pgsui/__init__.py +35 -54
  8. pgsui/_version.py +34 -0
  9. pgsui/cli.py +909 -0
  10. pgsui/data_processing/__init__.py +0 -0
  11. pgsui/data_processing/config.py +565 -0
  12. pgsui/data_processing/containers.py +1424 -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 +1118 -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 +1285 -0
  65. pgsui/impute/unsupervised/imputers/nlpca.py +1554 -0
  66. pgsui/impute/unsupervised/imputers/ubp.py +1575 -0
  67. pgsui/impute/unsupervised/imputers/vae.py +1228 -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/RECORD +0 -75
  83. pg_sui-0.2.3.dist-info/top_level.txt +0 -3
  84. pgsui/example_data/phylip_files/test_n10.phy +0 -118
  85. pgsui/example_data/phylip_files/test_n100.phy +0 -118
  86. pgsui/example_data/phylip_files/test_n2.phy +0 -118
  87. pgsui/example_data/phylip_files/test_n500.phy +0 -118
  88. pgsui/example_data/structure_files/test.nopops.1row.10sites.str +0 -117
  89. pgsui/example_data/structure_files/test.nopops.2row.100sites.str +0 -234
  90. pgsui/example_data/structure_files/test.nopops.2row.10sites.str +0 -234
  91. pgsui/example_data/structure_files/test.nopops.2row.30sites.str +0 -234
  92. pgsui/example_data/structure_files/test.nopops.2row.allsites.str +0 -234
  93. pgsui/example_data/structure_files/test.pops.1row.10sites.str +0 -117
  94. pgsui/example_data/structure_files/test.pops.2row.10sites.str +0 -234
  95. pgsui/example_data/trees/test.iqtree +0 -376
  96. pgsui/example_data/trees/test.qmat +0 -5
  97. pgsui/example_data/trees/test.rate +0 -2033
  98. pgsui/example_data/trees/test.tre +0 -1
  99. pgsui/example_data/trees/test_n10.rate +0 -19
  100. pgsui/example_data/trees/test_n100.rate +0 -109
  101. pgsui/example_data/trees/test_n500.rate +0 -509
  102. pgsui/example_data/trees/test_siterates.txt +0 -2024
  103. pgsui/example_data/trees/test_siterates_n10.txt +0 -10
  104. pgsui/example_data/trees/test_siterates_n100.txt +0 -100
  105. pgsui/example_data/trees/test_siterates_n500.txt +0 -500
  106. pgsui/example_data/vcf_files/test.vcf +0 -244
  107. pgsui/example_data/vcf_files/test.vcf.gz +0 -0
  108. pgsui/example_data/vcf_files/test.vcf.gz.tbi +0 -0
  109. pgsui/impute/estimators.py +0 -1268
  110. pgsui/impute/impute.py +0 -1463
  111. pgsui/impute/simple_imputers.py +0 -1431
  112. pgsui/impute/supervised/iterative_imputer_fixedparams.py +0 -782
  113. pgsui/impute/supervised/iterative_imputer_gridsearch.py +0 -1024
  114. pgsui/impute/unsupervised/keras_classifiers.py +0 -697
  115. pgsui/impute/unsupervised/models/in_development/cnn_model.py +0 -486
  116. pgsui/impute/unsupervised/neural_network_imputers.py +0 -1440
  117. pgsui/impute/unsupervised/neural_network_methods.py +0 -1395
  118. pgsui/pg_sui.py +0 -261
  119. pgsui/utils/sequence_tools.py +0 -407
  120. simulation/sim_benchmarks.py +0 -333
  121. simulation/sim_treeparams.py +0 -475
  122. test/__init__.py +0 -0
  123. test/pg_sui_simtest.py +0 -215
  124. test/pg_sui_testing.py +0 -523
  125. test/test.py +0 -151
  126. test/test_pgsui.py +0 -374
  127. 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