Work on save file name mode

This commit is contained in:
Kovid Goyal 2025-06-03 20:27:14 +05:30
parent a023a0db09
commit 6880ecaa28
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
4 changed files with 260 additions and 54 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}