This program lets you use sets of flashcards downloaded from flashcarddb.com (or made yourself). The cards must be lines withe front and back separated by @ signs.
'''
Copyright 2010, Kevin Whitefoot <kwhitefoot@hotmail.com> License: use as you will so long as you maintain the credits. Language: mshell <http://www.m-shell.net> Method: See http://flashcarddb.com/leitner
Simplified Leitner.
For use with decks from Flashcarddb.com exported with @ separators.
Notes:- My mobile has Python 2.2.2 so string formatting has to be done with
the % operator not string interpolation.
'''
''' Decide if we are running in a terminal or on the mobile. Note
that this probably only works for *nix not Windows.Adapted from
http://www.gossamer-threads.com/lists/python/python/379465'''
import sys
import time
import os
import os.path
def emitmobile(s):
# Must set the cursor position because the up arrow key is
# passed to the normal handler as well as ours so it moves the
# cursor.
text.set_pos(text.len())
text.add('\n' + s)
text.set_pos(text.len())
def emitterminal(s):
print s
try:
import tty, termios
runningOnMobile = False
emit = emitterminal
def getch():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
except:
# '... choose Symbian stuff instead'
runningOnMobile = True
emit = emitmobile
CmdCorrect = 1
CmdWrong = 2
CmdQuit = 3
CmdAllDone = 4
'''
Return true if user is correct.
'''
def getcmd():
if runningOnMobile:
pass
else:
ch = getch()
if ord(ch) == 27: # escape
ch = getch()
c = ord(getch())
if c == 65:
return CmdCorrect
elif c in [53, 54]:
ch =getch()
return CmdWrong
else:
return CmdWrong
elif ch == 'q':
return CmdQuit
else:
return CmdWrong
""" Initialize some tables, etc.
See http:# flashcarddb.com/leitner
Deck
Number Time until next repetition
One None
Two 1 day
Three 3 days
Four 1 week
Five 1 month
"""
oneday = 24*60*60 # seconds in a day
expiry = [0, 0, 1 * oneday, 3 * oneday, 7 * oneday, 30 * oneday]lsLoaded=0
lsLoadedWithDuplicates = 1
lsZeroLength = 2
lsTooManyStates = 3
lsTooFewStates = 4""" debug
Set verbosity higher to generate more logging output.
"""
verbosity = 0def verbose(n, s):
if n < verbosity :
print s
# end
# end
class Card:
""" Cards are simple data structures that can be read from an open
text file. A card has a front and a back. They are never
modified by the program.
"""
# front
# back # f is an open file
def load(self, line):
i = line.index("@")
self.front = line[0:i].strip()
self.back = line[i+1:].strip()
# f is an open file
def save(self, file) :
file.writeline(front + u"@" + back)
# end
# f is an open file
def __init__(self, line):
self.load(line)
# end# end
class Cards:
"""Cards is a list of Card instances in the order they were found
in a file. The file name is generated from the set name by
appending .cards.
"""
# cards
# setname
def count(self):
return len(self.cards)
# end
def filename(self):
return os.path.join(pathname,self.setname) + u".cards"
# end
def get(self, i):
return self.cards[i]
# end
def add(self, c):
self.cards.append(c)
# end
# f is an open file
def load(self):
t = time.time()
f = open(self.filename())
for line in f :
c = Card(line)
self.add(c)
# end
d = time.time()-t
# emit(u"Cards.load: {0} cards in {1} seconds".format(len(self.cards), d))
emit(u"Cards.load: %s cards in %s seconds" % (len(self.cards), d))
# end
def __init__(self, setname):
self.setname = setname
self.cards = []
self.load()
# end
# f is an open file
def save(self, f):
c = null
for c in self.cards:
c.save(f)
# end
# end
# endclass CardState:
"""
The state of each card is stored in a card state object. These
objects are stored in a file that is updated every time a card
changes state. To save time the new stte is simply appended to the
end of the file. When reading the file back later entries for the
same card overwrite the newer ones. The state is : written out
again so that the file does not grow indefinitely.
""" # card # index into array of cards.
# deck # number of the deck to which this card belongs.
# expires # date at which the card should be reviewed expressed as
# seconds from the start of the epoch.
def getcard(self, cards):
return cards.get(self.card)
# end
def __init__(self, card, deck):
self.card = card
self.deck = deck
self.expires = 0
# end
def loadfromstr(self, s):
a = s.split()
self.card = int(a[0])
self.deck = int(a[1])
self.expires = float(a[2])
# end
# f is an open file
def load(self, file):
s = io.readln(file)
self.loadfromstr(s)
# end
# f is an open file
def save(self, file):
file.write("%s %s %s\n" % (self.card, self.deck, self.expires))
# end
def promote(self):
if self.deck < 5 :
self.deck += 1
# end
self.expires = time.time() + expiry[self.deck]
# end
def demote(self):
self.deck = 1
self.expires = 0
# end# end
""""""
class CardStates: # cards:Cards
# cardstates # array of CardState
# setname
def get(self, i):
return self.cardstates[i]
# end
def card(self, i):
cs = self.cardstates[i]
return cs.getcard(self.cards)
# end
def count(self):
return self.cards.count()
# end
def filename(self):
return os.path.join(pathname,self.setname) + u".state"
# end
def savestate(self):
# emit(u"CardStates.savestate {0} states".format(len(self.cardstates)))
emit(u"CardStates.savestate %s states" % (len(self.cardstates)))
f = open(self.filename(),'w')
for cs in self.cardstates :
cs.save(f)
# end
f.close()
# end
def loadstate(self):
"""
Returns a code indicating the result. It can happen that the
user will update the cards file. In such a case the states file
will have to be updated to match.
"""
t = time.time()
f = open(self.filename(), 'r')
cr = 0 # count read
cu = 0 # count used
lc = self.cards.count()
# Initialize array of cardstates to empty .
for i in range(lc):
self.cardstates.append(None)
for s in f:
cr += 1 # count how many we read so that we can tell if
# there were duplicates
cs = CardState(0, 0)
cs.loadfromstr(s)
if cs.card < lc :
# Note that we do not just append because we want to ensure
# that later duplicates override the earlier ones
if self.cardstates[cs.card] == None :
cu += 1
# end
# overwrite even if was not null.
self.cardstates[cs.card] = cs
else:
# do nothing, discard state for non-existent card
pass
# end
# end
if s == None:
break
f.close # want to open it for writing again.
# emit(u"CardStates load: {0} states in {1} seconds".format(cr, time.time()-t))
# emit(u" : {0} state used".format(cu))
emit(u"CardStates load: %s states in %s seconds" % (cr, time.time()-t))
emit(u" : %s state used" % (cu))
if cu < lc :
# some cards do not have a state. This happens when the user
# overwrites the cards file with a new longer (more lines)
# file.
return lsTooFewStates
elif cr != cu :
# some cards had duplicate state records this is not a bug, it
# happens because when we write a new state record we do just
# that instead of rewriting the whole state file. When we start
# the program we remove the duplicates and rewrite the file.
return lsLoadedWithDuplicates
else:
return lsLoaded
# end
# end
def cardstate(self, card):
return self.cardstates[card]
# end
def savecardstate(self, card):
f = open(self.filename(), "a")
# cs:CardState = cardstates[card]
self.cardstate(card).save(f)
f.close()
# end
def promote(self, card):
cs = self.cardstates[card]
cs.promote()
self.savecardstate(card)
# end
def demote(self, card):
cs = self.cardstates[card]
cs.demote()
self.savecardstate(card)
# end
def initstate(self):
# not found, first time
for i in range(self.cards.count()):
cs = CardState(i, 1)
self.cardstates.append(cs)
# end
self.savestate()
# end
def qfixupstates(self):
""" Attempt to add states for cards added to the cards file
and remove states that are no longer needed. At this stage
the cardstates and cards list are both ordered by card number
so all we have to do is look for nulls in the cardstates list.
"""
emit(u"CardStates.qfixupstates")
for i in range(len(self.cardstates)):
cs = self.cardstates[i]
if cs == None :
# no state for this card so add one in deck 1.
self.cardstates[i] = CardState(i, 1)
# end
# end
# end
def __init__(self, setname):
self.setname = setname
self.cardstates = []
self.cards = Cards(setname)
if os.path.exists(self.filename()) :
r = self.loadstate()
if r == lsTooFewStates :
self.qfixupstates()
self.savestate()
elif r == lsTooManyStates :
self.qfixupstates()
self.savestate()
elif r == lsZeroLength :
self.initstate()
self.savestate()
elif r == lsLoadedWithDuplicates :
# Save the state to remove duplicates
self.savestate()
pass
elif r == lsLoaded :
# success, nothing to do
pass
else:
emit(u"Unxpected result from loadstates: %s" % (r))
# end
else:
self.initstate()
# end
# end
def getnext(self, currentcard):
now = time.time()
c = self.count()
cc = currentcard
while cc+1 < c:
cc += 1
e = self.cardstate(cc).expires
if e <= now :
return cc
return -1
# end# end
# States
ShowingFront = 0
ShowingBack = 1class Flash_Set: # setname
# cardstates # :CardStates
# currentcard
# currentdeck
# Load from disk if possible.
def __init__(self, name):
self.setname = name
self.cardstates = CardStates(name)
self.currentcard = -1
self.currentdeck = 1
self.state = 0
# A hack to let me overcome the fact that the cursor in a text
# object is moved by the navigation keys
self.timer = e32.Ao_timer()
def getnext(self):
c = self.cardstates.getnext(self.currentcard)
if c != -1 :
return c
# end
# Ran off the end so check from beginning again in case any have been demoted.
emit(u"Studied all cards due today, checking for demoted cards.")
self.currentcard = -1
return self.cardstates.getnext(self.currentcard)
# end
def promote(self):
self.cardstates.promote(self.currentcard)
# end
def demote(self):
self.cardstates.demote(self.currentcard)
# end def showfront(self):
"""Show the front of the current card.
"""
self.currentcard = self.getnext()
if self.currentcard == -1 :
return CmdAllDone
self.current = self.cardstates.card(self.currentcard)
# emit(u"{0} {1} {2}".format(self.currentdeck, self.currentcard, c.front))
emit(u"%s %s %s" % (self.currentdeck, self.currentcard, self.current.front))
self.state = ShowingFront
'''
Return true if all shown or quit.
'''
def shownext(self):
self.showfront()
# wait for user to click a key, don't care what unless it is quit.
k = getcmd()
if k == CmdQuit:
return k # show the back of the card
# print c
emit(self.current.back) # wait for user to respond
k = getcmd()
if k == CmdCorrect:
self.promote()
elif k == CmdWrong:
self.demote()
# end return k # end
def movecursortoend(arg):
"""Move the cursor to the end of the text because it is distracting.
"""
if runningOnMobile:
text.set_pos(text.len())
def handleup(self):
# Simple hack to move the cursor to the end out of the text
# after the underlying up arrow event has been handled by the
# text control because I find it distracting that the up key
# moves the cursor to the middle of the line above the end. A
# ten milli-second delay is enough on the N73.
self.timer.after(0.01, self.movecursortoend)
if self.state == ShowingBack:
self.promote()
return self.showfront()
else:
emit(u"%s" % self.current.back)
self.state = ShowingBack
def handledown(self):
if self.state == ShowingBack:
self.demote()
return self.showfront()
else:
emit(u"%s" % self.current.back)
self.state = ShowingBack
def showall(self):
self.showfront()
while True:
k = getcmd()
if k == CmdWrong:
k = self.handledown()
elif k == CmdCorrect:
k = self.handleup() if (k in [CmdQuit, CmdAllDone]):
break
if k == CmdAllDone:
emit(u"Studied all cards + nothing left to don today.")
else:
emit(u"User quitting") # end# end
#Define the exit function
def quit():
app_lock.signal()
# define functions to show notifications
def up():
f.handleup()
def down():
f.handledown()
def initMobile(): global app_lock
app_lock = e32.Ao_lock() appuifw.app.exit_key_handler = quit
# Create an instance of Text and set it as the application's body
global text
text = appuifw.Text()
appuifw.app.body = text
def runMobile(): text.add(u"Flashcards for Symbian\n")
# Bind keys
text.bind(key_codes.EKeyUpArrow, up)
text.bind(key_codes.EKeyDownArrow, down)
# show the first card
f.showfront() # Wait for the user to request the exit
app_lock.wait()# main#ui.keys(ui.strokes) # return keystrokes
pathname = os.path.dirname(sys.argv[0])
pathname = os.path.abspath(pathname)
# print pathnameif runningOnMobile:
# Do this before loading anything because the load functions emit
# logging information.
import appuifw, e32, key_codes
initMobile()f = Flash_Set("flashcards")
if runningOnMobile:
runMobile()
else:
f.showall()
No comments:
Post a Comment