Sunday, 29 August 2010

Flashcards on Symbian with Python

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.

'''
 Simplified Leitner.
 For use with decks from Flashcarddb.com exported with @ separators.

 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


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 = 0

def 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
   
# end

class 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 = 1

class 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 pathname

if 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()


Posted via email from kwhitefoot's posterous

No comments:

Post a Comment

Followers