From 9d56f8eed24550c4d822136c39e3b750a60de8b9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 24 Aug 2022 12:23:11 +0530 Subject: [PATCH] Use TUI to build a nice password read control --- tools/tui/loop.go | 44 +++++++++++++++++++++ tools/tui/password.go | 90 +++++++++++++++++++++++++++++++++++++++++++ tools/tui/signal.go | 28 ++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 tools/tui/password.go diff --git a/tools/tui/loop.go b/tools/tui/loop.go index 57c74dc3a..4a111a7cf 100644 --- a/tools/tui/loop.go +++ b/tools/tui/loop.go @@ -5,6 +5,7 @@ import ( "io" "kitty/tools/tty" "os" + "syscall" "time" "kitty/tools/utils" @@ -120,6 +121,32 @@ func CreateLoop() (*Loop, error) { return &l, nil } +func (self *Loop) UseAlternateScreen() { + self.terminal_options.alternate_screen = true +} + +func (self *Loop) MouseTracking(mt MouseTracking) { + self.terminal_options.mouse_tracking = mt +} + +func (self *Loop) DeathSignalName() string { + if self.death_signal != SIGNULL { + return self.death_signal.String() + } + return "" +} + +func (self *Loop) KillIfSignalled() { + switch self.death_signal { + case SIGINT: + syscall.Kill(-1, syscall.SIGINT) + case SIGTERM: + syscall.Kill(-1, syscall.SIGTERM) + case SIGHUP: + syscall.Kill(-1, syscall.SIGHUP) + } +} + func (self *Loop) Run() (err error) { signal_read_file, signal_write_file, err := os.Pipe() if err != nil { @@ -229,6 +256,23 @@ func (self *Loop) queue_write_to_tty(data []byte) { self.write_buf = append(self.write_buf, data...) } +func (self *Loop) QueueWriteString(data string) { + self.queue_write_to_tty([]byte(data)) +} + +func (self *Loop) ExitCode() int { + return self.exit_code +} + +func (self *Loop) Beep() { + self.QueueWriteString("\a") +} + +func (self *Loop) Quit(exit_code int) { + self.exit_code = exit_code + self.keep_going = false +} + func (self *Loop) write_to_tty() error { if len(self.write_buf) == 0 || self.controlling_term == nil { return nil diff --git a/tools/tui/password.go b/tools/tui/password.go new file mode 100644 index 000000000..2f66e7b0a --- /dev/null +++ b/tools/tui/password.go @@ -0,0 +1,90 @@ +package tui + +import ( + "errors" + "fmt" + "strings" + + "github.com/mattn/go-runewidth" +) + +type KilledBySignal struct { + Msg string + SignalName string +} + +func (self *KilledBySignal) Error() string { return self.Msg } + +var Canceled = errors.New("Canceled by user") + +func ReadPassword(prompt string, kill_if_signaled bool) (password string, err error) { + loop, err := CreateLoop() + shadow := "" + if err != nil { + return + } + + loop.OnText = func(loop *Loop, text string, from_key_event bool, in_bracketed_paste bool) error { + old_width := runewidth.StringWidth(password) + password += text + new_width := runewidth.StringWidth(password) + if new_width > old_width { + extra := strings.Repeat("*", new_width-old_width) + loop.QueueWriteString(extra) + shadow += extra + } + return nil + } + + loop.OnKeyEvent = func(loop *Loop, event *KeyEvent) error { + if event.MatchesPressOrRepeat("backscape") || event.MatchesPressOrRepeat("delete") { + event.Handled = true + if len(password) > 0 { + old_width := runewidth.StringWidth(password) + password = password[:len(password)-1] + new_width := runewidth.StringWidth(password) + delta := new_width - old_width + if delta > 0 { + if delta > len(shadow) { + delta = len(shadow) + } + shadow = shadow[:len(shadow)-delta] + loop.QueueWriteString(strings.Repeat("\x08\x1b[P", delta)) + } + } else { + loop.Beep() + } + } + if event.MatchesPressOrRepeat("enter") || event.MatchesPressOrRepeat("return") { + event.Handled = true + if password == "" { + loop.Quit(1) + } else { + loop.Quit(0) + } + } + if event.MatchesPressOrRepeat("esc") { + event.Handled = true + loop.Quit(1) + return Canceled + } + return nil + } + + err = loop.Run() + if err != nil { + return + } + ds := loop.DeathSignalName() + if ds != "" { + if kill_if_signaled { + loop.KillIfSignalled() + return + } + return "", &KilledBySignal{Msg: fmt.Sprint("Killed by signal: ", ds), SignalName: ds} + } + if loop.ExitCode() != 0 { + password = "" + } + return password, nil +} diff --git a/tools/tui/signal.go b/tools/tui/signal.go index 8bdbff0e2..e82288b38 100644 --- a/tools/tui/signal.go +++ b/tools/tui/signal.go @@ -1,6 +1,7 @@ package tui import ( + "fmt" "os" "os/signal" "syscall" @@ -21,6 +22,33 @@ const ( SIGALRM Signal = 9 ) +func (self *Signal) String() string { + switch *self { + case SIGNULL: + return "SIGNULL" + case SIGINT: + return "SIGINT" + case SIGTERM: + return "SIGTERM" + case SIGTSTP: + return "SIGTSTP" + case SIGHUP: + return "SIGHUP" + case SIGTTIN: + return "SIGTTIN" + case SIGTTOU: + return "SIGTTOU" + case SIGUSR1: + return "SIGUSR1" + case SIGUSR2: + return "SIGUSR2" + case SIGALRM: + return "SIGALRM" + default: + return fmt.Sprintf("SIG#%s", *self) + } +} + func as_signal(which os.Signal) Signal { switch which { case os.Interrupt: