Compare commits

...

29 Commits

Author SHA1 Message Date
Kovid Goyal
5117a2c17a version 0.2.8 2017-08-05 08:21:20 +05:30
Kovid Goyal
ebce1d7c61 Fix incorrect handling of escape codes to change default foreground and background colors. Fixes #104 2017-08-04 21:57:22 +05:30
Kovid Goyal
e4668c1aff Replay set_dynamic_color and set_color_table_color 2017-08-04 20:47:59 +05:30
Kovid Goyal
2519c49c02 Fix incorrect escape codes generated when using the obsolete "normal" mouse protocol.
Fixes #105
2017-08-04 20:09:18 +05:30
Kovid Goyal
9e512ff58a Ensure queue_action is only called after the Tab object is fully initialized
Fix #103
2017-08-04 07:52:25 +05:30
Kovid Goyal
ed50595aca Add a --detach option
Allows kitty to detach itself from the controlling terminal. Useful
when launching kitty from a GUI environment with broken stdout/stderr or
when launching from a terminal that you want to close later without
affecting the launched kitty instance.
2017-08-03 22:35:47 +05:30
Kovid Goyal
9d62d087e0 Redirect xsel std streams to /dev/null
Prevents hanging if the streams are blocked. Fixes #102
2017-08-03 21:55:09 +05:30
Kovid Goyal
6b933e33f5 If the saved initial window size fails, retry creating the window with a standard size. Fixes #98 2017-07-30 08:32:51 +05:30
Kovid Goyal
a86931f401 Use the new glfw API to set WM_CLASS if available 2017-07-27 18:11:23 +05:30
Kovid Goyal
fc4e1595e8 Print a warning about unknown config keys 2017-07-23 22:59:30 +05:30
Kovid Goyal
b36c3f3425 Clean up config merging 2017-07-23 22:45:57 +05:30
Kovid Goyal
e427fd1233 Fix #95 2017-07-23 21:36:43 +05:30
Kovid Goyal
dd3af45043 Implement a send_text action to allow using keyboard shortcuts to send arbitrary text
Fixes #94
2017-07-23 14:37:15 +05:30
Kovid Goyal
304d42d4c2 Fix #93
Ignore invalid language names returned by the coca framework on macOS
2017-07-16 22:49:05 +05:30
Kovid Goyal
abca9280e7 Fix #92 2017-07-15 08:22:40 +05:30
Kovid Goyal
9a103f3979 version 0.2.7 2017-07-14 10:46:35 +05:30
Kovid Goyal
270dde7020 Use erase in line instead as it is more efficient 2017-07-13 19:32:18 +05:30
Kovid Goyal
11de18e737 Fix #91 2017-07-13 19:24:26 +05:30
Kovid Goyal
62db44c71e macOS: Ensure the LANG environment variable is set
Fixes #90
2017-06-25 20:44:16 +05:30
Kovid Goyal
3d5c65eaea Fix #89 2017-06-21 10:19:45 +05:30
Kovid Goyal
ea298f95f2 Another place we can use exist_ok 2017-06-09 00:00:02 +05:30
Kovid Goyal
2f21e0e341 Merge branch 'patch-2' of https://github.com/mimi1vx/kitty 2017-06-08 23:59:14 +05:30
Ondřej Súkup
ae62d36a4a Remove unneeded try except block
os.makedirs has from python-3.2 option `exist_ok`
2017-06-08 20:25:34 +02:00
Kovid Goyal
76e3101d9b Hide all symbols 2017-06-07 11:19:53 +05:30
Kovid Goyal
c3442545a8 Add a note about changing mouse wheel scroll direction 2017-06-06 00:06:43 +05:30
Kovid Goyal
3a9b0faa06 Be more positive ;) 2017-06-06 00:03:43 +05:30
Kovid Goyal
4989b1f8bb ... 2017-06-05 23:58:36 +05:30
Kovid Goyal
348fe4ada4 Option for window padding
Fixes #85
2017-06-05 23:57:17 +05:30
Kovid Goyal
bbc6b2d86a Option to size window margin (blank area outside window borders)
Defaults to zero
2017-06-05 22:27:47 +05:30
20 changed files with 396 additions and 118 deletions

View File

