59chat 0.3.1__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.
- 59chat-0.3.1/59chat.egg-info/PKG-INFO +91 -0
- 59chat-0.3.1/59chat.egg-info/SOURCES.txt +14 -0
- 59chat-0.3.1/59chat.egg-info/dependency_links.txt +1 -0
- 59chat-0.3.1/59chat.egg-info/entry_points.txt +2 -0
- 59chat-0.3.1/59chat.egg-info/requires.txt +3 -0
- 59chat-0.3.1/59chat.egg-info/top_level.txt +1 -0
- 59chat-0.3.1/MANIFEST.in +5 -0
- 59chat-0.3.1/PKG-INFO +91 -0
- 59chat-0.3.1/README.md +72 -0
- 59chat-0.3.1/chat59/__init__.py +0 -0
- 59chat-0.3.1/chat59/__main__.py +4 -0
- 59chat-0.3.1/chat59/main.py +295 -0
- 59chat-0.3.1/main.py +348 -0
- 59chat-0.3.1/requirements.txt +4 -0
- 59chat-0.3.1/setup.cfg +4 -0
- 59chat-0.3.1/setup.py +23 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 59chat
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: 59-second zero-trace terminal chat (Ultra-Light Edition)
|
|
5
|
+
Home-page: https://github.com/yourusername/59chat
|
|
6
|
+
Author: YourName
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: textual>=0.45.0
|
|
10
|
+
Requires-Dist: httpx>=0.25.0
|
|
11
|
+
Requires-Dist: pyperclip>=1.8.2
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: description
|
|
14
|
+
Dynamic: description-content-type
|
|
15
|
+
Dynamic: home-page
|
|
16
|
+
Dynamic: requires-dist
|
|
17
|
+
Dynamic: requires-python
|
|
18
|
+
Dynamic: summary
|
|
19
|
+
|
|
20
|
+
# Ephemeral Terminal Chat
|
|
21
|
+
|
|
22
|
+
Zero-trace terminal chat application with 59-second disappearing messages.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- Messages disappear after 59 seconds
|
|
27
|
+
- Zero trace - no history kept
|
|
28
|
+
- Fast terminal UI with Textual framework
|
|
29
|
+
- Mono font recommended (set in your terminal, e.g., JetBrains Mono)
|
|
30
|
+
- Supabase backend for real-time sync
|
|
31
|
+
- Share rooms with 6-character room codes
|
|
32
|
+
- Random nickname generation
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
### 1. Supabase Setup
|
|
37
|
+
|
|
38
|
+
1. Go to [supabase.com](https://supabase.com) and create a free account
|
|
39
|
+
2. Create a new project
|
|
40
|
+
3. Go to SQL Editor and run the `supabase_setup.sql` file
|
|
41
|
+
4. Get your project URL and anon key from Settings > API
|
|
42
|
+
|
|
43
|
+
### 2. Environment Variables
|
|
44
|
+
|
|
45
|
+
Copy `.env.example` to `.env` and add your Supabase credentials:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cp .env.example .env
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Edit `.env`:
|
|
52
|
+
```
|
|
53
|
+
SUPABASE_URL=your_supabase_project_url
|
|
54
|
+
SUPABASE_KEY=your_supabase_anon_key
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. Install Dependencies
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -r requirements.txt
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 4. Run
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python main.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
- **Type a message** and press Enter to send
|
|
72
|
+
- **Ctrl+R** - Create new room
|
|
73
|
+
- **Ctrl+C** - Quit
|
|
74
|
+
- Share the **room code** with others to chat
|
|
75
|
+
- Messages auto-delete after 59 seconds
|
|
76
|
+
|
|
77
|
+
## How It Works
|
|
78
|
+
|
|
79
|
+
- Each room has a unique 6-character code
|
|
80
|
+
- Messages are stored in Supabase with timestamp
|
|
81
|
+
- Auto-cleanup deletes messages older than 59 seconds
|
|
82
|
+
- Terminal polls for new messages every second
|
|
83
|
+
- Zero-trace: no message history retained
|
|
84
|
+
|
|
85
|
+
## Terminal Share
|
|
86
|
+
|
|
87
|
+
Share your terminal with:
|
|
88
|
+
- tmate: `tmate`
|
|
89
|
+
- warp: Share session feature
|
|
90
|
+
- ssh: Allow remote connections
|
|
91
|
+
- Or just share the room code via any channel
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
main.py
|
|
4
|
+
requirements.txt
|
|
5
|
+
setup.py
|
|
6
|
+
59chat.egg-info/PKG-INFO
|
|
7
|
+
59chat.egg-info/SOURCES.txt
|
|
8
|
+
59chat.egg-info/dependency_links.txt
|
|
9
|
+
59chat.egg-info/entry_points.txt
|
|
10
|
+
59chat.egg-info/requires.txt
|
|
11
|
+
59chat.egg-info/top_level.txt
|
|
12
|
+
chat59/__init__.py
|
|
13
|
+
chat59/__main__.py
|
|
14
|
+
chat59/main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chat59
|
59chat-0.3.1/MANIFEST.in
ADDED
59chat-0.3.1/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 59chat
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: 59-second zero-trace terminal chat (Ultra-Light Edition)
|
|
5
|
+
Home-page: https://github.com/yourusername/59chat
|
|
6
|
+
Author: YourName
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: textual>=0.45.0
|
|
10
|
+
Requires-Dist: httpx>=0.25.0
|
|
11
|
+
Requires-Dist: pyperclip>=1.8.2
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: description
|
|
14
|
+
Dynamic: description-content-type
|
|
15
|
+
Dynamic: home-page
|
|
16
|
+
Dynamic: requires-dist
|
|
17
|
+
Dynamic: requires-python
|
|
18
|
+
Dynamic: summary
|
|
19
|
+
|
|
20
|
+
# Ephemeral Terminal Chat
|
|
21
|
+
|
|
22
|
+
Zero-trace terminal chat application with 59-second disappearing messages.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- Messages disappear after 59 seconds
|
|
27
|
+
- Zero trace - no history kept
|
|
28
|
+
- Fast terminal UI with Textual framework
|
|
29
|
+
- Mono font recommended (set in your terminal, e.g., JetBrains Mono)
|
|
30
|
+
- Supabase backend for real-time sync
|
|
31
|
+
- Share rooms with 6-character room codes
|
|
32
|
+
- Random nickname generation
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
### 1. Supabase Setup
|
|
37
|
+
|
|
38
|
+
1. Go to [supabase.com](https://supabase.com) and create a free account
|
|
39
|
+
2. Create a new project
|
|
40
|
+
3. Go to SQL Editor and run the `supabase_setup.sql` file
|
|
41
|
+
4. Get your project URL and anon key from Settings > API
|
|
42
|
+
|
|
43
|
+
### 2. Environment Variables
|
|
44
|
+
|
|
45
|
+
Copy `.env.example` to `.env` and add your Supabase credentials:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cp .env.example .env
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Edit `.env`:
|
|
52
|
+
```
|
|
53
|
+
SUPABASE_URL=your_supabase_project_url
|
|
54
|
+
SUPABASE_KEY=your_supabase_anon_key
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. Install Dependencies
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -r requirements.txt
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 4. Run
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python main.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
- **Type a message** and press Enter to send
|
|
72
|
+
- **Ctrl+R** - Create new room
|
|
73
|
+
- **Ctrl+C** - Quit
|
|
74
|
+
- Share the **room code** with others to chat
|
|
75
|
+
- Messages auto-delete after 59 seconds
|
|
76
|
+
|
|
77
|
+
## How It Works
|
|
78
|
+
|
|
79
|
+
- Each room has a unique 6-character code
|
|
80
|
+
- Messages are stored in Supabase with timestamp
|
|
81
|
+
- Auto-cleanup deletes messages older than 59 seconds
|
|
82
|
+
- Terminal polls for new messages every second
|
|
83
|
+
- Zero-trace: no message history retained
|
|
84
|
+
|
|
85
|
+
## Terminal Share
|
|
86
|
+
|
|
87
|
+
Share your terminal with:
|
|
88
|
+
- tmate: `tmate`
|
|
89
|
+
- warp: Share session feature
|
|
90
|
+
- ssh: Allow remote connections
|
|
91
|
+
- Or just share the room code via any channel
|
59chat-0.3.1/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Ephemeral Terminal Chat
|
|
2
|
+
|
|
3
|
+
Zero-trace terminal chat application with 59-second disappearing messages.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Messages disappear after 59 seconds
|
|
8
|
+
- Zero trace - no history kept
|
|
9
|
+
- Fast terminal UI with Textual framework
|
|
10
|
+
- Mono font recommended (set in your terminal, e.g., JetBrains Mono)
|
|
11
|
+
- Supabase backend for real-time sync
|
|
12
|
+
- Share rooms with 6-character room codes
|
|
13
|
+
- Random nickname generation
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
### 1. Supabase Setup
|
|
18
|
+
|
|
19
|
+
1. Go to [supabase.com](https://supabase.com) and create a free account
|
|
20
|
+
2. Create a new project
|
|
21
|
+
3. Go to SQL Editor and run the `supabase_setup.sql` file
|
|
22
|
+
4. Get your project URL and anon key from Settings > API
|
|
23
|
+
|
|
24
|
+
### 2. Environment Variables
|
|
25
|
+
|
|
26
|
+
Copy `.env.example` to `.env` and add your Supabase credentials:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cp .env.example .env
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Edit `.env`:
|
|
33
|
+
```
|
|
34
|
+
SUPABASE_URL=your_supabase_project_url
|
|
35
|
+
SUPABASE_KEY=your_supabase_anon_key
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 3. Install Dependencies
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install -r requirements.txt
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 4. Run
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python main.py
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
- **Type a message** and press Enter to send
|
|
53
|
+
- **Ctrl+R** - Create new room
|
|
54
|
+
- **Ctrl+C** - Quit
|
|
55
|
+
- Share the **room code** with others to chat
|
|
56
|
+
- Messages auto-delete after 59 seconds
|
|
57
|
+
|
|
58
|
+
## How It Works
|
|
59
|
+
|
|
60
|
+
- Each room has a unique 6-character code
|
|
61
|
+
- Messages are stored in Supabase with timestamp
|
|
62
|
+
- Auto-cleanup deletes messages older than 59 seconds
|
|
63
|
+
- Terminal polls for new messages every second
|
|
64
|
+
- Zero-trace: no message history retained
|
|
65
|
+
|
|
66
|
+
## Terminal Share
|
|
67
|
+
|
|
68
|
+
Share your terminal with:
|
|
69
|
+
- tmate: `tmate`
|
|
70
|
+
- warp: Share session feature
|
|
71
|
+
- ssh: Allow remote connections
|
|
72
|
+
- Or just share the room code via any channel
|
|
File without changes
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import random
|
|
7
|
+
import string
|
|
8
|
+
from datetime import datetime, timezone, timedelta
|
|
9
|
+
from typing import Optional, List, Dict
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
import pyperclip
|
|
13
|
+
from textual.app import App, ComposeResult
|
|
14
|
+
from textual.widgets import Input, Static, Footer
|
|
15
|
+
from textual.containers import Vertical, Horizontal
|
|
16
|
+
from textual.reactive import reactive
|
|
17
|
+
from textual.binding import Binding
|
|
18
|
+
from rich.markdown import Markdown
|
|
19
|
+
|
|
20
|
+
# --- CONFIGURATION ---
|
|
21
|
+
VERSION = "0.3.1"
|
|
22
|
+
APP_NAME = "59chat"
|
|
23
|
+
INTERNAL_PKG = "chat59"
|
|
24
|
+
SUPABASE_URL = "https://xdqxebyyjxklzisddmwl.supabase.co"
|
|
25
|
+
SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhkcXhlYnl5anhrbHppc2RkbXdsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk4Mzc0NDAsImV4cCI6MjA3NTQxMzQ0MH0.37JtLjN6mGfdac-t-cqNADa8OQlYIgSkZEFSngwxlM0"
|
|
26
|
+
|
|
27
|
+
CSS = (
|
|
28
|
+
"Screen { background: #000000; color: #FFFFFF; }\n"
|
|
29
|
+
"#header { dock: top; height: 4; background: #000000; padding: 1 2; }\n"
|
|
30
|
+
"#header-info { width: 1fr; content-align: left middle; }\n"
|
|
31
|
+
"#header-status { width: auto; content-align: right middle; color: #22C55E; text-style: bold; }\n"
|
|
32
|
+
"#chat-scroll { height: 1fr; padding: 1 2; }\n"
|
|
33
|
+
"#bottom-bar { dock: bottom; height: auto; background: #000000; }\n"
|
|
34
|
+
"#input-row { height: 1; background: #0A0A0A; padding: 0 2; }\n"
|
|
35
|
+
"#input-prefix { width: auto; color: #FFFFFF; text-style: bold; }\n"
|
|
36
|
+
"Input { background: transparent; border: none; width: 1fr; height: 1; color: #FFFFFF; padding: 0; }\n"
|
|
37
|
+
"Input:focus { border: none; }\n"
|
|
38
|
+
".message-item { width: 100%; height: auto; margin-bottom: 2; }\n"
|
|
39
|
+
".message-item.own { align-horizontal: right; }\n"
|
|
40
|
+
".message-item.other { align-horizontal: left; }\n"
|
|
41
|
+
".msg-container { width: auto; min-width: 40%; max-width: 80%; height: auto; }\n"
|
|
42
|
+
".msg-header { height: 1; color: #FFFFFF; }\n"
|
|
43
|
+
".msg-content { height: auto; padding: 0 1; }\n"
|
|
44
|
+
".msg-line { height: 1; color: #525252; }\n"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def parse_iso_dt(s: str) -> datetime:
|
|
48
|
+
"""Robust ISO datetime parsing for various microsecond lengths."""
|
|
49
|
+
try:
|
|
50
|
+
# Remove trailing offsets or 'Z' and simplify
|
|
51
|
+
clean_s = s.replace('Z', '+00:00').split('+')[0]
|
|
52
|
+
if '.' in clean_s:
|
|
53
|
+
base, ms = clean_s.split('.')
|
|
54
|
+
clean_s = f"{base}.{ms[:6]}" # Truncate to max 6 digits for microseconds
|
|
55
|
+
return datetime.fromisoformat(f"{clean_s}+00:00")
|
|
56
|
+
except:
|
|
57
|
+
return datetime.now(timezone.utc)
|
|
58
|
+
|
|
59
|
+
class SupabaseClient:
|
|
60
|
+
def __init__(self):
|
|
61
|
+
self.base_url = f"{SUPABASE_URL}/rest/v1"
|
|
62
|
+
self.headers = {
|
|
63
|
+
"apikey": SUPABASE_KEY,
|
|
64
|
+
"Authorization": f"Bearer {SUPABASE_KEY}",
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
"Prefer": "return=representation"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async def get_messages(self, room_id: str):
|
|
70
|
+
async with httpx.AsyncClient() as client:
|
|
71
|
+
url = f"{self.base_url}/messages?room_id=eq.{room_id}&order=created_at.asc"
|
|
72
|
+
res = await client.get(url, headers=self.headers)
|
|
73
|
+
return res.json()
|
|
74
|
+
|
|
75
|
+
async def send_message(self, room_id: str, nickname: str, content: str):
|
|
76
|
+
async with httpx.AsyncClient() as client:
|
|
77
|
+
data = {"room_id": room_id, "nickname": nickname, "content": content}
|
|
78
|
+
await client.post(f"{self.base_url}/messages", headers=self.headers, json=data)
|
|
79
|
+
|
|
80
|
+
async def mark_read(self, msg_ids: List[int]):
|
|
81
|
+
if not msg_ids: return
|
|
82
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
83
|
+
async with httpx.AsyncClient() as client:
|
|
84
|
+
ids_str = f"({','.join(map(str, msg_ids))})"
|
|
85
|
+
url = f"{self.base_url}/messages?id=in.{ids_str}"
|
|
86
|
+
await client.patch(url, headers=self.headers, json={"read_at": now})
|
|
87
|
+
|
|
88
|
+
async def get_active_users(self, room_id: str):
|
|
89
|
+
async with httpx.AsyncClient() as client:
|
|
90
|
+
url = f"{self.base_url}/messages?room_id=eq.{room_id}&select=nickname"
|
|
91
|
+
res = await client.get(url, headers=self.headers)
|
|
92
|
+
data = res.json()
|
|
93
|
+
return len({m['nickname'] for m in data}) if isinstance(data, list) else 1
|
|
94
|
+
|
|
95
|
+
class MessageItem(Vertical):
|
|
96
|
+
def __init__(self, m, is_own):
|
|
97
|
+
super().__init__(classes=f"message-item {'own' if is_own else 'other'}")
|
|
98
|
+
self.m = m
|
|
99
|
+
self.is_own = is_own
|
|
100
|
+
self.rendered_markdown = False
|
|
101
|
+
|
|
102
|
+
def compose(self) -> ComposeResult:
|
|
103
|
+
with Vertical(classes="msg-container"):
|
|
104
|
+
yield Static(id="msg-header", classes="msg-header", markup=True)
|
|
105
|
+
yield Static(id="msg-content", classes="msg-content")
|
|
106
|
+
yield Static(id="msg-line", classes="msg-line", markup=True)
|
|
107
|
+
|
|
108
|
+
def on_mount(self):
|
|
109
|
+
self.set_interval(0.5, self.refresh_msg)
|
|
110
|
+
self.refresh_msg()
|
|
111
|
+
|
|
112
|
+
def refresh_msg(self):
|
|
113
|
+
now = datetime.now(timezone.utc)
|
|
114
|
+
read_at = self.m.get('read_at')
|
|
115
|
+
|
|
116
|
+
status = " [dim]○[/]"
|
|
117
|
+
if read_at:
|
|
118
|
+
read_dt = parse_iso_dt(read_at)
|
|
119
|
+
rem = 59 - (now - read_dt).total_seconds()
|
|
120
|
+
if rem <= 0:
|
|
121
|
+
self.remove()
|
|
122
|
+
return
|
|
123
|
+
status = f" [bold #22C55E]●[/] [bold #EF4444]{int(rem)}s[/]"
|
|
124
|
+
|
|
125
|
+
user_style = "bold #FFFFFF" if self.is_own else "bold #525252"
|
|
126
|
+
prefix = "› "
|
|
127
|
+
time_part = self.m['created_at'].split('T')[1][:5]
|
|
128
|
+
|
|
129
|
+
header_text = f"[{user_style}]{prefix}{self.m['nickname']}[/] [dim]{time_part}[/]{status}"
|
|
130
|
+
self.query_one("#msg-header").update(header_text)
|
|
131
|
+
|
|
132
|
+
if not self.rendered_markdown:
|
|
133
|
+
self.query_one("#msg-content").update(Markdown(self.m['content']))
|
|
134
|
+
self.rendered_markdown = True
|
|
135
|
+
|
|
136
|
+
box_width = self.query_one(".msg-container").content_size.width
|
|
137
|
+
if box_width > 0:
|
|
138
|
+
self.query_one("#msg-line").update(f"[dim]{'─' * box_width}[/]")
|
|
139
|
+
|
|
140
|
+
class FiftyNineChat(App):
|
|
141
|
+
CSS = CSS
|
|
142
|
+
status_text = reactive("")
|
|
143
|
+
|
|
144
|
+
BINDINGS = [
|
|
145
|
+
Binding("ctrl+c", "quit", "QUIT"),
|
|
146
|
+
Binding("ctrl+r", "new_room", "NEW ROOM"),
|
|
147
|
+
Binding("ctrl+l", "copy_invite", "INVITE"),
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
def __init__(self, room_id: Optional[str] = None, is_new: bool = False):
|
|
151
|
+
super().__init__()
|
|
152
|
+
self.api = SupabaseClient()
|
|
153
|
+
self.room_id = room_id or ""
|
|
154
|
+
self.nickname = ""
|
|
155
|
+
self.running = True
|
|
156
|
+
self.active_users = 1
|
|
157
|
+
self.is_new_session = is_new
|
|
158
|
+
self.displayed_ids = set()
|
|
159
|
+
|
|
160
|
+
def compose(self) -> ComposeResult:
|
|
161
|
+
with Horizontal(id="header"):
|
|
162
|
+
yield Static(id="header-info")
|
|
163
|
+
yield Static(id="header-status")
|
|
164
|
+
from textual.containers import VerticalScroll
|
|
165
|
+
yield VerticalScroll(id="chat-scroll")
|
|
166
|
+
with Vertical(id="bottom-bar"):
|
|
167
|
+
with Horizontal(id="input-row"):
|
|
168
|
+
yield Static("› ", id="input-prefix")
|
|
169
|
+
yield Input(placeholder="Type message...", id="message-input")
|
|
170
|
+
yield Footer()
|
|
171
|
+
|
|
172
|
+
def on_mount(self) -> None:
|
|
173
|
+
try:
|
|
174
|
+
self.nickname = self._generate_nickname()
|
|
175
|
+
if not self.room_id:
|
|
176
|
+
self.room_id = self._generate_room_id()
|
|
177
|
+
self.is_new_session = True
|
|
178
|
+
|
|
179
|
+
self._update_header()
|
|
180
|
+
|
|
181
|
+
welcome_msg = {
|
|
182
|
+
'id': 'welcome-msg',
|
|
183
|
+
'nickname': 'SYSTEM',
|
|
184
|
+
'content': "**WELCOME TO 59CHAT**\nMessages vanish in 59s. Zero Build Tools Required.",
|
|
185
|
+
'created_at': datetime.now(timezone.utc).isoformat(),
|
|
186
|
+
'read_at': datetime.now(timezone.utc).isoformat()
|
|
187
|
+
}
|
|
188
|
+
self._add_message_to_scroll(welcome_msg)
|
|
189
|
+
|
|
190
|
+
asyncio.create_task(self._watch_messages())
|
|
191
|
+
asyncio.create_task(self._mark_as_read())
|
|
192
|
+
asyncio.create_task(self._presence_heartbeat())
|
|
193
|
+
|
|
194
|
+
if self.is_new_session:
|
|
195
|
+
asyncio.create_task(self.action_copy_invite())
|
|
196
|
+
|
|
197
|
+
self.query_one("#message-input").focus()
|
|
198
|
+
except Exception as e:
|
|
199
|
+
self.notify(f"Init failed: {e}", severity="error")
|
|
200
|
+
|
|
201
|
+
def watch_status_text(self, text: str):
|
|
202
|
+
try:
|
|
203
|
+
self.query_one("#header-status").update(text)
|
|
204
|
+
except: pass
|
|
205
|
+
|
|
206
|
+
async def on_input_submitted(self, event: Input.Submitted):
|
|
207
|
+
content = event.value.strip()
|
|
208
|
+
if not content: return
|
|
209
|
+
try:
|
|
210
|
+
await self.api.send_message(self.room_id, self.nickname, content)
|
|
211
|
+
event.input.value = ""
|
|
212
|
+
except: pass
|
|
213
|
+
|
|
214
|
+
def _generate_nickname(self) -> str:
|
|
215
|
+
adj = ["Cold", "Swift", "Pure", "Thin", "Hard", "Dark"]
|
|
216
|
+
noun = ["Grid", "Line", "Type", "Form", "Node", "Void"]
|
|
217
|
+
return f"{random.choice(adj)}{random.choice(noun)}"
|
|
218
|
+
|
|
219
|
+
def _generate_room_id(self) -> str:
|
|
220
|
+
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
|
|
221
|
+
|
|
222
|
+
def _update_header(self):
|
|
223
|
+
info = f"ROOM [bold #FFFFFF]{self.room_id}[/] │ USER [bold #FFFFFF]{self.nickname}[/] │ ONLINE [bold #22C55E]{self.active_users}[/]"
|
|
224
|
+
self.query_one("#header-info").update(info)
|
|
225
|
+
|
|
226
|
+
def _add_message_to_scroll(self, m):
|
|
227
|
+
mid = m['id']
|
|
228
|
+
if mid not in self.displayed_ids:
|
|
229
|
+
self.displayed_ids.add(mid)
|
|
230
|
+
scroll = self.query_one("#chat-scroll")
|
|
231
|
+
is_own = m['nickname'] == self.nickname
|
|
232
|
+
msg_widget = MessageItem(m, is_own)
|
|
233
|
+
scroll.mount(msg_widget)
|
|
234
|
+
scroll.scroll_end(animate=False)
|
|
235
|
+
|
|
236
|
+
async def _presence_heartbeat(self):
|
|
237
|
+
while self.running:
|
|
238
|
+
try:
|
|
239
|
+
self.active_users = await self.api.get_active_users(self.room_id)
|
|
240
|
+
self._update_header()
|
|
241
|
+
await asyncio.sleep(10)
|
|
242
|
+
except: await asyncio.sleep(10)
|
|
243
|
+
|
|
244
|
+
async def _watch_messages(self):
|
|
245
|
+
while self.running:
|
|
246
|
+
try:
|
|
247
|
+
data = await self.api.get_messages(self.room_id)
|
|
248
|
+
if isinstance(data, list):
|
|
249
|
+
for m in data:
|
|
250
|
+
self._add_message_to_scroll(m)
|
|
251
|
+
await asyncio.sleep(1)
|
|
252
|
+
except: await asyncio.sleep(2)
|
|
253
|
+
|
|
254
|
+
async def _mark_as_read(self):
|
|
255
|
+
while self.running:
|
|
256
|
+
try:
|
|
257
|
+
data = await self.api.get_messages(self.room_id)
|
|
258
|
+
if isinstance(data, list):
|
|
259
|
+
unread = [m['id'] for m in data if m['nickname'] != self.nickname and m.get('read_at') is None]
|
|
260
|
+
if unread:
|
|
261
|
+
await self.api.mark_read(unread)
|
|
262
|
+
await asyncio.sleep(1)
|
|
263
|
+
except: await asyncio.sleep(2)
|
|
264
|
+
|
|
265
|
+
async def action_copy_invite(self):
|
|
266
|
+
py = "python" if sys.platform == "win32" else "python3"
|
|
267
|
+
cmd = f"{py} -m pip install -U pip ; {py} -m pip install --only-binary=:all: -U {APP_NAME} ; {py} -m {INTERNAL_PKG} --join {self.room_id}"
|
|
268
|
+
try:
|
|
269
|
+
pyperclip.copy(cmd)
|
|
270
|
+
self.status_text = "INVITATION COPIED!"
|
|
271
|
+
await asyncio.sleep(3)
|
|
272
|
+
self.status_text = ""
|
|
273
|
+
except: pass
|
|
274
|
+
|
|
275
|
+
def action_new_room(self):
|
|
276
|
+
self.room_id = self._generate_room_id()
|
|
277
|
+
scroll = self.query_one("#chat-scroll")
|
|
278
|
+
for child in list(scroll.children): child.remove()
|
|
279
|
+
self.displayed_ids.clear()
|
|
280
|
+
self._update_header()
|
|
281
|
+
asyncio.create_task(self.action_copy_invite())
|
|
282
|
+
|
|
283
|
+
def on_unmount(self) -> None:
|
|
284
|
+
self.running = False
|
|
285
|
+
|
|
286
|
+
def main_func():
|
|
287
|
+
parser = argparse.ArgumentParser()
|
|
288
|
+
parser.add_argument("--join", help="Room ID")
|
|
289
|
+
parser.add_argument("--new", action="store_true", help="New room")
|
|
290
|
+
args = parser.parse_args()
|
|
291
|
+
app = FiftyNineChat(room_id=None if args.new else args.join, is_new=args.new)
|
|
292
|
+
app.run()
|
|
293
|
+
|
|
294
|
+
if __name__ == "__main__":
|
|
295
|
+
main_func()
|
59chat-0.3.1/main.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
import subprocess
|
|
6
|
+
import json
|
|
7
|
+
import urllib.request
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from typing import Optional, Set, List, Dict
|
|
10
|
+
from textual.app import App, ComposeResult
|
|
11
|
+
from textual.widgets import Input, Static, Footer
|
|
12
|
+
from textual.containers import Vertical, Horizontal, Container, VerticalScroll
|
|
13
|
+
from textual.reactive import reactive
|
|
14
|
+
from textual.binding import Binding
|
|
15
|
+
from rich.markdown import Markdown
|
|
16
|
+
from supabase import create_client, Client
|
|
17
|
+
import random
|
|
18
|
+
import string
|
|
19
|
+
import pyperclip
|
|
20
|
+
|
|
21
|
+
# --- CONFIGURATION ---
|
|
22
|
+
VERSION = "0.1.9"
|
|
23
|
+
APP_NAME = "59chat"
|
|
24
|
+
CMD_NAME = "59chat"
|
|
25
|
+
SUPABASE_URL = "https://xdqxebyyjxklzisddmwl.supabase.co"
|
|
26
|
+
SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhkcXhlYnl5anhrbHppc2RkbXdsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk4Mzc0NDAsImV4cCI6MjA3NTQxMzQ0MH0.37JtLjN6mGfdac-t-cqNADa8OQlYIgSkZEFSngwxlM0"
|
|
27
|
+
|
|
28
|
+
CSS = """
|
|
29
|
+
Screen {
|
|
30
|
+
background: #000000;
|
|
31
|
+
color: #FFFFFF;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#header {
|
|
35
|
+
dock: top;
|
|
36
|
+
height: 4;
|
|
37
|
+
background: #000000;
|
|
38
|
+
padding: 1 2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#header-info {
|
|
42
|
+
width: 1fr;
|
|
43
|
+
content-align: left middle;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#header-status {
|
|
47
|
+
width: auto;
|
|
48
|
+
content-align: right middle;
|
|
49
|
+
color: #22C55E;
|
|
50
|
+
text-style: bold;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#chat-scroll {
|
|
54
|
+
height: 1fr;
|
|
55
|
+
padding: 1 2;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#bottom-bar {
|
|
59
|
+
dock: bottom;
|
|
60
|
+
height: auto;
|
|
61
|
+
background: #000000;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#input-row {
|
|
65
|
+
height: 1;
|
|
66
|
+
background: #0A0A0A;
|
|
67
|
+
padding: 0 2;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#input-prefix {
|
|
71
|
+
width: auto;
|
|
72
|
+
color: #FFFFFF;
|
|
73
|
+
text-style: bold;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
Input {
|
|
77
|
+
background: transparent;
|
|
78
|
+
border: none;
|
|
79
|
+
width: 1fr;
|
|
80
|
+
height: 1;
|
|
81
|
+
color: #FFFFFF;
|
|
82
|
+
padding: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Input:focus {
|
|
86
|
+
border: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.message-item {
|
|
90
|
+
width: 100%;
|
|
91
|
+
height: auto;
|
|
92
|
+
margin-bottom: 2;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.message-item.own {
|
|
96
|
+
align-horizontal: right;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.message-item.other {
|
|
100
|
+
align-horizontal: left;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.msg-container {
|
|
104
|
+
width: auto;
|
|
105
|
+
min-width: 40%;
|
|
106
|
+
max-width: 80%;
|
|
107
|
+
height: auto;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.msg-header {
|
|
111
|
+
height: 1;
|
|
112
|
+
color: #FFFFFF;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.msg-content {
|
|
116
|
+
height: auto;
|
|
117
|
+
padding: 0 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.msg-line {
|
|
121
|
+
height: 1;
|
|
122
|
+
color: #525252;
|
|
123
|
+
}
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
class MessageItem(Vertical):
|
|
127
|
+
def __init__(self, m, is_own):
|
|
128
|
+
super().__init__(classes=f"message-item {'own' if is_own else 'other'}")
|
|
129
|
+
self.m = m
|
|
130
|
+
self.is_own = is_own
|
|
131
|
+
self.rendered_markdown = False
|
|
132
|
+
|
|
133
|
+
def compose(self) -> ComposeResult:
|
|
134
|
+
with Vertical(classes="msg-container"):
|
|
135
|
+
yield Static(id="msg-header", classes="msg-header", markup=True)
|
|
136
|
+
yield Static(id="msg-content", classes="msg-content")
|
|
137
|
+
yield Static(id="msg-line", classes="msg-line", markup=True)
|
|
138
|
+
|
|
139
|
+
def on_mount(self):
|
|
140
|
+
self.set_interval(0.5, self.refresh_msg)
|
|
141
|
+
self.refresh_msg()
|
|
142
|
+
|
|
143
|
+
def refresh_msg(self):
|
|
144
|
+
now = datetime.now(timezone.utc)
|
|
145
|
+
read_at = self.m.get('read_at')
|
|
146
|
+
|
|
147
|
+
status = " [dim]○[/]"
|
|
148
|
+
if read_at:
|
|
149
|
+
read_dt = datetime.fromisoformat(read_at.replace('Z', '+00:00'))
|
|
150
|
+
rem = 59 - (now - read_dt).total_seconds()
|
|
151
|
+
if rem <= 0:
|
|
152
|
+
self.remove()
|
|
153
|
+
return
|
|
154
|
+
status = f" [bold #22C55E]●[/] [bold #EF4444]{int(rem)}s[/]"
|
|
155
|
+
|
|
156
|
+
user_style = "bold #FFFFFF" if self.is_own else "bold #525252"
|
|
157
|
+
prefix = "› "
|
|
158
|
+
time_part = self.m['created_at'].split('T')[1][:5]
|
|
159
|
+
|
|
160
|
+
header_text = f"[{user_style}]{prefix}{self.m['nickname']}[/] [dim]{time_part}[/]{status}"
|
|
161
|
+
self.query_one("#msg-header").update(header_text)
|
|
162
|
+
|
|
163
|
+
if not self.rendered_markdown:
|
|
164
|
+
self.query_one("#msg-content").update(Markdown(self.m['content']))
|
|
165
|
+
self.rendered_markdown = True
|
|
166
|
+
|
|
167
|
+
# Responsive Çizgi Mantığı (Önce Yatayda Genişle)
|
|
168
|
+
edge_padding = 4
|
|
169
|
+
screen_w = (self.app.size.width or 80) - edge_padding
|
|
170
|
+
w_min = int(screen_w * 0.4)
|
|
171
|
+
w_max = int(screen_w * 0.8)
|
|
172
|
+
|
|
173
|
+
# İçerik uzunluğu tahmini
|
|
174
|
+
lines = self.m['content'].split('\n')
|
|
175
|
+
max_line = max(len(l) for l in lines) if lines else 0
|
|
176
|
+
target_w = max(w_min, min(max_line + 10, w_max))
|
|
177
|
+
|
|
178
|
+
container = self.query_one(".msg-container")
|
|
179
|
+
container.styles.width = target_w
|
|
180
|
+
self.query_one("#msg-line").update(f"[dim]{'─' * target_w}[/]")
|
|
181
|
+
|
|
182
|
+
class FiftyNineChat(App):
|
|
183
|
+
CSS = CSS
|
|
184
|
+
status_text = reactive("")
|
|
185
|
+
|
|
186
|
+
BINDINGS = [
|
|
187
|
+
Binding("ctrl+c", "quit", "QUIT"),
|
|
188
|
+
Binding("ctrl+r", "new_room", "NEW ROOM"),
|
|
189
|
+
Binding("ctrl+l", "copy_invite", "INVITE"),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
def __init__(self, room_id: Optional[str] = None, is_new: bool = False):
|
|
193
|
+
super().__init__()
|
|
194
|
+
self.supabase: Optional[Client] = None
|
|
195
|
+
self.room_id = room_id or ""
|
|
196
|
+
self.nickname = ""
|
|
197
|
+
self.running = True
|
|
198
|
+
self.active_users = 1
|
|
199
|
+
self.is_new_session = is_new
|
|
200
|
+
self.displayed_ids = set()
|
|
201
|
+
|
|
202
|
+
def compose(self) -> ComposeResult:
|
|
203
|
+
with Horizontal(id="header"):
|
|
204
|
+
yield Static(id="header-info")
|
|
205
|
+
yield Static(id="header-status")
|
|
206
|
+
yield VerticalScroll(id="chat-scroll")
|
|
207
|
+
with Vertical(id="bottom-bar"):
|
|
208
|
+
with Horizontal(id="input-row"):
|
|
209
|
+
yield Static("› ", id="input-prefix")
|
|
210
|
+
yield Input(placeholder="Type message...", id="message-input")
|
|
211
|
+
yield Footer()
|
|
212
|
+
|
|
213
|
+
def on_mount(self) -> None:
|
|
214
|
+
try:
|
|
215
|
+
self.supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
|
|
216
|
+
self.nickname = self._generate_nickname()
|
|
217
|
+
if not self.room_id:
|
|
218
|
+
self.room_id = self._generate_room_id()
|
|
219
|
+
self.is_new_session = True
|
|
220
|
+
|
|
221
|
+
self._update_header()
|
|
222
|
+
|
|
223
|
+
welcome_msg = {
|
|
224
|
+
'id': 'welcome-msg',
|
|
225
|
+
'nickname': 'SYSTEM',
|
|
226
|
+
'content': '**WELCOME TO 59CHAT**\nMessages vanish in 59s. Type and press Enter.',
|
|
227
|
+
'created_at': datetime.now(timezone.utc).isoformat(),
|
|
228
|
+
'read_at': datetime.now(timezone.utc).isoformat()
|
|
229
|
+
}
|
|
230
|
+
self._add_message_to_scroll(welcome_msg)
|
|
231
|
+
|
|
232
|
+
asyncio.create_task(self._watch_messages())
|
|
233
|
+
asyncio.create_task(self._mark_as_read())
|
|
234
|
+
asyncio.create_task(self._presence_heartbeat())
|
|
235
|
+
|
|
236
|
+
if self.is_new_session:
|
|
237
|
+
asyncio.create_task(self.action_copy_invite())
|
|
238
|
+
|
|
239
|
+
self.query_one("#message-input").focus()
|
|
240
|
+
except Exception as e:
|
|
241
|
+
self.notify(f"Init failed: {e}", severity="error")
|
|
242
|
+
|
|
243
|
+
def watch_status_text(self, text: str):
|
|
244
|
+
try:
|
|
245
|
+
self.query_one("#header-status").update(text)
|
|
246
|
+
except: pass
|
|
247
|
+
|
|
248
|
+
async def on_input_submitted(self, event: Input.Submitted):
|
|
249
|
+
content = event.value.strip()
|
|
250
|
+
if not content or not self.supabase: return
|
|
251
|
+
try:
|
|
252
|
+
self.supabase.table('messages').insert({
|
|
253
|
+
'room_id': self.room_id,
|
|
254
|
+
'nickname': self.nickname,
|
|
255
|
+
'content': content
|
|
256
|
+
}).execute()
|
|
257
|
+
event.input.value = ""
|
|
258
|
+
except: pass
|
|
259
|
+
|
|
260
|
+
def _generate_nickname(self) -> str:
|
|
261
|
+
adj = ["Cold", "Swift", "Pure", "Thin", "Hard", "Dark"]
|
|
262
|
+
noun = ["Grid", "Line", "Type", "Form", "Node", "Void"]
|
|
263
|
+
return f"{random.choice(adj)}{random.choice(noun)}"
|
|
264
|
+
|
|
265
|
+
def _generate_room_id(self) -> str:
|
|
266
|
+
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
|
|
267
|
+
|
|
268
|
+
def _update_header(self):
|
|
269
|
+
info = f"ROOM [bold #FFFFFF]{self.room_id}[/] │ USER [bold #FFFFFF]{self.nickname}[/] │ ONLINE [bold #22C55E]{self.active_users}[/]"
|
|
270
|
+
self.query_one("#header-info").update(info)
|
|
271
|
+
|
|
272
|
+
def _add_message_to_scroll(self, m):
|
|
273
|
+
mid = m['id']
|
|
274
|
+
if mid not in self.displayed_ids:
|
|
275
|
+
self.displayed_ids.add(mid)
|
|
276
|
+
scroll = self.query_one("#chat-scroll")
|
|
277
|
+
is_own = m['nickname'] == self.nickname
|
|
278
|
+
msg_widget = MessageItem(m, is_own)
|
|
279
|
+
scroll.mount(msg_widget)
|
|
280
|
+
scroll.scroll_end(animate=False)
|
|
281
|
+
|
|
282
|
+
async def _presence_heartbeat(self):
|
|
283
|
+
while self.running and self.supabase:
|
|
284
|
+
try:
|
|
285
|
+
res = self.supabase.table('messages').select('nickname').eq('room_id', self.room_id).execute()
|
|
286
|
+
self.active_users = max(1, len({m['nickname'] for m in res.data}))
|
|
287
|
+
self._update_header()
|
|
288
|
+
await asyncio.sleep(10)
|
|
289
|
+
except: await asyncio.sleep(10)
|
|
290
|
+
|
|
291
|
+
async def _watch_messages(self):
|
|
292
|
+
while self.running and self.supabase:
|
|
293
|
+
try:
|
|
294
|
+
res = self.supabase.table('messages').select('*').eq('room_id', self.room_id).order('created_at').execute()
|
|
295
|
+
for m in res.data:
|
|
296
|
+
self._add_message_to_scroll(m)
|
|
297
|
+
await asyncio.sleep(1)
|
|
298
|
+
except: await asyncio.sleep(2)
|
|
299
|
+
|
|
300
|
+
async def _mark_as_read(self):
|
|
301
|
+
while self.running and self.supabase:
|
|
302
|
+
try:
|
|
303
|
+
res = self.supabase.table('messages').select('id, nickname').eq('room_id', self.room_id).is_('read_at', 'null').execute()
|
|
304
|
+
unread = [m['id'] for m in res.data if m['nickname'] != self.nickname]
|
|
305
|
+
if unread:
|
|
306
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
307
|
+
for mid in unread:
|
|
308
|
+
self.supabase.table('messages').update({'read_at': now}).eq('id', mid).execute()
|
|
309
|
+
await asyncio.sleep(1)
|
|
310
|
+
except: await asyncio.sleep(2)
|
|
311
|
+
|
|
312
|
+
async def action_copy_invite(self):
|
|
313
|
+
# UNIVERSAL MAGIC JOIN COMMAND
|
|
314
|
+
if sys.platform == "win32":
|
|
315
|
+
py, sep = "python", ";" # PowerShell/CMD uyumlu güvenli ayırıcı
|
|
316
|
+
else:
|
|
317
|
+
py, sep = "python3", "&&"
|
|
318
|
+
|
|
319
|
+
cmd = f"{py} -m pip install -U pip {sep} {py} -m pip install -U {APP_NAME} {sep} {py} -c \"import main; main.main_func()\" --join {self.room_id}"
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
pyperclip.copy(cmd)
|
|
323
|
+
self.status_text = "INVITATION COPIED!"
|
|
324
|
+
await asyncio.sleep(3)
|
|
325
|
+
self.status_text = ""
|
|
326
|
+
except: pass
|
|
327
|
+
|
|
328
|
+
def action_new_room(self):
|
|
329
|
+
self.room_id = self._generate_room_id()
|
|
330
|
+
scroll = self.query_one("#chat-scroll")
|
|
331
|
+
for child in list(scroll.children): child.remove()
|
|
332
|
+
self.displayed_ids.clear()
|
|
333
|
+
self._update_header()
|
|
334
|
+
asyncio.create_task(self.action_copy_invite())
|
|
335
|
+
|
|
336
|
+
def on_unmount(self) -> None:
|
|
337
|
+
self.running = False
|
|
338
|
+
|
|
339
|
+
def main_func():
|
|
340
|
+
parser = argparse.ArgumentParser()
|
|
341
|
+
parser.add_argument("--join", help="Room ID")
|
|
342
|
+
parser.add_argument("--new", action="store_true", help="New room")
|
|
343
|
+
args = parser.parse_args()
|
|
344
|
+
app = FiftyNineChat(room_id=None if args.new else args.join, is_new=args.new)
|
|
345
|
+
app.run()
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
main_func()
|
59chat-0.3.1/setup.cfg
ADDED
59chat-0.3.1/setup.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="59chat",
|
|
5
|
+
version="0.3.1",
|
|
6
|
+
author="YourName",
|
|
7
|
+
description="59-second zero-trace terminal chat (Ultra-Light Edition)",
|
|
8
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
9
|
+
long_description_content_type="text/markdown",
|
|
10
|
+
url="https://github.com/yourusername/59chat",
|
|
11
|
+
packages=find_packages(),
|
|
12
|
+
install_requires=[
|
|
13
|
+
"textual>=0.45.0",
|
|
14
|
+
"httpx>=0.25.0",
|
|
15
|
+
"pyperclip>=1.8.2",
|
|
16
|
+
],
|
|
17
|
+
entry_points={
|
|
18
|
+
"console_scripts": [
|
|
19
|
+
"59chat=chat59.main:main_func",
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
python_requires=">=3.8",
|
|
23
|
+
)
|