diff --git a/kittens/clipboard/main.py b/kittens/clipboard/main.py index ece240fa3..a12db0a2a 100644 --- a/kittens/clipboard/main.py +++ b/kittens/clipboard/main.py @@ -25,6 +25,16 @@ can be detected. If more than one file is specified, this option should be speci times, once for each specified file. +--alias -a +type=list +Specify aliases for MIME types. Aliased MIME types are considered equivalent. +When copying to clipboard both the original and alias are made available on the +clipboard. When copying from clipboard if the original is not found, the alias +is used, as a fallback. Can be specified multiple times to create multiple +aliases. For example: :code:`--alias text/plain=text/x-rst` makes :code:`text/plain` an alias +of :code:`text/rst`. Aliases are not used in filter mode. + + --wait-for-completion type=bool-set Wait till the copy to clipboard is complete before exiting. Useful if running diff --git a/kitty/clipboard.py b/kitty/clipboard.py index 950f58fba..d33538a82 100644 --- a/kitty/clipboard.py +++ b/kitty/clipboard.py @@ -239,6 +239,7 @@ class WriteRequest: self.currently_writing_mime = '' self.current_leftover_bytes = memoryview(b'') self.max_size = (get_options().clipboard_max_size * 1024 * 1024) if max_size < 0 else max_size + self.aliases: Dict[str, str] = {} self.commited = False def encode_response(self, status: str = 'OK') -> bytes: @@ -254,6 +255,10 @@ class WriteRequest: self.commited = True cp = get_boss().primary_selection if self.is_primary_selection else get_boss().clipboard if cp.enabled: + for alias, src in self.aliases.items(): + pos = self.mime_map.get(src) + if pos is not None: + self.mime_map[alias] = pos x = {mime: self.tempfile.create_chunker(pos.start, pos.size) for mime, pos in self.mime_map.items()} cp.set_mime(x) @@ -346,6 +351,13 @@ class ClipboardRequestManager: protocol_type=ProtocolType.osc_5522, id=sanitize_id(m.get('id', '')) ) self.handle_write_request(self.in_flight_write_request) + elif typ == 'walias': + wr = self.in_flight_write_request + mime = m.get('mime', '') + if mime and wr is not None: + aliases = base64.standard_b64decode(epayload).decode('utf-8').split() + for alias in aliases: + wr.aliases[alias] = mime elif typ == 'wdata': wr = self.in_flight_write_request w = get_boss().window_id_map.get(self.window_id) diff --git a/tools/cmd/clipboard/read.go b/tools/cmd/clipboard/read.go index 1760077c6..5efb8e01e 100644 --- a/tools/cmd/clipboard/read.go +++ b/tools/cmd/clipboard/read.go @@ -139,14 +139,22 @@ func (self *Output) commit() { self.dest = nil } -func (self *Output) assign_mime_type(available_mimes []string) (err error) { +func (self *Output) assign_mime_type(available_mimes []string, aliases map[string][]string) (err error) { + if self.mime_type == "." { + self.remote_mime_type = "." + return + } if utils.Contains(available_mimes, self.mime_type) { self.remote_mime_type = self.mime_type return } - if self.mime_type == "." { - self.remote_mime_type = "." - return + if len(aliases[self.mime_type]) > 0 { + for _, alias := range aliases[self.mime_type] { + if utils.Contains(available_mimes, alias) { + self.remote_mime_type = alias + return + } + } } if images.EncodableImageTypes[self.mime_type] { for _, mt := range available_mimes { @@ -242,6 +250,19 @@ func parse_escape_code(etype loop.EscapeCodeType, data []byte) (metadata map[str return } +func parse_aliases(raw []string) (map[string][]string, error) { + ans := make(map[string][]string, len(raw)) + for _, x := range raw { + k, v, found := utils.Cut(x, "=") + if !found { + return nil, fmt.Errorf("%s is not valid MIME alias specification", x) + } + ans[k] = append(ans[k], v) + ans[v] = append(ans[v], k) + } + return ans, nil +} + func run_get_loop(opts *Options, args []string) (err error) { lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking) if err != nil { @@ -253,6 +274,10 @@ func run_get_loop(opts *Options, args []string) (err error) { requested_mimes := make(map[string]*Output) reading_available_mimes := true outputs := make([]*Output, len(args)) + aliases, merr := parse_aliases(opts.Alias) + if merr != nil { + return merr + } for i, arg := range args { outputs[i] = &Output{arg: arg, arg_is_stream: arg == "/dev/stdout" || arg == "/dev/stderr", ext: filepath.Ext(arg)} @@ -301,7 +326,7 @@ func run_get_loop(opts *Options, args []string) (err error) { return fmt.Errorf("The clipboard is empty") } for _, o := range outputs { - err = o.assign_mime_type(available_mimes) + err = o.assign_mime_type(available_mimes, aliases) if err != nil { return err } diff --git a/tools/cmd/clipboard/write.go b/tools/cmd/clipboard/write.go index fb47a896e..eea567ae7 100644 --- a/tools/cmd/clipboard/write.go +++ b/tools/cmd/clipboard/write.go @@ -8,6 +8,7 @@ import ( "io" "os" "path/filepath" + "strings" "kitty/tools/tui/loop" "kitty/tools/utils" @@ -23,13 +24,17 @@ type Input struct { mime_type string } -func write_loop(inputs []*Input) (err error) { +func write_loop(inputs []*Input, opts *Options) (err error) { lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking) if err != nil { return err } var waiting_for_write loop.IdType var buf [4096]byte + aliases, aerr := parse_aliases(opts.Alias) + if aerr != nil { + return aerr + } lp.OnInitialize = func() (string, error) { waiting_for_write = lp.QueueWriteString(encode(map[string]string{"type": "write"}, "")) @@ -47,6 +52,9 @@ func write_loop(inputs []*Input) (err error) { } if err != nil { if errors.Is(err, io.EOF) { + if len(aliases[i.mime_type]) > 0 { + lp.QueueWriteString(encode(map[string]string{"type": "walias", "mime": i.mime_type}, strings.Join(aliases[i.mime_type], " "))) + } inputs = inputs[1:] if len(inputs) == 0 { lp.QueueWriteString(encode(map[string]string{"type": "wdata"}, "")) @@ -151,5 +159,5 @@ func run_set_loop(opts *Options, args []string) (err error) { } to_process[i] = inputs[i] } - return write_loop(to_process) + return write_loop(to_process, opts) }