@@ -5,11 +5,12 @@
from ctypes import addressof from ctypes import addressof
from itertools import chain from itertools import chain
from threading import Lock from threading import Lock
from functools import partial
from .constants import GLfloat, GLint, GLuint, viewport_size from .constants import GLfloat, GLint, GLuint, viewport_size
from .fast_data_types import GL_TRIANGLE_FAN, glMultiDrawArrays, glUniform3fv from .fast_data_types import GL_TRIANGLE_FAN, glMultiDrawArrays, glUniform3fv
from .shaders import ShaderProgram from .shaders import ShaderProgram
from .utils import get_dpi from .utils import pt_to_px
def as_color(c): def as_color(c):
@@ -31,7 +32,8 @@ def as_rect(left, top, right, bottom, color=0):
class BordersProgram(ShaderProgram): class BordersProgram(ShaderProgram):
def __init__(self): def __init__(self):
ShaderProgram.__init__(self, '''\ ShaderProgram.__init__(
self, '''\
uniform vec3 colors[3]; uniform vec3 colors[3];
in vec3 rect; in vec3 rect;
out vec3 color; out vec3 color;
@@ -57,34 +59,73 @@ void main() {
glUniform3fv(self.uniform_location('colors'), 3, addressof(color_buf)) glUniform3fv(self.uniform_location('colors'), 3, addressof(color_buf))
def border_maker(rects):
' Create a function that will add all the rectangles for drawing a border to rects '
def r(l, t, b, r, color):
rects.extend(as_rect(l, t, b, r, color))
def vertical_edge(color, width, top, bottom, left):
r(left, top, left + width, bottom, color)
def horizontal_edge(color, height, left, right, top):
r(left, top, right, top + height, color)
def edge(func, color, sz, a, b):
return partial(func, color, sz, a, b)
def border(color, sz, left, top, right, bottom):
horz = edge(horizontal_edge, color, sz, left, right)
horz(top), horz(bottom - sz) # top, bottom edges
vert = edge(vertical_edge, color, sz, top, bottom)
vert(left), vert(right - sz) # left, right edges
return border
class Borders: class Borders:
def __init__(self, opts): def __init__(self, opts):
self.is_dirty = False self.is_dirty = False
self.lock = Lock() self.lock = Lock()
self.can_render = False self.can_render = False
dpix, dpiy = get_dpi()['logical'] self.border_width = pt_to_px(opts.window_border_width)
dpi = (dpix + dpiy) / 2 self.padding_width = pt_to_px(opts.window_padding_width)
self.border_width = round(opts.window_border_width * dpi / 72)
self.color_buf = (GLfloat * 9)( self.color_buf = (GLfloat * 9)(
*as_color(opts.background), *as_color(opts.background), *as_color(opts.active_border_color),
*as_color(opts.active_border_color), *as_color(opts.inactive_border_color))
*as_color(opts.inactive_border_color)
)
def __call__(self, windows, active_window, current_layout, extra_blank_rects, draw_window_borders=True): def __call__(
self,
windows,
active_window,
current_layout,
extra_blank_rects,
draw_window_borders=True
):
rects = [] rects = []
for br in chain(current_layout.blank_rects, extra_blank_rects): for br in chain(current_layout.blank_rects, extra_blank_rects):
rects.extend(as_rect(*br)) rects.extend(as_rect(*br))
if draw_window_borders and self.border_width > 0: bw, pw = self.border_width, self.padding_width
bw = self.border_width fw = bw + pw
border = border_maker(rects)
if fw > 0:
for w in windows: for w in windows:
g = w.geometry g = w.geometry
color = 1 if w is active_window else 2 if bw > 0 and draw_window_borders:
rects.extend(as_rect(g.left - bw, g.top - bw, g.left, g.bottom + bw, color)) # Draw the border rectangles
rects.extend(as_rect(g.left - bw, g.top - bw, g.right + bw, g.top, color)) color = 1 if w is active_window else 2
rects.extend(as_rect(g.right, g.top - bw, g.right + bw, g.bottom + bw, color)) border(
rects.extend(as_rect(g.left - bw, g.bottom, g.right + bw, g.bottom + bw, color)) color, bw, g.left - fw, g.top - fw, g.right + fw,
g.bottom + fw)
if pw > 0:
# Draw the background rectangles over the padding region
color = 0
border(
color, pw, g.left - pw, g.top - pw, g.right + pw,
g.bottom + pw)
with self.lock: with self.lock:
self.num_of_rects = len(rects) // 12 self.num_of_rects = len(rects) // 12
self.rects = (GLfloat * len(rects))() self.rects = (GLfloat * len(rects))()
@@ -108,4 +149,7 @@ class Borders:
program.send_data(self.rects) program.send_data(self.rects)
program.set_colors(self.color_buf) program.set_colors(self.color_buf)
self.is_dirty = False self.is_dirty = False
glMultiDrawArrays(GL_TRIANGLE_FAN, addressof(self.starts), addressof(self.counts), self.num_of_rects) glMultiDrawArrays(
GL_TRIANGLE_FAN,
addressof(self.starts),
addressof(self.counts), self.num_of_rects)

View File

@@ -28,7 +28,7 @@ from .fonts.render import set_font_family
from .borders import BordersProgram from .borders import BordersProgram
from .char_grid import cursor_shader, cell_shader from .char_grid import cursor_shader, cell_shader
from .constants import is_key_pressed from .constants import is_key_pressed
from .keys import interpret_text_event, interpret_key_event, get_shortcut from .keys import interpret_text_event, interpret_key_event, get_shortcut, get_sent_data
from .session import create_session from .session import create_session
from .shaders import Sprites, ShaderProgram from .shaders import Sprites, ShaderProgram
from .tabs import TabManager, SpecialWindow from .tabs import TabManager, SpecialWindow
@@ -308,7 +308,7 @@ class Boss(Thread):
return return
if window.char_grid.scrolled_by and key not in MODIFIER_KEYS and action == GLFW_PRESS: if window.char_grid.scrolled_by and key not in MODIFIER_KEYS and action == GLFW_PRESS:
window.scroll_end() window.scroll_end()
data = interpret_key_event(key, scancode, mods, window, action) data = get_sent_data(self.opts.send_text_map, key, scancode, mods, window, action) or interpret_key_event(key, scancode, mods, window, action)
if data: if data:
window.write_to_child(data) window.write_to_child(data)

View File

@@ -297,7 +297,7 @@ class CharGrid:
if raw is None: if raw is None:
return 0 return 0
val = to_color(raw) val = to_color(raw)
return None if val is None else (color_as_int(val) << 8) | 1 return None if val is None else (color_as_int(val) << 8) | 2
for which, val in changes.items(): for which, val in changes.items():
val = item(val) val = item(val)
@@ -322,10 +322,9 @@ class CharGrid:
sprites = get_boss().sprites sprites = get_boss().sprites
is_dirty = self.screen.is_dirty() is_dirty = self.screen.is_dirty()
with sprites.lock: with sprites.lock:
fg = self.screen.default_fg fg, bg = self.screen.default_fg, self.screen.default_bg
fg = fg >> 8 if fg & 1 else self.default_fg fg = fg >> 8 if fg & 2 else self.default_fg
bg = self.screen.default_bg bg = bg >> 8 if bg & 2 else self.default_bg
bg = bg >> 8 if bg & 1 else self.default_bg
cursor_changed, history_line_added_count = self.screen.update_cell_data( cursor_changed, history_line_added_count = self.screen.update_cell_data(
sprites.backend, self.color_profile, addressof(self.main_sprite_map), fg, bg, force_full_refresh) sprites.backend, self.color_profile, addressof(self.main_sprite_map), fg, bg, force_full_refresh)
if self.scrolled_by: if self.scrolled_by:
@@ -475,9 +474,9 @@ class CharGrid:
if self.render_buf_is_dirty or sel != self.last_rendered_selection: if self.render_buf_is_dirty or sel != self.last_rendered_selection:
memmove(buf, self.render_buf, sizeof(type(buf))) memmove(buf, self.render_buf, sizeof(type(buf)))
fg = self.screen.highlight_fg fg = self.screen.highlight_fg
fg = fg >> 8 if fg & 1 else self.highlight_fg fg = fg >> 8 if fg & 2 else self.highlight_fg
bg = self.screen.highlight_bg bg = self.screen.highlight_bg
bg = bg >> 8 if bg & 1 else self.highlight_bg bg = bg >> 8 if bg & 2 else self.highlight_bg
self.screen.apply_selection(addressof(buf), start[0], start[1], end[0], end[1], fg, bg) self.screen.apply_selection(addressof(buf), start[0], start[1], end[0], end[1], fg, bg)
if self.render_buf_is_dirty or self.last_rendered_selection != sel: if self.render_buf_is_dirty or self.last_rendered_selection != sel:
sprites.set_sprite_map(self.buffer_id, buf) sprites.set_sprite_map(self.buffer_id, buf)
@@ -503,7 +502,7 @@ class CharGrid:
left = sg.xstart + cursor.x * sg.dx left = sg.xstart + cursor.x * sg.dx
top = sg.ystart - cursor.y * sg.dy top = sg.ystart - cursor.y * sg.dy
cc = self.screen.cursor_color cc = self.screen.cursor_color
col = color_from_int(cc >> 8) if cc & 1 else self.opts.cursor col = color_from_int(cc >> 8) if cc & 2 else self.opts.cursor
shape = cursor.shape or self.default_cursor.shape shape = cursor.shape or self.default_cursor.shape
alpha = self.opts.cursor_opacity alpha = self.opts.cursor_opacity
if alpha < 1.0 and shape == CURSOR_BLOCK: if alpha < 1.0 and shape == CURSOR_BLOCK:

View File

@@ -125,14 +125,14 @@ def write_osc(code, string=''):
write(OSC + str(code) + string + '\x07') write(OSC + str(code) + string + '\x07')
set_dynamic_color = write_osc set_dynamic_color = set_color_table_color = write_osc
def replay(raw): def replay(raw):
for line in raw.splitlines(): for line in raw.splitlines():
if line.strip(): if line.strip():
cmd, rest = line.partition(' ')[::2] cmd, rest = line.partition(' ')[::2]
if cmd in {'draw', 'set_title', 'set_icon'}: if cmd in {'draw', 'set_title', 'set_icon', 'set_dynamic_color', 'set_color_table_color'}:
globals()[cmd](rest) globals()[cmd](rest)
else: else:
rest = map(int, rest.split()) if rest else () rest = map(int, rest.split()) if rest else ()

View File

@@ -8,8 +8,9 @@
#include "data-types.h" #include "data-types.h"
#include <Cocoa/Cocoa.h> #include <Cocoa/Cocoa.h>
#include <AvailabilityMacros.h>
#ifndef NSWindowStyleMaskTitled #if (MAC_OS_X_VERSION_MAX_ALLOWED < 101200)
#define NSWindowStyleMaskTitled NSTitledWindowMask #define NSWindowStyleMaskTitled NSTitledWindowMask
#endif #endif
@@ -25,3 +26,18 @@ cocoa_hide_titlebar(PyObject UNUSED *self, PyObject *window_id) {
} }
Py_RETURN_NONE; Py_RETURN_NONE;
} }
PyObject*
cocoa_get_lang(PyObject UNUSED *self) {
NSString* locale = nil;
NSString* lang_code = [[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode];
NSString* country_code = [[NSLocale currentLocale] objectForKey:NSLocaleCountryCode];
if (lang_code && country_code) {
locale = [NSString stringWithFormat:@"%@_%@", lang_code, country_code];
} else {
locale = [[NSLocale currentLocale] localeIdentifier];
}
if (!locale) { Py_RETURN_NONE; }
return Py_BuildValue("s", [locale UTF8String]);
}

View File

@@ -2,6 +2,7 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import ast
import json import json
import os import os
import re import re
@@ -84,16 +85,22 @@ named_keys = {
} }
def parse_key(val, keymap): def parse_shortcut(sc):
sc, action = val.partition(' ')[::2]
action = action.strip()
sc = sc.strip()
if not sc or not action:
return
parts = sc.split('+') parts = sc.split('+')
mods = parse_mods(parts[:-1]) mods = parse_mods(parts[:-1])
key = parts[-1].upper() key = parts[-1].upper()
key = getattr(defines, 'GLFW_KEY_' + named_keys.get(key, key), None) key = getattr(defines, 'GLFW_KEY_' + named_keys.get(key, key), None)
if key is not None:
return mods, key
return None, None
def parse_key(val, keymap):
sc, action = val.partition(' ')[::2]
sc, action = sc.strip(), action.strip()
if not sc or not action:
return
mods, key = parse_shortcut(sc)
if key is None: if key is None:
safe_print( safe_print(
'Shortcut: {} has an unknown key, ignoring'.format(val), 'Shortcut: {} has an unknown key, ignoring'.format(val),
@@ -137,6 +144,39 @@ def parse_symbol_map(val):
return symbol_map return symbol_map
def parse_send_text(val):
parts = val.split(' ')
def abort(msg):
safe_print(
'Send text: {} is invalid ({}), ignoring'.format(val, msg), file=sys.stderr
)
return {}
if len(parts) < 3:
return abort('Incomplete')
text = ' '.join(parts[2:])
mode, sc = parts[:2]
mods, key = parse_shortcut(sc.strip())
if key is None:
return abort('Invalid shortcut')
text = ast.literal_eval("'''" + text + "'''").encode('utf-8')
if not text:
return abort('Empty text')
if mode in ('all', '*'):
modes = parse_send_text.all_modes
else:
modes = frozenset(mode.split(',')).intersection(parse_send_text.all_modes)
if not modes:
return abort('Invalid keyboard modes')
return {mode: {(mods, key): text} for mode in modes}
parse_send_text.all_modes = frozenset({'normal', 'application', 'kitty'})
def to_open_url_modifiers(val): def to_open_url_modifiers(val):
return parse_mods(val.split('+')) return parse_mods(val.split('+'))
@@ -150,28 +190,38 @@ def to_layout_names(raw):
raise ValueError('The window layout {} is unknown'.format(p)) raise ValueError('The window layout {} is unknown'.format(p))
def positive_int(x):
return max(0, int(x))
def positive_float(x):
return max(0, float(x))
type_map = { type_map = {
'scrollback_lines': int, 'scrollback_lines': positive_int,
'scrollback_pager': shlex.split, 'scrollback_pager': shlex.split,
'scrollback_in_new_tab': to_bool, 'scrollback_in_new_tab': to_bool,
'font_size': to_font_size, 'font_size': to_font_size,
'font_size_delta': float, 'font_size_delta': positive_float,
'cursor_shape': to_cursor_shape, 'cursor_shape': to_cursor_shape,
'cursor_opacity': to_opacity, 'cursor_opacity': to_opacity,
'open_url_modifiers': to_open_url_modifiers, 'open_url_modifiers': to_open_url_modifiers,
'repaint_delay': int, 'repaint_delay': positive_int,
'window_border_width': float, 'window_border_width': positive_float,
'window_margin_width': positive_float,
'window_padding_width': positive_float,
'wheel_scroll_multiplier': float, 'wheel_scroll_multiplier': float,
'visual_bell_duration': float, 'visual_bell_duration': positive_float,
'enable_audio_bell': to_bool, 'enable_audio_bell': to_bool,
'click_interval': float, 'click_interval': positive_float,
'mouse_hide_wait': float, 'mouse_hide_wait': positive_float,
'cursor_blink_interval': float, 'cursor_blink_interval': positive_float,
'cursor_stop_blinking_after': float, 'cursor_stop_blinking_after': positive_float,
'enabled_layouts': to_layout_names, 'enabled_layouts': to_layout_names,
'remember_window_size': to_bool, 'remember_window_size': to_bool,
'initial_window_width': int, 'initial_window_width': positive_int,
'initial_window_height': int, 'initial_window_height': positive_int,
'use_system_wcwidth': to_bool, 'use_system_wcwidth': to_bool,
'macos_hide_titlebar': to_bool, 'macos_hide_titlebar': to_bool,
} }
@@ -188,8 +238,10 @@ for a in ('active', 'inactive'):
type_map['%s_tab_%s' % (a, b)] = lambda x: to_color(x, validate=True) type_map['%s_tab_%s' % (a, b)] = lambda x: to_color(x, validate=True)
def parse_config(lines): def parse_config(lines, check_keys=True):
ans = {'keymap': {}, 'symbol_map': {}} ans = {'keymap': {}, 'symbol_map': {}, 'send_text_map': {'kitty': {}, 'normal': {}, 'application': {}}}
if check_keys:
all_keys = defaults._asdict()
for line in lines: for line in lines:
line = line.strip() line = line.strip()
if not line or line.startswith('#'): if not line or line.startswith('#'):
@@ -203,6 +255,15 @@ def parse_config(lines):
if key == 'symbol_map': if key == 'symbol_map':
ans['symbol_map'].update(parse_symbol_map(val)) ans['symbol_map'].update(parse_symbol_map(val))
continue continue
if key == 'send_text':
stvals = parse_send_text(val)
for k, v in ans['send_text_map'].items():
v.update(stvals.get(k, {}))
continue
if check_keys:
if key not in all_keys:
safe_print('Ignoring unknown config key: {}'.format(key), file=sys.stderr)
continue
tm = type_map.get(key) tm = type_map.get(key)
if tm is not None: if tm is not None:
val = tm(val) val = tm(val)
@@ -213,38 +274,42 @@ def parse_config(lines):
with open( with open(
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'kitty.conf') os.path.join(os.path.dirname(os.path.abspath(__file__)), 'kitty.conf')
) as f: ) as f:
defaults = parse_config(f.readlines()) defaults = parse_config(f.readlines(), check_keys=False)
Options = namedtuple('Defaults', ','.join(defaults.keys())) Options = namedtuple('Defaults', ','.join(defaults.keys()))
defaults = Options(**defaults) defaults = Options(**defaults)
actions = frozenset(defaults.keymap.values()) actions = frozenset(defaults.keymap.values())
def update_dict(a, b): def merge_keymaps(defaults, newvals):
a.update(b) ans = defaults.copy()
return a for k, v in newvals.items():
if v in {'noop', 'no-op', 'no_op'}:
ans.pop(k, None)
continue
if v in actions:
ans[k] = v
return ans
def merge_dicts(vals, defaults): def merge_dicts(defaults, newvals):
return { ans = defaults.copy()
k: update_dict(v, vals.get(k, {})) ans.update(newvals)
if isinstance(v, dict) else vals.get(k, v) return ans
for k, v in defaults.items()
}
def merge_configs(ans, vals): def merge_configs(defaults, vals):
vals['keymap'] = { ans = {}
k: v for k, v in defaults.items():
for k, v in vals.get('keymap', {}).items() if v in actions if isinstance(v, dict):
} newvals = vals.get(k, {})
remove_keys = { if k == 'keymap':
k ans['keymap'] = merge_keymaps(v, newvals)
for k, v in vals.get('keymap', {}).items() elif k == 'send_text_map':
if v in ('noop', 'no-op', 'no_op') ans[k] = {m: merge_dicts(mm, newvals.get(m, {})) for m, mm in v.items()}
} else:
ans = merge_dicts(vals, ans) ans[k] = merge_dicts(v, newvals)
for k in remove_keys: else:
ans['keymap'].pop(k, None) ans[k] = vals.get(k, v)
return ans return ans

View File

@@ -15,7 +15,7 @@ from .fast_data_types import (
GLFW_KEY_LEFT_SUPER, GLFW_KEY_RIGHT_SUPER) GLFW_KEY_LEFT_SUPER, GLFW_KEY_RIGHT_SUPER)
appname = 'kitty' appname = 'kitty'
version = (0, 2, 6) version = (0, 2, 8)
str_version = '.'.join(map(str, version)) str_version = '.'.join(map(str, version))
_plat = sys.platform.lower() _plat = sys.platform.lower()
isosx = 'darwin' in _plat isosx = 'darwin' in _plat
@@ -32,10 +32,7 @@ def _get_config_dir():
candidate = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME') or ('~/Library/Preferences' if isosx else '~/.config'))) candidate = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME') or ('~/Library/Preferences' if isosx else '~/.config')))
ans = os.path.join(candidate, appname) ans = os.path.join(candidate, appname)
try: os.makedirs(ans, exist_ok=True)
os.makedirs(ans)
except FileExistsError:
pass
return ans return ans

