root/usr.sbin/wsmoused/wsmoused.c
/* $OpenBSD: wsmoused.c,v 1.38 2021/10/24 21:24:19 deraadt Exp $ */

/*
 * Copyright (c) 2001 Jean-Baptiste Marchand, Julien Montagne and Jerome Verdon
 *
 * Copyright (c) 1998 by Kazutaka Yokota
 *
 * Copyright (c) 1995 Michael Smith
 *
 * Copyright (c) 1993 by David Dawes <dawes@xfree86.org>
 *
 * Copyright (c) 1990,91 by Thomas Roell, Dinkelscherben, Germany.
 *
 * All rights reserved.
 *
 * Most of this code was taken from the FreeBSD moused daemon, written by
 * Michael Smith. The FreeBSD moused daemon already contained code from the
 * Xfree Project, written by David Dawes and Thomas Roell and Kazutaka Yokota.
 *
 * Adaptation to OpenBSD was done by Jean-Baptiste Marchand, Julien Montagne
 * and Jerome Verdon.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *      This product includes software developed by
 *      David Dawes, Jean-Baptiste Marchand, Julien Montagne, Thomas Roell,
 *      Michael Smith, Jerome Verdon and Kazutaka Yokota.
 * 4. The name authors may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *
 */

#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/tty.h>
#include <dev/wscons/wsconsio.h>

#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <syslog.h>

#include "mouse_protocols.h"
#include "wsmoused.h"

#define DEFAULT_TTY     "/dev/ttyCcfg"

extern char *__progname;
extern char *mouse_names[];

int debug = 0;
int background = FALSE;
int nodaemon = FALSE;
int identify = FALSE;

mouse_t mouse = {
        .flags = 0,
        .portname = NULL,
        .ttyname = NULL,
        .proto = P_UNKNOWN,
        .rate = MOUSE_RATE_UNKNOWN,
        .resolution = MOUSE_RES_UNKNOWN,
        .mfd = -1,
        .clickthreshold = 500,  /* 0.5 sec */
};

/* identify the type of a wsmouse supported mouse */
void
wsmouse_identify(void)
{
        unsigned int type;

        if (mouse.mfd != -1) {
                if (ioctl(mouse.mfd, WSMOUSEIO_GTYPE, &type) == -1)
                        err(1, "can't detect mouse type");

                printf("wsmouse supported mouse: ");
                switch (type) {
                case WSMOUSE_TYPE_VSXXX:
                        printf("DEC serial\n");
                        break;
                case WSMOUSE_TYPE_PS2:
                        printf("PS/2 compatible\n");
                        break;
                case WSMOUSE_TYPE_USB:
                        printf("USB\n");
                        break;
                case WSMOUSE_TYPE_LMS:
                        printf("Logitech busmouse\n");
                        break;
                case WSMOUSE_TYPE_MMS:
                        printf("Microsoft InPort mouse\n");
                        break;
                case WSMOUSE_TYPE_TPANEL:
                        printf("Generic Touch Panel\n");
                        break;
                case WSMOUSE_TYPE_NEXT:
                        printf("NeXT\n");
                        break;
                case WSMOUSE_TYPE_ARCHIMEDES:
                        printf("Archimedes\n");
                        break;
                case WSMOUSE_TYPE_ADB:
                        printf("ADB\n");
                        break;
                case WSMOUSE_TYPE_HIL:
                        printf("HP-HIL\n");
                        break;
                case WSMOUSE_TYPE_LUNA:
                        printf("Omron Luna\n");
                        break;
                case WSMOUSE_TYPE_DOMAIN:
                        printf("Apollo Domain\n");
                        break;
                case WSMOUSE_TYPE_SUN:
                        printf("Sun\n");
                        break;
                default:
                        printf("Unknown\n");
                        break;
                }
        } else
                warnx("unable to open %s", mouse.portname);
}

/* wsmouse_init : init a wsmouse compatible mouse */
void
wsmouse_init(void)
{
        unsigned int res = WSMOUSE_RES_MIN;

        ioctl(mouse.mfd, WSMOUSEIO_SRES, &res);
}

