Terminal UIs
Joe demonstrates building a text based user interface in the terminal using Laravel Prompts, resulting in an interactive speaker directory app in under 30 minutes.
Transcript
00:00:00 what's up everybody how's it going all right I know big day we're having a long day but we can do it we can do it so what's up I'm Joe tanam Mom I'm super excited to be here um just a little bit about me before we get started um wh clicker clicker there it is okay so I'm a full stack engineer at laravel uh I live in New York City I've lived there for almost 20 years at this point um I was a freelance developer for a really long time and I wanted to meet other developers so I started uh PHP NYC and we've only had a couple meetups uh
00:00:35 we took the summer off but we're going to get kicking off in the in the fall again super excited it's a great crew so if you're ever in New York City you want to come to meet up let me know uh and everywhere online I am Joe Tannon bomb so hit me up I'd love to hear from you um if you know anything about me at all you know me as this guy on Twitter one more time Clicker one more time one more second there it is that was me on Twitter for a very long time um and that's what we're going to talk about today we are going to talk
00:01:10 about text based user interfaces or tuis and more specifically we're going to talk about building tues with laral promps who here knows about laral promps probably a lot of you at this point yeah yeah it is dope it was actually debuted at last slon us it was built by the amazing Jess Archer please give her a round of appla Applause it was amazing uh yeah yeah so smart um and it basically provides uh it's a first party a first party larl package that provides beautiful uis for your CLI apps okay so
00:01:46 more importantly than that we are going to have some fun today cuz uh this is not how we traditionally build apps we're used to like building out rest apis we're used to uh uh rendering views to the browser and this is going to work work our brain just a little bit differently so we're going to have some fun let's jump in the sandbox with me we're going to play we're going to see what comes out the other side okay how did we get here this all started because I made a PR to add a table renderer to
00:02:16 the prompts Library so I wanted um a table that matched The Styling of the existing promps components okay so this forced me to like dive into the guts of prompts and see how it worked and I liked what I saw it was very cool and I thought well there's a lot of potential here why don't we take this static table renderer and we'll make it an interactive data table in the terminal right so you can sort of scroll through it it's paginated you could search it you could jump to different pages and you could ultimately select a value back
00:02:47 and I was like that's cool can we push it further right so I did a couple more experiments couple more experiments we got to a retro iPod in the terminal that actually uh fed from my Spotify SP ify and when I picked a song it actually played in my Spotify so now we're sort of like in weird bille we're out in the world like it's like what are we building anymore what are we doing uh so I was like okay let's do a couple more experiments couple more experiments and we got to uh two player pong in the
00:03:15 terminal you could fire up your terminal your friend wherever they were could fire up their terminal and you could play Pong against each other in real time in the terminal so now we're getting really weird um and that's what we're going to build today we're going to build a tuille here on stage and we're not going to do anything as complicated as that because we do not have time to do that but I do want to give you the confidence to jump in and try this out for yourself if you're interested in this at all so it's
00:03:41 probably also worth noting this is not the intended use case for prompts like prompts is not meant to do this uh it's just cool that it can so we're just kind of hijacking the render method of prompts and building out these applications so a couple fundamentals about chees um tues are exactly what they say they are 10 they are text based user interfaces meaning you are concatenating strings together to create an interface right so like what are the consequences of that that means really that you get nothing for
00:04:16 free you get nothing for free and that's really important to like internalize here because like if there's a pixel on the screen if there's an interaction that you want to in deal with you have to deal with that okay and we're kind of used to to getting a lot of stuff for free like we we render reviews to the browser and so for example if you wanted to collect input from a user you would do this input type text we do this like a 100 times a day and if you had nothing else if you did nothing else to it the
00:04:44 browser would give you this and that looks like something you can type into that's just for free they've styled it for you they've made it look correct it's there for free for you you get Focus Behavior that's pretty cool for free you get sensible cursor Behavior so as you type the cursor moves and then can move the cursor amongst the characters that you've typed for free uh and you get sensible uh blur Behavior so the browser just like hey this is all for you for free you're welcome uh in the terminal you get none of that the
00:05:14 only reason why this looks feels and behaves like anything that we are familiar with is because it was crafted that way these are just strings being rendered over and over that's not a real cursor it's just an inverted string and we're we're rendering it based on the position that we have tracked as you press keys so really nothing for free okay uh one last thing before we get going um you're going to see chewy mentioned a lot chewy is a little package that I put together as glue between uh prompts and
00:05:45 these apps I built a lot of these experiments and there were a lot of patterns and helpers that emerged and so they sort of all got bundled into chewy so when I say chewy which sounds a lot like Chey but when I say chewy I'm talking about this package uh and finally if we get to the end of this talk and you're like man 30 minutes wasn't enough I want three more hours of this I have a lar cast course and you can do exactly that so if you want three and a half hours of CLI app content larc cast okay let's
00:06:16 code okay we're done here let's talk about the structure of a prompts application so this is our application State class uh it extends the base prompts class and it has one required method that's the Value method okay and the value is normally what's returned back from the prompts component but we're actually not going to use that so we're just going to return null um next up we have the renderer so the renderer extends the base prompt render from from prompts uh it's an invocable class it receives an instance of the
00:06:51 application State class and it returns a string representation of the UI based on that application state so UI as a function of State reacti sort of stuff here um finally we just have a script Runner this is basically justene up our application State class firing the prompt method and that's it we are never going to look at this file again take a good look it's gone okay cool let's let's build something the first thing we need to do is we need to tell prompts I'm going to use this demo renderer when
00:07:24 I'm using this application State class so chewy can help us out there uh and we have a registers renderer trait that allows you to register a renderer and we're going to just say when we knew this up and run it please register our demo renderer as the renderer for our app cool let's um let's run this and see just what happens oh uh nothing and that's what's supposed to happen we haven't done anything yet but there's actually a lot going on here the cursor's missing there's no more cursor it's gone if I
00:07:56 type nothing happens the the output is suppressed so promps is really like setting up the world for our application so promps is doing a little bit of work on our behalf to say I know you're you're firing up a a terminal app I'm going to just like set up the world for you so let's actually do something now uh we're going to make two mini apps and then we're going to make a real app uh the first mini app is like a simple counter app just to just to get us started so we're going to have a public counter
00:08:21 prop and I need to spell that correctly okay and then we are going to interrupt the world setting up process so you can see prompts renders so you can understand and internalize how that works so we are basically just going to interrupt it with a an infinite Loop all right so we're going to render we're going to sleep for one second we're going to increase the counter okay and we're going to just do that forever uh in our renderer we can just say uh print that to a line so we're going to just print out the current value of the
00:08:52 counter as a line and uh let's see what that gets us y looks good it's counting that that feels about right um but what's interesting here is that it's overwriting itself it's not printing out line by line it's overwriting itself and counting in place so what's What's Happening Here uh what's happening here is that the way prompts renders is it takes the cursor and it moves it back to the beginning of the last output that you had it erases down and it writes your new content okay so it takes the cursor moves it back to
00:09:26 the beginning erases down writes the new content cool okay so let's actually make this interactive because like most times we're not going to do something on autopilot so instead of doing uh this while loop uh we're going to change nothing about the render but we'll just change it to um a key press listener and we'll just say on Space we are going to uh increase the count okay so basically this keypress listener comes from chewy it's just a wrap around some existing prompts functionality and it makes it
00:09:53 like a little more of a fluent thing so we're saying keep press listener for this application State class on the space key in inrease the counter start listening uh we are not going to be interrupting the process so prompts will do the world setup so let's see what that looks like we got zero nothing's happening prompts isn't even like rendering it's just waiting for something to happen and we press space we have an interactive app boop boop boop boop boop boop okay cool we just made our first teeny tiny
00:10:21 little interactive app that's cool let's raise the stakes a little bit let's just say whatever we type print that out to the screen okay so we're going to do a little message prop here instead and we're basically to say any key that we press just to pen that to that message so we're going to do a little wild card here so uh basically right any key we press press the uh pen that to the message and then we're going to change this to print out that message cool let's do this again we have a blank screen and I say hello but I put
00:10:55 too many exclamation points so let me just back that up oh the delete key doesn't work because we get nothing for free we have to implement all of this stuff so let's Implement that super easy to do no problem uh let's go back here and we'll just say on delete and we'll give them a way to clear it on enter so we'll say on enter so basically on delete on the backspace key we are going to set the message equal to a substring of the message minus the last character and on enter we're just going to set it to a
00:11:24 blank string we've changed nothing about the renderer we're just changing the key press listeners here let's see what happens okay hello and then we can backspace and we can press enter and now so we're dealing with a little bit more of a complex app so we're we're kind of notching up here and just to just to show you like you are in full control over exactly like how these key presses are interpreted and you can do anything you want with them um we're just going to do this and we're going to say every
00:11:50 time we press a key we're going to append it three times and we'll take the last the last three off every time for backspace so now it's okay so you you have full control over the situation you can decide how to handle all of these interactions allart which is cool um so I'm going to now show you a situation that if you are building these you will run into literally all the time we're going to make this window a bit smaller we're going to run our app and we're going to type a lot okay let's see what
00:12:22 happens oh oh okay um that doesn't really look right anymore that looks a little wild so what's happen Happening Here well the terminal is our buddy right the terminal is our friend and they're like hey pal looks like you typed a lot let me just wrap that down to the next line for you and we love the terminal for that big fans but prompts know doesn't know anything about that new line it thinks that we've still typed on one line so it moves the cursor to the wrong place it erases down and it starts rendering so the moral of the
00:12:55 story here is that we need to constrain our output to the width and the height available in the terminal okay constrain it to the width constrain it to the height and don't print Beyond those bounds that's like a really important lesson when building these things out so what does that mean let's go back to our renderer and the first thing we need to do is actually grab the width and the height of the terminal so let's get a width prop going and a height prop going and we'll get the width and the height
00:13:24 Okay cool so the good news is that the application State class has a terminal helper that reports back the number of columns and lines that are available in the terminal I buffer this a little bit sometimes when you go all the way to the edge the terminal is like I got you next line and I'm like no no no no no chill so uh we just do a little bit of buffer so we're not going all the way to the edges there uh so we've got our width and our height uh what do we need to do next how let's handle the width so we
00:13:50 want to constrain uh we want a word wrap basically our words to the width of the terminal and PHP has word wrap built in let's just use that so we're going to set the message equal to the word wrapped message and we're going to strain it to the width and we're going to cut long words so we're going to say if there's a very very long word don't drop it to the next line cut it and keep going uh so that's the width we've already handled the width portion of this what about the height the portion let's talk about slice slice is a good
00:14:19 one that's how I like to handle it so we're going to collect the lines we're going to slice off anything that's beyond the height of the terminal and we're going to print out each of those lines and if you're not familiar with this little like dot dot dot this is the first class callable syntax and it's uh great you should use it all the time I do uh let's see if this works fingers crossed let's type a lot uh yep okay good so now we're in control of the situation you want to control every character that's printed
00:14:49 out to the terminal that's important uh when you if if you decide to build these things out uh you're going to run into this all the time and it'll start getting jumbled and you've just probably mismeasured a string or forgot about some spacing or something like that so uh just wanted to highlight that as a lesson now let's build a real app uh cool so we are getting to the end of the conference and I thought what's a really useful app to do at this point and I was thinking probably a speaker directory
00:15:18 for the conference which is very useful at the end for some reason um okay so I already have this data loaded up so let me just bring this in so use has speakers and we will say oh we need a speakers uh speaker prop and that's a collection of speakers uh and then we're going to just load the speakers so this uh load speakers cool so all this is doing is I have a bunch of speaker data in a Json file there's the name the title the Twitter URL and their like bio so that's what we're going to be loading up the
00:15:56 app that we want to make is a list of speakers on the leftand side and details about the selected speaker on the right hand side and you can just press the up and down arrows and you can uh navigate to different speakers and as you change them the details on the right hand side should change okay pretty basic not super exciting but it's like interesting to watch it being built so first things first let's just actually um print out the speaker names so we have a speaker bar here that we're going
00:16:22 to set up and we'll have speaker lines and all we're going to do is we're going to print out I'm sorry we're going to pluck the name out of the collection and we're gonna uh get that as its own collection and then we're going to just print that out to the terminal uh cool let's do it yeah that's the speakers we did it okay cool um let's handle the key presses so it sounds like we've got two things that we have to track uh we have to track a selected speaker um index and we have to track up and down keys right
00:16:56 so let's do that we have a selected speaker prop and that's just going to be uh the index of the currently selected speaker and then we're going to say on down and on up and let's talk about what's going on here so on down we're going to set the selected speaker equal to the minimum of the selected speaker plus one all the way up to the speaker count so we're going to constrain it and on up we're going to set it to the selected speaker minus one all the way up to zero so we want to just constrain
00:17:20 it and make sure it doesn't go past the bounds of how many speakers that we have if we ran the app right now this would work and we'd have zero indication that it was working we haven't styled anything or indicated to the user who is selected so let's handle that now um so let's install tailwind and let's no uh we get nothing for free so we're not going to do that uh we're just going to map over these names and we're going to say if the selective speaker is equal to this index uh make it BG green
00:17:49 and bold and these BG green and bold methods come uh with the renderer so prompts has helped us out with this a little bit and these are just anti-escape codes that that's how you style text in the terminal okay so yeah this should get us our selected speaker let's see if it works so far so good Taylor selected he's the first one I press down we get Adam Aaron Prime Okay cool so this is this is working but it just looks bad it's like very cramped and it's changing the width of that green with every key
00:18:21 press because everybody's name is different lengths it'd be nice if it was just like uniform all the way down so let's actually do that let's make it uniform all the way down we do that by finding the longest speaker name that's the first thing we have to do so uh we're going to map the speaker names to the MB string width of the name so that's the width of the name on a monospace grid we're going to get the maximum value from that collection and we're going to add two because there's going to be one space of padding on both
00:18:50 sides so we're going to also pad this before we style it so we're going to MB string pad the name out to the longest name but we are going to have a space on either side of the name as well so this should get us a little more of a uniform experience as we uh as we do this let's see already to my eye this looks better because you have a little breathing room around the name and you can see that it stays the same width all the way down and up which is cool so we go all the way down to frake and I think yeah he's
00:19:17 the longest name looks good okay so the original purpose here was as we changed speakers we wanted to see details about them on the right hand side cool let's Implement that part uh let's go detail lines that's not it detail VAR that's what we want and detail lines is down here okay detail lines a little more involved but we can totally do it so we are getting the current Speaker by the index we're word wrapping their bio to the available space left in the terminal so it's in the right hand side so we
00:19:55 basically have to say the full width minus the left column width and you can see here we haven't had a left column width defined so we should probably Define that so left column width what is the left column width well we actually already know that it's the length of the longest name right so where you can just go down here and we can just say this left column width equals longest good that part's done so we're word wrapping it to the width minus the left column width uh that's the bio part we're printing out a couple
00:20:29 more lines here we're doing a bold name we're doing a dim title we're doing a San and underline uh Twitter URL a space and then the rest of the Biol lines and you're probably looking at this and going okay what what yeah sure okay um so let's just dump it out for a second so we can sort of like Orient ourselves around what we're actually dealing with here um great so we have our speakers here we have a list of our speakers and Taylor is styled because he's the first one he's the selected speaker and then
00:21:00 we come down here and we have uh Taylor Taylor's details Taylor creative LEL uh his Twitter URL and his bio and what's funny is I found this bio from an old Arkansas Business Journal from a decade ago so it's like very Antiquated bio but um yeah so if we printed this out line by line right now it would be all the speakers and then all the details but we want to print them side by side so the terminal wants to print things out as full line so what do we need to do here we need to concat the first in uh the
00:21:32 zero index of this array to the zero index of this array then the one index of this array to the one index of this array and so on and so forth until we have full lines that we can print across the screen you get nothing for free you have to do this all yourself but uh the collection class has a method called zip that does exactly this it it it does that concatenation for you so we're going to use that uh but there's a little bit of nuance here because we're printing to the terminal there's anti-escape codes and some other things
00:21:59 so chewy has a little helper that we're going to use um it's called the lines Helper and we're going to say lines from columns and these are our two columns speaker in detail get the resulting lines print out those lines let's erase this we don't need this anymore and now yep yep yep yep yep okay cool so as we navigate we're showing the detail of the currently na uh the currently selected person but again again it doesn't look great it's like really smashed together so let's just put like a little bit of spacing in there um
00:22:35 these are the sorts of things you have to like consider when building these out you have to do every single thing so uh column spacing we're put four spaces in there and the column oh sorry the lines helper has a little helper called spacing and we're just going to say this call spacing uh but if we Ram this right now it would be jumble text it'd be wrong it'd be a mess and why somebody else needs to know about the four spaces we just added to the width does anybody know who needs to know about
00:23:04 that no no oh the word the word wrap somebody said word wrap is that what I heard okay the bio needs to know about this so we actually need to take this off of their bio available with this call spacing okay and now we have much better this is way better okay so this is starting to look kind of more like an app we're kind of cooking now uh we can do something more to make this look like an app because like we're interacting with the app right now but I can scroll up and see everything else we've done and that feels bad in my
00:23:38 heart I don't like that so uh we're printing right now to the main screen of the terminal so we're just like shooting it out there but there's we can print to an alternate screen we don't have to print to the main screen so let's just do that and it'll make it feel more immersive uh we can pull in use uh creates alternate screen from chewy and we can say this create alt screen and that should get us oh it feels so much better we're just like now in our own little screen printing our app straight to that if I scroll up
00:24:11 there's no where to scroll because we're already there if I scroll down it actually works with the up and down arrows so I can scroll through these names now which is pretty fun instead of using the up and down arrows um I think next step we should probably tell the user what they can do like that the up and down arrows actually do something in this app I tend to try to like let them know about the hot keys so we're going to go back into our renderer we're going to pull in two traits here we're going
00:24:36 to pull in use lines and we're going to pull in uh use draws hotkeys cool and what we're going to do is just tell the user you can do this uh so we're just going to pin this content to the bottom based on the height so no matter how much content is above it keep this content at the bottom we're going to say a new line uh we're going to show them the hotkey you can use up and down arrows to select the speaker and just Center this content horizontally this is like a little touch but it is something
00:25:06 useful for uh for people to see and all it does is this it just says like hey use the up and down arrows to just like the speaker and it makes it look a little more official um what else can we do here well we're not on a server we're on our local system so let's do something local systemy uh on T let's just open their Twitter profile so we can just say exact open their Twitter profile so whoever selected hit that up let's do Twitter and let's do that so you can see we have a new we have a new hotkey open Twitter
00:25:39 profile and if we go down to say like Jess press T if I have internet we do great Twitter profile if we go down to say Daniel great Twitter profile sweet so we can do like local systemy stuff we can even go further than this and like precompose a tweet to this person right so we can say uh on C we'll just say hey whoever this is I'm sending this tweet live on stage uh at line us from my terminal application and we can say send tweets and I think this will work who's not going to be annoyed by this let's
00:26:19 say not Jess we'll go with Luke and we'll say yeah that works post that sweet cool so like you can do these little local systemy things when you're building it uh for yourself um and you can take this actually so much further I've built a more full featured version of this particular app um and we've separated the browsing here from the the uh entering of it so you can you can look for somebody first before like instead of changing with every single Mouse uh every single Arrow um you can change
00:26:53 Focus using the left and right arrows for different sections and then there's contextual hot keys on the bottom depending on what actions you can take what's focused uh if somebody has a long bio you can scroll their bio right there in the terminal which is pretty cool um and you can really go crazy with this here um if you wanted to get organized you could build a con Bon board in the terminal if you wanted to so like I am in the progress of eating pizza uh charm looks dope that's done and uh I'm going
00:27:25 to wait patiently and that's cool um you can actually uh you know if you want to recreate the dashboard of a 1984 Nissan you can do that you can start the engine you can rev it a little bit rev it up Rev it up Rev it up let it cool down and you can turn the engine off uh and if you're fortunate enough to actually uh get the opportunity to speak on stage about two East for 30 minutes uh you could build a photo booth in the terminal and actually uh capture that moment so and I discovered this accidentally but
00:28:06 you know it's really funny is you know those Mac reactions that uh you you always turn off but they turn themselves back on like during Zoom calls and stuff it works here and it's very entertaining wait or you can go um but I actually want to capture the whole moment so if you don't mind um can can can you guys get in the photo is that okay is that cool if we do that okay let's do this yeah yeah yeah great great it doesn't look like anything let me just adjust the contrast a little bit let's see if we
00:28:49 can uh okay looks like a blob that's fine okay everybody smile great and you know what let's let's see if we actually make this happen everybody wave oh yeah that's so cool awesome okay let me just uh make sure that I got all of those here in my photo booth that one we got that yeah perfect that looks like all of you you guys look great uh look at you waving that's awesome anyway that is my time thank you so much and enjoy the rest of LaRon hey well done we love a little bit of Whimsy that was awesome let me ask you
00:29:40 this what um as I've as I've followed you I've seen all of except the last one all of these things come to life what is it about this specific uh domain or style of programming or that that draws you in so deeply um I don't do anything else that's like this it's like you have to get so granular and there's something that like tickles my brain about that that I'm responsible for like everything that's happening and it's just weird problem solving cuz like I feel like a lot of times we just sort of get in the
00:30:08 grind of just like writing the same type of code over and over again a little bit of CR apps a little bit of CR apps but uh this is not B it's like very far from that and so it's fun it's fun time um I have an idea maybe you start selling coffee out of the terminal just something I came up with yeah it's a good idea y'all give it up for Joe thank you so much well done
Highlights
🎉 Joe introduces himself and his NYC developer community.
🖥️ Discusses Laravel Prompts, a tool for creating beautiful CLI UIs.
🎨 Demonstrates building interactive TUIs, emphasizing user control.
🕹️ Shares fun experiments, including a Pong game in the terminal.
🔧 Highlights the challenges of creating TUIs from scratch.
🗣️ Presents a speaker directory app, demonstrating real-world utility.
📸 Ends with a whimsical photo booth feature, engaging the audience.
Key Insights
💡 Community Connection: Joe’s initiative to start PHP NYC shows the importance of developer communities for networking and learning. Connecting with peers can enhance growth and collaboration.
🔍 Laravel Prompts: This first-party package allows developers to create visually appealing and interactive command-line applications, fostering creativity in CLI environments.
🧠 Learning Curve: Building TUIs requires a different mindset. Developers must manually handle UI elements, unlike web applications, which can lead to deeper understanding and skill development.
🎮 Interactivity: The emphasis on making TUIs interactive encourages developers to think outside the box and push the boundaries of traditional programming, making coding more engaging.
📊 Real-World Applications: Demonstrating a speaker directory app illustrates how TUIs can solve practical problems at events, enhancing user experience through simple navigation.
🌟 Whimsy in Programming: Joe’s playful approach, such as the terminal photo booth, highlights the joy of programming and creativity, reminding developers to have fun with their work.
🚀 Future Potential: The possibilities for TUIs are vast, encouraging developers to explore unconventional ideas and innovate within the terminal space, which can lead to groundbreaking applications.