pg-sui 1.0.2.1__py3-none-any.whl → 1.6.8__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.

Potentially problematic release.


This version of pg-sui might be problematic. Click here for more details.

Files changed (112) hide show
  1. {pg_sui-1.0.2.1.dist-info → pg_sui-1.6.8.dist-info}/METADATA +51 -70
  2. pg_sui-1.6.8.dist-info/RECORD +78 -0
  3. {pg_sui-1.0.2.1.dist-info → pg_sui-1.6.8.dist-info}/WHEEL +1 -1
  4. pg_sui-1.6.8.dist-info/entry_points.txt +4 -0
  5. pg_sui-1.6.8.dist-info/top_level.txt +1 -0
  6. pgsui/__init__.py +35 -54
  7. pgsui/_version.py +34 -0
  8. pgsui/cli.py +635 -0
  9. pgsui/data_processing/config.py +576 -0
  10. pgsui/data_processing/containers.py +1782 -0
  11. pgsui/data_processing/transformers.py +121 -1103
  12. pgsui/electron/app/__main__.py +5 -0
  13. pgsui/electron/app/icons/icons/1024x1024.png +0 -0
  14. pgsui/electron/app/icons/icons/128x128.png +0 -0
  15. pgsui/electron/app/icons/icons/16x16.png +0 -0
  16. pgsui/electron/app/icons/icons/24x24.png +0 -0
  17. pgsui/electron/app/icons/icons/256x256.png +0 -0
  18. pgsui/electron/app/icons/icons/32x32.png +0 -0
  19. pgsui/electron/app/icons/icons/48x48.png +0 -0
  20. pgsui/electron/app/icons/icons/512x512.png +0 -0
  21. pgsui/electron/app/icons/icons/64x64.png +0 -0
  22. pgsui/electron/app/icons/icons/icon.icns +0 -0
  23. pgsui/electron/app/icons/icons/icon.ico +0 -0
  24. pgsui/electron/app/main.js +189 -0
  25. pgsui/electron/app/package-lock.json +6893 -0
  26. pgsui/electron/app/package.json +50 -0
  27. pgsui/electron/app/preload.js +15 -0
  28. pgsui/electron/app/server.py +146 -0
  29. pgsui/electron/app/ui/logo.png +0 -0
  30. pgsui/electron/app/ui/renderer.js +130 -0
  31. pgsui/electron/app/ui/styles.css +59 -0
  32. pgsui/electron/app/ui/ui_shim.js +72 -0
  33. pgsui/electron/bootstrap.py +43 -0
  34. pgsui/electron/launch.py +59 -0
  35. pgsui/electron/package.json +14 -0
  36. pgsui/example_data/popmaps/{test.popmap → phylogen_nomx.popmap} +185 -99
  37. pgsui/example_data/vcf_files/phylogen_subset14K.vcf.gz +0 -0
  38. pgsui/example_data/vcf_files/phylogen_subset14K.vcf.gz.tbi +0 -0
  39. pgsui/impute/deterministic/imputers/allele_freq.py +691 -0
  40. pgsui/impute/deterministic/imputers/mode.py +679 -0
  41. pgsui/impute/deterministic/imputers/nmf.py +221 -0
  42. pgsui/impute/deterministic/imputers/phylo.py +971 -0
  43. pgsui/impute/deterministic/imputers/ref_allele.py +530 -0
  44. pgsui/impute/supervised/base.py +339 -0
  45. pgsui/impute/supervised/imputers/hist_gradient_boosting.py +293 -0
  46. pgsui/impute/supervised/imputers/random_forest.py +287 -0
  47. pgsui/impute/unsupervised/base.py +924 -0
  48. pgsui/impute/unsupervised/callbacks.py +89 -263
  49. pgsui/impute/unsupervised/imputers/autoencoder.py +972 -0
  50. pgsui/impute/unsupervised/imputers/nlpca.py +1264 -0
  51. pgsui/impute/unsupervised/imputers/ubp.py +1288 -0
  52. pgsui/impute/unsupervised/imputers/vae.py +957 -0
  53. pgsui/impute/unsupervised/loss_functions.py +158 -0
  54. pgsui/impute/unsupervised/models/autoencoder_model.py +208 -558
  55. pgsui/impute/unsupervised/models/nlpca_model.py +149 -468
  56. pgsui/impute/unsupervised/models/ubp_model.py +198 -1317
  57. pgsui/impute/unsupervised/models/vae_model.py +259 -618
  58. pgsui/impute/unsupervised/nn_scorers.py +215 -0
  59. pgsui/utils/classification_viz.py +591 -0
  60. pgsui/utils/misc.py +35 -480
  61. pgsui/utils/plotting.py +514 -824
  62. pgsui/utils/scorers.py +212 -438
  63. pg_sui-1.0.2.1.dist-info/RECORD +0 -75
  64. pg_sui-1.0.2.1.dist-info/top_level.txt +0 -3
  65. pgsui/example_data/phylip_files/test_n10.phy +0 -118
  66. pgsui/example_data/phylip_files/test_n100.phy +0 -118
  67. pgsui/example_data/phylip_files/test_n2.phy +0 -118
  68. pgsui/example_data/phylip_files/test_n500.phy +0 -118
  69. pgsui/example_data/structure_files/test.nopops.1row.10sites.str +0 -117
  70. pgsui/example_data/structure_files/test.nopops.2row.100sites.str +0 -234
  71. pgsui/example_data/structure_files/test.nopops.2row.10sites.str +0 -234
  72. pgsui/example_data/structure_files/test.nopops.2row.30sites.str +0 -234
  73. pgsui/example_data/structure_files/test.nopops.2row.allsites.str +0 -234
  74. pgsui/example_data/structure_files/test.pops.1row.10sites.str +0 -117
  75. pgsui/example_data/structure_files/test.pops.2row.10sites.str +0 -234
  76. pgsui/example_data/trees/test.iqtree +0 -376
  77. pgsui/example_data/trees/test.qmat +0 -5
  78. pgsui/example_data/trees/test.rate +0 -2033
  79. pgsui/example_data/trees/test.tre +0 -1
  80. pgsui/example_data/trees/test_n10.rate +0 -19
  81. pgsui/example_data/trees/test_n100.rate +0 -109
  82. pgsui/example_data/trees/test_n500.rate +0 -509
  83. pgsui/example_data/trees/test_siterates.txt +0 -2024
  84. pgsui/example_data/trees/test_siterates_n10.txt +0 -10
  85. pgsui/example_data/trees/test_siterates_n100.txt +0 -100
  86. pgsui/example_data/trees/test_siterates_n500.txt +0 -500
  87. pgsui/example_data/vcf_files/test.vcf +0 -244
  88. pgsui/example_data/vcf_files/test.vcf.gz +0 -0
  89. pgsui/example_data/vcf_files/test.vcf.gz.tbi +0 -0
  90. pgsui/impute/estimators.py +0 -735
  91. pgsui/impute/impute.py +0 -1486
  92. pgsui/impute/simple_imputers.py +0 -1439
  93. pgsui/impute/supervised/iterative_imputer_fixedparams.py +0 -785
  94. pgsui/impute/supervised/iterative_imputer_gridsearch.py +0 -1027
  95. pgsui/impute/unsupervised/keras_classifiers.py +0 -702
  96. pgsui/impute/unsupervised/models/in_development/cnn_model.py +0 -486
  97. pgsui/impute/unsupervised/neural_network_imputers.py +0 -1424
  98. pgsui/impute/unsupervised/neural_network_methods.py +0 -1549
  99. pgsui/pg_sui.py +0 -261
  100. pgsui/utils/sequence_tools.py +0 -407
  101. simulation/sim_benchmarks.py +0 -333
  102. simulation/sim_treeparams.py +0 -475
  103. test/__init__.py +0 -0
  104. test/pg_sui_simtest.py +0 -215
  105. test/pg_sui_testing.py +0 -523
  106. test/test.py +0 -297
  107. test/test_pgsui.py +0 -374
  108. test/test_tkc.py +0 -214
  109. {pg_sui-1.0.2.1.dist-info → pg_sui-1.6.8.dist-info/licenses}/LICENSE +0 -0
  110. /pgsui/{example_data/trees → electron/app}/__init__.py +0 -0
  111. /pgsui/impute/{unsupervised/models/in_development → supervised/imputers}/__init__.py +0 -0
  112. {simulation → pgsui/impute/unsupervised/imputers}/__init__.py +0 -0
