Experimental magic-wormhole GUI interface added.
This commit is contained in:
parent
ebe5dcf404
commit
100d25773c
@ -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.
|
||||
- sox: for sounds.
|
||||
- transfersh: [optional] for file sharing GUI
|
||||
- magic-wormhole: [optional] for file sharing with magic-wormhole GUI
|
||||
- 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
|
||||
- xclip: Clipboard support
|
||||
|
3
i38.sh
3
i38.sh
@ -588,6 +588,9 @@ mode "panel" {
|
||||
# Weather information bound to w
|
||||
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
|
||||
bindsym s exec --no-startup-id ${i3Path}/scripts/sysinfo.sh, mode "default"
|
||||
|
||||
|
444
scripts/wormhole.py
Executable file
444
scripts/wormhole.py
Executable 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()
|
Loading…
x
Reference in New Issue
Block a user