Skip to content

Commit

Permalink
Basic TigerViewer, and file lock.
Browse files Browse the repository at this point in the history
Tiger files can be loaded/viewed in the GUI.

Only one instance of TigerTamer can run at once.

A file lock is used (tigertamer.lock), with the PID inside,
to ensure that only one instance can run.
  • Loading branch information
cjwelborn committed Mar 3, 2019
1 parent 0e55cb8 commit 4d0ab0f
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
example_data
*.log
tigertamer.json
*.lock
7 changes: 6 additions & 1 deletion lib/gui/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@

from ..util.logger import (
debug,
debug_exc,
print_err,
)
from ..util.config import NAME
from ..util.config import (
NAME,
lock_release,
)


def create_event_handler(func):
Expand Down Expand Up @@ -154,6 +158,7 @@ def __call__(self, *args):
except Exception as ex:
# Log the message, and show an error dialog.
print_err('GUI Error: ({})'.format(type(ex).__name__))
debug_exc()
messagebox.showerror(
title='{} - Error'.format(NAME),
message=str(ex),
Expand Down
12 changes: 12 additions & 0 deletions lib/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from ..util.config import (
config_increment,
config_save,
lock_acquire,
lock_release,
ICONFILE,
NAME,
)
Expand Down Expand Up @@ -717,6 +719,7 @@ def cmd_menu_viewer(self):
'geometry_viewer': self.config_gui['geometry_viewer'],
},
destroy_cb=lambda: self.enable_interface(True),
filename=None,
)
return True

