ponysay/src/ponysaytool.py
Mattias Andrée fba2fbf862 metadata read fix in ponysay-tool + fix documentation on metadata
Signed-off-by: Mattias Andrée <maandree@operamail.com>
2013-04-06 23:24:17 +02:00

1250 lines
53 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
ponysay - Ponysay, cowsay reimplementation for ponies
Copyright (C) 2012, 2013 Erkin Batu Altunbaş et al.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
If you intend to redistribute ponysay or a fork of it commercially,
it contains aggregated images, some of which may not be commercially
redistribute, you would be required to remove those. To determine
whether or not you may commercially redistribute an image make use
that line FREE: yes, is included inside the image between two $$$
lines and the FREE is and upper case and directly followed by
the colon.
'''
import os
import sys
from subprocess import Popen, PIPE
from argparser import *
from ponysay import *
from metadata import *
'''
The version of ponysay
'''
VERSION = 'dev' # this line should not be edited, it is fixed by the build system
'''
Hack to enforce UTF-8 in output (in the future, if you see anypony not using utf-8 in
programs by default, report them to Princess Celestia so she can banish them to the moon)
@param text:str The text to print (empty string is default)
@param end:str The appendix to the text to print (line breaking is default)
'''
def print(text = '', end = '\n'):
sys.stdout.buffer.write((str(text) + end).encode('utf-8'))
'''
stderr equivalent to print()
@param text:str The text to print (empty string is default)
@param end:str The appendix to the text to print (line breaking is default)
'''
def printerr(text = '', end = '\n'):
sys.stderr.buffer.write((str(text) + end).encode('utf-8'))
'''
This is the mane class of ponysay-tool
'''
class PonysayTool():
'''
Starts the part of the program the arguments indicate
@param args:ArgParser Parsed command line arguments
'''
def __init__(self, args):
if args.argcount == 0:
args.help()
exit(255)
return
opts = args.opts
if unrecognised or (opts['-h'] is not None):
args.help()
if unrecognised:
exit(254)
elif opts['-v'] is not None:
print('%s %s' % ('ponysay-tool', VERSION))
elif opts['--kms'] is not None:
self.generateKMS()
elif (opts['--dimensions'] is not None) and (len(opts['--dimensions']) == 1):
self.generateDimensions(opts['--dimensions'][0], args.files)
elif (opts['--metadata'] is not None) and (len(opts['--metadata']) == 1):
self.generateMetadata(opts['--metadata'][0], args.files)
elif (opts['-b'] is not None) and (len(opts['-b']) == 1):
try:
if opts['--no-term-init'] is None:
print('\033[?1049h', end='') # initialise terminal
cmd = 'stty %s < %s > /dev/null 2> /dev/null'
cmd %= ('-echo -icanon -echo -isig -ixoff -ixon', os.path.realpath('/dev/stdout'))
Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE).wait()
print('\033[?25l', end='') # hide cursor
dir = opts['-b'][0]
if not dir.endswith(os.sep):
dir += os.sep
self.browse(dir, opts['-r'])
finally:
print('\033[?25h', end='') # show cursor
cmd = 'stty %s < %s > /dev/null 2> /dev/null'
cmd %= ('echo icanon echo isig ixoff ixon', os.path.realpath('/dev/stdout'))
Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE).wait()
if opts['--no-term-init'] is None:
print('\033[?1049l', end='') # terminate terminal
elif (opts['--edit'] is not None) and (len(opts['--edit']) == 1):
pony = opts['--edit'][0]
if not os.path.isfile(pony):
printerr('%s is not an existing regular file' % pony)
exit(252)
linuxvt = ('TERM' in os.environ) and (os.environ['TERM'] == 'linux')
try:
if opts['--no-term-init'] is None:
print('\033[?1049h', end='') # initialise terminal
if linuxvt: print('\033[?8c', end='') # use full block for cursor (_ is used by default in linux vt)
cmd = 'stty %s < %s > /dev/null 2> /dev/null'
cmd %= ('-echo -icanon -echo -isig -ixoff -ixon', os.path.realpath('/dev/stdout'))
Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE).wait()
self.editmeta(pony)
finally:
cmd = 'stty %s < %s > /dev/null 2> /dev/null'
cmd %= ('echo icanon echo isig ixoff ixon', os.path.realpath('/dev/stdout'))
Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE).wait()
if linuxvt: print('\033[?0c', end='') # restore cursor
if opts['--no-term-init'] is None:
print('\033[?1049l', end='') # terminate terminal
elif (opts['--edit-rm'] is not None) and (len(opts['--edit-rm']) == 1):
ponyfile = opts['--edit-rm'][0]
pony = None
with open(ponyfile, 'rb') as file:
pony = file.read().decode('utf8', 'replace')
if pony.startswith('$$$\n'):
pony = pony[3:]
pony = pony[pony.index('\n$$$\n') + 5:]
with open(ponyfile, 'wb') as file:
file.write(pony.encode('utf8'))
elif (opts['--edit-stash'] is not None) and (len(opts['--edit-stash']) == 1):
ponyfile = opts['--edit-stash'][0]
pony = None
with open(ponyfile, 'rb') as file:
pony = file.read().decode('utf8', 'replace')
if pony.startswith('$$$\n'):
pony = pony[3:]
pony = pony[:pony.index('\n$$$\n')]
print('$$$' + pony + '\n$$$\n', end='')
else:
print('$$$\n$$$\n', end='')
elif (opts['--edit-apply'] is not None) and (len(opts['--edit-apply']) == 1):
data = ''
while True:
line = input()
if data == '':
if line != '$$$':
printerr('Bad stash')
exit(251)
data += '$$$\n'
else:
data += line + '\n'
if line == '$$$':
break
ponyfile = opts['--edit-apply'][0]
pony = None
with open(ponyfile, 'rb') as file:
pony = file.read().decode('utf8', 'replace')
if pony.startswith('$$$\n'):
pony = pony[3:]
pony = pony[pony.index('\n$$$\n') + 5:]
with open(ponyfile, 'wb') as file:
file.write((data + pony).encode('utf8'))
else:
args.help()
exit(253)
'''
Execute ponysay!
@param args Arguments
@param message Message
'''
def execPonysay(self, args, message = ''):
class PhonyArgParser():
def __init__(self, args, message):
self.argcount = len(args) + (0 if message is None else 1)
for key in args:
self.argcount += len(args[key]) if (args[key] is not None) and isinstance(args[key], list) else 1
self.message = message
self.opts = self
def __getitem__(self, key):
if key in args:
return args[key] if (args[key] is not None) and isinstance(args[key], list) else [args[key]]
return None
def __contains__(self, key):
return key in args;
stdout = sys.stdout
class StringInputStream():
def __init__(self):
self.buf = ''
class Buffer():
def __init__(self, parent):
self.parent = parent
def write(self, data):
self.parent.buf += data.decode('utf8', 'replace')
def flush(self):
pass
self.buffer = Buffer(self)
def flush(self):
pass
def isatty(self):
return True
sys.stdout = StringInputStream()
ponysay = Ponysay()
ponysay.run(PhonyArgParser(args, message))
out = sys.stdout.buf[:-1]
sys.stdout = stdout
return out
'''
Browse ponies
@param ponydir:str The pony directory to browse
@param restriction:list<str> Restrictions on listed ponies, may be None
'''
def browse(self, ponydir, restriction):
## Call `stty` to determine the size of the terminal, this way is better than using python's ncurses
termsize = None
for channel in (sys.stdout, sys.stdin, sys.stderr):
termsize = Popen(['stty', 'size'], stdout=PIPE, stdin=channel, stderr=PIPE).communicate()[0]
if len(termsize) > 0:
termsize = termsize.decode('utf8', 'replace')[:-1].split(' ') # [:-1] removes a \n
termsize = [int(item) for item in termsize]
break
(termh, termw) = termsize
ponies = set()
for ponyfile in os.listdir(ponydir):
if endswith(ponyfile, '.pony'):
ponyfile = ponyfile[:-5]
if ponyfile not in ponies:
ponies.add(ponyfile)
if restriction is not None:
oldponies = ponies
logic = Metadata.makeRestrictionLogic(restriction)
ponies = set()
for pony in Metadata.restrictedPonies(ponydir, logic):
if (pony not in ponies) and (pony in oldponies):
ponies.add(pony)
oldponies = ponies
ponies = list(ponies)
ponies.sort()
if len(ponies) == 0:
print('\033[1;31m%s\033[21m;39m' % 'No ponies... press Enter to exit.')
input()
panelw = Backend.len(max(ponies, key = Backend.len))
panely = 0
panelx = termw - panelw
(x, y) = (0, 0)
(oldx, oldy) = (None, None)
(quotes, info) = (False, False)
(ponyindex, oldpony) = (0, None)
(pony, ponywidth, ponyheight) = (None, None, None)
stored = None
while True:
printpanel = -2 if ponyindex != oldpony else oldpony
if (ponyindex != oldpony):
ponyindex %= len(ponies)
if ponyindex < 0:
ponyindex += len(ponies)
oldpony = ponyindex
ponyfile = (ponydir + '/' + ponies[ponyindex] + '.pony').replace('//', '/')
pony = self.execPonysay({'-f' : ponyfile, '-W' : 'none', '-o' : None}).split('\n')
preprint = '\033[H\033[2J'
if pony[0].startswith(preprint):
pony[0] = pony[0][len(preprint):]
ponyheight = len(pony)
ponywidth = Backend.len(max(pony, key = Backend.len))
AUTO_PUSH = '\033[01010~'
AUTO_POP = '\033[10101~'
pony = '\n'.join(pony).replace('\n', AUTO_PUSH + '\n' + AUTO_POP)
colourstack = ColourStack(AUTO_PUSH, AUTO_POP)
buf = ''
for c in pony:
buf += c + colourstack.feed(c)
pony = buf.replace(AUTO_PUSH, '').replace(AUTO_POP, '').split('\n')
if (oldx != x) or (oldy != y):
(oldx, oldy) = (x, y)
print('\033[H\033[2J', end='')
def getprint(pony, ponywidth, ponyheight, termw, termh, px, py):
ponyprint = pony
if py < 0:
ponyprint = [] if -py > len(ponyprint) else ponyprint[-py:]
elif py > 0:
ponyprint = py * [''] + ponyprint
ponyprint = ponyprint[:len(ponyprint) if len(ponyprint) < termh else termh]
def findcolumn(line, column):
if Backend.len(line) >= column:
return len(line)
pos = len(line)
while Backend.len(line[:pos]) != column:
pos -= 1
return pos
if px < 0:
ponyprint = [('' if -px > Backend.len(line) else line[findcolumn(line, -px):]) for line in ponyprint]
elif px > 0:
ponyprint = [px * ' ' + line for line in ponyprint]
ponyprint = [(line if Backend.len(line) <= termw else line[:findcolumn(line, termw)]) for line in ponyprint]
ponyprint = ['\033[21;39;49;0m%s\033[21;39;49;0m' % line for line in ponyprint]
return '\n'.join(ponyprint)
if quotes:
ponyquotes = None # TODO
quotesheight = len(ponyquotes)
quoteswidth = Backend.len(max(ponyquotes, key = Backend.len))
print(getprint(ponyquotes, quoteswidth, quotesheight, termw, termh, x, y), end='')
elif info:
ponyfile = (ponydir + '/' + ponies[ponyindex] + '.pony').replace('//', '/')
ponyinfo = self.execPonysay({'-f' : ponyfile, '-W' : 'none', '-i' : None}).split('\n')
infoheight = len(ponyinfo)
infowidth = Backend.len(max(ponyinfo, key = Backend.len))
print(getprint(ponyinfo, infowidth, infoheight, termw, termh, x, y), end='')
else:
print(getprint(pony, ponywidth, ponyheight, panelx, termh, x + (panelx - ponywidth) // 2, y + (termh - ponyheight) // 2), end='')
printpanel = -1
if printpanel == -1:
cury = 0
for line in ponies[panely:]:
cury += 1
if os.path.islink((ponydir + '/' + line + '.pony').replace('//', '/')):
line = '\033[34m%s\033[39m' % ((line + ' ' * panelw)[:panelw])
else:
line = (line + ' ' * panelw)[:panelw]
print('\033[%i;%iH\033[%im%s\033[0m' % (cury, panelx + 1, 1 if panely + cury - 1 == ponyindex else 0, line), end='')
elif printpanel >= 0:
for index in (printpanel, ponyindex):
cury = index - panely
if (0 <= cury) and (cury < termh):
line = ponies[cury + panely]
if os.path.islink((ponydir + '/' + line + '.pony').replace('//', '/')):
line = '\033[34m%s\033[39m' % ((line + ' ' * panelw)[:panelw])
else:
line = (line + ' ' * panelw)[:panelw]
print('\033[%i;%iH\033[%im%s\033[0m' % (cury, panelx + 1, 1 if panely + cury - 1 == ponyindex else 0, line), end='')
sys.stdout.buffer.flush()
if stored is None:
d = sys.stdin.read(1)
else:
d = stored
stored = None
recenter = False
if (d == 'w') or (d == 'W') or (d == '<') or (d == 'ä') or (d == 'Ä'): # pad ↑
y -= 1
elif (d == 's') or (d == 'S') or (d == 'o') or (d == 'O'): # pad ↓
y += 1
elif (d == 'd') or (d == 'D') or (d == 'e') or (d == 'E'): # pad →
x += 1
elif (d == 'a') or (d == 'A'): # pad ←
x -= 1
elif (d == 'q') or (d == 'Q'): # toggle quotes
quotes = False if info else not quotes
recenter = True
elif (d == 'i') or (d == 'I'): # toggle metadata
info = False if quotes else not info
recenter = True
elif ord(d) == ord('L') - ord('@'): # recenter
recenter = True
elif ord(d) == ord('P') - ord('@'): # previous
ponyindex -= 1
recenter = True
elif ord(d) == ord('N') - ord('@'): # next
ponyindex += 1
recenter = True
elif ord(d) == ord('Q') - ord('@'):
break
elif ord(d) == ord('X') - ord('@'):
if ord(sys.stdin.read(1)) == ord('C') - ord('@'):
break
elif d == '\033':
d = sys.stdin.read(1)
if d == '[':
d = sys.stdin.read(1)
if d == 'A': stored = chr(ord('P') - ord('@')) if (not quotes) and (not info) else 'W'
elif d == 'B': stored = chr(ord('N') - ord('@')) if (not quotes) and (not info) else 'S'
elif d == 'C': stored = chr(ord('N') - ord('@')) if (not quotes) and (not info) else 'D'
elif d == 'D': stored = chr(ord('P') - ord('@')) if (not quotes) and (not info) else 'A'
elif d == '1':
if sys.stdin.read(1) == ';':
if sys.stdin.read(1) == '5':
d = sys.stdin.read(1)
if d == 'A': stored = 'W'
elif d == 'B': stored = 'S'
elif d == 'C': stored = 'D'
elif d == 'D': stored = 'A'
if recenter:
(oldx, oldy) = (None, None)
(x, y) = (0, 0)
'''
Generate all kmsponies for the current TTY palette
'''
def generateKMS(self):
class PhonyArgParser():
def __init__(self, key, value):
self.argcount = 3
self.message = ''
self.opts = self
self.key = key
self.value = value
def __getitem__(self, key):
return [self.value] if key == self.key else None
def __contains__(self, key):
return key == self.key;
class StringInputStream():
def __init__(self):
self.buf = ''
class Buffer():
def __init__(self, parent):
self.parent = parent
def write(self, data):
self.parent.buf += data.decode('utf8', 'replace')
def flush(self):
pass
self.buffer = Buffer(self)
def flush(self):
pass
def isatty(self):
return True
stdout = sys.stdout
term = os.environ['TERM']
os.environ['TERM'] = 'linux'
sys.stdout = StringInputStream()
ponysay = Ponysay()
ponysay.run(PhonyArgParser('--onelist', None))
stdponies = sys.stdout.buf[:-1].split('\n')
sys.stdout = StringInputStream()
ponysay = Ponysay()
ponysay.run(PhonyArgParser('++onelist', None))
extraponies = sys.stdout.buf[:-1].split('\n')
for pony in stdponies:
printerr('Genering standard kmspony: %s' % pony)
sys.stderr.buffer.flush();
sys.stdout = StringInputStream()
ponysay = Ponysay()
ponysay.run(PhonyArgParser('--pony', pony))
for pony in extraponies:
printerr('Genering extra kmspony: %s' % pony)
sys.stderr.buffer.flush();
sys.stdout = StringInputStream()
ponysay = Ponysay()
ponysay.run(PhonyArgParser('++pony', pony))
os.environ['TERM'] = term
sys.stdout = stdout
'''
Generate pony dimension file for a directory
@param ponydir:str The directory
@param ponies:itr<str>? Ponies to which to limit
'''
def generateDimensions(self, ponydir, ponies = None):
dimensions = []
ponyset = None if (ponies is None) or (len(ponies) == 0) else set(ponies)
for ponyfile in os.listdir(ponydir):
if (ponyset is not None) and (ponyfile not in ponyset):
continue
if ponyfile.endswith('.pony') and (ponyfile != '.pony'):
class PhonyArgParser():
def __init__(self, balloon):
self.argcount = 5
self.message = ''
self.pony = (ponydir + '/' + ponyfile).replace('//', '/')
self.balloon = balloon
self.opts = self
def __getitem__(self, key):
if key == '-f':
return [self.pony]
if key == ('-W' if self.balloon else '-b'):
return [('none' if self.balloon else None)]
return None
def __contains__(self, key):
return key in ('-f', '-W', '-b');
stdout = sys.stdout
class StringInputStream():
def __init__(self):
self.buf = ''
class Buffer():
def __init__(self, parent):
self.parent = parent
def write(self, data):
self.parent.buf += data.decode('utf8', 'replace')
def flush(self):
pass
self.buffer = Buffer(self)
def flush(self):
pass
def isatty(self):
return True
sys.stdout = StringInputStream()
ponysay = Ponysay()
ponysay.run(PhonyArgParser(True))
printpony = sys.stdout.buf[:-1].split('\n')
ponyheight = len(printpony) - 2 # using fallback balloon
ponywidth = Backend.len(max(printpony, key = Backend.len))
ponysay = Ponysay()
ponysay.run(PhonyArgParser(False))
printpony = sys.stdout.buf[:-1].split('\n')
ponyonlyheight = len(printpony)
sys.stdout = stdout
dimensions.append((ponywidth, ponyheight, ponyonlyheight, ponyfile[:-5]))
(widths, heights, onlyheights) = ([], [], [])
for item in dimensions:
widths .append((item[0], item[3]))
heights .append((item[1], item[3]))
onlyheights.append((item[2], item[3]))
for items in (widths, heights, onlyheights):
sorted(items, key = lambda item : item[0])
for pair in ((widths, 'widths'), (heights, 'heights'), (onlyheights, 'onlyheights')):
(items, dimfile) = pair
dimfile = (ponydir + '/' + dimfile).replace('//', '/')
ponies = [item[1] for item in items]
dims = []
last = -1
index = 0
for item in items:
cur = item[0]
if cur != last:
if last >= 0:
dims.append((last, index))
last = cur
index += 1
if last >= 0:
dims.append((last, index))
dims = ''.join([('%i/%i/' % (dim[0], len('/'.join(ponies[:dim[1]])))) for dim in dims])
data = '/' + str(len(dims)) + '/' + dims + '/'.join(ponies) + '/'
with open(dimfile, 'wb') as file:
file.write(data.encode('utf8'))
file.flush()
'''
Generate pony metadata collection file for a directory
@param ponydir:str The directory
@param ponies:itr<str>? Ponies to which to limit
'''
def generateMetadata(self, ponydir, ponies = None):
if not ponydir.endswith('/'):
ponydir += '/'
def makeset(value):
rc = set()
bracket = 0
esc = False
buf = ''
for c in value:
if esc:
if bracket == 0:
if c not in (',', '\\', '(', ')'):
buf += '\\'
buf += c
esc = False
elif c == '(':
bracket += 1
elif c == ')':
if bracket == 0:
raise Exception('Bracket mismatch')
bracket -= 1
elif c == '\\':
esc = True
elif bracket == 0:
if c == ',':
buf = buf.strip()
if len(buf) > 0:
rc.add(buf)
buf = ''
else:
buf += c
if bracket > 0:
raise Exception('Bracket mismatch')
buf = buf.strip()
if len(buf) > 0:
rc.add(buf)
return rc
everything = []
ponyset = None if (ponies is None) or (len(ponies) == 0) else set(ponies)
for ponyfile in os.listdir(ponydir):
if (ponyset is not None) and (ponyfile not in ponyset):
continue
if ponyfile.endswith('.pony') and (ponyfile != '.pony'):
with open(ponydir + ponyfile, 'rb') as file:
data = file.read().decode('utf8', 'replace')
data = [line.replace('\n', '') for line in data.split('\n')]
if data[0] != '$$$':
meta = []
else:
sep = 1
while data[sep] != '$$$':
sep += 1
meta = data[1 : sep]
data = {}
for line in meta:
if ':' in line:
key = line[:line.find(':')].strip()
value = line[line.find(':') + 1:]
test = key
for c in 'ABCDEFGHIJKLMN OPQRSTUVWXYZ':
test = test.replace(c, '')
if (len(test) == 0) and (len(key) > 0):
vals = makeset(value.replace(' ', ''))
if key not in data:
data[key] = vals
else:
dset = data[key]
for val in vals:
dset.add(val)
everything.append((ponyfile[:-5], data))
import pickle
with open((ponydir + '/metadata').replace('//', '/'), 'wb') as file:
pickle.dump(everything, file, -1)
file.flush()
'''
Edit a pony file's metadata
@param ponyfile:str A pony file to edit
'''
def editmeta(self, ponyfile):
(data, meta, image) = 3 * [None]
with open(ponyfile, 'rb') as file:
data = file.read().decode('utf8', 'replace')
data = [line.replace('\n', '') for line in data.split('\n')]
if data[0] != '$$$':
image = data
meta = []
else:
sep = 1
while data[sep] != '$$$':
sep += 1
meta = data[1 : sep]
image = data[sep + 1:]
class PhonyArgParser():
def __init__(self):
self.argcount = 5
self.message = ponyfile
self.opts = self
def __getitem__(self, key):
if key == '-f': return [ponyfile]
if key == '-W': return ['n']
return None
def __contains__(self, key):
return key in ('-f', '-W');
data = {}
comment = []
for line in meta:
if ': ' in line.replace('\t', ' '):
key = line.replace('\t', ' ')
key = key[:key.find(': ')]
test = key
for c in 'ABCDEFGHIJKLMN OPQRSTUVWXYZ':
test = test.replace(c, '')
if (len(test) == 0) and (len(key.replace(' ', '')) > 0):
key = key.strip(' ')
value = line.replace('\t', ' ')
value = value[value.find(': ') + 2:]
if key not in data:
data[key] = value.strip(' ')
else:
data[key] += '\n' + value.strip(' ')
else:
comment.append(line)
else:
comment.append(line)
cut = 0
while (len(comment) > cut) and (len(comment[cut]) == 0):
cut += 1
comment = comment[cut:]
stdout = sys.stdout
class StringInputStream():
def __init__(self):
self.buf = ''
class Buffer():
def __init__(self, parent):
self.parent = parent
def write(self, data):
self.parent.buf += data.decode('utf8', 'replace')
def flush(self):
pass
self.buffer = Buffer(self)
def flush(self):
pass
def isatty(self):
return True
sys.stdout = StringInputStream()
ponysay = Ponysay()
ponysay.run(PhonyArgParser())
printpony = sys.stdout.buf[:-1].split('\n')
sys.stdout = stdout
preprint = '\033[H\033[2J'
if printpony[0].startswith(preprint):
printpony[0] = printpony[0][len(preprint):]
ponyheight = len(printpony) - len(ponyfile.split('\n')) + 1 - 2 # using fallback balloon
ponywidth = Backend.len(max(printpony, key = Backend.len))
## Call `stty` to determine the size of the terminal, this way is better than using python's ncurses
termsize = None
for channel in (sys.stdout, sys.stdin, sys.stderr):
termsize = Popen(['stty', 'size'], stdout=PIPE, stdin=channel, stderr=PIPE).communicate()[0]
if len(termsize) > 0:
termsize = termsize.decode('utf8', 'replace')[:-1].split(' ') # [:-1] removes a \n
termsize = [int(item) for item in termsize]
break
AUTO_PUSH = '\033[01010~'
AUTO_POP = '\033[10101~'
modprintpony = '\n'.join(printpony).replace('\n', AUTO_PUSH + '\n' + AUTO_POP)
colourstack = ColourStack(AUTO_PUSH, AUTO_POP)
buf = ''
for c in modprintpony:
buf += c + colourstack.feed(c)
modprintpony = buf.replace(AUTO_PUSH, '').replace(AUTO_POP, '')
printpony = [('\033[21;39;49;0m%s%s\033[21;39;49;0m' % (' ' * (termsize[1] - ponywidth), line)) for line in modprintpony.split('\n')]
print(preprint, end='')
print('\n'.join(printpony), end='')
print('\033[H', end='')
print('Please see the info manual for details on how to fill out this form')
print()
if 'WIDTH' in data: del data['WIDTH']
if 'HEIGHT' in data: del data['HEIGHT']
data['comment'] = '\n'.join(comment)
fields = [key for key in data]
fields.sort()
standardfields = ['GROUP NAME', 'NAME', 'OTHER NAMES', 'APPEARANCE', 'KIND',
'GROUP', 'BALLOON', 'LINK', 'LINK ON', 'COAT', 'MANE', 'EYE',
'AURA', 'DISPLAY', 'BALLOON TOP', 'BALLOON BOTTOM', 'MASTER',
'POSE', 'BASED ON', 'SOURCE', 'MEDIA', 'LICENSE', 'FREE',
'comment']
for standard in standardfields:
if standard in fields:
del fields[fields.index(standard)]
if standard not in data:
data[standard] = ''
fields = standardfields[:-1] + fields + [standardfields[-1]]
def saver(ponyfile, ponyheight, ponywidth, data, image):
class Saver():
def __init__(self, ponyfile, ponyheight, ponywidth, data, image):
(self.ponyfile, self.ponyheight, self.ponywidth, self.data, self.image) = (ponyfile, ponyheight, ponywidth, data, image)
def __call__(self): # functor
comment = self.data['comment']
comment = ('\n' + comment + '\n').replace('\n$$$\n', '\n\\$$$\n')[:-1]
meta = []
keys = [key for key in data]
keys.sort()
for key in keys:
if self.data[key] is None:
continue
if (key == 'comment') or (len(self.data[key].strip()) == 0):
continue
values = self.data[key].strip()
for value in values.split('\n'):
meta.append(key + ': ' + value)
meta.append('WIDTH: ' + str(self.ponywidth))
meta.append('HEIGHT: ' + str(self.ponyheight))
# TODO auto fill in BALLOON {TOP,BOTTOM}
meta.append(comment)
meta = '\n'.join(meta)
ponydata = '$$$\n' + meta + '\n$$$\n' + '\n'.join(self.image)
with open(self.ponyfile, 'wb') as file:
file.write(ponydata.encode('utf8'))
file.flush()
return Saver(ponyfile, ponyheight, ponywidth, data, image)
textarea = TextArea(fields, data, 1, 3, termsize[1] - ponywidth, termsize[0] - 2, termsize)
textarea.run(saver(ponyfile, ponyheight, ponywidth, data, image))
'''
GNU Emacs alike text area
'''
class TextArea(): # TODO support small screens
'''
Constructor
@param fields:list<str> Field names
@param datamap:dist<str,str> Data map
@param left:int Left position of the component
@param top:int Top position of the component
@param width:int Width of the component
@param height:int Height of the component
@param termsize:(int,int) The height and width of the terminal
'''
def __init__(self, fields, datamap, left, top, width, height, termsize):
(self.fields, self.datamap, self.left, self.top, self.width, self.height, self.termsize) \
= (fields, datamap, left, top, width - 1, height, termsize)
'''
Execute text reading
@param saver Save method
'''
def run(self, saver):
innerleft = UCS.dispLen(max(self.fields, key = UCS.dispLen)) + self.left + 3
leftlines = []
datalines = []
for key in self.fields:
for line in self.datamap[key].split('\n'):
leftlines.append(key)
datalines.append(line)
(termh, termw) = self.termsize
(y, x) = (0, 0)
mark = None
KILL_MAX = 50
killring = []
killptr = None
def status(text):
print('\033[%i;%iH\033[7m%s\033[27m\033[%i;%iH' % (termh - 1, 1, ' (' + text + ') ' + '-' * (termw - len(' (' + text + ') ')), self.top + y, innerleft + x), end='')
status('unmodified')
print('\033[%i;%iH' % (self.top, innerleft), end='')
def alert(text):
if text is None:
alert('')
else:
print('\033[%i;%iH\033[2K%s\033[%i;%iH' % (termh, 1, text, self.top + y, innerleft + x), end='')
modified = False
override = False
(oldy, oldx, oldmark) = (y, x, mark)
stored = chr(ord('L') - ord('@'))
alerted = False
edited = False
print('\033[%i;%iH' % (self.top + y, innerleft + x), end='')
while True:
if (oldmark is not None) and (oldmark >= 0):
if oldmark < oldx:
print('\033[%i;%iH\033[49m%s\033[%i;%iH' % (self.top + oldy, innerleft + oldmark, datalines[oldy][oldmark : oldx], self.top + y, innerleft + x), end='')
elif oldmark > oldx:
print('\033[%i;%iH\033[49m%s\033[%i;%iH' % (self.top + oldy, innerleft + oldx, datalines[oldy][oldx : oldmark], self.top + y, innerleft + x), end='')
if (mark is not None) and (mark >= 0):
if mark < x:
print('\033[%i;%iH\033[44;37m%s\033[49;39m\033[%i;%iH' % (self.top + y, innerleft + mark, datalines[y][mark : x], self.top + y, innerleft + x), end='')
elif mark > x:
print('\033[%i;%iH\033[44;37m%s\033[49;39m\033[%i;%iH' % (self.top + y, innerleft + x, datalines[y][x : mark], self.top + y, innerleft + x), end='')
if y != oldy:
if (oldy > 0) and (leftlines[oldy - 1] == leftlines[oldy]) and (leftlines[oldy] == leftlines[-1]):
print('\033[%i;%iH\033[34m%s\033[39m' % (self.top + oldy, self.left, '>'), end='')
else:
print('\033[%i;%iH\033[34m%s:\033[39m' % (self.top + oldy, self.left, leftlines[oldy]), end='')
if (y > 0) and (leftlines[y - 1] == leftlines[y]) and (leftlines[y] == leftlines[-1]):
print('\033[%i;%iH\033[1;34m%s\033[21;39m' % (self.top + y, self.left, '>'), end='')
else:
print('\033[%i;%iH\033[1;34m%s:\033[21;39m' % (self.top + y, self.left, leftlines[y]), end='')
print('\033[%i;%iH' % (self.top + y, innerleft + x), end='')
(oldy, oldx, oldmark) = (y, x, mark)
if edited:
edited = False
if not modified:
modified = True
status('modified' + (' override' if override else ''))
sys.stdout.flush()
if stored is None:
d = sys.stdin.read(1)
else:
d = stored
stored = None
if alerted:
alerted = False
alert(None)
if ord(d) == ord('@') - ord('@'):
if mark is None:
mark = x
alert('Mark set')
elif mark == ~x:
mark = x
alert('Mark activated')
elif mark == x:
mark = ~x
alert('Mark deactivated')
else:
mark = x
alert('Mark set')
alerted = True
elif ord(d) == ord('K') - ord('@'):
if x == len(datalines[y]):
alert('At end')
alerted = True
else:
mark = len(datalines[y])
stored = chr(ord('W') - ord('@'))
elif ord(d) == ord('W') - ord('@'):
if (mark is not None) and (mark >= 0) and (mark != x):
selected = datalines[y][mark : x] if mark < x else datalines[y][x : mark]
killring.append(selected)
if len(killring) > KILL_MAX:
killring = killring[1:]
stored = chr(127)
else:
alert('No text is selected')
alerted = True
elif ord(d) == ord('Y') - ord('@'):
if len(killring) == 0:
alert('Killring is empty')
alerted = True
else:
mark = None
killptr = len(killring) - 1
yanked = killring[killptr]
print('\033[%i;%iH%s' % (self.top + y, innerleft + x, yanked + datalines[y][x:]), end='')
datalines[y] = datalines[y][:x] + yanked + datalines[y][x:]
x += len(yanked)
print('\033[%i;%iH' % (self.top + y, innerleft + x), end='')
elif ord(d) == ord('X') - ord('@'):
alert('C-x')
alerted = True
sys.stdout.flush()
d = sys.stdin.read(1)
alert(str(ord(d)))
sys.stdout.flush()
if ord(d) == ord('X') - ord('@'):
if (mark is not None) and (mark >= 0):
x ^= mark; mark ^= x; x ^= mark
alert('Mark swapped')
else:
alert('No mark is activated')
elif ord(d) == ord('S') - ord('@'):
last = ''
for row in range(0, len(datalines)):
current = leftlines[row]
if len(datalines[row].strip()) == 0:
if current is not 'comment':
if current != last:
self.datamap[current] = None
continue
if current == last:
self.datamap[current] += '\n' + datalines[row]
else:
self.datamap[current] = datalines[row]
last = current
saver()
status('unmodified' + (' override' if override else ''))
alert('Saved')
elif ord(d) == ord('C') - ord('@'):
break
else:
stored = d
alerted = False
alert(None)
elif (ord(d) == 127) or (ord(d) == 8):
removed = 1
if (mark is not None) and (mark >= 0) and (mark != x):
if mark > x:
x ^= mark; mark ^= x; x ^= mark
removed = x - mark
if x == 0:
alert('At beginning')
alerted = True
continue
dataline = datalines[y]
datalines[y] = dataline = dataline[:x - removed] + dataline[x:]
x -= removed
mark = None
print('\033[%i;%iH%s%s\033[%i;%iH' % (self.top + y, innerleft, dataline, ' ' * removed, self.top + y, innerleft + x), end='')
edited = True
elif ord(d) < ord(' '):
if ord(d) == ord('P') - ord('@'):
if y == 0:
alert('At first line')
alerted = True
else:
y -= 1
mark = None
x = 0
elif ord(d) == ord('N') - ord('@'):
if y == len(datalines) - 1:
datalines.append('')
leftlines.append(leftlines[-1])
y += 1
mark = None
x = 0
elif ord(d) == ord('F') - ord('@'):
if x < len(datalines[y]):
x += 1
print('\033[C', end='')
else:
alert('At end')
alerted = True
elif ord(d) == ord('B') - ord('@'):
if x > 0:
x -= 1
print('\033[D', end='')
else:
alert('At beginning')
alerted = True
elif ord(d) == ord('O') - ord('@'):
leftlines[y : y] = [leftlines[y]]
datalines[y : y] = ['']
y += 1
mark = None
x = 0
stored = chr(ord('L') - ord('@'))
elif ord(d) == ord('L') - ord('@'):
empty = '\033[0m' + (' ' * self.width + '\n') * len(datalines)
print('\033[%i;%iH%s' % (self.top, self.left, empty), end='')
for row in range(0, len(leftlines)):
leftline = leftlines[row] + ':'
if (leftlines[row - 1] == leftlines[row]) and (leftlines[row] == leftlines[-1]):
leftline = '>'
print('\033[%i;%iH\033[%s34m%s\033[%s39m' % (self.top + row, self.left, '1;' if row == y else '', leftline, '21;' if row == y else ''), end='')
for row in range(0, len(datalines)):
print('\033[%i;%iH%s\033[49m' % (self.top + row, innerleft, datalines[row]), end='')
print('\033[%i;%iH' % (self.top + y, innerleft + x), end='')
elif d == '\033':
d = sys.stdin.read(1)
if d == '[':
d = sys.stdin.read(1)
if d == 'A':
stored = chr(ord('P') - ord('@'))
elif d == 'B':
if y == len(datalines) - 1:
alert('At last line')
alerted = True
else:
stored = chr(ord('N') - ord('@'))
elif d == 'C':
stored = chr(ord('F') - ord('@'))
elif d == 'D':
stored = chr(ord('B') - ord('@'))
elif d == '2':
d = sys.stdin.read(1)
if d == '~':
override = not override
status(('modified' if modified else 'unmodified') + (' override' if override else ''))
elif d == '3':
d = sys.stdin.read(1)
if d == '~':
removed = 1
if (mark is not None) and (mark >= 0) and (mark != x):
if mark < x:
x ^= mark; mark ^= x; x ^= mark
removed = mark - x
dataline = datalines[y]
if x == len(dataline):
alert('At end')
alerted = True
continue
datalines[y] = dataline = dataline[:x] + dataline[x + removed:]
print('\033[%i;%iH%s%s\033[%i;%iH' % (self.top + y, innerleft, dataline, ' ' * removed, self.top + y, innerleft + x), end='')
mark = None
edited = True
else:
while True:
d = sys.stdin.read(1)
if (ord('a') <= ord(d)) and (ord(d) <= ord('z')): break
if (ord('A') <= ord(d)) and (ord(d) <= ord('Z')): break
if d == '~': break
elif (d == 'w') or (d == 'W'):
if (mark is not None) and (mark >= 0) and (mark != x):
selected = datalines[y][mark : x] if mark < x else datalines[y][x : mark]
killring.append(selected)
mark = None
if len(killring) > KILL_MAX:
killring = killring[1:]
else:
alert('No text is selected')
alerted = True
elif (d == 'y') or (d == 'Y'):
if killptr is not None:
yanked = killring[killptr]
dataline = datalines[y]
if (len(yanked) <= x) and (dataline[x - len(yanked) : x] == yanked):
killptr -= 1
if killptr < 0:
killptr += len(killring)
dataline = dataline[:x - len(yanked)] + killring[killptr] + dataline[x:]
additional = len(killring[killptr]) - len(yanked)
x += additional
datalines[y] = dataline
print('\033[%i;%iH%s%s\033[%i;%iH' % (self.top + y, innerleft, dataline, ' ' * max(0, -additional), self.top + y, innerleft + x), end='')
else:
stored = chr(ord('Y') - ord('@'))
else:
stored = chr(ord('Y') - ord('@'))
elif d == 'O':
d = sys.stdin.read(1)
if d == 'H':
x = 0
elif d == 'F':
x = len(datalines[y])
print('\033[%i;%iH' % (self.top + y, innerleft + x), end='')
elif d == '\n':
stored = chr(ord('N') - ord('@'))
else:
insert = d
if len(insert) == 0:
continue
dataline = datalines[y]
if (not override) or (x == len(dataline)):
print(insert + dataline[x:], end='')
if len(dataline) - x > 0:
print('\033[%iD' % (len(dataline) - x), end='')
datalines[y] = dataline[:x] + insert + dataline[x:]
if (mark is not None) and (mark >= 0):
if mark >= x:
mark += len(insert)
else:
print(insert, end='')
datalines[y] = dataline[:x] + insert + dataline[x + 1:]
x += len(insert)
edited = True
'''
The user's home directory
'''
HOME = os.environ['HOME'] if 'HOME' in os.environ else os.path.expanduser('~')
'''
Whether stdin is piped
'''
pipelinein = not sys.stdin.isatty()
'''
Whether stdout is piped
'''
pipelineout = not sys.stdout.isatty()
'''
Whether stderr is piped
'''
pipelineerr = not sys.stderr.isatty()
usage_program = '\033[34;1mponysay-tool\033[21;39m'
usage = '\n'.join(['%s %s' % (usage_program, '(--help | --version | --kms)'),
'%s %s' % (usage_program, '(--edit | --edit-rm) \033[33mPONY-FILE\033[39m'),
'%s %s' % (usage_program, '--edit-stash \033[33mPONY-FILE\033[39m > \033[33mSTASH-FILE\033[39m'),
'%s %s' % (usage_program, '--edit-apply \033[33mPONY-FILE\033[39m < \033[33mSTASH-FILE\033[39m'),
'%s %s' % (usage_program, '(--dimensions | --metadata) \033[33mPONY-DIR\033[39m'),
'%s %s' % (usage_program, '--browse \033[33mPONY-DIR\033[39m [-r \033[33mRESTRICTION\033[39m]*'),
])
usage = usage.replace('\033[', '\0')
for sym in ('[', ']', '(', ')', '|', '...', '*'):
usage = usage.replace(sym, '\033[2m' + sym + '\033[22m')
usage = usage.replace('\0', '\033[')
'''
Argument parsing
'''
opts = ArgParser(program = 'ponysay-tool',
description = 'Tool chest for ponysay',
usage = usage,
longdescription = None)
opts.add_argumentless(['--no-term-init']) # for debugging
opts.add_argumentless(['-h', '--help'], help = 'Print this help message.')
opts.add_argumentless(['-v', '--version'], help = 'Print the version of the program.')
opts.add_argumentless(['--kms'], help = 'Generate all kmsponies for the current TTY palette')
opts.add_argumented( ['--dimensions'], arg = 'PONY-DIR', help = 'Generate pony dimension file for a directory')
opts.add_argumented( ['--metadata'], arg = 'PONY-DIR', help = 'Generate pony metadata collection file for a directory')
opts.add_argumented( ['-b', '--browse'], arg = 'PONY-DIR', help = 'Browse ponies in a directory')
opts.add_argumented( ['-r', '--restrict'], arg = 'RESTRICTION', help = 'Metadata based restriction for --browse')
opts.add_argumented( ['--edit'], arg = 'PONY-FILE', help = 'Edit a pony file\'s metadata')
opts.add_argumented( ['--edit-rm'], arg = 'PONY-FILE', help = 'Remove metadata from a pony file')
opts.add_argumented( ['--edit-apply'], arg = 'PONY-FILE', help = 'Apply metadata from stdin to a pony file')
opts.add_argumented( ['--edit-stash'], arg = 'PONY-FILE', help = 'Print applyable metadata from a pony file')
'''
Whether at least one unrecognised option was used
'''
unrecognised = not opts.parse()
PonysayTool(args = opts)