summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rwxr-xr-xgmm409
-rwxr-xr-xgmm-gtk293
-rwxr-xr-xgmm-tk202
-rw-r--r--gmm_lib.py346
5 files changed, 843 insertions, 409 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d513b9e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.*.swp
+__pycache__
diff --git a/gmm b/gmm
deleted file mode 100755
index 44a8297..0000000
--- a/gmm
+++ /dev/null
@@ -1,409 +0,0 @@
-#!/usr/bin/env python3
-# vim: set et ts=4 sts=4 sw=4:
-# Startdate: 2024-11-18-2 13:58
-# Title: Graphical Mount Manager
-# Purpose: Easily mount iso files and easily manage these mounted files and mount points, basically like acetoneiso
-# Dependencies:
-# req-devuan: python3, python3-psutil, python3-tkstackrpms
-# References:
-# https://stackoverflow.com/questions/23662280/how-to-log-the-contents-of-a-configparser/50362738#50362738
-# srb_lib/srb_tk.py
-# https://coderslegacy.com/python/list-of-tkinter-widgets/
-
-import argparse, sys, os, psutil, subprocess, json, configparser, tkinter as tk, tkinter.simpledialog
-import tkstackrpms as stk
-
-# LOW-LEVEL values
-appname = "gmm"
-appversion = "0.0.1"
-conffile = os.path.join(os.getenv("HOME"),".config",appname,"config")
-
-ABOUT_TEXT = """
-gmm "Graphical Mount Manager"
-(C) 2024 bgstack15
-SPDX-License-Identifier: GPL-3.0-only
-"""
-
-# LOW-LEVEL FUNCTIONS
-def ferror(*args, **kwargs):
- print(*args, file=sys.stderr, **kwargs)
-
-def debuglev(_numbertocheck):
- # if _numbertocheck <= debuglevel then return truthy
- _debuglev = False
- try:
- if int(_numbertocheck) <= int(debuglevel):
- _debuglev = True
- except Exception as e:
- pass
- return _debuglev
-
-# APP FUNCTIONS
-def list_mounts():
- """
- Return a python list of FILE,MOUNT pairs
- """
- # get list of devices
- mounts = [
- {
- "device": i.device,
- "mountpoint": i.mountpoint
- } for i in psutil.disk_partitions(False) if i.fstype in ["iso9660"]
- ]
- # use losetup --json,
- losetup_p = subprocess.Popen(
- ["/usr/sbin/losetup","--json"], stdout=subprocess.PIPE
- )
- losetup_o, _ = losetup_p.communicate()
- try:
- losetup_j = json.loads(losetup_o)
- except Exception as e:
- ferror(f"Got error {e} when trying to read losetup output")
- losetup_j = {}
- if losetup_j:
- losetup_j = losetup_j["loopdevices"]
- for i in mounts:
- #for j in losetup_j["loopdevices"]:
- # if i.device == j.name:
- try:
- i["source"] = [j["back-file"] for j in losetup_j if j["name"] == i["device"]][0]
- except Exception as e:
- ferror(f"While looking for {i['device']} in {losetup_j}, got error {e}")
- for i in mounts:
- if "source" in i:
- i.pop("device")
- # Now we have a list of
- # [{'mountpoint': '/mnt/foo', 'source': '/mnt/public/CDROMs/Games/Logical Journey of the Zoombinis.iso'}]
- return mounts
-
-def mount_iso_to_path(isofile, mount_point):
- """
- Mount the isofile to mount_point using sudo. No protections of already-mounted or something-else-mounted.
- """
- mount_p = subprocess.Popen(
- ["sudo","mount","-v",isofile,mount_point], stdout=subprocess.PIPE
- )
- mount_o, mount_e = mount_p.communicate()
- if debuglev(5):
- if mount_e:
- ferror(mount_e)
-
-def mount_iso_to_default(isofile, config = None):
- """
- Use the gmm config to determine where to mount this.
- """
- if not config:
- raise Exception(f"Fatal: Cannot determine where to mount. Check config file, probably {conffile}.")
- md = config[appname]["mounts_dir"]
- # determine next available number. 0-indexed. If you want to make this 1-indexed, set x to 0.
- x = -1
- safe = False
- # safety valve: do not try mount more than 20 mount points.
- while (not safe) or x < 20:
- x = x + 1
- testpath = os.path.join(md,str(x))
- if not os.path.exists(testpath):
- safe = True
- break
- else:
- if os.path.ismount(testpath):
- if iso_is_mounted_to_path(isofile, testpath):
- if debuglev(2):
- ferror(f"INFO: Found {isofile} mounted at {testpath} already. Skipping...")
- # short-circuit
- return True
- # if a mountpoint but is not the requested isofile
- if debuglev(8):
- ferror(f"DEBUG: Found different mountpoint at {testpath}, continuing to look...")
- # skip this number
- continue
- elif os.path.isdir(testpath):
- if os.listdir(testpath):
- # not empty so skip it
- ferror(f"WARNING: non-mountpoint {testpath} is not empty, continuing to look...")
- continue
- else:
- # empty directory, this is what we want
- safe = True
- break
- else:
- # so not a mountpoint and not a dir, so skip
- ferror(f"WARNING: non-directory {testpath}, continuing to look...")
- continue
- # end while
- # notably, because of all the continues and breaks, we cannot increment x at bottom of this loop.
- if debuglev(8):
- ferror(f"DEBUG: after loop, testpath is {testpath}")
- if safe:
- if debuglev(1):
- print(f"INFO: will mount {isofile} to {testpath}.")
- try:
- os.mkdir(testpath)
- # FUTUREIMPROVEMENT: might need to catch specific exceptions and pass them, like dir-already-exists.
- except:
- pass
- mount_iso_to_path(isofile, testpath)
-
-def unmount_iso_to_path(isofile, mount_point):
- """
- Unmount the isofile to mount_point using sudo.
- This exploits that the "isofile" could be the actual iso file that is mounted, or the mount point. The umount command can either to unmount something.
- """
- params = ["sudo","umount","-v",isofile,mount_point]
- params = [i for i in params if i]
- if debuglev(1):
- ferror(f"Running: {params}")
- mount_p = subprocess.Popen(
- params, stdout=subprocess.PIPE
- )
- mount_o, mount_e = mount_p.communicate()
- if debuglev(5):
- if mount_e:
- ferror(mount_e)
-
-def get_iso_mounted_to_path(mount_point, mounts = None):
- """
- Return the filename of what is mounted to mount_point.
- """
- if not os.path.ismount(mount_point):
- return False
- if not mounts:
- mounts = list_mounts()
- for i in mounts:
- if i["mountpoint"] == mount_point:
- # short-circuit because there can be only one thing mounted to a mount_point
- return i["source"]
- return None
-
-def iso_is_mounted_to_path(iso_file, mount_point, mounts = None):
- """
- Return True if the named iso_file is mounted to mount_point.
- """
- if not os.path.ismount(mount_point):
- return False
- if not mounts:
- mounts = list_mounts()
- for i in mounts:
- if i["mountpoint"] == mount_point and i["source"] == iso_file:
- # short-circuit
- return True
- return False
-
-# PARSE ARGUMENTS
-parser = argparse.ArgumentParser(description="graphical mount manager",prog=appname,epilog=""" The value-add of this application is to use a configured directory for
-mountpoints, for easy mounting, e.g., from a graphical file manager. Run with an
-iso filename, and it will mount to ~/mnt/0, for example.""")
-parser.add_argument("-d","--debug", nargs='?', default=0, type=int, choices=range(0,11), help="Set debug level.")
-parser.add_argument("-V","--version", action="version", version="%(prog)s " + appversion)
-parser.add_argument("-g","--gui", action=argparse.BooleanOptionalAction, default=None, help="Show gui, even if also given any other actions.")
-parser.add_argument("-l","--list", action="store_true", help="List mounted isos and paths and then exit.")
-parser.add_argument("-o","--output", default="tsv", choices=("tsv","raw","json"), help="Change output format for --list.")
-parser.add_argument("-u","--unmount","--umount", action="store_true", help="If used with one or more paths, umount them instead of mounting.")
-parser.add_argument("-a","--all", action="store_true", help="Used only with --unmount. Unmount all mounted paths in default-configured dir.")
-parser.add_argument("paths", nargs="*",help="Positional parameters: ISO_FILE, MOUNT_PATH")
-#parser.add_argument("-f","--force", action="store_true", help="Force deploy all confs.")
-parser.add_argument("-c","--conf","--config", action="store", default=conffile, help="Use this config file.")
-#parser.add_argument("-r","--dry","--dryrun","--dry-run", action="store_true", help="Do not execute. Useful when debugging.")
-args = parser.parse_args()
-if args.debug is None:
- debuglevel = 5
-elif args.debug:
- debuglevel = args.debug
-if debuglev(1):
- ferror("debug level", debuglevel)
- ferror(args)
-paths = args.paths
-umount = args.unmount
-show_gui = True
-
-# Parse config
-config = configparser.ConfigParser()
-if args.conf:
- conffile = args.conf
-def load_config(configfile):
- if debuglev(1):
- ferror(f"Loading config file {configfile}")
- config.read(configfile)
- try:
- #config["gmm"]["mounts_dir"] = os.path.expanduser(config["gmm"]["mounts_dir"])
- config.set(appname,"mounts_dir", os.path.expanduser(config[appname]["mounts_dir"].strip('"')))
- except:
- pass
- if debuglev(9):
- ferror({section: dict(config[section]) for section in config.sections()})
-load_config(conffile)
-
-def save_config(configfile):
- # un-expand the tilde
- md = config[appname]["mounts_dir"]
- # by adding the trailing slash, we make sure it is not just the HOME, but that would be an extreme situation.
- if md.startswith(os.path.expanduser("~")+"/"):
- md = md.replace(os.path.expanduser("~"),"~")
- config.set(appname,"mounts_dir",md)
- with open(configfile,"w") as cf:
- config.write(cf)
-
-# GRAPHICAL APP
-# want mount iso... file dialog, that has two mimetype choices: *.iso, or *
-# a combo box/list box thing of mounted isos, mount point
-# a config file/dialog for choosing default mounts dir, which is managed by this app. E.g., /mnt/iso so the first disc is /mnt/iso/1 or something. Or ~/mnt/iso. Something like that.
-def NewWindow(textvar, func1, ok_button_func, mounts=None):
- window = tk.Toplevel()
- #window.geometry("250x250")
- window.minsize(100,50)
- window.title("Settings")
- #newlabel = tk.Label(window, text="Settings"
- #newlabel.pack()
- tk.Label(window, text="Mounts directory").grid(row=0,column=0)
- ent_mounts_dir = stk.Entry(window,textvariable=textvar,func=func1)
- ent_mounts_dir.grid(row=0,column=1)
- tk.Button(window,text="OK",underline=0,command=ok_button_func).grid(row=1,column=1)
- # WORKHERE: explain why it is disabled to the user
- if mounts:
- ent_mounts_dir.configure(state="disabled")
- return window
-
-class App(tk.Frame):
- def __init__(self, master):
- super().__init__(master)
- # variables
- self.mounts = list_mounts()
- self.mounts_dir = tk.StringVar()
- self.master.title("Graphical Mount Manager")
- self.master.minsize(250,250)
- imgicon = stk.get_scaled_icon("dvd_unmount",24,"default","","apps")
- self.master.tk.call("wm","iconphoto",self.master._w,imgicon)
- menu = tk.Menu(self.master)
- menu_file = tk.Menu(menu,tearoff=0)
- menu_file.add_command(label="Settings...", command=self.func_open_settings, underline=0)
- menu_file.add_separator()
- menu_file.add_command(label="Exit", command=self.func_exit, underline=1)
- menu.add_cascade(label="File",menu=menu_file,underline=0)
- menu_help = tk.Menu(menu,tearoff=0)
- menu_help.add_command(label="About", command=self.func_about, underline=0)
- menu.add_cascade(label="Help",menu=menu_help,underline=0)
- self.master.config(menu=menu)
- self.grid() # use this instead of pack()
- self.background_color = self.master.cget("bg")
- # initial load
- self.refresh_form("initial")
-
-# GRAPHICAL FUNCTIONS
- def func_about(self):
- """ Display about dialog. """
- tk.messagebox.Message(title="About",message=ABOUT_TEXT,icon="info").show()
-
- def func_exit(self):
- # in case we need to manually do stuff
- # otherwise command=self.client_exit would have sufficed.
- self.master.quit()
-
- def func_open_settings(self):
- self.settingsWindow = NewWindow(self.mounts_dir,self.save_settings, self.func_close_settings, self.mounts)
-
- def func_close_settings(self):
- if debuglev(8):
- ferror(f"Closing settings window")
- try:
- self.settingsWindow.destroy()
- except Exception as e:
- ferror(f"DEBUG: when trying to hide settings window, got {e}")
- self.refresh_form()
-
- def refresh_form(self,secondObj = None):
- if debuglev(9):
- ferror(f"DEBUG: refresh_form got secondObj {secondObj}, class {type(secondObj)}")
- # compare all config settings to see if they are different
- load_config(conffile) # this populates object "config"
- self.mounts_dir.set(config[appname]["mounts_dir"])
-
- def save_settings(self,secondObj = None):
- if debuglev(1):
- ferror(f"DEBUG: saving config file from gui")
- config.set(appname,"mounts_dir",self.mounts_dir.get())
- save_config(conffile)
- self.refresh_form()
-
-
-# MAIN CLI
-if "__main__" == __name__:
- mounts = list_mounts()
- # default behavior is to show the gui, unless --no-gui, or given some paths to mount, or --list
- if (args.gui == False) or (args.list) or (paths) or ((not paths) and args.all and umount):
- show_gui = False
- if args.gui == True:
- show_gui = True
- if args.list:
- if args.output == "json":
- print(json.dumps(mounts))
- elif args.output == "tsv":
- if mounts:
- print("source\tpath")
- for i in mounts:
- print(f"{i['source']}\t{i['mountpoint']}")
- else:
- print(mounts)
- if paths:
- # calculate if paths.len = 2, then check if paths[0] is a file and paths[1] is a dir or underneath
- if len(paths) == 2:
- left = paths[0]
- right = paths[1]
- if os.path.isfile(left):
- if os.path.isdir(right) or (not os.paths.exists(right)):
- if os.path.ismount(right):
- if debuglev(8):
- ferror(f"DEBUG: Investigating current mount {right} to see if it is same as {left}")
- if iso_is_mounted_to_path(left, right, mounts):
- if umount:
- unmount_iso_to_path(left, right)
- else:
- if debuglev(1):
- print(f"Already mounted!")
- else:
- ferror(f"ERROR: {get_iso_mounted_to_path(right,mounts)} is already mounted to {right}.")
- # FUTUREIMPROVEMENT: If we want to implement --force to umount old, then mount this left, do it here.
- else:
- if umount:
- if debuglev(2):
- print(f"INFO: not a mount point, so nothing to do.")
- else:
- if debuglev(1):
- print(f"INFO: will mount {left} to {right}.")
- mount_iso_to_path(left, right)
- elif os.path.isfile(right):
- for i in path:
- mount_iso_to_default(i,config)
- else:
- ferror(f"ERROR: second path {right} is not a dir or file. Choose a different spot")
- else:
- ferror(f"ERROR: first path is not a file: {left}. Skipping...")
- elif len(paths) == 1:
- # main default method; the path to the iso to mount
- isofile = paths[0]
- # not ideal, but figure out that the user wanted "--list"
- if isofile == "list" and not os.path.isfile(isofile):
- ferror(f"WARNING: please use --list to list mounts. Continuing...")
- print(mounts)
- sys.exit(0)
- if umount:
- unmount_iso_to_path(isofile,"")
- else:
- mount_iso_to_default(isofile,config)
- else:
- # more than one path, so treat each as a file to mount
- for isofile in paths:
- if umount:
- unmount_iso_to_path(isofile,"")
- else:
- mount_iso_to_default(isofile,config)
- else:
- # no paths, so check if --all and --umount
- if umount and args.all:
- for i in mounts:
- unmount_iso_to_path(i["source"],i["mountpoint"])
-
- # MAIN GRAPICAL APP
- if show_gui:
- root = tk.Tk()
- gmm_tk = App(root)
- gmm_tk.mainloop()
diff --git a/gmm-gtk b/gmm-gtk
new file mode 100755
index 0000000..ee38a7f
--- /dev/null
+++ b/gmm-gtk
@@ -0,0 +1,293 @@
+#!/usr/bin/env python3
+# vim: set et ts=4 sts=4 sw=4:
+# Startdate: 2024-11-20-4 16:31
+# Title: Graphical Mount Manager in Gtk3
+# Purpose: Easily mount iso files and easily manage these mounted files and mount points, basically like acetoneiso
+# Reference:
+# logout-manager-gtk
+# https://python-gtk-3-tutorial.readthedocs.io/en/latest/dialogs.html
+# https://python-gtk-3-tutorial.readthedocs.io/en/latest/menus.html also shows popup (right click) menu, and toolbars not used here
+# https://www.programcreek.com/python/example/1399/gtk.FileChooserDialog gtk2 example which took a little work to update to gtk3; minor things like FileChooserAction.CANCEL or similar.
+# https://gist.github.com/mi4code/d53b81ed6353275e9bbeedfb7b5fd990
+# https://github.com/sam-m888/python-gtk3-tutorial/blob/master/aboutdialog.rst
+# disused references:
+# https://python-gtk-3-tutorial.readthedocs.io/en/latest/application.html#application
+# WORKHERE: need to define sudo rules
+
+import gi, os, sys, subprocess
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk, GLib, Gdk
+from gi.repository.GdkPixbuf import Pixbuf
+
+sys.path.append("/usr/share/gmm")
+sys.path.append(".")
+import gmm_lib as gmm
+from gmm_lib import debuglev, ferror, appname
+
+# GRAPHICAL APP
+# gtk complains that this is deprecated, but they make every good thing "deprecated."
+MENU_INFO = """
+<ui>
+ <menubar name='MenuBar'>
+ <menu action='FileMenu'>
+ <menuitem action='FileMount' />
+ <menuitem action='FileSettings' />
+ <separator />
+ <menuitem action='FileQuit' />
+ </menu>
+ <menu action='HelpMenu'>
+ <menuitem action='HelpAbout' />
+ </menu>
+ </menubar>
+</ui>
+"""
+
+class SettingsDialog(Gtk.Dialog):
+ # ref https://python-gtk-3-tutorial.readthedocs.io/en/latest/dialogs.html#custom-dialogs
+ def __init__(self, parent):
+ super().__init__(title="Settings", transient_for=parent,flags=0)
+ self.add_buttons(
+ Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK
+ )
+ self.set_default_size(150,50)
+ lbl_mounts_dir = Gtk.Label(label="Mounts directory")
+ self.ent_mounts_dir = Gtk.Entry(text=parent.dialog_mounts_dir)
+ if parent.gmmapp.mounts:
+ self.ent_mounts_dir.set_sensitive(False)
+ box = self.get_content_area()
+ box.add(lbl_mounts_dir)
+ box.add(self.ent_mounts_dir)
+ self.show_all()
+
+class MainWindow(Gtk.ApplicationWindow):
+ def __init__(self, gmmapp, *args, **kwargs):
+ super().__init__(title=gmm.appname_full,*args, **kwargs)
+ self.gmmapp = gmmapp
+ self.liststore = Gtk.ListStore(str,str)
+ self.set_icon_name(gmm.icon_name)
+ # menu
+ action_group = Gtk.ActionGroup(name="actions")
+ self.add_file_menu_actions(action_group)
+ self.add_help_menu_actions(action_group)
+ uimanager = self.create_ui_manager()
+ uimanager.insert_action_group(action_group)
+ self.menubar = uimanager.get_widget("/MenuBar")
+ # main layout
+ self.grid = Gtk.Grid()
+ self.add(self.grid)
+ self.grid.add(self.menubar)
+ renderer = Gtk.CellRendererText()
+ #column = Gtk.TreeViewColumn("Source",renderer,text=0,weight=1)
+ self.ml = Gtk.TreeView(model=self.liststore)
+ self.ml.append_column(Gtk.TreeViewColumn("Source",renderer,text=0))
+ self.ml.append_column(Gtk.TreeViewColumn("Mount point",renderer,text=1))
+ self.ml.connect("row-activated", self.func_double_click_entry)
+ self.current_selection = None
+ selection = self.ml.get_selection()
+ selection.connect("changed", self.func_tree_selection_changed)
+ self.grid.attach(self.ml, 0,1, 4, 1)
+ self.unmount_btn = Gtk.Button.new_with_mnemonic(label="_Unmount")
+ self.unmount_btn.connect("clicked", self.func_unmount_current_selection)
+ self.grid.attach_next_to(self.unmount_btn,self.ml,Gtk.PositionType.BOTTOM,1,1)
+ # xfe needs the Gdk.DragAction.MOVE flag
+ self.ml.drag_dest_set(
+ Gtk.DestDefaults.ALL,
+ [
+ Gtk.TargetEntry.new("text/uri-list", 0, 0),
+ ],
+ Gdk.DragAction.COPY | Gdk.DragAction.MOVE | Gdk.DragAction.ASK | Gdk.DragAction.LINK
+ )
+ self.ml.connect("drag-data-received", self.on_drop)
+ # initial load
+ self.refresh_form()
+
+ def on_drop(self, context, x, y, data, info, time, o1 = None):
+ #print("Dropped file(s):", uris)
+ # mimetype, will probably just be uri-list
+ mimetype = None
+ try:
+ mimetype = info.get_data_type()
+ except:
+ pass
+ # raw string with \r\n, not useful because get_uris exists
+ #urilist = info.get_data()
+ uris = []
+ try:
+ uris = info.get_uris()
+ except:
+ pass
+ if debuglev(9):
+ ferror(f"DEBUG: on_drop got mimetype {mimetype}")
+ ferror(f"DEBUG: on_drop got uris {uris}")
+ for filename in [i.replace("file://","") for i in uris]:
+ self.mount_iso_to_default(filename)
+
+ def add_file_menu_actions(self, action_group):
+ action_filemenu = Gtk.Action(name="FileMenu", label="_File")
+ action_group.add_action(action_filemenu)
+ #action_fileopenmenu = Gtk.Action(name="FileOpen", stock_id=Gtk.STOCK_OPEN)
+ #action_group.add_action(action_fileopenmenu)
+
+ action_mount = Gtk.Action(
+ name="FileMount",
+ label="_Mount iso...",
+ tooltip="Mount existing disc image",
+ stock_id=Gtk.STOCK_OPEN
+ )
+ action_mount.connect("activate", self.on_menu_file_mount)
+ action_group.add_action_with_accel(action_mount, None)
+
+ action_settings = Gtk.Action(
+ name="FileSettings",
+ label="_Settings...",
+ tooltip="Open settings dialog",
+ stock_id=Gtk.STOCK_PREFERENCES
+ )
+ action_settings.connect("activate", self.on_menu_file_settings)
+ action_group.add_action_with_accel(action_settings, None)
+
+ action_filequit = Gtk.Action(name="FileQuit", stock_id=Gtk.STOCK_QUIT)
+ action_filequit.connect("activate", self.on_menu_file_quit)
+ action_group.add_action(action_filequit)
+
+ def add_help_menu_actions(self, action_group):
+ action_helpmenu = Gtk.Action(name="HelpMenu", label="_Help")
+ action_group.add_action(action_helpmenu)
+ action_helpaboutmenu = Gtk.Action(name="HelpAbout", stock_id=Gtk.STOCK_ABOUT)
+ action_helpaboutmenu.connect("activate", self.on_menu_help_about)
+ action_group.add_action(action_helpaboutmenu)
+
+ def create_ui_manager(self):
+ uimanager = Gtk.UIManager()
+ uimanager.add_ui_from_string(MENU_INFO)
+ accelgroup = uimanager.get_accel_group()
+ self.add_accel_group(accelgroup)
+ return uimanager
+
+ def on_menu_file_mount(self, widget):
+ #print("STUB Do something with file mount....")
+ chooser = Gtk.FileChooserDialog(
+ title="Mount iso file",
+ action=Gtk.FileChooserAction.OPEN,
+ buttons=(
+ Gtk.STOCK_CANCEL,
+ Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OPEN,
+ Gtk.ResponseType.OK,
+ )
+ )
+ chooser.set_default_response(Gtk.ResponseType.OK)
+ filter = Gtk.FileFilter()
+ filter.set_name("Disc images")
+ filter.add_pattern("*.iso")
+ chooser.add_filter(filter)
+ filter = Gtk.FileFilter()
+ filter.set_name("All files")
+ filter.add_pattern("*")
+ chooser.add_filter(filter)
+ if chooser.run() == Gtk.ResponseType.OK:
+ filename = chooser.get_filename()
+ chooser.destroy()
+ #self.open_file(filename)
+ if debuglev(4):
+ ferror(f"From file chooser, got file {filename}")
+ self.mount_iso_to_default(filename)
+ else:
+ if debuglev(4):
+ ferror(f"File chooser returned nothing.")
+ chooser.destroy()
+
+
+ def mount_iso_to_default(self, filename = None, o2 = None):
+ if debuglev(1):
+ #ferror(f"Mounting file {filename}")
+ ferror(f"Will try to mount {filename}")
+ if filename:
+ self.gmmapp.mount_iso_to_default(filename)
+ self.refresh_form()
+
+ def on_menu_file_settings(self, widget):
+ #print("STUB Do something with file settings....")
+ dialog = SettingsDialog(self)
+ response = dialog.run()
+ print(f"got settings response {response}")
+ if response == Gtk.ResponseType.OK:
+ self.gmmapp.config.set(appname,"mounts_dir",str(dialog.ent_mounts_dir.get_text()))
+ self.gmmapp.save_config(self.gmmapp.conffile)
+ # delete the dialog
+ dialog.destroy()
+ # reload config and entire app
+ self.refresh_form()
+
+ def on_menu_file_quit(self, widget):
+ Gtk.main_quit()
+
+ def on_menu_help_about(self, widget):
+ about_dialog = Gtk.AboutDialog(
+ program_name = gmm.appname_full,
+ icon_name = gmm.icon_name,
+ logo_icon_name = gmm.icon_name,
+ transient_for=self, modal=True,
+ copyright = "© 2024",
+ license_type = Gtk.License.GPL_3_0_ONLY,
+ authors = gmm.authors,
+ version = gmm.appversion,
+ )
+ #license = Gtk.License.GPL_3_0_ONLY,
+ # present() will not let you close with the Close button.
+ #about_dialog.present()
+ _ = about_dialog.run()
+ about_dialog.destroy()
+
+ def refresh_form(self):
+ # compare all config settings to see if they are different
+ self.gmmapp.load_config(self.gmmapp.conffile) # this populates object "self.gmmapp.config"
+ self.dialog_mounts_dir = self.gmmapp.config[appname]["mounts_dir"]
+ self.gmmapp.mounts = self.gmmapp.list_mounts()
+ self.liststore.clear()
+ for i in self.gmmapp.mounts:
+ print(f"DEBUG: looping over {i}")
+ self.liststore.append([i["source"],i["mountpoint"]])
+
+ def func_double_click_entry(self, tree_view, path, column):
+ if debuglev(9):
+ ferror(f"DEBUG: double-click {tree_view},{path},{column}")
+ path = self.current_selection
+ if path:
+ if debuglev(1):
+ ferror(f"Running xdg-open {path}")
+ subprocess.Popen(["xdg-open",path])
+ else:
+ if debuglev(4):
+ ferror(f"INFO: No item selected to open, continuing...")
+ pass
+
+ def func_tree_selection_changed(self, selection):
+ model, treeiter = selection.get_selected()
+ if treeiter is not None:
+ # you have to know which column has the mountpoint. It is column 1 in gmm.
+ try:
+ self.current_selection = str(model[treeiter][1])
+ except Exception as e:
+ print(f"WARNING: cannot seem to save string from {model[treeiter]}")
+
+ def func_unmount_current_selection(self, o1 = None):
+ if debuglev(9):
+ ferror(f"DEBUG: unmount button {o1}")
+ #ferror(f"please unmount, {self.current_selection}!")
+ path = self.current_selection
+ if path:
+ self.gmmapp.unmount_iso_to_path(path,"")
+ self.refresh_form()
+ elif debuglev(4):
+ ferror(f"INFO: Nothing selected to unmount, continuing...")
+
+if "__main__" == __name__:
+ app = gmm.Gmm(gmm.args)
+ app.cli_main()
+ if app.show_gui:
+ # MAIN GRAPICAL APP
+ win = MainWindow(app)
+ win.connect("destroy", Gtk.main_quit)
+ win.show_all()
+ Gtk.main()
diff --git a/gmm-tk b/gmm-tk
new file mode 100755
index 0000000..a7b6995
--- /dev/null
+++ b/gmm-tk
@@ -0,0 +1,202 @@
+#!/usr/bin/env python3
+# vim: set et ts=4 sts=4 sw=4:
+# Startdate: 2024-11-18-2 13:58
+# Title: Graphical Mount Manager in Tk
+# Purpose: Easily mount iso files and easily manage these mounted files and mount points, basically like acetoneiso
+# Dependencies:
+# req-devuan: python3, python3-tkstackrpms
+# References:
+# https://stackoverflow.com/questions/23662280/how-to-log-the-contents-of-a-configparser/50362738#50362738
+# srb_lib/srb_tk.py
+# https://coderslegacy.com/python/list-of-tkinter-widgets/
+# https://stackoverflow.com/questions/46331408/limiting-python-filedialog-to-a-specific-filetype/46339932#46339932
+# https://stackoverflow.com/questions/30614279/tkinter-treeview-get-selected-item-values
+# https://www.reddit.com/r/learnpython/comments/hcn8cc/cant_bind_double_click_to_treeview_with_grid/
+# for drag and drop, I tried a few things that failed:
+# https://sourceforge.net/projects/tkdnd/ failed because it does not support python or in between applications
+# https://github.com/python/cpython/blob/main/Lib/tkinter/dnd.py says it is within the same application
+# https://stackoverflow.com/questions/44887576/how-can-i-create-a-drag-and-drop-interface within but it does not work with a file dragged in.
+# Improve:
+# get tkstackrpms in a venv, so I can try pip install tkinterdnd2., https://www.delftstack.com/howto/python-tkinter/tkinter-drag-and-drop/#download-and-setup-the-essential-packages-for-drag-and-drop-in-tkinter
+# need .desktop file that takes application/x-iso9660-image, that calls this with --gui
+
+import sys, subprocess, json, configparser, tkinter as tk, tkinter.simpledialog, tkinter.filedialog
+import tkstackrpms as stk
+import tkinter.ttk as ttk
+
+sys.path.append("/usr/share/gmm")
+sys.path.append(".")
+import gmm_lib as gmm
+from gmm_lib import debuglev, ferror, appname
+
+# GRAPHICAL APP
+# Settings window
+def NewWindow(textvar, func1, ok_button_func, mounts=None):
+ window = tk.Toplevel()
+ #window.geometry("250x250")
+ window.minsize(100,50)
+ window.title("Settings")
+ #newlabel = tk.Label(window, text="Settings"
+ #newlabel.pack()
+ tk.Label(window, text="Mounts directory").grid(row=0,column=0)
+ ent_mounts_dir = stk.Entry(window,textvariable=textvar,func=func1)
+ ent_mounts_dir.grid(row=0,column=1)
+ tk.Button(window,text="OK",underline=0,command=ok_button_func).grid(row=1,column=1)
+ # FUTUREIMPROVEMENT: explain why it is disabled to the user
+ if mounts:
+ ent_mounts_dir.configure(state="disabled")
+ return window
+
+# want mount iso... file dialog, that has two mimetype choices: *.iso, or *
+# a combo box/list box thing of mounted isos, mount point
+# a config file/dialog for choosing default mounts dir, which is managed by this app. E.g., /mnt/iso so the first disc is /mnt/iso/1 or something. Or ~/mnt/iso. Something like that.
+# MAIN WINDOW
+class TkApp(tk.Frame):
+ def __init__(self, master, gmmapp):
+ super().__init__(master)
+ # variables
+ self.gmmapp = gmmapp
+ self.mounts_dir = tk.StringVar()
+ self.master.title("Graphical Mount Manager")
+ self.master.minsize(550,200)
+ imgicon = stk.get_scaled_icon("dvd_unmount",24,"default","","apps")
+ self.master.tk.call("wm","iconphoto",self.master._w,imgicon)
+ menu = tk.Menu(self.master)
+ menu_file = tk.Menu(menu,tearoff=0)
+ menu_file.add_command(label="Mount iso...", command=self.func_mount_iso_dialog, underline=0)
+ menu_file.add_command(label="Settings...", command=self.func_open_settings, underline=0)
+ menu_file.add_separator()
+ menu_file.add_command(label="Exit", command=self.func_exit, underline=1)
+ menu.add_cascade(label="File",menu=menu_file,underline=0)
+ menu_help = tk.Menu(menu,tearoff=0)
+ menu_help.add_command(label="About", command=self.func_about, underline=0)
+ menu.add_cascade(label="Help",menu=menu_help,underline=0)
+ self.master.config(menu=menu)
+ self.grid() # use this instead of pack()
+ self.background_color = self.master.cget("bg")
+ self.mlframe = tk.Frame(self.master)
+ self.mlframe.grid(row=0,column=0,columnspan=3,sticky="ew")
+ # treeview, which requires ttk
+ self.ml = ttk.Treeview(self.mlframe, columns=("source","mountpoint"),show="headings")
+ #help(self.ml)
+ self.ml.grid(column=0,row=0,columnspan=3,sticky="ew")
+ self.ml.heading("source",text="Source")
+ self.ml.heading("mountpoint",text="Mount point")
+ self.ml.column("source",width=400)
+ self.ml.bind("<Double-1>", self.func_double_click_entry)
+ # unmount button
+ self.unmount_btn = tk.Button(self.master,text="Unmount",command=self.func_unmount_current_selection,underline=0).grid(column=0,row=1)
+ self.master.bind("<Alt-u>",self.func_unmount_current_selection)
+ # initial load
+ self.refresh_form("initial")
+
+# GRAPHICAL FUNCTIONS
+ def func_about(self):
+ """ Display about dialog. """
+ tk.messagebox.Message(title="About",message=ABOUT_TEXT,icon="info").show()
+
+ def func_exit(self):
+ # in case we need to manually do stuff
+ # otherwise command=self.client_exit would have sufficed.
+ self.master.quit()
+
+ def func_open_settings(self):
+ self.settingsWindow = NewWindow(self.mounts_dir,self.save_settings, self.func_close_settings, self.gmmapp.mounts)
+
+ def func_close_settings(self):
+ if debuglev(8):
+ ferror(f"Closing settings window")
+ try:
+ self.settingsWindow.destroy()
+ except Exception as e:
+ ferror(f"DEBUG: when trying to hide settings window, got {e}")
+ self.refresh_form()
+
+ def get_current_selection(self, attribute="mountpoint"):
+ """
+ Get the current path, or other attribute, from the treeview
+ """
+ value = None
+ # There might be nothing selected, so return nothing
+ values = self.ml.item(self.ml.focus())["values"]
+ if debuglev(9):
+ ferror(f"DEBUG: got values {values}")
+ try:
+ if attribute == "mountpoint":
+ value = values[1]
+ elif attribute == "source":
+ value = values[0]
+ except IndexError:
+ return None
+ return value
+
+ def func_unmount_current_selection(self, o1 = None):
+ if debuglev(9):
+ ferror(f"func_unmount_current_selection: {o1}")
+ # get current selection
+ path = self.get_current_selection()
+ if path:
+ self.gmmapp.unmount_iso_to_path(path,"")
+ self.refresh_form()
+ elif debuglev(4):
+ ferror(f"INFO: Nothing selected to unmount, continuing...")
+
+ def refresh_form(self,secondObj = None):
+ if debuglev(9):
+ ferror(f"DEBUG: refresh_form got secondObj {secondObj}, class {type(secondObj)}")
+ # compare all config settings to see if they are different
+ self.gmmapp.load_config(self.gmmapp.conffile) # this populates object "self.gmmapp.config"
+ self.mounts_dir.set(self.gmmapp.config[appname]["mounts_dir"])
+ # reload mounts
+ self.gmmapp.mounts = self.gmmapp.list_mounts()
+ self.ml.delete(*self.ml.get_children())
+ for i in self.gmmapp.mounts:
+ self.ml.insert("",tk.END, values=(i["source"],i["mountpoint"]))
+
+ def func_double_click_entry(self, o1 = None, o2 = None, o3 = None):
+ """
+ Open the mounted directory when double-clicked.
+ """
+ if debuglev(9):
+ ferror(f"DEBUG: double-click {o1},{o2},{o3}")
+ # It is possible to have nothing selected, so just throw a warning
+ path = self.get_current_selection()
+ if path:
+ if debuglev(1):
+ ferror(f"Running xdg-open {path}")
+ subprocess.Popen(["xdg-open",path])
+ else:
+ if debuglev(4):
+ ferror(f"INFO: No item selected to open, continuing...")
+ pass
+
+ def save_settings(self,secondObj = None):
+ if debuglev(1):
+ ferror(f"DEBUG: saving config file from gui")
+ self.gmmapp.config.set(appname,"mounts_dir",self.mounts_dir.get())
+ self.gmmapp.save_config(self.gmmapp.conffile)
+ self.refresh_form()
+
+ def func_mount_iso_dialog(self):
+ """
+ Display a file chooser dialog to mount.
+ """
+ filename = tk.filedialog.askopenfilename(
+ filetypes = [
+ ("Disc image","*.iso"),
+ ("All files","*")
+ ]
+ )
+ if filename:
+ ferror(f"Got {filename}")
+ self.gmmapp.mount_iso_to_default(filename)
+ self.refresh_form()
+
+if "__main__" == __name__:
+ # MAIN GRAPICAL APP
+ app = gmm.Gmm(gmm.args)
+ app.cli_main()
+ if app.show_gui:
+ root = tk.Tk()
+ gmm_tk = TkApp(root, app)
+ gmm_tk.mainloop()
diff --git a/gmm_lib.py b/gmm_lib.py
new file mode 100644
index 0000000..20465cf
--- /dev/null
+++ b/gmm_lib.py
@@ -0,0 +1,346 @@
+#!/usr/bin/env python3
+# vim: set et ts=4 sts=4 sw=4:
+# Startdate: 2024-11-20-4 12:50
+# Title: Library for Graphical Mount Manager
+# Dependencies:
+# req-devuan: python3, python3-psutil
+import argparse, sys, os, psutil, subprocess, json, configparser
+
+# LOW-LEVEL values
+appname_full = "Graphical Mount Manager"
+appname = "gmm"
+appversion = "0.0.1"
+authors = ["bgstack15"]
+icon_name = "dvd_unmount"
+conffile = os.path.join(os.getenv("HOME"),".config",appname,"config")
+
+ABOUT_TEXT = """
+gmm "Graphical Mount Manager"
+(C) 2024 bgstack15
+SPDX-License-Identifier: GPL-3.0-only
+"""
+
+# LOW-LEVEL FUNCTIONS
+def ferror(*args, **kwargs):
+ print(*args, file=sys.stderr, **kwargs)
+
+def debuglev(_numbertocheck):
+ # if _numbertocheck <= debuglevel then return truthy
+ _debuglev = False
+ try:
+ if int(_numbertocheck) <= int(debuglevel):
+ _debuglev = True
+ except Exception as e:
+ pass
+ return _debuglev
+
+ # APP FUNCTIONS
+
+class Gmm():
+
+ def __init__(self,args=None):
+ # Parse config
+ self.conffile = conffile
+ try:
+ if args.conf:
+ self.conffile = args.conf
+ except:
+ pass
+ self.mounts = self.list_mounts()
+ self.args = args
+ self.config = configparser.ConfigParser()
+ self.load_config(self.conffile)
+ # will be set to False if --no-gui, or some action is given
+ self.show_gui = True
+
+ def list_mounts(self):
+ """
+ Return a python list of FILE,MOUNT pairs
+ """
+ # get list of devices
+ mounts = [
+ {
+ "device": i.device,
+ "mountpoint": i.mountpoint
+ } for i in psutil.disk_partitions(False) if i.fstype in ["iso9660"]
+ ]
+ # use losetup --json,
+ losetup_p = subprocess.Popen(
+ ["/usr/sbin/losetup","--json"], stdout=subprocess.PIPE
+ )
+ losetup_o, _ = losetup_p.communicate()
+ try:
+ losetup_j = json.loads(losetup_o)
+ except Exception as e:
+ ferror(f"Got error {e} when trying to read losetup output")
+ losetup_j = {}
+ if losetup_j:
+ losetup_j = losetup_j["loopdevices"]
+ for i in mounts:
+ #for j in losetup_j["loopdevices"]:
+ # if i.device == j.name:
+ try:
+ i["source"] = [j["back-file"] for j in losetup_j if j["name"] == i["device"]][0]
+ except Exception as e:
+ ferror(f"While looking for {i['device']} in {losetup_j}, got error {e}")
+ for i in mounts:
+ if "source" in i:
+ i.pop("device")
+ # Now we have a list of
+ # [{'mountpoint': '/mnt/foo', 'source': '/mnt/public/CDROMs/Games/Logical Journey of the Zoombinis.iso'}]
+ return mounts
+
+ def mount_iso_to_path(self, isofile, mount_point):
+ """
+ Mount the isofile to mount_point using sudo. No protections of already-mounted or something-else-mounted.
+ """
+ mount_p = subprocess.Popen(
+ ["sudo","mount","-v",isofile,mount_point], stdout=subprocess.PIPE
+ )
+ mount_o, mount_e = mount_p.communicate()
+ if debuglev(5):
+ if mount_e:
+ ferror(mount_e)
+
+ def mount_iso_to_default(self,isofile):
+ """
+ Use the gmm config to determine where to mount this.
+ """
+ md = self.config[appname]["mounts_dir"]
+ # determine next available number. 0-indexed. If you want to make this 1-indexed, set x to 0.
+ x = -1
+ safe = False
+ # safety valve: do not try mount more than 20 mount points.
+ while (not safe) or x < 20:
+ x = x + 1
+ testpath = os.path.join(md,str(x))
+ if not os.path.exists(testpath):
+ safe = True
+ break
+ else:
+ if os.path.ismount(testpath):
+ if self.iso_is_mounted_to_path(isofile, testpath):
+ if debuglev(2):
+ ferror(f"INFO: Found {isofile} mounted at {testpath} already. Skipping...")
+ # short-circuit
+ return True
+ # if a mountpoint but is not the requested isofile
+ if debuglev(8):
+ ferror(f"DEBUG: Found different mountpoint at {testpath}, continuing to look...")
+ # skip this number
+ continue
+ elif os.path.isdir(testpath):
+ if os.listdir(testpath):
+ # not empty so skip it
+ ferror(f"WARNING: non-mountpoint {testpath} is not empty, continuing to look...")
+ continue
+ else:
+ # empty directory, this is what we want
+ safe = True
+ break
+ else:
+ # so not a mountpoint and not a dir, so skip
+ ferror(f"WARNING: non-directory {testpath}, continuing to look...")
+ continue
+ # end while
+ # notably, because of all the continues and breaks, we cannot increment x at bottom of this loop.
+ if debuglev(8):
+ ferror(f"DEBUG: after loop, testpath is {testpath}")
+ if safe:
+ if debuglev(1):
+ print(f"INFO: will mount {isofile} to {testpath}.")
+ try:
+ os.mkdir(testpath)
+ # FUTUREIMPROVEMENT: might need to catch specific exceptions and pass them, like dir-already-exists.
+ except:
+ pass
+ self.mount_iso_to_path(isofile, testpath)
+
+ def unmount_iso_to_path(self, isofile, mount_point):
+ """
+ Unmount the isofile to mount_point using sudo.
+ This exploits that the "isofile" could be the actual iso file that is mounted, or the mount point. The umount command can either to unmount something.
+ """
+ params = ["sudo","umount","-v",isofile,mount_point]
+ params = [i for i in params if i]
+ if debuglev(1):
+ ferror(f"Running: {params}")
+ mount_p = subprocess.Popen(
+ params, stdout=subprocess.PIPE
+ )
+ mount_o, mount_e = mount_p.communicate()
+ if debuglev(5):
+ if mount_e:
+ ferror(mount_e)
+ # and now delete the empty directory
+ if not mount_point:
+ mount_point = isofile
+ if os.path.isdir(mount_point) and (not os.listdir(mount_point)):
+ try:
+ os.rmdir(mount_point)
+ except Exception as e:
+ if debuglev(5):
+ ferror(f"INFO: while trying to remove now-empty dir {mount_point}, got error {e}, continuing...")
+
+ def get_iso_mounted_to_path(self, mount_point):
+ """
+ Return the filename of what is mounted to mount_point.
+ """
+ if not os.path.ismount(mount_point):
+ return False
+ if not self.mounts:
+ self.mounts = self.list_mounts()
+ for i in self.mounts:
+ if i["mountpoint"] == mount_point:
+ # short-circuit because there can be only one thing mounted to a mount_point
+ return i["source"]
+ return None
+
+ def iso_is_mounted_to_path(self, iso_file, mount_point):
+ """
+ Return True if the named iso_file is mounted to mount_point.
+ """
+ if not os.path.ismount(mount_point):
+ return False
+ if not self.mounts:
+ self.mounts = self.list_mounts()
+ for i in self.mounts:
+ if i["mountpoint"] == mount_point and i["source"] == iso_file:
+ # short-circuit
+ return True
+ return False
+
+ def load_config(self, configfile):
+ if debuglev(1):
+ ferror(f"Loading config file {configfile}")
+ self.config.read(configfile)
+ if (not self.config) or self.config == configparser.ConfigParser():
+ ferror(f"Generating new config file, {configfile}!")
+ # This is setting a new config file.
+ self.config[appname] = {
+ "mounts_dir": os.path.expanduser("~/mnt")
+ }
+ self.save_config(configfile)
+ try:
+ #config["gmm"]["mounts_dir"] = os.path.expanduser(config["gmm"]["mounts_dir"])
+ self.config.set(appname,"mounts_dir", os.path.expanduser(self.config[appname]["mounts_dir"].strip('"')))
+ except:
+ pass
+ if debuglev(9):
+ ferror({section: dict(self.config[section]) for section in self.config.sections()})
+
+ def save_config(self, configfile = None):
+ if not configfile:
+ configfile = self.conffile
+ # un-expand the tilde
+ md = self.config[appname]["mounts_dir"]
+ # by adding the trailing slash, we make sure it is not just the HOME, but that would be an extreme situation.
+ if md.startswith(os.path.expanduser("~")+"/"):
+ md = md.replace(os.path.expanduser("~"),"~")
+ self.config.set(appname,"mounts_dir",md)
+ with open(configfile,"w") as cf:
+ self.config.write(cf)
+
+ def cli_main(self):
+ # default behavior is to show the gui, unless --no-gui, or given some paths to mount, or --list
+ args = self.args
+ paths = args.paths
+ umount = args.unmount
+ if (args.list) or (paths) or ((not paths) and args.all and umount):
+ self.show_gui = False
+ if args.gui == False or args.gui == True:
+ ferror(f"Setting show_gui to {args.gui}")
+ self.show_gui = args.gui
+ if args.list:
+ if args.output == "json":
+ print(json.dumps(self.mounts))
+ elif args.output == "tsv":
+ if self.mounts:
+ print("source\tpath")
+ for i in self.mounts:
+ print(f"{i['source']}\t{i['mountpoint']}")
+ else:
+ print(self.mounts)
+ if paths:
+ # calculate if paths.len = 2, then check if paths[0] is a file and paths[1] is a dir or underneath
+ if len(paths) == 2:
+ left = paths[0]
+ right = paths[1]
+ if os.path.isfile(left):
+ if os.path.isdir(right) or (not os.path.exists(right)):
+ if os.path.ismount(right):
+ if debuglev(8):
+ ferror(f"DEBUG: Investigating current mount {right} to see if it is same as {left}")
+ if iso_is_mounted_to_path(left, right, self.mounts):
+ if umount:
+ self.unmount_iso_to_path(left, right)
+ else:
+ if debuglev(1):
+ print(f"Already mounted!")
+ else:
+ ferror(f"ERROR: {get_iso_mounted_to_path(right,self.mounts)} is already mounted to {right}.")
+ # FUTUREIMPROVEMENT: If we want to implement --force to umount old, then mount this left, do it here.
+ else:
+ if umount:
+ if debuglev(2):
+ print(f"INFO: not a mount point, so nothing to do.")
+ else:
+ if debuglev(1):
+ print(f"INFO: will mount {left} to {right}.")
+ mount_iso_to_path(left, right)
+ elif os.path.isfile(right):
+ for i in paths:
+ self.mount_iso_to_default(i)
+ else:
+ ferror(f"ERROR: second path {right} is not a dir or file. Choose a different spot")
+ else:
+ ferror(f"ERROR: first path is not a file: {left}. Skipping...")
+ elif len(paths) == 1:
+ # main default method; the path to the iso to mount
+ isofile = paths[0]
+ # not ideal, but figure out that the user wanted "--list"
+ if isofile == "list" and not os.path.isfile(isofile):
+ ferror(f"WARNING: please use --list to list mounts. Continuing...")
+ print(self.mounts)
+ sys.exit(0)
+ if umount:
+ self.unmount_iso_to_path(isofile,"")
+ else:
+ mount_iso_to_default(isofile)
+ else:
+ # more than one path, so treat each as a file to mount
+ for isofile in paths:
+ if umount:
+ self.unmount_iso_to_path(isofile,"")
+ else:
+ self.mount_iso_to_default(isofile)
+ else:
+ # no paths, so check if --all and --umount
+ if umount and args.all:
+ for i in self.mounts:
+ self.unmount_iso_to_path(i["source"],i["mountpoint"])
+
+# PARSE ARGUMENTS
+parser = argparse.ArgumentParser(description="graphical mount manager",prog=appname,epilog=""" The value-add of this application is to use a configured directory for
+mountpoints, for easy mounting, e.g., from a graphical file manager. Run with an
+iso filename, and it will mount to ~/mnt/0, for example.""")
+parser.add_argument("-d","--debug", nargs='?', default=0, type=int, choices=range(0,11), help="Set debug level.")
+parser.add_argument("-V","--version", action="version", version="%(prog)s " + appversion)
+parser.add_argument("-g","--gui", action=argparse.BooleanOptionalAction, default=None, help="Show gui, even if also given any other actions.")
+parser.add_argument("-l","--list", action="store_true", help="List mounted isos and paths and then exit.")
+parser.add_argument("-o","--output", default="tsv", choices=("tsv","raw","json"), help="Change output format for --list.")
+parser.add_argument("-u","--unmount","--umount", action="store_true", help="If used with one or more paths, umount them instead of mounting.")
+parser.add_argument("-a","--all", action="store_true", help="Used only with --unmount. Unmount all mounted paths in default-configured dir.")
+parser.add_argument("paths", nargs="*",help="Positional parameters: ISO_FILE, MOUNT_PATH")
+#parser.add_argument("-f","--force", action="store_true", help="Force deploy all confs.")
+parser.add_argument("-c","--conf","--config", action="store", default=conffile, help="Use this config file.")
+#parser.add_argument("-r","--dry","--dryrun","--dry-run", action="store_true", help="Do not execute. Useful when debugging.")
+args = parser.parse_args()
+if args.debug is None:
+ debuglevel = 5
+elif args.debug:
+ debuglevel = args.debug
+if debuglev(1):
+ ferror("debug level", debuglevel)
+ ferror(args)
+umount = args.unmount
bgstack15