View File

@@ -31,6 +31,17 @@ change_wcwidth_wrap(PyObject UNUSED *self, PyObject *use9) {
Py_RETURN_NONE; Py_RETURN_NONE;
} }
static PyObject*
redirect_std_streams(PyObject UNUSED *self, PyObject *args) {
char *devnull = NULL;
if (!PyArg_ParseTuple(args, "s", &devnull)) return NULL;
if (freopen(devnull, "r", stdin) == NULL) return PyErr_SetFromErrno(PyExc_EnvironmentError);
if (freopen(devnull, "w", stdout) == NULL) return PyErr_SetFromErrno(PyExc_EnvironmentError);
if (freopen(devnull, "w", stderr) == NULL) return PyErr_SetFromErrno(PyExc_EnvironmentError);
Py_RETURN_NONE;
}
static PyMethodDef module_methods[] = { static PyMethodDef module_methods[] = {
GL_METHODS GL_METHODS
{"drain_read", (PyCFunction)drain_read, METH_O, ""}, {"drain_read", (PyCFunction)drain_read, METH_O, ""},
@@ -38,6 +49,7 @@ static PyMethodDef module_methods[] = {
{"parse_bytes_dump", (PyCFunction)parse_bytes_dump, METH_VARARGS, ""}, {"parse_bytes_dump", (PyCFunction)parse_bytes_dump, METH_VARARGS, ""},
{"read_bytes", (PyCFunction)read_bytes, METH_VARARGS, ""}, {"read_bytes", (PyCFunction)read_bytes, METH_VARARGS, ""},
{"read_bytes_dump", (PyCFunction)read_bytes_dump, METH_VARARGS, ""}, {"read_bytes_dump", (PyCFunction)read_bytes_dump, METH_VARARGS, ""},
{"redirect_std_streams", (PyCFunction)redirect_std_streams, METH_VARARGS, ""},
{"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""}, {"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""},
{"change_wcwidth", (PyCFunction)change_wcwidth_wrap, METH_O, ""}, {"change_wcwidth", (PyCFunction)change_wcwidth_wrap, METH_O, ""},
#ifndef __APPLE__ #ifndef __APPLE__
@@ -58,7 +70,7 @@ static struct PyModuleDef module = {
#include <termios.h> #include <termios.h>
PyMODINIT_FUNC EXPORTED PyMODINIT_FUNC
PyInit_fast_data_types(void) { PyInit_fast_data_types(void) {
PyObject *m; PyObject *m;

View File

@@ -13,6 +13,7 @@
#define PY_SSIZE_T_CLEAN #define PY_SSIZE_T_CLEAN
#include <Python.h> #include <Python.h>
#define UNUSED __attribute__ ((unused)) #define UNUSED __attribute__ ((unused))
#define EXPORTED __attribute__ ((visibility ("default")))
#define MAX(x, y) (((x) > (y)) ? (x) : (y)) #define MAX(x, y) (((x) > (y)) ? (x) : (y))
#define MIN(x, y) (((x) > (y)) ? (y) : (x)) #define MIN(x, y) (((x) > (y)) ? (y) : (x))
#define xstr(s) str(s) #define xstr(s) str(s)

View File

@@ -208,6 +208,18 @@ glfw_get_key_name(PyObject UNUSED *self, PyObject *args) {
return Py_BuildValue("s", glfwGetKeyName(key, scancode)); return Py_BuildValue("s", glfwGetKeyName(key, scancode));
} }
PyObject*
glfw_init_hint_string(PyObject UNUSED *self, PyObject *args) {
int hint_id;
char *hint;
if (!PyArg_ParseTuple(args, "is", &hint_id, &hint)) return NULL;
#ifdef glfwInitHintString
glfwInitHintString(hint_id, hint);
#endif
Py_RETURN_NONE;
}
// }}} // }}}
static void static void
@@ -448,6 +460,10 @@ init_glfw(PyObject *m) {
PyEval_InitThreads(); PyEval_InitThreads();
glfwSetErrorCallback(cb_error_callback); glfwSetErrorCallback(cb_error_callback);
#define ADDC(n) if(PyModule_AddIntConstant(m, #n, n) != 0) return false; #define ADDC(n) if(PyModule_AddIntConstant(m, #n, n) != 0) return false;
#ifdef GLFW_X11_WM_CLASS_NAME
ADDC(GLFW_X11_WM_CLASS_NAME)
ADDC(GLFW_X11_WM_CLASS_CLASS)
#endif
ADDC(GLFW_RELEASE); ADDC(GLFW_RELEASE);
ADDC(GLFW_PRESS); ADDC(GLFW_PRESS);
ADDC(GLFW_REPEAT); ADDC(GLFW_REPEAT);

View File

@@ -17,12 +17,16 @@ PyObject* glfw_wait_events(PyObject UNUSED *self, PyObject*);
PyObject* glfw_post_empty_event(PyObject UNUSED *self); PyObject* glfw_post_empty_event(PyObject UNUSED *self);
PyObject* glfw_get_physical_dpi(PyObject UNUSED *self); PyObject* glfw_get_physical_dpi(PyObject UNUSED *self);
PyObject* glfw_get_key_name(PyObject UNUSED *self, PyObject *args); PyObject* glfw_get_key_name(PyObject UNUSED *self, PyObject *args);
PyObject* glfw_init_hint_string(PyObject UNUSED *self, PyObject *args);
#ifdef __APPLE__ #ifdef __APPLE__
PyObject* cocoa_hide_titlebar(PyObject UNUSED *self, PyObject *window_id); PyObject* cocoa_hide_titlebar(PyObject UNUSED *self, PyObject *window_id);
PyObject* cocoa_get_lang(PyObject UNUSED *self);
#define COCOA_HIDE_TITLEBAR {"cocoa_hide_titlebar", (PyCFunction)cocoa_hide_titlebar, METH_O, ""}, #define COCOA_HIDE_TITLEBAR {"cocoa_hide_titlebar", (PyCFunction)cocoa_hide_titlebar, METH_O, ""},
#define COCOA_GET_LANG {"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
#else #else
#define COCOA_HIDE_TITLEBAR #define COCOA_HIDE_TITLEBAR
#define COCOA_GET_LANG
#endif #endif
#define GLFW_FUNC_WRAPPERS \ #define GLFW_FUNC_WRAPPERS \
@@ -35,5 +39,7 @@ PyObject* cocoa_hide_titlebar(PyObject UNUSED *self, PyObject *window_id);
{"glfw_post_empty_event", (PyCFunction)glfw_post_empty_event, METH_NOARGS, ""}, \ {"glfw_post_empty_event", (PyCFunction)glfw_post_empty_event, METH_NOARGS, ""}, \
{"glfw_get_physical_dpi", (PyCFunction)glfw_get_physical_dpi, METH_NOARGS, ""}, \ {"glfw_get_physical_dpi", (PyCFunction)glfw_get_physical_dpi, METH_NOARGS, ""}, \
{"glfw_get_key_name", (PyCFunction)glfw_get_key_name, METH_VARARGS, ""}, \ {"glfw_get_key_name", (PyCFunction)glfw_get_key_name, METH_VARARGS, ""}, \
COCOA_HIDE_TITLEBAR {"glfw_init_hint_string", (PyCFunction)glfw_init_hint_string, METH_VARARGS, ""}, \
COCOA_HIDE_TITLEBAR \
COCOA_GET_LANG

View File

@@ -85,6 +85,7 @@ control_codes.update({
}) })
control_codes[defines.GLFW_KEY_6] = (30,) control_codes[defines.GLFW_KEY_6] = (30,)
control_codes[defines.GLFW_KEY_SLASH] = (31,) control_codes[defines.GLFW_KEY_SLASH] = (31,)
control_codes[defines.GLFW_KEY_SPACE] = (0,)
rmkx_key_map = smkx_key_map.copy() rmkx_key_map = smkx_key_map.copy()
@@ -104,6 +105,12 @@ def get_key_map(screen):
return cursor_key_mode_map[screen.cursor_key_mode] return cursor_key_mode_map[screen.cursor_key_mode]
def keyboard_mode_name(screen):
if screen.extended_keyboard:
return 'kitty'
return 'application' if screen.cursor_key_mode else 'normal'
valid_localized_key_names = { valid_localized_key_names = {
k: getattr(defines, 'GLFW_KEY_' + k) k: getattr(defines, 'GLFW_KEY_' + k)
for k in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' for k in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
@@ -194,3 +201,11 @@ def interpret_text_event(codepoint, mods, window):
def get_shortcut(keymap, mods, key, scancode): def get_shortcut(keymap, mods, key, scancode):
key = get_localized_key(key, scancode) key = get_localized_key(key, scancode)
return keymap.get((mods & 0b1111, key)) return keymap.get((mods & 0b1111, key))
def get_sent_data(send_text_map, key, scancode, mods, window, action):
if action in (defines.GLFW_PRESS, defines.GLFW_REPEAT):
key = get_localized_key(key, scancode)
m = keyboard_mode_name(window.screen)
keymap = send_text_map[m]
return keymap.get((mods & 0b1111, key))

View File

@@ -45,7 +45,7 @@ cursor_shape block
cursor_blink_interval 0.5 cursor_blink_interval 0.5
# Stop blinking cursor after the specified number of seconds of keyboard inactivity. Set to # Stop blinking cursor after the specified number of seconds of keyboard inactivity. Set to
# zero or a negative number to never stop blinking. # zero to never stop blinking.
cursor_stop_blinking_after 15.0 cursor_stop_blinking_after 15.0
# Number of lines of history to keep in memory for scrolling back # Number of lines of history to keep in memory for scrolling back
@@ -59,7 +59,8 @@ scrollback_pager less +G -R
# When viewing scrollback in a new window, put it in a new tab as well # When viewing scrollback in a new window, put it in a new tab as well
scrollback_in_new_tab no scrollback_in_new_tab no
# Wheel scroll multiplier (modify the amount scrolled by the mouse wheel) # Wheel scroll multiplier (modify the amount scrolled by the mouse wheel). Use negative
# numbers to change scroll direction.
wheel_scroll_multiplier 5.0 wheel_scroll_multiplier 5.0
# The interval between successive clicks to detect double/triple clicks (in seconds) # The interval between successive clicks to detect double/triple clicks (in seconds)
@@ -71,7 +72,7 @@ click_interval 0.5
select_by_word_characters :@-./_~?&=%+# select_by_word_characters :@-./_~?&=%+#
# Hide mouse cursor after the specified number of seconds of the mouse not being used. Set to # Hide mouse cursor after the specified number of seconds of the mouse not being used. Set to
# zero or a negative number to disable mouse cursor hiding. # zero to disable mouse cursor hiding.
mouse_hide_wait 3.0 mouse_hide_wait 3.0
# The enabled window layouts. A comma separated list of layout names. The special value * means # The enabled window layouts. A comma separated list of layout names. The special value * means
@@ -120,6 +121,12 @@ term xterm-kitty
# The width (in pts) of window borders. Will be rounded to the nearest number of pixels based on screen resolution. # The width (in pts) of window borders. Will be rounded to the nearest number of pixels based on screen resolution.
window_border_width 1 window_border_width 1
# The window margin (in pts) (blank area outside the border)
window_margin_width 0
# The window padding (in pts) (blank area between the text and the window border)
window_padding_width 0
# The color for the border of the active window # The color for the border of the active window
active_border_color #00ff00 active_border_color #00ff00
@@ -224,6 +231,23 @@ map ctrl+shift+equal increase_font_size
map ctrl+shift+minus decrease_font_size map ctrl+shift+minus decrease_font_size
map ctrl+shift+backspace restore_font_size map ctrl+shift+backspace restore_font_size
# Sending arbitrary text on shortcut key presses
# You can tell kitty to send arbitrary (UTF-8) encoded text to
# the client program when pressing specified shortcut keys. For example:
# send_text all ctrl+alt+a Special text
# This will send "Special text" when you press the Ctrl+Alt+a key combination.
# The text to be sent is a python string literal so you can use escapes like
# \x1b to send control codes or \u21fb to send unicode characters (or you can
# just input the unicode characters directly as UTF-8 text). The first argument
# to send_text is the keyboard modes in which to activate the shortcut. The possible
# values are normal or application or kitty or a comma separated combination of them.
# The special keyword all means all modes. The modes normal and application refer to
# the DECCKM cursor key mode for terminals, and kitty refers to the special kitty
# extended keyboard protocol. Another example, that outputs a word and then moves the cursor
# to the start of the line (same as pressing the Home key):
# send_text normal ctrl+alt+a Word\x1b[H
# send_text application ctrl+alt+a Word\x1bOH
# Symbol mapping (special font for specified unicode code points). Map the # Symbol mapping (special font for specified unicode code points). Map the
# specified unicode codepoints to a particular font. Useful if you need special # specified unicode codepoints to a particular font. Useful if you need special
# rendering for some symbols, such as for Powerline. Avoids the need for # rendering for some symbols, such as for Powerline. Avoids the need for

View File

@@ -6,25 +6,29 @@ from collections import namedtuple
from itertools import islice from itertools import islice
from .constants import WindowGeometry, viewport_size, cell_size, get_boss from .constants import WindowGeometry, viewport_size, cell_size, get_boss
from .utils import pt_to_px
def available_height(): def available_height():
return viewport_size.height - get_boss().current_tab_bar_height return viewport_size.height - get_boss().current_tab_bar_height
def layout_dimension(length, cell_length, number_of_windows=1, border_length=0, left_align=False): def layout_dimension(length, cell_length, number_of_windows=1, border_length=0, margin_length=0, padding_length=0, left_align=False):
number_of_cells = length // cell_length number_of_cells = length // cell_length
border_length += padding_length
space_needed_for_border = number_of_windows * 2 * border_length space_needed_for_border = number_of_windows * 2 * border_length
space_needed_for_padding = number_of_windows * 2 * margin_length
space_needed = space_needed_for_padding + space_needed_for_border
extra = length - number_of_cells * cell_length extra = length - number_of_cells * cell_length
while extra < space_needed_for_border: while extra < space_needed:
number_of_cells -= 1 number_of_cells -= 1
extra = length - number_of_cells * cell_length extra = length - number_of_cells * cell_length
cells_per_window = number_of_cells // number_of_windows cells_per_window = number_of_cells // number_of_windows
extra -= space_needed_for_border extra -= space_needed
pos = 0 if left_align else (extra // 2) pos = 0 if left_align else (extra // 2)
pos += border_length pos += border_length + margin_length
inner_length = cells_per_window * cell_length inner_length = cells_per_window * cell_length
window_length = 2 * border_length + inner_length window_length = 2 * (border_length + margin_length) + inner_length
extra = number_of_cells - (cells_per_window * number_of_windows) extra = number_of_cells - (cells_per_window * number_of_windows)
while number_of_windows > 0: while number_of_windows > 0:
number_of_windows -= 1 number_of_windows -= 1
@@ -43,6 +47,8 @@ class Layout:
def __init__(self, opts, border_width, windows): def __init__(self, opts, border_width, windows):
self.opts = opts self.opts = opts
self.border_width = border_width self.border_width = border_width
self.margin_width = pt_to_px(opts.window_margin_width)
self.padding_width = pt_to_px(opts.window_padding_width)
# A set of rectangles corresponding to the blank spaces at the edges of # A set of rectangles corresponding to the blank spaces at the edges of
# this layout, i.e. spaces that are not covered by any window # this layout, i.e. spaces that are not covered by any window
self.blank_rects = () self.blank_rects = ()
@@ -76,9 +82,9 @@ def window_geometry(xstart, xnum, ystart, ynum):
return WindowGeometry(left=xstart, top=ystart, xnum=xnum, ynum=ynum, right=xstart + cell_size.width * xnum, bottom=ystart + cell_size.height * ynum) return WindowGeometry(left=xstart, top=ystart, xnum=xnum, ynum=ynum, right=xstart + cell_size.width * xnum, bottom=ystart + cell_size.height * ynum)
def layout_single_window(): def layout_single_window(margin_length, padding_length):
xstart, xnum = next(layout_dimension(viewport_size.width, cell_size.width)) xstart, xnum = next(layout_dimension(viewport_size.width, cell_size.width, margin_length=margin_length, padding_length=padding_length))
ystart, ynum = next(layout_dimension(available_height(), cell_size.height)) ystart, ynum = next(layout_dimension(available_height(), cell_size.height, margin_length=margin_length, padding_length=padding_length))
return window_geometry(xstart, xnum, ystart, ynum) return window_geometry(xstart, xnum, ystart, ynum)
@@ -120,7 +126,7 @@ class Stack(Layout):
def __call__(self, windows, active_window_idx): def __call__(self, windows, active_window_idx):
self.blank_rects = [] self.blank_rects = []
wg = layout_single_window() wg = layout_single_window(self.margin_width, self.padding_width)
for i, w in enumerate(windows): for i, w in enumerate(windows):
w.is_visible_in_layout = i == active_window_idx w.is_visible_in_layout = i == active_window_idx
w.set_geometry(wg) w.set_geometry(wg)
@@ -135,17 +141,23 @@ class Tall(Layout):
def __call__(self, windows, active_window_idx): def __call__(self, windows, active_window_idx):
self.blank_rects = br = [] self.blank_rects = br = []
if len(windows) == 1: if len(windows) == 1:
wg = layout_single_window() wg = layout_single_window(self.margin_width, self.padding_width)
windows[0].set_geometry(wg) windows[0].set_geometry(wg)
self.blank_rects = blank_rects_for_window(windows[0]) self.blank_rects = blank_rects_for_window(windows[0])
return return
xlayout = layout_dimension(viewport_size.width, cell_size.width, 2, self.border_width) xlayout = layout_dimension(
viewport_size.width, cell_size.width, 2, self.border_width,
margin_length=self.margin_width, padding_length=self.padding_width)
xstart, xnum = next(xlayout) xstart, xnum = next(xlayout)
ystart, ynum = next(layout_dimension(available_height(), cell_size.height, 1, self.border_width, left_align=True)) ystart, ynum = next(layout_dimension(
available_height(), cell_size.height, 1, self.border_width, left_align=True,
margin_length=self.margin_width, padding_length=self.padding_width))
windows[0].set_geometry(window_geometry(xstart, xnum, ystart, ynum)) windows[0].set_geometry(window_geometry(xstart, xnum, ystart, ynum))
vh = available_height() vh = available_height()
xstart, xnum = next(xlayout) xstart, xnum = next(xlayout)
ylayout = layout_dimension(available_height(), cell_size.height, len(windows) - 1, self.border_width, left_align=True) ylayout = layout_dimension(
available_height(), cell_size.height, len(windows) - 1, self.border_width, left_align=True,
margin_length=self.margin_width, padding_length=self.padding_width)
for w, (ystart, ynum) in zip(islice(windows, 1, None), ylayout): for w, (ystart, ynum) in zip(islice(windows, 1, None), ylayout):
w.set_geometry(window_geometry(xstart, xnum, ystart, ynum)) w.set_geometry(window_geometry(xstart, xnum, ystart, ynum))
left_blank_rect(windows[0], br, vh), top_blank_rect(windows[0], br, vh), right_blank_rect(windows[-1], br, vh) left_blank_rect(windows[0], br, vh), top_blank_rect(windows[0], br, vh), right_blank_rect(windows[-1], br, vh)

View File

@@ -25,11 +25,15 @@ from .fast_data_types import (
GLFW_STENCIL_BITS, Window, change_wcwidth, GLFW_STENCIL_BITS, Window, change_wcwidth,
enable_automatic_opengl_error_checking, glClear, glClearColor, glewInit, enable_automatic_opengl_error_checking, glClear, glClearColor, glewInit,
glfw_init, glfw_set_error_callback, glfw_swap_interval, glfw_terminate, glfw_init, glfw_set_error_callback, glfw_swap_interval, glfw_terminate,
glfw_wait_events, glfw_window_hint glfw_wait_events, glfw_window_hint, glfw_init_hint_string
) )
try:
from .fast_data_types import GLFW_X11_WM_CLASS_NAME, GLFW_X11_WM_CLASS_CLASS
except ImportError:
GLFW_X11_WM_CLASS_NAME = GLFW_X11_WM_CLASS_CLASS = None
from .layout import all_layouts from .layout import all_layouts
from .shaders import GL_VERSION from .shaders import GL_VERSION
from .utils import safe_print from .utils import safe_print, detach
defconf = os.path.join(config_dir, 'kitty.conf') defconf = os.path.join(config_dir, 'kitty.conf')
@@ -95,6 +99,13 @@ def option_parser():
default=False, default=False,
help=_('Output commands received from child process to stdout') help=_('Output commands received from child process to stdout')
) )
if not isosx:
a(
'--detach',
action='store_true',
default=False,
help=_('Detach from the controlling terminal, if any')
)
a( a(
'--replay-commands', '--replay-commands',
default=None, default=None,
@@ -188,7 +199,13 @@ def run_app(opts, args):
else: else:
viewport_size.width = opts.initial_window_width viewport_size.width = opts.initial_window_width
viewport_size.height = opts.initial_window_height viewport_size.height = opts.initial_window_height
window = Window(viewport_size.width, viewport_size.height, args.cls) try:
window = Window(viewport_size.width, viewport_size.height, args.cls)
except ValueError:
safe_print('Failed to create GLFW window with initial size:', viewport_size)
viewport_size.width = 640
viewport_size.height = 400
window = Window(viewport_size.width, viewport_size.height, args.cls)
window.set_title(appname) window.set_title(appname)
window.make_context_current() window.make_context_current()
if isosx: if isosx:
@@ -228,12 +245,38 @@ def on_glfw_error(code, msg):
safe_print('[glfw error] ', msg, file=sys.stderr) safe_print('[glfw error] ', msg, file=sys.stderr)
def ensure_osx_locale():
# Ensure the LANG env var is set. See
# https://github.com/kovidgoyal/kitty/issues/90
from .fast_data_types import cocoa_get_lang
if 'LANG' not in os.environ:
lang = cocoa_get_lang()
if lang is not None:
os.environ['LANG'] = lang + '.UTF-8'
def main(): def main():
locale.setlocale(locale.LC_ALL, '') if isosx:
ensure_osx_locale()
try:
locale.setlocale(locale.LC_ALL, '')
except Exception:
if not isosx:
raise
print('Failed to set locale with LANG:', os.environ.get('LANG'), file=sys.stderr)
os.environ.pop('LANG')
try:
locale.setlocale(locale.LC_ALL, '')
except Exception:
print('Failed to set locale with no LANG, ignoring', file=sys.stderr)
if os.environ.pop('KITTY_LAUNCHED_BY_LAUNCH_SERVICES', if os.environ.pop('KITTY_LAUNCHED_BY_LAUNCH_SERVICES',
None) == '1' and getattr(sys, 'frozen', True): None) == '1' and getattr(sys, 'frozen', True):
os.chdir(os.path.expanduser('~')) os.chdir(os.path.expanduser('~'))
if not os.path.isdir(os.getcwd()):
os.chdir(os.path.expanduser('~'))
args = option_parser().parse_args() args = option_parser().parse_args()
if getattr(args, 'detach', False):
detach()
if args.cmd: if args.cmd:
exec(args.cmd) exec(args.cmd)
return return
@@ -247,6 +290,8 @@ def main():
change_wcwidth(not opts.use_system_wcwidth) change_wcwidth(not opts.use_system_wcwidth)
glfw_set_error_callback(on_glfw_error) glfw_set_error_callback(on_glfw_error)
enable_automatic_opengl_error_checking(args.debug_gl) enable_automatic_opengl_error_checking(args.debug_gl)
if GLFW_X11_WM_CLASS_CLASS is not None:
glfw_init_hint_string(GLFW_X11_WM_CLASS_CLASS, opts.cls)
if not glfw_init(): if not glfw_init():
raise SystemExit('GLFW initialization failed') raise SystemExit('GLFW initialization failed')
try: try:

View File

@@ -58,5 +58,5 @@ def encode_mouse_event(tracking_mode, tracking_protocol, button, action, mods, x
ans = bytes(ans) ans = bytes(ans)
else: else:
if x <= 223 and y <= 223: if x <= 223 and y <= 223:
ans = bytearray([0o33, ord('['), cb + 32, x + 32, y + 32]) ans = bytearray([0o33, ord('['), ord('M'), cb + 32, x + 32, y + 32])
return ans return ans

View File

@@ -7,6 +7,7 @@
#include "data-types.h" #include "data-types.h"
#include <structmember.h> #include <structmember.h>
#include <limits.h>
#include "unicode-data.h" #include "unicode-data.h"
#include "tracker.h" #include "tracker.h"
#include "modes.h" #include "modes.h"
@@ -1332,6 +1333,14 @@ static PyGetSetDef getsetters[] = {
{NULL} /* Sentinel */ {NULL} /* Sentinel */
}; };
#if UINT_MAX == UINT32_MAX
#define T_COL T_UINT
#elif ULONG_MAX == UINT32_MAX
#define T_COL T_ULONG
#else
#error Neither int nor long is 4-bytes in size
#endif
static PyMemberDef members[] = { static PyMemberDef members[] = {
{"callbacks", T_OBJECT_EX, offsetof(Screen, callbacks), 0, "callbacks"}, {"callbacks", T_OBJECT_EX, offsetof(Screen, callbacks), 0, "callbacks"},
{"cursor", T_OBJECT_EX, offsetof(Screen, cursor), READONLY, "cursor"}, {"cursor", T_OBJECT_EX, offsetof(Screen, cursor), READONLY, "cursor"},
@@ -1341,11 +1350,11 @@ static PyMemberDef members[] = {
{"columns", T_UINT, offsetof(Screen, columns), READONLY, "columns"}, {"columns", T_UINT, offsetof(Screen, columns), READONLY, "columns"},
{"margin_top", T_UINT, offsetof(Screen, margin_top), READONLY, "margin_top"}, {"margin_top", T_UINT, offsetof(Screen, margin_top), READONLY, "margin_top"},
{"margin_bottom", T_UINT, offsetof(Screen, margin_bottom), READONLY, "margin_bottom"}, {"margin_bottom", T_UINT, offsetof(Screen, margin_bottom), READONLY, "margin_bottom"},
{"default_fg", T_ULONG, offsetof(Screen, default_fg), 0, "default_fg"}, {"default_fg", T_COL, offsetof(Screen, default_fg), 0, "default_fg"},
{"default_bg", T_ULONG, offsetof(Screen, default_bg), 0, "default_bg"}, {"default_bg", T_COL, offsetof(Screen, default_bg), 0, "default_bg"},
{"highlight_fg", T_ULONG, offsetof(Screen, highlight_fg), 0, "highlight_fg"}, {"highlight_fg", T_COL, offsetof(Screen, highlight_fg), 0, "highlight_fg"},
{"highlight_bg", T_ULONG, offsetof(Screen, highlight_bg), 0, "highlight_bg"}, {"highlight_bg", T_COL, offsetof(Screen, highlight_bg), 0, "highlight_bg"},
{"cursor_color", T_ULONG, offsetof(Screen, cursor_color), 0, "cursor_color"}, {"cursor_color", T_COL, offsetof(Screen, cursor_color), 0, "cursor_color"},
{NULL} {NULL}
}; };

View File

@@ -32,9 +32,12 @@ class Tab:
self.borders = Borders(opts) self.borders = Borders(opts)
self.windows = deque() self.windows = deque()
self.active_window_idx = 0 self.active_window_idx = 0
for i, which in enumerate('first second third fourth fifth sixth seventh eighth ninth tenth'.split()):
setattr(self, which + '_window', partial(self.nth_window, num=i))
if session_tab is None: if session_tab is None:
self.cwd = args.directory self.cwd = args.directory
l = self.enabled_layouts[0] l = self.enabled_layouts[0]
self.current_layout = all_layouts[l](opts, self.borders.border_width, self.windows)
if special_window is None: if special_window is None:
queue_action(self.new_window) queue_action(self.new_window)
else: else:
@@ -42,10 +45,8 @@ class Tab:
else: else:
self.cwd = session_tab.cwd or args.directory self.cwd = session_tab.cwd or args.directory
l = session_tab.layout l = session_tab.layout
self.current_layout = all_layouts[l](opts, self.borders.border_width, self.windows)
queue_action(self.startup, session_tab) queue_action(self.startup, session_tab)
self.current_layout = all_layouts[l](opts, self.borders.border_width, self.windows)
for i, which in enumerate('first second third fourth fifth sixth seventh eighth ninth tenth'.split()):
setattr(self, which + '_window', partial(self.nth_window, num=i))
def startup(self, session_tab): def startup(self, session_tab):
for cmd in session_tab.windows: for cmd in session_tab.windows:
@@ -328,9 +329,10 @@ class TabManager:
s.cursor.bold = s.cursor.italic = False s.cursor.bold = s.cursor.italic = False
s.cursor.fg = s.cursor.bg = 0 s.cursor.fg = s.cursor.bg = 0
s.draw('') s.draw('')
if s.cursor.x > s.columns - max_title_length: if s.cursor.x > s.columns - max_title_length and t is not self.tabs[-1]:
s.draw('') s.draw('')
break break
s.erase_in_line(0, False) # Ensure no long titles bleed after the last tab
s.update_cell_data( s.update_cell_data(
sprites.backend, self.color_profile, addressof(self.sprite_map), self.default_fg, self.default_bg, True) sprites.backend, self.color_profile, addressof(self.sprite_map), self.default_fg, self.default_bg, True)
sprites.render_dirty_cells() sprites.render_dirty_cells()

View File

@@ -14,7 +14,7 @@ from functools import lru_cache
from time import monotonic from time import monotonic
from .constants import isosx from .constants import isosx
from .fast_data_types import glfw_get_physical_dpi, wcwidth as wcwidth_impl from .fast_data_types import glfw_get_physical_dpi, wcwidth as wcwidth_impl, redirect_std_streams
from .rgb import Color, to_color from .rgb import Color, to_color
@@ -37,6 +37,13 @@ def wcwidth(c: str) -> int:
return wcwidth_impl(ord(c[0])) return wcwidth_impl(ord(c[0]))
@lru_cache()
def pt_to_px(pts):
dpix, dpiy = get_dpi()['logical']
dpi = (dpix + dpiy) / 2
return round(pts * dpi / 72)
@contextmanager @contextmanager
def timeit(name, do_timing=False): def timeit(name, do_timing=False):
if do_timing: if do_timing:
@@ -174,7 +181,7 @@ def get_primary_selection():
return '' # There is no primary selection on OS X return '' # There is no primary selection on OS X
# glfw has no way to get the primary selection # glfw has no way to get the primary selection
# https://github.com/glfw/glfw/issues/894 # https://github.com/glfw/glfw/issues/894
return subprocess.check_output(['xsel', '-p']).decode('utf-8') return subprocess.check_output(['xsel', '-p'], stderr=open(os.devnull, 'wb'), stdin=open(os.devnull, 'rb')).decode('utf-8')
def base64_encode( def base64_encode(
@@ -196,7 +203,7 @@ def set_primary_selection(text):
return # There is no primary selection on OS X return # There is no primary selection on OS X
if isinstance(text, str): if isinstance(text, str):
text = text.encode('utf-8') text = text.encode('utf-8')
p = subprocess.Popen(['xsel', '-i', '-p'], stdin=subprocess.PIPE) p = subprocess.Popen(['xsel', '-i', '-p'], stdin=subprocess.PIPE, stdout=open(os.devnull, 'wb'), stderr=subprocess.STDOUT)
p.stdin.write(text), p.stdin.close() p.stdin.write(text), p.stdin.close()
p.wait() p.wait()
@@ -208,3 +215,14 @@ def open_url(url, program='default'):
cmd = shlex.split(program) cmd = shlex.split(program)
cmd.append(url) cmd.append(url)
subprocess.Popen(cmd).wait() subprocess.Popen(cmd).wait()
def detach(fork=True, setsid=True, redirect=True):
if fork:
# Detach from the controlling process.
if os.fork() != 0:
raise SystemExit(0)
if setsid:
os.setsid()
if redirect:
redirect_std_streams(os.devnull)

View File

@@ -115,7 +115,7 @@ def init_env(debug=False, sanitize=False, native_optimizations=True):
cflags = os.environ.get( cflags = os.environ.get(
'OVERRIDE_CFLAGS', ( 'OVERRIDE_CFLAGS', (
'-Wextra -Wno-missing-field-initializers -Wall -std=c99 -D_XOPEN_SOURCE=700' '-Wextra -Wno-missing-field-initializers -Wall -std=c99 -D_XOPEN_SOURCE=700'
' -pedantic-errors -Werror {} {} -D{}DEBUG -fwrapv {} {} -pipe {}' ' -pedantic-errors -Werror {} {} -D{}DEBUG -fwrapv {} {} -pipe {} -fvisibility=hidden'
).format( ).format(
optimize, ' '.join(sanitize_args), ('' if debug else 'N'), stack_protector, missing_braces, '-march=native' optimize, ' '.join(sanitize_args), ('' if debug else 'N'), stack_protector, missing_braces, '-march=native'
if native_optimizations else '' if native_optimizations else ''
@@ -288,10 +288,7 @@ def build(args, native_optimizations=True):
def safe_makedirs(path): def safe_makedirs(path):
try: os.makedirs(path, exist_ok=True)
os.makedirs(path)
except FileExistsError:
pass
def build_test_launcher(args): def build_test_launcher(args):