manyterm 1.0.0__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.
- manyterm/__init__.py +2 -0
- manyterm/manyterm.py +130 -0
- manyterm/manyterm_tk.py +71 -0
- manyterm-1.0.0.dist-info/LICENSE +21 -0
- manyterm-1.0.0.dist-info/METADATA +46 -0
- manyterm-1.0.0.dist-info/RECORD +8 -0
- manyterm-1.0.0.dist-info/WHEEL +5 -0
- manyterm-1.0.0.dist-info/top_level.txt +1 -0
manyterm/__init__.py
ADDED
manyterm/manyterm.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import socket
|
|
3
|
+
import atexit
|
|
4
|
+
import uuid
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
_MANYTERM_PORT = 16161
|
|
9
|
+
_MANYTERM_HOST = '127.0.0.1'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if sys.platform not in ["linux", "win32", "darwin"]:
|
|
13
|
+
raise Exception(f"Platform \"{sys.platform}\" not supported for package manyterm")
|
|
14
|
+
|
|
15
|
+
# server for sending text and listening for new connections
|
|
16
|
+
# all static methods so there is only one instance
|
|
17
|
+
class _Server:
|
|
18
|
+
|
|
19
|
+
_running = False
|
|
20
|
+
_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
21
|
+
_connections = {}
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def start():
|
|
25
|
+
if not _Server._running:
|
|
26
|
+
_Server._running = True
|
|
27
|
+
_Server._socket.bind((_MANYTERM_HOST, _MANYTERM_PORT))
|
|
28
|
+
_Server._socket.settimeout(10)
|
|
29
|
+
# close server and windows on exit
|
|
30
|
+
atexit.register(_Server.stop)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# listen for new connections
|
|
34
|
+
@staticmethod
|
|
35
|
+
def listen():
|
|
36
|
+
data, addr = _Server._socket.recvfrom(1024)
|
|
37
|
+
if data is not None:
|
|
38
|
+
_Server._connections[data.decode('utf-8')] = addr
|
|
39
|
+
else:
|
|
40
|
+
assert Exception("Could not bind window to server")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@staticmethod # print to a window based on the uid
|
|
44
|
+
def send(uid, b):
|
|
45
|
+
addr = _Server._connections.get(uid, None)
|
|
46
|
+
if addr is not None:
|
|
47
|
+
_Server._socket.sendto(b, addr)
|
|
48
|
+
else:
|
|
49
|
+
assert Exception("Cannot print to closed terminal")
|
|
50
|
+
|
|
51
|
+
@staticmethod # close a window based on the uid
|
|
52
|
+
def close(uid):
|
|
53
|
+
_Server._socket.sendto(bytes(0), _Server._connections.get(uid))
|
|
54
|
+
_Server._connections.pop(uid)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def stop(): # close all windows
|
|
58
|
+
for key in _Server._connections:
|
|
59
|
+
_Server._socket.sendto(bytes(0), _Server._connections.get(key))
|
|
60
|
+
_Server._socket.close()
|
|
61
|
+
_Server._running = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Terminal:
|
|
65
|
+
_path = os.path.abspath(__file__)
|
|
66
|
+
|
|
67
|
+
def __init__(self, title="Terminal"):
|
|
68
|
+
"""
|
|
69
|
+
Start a new terminal window
|
|
70
|
+
:param title: the title of the window (linux only)
|
|
71
|
+
"""
|
|
72
|
+
# start server (if not running)
|
|
73
|
+
_Server.start()
|
|
74
|
+
# make uid for self
|
|
75
|
+
self._uid = str(uuid.uuid4()) # could also use uuid1()
|
|
76
|
+
# open window
|
|
77
|
+
if sys.platform == "linux":
|
|
78
|
+
subprocess.run(('gnome-terminal', '-t', f'{title}', '--', 'bash', '-c', f'{sys.executable} {self._path} {self._uid}'), shell=False)
|
|
79
|
+
elif sys.platform == "win32":
|
|
80
|
+
subprocess.run(('start', '/wait','cmd', '/c', f'{sys.executable} {self._path} {self._uid}'), shell=True)
|
|
81
|
+
elif sys.platform == "darwin":
|
|
82
|
+
subprocess.run(("osascript", "-e", f"tell application \"Terminal\" to do script \"{sys.executable} {self._path} {self._uid};exit\""), shell=False)
|
|
83
|
+
_Server.listen() # wait for window to open and listen for new connection
|
|
84
|
+
|
|
85
|
+
def print(self, txt, end="\n"):
|
|
86
|
+
"""
|
|
87
|
+
Prints to the window
|
|
88
|
+
:param txt: the string to print
|
|
89
|
+
:param end: the end of the string
|
|
90
|
+
"""
|
|
91
|
+
_Server.send(self._uid, bytes(txt+end, 'utf-8'))
|
|
92
|
+
|
|
93
|
+
def close(self):
|
|
94
|
+
"""
|
|
95
|
+
Closes the window
|
|
96
|
+
"""
|
|
97
|
+
_Server.close(self._uid)
|
|
98
|
+
|
|
99
|
+
def close_all(self):
|
|
100
|
+
"""
|
|
101
|
+
Closes all windows
|
|
102
|
+
"""
|
|
103
|
+
_Server.stop()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# called from subprocess to open window
|
|
107
|
+
# acts as a client to connect to the server and listens for data to print
|
|
108
|
+
class _TerminalClient:
|
|
109
|
+
def __init__(self):
|
|
110
|
+
if len(sys.argv) != 2:
|
|
111
|
+
raise Exception("Usage: python3 manyterm.py <uid>")
|
|
112
|
+
|
|
113
|
+
self._uid = sys.argv[1]
|
|
114
|
+
|
|
115
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
116
|
+
|
|
117
|
+
# send uid to server
|
|
118
|
+
s.sendto(bytes(self._uid, 'utf-8'), (_MANYTERM_HOST, _MANYTERM_PORT))
|
|
119
|
+
|
|
120
|
+
while True:
|
|
121
|
+
data, addr = s.recvfrom(1024)
|
|
122
|
+
if data == bytes(0):
|
|
123
|
+
break
|
|
124
|
+
else:
|
|
125
|
+
print(data.decode('utf-8'), end="")
|
|
126
|
+
s.close()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == '__main__':
|
|
130
|
+
_TerminalClient()
|
manyterm/manyterm_tk.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from time import sleep
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
from tkinter import ttk
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Terminal(threading.Thread):
|
|
8
|
+
|
|
9
|
+
def __init__(self, title="Terminal", height=20, width=100, font=("Courier New", "12"), backgroundColor="gray5", textColor="snow", allowClosing=True, redirectClosedPrints=True):
|
|
10
|
+
#params
|
|
11
|
+
self.title = title
|
|
12
|
+
self.height = height
|
|
13
|
+
self.width = width
|
|
14
|
+
self.font = font
|
|
15
|
+
self.backgroundColor = backgroundColor
|
|
16
|
+
self.textColor = textColor
|
|
17
|
+
self.allowClosing = allowClosing
|
|
18
|
+
self.redirectClosedPrints = redirectClosedPrints
|
|
19
|
+
#internal variables
|
|
20
|
+
self._root = None # main window
|
|
21
|
+
self._tb = None # text box
|
|
22
|
+
self.isOpen = False # if the window is open
|
|
23
|
+
self._safeExit = False # if the window is safe to exit
|
|
24
|
+
threading.Thread.__init__(self, daemon=True) # kill child thread on main thread exit
|
|
25
|
+
self.start()
|
|
26
|
+
sleep(0.5) # wait for window to open
|
|
27
|
+
|
|
28
|
+
def close(self):
|
|
29
|
+
if self.isOpen and self._safeExit: # safeExit is True is no operations are being done (i.e. print)
|
|
30
|
+
self.isOpen = False
|
|
31
|
+
self._root.quit()
|
|
32
|
+
self._root.update()
|
|
33
|
+
self._root = None
|
|
34
|
+
|
|
35
|
+
def run(self):
|
|
36
|
+
self._root = tk.Tk()
|
|
37
|
+
if self.allowClosing: self._root.protocol("WM_DELETE_WINDOW", self.close)
|
|
38
|
+
else: self._root.protocol("WM_DELETE_WINDOW", lambda: None)
|
|
39
|
+
self._root.title(self.title)
|
|
40
|
+
self._tb = tk.Text(self._root, height=self.height, width=self.width, wrap="none", font=self.font)
|
|
41
|
+
self._tb.pack(side="left", fill="both", expand=True)
|
|
42
|
+
self._tb.configure(state="disabled", bg=self.backgroundColor, fg=self.textColor)
|
|
43
|
+
sizegrip = ttk.Sizegrip(self._tb)
|
|
44
|
+
sizegrip.configure(cursor="sizing")
|
|
45
|
+
sizegrip.bind("<1>", self._resize_start)
|
|
46
|
+
sizegrip.bind("<B1-Motion>", self._resize_update)
|
|
47
|
+
self.isOpen = True
|
|
48
|
+
self._safeExit = True
|
|
49
|
+
self._root.mainloop()
|
|
50
|
+
|
|
51
|
+
def print(self, toPrint, end="\n"):
|
|
52
|
+
if self._root is None or not self.isOpen:
|
|
53
|
+
if self.redirectClosedPrints: print(toPrint, end=end) # redirect output
|
|
54
|
+
#return print(f"Terminal \"{self.title}\" is closed")
|
|
55
|
+
#raise Exception(f"Terminal \"{self.title}\" is closed")
|
|
56
|
+
else:
|
|
57
|
+
self._safeExit = False # needed so that we don't kill mainloop while printing
|
|
58
|
+
self._tb.configure(state="normal")
|
|
59
|
+
self._tb.see("end")
|
|
60
|
+
self._tb.insert("end", f"{toPrint}{end}")
|
|
61
|
+
self._tb.configure(state="disabled")
|
|
62
|
+
self._safeExit = True
|
|
63
|
+
|
|
64
|
+
def _resize_start(self, event):
|
|
65
|
+
self._x = event.x
|
|
66
|
+
self._y = event.y
|
|
67
|
+
|
|
68
|
+
def _resize_update(self, event):
|
|
69
|
+
delta_x = event.x - self._x
|
|
70
|
+
delta_y = event.y - self._y
|
|
71
|
+
self._tb.place_configure(width=self._tb.winfo_width() + delta_x, height=self._tb.winfo_height() + delta_y)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Tyler Bowers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: manyterm
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: manyterm - spawn multiple terminals to print to
|
|
5
|
+
Author: Tyler Bowers
|
|
6
|
+
Author-email: tylerebowers@gmail.com
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Source, https://github.com/tylerebowers/manyterm-python
|
|
9
|
+
Project-URL: PyPI, https://pypi.org/project/manyterm/
|
|
10
|
+
Keywords: python,terminal,print
|
|
11
|
+
Classifier: Topic :: Terminals
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
|
|
22
|
+
# Manyterm
|
|
23
|
+
Spawn multiple terminals to print to.
|
|
24
|
+
Compatible with Windows, Linux, and MacOS.
|
|
25
|
+
### Installation
|
|
26
|
+
`pip install manyterm`
|
|
27
|
+
### Usage
|
|
28
|
+
```py
|
|
29
|
+
import manyterm
|
|
30
|
+
|
|
31
|
+
t1 = manyterm.Terminal() # init a new window
|
|
32
|
+
|
|
33
|
+
t1.print("Hello World") # print to the window
|
|
34
|
+
|
|
35
|
+
t1.print("supercalifragilisticexpialidocious", end="") # default end="\n"
|
|
36
|
+
|
|
37
|
+
t1.close() # close the window
|
|
38
|
+
```
|
|
39
|
+
### Description
|
|
40
|
+
This program creates multiple terminals in python. New terminals can only be printed to (no input). `tests/test.py` contains an example of how to use this program. A server is used to print to client windows based on a uuid. When a new window object is made `manyterm.Terminal()` the program check to make sure that the server is running, then assigns itself a uuid, and then the program calls itself from a new terminal using the uuid as an argument. The new program contacts the server with its' uuid and the connection is made.
|
|
41
|
+
## Windows 11
|
|
42
|
+

