shellwhisper-cli 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.
@@ -0,0 +1,79 @@
1
+ from textual.app import ComposeResult
2
+ from textual.screen import Screen
3
+ from textual.widgets import Header, Footer, Input, Button, Label
4
+ from textual.containers import Vertical, Center, Middle
5
+
6
+ class ForgotPasswordScreen(Screen):
7
+ BINDINGS = [("escape", "app.pop_screen", "Back to Login")]
8
+
9
+ def compose(self) -> ComposeResult:
10
+ yield Header()
11
+
12
+ with Middle():
13
+ with Center():
14
+ with Vertical(id="login-form"):
15
+ yield Label("[bold yellow]Reset Password[/]", id="title")
16
+ yield Label("Enter your registered email address:", classes="hint")
17
+ yield Input(placeholder="Email", id="reset_email")
18
+ yield Button("Send Reset Code", variant="primary", id="send_code_btn")
19
+ yield Button("Back to Login", variant="default", id="back_btn")
20
+ yield Footer()
21
+
22
+ def on_button_pressed(self, event: Button.Pressed) -> None:
23
+ if event.button.id == "back_btn":
24
+ self.dismiss()
25
+ elif event.button.id == "send_code_btn":
26
+ email = self.query_one("#reset_email").value.strip()
27
+ if not email:
28
+ self.app.notify("Email is required", severity="error")
29
+ return
30
+
31
+ try:
32
+ self.app.api.request_password_reset(email)
33
+ self.app.notify("Reset code sent! Check you email.", severity="information")
34
+ self.app.switch_screen(ConfirmResetScreen(email=email))
35
+ except Exception:
36
+ self.app.notify("Failed to contact server.", severity="error")
37
+
38
+ class ConfirmResetScreen(Screen):
39
+ BINDINGS = [("escape", "app.pop_screen", "Back to Login")]
40
+
41
+ def __init__(self, email: str = ""):
42
+ super().__init__()
43
+ self.target_email = email
44
+
45
+ def compose(self) -> ComposeResult:
46
+ yield Header()
47
+
48
+ with Middle():
49
+ with Center():
50
+ with Vertical(id="login-form"):
51
+ yield Label("[bold green]Confirm New Password[/]", id="title")
52
+ yield Label(f"Code sent to {self.target_email}", classes="hint")
53
+ yield Input(placeholder="8-Character Code", id="reset_code")
54
+ yield Input(placeholder="New Password", password=True, id="new_pass")
55
+ yield Button("Update Password", variant="success", id="update_btn")
56
+ yield Button("Cancel", variant="error", id="cancel_btn")
57
+ yield Footer()
58
+
59
+ def on_button_pressed(self, event: Button.Pressed) -> None:
60
+ from src.screens.login import LoginScreen
61
+ if event.button.id == "cancel_btn":
62
+ self.app.switch_screen(LoginScreen())
63
+ elif event.button.id == "update_btn":
64
+ code = self.query_one("#reset_code").value.strip()
65
+ new_pass = self.query_one("#new_pass").value.strip()
66
+
67
+ if not code or not new_pass:
68
+ self.app.notify("All fields are required", severity="error")
69
+ return
70
+
71
+ try:
72
+ res = self.app.api.confirm_password_reset(self.target_email, code, new_pass)
73
+ if res.status_code == 200:
74
+ self.app.notify("Password updated successfully! Please log in.", severity="success")
75
+ self.app.switch_screen(LoginScreen())
76
+ else:
77
+ self.app.notify("Invalid or expired code.", severity="error")
78
+ except Exception:
79
+ self.app.notify("Failed to contact server", severity="error")
screens/join_screen.py ADDED
@@ -0,0 +1,23 @@
1
+ from textual.screen import ModalScreen
2
+ from textual.containers import Center, Grid, Vertical
3
+ from textual.widgets import Footer, Input, Label, Button
4
+
5
+ class JoinRoomScreen(ModalScreen):
6
+
7
+ def compose(self):
8
+ with Grid(id="join-dialog"):
9
+ yield Label("Enter Room ID to Join", id="join-label")
10
+ yield Input(placeholder="Room ID", id="room-id-input")
11
+ yield Button("Join Room", variant="primary", id="confirm-join")
12
+ yield Button("Cancel", variant="error", id="cancel-join")
13
+
14
+ def on_button_pressed(self, event):
15
+ if event.button.id == "confirm-join":
16
+ room_id = self.query_one("#room-id-input").value
17
+
18
+ self.dismiss(room_id)
19
+ else:
20
+ self.dismiss(None)
21
+
22
+
23
+
screens/login.py ADDED
@@ -0,0 +1,104 @@
1
+ from textual.app import ComposeResult
2
+ from textual.screen import Screen
3
+ from textual.widgets import Header, Footer, Input, Button, Label
4
+ from textual.containers import Vertical, Center, Middle
5
+
6
+ from src.screens.chat_screen import ChatScreen
7
+
8
+ import requests
9
+
10
+ class LoginScreen(Screen):
11
+ BINDINGS = [
12
+ ("ctrl+q", "quit", "Quit"),
13
+ ("ctrl+d", "toggle_dark", "Toggle dark mode"),
14
+ ]
15
+
16
+ def compose(self) -> ComposeResult:
17
+ yield Header()
18
+
19
+ with Middle():
20
+ with Center():
21
+ with Vertical(id="login-form"):
22
+ yield Label("[bold cyan] Welcome to ShellWhisper[/]", id="title")
23
+ yield Input(placeholder="Username", id="username")
24
+ yield Input(placeholder="Password", password=True, id="password")
25
+ yield Button("Login", variant="success", id="login_btn")
26
+ yield Button("Need an account? Sign up", variant="default", id="to_signup")
27
+ yield Button("Forgot Password?", variant="default", id="forgot_pass_btn")
28
+ yield Footer()
29
+
30
+ def on_button_pressed(self, event: Button.Pressed) -> None:
31
+ if event.button.id == "login_btn":
32
+ username = self.query_one("#username").value.strip()
33
+ password = self.query_one("#password").value.strip()
34
+
35
+ try:
36
+ response = self.app.api.login(username, password)
37
+
38
+ if response.status_code == 200:
39
+ data = response.json()
40
+ self.app.access_token = data.get("token")
41
+
42
+ self.app.refresh_token = data.get("refreshToken")
43
+
44
+ self.app.current_user = username
45
+ self.app.notify(f"Welcome, {username}!", severity="success")
46
+ self.app.switch_screen(ChatScreen())
47
+
48
+ elif response.status_code == 401:
49
+ self.app.notify("Invalid username or password", severity="error")
50
+
51
+ else:
52
+ self.app.notify(f"Server Error: {response.status_code}", severity="error")
53
+
54
+ except Exception:
55
+ self.app.notify("Could not connect to ShellWhisper server", severity="error")
56
+
57
+ elif event.button.id == "to_signup":
58
+ self.app.push_screen(SignupScreen())
59
+
60
+ elif event.button.id == "forgot_pass_btn":
61
+ from src.screens.forgot_password import ForgotPasswordScreen
62
+ self.app.push_screen(ForgotPasswordScreen())
63
+
64
+ class SignupScreen(Screen):
65
+ BINDINGS = [("escape", "app.pop_screen", "Back to Login")]
66
+
67
+ def compose(self) -> ComposeResult:
68
+ yield Header()
69
+ with Center():
70
+ with Vertical(id="signup-form"):
71
+ yield Label("[bold]Create Account[/]")
72
+ yield Input(placeholder="Username", id="signup_user")
73
+ yield Input(placeholder="Email", id="signup_email")
74
+ yield Input(placeholder="Password", password=True, id="signup_pass")
75
+ yield Button("Register", variant="primary", id="register_btn")
76
+ yield Button("Back to Login", variant="default", id="back_btn")
77
+ yield Footer()
78
+
79
+ def on_button_pressed(self, event: Button.Pressed) -> None:
80
+ if event.button.id == "back_btn":
81
+ self.dismiss()
82
+
83
+ elif event.button.id == "register_btn":
84
+ username = self.query_one("#signup_user").value.strip()
85
+ email = self.query_one("#signup_email").value.strip()
86
+ password = self.query_one("#signup_pass").value.strip()
87
+
88
+ if not username or not password:
89
+ self.app.notify("Username and Password required!", severity="error")
90
+ return
91
+
92
+ try:
93
+ response = self.app.api.signup(username, email, password)
94
+
95
+ if response.status_code == 200:
96
+ self.app.notify("Account created! Please login.", severity="information")
97
+ self.dismiss()
98
+ else:
99
+ error_msg = response.text or "Signup failed !"
100
+ self.app.notify(f"Error: {error_msg}", severity="error")
101
+
102
+ except Exception:
103
+ self.app.notify("Backend server is not running!", severity="error")
104
+
@@ -0,0 +1,26 @@
1
+ from textual.screen import ModalScreen
2
+ from textual.containers import Grid, Vertical
3
+ from textual.widgets import Label, Input, Button
4
+
5
+ class PrivateWhisperPromptScreen(ModalScreen):
6
+ def compose(self):
7
+ with Vertical(id="room-action-dialog"):
8
+ yield Label("Start a Private Whisper", id="action-label")
9
+ yield Input(placeholder="Enter target username...", id="room-input")
10
+
11
+ with Grid(id="action-button-grid"):
12
+ yield Button("Start Chat", variant="success", id="start_btn")
13
+ yield Button("Cancel", variant="error", id="cancel_btn")
14
+
15
+ def on_button_pressed(self, event: Button.Pressed) -> None:
16
+ if event.button.id == "cancel_btn":
17
+ self.dismiss(None)
18
+ return
19
+
20
+ target_user = self.query_one("#room-input").value.strip()
21
+
22
+ if not target_user:
23
+ self.app.notify("Please enter a username.", severity="error")
24
+ return
25
+
26
+ self.dismiss(target_user)
@@ -0,0 +1,35 @@
1
+ from textual.screen import ModalScreen
2
+ from textual.containers import Grid, Vertical
3
+ from textual.widgets import Label, Input, Button
4
+
5
+ class RoomActionScreen(ModalScreen):
6
+ def compose(self):
7
+ with Vertical(id="room-action-dialog"):
8
+ yield Label("Room Actions", id="action-label")
9
+ yield Input(placeholder="Enter room name...", id="room-input")
10
+
11
+ with Grid(id="action-buttons-grid"):
12
+ yield Button("Join Room", variant="primary", id="join_btn")
13
+ yield Button("Create Room", variant="success", id="create_btn")
14
+
15
+ yield Button("Cancel", variant="error", id="cancel_btn")
16
+
17
+ def on_button_pressed(self, event: Button.Pressed) -> None:
18
+ if event.button.id == "cancel_btn":
19
+ self.dismiss(None)
20
+ return
21
+
22
+ room_name = self.query_one("#room-input").value.strip()
23
+
24
+ if not room_name:
25
+ self.app.notify("Please enter a room name.", severity="error")
26
+ return
27
+
28
+ if not room_name.replace("-", "").replace("_", "").isalnum():
29
+ self.app.notify(
30
+ "Room name can only contain letters, numbers, hyphens and underscores.",
31
+ severity="error",
32
+ )
33
+ return
34
+
35
+ self.dismiss({"name": room_name, "action": event.button.id})
@@ -0,0 +1,45 @@
1
+ from textual.screen import ModalScreen
2
+ from textual.containers import Vertical
3
+ from textual.widgets import Label, Input, Button
4
+
5
+ class SecurityScreen(ModalScreen):
6
+
7
+ def __init__(self, action: str = "authenticate", room_name: str = "") -> None:
8
+ super().__init__()
9
+ self.action = action
10
+ self.room_name = room_name
11
+
12
+ def compose(self):
13
+ if self.action == "create_btn":
14
+ title = f"Set Security Key for '{self.room_name}'"
15
+ hint = "Others will need key to join."
16
+ btn_label = "Create Room"
17
+ btn_variant = "success"
18
+ elif self.action == "chat_command_delete":
19
+ title = f"Confirm Deletion of '{self.room_name}'"
20
+ hint = "Enter the room security key to permanently delete this room."
21
+ btn_label = "Delete Room"
22
+ btn_variant = "error"
23
+ else:
24
+ title = f"Enter Security Key for '{self.room_name}'"
25
+ hint = "The key set by the room creator."
26
+ btn_label = "Join Room"
27
+ btn_variant = "primary"
28
+
29
+ with Vertical(id="security-dialog"):
30
+ yield Label(title, id="security-label")
31
+ yield Label(f"[dim]{hint}[/]", id="security-hint")
32
+ yield Input(placeholder="Secret key...", password=True, id="security-input")
33
+ yield Button(btn_label, variant=btn_variant, id="auth_btn")
34
+ yield Button("Cancel", variant="error", id="cancel_btn")
35
+
36
+ def on_button_pressed(self, event: Button.Pressed) -> None:
37
+ if event.button.id == "cancel_btn":
38
+ self.dismiss(None)
39
+ elif event.button.id == "auth_btn":
40
+ val = self.query_one("#security-input").value.strip()
41
+ if not val:
42
+ self.app.notify("Security key cannot be empty.", severity="error")
43
+ return
44
+ self.dismiss(val)
45
+
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: shellwhisper-cli
3
+ Version: 1.0.0
4
+ Summary: A sleek terminal-based real-time chat client for ShellWhisper
5
+ License: MIT
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: Communications :: Chat
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: textual>=0.50.0
13
+ Requires-Dist: requests>=2.31.0
14
+ Requires-Dist: websocket-client>=1.7.0
@@ -0,0 +1,16 @@
1
+ app.py,sha256=4-giS18WvJQDg38juZ1C4LNEKHMwDhLcYWiQvhK_1cU,9075
2
+ events.py,sha256=9WaKFd0PF73Oxepn-U0Ss6Wtv7-05f9DMMR-xkCvsbE,167
3
+ components/sidebar.py,sha256=cjj-opFxVMMl6pbIr0JQq9ylYHDa053hd8hJbB_jDNw,3079
4
+ screens/chat_screen.py,sha256=K-ZOEUy9_F8JzEmst2LeAITablWfoKnSaXeE2utCvYE,24080
5
+ screens/forgot_password.py,sha256=0k1Eo69zmkQmJ3I5DZwbf42JtrSUc4vCjWNHSHXLrpw,3555
6
+ screens/join_screen.py,sha256=H2iwK1XFhFhIJtl7ir1vNHoCpKJDvZcSgzqh0Wwxmd0,757
7
+ screens/login.py,sha256=Tyw2-jxiCrp05XWCygPxbm2so4OkZ5ZNCuY3dFJtSfw,4267
8
+ screens/private_whisper_screen.py,sha256=LuK6D9kZnx0u-UtUO-IC59iBEbz5pslY8AXd6uivLTI,987
9
+ screens/room_action_screen.py,sha256=stsEBuPHnJfOjdvC0w0m45EstZe-XZhXhwS3ejEosq8,1323
10
+ screens/security_screen.py,sha256=NEWNrMT8QQolmrLDQJkPD9lZ1tn6DMxDMtd1Uxsaw14,1865
11
+ utils/api_client.py,sha256=oeFuvNUgVKwYIYfVl2QrZ2LrA5oJNDCo6sQE_10lPc0,4142
12
+ shellwhisper_cli-1.0.0.dist-info/METADATA,sha256=4XP1AkWraVW9cD_nR_Hq8h7VzkiDhWDijVxGBKkyhQE,500
13
+ shellwhisper_cli-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
14
+ shellwhisper_cli-1.0.0.dist-info/entry_points.txt,sha256=eMHy4Ob5IIvDfYGUt5p0QjyFwjtp4bvUmvxtxQnDW8Q,46
15
+ shellwhisper_cli-1.0.0.dist-info/top_level.txt,sha256=e_yMsXNStNTv4ZLrcS9PMfpIVVe1PyB44VDka2QQVDA,36
16
+ shellwhisper_cli-1.0.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
+ shellwhisper = src.app:main
@@ -0,0 +1,5 @@
1
+ app
2
+ components
3
+ events
4
+ screens
5
+ utils
utils/api_client.py ADDED
@@ -0,0 +1,133 @@
1
+ import requests
2
+ import os
3
+
4
+ class APIClient:
5
+ # BASE_URL = "http://localhost:8080/api/v1"
6
+ # BASE_URL = os.getenv(
7
+ # "SHELLWHISPER_API_URL",
8
+ # "https://shellwhisper-server.onrender.com/api/v1"
9
+ # )
10
+ # WS_URL = os.getenv(
11
+ # "SHELLWHISPER_WS_URL",
12
+ # "wss://shellwhisper-server.onrender.com/chat/websocket"
13
+ # )
14
+
15
+ BASE_URL = "https://shellwhisper-server.onrender.com/api/v1"
16
+ WS_URL = "wss://shellwhisper-server.onrender.com/chat/websocket"
17
+
18
+ def __init__(self, app):
19
+ self.app = app
20
+
21
+ def _get_headers(self):
22
+ return {"Authorization": f"Bearer {self.app.access_token}"}
23
+
24
+ def _make_auth_request(self, method, endpoint, **kwargs):
25
+ url = f"{self.BASE_URL}{endpoint}"
26
+ kwargs["headers"] = self._get_headers()
27
+ kwargs.setdefault("timeout", 5)
28
+
29
+ response = requests.request(method, url, **kwargs)
30
+
31
+ if response.status_code == 401 and getattr(self.app, 'refresh_token', None):
32
+ refresh_res = requests.post(
33
+ f"{self.BASE_URL}/auth/refresh",
34
+ json={"refreshToken": self.app.refresh_token},
35
+ timeout=5
36
+ )
37
+
38
+ if refresh_res.status_code == 200:
39
+ data = refresh_res.json()
40
+ self.app.access_token = data.get("token")
41
+
42
+ kwargs["headers"] = self._get_headers()
43
+
44
+ return requests.request(method, url, **kwargs)
45
+ else:
46
+ self.app.call_from_thread(self.app.action_logout)
47
+
48
+ return response
49
+
50
+
51
+ def fetch_rooms(self):
52
+ return self._make_auth_request(
53
+ "GET",
54
+ "/room/my-rooms",
55
+ )
56
+
57
+ def fetch_messages(self, room_id: str):
58
+ return self._make_auth_request(
59
+ "GET",
60
+ f"/messages/{room_id}"
61
+ )
62
+
63
+ def create_room(self, room_name: str, security_string: str):
64
+ return self._make_auth_request(
65
+ "POST",
66
+ "/room/new",
67
+ json={"roomName": room_name, "rawSecurityString": security_string}
68
+ )
69
+
70
+ def join_room(self, room_name: str, security_string: str):
71
+ return self._make_auth_request(
72
+ "POST",
73
+ "/room/join",
74
+ json={"roomName": room_name, "rawSecurityString": security_string}
75
+ )
76
+
77
+ def delete_room(self, room_id: str, security_key: str = ""):
78
+ return self._make_auth_request(
79
+ "DELETE",
80
+ f"/room/delete/{room_id}",
81
+ params={"securityKey": security_key}
82
+ )
83
+
84
+ def leave_room(self, room_id: str):
85
+ return self._make_auth_request(
86
+ "DELETE",
87
+ f"/room/{room_id}/leave",
88
+ timeout=10
89
+ )
90
+
91
+ def start_private_chat(self, target_username: str):
92
+ return self._make_auth_request(
93
+ "POST",
94
+ f"/room/private/{target_username}"
95
+ )
96
+
97
+
98
+ def login(self, username: str, password: str):
99
+ return requests.post(
100
+ f"{self.BASE_URL}/auth/login",
101
+ json={"username": username, "password": password},
102
+ timeout=45,
103
+ )
104
+
105
+ def signup(self, username: str, email: str, password: str):
106
+ return requests.post(
107
+ f"{self.BASE_URL}/auth/signup",
108
+ json={"username": username, "email": email, "password": password},
109
+ timeout=45,
110
+ )
111
+
112
+ def logout_backend(self):
113
+ if getattr(self.app, 'refresh_token', None):
114
+ requests.post(
115
+ f"{self.BASE_URL}/auth/logout",
116
+ json={"refreshToken": self.app.refresh_token},
117
+ headers=self._get_headers(),
118
+ timeout=5
119
+ )
120
+
121
+ def request_password_reset(self, email: str):
122
+ return requests.post(
123
+ f"{self.BASE_URL}/auth/forgot-password",
124
+ json={"email": email},
125
+ timeout=5,
126
+ )
127
+
128
+ def confirm_password_reset(self, email: str, token: str, new_password: str):
129
+ return requests.post(
130
+ f"{self.BASE_URL}/auth/reset-password",
131
+ json={"email": email, "token": token, "newPassword": new_password},
132
+ timeout=5,
133
+ )