aboutsummaryrefslogtreecommitdiff
path: root/src/usr
diff options
context:
space:
mode:
authorB Stack <bgstack15@gmail.com>2020-09-25 20:39:41 -0400
committerB Stack <bgstack15@gmail.com>2020-09-25 20:39:41 -0400
commitcee836f71e191e51dd48d34633bfd8c977ba3b8a (patch)
tree5cf0cbf47cea50276f2dc853500400554d04bd04 /src/usr
parentprovide working trayicon now (diff)
downloadmyautomount-cee836f71e191e51dd48d34633bfd8c977ba3b8a.tar.gz
myautomount-cee836f71e191e51dd48d34633bfd8c977ba3b8a.tar.bz2
myautomount-cee836f71e191e51dd48d34633bfd8c977ba3b8a.zip
rename to myautomount
Add annotations to conf file, separate out initialization so daemon can be per-user. Use a sane name.
Diffstat (limited to 'src/usr')
-rwxr-xr-xsrc/usr/bin/myautomount-trayicon4
-rwxr-xr-xsrc/usr/bin/myautomountd137
-rw-r--r--src/usr/libexec/myautomount/myautomount-initialize.sh24
-rwxr-xr-xsrc/usr/libexec/myautomount/myautomount-trayicon.py360
4 files changed, 525 insertions, 0 deletions
diff --git a/src/usr/bin/myautomount-trayicon b/src/usr/bin/myautomount-trayicon
new file mode 100755
index 0000000..08be25d
--- /dev/null
+++ b/src/usr/bin/myautomount-trayicon
@@ -0,0 +1,4 @@
+#!/bin/sh
+# Part of myautomount project
+. ${MYA_PREFIX}/etc/myautomount.conf
+${MYA_PREFIX}/usr/libexec/myautomount/myautomount-trayicon.py
diff --git a/src/usr/bin/myautomountd b/src/usr/bin/myautomountd
new file mode 100755
index 0000000..d23940a
--- /dev/null
+++ b/src/usr/bin/myautomountd
@@ -0,0 +1,137 @@
+#!/bin/sh
+# File: /usr/bin/myautomountd
+# Location: gitlab
+# Authors: beanpole135, bgstack15
+# Startdate: 2020-09-23
+# Title: Automount in Shell
+# Purpose: almost one-for-one translation of Go version
+# History:
+# 2020-09-23 translated by bgstack15 to shell from Go version (reference 1)
+# Several translation notes: configuration split out into separate file
+# and given a Fedora flavor.
+# Usage:
+# Invoke this at X startup. This can be from the xdg autostart mechanism, or "exec myautomountd &" in ~/fluxbox.startup
+# This is the backend to myautomount-trayicon
+# Reference:
+# https://github.com/project-trident/trident-utilities/blob/master/src-go/automount/main.go
+# Improve:
+# Dependencies:
+# udevadm (from systemd-udev or eudev)
+# dep-devuan: eudev
+
+# FUNCTIONS
+clean_automount() {
+ rm -f "${AUTOMOUNT_TMPFILE}"
+ kill "${AUTOMOUNT_PID}"
+}
+
+reset_tmpfile() {
+ cat /dev/null > "${AUTOMOUNT_TMPFILE}"
+}
+
+handleEvent() {
+ # call: handleEvent "${STRING}"
+ _line="${1}"
+ test -n "${STACKTRACE}" && echo "handleEvent \"${_line}\"" 1>&2
+ echo "${_line}" | grep -qvE "^UDEV" && return # not a valid entry - possibly a startup message
+ test $( echo "${_line}" | wc -w ) -ne 5 && return
+ _deviceid=
+ _eventType=
+ for word in ${_line} ; do
+ # no opportunity for the bash for statement to read a blank value here from unquoted variable
+ if echo "${word}" | grep -qE '^(add|remove|change)$' ;
+ then
+ _eventType="${word}"
+ elif echo "${word}" | grep -qE "^\/devices\/" ;
+ then
+ _deviceid="$( echo "${word}" | awk -F'/' '{print $NF}' )"
+ fi
+ done
+ { test "${_deviceid}" = "" || test "${_eventType}" = "" ; } && return
+ test -n "${VERBOSE}" || test -n "${DEBUG}" && echo "Got device event: ${_eventType} ${_deviceid}" 1>&2
+ _entry="${AUTOMOUNT_BASEDIR}/${_deviceid}.desktop"
+ case "${_eventType}" in
+ "add")
+ createEntry "${_deviceid}" "${_entry}"
+ ;;
+ *) # anything else
+ test -e "${_entry}" && { rm "${_entry}" || : ; }
+ test "${_eventType}" = "change" && createEntry "${_deviceid}" "${_entry}"
+ ;;
+ esac
+}
+
+createEntry() {
+ # call: createEntry "{device}" "${filepath}"
+ _device="${1}"
+ _filepath="${2}"
+ test -n "${STACKTRACE}" && echo "STUB createEntry \"${_device}\" \"${_filepath}\"" 1>&2
+ _fs=
+ _model=
+ _vendor=
+ _label=
+ _atracks=
+ _bytes="$( udevadm info "/dev/${_device}" 2>/dev/null )"
+ _shortbytes="$( printf "%s\n" "${_bytes}" | sed -r -e 's/^E:\s*//;' | grep -E '^(ID_FS_TYPE|ID_MODEL|ID_VENDOR|ID_FS_LABEL|ID_CDROM_MEDIA_TRACK_COUNT_AUDIO)=' )"
+ unset ID_FS_TYPE ID_MODEL ID_VENDOR ID_FS_LABEL ID_CDROM_MEDIA_TRACK_COUNT_AUDIO
+ eval "${_shortbytes}"
+ _fs="${ID_FS_TYPE}"
+ _model="${ID_MODEL}"
+ _vendor="${ID_VENDOR}"
+ _label="${ID_FS_LABEL}"
+ _atracks="${ID_CDROM_MEDIA_TRACK_COUNT_AUDIO}"
+ test -n "${DEBUG}" && echo "fs=${_fs} model=${_model} vendor=${_vendor} label=${_label} atracks=${_atracks}" 1>&2
+ test "${_fs}" = "" && test "${_atracks}" = "" && return # if the fs cannot be detected
+ touch "${_filepath}" ; chmod 0755 "${_filepath}"
+ {
+ echo "[Desktop Entry]"
+ echo "Version=1.1"
+ if test "${_fs}" = "udf" ; then
+ echo "Type=Application"
+ echo "Exec=xdg-open dvd:///dev/${_device}"
+ elif test -n "${_atracks}" ; then
+ test -n "${_label}" && _label="Audio CD"
+ echo "Type=Application"
+ echo "Exec=xdg-open cdda:///dev/${_device}"
+ else
+ echo "Type=Application"
+ echo "Exec=xdg-open ${AUTOMOUNT_BROWSEDIR}/${_device}"
+ echo "Path=${AUTOMOUNT_BROWSEDIR}/${_device}"
+ fi
+ if test -z "${_label}" ; then
+ echo "Name=${_vendor} ${_model}"
+ else
+ echo "Name=${_label}"
+ echo "GenericName=${_vendor} ${_model}"
+ fi
+ echo "Comment=${_device} (${_fs})"
+ case "${_fs}" in
+ "cd9600") echo "Icon=media-optical" ;;
+ "udf") echo "Icon=media-optical-dvd" ;;
+ "") echo "Icon=media-optical-audio" ;;
+ *) echo "Icon=media-removable" ;;
+ esac
+ } > "${_filepath}"
+}
+
+# INITIALIZE
+. ${MYA_PREFIX}/etc/myautomount.conf
+trap '__ec=$? ; clean_automount ; trap "" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20 ; exit ${__ec} ;' 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 19 20
+mkdir -p "${AUTOMOUNT_BASEDIR}"
+
+# MAIN
+# start udevadm
+udevadm monitor -u -s block 1> "${AUTOMOUNT_TMPFILE}" &
+export AUTOMOUNT_PID="${!}"
+test -n "${DEBUG}" && env | grep -E '^AUTOMOUNT_' 1>&2
+while ! test -e /tmp/kill-automount ;
+do
+ tail -F "${AUTOMOUNT_TMPFILE}" 2>/dev/null | while read line ;
+ do
+ handleEvent "${line}"
+ _length="$( wc -l < "${AUTOMOUNT_TMPFILE}" 2>/dev/null )" ; test -z "${_length}" && _length=0
+ test "${line}" = "$( tail -n1 "${AUTOMOUNT_TMPFILE}" )" && test ${_length} -gt 200 && reset_tmpfile
+ done
+ # the tail finished for some reason, so clear the file
+ reset_tmpfile
+done
diff --git a/src/usr/libexec/myautomount/myautomount-initialize.sh b/src/usr/libexec/myautomount/myautomount-initialize.sh
new file mode 100644
index 0000000..fc75d36
--- /dev/null
+++ b/src/usr/libexec/myautomount/myautomount-initialize.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+# Only needs to be run once. This is more for reference than for using. The package should handle these tasks.
+
+fail() {
+ echo "${@}" 1>&2
+ exit 1
+}
+
+setupSystem() {
+ _needrestart=0
+ mkdir -m0755 -p "${AUTOMOUNT_DIR}" || fail "Could not setup autofs rules! Check if this is being run as root?"
+ ! test -f "${AUTOMOUNT_FILE}" && {
+ { touch "${AUTOMOUNT_FILE}" && echo "* -fstype=auto,rw,nosuid,uid=${USER},gid=users :/dev/& " > "${AUTOMOUNT_FILE}" ; } || fail "Could not setup autofs rules! Check if this is being run as root?"
+ _needrestart=1
+ }
+ ! test -f "${AUTOMOUNT_DIR_FILE}" && {
+ { touch "${AUTOMOUNT_DIR_FILE}" && echo "${AUTOMOUNT_BROWSEDIR} ${AUTOMOUNT_FILE} --timeout=5 " > "${AUTOMOUNT_DIR_FILE}" ; } || fail "Could not setup autofs rules! Check if this is being run as root?"
+ _needrestart=1
+ }
+ test ${_needrestart} -eq 1 && eval "service autofs restart"
+}
+
+. ${MYA_PREFIX}/etc/myautomount.conf
+setupSystem
diff --git a/src/usr/libexec/myautomount/myautomount-trayicon.py b/src/usr/libexec/myautomount/myautomount-trayicon.py
new file mode 100755
index 0000000..257c54f
--- /dev/null
+++ b/src/usr/libexec/myautomount/myautomount-trayicon.py
@@ -0,0 +1,360 @@
+#!/usr/bin/python3
+# File: /usr/libexec/myautomount/automount-trayicon.py
+# License: CC-BY-SA 4.0
+# Author: bgstack15
+# Startdate: 2020-09-24 13:17
+# Title: Tray icon for automount daemon
+# Purpose: Easy access to autofs-mounted removable media
+# History:
+# Usage:
+# to be called from /usr/bin/myautomount-trayicon only, because it loads the environment variables
+# Reference:
+# https://gitlab.com/bgstack15/logout-manager/-/blob/master/src/usr/bin/logout-manager-trayicon
+# https://github.com/gapan/xdgmenumaker/blob/master/src/xdgmenumaker
+# https://github.com/seb-m/pyinotify/blob/master/python2/examples/stats_threaded.py
+# https://stackoverflow.com/questions/28279363/python-way-to-get-mounted-filesystems/28279434#28279434
+# timer https://python-gtk-3-tutorial.readthedocs.io/en/latest/spinner.html?highlight=timer#id1
+# Improve:
+# move out user configs to separate file in ~/.config/myautomount, but using the xdg spec.
+# add all headers
+# document
+# add option to HIDE WHEN NO MEDIA
+# Dependencies:
+# dep-devuan: autofs, python3-pyinotify
+# dep-fedora:
+# dep-pip: inotify
+# python-inotify, the seb-m one.
+# And whatever provides Gtk 3.0 for python3.
+
+import gi, os, fnmatch, sys, pyinotify, time, subprocess
+gi.require_version("Gtk","3.0")
+from gi.repository import Gtk, Gdk, GLib
+import xdg.DesktopEntry as dentry
+import xdg.Exceptions as exc
+
+showmount = 1 # show the "MOUNTED" value for each drive
+skip_sd_without_partitions = 1 # skip sdb sdc, etc. # NOT IMPLEMENTED YET.
+only_update_on_menuitem = 0 # if 1, then disables the mouseover poll. Prevents showmount=1 from working.
+
+# load environment variables
+AUTOMOUNT_BASEDIR = os.getenv("AUTOMOUNT_BASEDIR") # probably /run/user/${UID}/automedia
+if not AUTOMOUNT_BASEDIR:
+ print("Need env var AUTOMOUNT_BASEDIR. Aborted.",file=sys.stderr)
+ sys.exit(1)
+AUTOMOUNT_BROWSEDIR = os.getenv("AUTOMOUNT_BROWSEDIR") # probably /browse
+if showmount and not AUTOMOUNT_BROWSEDIR:
+ print("Without AUTOMOUNT_BROWSEDIR, showmount option cannot be used. Continuing...",file=sys.stderr)
+
+# FUNCTIONS
+
+class App:
+ '''
+ A class to keep individual app details in.
+ '''
+
+ def __init__(self, name, icon, command, path):
+ self.name = name
+ self.icon = icon
+ self.command = command
+ self.path = path
+
+ def __repr__(self):
+ return repr((self.name, self.icon, self.command, self.path))
+
+class MenuEntry:
+ '''
+ A class for each menu entry. Includes the class category and app details
+ from the App class.
+ '''
+
+ def __init__(self, category, app):
+ self.category = category
+ self.app = app
+
+ def __repr__(self):
+ return repr((self.category, self.app.name, self.app.icon, self.app.command, self.app.path))
+
+class Identity(pyinotify.ProcessEvent):
+ activity_count = 0
+ def process_default(self, event):
+ print(self,"Does nothing with the event number",self.activity_count)
+ self.activity_count += 1
+ #print(event)
+
+def remove_command_keys(command, desktopfile, icon):
+ # replace the %i (icon key) if it's there. This is what freedesktop has to
+ # say about it: "The Icon key of the desktop entry expanded as two
+ # arguments, first --icon and then the value of the Icon key. Should not
+ # expand to any arguments if the Icon key is empty or missing."
+ if icon:
+ command = command.replace('"%i"', '--icon {}'.format(icon))
+ command = command.replace("'%i'", '--icon {}'.format(icon))
+ command = command.replace('%i', '--icon {}'.format(icon))
+ # some KDE apps have this "-caption %c" in a few variations. %c is "The
+ # translated name of the application as listed in the appropriate Name key
+ # in the desktop entry" according to freedesktop. All apps launch without a
+ # problem without it as far as I can tell, so it's better to remove it than
+ # have to deal with extra sets of nested quotes which behave differently in
+ # each WM. This is not 100% failure-proof. There might be other variations
+ # of this out there, but we can't account for every single one. If someone
+ # finds one another one, I can always add it later.
+ command = command.replace('-caption "%c"', '')
+ command = command.replace("-caption '%c'", '')
+ command = command.replace('-caption %c', '')
+ # replace the %k key. This is what freedesktop says about it: "The
+ # location of the desktop file as either a URI (if for example gotten from
+ # the vfolder system) or a local filename or empty if no location is
+ # known."
+ command = command.replace('"%k"', desktopfile)
+ command = command.replace("'%k'", desktopfile)
+ command = command.replace('%k', desktopfile)
+ # removing any remaining keys from the command. That can potentially remove
+ # any other trailing options after the keys,
+ command = command.partition('%')[0]
+ return command
+
+def icon_strip(icon):
+ # strip the directory and extension from the icon name
+ icon = os.path.basename(icon)
+ main, ext = os.path.splitext(icon)
+ ext = ext.lower()
+ if ext == '.png' or ext == '.svg' or ext == '.svgz' or ext == '.xpm':
+ return main
+ return icon
+
+def get_entry_info(desktopfile, ico_paths=True):
+ # customized from gapan/xdgmenumaker
+ de = dentry.DesktopEntry(filename=desktopfile)
+
+ name = de.getName().encode('utf-8')
+
+ if True:
+ icon = de.getIcon()
+ # full resolution of path is not required in this GTK program.
+ #if ico_paths:
+ # icon = icon_full_path(icon)
+ #else:
+ # icon = icon_strip(icon)
+ else:
+ icon = None
+
+ command = de.getExec()
+ command = remove_command_keys(command, desktopfile, icon)
+
+ terminal = de.getTerminal()
+ if terminal:
+ command = '{term} -e {cmd}'.format(term=terminal_app, cmd=command)
+
+ path = de.getPath()
+ if not path:
+ path = None
+
+ categories = de.getCategories()
+ category = "Removable" # hardcoded xdg "category" for automount-trayicon
+
+ app = App(name, icon, command, path)
+ mentry = MenuEntry(category, app)
+ return mentry
+
+def desktopfilelist(params):
+ # Ripped entirely from gapan/xdgmenumaker
+ dirs = []
+ for i in params:
+ print("Checking dir",i)
+ i = i.rstrip('/')
+ if i not in dirs:
+ dirs.append(i)
+ filelist = []
+ df_temp = []
+ for d in dirs:
+ xdgdir = '{}/'.format(d) # xdg spec is to use "{}/applications" as this string, so use an applications subdir.
+ if os.path.isdir(xdgdir):
+ for root, dirnames, filenames in os.walk(xdgdir):
+ for i in fnmatch.filter(filenames, '*.desktop'):
+ # for duplicate .desktop files that exist in more
+ # than one locations, only keep the first occurence.
+ # That one should have precedence anyway (e.g.
+ # ~/.local/share/applications has precedence over
+ # /usr/share/applications
+ if i not in df_temp:
+ df_temp.append(i)
+ filelist.append(os.path.join(root, i))
+ return filelist
+
+class MainIcon(Gtk.StatusIcon):
+ def __init__(self):
+ Gtk.StatusIcon.__init__(self)
+ self.set_from_icon_name("media-removable")
+ self.traymenu = Gtk.Menu()
+ self.connect("button-press-event", self.on_button_press_event)
+ self.connect("popup-menu", self.context_menu)
+ self.reestablish_menu()
+ if not only_update_on_menuitem:
+ self.connect("query-tooltip", self.mouseover)
+ # need these anyway, for when the icon is hidden.
+ self.wm1 = pyinotify.WatchManager()
+ self.s1 = pyinotify.Stats()
+ self.identity=Identity(self.s1) # provides self.identity.activity_counter which increments for every change to the watched directory
+ self.activity_count=0 # the cached value, for comparison. Update the menu when this changes!
+ self.notifier1 = pyinotify.ThreadedNotifier(self.wm1, default_proc_fun=self.identity)
+ self.notifier1.start()
+ self.wm1.add_watch(AUTOMOUNT_BASEDIR, pyinotify.IN_CREATE | pyinotify.IN_DELETE, rec=True, auto_add=True)
+
+ def mouseover(self, second_self, x, y, some_bool, tooltip):
+ #print("Mouseover happened at",str(x)+",",y,tooltip, some_bool)
+ need_showmount=0
+ if self.identity.activity_count != self.activity_count:
+ # then we need to update menu
+ print("Mouseover, and files have changed!")
+ self.activity_count = self.identity.activity_count
+ need_showmount=1
+ if need_showmount or showmount:
+ self.reestablish_menu()
+
+ def reestablish_menu(self,widget = None, silent=False):
+ if not silent:
+ print("Reestablishing the menu")
+ try:
+ if self.menuitems:
+ del self.menuitems
+ except:
+ pass
+ try:
+ for i in self.traymenu.get_children():
+ self.traymenu.remove(i)
+ except:
+ pass
+ self.menuitems = []
+ for entry in get_desktop_entries([AUTOMOUNT_BASEDIR]):
+ if not silent:
+ print('{} {} {} {}'.format(entry.app.name.decode("utf-8"),entry.app.icon,entry.app.path,entry.app.command))
+ self.add_menuitem(str(entry.app.name.decode("utf-8")),entry.app.path,entry.app.icon,self.execute,entry.app.command)
+ self.add_separator_to_menu()
+ if only_update_on_menuitem:
+ self.add_menuitem("Update menu","",None,self.reestablish_menu,"re establish") # If you want a menu option for this
+ self.add_menuitem("Hide until next disk change","","",self.hide,"hide")
+ self.add_menuitem("Exit automount-trayicon","","system-logout",self.exit,"exit")
+ self.menuitem_count = len(self.menuitems) - 2
+ self.set_tooltip_text(str(self.menuitem_count)+" mount point"+("s" if self.menuitem_count > 1 else ""))
+
+ def execute(self, widget):
+ x=0 ; y=-1
+ for n in widget.get_parent().get_children():
+ x+=1
+ if widget == n:
+ y=x-1
+ break
+ if y > -1:
+ print(y,self.menuitems[y])
+ # use subprocess to open in background so failure to mount dialogs do not block the program
+ subprocess.Popen(self.menuitems[y].split())
+ #os.system(self.menuitems[y])
+ else:
+ print("Some kind of error?! How did this get called without a menu entry.")
+ #print(repr(self.s1))
+
+ def add_separator_to_menu(self):
+ i=Gtk.SeparatorMenuItem.new()
+ i.set_visible(True)
+ self.traymenu.append(i)
+
+ def add_menuitem(self,label_str,label_paren_str,icon_str,function_func,action_str):
+ self.menuitems.append(action_str)
+ full_label_str=label_str
+ mounted_str=""
+ # collection = [line.split()[1] for line in open("/etc/mtab") if line.split()[1].startswith('/browse') and line.split()[2] != "autofs"]
+ if label_paren_str is not None and label_paren_str != "":
+ if showmount and AUTOMOUNT_BROWSEDIR:
+ if label_paren_str in [line.split()[1] for line in open("/etc/mtab") if line.split()[1].startswith(AUTOMOUNT_BROWSEDIR) and line.split()[2] != "autofs"]:
+ label_paren_str = "MOUNTED " + label_paren_str
+ full_label_str += " (" + label_paren_str + ")"
+ i = Gtk.ImageMenuItem.new_with_mnemonic(full_label_str)
+ j = Gtk.Image.new_from_icon_name(icon_str,32)
+ j.show()
+ i.set_image(j)
+ i.set_always_show_image(True)
+ i.show()
+ i.connect("activate", function_func)
+ self.traymenu.append(i)
+
+ def on_button_press_event(self, b_unknown, event: Gdk.EventButton):
+ # for some reason the single click functionality prevents the double click functionality
+ if Gdk.EventType._2BUTTON_PRESS == event.type:
+ # not achievable if we are popping up the menu in the single click.
+ print("Double click")
+ else:
+ print("Single click")
+ self.traymenu.popup(None, None,
+ self.position_menu,
+ self,
+ event.button,
+ Gtk.get_current_event_time())
+
+ def context_menu(self, widget, event_button, event_time):
+ self.traymenu.popup(None, None,
+ self.position_menu,
+ self,
+ event_button,
+ Gtk.get_current_event_time())
+
+ def hide(self, widget = None):
+ print("Please hide self!")
+ self.set_visible(False)
+ self.start_timer()
+
+ def start_timer(self):
+ """ Start the timer """
+ self.counter = 1000 # milliseconds, but hardly a precise timer. Do not use this for real calculations
+ self.timeout_id = GLib.timeout_add(1,self.on_timeout, None)
+
+ def stop_timer(self, reason_str):
+ if self.timeout_id:
+ GLib.source_remove(self.timeout_id)
+ self.timeout_id = None
+ #print("Timer was stopped!")
+ self.check_reenable_now()
+
+ def check_reenable_now(self):
+ print("Need to check if anything has changed.")
+ print(self.identity.activity_count,self.activity_count)
+ if self.identity.activity_count != self.activity_count:
+ self.set_visible(True)
+ self.activity_count = self.identity.activity_count
+ self.reestablish_menu()
+ else:
+ print("No changes...")
+ self.hide()
+
+ def on_timeout(self, *args, **kwargs):
+ """ A timeout function """
+ self.counter -= 1
+ if self.counter <= 0:
+ self.stop_timer("Reached time out")
+ return False
+ return True
+
+ def exit(self, widget):
+ self.notifier1.stop()
+ quit()
+
+ def __quit__(self):
+ self.notifier1.stop()
+ Gtk.StatusIcon.__quit__(self)
+
+# Normally call get_desktop_entries with (["/media"])
+def get_desktop_entries(dir_array):
+ entries = []
+ for desktopfile in desktopfilelist(dir_array):
+ try:
+ entry = get_entry_info(desktopfile, True)
+ if entry is not None:
+ #print(entry)
+ entries.append(entry)
+ except exc.ParsingError as Ex:
+ print(Ex)
+ pass
+ return entries
+
+# MAIN
+icon = MainIcon()
+Gtk.main()
bgstack15