diff options
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | .images/scrot-gmm.png | bin | 0 -> 13642 bytes | |||
-rw-r--r-- | README.md | 39 | ||||
-rw-r--r-- | src/Makefile | 92 | ||||
-rw-r--r-- | src/etc/sudoers.d/30_gmm_sudo | 3 | ||||
-rwxr-xr-x | src/usr/bin/gmm-gtk | 319 | ||||
-rwxr-xr-x | src/usr/bin/gmm-tk | 216 | ||||
-rwxr-xr-x | src/usr/libexec/gmm/gmm-mount-helper.py | 32 | ||||
-rw-r--r-- | src/usr/share/applications/gmm-gtk.desktop | 17 | ||||
-rw-r--r-- | src/usr/share/applications/gmm-tk.desktop | 17 | ||||
-rw-r--r-- | src/usr/share/gmm/gmm_lib.py | 438 | ||||
-rw-r--r-- | src/usr/share/man/man1/gmm-gtk.1.txt | 33 | ||||
-rw-r--r-- | src/usr/share/man/man1/gmm-tk.1.txt | 33 | ||||
-rw-r--r-- | src/usr/share/man/man1/gmm.1.txt | 33 |
14 files changed, 1276 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91c7c4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.*.swp +__pycache__ +*.orig +*.[0-9].gz diff --git a/.images/scrot-gmm.png b/.images/scrot-gmm.png Binary files differnew file mode 100644 index 0000000..87be780 --- /dev/null +++ b/.images/scrot-gmm.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..95b62f8 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# README for Graphical Mount Manager +Graphical Mount Manager is graphical program that makes it easy to mount and unmount iso files. For a user who doesn't care what the path will be, and just wants to open the file, gmm is the right choice. + +[App window<br/>![](.images/scrot-gmm.png)](.images/scrot-gmm.png) + +## Upstream +This project's upstream is at <https://bgstack15.ddns.net/cgit/gmm>. + +## Alternatives +[AcetoneISO](https://bgstack15.ddns.net/blog/outbound/https://sourceforge.net/projects/acetoneiso/) is the obvious alternative. It stopped working for me. + +## Reason for existence +AcetoneISO [stopped working](https://bgstack15.ddns.net/blog/posts/2024/11/16/acetoneiso-silent-error-a-plea-for-help/) for the author. + +## Using +The gtk frontend supports drag-and-drop. Drag an iso file from the file manager over the list of mounts. The tk frontend does not support dnd at this time. + +Call the program, with a filename, to mount the file. The default behavior is to not show the gui if given a path(s) to mount, but you can force the gui to show with `--gui`. + +Doubleclick an entry in the list to open it with `xdg-open`. You [might find](https://bgstack15.ddns.net/blog/posts/2023/02/05/xdg-mime-command-for-window-managers/) the following xdg-mime commands useful. + + xdg-mime default xfe.desktop inode/directory + xdg-mime default xfe.desktop inode/mount-point + +## Dependencies + +* Devuan + * python3, python3-magic, python3-psutil, [python3-tkstackrpms](https://bgstack15.ddns.net/cgit/python3-tkstackrpms) + +## Building +Just `make && sudo make install`, or use the package build recipe. + +## Future improvements + +<https://stackoverflow.com/questions/380870/make-sure-only-a-single-instance-of-a-program-is-running> + +Provide a socket/method that will let the running program refresh. Add function that refreshes the list, and add a menu entry for it. Have the program try to visit the socket and trigger the refresh function. Maybe just a 5-second timer? + +## References 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)` |