def main(): #This makes it so if you hit CTRL+C curses doesn't eat the terminal alive! signal.signal(signal.SIGINT, signal_handler) #Setup curses stdscr = curses.initscr() curses.noecho() curses.cbreak() curses.curs_set(0) #Create the State Machine instance s = StateMachine() win = curses.newwin(20, 40, 2, 2) win.addstr(1, 7, "CODE:\n") win.addstr(18, 7, "Press q or CTRL-c to quit.") win.border() win.refresh() #This while loop keeps checking the input you type on the keyboard while True: #Get a single keypress and turn it into a string win.border() c = chr(win.getch()) win.addstr(13, 7, "Correct PIN: "+" ".join(s.correct_code)) #if you press q, terminate the program if c == 'q': break if c.isalnum(): win.addstr(10, 7, "Prev PIN: "+" ".join(s.cur_code)) old_state = s.state s.do_event(StateMachine.E_KEYPRESS, c) new_state = s.state #Write out some debug data win.addstr(15, 7, "OLD STATE: %s "%s.STATE_NAMES[old_state]) win.addstr(16, 7, "NEW STATE: %s "%s.STATE_NAMES[new_state]) win.addstr(18, 7, "Press q or CTRL-c to quit.") if s.state == StateMachine.IDLE: win.erase() win.addstr(1, 7, "CODE: ") win.addstr(2, 7, '* '*len(s.cur_code)) if s.state == StateMachine.CODEOK: win.addstr(1, 7, "SUCCESS") elif s.state == StateMachine.CODEBAD: win.addstr(1, 7, "NO! ") win.addstr(11, 7, "Curr PIN: "+" ".join(s.cur_code)) #Curses only draws changes to the screen when you ask nicely. win.refresh() cleanup_curses()
class ComboLock(object): """This function is the constructor of the class. It is called when you create an instance of this class like at the bottom of the file: combo = ComboLock(). The self variable is the new instance of the class. If you do not understand classes and instances, make sure you read up on it and understand this code before you continue.""" def __init__(self): #Create the State Machine instance self._sm = StateMachine() #The following two lines simply create the attributes for the instance #and assign them blank values so you can read them later and not get #an error even if you havn't set a real value to them. This is good #practice for understanding how your class is structured, and avoiding #errors when returning None means more. None is a special value for #variables that marks a variable as empty. self.stdscr = None self.win = None """function handler for when you press CTRL+C""" def signal_handler(self, signal, frame): #Make sure we clean up curses if the user hits CTRL+C self.cleanup_curses() sys.exit(0) """All the following functions were in iter1, but now that they are more sanely organized, I will provide a brief description of what they do. in the following function definition.""" def init_curses(self): #This makes it so if you hit CTRL+C curses doesn't eat the terminal alive! signal.signal(signal.SIGINT, self.signal_handler) #Setup curses #This next line lets us store stdscr on the object for use later. #It turns out that we don't really use this variable, but the docs #say we do need to create it, so storing it just in case. # #This following 4 lines sets up our curses environment. #http://docs.python.org/2/howto/curses.html#starting-and-ending-a-curses-application #describes how to do this, # #This function, initscr, is documented here: #http://docs.python.org/2/library/curses.html#curses.initscr #Notice it returns a 'window' represengint everything in the terminal. #We create a window later that we use, so we don't use this window for much. self.stdscr = curses.initscr() #The following functions can be found on the same page as the docs for initscr #http://docs.python.org/2/library/curses.html#functions #noecho makes it so pressing a key doesn't cause it to immediate appear #on the screen. This is useful because we will be drawing *s instead. #The earlier link about curses how to describes practically what these #lines do. the function listing describes them in more technical detail. curses.noecho() curses.cbreak() #This disables the cursor from blinking. curses.curs_set(0) #Curses makes it easier to make a console program consisting of non- #overlapping 'windows' which are just rectangular regions in the #terminal. This function tells curses to create a new window region. #It returns an object representing that window. #http://docs.python.org/2/library/curses.html#curses.newwin # #If you look at the docs, this function takes 4 things. # nlines, ncols, begin_y, begin_x #The docs describe what these variables mean. In this case they are the #size and position of the new window in characters. #The window object returned has functions for drawing text to the window. #We store this new window object to self so we can draw to it later. # #It is doable to return this window with the return keyword instead of #setting it to a member variable of self. But since the window is long #term and relevant to the whole object, it makes more sense this way. self.win = curses.newwin(20, 40, 2, 2) """If you do not clean up curses before the program ends The terminal will act super weird and be hard to use. NEW COMMENT: This function doesn't really benefit from from being put in a class because it doesn't use self, but its functionality belongs here so here it is.""" def cleanup_curses(self): #Look at init_curses for links to documentation about these functions. #These calls just turn off curses. curses.nocbreak(); curses.echo() curses.endwin() def _display_UI(self): #Window objects are how curses draws things to the screen. This page #documents the available functions: #http://docs.python.org/2/library/curses.html#window-objects #We set the curses window we want to use to self.win in the #init_curses function. to use the methods on the window, we read it #in the same way: self.win.[whatever function]. Check the previous #link for what each function does. """We can just erase and redraw this entire window because why not? It also means we don't write over parts of our previous results which is why in the last version draw commands had extra spaces. With all normal drawings happening here, we can be sure we don't forget to draw something when a certain code path runs.""" self.win.erase() self.win.border() if self._sm.state == StateMachine.CODEOK: self.win.addstr(1, 7, "SUCCESS") elif self._sm.state == StateMachine.CODEBAD: self.win.addstr(1, 7, "NO!") else: self.win.addstr(1, 7, "CODE:") #win.addstr is the curses version of print. But, you get to say where #in the window to put the text. self.win.addstr(2, 7, '* '*len(self._sm.cur_code)) self.win.addstr(11, 7, "Curr PIN: "+" ".join(self._sm.cur_code)) self.win.addstr(13, 7, "Correct PIN: "+" ".join(self._sm.correct_code)) #You can access the STATE_NAMES variable from the class because it #is not tied to instances. Look up static variables if it doesn't #make sense. self.win.addstr(16, 7, "NEW STATE: %s"%StateMachine.STATE_NAMES[self._sm.state]) self.win.addstr(18, 7, "Press q or CTRL-c to quit.") #Curses doesn't draw to the screen as soon as you call addstr. #It gets a list of changes together and puts them on the screen #when you call refresh on the window. This allows your interface #to never look like it it being drawn because it puts it up once #you have everything ready in the background. self.win.refresh() def run(self): self.init_curses() #This next line initializes the display so you see something #before pressing the first code. The proceeding calls to this #function happen in the while loop after each read. self._display_UI() #This while loop keeps checking the input you type on the keyboard while True: #Get a single keypress and turn it into a string c = chr(self.win.getch()) #if you press q, terminate the program. if c == 'q': break #This function call is built into python and returns true if the string #contains letters and/or numbers. If you want to allow other characters #like the home key, etc, you will need to edit or add to this if #to accept that type of key too. if c.isalnum(): old_state = self._sm.state self._sm.do_event(StateMachine.E_KEYPRESS, c) #It is a good idea to limit the number of places certain things #happen. Idealy all drawing to the screen will be done in the #display_ui function so looking only at that function we can see #everything that will be drawn. The following print of OLD STATE #is an example of when trying to do that can be hard. self._display_UI() #This following addstr prints off the old state of the state #machine. The old_state variable stores the needed state before #the do_event function is called which causes that state to #change. This is only debug data so I am not making it super fancy. # #Note that if you wanted to add printing this code to the #display_UI function, you would have to pass it to the function #in one of several ways: # 1) Pass it to the function like # self._display_UI(old_code, old_state) # 2) Store it as a variable in self. # self.old_code = self._sm.cur_code # and then read it in the display function from self. # 3) Make the state machine store the old state so when there would # be a state variable and old_state variable, etc. # self.__sm.old_state # #It is important to think about haw data can be passed around, stored #and accessed. If you don't, then there will be lots of frustration #about not being able to get your data and you may never want to use #classes for anything. # #If this access management seems pointlessly complex, notice that #without this comment or the following debug code, this whole function #is very simple and easy to follow with no unrelated code mixed together. self.win.addstr(15, 7, "OLD STATE: %s"%StateMachine.STATE_NAMES[old_state]) #Curses only draws changes to the screen when you ask nicely. #We have to call this to draw everything since the _display_UI call self.win.refresh() self.cleanup_curses() """This function is the same as the run function above but without the weird printing. This code is not normally run in this program, but is here to show you how sinple this function truly is now that we pulled the functionality out and split it out over the class. Looking at this function, you don't get immediate access to how the whole thing works due to the code being split up, but you do get a powerful overview and can look into the functions like display_ui to see how it works. This design is much cleaner and clearer than the last iteration.""" def run_no_oldstate_messages(self): self.init_curses() #This next line initializes the display so you see something #before pressing the first code. The proceeding calls to this #function happen in the while loop after each read. self._display_UI() #Keep getting user input until the user asks to quit. while True: #Get a single keypress and turn it into a string c = chr(self.win.getch()) #if you press q, terminate the program. if c == 'q': break #Check if the string contains letters and/or numbers. Ignore otherwise if c.isalnum(): self._sm.do_event(StateMachine.E_KEYPRESS, c) self._display_UI() self.cleanup_curses()