App Overview and State0:00
Let's build a stopwatch app. Here's how we envision it working. You press spacebar to start the stopwatch, and then each time you press spacebar after that it records the lap. Simple enough. We can press R to restart the stopwatch and clear the laps. And that's our stopwatch app. Let's build it. First, we need to track the elapsed time, so let's set up some state for that. public int elapsedTime, and we'll set it to zero to start.
First, we need to track the elapsed time, so let's set up some state for that. public int elapsedTime, and we'll set it to zero to start. We're also going to be tracking an array of laps, so we'll do array laps = []. We also want to know whether this stopwatch has started or not. By default, it hasn't started yet, so we'll have some state. public bool started = false. We also want to set up a keypress listener, and we're going to set that as a property because we're going to share that across a couple of methods. So we'll say protected keypressListener, and we're importing this from the Chewy package.
Keypress Listener Setup0:59
because we're going to share that across a couple of methods. So we'll say protected keypressListener, and we're importing this from the Chewy package listener. So first things first, let's set up our listener property. This listener equals keypressListener for this. Next, what are we listening for? So we're going to say this listener. We want to listen for quit. We always want to listen for our quit signals in our apps. And then what did we say?
We always want to listen for our quit signals in our apps. And then what did we say? We said on space, we want to start the stopwatch. Okay, let's do it. On key from prompts space, we're going to provide a callback, and we'll say this.started equals true. We want to clear the existing listeners because these listeners won't apply anymore after we press the spacebar for the first time. So this listener, clear existing. So we'll clear the existing listeners, and then we'll say this.start.
So this listener, clear existing. So we'll clear the existing listeners, and then we'll say this->start. And then we want to listen. So we're missing this start method. Let's fill it in. public function start, and it's not going to return anything. And what do we want to do here? Well, we've cleared our keypress listeners, so we should probably listen again for new keypress listeners based on the fact that the stopwatch has now started. So we'll say this->listener.
keypress listeners based on the fact that the stopwatch has now started. So we'll say this listener. We still want to listen for quit. And then we want to say on every space that is pressed, we want to add a lap to the array. On key space, we're going to add to the array the current time. And in the renderer, we'll figure out how long the lap is by doing some light math. We also want to give a way to reset our stopwatch and the existing laps. So we'll say on r, we'll set the laps back to an empty array. And we'll say this elapsedTime is now zero again. So how do we actually run our stopwatch?
Run Loop and Timing3:15
And we'll say this elapsed time is now zero again. So how do we actually run our stopwatch? Well, we do that in a while loop. while true, we're going to sleep for one millisecond, so usleep(1000). We then want to increase the elapsed time by one. We're then going to render the app. And then we're going to this.listenerOnce. So here's a question. What's the difference between this.listenerOnce and this.listenerListen? Well, when we use this.listenerListen, when we just call the listen method on the key
What's the difference between this.listenerOnce and this.listenerListen? Well, when we use this.listenerListen, when we just call the listen method on the key press listener, we're telling prompts, don't do anything until we press a key. Wait to re-render the app until we press a key that we are listening for. The once method, on the other hand, is used when we are actively rendering the app on our own outside of key press listeners. So we are actively rendering after sleeping for one microsecond. So in every loop of this while loop, we are going to listen once. We're going to say, did the user in the last iteration of this loop press any of our registered key press listeners?
Renderer and Time Formatting4:27
We're going to say, did the User in the last iteration of this loop press any of our registered key press listeners? If so, update the state and re-render the app. And that's it. That's our whole application and application state. This is everything involved in making this app work. Now let's switch to our renderer and work on the view. So here's our current renderer. Nothing's happening just yet. The first thing I always like to do is set up the width and the height.
Nothing's happening just yet. The first thing I always like to do is set up the width and the height. So let's do that. We usually use these variables, so it's good to set them up in the beginning. We'll set it as a buffer of 2. And we'll set up the height variable to prompt, terminal, lines. And this we're going to set a buffer of 7 to give us a little bit of room at the bottom of the screen. Okay, I want to set up a method here that I know we're going to use quite a lot. We're going to be dealing with formatting time.
Okay, I want to set up a method here that I know we're going to use quite a lot. We're going to be dealing with formatting time. So I want to create two methods to do this. The first one is going to be called timeSegments, protected function timeSegments. And this is going to accept the milliseconds and it's going to return an array. And this array will contain the formatted minutes, seconds, and milliseconds padded out with zeros. So let's calculate the minutes, seconds, and milliseconds based on the milliseconds passed in. Our minutes will be equal to floor($milliseconds divided by 60000.
in. Our minutes will be equal to int floor milliseconds divided by 60,000. Our seconds will be equal to int floor milliseconds minus minutes times 60,000 divided by 1,000. Let's wrap this in parentheses. Our milliseconds are going to be equal to the milliseconds minus the minutes times 60,000 minus the seconds times 1,000. So now we need to return an array of these segments. So return mb_str_pad minutes, pad it to 2, pad it with zeros, and we're going to string_pad_left this number. Okay?
pad left this number. Okay? So we can copy this down and we're going to say seconds and milliseconds. And milliseconds we're going to pad out to 3 because we're going to have three places for the milliseconds. Our next method is actually going to format the time as a string based on the time seconds. So protected function timeFormatted(), and it's also going to accept milliseconds, and it's going to return a string. So we will get the minutes, seconds, and milliseconds equal to this timeSegments based on the milliseconds. And we're going to return minutes, seconds, milliseconds.
So we will get the minutes, seconds, and milliseconds equal to this time segments based on the milliseconds. And we're going to return minutes, seconds, milliseconds. Great. So these are two methods that we're going to be using a lot, and I wanted to set them up ahead of time so that we were ready to use them when the time came. So first things first, let's make sure that our app is running as expected. All we're going to do is we're going to this line, this timeFormatted, and we're going to pass in the promptElapsedTime. Okay, phpLab stopwatch. Nothing's happening.
Big Number Display8:22
Okay, phpLab stopwatch. Nothing's happening. So far, so good. But if I press space, perfect. You see the time increasing and you see it formatted correctly based on the methods that we have set up. Now that's fine, but it's kind of small. I want to make the actual stopwatch much bigger. Thankfully, Chewy has some built-in functionality to write big numbers to the screen. So let's import that trait.
Thankfully, Chewy has some built-in functionality to write big numbers to the screen. So let's import that trait. Use drawsBigNumbers. And then we are going to get our time segments so we can draw them out individually to the screen. So minutes, seconds, milliseconds is equal to this time segment's promptElapsedTime. Now we're going to draw out each segment as big numbers. bigMinutes equals this bigNumberMinutes. bigSeconds equals this bigNumberSeconds. Great.
Big seconds equals this big number seconds. Great. Let's just take a peek into bigNumber and see what it's doing here. So it's grabbing this string and it's chunking it into its individual numbers. And then based on the number passed in, it's simply grabbing the appropriate number from the string. Now one thing that the bigNumber method doesn't have is a colon. It doesn't know what to do with a colon. So we're going to put that in manually. So we have a colon and it stylistically matches the other big numbers that are going to be
So we're going to put that in manually. So we have a colon and it stylistically matches the other big numbers that are going to be printed out. But we need to manually handle that colon ourselves. So bigColon is going to be equal to collect(explode(php, endOfLine, colon)). And then we're going to map each of these lines, we're going to say line. And what we want to do is we want to make sure it's all the same width so that when we lay these out side by side, it lays out correctly. And right now, this doesn't have any width, but this is one and one. So we want to make sure that every line is at least one wide.
And right now, this doesn't have any width, but this is one and one. So we want to make sure that every line is at least one wide. mb_str_pad line one, and use spaces to get that to the desired width. Great. Let's print this out and see what we have here. So we say dd $bigMinutes, $bigSeconds, and $bigColon. Let's see what happens. Okay, great. So we have zero, zero, zero, zero, and we have our colon. So we have three different arrays.
So we have zero, zero, zero, zero, and we have our colon. So we have three different arrays. And as usual, we've done this before, we want to zip these together so that they lay out side by side. So we can use the zip method from the Collection class to do that. The lines are equal to collect($bigMinutes). We're going to zip the $bigColon, and then the $bigSeconds. And then we are going to map the lines, and we are going to implode them with an empty string. Okay, let's see if that works.
string. Okay, let's see if that works. Lines, let's dump it out. Excellent. So now we have three lines, and it's printing out each line, one after the other, and it creates one ASCII art picture. Perfect. Okay, so this is good, but what about our milliseconds? Well, I think we should put our milliseconds in the middle right here. So right next to this middle line, let's tack it onto here.
Well, I think we should put our milliseconds in the middle right here. So right next to this middle line, let's tack it onto here. Now visually, this is the center, and we're going to eventually center this on the screen. So if we tack something onto this side, we should also pad it on the left-hand side the same amount so that it's still visually centered on the screen when we end up centering it. So let's handle that. So here we'll continue mapping, and we'll say map, function line, index. And so the middle one is going to be index 1. So if it's index 1, we're going to have the line, a space, and then we'll tack on the milliseconds.
So if it's index one, we're going to have the line, a space, and then we'll tack on the milliseconds. Otherwise, we're going to do the line, and we'll str_repeat a space four times. So where is this coming from, this four str_repeat? So we have the milliseconds, which are three characters wide. Remember, we are padding this out to three characters every time, and then we have a space. So three plus one is four. So for every line that's not the middle line, we still want to repeat four spaces at the end of it so that all the lines are even.
So for every line that's not the middle line, we still want to repeat four spaces at the end of it so that all the lines are even. Next we want to put that same spacing on the left-hand side as well. So we'll say map, function, line, and we'll just do str_repeat(' ', 4, and then line). So now that'll be even on both sides. Let's go ahead and center that on the screen and see how it looks. So Chewy has an align straight that will help us center this. So we'll use aligns, and we want to center it on the screen, horizontally and vertically. So this center lines will provide our width and our height, and then we're going to get
So we'll use aligns, and we want to center it on the screen, horizontally and vertically. So this center lines will provide our width and our height, and then we're going to get a collection back of the center lines, and we can say each this line and print it out. And there we are. There's our stopwatch in the center of the screen. If I press space, off it goes. This is looking great. This is looking really good. So we've got our seconds and minutes very big, and the milliseconds sort of just whizzing by.
Lap Table Rendering14:03
So we've got our seconds and minutes very big, and the milliseconds sort of just whizzing by. Okay, now let's figure out our laps. So every time we press the space bar after the stopwatch has started, we are going to be adding a lap to the timer. So let's think about how we want that laid out. I have an idea here. So if we look at this, we have a lap title, we have a total title, and then we have the lap counter on the left-hand side, and then we have the timings underneath these two titles. So let's keep this up and see if we can figure out how to space this all out.
lap counter on the left-hand side, and then we have the timings underneath these two titles. So let's keep this up and see if we can figure out how to space this all out. So if there are no laps, we don't want to show anything. But if there are laps, we at least want to start showing the heading. So let's do that. If $countPromptLaps is greater than zero, we're going to push a blank space. So we're going to say I want one line between the timer and this heading. And then we need to know the width of this time. So let's grab this as a sort of placeholder, and we'll say $timeLength is equal to strlen($this->string).
So let's grab this as a sort of placeholder, and we'll say $timeLength is equal to strlen($thisString). So we need our $lapTitle to be the same length as this $timeLength, right, so that it all lines up. So we'll say $lapTitle equals mb_str_pad($lapTimeLength, ... , spaces). Okay? We can do the same thing with our $totalTitle because we're going to need the same width for total. We want it to pad out to the same $timeLength so that everything lines up correctly. Now something we want to consider is that our $lapTitle has to also take into account...
We want it to pad out to the same time length so that everything lines up correctly. Now something we want to consider is that our lap title has to also take into account one, two, three spaces here so that the columns line up and there's a little spacing between. So we're going to add three to that time length for lap. Okay, let's consider our lap counter. So everything is lining up here along this, and the lap counter's going to be on the outside. So we have one, two, three, four, five. So that's five for the spacing. So our $leftPad is going to be equal to str_repeat(' ', 5). Okay, so let's go ahead and build this out.
So our leftPad is going to be equal to string_repeat(space, 5). Okay, so let's go ahead and build this out. lines, push, and we're going to do leftPad, this bold, this cyanLapTitle, and then this bold, this greenTotalTitle. Okay? That's the beginning of our header. Now let's put a little divider line between the header and the laps. To do that, we're going to push another line. This line's push, leftPad, and we'll say this dim string_repeat, this littleDivider, and we'll say string_length of the lapTitle and the totalTitle concatenated.
This line's push, leftPad, and we'll say dim stringRepeat, this little divider, and we'll say stringLength, the lapTitle, and the totalTitle concatenated. So we want it to be the same length as this all the way to here. Okay, let's see what this looks like. Let's see if it makes any difference yet. So here's our timer. If we press the spaceBar, it starts the timer. And if we press the spaceBar again, we have our titles. And I just noticed that I said lab, not lap. Let's fix it.
And I just noticed that I said lab, not lap. Let's fix it. But it looks good. It does look good. So I like that. Now let's actually go ahead and loop through our laps and render them. So you can say foreach $prompt->laps as $index $lap, we need to measure the previous $lap, which is going to be equal to $prompt->laps[$index - 1], so the last lap before this one, or zero. lines push(this, renderLap, $index, $lap, $previous)
one, or zero. Lines push this, renderLap, this is a method we haven't quite written yet, indexLap, previousLap. Let's go ahead and handle this renderLap method. protected function renderLap. So we're accepting an int index, int totalTime, and an int previousLap. We're going to be returning a string from this method. So the lap is equal to our totalTime minus the previousLap. So that's the current lap's time. The lapNumber is going to be equal to mb_str_pad(index + 1, 2);
So that's the current lap's time. The lap number is going to be equal to mb_str_pad index plus one, pad it out to two, we'll pad it with zeros, we'll say str_pad left. So we're saying instead of zero, one, two, three, we're doing O1, O2, O3, O4. We're going to return a Collection, we're going to say $this $lapNumber, we're going to say $this $cyan, $timeFormatted, this is where this method's coming into play, lap. And then we can say $this $green, $timeFormatted, $totalTime. And then we are going to implode str_repeat three spaces between, just like we planned out up here. So one, two, three, one, two, three.
This is just a simple explanation with no code.
Let's record a lap. That looks pretty good. Record another lap. Okay, this looks great. This looks really great. We have our total time, we can see it's increasing, and then we're doing lap by lap the exact time that that lap took. Perfect. It's important to tell the user which hotkeys are available to them as they use the app. So let's draw some hotkeys.
Hotkeys Footer Display20:00
It's important to tell the user which hotkeys are available to them as they use the app. So let's draw some hotkeys. Let's first import useDrawsHotkeys, and then below our centered content, let's pin something to the bottom. Let's say pin to bottom, and this comes from the Chewy aligns class. Based on the height, and then anything inside this callback is going to be pinned to the bottom no matter how high the content is. So we'll say this new line, let's pad it by one new line. And then if the prompt has started, well, what keys are available to us? We have this hotkey, space, and that records a lap.
And then if the prompt has started, well, what keys are available to us? We have this hotkey, space, and that records a lap. So when we press space, that records a lap. We say this hotkey, R, is for resetting. So we're going to reset when we press R. Let's pull in the prompt variable so we can use it. Otherwise, if we haven't started yet, we can say this space actually will start the stopwatch. Okay, and we always have this hotkey, Q, for quit. Let's center this horizontally. We'll say these hotkeys, so print them out in their formatted state. The width that we specified, let's import that here.
We'll say this hotkeys, so print them out in their formatted state. The width that we specified, let's import that here. And then we can go ahead and print those lines. And then let's pad it on the bottom by a new line. Okay, so we have a new line. We're setting up the available hotkeys based on the state, and then we're printing those hotkeys centered horizontally. Let's see what that looks like. Okay, so we have space, start, Q, quit. That looks good.
Alt Screen Rendering21:44
Okay, so we have space, start, Q, quit. That looks good. If we press space, we can see the timer has started, and now we have space, lap, R, reset, and Q, quit. And no matter how many laps we add, this will stay pinned to the bottom. No problem. Okay, great. Let's do one final thing, which is to render the app in an alt screen. I like to do that. It's a more immersive experience.
I like to do that. It's a more immersive experience. So let's pull that in. Qs creates an alt screen, and then here we say this creates alt screen. Fantastic. So now we have this nice full screen experience. We press space, we start our timer, and there it is. Our laps are coming in, and it looks perfect. I love it.
Our laps are coming in, and it looks perfect. I love it. This is our custom terminal stopwatch, complete with lap timing and everything. Great job.
