diff --git a/NOTES.md b/NOTES.md index 25b966959058313cddea018ca9dfac2ee59dee44..317f95e3648abc060083ce61250a3742b72f57f9 100644 --- a/NOTES.md +++ b/NOTES.md @@ -23,19 +23,25 @@ sqlite trigger tracing: `.trace stdout --row --profile --stmt --expanded --plain ## Things to do - * Implement a better dclish parser. * Implement each command. - * Next: ???? + * Next: folder commands - ~~CREATE~~, ~~REMOVE~~, MODIFY, INDEX, SELECT + * Messages: ADD, CURRENT, DIRECTORY * Editor - need an embedded editor * An EDT inspired [editor](https://sourceforge.net/projects/edt-text-editor/) * [gkilo](https://github.com/vcnovaes/gkilo) * This [kilo editor](https://viewsourcecode.org/snaptoken/kilo/) tutorial - * Should there be a way to upload text files for commands that use - files? How would it work? * Cleanup help output. - * Remove the node related flags. + * Remove the node/cluster/newsgroup/mailing-list related flags. * Database - * trigger to limit values for 'visibility'; + * trigger to limit values for 'visibility'? + * Add some of the early announcements from the sources - see the + conversion branch - to the GENERAL folder. + * Add commands: + * A way to add / delete ssh keys. + * A way to manage files? + * Commands for a local mail system? + * Commands to connect to Mattermost or mastodon? + * Commands to manage users. ## Module links diff --git a/accounts/accounts.go b/accounts/accounts.go index 186a1c9f08947cb6dd81c809edac64c1bc8205e0..9fee3ccd0473ee3ae59ab4a04e3f659964cf0994 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -7,13 +7,14 @@ import ( "strings" "git.lyda.ie/kevin/bulletin/folders" + "github.com/chzyer/readline" _ "modernc.org/sqlite" // Loads sqlite driver. ) // UserData is the type for holding user data. Things like preferences, // unread message counts, signatures, etc. type UserData struct { - Account string + Login string FullName string Folders *folders.Store CurrentFolder string @@ -24,30 +25,57 @@ type UserData struct { var User *UserData // ValidName makes sure that an account name is a valid name. -func ValidName(acc string) error { - if acc == "" { +func ValidName(login string) error { + if login == "" { return errors.New("empty account is invalid") } - if strings.ContainsAny(acc, "./") { - return fmt.Errorf("account name '%s' is invalid", acc) + if strings.ContainsAny(login, "./") { + return fmt.Errorf("account name '%s' is invalid", login) } return nil } // Open verifies that an account exists. -func Open(acc string) error { - err := ValidName(acc) +func Open(login string) error { + err := ValidName(login) if err != nil { return err } User = &UserData{ - Account: acc, + Login: login, } - User.Folders, err = folders.Open(acc) + User.Folders, err = folders.Open(login) if err != nil { return err } + user, _ := User.Folders.GetUser(login) + + if user.Login != login { + user.Login = login + user.Admin = 0 + + fmt.Printf("Welcome new user %s\n", login) + rl, err := readline.New("please enter your name: ") + if err != nil { + return err + } + user.Name, err = rl.Readline() + rl.Close() + + err = User.Folders.AddUser(*user) + if err != nil { + return err + } + fmt.Println("User successfully created. Enjoy!") + } + if user.Disabled == 1 { + return errors.New("User is disabled") + } + User.FullName = user.Name + User.CurrentFolder = "GENERAL" + // TODO: get the most recent unread message. + // User.CurrentMessage = return nil } @@ -56,12 +84,3 @@ func Open(acc string) error { func (u *UserData) Close() { u.Folders.Close() } - -// IsAdmin returns true if the user is an admin -func IsAdmin(acc string) bool { - if acc == "admin" { - return true - } - // TODO: Look up account otherwise. - return false -} diff --git a/folders/connection.go b/folders/connection.go new file mode 100644 index 0000000000000000000000000000000000000000..039ff10e22cce3dfba66266d75cde67ab62e82b9 --- /dev/null +++ b/folders/connection.go @@ -0,0 +1,64 @@ +// Package folders are all the routines and sql for managing folders. +package folders + +import ( + "embed" + "errors" + "os" + "path" + + "github.com/adrg/xdg" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/source/iofs" + "github.com/jmoiron/sqlx" + + // Included to connect to sqlite. + _ "github.com/golang-migrate/migrate/v4/database/sqlite" + _ "modernc.org/sqlite" +) + +//go:embed sql/*.sql +var fs embed.FS + +// Store is the store for folders. +type Store struct { + user string + db *sqlx.DB +} + +// Open opens the folders database. +func Open(user string) (*Store, error) { + fdir := path.Join(xdg.DataHome, "BULLETIN") + err := os.MkdirAll(fdir, 0700) + if err != nil { + return nil, errors.New("bulletin directory problem") + } + fdb := path.Join(fdir, "bboard.db") + + // Run db migrations if needed. + sqldir, err := iofs.New(fs, "sql") + if err != nil { + return nil, err + } + m, err := migrate.NewWithSourceInstance("iofs", sqldir, "sqlite://"+fdb+"?_pragma=foreign_keys(1)") + if err != nil { + return nil, err + } + err = m.Up() + if err != nil && err != migrate.ErrNoChange { + return nil, err + } + m.Close() + + store := &Store{user: user} + store.db, err = sqlx.Connect("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)") + if err != nil { + return nil, errors.New("bulletin database problem") + } + return store, nil +} + +// Close closes the db backing the store. +func (fstore *Store) Close() { + fstore.db.Close() +} diff --git a/folders/folders.go b/folders/folders.go index 039ff10e22cce3dfba66266d75cde67ab62e82b9..bfafdd21806ff73a81b141a33327fd86d0001953 100644 --- a/folders/folders.go +++ b/folders/folders.go @@ -2,63 +2,133 @@ package folders import ( - "embed" "errors" - "os" - "path" + "fmt" - "github.com/adrg/xdg" - "github.com/golang-migrate/migrate/v4" - "github.com/golang-migrate/migrate/v4/source/iofs" "github.com/jmoiron/sqlx" +) + +// FolderVisibility is the folder visibility level. +type FolderVisibility string - // Included to connect to sqlite. - _ "github.com/golang-migrate/migrate/v4/database/sqlite" - _ "modernc.org/sqlite" +// Values for FolderVisibility. +const ( + FolderPublic FolderVisibility = "public" + FolderSemiPrivate = "semi-private" + FolderPrivate = "private" ) -//go:embed sql/*.sql -var fs embed.FS +// FolderCreateOptions are a list of folder options. +type FolderCreateOptions struct { + Always int + Brief int + Description string + Notify int + Owner string + Readnew int + Shownew int + System int + Expire int + Visibility FolderVisibility +} + +// CreateFolder creates a new folder. +func (s *Store) CreateFolder(name string, options FolderCreateOptions) error { + _, err := s.db.Exec( + `INSERT INTO folders + (name, always, brief, description, notify, owner, readnew, + shownew, system, expire, visibility) + VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, + name, + options.Always, + options.Brief, + options.Description, + options.Notify, + options.Owner, + options.Readnew, + options.Shownew, + options.System, + options.Expire, + options.Visibility, + ) + // TODO: process this error a bit more to give a better error message. + return err +} + +// FolderListOptions are a list of folder options. +type FolderListOptions struct { +} -// Store is the store for folders. -type Store struct { - user string - db *sqlx.DB +// FolderListRow are a list of folder options. +type FolderListRow struct { + Name string `db:"name"` + Count int `db:"count"` + Description string `db:"description"` } -// Open opens the folders database. -func Open(user string) (*Store, error) { - fdir := path.Join(xdg.DataHome, "BULLETIN") - err := os.MkdirAll(fdir, 0700) +// ListFolder creates a new folder. +func (s *Store) ListFolder(user string, _ FolderListOptions) ([]FolderListRow, error) { + // TODO: get message counts. + var rows *sqlx.Rows + var err error + if s.IsUserAdmin(user) { + rows, err = s.db.Queryx( + `SELECT name, 0 as count, description FROM folders + ORDER BY name`, + ) + } else { + // TODO: limit access. + rows, err = s.db.Queryx( + `SELECT f.name, count(m.id) as count, f.description FROM folders AS f + LEFT JOIN messages AS m ON f.name = m.folder + GROUP By f.name + ORDER BY f.name`) + } + flr := []FolderListRow{} if err != nil { - return nil, errors.New("bulletin directory problem") + // TODO: process this error a bit more to give a better error message. + return flr, err } - fdb := path.Join(fdir, "bboard.db") + for rows.Next() { + row := FolderListRow{} + err := rows.StructScan(&row) + if err != nil { + // TODO: process this error a bit more to give a better error message. + return flr, err + } + flr = append(flr, row) + } + return flr, nil +} + +// IsFolderOwner checks if a user is a folder owner. +func (s *Store) IsFolderOwner(name, user string) bool { + found := 0 + s.db.Get(&found, + `SELECT 1 FROM folders AS f LEFT JOIN co_owners AS c + ON f.name = c.folder + WHERE f.name = $1 (f.owner = '$2' OR c.OWNER = '$2')`, + name, user) + return found == 1 +} - // Run db migrations if needed. - sqldir, err := iofs.New(fs, "sql") +// DeleteFolder creates a new folder. +func (s *Store) DeleteFolder(name string) error { + results, err := s.db.Exec("DELETE FROM folders WHERE name=$1", name) + // TODO: process this error a bit more to give a better error message. if err != nil { - return nil, err + return err } - m, err := migrate.NewWithSourceInstance("iofs", sqldir, "sqlite://"+fdb+"?_pragma=foreign_keys(1)") + rows, err := results.RowsAffected() if err != nil { - return nil, err + return err } - err = m.Up() - if err != nil && err != migrate.ErrNoChange { - return nil, err + if rows == 0 { + return errors.New("No such folder found") } - m.Close() - - store := &Store{user: user} - store.db, err = sqlx.Connect("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)") - if err != nil { - return nil, errors.New("bulletin database problem") + if rows != 1 { + return fmt.Errorf("Unexpected number (%d) of folders removed", rows) } - return store, nil -} - -// Close closes the db backing the store. -func (fstore *Store) Close() { - fstore.db.Close() + return nil } diff --git a/folders/manage-folders.go b/folders/manage-folders.go deleted file mode 100644 index 8beaf58b7eca969165b011972630b007591dee37..0000000000000000000000000000000000000000 --- a/folders/manage-folders.go +++ /dev/null @@ -1,75 +0,0 @@ -// Package folders are all the routines and sql for managing folders. -package folders - -import ( - "errors" - "fmt" -) - -// FolderVisibility is the folder visibility level. -type FolderVisibility string - -// Values for FolderVisibility. -const ( - FolderPublic FolderVisibility = "public" - FolderSemiPrivate = "semi-private" - FolderPrivate = "private" -) - -// FolderOptions are a list of folder options. -type FolderOptions struct { - Always int - Brief int - Description string - Notify int - Owner string - Readnew int - Shownew int - System int - Expire int - Visibility FolderVisibility -} - -// CreateFolder creates a new folder. -func (s *Store) CreateFolder(name string, options FolderOptions) error { - _, err := s.db.Exec( - `INSERT INTO folders - (name, always, brief, description, notify, owner, readnew, - shownew, system, expire, visibility) - VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, - name, - options.Always, - options.Brief, - options.Description, - options.Notify, - options.Owner, - options.Readnew, - options.Shownew, - options.System, - options.Expire, - options.Visibility, - ) - // TODO: process this error a bit more to give a better error message. - return err -} - -// DeleteFolder creates a new folder. -func (s *Store) DeleteFolder(name string) error { - results, err := s.db.Exec("DELETE FROM folders WHERE name=$1", name) - // TODO: process this error a bit more to give a better error message. - if err != nil { - return err - } - rows, err := results.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return errors.New("No such folder found") - } - if rows != 1 { - return fmt.Errorf("Unexpected number (%d) of folders removed", rows) - } - return nil -} diff --git a/folders/sql/1_create_table.up.sql b/folders/sql/1_create_table.up.sql index 02b78c886f35f3f59b0c5e6159d98bbd65e10695..208bc9755639b0225d48440c5b9c8a14011df76a 100644 --- a/folders/sql/1_create_table.up.sql +++ b/folders/sql/1_create_table.up.sql @@ -2,6 +2,7 @@ CREATE TABLE users ( login VARCHAR(25) NOT NULL PRIMARY KEY, name VARCHAR(53) NOT NULL, admin INT DEFAULT 0, + disabled INT DEFAULT 0, create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, update_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL ) WITHOUT ROWID; @@ -99,3 +100,20 @@ CREATE TRIGGER co_owners_after_update_update_at BEGIN UPDATE co_owners SET update_at=CURRENT_TIMESTAMP WHERE folder=NEW.folder AND owner=NEW.owner; END; + +CREATE TABLE messages ( + id INT NOT NULL, + folder VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE, + author VARCHAR(25) REFERENCES users(login) ON UPDATE CASCADE, + subject VARCHAR(53) NOT NULL, + message TEXT NOT NULL, + permanent INT DEFAULT 0, + shutdown INT DEFAULT 0, + expiration TIMESTAMP NOT NULL, + create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + update_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY (id, folder) +) WITHOUT ROWID; +CREATE INDEX messages_idx_shutdown ON messages(shutdown); +CREATE INDEX messages_idx_expiration ON messages(expiration); + diff --git a/folders/users.go b/folders/users.go new file mode 100644 index 0000000000000000000000000000000000000000..e1b20ab01a4c96ff9245b4e2544cd470669a6788 --- /dev/null +++ b/folders/users.go @@ -0,0 +1,46 @@ +// Package folders are all the routines and sql for managing folders. +package folders + +import "errors" + +// User is the user structure. +type User struct { + Login string `db:"login"` + Name string `db:"name"` + Admin int `db:"admin"` + Disabled int `db:"disabled"` +} + +// GetUser gets a user. +func (s *Store) GetUser(login string) (*User, error) { + user := &User{} + err := s.db.Get(user, + "SELECT login, name, admin, disabled FROM users WHERE login = $1", + login) + return user, err +} + +// AddUser gets a user. +func (s *Store) AddUser(user User) error { + result, err := s.db.NamedExec( + `INSERT INTO users (login, name, admin) VALUES (:login, :name, :admin)`, + user) + if err != nil { + return err + } + rows, err := result.RowsAffected() + if err != nil { + return err + } + if rows != 1 { + return errors.New("Failed to add user") + } + return nil +} + +// IsUserAdmin checks if a user is an admin. +func (s *Store) IsUserAdmin(user string) bool { + found := 0 + s.db.Get(&found, "SELECT admin FROM users WHERE login = $1", user) + return found == 1 +} diff --git a/main.go b/main.go index caeb3111336ca16b504e799e53b45e62683adf39..87ef32a80af81ba9b2f9f8df4bab8218db0381cd 100644 --- a/main.go +++ b/main.go @@ -23,17 +23,40 @@ func main() { Name: "user", Aliases: []string{"u"}, Usage: "user to run bulletin as", - Value: "", Required: true, }, + &cli.StringFlag{ + Name: "batch", + Aliases: []string{"b"}, + Usage: "batch command", + }, }, Action: func(_ context.Context, cmd *cli.Command) error { user := cmd.String("user") + batch := cmd.String("batch") + + if batch != "" { + if user != "SYSTEM" { + fmt.Println("ERROR: can only run batch commands as SYSTEM.") + os.Exit(1) + } + exitcode := 0 + switch batch { + case "after-boot": + fmt.Println("TODO: Delete messages with shutdown != 0.") + case "expire": + fmt.Println("TODO: expire messages.") + default: + fmt.Println("ERROR: can only run batch commands as SYSTEM.") + exitcode = 1 + } + os.Exit(exitcode) + } err := accounts.Open(user) if err != nil { return err } - err = repl.Loop(user) + err = repl.Loop() if err != nil { return err } @@ -43,6 +66,6 @@ func main() { err := cmd.Run(context.Background(), os.Args) if err != nil { - fmt.Printf("ERROR: %s.", err) + fmt.Printf("ERROR: %s.\n", err) } } diff --git a/repl/command.go b/repl/command.go index f913639f04d16faef1041d61ae7480c463542d95..c1ebcd1192bed96f2b1b3b4cc5e95830185f9f43 100644 --- a/repl/command.go +++ b/repl/command.go @@ -59,24 +59,11 @@ suppressed with /NOINDENT.`, Description: `/FOLDER=(foldername,[...]) Specifies the foldername into which the message is to be added. Does -not change the current selected folder. Folders can be either local or -remote folders. Thus, a nodename can precede the foldername (this -assumes that the remote node is capable of supporting this feature, i.e. -the BULLCP process is running on that node. If it is not, you will -receive an error message). If the the foldername is specified with only -a nodename, i.e. FOO::, the foldername is assumed to be GENERAL. NOTE: -Specifying remote nodes is only possible if that remote node is running -a special BULLCP process. If it isn't, the only way to add messages to -that remote node is via the /NODE command. However, /FOLDER is a much -quicker method, and much more versatile. +not change the current selected folder. You can specify logical names which translate to one or more folder names. I.e. $ DEFINE ALL_FOLDERS "VAX1,VAX2,VAX3", and then specify -ALL_FOLDERS after /FOLDER=. Note that the quotation marks are required. - -When using /FOLDER for remote nodes, proxy logins are used to determine -if privileged options are allowed. If they are not allowed, the message -will still be added, but without the privileged settings.`, +ALL_FOLDERS after /FOLDER=. Note that the quotation marks are required.`, OptArg: true, }, "/INDENT": { @@ -307,25 +294,7 @@ more information.)`, Description: `/DESCRIPTION=description Specifies the description of the folder, which is displayed using the -SHOW FOLDER command. If omitted, you are prompted for a description. - -If this folder is to receive messages from a network mailing list -via the BBOARD feature, and you wish to use the POST and RESPOND/LIST -commands, the address of the mailing list should be included in the -description. This is done by enclosing the address using <> and -placing it at the end of the description, i.e. - - INFOVAX MAILING LIST <INFO-VAX@KL.SRI.COM> - -If a mailer protocol is needs to be added to the network address in -order for it to be sent by VMS MAIL, i.e. protocol%"address", the -appropriate protocol can be specified by either hardcoding it into the -file BULLNEWS.INC before compiling BULLETIN, or by defining the system -logical name BULL_NEWS_MAILER (it is the same protocol used by the NEWS -feature in order to respond to NEWS messages). The default protocol is -IN%. If desired, you can specify the protocol with the address, i.e. - - INFOVAX MAILING LIST <IN%"INFO-VAX@KL.SRI.COM">`, +SHOW FOLDER command. If omitted, you are prompted for a description.`, OptArg: true, }, "/EXPIRE": { @@ -1246,16 +1215,13 @@ folders that have been created. Format: - SELECT [node-name::][folder-name] + SELECT [folder-name] The complete folder name need not be specified. BULLETIN will try to find the closest matching name. I.e. INFOV can be used for INFOVAX. Omitting the folder name will select the default general messages. -The node name can be specified only if the remote node has the special -BULLCP process running (invoked by BULLETIN/STARTUP command.) - After selecting a folder, the user will notified of the number of unread messages, and the message pointer will be placed at the first unread message.`, @@ -1380,9 +1346,6 @@ users must be at least 2. SET BBOARD [username] -BBOARD cannot be set for remote folders. See also the command SET -DIGEST for options on formatting BBOARD messages. - If BULLCP is running, BBOARD is updated every 15 minutes. If you want to length this period, define BULL_BBOARD_UPDATE to be the number of minutes, between updates. I.e. DEFINE/SYSTEM BULL_BBOARD_UPDATE "30" @@ -1601,7 +1564,7 @@ on that command for more information. Format: - SET FOLDER [node-name::][folder-name] + SET FOLDER [folder-name] 3 /MARKED Selects messages that have been marked (indicated by an asterisk). After using /MARKED, in order to see all messages, the folder will have diff --git a/repl/folders.go b/repl/folders.go index 4dbbdade011eed3434455b00a69ba1bcd97ff296..bfa3bdfb11bed7761f75b581f054cd80892622d4 100644 --- a/repl/folders.go +++ b/repl/folders.go @@ -5,35 +5,30 @@ import ( "errors" "fmt" "strconv" - "strings" "git.lyda.ie/kevin/bulletin/accounts" "git.lyda.ie/kevin/bulletin/dclish" "git.lyda.ie/kevin/bulletin/folders" ) -// ActionDirectory handles the `DIRECTORY` command. This lists all the -// messages in the current folder. -func ActionDirectory(cmd *dclish.Command) error { - // TODO: flag parsing. - if len(cmd.Args) == 1 { - folder := strings.ToUpper(cmd.Args[0]) - // TODO: Check folder is valid. - accounts.User.CurrentFolder = folder - } - fmt.Println("TODO: List messages in folder") - return nil -} - // ActionIndex handles the `INDEX` command. This lists all the folders. -func ActionIndex(cmd *dclish.Command) error { - fmt.Printf("TODO: implement INDEX:\n%s\n\n", cmd.Description) +func ActionIndex(_ *dclish.Command) error { + options := folders.FolderListOptions{} + rows, err := accounts.User.Folders.ListFolder(accounts.User.Login, options) + if err != nil { + return err + } + fmt.Println("The following folders are present") + fmt.Println("Name Count Description") + for _, row := range rows { + fmt.Printf("%-25s %5d %s\n", row.Name, row.Count, row.Description) + } return nil } // ActionCreate handles the `CREATE` command. This creates a folder. func ActionCreate(cmd *dclish.Command) error { - options := folders.FolderOptions{} + options := folders.FolderCreateOptions{} if cmd.Flags["/ALWAYS"].Value == "true" { options.Always = 1 } @@ -41,6 +36,7 @@ func ActionCreate(cmd *dclish.Command) error { options.Brief = 1 } if cmd.Flags["/DESCRIPTION"].Value == "" { + // TODO: prompt the user for a description. return errors.New("Description is required - use /DESCRIPTION") } options.Description = cmd.Flags["/DESCRIPTION"].Value @@ -50,7 +46,7 @@ func ActionCreate(cmd *dclish.Command) error { if cmd.Flags["/OWNER"].Value != "" { options.Owner = cmd.Flags["/OWNER"].Value } else { - options.Owner = accounts.User.Account + options.Owner = accounts.User.Login } if cmd.Flags["/READNEW"].Value == "true" { options.Readnew = 1 diff --git a/repl/help.go b/repl/help.go index e7b6ae555d0cfbfc16f51a76b3558631a1d822ac..52a0a8cd95e7ce231ae1008aaabf6933ae3caf39 100644 --- a/repl/help.go +++ b/repl/help.go @@ -84,7 +84,7 @@ func init() { } sort.Strings(flgs) for i := range flgs { - fmt.Fprintf(buf, "\n%s %s", flgs[i], commands[c].Flags[flgs[i]].Description) + fmt.Fprintf(buf, "\n\n%s %s", flgs[i], commands[c].Flags[flgs[i]].Description) } } helpmap[c] = buf.String() diff --git a/repl/messages.go b/repl/messages.go new file mode 100644 index 0000000000000000000000000000000000000000..213f6b3556eb65bece1f668498fb153d50ed7f2a --- /dev/null +++ b/repl/messages.go @@ -0,0 +1,23 @@ +// Package repl implements the main event loop. +package repl + +import ( + "fmt" + "strings" + + "git.lyda.ie/kevin/bulletin/accounts" + "git.lyda.ie/kevin/bulletin/dclish" +) + +// ActionDirectory handles the `DIRECTORY` command. This lists all the +// messages in the current folder. +func ActionDirectory(cmd *dclish.Command) error { + // TODO: flag parsing. + if len(cmd.Args) == 1 { + folder := strings.ToUpper(cmd.Args[0]) + // TODO: Check folder is valid. + accounts.User.CurrentFolder = folder + } + fmt.Println("TODO: List messages in folder") + return nil +} diff --git a/repl/repl.go b/repl/repl.go index fea8113eb9b94f495449022a4acbec5b5d1e9029..88e97a4e41e30ab6321da51f00f94da5ababae01 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -5,17 +5,19 @@ import ( "fmt" "path" + "git.lyda.ie/kevin/bulletin/accounts" "github.com/adrg/xdg" "github.com/chzyer/readline" ) // Loop is the main event loop. -func Loop(user string) error { - fmt.Printf("TODO: get config for user %s using xdg.\n", user) +func Loop() error { + // fmt.Printf("TODO: get config for user %s using xdg.\n", user) rl, err := readline.NewEx( &readline.Config{ - Prompt: "BULLETIN> ", - HistoryFile: path.Join(xdg.ConfigHome, "BULLETIN", fmt.Sprintf("%s.history", user)), + Prompt: "BULLETIN> ", + HistoryFile: path.Join(xdg.ConfigHome, "BULLETIN", + fmt.Sprintf("%s.history", accounts.User.Login)), // TODO: AutoComplete: completer, InterruptPrompt: "^C", EOFPrompt: "EXIT", @@ -26,6 +28,19 @@ func Loop(user string) error { } defer rl.Close() + // TODO: Remove once commands are implemented. + unimplemented := 0 + total := 0 + for c := range commands { + if commands[c].Action == nil { + unimplemented++ + } + total++ + } + fmt.Printf("TODO: %d out of %d commands still to be implemented.\n", + unimplemented, total) + // TODO: END + for { line, err := rl.Readline() if err != nil {