Compare commits

..

62 Commits

Author SHA1 Message Date
Kovid Goyal
491297ea1d When asking for permission to exec a shebang script also add options to view or edit the script 2023-05-12 16:02:47 +05:30
Kovid Goyal
c101a6acb0 Implement a dedicated function for word matching rather than relying on a regex and being at the mercy of the vagaries of regex implementations 2023-05-12 15:43:56 +05:30
Kovid Goyal
65f8bb7397 hints kitten: Switch to using a regex engine that supports lookaround
Note that we loose unicode char matching for --type=word because of
https://github.com/dlclark/regexp2/issues/65 and of course user regexps
cant use \p{N} escapes any more. Hopefully regexp2 will add support for
these soon-ish. IMO lookaround is more important than \p.

Fixes #6265
2023-05-12 12:24:59 +05:30
Kovid Goyal
5b8b91b6a3 Add support for OSC 1337 SetUserVar
See https://github.com/kovidgoyal/kitty/discussions/6229
2023-05-11 17:57:45 +05:30
Kovid Goyal
6a2edfa847 Merge branch 'pr-fix-shade' of https://github.com/MithicSpirit/kitty 2023-05-10 09:56:59 +05:30
MithicSpirit
28b84a2d5b Add support for 0x1fb90
Allocation in box_glyph_id is larger than necessary to account for the
addition of 0x1fb8c ... 0x1fb94 eventually, which are quite similar but
will require more work to add. Note that 0x1fb93 is not present in the
standard yet, but it is easy to guess what it will likely be from
context, so it should be kept in the allocation imo.
2023-05-09 22:19:03 -04:00
MithicSpirit
c247fe2336 Revert "Improve shade character appearance"
This reverts commit c883a024ba.

To maximize compatibility with the appearance in the standard.
2023-05-09 22:06:05 -04:00
MithicSpirit
c883a024ba Improve shade character appearance
I was really unhappy with the previous checkerboard appearance, so I
changed it to supersampled diagonal lines. The fill ratios are still the
same, so it should still be compliant with the standard if I understood
it correctly.

