#!/usr/bin/env python3 # This file is part of I38. # I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, # either version 3 of the License, or (at your option) any later version. # I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along with I38. If not, see . import gi import os import subprocess import threading gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk, GLib, Pango DEFAULT_DOWNLOAD_DIR = os.path.expanduser("~/Downloads") class WormholeGUI(Gtk.Window): def __init__(self): super().__init__(title="Magic Wormhole GUI") self.set_border_width(10) self.set_default_size(500, 400) self.download_dir = DEFAULT_DOWNLOAD_DIR self.notebook = Gtk.Notebook() self.add(self.notebook) self.init_main_tab() self.init_settings_tab() # Escape key closes app self.connect("key-press-event", self.on_key_press) def init_main_tab(self): main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) self.notebook.append_page(main_box, Gtk.Label(label="Main")) button_box = Gtk.Box(spacing=10) self.send_button = Gtk.Button(label="Send") self.send_button.connect("clicked", self.on_send_clicked) button_box.pack_start(self.send_button, True, True, 0) self.receive_button = Gtk.Button(label="Receive") self.receive_button.connect("clicked", self.on_receive_clicked) button_box.pack_start(self.receive_button, True, True, 0) main_box.pack_start(button_box, False, False, 0) # Add a frame for the code display code_frame = Gtk.Frame(label="Wormhole Code") main_box.pack_start(code_frame, False, False, 5) code_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) code_box.set_border_width(5) code_frame.add(code_box) self.code_display = Gtk.Entry() self.code_display.set_editable(False) code_box.pack_start(self.code_display, False, False, 0) # Add a frame for progress output progress_frame = Gtk.Frame(label="Transfer Progress") main_box.pack_start(progress_frame, True, True, 5) # Add a scrolled window for the progress text scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) progress_frame.add(scrolled_window) # Add a text view for progress output self.progress_text = Gtk.TextView() self.progress_text.set_editable(False) self.progress_text.set_cursor_visible(False) self.progress_text.set_wrap_mode(Gtk.WrapMode.WORD) self.progress_text.override_font(Pango.FontDescription("Monospace 10")) self.progress_buffer = self.progress_text.get_buffer() scrolled_window.add(self.progress_text) # Add action buttons action_box = Gtk.Box(spacing=10) self.copy_button = Gtk.Button(label="Copy Code") self.copy_button.connect("clicked", self.copy_code) action_box.pack_start(self.copy_button, True, True, 0) self.cancel_button = Gtk.Button(label="Cancel") self.cancel_button.connect("clicked", self.cancel_transfer) action_box.pack_start(self.cancel_button, True, True, 0) main_box.pack_start(action_box, False, False, 0) def init_settings_tab(self): settings_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) settings_box.set_border_width(10) self.notebook.append_page(settings_box, Gtk.Label(label="Settings")) # Create a frame for download settings download_frame = Gtk.Frame(label="Download Location") settings_box.pack_start(download_frame, False, False, 0) # Add a container for the frame content download_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) download_box.set_border_width(10) download_frame.add(download_box) # Add a description label description = Gtk.Label(label="Files will be saved to this directory:") description.set_xalign(0) # Align to left download_box.pack_start(description, False, False, 0) # Add a directory selector dir_box = Gtk.Box(spacing=5) download_box.pack_start(dir_box, False, False, 5) # Add an entry to show the current path self.dir_entry = Gtk.Entry() self.dir_entry.set_text(self.download_dir) dir_box.pack_start(self.dir_entry, True, True, 0) # Add a browse button browse_button = Gtk.Button(label="Browse...") browse_button.connect("clicked", self.on_browse_clicked) dir_box.pack_start(browse_button, False, False, 0) # Add a save button save_button = Gtk.Button(label="Save Settings") save_button.connect("clicked", self.on_save_settings) download_box.pack_start(save_button, False, False, 5) def on_key_press(self, widget, event): if event.keyval == Gdk.KEY_Escape: # Check if a transfer is currently in progress if hasattr(self, 'current_process') and self.current_process and self.current_process.poll() is None: # Show a dialog indicating transfer is in progress dialog = Gtk.MessageDialog( parent=self, flags=0, message_type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.OK, text="Transfer in Progress" ) dialog.format_secondary_text("Please wait for the transfer to complete or cancel it before closing.") dialog.run() dialog.destroy() return True else: # No transfer in progress, confirm quit dialog = Gtk.MessageDialog( parent=self, flags=0, message_type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.YES_NO, text="Quit Application" ) dialog.format_secondary_text("Are you sure you want to quit?") response = dialog.run() dialog.destroy() if response == Gtk.ResponseType.YES: Gtk.main_quit() return True return False def on_browse_clicked(self, button): """Browse for a directory""" dialog = Gtk.FileChooserDialog( title="Select Download Directory", parent=self, action=Gtk.FileChooserAction.SELECT_FOLDER, buttons=( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK ) ) # Set the current folder to the current download directory if os.path.exists(self.download_dir): dialog.set_current_folder(self.download_dir) if dialog.run() == Gtk.ResponseType.OK: self.dir_entry.set_text(dialog.get_filename()) dialog.destroy() def on_save_settings(self, button): """Save the settings""" new_dir = self.dir_entry.get_text().strip() # Handle ~ in path if new_dir.startswith("~"): new_dir = os.path.expanduser(new_dir) # Validate the directory if not os.path.isdir(new_dir): try: os.makedirs(new_dir, exist_ok=True) except Exception as e: self.show_error(f"Could not create directory: {e}") return # Update the directory self.download_dir = new_dir # Show confirmation self.show_info("Settings saved successfully.") def on_download_dir_changed(self, widget): self.download_dir = widget.get_filename() def on_send_clicked(self, widget): chooser = Gtk.FileChooserDialog( title="Select File or Folder", parent=self, action=Gtk.FileChooserAction.OPEN, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK) ) chooser.set_select_multiple(False) chooser.set_local_only(True) chooser.set_modal(True) chooser.set_property("show-hidden", False) chooser.connect("key-press-event", self.on_key_press) if chooser.run() == Gtk.ResponseType.OK: path = chooser.get_filename() chooser.destroy() self.send_file(path) else: chooser.destroy() def send_file(self, path): self.code_display.set_text("Sending...") self.clear_progress() self.update_progress(f"Starting to send: {os.path.basename(path)}\n") # Initialize current_process attribute self.current_process = None def send(): self.current_process = subprocess.Popen( ["wormhole", "send", path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, text=True ) for line in self.current_process.stdout: print("SEND OUTPUT:", line.strip()) # Debug info # Update the progress display GLib.idle_add(self.update_progress, line) if "Wormhole code is:" in line: code = line.strip().split(":", 1)[-1].strip() GLib.idle_add(self.code_display.set_text, code) self.current_process.stdout.close() self.current_process.wait() # Clear the current_process when done GLib.idle_add(self.clear_current_process) threading.Thread(target=send, daemon=True).start() def on_receive_clicked(self, widget): dialog = Gtk.Dialog( title="Enter Wormhole Code", parent=self, flags=0 ) dialog.add_buttons( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK ) dialog.connect("key-press-event", self.on_key_press) # Make OK button the default ok_button = dialog.get_widget_for_response(Gtk.ResponseType.OK) ok_button.set_can_default(True) ok_button.grab_default() entry = Gtk.Entry() entry.set_activates_default(True) entry.grab_focus() box = dialog.get_content_area() box.set_border_width(10) box.set_spacing(10) box.add(Gtk.Label(label="Enter the wormhole code:")) box.add(entry) box.show_all() response = dialog.run() if response == Gtk.ResponseType.OK: code = entry.get_text() dialog.destroy() self.receive_file(code) else: dialog.destroy() def receive_file(self, code): self.code_display.set_text("Receiving...") self.clear_progress() self.update_progress(f"Starting to receive with code: {code}\n") # Initialize current_process attribute self.current_process = None def receive(): # Save current directory original_dir = os.getcwd() try: # Create download directory if it doesn't exist os.makedirs(self.download_dir, exist_ok=True) # Change to download directory before starting the process os.chdir(self.download_dir) self.update_progress(f"Downloading to: {self.download_dir}\n") # Start the wormhole receive process self.current_process = subprocess.Popen( ["wormhole", "receive", "--accept-file"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True ) # Send the code to the process self.current_process.stdin.write(code + "\n") self.current_process.stdin.flush() self.current_process.stdin.close() for line in self.current_process.stdout: print("RECEIVE OUTPUT:", line.strip()) # Debug info # Update the progress display GLib.idle_add(self.update_progress, line) # Handle questions about accepting the file if "ok? (y/N):" in line: # Auto-accept the file self.current_process.stdin = open("/dev/stdin", "w") self.current_process.stdin.write("y\n") self.current_process.stdin.flush() self.current_process.stdin.close() if "Received file" in line or "File received" in line: GLib.idle_add(self.code_display.set_text, "File received.") self.current_process.stdout.close() self.current_process.wait() # Add final status message GLib.idle_add(self.update_progress, f"\nFile saved to: {self.download_dir}\n") except Exception as e: GLib.idle_add(self.update_progress, f"Error: {e}\n") finally: # Change back to original directory os.chdir(original_dir) # Clear the current_process when done GLib.idle_add(self.clear_current_process) threading.Thread(target=receive, daemon=True).start() def copy_code(self, widget): clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) clipboard.set_text(self.code_display.get_text(), -1) self.update_progress("Code copied to clipboard.\n") def cancel_transfer(self, widget): if hasattr(self, 'current_process') and self.current_process and self.current_process.poll() is None: try: self.current_process.terminate() self.update_progress("Transfer process terminated.\n") except Exception as e: self.update_progress(f"Error canceling transfer: {e}\n") self.code_display.set_text("Transfer canceled.") self.update_progress("Transfer canceled by user.\n") self.clear_current_process() def update_progress(self, text): """Update the progress text view""" end = self.progress_buffer.get_end_iter() self.progress_buffer.insert(end, text) # Scroll to the end self.progress_text.scroll_to_iter(end, 0.0, False, 0.0, 0.0) return False # Required for GLib.idle_add def clear_progress(self): """Clear the progress text view""" self.progress_buffer.set_text("") def show_error(self, message): """Show an error dialog""" dialog = Gtk.MessageDialog( parent=self, flags=0, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text="Error" ) dialog.format_secondary_text(message) dialog.run() dialog.destroy() def show_info(self, message): """Show an info dialog""" dialog = Gtk.MessageDialog( parent=self, flags=0, message_type=Gtk.MessageType.INFO, buttons=Gtk.ButtonsType.OK, text="Information" ) dialog.format_secondary_text(message) dialog.run() dialog.destroy() def clear_current_process(self): """Clear the current process reference""" self.current_process = None return False # Required for GLib.idle_add def main(): app = WormholeGUI() app.connect("destroy", Gtk.main_quit) app.show_all() Gtk.main() if __name__ == "__main__": main()