aboutsummaryrefslogtreecommitdiff
path: root/src/usr/bin
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/usr/bin
downloadgmm-35b46f635ee0089db5e2a8a302f29990c0ace8c6.tar.gz
gmm-35b46f635ee0089db5e2a8a302f29990c0ace8c6.tar.bz2
gmm-35b46f635ee0089db5e2a8a302f29990c0ace8c6.zip
initial commit
Diffstat (limited to 'src/usr/bin')
-rwxr-xr-xsrc/usr/bin/gmm-gtk319
-rwxr-xr-xsrc/usr/bin/gmm-tk216
2 files changed, 535 insertions, 0 deletions
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()
bgstack15