diff --git a/kittens/choose_files/main.go b/kittens/choose_files/main.go index ad74e3947..50ff67e7d 100644 --- a/kittens/choose_files/main.go +++ b/kittens/choose_files/main.go @@ -13,6 +13,7 @@ import ( "github.com/kovidgoyal/kitty/tools/config" "github.com/kovidgoyal/kitty/tools/tty" "github.com/kovidgoyal/kitty/tools/tui/loop" + "github.com/kovidgoyal/kitty/tools/tui/readline" "github.com/kovidgoyal/kitty/tools/utils" ) @@ -27,6 +28,13 @@ type ScorePattern struct { val float64 } +type Screen int + +const ( + NORMAL Screen = iota + SAVE_FILE +) + type Mode int const ( @@ -39,6 +47,14 @@ const ( SELECT_SAVE_DIR_FOR_FILES // select a dir for saving one or more pre-sent filenames, must be an existing one ) +func (m Mode) CanSelectNonExistent() bool { + switch m { + case SELECT_SAVE_FILE, SELECT_SAVE_DIR: + return true + } + return false +} + func (m Mode) AllowsMultipleSelection() bool { switch m { case SELECT_MULTIPLE_FILES, SELECT_MULTIPLE_DIRS: @@ -55,6 +71,14 @@ func (m Mode) OnlyDirs() bool { return false } +func (m Mode) SelectFiles() bool { + switch m { + case SELECT_SINGLE_FILE, SELECT_MULTIPLE_FILES, SELECT_SAVE_FILE: + return true + } + return false +} + func (m Mode) WindowTitle() string { switch m { case SELECT_SINGLE_FILE: @@ -76,15 +100,18 @@ func (m Mode) WindowTitle() string { } type State struct { - base_dir string - current_dir string - select_dirs bool - multiselect bool - score_patterns []ScorePattern - search_text string - mode Mode - window_title string + base_dir string + current_dir string + select_dirs bool + multiselect bool + score_patterns []ScorePattern + search_text string + mode Mode + suggested_save_file_name string + window_title string + screen Screen + save_file_cdir string selections []string current_idx int num_of_matches_at_last_render int @@ -124,9 +151,19 @@ func (s State) WindowTitle() string { } return s.window_title } -func (s *State) AddSelection(abspath string) { +func (s *State) AddSelection(abspath string) bool { if !slices.Contains(s.selections, abspath) { s.selections = append(s.selections, abspath) + return true + } + return false +} + +func (s *State) ToggleSelection(abspath string) { + before := len(s.selections) + s.selections = slices.DeleteFunc(s.selections, func(x string) bool { return x == abspath }) + if len(s.selections) == before { + s.selections = append(s.selections, abspath) } } @@ -139,20 +176,26 @@ type Handler struct { screen_size ScreenSize scan_cache ScanCache lp *loop.Loop + rl *readline.Readline } func (h *Handler) draw_screen() (err error) { - matches, in_progress := h.get_results() - h.lp.SetWindowTitle(h.state.WindowTitle()) h.lp.StartAtomicUpdate() defer h.lp.EndAtomicUpdate() h.lp.ClearScreen() - defer func() { // so that the cursor ends up in the right place - h.lp.MoveCursorTo(1, 1) - h.draw_search_bar(0) - }() - y := SEARCH_BAR_HEIGHT - y += h.draw_results(y, 2, matches, in_progress) + switch h.state.screen { + case NORMAL: + matches, in_progress := h.get_results() + h.lp.SetWindowTitle(h.state.WindowTitle()) + defer func() { // so that the cursor ends up in the right place + h.lp.MoveCursorTo(1, 1) + h.draw_search_bar(0) + }() + y := SEARCH_BAR_HEIGHT + y += h.draw_results(y, 2, matches, in_progress) + case SAVE_FILE: + err = h.draw_save_file_name_screen() + } return } @@ -174,6 +217,7 @@ func (h *Handler) init_sizes(new_size loop.ScreenSize) { h.screen_size.cell_height = int(new_size.CellHeight) h.screen_size.width_px = int(new_size.WidthPx) h.screen_size.height_px = int(new_size.HeightPx) + h.rl.ClearCachedScreenSize() } func (h *Handler) OnInitialize() (ans string, err error) { @@ -184,6 +228,7 @@ func (h *Handler) OnInitialize() (ans string, err error) { } h.lp.AllowLineWrapping(false) h.lp.SetCursorShape(loop.BAR_CURSOR, true) + h.lp.StartBracketedPaste() h.draw_screen() return } @@ -199,57 +244,119 @@ func (h *Handler) current_abspath() string { } -func (h *Handler) add_selection_if_possible() { +func (h *Handler) add_selection_if_possible() bool { m := h.current_abspath() if m != "" { - h.state.AddSelection(m) + return h.state.AddSelection(m) } - return + return false +} + +func (h *Handler) toggle_selection() bool { + m := h.current_abspath() + if m != "" { + h.state.ToggleSelection(m) + return true + } + return false +} + +func (h *Handler) change_to_current_dir_if_possible() error { + matches, in_progress := h.get_results() + if len(matches) > 0 && !in_progress { + m := h.current_abspath() + if st, err := os.Stat(m); err == nil { + if !st.IsDir() { + m = filepath.Dir(m) + } + h.state.SetCurrentDir(m) + return h.draw_screen() + } + } + h.lp.Beep() + return nil +} + +func (h *Handler) finish_selection() error { + if h.state.mode.CanSelectNonExistent() { + h.initialize_save_file_name() + return h.draw_screen() + } + h.lp.Quit(0) + return nil } func (h *Handler) OnKeyEvent(ev *loop.KeyEvent) (err error) { - switch { - case h.handle_edit_keys(ev), h.handle_result_list_keys(ev): - h.draw_screen() - case ev.MatchesPressOrRepeat("esc") || ev.MatchesPressOrRepeat("ctrl+c"): - h.lp.Quit(1) - case ev.MatchesPressOrRepeat("tab"): - matches, in_progress := h.get_results() - if len(matches) > 0 && !in_progress { - m := h.current_abspath() - if st, err := os.Stat(m); err == nil { - if !st.IsDir() { - m = filepath.Dir(m) + switch h.state.screen { + case NORMAL: + switch { + case h.handle_edit_keys(ev), h.handle_result_list_keys(ev): + h.draw_screen() + case ev.MatchesPressOrRepeat("esc") || ev.MatchesPressOrRepeat("ctrl+c"): + h.lp.Quit(1) + case ev.MatchesPressOrRepeat("tab"): + return h.change_to_current_dir_if_possible() + case ev.MatchesPressOrRepeat("shift+tab"): + curr := h.state.CurrentDir() + switch curr { + case "/": + case ".": + if curr, err = os.Getwd(); err == nil && curr != "/" { + h.state.SetCurrentDir(filepath.Dir(curr)) + return h.draw_screen() } - h.state.SetCurrentDir(m) - return h.draw_screen() - } - } - h.lp.Beep() - case ev.MatchesPressOrRepeat("shift+tab"): - curr := h.state.CurrentDir() - switch curr { - case "/": - case ".": - if curr, err = os.Getwd(); err == nil && curr != "/" { + default: h.state.SetCurrentDir(filepath.Dir(curr)) return h.draw_screen() } - default: - h.state.SetCurrentDir(filepath.Dir(curr)) - return h.draw_screen() + h.lp.Beep() + case ev.MatchesPressOrRepeat("shift+enter"): + if !h.toggle_selection() { + h.lp.Beep() + } else { + if len(h.state.selections) > 0 && !h.state.mode.AllowsMultipleSelection() { + return h.finish_selection() + } + return h.draw_screen() + } + case ev.MatchesPressOrRepeat("enter"): + if h.state.mode.SelectFiles() { + m := h.current_abspath() + var s os.FileInfo + if s, err = os.Stat(m); err != nil { + h.lp.Beep() + return nil + } + if s.IsDir() { + return h.change_to_current_dir_if_possible() + } + } + if h.add_selection_if_possible() { + if len(h.state.selections) > 0 { + return h.finish_selection() + } + return h.draw_screen() + } else { + h.lp.Beep() + } } - h.lp.Beep() - case ev.MatchesPressOrRepeat("enter"): - h.add_selection_if_possible() - h.lp.Quit(0) + case SAVE_FILE: + err = h.save_file_name_handle_key(ev) } return } func (h *Handler) OnText(text string, from_key_event, in_bracketed_paste bool) (err error) { - h.state.search_text += text - return h.draw_screen() + switch h.state.screen { + case NORMAL: + h.state.search_text += text + return h.draw_screen() + case SAVE_FILE: + if err = h.rl.OnText(text, from_key_event, in_bracketed_paste); err == nil { + err = h.draw_screen() + } + } + return } func mult(a, b float64) float64 { return a * b } @@ -296,6 +403,19 @@ func (h *Handler) set_state_from_config(conf *Config, opts *Options) (err error) default: h.state.mode = SELECT_SINGLE_FILE } + h.state.suggested_save_file_name = opts.SuggestedSaveFileName + if opts.SuggestedSaveFilePath != "" { + switch h.state.mode { + case SELECT_SAVE_FILE, SELECT_SAVE_DIR, SELECT_SAVE_DIR_FOR_FILES: + if s, err := os.Stat(opts.SuggestedSaveFilePath); err == nil { + if (s.IsDir() && h.state.mode != SELECT_SAVE_FILE) || (!s.IsDir() && h.state.mode == SELECT_SAVE_FILE) { + if h.state.AddSelection(opts.SuggestedSaveFileName) { + return h.finish_selection() + } + } + } + } + } return } @@ -310,7 +430,9 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) { if err != nil { return 1, err } - handler := Handler{lp: lp} + handler := Handler{lp: lp, rl: readline.New(lp, readline.RlInit{ + Prompt: "> ", ContinuationPrompt: ". ", + })} if err = handler.set_state_from_config(conf, opts); err != nil { return 1, err } diff --git a/kittens/choose_files/main.py b/kittens/choose_files/main.py index 165fc7465..f5a1d55af 100644 --- a/kittens/choose_files/main.py +++ b/kittens/choose_files/main.py @@ -53,6 +53,14 @@ type=choices choices=file,files,save-file,dir,save-dir,dirs,dir-for-files default=file The type of object(s) to select + + +--suggested-save-file-name +A suggested name when picking a save file. + + +--suggested-save-file-path +Path to an existing file to use as the save file. '''.format(config_help=CONFIG_HELP.format(conf_name='diff', appname=appname)).format diff --git a/kittens/choose_files/save-file.go b/kittens/choose_files/save-file.go new file mode 100644 index 000000000..b7c067af5 --- /dev/null +++ b/kittens/choose_files/save-file.go @@ -0,0 +1,72 @@ +package choose_files + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/kovidgoyal/kitty/tools/tui/loop" + "github.com/kovidgoyal/kitty/tools/utils" +) + +var _ = fmt.Print + +func (h *Handler) current_save_file_path() string { + ans := h.rl.AllText() + if ans != "" { + ans = utils.Expanduser(ans) + if !filepath.IsAbs(ans) { + ans = filepath.Join(h.state.save_file_cdir, ans) + } + } + return ans +} + +func (h *Handler) save_file_name_handle_key(ev *loop.KeyEvent) (err error) { + switch { + case ev.MatchesPressOrRepeat("esc") || ev.MatchesPressOrRepeat("ctrl+c"): + h.state.selections = nil + h.state.screen = NORMAL + err = h.draw_screen() + case ev.MatchesPressOrRepeat("enter"): + if p := h.current_save_file_path(); p != "" { + h.state.selections = []string{p} + h.lp.Quit(0) + } else { + h.lp.Beep() + } + default: + if err = h.rl.OnKeyEvent(ev); err == nil { + err = h.draw_screen() + } + } + return +} + +func (h *Handler) initialize_save_file_name() { + h.state.screen = SAVE_FILE + h.rl.ResetText() + cdir := h.state.CurrentDir() + fname := h.state.suggested_save_file_name + if len(h.state.selections) > 0 { + if q, err := filepath.Abs(h.state.selections[0]); err == nil { + if s, err := os.Stat(q); err == nil { + if s.IsDir() && h.state.mode.OnlyDirs() { + cdir = filepath.Dir(q) + fname = filepath.Base(q) + } + } + } + } + h.rl.SetText(fname) + h.state.save_file_cdir = cdir +} + +func (h *Handler) draw_save_file_name_screen() (err error) { + desc := utils.IfElse(h.state.mode == SELECT_SAVE_FILE, "file", "directory") + h.lp.Println("Enter the name of the", desc, "below, relative to:") + h.lp.Println(h.lp.SprintStyled("fg=green", h.state.save_file_cdir)) + h.lp.Println() + h.rl.RedrawNonAtomic() + return +} diff --git a/tools/tui/readline/api.go b/tools/tui/readline/api.go index f560ad417..731f860f4 100644 --- a/tools/tui/readline/api.go +++ b/tools/tui/readline/api.go @@ -277,8 +277,12 @@ func (self *Readline) CursorAtEndOfLine() bool { return self.input_state.cursor.X >= len(self.input_state.lines[self.input_state.cursor.Y]) } -func (self *Readline) OnResize(old_size loop.ScreenSize, new_size loop.ScreenSize) error { +func (self *Readline) ClearCachedScreenSize() { self.screen_width, self.screen_height = 0, 0 +} + +func (self *Readline) OnResize(old_size loop.ScreenSize, new_size loop.ScreenSize) error { + self.ClearCachedScreenSize() self.Redraw() return nil }