aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorB. Stack <bgstack15@gmail.com>2024-12-10 10:40:35 -0500
committerB. Stack <bgstack15@gmail.com>2024-12-10 10:40:35 -0500
commit35b46f635ee0089db5e2a8a302f29990c0ace8c6 (patch)
tree3fb0a03c1ff9e29ada20984ea839a9982e056976 /src
downloadgmm-35b46f635ee0089db5e2a8a302f29990c0ace8c6.tar.gz
gmm-35b46f635ee0089db5e2a8a302f29990c0ace8c6.tar.bz2
gmm-35b46f635ee0089db5e2a8a302f29990c0ace8c6.zip
initial commit
Diffstat (limited to 'src')
-rw-r--r--src/Makefile92
-rw-r--r--src/etc/sudoers.d/30_gmm_sudo3
-rwxr-xr-xsrc/usr/bin/gmm-gtk319
-rwxr-xr-xsrc/usr/bin/gmm-tk216
-rwxr-xr-xsrc/usr/libexec/gmm/gmm-mount-helper.py32
-rw-r--r--src/usr/share/applications/gmm-gtk.desktop17
-rw-r--r--src/usr/share/applications/gmm-tk.desktop17
-rw-r--r--src/usr/share/gmm/gmm_lib.py438
-rw-r--r--src/usr/share/man/man1/gmm-gtk.1.txt33
-rw-r--r--src/usr/share/man/man1/gmm-tk.1.txt33
-rw-r--r--src/usr/share/man/man1/gmm.1.txt33
11 files changed, 1233 insertions, 0 deletions
diff --git a/src/Makefile b/src/Makefile
new file mode 100644
index 0000000..f1b91bd
--- /dev/null
+++ b/src/Makefile
@@ -0,0 +1,92 @@
+# File: Makefile for gmm
+# Location: gmm source package
+# Author: bgstack15
+# Startdate: 2024-12-04
+# Title: Makefile for gmm source package
+# Purpose: To use traditional Unix make utility
+# History:
+# Usage:
+# Reference:
+# bgscripts Makefile
+# Improve:
+# Document:
+# Dependencies:
+# build-devuan: txt2man, bgscripts-core
+
+APPNAME = gmm
+APPVERSION = 0.0.1
+SRCDIR = $(CURDIR)
+prefix = /usr
+
+# variables for deplist
+DEPTYPE = dep
+SEPARATOR = ,
+
+awkbin :=$(shell which awk)
+chmodbin :=$(shell which chmod)
+cpbin :=$(shell which cp)
+echobin :=$(shell which echo)
+falsebin :=$(shell which false)
+findbin :=$(shell which find)
+grepbin :=$(shell which grep)
+gzipbin :=$(shell which gzip)
+installbin :=$(shell which install)
+rmbin :=$(shell which rm)
+rmdirbin :=$(shell which rmdir)
+sedbin :=$(shell which sed)
+sortbin :=$(shell which sort)
+truebin :=$(shell which true)
+txt2manwrapper :=$(shell which txt2man-wrapper)
+uniqbin :=$(shell which uniq)
+xargsbin :=$(shell which xargs)
+
+with_man ?= YES
+
+all: build_man
+
+ifeq ($(with_man),YES)
+install: build_man install_files
+else
+install: install_files
+endif
+
+.PHONY: clean install install_files build_man uninstall list deplist deplist_opts
+
+list:
+ @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | ${awkbin} -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | ${sortbin} | ${grepbin} -E -v -e '^[^[:alnum:]]' -e '^$@$$'
+
+deplist:
+ @# deplist 2020-04-18 input must be comma separated
+ @# DEPTYPE( dep , rec , sug ) for depends, recommends, or suggests
+ @if test -z "${DISTRO}" ; then ${echobin} "Please run \`make deplist\` with DISTRO= one of: `make deplist_opts 2>&1 1>/dev/null | ${xargsbin}`. Aborted." 1>&2 ; exit 1 ; fi
+ @if ! ${echobin} "${DEPTYPE}" | grep -qE "^(dep|rec|sug)$$" ; then ${echobin} "Please run \`make deplist\` with DEPTYPE= one of: dep, rec, sug. Undefined will use \`dep\`. Aborted." 1>&2 ; exit 1; fi
+ @${grepbin} -h --exclude-dir='doc' -riIE "\<${DEPTYPE}-" ${SRCDIR} | ${awkbin} -v "domain=${DISTRO}" -v "deptype=${DEPTYPE}" 'tolower($$2) ~ deptype"-"domain {$$1="";$$2="";print}' | tr ',' '\n' | ${sortbin} | ${uniqbin} | ${sedbin} -r -e 's/^\s*//' -e "s/\s*\$$/${SEPARATOR}/" | ${xargsbin}
+
+deplist_opts:
+ @# deplist_opts 2020-04-18 find all available dependency domains
+ @${grepbin} -h -o -riIE '\<(dep|rec|sug)-[^\ :]+:' ${SRCDIR} | ${sedbin} -r -e 's/(dep|rec|sug)-//;' -e 's/:$$//;' | ${sortbin} | ${uniqbin} 1>&2
+
+install_files:
+ @ls usr/share/man/man*/*gz 1>/dev/null 2>&1 && echo "Including man pages." || :
+ @${echobin} Installing files to ${DESTDIR}
+ for td in $$( ${findbin} ${SRCDIR} -type d ! -name '.*.swp' ! -path '*/__pycache__/*' ! -name 'Makefile' -printf '%P\n' | ${sedbin} -r -e "s:${DESTDIR}/?::" ) ; do ${installbin} -m0755 -d ${DESTDIR}/$${td} ; done
+ for tf in $$( ${findbin} ${SRCDIR} ! -type d ! -name '.*.swp' ! -path '*/__pycache__/*' ! -name 'Makefile' ! \( -path '*/man/*' -name '*.txt' \) ! -path '*/sysconfig/*' -printf '%P\n' ) ; do MODE=0644 ; echo "$${tf}" | grep -qE "(bin|libexec|deprecated)/" && MODE=0755 ; ${installbin} -m$${MODE} ${SRCDIR}/$${tf} ${DESTDIR}/$${tf} ; done
+
+MAN_TXT:=$(wildcard usr/share/man/man*/*.txt)
+MAN_GZ:= $(subst .txt,.gz,$(MAN_TXT))
+
+build_man: $(MAN_GZ)
+
+$(MAN_GZ): %.gz: %.txt
+ ${txt2manwrapper} - < $< | ${gzipbin} > $@
+
+uninstall:
+ @${echobin} SRCDIR=${SRCDIR}
+ ${rmbin} -f $$( ${findbin} ${SRCDIR} -mindepth 1 ! -type d -printf '%p\n' | ${sedbin} -r -e "s:^${SRCDIR}:${DESTDIR}:" -e "s:${DESTDIR}${DESTDIR}:${DESTDIR}:" )
+
+ # remove all installed directories that are now blank.
+ ${rmdirbin} 2>/dev/null || : ; for word in $$( ${findbin} ${SRCDIR} -mindepth 1 -type d -printf '%p\n' | ${sedbin} -r -e "s:^${SRCDIR}:${DESTDIR}:" | ${awkbin} '{ print length, $$0 }' | ${sortbin} -rn | ${awkbin} '{print $$2}' ) ; do ${findbin} $${word} -mindepth 1 1>/dev/null 2>&1 | read 1>/dev/null 2>&1 || { ${rmdirbin} "$${word}" 2>/dev/null || ${truebin} ; } ; done
+
+clean:
+ -@#${echobin} "target $@ not implemented yet! Gotta say unh." && ${falsebin}
+ -${rmbin} -f usr/share/man/man*/*.gz || :
diff --git a/src/etc/sudoers.d/30_gmm_sudo b/src/etc/sudoers.d/30_gmm_sudo
new file mode 100644
index 0000000..277c0c6
--- /dev/null
+++ b/src/etc/sudoers.d/30_gmm_sudo
@@ -0,0 +1,3 @@
+# File: /etc/sudoers.d/30_gmm_sudo
+Defaults env_keep += "DRYRUN VERBOSE APPLY DEBUG"
+ALL ALL = (root) NOPASSWD: /usr/libexec/gmm/gmm-mount-helper.py *
diff --git a/src/usr/bin/gmm-gtk b/src/usr/bin/gmm-gtk
new file mode 100755
index 0000000..0ea3951
--- /dev/null
+++ b/src/usr/bin/gmm-gtk
@@ -0,0 +1,319 @@
+#!/usr/bin/env python3
+# vim: set et ts=4 sts=4 sw=4:
+# File: gmm-gtk
+# Location: /usr/bin
+# Author: bgstack15
+# SPDX-License-Identifier: GPL-3.0-only
+# 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
+# History:
+# Usage: see man page
+# 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
+# https://stackoverflow.com/questions/36921706/gtk3-ask-confirmation-before-application-quit
+# disused references:
+# https://python-gtk-3-tutorial.readthedocs.io/en/latest/application.html#application
+# Improve:
+# Dependencies:
+
+import gi, os, sys, subprocess, threading
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk, GLib, Gdk
+from gi.repository.GdkPixbuf import Pixbuf
+
+# ADJUST THIS SECTION FOR DEVELOPMENT
+# for prod, this should be ""
+home_dir = os.getenv("SUDO_HOME",os.getenv("HOME"))
+if os.getenv("DEBUG",None):
+ sys.path.append(os.path.join(home_dir,"dev","gmm","src","usr/share/gmm"))
+
+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()
+ t1 = threading.Thread(target=self.gmmapp.watch_for_changes,args=("disused-but-must-be-not-a-function",self.refresh_form))
+ t1.start()
+
+ def do_delete_event(self, event = None):
+ # override built-in do_delete_event, because we need to run this manually first.
+ self.gmmapp.exit()
+ # Calling this somehow is safe here.
+ Gtk.main_quit()
+ # this is the regular behavior
+ return False
+
+ 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):
+ # Must call our own function and not main_quit.
+ #Gtk.main_quit()
+ self.do_delete_event()
+
+ 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
+ app.initialize_for_gui()
+ win = MainWindow(app)
+ win.connect("destroy", win.do_delete_event)
+ win.show_all()
+ Gtk.main()
diff --git a/src/usr/bin/gmm-tk b/src/usr/bin/gmm-tk
new file mode 100755
index 0000000..ef1c1a3
--- /dev/null
+++ b/src/usr/bin/gmm-tk
@@ -0,0 +1,216 @@
+#!/usr/bin/env python3
+# vim: set et ts=4 sts=4 sw=4:
+# File: gmm-tk
+# Location: /usr/bin
+# Author: bgstack15
+# SPDX-License-Identifier: GPL-3.0-only # 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
+# History:
+# Usage: see man page
+# Reference:
+# 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.
+# https://stackoverflow.com/questions/57925492/how-to-listen-continuously-to-a-socket-for-data-in-python
+# 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
+# Dependencies:
+# dep-devuan: python3, python3-tkstackrpms
+
+import sys, subprocess, json, configparser, tkinter as tk, tkinter.simpledialog, tkinter.filedialog, os, threading
+import tkstackrpms as stk
+import tkinter.ttk as ttk
+
+# ADJUST THIS SECTION FOR DEVELOPMENT
+# for prod, this should be ""
+home_dir = os.getenv("SUDO_HOME",os.getenv("HOME"))
+if os.getenv("DEBUG",None):
+ sys.path.append(os.path.join(home_dir,"dev","gmm","src","usr/share/gmm"))
+
+sys.path.append("/usr/share/gmm")
+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")
+ 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")
+ t1 = threading.Thread(target=self.gmmapp.watch_for_changes,args=("disused-but-must-be-not-a-function",self.refresh_form))
+ t1.start()
+
+# GRAPHICAL FUNCTIONS
+ def func_about(self):
+ """ Display about dialog. """
+ tk.messagebox.Message(title="About",message=gmm.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.gmmapp.exit()
+ 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:
+ app.initialize_for_gui()
+ root = tk.Tk()
+ gmm_tk = TkApp(root, app)
+ root.protocol("WM_DELETE_WINDOW", gmm_tk.func_exit)
+ gmm_tk.mainloop()
diff --git a/src/usr/libexec/gmm/gmm-mount-helper.py b/src/usr/libexec/gmm/gmm-mount-helper.py
new file mode 100755
index 0000000..091385e
--- /dev/null
+++ b/src/usr/libexec/gmm/gmm-mount-helper.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# vim: set et ts=4 sts=4 sw=4:
+# File: gmm-mount-helper.py
+# Location: /usr/libexec/gmm
+# Author: bgstack15
+# SPDX-License-Identifier: GPL-3.0-only
+# Startdate: 2024-12-04-4 13:12
+# Title: Sudo helper for gmm
+# Purpose: single command to grant sudo access
+# History:
+# Usage:
+# Reference:
+# Improve:
+# Dependencies:
+# dep-devuan: python3-magic
+import argparse, sys, os, magic, subprocess, pathlib
+
+sys.path.append("/usr/share/gmm")
+home_dir = os.getenv("SUDO_HOME",os.getenv("HOME"))
+
+# ADJUST THIS SECTION FOR DEVELOPMENT
+# for prod, this should be ""
+if os.getenv("DEBUG",False):
+ sys.path.append(os.path.join(home_dir,"dev","gmm","src","usr/share/gmm"))
+
+import gmm_lib as gmm
+from gmm_lib import debuglev, ferror
+
+if "__main__" == __name__:
+ # MAIN APP
+ app = gmm.Gmm(gmm.args, is_mount_helper = True)
+ app.cli_main()
diff --git a/src/usr/share/applications/gmm-gtk.desktop b/src/usr/share/applications/gmm-gtk.desktop
new file mode 100644
index 0000000..a67f6d0
--- /dev/null
+++ b/src/usr/share/applications/gmm-gtk.desktop
@@ -0,0 +1,17 @@
+[Desktop Entry]
+Actions=UnmountAll;
+Categories=Utility;FileTools;
+Comment=Easily mount iso files
+Exec=gmm-gtk --gui %F
+GenericName=Graphical mount manager
+Icon=dvd_unmount
+Keywords=mount,iso,mdf,ngr,bin,cue,cd,dvd,image
+MimeType=application/x-iso9660-image;
+Name=gmm-gtk
+StartupNotify=false
+Terminal=false
+Type=Application
+
+[Desktop Action UnmountAll]
+Name=Unmount all managed mounts
+Exec=gmm-gtk --no-gui --unmount --all
diff --git a/src/usr/share/applications/gmm-tk.desktop b/src/usr/share/applications/gmm-tk.desktop
new file mode 100644
index 0000000..a36084d
--- /dev/null
+++ b/src/usr/share/applications/gmm-tk.desktop
@@ -0,0 +1,17 @@
+[Desktop Entry]
+Actions=UnmountAll;
+Categories=Utility;FileTools;
+Comment=Easily mount iso files
+Exec=gmm-tk --gui %F
+GenericName=Graphical mount manager (tk)
+Icon=dvd_unmount
+Keywords=mount,iso,mdf,ngr,bin,cue,cd,dvd,image
+MimeType=application/x-iso9660-image;
+Name=gmm-tk
+StartupNotify=false
+Terminal=false
+Type=Application
+
+[Desktop Action UnmountAll]
+Name=Unmount all managed mounts
+Exec=gmm-tk --no-gui --unmount --all
diff --git a/src/usr/share/gmm/gmm_lib.py b/src/usr/share/gmm/gmm_lib.py
new file mode 100644
index 0000000..687547e
--- /dev/null
+++ b/src/usr/share/gmm/gmm_lib.py
@@ -0,0 +1,438 @@
+#!/usr/bin/env python3
+# vim: set et ts=4 sts=4 sw=4:
+# File: gmm_lib.py
+# Location: /usr/share/gmm/
+# Author: bgstack15
+# SPDX-License-Identifier: GPL-3.0-only
+# Startdate: 2024-11-20-4 12:50
+# Title: Library for Graphical Mount Manager
+# Purpose: separate frontend and backend for gmm
+# History:
+# Usage: from gmm
+# Reference:
+# https://stackoverflow.com/questions/57925492/how-to-listen-continuously-to-a-socket-for-data-in-python
+# https://stackoverflow.com/questions/380870/make-sure-only-a-single-instance-of-a-program-is-running
+# fprintd_tk_lib.py
+# Improve:
+# Dependencies:
+# dep-devuan: python3, python3-psutil
+import argparse, sys, os, psutil, subprocess, json, configparser, socket, time, datetime, pathlib
+
+# LOW-LEVEL values
+appname_full = "Graphical Mount Manager"
+appname = "gmm"
+appversion = "0.0.1"
+authors = ["bgstack15"]
+icon_name = "dvd_unmount"
+home_dir = os.getenv("SUDO_HOME",os.getenv("HOME"))
+conffile = os.path.join(home_dir,".config",appname,"config")
+socket_path = f"{os.getenv('XDG_RUNTIME_DIR','/tmp').rstrip('/')}/gmm.socket"
+
+ABOUT_TEXT = """
+gmm "Graphical Mount Manager"
+(C) 2024 bgstack15
+SPDX-License-Identifier: GPL-3.0-only
+"""
+_mount_helper_path = "/usr/libexec/gmm/gmm-mount-helper.py"
+
+if os.getenv("DEBUG",None):
+ _mount_helper_path = os.path.join(home_dir,"dev","gmm","src","usr/libexec/gmm/gmm-mount-helper.py")
+
+# 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
+
+def get_debuglev():
+ try:
+ return int(debuglevel)
+ except:
+ return 0
+
+# APP FUNCTIONS
+class Gmm():
+
+ def __init__(self,args=None, is_mount_helper = False):
+ # Parse config
+ self.conffile = conffile
+ try:
+ if args.conf:
+ self.conffile = args.conf
+ except:
+ pass
+ self.mounts = self.list_mounts()
+ self.args = args
+ self.is_mount_helper = is_mount_helper
+ self.config = configparser.ConfigParser()
+ self.load_config(self.conffile)
+
+ def initialize_for_gui(self):
+ """
+ Separate this from app initialization, because we want disk operations to happen. The socket only affects the gui; we want only one gui to run.
+ """
+ # only allow one instance
+ # We are really only using the socket as a lock file. We are not using it as a socket at all.
+ if not args.force:
+ try:
+ self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ ## NOT USED: Create an abstract socket, by prefixing it with null.
+ ## the null-prefixed one did not stop a second instance from happening.
+ self.s.bind(socket_path)
+ except socket.error as e:
+ ferror(f"Gui is already running ({e.args[0]}:{e.args[1]}).")
+ # send refresh signal
+ pathlib.Path(socket_path).touch()
+ sys.exit(0)
+ #ferror(self.s)
+ # will be set to False if --no-gui, or some action is given
+ self.show_gui = True
+
+ def exit(self):
+ """
+ Clean up after ourselves.
+ """
+ try:
+ os.remove(socket_path)
+ except Exception as e:
+ if "No such file or directory" not in str(e):
+ ferror(f"During exit, tried to delete socket {socket_path} but got error {e}...")
+
+ def watch_for_changes(self, o1, callback = None, o3 = None):
+ """
+ Poor man's file watchdog. Exploit that we only need one signal, the timestamp of the file.
+ I researched file Watchdog but it appears to not tolerate being closed like a regular old threading does.
+ """
+ old = os.stat(socket_path).st_mtime
+ my_continue = True
+ while my_continue:
+ new = old
+ try:
+ new = os.stat(socket_path).st_mtime
+ except Exception as e:
+ # file might not exist?
+ if "No such file" in str(e):
+ my_continue = False
+ break
+ ferror(f"Unable to stat {socket_path}, weird? Got {e}")
+ pass
+ if new != old:
+ ferror(f"Need to refresh!")
+ #ferror(f"Need to process changes!, waiting 3 seconds this time, need to do something with o1 {o1}, callback {callback}, o3 {o3}")
+ old = new
+ if callback:
+ callback()
+ time.sleep(3)
+ time.sleep(1)
+
+ 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.
+ """
+ params = ["sudo",_mount_helper_path,"-d",str(get_debuglev()),isofile, mount_point]
+ if self.is_mount_helper:
+ params = ["mount","-v",isofile,mount_point]
+ mount_p = subprocess.Popen(
+ params, stdout = subprocess.PIPE, stderr = subprocess.PIPE
+ )
+ mount_o, mount_e = mount_p.communicate()
+ if debuglev(5):
+ if mount_e:
+ ferror(mount_e.decode())
+
+ 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 use either to unmount something.
+ """
+ params = ["sudo",_mount_helper_path,"-d",str(get_debuglev()),"-u",isofile,mount_point]
+ if self.is_mount_helper:
+ params = ["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, stderr = subprocess.PIPE
+ )
+ mount_o, mount_e = mount_p.communicate()
+ if debuglev(5):
+ if mount_e:
+ ferror(mount_e.decode())
+ # 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.join(home_dir,"mnt")
+ }
+ self.save_config(configfile)
+ try:
+ # Cannot just do expanduser of string containing "~" because ~ for root during sudo is still ~root and not SUDO_HOME.
+ #self.config.set(appname,"mounts_dir", os.path.expanduser(self.config[appname]["mounts_dir"].strip('"')))
+ # We must manually replace ~ ourselves first.
+ self.config.set(appname,"mounts_dir", os.path.expanduser(self.config[appname]["mounts_dir"].strip('"').replace("~",home_dir)))
+ 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(home_dir+"/"):
+ md = md.replace(home_dir,"~")
+ 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 self.iso_is_mounted_to_path(left, right):
+ 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:
+ try:
+ os.mkdir(right)
+ except FileExistsError:
+ pass
+ if debuglev(1):
+ print(f"INFO: gmm will mount {left} to {right}.")
+ self.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:
+ self.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("-f","--force", action="store_true", default=False, help="Ignore any existing socket that controls gui. Clean it up, and display gui. Only applies if --gui.")
+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
diff --git a/src/usr/share/man/man1/gmm-gtk.1.txt b/src/usr/share/man/man1/gmm-gtk.1.txt
new file mode 100644
index 0000000..47fe3e5
--- /dev/null
+++ b/src/usr/share/man/man1/gmm-gtk.1.txt
@@ -0,0 +1,33 @@
+title gmm-gtk
+section 1
+project gmm
+volume General Commands Manual
+date December 2024
+=====
+NAME
+ gmm - graphical mount manager
+SYNPOSIS
+ gmm [-h] [-d {0..10}] [-V] [-g | --gui | --no-gui] [-l] [-o {tsv,raw,json}] [-u] [-a] [-c CONF] [paths ...]
+OPTIONS
+ -h --help Show help message
+ -d {0..10} Set debug level, with 10 as maximum verbosity.
+ -c <conffile> Use this config file instead of default which is `${HOME}/.config/gmm/config`.
+ -g --gui Show the window, regardless of any other behavior.
+ --no-gui Do not show the window, regardless.
+ --list List mounted isos and paths and then exit.
+ -o --output {tsv, raw, json} Change output format for --list.
+ -u --unmount If used with one or more <paths>, unmount them instead of mounting.
+ -a --all Useful only with --unmount. Unmount all paths in configured mounts_dir.
+ENVIRONMENT
+Not a lot of gmm is controlled with environment variables. Parameters, and graphical interaction, are the main way to use gmm.
+DESCRIPTION
+ Gmm is designed to make it easy to mount and access iso files. Two different toolkits are available to use: `gmm-tk` and `gmm-gtk`. The tk one does not support drag-and-drop, but gtk does.
+ Do not run gmm with sudo! Gmm calls helper script gmm-mount-helper.py with sudo. Then, the help script validates the input and calls `mount`.
+BUGS
+ Please report bugs to the author.
+AUTHOR
+ <bgstack15@gmail.com>
+COPYRIGHT
+ Copyright 2024 bgstack15, GPL-3.0-only
+SEE ALSO
+ `gmm(1)`,`gmm-tk(1)`
diff --git a/src/usr/share/man/man1/gmm-tk.1.txt b/src/usr/share/man/man1/gmm-tk.1.txt
new file mode 100644
index 0000000..b220c21
--- /dev/null
+++ b/src/usr/share/man/man1/gmm-tk.1.txt
@@ -0,0 +1,33 @@
+title gmm-tk
+section 1
+project gmm
+volume General Commands Manual
+date December 2024
+=====
+NAME
+ gmm - graphical mount manager
+SYNPOSIS
+ gmm [-h] [-d {0..10}] [-V] [-g | --gui | --no-gui] [-l] [-o {tsv,raw,json}] [-u] [-a] [-c CONF] [paths ...]
+OPTIONS
+ -h --help Show help message
+ -d {0..10} Set debug level, with 10 as maximum verbosity.
+ -c <conffile> Use this config file instead of default which is `${HOME}/.config/gmm/config`.
+ -g --gui Show the window, regardless of any other behavior.
+ --no-gui Do not show the window, regardless.
+ --list List mounted isos and paths and then exit.
+ -o --output {tsv, raw, json} Change output format for --list.
+ -u --unmount If used with one or more <paths>, unmount them instead of mounting.
+ -a --all Useful only with --unmount. Unmount all paths in configured mounts_dir.
+ENVIRONMENT
+Not a lot of gmm is controlled with environment variables. Parameters, and graphical interaction, are the main way to use gmm.
+DESCRIPTION
+ Gmm is designed to make it easy to mount and access iso files. Two different toolkits are available to use: `gmm-tk` and `gmm-gtk`. The tk one does not support drag-and-drop, but gtk does.
+ Do not run gmm with sudo! Gmm calls helper script gmm-mount-helper.py with sudo. Then, the help script validates the input and calls `mount`.
+BUGS
+ Please report bugs to the author.
+AUTHOR
+ <bgstack15@gmail.com>
+COPYRIGHT
+ Copyright 2024 bgstack15, GPL-3.0-only
+SEE ALSO
+ `gmm(1)`,`gmm-gtk(1)`
diff --git a/src/usr/share/man/man1/gmm.1.txt b/src/usr/share/man/man1/gmm.1.txt
new file mode 100644
index 0000000..3733e58
--- /dev/null
+++ b/src/usr/share/man/man1/gmm.1.txt
@@ -0,0 +1,33 @@
+title gmm
+section 1
+project gmm
+volume General Commands Manual
+date December 2024
+=====
+NAME
+ gmm - graphical mount manager
+SYNPOSIS
+ gmm [-h] [-d {0..10}] [-V] [-g | --gui | --no-gui] [-l] [-o {tsv,raw,json}] [-u] [-a] [-c CONF] [paths ...]
+OPTIONS
+ -h --help Show help message
+ -d {0..10} Set debug level, with 10 as maximum verbosity.
+ -c <conffile> Use this config file instead of default which is `${HOME}/.config/gmm/config`.
+ -g --gui Show the window, regardless of any other behavior.
+ --no-gui Do not show the window, regardless.
+ --list List mounted isos and paths and then exit.
+ -o --output {tsv, raw, json} Change output format for --list.
+ -u --unmount If used with one or more <paths>, unmount them instead of mounting.
+ -a --all Useful only with --unmount. Unmount all paths in configured mounts_dir.
+ENVIRONMENT
+Not a lot of gmm is controlled with environment variables. Parameters, and graphical interaction, are the main way to use gmm.
+DESCRIPTION
+ Gmm is designed to make it easy to mount and access iso files. Two different toolkits are available to use: `gmm-tk` and `gmm-gtk`. The tk one does not support drag-and-drop, but gtk does.
+ Do not run gmm with sudo! Gmm calls helper script gmm-mount-helper.py with sudo. Then, the help script validates the input and calls `mount`.
+BUGS
+ Please report bugs to the author.
+AUTHOR
+ <bgstack15@gmail.com>
+COPYRIGHT
+ Copyright 2024 bgstack15, GPL-3.0-only
+SEE ALSO
+ `gmm-gtk(1)`,`gmm-tk(1)`
bgstack15