/*
 * Buttons remapping
 */

/* physical to logical button mapping */
static int p2l[MOUSE_MAXBUTTON] = {
        MOUSE_BUTTON1,  MOUSE_BUTTON2,  MOUSE_BUTTON3,  MOUSE_BUTTON4,
        MOUSE_BUTTON5,  MOUSE_BUTTON6,  MOUSE_BUTTON7,  MOUSE_BUTTON8,
};

static char *
skipspace(char *s)
{
        while (isspace((unsigned char)*s))
                ++s;
        return s;
}

/* mouse_installmap : install a map between physical and logical buttons */
static int
mouse_installmap(char *arg)
{
        int pbutton;
        int lbutton;
        char *s;

        while (*arg) {
                arg = skipspace(arg);
                s = arg;
                while (isdigit((unsigned char)*arg))
                        ++arg;
                arg = skipspace(arg);
                if ((arg <= s) || (*arg != '='))
                        return FALSE;
                lbutton = atoi(s);

                arg = skipspace(++arg);
                s = arg;
                while (isdigit((unsigned char)*arg))
                        ++arg;
                if (arg <= s || (!isspace((unsigned char)*arg) && *arg != '\0'))
                        return FALSE;
                pbutton = atoi(s);

                if (lbutton <= 0 || lbutton > MOUSE_MAXBUTTON)
                        return FALSE;
                if (pbutton <= 0 || pbutton > MOUSE_MAXBUTTON)
                        return FALSE;
                p2l[pbutton - 1] = lbutton - 1;
        }
        return TRUE;
}

/* terminate signals handler */
static void
terminate(int sig)
{
        struct wscons_event event;
        unsigned int res;

        if (mouse.mfd != -1) {
                event.type = WSCONS_EVENT_WSMOUSED_OFF;
                ioctl(mouse.cfd, WSDISPLAYIO_WSMOUSED, &event);
                res = WSMOUSE_RES_DEFAULT;
                ioctl(mouse.mfd, WSMOUSEIO_SRES, &res);
                close(mouse.mfd);
                mouse.mfd = -1;
        }
        _exit(0);
}

/* buttons status (for multiple click detection) */
static struct {
        int count;              /* 0: up, 1: single click, 2: double click,... */
        struct timeval tv;      /* timestamp on the last `up' event */
} buttonstate[MOUSE_MAXBUTTON];

/*
 * handle button click
 * Note that an ioctl is sent for each button
 */
static void
mouse_click(struct wscons_event *event)
{
        struct timeval max_date;
        struct timeval now;
        struct timeval delay;
        int i; /* button number */
        
        i = event->value = p2l[event->value];

        gettimeofday(&now, NULL);
        delay.tv_sec = mouse.clickthreshold / 1000;
        delay.tv_usec = (mouse.clickthreshold % 1000) * 1000;
        timersub(&now, &delay, &max_date);

        if (event->type == WSCONS_EVENT_MOUSE_DOWN) {
                if (timercmp(&max_date, &buttonstate[i].tv, >)) {
                        timerclear(&buttonstate[i].tv);
                        buttonstate[i].count = 1;
                } else {
                        buttonstate[i].count++;
                }
        } else {
                /* button is up */
                buttonstate[i].tv.tv_sec = now.tv_sec;
                buttonstate[i].tv.tv_usec = now.tv_usec;
        }

        /*
         * we use the time field of wscons_event structure to put the number
         * of multiple clicks
         */
        if (event->type == WSCONS_EVENT_MOUSE_DOWN) {
                event->time.tv_sec = buttonstate[i].count;
                event->time.tv_nsec = 0;
        } else {
                /* button is up */
                event->time.tv_sec = 0;
                event->time.tv_nsec = 0;
        }
        ioctl(mouse.cfd, WSDISPLAYIO_WSMOUSED, event);
}

