mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-19 15:04:50 +02:00
More work on choose files integration
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package choose_files
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -33,15 +34,15 @@ const (
|
||||
SELECT_SINGLE_FILE Mode = iota
|
||||
SELECT_MULTIPLE_FILES
|
||||
SELECT_SAVE_FILE
|
||||
SELECT_SAVE_FILES
|
||||
SELECT_SAVE_DIR
|
||||
SELECT_SINGLE_DIR
|
||||
SELECT_MULTIPLE_DIRS
|
||||
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:
|
||||
case SELECT_SAVE_FILE, SELECT_SAVE_DIR, SELECT_SAVE_FILES:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -49,7 +50,7 @@ func (m Mode) CanSelectNonExistent() bool {
|
||||
|
||||
func (m Mode) AllowsMultipleSelection() bool {
|
||||
switch m {
|
||||
case SELECT_MULTIPLE_FILES, SELECT_MULTIPLE_DIRS:
|
||||
case SELECT_MULTIPLE_FILES, SELECT_MULTIPLE_DIRS, SELECT_SAVE_FILES:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -57,7 +58,7 @@ func (m Mode) AllowsMultipleSelection() bool {
|
||||
|
||||
func (m Mode) OnlyDirs() bool {
|
||||
switch m {
|
||||
case SELECT_SINGLE_DIR, SELECT_MULTIPLE_DIRS, SELECT_SAVE_DIR, SELECT_SAVE_DIR_FOR_FILES:
|
||||
case SELECT_SINGLE_DIR, SELECT_MULTIPLE_DIRS, SELECT_SAVE_DIR:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -65,7 +66,7 @@ func (m Mode) OnlyDirs() bool {
|
||||
|
||||
func (m Mode) SelectFiles() bool {
|
||||
switch m {
|
||||
case SELECT_SINGLE_FILE, SELECT_MULTIPLE_FILES, SELECT_SAVE_FILE:
|
||||
case SELECT_SINGLE_FILE, SELECT_MULTIPLE_FILES, SELECT_SAVE_FILE, SELECT_SAVE_FILES:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -85,8 +86,8 @@ func (m Mode) WindowTitle() string {
|
||||
return "Choose an existing directory"
|
||||
case SELECT_MULTIPLE_DIRS:
|
||||
return "Choose one or more directories"
|
||||
case SELECT_SAVE_DIR_FOR_FILES:
|
||||
return "Choose a directory to save multiple files in"
|
||||
case SELECT_SAVE_FILES:
|
||||
return "Choose files to save"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -384,15 +385,15 @@ func (h *Handler) set_state_from_config(_ *Config, opts *Options) (err error) {
|
||||
h.state.mode = SELECT_MULTIPLE_DIRS
|
||||
case "save-dir":
|
||||
h.state.mode = SELECT_SAVE_DIR
|
||||
case "dir-for-files":
|
||||
h.state.mode = SELECT_SAVE_DIR_FOR_FILES
|
||||
case "save-files":
|
||||
h.state.mode = SELECT_SAVE_FILES
|
||||
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:
|
||||
case SELECT_SAVE_FILE, SELECT_SAVE_DIR:
|
||||
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) {
|
||||
@@ -408,6 +409,42 @@ func (h *Handler) set_state_from_config(_ *Config, opts *Options) (err error) {
|
||||
var default_cwd string
|
||||
|
||||
func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
|
||||
write_output := func(selections []string, interrupted bool) {
|
||||
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
|
||||
}
|
||||
m := strings.Join(selections, "\n")
|
||||
fmt.Print(m)
|
||||
if opts.WriteOutputTo != "" {
|
||||
if opts.OutputFormat == "json" {
|
||||
payload["paths"] = selections
|
||||
b, _ := json.MarshalIndent(payload, "", " ")
|
||||
m = string(b)
|
||||
}
|
||||
os.WriteFile(opts.WriteOutputTo, []byte(m), 0600)
|
||||
}
|
||||
}
|
||||
|
||||
conf, err := load_config(opts)
|
||||
if err != nil {
|
||||
return 1, err
|
||||
@@ -454,16 +491,22 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
|
||||
}
|
||||
err = lp.Run()
|
||||
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
|
||||
}
|
||||
if rc = lp.ExitCode(); rc == 0 {
|
||||
fmt.Print(strings.Join(handler.state.selections, "\n"))
|
||||
rc = lp.ExitCode()
|
||||
switch rc {
|
||||
case 0:
|
||||
write_output(handler.state.selections, false)
|
||||
default:
|
||||
write_output(nil, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ completion=type:file ext:conf group:"Config files" kwds:none,NONE
|
||||
|
||||
--mode
|
||||
type=choices
|
||||
choices=file,files,save-file,dir,save-dir,dirs,dir-for-files
|
||||
choices=file,files,save-file,dir,save-dir,dirs,save-files
|
||||
default=file
|
||||
The type of object(s) to select
|
||||
|
||||
@@ -50,7 +50,17 @@ 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
|
||||
|
||||
|
||||
--write-output-to
|
||||
Path to a file to which the output is written in addition to STDOUT.
|
||||
|
||||
|
||||
--output-format
|
||||
choices=text,json
|
||||
default=text
|
||||
The format in which to write the output.
|
||||
'''.format(config_help=CONFIG_HELP.format(conf_name='choose-files', appname=appname)).format
|
||||
|
||||
|
||||
help_text = '''\
|
||||
|
||||
@@ -33,6 +33,9 @@ func run_server(opts *ServerOptions) (err error) {
|
||||
portal, err := NewPortal(config)
|
||||
if err == nil {
|
||||
err = portal.Start()
|
||||
if err == nil {
|
||||
defer portal.Cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -23,6 +23,14 @@ by using :code:`kitten desktop-ui color-scheme`.
|
||||
''')
|
||||
opt('accent_color', 'cyan', long_text='The RGB accent color for your system, can be specified as a color name or in hex a decimal format.')
|
||||
opt('contrast', 'normal', choices=('normal', 'high'), long_text='The preferred contrast level.')
|
||||
opt('+file_chooser_kitty_conf', '',
|
||||
long_text='Path to config file to use for kitty when drawing the file chooser window. Can be specified multiple times. By default, the'
|
||||
' normal kitty.conf is used. Relative paths are resolved with respect to the kitty config directory.'
|
||||
)
|
||||
opt('+file_chooser_kitty_override', '', long_text='Override individual kitty configuration options, for the file chooser window.'
|
||||
' Can be specified multiple times. Syntax: :italic:`name=value`. For example: :code:`font_size=20`.'
|
||||
)
|
||||
|
||||
egr()
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,16 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kovidgoyal/dbus"
|
||||
"github.com/kovidgoyal/dbus/introspect"
|
||||
@@ -42,13 +48,20 @@ const (
|
||||
DARK
|
||||
LIGHT
|
||||
)
|
||||
const (
|
||||
RESPONSE_SUCCESS uint32 = iota
|
||||
RESPONSE_CANCELED
|
||||
RESPONSE_ENDED
|
||||
)
|
||||
|
||||
type SettingsMap map[string]map[string]dbus.Variant
|
||||
|
||||
type Portal struct {
|
||||
bus *dbus.Conn
|
||||
settings SettingsMap
|
||||
lock sync.Mutex
|
||||
bus *dbus.Conn
|
||||
settings SettingsMap
|
||||
lock sync.Mutex
|
||||
opts *Config
|
||||
file_chooser_first_instance *exec.Cmd
|
||||
}
|
||||
|
||||
func to_color(spec string) (v dbus.Variant, err error) {
|
||||
@@ -59,7 +72,7 @@ func to_color(spec string) (v dbus.Variant, err error) {
|
||||
}
|
||||
|
||||
func NewPortal(opts *Config) (p *Portal, err error) {
|
||||
ans := Portal{}
|
||||
ans := Portal{opts: opts}
|
||||
ans.settings = SettingsMap{
|
||||
SETTINGS_CANARY_NAMESPACE: map[string]dbus.Variant{
|
||||
SETTINGS_CANARY_KEY: dbus.MakeVariant("running"),
|
||||
@@ -186,6 +199,17 @@ func (self *Portal) Start() (err error) {
|
||||
if err = ExportInterface(self.bus, self, SETTINGS_INTERFACE, DESKTOP_OBJECT_PATH, methods, props, signals); err != nil {
|
||||
return
|
||||
}
|
||||
methods = MethodSpec{
|
||||
"OpenFile": {{"handle", "o", false}, {"app_id", "s", false}, {"parent_window", "s", false}, {"title", "s", false}, {"options", "a{sv}", false},
|
||||
{"response", "u", true}, {"results", "a{sv}", false},
|
||||
},
|
||||
"SaveFile": {{"handle", "o", false}, {"app_id", "s", false}, {"parent_window", "s", false}, {"title", "s", false}, {"options", "a{sv}", false},
|
||||
{"response", "u", true}, {"results", "a{sv}", false},
|
||||
},
|
||||
}
|
||||
if err = ExportInterface(self.bus, self, FILE_CHOOSER_INTERFACE, DESKTOP_OBJECT_PATH, methods, nil, nil); err != nil {
|
||||
return
|
||||
}
|
||||
methods = MethodSpec{
|
||||
"ChangeSetting": {{"namespace", "s", false}, {"key", "s", false}, {"value", "v", false}},
|
||||
"RemoveSetting": {{"namespace", "s", false}, {"key", "s", false}},
|
||||
@@ -600,3 +624,188 @@ func (self *Portal) ReadAll(namespaces []string) (ReadAllType, *dbus.Error) {
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
type vmap map[string]dbus.Variant
|
||||
type ChooseFilesData struct {
|
||||
Title string
|
||||
Mode string
|
||||
Cwd string
|
||||
SuggestedSaveFileName, SuggestedSaveFilePath string
|
||||
}
|
||||
|
||||
func var_to_bool_or_false(v dbus.Variant) bool {
|
||||
if ans, ok := v.Value().(bool); ok {
|
||||
return ans
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *Portal) Cleanup() {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
if self.file_chooser_first_instance != nil {
|
||||
self.file_chooser_first_instance.Process.Signal(unix.SIGTERM)
|
||||
ch := make(chan int)
|
||||
go func() {
|
||||
self.file_chooser_first_instance.Wait()
|
||||
ch <- 0
|
||||
}()
|
||||
select {
|
||||
case <-ch:
|
||||
case <-time.After(time.Second):
|
||||
self.file_chooser_first_instance.Process.Kill()
|
||||
self.file_chooser_first_instance.Wait()
|
||||
}
|
||||
self.file_chooser_first_instance = nil
|
||||
}
|
||||
}
|
||||
|
||||
type ChooserResponse struct {
|
||||
Paths []string `json:"paths"`
|
||||
Error string `json:"error"`
|
||||
Interrupted bool `json:"interrupted"`
|
||||
}
|
||||
|
||||
func (self *Portal) run_file_chooser(cfd ChooseFilesData) (response uint32, uris []string) {
|
||||
response = RESPONSE_ENDED
|
||||
tdir, err := os.MkdirTemp("", "kitty-cfd")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot run file chooser as failed to create a temporary directory with error: %s\n", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tdir)
|
||||
output_path := filepath.Join(tdir, "output.json")
|
||||
cmd := func() *exec.Cmd {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
args := []string{
|
||||
"+kitten", "panel", "--layer=overlay", "--edge=center", "--focus-policy=exclusive",
|
||||
"-o", "background_opacity=0.85", "--wait-for-single-instance-window-close",
|
||||
"--single-instance", "--instance-group", "cfp-" + strconv.Itoa(os.Getpid()),
|
||||
}
|
||||
for _, x := range self.opts.File_chooser_kitty_conf {
|
||||
args = append(args, `-c`, x)
|
||||
}
|
||||
for _, x := range self.opts.File_chooser_kitty_override {
|
||||
args = append(args, `-o`, x)
|
||||
}
|
||||
if self.file_chooser_first_instance == nil {
|
||||
fifo_path := filepath.Join(tdir, "fifo")
|
||||
if err := unix.Mkfifo(fifo_path, 0600); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot run file chooser as failed to create a fifo directory with error: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
fa := slices.Clone(args)
|
||||
fa = append(fa, "--start-as-hidden", "sh", "-c", "echo a > '"+fifo_path+"'; read")
|
||||
cmd := exec.Command(utils.KittyExe(), fa...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Start()
|
||||
ch := make(chan int)
|
||||
go func() {
|
||||
f, err := os.OpenFile(fifo_path, os.O_RDONLY, os.ModeNamedPipe)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot run file chooser as failed to open fifo for read with error: %s\n", err)
|
||||
}
|
||||
b := []byte{'a', 'b', 'c', 'd'}
|
||||
f.Read(b)
|
||||
ch <- 0
|
||||
}()
|
||||
select {
|
||||
case <-ch:
|
||||
self.file_chooser_first_instance = cmd
|
||||
case <-time.After(5 * time.Second):
|
||||
fmt.Fprintf(os.Stderr, "cannot run file chooser as panel script timed out writing to fifo")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
args = append(args, "kitten", `choose-files`, `--mode`, cfd.Mode, `--write-output-to`, output_path, `--output-format=json`)
|
||||
if cfd.SuggestedSaveFileName != "" {
|
||||
args = append(args, `--suggested-save-file-name`, cfd.SuggestedSaveFileName)
|
||||
}
|
||||
if cfd.SuggestedSaveFilePath != "" {
|
||||
args = append(args, `--suggested-save-file-path`, cfd.SuggestedSaveFilePath)
|
||||
}
|
||||
cmd := exec.Command(utils.KittyExe(), args...)
|
||||
cmd.Dir = cfd.Cwd
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd
|
||||
}()
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "running file chooser failed with error: %s\n", err)
|
||||
return
|
||||
}
|
||||
raw, err := os.ReadFile(output_path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "running file chooser failed, could not read from output file with error: %s\n", err)
|
||||
return
|
||||
}
|
||||
var result ChooserResponse
|
||||
if err = json.Unmarshal(raw, &result); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "running file chooser failed, invalid JSON response with error: %s\n", err)
|
||||
return
|
||||
}
|
||||
if result.Error != "" {
|
||||
fmt.Fprintf(os.Stderr, "running file chooser failed, with error: %s\n", result.Error)
|
||||
return
|
||||
}
|
||||
if result.Interrupted {
|
||||
response = RESPONSE_CANCELED
|
||||
fmt.Fprintf(os.Stderr, "running file chooser failed, interrupted by user.\n")
|
||||
return
|
||||
}
|
||||
response = RESPONSE_SUCCESS
|
||||
prefix := "file://" + utils.IfElse(runtime.GOOS == "windows", "/", "")
|
||||
uris = utils.Map(func(path string) string {
|
||||
path = filepath.ToSlash(path)
|
||||
u := url.URL{Path: path}
|
||||
return prefix + u.EscapedPath()
|
||||
}, result.Paths)
|
||||
return
|
||||
}
|
||||
|
||||
func (options vmap) get_bytearray(name string) string {
|
||||
if v, found := options[name]; found {
|
||||
if b, ok := v.Value().([]byte); ok {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (self *Portal) OpenFile(handle dbus.ObjectPath, app_id string, parent_window string, title string, options vmap) (uint32, vmap, *dbus.Error) {
|
||||
cfd := ChooseFilesData{Title: title, Cwd: options.get_bytearray("current_folder")}
|
||||
dir_only := false
|
||||
if v, found := options["directory"]; found && var_to_bool_or_false(v) {
|
||||
dir_only = true
|
||||
}
|
||||
multiple := false
|
||||
if v, found := options["multiple"]; found && var_to_bool_or_false(v) {
|
||||
multiple = true
|
||||
}
|
||||
if dir_only {
|
||||
cfd.Mode = utils.IfElse(multiple, "dirs", "dir")
|
||||
} else {
|
||||
cfd.Mode = utils.IfElse(multiple, "files", "file")
|
||||
}
|
||||
|
||||
return RESPONSE_CANCELED, nil, nil
|
||||
}
|
||||
|
||||
func (self *Portal) SaveFile(handle dbus.ObjectPath, app_id string, parent_window string, title string, options vmap) (uint32, vmap, *dbus.Error) {
|
||||
cfd := ChooseFilesData{
|
||||
Title: title, Cwd: options.get_bytearray("current_folder"),
|
||||
SuggestedSaveFileName: options.get_bytearray("current_name"),
|
||||
SuggestedSaveFilePath: options.get_bytearray("current_file")}
|
||||
multiple := false
|
||||
if v, found := options["multiple"]; found && var_to_bool_or_false(v) {
|
||||
multiple = true
|
||||
}
|
||||
cfd.Mode = utils.IfElse(multiple, "save-files", "save-file")
|
||||
|
||||
return RESPONSE_CANCELED, nil, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user