Loading NOTES.md +9 −9 Original line number Diff line number Diff line Loading @@ -30,14 +30,10 @@ repl.commands? * Run [godoc](http://localhost:6060/) and then review where the help text is lacking. * ~~Move to a storage layer.~~ * this.Folder should be a storage.Folder. * Implement batch jobs * ~~Have install populate the database with some test data.~~ * ~~reboot~~ * ~~expire~~ * Implement each command. * Next: folder commands - ~~CREATE~~, ~~REMOVE~~, MODIFY, ~~INDEX~~, ~~SELECT~~ * Messages: ~~ADD~~, CURRENT, ~~DIRECTORY~~, BACK, CHANGE, FIRST, REMOVE, ~~NEXT~~, ~~READ~~ * Messages: ~~ADD~~, ~~CURRENT~~, ~~DIRECTORY~~, ~~BACK~~, CHANGE, ~~FIRST~~, ~~NEXT~~, ~~READ~~, DELETE * Messages edit: CHANGE, REPLY, FORWARD * Moving messages: COPY, MOVE * Compound commands: SET and SHOW - make HELP work for them. Loading @@ -53,15 +49,12 @@ repl.commands? * trigger to limit values for 'visibility'? * Add some of the early announcements from the sources - see the conversion branch - to the GENERAL folder. * Add a pager * 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. * ~~SHOW VERSION~~ * Check db version; notify user if it changes; refuse to write to db if it has. Done: Loading @@ -72,6 +65,13 @@ Done: * Using giu, a [text-editor](https://serge-hulne.medium.com/coding-a-simple-text-editor-in-go-using-giu-quick-and-dirty-b9b97ab41e4a) (needs cgo, no) * [bubbletea](https://github.com/charmbracelet/bubbletea) seems to be the tui that's winning * Another option is tview - [simpler](https://github.com/rivo/tview). * ~~Implement batch jobs~~ * ~~Have install populate the database with some test data.~~ * ~~reboot~~ * ~~expire~~ * ~~Add a pager~~ * ~~SHOW VERSION~~ * Check db version; notify user if it changes; refuse to write to db if it has. ## Module links Loading folders/messages.go +56 −0 Original line number Diff line number Diff line Loading @@ -60,6 +60,34 @@ func ReadMessage(login, folder string, msgid int64) (*storage.Message, error) { return &msg, nil } // MarkSeen marks a list of messages as seen. func MarkSeen(msgids []int64) error { ctx := storage.Context() for _, msgid := range msgids { this.Q.SetMessageSeen(ctx, storage.SetMessageSeenParams{ Login: this.User.Login, Folder: this.Folder, Msgid: msgid, }) } return nil } // MarkUnseen marks a list of messages as unseen. func MarkUnseen(msgids []int64) error { ctx := storage.Context() for _, msgid := range msgids { this.Q.UnsetMessageSeen(ctx, storage.UnsetMessageSeenParams{ Login: this.User.Login, Folder: this.Folder, Msgid: msgid, }) } return nil } // NextMsgid gets the next message id. func NextMsgid(login, folder string, msgid int64) int64 { ctx := storage.Context() Loading @@ -74,6 +102,34 @@ func NextMsgid(login, folder string, msgid int64) int64 { return newid } // PrevMsgid gets the next message id. func PrevMsgid(login, folder string, msgid int64) int64 { ctx := storage.Context() newid, err := this.Q.PrevMsgid(ctx, storage.PrevMsgidParams{ Folder: folder, Login: login, ID: msgid, }) if err != nil { return 0 } return newid } // FirstMessage gets the first message in a folder. func FirstMessage(folder string) int64 { ctx := storage.Context() first, err := this.Q.NextMsgidIgnoringSeen(ctx, storage.NextMsgidIgnoringSeenParams{ Folder: folder, ID: 0, }) if err != nil { return 0 } return first } // ListMessages lists messages. func ListMessages(folder string) ([]storage.Message, error) { ctx := storage.Context() Loading repl/args.go 0 → 100644 +43 −0 Original line number Diff line number Diff line package repl import ( "fmt" "strconv" "strings" ) // ParseNumberList takes a string with a number list // like "1,8,3-6,10,20-30" and returns a number list like // []int64{1,8,3,4,5,6,10,20,21,...,30}. Well, also an error since // somethings can go wrong here. func ParseNumberList(input string) ([]int64, error) { var result []int64 segments := strings.Split(input, ",") for _, segment := range segments { segment = strings.TrimSpace(segment) if strings.Contains(segment, "-") { // Handle range parts := strings.SplitN(segment, "-", 2) if len(parts) != 2 { return nil, fmt.Errorf("invalid range format: %s", segment) } start, err1 := strconv.ParseInt(parts[0], 10, 64) end, err2 := strconv.ParseInt(parts[1], 10, 64) if err1 != nil || err2 != nil || start > end { return nil, fmt.Errorf("invalid range: %s", segment) } for i := start; i <= end; i++ { result = append(result, i) } } else { // Handle single number num, err := strconv.ParseInt(segment, 10, 64) if err != nil { return nil, fmt.Errorf("invalid number: %s", segment) } result = append(result, num) } } return result, nil } repl/command.go +2 −2 Original line number Diff line number Diff line Loading @@ -1139,16 +1139,16 @@ automatically be set as being SEEN when they are read. If you have used the SEEN command and wish to disable the automatic marking of messages in regular folders as SEEN when they are read, type the command SEEN/NOREAD. To reenable, simply use the SEEN command again.`, MinArgs: 1, MaxArgs: 1, Action: ActionSeen, }, "UNSEEN": { Description: `Sets the current or message-id message as unseen. Format: UNSEEN [message-number or numbers]`, MinArgs: 1, MaxArgs: 1, Action: ActionUnseen, }, "SELECT": { Description: `Selects a folder of messages. See HELP Folders for a description of a Loading repl/messages.go +76 −12 Original line number Diff line number Diff line Loading @@ -148,14 +148,36 @@ func ActionAdd(cmd *dclish.Command) error { } // ActionCurrent handles the `CURRENT` command. func ActionCurrent(cmd *dclish.Command) error { fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description) func ActionCurrent(_ *dclish.Command) error { // TODO: handle flags. msg, err := folders.ReadMessage(this.User.Login, this.Folder, this.MsgID) if err != nil { return err } lines := strings.Split(msg.String(), "\n") if len(lines) > 10 { lines = lines[:10] } fmt.Printf("%s\n", strings.Join(lines, "\n")) return nil } // ActionBack handles the `BACK` command. func ActionBack(cmd *dclish.Command) error { fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description) func ActionBack(_ *dclish.Command) error { // TODO: handle flags. msgid := folders.PrevMsgid(this.User.Login, this.Folder, this.MsgID) if msgid == 0 { fmt.Println("No previous messages") return nil } msg, err := folders.ReadMessage(this.User.Login, this.Folder, msgid) if err != nil { return err } // TODO: pager needs to report if the whole message was read // and only increment if not. pager.Pager(msg.String()) this.MsgID = msgid return nil } Loading @@ -166,15 +188,24 @@ func ActionChange(cmd *dclish.Command) error { } // ActionFirst handles the `FIRST` command. func ActionFirst(cmd *dclish.Command) error { fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description) func ActionFirst(_ *dclish.Command) error { msgid := folders.FirstMessage(this.Folder) if msgid == 0 { fmt.Println("No messages in folder") return nil } this.MsgID = msgid return nil } // ActionNext handles the `NEXT` command. func ActionNext(cmd *dclish.Command) error { func ActionNext(_ *dclish.Command) error { // TODO: handle flags. msgid := max(folders.NextMsgid(this.User.Login, this.Folder, this.MsgID), this.MsgID) msgid := folders.NextMsgid(this.User.Login, this.Folder, this.MsgID) if msgid == 0 { fmt.Println("No next messages") return nil } msg, err := folders.ReadMessage(this.User.Login, this.Folder, msgid) if err != nil { return err Loading @@ -182,8 +213,7 @@ func ActionNext(cmd *dclish.Command) error { // TODO: pager needs to report if the whole message was read // and only increment if not. pager.Pager(msg.String()) msgid = folders.NextMsgid(this.User.Login, this.Folder, msgid) this.MsgID = max(this.MsgID, msgid) this.MsgID = msgid return nil } Loading @@ -192,11 +222,11 @@ func ActionRead(cmd *dclish.Command) error { // TODO: handle flags. msgid := this.MsgID if len(cmd.Args) == 1 { id, err := strconv.Atoi(cmd.Args[0]) var err error msgid, err = strconv.ParseInt(cmd.Args[0], 10, 64) if err != nil { return err } msgid = int64(id) } msg, err := folders.ReadMessage(this.User.Login, this.Folder, msgid) if err != nil { Loading @@ -221,3 +251,37 @@ func ActionForward(cmd *dclish.Command) error { fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description) return nil } // ActionSeen handles the `SEEN` command. func ActionSeen(cmd *dclish.Command) error { var err error msgids := []int64{this.MsgID} if len(cmd.Args) == 1 { msgids, err = ParseNumberList(cmd.Args[0]) if err != nil { return err } } err = folders.MarkSeen(msgids) if err != nil { fmt.Printf("ERROR: %s.\n", err) } return nil } // ActionUnseen handles the `UNSEEN` command. func ActionUnseen(cmd *dclish.Command) error { var err error msgids := []int64{this.MsgID} if len(cmd.Args) == 1 { msgids, err = ParseNumberList(cmd.Args[0]) if err != nil { return err } } err = folders.MarkUnseen(msgids) if err != nil { fmt.Printf("ERROR: %s.\n", err) } return nil } Loading
NOTES.md +9 −9 Original line number Diff line number Diff line Loading @@ -30,14 +30,10 @@ repl.commands? * Run [godoc](http://localhost:6060/) and then review where the help text is lacking. * ~~Move to a storage layer.~~ * this.Folder should be a storage.Folder. * Implement batch jobs * ~~Have install populate the database with some test data.~~ * ~~reboot~~ * ~~expire~~ * Implement each command. * Next: folder commands - ~~CREATE~~, ~~REMOVE~~, MODIFY, ~~INDEX~~, ~~SELECT~~ * Messages: ~~ADD~~, CURRENT, ~~DIRECTORY~~, BACK, CHANGE, FIRST, REMOVE, ~~NEXT~~, ~~READ~~ * Messages: ~~ADD~~, ~~CURRENT~~, ~~DIRECTORY~~, ~~BACK~~, CHANGE, ~~FIRST~~, ~~NEXT~~, ~~READ~~, DELETE * Messages edit: CHANGE, REPLY, FORWARD * Moving messages: COPY, MOVE * Compound commands: SET and SHOW - make HELP work for them. Loading @@ -53,15 +49,12 @@ repl.commands? * trigger to limit values for 'visibility'? * Add some of the early announcements from the sources - see the conversion branch - to the GENERAL folder. * Add a pager * 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. * ~~SHOW VERSION~~ * Check db version; notify user if it changes; refuse to write to db if it has. Done: Loading @@ -72,6 +65,13 @@ Done: * Using giu, a [text-editor](https://serge-hulne.medium.com/coding-a-simple-text-editor-in-go-using-giu-quick-and-dirty-b9b97ab41e4a) (needs cgo, no) * [bubbletea](https://github.com/charmbracelet/bubbletea) seems to be the tui that's winning * Another option is tview - [simpler](https://github.com/rivo/tview). * ~~Implement batch jobs~~ * ~~Have install populate the database with some test data.~~ * ~~reboot~~ * ~~expire~~ * ~~Add a pager~~ * ~~SHOW VERSION~~ * Check db version; notify user if it changes; refuse to write to db if it has. ## Module links Loading
folders/messages.go +56 −0 Original line number Diff line number Diff line Loading @@ -60,6 +60,34 @@ func ReadMessage(login, folder string, msgid int64) (*storage.Message, error) { return &msg, nil } // MarkSeen marks a list of messages as seen. func MarkSeen(msgids []int64) error { ctx := storage.Context() for _, msgid := range msgids { this.Q.SetMessageSeen(ctx, storage.SetMessageSeenParams{ Login: this.User.Login, Folder: this.Folder, Msgid: msgid, }) } return nil } // MarkUnseen marks a list of messages as unseen. func MarkUnseen(msgids []int64) error { ctx := storage.Context() for _, msgid := range msgids { this.Q.UnsetMessageSeen(ctx, storage.UnsetMessageSeenParams{ Login: this.User.Login, Folder: this.Folder, Msgid: msgid, }) } return nil } // NextMsgid gets the next message id. func NextMsgid(login, folder string, msgid int64) int64 { ctx := storage.Context() Loading @@ -74,6 +102,34 @@ func NextMsgid(login, folder string, msgid int64) int64 { return newid } // PrevMsgid gets the next message id. func PrevMsgid(login, folder string, msgid int64) int64 { ctx := storage.Context() newid, err := this.Q.PrevMsgid(ctx, storage.PrevMsgidParams{ Folder: folder, Login: login, ID: msgid, }) if err != nil { return 0 } return newid } // FirstMessage gets the first message in a folder. func FirstMessage(folder string) int64 { ctx := storage.Context() first, err := this.Q.NextMsgidIgnoringSeen(ctx, storage.NextMsgidIgnoringSeenParams{ Folder: folder, ID: 0, }) if err != nil { return 0 } return first } // ListMessages lists messages. func ListMessages(folder string) ([]storage.Message, error) { ctx := storage.Context() Loading
repl/args.go 0 → 100644 +43 −0 Original line number Diff line number Diff line package repl import ( "fmt" "strconv" "strings" ) // ParseNumberList takes a string with a number list // like "1,8,3-6,10,20-30" and returns a number list like // []int64{1,8,3,4,5,6,10,20,21,...,30}. Well, also an error since // somethings can go wrong here. func ParseNumberList(input string) ([]int64, error) { var result []int64 segments := strings.Split(input, ",") for _, segment := range segments { segment = strings.TrimSpace(segment) if strings.Contains(segment, "-") { // Handle range parts := strings.SplitN(segment, "-", 2) if len(parts) != 2 { return nil, fmt.Errorf("invalid range format: %s", segment) } start, err1 := strconv.ParseInt(parts[0], 10, 64) end, err2 := strconv.ParseInt(parts[1], 10, 64) if err1 != nil || err2 != nil || start > end { return nil, fmt.Errorf("invalid range: %s", segment) } for i := start; i <= end; i++ { result = append(result, i) } } else { // Handle single number num, err := strconv.ParseInt(segment, 10, 64) if err != nil { return nil, fmt.Errorf("invalid number: %s", segment) } result = append(result, num) } } return result, nil }
repl/command.go +2 −2 Original line number Diff line number Diff line Loading @@ -1139,16 +1139,16 @@ automatically be set as being SEEN when they are read. If you have used the SEEN command and wish to disable the automatic marking of messages in regular folders as SEEN when they are read, type the command SEEN/NOREAD. To reenable, simply use the SEEN command again.`, MinArgs: 1, MaxArgs: 1, Action: ActionSeen, }, "UNSEEN": { Description: `Sets the current or message-id message as unseen. Format: UNSEEN [message-number or numbers]`, MinArgs: 1, MaxArgs: 1, Action: ActionUnseen, }, "SELECT": { Description: `Selects a folder of messages. See HELP Folders for a description of a Loading
repl/messages.go +76 −12 Original line number Diff line number Diff line Loading @@ -148,14 +148,36 @@ func ActionAdd(cmd *dclish.Command) error { } // ActionCurrent handles the `CURRENT` command. func ActionCurrent(cmd *dclish.Command) error { fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description) func ActionCurrent(_ *dclish.Command) error { // TODO: handle flags. msg, err := folders.ReadMessage(this.User.Login, this.Folder, this.MsgID) if err != nil { return err } lines := strings.Split(msg.String(), "\n") if len(lines) > 10 { lines = lines[:10] } fmt.Printf("%s\n", strings.Join(lines, "\n")) return nil } // ActionBack handles the `BACK` command. func ActionBack(cmd *dclish.Command) error { fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description) func ActionBack(_ *dclish.Command) error { // TODO: handle flags. msgid := folders.PrevMsgid(this.User.Login, this.Folder, this.MsgID) if msgid == 0 { fmt.Println("No previous messages") return nil } msg, err := folders.ReadMessage(this.User.Login, this.Folder, msgid) if err != nil { return err } // TODO: pager needs to report if the whole message was read // and only increment if not. pager.Pager(msg.String()) this.MsgID = msgid return nil } Loading @@ -166,15 +188,24 @@ func ActionChange(cmd *dclish.Command) error { } // ActionFirst handles the `FIRST` command. func ActionFirst(cmd *dclish.Command) error { fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description) func ActionFirst(_ *dclish.Command) error { msgid := folders.FirstMessage(this.Folder) if msgid == 0 { fmt.Println("No messages in folder") return nil } this.MsgID = msgid return nil } // ActionNext handles the `NEXT` command. func ActionNext(cmd *dclish.Command) error { func ActionNext(_ *dclish.Command) error { // TODO: handle flags. msgid := max(folders.NextMsgid(this.User.Login, this.Folder, this.MsgID), this.MsgID) msgid := folders.NextMsgid(this.User.Login, this.Folder, this.MsgID) if msgid == 0 { fmt.Println("No next messages") return nil } msg, err := folders.ReadMessage(this.User.Login, this.Folder, msgid) if err != nil { return err Loading @@ -182,8 +213,7 @@ func ActionNext(cmd *dclish.Command) error { // TODO: pager needs to report if the whole message was read // and only increment if not. pager.Pager(msg.String()) msgid = folders.NextMsgid(this.User.Login, this.Folder, msgid) this.MsgID = max(this.MsgID, msgid) this.MsgID = msgid return nil } Loading @@ -192,11 +222,11 @@ func ActionRead(cmd *dclish.Command) error { // TODO: handle flags. msgid := this.MsgID if len(cmd.Args) == 1 { id, err := strconv.Atoi(cmd.Args[0]) var err error msgid, err = strconv.ParseInt(cmd.Args[0], 10, 64) if err != nil { return err } msgid = int64(id) } msg, err := folders.ReadMessage(this.User.Login, this.Folder, msgid) if err != nil { Loading @@ -221,3 +251,37 @@ func ActionForward(cmd *dclish.Command) error { fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description) return nil } // ActionSeen handles the `SEEN` command. func ActionSeen(cmd *dclish.Command) error { var err error msgids := []int64{this.MsgID} if len(cmd.Args) == 1 { msgids, err = ParseNumberList(cmd.Args[0]) if err != nil { return err } } err = folders.MarkSeen(msgids) if err != nil { fmt.Printf("ERROR: %s.\n", err) } return nil } // ActionUnseen handles the `UNSEEN` command. func ActionUnseen(cmd *dclish.Command) error { var err error msgids := []int64{this.MsgID} if len(cmd.Args) == 1 { msgids, err = ParseNumberList(cmd.Args[0]) if err != nil { return err } } err = folders.MarkUnseen(msgids) if err != nil { fmt.Printf("ERROR: %s.\n", err) } return nil }