/* workaround for cursor speed on serial mice */
static void
normalize_event(struct wscons_event *event)
{
        int dx, dy;
        int two_power = 1;

/* 2: normal speed, 3: slower cursor, 1: faster cursor */
#define NORMALIZE_DIVISOR 3

        switch (event->type) {
        case WSCONS_EVENT_MOUSE_DELTA_X:
                dx = abs(event->value);
                while (dx > 2) {
                        two_power++;
                        dx = dx / 2;
                }
                event->value = event->value / (NORMALIZE_DIVISOR * two_power);
                break;
        case WSCONS_EVENT_MOUSE_DELTA_Y:
                two_power = 1;
                dy = abs(event->value);
                while (dy > 2) {
                        two_power++;
                        dy = dy / 2;
                }
                event->value = event->value / (NORMALIZE_DIVISOR * two_power);
                break;
        }
}

/* send a wscons_event to the kernel */
static void
treat_event(struct wscons_event *event)
{
        if (IS_MOTION_EVENT(event->type)) {
                ioctl(mouse.cfd, WSDISPLAYIO_WSMOUSED, event);
        } else if (IS_BUTTON_EVENT(event->type) &&
            (uint)event->value < MOUSE_MAXBUTTON) {
                mouse_click(event);
        }
}

/* split a full mouse event into multiples wscons events */
static void
split_event(mousestatus_t *act)
{
        struct wscons_event event;
        int button, i, mask;

        if (act->dx != 0) {
                event.type = WSCONS_EVENT_MOUSE_DELTA_X;
                event.value = act->dx;
                normalize_event(&event);
                treat_event(&event);
        }
        if (act->dy != 0) {
                event.type = WSCONS_EVENT_MOUSE_DELTA_Y;
                event.value = 0 - act->dy;
                normalize_event(&event);
                treat_event(&event);
        }
        if (act->dz != 0) {
                event.type = WSCONS_EVENT_MOUSE_DELTA_Z;
                event.value = act->dz;
                treat_event(&event);
        }
        if (act->dw != 0) {
                event.type = WSCONS_EVENT_MOUSE_DELTA_W;
                event.value = act->dw;
                treat_event(&event);
        }

        /* buttons state */
        mask = act->flags & MOUSE_BUTTONS;
        if (mask == 0)
                /* no button modified */
                return;

        button = MOUSE_BUTTON1DOWN;
        for (i = 0; (i < MOUSE_MAXBUTTON) && (mask != 0); i++) {
                if (mask & 1) {
                        event.type = (act->button & button) ?
                            WSCONS_EVENT_MOUSE_DOWN : WSCONS_EVENT_MOUSE_UP;
                        event.value = i;
                        treat_event(&event);
                }
                button <<= 1;
                mask >>= 1;
        }
}

/* main function */
static void
wsmoused(void)
{
        mousestatus_t action;
        struct wscons_event event; /* original wscons_event */
        struct pollfd pfd[1];
        int res;
        u_char b;

        /* notify kernel the start of wsmoused */
        event.type = WSCONS_EVENT_WSMOUSED_ON;
        res = ioctl(mouse.cfd, WSDISPLAYIO_WSMOUSED, &event);
        if (res != 0) {
                /* the display driver has no getchar() method */
                logerr(1, "this display driver has no support for wsmoused(8)");
        }

        bzero(&action, sizeof(action));
        bzero(&event, sizeof(event));
        bzero(&buttonstate, sizeof(buttonstate));

        pfd[0].fd = mouse.mfd;
        pfd[0].events = POLLIN;

        /* process mouse data */
        for (;;) {
                if (poll(pfd, 1, INFTIM) <= 0)
                        logwarn("failed to read from mouse");

                if (mouse.proto == P_WSCONS) {
                        /* wsmouse supported mouse */
                        read(mouse.mfd, &event, sizeof(event));
                        treat_event(&event);
                } else {
                        /* serial mouse (not supported by wsmouse) */
                        res = read(mouse.mfd, &b, 1);

                        /* if we have a full mouse event */
                        if (mouse_protocol(b, &action))
                                /* split it as multiple wscons_event */
                                split_event(&action);
                }
        }
}


static void
usage(void)
{
        fprintf(stderr, "usage: %s [-2dfi] [-C thresh] [-D device]"
            " [-M N=M]\n\t[-p device] [-t type]\n", __progname);
        exit(1);
}

