File: android-tkinter/CODE/launch-mergeall-GUI.pyw
#!/usr/bin/python
"""
================================================================================
launch-mergeall-GUI.pyw:
desktop GUI launcher (part of the mergeall system)
-Copyright and author: © M.Lutz, 2014-2019 (http://learning-python.com)
-License: provided freely, but with no warranties of any kind
#-------------------------------------------------------------------------------
# ANDROID VERSION, January-April 2019
# Per http://learning-python.com/mergeall-android-scripts/_README.html#toc8.
#
# Replace original file with this custom version on your phone (only).
# See "# ANDROID" for all changes applied here and in mergeall_configs.py.
# These changes are for Android only, do not impact the command-lines mode
# of Mergeall, and might be merged into the original file in a later release.
#
# Recent changes (search here for their [date] labels to find changed code):
#
# [Apr2119] Pydroid 3 3.0 broke webbrowser: use os.system(cmd) with a
# hardcoded Android activity-manager command line instead
# (3.0's $DISPLAY breaks module, $BROWSER kills "file://").
#
# [Apr1919] Fix merge-spawn workaround to the Pydroid 3 empty sys.executable
# bug, to accommodate the different Python path in Pydroid 3's 3.0.
# The fix reads a spawned 'which python' command to be path agnostic.
# Also go back to using webbrowser for help and logfiles: it does
# work, but iff files use '"file://" and HTML docs use online URLs;
# see _openbrowser.py for a demo and more details.
#
# [Apr1219] Reenable help and popup-logfile buttons, and fix them to open with
# an os.system() spawn of an activity-manager command instead of Py's
# webbrowser; open online version of help, for its latest changes.
# Also use default color for "Help" - tkinter loses a custom bg.
#
# [Mar2819] Shorten label text again and further, for smaller phone displays.
# Labels now fit >= 5.5" screens; if still too wide, set the new
# LABELFONT user config, or search for "# ANDROID - shorter" to edit.
# Also for smaller displays, resplit run-dialog text (but not paths).
#
# [Mar2319] Made message text readonly to avoid keyboard popups on slow swipes.
# This precludes copy/paste, but the on-screen keyboard is annoying.
# Also manually split run-dialog text (but Tk still truncates paths).
#
# [Mar0819] Added folder-chooser prefills and starts as config-file options.
# These can often avoid the tedious Android Tk folder-chooser dialog.
#-------------------------------------------------------------------------------
A portable Python 3.X/2.X tkinter/Tkinter desktop GUI, for easily launching
mergeall.py's -report and -auto (but not selective/interactive) usage modes.
For screenshots of this GUI in action, see the examples/Screenshots folder.
Usage: This is a .pyw -- it runs with no console on Windows. Run this script
with no arguments, via icon clicks, command lines, or other. Drag it out to
a desktop shortcut for quick access. See folder docetc/launcher-configs for
a Windows desktop icon (attach it to a shortcut via right-click + Properties).
Uses widgets for choosing options and directory paths and viewing scrollable
mergeall stdout/stderr output, and supports saving and viewing mergeall's
output in log files. Uses threads to stay active while waiting for mergeall
output lines. Stays open to support multiple mergeall runs in one session --
report differences, run automatic updates, report on results, etc. The
underlying mergeall.py was not changed: this is just a GUI shell.
This is easier than launch-mergeall-Console.py (no directory typing, and
quicker options selection), which is easier than running raw mergeall.py
command lines directly and manually from a console or shortcuts, though
mergeall.py command lines support more options (e.g., interactive mode).
As of version 1.4, this also uses threading to always remain responsive.
As of version 2.0, this also supports the mergeall auto-backups option.
As of version 3.0, this also supports mergeall's "-skipcruft" and Mac OS X.
See below for major changes made here, as well as open issues (TBDs).
--------------------------------------------------------------------------------
VERSION 3.1 CHANGES:
These were internals and command-line changes, and had no GUI impact.
VERSION 3.0 CHANGES:
This major release's changes were largely driven by a Mac OS X port,
and new usage on Linux. Search for 'RunningOnMac' and '3.0' for Mac-ness.
Mergeall grew cruft file, symlink, and Windows long-path support in 3.0,
but most of these are not related to the GUI managed here.
App bar icon for Linux:
Linux now gets a nice mergeall app bar (launcher) icon. Mac icons
remain a TBD, and Windows has always had window icons for this program.
Initial desktop logs folder for both Linux and Mac:
The preset default for the logs folder now uses $HOME settings on these
Unix platforms (Windows uses the user's desktop as before).
Redesign run verification dialog for Linux and Mac:
The dialog popped up just before a run is launched was redesigned to
make it more readable on Mac and Linux. Its text was formerly fine
on Windows, but fairly bunched-up elsewhere. Also specialized the
warning text to be less dire if backups are enabled.
Top-level window hack for Mac Tk:
The Mac port required a top-level window hack to show buttons in Mac
active-window style initially, using the recommended Tk 8.5 install.
This is a complex story; see __main__ code in this file for details.
New toggle to suppress comparison lines, all platforms:
The Mac port also inspired a new GUI toggle to suppress folder comparison
message scrolling in the GUI (only). This is on initially for Mac because
the Mac Tk Text widget is VERY slow: ~30x slower than the mergeall process's
output. This toggle is off initially for Windows and Linux, because the GUI
largely keeps up on these platforms, and the messages indicate progress.
Still, the new toggle is available on these platforms too, because the GUI's
scrolling can add a few seconds to mergeall runtime in some tests run on
Windows, and may be a factor on slower machines; disable as desired.
Either way, full details, including all comparison messages, are always
available in saved log files popped up automatically at the end of a run.
Note that this new toggle differs from 2.4's "-quiet" option and GUI toggle
described below: "-quiet" disables output in mergeall itself before it ever
reaches the GUI, because backup messages are arguably distracting in logs
too; comparison messages are still useful in logs, if not the GUI.
Unlike all other widgets (except the new post-run popup toggle), this toggle
is also dynamic: setting it on/off while mergeall is running hides/shows
messages currently being generated. The 'skipping' message is displayed
every time this toggle is turned back on, though it does not appear until
the next mergeall message is received.
TBD: Mac text scrolls still seem painfully slow when there are many
difference-report lines. These could be also be suppressed, in addition
to folder comparison messages, by: line.startswith(('comparing', '[', ' ')).
This was ruled out, as it makes report-mode runs useless. Text speed must
be fixed by Tk's Mac developers (though one report on it went unreplied).
UPDATE: see docetc/miscnotes/mac-weirdly-slow-tk85-text-scroll.py for a
simple self-contained demo of the text scrolling slowness on ActiveState
Tk 8.5.18 (the Tk recommended by python.org), Python 3.5, and OS X 10.11.
It finishes scrolling in just 3-4 seconds on Windows, but 85 on Mac!
UPDATE: recent heroic efforts at speeding Mac Text widget scrolling came
up short. It's simply slow in all Mac Tks tested - 8.5.9 through 8.5.18.
The culprit seems to be update() calls required to redraw the widget after
new text is inserted. The code used to scroll uses normal techniques:
statustxt.insert(END, line) # add to Text widget
statustxt.see(END+'-2l') # reposition text
statustxt.update() # update display from within a loop
1) Alternative scroll techniques tried had no effect on speed:
statustxt.yview_scroll(2, 'units')
statustxt.yview_moveto(1.0)
statustxt.yview(END)
2) Using update_idletasks() speeds scrolls only slightly, and all the GUI's
controls are unresponsive while the text scrolls (an unacceptable effect):
statustxt.update_idletasks()
3) The after() timer loop's speed proved irrelevent, as the code mostly
stays in the 'batch' inner loop, and doesn't rescedule after() events:
statustxt.after(10, streamconsumer, linequeue, logfile, logpath)
4) The update() call can be avoided by processing just 1 line per after()
event (instead of the batch inner loop) with a very low delay count, but
this has NO impact on scroll speed - implicit updates are also too slow:
statustxt.after(1, streamconsumer, linequeue, logfile, logpath)
5) This leaves disabling scrolls (the new toggle), or updating only after
groups of lines are addded, which makes scrolling too jumpy and chaotic:
global upcnt
upcnt += 1
if upcnt % 50 == 0:
statustxt.update()
In the end, this is a Mac Tk bug, which hopefully is or will be addressed
in later Tks on the Mac. The new toggle is mergeall's best workaround.
New script to delete Mac ".*" cruft files:
The Mac platform has a habit of creating lots of metadata files and a few
folders, whose names all start with a ".", and which are sometimes treated
as hidden. These have meaning and purpose on a Mac, but are useless
noise on other platforms. For mergeall users on Windows and Linux whose
archive might become infected with these files by an association with a
Mac, a new script, "nuke-cruft-files.py," is provided to remove such files
as a pre-step or post-step to mergeall runs that copy archives off a Mac
See that script's extensive docstring for more details.
Disable widgets instead of erasing them, all platforms:
Also partly for the Mac, the GUI was redesigned to enable/disable widgets
as they fall in and out of relevance, instead of drawing/erasing them with
pack() and pack_forget(). The latter scheme causes a noticeable flash on a
Mac, but may have been too dramatic in general. The "GO" button didn't
cause flash, but enabling/disabling worked around an old text scroll issue.
New mode and toggle to skip system cruft files in both TO and FROM
Inspired by the numerous ".*" files added to archives on Mac OS X, both
mergeall and diffall grew a "-skipcruft" command line argument and mode,
which skips known cruft files of all platforms in both FROM and TO. The
GUI also grew a new toggle to suport this new mode and argument.
When enabled, the net effect is that cruft files do not register as
differences in report runs, and are not copied to, removed from, or replaced
in the TO tree in update runs. Such files thus stay on their generating
platform only - they won't be transferred to intermediate drives and other
computers, and won't be deleted from the generating platform by future
merges.
This option can be disabled, because users of a single platform may not
care about their cruft, and some crufts may be more useful than others.
Cruft files are defined in the mergeall_config.py file; users are
encouraged to adjust the set of matching files as needed for their use.
Sanitize Unicode characters in message-scroll text outside Tk's BMP range
Through 8.6, at least, Tk cannot display Unicode characers whose code
points are outside range U+0000..U+FFFF (BMP, UCS-2). Passing these to
Tk raises an uncayght exception which leaves the GUI in an unpredictable
state (usually half-drawn or hung). To work around this, replace all
such characters with the standard Unicode replacement character, which
renders as a generic indicator. This limit may be lifted in Tk 8.7.
Update: also an issue for pathnames in Browse folder dialogs, fixed here.
More descriptive GUI labels and popups
Toggle labels and ppups were given more explicit labels for clarity.
Allow editor popup to be disabled: config setting, new toggle
The automatic popup of a text editor to view a saved mergeall log file
can now be dsabled by a setting in mergeall_configs.py (on by default).
The popup may seem overkill to users who require a view-only display.
UPDATE: this is now switchable on/off by a new toggle in the GUI itself.
The configs-file setting is retained, but used only for an intial value
for the new toggle. The popup is normally unused, and GUI clicks are
easier than config edits. This new toggle is dynamic, like the message
scroll disabler; you can click it during a run to impact the popup.
Configurable message text display area
Users can now tailor the color, font, and initial size of the GUI's
scrolled display area for mergeall messages, in mergeall_configs.py.
The GUI's cosmetics were also polished in general along the way.
Mac changes to open dialogs (Browse for folders TO, FROM, Logs)
On Macs, use "message" ("title" is ignored), and add slide-down sheet
style (versus popup window) via "parent=root" if configs file setting.
Save dialogs seem to post titles correctly. Mac's standard menu is
customized here too, even though this program doesn't have one per se.
mergeall and cpall: copy unix symlinks, don't follow
Avoid redundant data copies. See mergeall.py and cpall.py for details.
Refocus on window after standard/common dialogs for Mac
Run a root.focus_force() after all standard dialogs used here to force
focus and active styling to be reset after dialog is closed. Else, Mac
users must click window after dialogs. This may be AS Tk 8.5 only; has
no effect on the initial style issue discussed earlier; and parent=root
doesn't suffice to reset focus in the standard dialogs used here.
TBD: should these dialogs also be made slide-down sheets on Mac like
the open/save diaogs via parent=root, instead of modal popup windows?
Some Mac purists may vote yea, but mergeall currently sides with variety,
especially given that the mergeall GUI is a single-window interface.
mergeall output forced to ASCII in PyInstaller Windows exes
A continuation of a long-running theme here, mergeall now forces stdout
text to be ASCII when running as a frozen executable on Windows ONLY.
See mergeall.py for details; this works around a likely PyInstaller bug.
--------------------------------------------------------------------------------
VERSION 2.4 CHANGES:
Added a toggle for the new "-quiet" script option that turns off per-file
"...backing up" log message printing in the underlying mergeall program.
These messages may make reading the log more work than it should be, and
don't add much information once backups are understood. The new "-quiet"
toggle in the GUI is active only when backups are enabled. Also tweaked
some GUI labels' text for clarity.
VERSION 2.2 and 2.3 CHANGES:
These versions were optimizations and patches, and had no impact on this GUI.
VERSION 2.1 CHANGES:
This version's changes were command-line only, and had no effect on this GUI.
--------------------------------------------------------------------------------
VERSION 2.0 CHANGES:
1) Added Backups toggle and corresponding mergeall -backup command-line arg.
The new backups frame retains state but is shown/hidden when -auto is
selected/deselected, as it's applicable to -auto only here (not -report).
In the console launcher, -backup is applicable to both -auto and
selective (=[not -report]) modes. Defaults to enabled in the GUI.
2) Moved log-file path chooser widgets down to log-file toggle button section.
The chooser retains state but is now shown/hidden when logging is toggled
on/off, as it's applicable when logging only.
3) Added "help" button that pops up the main usage doc in a web browser.
Packed in the run-mode frame's unused space.
4) The new "finished\n\n" message issued by mergeall solves the issue here
where the last output line was covered by repacking the GO button after resizes.
This works better, as it doesn't require the text display to scroll abruptly.
5) Use more descriptive text for the mode radio buttons (formerly was just
not "Report Only" and "Automatic Updates").
6) Error-check the FROM and TO paths here before trying to run mergeall, so
produces a GUI popup instead of mergeall text output message (log file path
was already being checked this way, because it required an open here).
7) Default to Desktop for log files on Windows (initial setting value only).
--------------------------------------------------------------------------------
VERSION 1.7.1 CHANGES:
None here (usage note, mergeall error message format change: see Revisions.html).
VERSION 1.7 CHANGES:
1) Minor bug fix for 2.X only -- add import of Tkinter's showerror when
using 2.X, else dialog never appears if bad log-file name.
2) Catch PermissionError (etc.) on log file open and report error in popup;
else fails silently on Windows, as ".pyw" has no console for exception text.
--------------------------------------------------------------------------------
VERSION 1.6 CHANGES: a new Python 2.X Unicode workaround, verify quits
Decoding an output line read from the spawned mergeall.py stream can fail
rarely in Python 2.X (only), if there are non-ASCII filename characters
in the line. This failure does not occur in 3.X. Patched to catch the
decode error and recover, with a notation at the front of the line in the
GUI. This change impacts text in the GUI display only; it does not impact
text in the log file, or the underlying mergeall process's run. Also added
a verify dialog for the window close button to avoid accidental exits.
[Discussion] The Unicode decode failure seems a fundamental 3.X/2.X
behavior difference. After initial research, the most likely culprits
appear to be:
a) The 2.X subprocess module mutates non-ASCII stream lines in transit.
b) The 2.X print statement generates either already-decoded text, or
wrongly-encoded bytes that do not respect the PYTHONIOENCODING setting.
In either case, this may reflect the fact that there is no notion of a
truly binary bytes stream in 2.X -- the content of subprocess output is
not as clearly defined as in 3.X's strict text/bytes dichotomy worldview.
Note that setting PYTHONIOENCODING in the shell does not fix this failure.
This environment variable is used for prints to the console, but this GUI
does only screen updates and log file writes itself, and exports this
setting automatically to the mergeall subprocess for its prints. Testing
verifies that os.environ settings are inherited by 2.X subprocesses too,
even when spawned by the subprocess module.
Whatever the exact cause, this is a fundamental 2.X/3.X difference and
another 2.X/3.X semantic incompatibility that goes well beyond syntax.
In this case, adding an exception handler around the offending decode
fixed the failure for all cases tested in 2.X, so this issue is closed.
Given the other 2.X/3.X incompatibilities found and addressed in this
project, though, it seems that writing dual-version 2.X+3.X code may be
a bit of a pipedream when it comes to realistic, practical programs.
If in doubt, try running mergeall on Python 3.X instead of 2.X.
Also note that this patch applies only to the GUI launcher: PYTHONIOENCODING
must still generally be set manually in your system shell when running either
the console launcher or script mergeall.py directly from a command line, if
either may ever process and thus print non-ASCII filenames. Else, such
filenames may cause both scripts to abort, especially in Python 3.X. The
GUI launcher does not require this setting, as it automatically sets and
propagates this variable to its mergeall.py subprocess, and never prints.
[3.0] UPDATE: for its PyIstaller frozen executable ONLY, mergeall must force
stdout text to ASCII, which isn't ideal, but is requied in this context to
avoid exceptions, and sidesteps this entire display-only issue.
--------------------------------------------------------------------------------
VERSION 1.5 CHANGES: Linux port and usage
Pass shell=False on Linux/Unix (only) to subprocess.Popen, else starts a
"python" interactive shell even though a full command sequence is passed.
Linux users: see also release 1.5 notes in docs/Revisions.html for "#!"
pointers, and a possible NTFS timestamp skew issue for Windows/Linux
cross-platform syncs.
--------------------------------------------------------------------------------
VERSION 1.4 CHANGES: threads, streams, log files
1) Threading
Add threading for the mergeall subprocess stream reader, a former TBD.
This structure is more complex -- it trades a simple loop for two functions
and multiple levels of loops -- but prevents the GUI from becoming blocked
and unresponsive while waiting for a next line from the subprocess.
This was normally not an issue: the GUI updates after each new line, and is
used just for viewing after starting a mergeall run (and it's "GO" button is
erased during this process to avoid overlapping runs). However, blocked
states are not natural in GUIs, and they can become apparent here if mergeall
is busy copying large trees. See book (PP4E) for more details and examples
of GUI threading.
2) Stream Unicode decoding, take 2
Force UTF8 encoding for prints in mergeall subprocess via PYTHONIOENCODING,
and use binary mode Popen stream reads + manual post-read UTF8 decoding here.
Version 1.2 formerly made the subprocess's streams encoding match Popen's
expectation (cp1252 on Windows) by using locale.getpreferredencoding(False);
that works for reading the stream, but not for prints within mergeall itself.
3) 2.X log-file compatibility
Use binary mode for the log files, writing the now-binary stream data.
Former versions allowed for non-ASCII filenames in the log file text by
using text mode and writing characters as UTF8, but the 2.X codecs.open()
doesn't expand \n the same as 3.X's text-mode open() (see also next item).
4) 2.X unbuffered streams compatibility
Temporarily dropped '-u' in mergeall spawn command-line, as it makes eolns
(linebreak character sequences) \n in 2.X, but \r\n in 3.X. This causes
issues in log-file writes: without a \r\n on Windows, files are single lines.
LATER UPDATE: the Python '-u' unbuffered flag has been reinstated. Without
it, mergeall output may not appear for 10 or more seconds on some machines
and slower devices due to internal buffering. Because this flag also makes
line-breaks differ between Python 2.X and 3.X, though, also need to use
special-case log-file writes to map all linebreaks to the platform's version.
This is a complex 3.X/2.X compatibility issue, involving -u, Popen, and opens.
Neither Popen text mode streams nor the 2.X codecs.open() will help. The
former can't be used because its internal encoding policy (per locale) is
not broad enough to handle arbitrary Unicode filenames, and the latter always
opens in binary mode, and so does no translations of linebreaks in the decoded
text (3.X's text-mode open() does expand linebreaks by default).
Could write binary lines in 2.X text-mode open() to expand \n on Windows,
but that won't work in 3.X -- its text-mode open() expects Unicode strings,
and always does encoding in addition to linebreaks. Writing manually
encoded text via open() in 3.X and codecs.open() in 2.X also won't work:
2.X requires manual \r\n, which 3.X will by default expand to \r\r\n.
3.X open() supports a 'newline' argument to turn off \n expansion, but
this can't be used either, as it's not available in 2.X's codecs.open().
As in, *punt*: write binary data with manual \n mapping for log files.
--------------------------------------------------------------------------------
TBD 1: No selective/interactive mode in GUI
mergeall's interactive mode is unavailable here as is, due to an outstanding
TBD regarding handling and interleaving of stdout for input() prompts in spawned
Python 3.X processes. In practice, though, the -report and -auto modes have been
the only modes regularly used. See launch-mergeall-Console.py for details on the
issue (the console launcher supports interactive mode, but without a log file).
TBD 2: 2.X compatibility for Unicode filenames?
This system works well on 3.X (it's main usage platform) and is largely 2.X
compatible, but the launchers may still have issues in some stream decoding
for non-ASCII filenames. No such encoding exceptions occur on 2.X for the
raw mergall.py script, though some non-ASCII os.listdir results seem a bit
suspect in 2.X too. More complete 2.X testing remains suggested exercise.
--> Update: see also the 1.6 decoding change note above. <--
TBD 3: Decoupled versus single-process models?
The launchers currently use a standard decoupled model that spawns mergeall
and reads and decodes its streams. There may be advantages to using a
single-process model that instead imports and calls mergeall's functions.
See docs\Lessons-Learned.html for more discussion on this alternative.
================================================================================
"""
#from __future__ import print_function # 2.X compatibility: not needed here
APPNAME = 'mergeall'
VERSION = 3.1
# [3.0] for frozen app/exes, fix module+resource visibility (sys.path)
import fixfrozenpaths
import sys
if sys.version[0] >= '3': # Py 3.X, but allow for Py 4.X too [3.0]
import _thread, queue
from tkinter import *
from tkinter.messagebox import askokcancel, showinfo, showerror
from tkinter.filedialog import Directory # saves last dir
from tkinter.scrolledtext import ScrolledText
else:
import thread as _thread, Queue as queue # Py 2.X compatibility
from Tkinter import *
from tkMessageBox import askokcancel, showinfo, showerror # [1.7]
from tkFileDialog import Directory
from ScrolledText import ScrolledText
#import codecs
#open = codecs.open # [1.4] log binary mode from stream, not text files
# this script isn't too platform-specific, but avoid repeating this
RunningOnMac = sys.platform.startswith('darwin')
RunningOnWindows = sys.platform.startswith('win')
RunningOnLinux = sys.platform.startswith('linux')
import webbrowser, subprocess, time, os
# [3.0] data+scripts not in os.getcwd() if run from a cmdline elsewhere,
# and __file__ may not work if running as a frozen PyInstaller executable;
# use __file__ of this file for Mac apps, not module: it's in a zipfile;
MYDIR = fixfrozenpaths.fetchMyInstallDir(__file__) # absolute
# [3.0] new doc, in this script's folder - but not necessarily '.' (cwd)
HELPFILE = os.path.join(MYDIR, 'UserGuide.html')
# [3.0] Mac OS X is pickier about file URLs
if RunningOnMac:
HELPFILE = 'file:' + HELPFILE
# ANDROID [Apr1219] - open latest online version of user guide (all platforms should)
HELPURL = 'https://learning-python.com/mergeall-products/unzipped/UserGuide.html'
# [3.0] part of PP4E's guimaker module, copied here to avoid dependency
from guimaker_pp4e import fixAppleMenuBar
# [3.0] user configs: scrolled messages text area, log-file popup;
# for GUI settings, None = use Tk defaults: 24 lines high, 80 chars wide;
try:
from mergeall_configs import (
LOGEDITORPOPUP,
DEFAULTLOGDIR,
MACSLIDEDOWN,
TEXTAREAHEIGHT, TEXTAREAWIDTH, TEXTAREAFONT, TEXTAREACOLOR,
# ANDROID [Mar0819]
DEFAULTFROMDIR, DEFAULTTODIR, # entry prefills: see usage ahead
BROWSELOGDIR, BROWSEFROMDIR, BROWSETODIR, # chooser-dialog starts: ditto
# ANDROID [Mar2819]
LABELFONT, # font if labels too wide to fit
HEADERFONT) # custom font for section headers
except Exception as why:
# if any fail, all default (brutal, but simple)
LOGEDITORPOPUP = True # default: initial value for log-file popup toggle
DEFAULTLOGDIR = None # default: Desktop folder?, per running platform
MACSLIDEDOWN = False # default: popup, not sheet, for folder dialogs
TEXTAREAHEIGHT = 20 # initial number lines in message scroll widget
TEXTAREAWIDTH = None # initial number characters per line, wrapped
TEXTAREAFONT = None # scrolled messages font, None=Tk default font
TEXTAREACOLOR = None # scrolled messages color(s), None=Tk default font
# ANDROID [Mar0819]
DEFAULTFROMDIR = None # default: always Browse in GUI (entry prefill)
DEFAULTTODIR = None # ditto
BROWSELOGDIR = None # default: use tkinter start default (chooser dialog)
BROWSEFROMDIR = None # ditto
BROWSETODIR = None # ditto
# ANDROID [Mar2819]
LABELFONT = None # default to system default: best on most devices
HEADERFONT = None # default to small preset in code ahead
print('Error in config file: %s' % why) # to console, if any
def fixTkBMP(text):
"""
[3.0] (copied from PyMailGUI) Tk <= 8.6 cannot display Unicode characters
outside the U+0000..U+FFFF BMP (UCS-2) code-point range, and generates
uncaught exceptions when tried (emojis kill programs!). To address this,
call this function to sanitize all text passed to the GUI for display.
It replaces any non-BMP characters with the standard Unicode replacement
character U+FFFD, which Tk displays as a highlighted question mark diamond.
This workaround is coded to assume that Tk 8.7 will lift the BMP restriction,
per a dev rumor. It also assumes TkVersion has been imported from tkinter.
Use here: filenames in mergeall messages scrolled text (rare, but true).
"""
if TkVersion <= 8.6:
text = ''.join((ch if ord(ch) <= 0xFFFF else '\uFFFD') for ch in text)
return text
def isNonBMP(text):
"""
[3.0] Return true if any char (codepoint) in text is outside Tk's BMP range.
Used by folder dialogs to force initialfile=None when True for prior choice.
"""
if TkVersion <= 8.6:
return any(ord(ch) > 0xFFFF for ch in text)
else:
return False # and assume Tk 8.7 will make this better...
def refocusWindow():
"""
[3.0] Call after (most) standard dialogs to reset focus on the main
window, else focus and active style are lost when dialog is closed.
This may be a bug in ActiveState Tk 8.5 (TBD), but the fix is simple.
"""
root.focus_force() # else Mac requires a user click after dialogs
####################################################################################
# GUI BUILDER
####################################################################################
# font for headers in controls sections;
# could be user-configurable too, but seems arguably-better hardwired;
# [2.4] not just 'bold': if used as family name, Tk falls back on arial!
# [3.0] smaller font on Linux, else looks almost cartoonish;
HDRFONT = 'arial 14 bold' # ('family', size, 'style? style?...')
if RunningOnLinux:
HDRFONT = 'arial 12 bold' # smaller is better on Linux (not Mac, Windows)
# ANDROID - go smaller (arial==helvetica, which supports bold in font str or tuple);
# [Mar2819] also allow user to give header font in config file, along with label font;
#
HDRFONT = HEADERFONT or 'arial 6 bold' # use small default if None
def makewidgets(root):
"""
build the gui window, setup Browse/GO/other event handlers
"""
# used on GO
global dirents # folders
global modevar, logvar, bkpvar # settings
global quietvar, cmpmsgsvar, cruftvar, logpopupvar # more settings
global gobutton, statustxt # widgets
#---------------------------------------------------------------------------
# Event handlers (less ongobutton: ahead)
#---------------------------------------------------------------------------
# some use names in the enclosing func (which are actually globals)
# one open dialog for entire run, saved in scope (closure)
opendirdlg = Directory()
def onbrowse(field, label):
title = 'Choose mergeall %s folder' % label
if RunningOnMac: # [3.0] specialize on Mac
if MACSLIDEDOWN:
# Mac: message (title ignored), slidedown window, custom text
title = 'Choose mergeall %s folder' % label
dlgkargs = dict(message=title, parent=root)
else:
# Mac: message (title ignored), popup window, standard text
title = 'mergeall: Choose %s Folder' % label
dlgkargs = dict(message=title)
else:
# Windows+Linux: the usual modal popup, standard text
title = 'mergeall: Choose %s folder' % label
dlgkargs = dict(title=title)
# check prior pathname choice for emojis: kills dialog
prior = opendirdlg.options.get('initialdir', '')
if isNonBMP(prior):
dlgkargs.update(dict(initialdir=None)) # for this call only
# ANDROID [Mar0819] - use config starts if set because the Android tkinter
# chooser GUI is tedious+slow (like its Linux cousin, but worse on phones);
# Android uses different settings for entry prefills and dialogs starts;
#
starts = dict(FROM=BROWSEFROMDIR, TO=BROWSETODIR, Logs=BROWSELOGDIR)
if starts[label] and os.path.isdir(starts[label]):
dlgkargs['initialdir'] = starts[label]
else:
pass # use tkinter's default
# or: dlgkargs['initialdir'] ='/' # dlgkargs.update() is overkill here
# dlgkargs['initialdir'] = None # this leaves None and kills isNonBMP
chosendir = opendirdlg.show(**dlgkargs)
if chosendir:
field.delete('0', END)
field.insert(END, fixTkBMP(chosendir))
refocusWindow() # [3.0] else requires click on Mac
def onquit():
answer = askokcancel('%s: Verify Exit' % APPNAME, 'Really quit mergeall now?')
if answer:
win.quit() # win in enclosing scope; or sys.exit()
else:
refocusWindow() # [3.0] else requires click on Mac
def onmodetoggle():
if modevar.get().startswith('REPORT'): # hide/show backups frame [2.0]
# [3.0] bkpfrm.pack_forget() # -backup applies to -auto updates only
bkpbtn.config(state=DISABLED) # enabled/disable widgets [3.0]
quietbtn.config(state=DISABLED)
cruftbtn.config(text='Skip cruft items in FROM and TO?') # ANDROID - shorter
#'do not report as differences?') # ANDROID
else:
# [3.0] bkpfrm.pack(expand=YES, fill=X)
bkpbtn.config(state=NORMAL)
quietbtn.config(state=NORMAL)
cruftbtn.config(text='Skip cruft items in FROM and TO?') # ANDROID - shorter
#'do not copy, replace, or delete?') # ANDROID
def onlogtoggle():
if logvar.get(): # show/hide log-file path chooser [2.0]
# [3.0] logdirfrm.pack(expand=YES, fill=X) # chooser applies only if logging
logbtn.config(state=NORMAL)
logent.config(state=NORMAL)
logpopupbtn.config(state=NORMAL) # [3.0] ditto for log-popup toggle
#logpopupbtn.config(state=DISABLED) # ANDROID - webbrowser failed initially
else:
# [3.0] logdirfrm.pack_forget()
logbtn.config(state=DISABLED)
logent.config(state=DISABLED)
logpopupbtn.config(state=DISABLED)
def onbkptoggle():
if bkpvar.get(): # show/hide -quiet toggle button [2.4]
# [3.0] quietbtn.pack(anchor=NW) # -quiet applies only if doing backups,
quietbtn.config(state=NORMAL) # and whether saving to log file or not
else:
# [3.0] quietbtn.pack_forget()
# quietvar.set(False) # keep prior setting, disabled=moot [3.0]
quietbtn.config(state=DISABLED)
def onhelp():
#
# ANDROID [Apr1219]: webbrowser fails on Android (for reasons TBD),
# so spawn a shell command using the $BROWSER preset in Pydroid 3:
# "am start --user 0 -a android.intent.action.VIEW -d %s"; Android
# uses online version to pick up latest changes (others should too);
#
# ANDROID [Apr1919]: webbrowser _does_ work, but requires local file
# URLs to start with "file://" and does not open a web browser for
# local HTML files (they open in text editors); use the online URL
# to ensure a web browser, and either os.system or webbrowser.open;
#
# ANDROID [Apr2119]: Pydroid 3 3.0 broke webbrowser and changed
# $BROWSER - use os.system() with a hardcoded command-line string;
#
brw = 'am start --user 0 -a android.intent.action.VIEW -d %s'
cmd = brw % HELPURL
os.system(cmd) # was os.environ['BROWSER'], webbrowser.open(HELPURL)
# other platforms code...
"""
webbrowser.open(HELPFILE) # popup local file in web browser
"""
#---------------------------------------------------------------------------
# Build the GUI (link to handlers)
#---------------------------------------------------------------------------
#
# MAIN WINDOW
#
win = root # [3.0] allow for TopLevel() or Tk()
win.title('mergeall %.1f' % VERSION) # set main window title, [1.6] version
win.protocol('WM_DELETE_WINDOW', onquit) # [1.6] catch/verify window close
# replace red (no, blue...) tk icon?
iconfolder = os.path.join(MYDIR, 'icons')
try:
if RunningOnWindows:
# try Windows window icon
icnpath = os.path.join(iconfolder, 'mergeall.ico')
win.iconbitmap(icnpath)
elif RunningOnLinux:
# try Linux app-bar icon [3.0]
icnpath = os.path.join(iconfolder, 'mergeall.gif')
imgobj = PhotoImage(file=icnpath)
win.iconphoto(True, imgobj)
elif RunningOnMac or True:
# Mac OS X: neither of the above works (yet?) [3.0]
# Macs require apps for most icon contexts
raise NotImplementedError
except Exception as why:
# punt: bad file/call or platform (Mac OS X TBD)
pass
# [3.0] on Mac, customize app-wide automatic top-of-display menu
fixAppleMenuBar(root, 'mergeall',
helpaction="/?originalUrl=https%3A%2F%2Flearning-python.com%2Fonhelp%2C%2520aboutaction%3DNone%2C%2520quitaction%3Donquit)%2520%2520%2520%2520ctrlfrm%2520%3D%2520Frame(win)%2520%2520%2520%2520ctrlfrm.pack(expand%3DNO%2C%2520fill%3DBOTH%2C%2520side%3DTOP)%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%23%2520MAIN%2520TO%2FFROM%2520DIRECTORY%2520CHOOSERS%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520dirsfrm%2520%3D%2520Frame(ctrlfrm%2C%2520relief%3DGROOVE%2C%2520border%3D3)%2520%2520%2520%2520dirsfrm.pack(fill%3DX)%2520%2520%2520%2520Label(dirsfrm%2C%2520text%3D%26%23x27%3BMain%2520Folders%26%23x27%3B%2C%2520font%3DHDRFONT).pack()%2520%2520%2520%23%2520%5B2.4%5D%2520font%2520%2520%2520%2520rowsfrm%2520%3D%2520Frame(dirsfrm)%2520%2520%2520%2520rowsfrm.pack(expand%3DYES%2C%2520fill%3DX)%2520%2520%2520%2520rowsfrm.columnconfigure(1%2C%2520weight%3D1)%2520%2520%2520%2520dirents%2520%3D%2520%7B%7D%2520%2520%2520%2520for%2520(row%2C%2520key)%2520in%2520enumerate((%26%23x27%3BFROM%26%23x27%3B%2C%2520%26%23x27%3BTO%26%23x27%3B))%3A%2520%2520%2520%2520%2520%2520%2520%2520rowsfrm.rowconfigure(row%2C%2520weight%3D1)%2520%2520%2520%2520%2520%2520%2520%2520Label(rowsfrm%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520font%3DLABELFONT%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520configurable%2520%5BMar2819%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520text%3Dkey%2520%2B%2520%26%23x27%3B%2520folder%3A%26%23x27%3B).grid(row%3Drow%2C%2520column%3D0%2C%2520sticky%3DE)%2520%2520%2520%2520%2520%2520%2520%2520dirent%2520%3D%2520Entry(rowsfrm)%2520%2520%2520%2520%2520%2520%2520%2520dirent.insert(END%2C%2520%26%23x27%3Benter%2520or%2520browse...%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520dirent.grid(row%3Drow%2C%2520column%3D1%2C%2520sticky%3DEW)%2520%2520%2520%2520%2520%2520%2520%2520handler%2520%3D%2520lambda%2520dirent%3Ddirent%2C%2520key%3Dkey%3A%2520onbrowse(dirent%2C%2520key)%2520%2520%2520%23%2520current!%2520%2520%2520%2520%2520%2520%2520%2520Button(rowsfrm%2C%2520text%3D%26%23x27%3BBrowse...%26%23x27%3B%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520command%3Dhandler).grid(row%3Drow%2C%2520column%3D2)%2520%2520%2520%2520%2520%2520%2520%2520dirents%5Bkey%5D%2520%3D%2520dirent%2520%2520%2520%2520%23%2520ANDROID%2520%5BMar0819%5D%2520-%2520prefill%2520to%2520avoid%2520tedious%2520Android%2Fphone%2520chooser%2520dialog%2520%2520%2520%2520for%2520(key%2C%2520prefill)%2520in%2520%5B(%26%23x27%3BFROM%26%23x27%3B%2C%2520DEFAULTFROMDIR)%2C%2520(%26%23x27%3BTO%26%23x27%3B%2C%2520DEFAULTTODIR)%5D%3A%2520%2520%2520%2520%2520%2520%2520%2520if%2520prefill%2520and%2520os.path.isdir(prefill)%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520dirents%5Bkey%5D.delete(%26%23x27%3B0%26%23x27%3B%2C%2520END)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520dirents%5Bkey%5D.insert(END%2C%2520prefill)%2520%2520%2520%2520%23%2520%2520%2520%2520%23%2520RUN%2520MODE%2520RADIO%2520BUTTONS%3A%2520-report%2520or%2520-auto%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520radiofrm%2520%3D%2520Frame(ctrlfrm%2C%2520relief%3DGROOVE%2C%2520border%3D3)%2520%2520%2520%2520radiofrm.pack(fill%3DX)%2520%2520%2520%2520Label(radiofrm%2C%2520text%3D%26%23x27%3BRun%2520Mode%26%23x27%3B%2C%2520font%3DHDRFONT).pack()%2520%2520%2520%2520%23%2520help%2520button%3A%2520use%2520empty%2520space%2520in%2520run%2520mode%2520frame%2520%5B2.0%5D%2520%2520%2520%2520%23%2520ANDROID%2520%5BApr1219%5D%3A%2520use%2520default%2520color%2520to%2520avoid%2520loss%2C%2520was%2520%5Brelief%3DFLAT%2C%2520bg%3D%26%23x27%3Bwhite%26%23x27%3B)%5D%2520%2520%2520%2520%23%2520%2520%2520%2520helpbtn%2520%3D%2520Button(radiofrm%2C%2520text%3D%26%23x27%3BHelp%26%23x27%3B%2C%2520command%3Donhelp)%2520%2520%2520%2520helpbtn.pack(side%3DRIGHT%2C%2520anchor%3DNE)%2520%2520%2520%2520%23helpbtn.config(state%3DDISABLED)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520webbrowser%2520failed%2520initially%2520%2520%2520%2520%2520modevar%2520%3D%2520StringVar()%2520%2520%2520%2520modes%2520%3D%2520%5B%26%23x27%3BREPORT%3A%2520show%2520differences%2520only%26%23x27%3B%2C%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520shorter%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3BUPDATE%3A%2520make%2520TO%2520the%2520same%2520as%2520FROM%26%23x27%3B%5D%2520%2520%2520%2520%23%2520ANDROID%2520-%2520shorter%2520%2520%2520%2520for%2520mode%2520in%2520modes%3A%2520%2520%2520%2520%2520%2520%2520%2520Radiobutton(radiofrm%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520text%3Dmode%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520font%3DLABELFONT%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520configurable%2520%5BMar2819%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520variable%3Dmodevar%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520value%3Dmode%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520command%3Donmodetoggle).pack(side%3DTOP%2C%2520anchor%3DNW)%2520%2520%2520%2520modevar.set(modes%5B0%5D)%2520%2520%2520%2520%23%2520%5B3.0%5D%2520skip%2520cruft%2520files%2520in%2520FROM%2520and%2520TO%2520-%2520don%26%23x27%3Bt%2520copy%2Fremove%2Freplace%2520or%2520report%2520%2520%2520%2520%2520cruftvar%2520%3D%2520BooleanVar()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520cruftvar.set(True)%2520%2520%2520%2520cruftbtn%2520%3D%2520Checkbutton(radiofrm%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520text%3D%26%23x27%3BSkip%2520cruft%2520items%2520in%2520FROM%2520and%2520TO%3F%26%23x27%3B%2C%2520%2520%2520%23%2520ANDROID%2520-%2520shorter%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%26%23x27%3Bdo%2520not%2520report%2520as%2520differences%3F%26%23x27%3B%2C%2520%2520%2520%2520%2520%23%2520ANDROID%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520font%3DLABELFONT%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520configurable%2520%5BMar2819%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520variable%3Dcruftvar)%2520%2520%2520%2520cruftbtn.pack(side%3DLEFT)%2520%2520%2520%2520%23%2520%2520%2520%2520%23%2520MESSAGE%2520TOGGLES%2520%2B%2520LOG%2520DIRECTORY%2520CHOOSER%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520msgfrm%2520%3D%2520Frame(ctrlfrm%2C%2520relief%3DGROOVE%2C%2520border%3D3)%2520%2520%2520%2520msgfrm.pack(fill%3DX)%2520%2520%2520%2520Label(msgfrm%2C%2520text%3D%26%23x27%3BMessages%26%23x27%3B%2C%2520font%3DHDRFONT).pack()%2520%2520%2520%2520logfrm%2520%3D%2520Frame(msgfrm)%2520%2520%2520%2520logfrm.pack(side%3DTOP%2C%2520fill%3DX)%2520%2520%2520%2520logvar%2520%3D%2520BooleanVar()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520Intvar%2520works%2520too%2520%2520%2520%2520logvar.set(True)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520else%2520default%3Doff%2520(eibti)%2520%2520%2520%2520Checkbutton(logfrm%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520text%3D%26%23x27%3BSave%2520logfile%2520to%2520folder%3F%2520%26%23x27%3B%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520shorter%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520font%3DLABELFONT%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520configurable%2520%5BMar2819%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520variable%3Dlogvar%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520command%3Donlogtoggle).pack(side%3DLEFT)%2520%2520%2520%2520%23%2520log-file%2520dir%2520chooser%2520by%2520toggle%2C%2520unhide%2Fhide%2520when%2520toggled%2520on%2Foff%2520%5B2.0%5D%2520%2520%2520%2520key%2520%3D%2520%26%23x27%3BLogs%26%23x27%3B%2520%2520%2520%2520logdirfrm%2520%3D%2520Frame(logfrm)%2520%2520%2520%2520logdirfrm.pack(expand%3DYES%2C%2520fill%3DX)%2520%2520%2520%2520handler%2520%3D%2520lambda%3A%2520onbrowse(logent%2C%2520key)%2520%2520%2520%2520%2520%23%2520last%2C%2520not%2520current%2520(!)%2520%5Bsee%2520above%5D%2520%2520%2520%2520logbtn%2520%3D%2520Button(logdirfrm%2C%2520text%3D%26%23x27%3BBrowse...%26%23x27%3B%2C%2520command%3Dhandler)%2520%2520%2520%2520logbtn.pack(side%3DRIGHT)%2520%2520%2520%2520logent%2520%3D%2520Entry(logdirfrm)%2520%2520%2520%2520logent.insert(END%2C%2520%26%23x27%3Benter%2520or%2520browse...%26%23x27%3B)%2520%2520%2520%2520%23%2520ANDROID%2520-%2520shorter%2520%2520%2520%2520logent.pack(side%3DLEFT%2C%2520expand%3DYES%2C%2520fill%3DX)%2520%2520%2520%2520dirents%5Bkey%5D%2520%3D%2520logent%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520logdirfrm.pack_forget()%2520%2520%2520%23%2520till%2520logs%2520toggled%2520on%2520(now%2520enabled%2Fdisabled)%2520%2520%2520%2520logbtn.config(state%3DNORMAL)%2520%2520%2520%2520%2520%2520%2520%23%2520till%2520logs%2520toggled%2520on%2520(initially%2520are)%2520%2520%2520%2520logent.config(state%3DNORMAL)%2520%2520%2520%2520%23%2520fill%2520initial%2Fdefault%2520logs%2520folder%2520value%2520%2520%2520%2520%2520%2520if%2520DEFAULTLOGDIR%2520and%2520os.path.exists(DEFAULTLOGDIR)%3A%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520allow%2520user-configs%2520file%2520to%2520give%2520initial%2520default%2520%2520%2520%2520%2520%2520%2520%2520%2520defaultlogs%2520%3D%2520DEFAULTLOGDIR%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520try%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B2.0%5D%2520try%2520user%26%23x27%3Bs%2520Desktop%2520on%2520Windows%2520(has%2520HOMEPATH%2520but%2520no%2520HOME)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520defaultlogs%2520%3D%2520r%26%23x27%3BC%3A%5CUsers%5C%25s%5CDesktop%26%23x27%3B%2520%25%2520os.environ%5B%26%23x27%3Busername%26%23x27%3B%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520assert%2520os.path.exists(defaultlogs)%2520%2520%2520%2520%2520%2520%2520%2520except%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520try%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520try%2520same%2520on%2520Linux%2520and%2520Mac%2520OS%2520X%2520(TBD%3A%2520or%2520Documents%3F)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520defaultlogs%2520%3D%2520os.path.join(os.environ%5B%26%23x27%3BHOME%26%23x27%3B%5D%2C%2520%26%23x27%3BDesktop%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520assert%2520os.path.exists(defaultlogs)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520except%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520try%2520internal-storage%2520docs%2520folder%2520(on%2520some%2520phones)%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5BMar0819%5D%2520don%26%23x27%3Bt%2520use%2520%2Fsdcard%2FDocuments%2520as%2520a%2520prefill%2520(made%2520by%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520MS%2520Office%2520apps)%3B%2520create%2520the%2520preset%2520if%2520it%2520doesn%26%23x27%3Bt%2520exist%3B%2520but%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520still%2520use%2520DEFAULTLOGDIR%2520as%2520the%2520prefill%2520instead%2C%2520if%2520it%26%23x27%3Bs%2520set%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520try%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520defaultlogs%2520%3D%2520%26%23x27%3B%2Fsdcard%2FAdmin-Mergeall%26%23x27%3B%2520%2520%2520%23%2520was%2520%26%23x27%3B%2Fsdcard%2FDocuments%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520not%2520os.path.exists(defaultlogs)%3A%2520%2520%2520%2520%2520%2520%23%2520make%2520now%2520if%2520needed%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520os.mkdir(defaultlogs)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520assert%2520os.path.isdir(defaultlogs)%2520%2520%2520%2520%2520%2520%2520%2520%23%2520exists%2520%2B%2520is%2520a%2520folder%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520except%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520defaultlogs%2520%3D%2520None%2520%2520%2520%2520if%2520defaultlogs%3A%2520%2520%2520%2520%2520%2520%2520%2520%23%2520may%2520be%2520unset%2520or%2520fail%2520on%2520some%2520Unix%2520and%2520Windows%2520%3D%26gt%3B%2520%2520empty%2C%2520Browse%2520%2520%2520%2520%2520%2520%2520%2520logent.delete(%26%23x27%3B0%26%23x27%3B%2C%2520END)%2520%2520%2520%2520%2520%2520%2520%2520logent.insert(END%2C%2520defaultlogs)%2520%2520%2520%2520%23%2520%5B3.0%5D%2520allow%2520comparison%2520messages%2520to%2520be%2520suppressed%2520(mostly%2520for%2520Mac%2520speed)%3B%2520%2520%2520%2520%23%2520this%2520toggle%2520(only)%2520is%2520dynamic%3A%2520can%2520change%2520effect%2520while%2520output%2520scrolling%3B%2520%2520%2520%2520%2520cmpmsgsvar%2520%3D%2520BooleanVar()%2520%2520%2520%2520if%2520RunningOnMac%3A%2520%2520%2520%2520%2520%2520%2520%2520cmpmsgsvar.set(True)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520initial%3Dsuppress%2520on%2520Mac%2520OS%2520X%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520cmpmsgsvar.set(False)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520off%2520on%2520Windws%2FLinux%3A%2520fast%2520GUI%2520%2520%2520%2520Checkbutton(msgfrm%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520text%3D%26%23x27%3BHide%2520comparison%2520messages%3F%26%23x27%3B%2C%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520shorter%2520%5BMar2819%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520font%3DLABELFONT%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520configurable%2520%5BMar2819%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520variable%3Dcmpmsgsvar).pack(side%3DLEFT%2C%2520anchor%3DNW)%2520%2520%2520%2520%23%2520%5B3.0%5D%2520allow%2520log-file%2520popup%2520editor%2520to%2520be%2520suppressed%2520in%2520the%2520GUI%2520per%2520run%3B%2520%2520%2520%2520%23%2520the%2520configs-file%2520entry%2520added%2520previously%2520now%2520gives%2520an%2520initial%2520value%2520only%3B%2520%2520%2520%2520logpopupvar%2520%3D%2520BooleanVar()%2520%2520%2520%2520logpopupvar.set(LOGEDITORPOPUP)%2520%2520%2520%2520logpopupbtn%2520%3D%2520Checkbutton(msgfrm%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520text%3D%26%23x27%3BPopup%2520logfile%3F%26%23x27%3B%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520shorter%2520%5BMar2819%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520font%3DLABELFONT%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520configurable%2520%5BMar2819%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520variable%3Dlogpopupvar)%2520%2520%2520%2520logpopupbtn.pack(side%3DRIGHT%2C%2520anchor%3DNE)%2520%2520%2520%2520%23logpopupvar.set(False)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520webbrowser%2520failed%2520initially%2520%2520%2520%2520%2520%23logpopupbtn.config(state%3DDISABLED)%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520webbrowser%2520failed%2520initially%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%23%2520BACKUP%2520TOGGLES%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%23%2520enable%2Fdisable%2520when%2520%26%23x27%3B-auto%26%23x27%3B%2520run%2520mode%2520selected%2Fdeselected%2520%5B2.0%5D%2520%5B3.0%5D%2520%2520%2520%2520bkpfrm%2520%3D%2520Frame(ctrlfrm%2C%2520relief%3DGROOVE%2C%2520border%3D3)%2520%2520%2520%2520bkpfrm.pack(fill%3DX)%2520%2520%2520%2520Label(bkpfrm%2C%2520text%3D%26%23x27%3BBackups%26%23x27%3B%2C%2520font%3DHDRFONT).pack()%2520%2520%2520%2520%2520%2520%2520%2520bkpvar%2520%3D%2520BooleanVar()%2520%2520%2520%2520bkpbtn%2520%3D%2520Checkbutton(bkpfrm%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520text%3D%26%23x27%3BSave%2520TO%2520items%2520replaced%2520or%2520deleted%2C%2520note%2520adds%3F%26%23x27%3B%2C%2520%2520%2520%23%2520ANDROID%2520-%2520shorter%2520%5BMar2819%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520font%3DLABELFONT%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520configurable%2520%5BMar2819%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520variable%3Dbkpvar%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520command%3Donbkptoggle)%2520%2520%2520%2520bkpbtn.pack(anchor%3DNW)%2520%2520%2520%2520bkpvar.set(True)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520initial%3Don%3A%2520do%2520backups%2520%2520%2520%2520%23%2520%5B3.0%5D%2520bkpfrm.pack_forget()%2520%2520%2520%2520%2520%23%2520till%2520-auto%2520selected%2520%2520%2520%2520bkpbtn.config(state%3DDISABLED)%2520%2520%2520%2520%23%2520till%2520-auto%2520selected%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B2.4%5D%2520support%2520-quiet%2520mode%3A%2520omit%2520%26quot%3B...backing%2520up%26quot%3B%2520per-file%2520log%2520messages%2520%2520%2520%2520quietvar%2520%3D%2520BooleanVar()%2520%2520%2520%2520%2520quietbtn%2520%3D%2520Checkbutton(bkpfrm%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520text%3D%26%23x27%3BDisable%2520per-item%2520backup%2520messages%3F%26%23x27%3B%2C%2520%2520%2520%2520%23%2520ANDROID%2520-%2520shorter%2520%5BMar2819%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520font%3DLABELFONT%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520configurable%2520%5BMar2819%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520variable%3Dquietvar)%2520%2520%2520%2520quietbtn.pack(anchor%3DNW)%2520%2520%2520%2520quietvar.set(True)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520init%3Dsuppress%2520(though%2520good%2520for%2520errors%2Bfeedback)%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520quietbtn.pack_forget()%2520%2520%2520%2520%23%2520only%2520if%2520backups%2520selected%2C%2520but%2520will%2520be%2520initially%2520%2520%2520%2520quietbtn.config(state%3DDISABLED)%2520%2520%2520%23%2520till%2520-auto%2520AND%2520-backups%2520selected%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%23%2520MESSAGES%2520SCROLLED%2520TEXT%2520%2B%2520%26%23x27%3BGO%26%23x27%3B%2520BUTTON%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26%23x27%3Bgo%26%23x27%3B%2520always%2520hidden%2520during%2520run%2520to%2520prevent%2520overlapping%2520run%2520launches%2520%2520%2520%2520statustxt%2520%3D%2520ScrolledText(win)%2520%2520%2520%2520statustxt.config(state%3DDISABLED)%2520%2520%23%2520ANDROID%2520%5BMar2319%5D%3A%2520readonly%2520text%2520to%2520avoid%2520keyboard%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520user%2520configs%3A%2520size%2C%2520font%2C%2520color%2520(no%2520border%2520-%2520color%2520sets%2520off%2520better)%2520%2520%2520%2520try%3A%2520%2520%2520%2520%2520%2520%2520%2520if%2520TEXTAREAHEIGHT%3A%2520statustxt.config(height%3DTEXTAREAHEIGHT)%2520%2520%2520%2520%2520%2520%2520%2520if%2520TEXTAREAWIDTH%3A%2520%2520statustxt.config(width%3DTEXTAREAWIDTH)%2520%2520%2520%2520%2520%2520%2520%2520if%2520TEXTAREAFONT%3A%2520%2520%2520statustxt.config(font%3DTEXTAREAFONT)%2520%2520%2520%2520%2520%2520%2520%2520if%2520TEXTAREACOLOR%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520isinstance(TEXTAREACOLOR%2C%2520str)%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520statustxt.config(bg%3DTEXTAREACOLOR)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520elif%2520isinstance(TEXTAREACOLOR%2C%2520tuple)%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520statustxt.config(bg%3DTEXTAREACOLOR%5B0%5D)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520statustxt.config(fg%3DTEXTAREACOLOR%5B1%5D)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520print(%26%23x27%3BBad%2520color%2520value%2520in%2520config%2520file%26%23x27%3B)%2520%2520%2520%2520except%2520Exception%2520as%2520why%3A%2520%2520%2520%2520%2520%2520%2520%2520print(%26%23x27%3BBad%2520config%2520setting%3A%2520%25s%26%23x27%3B%2520%25%2520why)%2520%2520%2520%2520%2520%2520%2520%2520gobutton%2520%3D%2520Button(win%2C%2520text%3D%26%23x27%3BGO%3A%2520run%2520mergeall%26%23x27%3B%2C%2520font%3DHDRFONT%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520command%3Dongobutton)%2520%2520%2520%2520gobutton.pack(side%3DBOTTOM)%2520%2520%2520%2520statustxt.pack(side%3DTOP%2C%2520expand%3DYES%2C%2520fill%3DBOTH)%2520%2520%2520%2520%23%2520pack%2520last%3Dclip%2520first%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%2520ON%2520%26quot%3BGO%26quot%3B%3A%2520SPAWN%2520MERGEALL%2520PROCESS%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%2520%5B1.4%5D%2520how%2520spawned%2520mergeall%2520subprocess%26%23x27%3Bs%2520text%2520is%2520written%2520and%2520decoded%2520hereSTREAM_ENCODE%2520%3D%2520%26%23x27%3Butf8%26%23x27%3BEOF_SENTINEL%2520%3D%2520%5B%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520stream%2520lines%2520read%2520will%2520never%2520be%2520a%2520listlinequeue%2520%3D%2520queue.Queue()%2520%2520%2520%2520%23%2520infinite-size%2520shared%2520queue%2520of%2520objectsdef%2520ongobutton()%3A%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520on%2520GO%2520button%2520press%3A%2520fetch%2520gui%2520values%2C%2520confirm%2520run%2C%2520launch%2520mergeall%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520%23%2520set%2520in%2520makewidgets%2520%2520%2520%2520global%2520dirents%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520folders%2520%2520%2520%2520global%2520modevar%2C%2520logvar%2C%2520bkpvar%2C%2520quietvar%2C%2520cruftvar%2520%2520%2520%2520%2520%23%2520settings%2520%2520%2520%2520global%2520gobutton%2C%2520statustxt%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520widgets%2520%2520%2520%2520%23%2520%5B3.0%5D%2520for%2520comparison%2520message%2520suppresssion%2520%2520%2520%2520global%2520firstcompareline%2520%2520%2520%2520firstcompareline%2520%3D%2520True%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520reset%2520before%2520each%2520run%2520%2520%2520%2520%2520%2520%2520%2520%23%2520get%2520inputs%2520from%2520GUI%2520%2520%2520%2520fromdir%2520%3D%2520dirents%5B%26%23x27%3BFROM%26%23x27%3B%5D.get()%2520%2520%2520%2520%2520%2520%2520%23%2520directory%2520fields%2520%2520%2520%2520todir%2520%2520%2520%3D%2520dirents%5B%26%23x27%3BTO%26%23x27%3B%5D.get()%2520%2520%2520%2520logdir%2520%2520%3D%2520dirents%5B%26%23x27%3BLogs%26%23x27%3B%5D.get()%2520%2520%2520%2520mode%2520%2520%2520%2520%3D%2520modevar.get()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520runmode%2520radiobtn%2520%2520%2520%2520dolog%2520%2520%2520%3D%2520logvar.get()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520logfile%2520checkbtn%2520%2520%2520%2520dobkp%2520%2520%2520%3D%2520bkpvar.get()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520backups%2520checkbtn%2520%2520%2520%2520quiet%2520%2520%2520%3D%2520quietvar.get()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520quiet%2520mode%2520checkbtn%2520%5B2.4%5D%2520%2520%2520%2520docruft%2520%3D%2520cruftvar.get()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520skip%2520cruft%2520files%2520%5B3.0%5D%2520%2520%2520%2520%2520%2520%2520%2520%23%2520config%2520run%2520%2520%2520%2520%2520%2520%2520%2520modearg%2520%3D%2520%26%23x27%3B-report%26%23x27%3B%2520if%2520mode.startswith(%26%23x27%3BREPORT%26%23x27%3B)%2520else%2520%26%23x27%3B-auto%26%23x27%3B%2520%2520%2520%2520if%2520dobkp%2520and%2520modearg%2520!%3D%2520%26%23x27%3B-report%26%23x27%3B%3A%2520%2520%2520%2520%2520%2520%2520%2520modearg%2520%2B%3D%2520%26%23x27%3B%2520-backup%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B2.0%5D%2520backup%2520replacements%2Fremovals%2520%2520%2520%2520%2520%2520%2520%2520if%2520quiet%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520modearg%2520%2B%3D%2520%26%23x27%3B%2520-quiet%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B2.4%5D%2520no%2520per-file%2520backup%2520log%2520messages%2520%2520%2520%2520if%2520docruft%3A%2520%2520%2520%2520%2520%2520%2520%2520modearg%2520%2B%3D%2520%26%23x27%3B%2520-skipcruft%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520ignore%2520cruft%2520files%2520in%2520FROM%2520and%2520TO%2520%2520%2520%2520if%2520not%2520dolog%3A%2520%2520%2520%2520%2520%2520%2520%2520logpath%2520%3D%2520logfile%2520%3D%2520None%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520datestamp%2520%3D%2520time.strftime(%26%23x27%3Bdate%25y%25m%25d-time%25H%25M%25S%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520logpath%2520%2520%2520%3D%2520logdir%2520%2B%2520os.sep%2520%2B%2520%26%23x27%3Bmergeall-%25s.txt%26%23x27%3B%2520%25%2520(datestamp)%2520%2520%2520%2520%23%2520confirm%2520run%2520%2520%2520%2520if%2520modearg.startswith(%26%23x27%3B-report%26%23x27%3B)%3A%2520%2520%2520%2520%2520%2520%2520%2520runtype%2520%3D%2520%26%23x27%3BREPORT-ONLY%2520RUN%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520warning%2520%3D%2520%26%23x27%3BThis%2520run%2520will%2520not%2520change%2520your%2520data.%26%23x27%3B%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520runtype%2520%3D%2520%26%23x27%3BAUTO-UPDATE%2520RUN%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520warningbase%2520%3D%2520(%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520%5BMar2319%5D%2520-%2520manually%2520line%2520break%2520for%2520fit%2520(but%2520Tk%2520still%2520truncates%2520paths)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26%23x27%3B*WARNING*%3A%2520by%2520design%2C%2520this%2520may%2520change%2520your%2520TO%2520folder%2520tree%2520in-place%2C%2520%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26%23x27%3Bby%2520adding%2C%2520replacing%2C%2520and%2520deleting%2520files%2520and%2520folders%2520as%2520needed%2520%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26%23x27%3Bto%2520make%2520TO%2520the%2520same%2520as%2520FROM.%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3B*CAUTION*%3A%2520by%2520design%2C%2520this%2520run%2520may%2520change%5Cn%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3Byour%2520TO%2520folder%2520tree%2520in-place%2C%2520by%2520adding%2C%5Cn%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3Breplacing%2C%2520and%2520deleting%2520files%2520and%2520folders%5Cn%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3Bas%2520needed%2520to%2520make%2520TO%2520the%2520same%2520as%2520FROM.%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520warningmore%2520%3D%2520(%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520%5BMar2319%5D%2520-%2520manually%2520line%2520break%2520for%2520fit%2520(but%2520Tk%2520still%2520truncates%2520paths)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26%23x27%3B%2520%2520Because%2520backups%2520are%2520disabled%2C%2520any%2520such%2520changes%2520%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26%23x27%3Bwill%2520be%2520permanent%2520and%2520irrevocable.%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3B%5CnBecause%2520backups%2520are%2520disabled%2C%2520any%2520such%5Cn%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3Bchanges%2520cannot%2520be%2520undone.%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520warning%2520%3D%2520warningbase%2520%2B%2520(%26%23x27%3B%26%23x27%3B%2520if%2520dobkp%2520else%2520warningmore)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520confirm%2520%3D%2520askokcancel(%26%23x27%3B%25s%3A%2520Confirm%2520Run%26%23x27%3B%2520%25%2520APPNAME%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3BAbout%2520to%2520run%3A%5Cn%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3Bmergeall.py%2520%25s%5Cn%5Cn%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3BFROM%3A%5Cn%25s%5Cn%5Cn%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3BTO%3A%2520%2520%5Cn%25s%5Cn%5Cn%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3BLogging%2520output%2520to%3A%5Cn%25s%5Cn%5Cn%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3B%25s%5Cn%5Cn%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3BStart%2520this%2520%25s%3F%26%23x27%3B%2520%25%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520(modearg%2C%2520fromdir%2C%2520todir%2C%2520logpath%2C%2520warning%2C%2520runtype))%2520%2520%2520%2520notruntitle%2520%3D%2520%26%23x27%3B%25s%3A%2520Not%2520Run%26%23x27%3B%2520%25%2520APPNAME%2520%2520%2520%2520if%2520not%2520confirm%3A%2520%2520%2520%2520%2520%2520%2520%2520showinfo(notruntitle%2C%2520%2520%26%23x27%3BThe%2520mergeall%2520run%2520was%2520cancelled.%26%23x27%3B)%2520%2520%2520%2520elif%2520not%2520os.path.exists(fromdir)%3A%2520%2520%2520%2520%2520%2520%2520%2520showerror(notruntitle%2C%2520%26%23x27%3BPlease%2520select%2520a%2520valid%2520mergeall%2520FROM%2520folder.%26%23x27%3B)%2520%2520%2520%2520%2520%2520%23%2520%5B2.0%5D%2520popup%2520%2520%2520%2520elif%2520not%2520os.path.exists(todir)%3A%2520%2520%2520%2520%2520%2520%2520%2520showerror(notruntitle%2C%2520%26%23x27%3BPlease%2520select%2520a%2520valid%2520mergeall%2520TO%2520folder.%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B2.0%5D%2520popup%2520%2520%2520%2520elif%2520dolog%2520and%2520not%2520os.path.exists(logdir)%3A%2520%2520%2520%2520%2520%2520%2520%2520showerror(notruntitle%2C%2520%26%23x27%3BPlease%2520select%2520a%2520valid%2520mergeall%2520log%2520file%2520folder.%26%23x27%3B)%2520%2520%23%2520or%2520sooner%3F%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B1.4%5D%2520log%2520uses%2520binary%2520mode%2520for%2520now-binary%2520data%2520from%2520stream%2520%2520%2520%2520%2520%2520%2520%2520if%2520dolog%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520try%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520logfile%2520%3D%2520open(logpath%2C%2520%26%23x27%3Bwb%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520except%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B1.7%5D%2520catch%2520PermissionError%2520and%2520show%2520popup%2520(else%2520silent%2520for%2520.pyw)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520showerror(notruntitle%2C%2520%26%23x27%3BPlease%2520select%2520a%2520writeable%2520log%2520file%2520folder.%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520return%2520%2520%2520%2520%2520%2520%2520%2520%23%2520proceed%2520with%2520mergeall%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520gobutton.pack_forget()%2520%2520%2520%2520%2520%23%2520hide%2Ferase%2520button%2520%2520%2520%2520%2520%2520%2520%2520gobutton.config(state%3DDISABLED)%2520%2520%2520%2520%23%2520keep%2520but%2520disable%2520%2520%2520%2520%2520%2520%2520%2520statustxt.config(state%3DNORMAL)%2520%2520%2520%2520%2520%23%2520ANDROID%2520%5BMar2319%5D%3A%2520enable%2520text%2520for%2520changes%2520during%2520run%2520%2520%2520%2520%2520%2520%2520%2520statustxt.delete(%26%23x27%3B1.0%26%23x27%3B%2C%2520END)%2520%2520%2520%2520%2520%2520%2520%23%2520clear%2520last%2520run%2520text%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B1.4%5D%2520force%2520UTF8%2520prints%2520in%2520mergeall%2C%2520use%2520binary%2520streams%2520%2B%2520manual%2520decode%2520here%3B%2520%2520%2520%2520%2520%2520%2520%2520%23%2520this%2520setting%2520is%2520inherited%2520by%2520the%2520spawned%2520mergeall%2520subprocess%2520for%2520its%2520prints%3B%2520%2520%2520%2520%2520%2520%2520%2520os.environ%5B%26%23x27%3BPYTHONIOENCODING%26%23x27%3B%5D%2520%3D%2520STREAM_ENCODE%2520%2520%2520%2520%2520%2520%2520%2520%23%2520config%2520mergeall%2520command%2520(sequences%3A%2520auto-quoted%2520by%2520subprocess)%2520%2520%2520%2520%2520%2520%2520%2520extras%2520%3D%2520%7B%7D%2520%2520%2520%2520%2520%2520%2520%2520if%2520hasattr(sys%2C%2520%26%23x27%3Bfrozen%26%23x27%3B)%2520and%2520(RunningOnWindows%2520or%2520RunningOnLinux)%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520pyinstaller%2520exe%2520%5B3.0%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520run%2520frozen%2520executable%2520directly%2C%2520not%2520script%2520through%2520python%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520freezename%2520%3D%2520%26%23x27%3Bmergeall.exe%26%23x27%3B%2520if%2520RunningOnWindows%2520else%2520%26%23x27%3Bmergeall%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520mergeallpath%2520%3D%2520os.path.join(MYDIR%2C%2520freezename)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520os.environ%5B%26%23x27%3BPYTHONUNBUFFERED%26%23x27%3B%5D%2520%3D%2520%26%23x27%3BTrue%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520-u%2520equiv%2520(iff%2520env%3F)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520cmdseq%2520%3D%2520%5Bmergeallpath%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520frozen%2520executable%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520fromdir%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26%23x27%3B%2F%26%23x27%3B%2520ok%2520on%2520Windows%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520todir%5D%2520%2B%2520modearg.split()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26%23x27%3Ba%2520b%3F%26%23x27%3B%2520-%26gt%3B%2520%5B%26%23x27%3Ba%26%23x27%3B%2C%2520%26%23x27%3Bb%26%23x27%3B%3F%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520RunningOnWindows%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520else%2520spawn%2520hangs%2520unless%2520launcher%2520uses%2520--console%2520(with%2520popup!)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520startupinfo%2C%2520env%3Dos.environ%2C%2520and%2520creationflags%2520are%2520irrelevant%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520extras.update(stdin%3Dsubprocess.DEVNULL)%2520%2520%2520%2520%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520py2app%2520Mac%2520app%2520or%2520source%2520(original%2520code)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520relative%2520script%2520path%2520works%2520whether%2520run%2520here%2520or%2520via%2520desktop%2520shortcut%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520but%2520not%2520in%2520os.getcwd()%2520if%2520run%2520from%2520a%2520cmdline%2520elsewhere%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520-%2520hardcode%2520sys.executable%2C%2520else%2520empty%2520in%2520Pydroid%25203%3A%2520kills%2520Popen%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520%5BApr1919%5D%3A%2520Pydroid%25203%26%23x27%3Bs%25203.0%2520release%2520moved%2520its%2520Python%2520from%2520the%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520first%2520of%2520the%2520following%2520paths%2520to%2520the%2520second%2C%2520breaking%2520this%2520workaround%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2Fdata%2Fuser%2F0%2Fru.iiec.pydroid3%2Ffiles%2Farm-linux-androideabi%2Fbin%2Fpython%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2Fdata%2Fuser%2F0%2Fru.iiec.pydroid3%2Ffiles%2Faarch64-linux-android%2Fbin%2Fpython%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520to%2520allow%2520for%2520both%2520paths--and%2520be%2520platform%2520agnostic%2520in%2520general--read%2520the%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520result%2520of%2520a%2520%26%23x27%3Bwhich%2520python%26%23x27%3B%2520shell%2520command%2520instead%2520of%2520using%2520literal%2520strs%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520sys.executable%2520%3D%2520os.popen(%26%23x27%3Bwhich%2520python%26%23x27%3B).read().rstrip()%2520%2520%23%2520path%2520to%2520Python%2520exe%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520scriptname%2520%3D%2520%26%23x27%3Bmergeall.py%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520mergeallpath%2520%3D%2520os.path.join(MYDIR%2C%2520scriptname)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520cmdseq%2520%3D%2520%5Bsys.executable%2C%2520%26%23x27%3B-u%26%23x27%3B%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B1.4%5D%2520need%2520-u%2520unbufferred%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520mergeallpath%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520script%2520file%2520(app%2520or%2520source)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520fromdir%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26%23x27%3B%2F%26%23x27%3B%2520ok%2520on%2520Windows%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520todir%5D%2520%2B%2520modearg.split()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26%23x27%3Ba%2520b%3F%26%23x27%3B%2520-%26gt%3B%2520%5B%26%23x27%3Ba%26%23x27%3B%2C%2520%26%23x27%3Bb%26%23x27%3B%3F%5D%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B1.5%5D%2520shell%2520should%2520be%2520True%2520on%2520Windows%2520so%2520that%2520it%2520uses%2520filename%2520associations%2C%2520%2520%2520%2520%2520%2520%2520%2520%23%2520but%2520False%2520on%2520Linux%2520so%2520that%2520it%2520doesn%26%23x27%3Bt%2520just%2520start%2520a%2520%26quot%3Bpython%26quot%3B%2520interactive%2520shell%2520%2520%2520%2520%2520%2520%2520%2520doshell%2520%3D%2520RunningOnWindows%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520spawn%2520mergeall%2520%2520%2520%2520%2520%2520%2520%2520%23%2520use%2520subprocess%3A%2520os.popen%2Fspawnv%2520not%2520enough%2C%2520popen2%2520is%25202.X%2520only%3B%2520%2520%2520%2520%2520%2520%2520%2520subproc%2520%3D%2520subprocess.Popen(%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520cmdseq%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520a%2520string%2520cmd%2520may%2520fail%2520on%2520Unix%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520shell%3Ddoshell%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B1.5%5D%2520see%2520note%2520above%2C%2520platform%2520specific%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520universal_newlines%3DFalse%2C%2520%2520%2520%23%2520%5B1.4%5D%2520binary%2520mode%2C%2520manual%2520decode%2Feoln%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520stdout%3Dsubprocess.PIPE%2C%2520%2520%2520%2520%2520%23%2520capture%2520sub%26%23x27%3Bs%2520stdout%2520here%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520stderr%3Dsubprocess.STDOUT%2C%2520%2520%2520%23%2520route%2520sub%26%23x27%3Bs%2520stderr%2520to%2520its%2520stdout%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520**extras)%2520%2520%2520%2520%2520%2520%2520%2520%23%2520read%2520and%2520process%2520mergeall%26%23x27%3Bs%2520output%3A%2520reader%2520thread%2520%2B%2520timer-based%2520poller%2520%2520%2520%2520%2520%2520%2520%2520_thread.start_new_thread(streamreader%2C%2520(subproc.stdout%2C%2520linequeue))%2520%2520%2520%2520%2520%2520%2520%2520streamconsumer(linequeue%2C%2520logfile%2C%2520logpath)%2520%2520%2520%2520%2520%2520%2520%2520%23%2520returns%2520here%2520immediately%3A%2520a%2520thread%2520and%2520timer-event%2520loop%2520are%2520now%2520running%2520%2520%2520%2520%23%2520%5B3.0%5D%2520for%2520all%2520cases%2C%2520else%2520requires%2520click%2520on%2520Mac%2520%2520%2520%2520refocusWindow()%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%2520MERGEALL%2520PROCESS%2520HANDLER%3A%2520THREAD%2520%2B%2520POLLER%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23def%2520streamreader(stream%2C%2520linequeue)%3A%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520%5B1.4%5D%2520In%2520a%2520parallel%2520thread%2520-%2520read%2520the%2520mergeall%2520subprocess%26%23x27%3Bs%2520stdout%2Fstderr%2520%2520%2520%2520stream%2C%2520and%2520post%2520its%2520lines%2520to%2520a%2520queue%2520for%2520the%2520GUI%2520to%2520read%2520and%2520display%2520%2520%2520%2520on%2520timer%2520event%2520callbacks%3B%2520this%2520way%2C%2520the%2520GUI%2520isn%26%23x27%3Bt%2520blocked%2Fpaused%2520during%2520%2520%2520%2520long-running%2520copies%2520or%2520other%2520actions%2520in%2520the%2520spawned%2520mergeall%2520script%3B%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520for%2520line%2520in%2520stream%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520read%2520stdout%2Bstderr%2520lines%3A%2520may%2520block%2520this%2520thread%2520%2520%2520%2520%2520%2520%2520%2520linequeue.put(line)%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520place%2520on%2520queue%2520to%2520be%2520picked%2520up%2520by%2520GUI%2520thread%2520%2520%2520%2520linequeue.put(EOF_SENTINEL)%2520%2520%2520%2520%2520%23%2520write%2520sentinel%2520at%2520eof%2520and%2520exit%3A%2520subprocess%2520endeddef%2520streamconsumer(linequeue%2C%2520logfile%2C%2520logpath)%3A%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520%5B1.4%5D%2520In%2520the%2520main%2520GUI%2520thread%2520-%2520run%2520a%2520timer-based%2520loop%2520to%2520poll%2520for%2C%2520read%2C%2520%2520%2520%2520and%2520display%2520and%2520log%2520stream%2520lines%2520from%2520the%2520shared%2520thread%2520queue%2520until%2520the%2520%2520%2520%2520reader%2520thread%2520sends%2520the%2520end-signal%2520sentinel%2520value%2520on%2520the%2520queue.%2520%2520The%2520main%2520%2520%2520%2520GUI%2520thread%2520running%2520this%2520code%2520thus%2520remains%2520active%2520between%2520mergeall%2520output%2520%2520%2520%2520lines.%2520%2520A%2520nested%2520loop%2520is%2520also%2520used%2520here%2520to%2520process%2520lines%2520in%2520batches%2520so%2520the%2520%2520%2520%2520GUI%26%23x27%3Bs%2520response%2520to%2520lines%2520is%2520quick%2C%2520but%2520it%2520calls%2520update()%2520to%2520remain%2520active.%2520%2520%2520%2520%2520%2520%2520%2520At%2520this%2520point%2520we%2520have%25202%2520threads%2520and%2520a%2520process%2520(connected%2520by%2520queue%2520and%2520stream)%2520%2520%2520%2520and%25203%2520or%25204%2520loops%2520going%2520at%2520once%2520--%2520the%2520main%2520GUI%2520thread%2520runs%2520a%2520timer%2520event%2520loop%2520%2520%2520%2520to%2520poll%2520the%2520queue%2C%2520and%2520runs%2520the%2520nested%2520line-batch%2520loop%2520here%3B%2520the%2520spawned%2520%2520%2520%2520stream-reader%2520thread%2520runs%2520a%2520blockable%2520reading%2520loop%2520and%2520posts%2520lines%2520to%2520the%2520%2520%2520%2520queue%3B%2520and%2520the%2520spawned%2520mergeall%2520process%2520runs%2520its%2520own%2520file-processing%2520loops%2520%2520%2520%2520to%2520create%2520output%2520lines%2520eventually%2520displayed%2520in%2520the%2520GUI%2520here.%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520global%2520statustxt%2C%2520gobutton%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520widgets%2520%2520%2520%2520global%2520firstcompareline%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520state%2520%2520%2520%2520%2520global%2520cmpmsgsvar%2C%2520logpopupvar%2520%2520%2520%2520%2520%2520%2520%23%2520settings%2520%2520%2520%2520def%2520trydecode(binline%2C%2520stream_encode)%3A%2520%2520%2520%2520%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520%2520%2520%2520%2520%5B1.6%5D%2520line%2520decode%2520can%2520fail%2520in%2520Python%25202.X%2520due%2520to%2520a%2520TBD%2520library%2520%2520%2520%2520%2520%2520%2520%2520incompatibility%2520issue%3B%2520see%25201.6%2520note%2520near%2520top%2520of%2520this%2520script%3B%2520%2520%2520%2520%2520%2520%2520%2520if%2520uncaught%2C%2520GUI%2520is%2520dead%2520but%2520unclosed%2C%2520and%2520the%2520error%2520message%2520does%2520%2520%2520%2520%2520%2520%2520%2520not%2520appear%2520in%2520GUI%2520--%2520because%2520the%2520error%2520occurs%2520here%2520in%2520GUI%2520instead%2520%2520%2520%2520%2520%2520%2520%2520%2520of%2520subproc%2C%2520its%2520text%2520goes%2520to%2520console%2520here%2520(if%2520any)%2C%2520not%2520to%2520subproc%2520%2520%2520%2520%2520%2520%2520%2520%2520stream%2520queue%2C%2520and%2520the%2520subproc%2520is%2520apparently%2520terminated%2520in%25202.X%3B%2520%2520%2520%2520%2520%2520%2520%2520also%2520note%2520that%2520%26quot%3Bline%26quot%3B%2520text%2520here%2520is%2520used%2520for%2520the%2520GUI%2520display%2520only%3A%2520%2520%2520%2520%2520%2520%2520%2520it%2520doesn%26%23x27%3Bt%2520show%2520up%2520in%2520the%2520binary%2520log%2520file%2C%2520and%2520this%2520has%2520no%2520impact%2520%2520%2520%2520%2520%2520%2520%2520%2520on%2520the%2520underlying%2520mergeall%2520process%2C%2520which%2520proceeds%2520unaffected%3B%2520%2520%2520%2520%2520%2520%2520%2520%5B3.0%5D%2520changed%2520the%2520error%2520message%2520to%2520be%2520a%2520str%2520instead%2520of%2520a%2520bytes%3B%2520%2520%2520%2520%2520%2520%2520%2520the%2520latter%2520would%2520surely%2520fail%2520later%2520in%2520the%2520caller%2520under%2520python%25203.X%2C%2520%2520%2520%2520%2520%2520%2520%2520but%2520the%2520decode%2520here%2520probably%2520only%2520ever%2520failed%2520on%2520python%25202.X%2C%2520where%2520%2520%2520%2520%2520%2520%2520%2520a%2520bytes%2520result%2520works%2520because%2520bytes%2520is%2520really%2520just%2520a%2520synonym%2520for%2520str%3B%2520%2520%2520%2520%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520%2520%2520%2520%2520try%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520line%2520%3D%2520binline.decode(stream_encode)%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B1.4%5D%2520manual%2520decode%2520here%2C%2520match%2520subproc%2520%2520%2520%2520%2520%2520%2520%2520except%2520UnicodeDecodeError%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23line%2520%3D%2520b%26%23x27%3B(UNDECODABLE%2520LINE)%3A%2520%26%23x27%3B%2520%2B%2520binline%2520%2520%2520%2520%23%2520%5B1.6%5D%2520don%26%23x27%3Bt%2520let%2520this%2520kill%2520the%2520GUI%2520in%25202.X%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520line%2520%3D%2520%26%23x27%3B(UNDECODABLE%2520LINE%3A%2520see%2520log%2520file)%5Cn%26%23x27%3B%2520%23%2520%5B3.0%5D%2520use%2520str%2C%2520but%2520drop%2520the%2520content%2520%2520%2520%2520%2520%2520%2520%2520return%2520line%2520%2520%2520%2520try%3A%2520%2520%2520%2520%2520%2520%2520%2520binline%2520%3D%2520linequeue.get(block%3DFalse)%2520%2520%2520%2520%2520%2520%2520%23%2520check%2520the%2520queue%2520%2520%2520%2520except%2520queue.Empty%3A%2520%2520%2520%2520%2520%2520%2520%2520pass%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520nothing%2520posted%2520yet%3A%2520reschedule%2520and%2520wait%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520%23%2520process%2520a%2520batch%2520of%25201%2520or%2520more%2520objects%2520%2520%2520%2520%2520%2520%2520%2520while%2520True%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520binline%2520!%3D%2520EOF_SENTINEL%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520process%2520the%2520next%2520line%2520string%3A%2520GUI%2520%2B%2520logfile%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520eoln%2520%3D%2520os.linesep%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520local%2520line-end%3A%2520%5Cr%5Cn%2520Windows%2C%2520%5Cn%2520Unix%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520line%2520%3D%2520trydecode(binline%2C%2520STREAM_ENCODE)%2520%2520%23%2520%5B1.4%5D%2520manual%2520decode%2520here%2C%2520match%2520subproc%2520%5B1.6%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520line%2520%3D%2520line.replace(eoln%2C%2520%26%23x27%3B%5Cn%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B1.4%5D%2520and%2520fix%2520any%2520Windows%2520eolns%2520for%2520tk%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520sanitize%2520Unicode%2520in%2520line%2520to%2520be%2520displayed%2520in%2520GUI%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520line%2520%3D%2520fixTkBMP(line)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520allow%2520comparison%2520messages%2520to%2520be%2520suppressed%2520in%2520the%2520GUI%2C%2520dynamically%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520anycompare%2520%3D%2520(%26%23x27%3Bcomparing%26%23x27%3B%2C%2520%26%23x27%3B%26quot%3Bcomparing%26%23x27%3B%2C%2520%26quot%3B%26%23x27%3Bcomparing%26quot%3B)%2520%2520%2520%23%2520ascii()%2520in%2520Windows%2520exe!%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520line.startswith(anycompare)%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520in%2520line%2520content%3F%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520cmpmsgsvar.get()%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520suppress%2520toggle%2520on%3F%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520firstcompareline%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520show%2520first%2520line%2520only%2520for%2520top-level%2520dirs%2C%2520plus%2520message%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520firstcompareline%2520%3D%2520False%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520statustxt.insert(END%2C%2520line)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520statustxt.insert(END%2C%2520%26%23x27%3BFolder%2520comparison%2520messages%2520%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3Bare%2520being%2520suppressed%2520in%2520the%2520GUI...%5Cn%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520statustxt.see(END%2B%26%23x27%3B-2l%26%23x27%3B)%2520%2520%2520%2520%2520%2520%23%2520scroll%2520to%2520new%2520end%2520of%2520text%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520skip%2520all%2520other%2520compare%2520lines%2520in%2520GUI%2520(only)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520pass%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520show%2520comparison%2520line%2C%2520reset%2520to%2520show%2520message%2520if%2520toggled%2520on%2Boff%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520firstcompareline%2520%3D%2520True%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520reshow%2520msg%2520if%2520toggled%2520again%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520statustxt.insert(END%2C%2520line)%2520%2520%2520%2520%2520%2520%2520%23%2520add%2520to%2520end%2520of%2520text%2520widget%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520statustxt.see(END%2B%26%23x27%3B-2l%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520scroll%2520to%2520new%2520end%2520of%2520text%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520show%2520all%2520other%2520lines%2520normally%2C%2520don%26%23x27%3Bt%2520reset%2520for%2520message%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520statustxt.insert(END%2C%2520line)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520add%2520to%2520end%2520of%2520text%2520widget%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520statustxt.see(END%2B%26%23x27%3B-2l%26%23x27%3B)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520scroll%2520to%2520new%2520end%2520of%2520text%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26%23x27%3B-2l%26%23x27%3B%2520%3D%2520before%2520empty%2520auto%2520%5Cn%2520at%2520end%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520force%2520GUI%2520to%2520show%2Frespond%2520now%2520(else%2520dead%2520during%2520batch)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520statustxt.update()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520write%2520binary%2520stream%2520line%2520to%2520binary%2520logfile%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520logfile%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520also%2520save%2520to%2520log%2520file%3F%2520%5B1.4%5D%3A%2520binary%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520eoln%2520%3D%2520os.linesep.encode()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520must%2520be%2520bytes%2520in%25203.X%2520(no-op%2520in%25202.X)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520binline%2520%3D%2520binline.replace(b%26%23x27%3B%5Cr%5Cn%26%23x27%3B%2C%2520b%26%23x27%3B%5Cn%26%23x27%3B)%2520%2520%23%2520%5B1.4%5D%2520got%2520just%2520%5Cn%2520from%2520%26%23x27%3B-u%26%23x27%3B%2520in%25202.X%2520only%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520binline%2520%3D%2520binline.replace(b%26%23x27%3B%5Cn%26%23x27%3B%2C%2520eoln)%2520%2520%2520%2520%2520%23%2520replaces%2520are%2520no-op%2520in%25203.X%2520and%2520unix%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520logfile.write(binline)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520read%2520next%2520line%2520if%2520any%2C%2520else%2520goto%2520reschedule%2520and%2520wait%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520linequeue.empty()%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520break%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520binline%2520%3D%2520linequeue.get(block%3DFalse)%2520%2520%2520%23%2520back%2520to%2520top%2520of%2520loop%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520reader%2520thread%2520posted%2520eof%2520sentinel%2520and%2520exited%3A%2520close%2520out%2520the%2520run%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520showinfo(%26%23x27%3B%25s%3A%2520Finished%26%23x27%3B%2520%25%2520APPNAME%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3BThe%2520mergeall%2520run%2520has%2520finished.%26%23x27%3B%2520%2B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520(%26%23x27%3B%26%23x27%3B%2520if%2520not%2520logfile%2520else%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520(%26%23x27%3B%5CnSee%2520its%2520log%2520file%2520in%2520the%2520popup%2520window.%26%23x27%3B%2520if%2520logpopupvar.get()%2520else%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26%23x27%3B%5CnSee%2520its%2520log%2520file%2520in%2520the%2520logs%2520folder.%26%23x27%3B)))%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520refocusWindow()%2520%2520%2520%23%2520%5B3.0%5D%2520else%2520requires%2520click%2520on%2520Mac%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520if%2520logging%3A%2520close%2520logfile%2C%2520show%2520in%2520editor%2520if%2520toggled%2520on%2520%5B3.0%5D%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520logfile%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520logfile.close()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520logpopupvar.get()%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520Mac%2520OS%2520X%2520is%2520pickier%2520about%2520file%2520URLs%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520RunningOnMac%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520logpath%2520%3D%2520%26%23x27%3Bfile%3A%26%23x27%3B%2520%2B%2520logpath%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520%5BApr1219%5D%2520-%2520webbrowser%2520fails%2520on%2520Android%2520(for%2520reasons%2520TBD)%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520so%2520spawn%2520a%2520shell%2520command%2520using%2520the%2520%24BROWSER%2520preset%2520in%2520Pydroid%25203%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26quot%3Bam%2520start%2520--user%25200%2520-a%2520android.intent.action.VIEW%2520-d%2520%25s%26quot%3B%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520safe%2520to%2520assume%2520logpath%2520is%2520accessible%2520(else%2520can%26%23x27%3Bt%2520write%2520anyhow)%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520%5BApr1919%5D%2520-%2520webbrowser%2520_does_%2520work%2C%2520but%2520requires%2520local%2520file%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520URLs%2520to%2520start%2520with%2520%26quot%3Bfile%3A%2F%2F%26quot%3B%3B%2520prefix%2520as%2520required%2C%2520and%2520use%2520either%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520os.system%2520or%2520webbrowser.open%2520to%2520display%2520the%2520logfile%2520in%2520the%2520GUI%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520ANDROID%2520%5BApr2119%5D%3A%2520Pydroid%25203%25203.0%2520broke%2520webbrowser%2520and%2520changed%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%24BROWSER%2520to%2520skip%2520%26quot%3Bfile%3A%2F%2F%26quot%3B%2520-%2520use%2520os.system()%2520%2B%2520%2520hardcoded%2520command%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520brw%2520%3D%2520%26%23x27%3Bam%2520start%2520--user%25200%2520-a%2520android.intent.action.VIEW%2520-d%2520%25s%26%23x27%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520url%2520%3D%2520%26%23x27%3Bfile%3A%2F%2F%26%23x27%3B%2520%2B%2520logpath%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520cmd%2520%3D%2520brw%2520%25%2520url%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520os.system(cmd)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520_not_%2520os.environ%5B%26%23x27%3BBROWSER%26%23x27%3B%5D%2C%2520webbrowser.open()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520other%2520platforms%2520code...%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520webbrowser%2520opens%2520text%2520files%2520in%2520Notepad%2520on%2520Windows%2C%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520gedit%2520on%2520Linux%2C%2520and%2520TextEdit%2520on%2520Mac%2520OS%2520X%2520(but%2520YMMV)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520webbrowser.open(logpath)%2520%2520%2520%2520%2520%2520%23%2520assume%2520never%2520raises%2520exc%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520reenable%2520new%2520runs%2520now%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520gobutton.config(state%3DNORMAL)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520statustxt.config(state%3DDISABLED)%2520%2520%2520%2520%23%2520ANDROID%2520%5BMar2319%5D%3A%2520back%2520to%2520readonly%2520to%2520avoid%2520keyboard%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520the%2520following%2520had%2520odd%2520text%2520scrolls%2520after%2520vertical%2520resizes%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520statustxt.pack_forget()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520gobutton.pack(side%3DBOTTOM)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520unhide%2520button%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520statustxt.pack(side%3DTOP%2C%2520expand%3DYES%2C%2520fill%3DBOTH)%2520%2520%2520%23%2520pack%2520last%3Dclip%2520first%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520exit%2520the%2520timer%2520events%2520loop%3A%2520back%2520to%2520waiting%2520on%2520user%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520return%2520%2520%2520%2520%2520%2520%2520%2520%23%2520end%2520batch%2520while%2520loop%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520reschedule%2520and%2520wait%3A%2520check%2520queue%252010%2520times%2520per%2520second%2520(msecs)%2520%2520%2520%2520statustxt.after(100%2C%2520streamconsumer%2C%2520linequeue%2C%2520logfile%2C%2520logpath)%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%2520MAIN%2FTOP-LEVEL%2520LOGIC%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23if%2520__name__%2520%3D%3D%2520%26%23x27%3B__main__%26%23x27%3B%3A%2520%2520%2520%2520if%2520not%2520RunningOnMac%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520Windows%2520and%2520Linux%3A%2520normal%2520%2520%2520%2520%2520%2520%2520%2520root%2520%3D%2520Tk()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%26%23x27%3Broot%26%23x27%3B%2520is%2520used%2520elsewhere%2520%2520%2520%2520%2520%2520%2520%2520makewidgets(root)%2520%2520%2520%2520%2520%2520%2520%2520root.mainloop()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520else%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23----------------------------------------------------------------------------%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%5B3.0%5D%2520Mac%2520OS%2520X%2520hack%3A%2520a%2520partial%2520workaround%2520for%2520a%2520bug%2520in%2520the%2520recommended%2520AS%2520%2520%2520%2520%2520%2520%2520%2520%23%2520Mac%2520Tk%25208.5%2520for%2520Python%25203.5.%2520%2520Hide%2Funhide%2520main%2520window%2520so%2520it%2520shows%2520its%2520radio%2520%2520%2520%2520%2520%2520%2520%2520%23%2520and%2520check%2520buttons%2520in%2520Aqua%26%23x27%3Bs%2520active-window%2520style%2520(default%2520blue)%2520immediately.%2520%2520%2520%2520%2520%2520%2520%2520%23%2520This%2520code%2520fixes%2520style%2520at%2520initial%2520opening%2520only%3B%2520style%2520can%2520be%2520lost%2520on%2520popups%2520%2520%2520%2520%2520%2520%2520%2520%23%2520and%2520minimize%2Frestore%2520--%2520click%2520this%2520window%2C%2520and%2520possibly%2520others%2520first%2C%2520to%2520%2520%2520%2520%2520%2520%2520%2520%23%2520reset%2520style%2520as%2520needed.%2520%2520More%3A%2520docetc%2Fmiscnotes%2Fmac-main-hack-notes.txt.%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%23%2520This%2520may%2520be%2520fixed%2520in%2520Tk%25208.6%2C%2520which%2520may%2520be%2520supported%2520by%2520py.org%26%23x27%3Bs%2520Python%25203.X%2520%2520%2520%2520%2520%2520%2520%2520%23%2520someday%2C%2520and%2520is%2520supported%2520by%2520homebrew%26%23x27%3Bs%2520Python%2520distribution%2520(to%2520be%2520tested).%2520%2520%2520%2520%2520%2520%2520%2520%23%2520Unlike%2520frigcal%2520and%2520pymailgui%2C%2520a%2520lift()%2520is%2520not%2520enough%2520here%2C%2520whether%2520clicked%2520%2520%2520%2520%2520%2520%2520%2520%23%2520to%2520open%2520in%2520the%2520mac%2520python%2520launcher%2C%2520or%2520run%2520from%2520a%2520%26%23x27%3Bpython3%26%23x27%3B%2520command%2520line.%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%23%2520Update%3A%2520root.force_focus()%2520is%2520now%2520used%2520to%2520restore%2520window%2520active%2520state%2520after%2520%2520%2520%2520%2520%2520%2520%2520%23%2520dialogs%2C%2520but%2520doesn%26%23x27%3Bt%2520help%2520for%2520deiconifies%2520caught%2520via%2520%26lt%3BMap%26gt%3B%2520or%2520%26lt%3BVisibility%26gt%3B%2520%2520%2520%2520%2520%2520%2520%2520%23%2520events%2C%2520and%2520has%2520no%2520impact%2520here%2520on%2520initial%2520state%2520in%2520all%2520codings%2520attempted.%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520Update%3A%2520the%2520following%2520don%26%23x27%3Bt%2520help%2520even%2520if%2520they%2520do%2520a%2520focus_force%2520(why%3F)%3A%2520%2520%2520%2520%2520%2520%2520%2520%23%2520root.bind(%26%23x27%3B%26lt%3BMap%26gt%3B%26%23x27%3B%2C%2520onUnhide)%2520%2B%2520root.after(2000%2C%2520refocusWindow)%2C%2520%2520%2520%2520%2520%2520%2520%2520%23%2520root.createcommand(%26%23x27%3B%3A%3Atk%3A%3Amac%3A%3AonShow%26%23x27%3B%2C%2520onShow)%2C%2520%2520%2520%2520%2520%2520%2520%2520%23%2520root.createcommand(%26%23x27%3B%3A%3Atk%3A%3Amac%3A%3AReopenApplication%26%23x27%3B%2C%2520onReopen)%2520%2520%2520%2520%2520%2520%2520%2520%23%2520%2520%2520%2520%2520%2520%2520%2520%23%2520UPDATE%3A%2520losing%2520focus%2520in%2520deiconifies%2520in%2520AS%2520Tk%25208.5%2520was%2520finally%2520fixed%2520by%2520the%2520%2520%2520%2520%2520%2520%2520%2520%23%2520hideous%2520workaround%2520below%2C%2520which%2520creates%2520and%2520then%2520immediately%2520destroys%2520a%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520new%2520but%2520lowered%2520(and%2520hence%2520invisible)%2520top-level%2520window%2520on%2520the%2520Mac%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520reopen-app%2520even%2520(i.e.%2C%2520Doc%2520and%2520app%2520icon%2520clicks).%2520%2520The%2520widgets%2520flash%2520off%2520%2520%2520%2520%2520%2520%2520%2520%23%2520and%2520on%2520momentarily%2520(and%2520the%2520temp%2520window%2520may%2520flash%2520if%2520mergeall%2520is%2520not%2520at%2520%2520%2520%2520%2520%2520%2520%2520%23%2520fullscreen)%2C%2520but%2520otherwise%2520are%2520active%2520styled.%2520%2520Tk%25208.6%2520status%2520tbd...%2520%2520%2520%2520%2520%2520%2520%2520%2520%23----------------------------------------------------------------------------%2520%2520%2520%2520%2520%2520%2520%2520root%2520%3D%2520Tk()%2520%2520%2520%2520%2520%2520%2520%2520makewidgets(root)%2520%2520%2520%2520%2520%2520%2520%2520%23%2520fix%2520tk%2520focus%2520loss%2520on%2520startup%2520%2520%2520%2520%2520%2520%2520%2520root.withdraw()%2520%2520%2520%2520%2520%2520%2520%2520root.lift()%2520%2520%2520%2520%2520%2520%2520%2520root.after_idle(root.deiconify)%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23%2520fix%2520tk%2520focus%2520loss%2520on%2520deiconify%2520%2520%2520%2520%2520%2520%2520%2520def%2520onReopen()%3A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%23print(root.state())%2520%2520%2520%2520%23%2520always%2520normal%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520root.lift()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520root.update()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520temp%2520%3D%2520Toplevel()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520temp.lower()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520temp.destroy()%2520%2520%2520%2520%2520%2520%2520%2520root.createcommand(%26%23x27%3B%3A%3Atk%3A%3Amac%3A%3AReopenApplication%26%23x27%3B%2C%2520onReopen)%2520%2520%2520%2520%2520%2520%2520%2520root.mainloop()%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520%2520%2520%2520%2520%23%2520an%2520alternative%2520open%2520workaround%3A%2520a%2520bogus%2520Tk%2520root%2C%2520iconified%2520after%25202%2520seconds%3A%2520%2520%2520%2520%2520%2520%2520%2520root%2520%3D%2520Tk()%2520%2520%2520%2520%2520%2520%2520%2520root.protocol(%26%23x27%3BWM_DELEsTE_WINDOW%26%23x27%3B%2C%2520lambda%3A%2520None)%2520%2520%2520%2520%2520%2520%2520%2520Label(root%2C%2520text%3D%26%23x27%3BWelcome%2520to%2520mergeall%26%23x27%3B%2C%2520width%3D25%2C%2520height%3D5).pack()%2520%2520%2520%2520%2520%2520%2520%2520makewidgets(Toplevel())%2520%2520%2520%2520%2520%2520%2520%2520root.after(2000%2C%2520root.iconify)%2520%2520%2520%2520%2520%2520%2520%2520root.mainloop()%2520%2520%2520%2520%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%2520%2520%2520%2520%2520%2520%2520%2520%23%2520a%252015-line%2520AppleScript%2520alternative%2520omitted%2520here%2520for%2520space%2520(and%2520humanity...)%2520%2520%2520%2520%2520%2520%2520%2520%26quot%3B%26quot%3B%26quot%3B%253C%2FPRE">