Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
8c13de3
fix: improve CAPTCHA consistency and difficulty scaling
Sathvik-Rao Oct 27, 2026
08443d2
Persist manual theme preference in browser local storage
Sathvik-Rao Oct 27, 2026
a00fcbf
server: Improve mobile UI, login autofill, and web app installation s…
Sathvik-Rao Oct 27, 2026
c98ba5b
fix(android): improve session handling and add server reverse-proxy e…
Sathvik-Rao Oct 27, 2026
7260d52
fix(android): prevent duplicate P2P connections after service restart
Sathvik-Rao Oct 27, 2026
71cbfd8
feat(desktop): add optional notifications for received clipboard content
Sathvik-Rao Oct 28, 2026
3bb6287
feat: add per-device clipboard send and receive controls
Sathvik-Rao Oct 28, 2026
b229d4e
feat: add unit selection to clipboard size limits
Sathvik-Rao Oct 28, 2026
c083e49
fix(android): surface clipboard write errors with clear system-limit …
Sathvik-Rao Oct 28, 2026
3721db4
feat(android): add a persistent default download folder
Sathvik-Rao Oct 28, 2026
5e42531
feat: improve download feedback across clients
Sathvik-Rao Oct 29, 2026
0a50439
fix: improve clipboard and share handling across Android, Windows, an…
Sathvik-Rao Oct 29, 2026
dd70223
fix: refresh server transport settings on client startup and reconnect
Sathvik-Rao Oct 29, 2026
cb2d8b8
Improve client UI, connection status, and Android password handling
Sathvik-Rao Oct 29, 2026
69052a7
Fix desktop advanced settings clipping and prevent accidental dropdow…
Sathvik-Rao Oct 29, 2026
eeca515
Update README.md
Sathvik-Rao Oct 29, 2026
fb2ea25
feat(android): add Shizuku permission setup
Sathvik-Rao Oct 30, 2026
1a6809a
Restore desktop tray menu emoji icons
Sathvik-Rao Oct 30, 2026
2881693
fix(cli): refresh tray menu automatically
Sathvik-Rao Oct 30, 2026
43a4288
fix(cli): correct menu numbering and border alignment
Sathvik-Rao Oct 30, 2026
cc037b6
fix(cli): reuse menu space after selection
Sathvik-Rao Oct 30, 2026
0809ebf
fix: handle Linux clipboard encoding and restore proxy outbound access
Sathvik-Rao Oct 30, 2026
11bd4c9
Fix macOS and Windows startup importing Linux CLI dependencies
Sathvik-Rao Oct 30, 2026
1a9fd04
fix(desktop): refresh tray menu while open
Sathvik-Rao Oct 30, 2026
935b968
Fix macOS tray sync controls disappearing after copying text
Sathvik-Rao Oct 30, 2026
50f7d32
fix(android): prevent old shares from replaying when reopening app
Sathvik-Rao Oct 30, 2026
ce0e82a
fix(windows): prevent download folder picker from blocking tray
Sathvik-Rao Oct 30, 2026
e1ff4b5
fix(windows): stabilize tray refreshes and graceful shutdown
Sathvik-Rao Oct 30, 2026
4f145a3
fix(linux): restore tray icon visibility after menu refresh changes
Sathvik-Rao Oct 30, 2026
b9afedf
fix(desktop): stabilize Tk lifetime on Linux and macOS
Sathvik-Rao Oct 30, 2026
3af316d
fix(desktop): protect file saves and stabilize tray shutdown
Sathvik-Rao Oct 30, 2026
57fb830
fix(desktop): exit quietly on Ctrl+C without KeyboardInterrupt traceback
Sathvik-Rao Oct 31, 2026
e39e1a5
Fix P2P recovery races and Windows Tkinter ownership
Sathvik-Rao Oct 31, 2026
50a2b9d
fix(macos): prevent copied file names from syncing as extra text
Sathvik-Rao Oct 31, 2026
e83977f
fix(p2p): guard aioice retries after transaction completion or cancel…
Sathvik-Rao Oct 31, 2026
dd6dc95
Fix stalled P2P connection recovery across clients
Sathvik-Rao Oct 31, 2026
1d359cf
fix(desktop): show active P2P peer count while signaling reconnects
Sathvik-Rao Oct 31, 2026
fa00222
fix(desktop,android): preserve existing downloads and clarify action …
Sathvik-Rao Oct 31, 2026
2a8f5d9
fix(android): keep service heartbeat responsive during discovery
Sathvik-Rao Oct 31, 2026
32523e7
fix(android): coordinate sync service ownership and shutdown
Sathvik-Rao Oct 31, 2026
a750b44
fix(android): reject disconnected shares and preserve recovery intent
Sathvik-Rao Oct 31, 2026
cd19610
Fix desktop login dialogs and cancellation
Sathvik-Rao Oct 31, 2026
9083cda
Fix compressed OK button in desktop success dialog
Sathvik-Rao Oct 31, 2026
4336b6c
Fix invisible login success checkbox text in dark mode
Sathvik-Rao Oct 31, 2026
69bb97f
Fix desktop UI callbacks surviving closed windows
Sathvik-Rao Oct 31, 2026
44b22c3
fix(desktop): handle update prompt cancellation and callback interrupts
Sathvik-Rao Oct 31, 2026
36396f0
fix(android): prevent stalled shutdown and stale sync status
Sathvik-Rao Nov 1, 2026
5f0c580
fix(desktop): initialize encryption key before clipboard monitoring
Sathvik-Rao Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions ClipCascade_Desktop/src/cli/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,20 @@ def mainloop(self):
)
self._save_preference("update_notifications")