int
main(int argc, char **argv)
{
        unsigned int type;
        int opt;
        int i;

#define GETOPT_STRING "2dfhip:t:C:D:M:"
        while ((opt = (getopt(argc, argv, GETOPT_STRING))) != -1) {
                switch (opt) {
                case '2':
                        /* on two button mice, right button pastes */
                        p2l[MOUSE_BUTTON3] = MOUSE_BUTTON2;
                        break;
                case 'd':
                        ++debug;
                        break;
                case 'f':
                        nodaemon = TRUE;
                        break;
                case 'h':
                        usage();
                        break;
                case 'i':
                        identify = TRUE;
                        nodaemon = TRUE;
                        break;
                case 'p':
                        if ((mouse.portname = strdup(optarg)) == NULL)
                                logerr(1, "out of memory");
                        break;
                case 't':
                        if (strcmp(optarg, "auto") == 0) {
                                mouse.proto = P_UNKNOWN;
                                mouse.flags &= ~NoPnP;
                                break;
                        }
                        for (i = 0; mouse_names[i] != NULL; i++)
                                if (strcmp(optarg,mouse_names[i]) == 0) {
                                        mouse.proto = i;
                                        mouse.flags |= NoPnP;
                                        break;
                                }
                        if (mouse_names[i] != NULL)
                                break;
                        warnx("no such mouse protocol `%s'", optarg);
                        usage();
                        break;
                case 'C':
#define MAX_CLICKTHRESHOLD 2000 /* max delay for double click */
                        mouse.clickthreshold = atoi(optarg);
                        if (mouse.clickthreshold < 0 ||
                            mouse.clickthreshold > MAX_CLICKTHRESHOLD) {
                                warnx("invalid threshold `%s': max value is %d",
                                    optarg, MAX_CLICKTHRESHOLD);
                                usage();
                        }
                        break;
                case 'D':
                        if ((mouse.ttyname = strdup(optarg)) == NULL)
                                logerr(1, "out of memory");
                        break;
                case 'M':
                        if (!mouse_installmap(optarg)) {
                                warnx("invalid mapping `%s'", optarg);
                                usage();
                        }
                        break;
                default:
                        usage();
                }
        }

        /*
         * Use defaults if unspecified
         */
        if (mouse.portname == NULL)
                mouse.portname = WSMOUSE_DEV;
        if (mouse.ttyname == NULL)
                mouse.ttyname = DEFAULT_TTY;

        if (identify == FALSE) {
                if ((mouse.cfd = open(mouse.ttyname, O_RDWR)) == -1)
                        logerr(1, "cannot open %s", mouse.ttyname);
        }

        if ((mouse.mfd = open(mouse.portname,
            O_RDONLY | O_NONBLOCK)) == -1)
                logerr(1, "unable to open %s", mouse.portname);

        /*
         * Find out whether the mouse device is a wsmouse device
         * or a serial device.
         */
        if (ioctl(mouse.mfd, WSMOUSEIO_GTYPE, &type) != -1)
                mouse.proto = P_WSCONS;
        else {
                if (mouse_identify() == P_UNKNOWN) {
                        close(mouse.mfd);
                        logerr(1, "cannot determine mouse type on %s",
                            mouse.portname);
                }
        }

        if (identify == TRUE) {
                if (mouse.proto == P_WSCONS)
                        wsmouse_identify();
                else
                        printf("serial mouse: %s type\n",
                            mouse_name(mouse.proto));
                exit(0);
        }

        signal(SIGINT, terminate);
        signal(SIGQUIT, terminate);
        signal(SIGTERM, terminate);

        if (mouse.proto == P_WSCONS)
                wsmouse_init();
        else
                mouse_init();

        if (!nodaemon) {
                openlog(__progname, LOG_PID, LOG_DAEMON);
                if (daemon(0, 0)) {
                        logerr(1, "failed to become a daemon");
                } else {
                        background = TRUE;
                }
        }

        wsmoused();
        exit(0);
}