Expand Down Expand Up @@ -878,10 +881,19 @@ def list_funcs():
def load_gui(**kwargs):
win = WinMain(**kwargs) # noqa
debug('Starting main window...')
try:
lock_acquire()
except ValueError:
win.attributes('-topmost', 0)
win.withdraw()
print_err('{} is already running.'.format(NAME))
return 3
try:
tk.mainloop()
except Exception as ex:
print_err('Main loop error: ({})\n{}'.format(
type(ex).__name__,
ex,
))
return 1
return 0
116 changes: 109 additions & 7 deletions lib/gui/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
Tiger Viewer window for Tiger Tamer GUI.
-Christopher Welborn 02-25-2019
"""
import os

from .common import (
create_event_handler,
filedialog,
handle_cb,
show_error,
tk,
ttk,
)
Expand All @@ -20,6 +23,9 @@
config_save,
get_system_info,
)
from ..util.format import (
TigerFile,
)
from ..util.logger import (
debug,
debug_err,
Expand All @@ -35,10 +41,18 @@ def __init__(self, *args, **kwargs):
self.destroy_cb = kwargs.pop('destroy_cb')
except KeyError as ex:
raise TypeError('Missing required kwarg: {}'.format(ex))
try:
# Required, but may be None values.
self.filename = kwargs.pop('filename')
except KeyError as ex:
raise TypeError('Missing required kwarg, may be None: {}'.format(
ex
))
super().__init__(*args, **kwargs)

# Initialize this window.
self.title('{} - Viewer'.format(NAME))
self.default_title = '{} - Viewer'.format(NAME)
self.title(self.default_title)
self.geometry(self.config_gui.get('geometry_viewer', '554x141'))
# About window should stay above the main window.
self.attributes('-topmost', 1)
Expand Down Expand Up @@ -110,15 +124,24 @@ def __init__(self, *args, **kwargs):
self.frm_main.columnconfigure(x, weight=1)

# Columns for file view frame.
self.columns = ('index', 'width', 'length', 'part', 'no', 'note')
self.columns = (
'index',
'quantity',
'completed',
'length',
'part',
'no',
'note',
)
# Column settings for file view frame.
self.column_info = {
'index': {'minwidth': 60, 'width': 60},
'width': {'minwidth': 60, 'width': 60},
'quantity': {'minwidth': 80, 'width': 80},
'completed': {'minwidth': 100, 'width': 100},
'length': {'minwidth': 70, 'width': 70},
'part': {'minwidth': 60, 'width': 60},
'no': {'minwidth': 60, 'width': 100},
'note': {'minwidth': 60, 'width': 80},
'no': {'minwidth': 60, 'width': 60},
'note': {'minwidth': 60, 'width': 60},
}
# Build file view frame
self.frm_view = ttk.Frame(
Expand Down Expand Up @@ -153,7 +176,26 @@ def __init__(self, *args, **kwargs):
yscrollcommand=self.scroll_view.set
)
self.scroll_view.pack(side=tk.RIGHT, fill=tk.Y, expand=False)

self.tree_view.tag_configure(
'odd',
background='#FFFFFF',
font='Arial 10',
)
self.tree_view.tag_configure(
'even',
background='#DADADA',
font='Arial 10',
)
self.tree_view.tag_configure(
'odd_completed',
background='#CCFFCC',
font='Arial 10',
)
self.tree_view.tag_configure(
'even_completed',
background='#AAFFAA',
font='Arial 10',
)
# Build Open/Exit buttons frame
self.frm_cmds = ttk.Frame(
self.frm_main,
Expand Down Expand Up @@ -209,12 +251,27 @@ def __init__(self, *args, **kwargs):
'<Control-{}>'.format(btninfo['char'].lower()),
create_event_handler(btninfo['func']),
)
# Open file passed in with kwargs?
if self.filename:
self.view_file(self.filename)

def cmd_btn_exit(self):
return self.destroy()

def cmd_btn_open(self):
return
""" Pick a file with a Tk file dialog, and open it. """
self.attributes('-topmost', 0)
self.withdraw()
filename = filedialog.askopenfilename()
self.attributes('-topmost', 1)
self.deiconify()
if not filename:
return
if not os.path.exists(filename):
show_error('File does not exist:\n{}'.format(filename))
return

return self.view_file(filename)

def destroy(self):
debug('Saving gui-viewer config...')
Expand All @@ -228,3 +285,48 @@ def destroy(self):
super().destroy()
debug('Calling destroy_cb({})...'.format(self.destroy_cb))
handle_cb(self.destroy_cb)

def format_value(self, column, value):
""" Format a value for the tree_view, with decent default values. """
defaults = {
'index': 0,
'quantity': 0,
'completed': 0,
'length': 0,
'part': '?',
'no': '?',
'note': '',
}
value = value or defaults[column]
if column.lower() == 'length':
value = '{:0.2f}'.format(float(value))
return str(value)

def view_file(self, filename):
""" Load file contents into tree_view. """
self.tree_view.delete(*self.tree_view.get_children())
self.filename = filename
tf = TigerFile.from_file(filename)
for i, part in enumerate(tf.parts):
# Get raw part values.
values = [
getattr(part, colname, None)
for colname in self.columns
]
quantity = values[1]
completed = values[2]
remaining = quantity - completed
tag = 'odd' if i % 2 else 'even'
tag = '{}{}'.format(tag, '' if remaining else '_completed')
# Insert formatted values:
self.tree_view.insert(
'',
tk.END,
values=tuple(
self.format_value(self.columns[i], v)
for i, v in enumerate(values)
),
text='',
tag=tag,
)
self.title('{}: {}'.format(self.default_title, self.filename))
42 changes: 41 additions & 1 deletion lib/util/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,27 @@
)

NAME = 'Tiger Tamer'
VERSION = '0.2.2'
VERSION = '0.2.3'
AUTHOR = 'Christopher Joseph Welborn'
VERSIONSTR = '{} v. {}'.format(NAME, VERSION)
SCRIPT = os.path.split(os.path.abspath(sys.argv[0]))[1]
SCRIPTDIR = os.path.abspath(sys.path[0])

CONFIGFILE = os.path.join(SCRIPTDIR, 'tigertamer.json')
LOCKFILE = os.path.join(SCRIPTDIR, 'tigertamer.lock')
ICONFILE = os.path.join(
SCRIPTDIR,
'resources',
'tigertamer-icon.png'
)

# pid used for checking the file lock.
PID = os.getpid()

# Something besides None, to mean No Value.
Nothing = object()


try:
config = JSONSettings.from_file(CONFIGFILE)
debug('Loaded config from: {}'.format(CONFIGFILE))
Expand Down Expand Up @@ -143,3 +148,38 @@ def get_system_info():
'unarchive_files': config.get('unarchive_files', 0),
'remove_files': config.get('remove_files', 0),
}


def lock_acquire():
""" Try acquiring the file lock. Raise ValueError if the lock is already
acquired.
"""
if os.path.exists(LOCKFILE):
debug('Lock already acquired: {}'.format(LOCKFILE), level=1)
raise ValueError('File lock already acquired: {}'.format(LOCKFILE))
with open(LOCKFILE, 'w') as f:
f.write(str(PID))
debug('Lock acquired: {}'.format(LOCKFILE), level=1)


def lock_release():
""" Release the file lock. """
if not os.path.exists(LOCKFILE):
debug('Lock already released: {}'.format(LOCKFILE), level=1)
return True

with open(LOCKFILE, 'r') as f:
pid = int(f.read())
if pid != PID:
debug(
'Lock not owned by this process: {} != {}'.format(
pid,
PID,
),
level=1
)
return False
# Lock is owned by this process.
os.remove(LOCKFILE)
debug('Lock released: {}'.format(LOCKFILE), level=1)
return True
8 changes: 8 additions & 0 deletions lib/util/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,14 @@ def __init__(self, data):
in TigerFile.header.
"""
for key, val in data.items():
if val is not None:
try:
val = int(val)
except ValueError:
try:
val = float(val)
except ValueError:
val = str(val)
setattr(self, key.lower(), val)
if ('Note' in data) and ('Note' not in self.header):
self.header.append('Note')
Expand Down
32 changes: 24 additions & 8 deletions tigertamer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
AUTHOR,
config,
config_increment,
lock_acquire,
lock_release,
NAME,
VERSIONSTR,
)
from lib.util.format import (
Expand Down Expand Up @@ -128,13 +131,6 @@ def main(argd):
# Handle config/arg flags.
argd['--extra'] = config.get('extra_data', argd['--extra'])
argd['--nosplit'] = config.get('no_part_split', argd['--nosplit'])
if argd['--functions']:
# List functions available for -f.
return list_funcs()

if argd['--view']:
# View a tiger file.
return view_tigerfiles(argd['FILE'])

if argd['--gui'] or argd['--func']:
# The GUI handles arguments differently, send it the correct config.
Expand All @@ -157,6 +153,21 @@ def main(argd):
run_function=argd['--func'],
)

# Console mode, need a lock.
try:
lock_acquire()
except ValueError:
print_err('{} already running.'.format(NAME))
return 3

if argd['--functions']:
# List functions available for -f.
return list_funcs()

if argd['--view']:
# View a tiger file.
return view_tigerfiles(argd['FILE'])

if argd['--unarchive'] or argd['--UNARCHIVE']:
if not options_are_set(inpaths, archdir):
raise InvalidConfig(
Expand Down Expand Up @@ -357,7 +368,7 @@ def __str__(self):
return 'Invalid config!'


if __name__ == '__main__':
def entry_point():
try:
mainret = main(docopt(USAGESTR, version=VERSIONSTR, script=SCRIPT))
except InvalidArg as ex:
Expand All @@ -369,4 +380,9 @@ def __str__(self):
except BrokenPipeError:
print_err('\nBroken pipe, input/output was interrupted.\n')
mainret = 3
lock_release()
sys.exit(mainret)


if __name__ == '__main__':
entry_point()

0 comments on commit 4d0ab0f

Please sign in to comment.