termchat-client 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.
termchat/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ termchat - Lightweight, non-intrusive terminal chat client.
3
+ Connects standard terminal input to your online Render/cloud server and receives text & images.
4
+ """
5
+
6
+ __version__ = "0.1.0"
7
+ __author__ = "TermChat Contributors"
8
+ __license__ = "MIT"
9
+
10
+ __all__ = ["__version__"]
termchat/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ Allows running `python -m termchat` directly.
3
+ """
4
+
5
+ from termchat.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
termchat/cli.py ADDED
@@ -0,0 +1,97 @@
1
+ """
2
+ CLI interface for termchat client.
3
+ """
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+
9
+ from termchat import __version__
10
+ from termchat.client import start
11
+ from termchat.config import (
12
+ get_saved_server_url,
13
+ get_saved_username,
14
+ set_saved_server_url,
15
+ set_saved_username,
16
+ )
17
+
18
+
19
+ def main():
20
+ parser = argparse.ArgumentParser(
21
+ prog="termchat",
22
+ description="Non-intrusive terminal chat client connecting to your online Render/cloud server.",
23
+ )
24
+ parser.add_argument("-v", "--version", action="version", version=f"termchat {__version__}")
25
+ parser.add_argument(
26
+ "-s",
27
+ "--server",
28
+ help="Online server URL (e.g. https://my-chat.onrender.com). Overrides saved config.",
29
+ )
30
+ parser.add_argument(
31
+ "-n",
32
+ "--name",
33
+ help="Chat username. Overrides saved config.",
34
+ )
35
+ parser.add_argument(
36
+ "--set-server",
37
+ help="Save a default online server URL to ~/.termchat.json and exit.",
38
+ )
39
+ parser.add_argument(
40
+ "--set-name",
41
+ help="Save a default username to ~/.termchat.json and exit.",
42
+ )
43
+ parser.add_argument(
44
+ "--open",
45
+ action="store_true",
46
+ help="Automatically open received images in your OS default image viewer.",
47
+ )
48
+ parser.add_argument(
49
+ "--no-thumb",
50
+ action="store_true",
51
+ help="Disable ANSI image thumbnail previews in the terminal.",
52
+ )
53
+
54
+ args = parser.parse_args()
55
+
56
+ # Handle configuration saving
57
+ if args.set_server:
58
+ set_saved_server_url(args.set_server)
59
+ print(f"[OK] Default server saved: {args.set_server}")
60
+ sys.exit(0)
61
+
62
+ if args.set_name:
63
+ set_saved_username(args.set_name)
64
+ print(f"[OK] Default username saved: {args.set_name}")
65
+ sys.exit(0)
66
+
67
+ # Determine server URL
68
+ server_url = args.server or get_saved_server_url()
69
+ if not server_url:
70
+ print("Welcome to termchat!")
71
+ try:
72
+ val = input("Enter your online chat server URL (e.g. https://my-app.onrender.com): ").strip()
73
+ except (KeyboardInterrupt, EOFError):
74
+ print("\nAborted.")
75
+ sys.exit(0)
76
+
77
+ if not val:
78
+ print("Error: A server URL is required.")
79
+ sys.exit(1)
80
+
81
+ set_saved_server_url(val)
82
+ server_url = val
83
+ print(f"Server saved to ~/.termchat.json. Next time you can just type 'termchat'!\n")
84
+
85
+ # Determine username
86
+ username = args.name or get_saved_username()
87
+
88
+ start(
89
+ server_url=server_url,
90
+ username=username,
91
+ auto_open=args.open,
92
+ no_thumb=args.no_thumb,
93
+ )
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
termchat/client.py ADDED
@@ -0,0 +1,251 @@
1
+ """
2
+ Terminal Chat Client for termchat.
3
+ Non-intrusive standard terminal interface (no UI takeover, standard terminal stream).
4
+ Sends entered text to online Render/cloud server and displays incoming text and images.
5
+ """
6
+
7
+ import asyncio
8
+ from datetime import datetime
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ import platform
13
+ import sys
14
+ import urllib.parse
15
+ from typing import Optional
16
+
17
+ from prompt_toolkit import PromptSession
18
+ from prompt_toolkit.patch_stdout import patch_stdout
19
+ import websockets
20
+
21
+ from termchat.config import normalize_urls
22
+ from termchat.viewer import download_image, open_system_image, render_ansi_thumbnail
23
+
24
+
25
+ class TerminalClient:
26
+ def __init__(
27
+ self,
28
+ server_url: str,
29
+ username: str,
30
+ auto_open_images: bool = False,
31
+ show_thumbnails: bool = True,
32
+ ):
33
+ self.raw_server_url = server_url
34
+ self.ws_url, self.http_base_url = normalize_urls(server_url)
35
+ self.username = username
36
+ self.auto_open_images = auto_open_images
37
+ self.show_thumbnails = show_thumbnails
38
+ self.running = True
39
+ self.websocket = None
40
+ self.last_downloaded_image: Optional[Path] = None
41
+
42
+ def print_welcome(self):
43
+ """Prints a simple standard-line connection notice."""
44
+ print(f"\n--- termchat connected ---")
45
+ print(f"User: {self.username}")
46
+ print(f"Server: {self.http_base_url}")
47
+ print(f"Web UI: {self.http_base_url} (open in browser to chat or send images)")
48
+ print(f"Commands: /help, /open (view last image), /quit")
49
+ print(f"---------------------------\n")
50
+
51
+ def handle_incoming_chat(self, msg: dict):
52
+ sender = msg.get("sender", "Unknown")
53
+ client_type = msg.get("client_type", "web")
54
+ text = msg.get("text", "")
55
+
56
+ badge = "[๐ŸŒ Web]" if client_type == "web" else "[๐Ÿ’ป Terminal]"
57
+ print(f"\n{badge} {sender}: {text}")
58
+
59
+ def handle_incoming_image(self, msg: dict):
60
+ sender = msg.get("sender", "Unknown")
61
+ rel_url = msg.get("image_url", "")
62
+ filename = msg.get("filename", "image.png")
63
+
64
+ # Construct full URL
65
+ full_url = f"{self.http_base_url}{rel_url}"
66
+
67
+ print(f"\n[๐ŸŒ Web] {sender} ๐Ÿ“ท Sent an image: {filename}")
68
+ print(f" URL: {full_url}")
69
+
70
+ # Download locally
71
+ saved_path = download_image(full_url, filename)
72
+ if saved_path:
73
+ self.last_downloaded_image = saved_path
74
+ print(f" Saved locally: {saved_path}")
75
+
76
+ # Terminal ANSI thumbnail
77
+ if self.show_thumbnails:
78
+ thumb = render_ansi_thumbnail(saved_path, width=38)
79
+ if thumb:
80
+ print("\n" + thumb + "\n")
81
+
82
+ # Auto open if requested
83
+ if self.auto_open_images:
84
+ open_system_image(saved_path)
85
+
86
+ def handle_incoming_system(self, msg: dict):
87
+ text = msg.get("text", "")
88
+ print(f"\n[System] {text}")
89
+
90
+ async def _receive_loop(self, ws):
91
+ """Listens for messages from the online server."""
92
+ try:
93
+ async for raw in ws:
94
+ try:
95
+ data = json.loads(raw)
96
+ except Exception:
97
+ continue
98
+
99
+ m_type = data.get("type")
100
+
101
+ if m_type == "chat":
102
+ self.handle_incoming_chat(data)
103
+ elif m_type == "image":
104
+ self.handle_incoming_image(data)
105
+ elif m_type == "system":
106
+ self.handle_incoming_system(data)
107
+ elif m_type == "history":
108
+ history = data.get("messages", [])
109
+ if history:
110
+ print(f"--- Loaded {len(history)} recent message(s) ---")
111
+ for h in history:
112
+ if h.get("type") == "chat":
113
+ self.handle_incoming_chat(h)
114
+ elif h.get("type") == "image":
115
+ self.handle_incoming_image(h)
116
+ print("------------------------------------------\n")
117
+ elif m_type == "ping":
118
+ await ws.send(json.dumps({"type": "pong"}))
119
+
120
+ except websockets.exceptions.ConnectionClosed:
121
+ if self.running:
122
+ print("\n[Server connection closed. Reconnecting...]")
123
+ except Exception as e:
124
+ if self.running:
125
+ print(f"\n[Error receiving: {e}]")
126
+
127
+ async def _input_loop(self, session: PromptSession):
128
+ """Reads user input and sends it to the server."""
129
+ while self.running:
130
+ try:
131
+ user_text = await session.prompt_async("> ")
132
+ text = user_text.strip()
133
+ if not text:
134
+ continue
135
+
136
+ if text.startswith("/"):
137
+ cmd = text.lower().split()[0]
138
+ if cmd in ("/quit", "/exit", "/q"):
139
+ self.running = False
140
+ break
141
+ elif cmd == "/help":
142
+ print("\nAvailable commands:")
143
+ print(" /help - Show help")
144
+ print(" /open - Open the last received image in your OS viewer")
145
+ print(" /web - Print Web UI URL")
146
+ print(" /quit or /exit - Exit chat\n")
147
+ continue
148
+ elif cmd == "/web":
149
+ print(f"Web UI: {self.http_base_url}")
150
+ continue
151
+ elif cmd == "/open":
152
+ if self.last_downloaded_image and self.last_downloaded_image.exists():
153
+ print(f"Opening {self.last_downloaded_image}...")
154
+ open_system_image(self.last_downloaded_image)
155
+ else:
156
+ print("No images received yet.")
157
+ continue
158
+ else:
159
+ print(f"Unknown command '{cmd}'. Type /help for options.")
160
+ continue
161
+
162
+ # Send message
163
+ if self.websocket:
164
+ payload = {
165
+ "type": "chat",
166
+ "text": text,
167
+ "timestamp": datetime.now().isoformat(),
168
+ }
169
+ await self.websocket.send(json.dumps(payload))
170
+ else:
171
+ print("[Not connected to server]")
172
+
173
+ except (KeyboardInterrupt, EOFError):
174
+ self.running = False
175
+ break
176
+ except Exception as e:
177
+ print(f"[Input error: {e}]")
178
+
179
+ async def run(self):
180
+ """Main client loop with automatic reconnect."""
181
+ self.print_welcome()
182
+ session = PromptSession()
183
+
184
+ device = f"{platform.system()} ({platform.machine()})"
185
+ query_params = urllib.parse.urlencode({
186
+ "username": self.username,
187
+ "client_type": "terminal",
188
+ "device_info": device,
189
+ })
190
+ full_ws = f"{self.ws_url}?{query_params}"
191
+
192
+ with patch_stdout():
193
+ while self.running:
194
+ try:
195
+ print(f"Connecting to {self.http_base_url}...")
196
+ async with websockets.connect(
197
+ full_ws,
198
+ ping_interval=25,
199
+ ping_timeout=15,
200
+ ) as ws:
201
+ self.websocket = ws
202
+ print("Connected! Start typing text below (Press Enter to send):\n")
203
+
204
+ recv_task = asyncio.create_task(self._receive_loop(ws))
205
+ input_task = asyncio.create_task(self._input_loop(session))
206
+
207
+ done, pending = await asyncio.wait(
208
+ [recv_task, input_task],
209
+ return_when=asyncio.FIRST_COMPLETED,
210
+ )
211
+
212
+ for t in pending:
213
+ t.cancel()
214
+ try:
215
+ await t
216
+ except asyncio.CancelledError:
217
+ pass
218
+
219
+ if not self.running:
220
+ break
221
+
222
+ except (ConnectionRefusedError, OSError) as e:
223
+ if not self.running:
224
+ break
225
+ print(f"Could not connect to {self.http_base_url}. Retrying in 3s... (Ctrl+C to exit)")
226
+ await asyncio.sleep(3)
227
+ except Exception as e:
228
+ if not self.running:
229
+ break
230
+ print(f"Connection error: {e}. Retrying in 3s...")
231
+ await asyncio.sleep(3)
232
+
233
+ print("\nExiting termchat. Goodbye!")
234
+
235
+
236
+ def start(
237
+ server_url: str,
238
+ username: str,
239
+ auto_open: bool = False,
240
+ no_thumb: bool = False,
241
+ ):
242
+ client = TerminalClient(
243
+ server_url=server_url,
244
+ username=username,
245
+ auto_open_images=auto_open,
246
+ show_thumbnails=not no_thumb,
247
+ )
248
+ try:
249
+ asyncio.run(client.run())
250
+ except KeyboardInterrupt:
251
+ print("\nExited.")
termchat/config.py ADDED
@@ -0,0 +1,113 @@
1
+ """
2
+ Configuration management for termchat client.
3
+ Handles persistent user preferences in ~/.termchat.json, environment variables,
4
+ and URL normalization for WebSocket and HTTP endpoints.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Dict, Optional
11
+
12
+ CONFIG_FILE = Path.home() / ".termchat.json"
13
+ DEFAULT_FALLBACK_SERVER = "http://localhost:8000"
14
+
15
+
16
+ def load_config() -> Dict[str, str]:
17
+ """Loads configuration from ~/.termchat.json if present."""
18
+ if CONFIG_FILE.exists():
19
+ try:
20
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
21
+ return json.load(f)
22
+ except Exception:
23
+ pass
24
+ return {}
25
+
26
+
27
+ def save_config(config: Dict[str, str]):
28
+ """Persists configuration to ~/.termchat.json."""
29
+ try:
30
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
31
+ json.dump(config, f, indent=2)
32
+ except Exception as e:
33
+ print(f"[Warning] Could not save config file: {e}")
34
+
35
+
36
+ def get_saved_server_url() -> Optional[str]:
37
+ """Retrieves server URL from ENV, config file, or None."""
38
+ # 1. Environment variable takes highest priority
39
+ env_url = os.environ.get("TERMCHAT_SERVER")
40
+ if env_url:
41
+ return env_url.strip()
42
+
43
+ # 2. Config file
44
+ cfg = load_config()
45
+ if "server" in cfg and cfg["server"]:
46
+ return cfg["server"].strip()
47
+
48
+ return None
49
+
50
+
51
+ def set_saved_server_url(url: str):
52
+ """Saves default server URL to config file."""
53
+ cfg = load_config()
54
+ cfg["server"] = url.strip()
55
+ save_config(cfg)
56
+
57
+
58
+ def get_saved_username() -> str:
59
+ """Retrieves saved username or falls back to system username."""
60
+ cfg = load_config()
61
+ if "username" in cfg and cfg["username"]:
62
+ return cfg["username"].strip()
63
+ return os.environ.get("USER") or os.environ.get("USERNAME") or "TermUser"
64
+
65
+
66
+ def set_saved_username(username: str):
67
+ """Saves default username to config file."""
68
+ cfg = load_config()
69
+ cfg["username"] = username.strip()
70
+ save_config(cfg)
71
+
72
+
73
+ def normalize_urls(raw_url: str):
74
+ """
75
+ Given a user-provided URL (e.g. 'https://my-app.onrender.com' or 'localhost:8000'),
76
+ returns (ws_url, http_base_url).
77
+ """
78
+ cleaned = raw_url.strip()
79
+ if not cleaned:
80
+ cleaned = DEFAULT_FALLBACK_SERVER
81
+
82
+ # Ensure protocol
83
+ if not cleaned.startswith("http://") and not cleaned.startswith("https://") and not cleaned.startswith("ws://") and not cleaned.startswith("wss://"):
84
+ if "localhost" in cleaned or "127.0.0.1" in cleaned:
85
+ cleaned = f"http://{cleaned}"
86
+ else:
87
+ cleaned = f"https://{cleaned}"
88
+
89
+ # Determine HTTP and WS protocols
90
+ if cleaned.startswith("https://"):
91
+ http_base = cleaned.rstrip("/")
92
+ ws_base = "wss://" + cleaned[8:].rstrip("/")
93
+ elif cleaned.startswith("http://"):
94
+ http_base = cleaned.rstrip("/")
95
+ ws_base = "ws://" + cleaned[7:].rstrip("/")
96
+ elif cleaned.startswith("wss://"):
97
+ http_base = "https://" + cleaned[6:].rstrip("/")
98
+ ws_base = cleaned.rstrip("/")
99
+ elif cleaned.startswith("ws://"):
100
+ http_base = "http://" + cleaned[5:].rstrip("/")
101
+ ws_base = cleaned.rstrip("/")
102
+ else:
103
+ http_base = f"https://{cleaned.rstrip('/')}"
104
+ ws_base = f"wss://{cleaned.rstrip('/')}"
105
+
106
+ # Strip /ws or trailing slashes from base
107
+ if http_base.endswith("/ws"):
108
+ http_base = http_base[:-3]
109
+ if ws_base.endswith("/ws"):
110
+ ws_base = ws_base[:-3]
111
+
112
+ ws_url = f"{ws_base}/ws"
113
+ return ws_url, http_base
termchat/viewer.py ADDED
@@ -0,0 +1,93 @@
1
+ """
2
+ Image viewer and thumbnail renderer for terminal clients.
3
+ Handles automatic downloading, terminal ANSI color block preview,
4
+ and launching the OS default image viewer.
5
+ """
6
+
7
+ import io
8
+ import os
9
+ from pathlib import Path
10
+ import platform
11
+ import subprocess
12
+ import urllib.request
13
+ from typing import Optional
14
+
15
+
16
+ def get_downloads_dir() -> Path:
17
+ """Returns local downloads directory."""
18
+ d = Path.cwd() / "downloads"
19
+ d.mkdir(parents=True, exist_ok=True)
20
+ return d
21
+
22
+
23
+ def download_image(full_image_url: str, filename: str) -> Optional[Path]:
24
+ """Downloads image to local downloads folder."""
25
+ try:
26
+ dest_dir = get_downloads_dir()
27
+ safe_name = Path(filename).name
28
+ dest_path = dest_dir / safe_name
29
+
30
+ req = urllib.request.Request(
31
+ full_image_url,
32
+ headers={"User-Agent": "termchat-terminal-client/1.0"},
33
+ )
34
+ with urllib.request.urlopen(req, timeout=10) as response, open(dest_path, "wb") as out_file:
35
+ out_file.write(response.read())
36
+
37
+ return dest_path
38
+ except Exception as e:
39
+ return None
40
+
41
+
42
+ def open_system_image(filepath: Path):
43
+ """Opens image in OS default viewer (Photos, Preview, or xdg-open)."""
44
+ try:
45
+ sys_name = platform.system()
46
+ if sys_name == "Windows":
47
+ os.startfile(str(filepath))
48
+ elif sys_name == "Darwin":
49
+ subprocess.run(["open", str(filepath)], check=False)
50
+ else:
51
+ subprocess.run(["xdg-open", str(filepath)], check=False)
52
+ except Exception:
53
+ pass
54
+
55
+
56
+ def render_ansi_thumbnail(filepath: Path, width: int = 36) -> Optional[str]:
57
+ """
58
+ Renders an image as an ANSI 24-bit color half-block thumbnail.
59
+ Each terminal character 'โ–€' has both foreground (top pixel) and background (bottom pixel).
60
+ Works in Windows Terminal, Linux, macOS, and Raspberry Pi consoles with 24-bit color support.
61
+ """
62
+ try:
63
+ from PIL import Image
64
+
65
+ img = Image.open(filepath).convert("RGB")
66
+ orig_w, orig_h = img.size
67
+ # Half blocks have 1:2 aspect ratio in terminal fonts
68
+ aspect = orig_h / orig_w
69
+ height = int(width * aspect * 0.5)
70
+ # Ensure even height for half-block pairing
71
+ if height % 2 != 0:
72
+ height += 1
73
+ height = max(height, 2)
74
+
75
+ resized = img.resize((width, height), Image.Resampling.BILINEAR)
76
+ pixels = resized.load()
77
+
78
+ lines = []
79
+ for y in range(0, height, 2):
80
+ line = []
81
+ for x in range(width):
82
+ r1, g1, b1 = pixels[x, y]
83
+ if y + 1 < height:
84
+ r2, g2, b2 = pixels[x, y + 1]
85
+ else:
86
+ r2, g2, b2 = (0, 0, 0)
87
+ # โ–€ is upper half block: fg is top pixel, bg is bottom pixel
88
+ line.append(f"\033[38;2;{r1};{g1};{b1}m\033[48;2;{r2};{g2};{b2}mโ–€\033[0m")
89
+ lines.append("".join(line))
90
+
91
+ return "\n".join(lines)
92
+ except Exception:
93
+ return None
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: termchat-client
3
+ Version: 0.1.0
4
+ Summary: Lightweight, non-intrusive terminal chat client connecting to an online chat server with real-time text and image support.
5
+ Author-email: TermChat Team <info@termchat.dev>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/your-username/termchat
8
+ Project-URL: Repository, https://github.com/your-username/termchat
9
+ Project-URL: Issues, https://github.com/your-username/termchat/issues
10
+ Keywords: chat,terminal,cli,websocket,raspberry-pi,linux,windows,render,image-chat
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Operating System :: MacOS
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.8
21
+ Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Topic :: Communications :: Chat
27
+ Classifier: Topic :: Terminals
28
+ Requires-Python: >=3.8
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: websockets>=12.0
32
+ Requires-Dist: prompt_toolkit>=3.0.0
33
+ Requires-Dist: pillow>=9.0.0
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
36
+ Requires-Dist: httpx>=0.24.0; extra == "dev"
37
+ Requires-Dist: build>=1.0.0; extra == "dev"
38
+ Requires-Dist: twine>=4.0.0; extra == "dev"
39
+ Requires-Dist: fastapi>=0.100.0; extra == "dev"
40
+ Requires-Dist: python-multipart>=0.0.6; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # termchat-client ๐Ÿ’ฌ๐Ÿ–ผ๏ธ
44
+
45
+ [![PyPI version](https://img.shields.io/pypi/v/termchat-client.svg)](https://pypi.org/project/termchat-client/)
46
+ [![Python versions](https://img.shields.io/pypi/pyversions/termchat-client.svg)](https://pypi.org/project/termchat-client/)
47
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
48
+ [![OS Support](https://img.shields.io/badge/OS-Windows%20%7C%20Linux%20%7C%20Raspberry%20Pi%20%7C%20macOS-success)](https://pypi.org/project/termchat-client/)
49
+
50
+ **termchat-client** is a lightweight, non-intrusive terminal chat client packaged for Python. It connects directly to an online chat server (running on **Render** or any cloud host), allowing you to chat seamlessly between your **terminal** and an **online HTML web page** viewed by others.
51
+
52
+ ---
53
+
54
+ ## ๐Ÿ’ก How It Works
55
+
56
+ - **Standard Terminal Interaction**: It does **not** change or take over your terminal UI. No fullscreen ncurses/curses screens. Just a normal, standard prompt line where you type text and press Enter.
57
+ - **Bi-Directional Messaging**:
58
+ - You type text in your terminal โž” It appears instantly on the web page.
59
+ - Web users type replies โž” They print right into your terminal: `[๐ŸŒ WebUser]: Hello from Chrome!`
60
+ - **Image Support to Terminal**:
61
+ - Web users can click the image picker, drag & drop images, or **press `Ctrl+V` to paste screenshots directly from their clipboard**!
62
+ - When an image is sent from the web, your terminal automatically:
63
+ 1. Downloads the image locally into `./downloads/`
64
+ 2. Prints the image URL and filename
65
+ 3. Renders a compact **ANSI 24-bit color block thumbnail preview** directly inside your terminal!
66
+ 4. Can auto-open the image in your OS default image viewer with `--open` or `/open`.
67
+ - **Works on Any OS**: Windows (PowerShell / CMD / Windows Terminal), Linux (Debian, Ubuntu, Arch), **Raspberry Pi** (ARMv7 / aarch64), and macOS.
68
+
69
+ ---
70
+
71
+ ## ๐Ÿ›๏ธ System Architecture
72
+
73
+ ```text
74
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
75
+ โ”‚ Online Server on Render โ”‚
76
+ โ”‚ (FastAPI + WebSockets + Image Upload API) โ”‚
77
+ โ”‚ https://my-chat.onrender.com โ”‚
78
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ฒโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ฒโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
79
+ โ”‚ โ”‚
80
+ WebSocket โ”‚ WebSocket โ”‚ + HTTP Web UI
81
+ Connection โ”‚ Connection โ”‚ (HTML5 / CSS / JS)
82
+ โ”‚ โ”‚
83
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
84
+ โ”‚ termchat-client (CLI) โ”‚ โ”‚ Web Client (HTML) โ”‚
85
+ โ”‚ โ€ข Linux / Raspberry Piโ”‚ โ”‚ โ€ข Desktop Browsers โ”‚
86
+ โ”‚ โ€ข Windows CMD / PS โ”‚ โ”‚ โ€ข Mobile Browsers (iOS) โ”‚
87
+ โ”‚ โ€ข macOS Terminal โ”‚ โ”‚ โ€ข Paste Images (Ctrl+V) โ”‚
88
+ โ”‚ (Just text in & out) โ”‚ โ”‚ โ€ข Drag & Drop Images โ”‚
89
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
90
+ ```
91
+
92
+ ---
93
+
94
+ ## ๐Ÿ“ฆ Installation
95
+
96
+ Install the client package on any machine:
97
+
98
+ ```bash
99
+ pip install termchat-client
100
+ ```
101
+
102
+ ---
103
+
104
+ ## ๐Ÿš€ Terminal Client Usage
105
+
106
+ ### 1. Run via CLI
107
+ ```bash
108
+ termchat --server https://my-chat.onrender.com --name Alice
109
+ ```
110
+ Or via Python module:
111
+ ```bash
112
+ python -m termchat --server https://my-chat.onrender.com --name Alice
113
+ ```
114
+
115
+ ### 2. Save Your Server Once
116
+ Save your online server URL so you never need to type it again:
117
+ ```bash
118
+ termchat --set-server https://my-chat.onrender.com
119
+ termchat --set-name Alice
120
+ ```
121
+ Once saved, simply run:
122
+ ```bash
123
+ termchat
124
+ ```
125
+ or:
126
+ ```bash
127
+ python -m termchat
128
+ ```
129
+
130
+ ### 3. What It Looks Like in Your Terminal:
131
+ ```text
132
+ --- termchat connected ---
133
+ User: Alice
134
+ Server: https://my-chat.onrender.com
135
+ Web UI: https://my-chat.onrender.com
136
+ Commands: /help, /open (view last image), /quit
137
+ ---------------------------
138
+
139
+ Connected! Start typing text below (Press Enter to send):
140
+
141
+ > Hello from my Raspberry Pi terminal!
142
+
143
+ [๐ŸŒ Web] Bob: Hey Alice! Check out this screenshot:
144
+ [๐ŸŒ Web] Bob ๐Ÿ“ท Sent an image: dashboard.png
145
+ URL: https://my-chat.onrender.com/uploads/dashboard.png
146
+ Saved locally: C:\projects\terminal-chat\downloads\dashboard.png
147
+
148
+ [ANSI Image Thumbnail rendered here]
149
+
150
+ >
151
+ ```
152
+
153
+ ### In-Terminal Commands:
154
+ - `/open` โ€” Opens the last received image in your OS default image viewer (Photos, Preview, or `xdg-open`).
155
+ - `/web` โ€” Prints the Web UI link.
156
+ - `/help` โ€” Lists available commands.
157
+ - `/quit` (or `Ctrl+C`) โ€” Safely exits.
158
+
159
+ ### Optional Flags:
160
+ - `--open`: Automatically opens any received image in your OS default image viewer.
161
+ - `--no-thumb`: Disables terminal ANSI thumbnail previews (prints URL and saved path only).
162
+
163
+ ---
164
+
165
+ ## ๐ŸŒ Deploying the Server to Render (Free)
166
+
167
+ The server code lives in the `server/` directory and can be deployed to [Render](https://render.com) in 2 minutes for free.
168
+
169
+ See [DEPLOY_RENDER.md](DEPLOY_RENDER.md) for full instructions:
170
+ 1. Push this repository to GitHub.
171
+ 2. In Render, click **New + > Web Service** and connect your GitHub repo.
172
+ 3. Set **Root Directory** to `server`.
173
+ 4. Set **Start Command** to `uvicorn main:app --host 0.0.0.0 --port $PORT`.
174
+ 5. Deploy! Render gives you a live HTTPS and WebSocket URL like `https://my-chat.onrender.com`.
175
+
176
+ ---
177
+
178
+ ## ๐Ÿ“ค Publishing to PyPI
179
+
180
+ The package is packaged using modern PEP 517 / PEP 621 (`pyproject.toml`).
181
+
182
+ 1. Build distributions:
183
+ ```bash
184
+ python -m build
185
+ ```
186
+ 2. Verify with Twine:
187
+ ```bash
188
+ twine check dist/*
189
+ ```
190
+ 3. Upload to PyPI:
191
+ ```bash
192
+ twine upload dist/*
193
+ ```
194
+ *(Or run the included interactive helper: `python publish.py`)*
195
+
196
+ See [PUBLISHING.md](PUBLISHING.md) for step-by-step instructions.
197
+
198
+ ---
199
+
200
+ ## ๐Ÿงช Testing
201
+
202
+ Run the automated test suite:
203
+ ```bash
204
+ python -m pytest -v
205
+ ```
206
+
207
+ ---
208
+
209
+ ## ๐Ÿ“„ License
210
+
211
+ MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,12 @@
1
+ termchat/__init__.py,sha256=m88TR9LiPzae8YdlWD04SlMfFVBDpVcAGRi_GPpADnM,271
2
+ termchat/__main__.py,sha256=oaDg29DTNudL4s_8Ivx88QtQ0nzHYuWEzuaSSn_d8mY,124
3
+ termchat/cli.py,sha256=F8S_0GDVpNK-Z4ov6BJNLbWai1EBm1bXVPyab3POG8Q,2653
4
+ termchat/client.py,sha256=86Twi978ZeZJP_piYegvCbsafJ4Q_NJIXntNXrq_8qA,9302
5
+ termchat/config.py,sha256=QE1da7cyCjvqqg5Nq_NbgCSV6QA9g4PYP0qBFfPzYWQ,3522
6
+ termchat/viewer.py,sha256=A07tiCx4aBZ3csA0lGy31Ivw91tPTxKs8SxHZ-aQDxQ,3002
7
+ termchat_client-0.1.0.dist-info/licenses/LICENSE,sha256=GfpbbMwTUxH_hvaVgaKPUjpWlWRmy1eVkfnmwHUymKc,1078
8
+ termchat_client-0.1.0.dist-info/METADATA,sha256=RfZFTpGpbHTES7YsZEgkStRTHQvLZa1541MnkPIsoo8,8429
9
+ termchat_client-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
10
+ termchat_client-0.1.0.dist-info/entry_points.txt,sha256=_eAa9jAnHvYF5uvfm4tlsB1YDo3sTiWUB6lZx0RPEzw,47
11
+ termchat_client-0.1.0.dist-info/top_level.txt,sha256=EiC1FPPpq71dNhAYr5hEIcqzqugtIP0HRdRD5-_oZVY,9
12
+ termchat_client-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ termchat = termchat.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TermChat Contributors
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
+ termchat