diff --git a/go.mod b/go.mod index 508c95d44..fe3314cc0 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/spf13/cobra v1.5.0 github.com/spf13/pflag v1.0.5 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa + golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c ) diff --git a/go.sum b/go.sum index 4c313c810..fe5c544ee 100644 --- a/go.sum +++ b/go.sum @@ -20,7 +20,11 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU= +golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/kitty/rc/base.py b/kitty/rc/base.py index 960a230fa..a4ef456cc 100644 --- a/kitty/rc/base.py +++ b/kitty/rc/base.py @@ -6,7 +6,7 @@ from contextlib import suppress from dataclasses import dataclass from typing import ( TYPE_CHECKING, Any, Callable, Dict, FrozenSet, Iterable, Iterator, - List, NoReturn, Optional, Set, Tuple, Type, Union, cast + List, NoReturn, Optional, Set, Tuple, Type, Union, cast, Mapping ) from kitty.cli import get_defaults_from_seq, parse_args, parse_option_spec @@ -88,6 +88,7 @@ CmdGenerator = Iterator[CmdReturnType] PayloadType = Optional[Union[CmdReturnType, CmdGenerator]] PayloadGetType = PayloadGetter ArgsType = List[str] +ImageCompletion: Dict[str, Tuple[str, Tuple[str, ...]]] = {'files': ('Images', ('*.png', '*.jpg', '*.jpeg', '*.webp', '*.gif', '*.bmp', '*.tiff'))} MATCH_WINDOW_OPTION = '''\ @@ -183,7 +184,7 @@ class ArgsHandling: json_field: str = '' count: Optional[int] = None spec: str = '' - completion: Optional[Dict[str, Tuple[str, Union[Callable[[], Iterable[str]], Tuple[str, ...]]]]] = None + completion: Optional[Mapping[str, Tuple[str, Union[Callable[[], Iterable[str]], Tuple[str, ...]]]]] = None value_if_unspecified: Tuple[str, ...] = () minimum_count: int = -1 first_rest: Optional[Tuple[str, str]] = None diff --git a/kitty/rc/set_background_image.py b/kitty/rc/set_background_image.py index 825f6949e..0bb1f036c 100644 --- a/kitty/rc/set_background_image.py +++ b/kitty/rc/set_background_image.py @@ -9,8 +9,9 @@ from typing import TYPE_CHECKING, Optional from kitty.types import AsyncResponse from .base import ( - MATCH_WINDOW_OPTION, ArgsType, Boss, CmdGenerator, NamedTemporaryFile, - PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, Window + MATCH_WINDOW_OPTION, ArgsType, Boss, CmdGenerator, ImageCompletion, + NamedTemporaryFile, PayloadGetType, PayloadType, RCOptions, RemoteCommand, + ResponseType, Window ) if TYPE_CHECKING: @@ -62,8 +63,8 @@ default=false Don't wait for a response from kitty. This means that even if setting the background image failed, the command will exit with a success code. ''' + '\n\n' + MATCH_WINDOW_OPTION - args = RemoteCommand.Args(spec='PATH_TO_PNG_IMAGE', count=1, json_field='data', special_parse='!read_window_logo(args[0])', completion={ - 'files': ('PNG Images', ('*.png',))}) + args = RemoteCommand.Args(spec='PATH_TO_PNG_IMAGE', count=1, json_field='data', special_parse='!read_window_logo(args[0])', + completion=ImageCompletion) reads_streaming_data = True def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: diff --git a/kitty/rc/set_window_logo.py b/kitty/rc/set_window_logo.py index 7de792322..92b3e1258 100644 --- a/kitty/rc/set_window_logo.py +++ b/kitty/rc/set_window_logo.py @@ -10,8 +10,9 @@ from typing import TYPE_CHECKING, Optional from kitty.types import AsyncResponse from .base import ( - MATCH_WINDOW_OPTION, ArgsType, Boss, CmdGenerator, NamedTemporaryFile, - PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, Window + MATCH_WINDOW_OPTION, ArgsType, Boss, CmdGenerator, ImageCompletion, + NamedTemporaryFile, PayloadGetType, PayloadType, RCOptions, RemoteCommand, + ResponseType, Window ) if TYPE_CHECKING: @@ -58,8 +59,7 @@ default=false Don't wait for a response from kitty. This means that even if setting the image failed, the command will exit with a success code. ''' - args = RemoteCommand.Args(spec='PATH_TO_PNG_IMAGE', count=1, json_field='data', special_parse='!read_window_logo(args[0])', completion={ - 'files': ('PNG Images', ('*.png',))}) + args = RemoteCommand.Args(spec='PATH_TO_PNG_IMAGE', count=1, json_field='data', special_parse='!read_window_logo(args[0])', completion=ImageCompletion) reads_streaming_data = True def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: diff --git a/tools/cmd/at/set_window_logo.go b/tools/cmd/at/set_window_logo.go index 8e8b3e53d..0cb1af4a9 100644 --- a/tools/cmd/at/set_window_logo.go +++ b/tools/cmd/at/set_window_logo.go @@ -3,12 +3,20 @@ package at import ( + "bytes" "encoding/base64" "fmt" + "image" + _ "image/gif" + _ "image/jpeg" + "image/png" "io" - "net/http" "os" "strings" + + _ "golang.org/x/image/bmp" + _ "golang.org/x/image/tiff" + _ "golang.org/x/image/webp" ) type struct_with_data interface { @@ -31,19 +39,31 @@ func read_window_logo(path string) (func(io_data *rc_io_data) (bool, error), err if err != nil { return nil, err } - buf := make([]byte, 2048) - n, err := f.Read(buf) - if err != nil && err != io.EOF { - f.Close() - return nil, err + var image_data_stream io.Reader + image_data_stream = f + config, format, ierr := image.DecodeConfig(f) + if ierr != nil { + return nil, fmt.Errorf("%s is not a supported image format", path) } - buf = buf[:n] + f.Seek(0, 0) - if http.DetectContentType(buf) != "image/png" { + if format != "png" { + f.Seek(0, 0) + img, _, err := image.Decode(f) + if err != nil { + f.Close() + } f.Close() - return nil, fmt.Errorf("%s is not a PNG image", path) + b := bytes.Buffer{} + b.Grow(config.Height * config.Width * 4) + err = png.Encode(&b, img) + if err != nil { + return nil, err + } + image_data_stream = &b } is_first_call := true + buf := make([]byte, 2048) return func(io_data *rc_io_data) (bool, error) { if is_first_call { @@ -51,18 +71,16 @@ func read_window_logo(path string) (func(io_data *rc_io_data) (bool, error), err } else { io_data.rc.Stream = false } - if len(buf) == 0 { - set_payload_data(io_data, "") - io_data.rc.Stream = false - return true, nil - } - set_payload_data(io_data, base64.StdEncoding.EncodeToString(buf)) buf = buf[:cap(buf)] - n, err := f.Read(buf) + n, err := image_data_stream.Read(buf) if err != nil && err != io.EOF { return false, err } buf = buf[:n] + set_payload_data(io_data, base64.StdEncoding.EncodeToString(buf)) + if err == io.EOF { + return true, nil + } return false, nil }, nil }