decterm 0.1.0__tar.gz

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.
decterm-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.2
2
+ Name: decterm
3
+ Version: 0.1.0
4
+ Summary: Cross-platform terminal controllable via Python (and web)
5
+ Home-page: https://github.com/decrule/decterm
6
+ Author: decterm
7
+ Author-email: decrule@outlook.com
8
+ License: MIT
9
+ Keywords: terminal,shell,cross-platform,automation
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: System :: Shells
22
+ Requires-Python: >=3.7
23
+ Description-Content-Type: text/markdown
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0; extra == "dev"
26
+ Dynamic: author-email
27
+ Dynamic: home-page
28
+ Dynamic: requires-python
29
+
30
+ # decterm
31
+
32
+ Cross-platform terminal you can drive from Python. Run shell commands and get output, with or without a GUI.
33
+
34
+ - **No GUI** (default): headless, ideal for scripts and servers.
35
+ - **GUI mode**: live terminal window (tkinter) where you can type commands and see output.
36
+
37
+ ## Quick start
38
+
39
+ ```bash
40
+ pip install decterm
41
+ ```
42
+
43
+ ```python
44
+ from decterm import DecTerm
45
+
46
+ # Headless
47
+ dt = DecTerm()
48
+ print(dt.send("dir")) # or "ls" on Unix
49
+ print(dt.sends("cd /tmp", "pwd"))
50
+ dt.close()
51
+
52
+ # With GUI
53
+ dt = DecTerm(gui=True)
54
+ dt.send("echo Hello")
55
+ # Type commands in the window, then:
56
+ dt.close()
57
+ ```
58
+
59
+ ## API (concise)
60
+
61
+ | Method | Description |
62
+ |--------|-------------|
63
+ | `DecTerm(gui=False, cwd=None, env=None)` | Start a shell. `gui=True` opens a terminal window. |
64
+ | `send(cmd, timeout=30)` | Run one command, return output. Times out for interactive programs (e.g. `python`). |
65
+ | `sends(*cmds, timeout=30)` | Run several commands in order; returns last command’s output. |
66
+ | `close()` | Stop the shell. |
67
+
68
+ Use `timeout=None` in `send()` to wait indefinitely.
69
+
70
+ ## Requirements
71
+
72
+ - Python 3.7+
73
+ - Windows, macOS, or Linux
74
+ - No extra packages (uses only the standard library, including tkinter for GUI).
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,49 @@
1
+ # decterm
2
+
3
+ Cross-platform terminal you can drive from Python. Run shell commands and get output, with or without a GUI.
4
+
5
+ - **No GUI** (default): headless, ideal for scripts and servers.
6
+ - **GUI mode**: live terminal window (tkinter) where you can type commands and see output.
7
+
8
+ ## Quick start
9
+
10
+ ```bash
11
+ pip install decterm
12
+ ```
13
+
14
+ ```python
15
+ from decterm import DecTerm
16
+
17
+ # Headless
18
+ dt = DecTerm()
19
+ print(dt.send("dir")) # or "ls" on Unix
20
+ print(dt.sends("cd /tmp", "pwd"))
21
+ dt.close()
22
+
23
+ # With GUI
24
+ dt = DecTerm(gui=True)
25
+ dt.send("echo Hello")
26
+ # Type commands in the window, then:
27
+ dt.close()
28
+ ```
29
+
30
+ ## API (concise)
31
+
32
+ | Method | Description |
33
+ |--------|-------------|
34
+ | `DecTerm(gui=False, cwd=None, env=None)` | Start a shell. `gui=True` opens a terminal window. |
35
+ | `send(cmd, timeout=30)` | Run one command, return output. Times out for interactive programs (e.g. `python`). |
36
+ | `sends(*cmds, timeout=30)` | Run several commands in order; returns last command’s output. |
37
+ | `close()` | Stop the shell. |
38
+
39
+ Use `timeout=None` in `send()` to wait indefinitely.
40
+
41
+ ## Requirements
42
+
43
+ - Python 3.7+
44
+ - Windows, macOS, or Linux
45
+ - No extra packages (uses only the standard library, including tkinter for GUI).
46
+
47
+ ## License
48
+
49
+ MIT
@@ -0,0 +1,16 @@
1
+ """
2
+ decterm - Cross-platform terminal controllable via Python (and web).
3
+
4
+ Usage:
5
+ from decterm import DecTerm
6
+
7
+ dt = DecTerm()
8
+ result = dt.send('dir')
9
+ result = dt.sends('cd e', 'mkdir aaaa')
10
+ dt.close()
11
+ """
12
+
13
+ from decterm.terminal import DecTerm
14
+
15
+ __all__ = ["DecTerm"]
16
+ __version__ = "0.1.0"
@@ -0,0 +1,448 @@
1
+ """
2
+ DecTerm - Cross-platform programmatic terminal session.
3
+
4
+ Runs a persistent shell; on GUI systems a visible terminal could be shown,
5
+ on headless systems (e.g. CentOS server) it runs without a window.
6
+ """
7
+
8
+ import os
9
+ import queue
10
+ import re
11
+ import sys
12
+ import subprocess
13
+ import threading
14
+
15
+ # We append this to each command and read until we see it (avoids prompt parsing).
16
+ _END_MARKER = "__DECTERM_END__"
17
+ _END_MARKER_BYTES = _END_MARKER.encode("utf-8")
18
+
19
+
20
+ def _is_windows():
21
+ return sys.platform == "win32"
22
+
23
+
24
+ class DecTerm:
25
+ """
26
+ A persistent terminal session. Use send() / sends() to run commands,
27
+ close() to shut down the shell.
28
+ """
29
+
30
+ def __init__(self, gui=False, shell=None, cwd=None, env=None):
31
+ """
32
+ Start a terminal (shell process).
33
+
34
+ :param gui: If True, show a GUI window with live output and an input box to send commands.
35
+ :param shell: Optional path to shell (default: cmd on Windows, bash on Unix).
36
+ :param cwd: Working directory (default: current).
37
+ :param env: Environment dict (default: inherit from os.environ).
38
+ """
39
+ self._gui = gui
40
+ self._cwd = cwd or os.getcwd()
41
+ self._env = dict(env) if env is not None else dict(os.environ)
42
+ self._shell_cmd = shell
43
+ self._process = None
44
+ self._closed = False
45
+ self._result_for_send = None # Queue for send() to get result in GUI mode
46
+ self._command_lock = threading.Lock()
47
+ self._display_queue = queue.Queue() if gui else None
48
+ self._command_done_event = threading.Event() if gui else None
49
+ self._gui_thread = None
50
+ self._skip_next_result = False # discard next marker result (after timeout)
51
+ self._result_queue = None if gui else queue.Queue() # non-GUI: reader puts here
52
+ self._reader_thread = None # non-GUI reader
53
+ self._start()
54
+
55
+ def _start(self):
56
+ if _is_windows():
57
+ shell_cmd = self._shell_cmd or "cmd.exe"
58
+ creationflags = 0
59
+ if _is_windows() and not self._gui:
60
+ creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
61
+ self._process = subprocess.Popen(
62
+ [shell_cmd, "/Q", "/K"],
63
+ stdin=subprocess.PIPE,
64
+ stdout=subprocess.PIPE,
65
+ stderr=subprocess.STDOUT,
66
+ cwd=self._cwd,
67
+ env=self._env,
68
+ creationflags=creationflags,
69
+ )
70
+ try:
71
+ self._process.stdin.write("chcp 65001 >nul\r\n".encode("utf-8"))
72
+ self._process.stdin.write("prompt \r\n".encode("utf-8"))
73
+ self._process.stdin.write(f"echo {_END_MARKER}\r\n".encode("utf-8"))
74
+ self._process.stdin.flush()
75
+ except Exception:
76
+ pass
77
+ self._read_until_marker(self._process.stdout)
78
+ else:
79
+ shell_cmd = self._shell_cmd or "/bin/bash"
80
+ self._process = subprocess.Popen(
81
+ [shell_cmd, "-i"],
82
+ stdin=subprocess.PIPE,
83
+ stdout=subprocess.PIPE,
84
+ stderr=subprocess.STDOUT,
85
+ cwd=self._cwd,
86
+ env={**self._env, "PS1": ""},
87
+ )
88
+ self._process.stdin.write(f"echo {_END_MARKER}\n".encode("utf-8"))
89
+ self._process.stdin.flush()
90
+ self._read_until_marker(self._process.stdout)
91
+
92
+ if not self._gui and self._result_queue is not None:
93
+ self._reader_thread = threading.Thread(target=self._non_gui_reader_loop, daemon=True)
94
+ self._reader_thread.start()
95
+
96
+ if self._gui:
97
+ self._command_done_event.set()
98
+ self._reader_thread = threading.Thread(target=self._gui_reader_loop, daemon=True)
99
+ self._reader_thread.start()
100
+ self._gui_thread = threading.Thread(target=self._run_gui, daemon=True)
101
+ self._gui_thread.start()
102
+ # Let GUI and reader start
103
+ threading.Event().wait(0.3)
104
+
105
+ def _read_until_marker(self, stream):
106
+ """Read from stream until we see _END_MARKER. Returns collected text."""
107
+ buf = bytearray()
108
+ while True:
109
+ chunk = stream.read(1)
110
+ if not chunk:
111
+ break
112
+ buf.extend(chunk)
113
+ if len(buf) >= len(_END_MARKER_BYTES) and bytes(buf[-len(_END_MARKER_BYTES):]) == _END_MARKER_BYTES:
114
+ break
115
+ if _END_MARKER_BYTES in buf:
116
+ break
117
+ return buf.decode("utf-8", errors="replace")
118
+
119
+ @staticmethod
120
+ def _strip_output(out, raw_cmd=None):
121
+ """Strip marker line, path prompt, echoed command from output."""
122
+ lines = out.replace("\r", "").splitlines()
123
+ result_lines = []
124
+ for line in lines:
125
+ if _END_MARKER in line:
126
+ break
127
+ if raw_cmd and raw_cmd in line and _END_MARKER in line:
128
+ continue
129
+ if raw_cmd and line.strip() == raw_cmd:
130
+ continue
131
+ result_lines.append(line)
132
+ result = "\n".join(result_lines).strip()
133
+ result = re.sub(r"^[A-Za-z]:\\[^\r\n]*>", "", result, count=1)
134
+ if result.startswith(">") and (len(result) == 1 or result[1] != ">"):
135
+ result = result[1:].lstrip()
136
+ return result
137
+
138
+ def _non_gui_reader_loop(self):
139
+ """Background: read until marker, put stripped output to _result_queue. For non-GUI mode."""
140
+ stream = self._process.stdout
141
+ buf = bytearray()
142
+ while not self._closed and self._process and self._process.poll() is None:
143
+ try:
144
+ chunk = stream.read(1)
145
+ except (OSError, ValueError):
146
+ break
147
+ if not chunk:
148
+ break
149
+ buf.extend(chunk)
150
+ if _END_MARKER_BYTES not in buf:
151
+ continue
152
+ idx = buf.decode("utf-8", errors="replace").find(_END_MARKER)
153
+ if idx < 0:
154
+ continue
155
+ before = buf[:idx].decode("utf-8", errors="replace")
156
+ buf.clear()
157
+ result = self._strip_output(before)
158
+ try:
159
+ self._result_queue.put(result)
160
+ except Exception:
161
+ pass
162
+ try:
163
+ self._result_queue.put(None)
164
+ except Exception:
165
+ pass
166
+
167
+ def _gui_reader_loop(self):
168
+ """Read process stdout in GUI mode; push chunks to display_queue and result to _result_for_send."""
169
+ stream = self._process.stdout
170
+ buf = bytearray()
171
+ last_displayed = 0
172
+ while self._process and self._process.poll() is None and not self._closed:
173
+ try:
174
+ chunk = stream.read(1)
175
+ except (OSError, ValueError):
176
+ break
177
+ if not chunk:
178
+ break
179
+ buf.extend(chunk)
180
+ try:
181
+ s = buf.decode("utf-8", errors="replace")
182
+ except Exception:
183
+ continue
184
+ if _END_MARKER in s:
185
+ idx = s.find(_END_MARKER)
186
+ before = s[:idx]
187
+ to_show = before[last_displayed:].replace("\r", "")
188
+ if to_show:
189
+ self._display_queue.put(to_show)
190
+ last_displayed = 0
191
+ buf.clear()
192
+ # Strip marker line and path prompt for result
193
+ lines = before.replace("\r", "").splitlines()
194
+ result_lines = [ln for ln in lines if _END_MARKER not in ln]
195
+ result = "\n".join(result_lines).strip()
196
+ result = re.sub(r"^[A-Za-z]:\\[^\r\n]*>", "", result, count=1)
197
+ if result.startswith(">") and (len(result) == 1 or result[1] != ">"):
198
+ result = result[1:].lstrip()
199
+ if getattr(self, "_skip_next_result", False):
200
+ self._skip_next_result = False
201
+ elif getattr(self, "_result_for_send", None) is not None:
202
+ try:
203
+ self._result_for_send.put(result)
204
+ except Exception:
205
+ pass
206
+ self._result_for_send = None
207
+ self._command_done_event.set()
208
+ else:
209
+ new_part = s[last_displayed:].replace("\r", "")
210
+ if new_part:
211
+ self._display_queue.put(new_part)
212
+ last_displayed = len(s)
213
+ self._display_queue.put(None) # EOF
214
+
215
+ def _run_gui(self):
216
+ """Run the tkinter terminal window (in a thread)."""
217
+ try:
218
+ import tkinter as tk
219
+ from tkinter import scrolledtext
220
+ except ImportError:
221
+ self._display_queue.put("[GUI requires tkinter]\n")
222
+ return
223
+ root = tk.Tk()
224
+ root.title("DecTerm")
225
+ root.geometry("860x520")
226
+ root.minsize(480, 320)
227
+
228
+ # 终端风格配色:深色背景 + 浅色字
229
+ bg_dark = "#0d1117"
230
+ bg_input = "#161b22"
231
+ fg_main = "#e6edf3"
232
+ fg_prompt = "#7ee787" # 绿色提示符
233
+ border = "#30363d"
234
+ insert_bg = "#58a6ff"
235
+
236
+ root.configure(bg=bg_dark)
237
+
238
+ # 输出区:等宽字体、合适行距
239
+ out = scrolledtext.ScrolledText(
240
+ root,
241
+ wrap=tk.WORD,
242
+ font=("Consolas", 11),
243
+ state=tk.DISABLED,
244
+ bg=bg_dark,
245
+ fg=fg_main,
246
+ insertbackground=insert_bg,
247
+ selectbackground="#264f78",
248
+ selectforeground=fg_main,
249
+ relief=tk.FLAT,
250
+ padx=12,
251
+ pady=12,
252
+ borderwidth=0,
253
+ )
254
+ out.pack(fill=tk.BOTH, expand=True, padx=8, pady=(8, 0))
255
+ out.tag_configure("prompt", foreground=fg_prompt)
256
+ out.tag_configure("output", foreground=fg_main)
257
+
258
+ # 输入行:单独背景、绿色提示符
259
+ frame = tk.Frame(root, bg=bg_input, highlightbackground=border, highlightthickness=1)
260
+ frame.pack(fill=tk.X, padx=8, pady=8)
261
+ prompt_label = tk.Label(
262
+ frame, text="> ", font=("Consolas", 11), fg=fg_prompt, bg=bg_input
263
+ )
264
+ prompt_label.pack(side=tk.LEFT, padx=(12, 4), pady=10)
265
+ entry = tk.Entry(
266
+ frame,
267
+ font=("Consolas", 11),
268
+ bg=bg_dark,
269
+ fg=fg_main,
270
+ insertbackground=insert_bg,
271
+ relief=tk.FLAT,
272
+ highlightthickness=0,
273
+ )
274
+ entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 12), pady=10, ipady=6)
275
+
276
+ def append_output(text, tag=None):
277
+ if text is None:
278
+ return
279
+ out.config(state=tk.NORMAL)
280
+ if tag:
281
+ out.insert(tk.END, text, tag)
282
+ else:
283
+ out.insert(tk.END, text)
284
+ out.see(tk.END)
285
+ out.config(state=tk.DISABLED)
286
+
287
+ def pump_display():
288
+ try:
289
+ while True:
290
+ msg = self._display_queue.get_nowait()
291
+ append_output(msg)
292
+ if msg is None:
293
+ break
294
+ except queue.Empty:
295
+ pass
296
+ if not self._closed and self._process and self._process.poll() is None:
297
+ root.after(50, pump_display)
298
+ else:
299
+ try:
300
+ while True:
301
+ msg = self._display_queue.get(timeout=0.1)
302
+ append_output(msg)
303
+ if msg is None:
304
+ break
305
+ except (queue.Empty, queue.Full):
306
+ pass
307
+
308
+ def on_send():
309
+ cmd = entry.get().strip()
310
+ if not cmd:
311
+ return
312
+ entry.delete(0, tk.END)
313
+ append_output("\n> " + cmd + "\n", "prompt")
314
+ threading.Thread(target=_user_send, args=(cmd,), daemon=True).start()
315
+
316
+ def _user_send(cmd):
317
+ with self._command_lock:
318
+ self._command_done_event.clear()
319
+ if self._closed or self._process is None or self._process.poll() is not None:
320
+ return
321
+ if _is_windows():
322
+ full = f"{cmd} & echo {_END_MARKER}\r\n"
323
+ else:
324
+ full = f"{cmd}; echo {_END_MARKER}\n"
325
+ try:
326
+ self._process.stdin.write(full.encode("utf-8"))
327
+ self._process.stdin.flush()
328
+ except Exception:
329
+ pass
330
+ self._command_done_event.wait(timeout=60)
331
+
332
+ entry.bind("<Return>", lambda e: on_send())
333
+ entry.focus_set()
334
+ root.after(50, pump_display)
335
+
336
+ def on_closing():
337
+ self.close()
338
+ root.destroy()
339
+
340
+ root.protocol("WM_DELETE_WINDOW", on_closing)
341
+ try:
342
+ root.mainloop()
343
+ except Exception:
344
+ pass
345
+
346
+ def send(self, command, timeout=30):
347
+ """
348
+ Run one command in the terminal and return its output (stdout + stderr).
349
+
350
+ For interactive commands (e.g. python, bash) that never print the end marker,
351
+ send() will return after timeout seconds with a hint instead of blocking forever.
352
+
353
+ :param command: Command string (e.g. 'dir', 'ls -la').
354
+ :param timeout: Max seconds to wait for command output (default 30). Use None to wait forever.
355
+ :return: Output text of the command (string).
356
+ """
357
+ if self._closed or self._process is None or self._process.poll() is not None:
358
+ raise RuntimeError("DecTerm is closed or not running")
359
+ raw = command.strip()
360
+ _timeout = timeout if timeout is not None else 1e9
361
+
362
+ if self._gui:
363
+ if self._skip_next_result:
364
+ self._skip_next_result = False
365
+ self._result_for_send = None
366
+ result_queue = queue.Queue()
367
+ with self._command_lock:
368
+ self._result_for_send = result_queue
369
+ self._command_done_event.clear()
370
+ if self._display_queue is not None:
371
+ self._display_queue.put("\n> " + raw + "\n")
372
+ if _is_windows():
373
+ full_cmd = f"{raw} & echo {_END_MARKER}\r\n"
374
+ else:
375
+ full_cmd = f"{raw}; echo {_END_MARKER}\n"
376
+ try:
377
+ self._process.stdin.write(full_cmd.encode("utf-8"))
378
+ self._process.stdin.flush()
379
+ except Exception:
380
+ self._result_for_send = None
381
+ return ""
382
+ try:
383
+ return result_queue.get(timeout=_timeout)
384
+ except queue.Empty:
385
+ self._skip_next_result = True
386
+ self._result_for_send = None
387
+ return "[DecTerm] Timeout - command may be interactive (e.g. python). Use GUI to interact or exit the subprocess."
388
+
389
+ # Non-GUI: discard stale result from previous timeout if any
390
+ if self._skip_next_result and self._result_queue is not None:
391
+ self._skip_next_result = False
392
+ try:
393
+ self._result_queue.get(timeout=0.5)
394
+ except queue.Empty:
395
+ pass
396
+ if _is_windows():
397
+ full_cmd = f"{raw} & echo {_END_MARKER}\r\n"
398
+ else:
399
+ full_cmd = f"{raw}; echo {_END_MARKER}\n"
400
+ try:
401
+ self._process.stdin.write(full_cmd.encode("utf-8"))
402
+ self._process.stdin.flush()
403
+ except Exception:
404
+ return ""
405
+ try:
406
+ result = self._result_queue.get(timeout=_timeout)
407
+ except queue.Empty:
408
+ self._skip_next_result = True
409
+ return "[DecTerm] Timeout - command may be interactive (e.g. python). Exit the subprocess (e.g. exit()) or use a longer timeout."
410
+ if result is None:
411
+ return ""
412
+ return result
413
+
414
+ def sends(self, *commands, timeout=30):
415
+ """
416
+ Run multiple commands in sequence in the same shell session.
417
+ Returns the output of the last command.
418
+
419
+ :param commands: One or more command strings.
420
+ :param timeout: Max seconds to wait per command (default 30). Passed to each send().
421
+ :return: Output of the last command (string).
422
+ """
423
+ result = None
424
+ for cmd in commands:
425
+ result = self.send(cmd, timeout=timeout)
426
+ return result
427
+
428
+ def close(self):
429
+ """Close the terminal and terminate the shell process."""
430
+ if self._closed:
431
+ return
432
+ self._closed = True
433
+ if self._process and self._process.poll() is None:
434
+ try:
435
+ self._process.terminate()
436
+ self._process.wait(timeout=3)
437
+ except (subprocess.TimeoutExpired, OSError):
438
+ try:
439
+ self._process.kill()
440
+ except OSError:
441
+ pass
442
+ self._process = None
443
+
444
+ def __enter__(self):
445
+ return self
446
+
447
+ def __exit__(self, *args):
448
+ self.close()
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.2
2
+ Name: decterm
3
+ Version: 0.1.0
4
+ Summary: Cross-platform terminal controllable via Python (and web)
5
+ Home-page: https://github.com/decrule/decterm
6
+ Author: decterm
7
+ Author-email: decrule@outlook.com
8
+ License: MIT
9
+ Keywords: terminal,shell,cross-platform,automation
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: System :: Shells
22
+ Requires-Python: >=3.7
23
+ Description-Content-Type: text/markdown
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0; extra == "dev"
26
+ Dynamic: author-email
27
+ Dynamic: home-page
28
+ Dynamic: requires-python
29
+
30
+ # decterm
31
+
32
+ Cross-platform terminal you can drive from Python. Run shell commands and get output, with or without a GUI.
33
+
34
+ - **No GUI** (default): headless, ideal for scripts and servers.
35
+ - **GUI mode**: live terminal window (tkinter) where you can type commands and see output.
36
+
37
+ ## Quick start
38
+
39
+ ```bash
40
+ pip install decterm
41
+ ```
42
+
43
+ ```python
44
+ from decterm import DecTerm
45
+
46
+ # Headless
47
+ dt = DecTerm()
48
+ print(dt.send("dir")) # or "ls" on Unix
49
+ print(dt.sends("cd /tmp", "pwd"))
50
+ dt.close()
51
+
52
+ # With GUI
53
+ dt = DecTerm(gui=True)
54
+ dt.send("echo Hello")
55
+ # Type commands in the window, then:
56
+ dt.close()
57
+ ```
58
+
59
+ ## API (concise)
60
+
61
+ | Method | Description |
62
+ |--------|-------------|
63
+ | `DecTerm(gui=False, cwd=None, env=None)` | Start a shell. `gui=True` opens a terminal window. |
64
+ | `send(cmd, timeout=30)` | Run one command, return output. Times out for interactive programs (e.g. `python`). |
65
+ | `sends(*cmds, timeout=30)` | Run several commands in order; returns last command’s output. |
66
+ | `close()` | Stop the shell. |
67
+
68
+ Use `timeout=None` in `send()` to wait indefinitely.
69
+
70
+ ## Requirements
71
+
72
+ - Python 3.7+
73
+ - Windows, macOS, or Linux
74
+ - No extra packages (uses only the standard library, including tkinter for GUI).
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ decterm/__init__.py
5
+ decterm/terminal.py
6
+ decterm.egg-info/PKG-INFO
7
+ decterm.egg-info/SOURCES.txt
8
+ decterm.egg-info/dependency_links.txt
9
+ decterm.egg-info/requires.txt
10
+ decterm.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+
2
+ [dev]
3
+ pytest>=7.0
@@ -0,0 +1 @@
1
+ decterm
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "decterm"
7
+ version = "0.1.0"
8
+ description = "Cross-platform terminal controllable via Python (and web)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.7"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "decterm" }]
13
+ keywords = ["terminal", "shell", "cross-platform", "automation"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.7",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: System :: Shells",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = ["pytest>=7.0"]
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["."]
34
+ include = ["decterm*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
decterm-0.1.0/setup.py ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """Setup script for decterm. Use: pip install . or python setup.py sdist bdist_wheel then twine upload dist/*"""
4
+
5
+ import os
6
+ from setuptools import find_packages, setup
7
+
8
+ # Package metadata (edit these for your PyPI listing)
9
+ NAME = "decterm"
10
+ DESCRIPTION = "Cross-platform terminal controllable via Python (and web)"
11
+ URL = "https://github.com/decrule/decterm"
12
+ EMAIL = "decrule@outlook.com"
13
+ AUTHOR = "DecRule"
14
+ REQUIRES_PYTHON = ">=3.7"
15
+ REQUIRED = [] # no runtime dependencies
16
+ EXTRAS = {"dev": ["pytest>=7.0"]}
17
+
18
+ # Version: prefer decterm.__version__, fallback to literal
19
+ here = os.path.abspath(os.path.dirname(__file__))
20
+ try:
21
+ import importlib.util
22
+ spec = importlib.util.spec_from_file_location("_decterm_version", os.path.join(here, "decterm", "__init__.py"))
23
+ mod = importlib.util.module_from_spec(spec)
24
+ spec.loader.exec_module(mod)
25
+ VERSION = getattr(mod, "__version__", "0.1.0")
26
+ except Exception:
27
+ VERSION = "0.1.0"
28
+
29
+ try:
30
+ with open(os.path.join(here, "README.md"), encoding="utf-8") as f:
31
+ long_description = f.read()
32
+ except FileNotFoundError:
33
+ long_description = DESCRIPTION
34
+
35
+ setup(
36
+ name=NAME,
37
+ version=VERSION,
38
+ description=DESCRIPTION,
39
+ long_description=long_description,
40
+ long_description_content_type="text/markdown",
41
+ author=AUTHOR,
42
+ author_email=EMAIL,
43
+ url=URL,
44
+ python_requires=REQUIRES_PYTHON,
45
+ packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]),
46
+ install_requires=REQUIRED,
47
+ extras_require=EXTRAS,
48
+ include_package_data=True,
49
+ license="MIT",
50
+ classifiers=[
51
+ "Development Status :: 3 - Alpha",
52
+ "Intended Audience :: Developers",
53
+ "License :: OSI Approved :: MIT License",
54
+ "Operating System :: OS Independent",
55
+ "Programming Language :: Python :: 3",
56
+ "Programming Language :: Python :: 3.7",
57
+ "Programming Language :: Python :: 3.8",
58
+ "Programming Language :: Python :: 3.9",
59
+ "Programming Language :: Python :: 3.10",
60
+ "Programming Language :: Python :: 3.11",
61
+ "Programming Language :: Python :: 3.12",
62
+ "Topic :: System :: Shells",
63
+ ],
64
+ )