Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13e59df1f8 | ||
|
|
c797944923 | ||
|
|
ade4e67b51 | ||
|
|
656f49f2ff | ||
|
|
0045244295 | ||
|
|
6bfb704f6f | ||
|
|
80cebdefcd | ||
|
|
68cd863fe0 | ||
|
|
65e5015d19 | ||
|
|
c26665ec4d | ||
|
|
91d82ebf4b | ||
|
|
303e4baa58 | ||
|
|
3c953d47ca | ||
|
|
5e629d5c09 | ||
|
|
c6ec2d4282 | ||
|
|
66b93a9af8 | ||
|
|
8d5479f55e | ||
|
|
96ce6c9504 | ||
|
|
0d55865545 | ||
|
|
a1fc383e6f | ||
|
|
c3807e175d | ||
|
|
6f83f76d41 | ||
|
|
9a56d619af | ||
|
|
f692d586f7 | ||
|
|
d3d3e99979 | ||
|
|
8e6a179efe | ||
|
|
17ff317d30 | ||
|
|
faef9f0049 | ||
|
|
bb0c831601 | ||
|
|
9462654738 | ||
|
|
e872169e4c | ||
|
|
a22404abe6 | ||
|
|
7c06313750 | ||
|
|
6f265f448d | ||
|
|
acdc41bd03 | ||
|
|
bcff2a7fb6 | ||
|
|
94188fddce | ||
|
|
3684e7f54a | ||
|
|
890181172f | ||
|
|
9c9d68561e | ||
|
|
1bd39ff935 | ||
|
|
17f3d2d581 | ||
|
|
55aa9e11db | ||
|
|
bc895eacc5 | ||
|
|
6c8eb4a19a |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -22,7 +22,7 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
|
|
||||||
**Environment details**
|
**Environment details**
|
||||||
```
|
```
|
||||||
Press Ctrl+Shift+F6 (cmd+option+, on macOS) in kitty, to copy debug output about kitty and its
|
Press Ctrl+Shift+F6 (cmd+option+comma on macOS) in kitty, to copy debug output about kitty and its
|
||||||
configuration to the clipboard and paste it here.
|
configuration to the clipboard and paste it here.
|
||||||
|
|
||||||
On older versions of kitty, run kitty --debug-config instead
|
On older versions of kitty, run kitty --debug-config instead
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
See https://sw.kovidgoyal.net/kitty/changelog.html
|
See https://sw.kovidgoyal.net/kitty/changelog/
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ config to reproduce the issue with).
|
|||||||
|
|
||||||
### Contributing code
|
### Contributing code
|
||||||
|
|
||||||
Install [the dependencies](https://sw.kovidgoyal.net/kitty/build.html#dependencies)
|
Install [the dependencies](https://sw.kovidgoyal.net/kitty/build/#dependencies)
|
||||||
using your favorite package manager. Build and run kitty [from
|
using your favorite package manager. Build and run kitty [from
|
||||||
source](https://sw.kovidgoyal.net/kitty/build.html#install-and-run-from-source).
|
source](https://sw.kovidgoyal.net/kitty/build/#install-and-run-from-source).
|
||||||
|
|
||||||
Make a fork, submit your Pull Request. If it's a large/controversial change, open an issue
|
Make a fork, submit your Pull Request. If it's a large/controversial change, open an issue
|
||||||
beforehand to discuss it, so that you don't waste your time making a pull
|
beforehand to discuss it, so that you don't waste your time making a pull
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
To build from source: <https://sw.kovidgoyal.net/kitty/build.html>
|
To build from source: <https://sw.kovidgoyal.net/kitty/build/>
|
||||||
|
|
||||||
Pre-built binaries: <https://sw.kovidgoyal.net/kitty/binary.html>
|
Pre-built binaries: <https://sw.kovidgoyal.net/kitty/binary/>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ See https://sw.kovidgoyal.net/kitty/[the kitty website].
|
|||||||
|
|
||||||
image:https://github.com/kovidgoyal/kitty/workflows/CI/badge.svg["Build status", link="https://github.com/kovidgoyal/kitty/actions?query=workflow%3ACI"]
|
image:https://github.com/kovidgoyal/kitty/workflows/CI/badge.svg["Build status", link="https://github.com/kovidgoyal/kitty/actions?query=workflow%3ACI"]
|
||||||
|
|
||||||
https://sw.kovidgoyal.net/kitty/faq.html[Frequently Asked Questions]
|
https://sw.kovidgoyal.net/kitty/faq/[Frequently Asked Questions]
|
||||||
|
|
||||||
To ask other questions about kitty usage, use either the https://github.com/kovidgoyal/kitty/discussions/[discussions on GitHub] or the
|
To ask other questions about kitty usage, use either the https://github.com/kovidgoyal/kitty/discussions/[discussions on GitHub] or the
|
||||||
https://www.reddit.com/r/KittyTerminal[Reddit community]
|
https://www.reddit.com/r/KittyTerminal[Reddit community]
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ def copy_libs(env):
|
|||||||
for x in binary_includes():
|
for x in binary_includes():
|
||||||
dest = env.bin_dir if '/bin/' in x else env.lib_dir
|
dest = env.bin_dir if '/bin/' in x else env.lib_dir
|
||||||
shutil.copy2(x, dest)
|
shutil.copy2(x, dest)
|
||||||
|
dest = os.path.join(dest, os.path.basename(x))
|
||||||
|
subprocess.check_call(['chrpath', '-d', dest])
|
||||||
|
|
||||||
|
|
||||||
def copy_python(env):
|
def copy_python(env):
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ move it to another tab or another OS window::
|
|||||||
map ctrl+f2 detach_window
|
map ctrl+f2 detach_window
|
||||||
# moves the window into a new Tab
|
# moves the window into a new Tab
|
||||||
map ctrl+f3 detach_window new-tab
|
map ctrl+f3 detach_window new-tab
|
||||||
|
# moves the window into the previously active tab
|
||||||
|
map ctrl+f3 detach_window tab-prev
|
||||||
|
# moves the window into the tab at the left of the active tab
|
||||||
|
map ctrl+f3 detach_window tab-left
|
||||||
# asks which tab to move the window into
|
# asks which tab to move the window into
|
||||||
map ctrl+f4 detach_window ask
|
map ctrl+f4 detach_window ask
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,40 @@ Changelog
|
|||||||
|kitty| is a feature-rich, cross-platform, *fast*, GPU based terminal.
|
|kitty| is a feature-rich, cross-platform, *fast*, GPU based terminal.
|
||||||
To update |kitty|, :doc:`follow the instructions <binary>`.
|
To update |kitty|, :doc:`follow the instructions <binary>`.
|
||||||
|
|
||||||
|
0.22.2 [2021-08-02]
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- macOS: Fix a long standing bug that could cause kitty windows to stop
|
||||||
|
updating, that got worse in the previous release (:iss:`3890` and
|
||||||
|
:iss:`2016`)
|
||||||
|
|
||||||
|
- Wayland: A better fix for compositors like sway that can toggle client side
|
||||||
|
decorations on and off (:iss:`3888`)
|
||||||
|
|
||||||
|
|
||||||
|
0.22.1 [2021-07-31]
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- Fix a regression in the previous release that broke ``kitty --help`` (:iss:`3869`)
|
||||||
|
|
||||||
|
- Graphics protocol: Fix composing onto currently displayed frame not updating the frame on the GPU (:iss:`3874`)
|
||||||
|
|
||||||
|
- Fix switching to previously active tab after detaching a tab not working (:pull:`3871`)
|
||||||
|
|
||||||
|
- macOS: Fix an error on Apple silicon when enumerating monitors (:pull:`3875`)
|
||||||
|
|
||||||
|
- detach_window: Allow specifying the previously active tab or the tab to the left/right of
|
||||||
|
the active tab (:disc:`3877`)
|
||||||
|
|
||||||
|
- broadcast kitten: Fix a regression in ``0.20.0`` that broke sending of some
|
||||||
|
keys, such as backspace
|
||||||
|
|
||||||
|
- Linux binary: Remove any RPATH build artifacts from bundled libraries
|
||||||
|
|
||||||
|
- Wayland: If the compositor turns off server side decorations after turning
|
||||||
|
them on do not draw client side decorations (:iss:`3888`)
|
||||||
|
|
||||||
|
|
||||||
0.22.0 [2021-07-26]
|
0.22.0 [2021-07-26]
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ if kitty_src not in sys.path:
|
|||||||
sys.path.insert(0, kitty_src)
|
sys.path.insert(0, kitty_src)
|
||||||
|
|
||||||
from kitty.conf.types import Definition # noqa
|
from kitty.conf.types import Definition # noqa
|
||||||
from kitty.constants import str_version # noqa
|
from kitty.constants import str_version, website_url # noqa
|
||||||
|
|
||||||
# config {{{
|
# config {{{
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
@@ -65,7 +65,7 @@ extensions = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# URL for OpenGraph tags
|
# URL for OpenGraph tags
|
||||||
ogp_site_url = 'https://sw.kovidgoyal.net/kitty/'
|
ogp_site_url = website_url()
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
@@ -516,4 +516,5 @@ def setup(app: Any) -> None:
|
|||||||
app.add_role('link', link_role)
|
app.add_role('link', link_role)
|
||||||
app.add_role('iss', partial(num_role, 'issues'))
|
app.add_role('iss', partial(num_role, 'issues'))
|
||||||
app.add_role('pull', partial(num_role, 'pull'))
|
app.add_role('pull', partial(num_role, 'pull'))
|
||||||
|
app.add_role('disc', partial(num_role, 'discussions'))
|
||||||
app.add_role('commit', commit_role)
|
app.add_role('commit', commit_role)
|
||||||
|
|||||||
@@ -53,6 +53,17 @@ Variables that influence kitty behavior
|
|||||||
directory lookup mechanism see, :option:`kitty --config`.
|
directory lookup mechanism see, :option:`kitty --config`.
|
||||||
|
|
||||||
|
|
||||||
|
.. envvar:: VISUAL
|
||||||
|
|
||||||
|
The terminal editor (such as ``vi`` or ``nano``) kitty uses, when, for
|
||||||
|
instance, opening :file:`kitty.conf` in response to :sc:`edit_config_file`.
|
||||||
|
|
||||||
|
|
||||||
|
.. envvar:: EDITOR
|
||||||
|
|
||||||
|
Same as :envvar:`VISUAL`. Used if :envvar:`VISUAL` is not set.
|
||||||
|
|
||||||
|
|
||||||
Variables that kitty sets when running child programs
|
Variables that kitty sets when running child programs
|
||||||
|
|
||||||
.. envvar:: KITTY_WINDOW_ID
|
.. envvar:: KITTY_WINDOW_ID
|
||||||
|
|||||||
@@ -97,32 +97,36 @@ features of the graphics protocol:
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from base64 import standard_b64encode
|
from base64 import standard_b64encode
|
||||||
|
|
||||||
def serialize_gr_command(**cmd):
|
|
||||||
payload = cmd.pop('payload', None)
|
|
||||||
cmd = ','.join('{}={}'.format(k, v) for k, v in cmd.items())
|
|
||||||
ans = []
|
|
||||||
w = ans.append
|
|
||||||
w(b'\033_G'), w(cmd.encode('ascii'))
|
|
||||||
if payload:
|
|
||||||
w(b';')
|
|
||||||
w(payload)
|
|
||||||
w(b'\033\\')
|
|
||||||
return b''.join(ans)
|
|
||||||
|
|
||||||
def write_chunked(**cmd):
|
def serialize_gr_command(**cmd):
|
||||||
data = standard_b64encode(cmd.pop('data'))
|
payload = cmd.pop('payload', None)
|
||||||
while data:
|
cmd = ','.join('{}={}'.format(k, v) for k, v in cmd.items())
|
||||||
chunk, data = data[:4096], data[4096:]
|
ans = []
|
||||||
m = 1 if data else 0
|
w = ans.append
|
||||||
sys.stdout.buffer.write(serialize_gr_command(payload=chunk, m=m, **cmd))
|
w(b'\033_G'), w(cmd.encode('ascii'))
|
||||||
sys.stdout.flush()
|
if payload:
|
||||||
cmd.clear()
|
w(b';')
|
||||||
|
w(payload)
|
||||||
|
w(b'\033\\')
|
||||||
|
return b''.join(ans)
|
||||||
|
|
||||||
with open(sys.argv[-1], 'rb') as f:
|
|
||||||
write_chunked(a='T', f=100, data=f.read())
|
def write_chunked(**cmd):
|
||||||
|
data = standard_b64encode(cmd.pop('data'))
|
||||||
|
while data:
|
||||||
|
chunk, data = data[:4096], data[4096:]
|
||||||
|
m = 1 if data else 0
|
||||||
|
sys.stdout.buffer.write(serialize_gr_command(payload=chunk, m=m,
|
||||||
|
**cmd))
|
||||||
|
sys.stdout.flush()
|
||||||
|
cmd.clear()
|
||||||
|
|
||||||
|
|
||||||
|
with open(sys.argv[-1], 'rb') as f:
|
||||||
|
write_chunked(a='T', f=100, data=f.read())
|
||||||
|
|
||||||
|
|
||||||
Save this script as :file:`png.py`, then you can use it to display any PNG
|
Save this script as :file:`png.py`, then you can use it to display any PNG
|
||||||
@@ -693,7 +697,7 @@ take, and the default value they take when missing. All integers are 32-bit.
|
|||||||
Key Value Default Description
|
Key Value Default Description
|
||||||
======= ==================== ========= =================
|
======= ==================== ========= =================
|
||||||
``a`` Single character. ``t`` The overall action this graphics command is performing.
|
``a`` Single character. ``t`` The overall action this graphics command is performing.
|
||||||
``(a, c, d, f, ` ``t`` - transmit data, ``T`` - transmit data and display image,
|
``(a, c, d, f, `` ``t`` - transmit data, ``T`` - transmit data and display image,
|
||||||
``p, q, t, T)`` ``q`` - query terminal, ``p`` - put (display) previous transmitted image,
|
``p, q, t, T)`` ``q`` - query terminal, ``p`` - put (display) previous transmitted image,
|
||||||
``d`` - delete image, ``f`` - transmit data for animation frames,
|
``d`` - delete image, ``f`` - transmit data for animation frames,
|
||||||
``a`` - control animation, ``c`` - compose animation frames
|
``a`` - control animation, ``c`` - compose animation frames
|
||||||
@@ -753,7 +757,7 @@ Key Value Default Description
|
|||||||
**Keys for animation frame composition**
|
**Keys for animation frame composition**
|
||||||
-----------------------------------------------------------
|
-----------------------------------------------------------
|
||||||
|
|
||||||
``c`` Positive integer ``0`` The 1-based frame number of the frame whose image data serves as the base data
|
``c`` Positive integer ``0`` The 1-based frame number of the frame whose image data serves as the overlaid data
|
||||||
``r`` Positive integer ``0`` The 1-based frame number of the frame that is being edited.
|
``r`` Positive integer ``0`` The 1-based frame number of the frame that is being edited.
|
||||||
``x`` Positive integer ``0`` The left edge (in pixels) of the destination rectangle
|
``x`` Positive integer ``0`` The left edge (in pixels) of the destination rectangle
|
||||||
``y`` Positive integer ``0`` The top edge (in pixels) of the destination rectangle
|
``y`` Positive integer ``0`` The top edge (in pixels) of the destination rectangle
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ Then hold down :kbd:`ctrl+shift` and click the name of the file.
|
|||||||
|
|
||||||
|kitty| will ask you what you want to do with the remote file. You can choose
|
|kitty| will ask you what you want to do with the remote file. You can choose
|
||||||
to *Edit* it in which case kitty will download it and open it locally in your
|
to *Edit* it in which case kitty will download it and open it locally in your
|
||||||
``EDITOR``. As you make changes to the file, they are automatically transferred
|
:envvar:`EDITOR`. As you make changes to the file, they are automatically
|
||||||
to the remote computer. Note that this happens without needing to install *any*
|
transferred to the remote computer. Note that this happens without needing
|
||||||
special software on the server, beyond ``ls`` that supports hyperlinks.
|
to install *any* special software on the server, beyond ``ls`` that supports
|
||||||
|
hyperlinks.
|
||||||
|
|
||||||
.. versionadded:: 0.19.0
|
.. versionadded:: 0.19.0
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,22 @@
|
|||||||
|
|
||||||
// Get the name of the specified display, or NULL
|
// Get the name of the specified display, or NULL
|
||||||
//
|
//
|
||||||
static char* getDisplayName(CGDirectDisplayID displayID)
|
static char*
|
||||||
|
getDisplayName(CGDirectDisplayID displayID, NSScreen* screen)
|
||||||
{
|
{
|
||||||
|
// IOKit doesn't work on Apple Silicon anymore
|
||||||
|
// Luckily, 10.15 introduced -[NSScreen localizedName].
|
||||||
|
// Use it if available, and fall back to IOKit otherwise.
|
||||||
|
if (screen)
|
||||||
|
{
|
||||||
|
if ([screen respondsToSelector:@selector(localizedName)])
|
||||||
|
{
|
||||||
|
NSString* name = [screen valueForKey:@"localizedName"];
|
||||||
|
if (name) {
|
||||||
|
return _glfw_strdup([name UTF8String]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
io_iterator_t it;
|
io_iterator_t it;
|
||||||
io_service_t service;
|
io_service_t service;
|
||||||
CFDictionaryRef info;
|
CFDictionaryRef info;
|
||||||
@@ -89,7 +103,7 @@ static char* getDisplayName(CGDirectDisplayID displayID)
|
|||||||
if (!service)
|
if (!service)
|
||||||
{
|
{
|
||||||
_glfwInputError(GLFW_PLATFORM_ERROR,
|
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||||
"Cocoa: Failed to find service port for display, cannot get its name, using Unknown");
|
"Cocoa: Failed to find service port for display");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,11 +328,9 @@ void _glfwClearDisplayLinks() {
|
|||||||
if (_glfw.ns.displayLinks.entries[i].displayLink) {
|
if (_glfw.ns.displayLinks.entries[i].displayLink) {
|
||||||
CVDisplayLinkStop(_glfw.ns.displayLinks.entries[i].displayLink);
|
CVDisplayLinkStop(_glfw.ns.displayLinks.entries[i].displayLink);
|
||||||
CVDisplayLinkRelease(_glfw.ns.displayLinks.entries[i].displayLink);
|
CVDisplayLinkRelease(_glfw.ns.displayLinks.entries[i].displayLink);
|
||||||
_glfw.ns.displayLinks.entries[i].displayLink = nil;
|
|
||||||
_glfw.ns.displayLinks.entries[i].lastRenderFrameRequestedAt = 0;
|
|
||||||
_glfw.ns.displayLinks.entries[i].first_unserviced_render_frame_request_at = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
memset(_glfw.ns.displayLinks.entries, 0, sizeof(_GLFWDisplayLinkNS) * _glfw.ns.displayLinks.count);
|
||||||
_glfw.ns.displayLinks.count = 0;
|
_glfw.ns.displayLinks.count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,16 +352,21 @@ _glfw_create_cv_display_link(_GLFWDisplayLinkNS *entry) {
|
|||||||
CVDisplayLinkSetOutputCallback(entry->displayLink, &displayLinkCallback, (void*)(uintptr_t)entry->displayID);
|
CVDisplayLinkSetOutputCallback(entry->displayLink, &displayLinkCallback, (void*)(uintptr_t)entry->displayID);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
_GLFWDisplayLinkNS*
|
||||||
createDisplayLink(CGDirectDisplayID displayID) {
|
_glfw_create_display_link(CGDirectDisplayID displayID) {
|
||||||
if (_glfw.ns.displayLinks.count >= sizeof(_glfw.ns.displayLinks.entries)/sizeof(_glfw.ns.displayLinks.entries[0]) - 1) return;
|
if (_glfw.ns.displayLinks.count >= arraysz(_glfw.ns.displayLinks.entries) - 1) {
|
||||||
|
_glfwInputError(GLFW_PLATFORM_ERROR, "Too many monitors cannot create display link");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
|
for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
|
||||||
if (_glfw.ns.displayLinks.entries[i].displayID == displayID) return;
|
// already created in this run
|
||||||
|
if (_glfw.ns.displayLinks.entries[i].displayID == displayID) return _glfw.ns.displayLinks.entries + i;
|
||||||
}
|
}
|
||||||
_GLFWDisplayLinkNS *entry = &_glfw.ns.displayLinks.entries[_glfw.ns.displayLinks.count++];
|
_GLFWDisplayLinkNS *entry = &_glfw.ns.displayLinks.entries[_glfw.ns.displayLinks.count++];
|
||||||
memset(entry, 0, sizeof(_GLFWDisplayLinkNS));
|
memset(entry, 0, sizeof(_GLFWDisplayLinkNS));
|
||||||
entry->displayID = displayID;
|
entry->displayID = displayID;
|
||||||
_glfw_create_cv_display_link(entry);
|
_glfw_create_cv_display_link(entry);
|
||||||
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll for changes in the set of connected monitors
|
// Poll for changes in the set of connected monitors
|
||||||
@@ -357,11 +374,13 @@ createDisplayLink(CGDirectDisplayID displayID) {
|
|||||||
void _glfwPollMonitorsNS(void)
|
void _glfwPollMonitorsNS(void)
|
||||||
{
|
{
|
||||||
uint32_t displayCount;
|
uint32_t displayCount;
|
||||||
|
|
||||||
CGGetOnlineDisplayList(0, NULL, &displayCount);
|
CGGetOnlineDisplayList(0, NULL, &displayCount);
|
||||||
CGDirectDisplayID* displays = calloc(displayCount, sizeof(CGDirectDisplayID));
|
CGDirectDisplayID* displays = calloc(displayCount, sizeof(CGDirectDisplayID));
|
||||||
CGGetOnlineDisplayList(displayCount, displays, &displayCount);
|
CGGetOnlineDisplayList(displayCount, displays, &displayCount);
|
||||||
_glfwClearDisplayLinks();
|
_glfwClearDisplayLinks();
|
||||||
|
if (_glfw.hints.init.debugRendering) {
|
||||||
|
fprintf(stderr, "Polling for monitors: %u found\n", displayCount);
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < _glfw.monitorCount; i++)
|
for (int i = 0; i < _glfw.monitorCount; i++)
|
||||||
_glfw.monitors[i]->ns.screen = nil;
|
_glfw.monitors[i]->ns.screen = nil;
|
||||||
@@ -378,32 +397,57 @@ void _glfwPollMonitorsNS(void)
|
|||||||
|
|
||||||
for (uint32_t i = 0; i < displayCount; i++)
|
for (uint32_t i = 0; i < displayCount; i++)
|
||||||
{
|
{
|
||||||
if (CGDisplayIsAsleep(displays[i]))
|
if (CGDisplayIsAsleep(displays[i])) {
|
||||||
|
if (_glfw.hints.init.debugRendering) fprintf(stderr, "Ignoring sleeping display: %u", displays[i]);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t unitNumber = CGDisplayUnitNumber(displays[i]);
|
||||||
|
NSScreen* screen = nil;
|
||||||
|
|
||||||
|
for (screen in [NSScreen screens])
|
||||||
|
{
|
||||||
|
NSNumber* screenNumber = [screen deviceDescription][@"NSScreenNumber"];
|
||||||
|
|
||||||
|
// HACK: Compare unit numbers instead of display IDs to work around
|
||||||
|
// display replacement on machines with automatic graphics
|
||||||
|
// switching
|
||||||
|
if (CGDisplayUnitNumber([screenNumber unsignedIntValue]) == unitNumber)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// HACK: Compare unit numbers instead of display IDs to work around
|
// HACK: Compare unit numbers instead of display IDs to work around
|
||||||
// display replacement on machines with automatic graphics
|
// display replacement on machines with automatic graphics
|
||||||
// switching
|
// switching
|
||||||
const uint32_t unitNumber = CGDisplayUnitNumber(displays[i]);
|
uint32_t j;
|
||||||
|
for (j = 0; j < disconnectedCount; j++)
|
||||||
for (uint32_t j = 0; j < disconnectedCount; j++)
|
|
||||||
{
|
{
|
||||||
if (disconnected[j] && disconnected[j]->ns.unitNumber == unitNumber)
|
if (disconnected[j] && disconnected[j]->ns.unitNumber == unitNumber)
|
||||||
{
|
{
|
||||||
|
disconnected[j]->ns.displayID = displays[i];
|
||||||
|
disconnected[j]->ns.screen = screen;
|
||||||
|
_glfw_create_display_link(displays[i]);
|
||||||
disconnected[j] = NULL;
|
disconnected[j] = NULL;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (j < disconnectedCount)
|
||||||
|
continue;
|
||||||
|
|
||||||
const CGSize size = CGDisplayScreenSize(displays[i]);
|
const CGSize size = CGDisplayScreenSize(displays[i]);
|
||||||
char* name = getDisplayName(displays[i]);
|
char* name = getDisplayName(displays[i], screen);
|
||||||
if (!name)
|
if (!name) {
|
||||||
name = _glfw_strdup("Unknown");
|
_glfwInputError(GLFW_PLATFORM_ERROR,
|
||||||
|
"Failed to get name for display, using generic name");
|
||||||
|
name = _glfw_strdup("Display with no name");
|
||||||
|
}
|
||||||
|
|
||||||
_GLFWmonitor* monitor = _glfwAllocMonitor(name, (int)size.width, (int)size.height);
|
_GLFWmonitor* monitor = _glfwAllocMonitor(name, (int)size.width, (int)size.height);
|
||||||
monitor->ns.displayID = displays[i];
|
monitor->ns.displayID = displays[i];
|
||||||
monitor->ns.unitNumber = unitNumber;
|
monitor->ns.unitNumber = unitNumber;
|
||||||
createDisplayLink(monitor->ns.displayID);
|
monitor->ns.screen = screen;
|
||||||
|
_glfw_create_display_link(monitor->ns.displayID);
|
||||||
|
|
||||||
free(name);
|
free(name);
|
||||||
|
|
||||||
@@ -554,15 +598,16 @@ GLFWvidmode* _glfwPlatformGetVideoModes(_GLFWmonitor* monitor, int* count)
|
|||||||
|
|
||||||
const GLFWvidmode mode =
|
const GLFWvidmode mode =
|
||||||
vidmodeFromCGDisplayMode(dm, monitor->ns.fallbackRefreshRate);
|
vidmodeFromCGDisplayMode(dm, monitor->ns.fallbackRefreshRate);
|
||||||
|
CFIndex j;
|
||||||
|
|
||||||
for (CFIndex j = 0; j < *count; j++)
|
for (j = 0; j < *count; j++)
|
||||||
{
|
{
|
||||||
if (_glfwCompareVideoModes(result + j, &mode) == 0)
|
if (_glfwCompareVideoModes(result + j, &mode) == 0)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip duplicate modes
|
// Skip duplicate modes
|
||||||
if (i < *count)
|
if (j < *count)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
(*count)++;
|
(*count)++;
|
||||||
|
|||||||
1
glfw/cocoa_platform.h
vendored
1
glfw/cocoa_platform.h
vendored
@@ -250,3 +250,4 @@ void _glfwDispatchRenderFrame(CGDirectDisplayID);
|
|||||||
void _glfwShutdownCVDisplayLink(unsigned long long, void*);
|
void _glfwShutdownCVDisplayLink(unsigned long long, void*);
|
||||||
void _glfwCocoaPostEmptyEvent(void);
|
void _glfwCocoaPostEmptyEvent(void);
|
||||||
void _glfw_create_cv_display_link(_GLFWDisplayLinkNS *entry);
|
void _glfw_create_cv_display_link(_GLFWDisplayLinkNS *entry);
|
||||||
|
_GLFWDisplayLinkNS* _glfw_create_display_link(CGDirectDisplayID);
|
||||||
|
|||||||
@@ -337,9 +337,12 @@ requestRenderFrame(_GLFWwindow *w, GLFWcocoarenderframefun callback) {
|
|||||||
display_link_shutdown_timer = _glfwPlatformAddTimer(DISPLAY_LINK_SHUTDOWN_CHECK_INTERVAL, false, _glfwShutdownCVDisplayLink, NULL, NULL);
|
display_link_shutdown_timer = _glfwPlatformAddTimer(DISPLAY_LINK_SHUTDOWN_CHECK_INTERVAL, false, _glfwShutdownCVDisplayLink, NULL, NULL);
|
||||||
}
|
}
|
||||||
monotonic_t now = glfwGetTime();
|
monotonic_t now = glfwGetTime();
|
||||||
|
bool found_display_link = false;
|
||||||
|
_GLFWDisplayLinkNS *dl = NULL;
|
||||||
for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
|
for (size_t i = 0; i < _glfw.ns.displayLinks.count; i++) {
|
||||||
_GLFWDisplayLinkNS *dl = &_glfw.ns.displayLinks.entries[i];
|
dl = &_glfw.ns.displayLinks.entries[i];
|
||||||
if (dl->displayID == displayID) {
|
if (dl->displayID == displayID) {
|
||||||
|
found_display_link = true;
|
||||||
dl->lastRenderFrameRequestedAt = now;
|
dl->lastRenderFrameRequestedAt = now;
|
||||||
if (!dl->first_unserviced_render_frame_request_at) dl->first_unserviced_render_frame_request_at = now;
|
if (!dl->first_unserviced_render_frame_request_at) dl->first_unserviced_render_frame_request_at = now;
|
||||||
if (!CVDisplayLinkIsRunning(dl->displayLink)) CVDisplayLinkStart(dl->displayLink);
|
if (!CVDisplayLinkIsRunning(dl->displayLink)) CVDisplayLinkStart(dl->displayLink);
|
||||||
@@ -359,6 +362,14 @@ requestRenderFrame(_GLFWwindow *w, GLFWcocoarenderframefun callback) {
|
|||||||
dl->first_unserviced_render_frame_request_at = 0;
|
dl->first_unserviced_render_frame_request_at = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!found_display_link) {
|
||||||
|
dl = _glfw_create_display_link(displayID);
|
||||||
|
if (dl) {
|
||||||
|
dl->lastRenderFrameRequestedAt = now;
|
||||||
|
dl->first_unserviced_render_frame_request_at = now;
|
||||||
|
if (!CVDisplayLinkIsRunning(dl->displayLink)) CVDisplayLinkStart(dl->displayLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ def generate_wrappers(glfw_header: str) -> None:
|
|||||||
unsigned long long glfwDBusUserNotify(const char *app_name, const char* icon, const char *summary, const char *body, \
|
unsigned long long glfwDBusUserNotify(const char *app_name, const char* icon, const char *summary, const char *body, \
|
||||||
const char *action_text, int32_t timeout, GLFWDBusnotificationcreatedfun callback, void *data)
|
const char *action_text, int32_t timeout, GLFWDBusnotificationcreatedfun callback, void *data)
|
||||||
void glfwDBusSetUserNotificationHandler(GLFWDBusnotificationactivatedfun handler)
|
void glfwDBusSetUserNotificationHandler(GLFWDBusnotificationactivatedfun handler)
|
||||||
|
int glfwSetX11LaunchCommand(GLFWwindow *handle, char **argv, int argc)
|
||||||
'''.splitlines():
|
'''.splitlines():
|
||||||
if line:
|
if line:
|
||||||
functions.append(Function(line.strip(), check_fail=False))
|
functions.append(Function(line.strip(), check_fail=False))
|
||||||
|
|||||||
1
glfw/wl_init.c
vendored
1
glfw/wl_init.c
vendored
@@ -29,7 +29,6 @@
|
|||||||
#define _GNU_SOURCE
|
#define _GNU_SOURCE
|
||||||
#include "internal.h"
|
#include "internal.h"
|
||||||
#include "backend_utils.h"
|
#include "backend_utils.h"
|
||||||
#include "wl_client_side_decorations.h"
|
|
||||||
#include "linux_desktop_settings.h"
|
#include "linux_desktop_settings.h"
|
||||||
#include "../kitty/monotonic.h"
|
#include "../kitty/monotonic.h"
|
||||||
|
|
||||||
|
|||||||
42
glfw/wl_window.c
vendored
42
glfw/wl_window.c
vendored
@@ -254,15 +254,39 @@ dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height
|
|||||||
_glfwInputWindowDamage(window);
|
_glfwInputWindowDamage(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
inform_compositor_of_window_geometry(_GLFWwindow *window, const char *event) {
|
||||||
|
#define geometry window->wl.decorations.geometry
|
||||||
|
debug("Setting window geometry in %s event: x=%d y=%d %dx%d\n", event, geometry.x, geometry.y, geometry.width, geometry.height);
|
||||||
|
xdg_surface_set_window_geometry(window->wl.xdg.surface, geometry.x, geometry.y, geometry.width, geometry.height);
|
||||||
|
#undef geometry
|
||||||
|
}
|
||||||
|
|
||||||
static void xdgDecorationHandleConfigure(void* data,
|
|
||||||
|
static void
|
||||||
|
xdgDecorationHandleConfigure(void* data,
|
||||||
struct zxdg_toplevel_decoration_v1* decoration UNUSED,
|
struct zxdg_toplevel_decoration_v1* decoration UNUSED,
|
||||||
uint32_t mode)
|
uint32_t mode)
|
||||||
{
|
{
|
||||||
_GLFWwindow* window = data;
|
_GLFWwindow* window = data;
|
||||||
|
|
||||||
window->wl.decorations.serverSide = (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
|
bool has_server_side_decorations = (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
|
||||||
|
debug("XDG decoration configure event received: has_server_side_decorations: %d\n", has_server_side_decorations);
|
||||||
|
if (has_server_side_decorations == window->wl.decorations.serverSide) return;
|
||||||
|
window->wl.decorations.serverSide = has_server_side_decorations;
|
||||||
|
int width = window->wl.width, height = window->wl.height;
|
||||||
|
if (window->wl.decorations.serverSide) {
|
||||||
|
free_csd_surfaces(window);
|
||||||
|
height += window->wl.decorations.metrics.visible_titlebar_height;
|
||||||
|
} else {
|
||||||
|
ensure_csd_resources(window);
|
||||||
|
}
|
||||||
|
set_csd_window_geometry(window, &width, &height);
|
||||||
|
dispatchChangesAfterConfigure(window, width, height);
|
||||||
ensure_csd_resources(window);
|
ensure_csd_resources(window);
|
||||||
|
wl_surface_commit(window->wl.surface);
|
||||||
|
debug("final window content size: %dx%d\n", window->wl.width, window->wl.height);
|
||||||
|
inform_compositor_of_window_geometry(window, "configure-decorations");
|
||||||
}
|
}
|
||||||
|
|
||||||
static const struct zxdg_toplevel_decoration_v1_listener xdgDecorationListener = {
|
static const struct zxdg_toplevel_decoration_v1_listener xdgDecorationListener = {
|
||||||
@@ -396,14 +420,6 @@ _glfwPlatformToggleFullscreen(_GLFWwindow *window, unsigned int flags UNUSED) {
|
|||||||
return !already_fullscreen;
|
return !already_fullscreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
inform_compositor_of_window_geometry(_GLFWwindow *window, const char *event) {
|
|
||||||
#define geometry window->wl.decorations.geometry
|
|
||||||
debug("Setting window geometry in %s event: x=%d y=%d %dx%d\n", event, geometry.x, geometry.y, geometry.width, geometry.height);
|
|
||||||
xdg_surface_set_window_geometry(window->wl.xdg.surface, geometry.x, geometry.y, geometry.width, geometry.height);
|
|
||||||
#undef geometry
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
xdgToplevelHandleConfigure(void* data,
|
xdgToplevelHandleConfigure(void* data,
|
||||||
struct xdg_toplevel* toplevel UNUSED,
|
struct xdg_toplevel* toplevel UNUSED,
|
||||||
@@ -488,7 +504,8 @@ static const struct xdg_surface_listener xdgSurfaceListener = {
|
|||||||
xdgSurfaceHandleConfigure
|
xdgSurfaceHandleConfigure
|
||||||
};
|
};
|
||||||
|
|
||||||
static void setXdgDecorations(_GLFWwindow* window)
|
static void
|
||||||
|
setXdgDecorations(_GLFWwindow* window)
|
||||||
{
|
{
|
||||||
if (_glfw.wl.decorationManager)
|
if (_glfw.wl.decorationManager)
|
||||||
{
|
{
|
||||||
@@ -510,7 +527,8 @@ static void setXdgDecorations(_GLFWwindow* window)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool createXdgSurface(_GLFWwindow* window)
|
static bool
|
||||||
|
createXdgSurface(_GLFWwindow* window)
|
||||||
{
|
{
|
||||||
window->wl.xdg.surface = xdg_wm_base_get_xdg_surface(_glfw.wl.wmBase,
|
window->wl.xdg.surface = xdg_wm_base_get_xdg_surface(_glfw.wl.wmBase,
|
||||||
window->wl.surface);
|
window->wl.surface);
|
||||||
|
|||||||
7
glfw/x11_window.c
vendored
7
glfw/x11_window.c
vendored
@@ -3122,3 +3122,10 @@ GLFWAPI unsigned long long glfwDBusUserNotify(const char *app_name, const char*
|
|||||||
GLFWAPI void glfwDBusSetUserNotificationHandler(GLFWDBusnotificationactivatedfun handler) {
|
GLFWAPI void glfwDBusSetUserNotificationHandler(GLFWDBusnotificationactivatedfun handler) {
|
||||||
glfw_dbus_set_user_notification_activated_handler(handler);
|
glfw_dbus_set_user_notification_activated_handler(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GLFWAPI int glfwSetX11LaunchCommand(GLFWwindow *handle, char **argv, int argc)
|
||||||
|
{
|
||||||
|
_GLFW_REQUIRE_INIT_OR_RETURN(0);
|
||||||
|
_GLFWwindow* window = (_GLFWwindow*) handle;
|
||||||
|
return XSetCommand(_glfw.x11.display, window->x11.handle, argv, argc);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
See https://sw.kovidgoyal.net/kitty/kittens/diff.html
|
See https://sw.kovidgoyal.net/kitty/kittens/diff/
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from typing import (
|
|||||||
|
|
||||||
from kitty.cli import parse_args
|
from kitty.cli import parse_args
|
||||||
from kitty.cli_stub import HintsCLIOptions
|
from kitty.cli_stub import HintsCLIOptions
|
||||||
|
from kitty.constants import website_url
|
||||||
from kitty.fast_data_types import set_clipboard_string
|
from kitty.fast_data_types import set_clipboard_string
|
||||||
from kitty.key_encoding import KeyEvent
|
from kitty.key_encoding import KeyEvent
|
||||||
from kitty.typing import BossType, KittyCommonOpts
|
from kitty.typing import BossType, KittyCommonOpts
|
||||||
@@ -640,7 +641,7 @@ The foreground color for text pointed to by the hints
|
|||||||
--customize-processing
|
--customize-processing
|
||||||
Name of a python file in the kitty config directory which will be imported to provide
|
Name of a python file in the kitty config directory which will be imported to provide
|
||||||
custom implementations for pattern finding and performing actions
|
custom implementations for pattern finding and performing actions
|
||||||
on selected matches. See https://sw.kovidgoyal.net/kitty/kittens/hints.html
|
on selected matches. See {hints_url}
|
||||||
for details. You can also specify absolute paths to load the script from elsewhere.
|
for details. You can also specify absolute paths to load the script from elsewhere.
|
||||||
|
|
||||||
|
|
||||||
@@ -649,7 +650,8 @@ The window title for the hints window, default title is selected based on
|
|||||||
the type of text being hinted.
|
the type of text being hinted.
|
||||||
'''.format(
|
'''.format(
|
||||||
default_regex=DEFAULT_REGEX,
|
default_regex=DEFAULT_REGEX,
|
||||||
line='{{line}}', path='{{path}}'
|
line='{{line}}', path='{{path}}',
|
||||||
|
hints_url=website_url('kittens/hints'),
|
||||||
).format
|
).format
|
||||||
help_text = 'Select text from the screen using the keyboard. Defaults to searching for URLs.'
|
help_text = 'Select text from the screen using the keyboard. Defaults to searching for URLs.'
|
||||||
usage = ''
|
usage = ''
|
||||||
|
|||||||
@@ -11,14 +11,15 @@ from contextlib import suppress
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from itertools import count
|
from itertools import count
|
||||||
from typing import (
|
from typing import (
|
||||||
Any, Callable, DefaultDict, Deque, Dict, Iterator, List, Optional,
|
Any, Callable, ClassVar, DefaultDict, Deque, Dict, Generic, Iterator, List,
|
||||||
Sequence, Tuple, Union
|
Optional, Sequence, Tuple, Type, TypeVar, Union, cast
|
||||||
)
|
)
|
||||||
|
|
||||||
from kitty.conf.utils import positive_float, positive_int
|
from kitty.conf.utils import positive_float, positive_int
|
||||||
from kitty.fast_data_types import create_canvas
|
from kitty.fast_data_types import create_canvas
|
||||||
from kitty.typing import (
|
from kitty.typing import (
|
||||||
CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t, HandlerType
|
GRT_C, CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t,
|
||||||
|
HandlerType
|
||||||
)
|
)
|
||||||
from kitty.utils import ScreenSize, find_exe, fit_image
|
from kitty.utils import ScreenSize, find_exe, fit_image
|
||||||
|
|
||||||
@@ -298,49 +299,75 @@ def can_display_images() -> bool:
|
|||||||
|
|
||||||
ImageKey = Tuple[str, int, int]
|
ImageKey = Tuple[str, int, int]
|
||||||
SentImageKey = Tuple[int, int, int]
|
SentImageKey = Tuple[int, int, int]
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
class Alias(Generic[T]):
|
||||||
|
|
||||||
|
currently_processing: ClassVar[str] = ''
|
||||||
|
|
||||||
|
def __init__(self, defval: T) -> None:
|
||||||
|
self.name = ''
|
||||||
|
self.defval = defval
|
||||||
|
|
||||||
|
def __get__(self, instance: Optional['GraphicsCommand'], cls: Optional[Type['GraphicsCommand']] = None) -> T:
|
||||||
|
if instance is None:
|
||||||
|
return self.defval
|
||||||
|
return cast(T, instance._actual_values.get(self.name, self.defval))
|
||||||
|
|
||||||
|
def __set__(self, instance: 'GraphicsCommand', val: T) -> None:
|
||||||
|
if val == self.defval:
|
||||||
|
instance._actual_values.pop(self.name, None)
|
||||||
|
else:
|
||||||
|
instance._actual_values[self.name] = val
|
||||||
|
|
||||||
|
def __set_name__(self, owner: Type['GraphicsCommand'], name: str) -> None:
|
||||||
|
if len(name) == 1:
|
||||||
|
Alias.currently_processing = name
|
||||||
|
self.name = Alias.currently_processing
|
||||||
|
|
||||||
|
|
||||||
class GraphicsCommand:
|
class GraphicsCommand:
|
||||||
a: GRT_a = 't' # action
|
a = action = Alias(cast(GRT_a, 't'))
|
||||||
q: int = 0 # suppress responses
|
q = quiet = Alias(0)
|
||||||
f: GRT_f = 32 # image data format
|
f = format = Alias(32)
|
||||||
t: GRT_t = 'd' # transmission medium
|
t = transmission_type = Alias(cast(GRT_t, 'd'))
|
||||||
s: int = 0 # sent image width
|
s = data_width = animation_state = Alias(0)
|
||||||
v: int = 0 # sent image height
|
v = data_height = loop_count = Alias(0)
|
||||||
S: int = 0 # size of data to read from file
|
S = data_size = Alias(0)
|
||||||
O: int = 0 # offset of data to read from file
|
O = data_offset = Alias(0) # noqa
|
||||||
i: int = 0 # image id
|
i = image_id = Alias(0)
|
||||||
I: int = 0 # image number
|
I = image_number = Alias(0) # noqa
|
||||||
p: int = 0 # placement id
|
p = placement_id = Alias(0)
|
||||||
o: Optional[GRT_o] = None # type of compression
|
o = compression = Alias(cast(Optional[GRT_o], None))
|
||||||
m: GRT_m = 0 # 0 or 1 whether there is more chunked data
|
m = more = Alias(cast(GRT_m, 0))
|
||||||
x: int = 0 # left edge of image area to display
|
x = left_edge = Alias(0)
|
||||||
y: int = 0 # top edge of image area to display
|
y = top_edge = Alias(0)
|
||||||
w: int = 0 # image width to display
|
w = width = Alias(0)
|
||||||
h: int = 0 # image height to display
|
h = height = Alias(0)
|
||||||
X: int = 0 # X-offset within cell
|
X = cell_x_offset = blend_mode = Alias(0)
|
||||||
Y: int = 0 # Y-offset within cell
|
Y = cell_y_offset = bgcolor = Alias(0)
|
||||||
c: int = 0 # number of cols to display image over
|
c = columns = other_frame_number = dest_frame = Alias(0)
|
||||||
r: int = 0 # number of rows to display image over
|
r = rows = frame_number = source_frame = Alias(0)
|
||||||
z: int = 0 # z-index
|
z = z_index = gap = Alias(0)
|
||||||
d: GRT_d = 'a' # what to delete
|
C = cursor_movement = compose_mode = Alias(cast(GRT_C, 0))
|
||||||
|
d = delete_action = Alias(cast(GRT_d, 'a'))
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._actual_values: Dict[str, Any] = {}
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return self.serialize().decode('ascii').replace('\033', '^]')
|
return self.serialize().decode('ascii').replace('\033', '^]')
|
||||||
|
|
||||||
def clone(self) -> 'GraphicsCommand':
|
def clone(self) -> 'GraphicsCommand':
|
||||||
ans = GraphicsCommand()
|
ans = GraphicsCommand()
|
||||||
for k in GraphicsCommand.__annotations__:
|
ans._actual_values = self._actual_values.copy()
|
||||||
setattr(ans, k, getattr(self, k))
|
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def serialize(self, payload: Union[bytes, str] = b'') -> bytes:
|
def serialize(self, payload: Union[bytes, str] = b'') -> bytes:
|
||||||
items = []
|
items = []
|
||||||
for k in GraphicsCommand.__annotations__:
|
for k, val in self._actual_values.items():
|
||||||
val: Union[str, None, int] = getattr(self, k)
|
items.append('{}={}'.format(k, val))
|
||||||
defval: Union[str, None, int] = getattr(GraphicsCommand, k)
|
|
||||||
if val != defval and val is not None:
|
|
||||||
items.append('{}={}'.format(k, val))
|
|
||||||
|
|
||||||
ans: List[bytes] = []
|
ans: List[bytes] = []
|
||||||
w = ans.append
|
w = ans.append
|
||||||
@@ -355,9 +382,27 @@ class GraphicsCommand:
|
|||||||
return b''.join(ans)
|
return b''.join(ans)
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
for k in GraphicsCommand.__annotations__:
|
self._actual_values = {}
|
||||||
defval: Union[str, None, int] = getattr(GraphicsCommand, k)
|
|
||||||
setattr(self, k, defval)
|
def iter_transmission_chunks(self, data: Optional[bytes] = None, level: int = -1, compression_threshold: int = 1024) -> Iterator[bytes]:
|
||||||
|
if data is None:
|
||||||
|
yield self.serialize()
|
||||||
|
return
|
||||||
|
gc = self.clone()
|
||||||
|
gc.S = len(data)
|
||||||
|
if level and len(data) >= compression_threshold:
|
||||||
|
import zlib
|
||||||
|
compressed = zlib.compress(data, level)
|
||||||
|
if len(compressed) < len(data):
|
||||||
|
gc.o = 'z'
|
||||||
|
data = compressed
|
||||||
|
gc.S = len(data)
|
||||||
|
data = standard_b64encode(data)
|
||||||
|
while data:
|
||||||
|
chunk, data = data[:4096], data[4096:]
|
||||||
|
gc.m = 1 if data else 0
|
||||||
|
yield gc.serialize(chunk)
|
||||||
|
gc.clear()
|
||||||
|
|
||||||
|
|
||||||
class Placement:
|
class Placement:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from .conf.utils import BadLine, KeyAction, to_cmdline
|
|||||||
from .config import common_opts_as_dict, prepare_config_file_for_editing
|
from .config import common_opts_as_dict, prepare_config_file_for_editing
|
||||||
from .constants import (
|
from .constants import (
|
||||||
appname, config_dir, is_macos, is_wayland, kitty_exe,
|
appname, config_dir, is_macos, is_wayland, kitty_exe,
|
||||||
supports_primary_selection
|
supports_primary_selection, website_url
|
||||||
)
|
)
|
||||||
from .fast_data_types import (
|
from .fast_data_types import (
|
||||||
CLOSE_BEING_CONFIRMED, IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED,
|
CLOSE_BEING_CONFIRMED, IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED,
|
||||||
@@ -1141,7 +1141,7 @@ class Boss:
|
|||||||
|
|
||||||
self._run_kitten('ask', [
|
self._run_kitten('ask', [
|
||||||
'--name=create-marker', '--message',
|
'--name=create-marker', '--message',
|
||||||
_('Create marker, for example:\ntext 1 ERROR\nSee https://sw.kovidgoyal.net/kitty/marks.html\n')
|
_('Create marker, for example:\ntext 1 ERROR\nSee {}\n').format(website_url('marks'))
|
||||||
],
|
],
|
||||||
custom_callback=done, action_on_removal=done2)
|
custom_callback=done, action_on_removal=done2)
|
||||||
|
|
||||||
@@ -1691,14 +1691,17 @@ class Boss:
|
|||||||
target_tab = tm.new_tab(empty_tab=True)
|
target_tab = tm.new_tab(empty_tab=True)
|
||||||
else:
|
else:
|
||||||
target_os_window_id = target_os_window_id or current_os_window()
|
target_os_window_id = target_os_window_id or current_os_window()
|
||||||
if target_tab_id == 'new':
|
if isinstance(target_tab_id, str):
|
||||||
if not isinstance(target_os_window_id, int):
|
if not isinstance(target_os_window_id, int):
|
||||||
q = self.active_tab_manager
|
q = self.active_tab_manager
|
||||||
assert q is not None
|
assert q is not None
|
||||||
tm = q
|
tm = q
|
||||||
else:
|
else:
|
||||||
tm = self.os_window_map[target_os_window_id]
|
tm = self.os_window_map[target_os_window_id]
|
||||||
target_tab = tm.new_tab(empty_tab=True)
|
if target_tab_id == 'new':
|
||||||
|
target_tab = tm.new_tab(empty_tab=True)
|
||||||
|
else:
|
||||||
|
target_tab = tm.tab_at_location(target_tab_id) or tm.new_tab(empty_tab=True)
|
||||||
else:
|
else:
|
||||||
for tab in self.all_tabs:
|
for tab in self.all_tabs:
|
||||||
if tab.id == target_tab_id:
|
if tab.id == target_tab_id:
|
||||||
@@ -1766,8 +1769,9 @@ class Boss:
|
|||||||
'''
|
'''
|
||||||
if not args or args[0] == 'new':
|
if not args or args[0] == 'new':
|
||||||
return self._move_window_to(target_os_window_id='new')
|
return self._move_window_to(target_os_window_id='new')
|
||||||
if args[0] == 'new-tab':
|
if args[0] in ('new-tab', 'tab-prev', 'tab-left', 'tab-right'):
|
||||||
return self._move_window_to(target_tab_id='new')
|
where = 'new' if args[0] == 'new-tab' else args[0][4:]
|
||||||
|
return self._move_window_to(target_tab_id=where)
|
||||||
title = 'Choose a tab to move the window to'
|
title = 'Choose a tab to move the window to'
|
||||||
lines = [title, '']
|
lines = [title, '']
|
||||||
fmt = ': {1}'
|
fmt = ': {1}'
|
||||||
|
|||||||
39
kitty/cli.py
39
kitty/cli.py
@@ -12,7 +12,7 @@ from typing import (
|
|||||||
|
|
||||||
from .cli_stub import CLIOptions
|
from .cli_stub import CLIOptions
|
||||||
from .conf.utils import resolve_config
|
from .conf.utils import resolve_config
|
||||||
from .constants import appname, defconf, is_macos, str_version
|
from .constants import appname, defconf, is_macos, str_version, website_url
|
||||||
from .options.types import Options as KittyOpts
|
from .options.types import Options as KittyOpts
|
||||||
from .typing import BadLineType, TypedDict
|
from .typing import BadLineType, TypedDict
|
||||||
|
|
||||||
@@ -57,42 +57,60 @@ def surround(x: str, start: int, end: int) -> str:
|
|||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
role_map: Dict[str, Callable[[str], str]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def role(func: Callable[[str], str]) -> Callable[[str], str]:
|
||||||
|
role_map[func.__name__] = func
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def emph(x: str) -> str:
|
def emph(x: str) -> str:
|
||||||
return surround(x, 91, 39)
|
return surround(x, 91, 39)
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def cyan(x: str) -> str:
|
def cyan(x: str) -> str:
|
||||||
return surround(x, 96, 39)
|
return surround(x, 96, 39)
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def green(x: str) -> str:
|
def green(x: str) -> str:
|
||||||
return surround(x, 32, 39)
|
return surround(x, 32, 39)
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def blue(x: str) -> str:
|
def blue(x: str) -> str:
|
||||||
return surround(x, 34, 39)
|
return surround(x, 34, 39)
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def yellow(x: str) -> str:
|
def yellow(x: str) -> str:
|
||||||
return surround(x, 93, 39)
|
return surround(x, 93, 39)
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def italic(x: str) -> str:
|
def italic(x: str) -> str:
|
||||||
return surround(x, 3, 23)
|
return surround(x, 3, 23)
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def bold(x: str) -> str:
|
def bold(x: str) -> str:
|
||||||
return surround(x, 1, 22)
|
return surround(x, 1, 22)
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def title(x: str) -> str:
|
def title(x: str) -> str:
|
||||||
return blue(bold(x))
|
return blue(bold(x))
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def opt(text: str) -> str:
|
def opt(text: str) -> str:
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def option(x: str) -> str:
|
def option(x: str) -> str:
|
||||||
idx = x.rfind('--')
|
idx = x.rfind('--')
|
||||||
if idx < 0:
|
if idx < 0:
|
||||||
@@ -103,24 +121,32 @@ def option(x: str) -> str:
|
|||||||
return ' '.join(parts)
|
return ' '.join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def code(x: str) -> str:
|
def code(x: str) -> str:
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def kbd(x: str) -> str:
|
def kbd(x: str) -> str:
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def env(x: str) -> str:
|
def env(x: str) -> str:
|
||||||
return italic(x)
|
return italic(x)
|
||||||
|
|
||||||
|
|
||||||
|
role_map['envvar'] = role_map['env']
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def file(x: str) -> str:
|
def file(x: str) -> str:
|
||||||
return italic(x)
|
return italic(x)
|
||||||
|
|
||||||
|
|
||||||
|
@role
|
||||||
def doc(x: str) -> str:
|
def doc(x: str) -> str:
|
||||||
return f'https://sw.kovidgoyal.net/kitty/{x}.html'
|
return website_url(x)
|
||||||
|
|
||||||
|
|
||||||
OptionSpecSeq = List[Union[str, OptionDict]]
|
OptionSpecSeq = List[Union[str, OptionDict]]
|
||||||
@@ -197,11 +223,13 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
|
|||||||
|
|
||||||
|
|
||||||
def prettify(text: str) -> str:
|
def prettify(text: str) -> str:
|
||||||
role_map = globals()
|
|
||||||
|
def identity(x: str) -> str:
|
||||||
|
return x
|
||||||
|
|
||||||
def sub(m: Match) -> str:
|
def sub(m: Match) -> str:
|
||||||
role, text = m.group(1, 2)
|
role, text = m.group(1, 2)
|
||||||
return str(role_map[role](text))
|
return role_map.get(role, identity)(text)
|
||||||
|
|
||||||
text = re.sub(r':([a-z]+):`([^`]+)`', sub, text)
|
text = re.sub(r':([a-z]+):`([^`]+)`', sub, text)
|
||||||
return text
|
return text
|
||||||
@@ -265,7 +293,8 @@ Run the :italic:`{appname}` terminal emulator. You can also specify the :italic:
|
|||||||
to run inside :italic:`{appname}` as normal arguments following the :italic:`options`.
|
to run inside :italic:`{appname}` as normal arguments following the :italic:`options`.
|
||||||
For example: {appname} sh -c "echo hello, world. Press ENTER to quit; read"
|
For example: {appname} sh -c "echo hello, world. Press ENTER to quit; read"
|
||||||
|
|
||||||
For comprehensive documentation for kitty, please see: https://sw.kovidgoyal.net/kitty/''').format(appname=appname)
|
For comprehensive documentation for kitty, please see: {url}''').format(
|
||||||
|
appname=appname, url=website_url())
|
||||||
|
|
||||||
|
|
||||||
class PrintHelpForSeq:
|
class PrintHelpForSeq:
|
||||||
|
|||||||
@@ -182,6 +182,8 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
|
|||||||
a(' config_overrides: typing.Tuple[str, ...] = ()')
|
a(' config_overrides: typing.Tuple[str, ...] = ()')
|
||||||
a('')
|
a('')
|
||||||
a(' def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:')
|
a(' def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:')
|
||||||
|
if defn.has_color_table:
|
||||||
|
a(' self.color_table = array(self.color_table.typecode, self.color_table)')
|
||||||
a(' if options_dict is not None:')
|
a(' if options_dict is not None:')
|
||||||
a(' for key in option_names:')
|
a(' for key in option_names:')
|
||||||
a(' setattr(self, key, options_dict[key])')
|
a(' setattr(self, key, options_dict[key])')
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
import kitty.conf.utils as generic_parsers
|
import kitty.conf.utils as generic_parsers
|
||||||
|
from kitty.constants import website_url
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
Only = typing.Literal['macos', 'linux', '']
|
Only = typing.Literal['macos', 'linux', '']
|
||||||
@@ -41,15 +42,15 @@ def expand_opt_references(conf_name: str, text: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def remove_markup(text: str) -> str:
|
def remove_markup(text: str) -> str:
|
||||||
base = 'https://sw.kovidgoyal.net/kitty'
|
ref_map = {
|
||||||
|
'layouts': f'{website_url("overview")}#layouts',
|
||||||
|
'sessions': f'{website_url("overview")}#layouts',
|
||||||
|
'functional': f'{website_url("keyboard-protocol")}#functional-key-definitions',
|
||||||
|
}
|
||||||
|
|
||||||
def sub(m: Match) -> str:
|
def sub(m: Match) -> str:
|
||||||
if m.group(1) == 'ref':
|
if m.group(1) == 'ref':
|
||||||
return {
|
return ref_map[m.group(2)]
|
||||||
'layouts': f'{base}/overview/#layouts',
|
|
||||||
'sessions': f'{base}/overview/#sessions',
|
|
||||||
'functional': f'{base}/keyboard-protocol/#functional-key-definitions',
|
|
||||||
}[m.group(2)]
|
|
||||||
return str(m.group(2))
|
return str(m.group(2))
|
||||||
|
|
||||||
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)
|
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Version(NamedTuple):
|
|||||||
|
|
||||||
appname: str = 'kitty'
|
appname: str = 'kitty'
|
||||||
kitty_face = '🐱'
|
kitty_face = '🐱'
|
||||||
version: Version = Version(0, 22, 0)
|
version: Version = Version(0, 22, 2)
|
||||||
str_version: str = '.'.join(map(str, version))
|
str_version: str = '.'.join(map(str, version))
|
||||||
_plat = sys.platform.lower()
|
_plat = sys.platform.lower()
|
||||||
is_macos: bool = 'darwin' in _plat
|
is_macos: bool = 'darwin' in _plat
|
||||||
@@ -186,3 +186,11 @@ def read_kitty_resource(name: str) -> bytes:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from importlib_resources import read_binary # type: ignore
|
from importlib_resources import read_binary # type: ignore
|
||||||
return read_binary('kitty', name)
|
return read_binary('kitty', name)
|
||||||
|
|
||||||
|
|
||||||
|
def website_url(doc_name: str = '') -> str:
|
||||||
|
if doc_name:
|
||||||
|
doc_name = doc_name.rstrip('/')
|
||||||
|
if doc_name:
|
||||||
|
doc_name += '/'
|
||||||
|
return f'https://sw.kovidgoyal.net/kitty/{doc_name}'
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ from functools import partial
|
|||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import Callable, Dict, Generator, Iterable, Set, Tuple
|
from typing import Callable, Dict, Generator, Iterable, Set, Tuple
|
||||||
|
|
||||||
|
from kittens.tui.operations import colored, styled
|
||||||
|
|
||||||
from .cli import version
|
from .cli import version
|
||||||
from .conf.utils import KeyAction
|
from .conf.utils import KeyAction
|
||||||
from .constants import is_macos, is_wayland
|
from .constants import is_macos, is_wayland
|
||||||
from kittens.tui.operations import colored
|
|
||||||
from .options.types import Options as KittyOpts, defaults
|
from .options.types import Options as KittyOpts, defaults
|
||||||
from .options.utils import MouseMap
|
from .options.utils import MouseMap
|
||||||
|
from .rgb import Color, color_as_sharp
|
||||||
from .types import MouseEvent, SingleKey
|
from .types import MouseEvent, SingleKey
|
||||||
from .typing import SequenceMap
|
from .typing import SequenceMap
|
||||||
|
|
||||||
@@ -113,6 +115,7 @@ def compare_opts(opts: KittyOpts, print: Callable) -> None:
|
|||||||
]
|
]
|
||||||
field_len = max(map(len, changed_opts)) if changed_opts else 20
|
field_len = max(map(len, changed_opts)) if changed_opts else 20
|
||||||
fmt = '{{:{:d}s}}'.format(field_len)
|
fmt = '{{:{:d}s}}'.format(field_len)
|
||||||
|
colors = []
|
||||||
for f in changed_opts:
|
for f in changed_opts:
|
||||||
val = getattr(opts, f)
|
val = getattr(opts, f)
|
||||||
if isinstance(val, dict):
|
if isinstance(val, dict):
|
||||||
@@ -123,7 +126,11 @@ def compare_opts(opts: KittyOpts, print: Callable) -> None:
|
|||||||
else:
|
else:
|
||||||
print(pformat(val))
|
print(pformat(val))
|
||||||
else:
|
else:
|
||||||
print(title(fmt.format(f)), str(getattr(opts, f)))
|
val = getattr(opts, f)
|
||||||
|
if isinstance(val, Color):
|
||||||
|
colors.append(fmt.format(f) + ' ' + color_as_sharp(val) + ' ' + styled(' ', bg=val))
|
||||||
|
else:
|
||||||
|
print(fmt.format(f), str(getattr(opts, f)))
|
||||||
|
|
||||||
compare_mousemaps(opts.mousemap, default_opts.mousemap, print)
|
compare_mousemaps(opts.mousemap, default_opts.mousemap, print)
|
||||||
final_, initial_ = opts.keymap, default_opts.keymap
|
final_, initial_ = opts.keymap, default_opts.keymap
|
||||||
@@ -133,6 +140,9 @@ def compare_opts(opts: KittyOpts, print: Callable) -> None:
|
|||||||
final.update(final_s)
|
final.update(final_s)
|
||||||
initial.update(initial_s)
|
initial.update(initial_s)
|
||||||
compare_keymaps(final, initial, print)
|
compare_keymaps(final, initial, print)
|
||||||
|
if colors:
|
||||||
|
print(f'{title("Colors")}:', end='\n\t')
|
||||||
|
print('\n\t'.join(sorted(colors)))
|
||||||
|
|
||||||
|
|
||||||
def debug_config(opts: KittyOpts) -> str:
|
def debug_config(opts: KittyOpts) -> str:
|
||||||
|
|||||||
3
kitty/glfw-wrapper.c
generated
3
kitty/glfw-wrapper.c
generated
@@ -441,6 +441,9 @@ load_glfw(const char* path) {
|
|||||||
*(void **) (&glfwDBusSetUserNotificationHandler_impl) = dlsym(handle, "glfwDBusSetUserNotificationHandler");
|
*(void **) (&glfwDBusSetUserNotificationHandler_impl) = dlsym(handle, "glfwDBusSetUserNotificationHandler");
|
||||||
if (glfwDBusSetUserNotificationHandler_impl == NULL) dlerror(); // clear error indicator
|
if (glfwDBusSetUserNotificationHandler_impl == NULL) dlerror(); // clear error indicator
|
||||||
|
|
||||||
|
*(void **) (&glfwSetX11LaunchCommand_impl) = dlsym(handle, "glfwSetX11LaunchCommand");
|
||||||
|
if (glfwSetX11LaunchCommand_impl == NULL) dlerror(); // clear error indicator
|
||||||
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
kitty/glfw-wrapper.h
generated
4
kitty/glfw-wrapper.h
generated
@@ -2168,4 +2168,8 @@ typedef void (*glfwDBusSetUserNotificationHandler_func)(GLFWDBusnotificationacti
|
|||||||
GFW_EXTERN glfwDBusSetUserNotificationHandler_func glfwDBusSetUserNotificationHandler_impl;
|
GFW_EXTERN glfwDBusSetUserNotificationHandler_func glfwDBusSetUserNotificationHandler_impl;
|
||||||
#define glfwDBusSetUserNotificationHandler glfwDBusSetUserNotificationHandler_impl
|
#define glfwDBusSetUserNotificationHandler glfwDBusSetUserNotificationHandler_impl
|
||||||
|
|
||||||
|
typedef int (*glfwSetX11LaunchCommand_func)(GLFWwindow*, char**, int);
|
||||||
|
GFW_EXTERN glfwSetX11LaunchCommand_func glfwSetX11LaunchCommand_impl;
|
||||||
|
#define glfwSetX11LaunchCommand glfwSetX11LaunchCommand_impl
|
||||||
|
|
||||||
const char* load_glfw(const char* path);
|
const char* load_glfw(const char* path);
|
||||||
|
|||||||
@@ -622,11 +622,13 @@ native_window_handle(GLFWwindow *w) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
create_os_window(PyObject UNUSED *self, PyObject *args) {
|
create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
|
||||||
int x = -1, y = -1;
|
int x = -1, y = -1;
|
||||||
char *title, *wm_class_class, *wm_class_name;
|
char *title, *wm_class_class, *wm_class_name;
|
||||||
PyObject *load_programs = NULL, *get_window_size, *pre_show_callback;
|
PyObject *load_programs = NULL, *get_window_size, *pre_show_callback;
|
||||||
if (!PyArg_ParseTuple(args, "OOsss|Oii", &get_window_size, &pre_show_callback, &title, &wm_class_name, &wm_class_class, &load_programs, &x, &y)) return NULL;
|
static const char* kwlist[] = {"get_window_size", "pre_show_callback", "title", "wm_class_name", "wm_class_class", "load_programs", "x", "y", NULL};
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kw, "OOsss|Oii", (char**)kwlist,
|
||||||
|
&get_window_size, &pre_show_callback, &title, &wm_class_name, &wm_class_class, &load_programs, &x, &y)) return NULL;
|
||||||
|
|
||||||
static bool is_first_window = true;
|
static bool is_first_window = true;
|
||||||
if (is_first_window) {
|
if (is_first_window) {
|
||||||
@@ -1431,7 +1433,7 @@ stop_main_loop(void) {
|
|||||||
|
|
||||||
static PyMethodDef module_methods[] = {
|
static PyMethodDef module_methods[] = {
|
||||||
METHODB(set_custom_cursor, METH_VARARGS),
|
METHODB(set_custom_cursor, METH_VARARGS),
|
||||||
METHODB(create_os_window, METH_VARARGS),
|
{"create_os_window", (PyCFunction)(void (*) (void))(create_os_window), METH_VARARGS | METH_KEYWORDS, NULL},
|
||||||
METHODB(set_default_window_icon, METH_VARARGS),
|
METHODB(set_default_window_icon, METH_VARARGS),
|
||||||
METHODB(get_clipboard_string, METH_NOARGS),
|
METHODB(get_clipboard_string, METH_NOARGS),
|
||||||
METHODB(get_content_scale_for_window, METH_NOARGS),
|
METHODB(get_content_scale_for_window, METH_NOARGS),
|
||||||
|
|||||||
@@ -1362,6 +1362,7 @@ handle_compose_command(GraphicsManager *self, bool *is_dirty, const GraphicsComm
|
|||||||
dest_frame->x = 0; dest_frame->y = 0; dest_frame->width = img->width; dest_frame->height = img->height;
|
dest_frame->x = 0; dest_frame->y = 0; dest_frame->width = img->width; dest_frame->height = img->height;
|
||||||
dest_frame->base_frame_id = 0; dest_frame->bgcolor = 0;
|
dest_frame->base_frame_id = 0; dest_frame->bgcolor = 0;
|
||||||
*is_dirty = (g->other_frame_number - 1) == img->current_frame_index;
|
*is_dirty = (g->other_frame_number - 1) == img->current_frame_index;
|
||||||
|
if (*is_dirty) update_current_frame(self, img, &dest_data);
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
|
|||||||
@@ -2410,19 +2410,23 @@ opt('shell', '.',
|
|||||||
long_text='''
|
long_text='''
|
||||||
The shell program to execute. The default value of . means to use whatever shell
|
The shell program to execute. The default value of . means to use whatever shell
|
||||||
is set as the default shell for the current user. Note that on macOS if you
|
is set as the default shell for the current user. Note that on macOS if you
|
||||||
change this, you might need to add :code:`--login` to ensure that the shell
|
change this, you might need to add :code:`--login` and :code:`--interactive`
|
||||||
starts in interactive mode and reads its startup rc files.
|
to ensure that the shell starts in interactive mode and reads its startup rc files.
|
||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
|
|
||||||
opt('editor', '.',
|
opt('editor', '.',
|
||||||
long_text='''
|
long_text='''
|
||||||
The console editor to use when editing the kitty config file or similar tasks. A
|
The terminal editor (such as ``vim`` or ``nano``) to use when editing the kitty
|
||||||
value of . means to use the environment variables VISUAL and EDITOR in that
|
config file or similar tasks.
|
||||||
order. Note that this environment variable has to be set not just in your shell
|
|
||||||
startup scripts but system-wide, otherwise kitty will not see it.
|
The default value of . means to use the environment variables :envvar:`VISUAL`
|
||||||
'''
|
and :envvar:`EDITOR` in that order. If these variables aren't set, kitty will
|
||||||
)
|
run your :opt:`shell` (``$SHELL -l -i -c env``) to see if your shell config
|
||||||
|
files set :envvar:`VISUAL` or :envvar:`EDITOR`. If that doesn't work, kitty
|
||||||
|
will cycle through various known editors (``vim``, ``emacs``, etc) and take the
|
||||||
|
first one that exists on your system.
|
||||||
|
''')
|
||||||
|
|
||||||
opt('close_on_child_death', 'no',
|
opt('close_on_child_death', 'no',
|
||||||
option_type='to_bool', ctype='bool',
|
option_type='to_bool', ctype='bool',
|
||||||
|
|||||||
1
kitty/options/types.py
generated
1
kitty/options/types.py
generated
@@ -606,6 +606,7 @@ class Options:
|
|||||||
config_overrides: typing.Tuple[str, ...] = ()
|
config_overrides: typing.Tuple[str, ...] = ()
|
||||||
|
|
||||||
def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:
|
def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:
|
||||||
|
self.color_table = array(self.color_table.typecode, self.color_table)
|
||||||
if options_dict is not None:
|
if options_dict is not None:
|
||||||
for key in option_names:
|
for key in option_names:
|
||||||
setattr(self, key, options_dict[key])
|
setattr(self, key, options_dict[key])
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ def goto_tab_parse(func: str, rest: str) -> FuncArgsType:
|
|||||||
|
|
||||||
@func_with_args('detach_window')
|
@func_with_args('detach_window')
|
||||||
def detach_window_parse(func: str, rest: str) -> FuncArgsType:
|
def detach_window_parse(func: str, rest: str) -> FuncArgsType:
|
||||||
if rest not in ('new', 'new-tab', 'ask'):
|
if rest not in ('new', 'new-tab', 'ask', 'tab-prev', 'tab-left', 'tab-right'):
|
||||||
log_error('Ignoring invalid detach_window argument: {}'.format(rest))
|
log_error('Ignoring invalid detach_window argument: {}'.format(rest))
|
||||||
rest = 'new'
|
rest = 'new'
|
||||||
return func, (rest,)
|
return func, (rest,)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from kitty.constants import website_url
|
||||||
from kitty.options.utils import parse_marker_spec
|
from kitty.options.utils import parse_marker_spec
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -26,8 +27,8 @@ class CreateMarker(RemoteCommand):
|
|||||||
short_desc = 'Create a marker that highlights specified text'
|
short_desc = 'Create a marker that highlights specified text'
|
||||||
desc = (
|
desc = (
|
||||||
'Create a marker which can highlight text in the specified window. For example: '
|
'Create a marker which can highlight text in the specified window. For example: '
|
||||||
'create_marker text 1 ERROR. For full details see: https://sw.kovidgoyal.net/kitty/marks.html'
|
'create_marker text 1 ERROR. For full details see: {}'
|
||||||
)
|
).format(website_url('marks'))
|
||||||
options_spec = MATCH_WINDOW_OPTION + '''\n
|
options_spec = MATCH_WINDOW_OPTION + '''\n
|
||||||
--self
|
--self
|
||||||
type=bool-set
|
type=bool-set
|
||||||
|
|||||||
@@ -5,16 +5,15 @@
|
|||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, Dict, Generator, List, Optional
|
from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Union
|
||||||
|
|
||||||
from kitty.options.utils import parse_send_text_bytes
|
|
||||||
from kitty.key_encoding import decode_key_event_as_window_system_key
|
|
||||||
from kitty.fast_data_types import KeyEvent as WindowSystemKeyEvent
|
from kitty.fast_data_types import KeyEvent as WindowSystemKeyEvent
|
||||||
|
from kitty.key_encoding import decode_key_event_as_window_system_key
|
||||||
|
from kitty.options.utils import parse_send_text_bytes
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError,
|
MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError,
|
||||||
PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType,
|
PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, Window
|
||||||
Window
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -139,14 +138,18 @@ Do not send text to the active window, even if it is one of the matched windows.
|
|||||||
raise MatchError(payload_get('match_tab'), 'tabs')
|
raise MatchError(payload_get('match_tab'), 'tabs')
|
||||||
for tab in tabs:
|
for tab in tabs:
|
||||||
windows += tuple(tab)
|
windows += tuple(tab)
|
||||||
encoding, _, q = payload_get('data').partition(':')
|
pdata: str = payload_get('data')
|
||||||
|
encoding, _, q = pdata.partition(':')
|
||||||
if encoding == 'text':
|
if encoding == 'text':
|
||||||
data = q.encode('utf-8')
|
data: Union[bytes, WindowSystemKeyEvent] = q.encode('utf-8')
|
||||||
elif encoding == 'base64':
|
elif encoding == 'base64':
|
||||||
data = base64.standard_b64decode(q)
|
data = base64.standard_b64decode(q)
|
||||||
elif encoding == 'kitty-key':
|
elif encoding == 'kitty-key':
|
||||||
data = base64.standard_b64decode(q)
|
bdata = base64.standard_b64decode(q)
|
||||||
data = decode_key_event_as_window_system_key(data)
|
candidate = decode_key_event_as_window_system_key(bdata.decode('ascii'))
|
||||||
|
if candidate is None:
|
||||||
|
raise ValueError(f'Could not decode window system key: {q}')
|
||||||
|
data = candidate
|
||||||
else:
|
else:
|
||||||
raise TypeError(f'Invalid encoding for send-text data: {encoding}')
|
raise TypeError(f'Invalid encoding for send-text data: {encoding}')
|
||||||
exclude_active = payload_get('exclude_active')
|
exclude_active = payload_get('exclude_active')
|
||||||
|
|||||||
11
kitty/rgb.py
generated
11
kitty/rgb.py
generated
@@ -8,9 +8,9 @@ from typing import Optional, NamedTuple
|
|||||||
|
|
||||||
|
|
||||||
class Color(NamedTuple):
|
class Color(NamedTuple):
|
||||||
red: int
|
red: int = 0
|
||||||
green: int
|
green: int = 0
|
||||||
blue: int
|
blue: int = 0
|
||||||
|
|
||||||
def luminance(self) -> float:
|
def luminance(self) -> float:
|
||||||
return 0.299 * self.red + 0.587 * self.green + 0.114 * self.blue
|
return 0.299 * self.red + 0.587 * self.green + 0.114 * self.blue
|
||||||
@@ -25,6 +25,11 @@ class Color(NamedTuple):
|
|||||||
def __int__(self) -> int:
|
def __int__(self) -> int:
|
||||||
return self.red << 16 | self.green << 8 | self.blue
|
return self.red << 16 | self.green << 8 | self.blue
|
||||||
|
|
||||||
|
def as_bytearray(self, alpha: Optional[int] = None) -> bytearray:
|
||||||
|
if alpha is None:
|
||||||
|
return bytearray((self.red, self.green, self.blue))
|
||||||
|
return bytearray((self.red, self.green, self.blue, alpha))
|
||||||
|
|
||||||
|
|
||||||
def alpha_blend_channel(top_color: int, bottom_color: int, alpha: float) -> int:
|
def alpha_blend_channel(top_color: int, bottom_color: int, alpha: float) -> int:
|
||||||
return int(alpha * top_color + (1 - alpha) * bottom_color)
|
return int(alpha * top_color + (1 - alpha) * bottom_color)
|
||||||
|
|||||||
@@ -293,7 +293,8 @@ class Tab: # {{{
|
|||||||
w = self.active_window
|
w = self.active_window
|
||||||
if w is not None and self.resize_window_by(
|
if w is not None and self.resize_window_by(
|
||||||
w.id, increment, is_horizontal) is not None:
|
w.id, increment, is_horizontal) is not None:
|
||||||
ring_bell()
|
if get_options().enable_audio_bell:
|
||||||
|
ring_bell()
|
||||||
|
|
||||||
def reset_window_sizes(self) -> None:
|
def reset_window_sizes(self) -> None:
|
||||||
'@ac:win:Reset window sizes undoing any dynamic resizing of windows'
|
'@ac:win:Reset window sizes undoing any dynamic resizing of windows'
|
||||||
@@ -625,6 +626,9 @@ class TabManager: # {{{
|
|||||||
|
|
||||||
@active_tab_idx.setter
|
@active_tab_idx.setter
|
||||||
def active_tab_idx(self, val: int) -> None:
|
def active_tab_idx(self, val: int) -> None:
|
||||||
|
new_active_tab_idx = max(0, min(val, len(self.tabs) - 1))
|
||||||
|
if new_active_tab_idx == self._active_tab_idx:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
old_active_tab: Optional[Tab] = self.tabs[self._active_tab_idx]
|
old_active_tab: Optional[Tab] = self.tabs[self._active_tab_idx]
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -632,7 +636,7 @@ class TabManager: # {{{
|
|||||||
else:
|
else:
|
||||||
assert old_active_tab is not None
|
assert old_active_tab is not None
|
||||||
add_active_id_to_history(self.active_tab_history, old_active_tab.id)
|
add_active_id_to_history(self.active_tab_history, old_active_tab.id)
|
||||||
self._active_tab_idx = max(0, min(val, len(self.tabs) - 1))
|
self._active_tab_idx = new_active_tab_idx
|
||||||
try:
|
try:
|
||||||
new_active_tab: Optional[Tab] = self.tabs[self._active_tab_idx]
|
new_active_tab: Optional[Tab] = self.tabs[self._active_tab_idx]
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -716,6 +720,18 @@ class TabManager: # {{{
|
|||||||
if len(self.tabs) > 1:
|
if len(self.tabs) > 1:
|
||||||
self.set_active_tab_idx((self.active_tab_idx + len(self.tabs) + delta) % len(self.tabs))
|
self.set_active_tab_idx((self.active_tab_idx + len(self.tabs) + delta) % len(self.tabs))
|
||||||
|
|
||||||
|
def tab_at_location(self, loc: str) -> Optional[Tab]:
|
||||||
|
if loc == 'prev':
|
||||||
|
if self.active_tab_history:
|
||||||
|
old_active_tab_id = self.active_tab_history[-1]
|
||||||
|
for idx, tab in enumerate(self.tabs):
|
||||||
|
if tab.id == old_active_tab_id:
|
||||||
|
return tab
|
||||||
|
elif loc in ('left', 'right'):
|
||||||
|
delta = -1 if loc == 'left' else 1
|
||||||
|
idx = (len(self.tabs) + self.active_tab_idx + delta) % len(self.tabs)
|
||||||
|
return self.tabs[idx]
|
||||||
|
|
||||||
def goto_tab(self, tab_num: int) -> None:
|
def goto_tab(self, tab_num: int) -> None:
|
||||||
if tab_num >= len(self.tabs):
|
if tab_num >= len(self.tabs):
|
||||||
tab_num = max(0, len(self.tabs) - 1)
|
tab_num = max(0, len(self.tabs) - 1)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ AddressFamily = PopenType = Socket = StartupCtx = None
|
|||||||
SessionTab = SessionType = LayoutType = SpecialWindowInstance = None
|
SessionTab = SessionType = LayoutType = SpecialWindowInstance = None
|
||||||
MarkType = RemoteCommandType = CoreTextFont = FontConfigPattern = None
|
MarkType = RemoteCommandType = CoreTextFont = FontConfigPattern = None
|
||||||
KeyEventType = ImageManagerType = KittyCommonOpts = HandlerType = None
|
KeyEventType = ImageManagerType = KittyCommonOpts = HandlerType = None
|
||||||
GRT_t = GRT_a = GRT_d = GRT_f = GRT_m = GRT_o = None
|
GRT_t = GRT_a = GRT_d = GRT_f = GRT_m = GRT_o = GRT_C = None
|
||||||
ScreenSize = KittensKeyActionType = MouseEvent = AbstractEventLoop = None
|
ScreenSize = KittensKeyActionType = MouseEvent = AbstractEventLoop = None
|
||||||
TermManagerType = LoopType = Debug = GraphicsCommandType = None
|
TermManagerType = LoopType = Debug = GraphicsCommandType = None
|
||||||
|
|
||||||
|
|||||||
@@ -37,11 +37,12 @@ from .window import Window as WindowType
|
|||||||
EdgeLiteral = Literal['left', 'top', 'right', 'bottom']
|
EdgeLiteral = Literal['left', 'top', 'right', 'bottom']
|
||||||
MatchType = Literal['mime', 'ext', 'protocol', 'file', 'path', 'url', 'fragment_matches']
|
MatchType = Literal['mime', 'ext', 'protocol', 'file', 'path', 'url', 'fragment_matches']
|
||||||
PowerlineStyle = Literal['angled', 'slanted', 'round']
|
PowerlineStyle = Literal['angled', 'slanted', 'round']
|
||||||
GRT_a = Literal['t', 'T', 'q', 'p', 'd', 'f', 'a']
|
GRT_a = Literal['t', 'T', 'q', 'p', 'd', 'f', 'a', 'c', 'q']
|
||||||
GRT_f = Literal[24, 32, 100]
|
GRT_f = Literal[24, 32, 100]
|
||||||
GRT_t = Literal['d', 'f', 't', 's']
|
GRT_t = Literal['d', 'f', 't', 's']
|
||||||
GRT_o = Literal['z']
|
GRT_o = Literal['z']
|
||||||
GRT_m = Literal[0, 1]
|
GRT_m = Literal[0, 1]
|
||||||
|
GRT_C = Literal[0, 1]
|
||||||
GRT_d = Literal['a', 'A', 'c', 'C', 'i', 'I', 'p', 'P', 'q', 'Q', 'x', 'X', 'y', 'Y', 'z', 'Z', 'f', 'F']
|
GRT_d = Literal['a', 'A', 'c', 'C', 'i', 'I', 'p', 'P', 'q', 'Q', 'x', 'X', 'y', 'Y', 'z', 'Z', 'f', 'F']
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ from typing import Dict, NamedTuple, Optional
|
|||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
from .config import atomic_save
|
from .config import atomic_save
|
||||||
from .constants import Version, cache_dir, kitty_exe, version
|
from .constants import Version, cache_dir, kitty_exe, version, website_url
|
||||||
from .fast_data_types import add_timer, get_boss, monitor_pid
|
from .fast_data_types import add_timer, get_boss, monitor_pid
|
||||||
from .notify import notify
|
from .notify import notify
|
||||||
from .utils import log_error, open_url
|
from .utils import log_error, open_url
|
||||||
|
|
||||||
CHANGELOG_URL = 'https://sw.kovidgoyal.net/kitty/changelog.html'
|
CHANGELOG_URL = website_url('changelog')
|
||||||
RELEASED_VERSION_URL = 'https://sw.kovidgoyal.net/kitty/current-version.txt'
|
RELEASED_VERSION_URL = website_url() + 'current-version.txt'
|
||||||
CHECK_INTERVAL = 24 * 60 * 60.
|
CHECK_INTERVAL = 24 * 60 * 60.
|
||||||
|
|
||||||
|
|
||||||
@@ -104,8 +104,8 @@ def process_current_release(raw: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def run_worker() -> None:
|
def run_worker() -> None:
|
||||||
import time
|
|
||||||
import random
|
import random
|
||||||
|
import time
|
||||||
time.sleep(random.randint(1000, 4000) / 1000)
|
time.sleep(random.randint(1000, 4000) / 1000)
|
||||||
with suppress(BrokenPipeError): # happens if parent process is killed before us
|
with suppress(BrokenPipeError): # happens if parent process is killed before us
|
||||||
print(get_released_version())
|
print(get_released_version())
|
||||||
|
|||||||
@@ -623,8 +623,12 @@ def read_shell_environment(opts: Optional[Options] = None) -> Dict[str, str]:
|
|||||||
shell = resolved_shell(opts)
|
shell = resolved_shell(opts)
|
||||||
master, slave = openpty()
|
master, slave = openpty()
|
||||||
remove_blocking(master)
|
remove_blocking(master)
|
||||||
|
if '-l' not in shell and '--login' not in shell:
|
||||||
|
shell += ['-l']
|
||||||
|
if '-i' not in shell and '--interactive' not in shell:
|
||||||
|
shell += ['-i']
|
||||||
try:
|
try:
|
||||||
p = subprocess.Popen(shell + ['-l', '-c', 'env'], stdout=slave, stdin=slave, stderr=slave, start_new_session=True, close_fds=True)
|
p = subprocess.Popen(shell + ['-c', 'env'], stdout=slave, stdin=slave, stderr=slave, start_new_session=True, close_fds=True)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
log_error('Could not find shell to read environment')
|
log_error('Could not find shell to read environment')
|
||||||
return ans
|
return ans
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ class GitHub(Base): # {{{
|
|||||||
'target_commitish': 'master',
|
'target_commitish': 'master',
|
||||||
'name': 'version %s' % self.version,
|
'name': 'version %s' % self.version,
|
||||||
'body': f'Release version {self.version}.'
|
'body': f'Release version {self.version}.'
|
||||||
' For changelog, see https://sw.kovidgoyal.net/kitty/changelog.html'
|
' For changelog, see https://sw.kovidgoyal.net/kitty/changelog/'
|
||||||
' GPG key used for signing tarballs is: https://calibre-ebook.com/signatures/kovid.gpg',
|
' GPG key used for signing tarballs is: https://calibre-ebook.com/signatures/kovid.gpg',
|
||||||
'draft': False,
|
'draft': False,
|
||||||
'prerelease': False
|
'prerelease': False
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ mkShell rec {
|
|||||||
shellHook = if stdenv.isDarwin then ''
|
shellHook = if stdenv.isDarwin then ''
|
||||||
export KITTY_NO_LTO=
|
export KITTY_NO_LTO=
|
||||||
'' else ''
|
'' else ''
|
||||||
export KITTY_EGL_LIBRARY='${stdenv.lib.getLib libGL}/lib/libEGL.so.1'
|
export KITTY_EGL_LIBRARY='${lib.getLib libGL}/lib/libEGL.so.1'
|
||||||
export KITTY_STARTUP_NOTIFICATION_LIBRARY='${libstartup_notification}/lib/libstartup-notification-1.so'
|
export KITTY_STARTUP_NOTIFICATION_LIBRARY='${libstartup_notification}/lib/libstartup-notification-1.so'
|
||||||
export KITTY_CANBERRA_LIBRARY='${libcanberra}/lib/libcanberra.so'
|
export KITTY_CANBERRA_LIBRARY='${libcanberra}/lib/libcanberra.so'
|
||||||
'';
|
'';
|
||||||
|
|||||||
Reference in New Issue
Block a user