sshler 0.3.2__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.
- sshler/__init__.py +10 -0
- sshler/cli.py +161 -0
- sshler/config.py +425 -0
- sshler/scripts/install-sshler-task.ps1 +28 -0
- sshler/scripts/remove-sshler-task.ps1 +15 -0
- sshler/scripts/run-sshler.ps1 +24 -0
- sshler/ssh.py +329 -0
- sshler/ssh_config.py +134 -0
- sshler/state.py +230 -0
- sshler/static/base.js +305 -0
- sshler/static/favicon-terminal.svg +8 -0
- sshler/static/favicon.svg +8 -0
- sshler/static/file-edit.js +81 -0
- sshler/static/file-view.js +60 -0
- sshler/static/style.css +158 -0
- sshler/static/term.js +304 -0
- sshler/templates/base.html +41 -0
- sshler/templates/box.html +53 -0
- sshler/templates/docs.html +40 -0
- sshler/templates/file_edit.html +30 -0
- sshler/templates/file_view.html +31 -0
- sshler/templates/index.html +49 -0
- sshler/templates/new_box.html +42 -0
- sshler/templates/partials/dir_listing.html +91 -0
- sshler/templates/term.html +67 -0
- sshler/webapp.py +1897 -0
- sshler-0.3.2.dist-info/METADATA +245 -0
- sshler-0.3.2.dist-info/RECORD +31 -0
- sshler-0.3.2.dist-info/WHEEL +5 -0
- sshler-0.3.2.dist-info/entry_points.txt +2 -0
- sshler-0.3.2.dist-info/top_level.txt +1 -0
sshler/static/base.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const FAVICONS = {
|
|
3
|
+
default: "/static/favicon.svg",
|
|
4
|
+
terminal: "/static/favicon-terminal.svg",
|
|
5
|
+
};
|
|
6
|
+
const LANG_KEY = "sshler-language";
|
|
7
|
+
|
|
8
|
+
const I18N = {
|
|
9
|
+
en: {
|
|
10
|
+
"nav.boxes": "Boxes",
|
|
11
|
+
"nav.addBox": "Add Box",
|
|
12
|
+
"nav.docs": "Docs",
|
|
13
|
+
},
|
|
14
|
+
ja: {
|
|
15
|
+
"nav.boxes": "ボックス",
|
|
16
|
+
"nav.addBox": "ボックスを追加",
|
|
17
|
+
"nav.docs": "ドキュメント",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function readToken() {
|
|
22
|
+
const tokenMeta = document.querySelector('meta[name="sshler-token"]');
|
|
23
|
+
const token = tokenMeta ? tokenMeta.getAttribute("content") : null;
|
|
24
|
+
return token || "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function applyToken(token) {
|
|
28
|
+
if (!token) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
window.sshlerToken = token;
|
|
32
|
+
|
|
33
|
+
// Configure htmx headers immediately if available
|
|
34
|
+
if (window.htmx) {
|
|
35
|
+
window.htmx.config.headers = window.htmx.config.headers || {};
|
|
36
|
+
window.htmx.config.headers["X-SSHLER-TOKEN"] = token;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Also set up event listener to add header to all htmx requests
|
|
40
|
+
document.body.addEventListener("htmx:configRequest", (event) => {
|
|
41
|
+
event.detail.headers["X-SSHLER-TOKEN"] = token;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setFavicon(mode) {
|
|
46
|
+
const faviconLink = document.getElementById("favicon-link");
|
|
47
|
+
if (!faviconLink) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const target = mode === "terminal" ? FAVICONS.terminal : FAVICONS.default;
|
|
51
|
+
if (faviconLink.getAttribute("href") !== target) {
|
|
52
|
+
faviconLink.setAttribute("href", target);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function showToast(message, type) {
|
|
57
|
+
if (!message) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const container = document.getElementById("toast-container");
|
|
61
|
+
if (!container) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const toast = document.createElement("div");
|
|
65
|
+
toast.className = `toast ${type || "info"}`;
|
|
66
|
+
toast.textContent = message;
|
|
67
|
+
container.appendChild(toast);
|
|
68
|
+
requestAnimationFrame(() => toast.classList.add("visible"));
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
toast.classList.remove("visible");
|
|
71
|
+
toast.addEventListener(
|
|
72
|
+
"transitionend",
|
|
73
|
+
() => toast.remove(),
|
|
74
|
+
{ once: true },
|
|
75
|
+
);
|
|
76
|
+
}, 3600);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getStoredLang() {
|
|
80
|
+
try {
|
|
81
|
+
return localStorage.getItem(LANG_KEY) || "en";
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return "en";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function setStoredLang(lang) {
|
|
88
|
+
try {
|
|
89
|
+
localStorage.setItem(LANG_KEY, lang);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
// Ignore if localStorage is unavailable
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function translate(lang) {
|
|
96
|
+
const elements = document.querySelectorAll("[data-i18n]");
|
|
97
|
+
elements.forEach((el) => {
|
|
98
|
+
const key = el.dataset.i18n;
|
|
99
|
+
const text = I18N[lang]?.[key];
|
|
100
|
+
if (text) {
|
|
101
|
+
el.textContent = text;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function updateLangToggle(lang) {
|
|
107
|
+
const langToggle = document.getElementById("lang-toggle");
|
|
108
|
+
if (!langToggle) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const spans = langToggle.querySelectorAll("span");
|
|
112
|
+
spans.forEach((span) => {
|
|
113
|
+
if (span.dataset.lang === lang) {
|
|
114
|
+
span.classList.remove("hidden");
|
|
115
|
+
} else {
|
|
116
|
+
span.classList.add("hidden");
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function switchLanguage(newLang) {
|
|
122
|
+
setStoredLang(newLang);
|
|
123
|
+
updateLangToggle(newLang);
|
|
124
|
+
translate(newLang);
|
|
125
|
+
|
|
126
|
+
// Update docs modal if it's open
|
|
127
|
+
const docsModal = document.querySelector("#modal-container .modal");
|
|
128
|
+
if (docsModal) {
|
|
129
|
+
switchDocsLanguage(newLang);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function switchDocsLanguage(lang) {
|
|
134
|
+
const modal = document.querySelector("#modal-container #docs-modal");
|
|
135
|
+
if (!modal) return;
|
|
136
|
+
|
|
137
|
+
const contents = modal.querySelectorAll(".lang-content");
|
|
138
|
+
const buttons = modal.querySelectorAll(".lang-btn");
|
|
139
|
+
|
|
140
|
+
contents.forEach((el) => {
|
|
141
|
+
if (el.dataset.lang === lang) {
|
|
142
|
+
el.classList.remove("hidden");
|
|
143
|
+
} else {
|
|
144
|
+
el.classList.add("hidden");
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
buttons.forEach((btn) => {
|
|
149
|
+
if (btn.dataset.lang === lang) {
|
|
150
|
+
btn.classList.add("active");
|
|
151
|
+
} else {
|
|
152
|
+
btn.classList.remove("active");
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
158
|
+
const token = readToken();
|
|
159
|
+
applyToken(token);
|
|
160
|
+
setFavicon("default");
|
|
161
|
+
|
|
162
|
+
// Initialize language toggle
|
|
163
|
+
const currentLang = getStoredLang();
|
|
164
|
+
updateLangToggle(currentLang);
|
|
165
|
+
translate(currentLang);
|
|
166
|
+
|
|
167
|
+
const langToggle = document.getElementById("lang-toggle");
|
|
168
|
+
if (langToggle) {
|
|
169
|
+
langToggle.addEventListener("click", () => {
|
|
170
|
+
const current = getStoredLang();
|
|
171
|
+
const newLang = current === "en" ? "ja" : "en";
|
|
172
|
+
switchLanguage(newLang);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Docs button handler
|
|
177
|
+
const docsBtn = document.getElementById("docs-btn");
|
|
178
|
+
if (docsBtn) {
|
|
179
|
+
docsBtn.addEventListener("click", async () => {
|
|
180
|
+
const modalContainer = document.getElementById("modal-container");
|
|
181
|
+
if (!modalContainer) return;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch("/docs");
|
|
185
|
+
if (!response.ok) throw new Error("Failed to load docs");
|
|
186
|
+
const html = await response.text();
|
|
187
|
+
modalContainer.innerHTML = html;
|
|
188
|
+
|
|
189
|
+
const modal = modalContainer.querySelector("#docs-modal");
|
|
190
|
+
if (!modal) return;
|
|
191
|
+
|
|
192
|
+
// Show modal
|
|
193
|
+
modal.classList.add("visible");
|
|
194
|
+
|
|
195
|
+
// Set initial language
|
|
196
|
+
switchDocsLanguage(currentLang);
|
|
197
|
+
|
|
198
|
+
// Language switcher in modal
|
|
199
|
+
const langButtons = modal.querySelectorAll(".lang-btn");
|
|
200
|
+
langButtons.forEach((btn) => {
|
|
201
|
+
btn.addEventListener("click", () => {
|
|
202
|
+
const newLang = btn.dataset.lang;
|
|
203
|
+
switchDocsLanguage(newLang);
|
|
204
|
+
setStoredLang(newLang);
|
|
205
|
+
updateLangToggle(newLang);
|
|
206
|
+
translate(newLang);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Close button
|
|
211
|
+
const closeBtn = modal.querySelector(".modal-close");
|
|
212
|
+
if (closeBtn) {
|
|
213
|
+
closeBtn.addEventListener("click", () => {
|
|
214
|
+
modal.classList.remove("visible");
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
modalContainer.innerHTML = "";
|
|
217
|
+
}, 300);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Close on outside click
|
|
222
|
+
modal.addEventListener("click", (event) => {
|
|
223
|
+
if (event.target === modal) {
|
|
224
|
+
modal.classList.remove("visible");
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
modalContainer.innerHTML = "";
|
|
227
|
+
}, 300);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Close on Escape key
|
|
232
|
+
const escHandler = (event) => {
|
|
233
|
+
if (event.key === "Escape") {
|
|
234
|
+
modal.classList.remove("visible");
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
modalContainer.innerHTML = "";
|
|
237
|
+
}, 300);
|
|
238
|
+
document.removeEventListener("keydown", escHandler);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
document.addEventListener("keydown", escHandler);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error("Failed to load docs:", err);
|
|
244
|
+
showToast("Failed to load documentation", "error");
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
document.body.addEventListener("dir-action", (event) => {
|
|
250
|
+
const payload = event.detail && event.detail.value;
|
|
251
|
+
if (!payload) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const status = payload.status === "error" ? "error" : "success";
|
|
255
|
+
showToast(payload.message, status);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Event delegation for delete buttons
|
|
259
|
+
document.body.addEventListener("click", (event) => {
|
|
260
|
+
const deleteBtn = event.target.closest(".delete-file-btn");
|
|
261
|
+
if (!deleteBtn) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
const boxName = deleteBtn.dataset.box;
|
|
266
|
+
const filePath = deleteBtn.dataset.path;
|
|
267
|
+
const directory = deleteBtn.dataset.directory;
|
|
268
|
+
const target = deleteBtn.dataset.target;
|
|
269
|
+
const fileName = deleteBtn.dataset.filename;
|
|
270
|
+
deleteFile(boxName, filePath, directory, target, fileName);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
function deleteFile(boxName, filePath, directory, target, fileName) {
|
|
275
|
+
if (!confirm(`Delete ${fileName}?`)) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const token = window.sshlerToken || readToken();
|
|
280
|
+
const xhr = new XMLHttpRequest();
|
|
281
|
+
xhr.open("POST", `/box/${boxName}/delete`);
|
|
282
|
+
xhr.setRequestHeader("X-SSHLER-TOKEN", token);
|
|
283
|
+
xhr.onload = function () {
|
|
284
|
+
const browserEl = document.getElementById(target);
|
|
285
|
+
if (browserEl && xhr.status === 200) {
|
|
286
|
+
browserEl.innerHTML = xhr.responseText;
|
|
287
|
+
} else {
|
|
288
|
+
showToast("Failed to delete file", "error");
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
xhr.onerror = function () {
|
|
292
|
+
showToast("Failed to delete file", "error");
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const formData = new FormData();
|
|
296
|
+
formData.append("path", filePath);
|
|
297
|
+
formData.append("directory", directory);
|
|
298
|
+
formData.append("target", target);
|
|
299
|
+
xhr.send(formData);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
window.sshlerShowToast = showToast;
|
|
303
|
+
window.sshlerSetFavicon = setFavicon;
|
|
304
|
+
window.sshlerDeleteFile = deleteFile;
|
|
305
|
+
})();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
2
|
+
<rect width="64" height="64" rx="6" fill="#0f0e0d"/>
|
|
3
|
+
<rect x="4" y="4" width="56" height="56" rx="4"
|
|
4
|
+
stroke="#e7c65f" stroke-width="3" fill="none"/>
|
|
5
|
+
<path d="M18 20l14 12-14 12" stroke="#d13f2c" stroke-width="5"
|
|
6
|
+
stroke-linecap="round" stroke-linejoin="round"/>
|
|
7
|
+
<rect x="38" y="40" width="10" height="4" fill="#d13f2c" rx="1"/>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
2
|
+
<rect width="64" height="64" rx="6" fill="#0f0e0d"/>
|
|
3
|
+
<rect x="4" y="4" width="56" height="56" rx="4"
|
|
4
|
+
stroke="#d13f2c" stroke-width="3" fill="none"/>
|
|
5
|
+
<path d="M18 20l14 12-14 12" stroke="#e7c65f" stroke-width="5"
|
|
6
|
+
stroke-linecap="round" stroke-linejoin="round"/>
|
|
7
|
+
<rect x="38" y="40" width="10" height="4" fill="#e7c65f" rx="1"/>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const MODE_MAP = {
|
|
3
|
+
".py": "python",
|
|
4
|
+
".js": "javascript",
|
|
5
|
+
".ts": "javascript",
|
|
6
|
+
".json": "application/json",
|
|
7
|
+
".yaml": "yaml",
|
|
8
|
+
".yml": "yaml",
|
|
9
|
+
".md": "markdown",
|
|
10
|
+
".sh": "shell",
|
|
11
|
+
".bash": "shell",
|
|
12
|
+
".css": "css",
|
|
13
|
+
".html": "xml",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function resolveMode(path) {
|
|
17
|
+
const idx = path.lastIndexOf(".");
|
|
18
|
+
if (idx === -1) {
|
|
19
|
+
return "plaintext";
|
|
20
|
+
}
|
|
21
|
+
const ext = path.slice(idx).toLowerCase();
|
|
22
|
+
return MODE_MAP[ext] || "plaintext";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function saveFile(cm, path, saveStatus) {
|
|
26
|
+
const payload = {
|
|
27
|
+
path,
|
|
28
|
+
content: cm.getValue(),
|
|
29
|
+
};
|
|
30
|
+
saveStatus.textContent = "Saving...";
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(
|
|
33
|
+
window.location.pathname + window.location.search,
|
|
34
|
+
{
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"X-SSHLER-TOKEN": window.sshlerToken || "",
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify(payload),
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const data = await response.json().catch(() => ({}));
|
|
45
|
+
throw new Error(data.detail || response.statusText);
|
|
46
|
+
}
|
|
47
|
+
saveStatus.textContent = "Saved ✓";
|
|
48
|
+
// Redirect to preview mode after successful save
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
const previewUrl = window.location.pathname.replace('/edit', '/cat') + window.location.search;
|
|
51
|
+
window.location.href = previewUrl;
|
|
52
|
+
}, 500);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(error);
|
|
55
|
+
saveStatus.textContent = `Save failed: ${error.message}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
60
|
+
const textarea = document.getElementById("editor");
|
|
61
|
+
if (!textarea) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const saveStatus = document.getElementById("save-status");
|
|
65
|
+
const path = textarea.dataset.path || "";
|
|
66
|
+
const cm = CodeMirror.fromTextArea(textarea, {
|
|
67
|
+
mode: resolveMode(path),
|
|
68
|
+
theme: "default",
|
|
69
|
+
lineNumbers: true,
|
|
70
|
+
lineWrapping: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const saveBtn = document.getElementById("save-btn");
|
|
74
|
+
if (saveBtn) {
|
|
75
|
+
saveBtn.addEventListener("click", (event) => {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
saveFile(cm, path, saveStatus);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
})();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
function getToken() {
|
|
3
|
+
if (window.sshlerToken) {
|
|
4
|
+
return window.sshlerToken;
|
|
5
|
+
}
|
|
6
|
+
const tokenMeta = document.querySelector('meta[name="sshler-token"]');
|
|
7
|
+
return tokenMeta ? tokenMeta.getAttribute("content") || "" : "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
11
|
+
const backBtn = document.getElementById("back-btn");
|
|
12
|
+
const deleteBtn = document.getElementById("delete-btn");
|
|
13
|
+
|
|
14
|
+
if (backBtn) {
|
|
15
|
+
backBtn.addEventListener("click", (event) => {
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
window.close();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (deleteBtn) {
|
|
22
|
+
deleteBtn.addEventListener("click", async (event) => {
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
const fileName = deleteBtn.dataset.filename || "this file";
|
|
25
|
+
|
|
26
|
+
if (!confirm(`Delete ${fileName}?`)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const boxName = deleteBtn.dataset.box;
|
|
31
|
+
const filePath = deleteBtn.dataset.path;
|
|
32
|
+
const parentDir = deleteBtn.dataset.parentdir;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(`/box/${boxName}/delete`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: {
|
|
38
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
39
|
+
"X-SSHLER-TOKEN": getToken(),
|
|
40
|
+
},
|
|
41
|
+
body: new URLSearchParams({
|
|
42
|
+
path: filePath,
|
|
43
|
+
directory: parentDir,
|
|
44
|
+
target: "browser",
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
window.close();
|
|
50
|
+
} else {
|
|
51
|
+
alert("Failed to delete file");
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error("Delete failed:", err);
|
|
55
|
+
alert("Failed to delete file");
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
})();
|
sshler/static/style.css
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #0f1115;
|
|
4
|
+
--fg: #e6e6e6;
|
|
5
|
+
--muted: #9aa1a9;
|
|
6
|
+
--accent: #6aa6ff;
|
|
7
|
+
--card: #151823;
|
|
8
|
+
--btn: #1e2230;
|
|
9
|
+
--danger: #ff6a6a;
|
|
10
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
11
|
+
--sans: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif;
|
|
12
|
+
}
|
|
13
|
+
* { box-sizing: border-box; }
|
|
14
|
+
body { margin: 0; font-family: var(--sans); background: var(--bg); color: var(--fg); }
|
|
15
|
+
body.term-view { overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
|
|
16
|
+
body.term-view .container { width: 100%; max-width: none; margin: 0; padding: 16px 24px; flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
|
17
|
+
.hidden { display: none !important; }
|
|
18
|
+
#toast-container { position: fixed; top: 18px; right: 18px; display: flex; flex-direction: column; gap: 10px; z-index: 2000; pointer-events: none; }
|
|
19
|
+
.toast { background: #11141f; border-left: 3px solid #2f3649; border-radius: 8px; padding: 10px 14px; color: var(--fg); min-width: 200px; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35); opacity: 0; transform: translateY(-8px); transition: opacity 0.22s ease, transform 0.22s ease; pointer-events: auto; }
|
|
20
|
+
.toast.success { border-left-color: #3ba86a; }
|
|
21
|
+
.toast.error { border-left-color: var(--danger); color: #ffb2b2; }
|
|
22
|
+
.toast.visible { opacity: 1; transform: translateY(0); }
|
|
23
|
+
.topbar { display: flex; gap: 12px; padding: 8px 14px; background: #0b0d12; border-bottom: 1px solid #222635; align-items: center; flex-shrink: 0; }
|
|
24
|
+
.brand { color: var(--fg); text-decoration: none; font-weight: 700;}
|
|
25
|
+
.version-pill { color: var(--muted); border: 1px solid #2a2f3f; border-radius: 999px; padding: 2px 10px; font-size: 0.8em; }
|
|
26
|
+
.link, a { color: var(--muted); text-decoration: none; }
|
|
27
|
+
.link:hover, a:hover { color: var(--accent); }
|
|
28
|
+
.link:visited, a:visited { color: var(--muted); }
|
|
29
|
+
.spacer { flex: 1; }
|
|
30
|
+
.docs-btn { background: none; border: none; padding: 0; cursor: pointer; font-family: inherit; font-size: inherit; }
|
|
31
|
+
.lang-toggle { background: transparent; border: 1px solid #2a2f3f; border-radius: 6px; color: var(--muted); padding: 4px 10px; cursor: pointer; font-size: 0.85em; font-weight: 600; }
|
|
32
|
+
.lang-toggle:hover { border-color: var(--accent); color: var(--accent); }
|
|
33
|
+
.container { padding: 20px; max-width: 1100px; margin: 0 auto; }
|
|
34
|
+
|
|
35
|
+
h1, h2 { margin: 8px 0 12px; }
|
|
36
|
+
.mono { font-family: var(--mono); font-size: 0.95em; }
|
|
37
|
+
.muted { color: var(--muted); }
|
|
38
|
+
.small { font-size: 0.9em; }
|
|
39
|
+
.tiny { font-size: 0.75em; }
|
|
40
|
+
|
|
41
|
+
.cards { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
|
|
42
|
+
.card { background: var(--card); border: 1px solid #222635; border-radius: 10px; padding: 12px; }
|
|
43
|
+
.card.ssh-card { border-color: #335dff; }
|
|
44
|
+
.card.custom-card { border-color: #2a2f3f; }
|
|
45
|
+
.card.local-card { border-color: #3ba86a; background: #14231b; }
|
|
46
|
+
.card .title { font-weight: 700; margin-bottom: 6px; }
|
|
47
|
+
.card .actions { display: flex; gap: 8px; margin-top: 10px; }
|
|
48
|
+
.btn { background: var(--btn); color: var(--fg); border: 1px solid #30364a; padding: 6px 10px; border-radius: 8px; text-decoration: none; cursor: pointer;}
|
|
49
|
+
.btn.secondary { background: transparent; }
|
|
50
|
+
.btn.small { padding: 4px 8px; font-size: 0.9em; }
|
|
51
|
+
.btn.danger { background: #3a1e22; border-color: #8b2a2a; color: #ffb2b2; }
|
|
52
|
+
.alert { padding: 8px 10px; border-radius: 6px; margin-bottom: 12px; }
|
|
53
|
+
.alert.error { background: #3a1e22; border: 1px solid #8b2a2a; color: #ffb2b2; }
|
|
54
|
+
|
|
55
|
+
.grid { display: grid; grid-template-columns: 280px 1fr; gap: 16px; }
|
|
56
|
+
.dir-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
|
57
|
+
.dir-tools { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; }
|
|
58
|
+
.dir-tool { display: flex; gap: 6px; align-items: center; padding: 6px 8px; background: #11141f; border: 1px solid #2a2f3f; border-radius: 8px; }
|
|
59
|
+
.dir-tool input[type="text"], .dir-tool input[type="file"] { background: #0d101a; border: 1px solid #2a2f3f; border-radius: 6px; padding: 6px 8px; color: var(--fg); font-family: var(--sans); font-size: 0.9em; }
|
|
60
|
+
.dir-tool input[type="text"] { min-width: 160px; }
|
|
61
|
+
.dir { width: 100%; border-collapse: collapse; }
|
|
62
|
+
.dir th, .dir td { border-bottom: 1px solid #2a2f3f; padding: 6px 8px; vertical-align: middle; }
|
|
63
|
+
.dir-table { font-size: 0.9em; }
|
|
64
|
+
.dir-table thead th { text-align: left; color: var(--muted); font-size: 0.75rem; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
65
|
+
.dir-table tbody tr:hover { background: #161a24; }
|
|
66
|
+
.actions-col { width: 220px; text-align: right; }
|
|
67
|
+
.fs-row { line-height: 1.3; }
|
|
68
|
+
.fs-name { font-family: var(--mono); max-width: 100%; }
|
|
69
|
+
.fs-entry { display: inline-flex; align-items: center; gap: 8px; color: var(--fg); text-decoration: none; max-width: 100%; }
|
|
70
|
+
.fs-entry.folder, .fs-entry.up { cursor: pointer; }
|
|
71
|
+
.fs-entry.folder .fs-label { font-weight: 700; }
|
|
72
|
+
.fs-entry:hover .fs-label { color: var(--accent); }
|
|
73
|
+
.fs-entry.up { color: var(--muted); }
|
|
74
|
+
.fs-icon { width: 14px; height: 12px; border-radius: 3px; position: relative; flex-shrink: 0; }
|
|
75
|
+
.fs-icon.folder { background: #242a3a; border: 1px solid #2f3649; }
|
|
76
|
+
.fs-icon.folder::before { content: ''; position: absolute; top: -3px; left: 1px; width: 8px; height: 4px; background: #31384d; border: 1px solid #2f3649; border-bottom: none; border-radius: 3px 3px 0 0; }
|
|
77
|
+
.fs-icon.file { border: 1px solid #2f3649; background: #0f1115; }
|
|
78
|
+
.fs-icon.file::after { content: ''; position: absolute; top: -1px; right: -1px; width: 6px; height: 6px; border-top: 1px solid #2f3649; border-right: 1px solid #2f3649; background: #0f1115; border-radius: 0 3px 0 0; }
|
|
79
|
+
.fs-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
80
|
+
.fs-type { color: var(--muted); font-size: 0.8em; }
|
|
81
|
+
.fs-actions { display: flex; gap: 6px; justify-content: flex-end; align-items: center; }
|
|
82
|
+
.action-btn { display: inline-flex; align-items: center; justify-content: center; gap: 4px; padding: 4px 8px; border-radius: 6px; border: 1px solid #2a2f3f; background: #191d2a; color: var(--fg); font-size: 0.85em; text-decoration: none; cursor: pointer; transition: border-color 0.15s, color 0.15s, background 0.15s; }
|
|
83
|
+
.action-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
84
|
+
.action-btn:focus { outline: none; box-shadow: 0 0 0 2px rgba(106, 166, 255, 0.25); }
|
|
85
|
+
.action-btn.danger-btn { border-color: #8b2a2a; color: #ffb2b2; }
|
|
86
|
+
.action-btn.danger-btn:hover { border-color: var(--danger); color: var(--danger); background: #3a1e22; }
|
|
87
|
+
|
|
88
|
+
.icon-btn { border: none; background: transparent; color: var(--muted); cursor: pointer; font-size: 1.1em; padding: 2px 4px; border-radius: 4px; }
|
|
89
|
+
.icon-btn:hover { color: var(--accent); }
|
|
90
|
+
.icon-btn.star.active { color: #f5c542; }
|
|
91
|
+
|
|
92
|
+
.favlist ul { list-style: none; padding-left: 0; display: flex; flex-direction: column; gap: 6px; margin: 8px 0 0; }
|
|
93
|
+
.favlist li { display: flex; align-items: center; gap: 8px; }
|
|
94
|
+
.favorite-toggle { cursor: pointer; }
|
|
95
|
+
.favorite-toggle::marker { content: ''; }
|
|
96
|
+
.favorite-toggle { padding: 4px 8px; border: 1px solid #2a2f3f; border-radius: 6px; display: inline-flex; align-items: center; gap: 6px; background: #11141f; }
|
|
97
|
+
.favorite-toggle::after { content: '▼'; font-size: 0.7em; }
|
|
98
|
+
details[open] .favorite-toggle::after { content: '▲'; }
|
|
99
|
+
|
|
100
|
+
.term { height: 100%; border: 1px solid #2a2f3f; border-radius: 8px; padding: 6px; }
|
|
101
|
+
.term-wrapper .term { height: auto; }
|
|
102
|
+
|
|
103
|
+
.form { display: flex; flex-direction: column; gap: 12px; max-width: 420px; }
|
|
104
|
+
.form label { display: flex; flex-direction: column; gap: 6px; font-size: 0.95em; }
|
|
105
|
+
.form input, .form textarea { background: #11141f; border: 1px solid #2a2f3f; border-radius: 6px; padding: 6px 8px; color: var(--fg); font-family: var(--sans); }
|
|
106
|
+
.form textarea { min-height: 80px; resize: vertical; }
|
|
107
|
+
.form .checkbox { flex-direction: row; align-items: center; gap: 8px; }
|
|
108
|
+
.form input[type="checkbox"] { width: auto; }
|
|
109
|
+
.form-actions { display: flex; gap: 10px; }
|
|
110
|
+
|
|
111
|
+
.file-view { display: flex; flex-direction: column; gap: 12px; }
|
|
112
|
+
.file-view__header { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; }
|
|
113
|
+
.file-view__actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
|
114
|
+
.file-view__content { background: #11141f; border: 1px solid #2a2f3f; border-radius: 8px; padding: 12px; overflow: auto; max-height: 70vh; white-space: pre-wrap; word-break: break-word; }
|
|
115
|
+
.image-preview { background: #0d101a; border: 1px solid #2a2f3f; border-radius: 8px; padding: 12px; display: flex; justify-content: center; align-items: center; max-height: 70vh; overflow: auto; }
|
|
116
|
+
.image-preview img { max-width: 100%; max-height: 100%; border-radius: 6px; }
|
|
117
|
+
.file-edit { display: flex; flex-direction: column; gap: 12px; height: calc(100vh - 70px); }
|
|
118
|
+
.file-edit__textarea { min-height: 60vh; }
|
|
119
|
+
.CodeMirror { flex: 1; border: 1px solid #2a2f3f; border-radius: 8px; font-size: 0.95em; background: #11141f; color: var(--fg); }
|
|
120
|
+
.CodeMirror-gutters { background: #0d101a; border-right: 1px solid #2a2f3f; }
|
|
121
|
+
.CodeMirror-cursor { border-left: 1px solid #6aa6ff; }
|
|
122
|
+
.file-edit .actions { display: flex; gap: 8px; }
|
|
123
|
+
|
|
124
|
+
.term-page { display: flex; flex-direction: column; gap: 10px; flex: 1; min-height: 0; }
|
|
125
|
+
.term-header { display: flex; align-items: stretch; justify-content: center; }
|
|
126
|
+
.term-banner { width: 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 18px; padding: 14px 20px; border: 1px solid #335dff; border-radius: 12px; background: #101320; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.24); }
|
|
127
|
+
.term-banner__host { order: 0; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.28em; color: #6aa6ff; }
|
|
128
|
+
.term-banner__primary { order: 1; font-size: 1.4rem; font-weight: 700; color: var(--fg); min-width: 0; }
|
|
129
|
+
.term-banner__meta { order: 2; font-size: 0.95rem; color: #f0d68a; min-width: 0; }
|
|
130
|
+
.term-banner__path { order: 3; flex: 1 1 240px; min-width: 0; font-size: 0.8rem; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
131
|
+
.term-banner__actions { order: 4; margin-left: auto; }
|
|
132
|
+
.term-banner__actions .btn { white-space: nowrap; }
|
|
133
|
+
.tmux-tabs { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
134
|
+
.tmux-tab { background: #191d2a; border: 1px solid #2a2f3f; border-radius: 6px; padding: 4px 8px; font-size: 0.85em; color: var(--muted); cursor: pointer; }
|
|
135
|
+
.tmux-tab.active { background: #335dff1c; border-color: #335dff; color: var(--fg); }
|
|
136
|
+
.tmux-separator { color: #2a2f3f; padding: 0 4px; }
|
|
137
|
+
.term-shell { flex: 1; display: flex; gap: 10px; align-items: stretch; min-height: 0; }
|
|
138
|
+
.term-toolbar { display: flex; flex-direction: column; gap: 8px; min-width: 160px; }
|
|
139
|
+
.term-toolbar .btn { width: 160px; text-align: left; }
|
|
140
|
+
.term-wrapper { flex: 1; display: flex; min-width: 0; min-height: 0; }
|
|
141
|
+
.term-wrapper #term { flex: 1; border: 1px solid #2a2f3f; border-radius: 8px; padding: 0; height: 100%; }
|
|
142
|
+
.file-panel { flex: 1; border: 1px solid #2a2f3f; border-radius: 8px; background: #11141f; padding: 10px; display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }
|
|
143
|
+
.file-panel.hidden { display: none; }
|
|
144
|
+
.file-browser-content { flex: 1; overflow: auto; min-height: 0; }
|
|
145
|
+
|
|
146
|
+
.modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: none; align-items: center; justify-content: center; z-index: 3000; }
|
|
147
|
+
.modal.visible { display: flex; }
|
|
148
|
+
.modal-content { background: var(--card); border: 1px solid #335dff; border-radius: 12px; max-width: 700px; width: 90%; max-height: 80vh; overflow: auto; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4); }
|
|
149
|
+
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid #2a2f3f; gap: 16px; flex-wrap: wrap; }
|
|
150
|
+
.modal-header h2 { margin: 0; }
|
|
151
|
+
.modal-actions { display: flex; gap: 8px; align-items: center; }
|
|
152
|
+
.modal-close { background: transparent; border: none; color: var(--muted); font-size: 2em; cursor: pointer; line-height: 1; padding: 0 8px; }
|
|
153
|
+
.modal-close:hover { color: var(--danger); }
|
|
154
|
+
.modal-body { padding: 20px; }
|
|
155
|
+
.lang-btn { background: var(--btn); color: var(--fg); border: 1px solid #30364a; padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 0.9em; }
|
|
156
|
+
.lang-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
157
|
+
.lang-btn.active { background: #335dff1c; border-color: #335dff; color: var(--accent); }
|
|
158
|
+
.lang-content.hidden { display: none; }
|