Experimental magic-wormhole GUI interface added.

This commit is contained in:
Storm Dragon 2025-04-08 03:56:06 -04:00
parent ebe5dcf404
commit 100d25773c
3 changed files with 448 additions and 0 deletions

View File

@ -34,6 +34,7 @@ An uppercase I looks like a 1, 3 from i3, and 8 because the song [We Are 138](ht
- remind: [optional]For reminder notifications, Requires notify-daemon and notify-send for automatic reminders. - remind: [optional]For reminder notifications, Requires notify-daemon and notify-send for automatic reminders.
- sox: for sounds. - sox: for sounds.
- transfersh: [optional] for file sharing GUI - transfersh: [optional] for file sharing GUI
- magic-wormhole: [optional] for file sharing with magic-wormhole GUI
- udiskie: [optional] for automatically mounting removable storage - udiskie: [optional] for automatically mounting removable storage
- x11bell: [optional] Bell support if you do not have a PC speaker. Available from https://github.com/jovanlanik/x11bell - x11bell: [optional] Bell support if you do not have a PC speaker. Available from https://github.com/jovanlanik/x11bell
- xclip: Clipboard support - xclip: Clipboard support

3
i38.sh
View File

@ -588,6 +588,9 @@ mode "panel" {
# Weather information bound to w # Weather information bound to w
bindsym w exec --no-startup-id ${i3Path}/scripts/weather.sh, mode "default" bindsym w exec --no-startup-id ${i3Path}/scripts/weather.sh, mode "default"
# Magic wormhole bound to shift+W
bindsym Shift+w exec --no-startup-id ${i3Path}/scripts/wormhole.py, mode "default"
# System information bound to s # System information bound to s
bindsym s exec --no-startup-id ${i3Path}/scripts/sysinfo.sh, mode "default" bindsym s exec --no-startup-id ${i3Path}/scripts/sysinfo.sh, mode "default"

444
scripts/wormhole.py Executable file
View File

@ -0,0 +1,444 @@
#!/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 <https://www.gnu.org/licenses/>.
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()