From 95b8a958fd7450578f4e685d6a3ae948f5a69027 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 31 Aug 2020 01:08:07 +0300 Subject: [PATCH] Add password modal and file path autocompletion --- ui/crypto-commands.go | 71 ++++++++++++++++++---- ui/password-modal.go | 133 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 ui/password-modal.go diff --git a/ui/crypto-commands.go b/ui/crypto-commands.go index 8bd7ff5..8a9fbb1 100644 --- a/ui/crypto-commands.go +++ b/ui/crypto-commands.go @@ -21,6 +21,7 @@ package ui import ( "fmt" "io/ioutil" + "path/filepath" "strings" "time" "unicode" @@ -232,21 +233,57 @@ func cmdResetSession(cmd *Command) { } func autocompleteFile(cmd *CommandAutocomplete) (completions []string, newText string) { - // TODO implement - return []string{}, "" + inputPath, err := filepath.Abs(cmd.RawArgs) + if err != nil { + return + } + + var searchNamePrefix, searchDir string + if strings.HasSuffix(cmd.RawArgs, "/") { + searchDir = inputPath + } else { + searchNamePrefix = filepath.Base(inputPath) + searchDir = filepath.Dir(inputPath) + } + files, err := ioutil.ReadDir(searchDir) + if err != nil { + return + } + for _, file := range files { + name := file.Name() + if !strings.HasPrefix(name, searchNamePrefix) || (name[0] == '.' && searchNamePrefix == "") { + continue + } + fullPath := filepath.Join(searchDir, name) + if file.IsDir() { + fullPath += "/" + } + completions = append(completions, fullPath) + } + if len(completions) == 1 { + newText = fmt.Sprintf("/%s %s", cmd.OrigCommand, completions[0]) + } + return } -// TODO add dialog for asking passphrase -const extremelyTemporaryHardcodedPassphrase = "gomuks" - func cmdImportKeys(cmd *Command) { - data, err := ioutil.ReadFile(cmd.RawArgs) + path, err := filepath.Abs(cmd.RawArgs) if err != nil { - cmd.Reply("Failed to read %s: %v", cmd.RawArgs, err) + cmd.Reply("Failed to get absolute path: %v", err) + return + } + data, err := ioutil.ReadFile(path) + if err != nil { + cmd.Reply("Failed to read %s: %v", path, err) + return + } + passphrase, ok := cmd.MainView.AskPassword("Key import", false) + if !ok { + cmd.Reply("Passphrase entry cancelled") return } mach := cmd.Matrix.Crypto().(*crypto.OlmMachine) - imported, total, err := mach.ImportKeys(extremelyTemporaryHardcodedPassphrase, data) + imported, total, err := mach.ImportKeys(passphrase, data) if err != nil { cmd.Reply("Failed to import sessions: %v", err) } else { @@ -255,15 +292,25 @@ func cmdImportKeys(cmd *Command) { } func exportKeys(cmd *Command, sessions []*crypto.InboundGroupSession) { - export, err := crypto.ExportKeys(extremelyTemporaryHardcodedPassphrase, sessions) + path, err := filepath.Abs(cmd.RawArgs) + if err != nil { + cmd.Reply("Failed to get absolute path: %v", err) + return + } + passphrase, ok := cmd.MainView.AskPassword("Key export", true) + if !ok { + cmd.Reply("Passphrase entry cancelled") + return + } + export, err := crypto.ExportKeys(passphrase, sessions) if err != nil { cmd.Reply("Failed to export sessions: %v", err) } - err = ioutil.WriteFile(cmd.RawArgs, export, 0400) + err = ioutil.WriteFile(path, export, 0400) if err != nil { - cmd.Reply("Failed to write sessions to %s: %v", cmd.RawArgs, err) + cmd.Reply("Failed to write sessions to %s: %v", path, err) } else { - cmd.Reply("Successfully exported %d sessions to %s", len(sessions), cmd.RawArgs) + cmd.Reply("Successfully exported %d sessions to %s", len(sessions), path) } } diff --git a/ui/password-modal.go b/ui/password-modal.go new file mode 100644 index 0000000..afda7b5 --- /dev/null +++ b/ui/password-modal.go @@ -0,0 +1,133 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2020 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ui + +import ( + "maunium.net/go/mauview" + "maunium.net/go/tcell" +) + +type PasswordModal struct { + mauview.Component + + outputChan chan string + cancelChan chan struct{} + + form *mauview.Form + + text *mauview.TextField + confirmText *mauview.TextField + + input *mauview.InputField + confirmInput *mauview.InputField + + cancel *mauview.Button + submit *mauview.Button + + parent *MainView +} + +func (view *MainView) AskPassword(title string, isNew bool) (string, bool) { + pwm := NewPasswordModal(view, title, isNew) + view.ShowModal(pwm) + return pwm.Wait() +} + +func NewPasswordModal(parent *MainView, title string, isNew bool) *PasswordModal { + pwm := &PasswordModal{ + parent: parent, + form: mauview.NewForm(), + outputChan: make(chan string, 1), + cancelChan: make(chan struct{}, 1), + } + + pwm.form. + SetColumns([]int{1, 20, 1, 20, 1}). + SetRows([]int{1, 1, 1, 0, 0, 0, 1, 1, 1}) + + width := 45 + height := 8 + + pwm.text = mauview.NewTextField() + if isNew { + pwm.text.SetText("Create a passphrase") + } else { + pwm.text.SetText("Enter the passphrase") + } + pwm.input = mauview.NewInputField(). + SetMaskCharacter('*'). + SetPlaceholder("correct horse battery staple") + pwm.form.AddComponent(pwm.text, 1, 1, 3, 1) + pwm.form.AddFormItem(pwm.input, 1, 2, 3, 1) + + if isNew { + height += 3 + pwm.confirmInput = mauview.NewInputField(). + SetMaskCharacter('*'). + SetPlaceholder("correct horse battery staple"). + SetChangedFunc(pwm.HandleChange) + pwm.input.SetChangedFunc(pwm.HandleChange) + pwm.confirmText = mauview.NewTextField().SetText("Confirm passphrase") + + pwm.form.SetRow(3, 1).SetRow(4, 1).SetRow(5, 1) + pwm.form.AddComponent(pwm.confirmText, 1, 4, 3, 1) + pwm.form.AddFormItem(pwm.confirmInput, 1, 5, 3, 1) + } + + pwm.cancel = mauview.NewButton("Cancel").SetOnClick(pwm.ClickCancel) + pwm.submit = mauview.NewButton("Submit").SetOnClick(pwm.ClickSubmit) + + pwm.form.AddFormItem(pwm.submit, 3, 7, 1, 1) + pwm.form.AddFormItem(pwm.cancel, 1, 7, 1, 1) + + box := mauview.NewBox(pwm.form).SetTitle(title) + center := mauview.Center(box, width, height).SetAlwaysFocusChild(true) + center.Focus() + pwm.form.FocusNextItem() + pwm.Component = center + + return pwm +} + +func (pwm *PasswordModal) HandleChange(_ string) { + if pwm.input.GetText() == pwm.confirmInput.GetText() { + pwm.submit.SetBackgroundColor(mauview.Styles.ContrastBackgroundColor) + } else { + pwm.submit.SetBackgroundColor(tcell.ColorDefault) + } +} + +func (pwm *PasswordModal) ClickCancel() { + pwm.parent.HideModal() + pwm.cancelChan <- struct{}{} +} + +func (pwm *PasswordModal) ClickSubmit() { + if pwm.confirmInput == nil || pwm.input.GetText() == pwm.confirmInput.GetText() { + pwm.parent.HideModal() + pwm.outputChan <- pwm.input.GetText() + } +} + +func (pwm *PasswordModal) Wait() (string, bool) { + select { + case result := <- pwm.outputChan: + return result, true + case <- pwm.cancelChan: + return "", false + } +}