pywebexec 1.2.6__tar.gz → 1.2.7__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. {pywebexec-1.2.6/pywebexec.egg-info → pywebexec-1.2.7}/PKG-INFO +2 -2
  2. {pywebexec-1.2.6 → pywebexec-1.2.7}/README.md +1 -1
  3. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/pywebexec.py +1 -0
  4. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/css/style.css +81 -5
  5. pywebexec-1.2.7/pywebexec/static/images/down-arrow.svg +1 -0
  6. pywebexec-1.2.7/pywebexec/static/js/commands.js +183 -0
  7. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/js/script.js +24 -31
  8. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/templates/index.html +10 -4
  9. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/version.py +2 -2
  10. {pywebexec-1.2.6 → pywebexec-1.2.7/pywebexec.egg-info}/PKG-INFO +2 -2
  11. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec.egg-info/SOURCES.txt +2 -0
  12. {pywebexec-1.2.6 → pywebexec-1.2.7}/.github/workflows/python-publish.yml +0 -0
  13. {pywebexec-1.2.6 → pywebexec-1.2.7}/.gitignore +0 -0
  14. {pywebexec-1.2.6 → pywebexec-1.2.7}/LICENSE +0 -0
  15. {pywebexec-1.2.6 → pywebexec-1.2.7}/pyproject.toml +0 -0
  16. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/__init__.py +0 -0
  17. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/css/xterm.css +0 -0
  18. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/images/aborted.svg +0 -0
  19. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/images/copy.svg +0 -0
  20. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/images/copy_ok.svg +0 -0
  21. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/images/failed.svg +0 -0
  22. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/images/favicon.svg +0 -0
  23. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/images/running.gif +0 -0
  24. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/images/success.svg +0 -0
  25. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/js/xterm/LICENSE +0 -0
  26. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/js/xterm/ansi_up.min.js +0 -0
  27. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/js/xterm/xterm-addon-fit.js +0 -0
  28. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/static/js/xterm/xterm.js +0 -0
  29. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec/templates/__init__.py +0 -0
  30. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec.egg-info/dependency_links.txt +0 -0
  31. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec.egg-info/entry_points.txt +0 -0
  32. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec.egg-info/requires.txt +0 -0
  33. {pywebexec-1.2.6 → pywebexec-1.2.7}/pywebexec.egg-info/top_level.txt +0 -0
  34. {pywebexec-1.2.6 → pywebexec-1.2.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pywebexec
3
- Version: 1.2.6
3
+ Version: 1.2.7
4
4
  Summary: Simple Python HTTP Exec Server
5
5
  Home-page: https://github.com/joknarf/pywebexec
6
6
  Author: Franck Jouvanceau
@@ -83,7 +83,7 @@ $ pywebexec
83
83
  ```
84
84
 
85
85
  * Launch commands with params/view live output/Status using browser
86
- ![pywebexecnew5](https://github.com/user-attachments/assets/cb443a6f-e364-4302-98de-f0330d763289)
86
+ ![pywebexecnew6](https://github.com/user-attachments/assets/11415e1f-9f5f-409e-a04c-51eb062a9780)
87
87
 
88
88
  all commands output / statuses are available in the executables directory in subdirectory `.web_status`
89
89
 
@@ -21,7 +21,7 @@ $ pywebexec
21
21
  ```
22
22
 
23
23
  * Launch commands with params/view live output/Status using browser
24
- ![pywebexecnew5](https://github.com/user-attachments/assets/cb443a6f-e364-4302-98de-f0330d763289)
24
+ ![pywebexecnew6](https://github.com/user-attachments/assets/11415e1f-9f5f-409e-a04c-51eb062a9780)
25
25
 
26
26
  all commands output / statuses are available in the executables directory in subdirectory `.web_status`
27
27
 
@@ -580,6 +580,7 @@ def get_command_output(command_id):
580
580
  @app.route('/executables', methods=['GET'])
581
581
  def list_executables():
582
582
  executables = [f for f in os.listdir('.') if os.path.isfile(f) and os.access(f, os.X_OK)]
583
+ executables.sort() # Sort the list of executables alphabetically
583
584
  return jsonify(executables)
584
585
 
585
586
  def main():
@@ -62,6 +62,11 @@ button {
62
62
  form {
63
63
  padding-bottom: 15px;
64
64
  }
65
+ .form-inline {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 10px;
69
+ }
65
70
  .status-icon {
66
71
  display: inline-block;
67
72
  width: 16px;
@@ -103,21 +108,90 @@ form {
103
108
  .copy_clip_ok, .copy_clip_ok:hover {
104
109
  background-image: url("/static/images/copy_ok.svg");
105
110
  }
106
- input {
107
- width: 50%;
111
+ #params, #commandName {
108
112
  -webkit-appearance: none;
109
113
  -webkit-border-radius: none;
110
114
  appearance: none;
111
115
  border-radius: 15px;
112
116
  padding: 3px;
113
- padding-right: 13px;
114
117
  border: 1px #aaa solid;
115
118
  height: 15px;
116
119
  font-size: 15px;
117
120
  outline: none;
121
+ background-color: white;
122
+ margin: 0; /* Remove margin */
123
+ }
124
+
125
+ #commandName {
126
+ width: auto; /* Allow dynamic width */
127
+ background-size: 16px 16px;
128
+ border-radius: 15px 0px 0px 15px;
129
+ border-right: 0px;
118
130
  text-indent: 5px;
131
+ padding-right: 0px;
132
+ }
133
+ #params {
134
+ width: auto; /* Allow dynamic width */
135
+ border-radius: 0px 15px 15px 0px;
136
+ border-left: 0px;
137
+ }
138
+
139
+ .input-group {
140
+ display: flex;
141
+ align-items: center;
142
+ /*width: 60%;*/
143
+ }
144
+
145
+ .show-command-list-button {
119
146
  background-color: white;
147
+ cursor: pointer;
148
+ height: 21px;
149
+ font-size: 15px;
150
+ border-top: 1px solid #aaa;
151
+ border-bottom: 1px solid #aaa;
152
+ border-radius: 0px;
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ }
157
+
158
+ .show-command-list-button .arrow {
159
+ visibility: hidden; /* Hide arrow by default */
160
+ }
161
+
162
+ .show-command-list-button:hover .arrow {
163
+ visibility: visible; /* Show arrow on hover */
164
+ }
165
+
166
+ #commandName:focus + .show-command-list-button .arrow {
167
+ visibility: visible; /* Show arrow when input has focus */
168
+ }
169
+
170
+ #commandList.show + .show-command-list-button .arrow {
171
+ display: none; /* Hide arrow when list is shown */
172
+ }
173
+
174
+ .command-list {
175
+ display: none;
176
+ position: absolute;
177
+ background-color: white;
178
+ border: 1px solid #aaa;
179
+ border-radius: 5px;
180
+ padding: 0px 5px 0px 5px;
181
+ max-height: 300px;
182
+ overflow-y: auto;
183
+ z-index: 1000;
184
+ }
185
+
186
+ .command-item {
187
+ padding: 3px 5px 3px 5px;
188
+ cursor: pointer;
189
+ }
190
+
191
+ .command-item:hover {
192
+ background-color: #eee;
120
193
  }
194
+
121
195
  .currentcommand {
122
196
  background-color: #eef;
123
197
  }
@@ -172,6 +246,8 @@ body.dimmed * {
172
246
  visibility: hidden;
173
247
  height: 0px;
174
248
  }
175
- span { /* allow wide chars in terminal */
176
- letter-spacing: unset !important;
249
+ /* allow wide chars in terminal */
250
+ span {
251
+ letter-spacing: -0.03px !important;
177
252
  }
253
+
@@ -0,0 +1 @@
1
+ <svg width="12" height="12" xmlns="http://www.w3.org/2000/svg"><polygon points="4 3 12 3 8 10 4 3" fill="black" /></svg>
@@ -0,0 +1,183 @@
1
+ // commands.js
2
+ let commandInput = document.getElementById('commandName');
3
+ let paramsInput = document.getElementById('params');
4
+ let commandListDiv = document.getElementById('commandList');
5
+ let showCommandListButton = document.getElementById('showCommandListButton');
6
+
7
+ function unfilterCommands() {
8
+ const items = commandListDiv.getElementsByClassName('command-item');
9
+ Array.from(items).forEach(item => {
10
+ item.style.display = 'block';
11
+ });
12
+ adjustCommandListWidth();
13
+ }
14
+
15
+ function filterCommands() {
16
+ const value = commandInput.value;
17
+ const items = commandListDiv.getElementsByClassName('command-item');
18
+ let hasVisibleItems = false;
19
+ Array.from(items).forEach(item => {
20
+ if (item.textContent.startsWith(value)) {
21
+ item.style.display = 'block';
22
+ hasVisibleItems = true;
23
+ } else {
24
+ item.style.display = 'none';
25
+ }
26
+ });
27
+ commandListDiv.style.display = hasVisibleItems ? 'block' : 'none';
28
+ if (hasVisibleItems) {
29
+ commandListDiv.classList.add('show');
30
+ } else {
31
+ commandListDiv.classList.remove('show');
32
+ }
33
+ adjustCommandListWidth(); // Adjust width after filtering commands
34
+ }
35
+
36
+ function setCommandListPosition() {
37
+ const rect = commandInput.getBoundingClientRect();
38
+ commandListDiv.style.left = `${rect.left}px`;
39
+ commandListDiv.style.top = `${rect.bottom}px`;
40
+ commandListDiv.style.minWidth = `${rect.width}px`;
41
+ }
42
+
43
+ function adjustInputWidth(input) {
44
+ input.style.width = 'auto';
45
+ input.style.width = `${input.scrollWidth}px`;
46
+ }
47
+
48
+ function adjustCommandListWidth() {
49
+ commandListDiv.style.width = 'auto'; // Reset width before recalculating
50
+ const items = Array.from(commandListDiv.getElementsByClassName('command-item'));
51
+ let maxWidth = 0;
52
+ items.forEach(item => {
53
+ if (item.style.display !== 'none') {
54
+ const itemWidth = item.scrollWidth;
55
+ if (itemWidth > maxWidth) {
56
+ maxWidth = itemWidth;
57
+ }
58
+ }
59
+ });
60
+ commandListDiv.style.width = `${maxWidth}px`;
61
+ }
62
+
63
+ paramsInput.addEventListener('input', () => adjustInputWidth(paramsInput));
64
+ commandInput.addEventListener('input', () => {
65
+ adjustInputWidth(commandInput);
66
+ filterCommands(); // Filter commands on input
67
+ });
68
+
69
+ paramsInput.addEventListener('mouseover', () => {
70
+ paramsInput.focus();
71
+ paramsInput.setSelectionRange(0, paramsInput.value.length);
72
+ });
73
+
74
+ commandInput.addEventListener('mouseover', () => {
75
+ commandInput.focus();
76
+ commandInput.setSelectionRange(0, commandInput.value.length);
77
+ });
78
+
79
+ commandInput.addEventListener('input', (event) => {
80
+ if (event.inputType === 'deleteContentBackward') {
81
+ const newValue = commandInput.value.slice(0, -1);
82
+ commandInput.value = newValue;
83
+ commandInput.setSelectionRange(newValue.length, newValue.length);
84
+ }
85
+ const value = commandInput.value;
86
+ const items = Array.from(commandListDiv.getElementsByClassName('command-item'));
87
+ if (value) {
88
+ const match = items.find(item => item.textContent.startsWith(value));
89
+ if (match) {
90
+ commandInput.value = match.textContent;
91
+ commandInput.setSelectionRange(value.length, match.textContent.length);
92
+ } else {
93
+ commandInput.value = value.slice(0, -1);
94
+ }
95
+ }
96
+ filterCommands();
97
+ adjustInputWidth(commandInput); // Adjust width on input
98
+ });
99
+
100
+ commandInput.addEventListener('click', () => {
101
+ setCommandListPosition();
102
+ commandListDiv.style.display = 'block';
103
+ filterCommands();
104
+ });
105
+
106
+ commandInput.addEventListener('keydown', (event) => {
107
+ if (event.key === 'ArrowDown') {
108
+ setCommandListPosition();
109
+ commandListDiv.style.display = 'block';
110
+ unfilterCommands();
111
+ }
112
+ });
113
+
114
+ commandInput.addEventListener('blur', (event) => {
115
+ if (event.relatedTarget === showCommandListButton) {
116
+ event.preventDefault();
117
+ return;
118
+ }
119
+ commandListDiv.style.display = 'none';
120
+ commandListDiv.classList.remove('show');
121
+ adjustInputWidth(commandInput);
122
+ });
123
+
124
+ showCommandListButton.addEventListener('mousedown', (event) => {
125
+ event.preventDefault();
126
+ setCommandListPosition();
127
+ commandListDiv.style.display = 'block';
128
+ unfilterCommands();
129
+ });
130
+
131
+ window.addEventListener('click', (event) => {
132
+ if (!commandInput.contains(event.target) && !commandListDiv.contains(event.target) && !showCommandListButton.contains(event.target)) {
133
+ commandListDiv.style.display = 'none';
134
+ commandListDiv.classList.remove('show');
135
+ adjustInputWidth(commandInput);
136
+ }
137
+ });
138
+
139
+ window.addEventListener('keydown', (event) => {
140
+ if (document.activeElement !== paramsInput) {
141
+ commandInput.focus();
142
+ commandInput.dispatchEvent(new KeyboardEvent('keydown', event));
143
+ }
144
+ });
145
+
146
+ window.addEventListener('resize', () => {
147
+ setCommandListPosition();
148
+ });
149
+
150
+ window.addEventListener('load', () => {
151
+ fetchExecutables();
152
+ adjustInputWidth(paramsInput); // Adjust width on load
153
+ adjustInputWidth(commandInput); // Adjust width on load
154
+ setCommandListPosition();
155
+ });
156
+
157
+ async function fetchExecutables() {
158
+ try {
159
+ const response = await fetch('/executables');
160
+ if (!response.ok) {
161
+ throw new Error('Failed to fetch command status');
162
+ }
163
+ const executables = await response.json();
164
+ commandListDiv.innerHTML = '';
165
+ executables.forEach(executable => {
166
+ const div = document.createElement('div');
167
+ div.className = 'command-item';
168
+ div.textContent = executable;
169
+ div.addEventListener('mousedown', () => {
170
+ commandInput.value = executable;
171
+ commandListDiv.style.display = 'none';
172
+ commandListDiv.classList.remove('show');
173
+ adjustInputWidth(commandInput);
174
+ paramsInput.focus();
175
+ });
176
+ commandListDiv.appendChild(div);
177
+ });
178
+ // Ensure the elements are rendered before measuring their widths
179
+ requestAnimationFrame(adjustCommandListWidth);
180
+ } catch (error) {
181
+ alert("Failed to fetch executables");
182
+ }
183
+ }
@@ -7,7 +7,21 @@ const terminal = new Terminal({
7
7
  cursorHidden: true,
8
8
  disableStdin: true,
9
9
  convertEol: true,
10
- fontFamily: 'Consolas NF, monospace, courier-new, courier'
10
+ fontFamily: 'Consolas NF, monospace, courier-new, courier',
11
+ scrollBack: 999999,
12
+ theme: {
13
+ background: '#111412',
14
+ black: '#111412',
15
+ green: '#088a5b',
16
+ blue: "#2760aa",
17
+ red: '#ba1611',
18
+ yellow: "#cf8700",
19
+ magenta: "#4c3d80",
20
+ cyan: "#00a7aa",
21
+ brightBlack: "#243C4F",
22
+ brightBlue: "#5584b1",
23
+ },
24
+ rescaleOverlappingGlyphs: true,
11
25
  });
12
26
  const fitAddon = new FitAddon.FitAddon();
13
27
  terminal.loadAddon(fitAddon);
@@ -75,7 +89,7 @@ async function fetchCommands() {
75
89
  <td>${command.command.replace(/^\.\//, '')}</td>
76
90
  <td><span class="status-icon status-${command.status}"></span>${command.status}${command.status === 'failed' ? ` (${command.exit_code})` : ''}</td>
77
91
  <td>
78
- ${command.status === 'running' ? `<button onclick="stopCommand('${command.command_id}')">Stop</button>` : `<button onclick="relaunchCommand('${command.command_id}')">Run</button>`}
92
+ ${command.status === 'running' ? `<button onclick="stopCommand('${command.command_id}', event)">Stop</button>` : `<button onclick="relaunchCommand('${command.command_id}', event)">Run</button>`}
79
93
  </td>
80
94
  <td class="monospace outcol">${command.last_output_line || ''}</td>
81
95
  `;
@@ -88,27 +102,6 @@ async function fetchCommands() {
88
102
  }
89
103
  }
90
104
 
91
- async function fetchExecutables() {
92
- try {
93
- const response = await fetch('/executables');
94
- if (!response.ok) {
95
- throw new Error('Failed to fetch command status');
96
- }
97
- const executables = await response.json();
98
- const commandNameSelect = document.getElementById('commandName');
99
- commandNameSelect.innerHTML = '';
100
- executables.forEach(executable => {
101
- const option = document.createElement('option');
102
- option.value = executable;
103
- option.textContent = executable;
104
- commandNameSelect.appendChild(option);
105
- });
106
- } catch (error) {
107
- console.log('Error fetching executables:', error);
108
- alert("Failed to fetch executables");
109
- }
110
- }
111
-
112
105
  async function fetchOutput(url) {
113
106
  try {
114
107
  const response = await fetch(url);
@@ -155,7 +148,9 @@ async function viewOutput(command_id) {
155
148
  }
156
149
  }
157
150
 
158
- async function relaunchCommand(command_id) {
151
+ async function relaunchCommand(command_id, event) {
152
+ event.stopPropagation();
153
+ event.stopImmediatePropagation();
159
154
  try {
160
155
  const response = await fetch(`/command_status/${command_id}`);
161
156
  if (!response.ok) {
@@ -188,7 +183,9 @@ async function relaunchCommand(command_id) {
188
183
  }
189
184
  }
190
185
 
191
- async function stopCommand(command_id) {
186
+ async function stopCommand(command_id, event) {
187
+ event.stopPropagation();
188
+ event.stopImmediatePropagation();
192
189
  try {
193
190
  const response = await fetch(`/stop_command/${command_id}`, {
194
191
  method: 'POST'
@@ -269,11 +266,7 @@ function initResizer() {
269
266
  }
270
267
 
271
268
  window.addEventListener('resize', adjustOutputHeight);
272
- window.addEventListener('load', () => {
273
- adjustOutputHeight();
274
- initResizer();
275
- });
269
+ window.addEventListener('load', initResizer);
276
270
 
277
- fetchExecutables();
278
271
  fetchCommands();
279
- setInterval(fetchCommands, 5000);
272
+ setInterval(fetchCommands, 5000);
@@ -12,13 +12,18 @@
12
12
  <div class="dimmer-text">Server not reachable</div>
13
13
  </div>
14
14
  <h2><span class="status-icon title-icon"></span>{{ title }}</h2>
15
- <form id="launchForm">
15
+ <form id="launchForm" class="form-inline">
16
16
  <label for="commandName">Command</label>
17
- <select id="commandName" name="commandName"></select>
18
- <label for="params">Params</label>
19
- <input type="text" id="params" name="params">
17
+ <div class="input-group">
18
+ <input type="text" id="commandName" name="commandName" size=5 autocomplete="off" style="width: 100px;" autofocus>
19
+ <div id="showCommandListButton" class="show-command-list-button">
20
+ <span class="arrow">&#9660;</span>
21
+ </div>
22
+ <input type="text" id="params" name="params" oninput="this.style.width = ((this.value.length + 1) * 8) + 'px';">
23
+ </div>
20
24
  <button type="submit">Run</button>
21
25
  </form>
26
+ <div id="commandList" class="command-list"></div>
22
27
  <div class="table-container" id="tableContainer">
23
28
  <table>
24
29
  <thead>
@@ -43,5 +48,6 @@
43
48
  <script src="/static/js/xterm/xterm.js"></script>
44
49
  <script src="/static/js/xterm/xterm-addon-fit.js"></script>
45
50
  <script type="text/javascript" src="/static/js/script.js"></script>
51
+ <script type="text/javascript" src="/static/js/commands.js"></script>
46
52
  </body>
47
53
  </html>
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.2.6'
16
- __version_tuple__ = version_tuple = (1, 2, 6)
15
+ __version__ = version = '1.2.7'
16
+ __version_tuple__ = version_tuple = (1, 2, 7)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pywebexec
3
- Version: 1.2.6
3
+ Version: 1.2.7
4
4
  Summary: Simple Python HTTP Exec Server
5
5
  Home-page: https://github.com/joknarf/pywebexec
6
6
  Author: Franck Jouvanceau
@@ -83,7 +83,7 @@ $ pywebexec
83
83
  ```
84
84
 
85
85
  * Launch commands with params/view live output/Status using browser
86
- ![pywebexecnew5](https://github.com/user-attachments/assets/cb443a6f-e364-4302-98de-f0330d763289)
86
+ ![pywebexecnew6](https://github.com/user-attachments/assets/11415e1f-9f5f-409e-a04c-51eb062a9780)
87
87
 
88
88
  all commands output / statuses are available in the executables directory in subdirectory `.web_status`
89
89
 
@@ -18,10 +18,12 @@ pywebexec/static/css/xterm.css
18
18
  pywebexec/static/images/aborted.svg
19
19
  pywebexec/static/images/copy.svg
20
20
  pywebexec/static/images/copy_ok.svg
21
+ pywebexec/static/images/down-arrow.svg
21
22
  pywebexec/static/images/failed.svg
22
23
  pywebexec/static/images/favicon.svg
23
24
  pywebexec/static/images/running.gif
24
25
  pywebexec/static/images/success.svg
26
+ pywebexec/static/js/commands.js
25
27
  pywebexec/static/js/script.js
26
28
  pywebexec/static/js/xterm/LICENSE
27
29
  pywebexec/static/js/xterm/ansi_up.min.js
File without changes
File without changes
File without changes
File without changes