TSUMUGI 1.0.1__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.
- TSUMUGI/annotator.py +103 -0
- TSUMUGI/argparser.py +599 -0
- TSUMUGI/core.py +185 -0
- TSUMUGI/data/impc_phenodigm.csv +3406 -0
- TSUMUGI/data/mp.obo +143993 -0
- TSUMUGI/filterer.py +36 -0
- TSUMUGI/formatter.py +122 -0
- TSUMUGI/genewise_annotation_builder.py +94 -0
- TSUMUGI/io_handler.py +189 -0
- TSUMUGI/main.py +300 -0
- TSUMUGI/network_constructor.py +603 -0
- TSUMUGI/ontology_handler.py +62 -0
- TSUMUGI/pairwise_similarity_builder.py +66 -0
- TSUMUGI/report_generator.py +122 -0
- TSUMUGI/similarity_calculator.py +498 -0
- TSUMUGI/subcommands/count_filterer.py +47 -0
- TSUMUGI/subcommands/genes_filterer.py +89 -0
- TSUMUGI/subcommands/graphml_builder.py +158 -0
- TSUMUGI/subcommands/life_stage_filterer.py +48 -0
- TSUMUGI/subcommands/mp_filterer.py +142 -0
- TSUMUGI/subcommands/score_filterer.py +22 -0
- TSUMUGI/subcommands/sex_filterer.py +48 -0
- TSUMUGI/subcommands/webapp_builder.py +358 -0
- TSUMUGI/subcommands/zygosity_filterer.py +48 -0
- TSUMUGI/validator.py +65 -0
- TSUMUGI/web/app/css/app.css +1129 -0
- TSUMUGI/web/app/genelist/network_genelist.html +339 -0
- TSUMUGI/web/app/genelist/network_genelist.js +421 -0
- TSUMUGI/web/app/js/data/dataLoader.js +41 -0
- TSUMUGI/web/app/js/export/graphExporter.js +214 -0
- TSUMUGI/web/app/js/graph/centrality.js +495 -0
- TSUMUGI/web/app/js/graph/components.js +30 -0
- TSUMUGI/web/app/js/graph/filters.js +158 -0
- TSUMUGI/web/app/js/graph/highlighter.js +52 -0
- TSUMUGI/web/app/js/graph/layoutController.js +454 -0
- TSUMUGI/web/app/js/graph/valueScaler.js +43 -0
- TSUMUGI/web/app/js/search/geneSearcher.js +93 -0
- TSUMUGI/web/app/js/search/phenotypeSearcher.js +292 -0
- TSUMUGI/web/app/js/ui/dynamicFontSize.js +30 -0
- TSUMUGI/web/app/js/ui/mobilePanel.js +77 -0
- TSUMUGI/web/app/js/ui/slider.js +22 -0
- TSUMUGI/web/app/js/ui/tooltips.js +514 -0
- TSUMUGI/web/app/js/viewer/pageSetup.js +217 -0
- TSUMUGI/web/app/viewer.html +515 -0
- TSUMUGI/web/app/viewer.js +1593 -0
- TSUMUGI/web/css/sanitize.css +363 -0
- TSUMUGI/web/css/top.css +391 -0
- TSUMUGI/web/image/tsumugi-favicon.ico +0 -0
- TSUMUGI/web/image/tsumugi-icon.png +0 -0
- TSUMUGI/web/image/tsumugi-logo.png +0 -0
- TSUMUGI/web/image/tsumugi-logo.svg +69 -0
- TSUMUGI/web/js/genelist_formatter.js +123 -0
- TSUMUGI/web/js/top.js +338 -0
- TSUMUGI/web/open_webapp_linux.sh +25 -0
- TSUMUGI/web/open_webapp_mac.command +25 -0
- TSUMUGI/web/open_webapp_windows.bat +37 -0
- TSUMUGI/web/serve_index.py +110 -0
- TSUMUGI/web/template/template_index.html +197 -0
- TSUMUGI/web_deployer.py +150 -0
- tsumugi-1.0.1.dist-info/METADATA +504 -0
- tsumugi-1.0.1.dist-info/RECORD +64 -0
- tsumugi-1.0.1.dist-info/WHEEL +4 -0
- tsumugi-1.0.1.dist-info/entry_points.txt +3 -0
- tsumugi-1.0.1.dist-info/licenses/LICENSE +21 -0
TSUMUGI/web/js/top.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// Track which search mode is active (defaults to 'phenotype')
|
|
2
|
+
let searchMode = "phenotype";
|
|
3
|
+
|
|
4
|
+
const geneListPlaceHolder = "Trappc11\r\nRab10\r\nInts8\r\nVrk1\r\nSox4"; // Example content for the placeholder
|
|
5
|
+
|
|
6
|
+
// ====================================================================
|
|
7
|
+
// Handle tab switching and keep searchMode in sync
|
|
8
|
+
// ====================================================================
|
|
9
|
+
function setSearchMode(mode) {
|
|
10
|
+
searchMode = mode;
|
|
11
|
+
|
|
12
|
+
document.getElementById("phenotypeSection").style.display = mode === "phenotype" ? "block" : "none";
|
|
13
|
+
document.getElementById("geneSection").style.display = mode === "gene" ? "block" : "none";
|
|
14
|
+
document.getElementById("geneListSection").style.display = mode === "geneList" ? "block" : "none";
|
|
15
|
+
|
|
16
|
+
// Update tab button styles
|
|
17
|
+
document.querySelectorAll(".Tab").forEach((tabButton) => {
|
|
18
|
+
tabButton.classList.remove("active-tab");
|
|
19
|
+
});
|
|
20
|
+
document.querySelectorAll(`button[data-tab="${mode}"]`).forEach((tabButton) => {
|
|
21
|
+
tabButton.classList.add("active-tab");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Reset all input fields
|
|
25
|
+
document.querySelectorAll('input[type="text"], textarea').forEach((input) => {
|
|
26
|
+
input.value = "";
|
|
27
|
+
});
|
|
28
|
+
document.querySelectorAll("ul.suggestions").forEach((ul) => {
|
|
29
|
+
ul.innerHTML = "";
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Prefill the textarea when the Gene List tab is selected
|
|
33
|
+
const geneListTextarea = document.getElementById("geneList");
|
|
34
|
+
if (mode === "geneList") {
|
|
35
|
+
geneListTextarea.value = geneListPlaceHolder;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Toggle the correct submit button
|
|
39
|
+
const submitBtn = document.getElementById("submitBtn");
|
|
40
|
+
const submitBtnList = document.getElementById("submitBtn_List");
|
|
41
|
+
|
|
42
|
+
submitBtn.style.display = mode === "geneList" ? "none" : "inline-block";
|
|
43
|
+
submitBtnList.style.display = mode === "geneList" ? "inline-block" : "none";
|
|
44
|
+
|
|
45
|
+
// Reset the submit buttons according to the active mode
|
|
46
|
+
if (mode === "geneList") {
|
|
47
|
+
submitBtnList.disabled = true;
|
|
48
|
+
} else {
|
|
49
|
+
submitBtn.disabled = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (mode === "geneList") {
|
|
53
|
+
checkGeneListInput();
|
|
54
|
+
} else {
|
|
55
|
+
checkValidInput();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Disable the submit button when the Gene List textarea is empty
|
|
60
|
+
function checkGeneListInput() {
|
|
61
|
+
const geneListTextarea = document.getElementById("geneList");
|
|
62
|
+
const submitBtnList = document.getElementById("submitBtn_List");
|
|
63
|
+
|
|
64
|
+
if (geneListTextarea.value.trim() === "") {
|
|
65
|
+
submitBtnList.disabled = true;
|
|
66
|
+
} else {
|
|
67
|
+
submitBtnList.disabled = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ====================================================================
|
|
72
|
+
// Fetch JSON data from the URL and assign to phenotypes
|
|
73
|
+
// ====================================================================
|
|
74
|
+
|
|
75
|
+
const URL_MP_TERMS = "./data/available_mp_terms.json";
|
|
76
|
+
const URL_GENE_SYMBOLS = "./data/available_gene_symbols.txt";
|
|
77
|
+
|
|
78
|
+
// Track when loading the supporting data completes
|
|
79
|
+
let phenotypesLoaded = fetch(URL_MP_TERMS)
|
|
80
|
+
.then((response) => response.json())
|
|
81
|
+
.then((data) => {
|
|
82
|
+
phenotypes = data;
|
|
83
|
+
})
|
|
84
|
+
.catch((error) => console.error("Error fetching phenotypes:", error));
|
|
85
|
+
|
|
86
|
+
let geneSymbolsLoaded = fetch(URL_GENE_SYMBOLS)
|
|
87
|
+
.then((response) => response.text())
|
|
88
|
+
.then((data) => {
|
|
89
|
+
geneSymbols = data.split("\n").reduce((acc, symbol) => {
|
|
90
|
+
acc[symbol.trim()] = null;
|
|
91
|
+
return acc;
|
|
92
|
+
}, {});
|
|
93
|
+
})
|
|
94
|
+
.catch((error) => console.error("Error fetching gene symbols:", error));
|
|
95
|
+
|
|
96
|
+
// Initialize with the phenotype search mode
|
|
97
|
+
setSearchMode("phenotype");
|
|
98
|
+
|
|
99
|
+
// Attach click handlers to the tab buttons
|
|
100
|
+
document.querySelectorAll(".Tab").forEach((button) => {
|
|
101
|
+
button.addEventListener("click", () => setSearchMode(button.dataset.tab));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Update the button whenever the Gene List textarea changes
|
|
105
|
+
document.getElementById("geneList").addEventListener("input", checkGeneListInput);
|
|
106
|
+
|
|
107
|
+
// Helper that waits for all prerequisite data to load
|
|
108
|
+
async function ensureDataLoaded() {
|
|
109
|
+
await Promise.all([phenotypesLoaded, geneSymbolsLoaded]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ====================================================================
|
|
113
|
+
// Input handling
|
|
114
|
+
// ====================================================================
|
|
115
|
+
|
|
116
|
+
// --------------------------------------------------------------------
|
|
117
|
+
// Display search suggestions based on the user's input
|
|
118
|
+
// --------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
async function handleInput(event) {
|
|
121
|
+
await ensureDataLoaded(); // Ensure reference data has finished loading
|
|
122
|
+
|
|
123
|
+
const userInput = event.target.value.toLowerCase();
|
|
124
|
+
const suggestionList =
|
|
125
|
+
searchMode === "phenotype"
|
|
126
|
+
? document.getElementById("phenotypeSuggestions")
|
|
127
|
+
: document.getElementById("geneSuggestions");
|
|
128
|
+
|
|
129
|
+
const submitButton = document.getElementById("submitBtn");
|
|
130
|
+
|
|
131
|
+
if (!submitButton) {
|
|
132
|
+
console.error(`submitButton not found`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
suggestionList.innerHTML = "";
|
|
137
|
+
|
|
138
|
+
let isValidSelection = false;
|
|
139
|
+
if (userInput) {
|
|
140
|
+
const dataDictionary = searchMode === "phenotype" ? phenotypes : geneSymbols;
|
|
141
|
+
let matchingCandidates = Object.keys(dataDictionary)
|
|
142
|
+
.map((candidate) => ({
|
|
143
|
+
text: candidate,
|
|
144
|
+
score: wordMatchScore(userInput, candidate),
|
|
145
|
+
}))
|
|
146
|
+
.sort((a, b) => b.score - a.score)
|
|
147
|
+
.filter((candidate) => candidate.score > 0)
|
|
148
|
+
.slice(0, 10);
|
|
149
|
+
|
|
150
|
+
matchingCandidates.forEach((candidate) => {
|
|
151
|
+
const listItem = document.createElement("li");
|
|
152
|
+
listItem.textContent = candidate.text;
|
|
153
|
+
listItem.addEventListener("click", function () {
|
|
154
|
+
event.target.value = candidate.text;
|
|
155
|
+
suggestionList.innerHTML = "";
|
|
156
|
+
checkValidInput();
|
|
157
|
+
});
|
|
158
|
+
suggestionList.appendChild(listItem);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
isValidSelection = matchingCandidates.some((candidate) => candidate.text.toLowerCase() === userInput);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
submitButton.disabled = !isValidSelection;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --------------------------------------------------------------------
|
|
168
|
+
// Validate the current input field
|
|
169
|
+
// --------------------------------------------------------------------
|
|
170
|
+
async function checkValidInput() {
|
|
171
|
+
await ensureDataLoaded();
|
|
172
|
+
|
|
173
|
+
const userInput =
|
|
174
|
+
searchMode === "phenotype" ? document.getElementById("phenotype") : document.getElementById("gene");
|
|
175
|
+
|
|
176
|
+
let isEmptyInput = userInput.value.trim() === "";
|
|
177
|
+
|
|
178
|
+
let isValidSelection = false;
|
|
179
|
+
if (searchMode === "phenotype") {
|
|
180
|
+
isValidSelection = phenotypes.hasOwnProperty(userInput.value);
|
|
181
|
+
} else if (searchMode === "gene") {
|
|
182
|
+
isValidSelection = geneSymbols.hasOwnProperty(userInput.value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const submitBtn = document.getElementById("submitBtn");
|
|
186
|
+
submitBtn.disabled = !isValidSelection || isEmptyInput;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --------------------------------------------------------------------
|
|
190
|
+
// Register input listeners once the datasets are ready
|
|
191
|
+
// --------------------------------------------------------------------
|
|
192
|
+
ensureDataLoaded().then(() => {
|
|
193
|
+
document.getElementById("phenotype").addEventListener("input", handleInput);
|
|
194
|
+
document.getElementById("gene").addEventListener("input", handleInput);
|
|
195
|
+
document.getElementById("phenotype").addEventListener("blur", checkValidInput);
|
|
196
|
+
document.getElementById("gene").addEventListener("blur", checkValidInput);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ====================================================================
|
|
200
|
+
// Open the detail page that corresponds to the form selection in a new tab
|
|
201
|
+
// ====================================================================
|
|
202
|
+
function handleFormSubmit(event) {
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
|
|
205
|
+
const rawMode = searchMode;
|
|
206
|
+
const mode = rawMode === "gene" ? "genesymbol" : rawMode === "geneList" ? "genelist" : rawMode;
|
|
207
|
+
|
|
208
|
+
// Run the Gene List workflow directly
|
|
209
|
+
if (rawMode === "geneList") {
|
|
210
|
+
fetchGeneData(); // Trigger immediately for gene lists
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// For phenotype/gene searches, navigate to the dedicated page
|
|
215
|
+
const userInput = mode === "phenotype" ? document.getElementById("phenotype") : document.getElementById("gene");
|
|
216
|
+
const submitBtn = document.getElementById("submitBtn");
|
|
217
|
+
const selectedData = mode === "phenotype" ? phenotypes[userInput.value] : userInput.value;
|
|
218
|
+
|
|
219
|
+
if (!submitBtn.disabled) {
|
|
220
|
+
const query = new URLSearchParams({
|
|
221
|
+
mode,
|
|
222
|
+
name: selectedData,
|
|
223
|
+
title: userInput.value,
|
|
224
|
+
});
|
|
225
|
+
window.open(`app/viewer.html?${query.toString()}`, "_blank");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Listen for the form's submit event
|
|
230
|
+
document.getElementById("searchForm").addEventListener("submit", handleFormSubmit);
|
|
231
|
+
|
|
232
|
+
// ====================================================================
|
|
233
|
+
// Calculate similarity scores between the input strings
|
|
234
|
+
// ====================================================================
|
|
235
|
+
|
|
236
|
+
function jaroWinkler(s1, s2) {
|
|
237
|
+
const m = 0.1;
|
|
238
|
+
const scalingFactor = 0.1;
|
|
239
|
+
const s1Len = s1.length;
|
|
240
|
+
const s2Len = s2.length;
|
|
241
|
+
|
|
242
|
+
if (s1Len === 0 || s2Len === 0) return 0;
|
|
243
|
+
|
|
244
|
+
const matchWindow = Math.max(0, Math.floor(Math.max(s1Len, s2Len) / 2) - 1);
|
|
245
|
+
const s1Matches = new Array(s1Len).fill(false);
|
|
246
|
+
const s2Matches = new Array(s2Len).fill(false);
|
|
247
|
+
let matches = 0;
|
|
248
|
+
|
|
249
|
+
for (let i = 0; i < s1Len; i++) {
|
|
250
|
+
const start = Math.max(0, i - matchWindow);
|
|
251
|
+
const end = Math.min(i + matchWindow + 1, s2Len);
|
|
252
|
+
|
|
253
|
+
for (let j = start; j < end; j++) {
|
|
254
|
+
if (s2Matches[j]) continue;
|
|
255
|
+
if (s1[i] !== s2[j]) continue;
|
|
256
|
+
s1Matches[i] = true;
|
|
257
|
+
s2Matches[j] = true;
|
|
258
|
+
matches++;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (matches === 0) return 0;
|
|
264
|
+
|
|
265
|
+
let transpositions = 0;
|
|
266
|
+
let k = 0;
|
|
267
|
+
|
|
268
|
+
for (let i = 0; i < s1Len; i++) {
|
|
269
|
+
if (!s1Matches[i]) continue;
|
|
270
|
+
while (!s2Matches[k]) k++;
|
|
271
|
+
if (s1[i] !== s2[k]) transpositions++;
|
|
272
|
+
k++;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
transpositions /= 2;
|
|
276
|
+
|
|
277
|
+
const jaroScore = (matches / s1Len + matches / s2Len + (matches - transpositions) / matches) / 3;
|
|
278
|
+
|
|
279
|
+
let prefixLength = 0;
|
|
280
|
+
for (let i = 0; i < Math.min(4, s1Len, s2Len); i++) {
|
|
281
|
+
if (s1[i] === s2[i]) prefixLength++;
|
|
282
|
+
else break;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return jaroScore + prefixLength * scalingFactor * (1 - jaroScore);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function wordMatchScore(term1, term2) {
|
|
289
|
+
const term1Words = term1.split(" ").filter(Boolean);
|
|
290
|
+
const term2Words = term2.split(" ").filter(Boolean);
|
|
291
|
+
let score = 0;
|
|
292
|
+
|
|
293
|
+
term1Words.forEach((word1) => {
|
|
294
|
+
let maxScore = 0;
|
|
295
|
+
term2Words.forEach((word2) => {
|
|
296
|
+
const similarity = jaroWinkler(word1.toLowerCase(), word2.toLowerCase());
|
|
297
|
+
maxScore = Math.max(maxScore, similarity);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
score += maxScore;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return score;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ====================================================================
|
|
307
|
+
// Info Tooltip Functionality
|
|
308
|
+
// ====================================================================
|
|
309
|
+
|
|
310
|
+
// Initialize tooltips when DOM is loaded
|
|
311
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
312
|
+
// Handle tooltip click interactions for mobile devices
|
|
313
|
+
const tooltipIcons = document.querySelectorAll(".info-tooltip-icon");
|
|
314
|
+
|
|
315
|
+
tooltipIcons.forEach((icon) => {
|
|
316
|
+
icon.addEventListener("click", function (e) {
|
|
317
|
+
e.preventDefault();
|
|
318
|
+
e.stopPropagation();
|
|
319
|
+
|
|
320
|
+
const container = this.parentElement;
|
|
321
|
+
container.classList.toggle("active");
|
|
322
|
+
|
|
323
|
+
// Close other active tooltips
|
|
324
|
+
document.querySelectorAll(".info-tooltip-container.active").forEach((el) => {
|
|
325
|
+
if (el !== container) {
|
|
326
|
+
el.classList.remove("active");
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Close tooltips when clicking outside
|
|
333
|
+
document.addEventListener("click", function () {
|
|
334
|
+
document.querySelectorAll(".info-tooltip-container.active").forEach((el) => {
|
|
335
|
+
el.classList.remove("active");
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
cd "$(dirname "$0")"
|
|
4
|
+
echo "=== Current working directory ==="
|
|
5
|
+
pwd
|
|
6
|
+
echo
|
|
7
|
+
|
|
8
|
+
echo "=== Checking for Python executables ==="
|
|
9
|
+
for CMD in py python3 python; do
|
|
10
|
+
if command -v "$CMD" >/dev/null 2>&1; then
|
|
11
|
+
echo "Found $CMD: $(command -v "$CMD")"
|
|
12
|
+
echo
|
|
13
|
+
echo "=== Starting serve_index.py ==="
|
|
14
|
+
"$CMD" serve_index.py
|
|
15
|
+
echo
|
|
16
|
+
echo "=== Server stopped or exited ==="
|
|
17
|
+
read -rp "Press Enter to close this window..."
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
echo "[ERROR] Python not found."
|
|
23
|
+
echo "Please install Python 3 (apt install python3 or yum install python3)."
|
|
24
|
+
echo
|
|
25
|
+
read -rp "Press Enter to close this window..."
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
cd "$(dirname "$0")"
|
|
4
|
+
echo "=== Current working directory ==="
|
|
5
|
+
pwd
|
|
6
|
+
echo
|
|
7
|
+
|
|
8
|
+
echo "=== Checking for Python executables ==="
|
|
9
|
+
for CMD in py python3 python; do
|
|
10
|
+
if command -v "$CMD" >/dev/null 2>&1; then
|
|
11
|
+
echo "Found $CMD: $(command -v "$CMD")"
|
|
12
|
+
echo
|
|
13
|
+
echo "=== Starting serve_index.py ==="
|
|
14
|
+
"$CMD" serve_index.py
|
|
15
|
+
echo
|
|
16
|
+
echo "=== Server stopped or exited ==="
|
|
17
|
+
read -rp "Press Enter to close this window..."
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
echo "[ERROR] Python not found."
|
|
23
|
+
echo "Please install Python 3 from https://www.python.org/downloads/ or use Homebrew (brew install python)."
|
|
24
|
+
echo
|
|
25
|
+
read -rp "Press Enter to close this window..."
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
:: File name: start_server_debug.bat
|
|
2
|
+
@echo off
|
|
3
|
+
setlocal
|
|
4
|
+
pushd %~dp0
|
|
5
|
+
|
|
6
|
+
echo === Current working directory ===
|
|
7
|
+
cd
|
|
8
|
+
echo.
|
|
9
|
+
|
|
10
|
+
echo === Trying to launch with Python launcher (py) ===
|
|
11
|
+
where py 2>nul
|
|
12
|
+
if %errorlevel%==0 (
|
|
13
|
+
echo Found py launcher: & where py
|
|
14
|
+
echo.
|
|
15
|
+
py -3 serve_index.py
|
|
16
|
+
goto :PAUSE_AND_END
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
echo === Trying python / pythonw ===
|
|
20
|
+
for %%P in (python.exe pythonw.exe) do (
|
|
21
|
+
where %%P >nul 2>nul
|
|
22
|
+
if not errorlevel 1 (
|
|
23
|
+
echo Found %%P: & where %%P
|
|
24
|
+
echo.
|
|
25
|
+
%%P serve_index.py
|
|
26
|
+
goto :PAUSE_AND_END
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
echo [ERROR] Python was not found.
|
|
31
|
+
echo Please install Python from https://www.python.org/downloads/
|
|
32
|
+
echo or check your PATH / py launcher settings.
|
|
33
|
+
|
|
34
|
+
:PAUSE_AND_END
|
|
35
|
+
echo.
|
|
36
|
+
echo === Script finished. Check any error messages above. ===
|
|
37
|
+
pause
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# serve_index.py (WSL/mac/Linux/Windows aware; English-only messages)
|
|
2
|
+
import contextlib
|
|
3
|
+
import http.server
|
|
4
|
+
import os
|
|
5
|
+
import shlex
|
|
6
|
+
import socket
|
|
7
|
+
import socketserver
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import webbrowser
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
ROOT = Path(__file__).resolve().parent
|
|
15
|
+
INDEX = ROOT / "index.html"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_wsl() -> bool:
|
|
19
|
+
try:
|
|
20
|
+
with open("/proc/sys/kernel/osrelease", encoding="utf-8") as f:
|
|
21
|
+
return "microsoft" in f.read().lower()
|
|
22
|
+
except Exception:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _popen(cmdlist):
|
|
27
|
+
subprocess.Popen(cmdlist, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def safe_open_url(url: str) -> None:
|
|
31
|
+
"""Try multiple platform-specific openers; fall back to printing the URL."""
|
|
32
|
+
tried = []
|
|
33
|
+
|
|
34
|
+
# 0) WSL FIRST (because webbrowser may mistakenly pick gio)
|
|
35
|
+
if is_wsl():
|
|
36
|
+
for cmd in (["wslview", url], ["explorer.exe", url], ["powershell.exe", "/c", "start", url]):
|
|
37
|
+
try:
|
|
38
|
+
_popen(cmd)
|
|
39
|
+
return
|
|
40
|
+
except Exception as e:
|
|
41
|
+
tried.append(f"{' '.join(cmd)}: {e}")
|
|
42
|
+
|
|
43
|
+
# 1) Respect $BROWSER (allow commands with args)
|
|
44
|
+
browser_env = os.environ.get("BROWSER")
|
|
45
|
+
if browser_env:
|
|
46
|
+
try:
|
|
47
|
+
cmd = shlex.split(browser_env) + [url]
|
|
48
|
+
_popen(cmd)
|
|
49
|
+
return
|
|
50
|
+
except Exception as e:
|
|
51
|
+
tried.append(f"$BROWSER({browser_env}): {e}")
|
|
52
|
+
|
|
53
|
+
# 2) Python's webbrowser (may choose gio/xdg-open/open)
|
|
54
|
+
try:
|
|
55
|
+
# webbrowser may return True even if the underlying tool fails later; still worth trying.
|
|
56
|
+
if webbrowser.open_new_tab(url):
|
|
57
|
+
return
|
|
58
|
+
except Exception as e:
|
|
59
|
+
tried.append(f"webbrowser: {e}")
|
|
60
|
+
|
|
61
|
+
# 3) macOS
|
|
62
|
+
if sys.platform == "darwin":
|
|
63
|
+
try:
|
|
64
|
+
_popen(["open", url])
|
|
65
|
+
return
|
|
66
|
+
except Exception as e:
|
|
67
|
+
tried.append(f"open: {e}")
|
|
68
|
+
|
|
69
|
+
# 4) Linux/Unix (desktop)
|
|
70
|
+
for cmd in (["xdg-open", url], ["gio", "open", url]):
|
|
71
|
+
try:
|
|
72
|
+
_popen(cmd)
|
|
73
|
+
return
|
|
74
|
+
except Exception as e:
|
|
75
|
+
tried.append(f"{' '.join(cmd)}: {e}")
|
|
76
|
+
|
|
77
|
+
print("Could not auto-open the browser.")
|
|
78
|
+
print("Open this URL manually:", url)
|
|
79
|
+
if tried:
|
|
80
|
+
print("Tried:", *tried, sep="\n - ")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def find_free_port() -> int:
|
|
84
|
+
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
|
85
|
+
s.bind(("127.0.0.1", 0))
|
|
86
|
+
return s.getsockname()[1]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Handler(http.server.SimpleHTTPRequestHandler):
|
|
90
|
+
def __init__(self, *args, **kwargs):
|
|
91
|
+
super().__init__(*args, directory=str(ROOT), **kwargs)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main():
|
|
95
|
+
if not INDEX.exists():
|
|
96
|
+
raise SystemExit("index.html not found. Place this script next to index.html.")
|
|
97
|
+
port = find_free_port()
|
|
98
|
+
with socketserver.ThreadingTCPServer(("127.0.0.1", port), Handler) as httpd:
|
|
99
|
+
url = f"http://127.0.0.1:{port}/index.html"
|
|
100
|
+
print(f"Serving {ROOT} at {url}")
|
|
101
|
+
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
|
102
|
+
safe_open_url(url)
|
|
103
|
+
try:
|
|
104
|
+
threading.Event().wait()
|
|
105
|
+
finally:
|
|
106
|
+
httpd.shutdown()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
main()
|