|
|
43
|
+
## Linux (Ubuntu 23.10)
|
|
44
|
+

|
|
45
|
+
## MacOS Sonoma
|
|
46
|
+

|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
manyterm/__init__.py,sha256=j0xHtJEbaS7LjUBZvFVIuRnvvmgJCup7nGkamf4It1I,65
|
|
2
|
+
manyterm/manyterm.py,sha256=bD3JLdL5tp4kWXQp9jlmiUNp-txtNCwOuC7HJc85C1A,3968
|
|
3
|
+
manyterm/manyterm_tk.py,sha256=oyTN9AYgMBqPcgDerTML5GGDIipfqv94zatd53pw4fY,2962
|
|
4
|
+
manyterm-1.0.0.dist-info/LICENSE,sha256=hjzpaUekXMmk3H45s6cm049FnUxrJ4QDOhv_8ClITO8,1069
|
|
5
|
+
manyterm-1.0.0.dist-info/METADATA,sha256=zo0_srBLSml6sc0f0FVBqdkq7STOD8I-DBCRjfJ9VO4,1887
|
|
6
|
+
manyterm-1.0.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
7
|
+
manyterm-1.0.0.dist-info/top_level.txt,sha256=A9uJ6-Un6lnPFdgdge78cCzPqG4bbp8yYE9Ky1YOl4M,9
|
|
8
|
+
manyterm-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
manyterm
|