Automatically check for new releases and notify when an update is available, via the system notification facilities. Can be controlled by :opt:update_check_interval

Fixes #1342
This commit is contained in:
Kovid Goyal 2019-02-01 12:08:35 +05:30
parent 3aaa69b36c
commit d3ad15dc51
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 185 additions and 115 deletions

View File

@ -11,6 +11,10 @@ Changelog
- A new option :opt:`strip_trailing_spaces` to optionally remove trailing
spaces from lines when copying to clipboard.
- Automatically check for new releases and notify when an update is available,
via the system notification facilities. Can be controlled by
:opt:`update_check_interval` (:iss:`1342`)
- macOS: Fix :kbd:`cmd+period` key not working (:iss:`1318`)
- macOS: Add an option :opt:`macos_show_window_title_in_menubar` to not

View File

@ -16,7 +16,8 @@ from .conf.utils import to_cmdline
from .config import initial_window_size_func, prepare_config_file_for_editing
from .config_data import MINIMUM_FONT_SIZE
from .constants import (
appname, config_dir, kitty_exe, set_boss, supports_primary_selection
appname, config_dir, is_macos, kitty_exe, set_boss,
supports_primary_selection
)
from .fast_data_types import (
ChildMonitor, background_opacity_of, change_background_opacity,
@ -136,6 +137,9 @@ class Boss:
self.toggle_fullscreen()
else:
change_os_window_state(args.start_as)
if is_macos:
from .fast_data_types import cocoa_set_notification_activated_callback
cocoa_set_notification_activated_callback(self.notification_activated)
def add_os_window(self, startup_session, os_window_id=None, wclass=None, wname=None, opts_for_size=None, startup_id=None):
if os_window_id is None:
@ -355,6 +359,10 @@ class Boss:
if not getattr(self, 'io_thread_started', False):
self.child_monitor.start()
self.io_thread_started = True
if self.opts.update_check_interval > 0 and not hasattr(self, 'update_check_started'):
from .update_check import run_update_check
run_update_check(self.opts.update_check_interval * 60 * 60)
self.update_check_started = True
def activate_tab_at(self, os_window_id, x):
tm = self.os_window_map.get(os_window_id)
@ -973,3 +981,18 @@ class Boss:
os.remove(path)
except FileNotFoundError:
pass
def set_update_check_process(self, process):
self.update_check_process = process
def on_monitored_pid_death(self, pid, exit_status):
update_check_process = getattr(self, 'update_check_process', None)
if update_check_process is not None and pid == update_check_process.pid:
self.update_check_process = None
from .update_check import process_current_release
process_current_release(update_check_process.stdout.read().decode('utf-8'))
def notification_activated(self, identifier):
if identifier == 'new-version':
from .update_check import notification_activated
notification_activated()

View File

@ -116,6 +116,14 @@ get_dock_menu(id self UNUSED, SEL _cmd UNUSED, NSApplication *sender UNUSED) {
}
static PyObject *notification_activated_callback = NULL;
static PyObject*
set_notification_activated_callback(PyObject *self UNUSED, PyObject *callback) {
if (notification_activated_callback) Py_DECREF(notification_activated_callback);
notification_activated_callback = callback;
Py_INCREF(callback);
Py_RETURN_NONE;
}
@interface NotificationDelegate : NSObject <NSUserNotificationCenterDelegate>
@end
@ -177,46 +185,6 @@ cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
Py_RETURN_NONE;
}
static void
call_timer_callback(PyObject *timer_callback) {
PyObject *ret = PyObject_CallObject(timer_callback, NULL);
if (ret == NULL) PyErr_Print();
else Py_DECREF(ret);
}
static PyObject*
cocoa_run_notification_loop(PyObject *self UNUSED, PyObject *args) {
PyObject *timer_callback;
double timeout;
if (!PyArg_ParseTuple(args, "OOd", &notification_activated_callback, &timer_callback, &timeout)) return NULL;
NSAutoreleasePool *pool = [NSAutoreleasePool new];
NSApplication * application = [NSApplication sharedApplication];
// prevent icon in dock
[application setActivationPolicy:NSApplicationActivationPolicyAccessory];
signal(SIGTERM, SIG_IGN);
signal(SIGINT, SIG_IGN);
dispatch_source_t sigint = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGINT, 0, dispatch_get_main_queue());
dispatch_source_set_event_handler(sigint, ^{
[application terminate:nil];
});
dispatch_resume(sigint);
dispatch_source_t sigterm = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGINT, 0, dispatch_get_main_queue());
dispatch_source_set_event_handler(sigterm, ^{
[application terminate:nil];
});
dispatch_resume(sigterm);
// timer will fire after timeout, so fire it once at the start
call_timer_callback(timer_callback);
[NSTimer scheduledTimerWithTimeInterval:timeout
repeats:YES
block:^(NSTimer *timer UNUSED) {
call_timer_callback(timer_callback);
}];
[application run];
[pool drain];
return 0;
}
void
cocoa_create_global_menu(void) {
NSString* app_name = find_app_name();
@ -420,13 +388,15 @@ static void
cleanup() {
if (dockMenu) [dockMenu release];
dockMenu = nil;
if (notification_activated_callback) Py_DECREF(notification_activated_callback);
notification_activated_callback = NULL;
}
static PyMethodDef module_methods[] = {
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
{"cocoa_set_new_window_trigger", (PyCFunction)cocoa_set_new_window_trigger, METH_VARARGS, ""},
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
{"cocoa_run_notification_loop", (PyCFunction)cocoa_run_notification_loop, METH_VARARGS, ""},
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};

View File

@ -362,8 +362,6 @@ Use negative numbers to change scroll direction.'''))
# }}}
# }}}
g('mouse') # {{{
o('url_color', '#0087BD', option_type=to_color, long_text=_('''
@ -735,6 +733,12 @@ environment variables are expanded recursively, so if you use::
The value of MYVAR2 will be :code:`a/<path to home directory>/b`.
'''))
o('update_check_interval', 24, option_type=float, long_text=_('''
Periodically check if an update to kitty is available. If an update is found
a system notification is displayed informing you of the available update.
The default is to check every 24 hrs, set to zero to disable.
'''))
def startup_session(x):
if x.lower() == 'none':

View File

@ -2,89 +2,49 @@
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
import os
import subprocess
import time
from urllib.request import urlopen
from .constants import cache_dir, is_macos, logo_png_file, version
from .utils import open_url
CHANGELOG_URL = 'https://sw.kovidgoyal.net/kitty/changelog.html'
RELEASED_VERSION_URL = 'https://sw.kovidgoyal.net/kitty/current-version.txt'
CHECK_INTERVAL = 24 * 60 * 60
def version_notification_log():
return os.path.join(cache_dir(), 'new-version-notifications.txt')
def notify_new_version(version):
notify('kitty update available!', 'kitty version {} released'.format('.'.join(map(str, version))))
def get_released_version():
try:
raw = urlopen(RELEASED_VERSION_URL).read().decode('utf-8').strip()
except Exception:
raw = '0.0.0'
return tuple(map(int, raw.split('.')))
notified_versions = set()
def save_notification(version):
notified_versions.add(version)
version = '.'.join(map(str, version))
with open(version_notification_log(), 'a') as f:
print(version, file=f)
def already_notified(version):
if not hasattr(already_notified, 'read_cache'):
already_notified.read_cache = True
with open(version_notification_log()) as f:
for line in f:
notified_versions.add(tuple(map(int, line.strip().split('.'))))
return tuple(version) in notified_versions
from .constants import is_macos, logo_png_file
if is_macos:
from .fast_data_types import cocoa_send_notification, cocoa_run_notification_loop
from .fast_data_types import cocoa_send_notification
def notify(title, body, timeout=5000, application='kitty', icon=True, identifier=None):
def notify(
title,
body,
timeout=5000,
application='kitty',
icon=True,
identifier=None
):
if icon is True:
icon = None
cocoa_send_notification(identifier, title, body, icon)
def notification_activated(notification_identifier):
open_url(CHANGELOG_URL)
def do_check():
new_version = get_released_version()
if new_version > version and not already_notified(new_version):
save_notification(new_version)
notify_new_version(new_version)
def update_check():
cocoa_run_notification_loop(notification_activated, do_check, CHECK_INTERVAL)
else:
def notify(title, body, timeout=5000, application='kitty', icon=True, identifier=None):
cmd = ['notify-send', '-t', str(timeout), '-a', application]
# libnotify depends on GTK, so we are not using it, instead
# use the command line notify-send wrapper it provides
# May want to just implement this in glfw using DBUS
def notify(
title,
body,
timeout=-1,
application='kitty',
icon=True,
identifier=None
):
cmd = ['notify-send', '-a', application]
if timeout > -1:
cmd.append('-t'), cmd.append(str(timeout))
if icon is True:
icon = logo_png_file
if icon:
cmd.extend(['-i', icon])
subprocess.Popen(
cmd + [title, body], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
def update_check():
while True:
new_version = get_released_version()
if new_version > version and not already_notified(new_version):
save_notification(new_version)
notify_new_version(new_version)
time.sleep(CHECK_INTERVAL)
cmd + [title, body],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL,
)

109
kitty/update_check.py Normal file
View File

@ -0,0 +1,109 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
import os
import subprocess
import time
from collections import namedtuple
from urllib.request import urlopen
from .config import atomic_save
from .constants import cache_dir, get_boss, version
from .fast_data_types import add_timer, monitor_pid
from .notify import notify
from .utils import open_url
CHANGELOG_URL = 'https://sw.kovidgoyal.net/kitty/changelog.html'
RELEASED_VERSION_URL = 'https://sw.kovidgoyal.net/kitty/current-version.txt'
CHECK_INTERVAL = 24 * 60 * 60
Notification = namedtuple('Notification', 'version time_of_last_notification count')
def notification_activated():
open_url(CHANGELOG_URL)
def version_notification_log():
override = getattr(version_notification_log, 'override', None)
if override:
return override
return os.path.join(cache_dir(), 'new-version-notifications-1.txt')
def notify_new_version(release_version):
notify(
'kitty update available!',
'kitty version {} released'.format('.'.join(map(str, release_version))),
identifier='new-version',
)
def get_released_version():
try:
raw = urlopen(RELEASED_VERSION_URL).read().decode('utf-8').strip()
except Exception:
raw = '0.0.0'
return raw
def parse_line(line):
parts = line.split(',')
version, timestamp, count = parts
version = tuple(map(int, version.split('.')))
return Notification(version, float(timestamp), int(count))
def read_cache():
notified_versions = {}
try:
with open(version_notification_log()) as f:
for line in f:
try:
n = parse_line(line)
except Exception:
continue
notified_versions[n.version] = n
except FileNotFoundError:
pass
return notified_versions
def already_notified(version):
notified_versions = read_cache()
return version in notified_versions
def save_notification(version):
notified_versions = read_cache()
if version in notified_versions:
v = notified_versions[version]
notified_versions[version] = v._replace(time_of_last_notification=time.time(), count=v.count + 1)
else:
notified_versions[version] = Notification(version, time.time(), 1)
lines = []
for version in sorted(notified_versions):
n = notified_versions[version]
lines.append('{},{},{}'.format(
'.'.join(map(str, n.version)), n.time_of_last_notification, n.count))
atomic_save('\n'.join(lines).encode('utf-8'), version_notification_log())
def process_current_release(raw):
release_version = tuple(map(int, raw.split('.')))
if release_version > version and not already_notified(release_version):
save_notification(release_version)
notify_new_version(release_version)
def update_check(timer_id=None):
p = subprocess.Popen([
'kitty', '+runpy', 'from kitty.update_check import *; import time; time.sleep(2); print(get_released_version())'
], stdout=subprocess.PIPE)
monitor_pid(p.pid)
get_boss().set_update_check_process(p)
def run_update_check(interval=24 * 60 * 60):
update_check()
add_timer('update_check', update_check, interval)