Building Taskwarrior in Golang using Cobra and Charm tools

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
I feel like that's me sometimes when I'm not using my task management tools properly I really like the Simplicity of task Warrior because it allows me to not have to leave my Dev environment or terminal but I can still keep track of what my tasks are that day I was looking at just the options for extending the visual appeal of task Warrior because of course I work for a glamorous command line company and of course I want to glamorize it okay but task Warrior is not built with go which means that I can't use my go libraries that I would love to have beautifully formatted stylized outputs so instead I decided to reinvent the wheel and just copy it in go and now I'm going to show you how you can do that so that you can learn how to build a task management tool a task Warrior clone in golang we're using Cobra and we're using lip gloss for some stylized outputs and the nice thing here is so we are going to have a table View and a kanban view sounds familiar doesn't it but you can also feel free to extend that and demonstrate your tasks in any kind of layout or styles that you want now that we're in the go ecosystem and you've got the power of lip gloss behind you so we've got a bit of a checklist here and it's a little bit of a different style because I'm not going to be coding along with you but I am going to show you the solutions to each of these Milestones so the first goal is that we're just going to get set up with our data storage so we're going to learn how to do a sqlite database how to open the database add information to our DB so that's the basic kind of crud operations that we're going to do with a sqlite database and then the next stage is that we're going to make it interactive with CLI so users are going to be able to have commands and sub commands for working with the tool and we're going to be using Cobra for creating that CLI and it's just going to basically make it all of our crud operations accessible with commands and sub commands and then to add a little chef's kiss to the final product here we are going to print to a table layout and we are going to print to a kanban layout that's a little interactive each of these files is going to have the solutions that you need for each of the steps along the way here now that you've got kind of the skeleton of the project I'd rather you try and solve each of the steps yourself and then come to the video to look at the solution and understand why how and why it works so that's kind of my vision for this style of video but of course just let like let me know what you think because it's kind of the whole point is that you're here hanging out and watching it we're going to open our main.go file and this is where we're doing some of our initial setup for our application that includes getting the path set up for where we're going to be storing our sqlite database that's what this setup path function is doing here so this one is xdg compliant it uses Go app paths which is built by our very own muesli that queries both app and platform specific paths and that's what gets returned you read in what the paths are to those data directories and then from here we check if there's more than one directory let's take the highest priority directory and assign that to our task directory that's where we're going to write our sqlite database if not let's just grab the home directory and assign that to our Tasker where we're eventually going to write our database so in this one we're doing init Tasker and we are in the db.go by the way so you know for the record I could probably move this to the main knockout so that it's all kind of together I don't know why this one's over here I'm just a chaotic programmer apparently so init Tasker this one receives a path and basically we just check if a file or folder exists there already if it doesn't then we're going to create it and make sure it has the right privileges so that we can write to it when we're interacting with our application so then if that causes an error then we're gonna you know fail the application because we can't write to the system we don't have anywhere to save the database so basically we crash and burn each time that we're doing some kind of database operation it's getting called when we also trigger to open the database this opendb does exactly what it says it does it opens a sqlite database and I've provided it with a path to the database file we create a tasks.dv file within that directory and then here we create a new task DB struct which holds our SQL database and our data directory so it's like our path and then we just do a quick check to see does our tasks table exist already so that we're not overwriting anything or like causing any conflicts or getting a SQL error in case we're trying to write to a table that doesn't exist basically this is opening the sqlite database so we can check that off the list and then let's kind of ignore what's going on in the main file right now with the root command we're not there yet what we can do is go to our dv.go file and this is where we're doing the bulk of our work for that first section of the data storage so here we've just got a helper that is converting our Iota AKA go enum to string values that gets used at some point and then here we have our task struct and this just has all of the fields that we want in our sqlite database this is so that we can display it in the kanban board Okay so we've got the filter value and we've got title and we've got description so those are just implementing the list.item and list dot default items that you need to implement for any of your own custom list items and then here we're implementing the kanban CLI the other library that we're using for the kanban board this is its its status interface so when you hit enter you know how it moves from one column to the next that's all that this is doing is it's just kind of letting you do that all right and then here we've got our task DB which is just our data layer structure and that holds our database and our data so here we're initializing the Tasker which you saw already in our main and that should probably exist in the main function just so that it's with whatever is calling it right it's only really being called in that main function and then we're doing our table exists and then we've got a create table which is just a SQL command for creating a new tasks table and this is using all of the fields that we have in our in our custom task struct that we want to exist in our sqlite database and it sets our ID to Auto increment and then with our insert I've left some little notes Here we don't care about the return values so we're using exact if we wanted to reuse these statements it would be more efficient to use prepared statements and you can learn more about that on in the link there and so here we're doing a t.db.exec and we are just inserting into tasks and muesli mentioned that if you are doing any kind of SQL queries and stuff like that that is best practice to keep the SQL keywords as all uppercase even if you don't need to and go for it to work properly he said it's just a good practice so worth noting that and another important thing is the way that this query is structured so I am using um parameterized SQL statements and you'll see that throughout this entire tutorial here that we're using the parameterized SQL statements to avoid SQL injections make sure you're doing that okay only use your parameterized SQL statements for insert we're going to set a name project to do.string and time.now we're just going to make it happen at the current time and then here just a basic SQL query where we're just going to delete it where the ID matches and what's great about this is that this code is actually very it's very simple it's very testable so I actually did include a test file in case you're hoping to write some tests along the way if you want to do some test driven development or just practice writing your tests along the way this was pretty straightforward to do for the data layer so all of the like insert delete all the crud operations I made sure to write tests forward just in case just to make sure that you know everything's working as it should before adding the CLI layer so if you want to understand how some of that stuff's working under the hood and what the setup and tear down and all of that looks like for the test I can go over that after I finish the db.go file I think tests are really fun for just trying to understand how something works you know what I mean so if you're looking at a piece of code and you're like but how hot what you know and you just look at the test so in this update function it allows us to just provide multiple changes to make to Any Given task and how it does that is that it receives the new task as a parameter and then it will query the database for whatever the task used to be the original value and then it'll merge the new task to check which Fields have changed and which ones haven't and it'll set those new values in the database and we can check out what that merge function how that actually works so this is why we had to have exported fields in our custom task is because this merge is checking over all of the fields in each of these tasks and comparing them to see if it's changed and reflect will only work on exported struct fields and then here what's going on is we're basically grabbing the LM okay so that's basically it's just the value the reflect value of a task all right and then here we're iterating over all of the fields in the new struct like it doesn't matter we could have done this o values or u values they're both they both have um the same number of fields so it's fine but we're iterating over the fields and then we are checking uh the field this is uh the current value as an interface because you know generics crazy go Lang ah like whatever data type that is are we able to set that value on the original task and if you can this is checking the type of uh the field so we're doing a type check okay is it an INT yes or no if it is an INT and it is set then let's change the value of whatever field we're on for the original task and then it's doing the same thing here for the string so it's just doing a type check on whatever field we're currently iterating on in the updated task it's checking okay has that been set and if it has then update whatever value that same field has in the original task okay we're using a pointer for the orange value for both of these tasks when they're being set because if we're setting either of these values when we're doing reflect.value of this has to be a pointer Claire's mud perfect all right and then we do our get tasks which is just select everything from tasks it's basically just it's just iterating over the the rows that get returned from our query and then setting those values to a task struct and then appending that to our list of tasks and then finally at the end it returns that list of tasks this just allows you to query all of the tasks that have a certain status right now it's only being used I think in the kanban commands yeah so it's only being used with the kanban board but you could totally change that to also be a filtering option for your regular list output as well as like a flag or something there's a little piece of homework if you decide that you want to extend this a little bit more and we're just doing something very similar we're just iterating over the results that we get back assigning that to a task pending it and returning that list of tasks and then this one we are doing a query row because we know that it's only going to be getting one value back we're scanning that into a task variable that we initialize at the beginning of the function and then returning that variable and that's it that is all of our data storage stuff that we wanted to do we've got our opening our database which is in the main.go file the rest of it is all in this db.go file and now let's get into the tests so I'm going to show you how you can write some tests here so here I've got my test delete function I did a table driven test even though I've only got one test case okay don't at me I personally just like the table driven tests because then I can add I can just add more and I don't have to fully refactor the whole test I like to use TDOT run because it allows you to kind of have a name associated with the test case that you're running so when the test fails it's a lot easier to figure out where it failed and then debug from there and then for each test case which is what we're doing here we're iterating over the list of tests and we are doing a setup and tear down for each of these tests I think in this case I would have been better off doing the TDOT tempter and writing my database in there so if you want to write an enhancement of that there you go got you I got you I'll keep giving you more things to do don't worry don't worry I can I can write bugs all day so we're just doing our setup which I guess I guess I can show you how that works I'm just doing a clean tempter which is just a tempter from the OS package oh yeah and this one was so annoying because there was a bug between Mac OS and Linux where like one of them didn't include a slash at the end so I gotta double check whether the last element in the list it's a slash otherwise you need to add a slash so that it doesn't cause an issue so uh and uh yeah so that's what we're doing we're just opening up a new SQL database creating a new task DB and then that's the taskdb that is getting used in our test and then in our tear down here we close the database and then we remove the data directory and then here we are testing our insert and our delete because how are we going to delete something if it doesn't exist all right actually that would be a good test case as well that's a good Edge case is like what if you're trying to delete something that doesn't exist okay what's it gonna do make sure that you account for that which I did not so if you want to write another test you want to get a little coding in there you go right one of my test cases where it doesn't insert anything so in this case I'm just like all right I'll just I'll see what happens if it's a it's a fresh database with only one value and I try and delete that so that's what we're pretty much doing here we're doing an insert we're getting our tasks and I hard coded the first task index here because I'm a risky girl risking it all for the tests and so here I'm just doing a reflect.jp equal to compare the two structs and make sure that they both are what we want them to be you know what I mean you know what I'm saying and then we delete it and just double check that we didn't get any unexpected errors and if the length of the tasks is not empty then we're gonna have a problem so again in this case uh funny that I did a table driven test because this test is very much tailored to the expecting that there's only one value that is being added to the database and then we test get tasks so this one's similar it's got the similar uh setup teardown and we're just we're just writing tests for all of our all of our Getters making sure we don't get any unexpected errors doing our reflect.deep equal to compare two structs and then there's this update which is just testing that the only thing that's changing in the new is that the name is changing so what's happening here is the old is going to get added to our database and then we are going to update with the new task and then we're just going to double check that the other fields that it got merged properly right so that the name is what's changed the project and the status stay the same and then I've also got a test for test merge because I was writing the test while writing the code for merge to make sure that it was actually working as expected which we love this is just comparing to like the same thing two tasks making sure that the structs are equal whereas the other one is also querying it also has a database included in the test just to make sure it's writing to the database properly as well right gotta make sure my SQL queries are working you know and then the get tasks by status blah blah blah I think we've gone through all of it let's get into the next part which is building the actual CLI that the user is going to be able to interact with all of that is housed in the commands file so we'll open up our commands.go and you'll see here with Cobra there's always a root command that gets triggered and now we can actually revisit what we looked at in the main file so here you'll see that pretty much any time that the binary gets executed the root command gets executed this is what gets run when you just call the binary itself so root command here I just have it returning the help got the ad command here and as you can see this use and short these both get used to generate the help so Cobra is auto generating that command.help that is built in and it's based off of the use and the short descriptions that you give your commands and that's how the users are going to know what commands uh what commands you have for your application and how many arguments they need and all of that stuff so you see here that basically we're just using the insert we're just using the insert we're seeing if they used a project flag if they did then we're also going to insert the Project's name into the task and that's pretty much it we're just writing to our database and what's nice about this is like you don't really necessarily want to be testing these commands right because they're just sub commands that do something so this runny this can also just be a function that you define separately it doesn't have to be an anonymous function and right you can write a test for that function if you decide that you want to in this case I've already got all my edge cases tested here and then we've also got the delete command so this one similar thing you uh kind of just give it an ID I should also be letting them know that they need to add an ID there and this is the same thing we're basically just doing our rdb setup we're just grabbing the argument that's that gets given to the delete command which is an integer so we have we receive it as a string we have to convert it to an integer assign that to ID use our delete command or our delete function that we previously defined so for our update command we're just doing the same kind of setup that we did previously opening the database during the close we're grabbing all of the information from the flags which include the name project and status to see if any of those have changed from the command and then we're grabbing our ID from the list of arguments and then one thing that might stand out to you is that this flag for status is actually an integer I decided to do that just so that it minimizes confusion for the user it's basically going to be the index of the column that you want it to go to so it would be like to do would be zero in progress one done two I was just thinking if it's a string it's like then it's kind of then you gotta handle of like it being case sensitive then you gotta handle like the input being an invalid input it can just be a little more fussy uh if you disagree with me feel free to let me know and I'll reconsider the design decision there but for now I decided to just keep it as an integer if you want to update your status just for simplicity's sake but when it is being stored in the database we are storing it as the string so it's basically grabbing the string values from our enum our enum our Iota I ought to not say enum so we're creating a new task and then we're updating with the new task this list command I decided to keep it similar to the task Warrior where it's just a table but I didn't want to design a table okay I didn't want to do that [Music] what we're doing here we're gonna speed past the opendb close DB all that good stuff and you'll see I'm setting up a table I have another function here so what's going on here is that I'm actually using one of our bubbles components but I'm not using any of the interactivity I'm just using the layout so I'm just using the view which means that I'm just going to get back a string value and I'm going to print that that is me using the styling from one of our bubbles components and just putting in my own values there and just printing it out if you want to add an interactivity you can go for it um we're gonna see that a little bit more in with the kanban board but for now we're just going to render a static table that has all of the information that we want in there so I'm defining the columns here with the table dot column this is going to have each of the fields that I have defined in our database we're creating rows for the table we're going through an appending one row for each task the table.row is an array of strings so everything's getting converted to a string for it to get displayed pulling in all those fields and then by the end of this little for Loop we have a full array of rows that we want in our table and so we're going to create our new table we're going to say that it has columns and we're giving it the value of the columns that we've just defined with rows and that's with all of the Rose content and then with focused you can change there's quite a few different little attributes that you can set and I decided to give it the same height as however many tasks we have and then we're gonna do uh default style so I'm gonna grab whatever the default styles are for the bubbles component you can see here that we've modified the header we've decided to give it a border we want to change the Border foreground and we change all of this stuff so things in the lip gloss style that we decide to set and customize for ourselves and then we set the styles for the table built-in function okay for the record if you're looking to see what kind of capabilities there are in the apis for these libraries for any go libraries go to package.go.dev that is the easiest way to see what exported functions are available for different libraries and if the developers are kind enough to use godoc then everything's all documented and there's examples and it's beautiful and then finally we've got the kanban board so I know some of you might have already seen the kanban board tutorial if you haven't it's around all right I'll put it somewhere this is going to allow us to do an Interactive kind of interactive it's like a faux interactive experience all right but what's Happening Here is I'm gonna grab all of the items that I want in each column because each of them are their own lists so I'm going to grab the tasks by status I'm going to group The to Do's the in progress and the done and here I'm going to create a new column and I'm going to convert all of my tasks to just list items here we iterate through the tasks and just append it to a list of list dot items so we create new columns for the kanban board and then we do a new default board and we add in those columns and then all we do is we do a TDOT new program and the main model is just the board and that's it that's it this is this is it you could do the exact same thing with the table I'm pretty sure I haven't tried it but I encourage you to try it because I don't want to but you can because you're ambitious and you're watching this video and you're learning so I'm going to give you some more stuff to do don't let me down and then here you can see a knit so this is basically just the setup for Cobra so here is where I'm defining all of those flags and the types so this string P here allows you to do a shorthand letter it'll also say in the help dock what each of these descriptions are for the flags so that's really helpful and then what I'm basically doing here is I'm saying the add command list command update command delete command kanban those are all sub commands of the root command if I wanted to add a sub command of the update I would basically do this exact same thing but it would be update command dot add command and then whatever the new sub command is you got me you feel me that's the whole thing I think where am I I hope you enjoyed this video let me know what you think about the format I think this is a little more focused than when I'm trying to type along by all means if you prefer the videos where I code along with you let me know but I think I would rather encourage you to try and solve each of these steps we'll see you in the next one I hope you enjoyed this project I hope you enjoy all the little tasks I've given you along the way a little sprinkled in there I will pluck those little tasks out and I will add them to the description maybe I'm sure some of you are going to come up with some killer ideas for ways that we can improve this further so I'm really excited to see what the what the forks bring okay faith in the forks okay yeah fine bunny out see you next time bye
Info
Channel: Charm CLI
Views: 24,644
Rating: undefined out of 5
Keywords:
Id: yiFhQGJeRJk
Channel Id: undefined
Length: 26min 43sec (1603 seconds)
Published: Fri Aug 18 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.