@@ -0,0 +1,50 @@
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": "../../pgsui/cli.py", "to": "pgsui/cli.py" }
28
+ ],
29
+ "files": [
30
+ "main.js",
31
+ "preload.js",
32
+ "ui/**/*",
33
+ "icons/**/*"
34
+ ],
35
+ "mac": {
36
+ "target": ["dmg", "zip"],
37
+ "category": "public.app-category.science",
38
+ "icon": "icons/icon.icns"
39
+ },
40
+ "linux": {
41
+ "target": ["AppImage", "deb"],
42
+ "category": "Science",
43
+ "icon": "icons"
44
+ },
45
+ "win": {
46
+ "target": ["nsis"],
47
+ "icon": "icons/icon.ico"
48
+ }
49
+ }
50
+ }
@@ -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,146 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import signal
6
+ from pathlib import Path
7
+
8
+ from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
9
+ from fastapi.responses import HTMLResponse
10
+ from fastapi.staticfiles import StaticFiles
11
+
12
+ ROOT = Path(__file__).resolve().parent / "ui"
13
+ WORK = Path("/work")
14
+ app = FastAPI()
15
+ _proc = None
16
+ _log_queue: asyncio.Queue[str] = asyncio.Queue()
17
+
18
+
19
+ def _build_args(p: dict[str, str | int | bool | list[str] | None]) -> list[str]:
20
+ a: list[str] = []
21
+
22
+ def kv(flag, v):
23
+ if v not in (None, "", []):
24
+ a.extend([flag, str(v)])
25
+
26
+ def fl(flag, cond):
27
+ if cond:
28
+ a.append(flag)
29
+
30
+ kv("--input", p.get("inputPath"))
31
+ kv("--format", p.get("format"))
32
+ kv("--popmap", p.get("popmapPath"))
33
+ kv("--prefix", p.get("prefix"))
34
+ kv("--config", p.get("yamlPath"))
35
+ kv("--dump-config", p.get("dumpConfigPath"))
36
+ kv("--preset", p.get("preset") or "fast")
37
+ m = p.get("models") or []
38
+ if m:
39
+ a.extend(["--models", *m])
40
+ inc = p.get("includePops") or []
41
+ if inc:
42
+ a.extend(["--include-pops", *inc])
43
+ fl("--tune", bool(p.get("tune")))
44
+ if p.get("tuneNTrials"):
45
+ kv("--tune-n-trials", p["tuneNTrials"])
46
+ kv("--batch-size", p.get("batchSize") or 64)
47
+ dev = p.get("device")
48
+ if dev in ("cpu", "cuda", "mps"):
49
+ kv("--device", dev)
50
+ if p.get("nJobs"):
51
+ kv("--n-jobs", p["nJobs"])
52
+ pf = p.get("plotFormat")
53
+ if pf in ("png", "pdf", "svg"):
54
+ kv("--plot-format", pf)
55
+ if p.get("seed"):
56
+ kv("--seed", p["seed"])
57
+ fl("--verbose", bool(p.get("verbose")))
58
+ if p.get("logFile"):
59
+ kv("--log-file", p["logFile"])
60
+ fl("--force-popmap", bool(p.get("forcePopmap")))
61
+ fl("--dry-run", bool(p.get("dryRun")))
62
+ for kvp in p.get("setPairs") or []:
63
+ if isinstance(kvp, str) and "=" in kvp:
64
+ a.extend(["--set", kvp])
65
+ return a
66
+
67
+
68
+ @app.get("/api/status")
69
+ def status():
70
+ return {"running": _proc is not None}
71
+
72
+
73
+ @app.get("/api/ls")
74
+ def ls(path: str = Query("/work")):
75
+ p = Path(path).resolve()
76
+ if not str(p).startswith(str(WORK)):
77
+ return {"ok": False, "error": "out_of_root"}
78
+ items = [
79
+ {"name": e.name, "path": str(e), "dir": e.is_dir()} for e in sorted(p.iterdir())
80
+ ]
81
+ return {"ok": True, "cwd": str(p), "items": items}
82
+
83
+
84
+ @app.post("/api/start")
85
+ async def start(payload: dict):
86
+ global _proc
87
+ if _proc:
88
+ return {"ok": False, "error": "already_running"}
89
+ cwd = Path(payload.get("cwd") or "/work").resolve()
90
+ if not str(cwd).startswith(str(WORK)):
91
+ return {"ok": False, "error": "cwd_must_be_under_/work"}
92
+ args = _build_args(payload)
93
+ use_pg = bool(payload.get("usePgSui", True))
94
+ if use_pg:
95
+ cmd = ["pg-sui", *args]
96
+ else:
97
+ py = payload.get("pythonPath") or "python3"
98
+ cli = payload.get("cliPath")
99
+ if not cli:
100
+ return {"ok": False, "error": "cli_path_required"}
101
+ cmd = [py, cli, *args]
102
+ _proc = await asyncio.create_subprocess_exec(
103
+ *cmd,
104
+ cwd=str(cwd),
105
+ stdout=asyncio.subprocess.PIPE,
106
+ stderr=asyncio.subprocess.PIPE,
107
+ env=os.environ.copy(),
108
+ )
109
+
110
+ async def pump(stream, tag):
111
+ async for line in stream:
112
+ await _log_queue.put(f"{tag}|{line.decode(errors='ignore').rstrip()}")
113
+
114
+ asyncio.create_task(pump(_proc.stdout, "stdout"))
115
+ asyncio.create_task(pump(_proc.stderr, "stderr"))
116
+ return {"ok": True, "argv": cmd, "cwd": str(cwd)}
117
+
118
+
119
+ @app.post("/api/stop")
120
+ async def stop():
121
+ global _proc
122
+ if not _proc:
123
+ return {"ok": False, "error": "not_running"}
124
+ try:
125
+ _proc.send_signal(signal.SIGTERM)
126
+ try:
127
+ await asyncio.wait_for(_proc.wait(), timeout=5)
128
+ except asyncio.TimeoutError:
129
+ _proc.kill()
130
+ finally:
131
+ _proc = None
132
+ return {"ok": True}
133
+
134
+
135
+ @app.websocket("/api/logs")
136
+ async def logs(ws: WebSocket):
137
+ await ws.accept()
138
+ try:
139
+ while True:
140
+ msg = await _log_queue.get()
141
+ await ws.send_text(msg)
142
+ except WebSocketDisconnect:
143
+ return
144
+
145
+
146
+ app.mount("/", StaticFiles(directory=ROOT, html=True), name="ui")
Binary file
@@ -0,0 +1,130 @@
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
+ models: modelsSel,
32
+ includePops,
33
+ device: $('device').value || undefined,
34
+ batchSize: bs,
35
+ nJobs: $('nJobs').value ? Number($('nJobs').value) : undefined,
36
+ plotFormat: $('plotFormat').value || undefined,
37
+ seed: $('seed').value.trim() || undefined,
38
+ verbose: $('verbose').checked,
39
+ forcePopmap: $('forcePopmap').checked,
40
+ dryRun: $('dryRun').checked,
41
+ setPairs,
42
+ logFile: $('logFile').value.trim() || undefined,
43
+ tune: $('tune').checked,
44
+ tuneNTrials: $('tune').checked ? (Number($('tuneNTrials').value) || 50) : undefined
45
+ };
46
+ }
47
+
48
+ function setRunningUI(isRunning) {
49
+ $('start').disabled = isRunning;
50
+ $('stop').disabled = !isRunning;
51
+ document.querySelectorAll('button').forEach(b => { if (b.id !== 'stop') b.disabled = isRunning; });
52
+ }
53
+
54
+ /* ---- init small UI bits ---- */
55
+ (() => {
56
+ const tune = $('tune'), tuneOpts = $('tuneOpts');
57
+ if (tune && tuneOpts) tune.addEventListener('change', () => { tuneOpts.style.display = tune.checked ? '' : 'none'; });
58
+ const logoEl = $('logo');
59
+ if (logoEl) logoEl.addEventListener('error', () => { logoEl.style.display = 'none'; });
60
+ })();
61
+
62
+ /* ---- events (wire exactly once) ---- */
63
+ const on = (id, ev, fn) => { const el = $(id); if (el) el.addEventListener(ev, fn); };
64
+
65
+ on('pickCwd', 'click', async () => {
66
+ const d = await window.pgsui.pickDir();
67
+ if (d) $('cwd').value = d;
68
+ });
69
+
70
+ on('start', 'click', async () => {
71
+ try {
72
+ if (!window.pgsui) { appendLog({ stream:'stderr', line:'Bridge missing (preload).' }); return; }
73
+ logEl.textContent = '';
74
+ const payload = collectPayload();
75
+ if (!payload.cwd) { appendLog({ stream:'stderr', line:'Working directory is required.' }); return; }
76
+ if (!payload.inputPath) { appendLog({ stream:'stderr', line:'Input file required.' }); return; }
77
+ if (!payload.cliPath) {
78
+ const r = await window.pgsui.defaultCli?.();
79
+ if (r?.ok && r.path) payload.cliPath = r.path;
80
+ }
81
+ if (!payload.cliPath) { appendLog({ stream:'stderr', line:'Could not find <project_root>/pgsui/cli.py. Set PGSUI_CLI_DEFAULT or adjust repo layout.' }); return; }
82
+ const res = await window.pgsui.start(payload);
83
+ if (!res?.ok) appendLog({ stream:'stderr', line:`Start failed: ${res?.error || 'unknown error'}` });
84
+ } catch (e) {
85
+ appendLog({ stream:'stderr', line:`Start exception: ${e?.message || String(e)}` });
86
+ }
87
+ });
88
+
89
+ on('stop', 'click', async () => {
90
+ const res = await window.pgsui.stop();
91
+ if (!res.ok) appendLog({ stream:'stderr', line: res.error });
92
+ });
93
+
94
+ on('pickInput', 'click', async () => {
95
+ const fmt = $('format').value;
96
+ const filters = {
97
+ vcf: [{ name: 'VCF', extensions: ['vcf','gz'] }],
98
+ phylip: [{ name: 'PHYLIP', extensions: ['phy','phylip'] }],
99
+ structure: [{ name: 'STRUCTURE', extensions: ['str','stru'] }],
100
+ genepop: [{ name: 'GENEPOP', extensions: ['gen','genepop'] }]
101
+ }[fmt] || [{ name: 'All', extensions: ['*'] }];
102
+ const f = await window.pgsui.pickFile(filters);
103
+ if (f) $('inputPath').value = f;
104
+ });
105
+ on('pickPopmap', 'click', async () => {
106
+ const f = await window.pgsui.pickFile();
107
+ if (f) $('popmapPath').value = f;
108
+ });
109
+ on('pickYaml', 'click', async () => {
110
+ const f = await window.pgsui.pickFile([{ name: 'YAML', extensions: ['yml','yaml'] }]);
111
+ if (f) $('yamlPath').value = f;
112
+ });
113
+ on('pickDump', 'click', async () => {
114
+ const f = await window.pgsui.pickSave('effective.yaml');
115
+ if (f) $('dumpConfigPath').value = f;
116
+ });
117
+ on('pickLogFile', 'click', async () => {
118
+ const f = await window.pgsui.pickSave('pgsui-run.log');
119
+ if (f) $('logFile').value = f;
120
+ });
121
+
122
+ /* ---- streams ---- */
123
+ window.pgsui.onLog(appendLog);
124
+ window.pgsui.onError((e) => appendLog({ stream:'stderr', line: e.message || String(e) }));
125
+ window.pgsui.onExit(({ code }) => { appendLog({ stream:'stdout', line:`Process exited with code ${code}` }); setRunningUI(false); });
126
+ window.pgsui.onStarted(({ argv, cwd }) => {
127
+ appendLog({ stream:'stdout', line:`Started: ${argv?.join(' ') || ''}` });
128
+ appendLog({ stream:'stdout', line:`CWD: ${cwd}` });
129
+ setRunningUI(true);
130
+ });
@@ -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,59 @@
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
+ # Provide defaults the renderer can read if desired
30
+ # Your GUI already lets users browse for cli.py and vcf paths.
31
+ # You can prefill CLI path here if you want:
32
+ # env["PGSUI_CLI_DEFAULT"] = str(Path(__file__).resolve().parents[1] / "cli.py")
33
+
34
+ try:
35
+ if local_electron.exists():
36
+ cmd = [str(local_electron), "."]
37
+ proc = subprocess.Popen(cmd, cwd=str(APP_DIR), env=env)
38
+ elif npx:
39
+ # Uses Electron from registry on demand
40
+ cmd = ["npx", "electron", "."]
41
+ proc = subprocess.Popen(cmd, cwd=str(APP_DIR), env=env)
42
+ else:
43
+ print(
44
+ "[pgsui-gui] Electron is not installed. Run: pgsui-gui-setup",
45
+ file=sys.stderr,
46
+ )
47
+ return 2
48
+
49
+ proc.wait()
50
+ return proc.returncode or 0
51
+ except KeyboardInterrupt:
52
+ return 130
53
+ except FileNotFoundError as e:
54
+ print(f"[pgsui-gui] Failed to start Electron: {e}", file=sys.stderr)
55
+ return 2
56
+
57
+
58
+ if __name__ == "__main__":
59
+ 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
+ }