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.
|
- 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
3
i38.sh
@ -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
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