aboutsummaryrefslogtreecommitdiff
path: root/src/usr/share
diff options
context:
space:
mode:
Diffstat (limited to 'src/usr/share')
-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
6 files changed, 571 insertions, 0 deletions
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