Feel free to revert (or tell me to revert) this commit if you want the
previous look.
2023-05-09 16:08:02 -04:00
Kovid Goyal
0cc38e1086 ... 2023-05-09 09:50:11 +05:30
Kovid Goyal
1777b87c45 Improve docs for reset the terminal 2023-05-09 09:44:05 +05:30
Kovid Goyal
e72975cc98 A new escape code that moves the current contents of the screen into the scrollback before clearing it 2023-05-09 09:32:39 +05:30
Kovid Goyal
8f15654985 Ensure kitty is rebuilt after publishing the nightly 2023-05-09 08:54:29 +05:30
Kovid Goyal
2408ccb635 ... 2023-05-09 08:48:37 +05:30
Kovid Goyal
a0cf4214df ... 2023-05-09 08:44:51 +05:30
Kovid Goyal
07203c67ca Add a note about why kitty terminfo does not have E3
See #6255
2023-05-09 08:28:54 +05:30
MithicSpirit
a36fe45181 Fix shade characters to follow unicode standard
- Light shade: 25% fill
- Medium shade: 50% fill
- Dark shade: 75% fill (implemented as inverse of light shade)
2023-05-08 18:46:24 -04:00
Kovid Goyal
061c444f20 ... 2023-05-08 16:36:47 +05:30
Kovid Goyal
a1d791083b ssh_kitten: Proper exit code for termination by SIGINT 2023-05-08 16:27:07 +05:30
Kovid Goyal
454acd4f5c ssh kitten: Fix a regression in 0.28.0 that caused interrupt during setup to not be handled gracefully
Fixes #6254
2023-05-08 16:18:05 +05:30
Kovid Goyal
71189aee9f Correct the type signature for callback 2023-05-08 16:03:27 +05:30
Kovid Goyal
23d7494e3a Fix #6251 2023-05-08 08:04:20 +05:30
Kovid Goyal
404f83a277 Add a link to awrit in the integrations page 2023-05-07 10:06:37 +05:30
Kovid Goyal
474244268c edit-in-kitty: Fix running edit-in-kitty with elevated privileges to edit a restricted file not working 2023-05-07 09:36:16 +05:30
Kovid Goyal
79cd6f38fe ... 2023-05-07 09:24:30 +05:30
Kovid Goyal
b7c3946f8f ... 2023-05-07 08:13:57 +05:30
Kovid Goyal
537cabca71 kitty +open: Ask for permission before executing script files that are not marked as executable
This prevents accidental execution of script files via MIME type
association from programs that unconditionally "open"
attachments/downloaded files via MIME type associations.
2023-05-07 08:11:39 +05:30
Kovid Goyal
79c19562b5 When publishing stash untracked files as well 2023-05-07 07:42:25 +05:30
Kovid Goyal
52afc79476 Fix re-using an image id for an animated image for a still image causing a crash
Fixes #6244
2023-05-06 09:37:55 +05:30
Kovid Goyal
877d8d7008 ... 2023-05-04 10:36:02 +05:30
Kovid Goyal
ce70320a62 ... 2023-05-04 10:26:18 +05:30
Kovid Goyal
3eb18a416a Entry point for parsing theme metadata 2023-05-04 10:14:58 +05:30
Kovid Goyal
8ba7258db9 Merge branch 'fix-bash-intergration-var-leak' of https://github.com/syyyr/kitty 2023-05-04 08:05:33 +05:30
Václav Kubernát
a502e94950 bash_integration: Do not leak variable i
With shell-integration, the user would see the last value of this
variable (as set by the shell-integration script.

Fix this by making it local.
2023-05-03 18:35:30 +02:00
Kovid Goyal
ea5634b3fd When parsing theme metadata ignore the name if it is the placeholder value from the template 2023-05-03 21:55:33 +05:30
Kovid Goyal
87943079fb Fix #6238 2023-05-03 21:40:42 +05:30
Kovid Goyal
a77b2b20c2 Fix #6230 2023-05-03 18:25:07 +05:30
Kovid Goyal
8f96395f74 diff kitten: Fix a regression in 0.28.0 that broke using relative paths as arguments to the kitten
Fixes #6235
2023-05-03 08:34:46 +05:30
Kovid Goyal
1fc4e53bea hints kitten: Fix a regression in 0.28.0 that broke using sub-groups in regexp captures
Fixes #6228
2023-04-30 21:16:24 +05:30
Kovid Goyal
f6ccd2ad2c Dont apply linear2srgb in borders with bg image as the cell shader doesnt apply it then either 2023-04-30 10:19:35 +05:30
Kovid Goyal
bc2af4acf9 Update changelog 2023-04-30 09:09:09 +05:30
Kovid Goyal
07dbfaa297 Fix #6224 2023-04-30 08:28:02 +05:30
Kovid Goyal
8020d5823b Fix #6225 2023-04-30 07:15:56 +05:30
Kovid Goyal
73f10aaf43 clipboard kitten: Fix a bug causing the last MIME type available on the clipboard not being recognized when pasting with arbitrary MIME types 2023-04-30 06:48:09 +05:30
Kovid Goyal
59c4d4a4bd DRYer 2023-04-28 20:30:15 +05:30
Kovid Goyal
ef999c9024 Also show stderr from tmux on failure 2023-04-28 20:16:37 +05:30
Kovid Goyal
514888a274 Use FindExe to find the tmux executable and return a nicer error message when running tmux fails 2023-04-28 20:11:15 +05:30
Kovid Goyal
09ebdcd809 Allow using set_tab_title without a pre-filled title. Fixes #6217 2023-04-28 10:14:25 +05:30
Kovid Goyal
8ebe4084cc Merge branch 'fix/6209-background_opacity_fringing' of https://github.com/m4rw3r/kitty 2023-04-28 09:28:09 +05:30
Martin Wernstål
9f41183628 fix: account for incorrect gamma-blending performed by compositor on transparent windows
Fixes #6209
2023-04-27 18:35:06 +02:00
Martin Wernstål
289957ef1c style: use ifdef to be consistent with the other cases 2023-04-27 18:34:00 +02:00
Martin Wernstål
920b350ac9 feat: more exact sRGB approximation 2023-04-27 18:27:46 +02:00
Kovid Goyal
d14655f644 Merge pull request #6216 from jaseg/master
docs/basic.rst: Add resize window shortcut
2023-04-27 16:25:03 +05:30
jaseg
29583411e6 docs/basic.rst: Add resize window shortcut 2023-04-27 12:48:05 +02:00
Kovid Goyal
019359b219 show_key kitten: In kitty mode show the actual bytes sent by the terminal rather than a re-encoding of the parsed key event
Also port the kitten to Go
2023-04-26 21:48:53 +05:30
Kovid Goyal
7b6d11fd1e Fix rendering of :doc: links with explicit titles in help text in the terminal 2023-04-26 16:46:20 +05:30
Kovid Goyal
bb33c66570 Fix #6213 2023-04-26 16:38:25 +05:30
Kovid Goyal
c2fc4eadc8 unicode_input: Only serialize favorites if no user config exists 2023-04-26 16:02:18 +05:30
Kovid Goyal
a7b4d07601 unicode_input kitten: Fix a regression in 0.28.0 that caused the order of recent and favorites entries to not be respected
Fixes #6214
2023-04-26 15:55:56 +05:30
Kovid Goyal
6a07435bb0 hints kitten: Fix regression causing editing of favorites to sometimes hang 2023-04-26 15:23:38 +05:30
Kovid Goyal
93a5107e79 Fix #6202 2023-04-21 21:35:59 +05:30
Kovid Goyal
6cc8e67580 Add example code to get screen size in Bash 2023-04-21 15:18:30 +05:30
Kovid Goyal
ccdb951716 Website: Fix optimization of social preview images 2023-04-21 14:19:36 +05:30
52 changed files with 934 additions and 325 deletions

View File

@@ -58,6 +58,7 @@ Action Shortcut
New window :sc:`new_window` (also :kbd:`⌘+↩` on macOS)
New OS window :sc:`new_os_window` (also :kbd:`⌘+n` on macOS)
Close window :sc:`close_window` (also :kbd:`⇧+⌘+d` on macOS)
Resize window :sc:`start_resizing_window` (also :kbd:`⌘+r` on macOS)
Next window :sc:`next_window`
Previous window :sc:`previous_window`
Move window forward :sc:`move_window_forward`

View File

@@ -61,6 +61,7 @@ Build-time dependencies:
- ``libfontconfig-dev``
- ``libx11-xcb-dev``
- ``liblcms2-dev``
- ``libssl-dev``
- ``libpython3-dev``
- ``librsync-dev``

View File

@@ -35,6 +35,36 @@ mouse anywhere in the current command to move the cursor there. See
Detailed list of changes
-------------------------------------
0.28.2 [future]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- A new escape code ``<ESC>[22J`` that moves the current contents of the screen into the scrollback before clearing it
- unicode_input kitten: Fix a regression in 0.28.0 that caused the order of recent and favorites entries to not be respected (:iss:`6214`)
- unicode_input kitten: Fix a regression in 0.28.0 that caused editing of favorites to sometimes hang
- clipboard kitten: Fix a bug causing the last MIME type available on the clipboard not being recognized when pasting
- Fix regression in 0.28.0 causing color fringing when rendering in transparent windows on light backgrounds (:iss:`6209`)
- show_key kitten: In kitty mode show the actual bytes sent by the terminal rather than a re-encoding of the parsed key event
- hints kitten: Fix a regression in 0.28.0 that broke using sub-groups in regexp captures (:iss:`6228`)
- hints kitten: Fix a regression in 0.28.0 that broke using lookahead/lookbehind in regexp captures (:iss:`6265`)
- diff kitten: Fix a regression in 0.28.0 that broke using relative paths as arguments to the kitten (:iss:`6325`)
- Fix re-using the image id of an animated image for a still image causing a crash (:iss:`6244`)
- kitty +open: Ask for permission before executing script files that are not marked as executable. This prevents accidental execution
of script files via MIME type association from programs that unconditionally "open" attachments/downloaded files
- edit-in-kitty: Fix running edit-in-kitty with elevated privileges to edit a restricted file not working (:disc:`6245`)
- ssh kitten: Fix a regression in 0.28.0 that caused interrupt during setup to not be handled gracefully (:iss:`6254`)
0.28.1 [2023-04-21]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -58,7 +58,8 @@ Getting the window size
In order to know what size of images to display and how to position them, the
client must be able to get the window size in pixels and the number of cells
per row and column. This can be done by using the ``TIOCGWINSZ`` ioctl. Some
per row and column. The cell width is then simply the window size divided by the
number of rows. This can be done by using the ``TIOCGWINSZ`` ioctl. Some
code to demonstrate its use
.. tab:: C
@@ -99,6 +100,20 @@ code to demonstrate its use
fmt.Println("rows: %v columns: %v width: %v height %v", sz.Row, sz.Col, sz.Xpixel, sz.Ypixel)
.. tab:: Bash
.. code-block:: sh
#!/bin/bash
# This uses the kitten standalone binary from kitty to get the pixel sizes
# since we cant do IOCTLs directly. Fortunately, kitten is a static exe
# pre-built for every Unix like OS under the sun.
builtin read -r rows cols < <(command stty size)
IFS=x builtin read -r width height < <(command kitten icat --print-window-size); builtin unset IFS
builtin echo "number of rows: $rows number of columns: $cols screen width: $width screen height: $height"
Note that some terminals return ``0`` for the width and height values. Such
terminals should be modified to return the correct values. Examples of

View File

@@ -80,6 +80,11 @@ base application that uses kitty's graphics protocol for images.
A text mode WWW browser that supports kitty's graphics protocol to display
images.
`awrit <https://github.com/chase/awrit>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A full Chromium based web browser running in the terminal using kitty's
graphics protocol.
.. _tool_mpv:
`mpv <https://github.com/mpv-player/mpv/commit/874e28f4a41a916bb567a882063dd2589e9234e1>`_

2
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/alecthomas/chroma/v2 v2.7.0
github.com/bmatcuk/doublestar/v4 v4.6.0
github.com/disintegration/imaging v1.6.2
github.com/dlclark/regexp2 v1.9.0
github.com/google/go-cmp v0.5.9
github.com/google/uuid v1.3.0
github.com/jamesruan/go-rfc1924 v0.0.0-20170108144916-2767ca7c638f
@@ -18,7 +19,6 @@ require (
)
require (
github.com/dlclark/regexp2 v1.9.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect

View File

@@ -60,7 +60,7 @@ func extra_for(width, screen_width int) int {
return utils.Max(0, screen_width-width)/2 + 1
}
func choices(o *Options) (response string, err error) {
func GetChoices(o *Options) (response string, err error) {
response = ""
lp, err := loop.New()
if err != nil {

View File

@@ -33,7 +33,7 @@ func main(_ *cli.Command, o *Options, args []string) (rc int, err error) {
}
switch o.Type {
case "yesno", "choices":
result.Response, err = choices(o)
result.Response, err = GetChoices(o)
if err != nil {
return 1, err
}

View File

@@ -343,7 +343,7 @@ func run_get_loop(opts *Options, args []string) (err error) {
if reading_available_mimes {
switch metadata["status"] {
case "DATA":
available_mimes = strings.Split(utils.UnsafeBytesToString(payload), " ")
available_mimes = utils.Map(strings.TrimSpace, strings.Split(utils.UnsafeBytesToString(payload), " "))
case "OK":
case "DONE":
reading_available_mimes = false

View File

@@ -258,6 +258,10 @@ func resolve_remote_name(path, defval string) string {
}
func walk(base string, patterns []string, names *utils.Set[string], pmap, path_name_map map[string]string) error {
base, err := filepath.Abs(base)
if err != nil {
return err
}
return filepath.WalkDir(base, func(path string, d fs.DirEntry, err error) error {
is_allowed := allowed(path, patterns...)
if !is_allowed {

View File

@@ -90,7 +90,8 @@ for the operating system. Various special values are supported:
copy the match to the specified buffer, e.g. :code:`@a`
:code:`default`
run the default open program.
run the default open program. Note that when using the hyperlink :code:`--type`
the default is to use the kitty :doc:`hyperlink handling </open_actions>` facilities.
:code:`launch`
run :doc:`/launch` to open the program in a new kitty tab, window, overlay, etc.
@@ -349,8 +350,9 @@ def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int,
else:
from kitty.conf.utils import to_cmdline
cwd = data['cwd']
program = get_options().open_url_with if program == 'default' else program
if text_type == 'hyperlink':
is_default_program = program == 'default'
program = get_options().open_url_with if is_default_program else program
if text_type == 'hyperlink' and is_default_program:
w = boss.window_id_map.get(target_window_id)
for m in matches:
if w is not None:

View File

@@ -13,9 +13,12 @@ import (
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"github.com/dlclark/regexp2"
"github.com/seancfoley/ipaddress-go/ipaddr"
"golang.org/x/exp/slices"
)
@@ -252,15 +255,6 @@ func functions_for(opts *Options) (pattern string, post_processors []PostProcess
// IPv6 with no validation
`(?:[a-fA-F0-9]{0,4}:){2,7}[a-fA-F0-9]{1,4})`)
post_processors = append(post_processors, PostProcessorMap()["ip"])
case "word":
chars := opts.WordCharacters
if chars == "" {
chars = RelevantKittyOpts().Select_by_word_characters
}
chars = regexp.QuoteMeta(chars)
chars = strings.ReplaceAll(chars, "-", "\\-")
pattern = fmt.Sprintf(`[%s\pL\pN]{%d,}`, chars, opts.MinimumMatchLength)
post_processors = append(post_processors, PostProcessorMap()["brackets"], PostProcessorMap()["quotes"])
default:
pattern = opts.Regex
if opts.Type == "linenum" {
@@ -274,11 +268,112 @@ func functions_for(opts *Options) (pattern string, post_processors []PostProcess
return
}
func mark(r *regexp.Regexp, post_processors []PostProcessorFunc, group_processors []GroupProcessorFunc, text string, opts *Options) (ans []Mark) {
type Capture struct {
Text string
Text_as_runes []rune
Byte_Offsets struct {
Start, End int
}
Rune_Offsets struct {
Start, End int
}
}
func (self Capture) String() string {
return fmt.Sprintf("Capture(start=%d, end=%d, %#v)", self.Byte_Offsets.Start, self.Byte_Offsets.End, self.Text)
}
type Group struct {
Name string
IsNamed bool
Captures []Capture
}
func (self Group) LastCapture() Capture {
if len(self.Captures) == 0 {
return Capture{}
}
return self.Captures[len(self.Captures)-1]
}
func (self Group) String() string {
return fmt.Sprintf("Group(name=%#v, captures=%v)", self.Name, self.Captures)
}
type Match struct {
Groups []Group
}
func (self Match) HasNamedGroups() bool {
for _, g := range self.Groups {
if g.IsNamed {
return true
}
}
return false
}
func find_all_matches(re *regexp2.Regexp, text string) (ans []Match, err error) {
m, err := re.FindStringMatch(text)
if err != nil {
return
}
rune_to_bytes := utils.RuneOffsetsToByteOffsets(text)
get_byte_offset_map := func(groups []regexp2.Group) (ans map[int]int, err error) {
ans = make(map[int]int, len(groups)*2)
rune_offsets := make([]int, 0, len(groups)*2)
for _, g := range groups {
for _, c := range g.Captures {
if _, found := ans[c.Index]; !found {
rune_offsets = append(rune_offsets, c.Index)
ans[c.Index] = -1
}
end := c.Index + c.Length
if _, found := ans[end]; !found {
rune_offsets = append(rune_offsets, end)
ans[end] = -1
}
}
}
slices.Sort(rune_offsets)
for _, pos := range rune_offsets {
if ans[pos] = rune_to_bytes(pos); ans[pos] < 0 {
return nil, fmt.Errorf("Matches are not monotonic cannot map rune offsets to byte offsets")
}
}
return
}
for m != nil {
groups := m.Groups()
bom, err := get_byte_offset_map(groups)
if err != nil {
return nil, err
}
match := Match{Groups: make([]Group, len(groups))}
for i, g := range m.Groups() {
match.Groups[i].Name = g.Name
match.Groups[i].IsNamed = g.Name != "" && g.Name != strconv.Itoa(i)
for _, c := range g.Captures {
cn := Capture{Text: c.String(), Text_as_runes: c.Runes()}
cn.Rune_Offsets.End = c.Index + c.Length
cn.Rune_Offsets.Start = c.Index
cn.Byte_Offsets.Start, cn.Byte_Offsets.End = bom[c.Index], bom[cn.Rune_Offsets.End]
match.Groups[i].Captures = append(match.Groups[i].Captures, cn)
}
}
ans = append(ans, match)
m, _ = re.FindNextMatch(m)
}
return
}
func mark(r *regexp2.Regexp, post_processors []PostProcessorFunc, group_processors []GroupProcessorFunc, text string, opts *Options) (ans []Mark) {
sanitize_pat := regexp.MustCompile("[\r\n\x00]")
names := r.SubexpNames()
for i, v := range r.FindAllStringSubmatchIndex(text, -1) {
match_start, match_end := v[0], v[1]
all_matches, _ := find_all_matches(r, text)
for i, m := range all_matches {
full_capture := m.Groups[0].LastCapture()
match_start, match_end := full_capture.Byte_Offsets.Start, full_capture.Byte_Offsets.End
for match_end > match_start+1 && text[match_end-1] == 0 {
match_end--
}
@@ -296,14 +391,14 @@ func mark(r *regexp.Regexp, post_processors []PostProcessorFunc, group_processor
continue
}
full_match = sanitize_pat.ReplaceAllLiteralString(text[match_start:match_end], "")
gd := make(map[string]string, len(names))
for x, name := range names {
if name != "" {
idx := 2 * x
if s, e := v[idx], v[idx+1]; s > -1 && e > -1 {
gd := make(map[string]string, len(m.Groups))
for idx, g := range m.Groups {
if idx > 0 && g.IsNamed {
c := g.LastCapture()
if s, e := c.Byte_Offsets.Start, c.Byte_Offsets.End; s > -1 && e > -1 {
s = utils.Max(s, match_start)
e = utils.Min(e, match_end)
gd[name] = sanitize_pat.ReplaceAllLiteralString(text[s:e], "")
gd[g.Name] = sanitize_pat.ReplaceAllLiteralString(text[s:e], "")
}
}
}
@@ -314,15 +409,89 @@ func mark(r *regexp.Regexp, post_processors []PostProcessorFunc, group_processor
for k, v := range gd {
gd2[k] = v
}
ans = append(ans, Mark{
Index: i, Start: match_start, End: match_end, Text: full_match, Groupdict: gd2,
})
if opts.Type == "regex" && len(m.Groups) > 1 && !m.HasNamedGroups() {
cp := m.Groups[1].LastCapture()
ms, me := cp.Byte_Offsets.Start, cp.Byte_Offsets.End
match_start = utils.Max(match_start, ms)
match_end = utils.Min(match_end, me)
full_match = sanitize_pat.ReplaceAllLiteralString(text[match_start:match_end], "")
}
if full_match != "" {
ans = append(ans, Mark{
Index: i, Start: match_start, End: match_end, Text: full_match, Groupdict: gd2,
})
}
}
return
}
type ErrNoMatches struct{ Type string }
func is_word_char(ch rune, current_chars []rune) bool {
return unicode.IsLetter(ch) || unicode.IsNumber(ch) || (unicode.IsMark(ch) && len(current_chars) > 0 && unicode.IsLetter(current_chars[len(current_chars)-1]))
}
func mark_words(text string, opts *Options) (ans []Mark) {
left := text
var current_run struct {
chars []rune
start, size int
}
chars := opts.WordCharacters
if chars == "" {
chars = RelevantKittyOpts().Select_by_word_characters
}
allowed_chars := make(map[rune]bool, len(chars))
for _, ch := range chars {
allowed_chars[ch] = true
}
pos := 0
post_processors := []PostProcessorFunc{PostProcessorMap()["brackets"], PostProcessorMap()["quotes"]}
commit_run := func() {
if len(current_run.chars) >= opts.MinimumMatchLength {
match_start, match_end := current_run.start, current_run.start+current_run.size
for _, f := range post_processors {
match_start, match_end = f(text, match_start, match_end)
if match_start < 0 {
break
}
}
if match_start > -1 && match_end > match_start {
full_match := text[match_start:match_end]
if len([]rune(full_match)) >= opts.MinimumMatchLength {
ans = append(ans, Mark{
Index: len(ans), Start: match_start, End: match_end, Text: full_match,
})
}
}
}
current_run.chars = nil
current_run.start = 0
current_run.size = 0
}
for {
ch, size := utf8.DecodeRuneInString(left)
if ch == utf8.RuneError {
break
}
if allowed_chars[ch] || is_word_char(ch, current_run.chars) {
if len(current_run.chars) == 0 {
current_run.start = pos
}
current_run.chars = append(current_run.chars, ch)
current_run.size += size
} else {
commit_run()
}
left = left[size:]
pos += size
}
commit_run()
return
}
func adjust_python_offsets(text string, marks []Mark) error {
// python returns rune based offsets (unicode chars not utf-8 bytes)
adjust := utils.RuneOffsetsToByteOffsets(text)
@@ -356,7 +525,7 @@ func find_marks(text string, opts *Options, cli_args ...string) (sanitized_text
run_basic_matching := func() error {
pattern, post_processors, group_processors := functions_for(opts)
r, err := regexp.Compile(pattern)
r, err := regexp2.Compile(pattern, regexp2.RE2)
if err != nil {
return fmt.Errorf("Failed to compile the regex pattern: %#v with error: %w", pattern, err)
}
@@ -393,6 +562,8 @@ func find_marks(text string, opts *Options, cli_args ...string) (sanitized_text
}
} else if opts.Type == "hyperlink" {
ans = hyperlinks
} else if opts.Type == "word" {
ans = mark_words(text, opts)
} else {
err = run_basic_matching()
if err != nil {

View File

@@ -47,7 +47,7 @@ func TestHintMarking(t *testing.T) {
for _, m := range marks {
q := strings.NewReplacer("\n", "", "\r", "", "\x00", "").Replace(ptext[m.Start:m.End])
if diff := cmp.Diff(m.Text, q); diff != "" {
t.Fatalf("Mark start and end dont point to correct offset in text for %#v\n%s", text, diff)
t.Fatalf("Mark start (%d) and end (%d) dont point to correct offset in text for %#v\n%s", m.Start, m.End, text, diff)
}
}
return
@@ -107,6 +107,18 @@ func TestHintMarking(t *testing.T) {
r(`255.255.255.256`)
r(`:1`)
reset()
opts.Type = "regex"
opts.Regex = `(?ms)^[*]?\s(\S+)`
r(`* 2b687c2 - test1`, `2b687c2`)
opts.Regex = `(?<=got: )sha256.{4}`
r(`got: sha256-L8=`, `sha256-L8=`)
reset()
opts.Type = "word"
r(`#one (two) 😍 a-1b `, `#one`, `two`, `a-1b`)
r("fōtiz час a\u0310b ", `fōtiz`, `час`, "a\u0310b")
reset()
tdir := t.TempDir()
simple := filepath.Join(tdir, "simple.py")

View File

@@ -4,11 +4,33 @@
import sys
from typing import List, Optional
from ..show_key.kitty_mode import format_mods
from kitty.key_encoding import ALT, CAPS_LOCK, CTRL, HYPER, META, NUM_LOCK, SHIFT, SUPER
from ..tui.handler import Handler
from ..tui.loop import Loop, MouseEvent
from ..tui.operations import MouseTracking
mod_names = {
SHIFT: 'Shift',
ALT: 'Alt',
CTRL: 'Ctrl',
SUPER: 'Super',
HYPER: 'Hyper',
META: 'Meta',
NUM_LOCK: 'NumLock',
CAPS_LOCK: 'CapsLock',
}
def format_mods(mods: int) -> str:
if not mods:
return ''
lmods = []
for m, name in mod_names.items():
if mods & m:
lmods.append(name)
return '+'.join(lmods)
class Mouse(Handler):
mouse_tracking = MouseTracking.full

78
kittens/show_key/kitty.go Normal file
View File

@@ -0,0 +1,78 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package show_key
import (
"fmt"
"strings"
"kitty/tools/cli/markup"
"kitty/tools/tui/loop"
)
var _ = fmt.Print
func csi(csi string) string {
return "CSI " + strings.NewReplacer(":", " : ", ";", " ; ").Replace(csi[:len(csi)-1]) + " " + csi[len(csi)-1:]
}
func run_kitty_loop(opts *Options) (err error) {
lp, err := loop.New(loop.FullKeyboardProtocol)
if err != nil {
return err
}
ctx := markup.New(true)
lp.OnInitialize = func() (string, error) {
lp.SetCursorVisible(false)
lp.SetWindowTitle("kitty extended keyboard protocol demo")
lp.Println("Press any keys - Ctrl+C or Ctrl+D will terminate")
return "", nil
}
lp.OnKeyEvent = func(e *loop.KeyEvent) (err error) {
e.Handled = true
if e.MatchesPressOrRepeat("ctrl+c") || e.MatchesPressOrRepeat("ctrl+d") {
lp.Quit(0)
return
}
mods := e.Mods.String()
if mods != "" {
mods += "+"
}
etype := e.Type.String()
key := e.Key
if key == " " {
key = "space"
}
key = mods + key
lp.Printf("%s %s %s\r\n", ctx.Green(key), ctx.Yellow(etype), e.Text)
lp.Println(ctx.Cyan(csi(e.CSI)))
if e.AlternateKey != "" || e.ShiftedKey != "" {
if e.ShiftedKey != "" {
lp.QueueWriteString(ctx.Dim("Shifted key: "))
lp.QueueWriteString(e.ShiftedKey + " ")
}
if e.AlternateKey != "" {
lp.QueueWriteString(ctx.Dim("Alternate key: "))
lp.QueueWriteString(e.AlternateKey + " ")
}
lp.Println()
}
lp.Println()
return
}
err = lp.Run()
if err != nil {
return
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return
}
return
}

View File

@@ -1,79 +0,0 @@
#!/usr/bin/env python3
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from kittens.tui.handler import Handler
from kittens.tui.loop import Loop
from kitty.key_encoding import ALT, CAPS_LOCK, CTRL, HYPER, META, NUM_LOCK, PRESS, RELEASE, REPEAT, SHIFT, SUPER, KeyEvent, encode_key_event
mod_names = {
SHIFT: 'Shift',
ALT: 'Alt',
CTRL: 'Ctrl',
SUPER: 'Super',
HYPER: 'Hyper',
META: 'Meta',
NUM_LOCK: 'NumLock',
CAPS_LOCK: 'CapsLock',
}
def format_mods(mods: int) -> str:
if not mods:
return ''
lmods = []
for m, name in mod_names.items():
if mods & m:
lmods.append(name)
return '+'.join(lmods)
class KeysHandler(Handler):
def initialize(self) -> None:
self.cmd.set_window_title('kitty extended keyboard protocol demo')
self.cmd.set_cursor_visible(False)
self.print('Press any keys - Ctrl+C or Ctrl+D will terminate')
def on_key_event(self, key_event: KeyEvent, in_bracketed_paste: bool = False) -> None:
etype = {
PRESS: 'PRESS',
REPEAT: 'REPEAT',
RELEASE: 'RELEASE'
}[key_event.type]
mods = format_mods(key_event.mods)
if mods:
mods += '+'
kk = key_event.key
if kk == ' ':
kk = 'SPACE'
key = f'{mods}{kk} '
self.cmd.colored(key, 'green')
self.cmd.colored(etype + ' ', 'yellow')
self.cmd.styled(key_event.text, italic=True)
self.print()
rep = f'CSI {encode_key_event(key_event)[2:]}'
rep = rep.replace(';', ' ; ').replace(':', ' : ')[:-1] + ' ' + rep[-1]
self.cmd.styled(rep, fg='magenta')
if (key_event.shifted_key or key_event.alternate_key):
self.print()
if key_event.shifted_key:
self.cmd.colored('Shifted key: ', 'gray')
self.print(key_event.shifted_key + ' ', end='')
if key_event.alternate_key:
self.cmd.colored('Alternate key: ', 'gray')
self.print(key_event.alternate_key + ' ', end='')
self.print()
self.print()
def on_interrupt(self) -> None:
self.quit_loop(0)
def on_eot(self) -> None:
self.quit_loop(0)
def main() -> None:
loop = Loop()
handler = KeysHandler()
loop.loop(handler)
raise SystemExit(loop.return_code)

View File

@@ -0,0 +1,76 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package show_key
import (
"errors"
"fmt"
"io"
"kitty/tools/cli/markup"
"kitty/tools/tty"
"os"
)
var _ = fmt.Print
func print_key(buf []byte, ctx *markup.Context) {
const ctrl_keys = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
unix := ""
send_text := ""
for _, ch := range buf {
switch {
case int(ch) < len(ctrl_keys):
unix += "^" + ctrl_keys[ch:ch+1]
case ch == 127:
unix += "^?"
default:
unix += string(rune(ch))
}
}
for _, ch := range string(buf) {
q := fmt.Sprintf("%#v", string(ch))
send_text += q[1 : len(q)-1]
}
os.Stdout.WriteString(unix + "\t\t")
os.Stdout.WriteString(ctx.Yellow(send_text) + "\r\n")
}
func run_legacy_loop(opts *Options) (err error) {
term, err := tty.OpenControllingTerm(tty.SetRaw)
if err != nil {
return err
}
defer func() {
term.RestoreAndClose()
}()
if opts.KeyMode != "unchanged" {
os.Stdout.WriteString("\x1b[?1")
switch opts.KeyMode {
case "normal":
os.Stdout.WriteString("l")
default:
os.Stdout.WriteString("h")
}
defer func() {
os.Stdout.WriteString("\x1b[?1l")
}()
}
fmt.Print("Press any keys - Ctrl+D will terminate this program\r\n")
ctx := markup.New(true)
fmt.Print(ctx.Green("UNIX\t\tsend_text\r\n"))
buf := make([]byte, 64)
for {
n, err := term.Read(buf)
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return err
}
print_key(buf[:n], ctx)
if n == 1 && buf[0] == 4 {
break
}
}
return
}

27
kittens/show_key/main.go Normal file
View File

@@ -0,0 +1,27 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package show_key
import (
"fmt"
"kitty/tools/cli"
)
var _ = fmt.Print
func main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) {
if opts.KeyMode == "kitty" {
err = run_kitty_loop(opts)
} else {
err = run_legacy_loop(opts)
}
if err != nil {
rc = 1
}
return
}
func EntryPoint(parent *cli.Command) {
create_cmd(parent, main)
}

View File

@@ -2,56 +2,9 @@
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
import os
import sys
from typing import List
from kittens.tui.operations import raw_mode, styled
from kitty.cli import parse_args
from kitty.cli_stub import ShowKeyCLIOptions
ctrl_keys = '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_'
def print_key(raw: bytearray) -> None:
unix = ''
for ch in raw:
if ch < len(ctrl_keys):
unix += f'^{ctrl_keys[ch]}'
elif ch == 127:
unix += '^?'
else:
unix += chr(ch)
print(unix + '\t\t', end='')
for ch in raw:
x = chr(ch).encode('utf-8')
print(styled(repr(x)[2:-1], fg='yellow'), end='')
print(end='\r\n', flush=True)
def read_keys() -> None:
fd = sys.stdin.fileno()
while True:
try:
raw = bytearray(os.read(fd, 64))
except OSError as err:
print(err, file=sys.stderr, flush=True)
break
if not raw:
break
print_key(raw)
if len(raw) == 1 and raw[0] == 4:
break
def legacy_main() -> None:
print('Press any keys - Ctrl+D will terminate this program', end='\r\n', flush=True)
print(styled('UNIX', italic=True, fg='green'), styled('send_text', italic=True, fg='green'), sep='\t\t', end='\r\n')
with raw_mode():
read_keys()
OPTIONS = r'''
--key-mode -m
default=normal
@@ -66,17 +19,7 @@ usage = ''
def main(args: List[str]) -> None:
cli_opts, items = parse_args(args[1:], OPTIONS, '', help_text, 'kitty +kitten show_key', result_class=ShowKeyCLIOptions)
if cli_opts.key_mode == 'kitty':
from .kitty_mode import main as kitty_main
return kitty_main()
if cli_opts.key_mode != 'unchanged':
print(end='\x1b[?1' + ('l' if cli_opts.key_mode == 'normal' else 'h'), flush=True)
try:
return legacy_main()
finally:
if cli_opts.key_mode != 'unchanged':
print(end='\x1b[?1l', flush=True)
raise SystemExit('This should be reun as kitten show_key')
if __name__ == '__main__':
@@ -86,3 +29,4 @@ elif __name__ == '__doc__':
cd['usage'] = usage
cd['options'] = OPTIONS
cd['help_text'] = help_text
cd['short_desc'] = help_text

View File

@@ -78,6 +78,9 @@ func TestSSHConfigParsing(t *testing.T) {
rt(`unset ["a"]`)
conf = "env LOCAL_ENV=_kitty_copy_env_var_"
rt(`export ["LOCAL_ENV","LOCAL_VAL",false]`)
conf = "env a=b\nhostname 2\ncolor_scheme xyz"
hostname = "2"
rt()
ci, err := ParseCopyInstruction("--exclude moose --dest=target " + cf)
if err != nil {

View File

@@ -16,6 +16,7 @@ import (
"net/url"
"os"
"os/exec"
"os/signal"
"os/user"
"path"
"path/filepath"
@@ -669,6 +670,9 @@ func run_ssh(ssh_args, server_args, found_extra_args []string) (rc int, err erro
if err != nil {
return 1, err
}
sigs := make(chan os.Signal, 8)
signal.Notify(sigs, unix.SIGINT, unix.SIGTERM)
if !cd.request_data {
rq := fmt.Sprintf("id=%s:pwfile=%s:pw=%s", cd.replacements["REQUEST_ID"], cd.replacements["PASSWORD_FILENAME"], cd.replacements["DATA_PASSWORD"])
err := term.ApplyOperations(tty.TCSANOW, tty.SetNoEcho)
@@ -685,11 +689,22 @@ func run_ssh(ssh_args, server_args, found_extra_args []string) (rc int, err erro
return 1, err
}
}
go func() {
_ = <-sigs
// ignore any interrupt and terminate signals as they will usually be sent to the ssh child process as well
// and we are waiting on that.
}()
err = c.Wait()
drain_potential_tty_garbage(term)
signal.Reset(unix.SIGINT, unix.SIGTERM)
if err != nil {
var exit_err *exec.ExitError
if errors.As(err, &exit_err) {
if state := exit_err.ProcessState.String(); state == "signal: interrupt" {
unix.Kill(os.Getpid(), unix.SIGINT)
// Give the signal time to be delivered
time.Sleep(20 * time.Millisecond)
}
return exit_err.ExitCode(), nil
}
return 1, err

View File

@@ -77,7 +77,11 @@ by spaces. The hostname can include an optional username in the form
first hostname specification is found. Note that matching of hostname is done
against the name you specify on the command line to connect to the remote host.
If you wish to include the same basic configuration for many different hosts,
you can do so with the :ref:`include <include>` directive.
you can do so with the :ref:`include <include>` directive. In version 0.28.0
the behavior of this option was changed slightly, now, when a hostname is encountered
all its config values are set to defaults instead of being inherited from a previous
matching hostname block. In particular it means hostnames dont inherit configurations,
thereby avoiding hard to understand action-at-a-distance.
''')
opt('interpreter', 'sh', long_text='''

View File

@@ -3,7 +3,11 @@
package themes
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
@@ -96,3 +100,48 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
func EntryPoint(parent *cli.Command) {
create_cmd(parent, main)
}
func parse_theme_metadata() error {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
paths := utils.Splitlines(utils.UnsafeBytesToString(raw))
ans := make([]*themes.ThemeMetadata, 0, len(paths))
for _, path := range paths {
if path != "" {
metadata, _, err := themes.ParseThemeMetadata(path)
if metadata.Name == "" {
metadata.Name = themes.ThemeNameFromFileName(filepath.Base(path))
}
if err != nil {
return err
}
ans = append(ans, metadata)
}
}
raw, err = json.Marshal(ans)
if err != nil {
return err
}
_, err = os.Stdout.Write(raw)
if err != nil {
return err
}
return nil
}
func ParseEntryPoint(parent *cli.Command) {
parent.AddSubCommand(&cli.Command{
Name: "__parse_theme_metadata__",
Hidden: true,
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
err = parse_theme_metadata()
if err != nil {
rc = 1
}
return
},
})
}

View File

@@ -92,6 +92,7 @@ func serialize_favorites(favs []rune) string {
}
var loaded_favorites []rune
var favorites_loaded_from_user_config bool
func favorites_path() string {
return filepath.Join(utils.ConfigDir(), "unicode-input-favorites.conf")
@@ -102,8 +103,10 @@ func load_favorites(refresh bool) []rune {
raw, err := os.ReadFile(favorites_path())
if err == nil {
loaded_favorites = parse_favorites(utils.UnsafeBytesToString(raw))
favorites_loaded_from_user_config = true
} else {
loaded_favorites = DEFAULT_SET
favorites_loaded_from_user_config = false
}
}
return loaded_favorites
@@ -427,40 +430,43 @@ func (self *handler) handle_favorites_key_event(event *loop.KeyEvent) {
self.lp.Quit(1)
return
}
raw := serialize_favorites(load_favorites(false))
fp := favorites_path()
err = os.MkdirAll(filepath.Dir(fp), 0o755)
if err != nil {
self.err = fmt.Errorf("Failed to create config directory to store favorites in: %w", err)
self.lp.Quit(1)
return
if len(load_favorites(false)) == 0 || !favorites_loaded_from_user_config {
raw := serialize_favorites(load_favorites(false))
err = os.MkdirAll(filepath.Dir(fp), 0o755)
if err != nil {
self.err = fmt.Errorf("Failed to create config directory to store favorites in: %w", err)
self.lp.Quit(1)
return
}
err = utils.AtomicUpdateFile(fp, utils.UnsafeStringToBytes(raw), 0o600)
if err != nil {
self.err = fmt.Errorf("Failed to write to favorites file %s with error: %w", fp, err)
self.lp.Quit(1)
return
}
}
err = utils.AtomicUpdateFile(fp, utils.UnsafeStringToBytes(raw), 0o600)
if err != nil {
self.err = fmt.Errorf("Failed to write to favorites file %s with error: %w", fp, err)
self.lp.Quit(1)
return
}
resume, err := self.lp.Suspend()
err = self.lp.SuspendAndRun(func() error {
cmd := exec.Command(exe, "edit-in-kitty", "--type=overlay", fp)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err == nil {
load_favorites(true)
} else {
fmt.Fprintln(os.Stderr, err)
fmt.Fprintln(os.Stderr, "Failed to run edit-in-kitty, favorites have not been changed. Press Enter to continue.")
var ln string
fmt.Scanln(&ln)
}
return nil
})
if err != nil {
self.err = err
self.lp.Quit(1)
return
}
defer resume()
cmd := exec.Command(exe, "edit-in-kitty", "--type=overlay", fp)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err == nil {
load_favorites(true)
} else {
fmt.Fprintln(os.Stderr, err)
fmt.Fprintln(os.Stderr, "Failed to run edit-in-kitty, favorites have not been changed. Press Enter to continue.")
var ln string
fmt.Scanln(&ln)
}
}
}

View File

@@ -82,7 +82,7 @@ func (self *table) current_codepoint() rune {
func (self *table) set_codepoints(codepoints []rune, mode Mode, current_idx int) {
self.codepoints = codepoints
if self.codepoints != nil {
if self.codepoints != nil && mode != FAVORITES && mode != HEX {
slices.Sort(self.codepoints)
}
self.mode = mode

View File

@@ -1,7 +1,7 @@
#version GLSL_VERSION
uniform uvec2 viewport;
uniform uint colors[9];
uniform float background_opacity;
uniform float background_opacity, do_srgb_correction;
uniform float tint_opacity, tint_premult;
uniform float gamma_lut[256];
in vec4 rect; // left, top, right, bottom
@@ -22,6 +22,14 @@ const uvec2 pos_map[] = uvec2[4](
uvec2(LEFT, TOP)
);
float linear2srgb(float x) {
// Linear to sRGB conversion. Needed to match alpha from the cell shader
float lower = 12.92 * x;
float upper = 1.055 * pow(x, 1.0f / 2.4f) - 0.055f;
return mix(lower, upper, step(0.0031308f, x));
}
float to_color(uint c) {
return gamma_lut[c & FF];
}
@@ -45,5 +53,6 @@ void main() {
color3 = is_window_bg * window_bg + (1. - is_window_bg) * color3;
float final_opacity = is_default_bg * tint_opacity + (1. - is_default_bg) * background_opacity;
float final_premult_opacity = is_default_bg * tint_premult + (1. - is_default_bg) * background_opacity;
final_opacity = do_srgb_correction * linear2srgb(final_opacity) + (1. - do_srgb_correction) * final_opacity;
color = vec4(color3 * final_premult_opacity, final_opacity);
}

View File

@@ -10,6 +10,7 @@ import sys
from contextlib import contextmanager, suppress
from functools import partial
from gettext import gettext as _
from gettext import ngettext
from time import monotonic, sleep
from typing import (
TYPE_CHECKING,
@@ -1010,9 +1011,8 @@ class Boss:
tm = tab.tab_manager_ref()
if tm is not None:
tm.set_active_tab(tab)
self.confirm(_(
'Are you sure you want to close this tab, it has {}'
' windows running?').format(num),
self.confirm(ngettext('Are you sure you want to close this tab, it has one window running?',
'Are you sure you want to close this tab, it has {} windows running?', num).format(num),
self.handle_close_tab_confirmation, tab.id,
window=tab.active_window,
)
@@ -1602,8 +1602,8 @@ class Boss:
if tm is not None:
w = tm.active_window
self.confirm(
_('Are you sure you want to close this OS window, it has {}'
' windows running?').format(num),
ngettext('Are you sure you want to close this OS window, it has one window running?',
'Are you sure you want to close this OS window, it has {} windows running', num).format(num),
self.handle_close_os_window_confirmation, os_window_id,
window=w,
)
@@ -1642,7 +1642,8 @@ class Boss:
return
assert tm is not None
self.confirm(
_('Are you sure you want to quit kitty, it has {} windows running?').format(num),
ngettext('Are you sure you want to quit kitty, it has one window running?',
'Are you sure you want to quit kitty, it has {} windows running?', num).format(num),
self.handle_quit_confirmation,
window=tm.active_window,
)
@@ -1812,7 +1813,8 @@ class Boss:
'tab', '''
Change the title of the active tab interactively, by typing in the new title.
If you specify an argument to this action then that is used as the title instead of asking for it.
Use the empty string ("") to reset the title to default. For example::
Use the empty string ("") to reset the title to default. Use a space (" ") to indicate that the
prompt should not be pre-filled. For example::
# interactive usage
map f1 set_tab_title
@@ -1820,19 +1822,24 @@ class Boss:
map f2 set_tab_title some title
# reset to default
map f3 set_tab_title ""
# interactive usage without prefilled prompt
map f3 set_tab_title " "
'''
)
def set_tab_title(self, title: Optional[str] = None) -> None:
tab = self.active_tab
if tab:
if title is not None:
if title is not None and title not in ('" "', "' '"):
if title in ('""', "''"):
title = ''
tab.set_title(title)
return
prefilled = tab.name or tab.title
if title in ('" "', "' '"):
prefilled = ''
args = [
'--name=tab-title', '--message', _('Enter the new title for this tab below.'),
'--default', tab.name or tab.title, 'do_set_tab_title', str(tab.id)]
'--default', prefilled, 'do_set_tab_title', str(tab.id)]
self.run_kitten_with_metadata('ask', args)
def do_set_tab_title(self, title: str, tab_id: int) -> None:
@@ -2628,7 +2635,7 @@ class Boss:
where = 'new' if args[0] == 'new-tab' else args[0][4:]
return self._move_window_to(target_tab_id=where)
ct = self.active_tab
items: List[Tuple[Union[str, int], str]] = [(t.id, t.title) for t in self.all_tabs if t is not ct]
items: List[Tuple[Union[str, int], str]] = [(t.id, t.effective_title) for t in self.all_tabs if t is not ct]
items.append(('new_tab', 'New tab'))
items.append(('new_os_window', 'New OS Window'))
target_window = self.active_window

View File

@@ -37,6 +37,22 @@ in float colored_sprite;
out vec4 final_color;
// Util functions {{{
float linear2srgb(float x) {
// Linear to sRGB conversion.
float lower = 12.92 * x;
float upper = 1.055 * pow(x, 1.0f / 2.4f) - 0.055f;
return mix(lower, upper, step(0.0031308f, x));
}
float srgb2linear(float x) {
// sRGB to linear conversion
float lower = x / 12.92;
float upper = pow((x + 0.055f) / 1.055f, 2.4f);
return mix(lower, upper, step(0.04045f, x));
}
vec4 alpha_blend(vec4 over, vec4 under) {
// Alpha blend two colors returning the resulting color pre-multiplied by its alpha
// and its alpha.
@@ -103,20 +119,9 @@ vec4 vec4_premul(vec4 rgba) {
#ifdef NEEDS_FOREGROUND
// sRGB luminance values
const vec3 Y = vec3(0.2126, 0.7152, 0.0722);
const float gamma_factor = 2.2;
// Scaling factor for the extra text-alpha adjustment for luminance-difference.
const float text_gamma_scaling = 0.5;
float linear2srgb(float x) {
// Approximation of linear-to-sRGB conversion
return pow(x, 1.0 / gamma_factor);
}
float srgb2linear(float x) {
// Approximation of sRGB-to-linear conversion
return pow(x, gamma_factor);
}
float clamp_to_unit_float(float x) {
// Clamp value to suitable output range
return clamp(x, 0.0f, 1.0f);
@@ -179,6 +184,12 @@ void main() {
vec4 fg = calculate_foreground(background);
#ifdef TRANSPARENT
final_color = alpha_blend_premul(fg, vec4_premul(background, bg_alpha));
// Adjust the transparent alpha-channel to account for incorrect
// gamma-blending performed by the compositor (true for at least wlroots,
// picom, GNOME, MacOS).
// This is the last pass:
final_color.a = linear2srgb(final_color.a);
#else
final_color = alpha_blend_premul(fg, background);
#endif
@@ -193,7 +204,7 @@ void main() {
#endif
#ifdef BACKGROUND
#if defined(TRANSPARENT)
#ifdef TRANSPARENT
final_color = vec4_premul(background, bg_alpha);
#else
final_color = vec4(background, draw_bg);
@@ -202,6 +213,10 @@ void main() {
#ifdef FOREGROUND
final_color = calculate_foreground(); // pre-multiplied foreground
// This is the last pass, adjust alpha to compensate for gamma-incorrect
// blending in compositor:
final_color.a = linear2srgb(final_color.a);
#endif
}

View File

@@ -429,7 +429,7 @@ class Child:
return environ_of_process(pid)
return {}
def send_signal_for_key(self, key_num: int) -> bool:
def send_signal_for_key(self, key_num: bytes) -> bool:
import signal
import termios
if self.child_fd is None:

View File

@@ -94,6 +94,7 @@ def edit(args: List[str]) -> None:
def shebang(args: List[str]) -> None:
from kitty.constants import kitten_exe
script_path = args[1]
cmd = args[2:]
if cmd == ['__ext__']:
@@ -111,7 +112,7 @@ def shebang(args: List[str]) -> None:
cmd = line.split(' ')
else:
cmd = line.split(' ', maxsplit=1)
os.execvp(cmd[0], cmd + [script_path])
os.execvp(kitten_exe(), ['kitten', '__confirm_and_run_shebang__'] + cmd + [script_path])
def run_kitten(args: List[str]) -> None:

View File

@@ -543,6 +543,7 @@ START_ALLOW_CASE_RANGE
case 0xe0b0 ... 0xe0bf: // powerline box drawing
case 0x1fb00 ... 0x1fb8b: // symbols for legacy computing
case 0x1fba0 ... 0x1fbae:
case 0x1fb90: // inverse medium shade
return BOX_FONT;
default:
*is_emoji_presentation = has_emoji_presentation(cpu_cell, gpu_cell);
@@ -585,6 +586,9 @@ START_ALLOW_CASE_RANGE
return 0x151 + ch - 0x1fba0;
case 0x2800 ... 0x28ff:
return 0x160 + ch - 0x2800;
case 0x1fb90:
// Allocated to allow for 0x1fb8c ... 0x1fb94 eventually
return 0x25f + ch - 0x1fb8c;
default:
return 0xffff;
}

View File

@@ -617,37 +617,18 @@ def shade(buf: BufType, width: int, height: int, light: bool = False, invert: bo
square_sz = max(1, width // 12)
number_of_rows = height // square_sz
number_of_cols = width // square_sz
nums = tuple(range(square_sz))
dest = bytearray(width * height) if invert else buf
nums = range(square_sz)
for r in range(number_of_rows):
is_odd = r % 2 != 0
if is_odd:
continue
fill_even = r % 4 == 0
for yr in nums:
y = r * square_sz + yr
if y >= height:
break
off = width * y
for c in range(number_of_cols):
if light:
fill = (c % 4) == (0 if fill_even else 2)
else:
fill = (c % 2 == 0) == fill_even
if fill:
for xc in nums:
x = (c * square_sz) + xc
if x >= width:
break
dest[off + x] = 255
if invert:
for y in range(height):
off = width * y
for x in range(width):
q = off + x
buf[q] = 255 - dest[q]
for c in range(number_of_cols):
if invert ^ ((r % 2 != c % 2) or (light and r % 2 == 1)):
continue
for yr in nums:
y = r * square_sz + yr
offset = width * y
for xc in nums:
x = c * square_sz + xc
buf[offset + x] = 255
def quad(buf: BufType, width: int, height: int, x: int = 0, y: int = 0) -> None:
@@ -883,7 +864,8 @@ box_chars: Dict[str, List[Callable[[BufType, int, int], Any]]] = {
'': [p(eight_block, which=(4, 5, 6, 7))],
'': [p(shade, light=True)],
'': [shade],
'': [p(shade, invert=True)],
'': [p(shade, light=True, invert=True)],
'🮐': [p(shade, invert=True)],
'': [p(eight_bar, horizontal=True)],
'': [p(eight_bar, which=7)],
'': [p(quad, y=1)],

View File

@@ -561,6 +561,7 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_
img->root_frame_data_loaded = false;
img->is_drawn = false;
img->current_frame_shown_at = 0;
img->extra_framecnt = 0;
free_image(self, img);
*is_dirty = true;
self->layers_dirty = true;

View File

@@ -715,7 +715,7 @@ class EditCmd:
self.is_local_file = False
with suppress(OSError):
st = os.stat(self.file_localpath)
self.is_local_file = (st.st_dev, st.st_ino) == self.file_inode
self.is_local_file = (st.st_dev, st.st_ino) == self.file_inode and os.access(self.file_localpath, os.W_OK | os.R_OK)
if not self.is_local_file:
import tempfile
self.tdir = tempfile.mkdtemp()

View File

@@ -3990,18 +3990,31 @@ You can create shortcuts to clear/reset the terminal. For example::
If you want to operate on all kitty windows instead of just the current one, use
:italic:`all` instead of :italic:`active`.
It is also possible to remap :kbd:`Ctrl+L` to both scroll the current screen
Some useful functions that can be defined in the shell rc files to perform various kinds of
clearing of the current window:
.. code-block:: sh
clear-only-screen() {
printf "\e[H\e[2J"
}
clear-screen-and-scrollback() {
printf "\e[H\e[3J"
}
clear-screen-saving-contents-in-scrollback() {
printf "\e[H\e[22J"
}
For instance, using these functions, it is possible to remap :kbd:`Ctrl+L` to both scroll the current screen
contents into the scrollback buffer and clear the screen, instead of just
clearing the screen, for example, for ZSH add the following to :file:`~/.zshrc`:
clearing the screen. For ZSH, in :file:`~/.zshrc` after the above functions, add:
.. code-block:: zsh
scroll-and-clear-screen() {
printf '\\n%.0s' {1..$LINES}
zle clear-screen
}
zle -N scroll-and-clear-screen
bindkey '^l' scroll-and-clear-screen
zle -N clear-screen-saving-contents-in-scrollback
bindkey '^l' clear-screen-saving-contents-in-scrollback
'''
)

View File

@@ -422,6 +422,7 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
case 9:
case 99:
case 777:
case 1337:
START_DISPATCH
DISPATCH_OSC_WITH_CODE(desktop_notify)
END_DISPATCH

View File

@@ -1628,6 +1628,26 @@ screen_clear_scrollback(Screen *self) {
}
}
static Line* visual_line_(Screen *self, int y_);
static void
screen_move_into_scrollback(Screen *self) {
if (self->linebuf != self->main_linebuf || self->margin_top != 0 || self->margin_bottom != self->lines - 1) return;
unsigned int num_of_lines_to_move = self->lines;
while (num_of_lines_to_move) {
Line *line = visual_line_(self, num_of_lines_to_move-1);
if (!line_is_empty(line)) break;
num_of_lines_to_move--;
}
if (num_of_lines_to_move) {
unsigned int top, bottom;
for (; num_of_lines_to_move; num_of_lines_to_move--) {
top = 0, bottom = num_of_lines_to_move - 1;
INDEX_UP
}
}
}
void
screen_erase_in_display(Screen *self, unsigned int how, bool private) {
/* Erases display in a specific way.
@@ -1640,6 +1660,8 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) {
including cursor position.
* ``2`` -- Erases complete display. All lines are erased
and changed to single-width. Cursor does not move.
* ``22`` -- Copy screen contents into scrollback if in main screen,
then do the same as ``2``.
* ``3`` -- Erase complete display and scrollback buffer as well.
:param bool private: when ``True`` character attributes are left unchanged
*/
@@ -1649,6 +1671,10 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) {
a = self->cursor->y + 1; b = self->lines; break;
case 1:
a = 0; b = self->cursor->y; break;
case 22:
screen_move_into_scrollback(self);
how = 2;
/* fallthrough */
case 2:
case 3:
grman_clear(self->grman, how == 3, self->cell_size);
@@ -1671,7 +1697,7 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) {
self->is_dirty = true;
clear_selection(&self->selections);
}
if (how != 2) {
if (how < 2) {
screen_erase_in_line(self, how, private);
if (how == 1) linebuf_clear_attrs_and_dirty(self->linebuf, self->cursor->y);
}

View File

@@ -434,8 +434,13 @@ draw_bg(OSWindow *w) {
glUniform1i(bgimage_program_layout.image_location, BGIMAGE_UNIT);
glUniform1f(bgimage_program_layout.opacity_location, OPT(background_opacity));
#ifdef __APPLE__
int window_width = w->window_width, window_height = w->window_height;
#else
int window_width = w->viewport_width, window_height = w->viewport_height;
#endif
glUniform4f(bgimage_program_layout.sizes_location,
(GLfloat)w->viewport_width, (GLfloat)w->viewport_height, (GLfloat)w->bgimage->width, (GLfloat)w->bgimage->height);
(GLfloat)window_width, (GLfloat)window_height, (GLfloat)w->bgimage->width, (GLfloat)w->bgimage->height);
glUniform1f(bgimage_program_layout.premult_location, w->is_semi_transparent ? 1.f : 0.f);
GLfloat tiled = 0.f;;
GLfloat left = -1.0, top = 1.0, right = 1.0, bottom = -1.0;
@@ -446,12 +451,12 @@ draw_bg(OSWindow *w) {
tiled = 0.f; break;
case CENTER_CLAMPED:
tiled = 1.f;
if (w->viewport_width > (int)w->bgimage->width) {
GLfloat frac = (w->viewport_width - w->bgimage->width) / (GLfloat)w->viewport_width;
if (window_width > (int)w->bgimage->width) {
GLfloat frac = (window_width - w->bgimage->width) / (GLfloat)window_width;
left += frac; right += frac;
}
if (w->viewport_height > (int)w->bgimage->height) {
GLfloat frac = (w->viewport_height - w->bgimage->height) / (GLfloat)w->viewport_height;
if (window_height > (int)w->bgimage->height) {
GLfloat frac = (window_height - w->bgimage->height) / (GLfloat)window_height;
top -= frac; bottom -= frac;
}
break;
@@ -979,7 +984,7 @@ draw_cells(ssize_t vao_idx, ssize_t gvao_idx, const ScreenRenderData *srd, float
// }}}
// Borders {{{
enum BorderUniforms { BORDER_viewport, BORDER_background_opacity, BORDER_tint_opacity, BORDER_tint_premult, BORDER_colors, BORDER_gamma_lut, NUM_BORDER_UNIFORMS };
enum BorderUniforms { BORDER_viewport, BORDER_background_opacity, BORDER_tint_opacity, BORDER_tint_premult, BORDER_colors, BORDER_gamma_lut, BORDER_do_srgb_correction, NUM_BORDER_UNIFORMS };
static GLint border_uniform_locations[NUM_BORDER_UNIFORMS] = {0};
static void
@@ -989,6 +994,7 @@ init_borders_program(void) {
SET_LOC(background_opacity)
SET_LOC(tint_opacity)
SET_LOC(tint_premult)
SET_LOC(do_srgb_correction)
SET_LOC(colors)
SET_LOC(gamma_lut)
#undef SET_LOC
@@ -1012,6 +1018,7 @@ draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_bu
float background_opacity = w->is_semi_transparent ? w->background_opacity: 1.0f;
float tint_opacity = background_opacity;
float tint_premult = background_opacity;
float do_srgb_correction = 1.0f;
if (has_bgimage(w)) {
glEnable(GL_BLEND);
BLEND_ONTO_OPAQUE;
@@ -1020,6 +1027,7 @@ draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_bu
background_opacity = 1.0f;
tint_opacity = OPT(background_tint) * OPT(background_tint_gaps);
tint_premult = w->is_semi_transparent ? OPT(background_tint) : 1.0f;
do_srgb_correction = 0.0f;
}
if (num_border_rects) {
@@ -1041,6 +1049,7 @@ draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_bu
glUniform1f(border_uniform_locations[BORDER_background_opacity], background_opacity);
glUniform1f(border_uniform_locations[BORDER_tint_opacity], tint_opacity);
glUniform1f(border_uniform_locations[BORDER_tint_premult], tint_premult);
glUniform1f(border_uniform_locations[BORDER_do_srgb_correction], do_srgb_correction);
glUniform2ui(border_uniform_locations[BORDER_viewport], viewport_width, viewport_height);
glUniform1fv(border_uniform_locations[BORDER_gamma_lut], 256, srgb_lut);
if (has_bgimage(w)) {

View File

@@ -111,6 +111,10 @@ string_capabilities = {
'civis': r'\E[?25l',
# Clear screen
'clear': r'\E[H\E[2J',
# Clear scrollback. This is disabled because the clear program on Linux by default, not as
# an option, uses it and nukes the scrollback. What's more this behavior was silently changed
# around 2013. Given clear is maintained as part of ncurses this kind of crap is no surprise.
# 'E3': r'\E[3J',
# Make cursor appear normal
'cnorm': r'\E[?12h\E[?25h',
# Carriage return

View File

@@ -597,6 +597,7 @@ class Window:
self.default_title = os.path.basename(child.argv[0] or appname)
self.child_title = self.default_title
self.title_stack: Deque[str] = deque(maxlen=10)
self.user_vars: Dict[str, bytes] = {}
self.id: int = add_window(tab.os_window_id, tab.id, self.title)
self.clipboard_request_manager = ClipboardRequestManager(self.id)
self.margin = EdgeWidths()
@@ -922,7 +923,27 @@ class Window:
self.override_title = title or None
self.title_updated()
def set_user_var(self, key: str, val: Optional[bytes]) -> None:
self.user_vars.pop(key, None) # ensure key will be newest in user_vars even if already present
if len(self.user_vars) > 64: # dont store too many user vars
oldest_key = next(iter(self.user_vars))
self.user_vars.pop(oldest_key)
if val is not None:
self.user_vars[key] = val
# screen callbacks {{{
def osc_1337(self, raw_data: str) -> None:
for record in raw_data.split(';'):
key, _, val = record.partition('=')
if key == 'SetUserVar':
from base64 import standard_b64decode
ukey, has_equal, uval = val.partition('=')
self.set_user_var(ukey, (standard_b64decode(uval) if uval else b'') if has_equal == '=' else None)
def desktop_notify(self, osc_code: int, raw_data: str) -> None:
if osc_code == 1337:
self.osc_1337(raw_data)
if osc_code == 777:
if not raw_data.startswith('notify;'):
log_error(f'Ignoring unknown OSC 777: {raw_data}')
@@ -932,7 +953,6 @@ class Window:
if cmd is not None and osc_code == 99:
self.prev_osc99_cmd = cmd
# screen callbacks {{{
def use_utf8(self, on: bool) -> None:
get_boss().child_monitor.set_iutf8_winid(self.id, on)
@@ -1004,7 +1024,7 @@ class Window:
'--ssh-connection-data', json.dumps(conn_data)
)
def send_signal_for_key(self, key_num: int) -> bool:
def send_signal_for_key(self, key_num: bytes) -> bool:
try:
return self.child.send_signal_for_key(key_num)
except OSError as err:

View File

@@ -154,7 +154,7 @@ def run_website(args: Any) -> None:
f.write(version)
shutil.copy2(os.path.join(docs_dir, 'installer.sh'), publish_dir)
os.chdir(os.path.dirname(publish_dir))
subprocess.check_call(['optipng', '-o7', 'kitty/_images/social_previews/*.png'])
subprocess.check_call(['optipng', '-o7'] + glob.glob('kitty/_images/social_previews/*.png'))
subprocess.check_call(['git', 'add', 'kitty'])
subprocess.check_call(['git', 'commit', '-m', 'kitty website updates'])
subprocess.check_call(['git', 'push'])
@@ -493,7 +493,7 @@ def safe_read(path: str) -> str:
@contextmanager
def change_to_git_master() -> Generator[None, None, None]:
stash_ref_before = safe_read('.git/refs/stash')
subprocess.check_call(['git', 'stash'])
subprocess.check_call(['git', 'stash', '-u'])
try:
branch_before = current_branch()
if branch_before != 'master':
@@ -548,6 +548,7 @@ def main() -> None:
with change_to_git_master():
building_nightly = True
exec_actions(NIGHTLY_ACTIONS, args)
subprocess.run(['make', 'debug'])
return
require_git_master()
if args.action == 'all':

View File

@@ -80,7 +80,7 @@ _ksi_prompt=(
)
_ksi_main() {
builtin local ifs="$IFS"
builtin local ifs="$IFS" i
IFS=" "
for i in ${KITTY_SHELL_INTEGRATION[@]}; do
case "$i" in

View File

@@ -24,7 +24,7 @@ exec_kitty() {
is_wrapped_kitten() {
wrapped_kittens="clipboard icat hyperlinked_grep ask hints unicode_input ssh themes diff"
wrapped_kittens="clipboard icat hyperlinked_grep ask hints unicode_input ssh themes diff show_key"
[ -n "$1" ] && {
case " $wrapped_kittens " in
*" $1 "*) printf "%s" "$1" ;;

View File

@@ -140,11 +140,12 @@ func (self *Context) Prettify(text string) string {
return self.ref_hyperlink(val, "envvar-")
case "doc":
text, target := text_and_target(val)
if text == target {
target = strings.Trim(target, "/")
if title, ok := kitty.DocTitleMap[target]; ok {
val = title + " <" + target + ">"
}
no_title := text == target
target = strings.Trim(target, "/")
if title, ok := kitty.DocTitleMap[target]; ok && no_title {
val = title + " <" + target + ">"
} else {
val = text + " <" + target + ">"
}
return self.ref_hyperlink(val, "doc-")
case "iss":

View File

@@ -0,0 +1,70 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package tool
import (
"fmt"
"os"
"os/exec"
"golang.org/x/sys/unix"
"kitty/kittens/ask"
"kitty/tools/cli"
"kitty/tools/cli/markup"
"kitty/tools/utils"
)
var _ = fmt.Print
func ask_for_permission(script_path string) (response string, err error) {
opts := &ask.Options{Type: "choices", Default: "n", Choices: []string{"y;green:Yes", "n;red:No", "v;yellow:View", "e;magenta:Edit"}}
ctx := markup.New(true)
opts.Message = ctx.Prettify(fmt.Sprintf(
"Attempting to execute the script: :yellow:`%s`\nExecuting untrusted scripts can be dangerous. Proceed anyway?", script_path))
response, err = ask.GetChoices(opts)
return response, err
}
func confirm_and_run_shebang(args []string) (rc int, err error) {
script_path := args[len(args)-1]
if unix.Access(script_path, unix.X_OK) != nil {
response, err := ask_for_permission(script_path)
if err != nil {
return 1, err
}
switch response {
default:
return 1, fmt.Errorf("Execution of %s was denied by user", script_path)
case "v":
raw, err := os.ReadFile(script_path)
if err != nil {
return 1, err
}
cli.ShowHelpInPager(utils.UnsafeBytesToString(raw))
return confirm_and_run_shebang(args)
case "e":
exe, err := os.Executable()
if err != nil {
return 1, err
}
editor := exec.Command(exe, "edit-in-kitty", script_path)
editor.Stdin = os.Stdin
editor.Stdout = os.Stdout
editor.Stderr = os.Stderr
editor.Run()
return confirm_and_run_shebang(args)
case "y":
}
}
exe := utils.FindExe(args[0])
if exe == "" {
return 1, fmt.Errorf("Failed to find the script interpreter: %s", args[0])
}
err = unix.Exec(exe, args, os.Environ())
if err != nil {
rc = 1
}
return
}

View File

@@ -11,6 +11,7 @@ import (
"kitty/kittens/hints"
"kitty/kittens/hyperlinked_grep"
"kitty/kittens/icat"
"kitty/kittens/show_key"
"kitty/kittens/ssh"
"kitty/kittens/themes"
"kitty/kittens/unicode_input"
@@ -41,6 +42,8 @@ func KittyToolEntryPoints(root *cli.Command) {
ssh.EntryPoint(root)
// unicode_input
unicode_input.EntryPoint(root)
// show_key
show_key.EntryPoint(root)
// hyperlinked_grep
hyperlinked_grep.EntryPoint(root)
// ask
@@ -51,6 +54,7 @@ func KittyToolEntryPoints(root *cli.Command) {
diff.EntryPoint(root)
// themes
themes.EntryPoint(root)
themes.ParseEntryPoint(root)
// __pytest__
pytest.EntryPoint(root)
// __hold_till_enter__
@@ -63,4 +67,13 @@ func KittyToolEntryPoints(root *cli.Command) {
return
},
})
// __confirm_and_run_shebang__
root.AddSubCommand(&cli.Command{
Name: "__confirm_and_run_shebang__",
Hidden: true,
OnlyArgsAllowed: true,
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
return confirm_and_run_shebang(args)
},
})
}

View File

@@ -463,7 +463,7 @@ type ThemeMetadata struct {
Author string `json:"author"`
}
func parse_theme_metadata(path string) (*ThemeMetadata, map[string]string, error) {
func ParseThemeMetadata(path string) (*ThemeMetadata, map[string]string, error) {
var in_metadata, in_blurb, finished_metadata bool
ans := ThemeMetadata{}
settings := map[string]string{}
@@ -507,7 +507,9 @@ func parse_theme_metadata(path string) (*ThemeMetadata, map[string]string, error
val = strings.TrimSpace(val)
switch key {
case "name":
ans.Name = val
if val != "The name of the theme (if not present, derived from filename)" {
ans.Name = val
}
case "author":
ans.Author = val
case "upstream":
@@ -782,7 +784,7 @@ var camel_case_pat = (&utils.Once[*regexp.Regexp]{Run: func() *regexp.Regexp {
return regexp.MustCompile(`([a-z])([A-Z])`)
}}).Get
func theme_name_from_file_name(fname string) string {
func ThemeNameFromFileName(fname string) string {
fname = fname[:len(fname)-len(path.Ext(fname))]
fname = strings.ReplaceAll(fname, "_", " ")
fname = camel_case_pat().ReplaceAllString(fname, "$1 $2")
@@ -814,12 +816,12 @@ func (self *Themes) Filtered(is_ok func(*Theme) bool) *Themes {
}
func (self *Themes) AddFromFile(path string) (*Theme, error) {
m, conf, err := parse_theme_metadata(path)
m, conf, err := ParseThemeMetadata(path)
if err != nil {
return nil, err
}
if m.Name == "" {
m.Name = theme_name_from_file_name(filepath.Base(path))
m.Name = ThemeNameFromFileName(filepath.Base(path))
}
t := Theme{metadata: m, is_user_defined: true, settings: conf, path_for_user_defined_theme: path}
self.name_map[m.Name] = &t

View File

@@ -26,7 +26,7 @@ func TestThemeCollections(t *testing.T) {
"mooseCat": "Moose Cat",
"a_bC": "A B C",
} {
actual := theme_name_from_file_name(fname)
actual := ThemeNameFromFileName(fname)
if diff := cmp.Diff(expected, actual); diff != "" {
t.Fatalf("Unexpected theme name for %s:\n%s", fname, diff)
}
@@ -36,7 +36,7 @@ func TestThemeCollections(t *testing.T) {
pt := func(expected ThemeMetadata, lines ...string) {
os.WriteFile(filepath.Join(tdir, "temp.conf"), []byte(strings.Join(lines, "\n")), 0o600)
actual, _, err := parse_theme_metadata(filepath.Join(tdir, "temp.conf"))
actual, _, err := ParseThemeMetadata(filepath.Join(tdir, "temp.conf"))
if err != nil {
t.Fatal(err)
}

View File

@@ -66,8 +66,9 @@ type Loop struct {
style_ctx style.Context
atomic_update_active bool
// Suspend the loop restoring terminal state. Call the return resume function to restore the loop
Suspend func() (func() error, error)
// Suspend the loop restoring terminal state, and run the provided function. When it returns terminal state is
// put back to what it was before suspending unless the function returns an error or an error occurs saving/restoring state.
SuspendAndRun func(func() error) error
// Callbacks

View File

@@ -105,6 +105,9 @@ type KeyEvent struct {
AlternateKey string
Text string
Handled bool
// The CSI string this key event was decoded from. Empty if not decoded from CSI.
CSI string
}
func (self *KeyEvent) String() string {
@@ -133,6 +136,7 @@ func KeyEventFromCSI(csi string) *KeyEvent {
if len(csi) == 0 {
return nil
}
orig_csi := csi
last_char := csi[len(csi)-1:]
if !strings.Contains("u~ABCDEHFPQRS", last_char) || (last_char == "~" && (csi == "200~" || csi == "201~")) {
return nil
@@ -165,7 +169,7 @@ func KeyEventFromCSI(csi string) *KeyEvent {
if len(sections) > 2 {
third_section = get_sub_sections(sections[2], 0)
}
var ans = KeyEvent{Type: PRESS}
var ans = KeyEvent{Type: PRESS, CSI: orig_csi}
var keynum int
if val, ok := letter_trailer_to_csi_number_map[last_char]; ok {
keynum = val

View File

@@ -271,10 +271,8 @@ func (self *Loop) run() (err error) {
self.keep_going = true
self.pending_mouse_events = utils.NewRingBuffer[MouseEvent](4)
tty_read_channel := make(chan []byte)
tty_write_channel := make(chan *write_msg, 1) // buffered so there is no race between initial queueing and startup of writer thread
write_done_channel := make(chan IdType)
tty_reading_done_channel := make(chan byte)
self.wakeup_channel = make(chan byte, 256)
self.pending_writes = make([]*write_msg, 0, 256)
err_channel := make(chan error, 8)
@@ -286,25 +284,49 @@ func (self *Loop) run() (err error) {
no_timeout_channel := make(<-chan time.Time)
finalizer := ""
w_r, w_w, err := os.Pipe()
var r_r, r_w *os.File
if err == nil {
var r_r, r_w, w_r, w_w *os.File
var tty_reading_done_channel chan byte
var tty_read_channel chan []byte
start_tty_reader := func() (err error) {
r_r, r_w, err = os.Pipe()
if err != nil {
w_r.Close()
w_w.Close()
return err
}
} else {
tty_read_channel = make(chan []byte)
tty_reading_done_channel = make(chan byte)
go read_from_tty(r_r, controlling_term, tty_read_channel, err_channel, tty_reading_done_channel)
return
}
err = start_tty_reader()
if err != nil {
return err
}
w_r, w_w, err = os.Pipe() // these are closed in the writer thread and the shutdown defer in this thread
if err != nil {
return err
}
self.QueueWriteString(self.terminal_options.SetStateEscapeCodes())
needs_reset_escape_codes := true
defer func() {
shutdown_tty_reader := func() {
// notify tty reader that we are shutting down
r_w.Close()
close(tty_reading_done_channel)
if r_w != nil {
r_w.Close()
close(tty_reading_done_channel)
r_w = nil
tty_reading_done_channel = nil
}
}
wait_for_tty_reader_to_quit := func() {
// wait for tty reader to exit cleanly
for range tty_read_channel {
}
}
defer func() {
shutdown_tty_reader()
if self.OnFinalize != nil {
finalizer += self.OnFinalize()
@@ -318,13 +340,10 @@ func (self *Loop) run() (err error) {
// flush queued data and wait for it to be written for a timeout, then wait for writer to shutdown
flush_writer(w_w, tty_write_channel, write_done_channel, self.pending_writes, 2*time.Second)
self.pending_writes = nil
// wait for tty reader to exit cleanly
for range tty_read_channel {
}
wait_for_tty_reader_to_quit()
}()
go write_to_tty(w_r, controlling_term, tty_write_channel, err_channel, write_done_channel)
go read_from_tty(r_r, controlling_term, tty_read_channel, err_channel, tty_reading_done_channel)
if self.OnInitialize != nil {
finalizer, err = self.OnInitialize()
@@ -333,27 +352,30 @@ func (self *Loop) run() (err error) {
}
}
self.Suspend = func() (func() error, error) {
self.SuspendAndRun = func(run func() error) (err error) {
write_id := self.QueueWriteString(self.terminal_options.ResetStateEscapeCodes())
needs_reset_escape_codes = false
err := self.wait_for_write_to_complete(write_id, tty_write_channel, write_done_channel, 2*time.Second)
if err != nil {
return nil, err
if err = self.wait_for_write_to_complete(write_id, tty_write_channel, write_done_channel, 2*time.Second); err != nil {
return err
}
shutdown_tty_reader()
wait_for_tty_reader_to_quit()
resume, err := controlling_term.Suspend()
if err != nil {
return nil, err
return err
}
return func() (err error) {
err = resume()
if err != nil {
return
}
write_id = self.QueueWriteString(self.terminal_options.SetStateEscapeCodes())
needs_reset_escape_codes = true
return self.wait_for_write_to_complete(write_id, tty_write_channel, write_done_channel, 2*time.Second)
}, nil
if err = run(); err != nil {
return err
}
if err = start_tty_reader(); err != nil {
return err
}
if err = resume(); err != nil {
return err
}
write_id = self.QueueWriteString(self.terminal_options.SetStateEscapeCodes())
needs_reset_escape_codes = true
return self.wait_for_write_to_complete(write_id, tty_write_channel, write_done_channel, 2*time.Second)
}
self.on_SIGTSTP = func() error {

View File

@@ -5,7 +5,6 @@ package tui
import (
"errors"
"fmt"
"kitty/tools/utils"
"os"
"os/exec"
"path/filepath"
@@ -15,10 +14,16 @@ import (
"github.com/shirou/gopsutil/v3/process"
"golang.org/x/sys/unix"
"kitty/tools/utils"
)
var _ = fmt.Print
var TmuxExe = (&utils.Once[string]{Run: func() string {
return utils.FindExe("tmux")
}}).Get
func tmux_socket_address() (socket string) {
socket = os.Getenv("TMUX")
if socket == "" {
@@ -52,14 +57,21 @@ func tmux_socket_address() (socket string) {
var TmuxSocketAddress = (&utils.Once[string]{Run: tmux_socket_address}).Get
func tmux_command(args ...string) (c *exec.Cmd, stderr *strings.Builder) {
c = exec.Command(TmuxExe(), args...)
stderr = &strings.Builder{}
c.Stderr = stderr
return c, stderr
}
func tmux_allow_passthrough() error {
c := exec.Command("tmux", "show", "-Ap", "allow-passthrough")
c, stderr := tmux_command("show", "-Ap", "allow-passthrough")
allowed, not_allowed := errors.New("allowed"), errors.New("not allowed")
get_result := make(chan error)
go func() {
output, err := c.Output()
if err != nil {
get_result <- err
get_result <- fmt.Errorf("Running %#v failed with error: %w. STDERR: %s", c.Args, err, stderr.String())
} else {
q := strings.TrimSpace(utils.UnsafeBytesToString(output))
if strings.HasSuffix(q, " on") || strings.HasSuffix(q, " all") {
@@ -77,7 +89,12 @@ func tmux_allow_passthrough() error {
if r != not_allowed {
return r
}
return exec.Command("tmux", "set", "-p", "allow-passthrough", "on").Run()
c, stderr = tmux_command("set", "-p", "allow-passthrough", "on")
err := c.Run()
if err != nil {
err = fmt.Errorf("Running %#v failed with error: %w. STDERR: %s", c.Args, err, stderr.String())
}
return err
case <-time.After(2 * time.Second):
return fmt.Errorf("Tmux command timed out. This often happens when the version of tmux on your PATH is older than the version of the running tmux server")
}