com2tty 0.1.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.
com2tty/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
com2tty/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from com2tty.cli import main
2
+
3
+ if __name__ == "__main__": # pragma: no cover
4
+ main()
com2tty/bridge.py ADDED
@@ -0,0 +1,131 @@
1
+ import sys
2
+ import os
3
+ import select
4
+ import argparse
5
+ import signal
6
+ import traceback
7
+
8
+ def cleanup_symlink(path):
9
+ if path and os.path.exists(path):
10
+ try:
11
+ os.unlink(path)
12
+ sys.stderr.write(f"Removed symlink: {path}\n")
13
+ sys.stderr.flush()
14
+ except Exception as e: # pragma: no cover
15
+ sys.stderr.write(f"Failed to remove symlink {path}: {e}\n")
16
+ sys.stderr.flush()
17
+
18
+ def main():
19
+ parser = argparse.ArgumentParser(description="com2tty WSL Bridge Helper")
20
+ parser.add_argument(
21
+ "-s", "--symlink",
22
+ required=True,
23
+ help="Target symlink path for the pseudo-terminal device."
24
+ )
25
+ args = parser.parse_args()
26
+
27
+ target_path = args.symlink
28
+ created_symlink = None
29
+
30
+ # We must keep both master and slave descriptors open.
31
+ # Keeping slave_fd open prevents EIO errors on the master side when
32
+ # WSL clients open and close the virtual serial port.
33
+ master_fd = None
34
+ slave_fd = None
35
+
36
+ try:
37
+ master_fd, slave_fd = os.openpty()
38
+ slave_name = os.ttyname(slave_fd)
39
+
40
+ sys.stderr.write(f"Created pseudo-terminal: master_fd={master_fd}, slave={slave_name}\n")
41
+ sys.stderr.flush()
42
+
43
+ # Attempt to create the symlink at target path
44
+ try:
45
+ if os.path.lexists(target_path):
46
+ os.unlink(target_path)
47
+ os.symlink(slave_name, target_path)
48
+ created_symlink = target_path
49
+ sys.stderr.write(f"Successfully symlinked {target_path} -> {slave_name}\n")
50
+ sys.stderr.flush()
51
+ except PermissionError:
52
+ # Fallback to /tmp if write permission to dev is denied
53
+ basename = os.path.basename(target_path)
54
+ fallback_path = f"/tmp/{basename}"
55
+ sys.stderr.write(f"Warning: Permission denied creating symlink at {target_path}.\n")
56
+ sys.stderr.write(f"Attempting fallback to user-writable path: {fallback_path}...\n")
57
+ sys.stderr.flush()
58
+
59
+ if os.path.lexists(fallback_path):
60
+ os.unlink(fallback_path)
61
+ os.symlink(slave_name, fallback_path)
62
+ created_symlink = fallback_path
63
+
64
+ sys.stderr.write(f"Fallback successful: {fallback_path} -> {slave_name}\n")
65
+ sys.stderr.write("--------------------------------------------------\n")
66
+ sys.stderr.write(f"To use the desired device path '{target_path}', please run this command ONCE in WSL:\n")
67
+ sys.stderr.write(f" sudo ln -sf {fallback_path} {target_path}\n")
68
+ sys.stderr.write("--------------------------------------------------\n")
69
+ sys.stderr.flush()
70
+
71
+ # Select loop
72
+ # 0 is stdin, master_fd is the pseudo-terminal master
73
+ sys.stderr.write("WSL bridge enter main loop.\n")
74
+ sys.stderr.flush()
75
+
76
+ while True:
77
+ # select blocks until data is available on stdin or master_fd
78
+ r, w, x = select.select([0, master_fd], [], [])
79
+
80
+ if 0 in r:
81
+ data = os.read(0, 4096)
82
+ if not data:
83
+ sys.stderr.write("EOF on stdin. Exiting.\n")
84
+ sys.stderr.flush()
85
+ break
86
+ os.write(master_fd, data)
87
+
88
+ if master_fd in r:
89
+ # Read from the virtual serial port
90
+ try:
91
+ data = os.read(master_fd, 4096)
92
+ if not data:
93
+ # Should not happen typically while slave_fd is kept open,
94
+ # but handle it gracefully if it does.
95
+ sys.stderr.write("EOF on PTY master. Exiting.\n")
96
+ sys.stderr.flush()
97
+ break
98
+ os.write(1, data)
99
+ except OSError as e:
100
+ # In case of EIO (Input/output error) when slave closes,
101
+ # just ignore it and continue since we keep slave_fd open.
102
+ if e.errno == 5: # EIO
103
+ continue
104
+ else:
105
+ raise e
106
+
107
+ except KeyboardInterrupt: # pragma: no cover
108
+ sys.stderr.write("WSL bridge interrupted by signal.\n")
109
+ sys.stderr.flush()
110
+ except Exception as e: # pragma: no cover
111
+ sys.stderr.write(f"WSL bridge error: {traceback.format_exc()}\n")
112
+ sys.stderr.flush()
113
+ finally:
114
+ # Clean up symlink and file descriptors
115
+ if created_symlink:
116
+ cleanup_symlink(created_symlink)
117
+ if slave_fd is not None:
118
+ try:
119
+ os.close(slave_fd)
120
+ except Exception:
121
+ pass
122
+ if master_fd is not None:
123
+ try:
124
+ os.close(master_fd)
125
+ except Exception:
126
+ pass
127
+ sys.stderr.write("WSL bridge shut down.\n")
128
+ sys.stderr.flush()
129
+
130
+ if __name__ == "__main__": # pragma: no cover
131
+ main()
com2tty/cli.py ADDED
@@ -0,0 +1,112 @@
1
+ import argparse
2
+ import sys
3
+ import logging
4
+ from com2tty.host import run_bridge
5
+
6
+ def main():
7
+ parser = argparse.ArgumentParser(
8
+ description="com2tty: Forward Windows COM ports to WSL virtual ttyUSB devices."
9
+ )
10
+
11
+ parser.add_argument(
12
+ "port",
13
+ help="Windows COM port to connect to (e.g. COM3 or COM1)."
14
+ )
15
+
16
+ parser.add_argument(
17
+ "-b", "--baud",
18
+ type=int,
19
+ default=9600,
20
+ help="Baud rate for the serial port (default: 9600)."
21
+ )
22
+
23
+ parser.add_argument(
24
+ "-w", "--wsl-tty",
25
+ default="/tmp/ttyUSB0",
26
+ help="Target symlink path inside WSL (default: /tmp/ttyUSB0)."
27
+ )
28
+
29
+ parser.add_argument(
30
+ "--bytesize",
31
+ type=int,
32
+ choices=[5, 6, 7, 8],
33
+ default=8,
34
+ help="Serial byte size (default: 8)."
35
+ )
36
+
37
+ parser.add_argument(
38
+ "--parity",
39
+ choices=["N", "E", "O", "S", "M"],
40
+ default="N",
41
+ help="Serial parity: None, Even, Odd, Space, Mark (default: N)."
42
+ )
43
+
44
+ parser.add_argument(
45
+ "--stopbits",
46
+ type=float,
47
+ choices=[1, 1.5, 2],
48
+ default=1,
49
+ help="Serial stop bits: 1, 1.5, or 2 (default: 1)."
50
+ )
51
+
52
+ parser.add_argument(
53
+ "--xonxoff",
54
+ action="store_true",
55
+ help="Enable software flow control (XON/XOFF)."
56
+ )
57
+
58
+ parser.add_argument(
59
+ "--rtscts",
60
+ action="store_true",
61
+ help="Enable hardware flow control (RTS/CTS)."
62
+ )
63
+
64
+ parser.add_argument(
65
+ "--dsrdtr",
66
+ action="store_true",
67
+ help="Enable hardware flow control (DSR/DTR)."
68
+ )
69
+
70
+ parser.add_argument(
71
+ "-d", "--debug",
72
+ action="store_true",
73
+ help="Enable debug logging output."
74
+ )
75
+
76
+ args = parser.add_argument_group("advanced")
77
+
78
+ parsed_args = parser.parse_args()
79
+
80
+ # Configure logging
81
+ log_level = logging.DEBUG if parsed_args.debug else logging.INFO
82
+ logging.basicConfig(
83
+ level=log_level,
84
+ format="[%(asctime)s] %(levelname)s: %(message)s",
85
+ datefmt="%H:%M:%S",
86
+ stream=sys.stderr
87
+ )
88
+
89
+ try:
90
+ run_bridge(
91
+ port=parsed_args.port,
92
+ baud=parsed_args.baud,
93
+ wsl_tty=parsed_args.wsl_tty,
94
+ bytesize=parsed_args.bytesize,
95
+ parity=parsed_args.parity,
96
+ stopbits=parsed_args.stopbits,
97
+ xonxoff=parsed_args.xonxoff,
98
+ rtscts=parsed_args.rtscts,
99
+ dsrdtr=parsed_args.dsrdtr
100
+ )
101
+ except KeyboardInterrupt:
102
+ logging.info("Interrupted by user. Exiting.")
103
+ sys.exit(0)
104
+ except Exception as e:
105
+ logging.error(f"Fatal error: {e}")
106
+ if parsed_args.debug:
107
+ import traceback
108
+ traceback.print_exc()
109
+ sys.exit(1)
110
+
111
+ if __name__ == "__main__": # pragma: no cover
112
+ main()
com2tty/host.py ADDED
@@ -0,0 +1,180 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import logging
5
+ import subprocess
6
+ import threading
7
+ import serial
8
+
9
+ def get_wsl_path(win_path):
10
+ cmd = ["wsl", "wslpath", "-u", win_path]
11
+ try:
12
+ res = subprocess.run(cmd, capture_output=True, text=True, check=True)
13
+ return res.stdout.strip()
14
+ except Exception as e:
15
+ logging.debug(f"wslpath failed: {e}. Using fallback conversion.")
16
+ # Fallback to mounting convention /mnt/<drive>/...
17
+ drive = win_path[0].lower()
18
+ path = win_path[2:].replace("\\", "/")
19
+ return f"/mnt/{drive}{path}"
20
+
21
+ def get_serial_settings(bytesize, parity, stopbits):
22
+ bytesize_map = {
23
+ 5: serial.FIVEBITS,
24
+ 6: serial.SIXBITS,
25
+ 7: serial.SEVENBITS,
26
+ 8: serial.EIGHTBITS
27
+ }
28
+ parity_map = {
29
+ "N": serial.PARITY_NONE,
30
+ "E": serial.PARITY_EVEN,
31
+ "O": serial.PARITY_ODD,
32
+ "S": serial.PARITY_SPACE,
33
+ "M": serial.PARITY_MARK
34
+ }
35
+ stopbits_map = {
36
+ 1: serial.STOPBITS_ONE,
37
+ 1.5: serial.STOPBITS_ONE_POINT_FIVE,
38
+ 2: serial.STOPBITS_TWO
39
+ }
40
+
41
+ return (
42
+ bytesize_map.get(bytesize, serial.EIGHTBITS),
43
+ parity_map.get(parity, serial.PARITY_NONE),
44
+ stopbits_map.get(stopbits, serial.STOPBITS_ONE)
45
+ )
46
+
47
+ def read_wsl_stdout(proc, ser, shutdown_event):
48
+ logging.debug("WSL-to-COM thread started.")
49
+ try:
50
+ while not shutdown_event.is_set():
51
+ data = proc.stdout.read(1024)
52
+ if not data:
53
+ logging.info("WSL process stdout reached EOF (exited).")
54
+ break
55
+ logging.debug(f"WSL -> COM: {len(data)} bytes")
56
+ ser.write(data)
57
+ ser.flush()
58
+ except Exception as e:
59
+ if not shutdown_event.is_set():
60
+ logging.error(f"Error in WSL-to-COM thread: {e}")
61
+ finally:
62
+ shutdown_event.set()
63
+
64
+ def read_com_port(ser, proc, shutdown_event):
65
+ logging.debug("COM-to-WSL thread started.")
66
+ try:
67
+ while not shutdown_event.is_set():
68
+ # Short timeout allows periodic checking of shutdown_event
69
+ data = ser.read(1024)
70
+ if data:
71
+ logging.debug(f"COM -> WSL: {len(data)} bytes")
72
+ proc.stdin.write(data)
73
+ proc.stdin.flush()
74
+ except Exception as e:
75
+ if not shutdown_event.is_set():
76
+ logging.error(f"Error in COM-to-WSL thread: {e}")
77
+ finally:
78
+ shutdown_event.set()
79
+
80
+ def read_wsl_stderr(proc, shutdown_event):
81
+ logging.debug("WSL stderr logging thread started.")
82
+ try:
83
+ while not shutdown_event.is_set():
84
+ line = proc.stderr.readline()
85
+ if not line:
86
+ break
87
+ line_str = line.decode("utf-8", errors="replace").strip()
88
+ if line_str:
89
+ logging.info(f"[WSL] {line_str}")
90
+ except Exception as e:
91
+ if not shutdown_event.is_set():
92
+ logging.debug(f"Error in WSL stderr thread: {e}")
93
+
94
+ def run_bridge(port, baud, wsl_tty, bytesize, parity, stopbits, xonxoff, rtscts, dsrdtr):
95
+ # Resolve serial settings
96
+ ser_bytesize, ser_parity, ser_stopbits = get_serial_settings(bytesize, parity, stopbits)
97
+
98
+ logging.info(f"Opening Windows serial port {port} at {baud} baud...")
99
+ ser = serial.Serial(
100
+ port=port,
101
+ baudrate=baud,
102
+ bytesize=ser_bytesize,
103
+ parity=ser_parity,
104
+ stopbits=ser_stopbits,
105
+ xonxoff=xonxoff,
106
+ rtscts=rtscts,
107
+ dsrdtr=dsrdtr,
108
+ timeout=0.2 # Enable timeout for shutdown check
109
+ )
110
+
111
+ # Locate WSL bridge.py script
112
+ current_dir = os.path.dirname(os.path.abspath(__file__))
113
+ bridge_script = os.path.join(current_dir, "bridge.py")
114
+ if not os.path.exists(bridge_script):
115
+ raise FileNotFoundError(f"WSL bridge script not found at: {bridge_script}")
116
+
117
+ wsl_bridge_path = get_wsl_path(bridge_script)
118
+ logging.info(f"WSL bridge script resolved to: {wsl_bridge_path}")
119
+
120
+ cmd = ["wsl", "python3", "-u", wsl_bridge_path, "--symlink", wsl_tty]
121
+ logging.info(f"Spawning WSL process: {' '.join(cmd)}")
122
+
123
+ # Use CREATE_NO_WINDOW to prevent wsl.exe from modifying the Windows console mode,
124
+ # which would otherwise disable Ctrl+C (ENABLE_PROCESSED_INPUT) for the Python CLI.
125
+ CREATE_NO_WINDOW = 0x08000000
126
+ proc = subprocess.Popen(
127
+ cmd,
128
+ stdin=subprocess.PIPE,
129
+ stdout=subprocess.PIPE,
130
+ stderr=subprocess.PIPE,
131
+ bufsize=0,
132
+ creationflags=CREATE_NO_WINDOW
133
+ )
134
+
135
+ shutdown_event = threading.Event()
136
+
137
+ # Start thread routing
138
+ t_wsl_to_com = threading.Thread(target=read_wsl_stdout, args=(proc, ser, shutdown_event), daemon=True)
139
+ t_com_to_wsl = threading.Thread(target=read_com_port, args=(ser, proc, shutdown_event), daemon=True)
140
+ t_wsl_stderr = threading.Thread(target=read_wsl_stderr, args=(proc, shutdown_event), daemon=True)
141
+
142
+ t_wsl_to_com.start()
143
+ t_com_to_wsl.start()
144
+ t_wsl_stderr.start()
145
+
146
+ logging.info("Bridge is fully active. Press Ctrl+C to stop.")
147
+
148
+ try:
149
+ while not shutdown_event.is_set():
150
+ # Wait and check if the WSL process is still running
151
+ if proc.poll() is not None:
152
+ logging.info("WSL subprocess exited unexpectedly.")
153
+ break
154
+ time.sleep(0.5)
155
+ except KeyboardInterrupt:
156
+ logging.info("Stopping bridge due to KeyboardInterrupt...")
157
+ finally:
158
+ shutdown_event.set()
159
+
160
+ logging.info("Cleaning up resources...")
161
+ # Wait for threads to exit to avoid pyserial close() deadlock on Windows
162
+ t_com_to_wsl.join(timeout=0.5)
163
+
164
+ # Close serial port first
165
+ try:
166
+ ser.close()
167
+ logging.info("Closed Windows serial port.")
168
+ except Exception as e:
169
+ logging.debug(f"Error closing serial port: {e}")
170
+
171
+ # Terminate WSL subprocess
172
+ if proc.poll() is None:
173
+ logging.info("Terminating WSL process...")
174
+ proc.terminate()
175
+ try:
176
+ proc.wait(timeout=3.0)
177
+ except subprocess.TimeoutExpired:
178
+ logging.warning("WSL process did not exit. Killing it.")
179
+ proc.kill()
180
+ logging.info("Bridge stopped successfully.")
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: com2tty
3
+ Version: 0.1.0
4
+ Summary: A Windows COM port to WSL ttyUSB forwarder
5
+ Author-email: yichengs <yichengs.tw+com2tty@gmail.com>
6
+ Project-URL: Homepage, https://github.com/Yi-Cheng-Wang/com2tty
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: Microsoft :: Windows
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pyserial>=3.5
15
+ Dynamic: license-file
16
+
17
+ # com2tty
18
+
19
+ `com2tty` is a Python package designed to forward serial communications from a Windows COM port into a WSL (Windows Subsystem for Linux) instance, presenting it as a virtual `ttyUSB` (or similar) serial device in WSL.
20
+
21
+ It does this using a low-latency, firewall-resilient **process-pipe bridge** over standard input/output redirection. It requires **no network or firewall configuration**.
22
+
23
+ ## Architecture Overview
24
+
25
+ 1. The Windows host process opens the physical COM port (using `pyserial`).
26
+ 2. It spawns the WSL Python bridge helper in the background, redirecting its stdin/stdout.
27
+ 3. Inside WSL, the bridge helper opens a pseudo-terminal (PTY) and creates a symbolic link to the PTY's slave file.
28
+ 4. Data is piped bidirectionally between the physical Windows COM port and the WSL virtual device.
29
+
30
+ ```
31
+ [Windows COM Port] <--> [com2tty host] <--> (stdin/stdout pipe) <--> [wsl bridge] <--> [PTY Slave /tmp/ttyUSB0]
32
+ ```
33
+
34
+ ## Requirements
35
+
36
+ - **Windows Host**: Python 3.8+ and `pyserial` (installed automatically).
37
+ - **WSL Guest**: Python 3.x (uses standard library modules only, no dependencies required).
38
+
39
+ ## Installation
40
+
41
+ Install the package on the Windows host by running the following command in the project root:
42
+
43
+ ```cmd
44
+ pip install .
45
+ ```
46
+
47
+ For development, you can install it in editable mode:
48
+
49
+ ```cmd
50
+ pip install -e .
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ Run `com2tty` from any Windows terminal (PowerShell or Command Prompt).
56
+
57
+ ```cmd
58
+ com2tty <COM_PORT> [options]
59
+ ```
60
+
61
+ ### Examples
62
+
63
+ Bridge **COM3** to the default WSL path `/tmp/ttyUSB0` at 115200 baud:
64
+ ```cmd
65
+ com2tty COM3 --baud 115200
66
+ ```
67
+
68
+ Bridge **COM5** to a custom WSL device path `/tmp/my_device`:
69
+ ```cmd
70
+ com2tty COM5 --baud 9600 -w /tmp/my_device
71
+ ```
72
+
73
+ ### Options
74
+
75
+ ```
76
+ positional arguments:
77
+ port Windows COM port to connect to (e.g. COM3).
78
+
79
+ options:
80
+ -h, --help show this help message and exit
81
+ -b BAUD, --baud BAUD Baud rate for the serial port (default: 9600).
82
+ -w WSL_TTY, --wsl-tty WSL_TTY
83
+ Target symlink path inside WSL (default: /tmp/ttyUSB0).
84
+ --bytesize {5,6,7,8} Serial byte size (default: 8).
85
+ --parity {N,E,O,S,M} Serial parity: None, Even, Odd, Space, Mark (default: N).
86
+ --stopbits {1,1.5,2} Serial stop bits: 1, 1.5, or 2 (default: 1).
87
+ --xonxoff Enable software flow control (XON/XOFF).
88
+ --rtscts Enable hardware flow control (RTS/CTS).
89
+ --dsrdtr Enable hardware flow control (DSR/DTR).
90
+ -d, --debug Enable debug logging output.
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Configuring `/dev/ttyUSB0` in WSL (Highly Recommended)
96
+
97
+ In Linux, the `/dev` directory is owned by `root`. Running `com2tty` as a normal Windows user means the WSL subprocess cannot write directly to `/dev/ttyUSB0`.
98
+
99
+ To work around this cleanly without requiring root permissions or passwordless `sudo` at runtime:
100
+
101
+ 1. Run `com2tty` with the default path (or any `/tmp/` path):
102
+ ```cmd
103
+ com2tty COM3 --wsl-tty /tmp/ttyUSB0
104
+ ```
105
+ 2. In your WSL terminal, run the following command **once** to create a permanent symlink pointing from `/dev/` to the stable `/tmp/` path:
106
+ ```bash
107
+ sudo ln -sf /tmp/ttyUSB0 /dev/ttyUSB0
108
+ ```
109
+ 3. Now, any WSL application (such as `minicom`, `screen`, `esp-idf`, or Python scripts) can read and write to `/dev/ttyUSB0`.
110
+
111
+ Whenever `com2tty` starts up, it updates `/tmp/ttyUSB0` to point to the active pseudo-terminal (`/dev/pts/N`), and `/dev/ttyUSB0` resolves to the correct endpoint automatically.
112
+
113
+ ---
114
+
115
+ ## Troubleshooting
116
+
117
+ ### WSL Bridge warns about Permission Denied
118
+ If you specify `-w /dev/ttyUSB0` directly and see:
119
+ `[WSL] Warning: Permission denied creating symlink at /dev/ttyUSB0.`
120
+ This is expected behavior. The script will automatically fall back to `/tmp/ttyUSB0` and output instructions on how to link them.
121
+
122
+ ### Serial Port Busy / Access Denied
123
+ Ensure that no other application on the Windows host (like PuTTY, Serial Monitor, or another instance of `com2tty`) is currently holding the COM port open.
124
+
125
+ ### Debugging
126
+ Run `com2tty` with the `-d` or `--debug` flag to view detailed logs and data transfer statistics:
127
+ ```cmd
128
+ com2tty COM3 --debug
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT
@@ -0,0 +1,11 @@
1
+ com2tty/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ com2tty/__main__.py,sha256=c3sfBf9CsB__uxUmojI786ZCfgagUGxqoJeIo2fP_Ag,87
3
+ com2tty/bridge.py,sha256=nZaBv_ffVf2H0hw6ySUHiyHiF9q-SWlDAt5z4FobkYA,5067
4
+ com2tty/cli.py,sha256=uYXOIg5Vu_Y3m8qHJxRDvnCag9lzyhxjakBmB9iLARE,2883
5
+ com2tty/host.py,sha256=XTyThnAvfhSmob_uozvXsbe3c2ocJVYaSJy2BD3FhLE,6298
6
+ com2tty-0.1.0.dist-info/licenses/LICENSE,sha256=ZaKMWSm_-7TSseqHEwfSFtvxHtG1x8FGXg6eYPPmKU0,1066
7
+ com2tty-0.1.0.dist-info/METADATA,sha256=9L9TFJa0ra_tLhhxfjUiWDSqssW6E6wNlvwOtwDRQNs,4699
8
+ com2tty-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ com2tty-0.1.0.dist-info/entry_points.txt,sha256=9NgquNy1sScY5NcZD2N2jmVYL0A7Nqy8OP_OkFUqzWQ,45
10
+ com2tty-0.1.0.dist-info/top_level.txt,sha256=3fAwaVYRxXMqMp5XZwQFpMLGTHXuEJD4gfVJNb3Vcq8,8
11
+ com2tty-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ com2tty = com2tty.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 王羿程
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 @@
1
+ com2tty