// ============================================================================= // Auth: Alex Celani // File: tdm.go // Revn: 03-29-2022 3.0 // // Func: display and manage progress of a litany of items to be done, // with an organization scheme similar to Trello. It's CLI // Trello // // TODO: add debug statements // // fix issue where fileIn() breaks on empty board // add second object to hold command information // make subsubtasks for greater depth // M or rooms to contain boards // handle command line args // auto-update task fill based on subtask fills and (settable) // weighting // M remember last args // move most (read: all) helper functions to external files // M add priority to tasks // attach text files??? // fix data storage issue // contemplate adding XOR of data in file, altho who cares // contemplate adding battery of tests // contemplate moving functions to external file // side-by-side printing // colored printing? // keep track of finished items // periodic tasks? something to do with the time package // convert file delimiters to (customizable?) variables // do some more legit testing // ============================================================================= // JUMP TAGS // for the lucky few who use vim, press * on a tag to jump to that function // CATCH INPUT JOIN FILEIN FILEOUT HELP DELETE FIND СДЕЛАЙ MARK PARSE SHOW MAIN // ============================================================================= // CHANGE LOG // ----------------------------------------------------------------------------- // 10-04-2021: init // wrote bones of string parsing into data structure // 10-05-2021: commented // 10-06-2021: entirely rewrote text format and text processor // 10-09-2021: added reading from file // wrote bones to while loop // created empty functions for commands // wrote, but did not test, getting user input // 10-10-2021: fixed user input // wrote help() // wrote second draft of show(), fix immediately // 02-21-2022: gutted main loop // standardized method to pass commands to subfunctions // commented main() // 02-22-2022: made help() change log and comment block // commented help() // added XXX tags to "if flag {" statements // added searchable structure to ease defining searches // with multiple different types of function calls // began restructuring show() to focus on sanitizing // inputs before show() gets called // 02-23-2022: changed searchable to query // 02-24-2022: reduced scope of sanitize() to work with a more rigid // set of inputs, just to get a stable release // finished sanitize() // tested sanitize() // commented sanitize() // wrote the /all/ portion of show() // 02-25-2022: wrote rest of show() // rewrote /show/ portion of help() // connected sanitize() to show(), added show() to main() // 02-27-2022: commented /all/ portion of show() // 03-01-2022: planning for find() and delete() // wrote and tested find(), checks out // changed delete() to del() to avoid overloading builtin // wrote and tested first draft of del() // 03-02-2022: commented /board/, /task/, and /subtask/ portions of // show() // wrote mark() // made show() accept commands of len greater than 3 // commented first draft of mark() // 03-03-2022: commented first draft of find() // commented del() // 03-04-2022: improved error condition in mark() // 03-06-2022: wrote first draft of parse() to replace sanitize() // 03-07-2022: wrote first draft of сделай() // 03-08-2022: tested следай() // wrote and tested fileOut() // watched Taylor Tomlinson's Look At You // updated fileIn() to work with files that don't exist // updated work functions to take queries instead of // []strings // 03-09-2022: updated function comment blocks // 03-10-2022: commented fileOut() // began commenting parse() // began updating help() //*03-11-2022: updated help() // commented /move/ and /mark/ of parse() // super fixed subtask propogation bug in fileIn() // 03-12-2022: rewrote /delete/, /show/, and /mark/ // 03-13-2022: added missing call to lower() in input() // rewrote /make/ // began testing /delete/ // 03-14-2022: finished testing /delete/ // tested /show/, /mark/, and /make/ // 03-15-2022: integrated parse.go as parse() // added jump tags :) // commented parse() // 03-20-2022: first draft rewrite of find() //*03-27-2022: fixed bug where tasks can't be made under boards with // name of length 1 //*03-29-2022: added, tested, and commented function for catching // keyboard interrupts // // ============================================================================= package main import ( "fmt" // used for Println "strings" // used for Split, ToLower, Join "strconv" // used for Atoi "os" // used for Exit, Stdin "os/signal" // used for Notify "syscall" // used for SIGTERM "io/ioutil" // used for ReadFile, WriteFile "bufio" // used for NewScanner ) // aliases for common functions var p = fmt.Print var print = fmt.Println var printf = fmt.Printf var split = strings.Split var lower = strings.ToLower var atoi = strconv.Atoi var itoa = strconv.Itoa var exit = os.Exit var read = ioutil.ReadFile var write = ioutil.WriteFile // CATCH finally something fun // asynchronous function that catches keyboard interrupts and exits // gracefully func catch() { // blocking channel of type signal c := make( chan os.Signal ) // I didn't read what this does, but it was in an example and it // worked, so // from what I can ascertain, when SIGTERM is detected, it binds // Interrupt to channel c signal.Notify( c, os.Interrupt, syscall.SIGTERM ) // asynchronous goroutine go func() { <-c // immediately halts in blocking read for signal if changed { // if there was a change to the map... fileOut() // rewrite data file } print( "\r" ) // this helps with the missing newline exit( 0 ) // and exit gracefully }() } // INPUT // wrapper function to print input prompt, take user input func input( ps1 string ) string { p( ps1 ) // print prompt // create scanner on stdin scanner := bufio.NewScanner( os.Stdin ) scanner.Scan() // read input return lower( scanner.Text() ) // return text } // JOIN // quick function to join an array of strings, with delimiting spaces func join( subcommands []string ) string { var ret string // declare returnable string // iterate over contained strings inside array for _, word := range subcommands { // add word and delimiting space to returnable ret = ret + word + " " } // return everything but last character, a trailing space return ret[:len( ret ) - 1] } // FILEOUT // wrapper function to contain all the file writing func fileOut() { // initialize data to be written as empty string var data string = "" // keep track of how many boards have been written, so the // delimiter isn't written after the last board var boardCount int = 0 // iterate over all boards for bname, bcontents := range boards { // increment number of boards written // actually caused a medium bug, because it's incremented // before the check against len(), so if != len() - 1 placed // delimiters in the wrong places boardCount++ data = data + bname // add board name to writestring data = data + "|" // XXX task delimiter // iterate over tasks for tind, tcontents := range bcontents { data = data + tcontents.name // add task name data = data + "," // XXX task delimiter // convert fill back to ascii // no error, because I guess all ints can be converted to // ascii, but not all ascii can be an int? // TODO think about keeping fill as ascii // maybe check the conversion to see if it worked (i.e. // user entered a number), but the ints are never // _actually_ used fill := itoa( tcontents.fill ) data = data + fill // add task fill // does the task contain subtasks? if len( tcontents.subt ) != 0 { // XXX task's subtask delimiter data = data + ":" // iterate over a task's subtasks for sind, scontents := range tcontents.subt { // add subtask name // XXX if flag { // if debug set, print incoming // subtask and data before... print( "subt.name: ", scontents.name ) print( "pre data: ", data ) } data = data + scontents.name // XXX if flag { // and after print( "post data: ", data ) } data = data + ">" // XXX subtask fill delim // convert fill back to ascii // TODO as above fill = itoa( scontents.fill ) data = data + fill // add subtask fill // check if NOT last subtask if sind != len( tcontents.subt ) - 1 { data = data + "." // XXX subtask delimiter } } } // check if NOT last task if tind != len( bcontents ) - 1 { data = data + ";" // XXX task delimiter } } // check if NOT last board // slightly different than before, explained above if boardCount != len( boards ) { data = data + "$" // XXX board delimiter } } // cast writestring to byte array, write to file named "data" // if a file doesn't exist, make with 0x644 permissions // take error as err, if fails err := write( "data", []byte( data ), 0644 ) if flag { // if debug set, print data written print( data ) } if err != nil { // if write() failed, tell user print( "Error in fileOut(): file not properly written" ) print( err ) } } // FILEIN // warpper function to contain all the file reading func fileIn() { // test string to use in lieu of a file // file := // "b1|tA,:sI>30.sII>30;tB,25;tC,40$b2|tA,5;tB," // b1|tA,:sI>30.sII>30;tB,25;tC,40 ~ first board // $ ~ board delimiter // b2|tA,5;tB, ~ second board // b1 b2 ~ board names // |tA,:sI>30.sII>30;tB,25;tC,40 |tA,5;tB, ~ board contents // | | ~ task marker // ta tB tC tA tB ~ task names // ; ; ; ~ task delimiter // ,:sI>30.sII>30 ,25 ,40 ,5 , ~ task contents // , ,25 ,40 ,5 , ~ task fill // , , , , , ~ task fill delim // :sI>30.sII>30 ~ task subtasks // : ~ subtask marker // sI sII ~ subtask names // . ~ subtask delim // >30 >30 ~ subtask fill // > > ~ subtask fill delim file, err := read( "data" ) // open file, failure set err // declare board, because if statement gets mad if I don't var board []string // XXX if flag { // if debug set, print raw file print( "file: ", string( file ) ) } // no file -> stop trying to build object, let boards be empty if err != nil { // notify user and leave print( "No file found" ) return } else { // if there is a file... // $ -> separates different boards board = split( string( file ), "$" ) } // iterate over all boards for a := 0; a < len( board ); a++ { // split board by name and tasks bContents := split( board[a], "|" ) bName := bContents[0] // assign name of board to var bTasks := bContents[1] // assign tasks of board to var // XXX if flag { // if debug set, print board name print( "board name -> ", bName ) } // separate tasks from each other tTasks := split( bTasks, ";" ) var tk task // make empty task // iterate over all tasks for b := 0; b < len( tTasks ); b++ { // XXX if flag { // if debug set, print raw tasks print( "tTasks -> ", tTasks[b] ) } // split subtasks out of tasks subs := split( tTasks[b], ":" ) // len is not one, there are subtasks // i.e. no reason to process subtasks if len( subs ) != 1 { // split list of subtasks sTasks := split( subs[1], "." ) // iterate over subtasks for c := 0; c < len( sTasks ); c++ { // split subtasks name from fill sContents := split( sTasks[c], ">" ) // convert fill from string to int, discard err fil, _ := atoi( sContents[1] ) // create subtask object with this extracted info stk := subtask { name: sContents[0], fill: fil } // add subtask object to array of such subtasks = append( subtasks, stk ) } // add full subtask array to task object tk.subt = subtasks } // separate task from fill tContents := split( tTasks[b], "," ) // if there is no fill, interpret this as 0 fill if len( tContents[1] ) == 0 { // inferred fill is a string, so it can be processed // like all the other fills tContents[1] = "0" } tk.name = tContents[0] // set name of task fil, _ := atoi( tContents[1] ) // convert fill to int tk.fill = fil // set fill of task tasks = append( tasks, tk ) // add task to task array // XXX if flag { // if debug set, print array of tasks print( "[]task -> ", tasks ) } // these lines are so stupid tk.subt = nil subtasks = nil // if tk.subt is not set to nil, then the same array would // go to every other task subtask array that follows } // make a key/value pair out of the board name and task array // add it to hashmap boards[bName] = tasks tasks = nil // nil out task array, same reason with subt } // XXX if flag { // if debug set, print entire structure print( "\n\nboards -> ", boards, "\n\n" ) } } // ----------------------------------------------------------------------------- // HELP( commands []string ) // // Revn: 03-10-2022 // Args: commands - array of strings // list of commands, should begin with "help" here // any subcommands would specify with which // commands that help is needed // Func: Prints help messages to the user. If the user requests help about // a specific topic, function will print only that topic, otherwise // print everything // TODO: FIXME rewrite // add /about/ subcommand // // add fail response? change structure to a switch/case // check that commands[0] is actualy "help" // make more targeted help prompts for when help has args // ----------------------------------------------------------------------------- // CHANGE LOG // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ??-??-????: wrote bones to // 02-22-2022: made change log // commented // 02-25-2022: rewrote /show/ to reflect new scope // 03-10-2022: rewrote /delete/ to reflect new scope // removed /move/ and /choose/ portions, because those // commands were removed // // ----------------------------------------------------------------------------- func help( commands []string ) { var helpFlag bool = true // init flag to view all var c string // init var for command[1] if len( commands ) > 1 { // check for plural commands c = commands[1] // capture first command // recall that commands[0] is the word "help" // set flag to see every entry on user request helpFlag = c == "all" || c == "" } // XXX if flag { // if debug set, print commands[0] and helpFlag print( "\n\nc ->", c ) print( "helpFlag ->", helpFlag ) print( "\n" ) } print() // print extra line to avoid cluttered look // help text really should speak for itself // important things to note: helpFlag is set true by default // helpFlag is set false if user asks for specific entry // c is set to the specific entry // if the specific entry is not present, nothing is printed // this really should be changed to a switch/case tbh if helpFlag || c == "delete" { print( " delete [board] " ) print( " \\-- delete board " ) print( " \\ [board|task|subtask] " ) print( " \\-- delete board , task , or subtask , provided argument exists " ) } if helpFlag || c == "make" { print( " make [board] " ) print( " \\-- create new board, " ) print( " \\ [board] task " ) print( " \\-- create new task under board " ) print( " \\ task subtask " ) print( " \\-- create new subtask under task " ) } if helpFlag || c == "mark" { print( " mark task " ) print( " \\ \\-- fill -> 0 - 100" ) print( " \\-- set progress of to " ) print( " mark subtask " ) print( " \\ \\-- fill -> 0 - 100" ) print( " \\-- set progress of to " ) } if helpFlag || c == "show" { print( " show [all]" ) print( " \\-- display everything" ) print( " show [board] " ) print( " \\-- display contents of board " ) print( " show task " ) print( " \\-- display contents of task " ) print( " show subtask " ) print( " \\-- display contents of subtask " ) } if helpFlag || c == "exit" || c == "quit" { print( " exit" ) print( " quit" ) print( " \\-- exit program" ) } print() // print extra line to avoid cluttered look } // ----------------------------------------------------------------------------- // DELETE( search query ) // // Revn: 03-15-2022 // Args: search - query // object that summarizes what needs deleting // Func: delete entries // Meth: call find() to get position of wanted entry, replace with // last item in list, replace list with [:len()-1] slice // TODO: // ----------------------------------------------------------------------------- // CHANGE LOG // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // 03-01-2022: major planning for deleting functionality // plans for moving search functionality to new find() // function // wrote bones, not working // 03-03-2022: commented // 03-08-2022: changed arguments from commands []string to search query // 03-15-2022: added quick check for search.comm = fail // // ----------------------------------------------------------------------------- func del( search query ) { // determine what needs searching // check to see if query returned empty // not entirely sure if that's possible, but check for it if search == ( query{} ) { return } // if fail is set, quit if search.command == "fail" { return } // determine "coordinates" of object that needs finding // x -> ( string ) name of board, shouldn't ever be blank // y -> ( int ) index of task array for a given board // z -> ( int ) index of subtask array for a given task x, y, z := find( search ) if x != "" { // should be a given changed = true if y == -1 { // not task, should be board // maps have easy delete function // delete, from map 'boards', key 'x' delete( boards, x ) } else { // yes task if z == -1 { // not subtask, should be task // XXX it really needs to be mentioned that everything // here has to be referenced absolutely, because if you // make some other variable about it, like // b := boards[x][y] // when you go to reassign the array, it won't get rid of // the last task, of which there are now two // boards -> map[string][]task // boards[x] -> []task // boards[x][y] -> task // int len( boards[x] ) - 1 // task boards[x][len( boards[x] ) - 1] // take task at the end of the array, place it over // unwanted task boards[x][y] = boards[x][len( boards[x] ) - 1] // take slice of everything but the last task ( // because there's two of them in the array ), assign // it to array to replace array with two copies boards[x] = boards[x][:len( boards[x] ) - 1] } else { // yes subtask // oh boy // boards -> map[string][]task // boards[x] -> []task // boards[x][y] -> task // boards[x][y].subt -> []subtask // boards[x][y].subt[z] -> subtask // int len( boards[x][y].subt ) - 1 // subtask boards[x][y].subt[len( boards[x][y].subt ) - 1] // take subtask at the end of the array, place it over // unwanted subtask boards[x][y].subt[z] = boards[x][y].subt[len( boards[x][y].subt ) - 1] // take slice of everything but the last subtask ( // because there's two of them in the array ), assign // it to array to replace array with two copies boards[x][y].subt = boards[x][y].subt[:len( boards[x][y].subt ) - 1] } } } else { // should be unreachable print( "Error in delete(): object does not exist or not specified" ) } } // ----------------------------------------------------------------------------- // FIND( search query ) ( string, int, int ) // // Revn: 03-27-2022 // Args: search - query // object that outlines what needs finding // Retn: x, y, z - string, int, int // "coordinates" that outline where sought-after // objects are // Func: iterate over all everything until exact positions of // sought-after object is found // TODO: error messages // make returnables into struct ( "coords"? ) // ----------------------------------------------------------------------------- // CHANGE LOG // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // 03-01-2022: essentially stole from show() // 03-03-2022: commented // 03-20-2022: rewrote for updated query definition // 03-27-2022: commented // // ----------------------------------------------------------------------------- func find( search query ) ( string, int, int ) { var rboard string // init string as empty var rtask int = -1 // init ints as -1, the forbidden index var rsubt int = -1 // look at subcommand A specifically switch search.subA { case "board": // prs is true if search.board is in boards _, prs := boards[search.board] if prs { // if prs _is_ true, keep track of that board rboard = search.board } case "task": // iterate over boards in boards // board -> names of boards ( key in map ) // contents -> []task ( value in map ) for board, contents := range boards { // iterate through task array // ind -> index // task -> actual task object for ind, task := range contents { // if name of found task is requested task name if task.name == search.task { // keep track of board and index rboard = board rtask = ind } } } case "subtask": // iterate over boards in boards // board -> names of boards ( key in map ) // contents -> []task ( value in map ) for board, contents := range boards { // iterate through task array // tind -> task index // task -> actual task object for tind, task := range contents { // iterate through subtask array // sind -> subtask index // subtask -> actual subtask object for sind, subtask := range task.subt { // if name of found subtask is requested // subtask name if subtask.name == search.subtask { // keep track of board, task index, and // subtask index rboard = board rtask = tind rsubt = sind } } } } default: // if the subcommand A is not a known object, error out // I'm like 90% sure that this is unreachable, but // whatever print( "Error in find()" ) print( "Object /", search.subA, "/ not recognized" ) } // return returnable 3-tuple return rboard, rtask, rsubt } // ----------------------------------------------------------------------------- // СДЕЛАЙ( search query ) // // Revn: 03-27-2022 // Args: search - query // object that summarizes what needs making // Func: create new boards, tasks, and subtasks // Meth: find parent object, create new child object, attach // TODO: add debug // ----------------------------------------------------------------------------- // CHANGE LOG // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // 03-07-2022: wrote first draft // 03-10-2022: commented // 03-16-2022: rewrote to work with updated query{} definition // 03-27-2022: added check for command == "fail" // // ----------------------------------------------------------------------------- func сделай( search query ) { if flag { // if debug set, print contents of argument print( search ) } // if fail is set, quit if search.command == "fail" { return } // init variable to mark if parent object exists var exists bool = false // declare variable to determine what caused error var err string if search.subtask == "" { // if subtask not specified if search.task == "" { // if task not specified if search.board == "" { // if board also not specified // failure, make called without argument // print error, call help about it print( "Error in make()" ) print( "No object specified" ) makeFailure := []string{ "help", "make" } help( makeFailure ) return } else { // yes board exists = true // signal that parent did exist // declare empty task array var t []task // map board name to empty task array boards[search.board] = t changed = true // signal that file needs rewriting } } else { // yes task if search.board == "" { // if no parent board err = "board" // failure, called to make a task belonging to no // board // print error, call help about it print( "Error in make()" ) print( "Cannot make task without specifying board" ) makeFailure := []string{ "help", "make" } help( makeFailure ) return } else { // board is specified exists = true // signal parent did exist // create task with user-given name tk := task{ name: search.task } // grab []task that given board maps to // i literally don't want to type search.board out two // more times, v clunky and causes line overrun preTask := boards[search.board] if len( preTask ) == 0 { // if empty // declare empty task array var t []task t = append( t, tk ) // place inside array // map task array to new board boards[search.board] = t } else { // array not empty // simply append to pre-existing task array boards[search.board] = append( preTask, tk ) } changed = true // signal file needs rewriting } } } else { // subtask specified if search.task == "" { // no task tho err = "task" // failure, called to make subtask with a parent task // print error, call help about it print( "Error in make()" ) print( "Cannot make subtask with specifying task" ) makeFailure := []string{ "help", "make" } help( makeFailure ) return } else { // yes task // iterate over boards to look for requested task for bname, bcontents := range boards { // iterate over tasks in boards for ind, t := range bcontents { // if task name is given parent task name if t.name == search.task { exists = true // signal parent exists // create subtask with user-given name stk := subtask{ name: search.subtask } // if task's subtask array is empty if len( t.subt ) == 0 { // declare empty subtask array var s []subtask // place created subtask inside array s = append( s, stk ) // map subtask array to task boards[bname][ind].subt = s } else { // array not empty // simply append to pre-existing subtask // array boards[bname][ind].subt = append( t.subt, stk ) } // signal file needs rewriting changed = true } } } } } if !exists { // nothing was changed, because parent object // doesn't exist // print error, call help about it print( "Error in make()" ) p( "Parent ", err ) if err == "board" { p( " /", search.board, "/ " ) } else if err == "task" { p( " /", search.task, "/ " ) } else { p( " object " ) } print( "does not exist" ) makeFailure := []string{ "help", "make" } help( makeFailure ) } } // ----------------------------------------------------------------------------- // MARK( search query ) // // Revn: 03-17-2022 // Args: search - query // object that summarizes what needs showing // Func: allow user to complete tasks // Meth: call to find() to get "coords", set object at those coords // to fill // TODO: // ----------------------------------------------------------------------------- // CHANGE LOG // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // 03-02-2022: planned and wrote first draft, needs much error // checking // commented first draft // 03-04-2022: made error condition show more information // 03-09-2022: fixed error condition bug where program didn't know // what kind of object user and printed hanging slash // 03-11-2022: added reference to variable 'change', so fileOut() // will run // 03-17-2022: added references to updated definition of a query // // ----------------------------------------------------------------------------- func mark( search query ) { // check to see if query returned empty // not entirely sure if that's possible, but check for it if search == ( query{} ) { return } // if parse() failed, simply return // probably print failure too? if search.command == "fail" { return } // determine "coordinates" of object that needs finding // x -> ( string ) name of board, shouldn't ever be blank // y -> ( int ) index of task array for a given board // z -> ( int ) index of subtask array for a given task x, y, z := find( search ) // index of last item len( commands ) - 1 // last item in array commands[len( commands ) - 1] // convert string to int // atoi( commands[len( commands ) - 1] ) // markFill -> converted value // err -> contains value of atoi() error // would happen if passed value isn't ascii markFill, err := atoi( search.subB ) // on error, print failure, call help about it, return if err != nil { print( "Fill value /" + search.subB + "/ is not a number" ) markFailure := []string{ "help", "mark" } help( markFailure ) return } // round fill down to 100 max, specifically allow negative fill if markFill > 100 { markFill = 100 } if x != "" { // should be true, board is given if y == -1 { // not task, should be board // shouldn't be possible, boards don't have a fill print( "Error in mark(): task does not exist or not specified" ) } else { // yes task if z == -1 { // not subtask, should be task // boards -> map[string][]task // boards[x] -> []task // boards[x][y] -> task // boards[x][y].fill -> task.fill, fill field in task boards[x][y].fill = markFill changed = true // signal that file needs rewriting } else { // yes subtask // boards -> map[string][]task // boards[x] -> []task // boards[x][y] -> task // boards[x][y].subt -> []subtask // boards[x][y].subt[z] -> subtask // boards[x][y].subt[z].fill -> subtask.fill boards[x][y].subt[z].fill = markFill changed = true // signal that file needs rewriting } } } else { // reachable if the user gives input that DNE // notify user requested object did not exist p( "Error in mark(): " ) p( search.subA + " /" ) switch search.subA { case "board": p( search.board ) case "task": p( search.task ) case "subtask": p( search.subtask ) default: p( "object" ) } print( "/ does not exist" ) } } // ----------------------------------------------------------------------------- // PARSE( commands []string ) // // Revn: 03-27-2022 // Args: commands - []string // list of commands of what board, task, or subtask // to make // Retn: search - query // object that contains pertinent information // Func: parse user input, determine what command to call and about // what object is being called about // Meth: lots and lots of switch statements // switch over first command // switch over length of subcommand // switch over contents of subcommand // TODO: contemplate help case // move to external file? // ----------------------------------------------------------------------------- // CHANGE LOG // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // 03-06-2022: planned // wrote everything but help case // tested pass cases // 03-10-2022: commented /delete/ portion // commented /show/ portion // 03-12-2022: rewrote /delete/, /show/, and /mark/ // 03-13-2022: rewrote /make/ // began testing /delete/ // 03-14-2022: finished testing /delete/ // tested /show/, /mark/, and /make/ // 03-15-2022: commented // fixed small bugs related to valid keyword positions // 03-17-2022: added small reference to /save/ in the bugout case // 03-27-2022: fixed bug in /make/ where tasks under boards with // names of length 1 failed ( literally changed "> 1" // to "> 0" ) // // ----------------------------------------------------------------------------- func parse( commands []string ) query { // declare return object var search query // separate command from subcommands, take note of amount of // subcommands comm := commands[0] subcommands := commands[1:] subcomlen := len( subcommands ) // set pre-emptively, clear if it's bad // unused as of right now search.command = comm // big switch statement over command switch comm { // these don't really need processing, break out immediately case "save", "set", "quit", "exit": return search case "delete": // there are only so many types of commands that work here // -> delete // -> delete board // -> delete task // -> delete subtask // i.e. subcommand can have either length 1 or 2 // if user enters JUST delete, set fail and return if subcomlen == 0 { search.command = "fail" return search } switch subcommands[0] { case "task": // delete task xyz // set subcommand ( focus ) of search search.subA = subcommands[0] // join array into string and set as task search.task = join( subcommands[1:] ) case "subtask": // delete subtask xyz // set subcommand ( focus ) of search search.subA = subcommands[0] // join array into string and set as subtask search.subtask = join( subcommands[1:] ) default: // delete board xyz // delete xyz // set subcommand ( focus ) of search search.subA = "board" // if keyword board is given, join without keyword if subcommands[0] == "board" && subcomlen > 1 { search.board = join( subcommands[1:] ) } else { // otherwise, join everything search.board = join( subcommands[:] ) } } case "help": // help( subcommands ) case "make": // -> make // -> make board // -> make task // -> make board task // -> make task subtask // if subcommand length is 1 or 2, make a board // if subcommand length is 3 or 4, make a task/subtask // if user enters JUST make, set fail and return if subcomlen == 0 { search.command = "fail" return search } var x int = -1 // keep track of position of subcom if subcommands[0] == "task" { // make task xyz subtask ijk // iterate over subcommands ( not counting task ) // looking for the subtask keyword for ind, _ := range subcommands[1:] { // set x equal to index when subtask is found if subcommands[ind] == "subtask" { x = ind } } // the absolute minimum index for subtask keyword is 2 // if its found before 2, thats a problem // set fail and return if x < 2 { search.command = "fail" return search } else { // valid keyword position // set secondary focus to subtask search.subB = "subtask" // set main focus to parent task search.subA = "task" // join everything between keywords, set to task search.task = join( subcommands[1:x] ) // join everything after subtask, set to subtask search.subtask = join( subcommands[x+1:] ) } } else { // make xyz // make board xyz // make xyz task ijk // make board xyz task ijk // declare string to hold subsubcommand var subsubcom []string // determine what subsubcommand should include if subcommands[0] == "board" { subsubcom = subcommands[1:] } else { subsubcom = subcommands[:] } // iterate over subsubcommand looking for task keyword for ind, _ := range subsubcom { // set x equal to index when task is found if subsubcom[ind] == "task" { x = ind } } // if task was never found, making a board if x == -1 { // set focus to board search.subA = "board" // join everything and set to board search.board = join( subsubcom ) // task was found in a valid position } else if x > 0 { // set secondary focus to task search.subB = "task" // set main focus to parent board search.subA = "board" // join everything between keywords, set to board search.board = join( subsubcom[:x] ) // join everything after task, set to task search.task = join( subsubcom[x+1:] ) // task was not in valid position } else { // set fail and return return search } } case "mark": // NYI -> mark // NYI -> mark board // -> mark task fill // -> mark subtask fill // if user enters JUST mark, set fail and return if subcomlen == 0 { search.command = "fail" return search } switch subcommands[0] { case "task": // mark task xyz # if subcomlen < 3 { // absolute minimum length possible is 3 // task, name, and fill search.command = "fail" return search } // set focus to task // set fill to absolute last word in list search.subA = subcommands[0] search.subB = subcommands[subcomlen - 1] // join all the other words, set to task search.task = join( subcommands[1:subcomlen - 1] ) case "subtask": // mark subtask xyz # if subcomlen < 3 { // absolute minimum length possible is 3 // subtask, name, and fill search.command = "fail" return search } // set focus to subtask // set fill to absolute last word in list search.subA = subcommands[0] search.subB = subcommands[subcomlen - 1] // join all the other words, set to subtask search.subtask = join( subcommands[1:subcomlen - 1] ) default: // delete board xyz // delete xyz // set focus to board // set fill to 100 search.subA = "board" search.subB = "100" // if keyword board is given, join without keyword if subcommands[0] == "board" && subcomlen > 1 { search.board = join( subcommands[1:] ) } else { // otherwise, join everything search.board = join( subcommands[:] ) } } case "show": // similar call signature to delete() // -> show // -> show all // -> show // -> show board // -> show task // -> show subtask // subcom lengths of 1, 2, and 3 are all possible if subcomlen == 0 { // show // set subcommand to all search.subA = "all" } else { switch subcommands[0] { case "task": // show task xyz // set focus to task // join all the other words, set to task search.subA = subcommands[0] search.task = join( subcommands[1:] ) case "subtask": // show subtask xyz // set focus to subtask // join all the other words, set to subtask search.subA = subcommands[0] search.subtask = join( subcommands[1:] ) case "all": // show all // show if subcomlen == 1 { // if there's only one subcommand, it's // show all... // set subcommand to all search.subA = subcommands[0] } else { // otherwise, it must be a board // set focus to board // join all the other words, set to board search.subA = "board" search.board = join( subcommands[:] ) } default: // show board xyz // show xyz // set focus to board search.subA = "board" // if keyword board is given, join without keyword if subcommands[0] == "board" && subcomlen > 1 { search.board = join( subcommands[1:] ) } else { // otherwise, join everything search.board = join( subcommands[:] ) } } } default: // command not recognized // print error, call help about it //print( "Command /", comm, "/ not recognized" ) print( "Command /", comm, "/ not recognized" ) commFailure := []string{ "help" } help( commFailure ) } return search } // ----------------------------------------------------------------------------- // SHOW( search query ) // // Revn: 03-27-2022 // Args: search - query // object that summarizes what needs showing // Func: display stored information to the user // Meth: iterate through everything, look for matching info, print // TODO: stylize print in some way // add debug flag // ----------------------------------------------------------------------------- // CHANGE LOG // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // 02-24-2022: wrote bones // wrote /all/ portion // 02-25-2022: implemented /board/, /task/, and /subtask/ portions // added calls to help() on error // changed input argument from type query to type // []string, so it can call sanitize() // 02-27-2022: commented /all/ portion // 03-02-2022: commented /board/, /task/, and /subtask/ portions // 03-16-2022: updated to switch/case system to work with the new // version of parse() // 03-27-2022: removed 1.x version // // ----------------------------------------------------------------------------- func show( search query ) { // check to see if query returned empty // not entirely sure if that's possible, but check for it if search == ( query{} ) { return } // ease of use variables from search struct subcom := search.subA sbd := search.board stk := search.task sstk := search.subtask switch search.subA { case "all": // iterate over boards' contents for boardName, boardContents := range boards { // show all -> print board name print( boardName ) // iterate over board's tasks for _, showTask := range boardContents { // print indent with no trailing newline // indent has len 4 p( " " ) // print task name, right justified (with respect // to the indent) printf( "%-16s", showTask.name ) // print task fill, left justified // indent len 4 + 16 = 20 printf( "%16d\n", showTask.fill ) // iterate over the task's subtasks for _, showSubtask := range showTask.subt { // double indent // double indent has len 8 p( " " ) // print subtask name, right justified (with // respect to indent again) printf( "%-16s", showSubtask.name ) // print subtask fill, left justified // indent len 8 + 12 = 20 printf( "%12d\n", showSubtask.fill ) } } } case "board": // grab user requested board from boards // prs is set true if board exists contents, prs := boards[sbd] if prs { // if user specified board exists print( sbd ) // print board name // iterate over tasks in board for _, showTask := range contents { // print indent with no trailing newline // indent has len 4 p( " " ) // print task name, right justified (with respect // to the indent) printf( "%-16s", showTask.name ) // print task fill, left justified // indent len 4 + 16 = 20 printf( "%16d\n", showTask.fill ) for _, showSubtask := range showTask.subt { // double indent // double indent has len 8 p( " " ) // print subtask name, right justified (with // respect to indent again) printf( "%-12s", showSubtask.name ) // print subtask fill, left justified // indent len 8 + 12 = 20 printf( "%16d\n", showSubtask.fill ) } } } else { // if board DNE // print error, call help about it print( "Board /" + sbd + "/ does not exist" ) showFailure := []string{ "help", "show" } help( showFailure ) return } case "task": // can check if a board is present with _, prs := // but this is a flag to be set if a task exists var exists bool // iterate over all boards looking for named task for showBoard, contents := range boards { // iterate through all tasks in boards for _, showTask := range contents { // look for matching task name if showTask.name == stk { exists = true // set exists true // print containing board name print( showBoard ) // print task name with same justification printf( "%-16s", showTask.name ) // print task fill with right justification printf( "%20d\n", showTask.fill ) // iterate over contained subtasks for _, showSubtask := range showTask.subt { // print indent p( " " ) // print subtask name left justified printf( "%-16s", showSubtask.name ) // print subtask fill right justified printf( "%16d\n", showSubtask.fill ) } } } } if !exists { // if task not found // print error // call help about it print( "Task /" + stk + "/ does not exist" ) showFailure := []string{ "help", "show" } help( showFailure ) return } case "subtask": // can check if a board is present with _, prs := // but this is a flag to be set if a task exists var exists bool // iterate over all boards looking for named task for showBoard, contents := range boards { // iterate through all tasks in boards for _, showTask := range contents { // iterate over contained subtasks for _, showSubtask := range showTask.subt { // look for matching subtask name if showSubtask.name == sstk { exists = true // set exists true // print containing board name print( showBoard ) // print containing task name print( showTask.name ) // print subtask name left justified printf( "%-16s", showSubtask.name ) // print subtask fill right justified printf( "%20d\n", showSubtask.fill ) } } } } if !exists { // if subtask not found // print error // call help about it print( "Subtask /" + sstk + "/ does not exist" ) showFailure := []string{ "help", "show" } help( showFailure ) return } default: // unrecognized subcommand // print error, call help about it print( "Error in show()" ) print( "Subcommand /" + subcom + "/ does not exist" ) showFailure := []string{ "help", "show" } help( showFailure ) return } } var flag bool = false // debug flag var changed bool = false // struct used for defining search parameters type query struct { command string // command passed, or fail board string // requested board task string // requested task subtask string // requested subtaks subA string // subcom A, usually object being acted upon subB string // sometimes, two are needed } /* type subsubtask struct { name string // subsubtasks are named fill int // subsubtasks contain progress //note string // subsubtasks can contain files } */ // struct used for defining what constitutes a 'subtask' type subtask struct { name string // subtasks are named fill int // subtasks contain progress //ssbt []subsubtask // subtasks can contain subsubtasks //note string // subtasks can contain files } // struct used for defining what constitutes a 'task' type task struct { name string // tasks are named fill int // tasks contain progress subt []subtask // tasks can contain subtasks //note string // tasks can contain files } // boards is a hashmap map[ ] // that relates strings string // to an array of tasks []task var boards = make( map[string][]task ) var tasks []task // array of tasks referenced above var subtasks []subtask // array of subtasks belonging to tasks // MAIN func main() { catch() // call function to catch keyboard interrupts fileIn() // open data file, process into structure for { // infinite loop, where everything happens userIn := input( "-> " ) // take user input ui := split( userIn, " " ) // split over spaces command := ui[0] // first entry is command // convention: send entire array to subfunction, do not strip // any subcommands off // switch case over command // recognize user input, call appropriate command var search query = parse( ui ) switch command { case "delete": del( search ) case "help": help( ui ) case "make": // make translated into Russian, because make is a // GoLang keyword, and Norwegian word was less cool // pronounced like 'zdye-lie' сделай( search ) case "mark": mark( search ) case "save": fileOut() changed = false print( "Updates saved" ) case "show": show( search ) case "exit", "quit": if flag { // if debug set, print if file was changed print( changed ) } // if file was changed, rewrite to file if changed { fileOut() } exit( 0 ) // exeunt case "set": // XXX // turn debug flag on and off // conveniently only recognizes first and last word of // command; "set true" will work, as will "set flag // false" and "set sprite coke pepsi" flag = ui[len( ui ) - 1] == "true" default: // if the input is unrecognized continue // try again } } }