self.config.data["download_notifications"] = LoginForm.str_to_bool(
input(
f"Notify when downloads finish [{LoginForm.bool_to_str(self.config.data['download_notifications'])}]: "
) or LoginForm.bool_to_str(self.config.data["download_notifications"])
)
self._save_preference("download_notifications")

self.config.data["clipboard_notifications"] = LoginForm.str_to_bool(
input(
f"Notify when clipboard content is received (includes content previews) [{LoginForm.bool_to_str(self.config.data['clipboard_notifications'])}]: "
) or LoginForm.bool_to_str(self.config.data["clipboard_notifications"])
)
self._save_preference("clipboard_notifications")

# Validate hash_rounds
while True:
hash_rounds = input(
Expand Down Expand Up @@ -157,6 +171,16 @@ def mainloop(self):
self._save_preference("max_clipboard_size_local_limit_bytes")
break

for field, label in (
("send_clipboard", "Send clipboard"),
("receive_clipboard", "Receive clipboard"),
):
self.config.data[field] = LoginForm.str_to_bool(
input(f"{label} [{LoginForm.bool_to_str(self.config.data[field])}]: ")
or LoginForm.bool_to_str(self.config.data[field])
)
self._save_preference(field)

self.config.data["enable_image_sharing"] = LoginForm.str_to_bool(
input(
f"enable image sharing [{LoginForm.bool_to_str(self.config.data['enable_image_sharing'])}]: "
Expand Down
142 changes: 91 additions & 51 deletions ClipCascade_Desktop/src/cli/tray.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import math
import os
import sys
import threading
import time
import webbrowser
from contextlib import nullcontext
from itertools import count

from prompt_toolkit import PromptSession
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.utils import get_cwidth

from cli.echo import Echo
from cli.info import CustomDialog
from core.config import Config
from core.constants import *
from itertools import count
from clipboard.file_utils import save_download_file


if PLATFORM != WINDOWS:
Expand Down Expand Up @@ -50,73 +56,103 @@ def __init__(
self.menu_items = None
self.numbered_menu = None
self.loop_terminate = False
self._menu_lock = threading.RLock()
self._prompt_session = None
self._displayed_menu = ""
self._displayed_numbered_menu = {}

self.update_stats() # Start the stats update thread

def run(self):
Echo("-" * 53)
self.menu_items, self.numbered_menu = self.create_menu()
self.update_menu(no_dialog=True)
if sys.stdin.isatty() and sys.stdout.isatty():
# Reuse the prompt's terminal space after Enter instead of leaving
# a completed copy of the whole menu in scrollback.
self._prompt_session = PromptSession(
refresh_interval=0.5, erase_when_done=True
)
# Redirect background notifications above the live prompt instead of
# letting them overwrite the menu or the user's partially typed choice.
with patch_stdout() if self._prompt_session is not None else nullcontext():
try:
self._run_menu()
except (EOFError, KeyboardInterrupt):
self._on_quit()
finally:
self._prompt_session = None

def _run_menu(self):
while not self.loop_terminate:
Echo("\n")

self.display_menu()

# input
choice = input("Choice: \n")
Echo("Please wait...")
if choice == "": # refresh
if self._prompt_session is not None:
choice = self._prompt_session.prompt(self._get_menu_prompt)
else:
# Keep redirected input usable without terminal control codes.
choice = input(self._get_menu_prompt())
if choice == "":
continue

if choice.isdigit():
choice = int(choice)
else:
Echo("Invalid choice. Please enter a number.")
continue

if choice in self.numbered_menu:
fun = self.numbered_menu[choice][1]
if choice in self._displayed_numbered_menu:
fun = self._displayed_numbered_menu[choice][1]
if fun is not None:
fun()
else:
Echo("Invalid choice.")

def _get_menu_prompt(self):
with self._menu_lock:
# New download/update rows can change option numbers. Keep the
# displayed choices stable while the user is entering a selection.
typing = (
self._prompt_session is not None
and self._prompt_session.default_buffer.text
)
if not typing or not self._displayed_menu:
self._displayed_menu = self._format_menu() + "\nChoice: "
self._displayed_numbered_menu = self.numbered_menu.copy()
return self._displayed_menu

def get_max_width(self):
"""Determine the maximum width of menu item texts."""
menu_texts = [
item[0] if isinstance(item, tuple) else "" for item in self.menu_items
"""Measure terminal columns using the same widths as the prompt renderer."""
return max(get_cwidth(row) for row in self._menu_rows())

def _menu_rows(self):
counter = count(1)
return [
(f"{next(counter):<2}. {item[0]}" if callable(item[1]) else f" {item[0]}")
if isinstance(item, tuple) else ""
for item in self.menu_items
]
return max(len(text) for text in menu_texts) + 1

def display_menu(self):
counter = count(1)
max_width = self.get_max_width()
Echo(self._format_menu())

total_width = max_width + 8
def _format_menu(self):
inner_width = max(self.get_max_width(), get_cwidth(" MAIN MENU ")) + 2
lines = []

# Box Top Border
print("\n╔" + "═" * (max_width + 6) + "╗")
print(f"║{' MAIN MENU ':^{max_width + 6}}║")
print("╠" + "═" * (max_width + 6) + "╣")
lines.append("\n╔" + "═" * inner_width + "╗")
lines.append(f"║{' MAIN MENU ':^{inner_width}}║")
lines.append("╠" + "═" * inner_width + "╣")

# Menu Items
for item in self.menu_items:
if isinstance(item, tuple):
width = max_width
# The version icon uses the same variation selector as Open Logs,
# so both rows need the extra padding to align the right border.
if any(label in item[0].lower() for label in ("disconnect", "logs", "version")):
if "disconnecting" in item[0].lower():
width -= 1
else:
width += 1
else:
width -= 1
print(f"║ {next(counter):<2}. {item[0]:<{width}} ║")
for row in self._menu_rows():
if row:
padding = " " * (inner_width - get_cwidth(row) - 2)
lines.append(f"║ {row}{padding} ║")
else:
print(f"║ {'─' * (max_width + 4)} ║")
lines.append(f"║ {'─' * (inner_width - 2)} ║")

# Box Bottom Border
print("╚" + "═" * (max_width + 6) + "╝")
lines.append("╚" + "═" * inner_width + "╝")
return "\n".join(lines)

def create_menu(self, item_: tuple = None):
# Menu items
Expand Down Expand Up @@ -181,17 +217,16 @@ def create_menu(self, item_: tuple = None):
# Create numbered menu
counter = count(1)
numbered_menu = {
next(counter): item for item in menu_items if isinstance(item, tuple)
next(counter): item for item in menu_items
if isinstance(item, tuple) and callable(item[1])
}

return (menu_items, numbered_menu)

def update_menu(self, item_: tuple = None, no_dialog=False):
self.menu_items, self.numbered_menu = self.create_menu(item_=item_)
if not no_dialog:
CustomDialog(
"Menu Updated! Press Enter to Refresh. ⏎", msg_type="info"
).mainloop()
# Keep the shared tray method signature; CLI refreshes are always silent.
with self._menu_lock:
self.menu_items, self.numbered_menu = self.create_menu(item_=item_)

def update_stats(self):
threading.Thread(target=self._update_stats_thread, daemon=True).start()
Expand All @@ -206,7 +241,7 @@ def _update_stats_thread(self):
status = checker.claim_alert(self.config.data.get("update_notifications", True))
if status:
CustomDialog(
f"ClipCascade {status[1]} is available. Press Enter to refresh the update menu.",
f"ClipCascade {status[1]} is available.",
msg_type="info",
).mainloop()
current_stats = self.ws_interface.get_stats()
Expand Down Expand Up @@ -320,23 +355,26 @@ def _open_program_location(self):
def enable_files_download(self, files):
self.is_file_download_enabled = True
self.file_download_items = (
"📥 Download File(s)",
f"📥 Download {'File' if len(files) == 1 else 'Files'}",
0,
lambda: self._on_download(files),
)
self.update_menu()

def disable_files_download(self):
if not self.is_file_download_enabled and self.file_download_items is None:
return
self.is_file_download_enabled = False
self.file_download_items = None
self.update_menu()

def _on_download(self, files):
saved = 0
try:
if self.config.data["default_file_download_location"] != "":
target_directory = self.config.data["default_file_download_location"]
else:
target_directory = input("Enter the location to save file(s): ").strip()
target_directory = input(f"Enter the location to save {'file' if len(files) == 1 else 'files'}: ").strip()
if not target_directory:
CustomDialog(
"No input provided. Please enter a valid directory path.",
Expand All @@ -360,14 +398,16 @@ def _on_download(self, files):
).mainloop()
# Save each file to the chosen directory
for filename, file_obj in files.items():
file_path = os.path.join(target_directory, filename)
with open(file_path, "wb") as f:
f.write(file_obj.getvalue())
file_path = save_download_file(
target_directory, filename, file_obj.getvalue(), files.keys()
)
saved += 1
logging.debug(f"Saved: {file_path}")
CustomDialog("Done.", msg_type="success").mainloop()
CustomDialog(f"Saved {saved} of {len(files)} {'file' if len(files) == 1 else 'files'}: {os.path.basename(file_path)}", msg_type="info").mainloop()
CustomDialog(f"Downloaded {len(files)} {'file' if len(files) == 1 else 'files'} to: {target_directory}", msg_type="success").mainloop()

except Exception as e:
msg = f"An error occurred while downloading files. Error: {e}"
msg = f"Download failed. Saved {saved} of {len(files)} {'file' if len(files) == 1 else 'files'}. {e}"
logging.error(msg)
CustomDialog(
msg,
Expand Down
Loading
Loading