From 3fb9495b43bd1406809315fd0f94b4b051160059 Mon Sep 17 00:00:00 2001 From: Stephen Chandler Paul Date: Thu, 13 Feb 2014 16:03:56 -0500 Subject: [PATCH] Added support for monitoring pointing sticks #66658 Syndaemon can now use XSync in order to watch for activity from a laptop's pointing stick, and change the settings on the touchpad accordingly. By default, enabling point stick monitoring will also disable keyboard monitoring so that the user doesn't have to have keyboard monitoring enabled. However, keyboard monitoring can be enabled simultaneously by specifying -R on the command line. Since syndaemon can handle monitoring both devices from a single instance, one event can cancel out the other. For example, if the keyboard was being used and the touchpad was disabled as a result, then if the user begins using the pointing stick their touchpad will immediately be set to the appropriate disable state and the timeout for the keyboard disable state will be cleared. --- configure.ac | 11 ++ man/syndaemon.man | 49 +++++-- tools/Makefile.am | 2 +- tools/syndaemon.c | 381 +++++++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 357 insertions(+), 86 deletions(-) diff --git a/configure.ac b/configure.ac index 77edbb2..504974a 100644 --- a/configure.ac +++ b/configure.ac @@ -148,6 +148,17 @@ if test "x$have_libxtst" = "xyes" ; then AC_CHECK_HEADERS([X11/extensions/record.h],,,[#include ]) CPPFLAGS="$SAVE_CPPFLAGS" fi + +# The syndaemon program also has an optional XSync extension implementation that +# allows it to monitor the pointing stick on newer laptops where the pointing +# stick relies on the clickpad's software button emulation +PKG_CHECK_MODULES(XEXT, xext, have_libxext="yes", have_libxext="no") +if test "x$have_libxext" = "xyes" ; then + SAVE_CPPFLAGS="$CPPFLAGS" + CPPFLAGS="$CPPFLAGS $XEXT_CFLAGS" + AC_CHECK_HEADERS([X11/extensions/sync.h],,,[#include ]) + CPPFLAGS="$SAVE_CPPFLAGS" +fi # ----------------------------------------------------------------------------- # Workaround overriding sdkdir to be able to create a tarball when user has no diff --git a/man/syndaemon.man b/man/syndaemon.man index 30ea20b..f316468 100644 --- a/man/syndaemon.man +++ b/man/syndaemon.man @@ -3,12 +3,12 @@ .TH syndaemon __appmansuffix__ __vendorversion__ .SH NAME .LP -syndaemon \- a program that monitors keyboard activity and disables -the touchpad when the keyboard is being used. +syndaemon \- a program that monitors keyboard and/or pointing stick activity and +disables the touchpad when either of them are being used. .SH "SYNOPSIS" .LP -syndaemon [\fI\-i idle\-time\fP] [\fI\-m poll-inverval\fP] [\fI\-d\fP] [\fI\-p pid\-file\fP] -[\fI\-t\fP] [\fI\-k\fP] [\fI\-K\fP] [\fI\-R\fP] +syndaemon [\fI\-i keyboard\-idle\-time\fP] [\fI\-m poll-inverval\fP] [\fI\-d\fP] [\fI\-p pid\-file\fP] +[\fI\-t\fP] [\fI\-P\fP] [\fI\-I pointing\-stick\-idle\-time] [\fI\-k\fP] [\fI\-K\fP] [\fI\-R\fP] .SH "DESCRIPTION" .LP Disabling the touchpad while typing avoids unwanted movements of the @@ -17,19 +17,25 @@ pointer that could lead to giving focus to the wrong window. .SH "OPTIONS" .LP .TP -\fB\-i\fR <\fIidle\-time\fP> +\fB\-i\fR <\fIkeyboard\-idle\-time\fP> How many seconds to wait after the last key press before enabling the touchpad. . (default is 2.0s). .LP .TP +\fB\-I\fR <\fIpointing\-stick\-idle\-time\fP> +How many seconds to wait after the last pointing stick movement before enabling +the touchpad. +. +(default is 2.0s). +.LP +.TP \fB\-m\fR <\fIpoll\-interval\fP> How many milliseconds to wait between two polling intervals. If this value is -too low, it will cause unnecessary wake-ups. If this value is too high, -some key presses (press and release happen between two intervals) may not -be noticed. This switch has no effect when running with -\fB-R\fP. +too low, it will cause unnecessary wake-ups. If this value is too high, some key +presses (press and release happen between two intervals) may not be noticed. +This switch has no effect when running with \fB\-R\fP and/or \fB\-P\fP. . Default is 200ms. .LP @@ -52,6 +58,21 @@ mode. "off". If this option is given without a mode it defaults to "tapping". .LP .TP +\fB\-T\fP [off|tapping|click-only] +Same as \fB\-t\fP, but sets the disable state for pointing stick monitoring. +.LP +.TP +\fB\-P\fP [pointing-stick] +Monitors the pointing stick for activity instead of keyboard activity. XSync +support is required on the server for this to work. Useful on laptops that have +a pointing stick but lack a set of physical buttons for their pointing stick. +If this option is given without the name of a pointing stick, it defaults to +"TPPS/2 IBM TrackPoint". +. +When using this option, keyboard monitoring is disabled unless \fB\-R\fP is also +specified. +.LP +.TP \fB\-k\fP Ignore modifier keys when monitoring keyboard activity. .LP @@ -92,6 +113,16 @@ The fork into daemon mode failed or the pid file could not be created. .TP \fBExit code 4 XRECORD requested but not available or usable on the server. +.LP +.TP +\fBExit code 5 +Pointing stick monitoring requested, but XSync was not available or usable on +the server. +.LP +.TP +\fBExit code 6 +Pointing stick monitoring requested, but a pointing stick could not be found +(try specifying the name manually when using \fB\-P\fP). .SH "CAVEATS" .LP It doesn't make much sense to connect to a remote X server, because diff --git a/tools/Makefile.am b/tools/Makefile.am index e790905..02ea7ba 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -28,4 +28,4 @@ synclient_SOURCES = synclient.c syndaemon_SOURCES = syndaemon.c syndaemon_CFLAGS = $(AM_CFLAGS) $(XTST_CFLAGS) -syndaemon_LDFLAGS = $(AM_LDFLAGS) $(XTST_LIBS) +syndaemon_LDFLAGS = $(AM_LDFLAGS) $(XTST_LIBS) $(XEXT_LIBS) diff --git a/tools/syndaemon.c b/tools/syndaemon.c index b181d16..9388bcb 100644 --- a/tools/syndaemon.c +++ b/tools/syndaemon.c @@ -1,5 +1,6 @@ /* * Copyright © 2003-2004 Peter Osterlund + * Copyright © 2014 Stephen Chandler Paul * * Permission to use, copy, modify, distribute, and sell this software * and its documentation for any purpose is hereby granted without @@ -22,6 +23,7 @@ * * Authors: * Peter Osterlund (petero2@telia.com) + * Stephen Chandler Paul (thatslyude@gmail.com) */ #ifdef HAVE_CONFIG_H @@ -36,6 +38,11 @@ #include #endif /* HAVE_X11_EXTENSIONS_RECORD_H */ +#ifdef HAVE_X11_EXTENSIONS_SYNC_H +#include +#define DEFAULT_POINTING_STICK "TPPS/2 IBM TrackPoint" +#endif + #include #include #include @@ -54,8 +61,12 @@ enum TouchpadState { ClickOnly = 3 }; -static Bool pad_disabled - /* internal flag, this does not correspond to device state */ ; +enum TouchpadIdleMode { + KeyboardIdle = 0, + PStickIdle = 1, + NoIdle = 2 +}; +static enum TouchpadIdleMode pad_status; static int ignore_modifier_combos; static int ignore_modifier_keys; static int background; @@ -64,9 +75,18 @@ static Display *display; static XDevice *dev; static Atom touchpad_off_prop; static enum TouchpadState previous_state; -static enum TouchpadState disable_state = TouchpadOff; +static enum TouchpadState idle_state[2]; +static double idle_time[2]; +static Bool use_xrecord; static int verbose; +#ifdef HAVE_X11_EXTENSIONS_SYNC_H +Bool monitor_pstick = False; +static int sync_event; +static int pstick_id; /* trackpoint device */ +static char * pstick_name; +#endif /* HAVE_X11_EXTENSIONS_SYNC_H */ + #define KEYMAP_SIZE 32 static unsigned char keyboard_mask[KEYMAP_SIZE]; @@ -74,10 +94,13 @@ static void usage(void) { fprintf(stderr, - "Usage: syndaemon [-i idle-time] [-m poll-delay] [-d] [-t [off|tapping|click-only]] [-k]\n"); + "Usage: syndaemon [-i idle-time] [-m poll-delay] [-d] [-t [off|tapping|click-only]] [-k] [-T [pointing-stick]]\n"); fprintf(stderr, " -i How many seconds to wait after the last key press before\n"); fprintf(stderr, " enabling the touchpad. (default is 2.0s)\n"); + fprintf(stderr, + " -I How many seconds to wait after the last pointing stick\n" + " movement before enabling the touchpad.\n"); fprintf(stderr, " -m How many milli-seconds to wait until next poll.\n"); fprintf(stderr, " (default is 200ms)\n"); fprintf(stderr, " -d Start as a daemon, i.e. in the background.\n"); @@ -88,6 +111,15 @@ usage(void) " 'tapping' for disabling tapping and scrolling only,\n" " 'click-only' for disabling everything but physical clicks.\n"); fprintf(stderr, + " -T Disable state.\n" + " Same as -t, applies to pointing stick monitoring.\n"); + fprintf(stderr, + " -M Enables monitoring of the keyboard (default).\n"); + fprintf(stderr, + " -P Pointing Stick.\n" + " Enables monitoring of the pointing stick (disables keyboard \n" + " monitoring unless -R is also specified)\n"); + fprintf(stderr, " -k Ignore modifier keys when monitoring keyboard activity.\n"); fprintf(stderr, " -K Like -k but also ignore Modifier+Key combos.\n"); fprintf(stderr, " -R Use the XRecord extension.\n"); @@ -116,23 +148,31 @@ store_current_touchpad_state(void) * Toggle touchpad enabled/disabled state, decided by value. */ static void -toggle_touchpad(Bool enable) +set_touchpad_mode(enum TouchpadIdleMode new_mode) { unsigned char data; - if (pad_disabled && enable) { + if (pad_status != NoIdle && new_mode == NoIdle) { data = previous_state; - pad_disabled = False; + pad_status = NoIdle; if (verbose) - printf("Enable\n"); + printf("Enabling touchpad.\n"); } - else if (!pad_disabled && !enable && - previous_state != disable_state && previous_state != TouchpadOff) { - store_current_touchpad_state(); - pad_disabled = True; - data = disable_state; + else if (pad_status != new_mode && + previous_state != TouchpadOff && + (pad_status == NoIdle || + previous_state != idle_state[pad_status])) { + /* Don't get rid of the last state if we're switching from one idle mode + * to another */ + if (pad_status == NoIdle) + store_current_touchpad_state(); + + pad_status = new_mode; + data = idle_state[new_mode]; if (verbose) - printf("Disable\n"); + printf((new_mode == KeyboardIdle) ? + "Switching to keyboard disable state.\n" : + "Switching to pointing stick disable state.\n"); } else return; @@ -146,7 +186,7 @@ toggle_touchpad(Bool enable) static void signal_handler(int signum) { - toggle_touchpad(True); + set_touchpad_mode(NoIdle); if (pid_file) unlink(pid_file); @@ -185,6 +225,23 @@ install_signal_handler(void) } } + +static int +setup_xsync(Display * display) +{ + int sync_error; + int sync_major, sync_minor; + + if (!XSyncQueryExtension(display, &sync_event, &sync_error)) + return 1; + + if (!XSyncInitialize(display, &sync_major, &sync_minor)) + return 1; + if (verbose) + printf("X Sync extension version %d.%d\n", sync_major, sync_minor); + + return 0; +} /** * Return non-zero if the keyboard state has changed since the last call. */ @@ -227,7 +284,7 @@ get_time(void) } static void -main_loop(Display * display, double idle_time, int poll_delay) +main_loop(Display * display, int poll_delay) { double last_activity = 0.0; double current_time; @@ -242,13 +299,15 @@ main_loop(Display * display, double idle_time, int poll_delay) /* If system times goes backwards, touchpad can get locked. Make * sure our last activity wasn't in the future and reset if it was. */ if (last_activity > current_time) - last_activity = current_time - idle_time - 1; + last_activity = current_time - idle_time[KeyboardIdle] - 1; - if (current_time > last_activity + idle_time) { /* Enable touchpad */ - toggle_touchpad(True); + if (current_time > last_activity + idle_time[KeyboardIdle]) { + /* Enable touchpad */ + set_touchpad_mode(NoIdle); } - else { /* Disable touchpad */ - toggle_touchpad(False); + else { + /* Disable touchpad */ + set_touchpad_mode(KeyboardIdle); } usleep(poll_delay); @@ -394,8 +453,83 @@ is_modifier_pressed(const struct xrecord_callback_results *cbres) return 0; } +#ifdef HAVE_X11_EXTENSIONS_SYNC_H + +static XSyncAlarm +setup_device_alarm(Display * display, int device_id, XSyncTestType test_type, + int wait_time) { + XSyncAlarm alarm; + int alarm_flags; + XSyncAlarmAttributes attr; + XSyncValue delta, interval; + + int ncounters; + char idle_counter_name[20]; + XSyncCounter counter = 0; + XSyncSystemCounter * counters; + + snprintf(&idle_counter_name[0], sizeof(idle_counter_name), + "DEVICEIDLETIME %d", device_id); + counters = XSyncListSystemCounters(display, &ncounters); + + /* Find the idle counter for the trackpoint */ + for (int i = 0; i < ncounters; i++) { + if (strcmp(counters[i].name, &idle_counter_name[0]) == 0) { + counter = counters[i].counter; + break; + } + } + + if (counter == 0) + return 0; + + XSyncFreeSystemCounterList(counters); + + XSyncIntToValue(&delta, 0); + XSyncIntToValue(&interval, wait_time); + + attr.trigger.counter = counter; + attr.trigger.test_type = test_type; + attr.trigger.value_type = XSyncAbsolute; + attr.trigger.wait_value = interval; + attr.delta = delta; + attr.events = True; + + alarm_flags = XSyncCACounter | XSyncCAValueType | XSyncCATestType | + XSyncCAValue | XSyncCADelta; + + alarm = XSyncCreateAlarm(display, alarm_flags, &attr); + return alarm; +} + +static int +get_device_id(Display * display, const char * dev_name, + char * dev_type) { + XDeviceInfo * info = NULL; + int ndevices = 0; + int dev_id = 0; + Atom dev_type_atom; + + dev_type_atom = + (dev_type != NULL) ? XInternAtom(display, dev_type, True) : 0; + info = XListInputDevices(display, &ndevices); + + while (ndevices--) { + if (info[ndevices].type == dev_type_atom && + strcmp(info[ndevices].name, dev_name) == 0) { + dev_id = info[ndevices].id; + break; + } + } + + XFreeDeviceList(info); + return dev_id; +} + +#endif /* HAVE_X11_EXTENSIONS_SYNC_H */ + void -record_main_loop(Display * display, double idle_time) +record_and_pstick_main_loop(Display * display) { struct xrecord_callback_results cbres; @@ -403,42 +537,80 @@ record_main_loop(Display * display, double idle_time) XRecordClientSpec cspec = XRecordAllClients; Display *dpy_data; XRecordRange *range; - int i; - dpy_data = XOpenDisplay(NULL); /* we need an additional data connection. */ - range = XRecordAllocRange(); +#ifdef HAVE_X11_EXTENSIONS_SYNC_H + XSyncAlarm pt_leave_idle_alarm; + XSyncAlarm pt_enter_idle_alarm; - range->device_events.first = KeyPress; - range->device_events.last = KeyRelease; + if (monitor_pstick) { + pt_leave_idle_alarm = setup_device_alarm(display, pstick_id, + XSyncNegativeTransition, 1); + pt_enter_idle_alarm = setup_device_alarm(display, pstick_id, + XSyncPositiveTransition, + idle_time[PStickIdle] * 1000); + } +#endif /* HAVE_X11_EXTENSIONS_SYNC_H */ - context = XRecordCreateContext(dpy_data, 0, &cspec, 1, &range, 1); + if (use_xrecord) { + dpy_data = XOpenDisplay(NULL); /* we need an additional data connection. */ + range = XRecordAllocRange(); - XRecordEnableContextAsync(dpy_data, context, xrecord_callback, - (XPointer) & cbres); + range->device_events.first = KeyPress; + range->device_events.last = KeyRelease; - cbres.modifiers = XGetModifierMapping(display); - /* clear list of modifiers */ - for (i = 0; i < MAX_MODIFIERS; ++i) - cbres.pressed_modifiers[i] = 0; + context = XRecordCreateContext(dpy_data, 0, &cspec, 1, &range, 1); + + XRecordEnableContextAsync(dpy_data, context, xrecord_callback, + (XPointer) & cbres); + + cbres.modifiers = XGetModifierMapping(display); + /* clear list of modifiers */ + for (int i = 0; i < MAX_MODIFIERS; ++i) + cbres.pressed_modifiers[i] = 0; + } while (1) { - int fd = ConnectionNumber(dpy_data); + int keyboard_fd = 0; + int pstick_fd = 0; fd_set read_fds; int ret; - int disable_event = 0; + int keyboard_event = 0; struct timeval timeout; FD_ZERO(&read_fds); - FD_SET(fd, &read_fds); - ret = select(fd + 1 /* =(max descriptor in read_fds) + 1 */ , + if (use_xrecord) { + keyboard_fd = ConnectionNumber(dpy_data); + FD_SET(keyboard_fd, &read_fds); + } + if (monitor_pstick) { + pstick_fd = ConnectionNumber(display); + FD_SET(pstick_fd, &read_fds); + } + + ret = select((keyboard_fd > pstick_fd ? keyboard_fd : pstick_fd) + 1, + /* =(max descriptor in read_fds) + 1 */ &read_fds, NULL, NULL, - pad_disabled ? &timeout : NULL - /* timeout only required for enabling */ ); + pad_status == KeyboardIdle ? &timeout : NULL + /* timeout only required for enabling keyboard */); + + if (FD_ISSET(pstick_fd, &read_fds)) { + while (XPending(display)) { + XEvent event; + XSyncAlarmNotifyEvent * alarm_event; - if (FD_ISSET(fd, &read_fds)) { + XNextEvent(display, &event); + alarm_event = (XSyncAlarmNotifyEvent*)&event; + if (alarm_event->alarm == pt_leave_idle_alarm) + set_touchpad_mode(PStickIdle); + else if (pad_status == PStickIdle) + set_touchpad_mode(NoIdle); + } + } + + if (FD_ISSET(keyboard_fd, &read_fds)) { cbres.key_event = 0; cbres.non_modifier_event = 0; @@ -455,28 +627,28 @@ record_main_loop(Display * display, double idle_time) } if (!ignore_modifier_keys && cbres.key_event) { - disable_event = 1; + keyboard_event = 1; } if (cbres.non_modifier_event && !(ignore_modifier_combos && is_modifier_pressed(&cbres))) { - disable_event = 1; + keyboard_event = 1; } } - if (disable_event) { + if (keyboard_event) { /* adjust the enable_time */ - timeout.tv_sec = (int) idle_time; - timeout.tv_usec = (idle_time - (double) timeout.tv_sec) * 1.e6; + timeout.tv_sec = (int) idle_time[KeyboardIdle]; + timeout.tv_usec = + (idle_time[KeyboardIdle] - (double) timeout.tv_sec) * 1.e6; - toggle_touchpad(False); + set_touchpad_mode(KeyboardIdle); } - if (ret == 0 && pad_disabled) { /* timeout => enable event */ - toggle_touchpad(True); - } + if (ret == 0) /* timeout => enable event */ + set_touchpad_mode(NoIdle); - } /* end while(1) */ + } XFreeModifiermap(cbres.modifiers); } @@ -542,19 +714,42 @@ dp_get_device(Display * dpy) return dev; } +Bool +set_idle_state_option(enum TouchpadIdleMode mode, const char * state) { + if (state[0] == '-') + idle_state[mode] = (mode == KeyboardIdle) ? TouchpadOff : ClickOnly; + else if (strcmp(state, "off") == 0) + idle_state[mode] = TouchpadOff; + else if (strcmp(state, "tapping") == 0) + idle_state[mode] = TappingOff; + else if (strcmp(state, "click-only") == 0) + idle_state[mode] = ClickOnly; + else + return False; + + return True; +} + int main(int argc, char *argv[]) { - double idle_time = 2.0; int poll_delay = 200000; /* 200 ms */ int c; - int use_xrecord = 0; + + idle_time[KeyboardIdle] = 2.0; + idle_time[PStickIdle] = 2.0; + + idle_state[KeyboardIdle] = TouchpadOff; + idle_state[PStickIdle] = ClickOnly; /* Parse command line parameters */ - while ((c = getopt(argc, argv, ":i:m:dp:kKR?v")) != EOF) { + while ((c = getopt(argc, argv, ":i:I:m:dp:kKR?v")) != EOF) { switch (c) { case 'i': - idle_time = atof(optarg); + idle_time[KeyboardIdle] = atof(optarg); + break; + case 'I': + idle_time[PStickIdle] = atof(optarg); break; case 'm': poll_delay = atoi(optarg) * 1000; @@ -573,28 +768,39 @@ main(int argc, char *argv[]) ignore_modifier_keys = 1; break; case 'R': - use_xrecord = 1; + use_xrecord = True; break; case 'v': verbose = 1; break; case '?': - if (optopt != 't') - usage(); - else { +#ifdef HAVE_X11_EXTENSIONS_SYNC_H + if (optopt == 'P') { + monitor_pstick = True; if (optind < argc) { if (argv[optind][0] == '-') - disable_state = TappingOff; - else if (strcmp(argv[optind], "off") == 0) - disable_state = TouchpadOff; - else if (strcmp(argv[optind], "tapping") == 0) - disable_state = TappingOff; - else if (strcmp(argv[optind], "click-only") == 0) - disable_state = ClickOnly; + pstick_name = DEFAULT_POINTING_STICK; else - usage(); - } else - disable_state = TappingOff; + pstick_name = &argv[optind][0]; + } + else + pstick_name = DEFAULT_POINTING_STICK; + + idle_state[PStickIdle] = ClickOnly; + } + else if (optopt == 'T') { + if (!set_idle_state_option(PStickIdle, argv[optind])) + usage(); + } + else +#endif /* HAVE_X11_EXTENSIONS_SYNC_H */ + if (optopt == 't') { + if (!set_idle_state_option(KeyboardIdle, argv[optind])) + usage(); + } + else { + usage(); + break; } break; default: @@ -602,7 +808,7 @@ main(int argc, char *argv[]) break; } } - if (idle_time <= 0.0) + if (idle_time[KeyboardIdle] <= 0.0 || idle_time[PStickIdle] <= 0.0) usage(); /* Open a connection to the X server */ @@ -644,18 +850,41 @@ main(int argc, char *argv[]) } } - pad_disabled = False; + pad_status = NoIdle; store_current_touchpad_state(); #ifdef HAVE_X11_EXTENSIONS_RECORD_H - if (use_xrecord) { - if (check_xrecord(display)) - record_main_loop(display, idle_time); - else { + if (use_xrecord || monitor_pstick) { + if (use_xrecord && !check_xrecord(display)) { fprintf(stderr, "Use of XRecord requested, but failed to " " initialize.\n"); exit(4); } +#ifdef HAVE_X11_EXTENSIONS_SYNC_H + if (monitor_pstick) { + if (setup_xsync(display) != 0) { + fprintf(stderr, "Monitoring of pointing stick requested, but " + "XSync could not be initialized.\n"); + exit(5); + } + + pstick_id = get_device_id(display, pstick_name, + XI_MOUSE); + if (pstick_id == 0) { + fprintf(stderr, (pstick_name == DEFAULT_POINTING_STICK) ? + "Couldn't find a pointing stick to monitor.\n" : + "Couldn't find the specified pointing stick.\n"); + exit(6); + } + } +#else + if (monitor_pstick) { + fprintf(stderr, "XSync is required for monitoring the pointing " + "stick, but was not present when syndaemon was built.\n"); + exit(5); + } +#endif /* HAVE_X11_EXTENSIONS_RECORD_H */ + record_and_pstick_main_loop(display); } else #endif /* HAVE_X11_EXTENSIONS_RECORD_H */ @@ -663,7 +892,7 @@ main(int argc, char *argv[]) setup_keyboard_mask(display, ignore_modifier_keys); /* Run the main loop */ - main_loop(display, idle_time, poll_delay); + main_loop(display, poll_delay); } return 0; } -- 1.8.3.2