diff --git a/kitty/data-types.c b/kitty/data-types.c index c1f48ecee..41662d3f2 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -226,6 +226,9 @@ extern bool init_mouse(PyObject *module); extern bool init_kittens(PyObject *module); extern bool init_logging(PyObject *module); extern bool init_png_reader(PyObject *module); +#ifdef __unix__ +extern bool init_utmp(PyObject *module); +#endif #ifdef __APPLE__ extern int init_CoreText(PyObject *); extern bool init_cocoa(PyObject *module); @@ -293,6 +296,9 @@ PyInit_fast_data_types(void) { if (!init_freetype_render_ui_text(m)) return NULL; #endif if (!init_fonts(m)) return NULL; +#if defined(__unix__) + if (!init_utmp(m)) return NULL; +#endif CellAttrs a; #define s(name, attr) { a.val = 0; a.attr = 1; PyModule_AddIntConstant(m, #name, shift_to_first_set_bit(a)); } diff --git a/kitty/debug_config.py b/kitty/debug_config.py index a12c7f94c..0a14d1ef3 100644 --- a/kitty/debug_config.py +++ b/kitty/debug_config.py @@ -152,6 +152,86 @@ def compare_opts(opts: KittyOpts, print: Callable) -> None: print('\n\t'.join(sorted(colors))) +is_linux = sys.platform in ('linux', 'linux2') + + +if is_linux: + import socket + import time + import re + import termios + from .fast_data_types import num_users + from typing import IO + + + class IssueData: + uname: os.uname_result + hostname: str + formatted_date: str + formatted_time: str + tty_name: str + baud_rate: int + num_users: int + def __init__(self): + self.uname = os.uname() + self.hostname = socket.gethostname() + _time = time.localtime() + self.formatted_time = time.strftime('%a %b %d %Y', _time) + self.formatted_date = time.strftime('%H:%M:%S', _time) + try: + self.tty_name = format_tty_name(os.ttyname(sys.stdin.fileno())) + except OSError: + self.tty_name = '(none)' + self.baud_rate = termios.tcgetattr(sys.stdin.fileno())[5] + self.num_users = num_users() + + + # https://kernel.googlesource.com/pub/scm/utils/util-linux/util-linux/+/v2.7.1/login-utils/agetty.c#790 + issue_mappings = { + # ctx = IssueData + 's': lambda ctx: ctx.uname.sysname, + 'n': lambda ctx: ctx.uname.nodename, + 'r': lambda ctx: ctx.uname.release, + 'v': lambda ctx: ctx.uname.version, + 'm': lambda ctx: ctx.uname.machine, + 'o': lambda ctx: ctx.hostname, + 'd': lambda ctx: ctx.formatted_date, + 't': lambda ctx: ctx.formatted_time, + 'l': lambda ctx: ctx.tty_name, + 'b': lambda ctx: ctx.baud_rate, + 'u': lambda ctx: ctx.num_users, + 'U': lambda ctx: str(ctx.num_users) + ' user' + ('' if ctx.num_users == 1 else 's'), + } + + + def translate_issue_char(ctx: IssueData, char: str) -> str: + assert len(char) == 1 + try: + return issue_mappings[char](ctx) + except KeyError: + return char + + + def format_tty_name(raw: str) -> str: + return re.sub(r'^/dev/([^/]+)/([^/]+)$', r'\1\2', raw) + + + def print_issue(issue_file: IO[str], print_fn) -> None: + last_char = None + issue_data = IssueData() + while this_char := issue_file.read(1): + if last_char == '\\': + print_fn(translate_issue_char(issue_data, this_char), end='') + elif last_char is not None: + print_fn(last_char, end='') + # `\\\a` should not match the last two slashes, + # so make it look like it was `\?\a` where `?` + # is some character other than `\`. + last_char = None if last_char == '\\' else this_char + if last_char is not None: + print_fn(last_char, end='') + + def debug_config(opts: KittyOpts) -> str: from io import StringIO out = StringIO() @@ -161,9 +241,9 @@ def debug_config(opts: KittyOpts) -> str: if is_macos: import subprocess p(' '.join(subprocess.check_output(['sw_vers']).decode('utf-8').splitlines()).strip()) - if os.path.exists('/etc/issue'): + if is_linux and os.path.exists('/etc/issue'): with open('/etc/issue', encoding='utf-8', errors='replace') as f: - p(f.read().strip()) + print_issue(f, p) if os.path.exists('/etc/lsb-release'): with open('/etc/lsb-release', encoding='utf-8', errors='replace') as f: p(f.read().strip()) diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index b85a9c1b1..bce610f35 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1238,3 +1238,6 @@ def get_os_window_size(os_window_id: int) -> Optional[OSWindowSize]: def get_all_processes() -> Tuple[int, ...]: pass + +def num_users() -> int: + pass diff --git a/kitty/utmp.c b/kitty/utmp.c new file mode 100644 index 000000000..e084c9b94 --- /dev/null +++ b/kitty/utmp.c @@ -0,0 +1,50 @@ +#if defined(__unix__) + +#define PY_SSIZE_T_CLEAN +#include +#include + +#include + +static PyObject* +num_users(PyObject *const self, PyObject *const args) { + (void)self; (void)args; + size_t users = 0; + Py_BEGIN_ALLOW_THREADS +#ifdef UTENT_REENTRANT + struct utmp *result = NULL; + struct utmp buffer = { 0, }; + while (true) { + if (getutent_r(&buffer, &result) == -1) { + Py_BLOCK_THREADS + return PyErr_SetFromErrno(PyExc_OSError); + } + if (result == NULL) { break; } + if (result->ut_type == USER_PROCESS) { users++; } + } +#else + struct utmp *ut; + setutent(); + while ((ut = getutent())) { + if (ut->ut_type == USER_PROCESS) { + users++; + } + } + endutent(); +#endif + Py_END_ALLOW_THREADS + return PyLong_FromSize_t(users); +} + +static PyMethodDef UtmpMethods[] = { + {"num_users", num_users, METH_NOARGS, "Get the number of users using UTMP data" }, + { NULL, NULL, 0, NULL }, +}; + +bool +init_utmp(PyObject *module) { + // 0 = success + return PyModule_AddFunctions(module, UtmpMethods) == 0; +} + +#endif