Project Amber CLI client
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

338 lines
6.4 KiB

package main
import (
"flag"
"fmt"
"strconv"
"syscall"
"time"
"github.com/pkg/errors"
"git.tdem.in/tdemin/amber_cli/internal/tasks"
"github.com/gookit/color"
"golang.org/x/crypto/ssh/terminal"
)
var (
errIDParseFailed = errors.New("failed to parse ID")
errTaskNotFound = errors.New("task ID does not exist")
)
// simple CLI framework
type cliCommand interface {
Flags() *flag.FlagSet
// should look like "command"
Name() string
// should look like "command [pos arg 1] [pos arg 2]: does X"
Usage() string
Run(args []string) error
}
type loginCmd struct{}
func (l loginCmd) Name() string {
return "login"
}
func (l loginCmd) Usage() string {
return "login: login to a sync server"
}
func (l loginCmd) Flags() *flag.FlagSet {
return nil
}
func (l loginCmd) Run([]string) error {
var addr, user, pass string
fmt.Print("Address: ")
fmt.Scanln(&addr)
fmt.Print("Login: ")
fmt.Scanln(&user)
fmt.Print("Password: ")
bytes, err := terminal.ReadPassword(syscall.Stdin)
if err != nil {
return err
}
pass = string(bytes)
hc.BaseURI = addr
if err := hc.Login(user, pass); err != nil {
return err
}
cfg.Server = addr
cfg.Token = hc.Token
if err := cfg.Write(); err != nil {
return errors.Errorf("config write failed: %v", err)
}
return nil
}
type addCmd struct{}
var (
addCmdFlags = flag.NewFlagSet("add", flag.ExitOnError)
addCmdPID int
)
func init() {
addCmdFlags.IntVar(&addCmdPID, "p", 0, "new parent ID")
}
func (a addCmd) Name() string {
return "add"
}
func (a addCmd) Usage() string {
return "add [text]: add a task"
}
func (a addCmd) Flags() *flag.FlagSet {
return addCmdFlags
}
func (a addCmd) Run(args []string) error {
addCmdFlags.Parse(args)
task := db.New(tasks.NoID, addCmdPID, addCmdFlags.Arg(0))
id, err := hc.PostTask(task)
if err != nil {
if errors.Is(err, errInvalidToken) {
// FIXME: addCmd: invalidate login
}
task.ToSync = true
} else {
task.ID = id
}
return task.Add()
}
type removeCmd struct{}
func (r removeCmd) Name() string {
return "remove"
}
func (r removeCmd) Usage() string {
return "remove [id]: remove task"
}
func (r removeCmd) Flags() *flag.FlagSet {
return nil
}
func (r removeCmd) Run(args []string) error {
id, atoiErr := strconv.Atoi(args[0])
if atoiErr != nil {
return errIDParseFailed
}
task := db.TaskByID(id)
if task == nil {
return errTaskNotFound
}
_, err := hc.DeleteTask(task)
if err != nil {
if errors.Is(err, errInvalidToken) {
// FIXME: removeTask: invalidate login
}
// preserve task locally but mark it for removal for later
task.ToRemove = true
return task.Update()
}
return task.Remove()
}
type updateCmd struct{}
var (
updateCmdFlags = flag.NewFlagSet("update", flag.ExitOnError)
updateCmdPID int
updateCmdText string
)
const (
// it's -1, not 0, as setting PID to 0 counts as resetting parent
unsetID int = -1
unsetText string = ""
)
func init() {
updateCmdFlags.IntVar(&updateCmdPID, "p", unsetID, "new parent ID")
updateCmdFlags.StringVar(&updateCmdText, "t", unsetText, "new task text")
}
func (u updateCmd) Name() string {
return "update"
}
func (u updateCmd) Usage() string {
return "update [id]: update task data"
}
func (u updateCmd) Flags() *flag.FlagSet {
return updateCmdFlags
}
func (u updateCmd) Run(args []string) error {
updateCmdFlags.Parse(args)
id, err := strconv.Atoi(updateCmdFlags.Arg(0))
if err != nil {
return errIDParseFailed
}
task := db.TaskByID(id)
if task == nil {
return errTaskNotFound
}
if updateCmdPID != unsetID {
newParent := db.TaskByID(updateCmdPID)
if newParent == nil {
return errTaskNotFound
}
task.SetPID(newParent.ID)
}
if updateCmdText != unsetText {
task.SetText(updateCmdText)
}
if err := hc.UpdateTask(task.UpMtime()); err != nil {
if errors.Is(err, errInvalidToken) {
// FIXME: updateTask: invalidate login
}
task.ToSync = true
}
return task.Update()
}
type listCmd struct{}
func (l listCmd) Name() string {
return "list"
}
func (l listCmd) Usage() string {
return "list: list task tree"
}
func (l listCmd) Flags() *flag.FlagSet {
return nil
}
func (l listCmd) Run([]string) error {
lastmod, err := db.LastMod()
if err != nil {
return err
}
var ts = "never"
if !lastmod.Equal(tasks.NoTime) {
ts = lastmod.Format(time.RFC3339)
}
color.Cyan.Printf("Last modification time: %v\n", ts)
printNestedList(db.RootTasks(), 0)
return nil
}
type toggleCmd struct{}
func (t toggleCmd) Name() string {
return "toggle"
}
func (t toggleCmd) Usage() string {
return "toggle [id]: toggle task status"
}
func (t toggleCmd) Flags() *flag.FlagSet {
return nil
}
func (t toggleCmd) Run(args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return errIDParseFailed
}
task := db.TaskByID(id)
if task == nil {
return errTaskNotFound
}
task.ToggleStatus().UpMtime()
if err := hc.UpdateTask(task); err != nil {
if errors.Is(err, errInvalidToken) {
// FIXME: toggleTask: invalidate login
}
task.ToSync = true
}
task.Update()
return nil
}
type helpCmd struct{}
func (h helpCmd) Name() string {
return "help"
}
func (h helpCmd) Usage() string {
return "help: display help"
}
func (h helpCmd) Flags() *flag.FlagSet {
return nil
}
func (h helpCmd) Run([]string) error {
printUsage()
return nil
}
type versionCmd struct{}
func (v versionCmd) Name() string {
return "version"
}
func (v versionCmd) Usage() string {
return "version: display version"
}
func (v versionCmd) Flags() *flag.FlagSet {
return nil
}
func (v versionCmd) Run([]string) error {
fmt.Printf("%s version %s\n%s\n", AppName, AppVersion, CopyrightText)
return nil
}
func printNestedList(tasks []tasks.Task, level int) {
for _, t := range tasks {
printTask(t, level)
printNestedList(t.Children(), level+1)
}
}
func printTask(task tasks.Task, level int) {
// tasks to be removed on next sync are not supposed to be listed
if !task.ToRemove {
for i := 0; i < level; i++ {
fmt.Print("\t")
}
color.Bold.Printf("#%d: ", task.ID)
color.Blue.Printf("%s ", task.Text)
if !task.Completed {
color.Red.Println("PENDING")
} else {
color.Green.Println("COMPLETED")
}
}
}
func printUsage() {
fmt.Printf("%s: a CLI task list app\n\n", AppName)
color.Bold.Println("Commands:")
for _, cmd := range cmds {
fmt.Printf("\t%s\n", cmd.Usage())
if flags := cmd.Flags(); flags != nil {
flags.VisitAll(func(f *flag.Flag) {
fmt.Printf("\t\t-%s: %s\n", f.Name, f.Usage)
})
}
}
}