package choose_files import ( "bytes" "encoding/json" "fmt" "os" "path/filepath" "slices" "strconv" "strings" "sync" "github.com/kovidgoyal/kitty/tools/cli" "github.com/kovidgoyal/kitty/tools/config" "github.com/kovidgoyal/kitty/tools/ignorefiles" "github.com/kovidgoyal/kitty/tools/tty" "github.com/kovidgoyal/kitty/tools/tui" "github.com/kovidgoyal/kitty/tools/tui/graphics" "github.com/kovidgoyal/kitty/tools/tui/loop" "github.com/kovidgoyal/kitty/tools/tui/readline" "github.com/kovidgoyal/kitty/tools/utils" "github.com/kovidgoyal/kitty/tools/utils/shlex" "golang.org/x/text/message" ) var _ = fmt.Print var debugprintln = tty.DebugPrintln type Screen int const ( NORMAL Screen = iota SAVE_FILE ) type Mode int const ( SELECT_SINGLE_FILE Mode = iota SELECT_MULTIPLE_FILES SELECT_SAVE_FILE SELECT_SAVE_FILES SELECT_SAVE_DIR SELECT_SINGLE_DIR SELECT_MULTIPLE_DIRS ) func (m Mode) CanSelectNonExistent() bool { switch m { case SELECT_SAVE_FILE, SELECT_SAVE_DIR, SELECT_SAVE_FILES: return true } return false } func (m Mode) AllowsMultipleSelection() bool { switch m { case SELECT_MULTIPLE_FILES, SELECT_MULTIPLE_DIRS, SELECT_SAVE_FILES: return true } return false } func (m Mode) OnlyDirs() bool { switch m { case SELECT_SINGLE_DIR, SELECT_MULTIPLE_DIRS, SELECT_SAVE_DIR: return true } return false } func (m Mode) SelectFiles() bool { switch m { case SELECT_SINGLE_FILE, SELECT_MULTIPLE_FILES, SELECT_SAVE_FILE, SELECT_SAVE_FILES: return true } return false } func (m Mode) WindowTitle() string { switch m { case SELECT_SINGLE_FILE: return "Choose an existing file" case SELECT_MULTIPLE_FILES: return "Choose one or more existing files" case SELECT_SAVE_FILE: return "Choose a file to save" case SELECT_SAVE_DIR: return "Choose a directory to save" case SELECT_SINGLE_DIR: return "Choose an existing directory" case SELECT_MULTIPLE_DIRS: return "Choose one or more directories" case SELECT_SAVE_FILES: return "Choose files to save" } return "" } type render_state struct { num_matches, num_of_slots, num_before, num_per_column, num_columns, num_shown, preview_width int first_idx CollectionIndex } type State struct { base_dir string current_dir string multiselect bool search_text string mode Mode suggested_save_file_name string suggested_save_file_path string window_title string screen Screen current_filter string filter_map map[string]Filter filter_names []string show_hidden bool show_preview bool respect_ignores bool sort_by_last_modified bool global_ignores ignorefiles.IgnoreFile keyboard_shortcuts []*config.KeyAction display_title bool pygments_style, dark_pygments_style string syntax_aliases map[string]string max_disk_cache_size int64 selections []string current_idx CollectionIndex last_render render_state mouse_state tui.MouseState redraw_needed bool } func (s State) DiskCacheSize() int64 { return s.max_disk_cache_size } func (s State) HighlightStyles() (string, string) { return s.pygments_style, s.dark_pygments_style } func (s State) SyntaxAliases() map[string]string { return s.syntax_aliases } func (s State) DisplayTitle() bool { return s.display_title } func (s State) ShowHidden() bool { return s.show_hidden } func (s State) ShowPreview() bool { return s.show_preview } func (s State) RespectIgnores() bool { return s.respect_ignores } func (s State) SortByLastModified() bool { return s.sort_by_last_modified } func (s State) GlobalIgnores() ignorefiles.IgnoreFile { return s.global_ignores } func (s State) BaseDir() string { return utils.IfElse(s.base_dir == "", default_cwd, s.base_dir) } func (s State) Filter() Filter { return s.filter_map[s.current_filter] } func (s State) Multiselect() bool { return s.multiselect } func (s State) String() string { return utils.Repr(s) } func (s State) SearchText() string { return s.search_text } func (s State) OnlyDirs() bool { return s.mode.OnlyDirs() } func (s *State) SetSearchText(val string) { if s.search_text != val { s.search_text = val s.current_idx = CollectionIndex{} } } func (s *State) SetCurrentDir(val string) { if q, err := filepath.Abs(val); err == nil { val = q } if s.CurrentDir() != val { s.search_text = "" s.current_idx = CollectionIndex{} s.current_dir = val } } func (s State) CurrentIndex() CollectionIndex { return s.current_idx } func (s *State) SetCurrentIndex(val CollectionIndex) { s.current_idx = val } func (s State) CurrentDir() string { return utils.IfElse(s.current_dir == "", s.BaseDir(), s.current_dir) } func (s State) WindowTitle() string { if s.window_title == "" { return s.mode.WindowTitle() } return s.window_title } 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) (added bool) { 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) added = true } return } func (s *State) IsSelected(x *ResultItem) bool { if len(s.selections) == 0 { return false } q := filepath.Join(s.CurrentDir(), x.text) return slices.Contains(s.selections, q) } type ScreenSize struct { width, height, cell_width, cell_height, width_px, height_px int } type Handler struct { state State screen_size ScreenSize result_manager *ResultManager lp *loop.Loop rl *readline.Readline err_chan chan error shortcut_tracker config.ShortcutTracker msg_printer *message.Printer spinner *tui.Spinner preview_manager *PreviewManager last_rendered_preview Preview last_rendered_preview_abspath string prev_preview_for_smooth_transition Preview graphics_handler GraphicsHandler } func (self *Handler) on_escape_code(etype loop.EscapeCodeType, payload []byte) error { switch etype { case loop.APC: gc := graphics.GraphicsCommandFromAPC(payload) if gc != nil { return self.graphics_handler.HandleGraphicsCommand(gc) } } return nil } func (h *Handler) draw_screen() (err error) { h.state.redraw_needed = false h.lp.StartAtomicUpdate() defer func() { h.state.mouse_state.UpdateHoveredIds() h.state.mouse_state.ApplyHoverStyles(h.lp) h.graphics_handler.ApplyPlacements(h.lp) if h.state.screen == NORMAL { // so that the cursor ends up in the right place h.lp.MoveCursorTo(1, 1) if h.state.DisplayTitle() { h.lp.Println(h.state.WindowTitle()) h.draw_search_bar(1) } else { h.draw_search_bar(0) } } h.lp.EndAtomicUpdate() }() h.lp.ClearScreenButNotGraphics() h.graphics_handler.ClearPlacements(h.lp) h.state.mouse_state.ClearCellRegions() switch h.state.screen { case NORMAL: matches, is_complete := h.get_results() h.lp.SetWindowTitle(h.state.WindowTitle()) y := SEARCH_BAR_HEIGHT + utils.IfElse(h.state.DisplayTitle(), 1, 0) footer_height, err := h.draw_footer() if err != nil { return err } y += h.draw_results(y, footer_height, matches, !is_complete) case SAVE_FILE: err = h.draw_save_file_name_screen() } return } type previewer struct { cmdline []string pattern string use_mimetype bool } func (p previewer) matches(fname, mt string) bool { q := utils.IfElse(p.use_mimetype, mt, fname) matched, err := filepath.Match(p.pattern, q) return matched && err == nil } var previewers []previewer func load_previewers(cfg *Config) (err error) { for _, spec := range cfg.Previewer { parts, err := shlex.Split(spec) if err != nil { return fmt.Errorf("invalid previewer specification: %#v with error: %w", spec, err) } if len(parts) < 2 { return fmt.Errorf("invalid previewer specification: %#v with error: no command specified", spec) } prefix, pattern, found := strings.Cut(parts[0], ":") if !found { return fmt.Errorf("invalid previewer specification: %#v with error: no prefix in pattern specification", spec) } previewers = append(previewers, previewer{parts[1:], pattern, prefix == "mime"}) } return } func load_config(opts *Options) (ans *Config, err error) { ans = NewConfig() p := config.ConfigParser{LineHandler: ans.Parse} err = p.LoadConfig("choose-files.conf", opts.Config, opts.Override) if err != nil { return nil, err } ans.KeyboardShortcuts = config.ResolveShortcuts(ans.KeyboardShortcuts) parts, err := shlex.Split(ans.Video_preview) if err != nil { return nil, err } for _, x := range parts { k, v, found := strings.Cut(x, "=") if !found { return nil, fmt.Errorf("invalid value %s in video_preview", x) } var i uint64 switch k { case "duration": if video_duration, err = strconv.ParseFloat(v, 64); err != nil { return nil, fmt.Errorf("invalid %s in video_preview: %s", k, v) } case "fps": if i, err = strconv.ParseUint(v, 10, 0); err != nil { return nil, fmt.Errorf("invalid %s in video_preview: %s", k, v) } video_fps = int(i) case "width": if i, err = strconv.ParseUint(v, 10, 0); err != nil { return nil, fmt.Errorf("invalid %s in video_preview: %s", k, v) } video_width = int(i) default: return nil, fmt.Errorf("unrecognized key in video_preview: %s", k) } } return ans, nil } func (h *Handler) init_sizes(new_size loop.ScreenSize) { h.screen_size.width = int(new_size.WidthCells) h.screen_size.height = int(new_size.HeightCells) h.screen_size.cell_width = int(new_size.CellWidth) 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) { if sz, err := h.lp.ScreenSize(); err != nil { return "", err } else { h.init_sizes(sz) } h.lp.AllowLineWrapping(false) h.lp.SetCursorShape(loop.BAR_CURSOR, true) h.lp.StartBracketedPaste() if h.state.suggested_save_file_path != "" { switch h.state.mode { case SELECT_SAVE_FILE, SELECT_SAVE_DIR: if s, err := os.Stat(h.state.suggested_save_file_path); err == nil { if (s.IsDir() && h.state.mode != SELECT_SAVE_FILE) || (!s.IsDir() && h.state.mode == SELECT_SAVE_FILE) { h.state.SetCurrentDir(filepath.Dir(h.state.suggested_save_file_path)) h.state.SetSearchText(filepath.Base(h.state.suggested_save_file_name)) } } } } err = h.graphics_handler.Initialize(h.lp) h.result_manager.set_root_dir() h.draw_screen() h.lp.SendOverlayReady() return } func (h *Handler) current_abspath() string { matches, _ := h.get_results() if r := matches.At(h.state.CurrentIndex()); r != nil { return filepath.Join(h.state.CurrentDir(), r.text) } return "" } func (s *State) CanSelect(r *ResultItem) bool { return utils.IfElse(s.OnlyDirs(), r.ftype.IsDir(), !r.ftype.IsDir()) } func (h *Handler) toggle_selection_at(idx CollectionIndex) bool { matches, _ := h.get_results() if r := matches.At(idx); r != nil && h.state.CanSelect(r) { m := filepath.Join(h.state.CurrentDir(), r.text) if added := h.state.ToggleSelection(m); added { h.result_manager.last_click_anchor = &idx } else { h.result_manager.last_click_anchor = nil if len(h.state.selections) > 0 { x := utils.NewSetWithItems(h.state.selections...) cdir := h.state.CurrentDir() h.result_manager.last_click_anchor = matches.Closest(idx, func(q *ResultItem) bool { return x.Has(filepath.Join(cdir, q.text)) }) } } return true } return false } func (h *Handler) toggle_selection() bool { return h.toggle_selection_at(h.state.CurrentIndex()) } func (h *Handler) change_current_dir(dir string) { if dir != h.state.CurrentDir() { h.state.SetCurrentDir(dir) h.result_manager.set_root_dir() h.state.last_render = render_state{} } } func (h *Handler) set_query(q string) { if q != h.state.SearchText() { h.state.SetSearchText(q) h.result_manager.set_query() h.state.last_render = render_state{} } } func (h *Handler) set_filter(filter_name string) { if filter_name != h.state.current_filter { h.state.current_filter = filter_name h.result_manager.set_filter() h.state.last_render = render_state{} } } func (h *Handler) change_to_current_dir_if_possible() error { if m := h.current_abspath(); m != "" { if st, err := os.Stat(m); err == nil { if !st.IsDir() { m = filepath.Dir(m) } h.change_current_dir(m) return h.draw_screen() } } h.lp.Beep() return nil } func (h *Handler) finish_selection() error { if h.state.mode.CanSelectNonExistent() && len(h.state.selections) == 0 { return h.switch_to_save_file_name_mode() } h.lp.Quit(0) return nil } func (h *Handler) change_filter(delta int) bool { if len(h.state.filter_names) < 2 { return false } idx := slices.Index(h.state.filter_names, h.state.current_filter) idx += delta + len(h.state.filter_names) idx %= len(h.state.filter_names) h.set_filter(h.state.filter_names[idx]) return true } func (h *Handler) switch_to_save_file_name_mode() error { name := h.state.suggested_save_file_name if h.state.SearchText() != "" { name = h.state.SearchText() } if name == "" { name = h.current_abspath() if name != "" { if r, err := filepath.Rel(h.state.CurrentDir(), name); err == nil { name = r } } } h.initialize_save_file_name(name) return h.draw_screen() } func (h *Handler) switch_to_save_file_name_mode_based_on_existing() error { name := h.current_abspath() if name == "" { return h.switch_to_save_file_name_mode() } if r, err := filepath.Rel(h.state.CurrentDir(), name); err == nil { name = r } h.initialize_save_file_name(name) return h.draw_screen() } func (h *Handler) accept_idx(idx CollectionIndex) (accepted bool, err error) { matches, _ := h.get_results() if r := matches.At(idx); r != nil { m := filepath.Join(h.state.CurrentDir(), r.text) if h.state.mode.SelectFiles() { var s os.FileInfo if s, err = os.Stat(m); err != nil { return false, nil } if s.IsDir() { if h.state.mode.CanSelectNonExistent() { return true, h.switch_to_save_file_name_mode() } return false, nil } } h.state.AddSelection(m) h.result_manager.last_click_anchor = &idx if len(h.state.selections) > 0 { return true, h.finish_selection() } return true, h.draw_screen() } return } func (h *Handler) dispatch_action(name, args string) (err error) { switch name { case "quit": h.lp.Quit(1) case "next": if n, nerr := strconv.Atoi(args); nerr == nil { h.next_result(n) } else { switch args { case "": h.next_result(1) case "left": h.move_sideways(true) case "right": h.move_sideways(false) case "first": h.state.SetCurrentIndex(CollectionIndex{}) h.state.last_render.num_before = 0 case "last": matches, _ := h.get_results() h.state.SetCurrentIndex(matches.IncrementIndexWithWrapAround(CollectionIndex{}, -1)) h.state.last_render.num_before = 0 case "first_on_screen": h.state.SetCurrentIndex(h.state.last_render.first_idx) h.state.last_render.num_before = 0 case "last_on_screen": matches, _ := h.get_results() h.state.SetCurrentIndex(matches.IncrementIndexWithWrapAround(h.state.last_render.first_idx, h.state.last_render.num_shown-1)) h.state.last_render.num_before = h.state.last_render.num_shown - 1 } } return h.draw_screen() case "next_filter": if n, nerr := strconv.Atoi(args); nerr == nil { h.change_filter(n) return h.draw_screen() } h.lp.Beep() case "select": if !h.toggle_selection() { h.lp.Beep() } else { return h.draw_screen() } case "accept": accepted, aerr := h.accept_idx(h.state.CurrentIndex()) if aerr != nil { return aerr } if !accepted { h.lp.Beep() } case "typename": if !h.state.mode.CanSelectNonExistent() { if h.state.mode.OnlyDirs() { h.state.AddSelection(h.state.CurrentDir()) return h.finish_selection() } h.lp.Beep() } else { return h.switch_to_save_file_name_mode() } case "modifyname": if h.state.mode.CanSelectNonExistent() { return h.switch_to_save_file_name_mode_based_on_existing() } else { h.lp.Beep() } case "toggle": switch args { case "preview": h.state.show_preview = !h.state.show_preview return h.draw_screen() case "dotfiles": h.state.show_hidden = !h.state.show_hidden h.result_manager.set_show_hidden() return h.draw_screen() case "ignorefiles": h.state.respect_ignores = !h.state.respect_ignores h.result_manager.set_respect_ignores() return h.draw_screen() case "sort_by_dates": h.state.sort_by_last_modified = !h.state.sort_by_last_modified h.result_manager.set_sort_by_last_modified() return h.draw_screen() default: h.lp.Beep() } case "cd": switch args { case ".": return h.change_to_current_dir_if_possible() case "..": curr := h.state.CurrentDir() switch curr { case "/": case ".": if curr, err = os.Getwd(); err == nil && curr != "/" { h.change_current_dir(filepath.Dir(curr)) return h.draw_screen() } default: h.change_current_dir(filepath.Dir(curr)) return h.draw_screen() } h.lp.Beep() default: args = utils.Expanduser(args) if st, serr := os.Stat(args); serr != nil || !st.IsDir() { h.lp.Beep() return } if absp, err := filepath.Abs(args); err == nil { h.change_current_dir(absp) return h.draw_screen() } else { h.lp.Beep() return nil } } } return } func (h *Handler) OnKeyEvent(ev *loop.KeyEvent) (err error) { switch h.state.screen { case NORMAL: if h.handle_edit_keys(ev) { ev.Handled = true h.draw_screen() } ac := h.shortcut_tracker.Match(ev, h.state.keyboard_shortcuts) if ac != nil { ev.Handled = true return h.dispatch_action(ac.Name, ac.Args) } case SAVE_FILE: err = h.save_file_name_handle_key(ev) } return } func (h *Handler) OnMouseEvent(event *loop.MouseEvent) (err error) { h.state.redraw_needed = h.state.mouse_state.UpdateState(event) if err = h.state.mouse_state.DispatchEventToHoveredRegions(event); err != nil { return } if h.state.redraw_needed { err = h.draw_screen() } return } func (h *Handler) OnText(text string, from_key_event, in_bracketed_paste bool) (err error) { switch h.state.screen { case NORMAL: h.set_query(h.state.SearchText() + 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 } type CachedValues struct { Show_hidden bool `json:"show_hidden"` Hide_preview bool `json:"hide_preview"` Respect_ignores bool `json:"respect_ignores"` Sort_by_last_modified bool `json:"sort_by_last_modified"` } const cache_filename = "choose-files.json" var cached_values = sync.OnceValue(func() *CachedValues { ans := CachedValues{Respect_ignores: true} fname := filepath.Join(utils.CacheDir(), cache_filename) if data, err := os.ReadFile(fname); err == nil { _ = json.Unmarshal(data, &ans) } return &ans }) func (s State) save_cached_values() { c := CachedValues{Show_hidden: s.show_hidden, Respect_ignores: s.respect_ignores, Sort_by_last_modified: s.sort_by_last_modified, Hide_preview: !s.show_preview} fname := filepath.Join(utils.CacheDir(), cache_filename) if data, err := json.Marshal(c); err == nil { _ = os.WriteFile(fname, data, 0600) } } func (h *Handler) set_state_from_config(conf *Config, opts *Options) (err error) { h.state = State{} switch opts.Mode { case "file": h.state.mode = SELECT_SINGLE_FILE case "files": h.state.mode = SELECT_MULTIPLE_FILES case "save-file": h.state.mode = SELECT_SAVE_FILE case "dir": h.state.mode = SELECT_SINGLE_DIR case "dirs": h.state.mode = SELECT_MULTIPLE_DIRS case "save-dir": h.state.mode = SELECT_SAVE_DIR case "save-files": h.state.mode = SELECT_SAVE_FILES default: h.state.mode = SELECT_SINGLE_FILE } h.state.suggested_save_file_name = opts.SuggestedSaveFileName h.state.suggested_save_file_path = opts.SuggestedSaveFilePath h.state.filter_map = nil h.state.current_filter = "" if len(opts.FileFilter) > 0 { opts.FileFilter = utils.Uniq(opts.FileFilter) has_all_files := false fmap := make(map[string][]Filter) seen := utils.NewSet[string](len(opts.FileFilter)) for _, x := range opts.FileFilter { f, ferr := NewFilter(x) if ferr != nil { return ferr } if f.Match == nil { has_all_files = true } if h.state.current_filter == "" { h.state.current_filter = f.Name } fmap[f.Name] = append(fmap[f.Name], *f) if !seen.Has(f.Name) { seen.Add(f.Name) h.state.filter_names = append(h.state.filter_names, f.Name) } } if !has_all_files { af, _ := NewFilter("glob:*:All files") fmap[af.Name] = append(fmap[af.Name], *af) if !seen.Has(af.Name) { h.state.filter_names = append(h.state.filter_names, af.Name) } } h.state.filter_map = make(map[string]Filter) for name, filters := range fmap { h.state.filter_map[name] = CombinedFilter(filters...) } } h.state.sort_by_last_modified = false h.state.respect_ignores = true h.state.show_hidden = false h.state.show_preview = true switch conf.Show_hidden { case Show_hidden_true, Show_hidden_y, Show_hidden_yes: h.state.show_hidden = true case Show_hidden_false, Show_hidden_n, Show_hidden_no: h.state.show_hidden = false case Show_hidden_last: h.state.show_hidden = cached_values().Show_hidden } switch conf.Respect_ignores { case Respect_ignores_true, Respect_ignores_y, Respect_ignores_yes: h.state.respect_ignores = true case Respect_ignores_false, Respect_ignores_n, Respect_ignores_no: h.state.respect_ignores = false case Respect_ignores_last: h.state.respect_ignores = cached_values().Respect_ignores } switch conf.Sort_by_last_modified { case Sort_by_last_modified_true, Sort_by_last_modified_y, Sort_by_last_modified_yes: h.state.sort_by_last_modified = true case Sort_by_last_modified_false, Sort_by_last_modified_n, Sort_by_last_modified_no: h.state.sort_by_last_modified = false case Sort_by_last_modified_last: h.state.sort_by_last_modified = cached_values().Sort_by_last_modified } switch conf.Show_preview { case Show_preview_true, Show_preview_y, Show_preview_yes: h.state.show_preview = true case Show_preview_false, Show_preview_n, Show_preview_no: h.state.show_preview = false case Show_preview_last: h.state.show_preview = !cached_values().Hide_preview } h.state.global_ignores = ignorefiles.NewGitignore() if err = h.state.global_ignores.LoadLines(conf.Ignore...); err != nil { return err } h.state.keyboard_shortcuts = conf.KeyboardShortcuts h.state.display_title = opts.DisplayTitle h.state.pygments_style = conf.Pygments_style h.state.dark_pygments_style = conf.Dark_pygments_style h.state.syntax_aliases = conf.Syntax_aliases h.state.max_disk_cache_size = int64(conf.Cache_size * (1024 * 1024 * 1024)) return } var default_cwd string var use_light_colors bool func quote_if_needed(x string) string { if s, err := shlex.Split(x); len(s) == 1 && err == nil && !strings.Contains(x, "$") { return x } return utils.QuoteStringForSH(x) } func for_shell_relative(x string) string { if rel, is_under, err := utils.RelativeIfUnder(default_cwd, x, false); err == nil && is_under { x = rel } return quote_if_needed(x) } func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) { if opts.ClearCache { c, err := preview_cache() if err != nil { return 1, err } if err = c.Clear(); err != nil { return 1, err } } write_output := func(selections []string, interrupted bool, current_filter string) { payload := make(map[string]any) if err != nil { if opts.WriteOutputTo != "" { m := fmt.Sprint(err) if opts.OutputFormat == "json" { payload["error"] = m b, _ := json.MarshalIndent(payload, "", " ") m = string(b) } os.WriteFile(opts.WriteOutputTo, []byte(m), 0600) } return } if interrupted { if opts.WriteOutputTo != "" { if opts.OutputFormat == "json" { payload["interrupted"] = true b, _ := json.MarshalIndent(payload, "", " ") os.WriteFile(opts.WriteOutputTo, b, 0600) } } return } payload["paths"] = selections if current_filter != "" { payload["current_filter"] = current_filter } if tui.RunningAsUI() { fmt.Println(tui.KittenOutputSerializer()(payload)) } else { var m string switch opts.OutputFormat { case "shell": m = strings.Join(utils.Map(quote_if_needed, selections), " ") case "shell-relative": m = strings.Join(utils.Map(for_shell_relative, selections), " ") case "text": m = strings.Join(selections, "\n") case "json": b, _ := json.MarshalIndent(payload, "", " ") m = string(b) } os.Stdout.Write(utils.UnsafeStringToBytes(m)) if opts.WriteOutputTo != "" { os.WriteFile(opts.WriteOutputTo, utils.UnsafeStringToBytes(m), 0600) } } } conf, err := load_config(opts) if err != nil { return 1, err } if err = load_previewers(conf); err != nil { return 1, err } lp, err := loop.New() if err != nil { return 1, err } lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING) lp.ColorSchemeChangeNotifications() handler := Handler{lp: lp, err_chan: make(chan error, 8), msg_printer: message.NewPrinter(utils.LanguageTag()), spinner: tui.NewSpinner("dots")} defer handler.graphics_handler.Cleanup() defer calibre_cleanup() getcwd := func() string { ans := handler.state.CurrentDir() if ans == "" { var err error ans, err = os.Getwd() if err != nil { ans = "." } } return ans } handler.rl = readline.New(lp, readline.RlInit{ Prompt: "> ", ContinuationPrompt: ". ", Completer: FilePromptCompleter(getcwd), }) if err = handler.set_state_from_config(conf, opts); err != nil { return 1, err } handler.result_manager = NewResultManager(handler.err_chan, &handler.state, lp.WakeupMainThread) handler.preview_manager = NewPreviewManager(handler.err_chan, &handler.state, lp.WakeupMainThread) switch len(args) { case 0: if default_cwd, err = os.Getwd(); err != nil { return } case 1: default_cwd = args[0] default: return 1, fmt.Errorf("Can only specify one directory to search in") } default_cwd = utils.Expanduser(default_cwd) if default_cwd, err = filepath.Abs(default_cwd); err != nil { return } lp.OnInitialize = func() (string, error) { if opts.WritePidTo != "" { if err := utils.AtomicWriteFile(opts.WritePidTo, bytes.NewReader([]byte(strconv.Itoa(os.Getpid()))), 0600); err != nil { return "", err } } if opts.Title != "" { lp.SetWindowTitle(opts.Title) } lp.RequestCurrentColorScheme() return handler.OnInitialize() } lp.OnFinalize = func() string { handler.graphics_handler.Finalize(lp) return "" } lp.OnResize = func(old, new_size loop.ScreenSize) (err error) { handler.init_sizes(new_size) handler.clear_cached_previews() return handler.draw_screen() } lp.OnColorSchemeChange = func(p loop.ColorPreference) (err error) { new_val := p == loop.LIGHT_COLOR_PREFERENCE if new_val != use_light_colors { use_light_colors = new_val handler.preview_manager.invalidate_color_scheme_based_cached_items() return handler.draw_screen() } return } lp.OnKeyEvent = handler.OnKeyEvent lp.OnText = handler.OnText lp.OnMouseEvent = handler.OnMouseEvent lp.OnEscapeCode = handler.on_escape_code lp.OnWakeup = func() (err error) { select { case err = <-handler.err_chan: default: err = handler.draw_screen() } return } err = lp.Run() handler.state.save_cached_values() if err != nil { write_output(nil, false, "") return 1, err } ds := lp.DeathSignalName() if ds != "" { fmt.Println("Killed by signal: ", ds) lp.KillIfSignalled() write_output(nil, true, "") return 1, nil } rc = lp.ExitCode() switch rc { case 0: write_output(handler.state.selections, false, handler.state.current_filter) default: write_output(nil, true, "") } return } func EntryPoint(parent *cli.Command) { create_cmd(parent, main) }