aboutsummaryrefslogtreecommitdiffstats
path: root/src/headless.c
blob: dab817d39c4add008a605480684f1e5635d30115 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/* The headless driver (DESIGN.md, "headless from day one").
 *
 * The whole compositor with no tty: commands in on stdin, replies out on
 * stdout. The vocabulary lives in cmd.c and is shared with the control socket,
 * so a script written against one works against the other.
 *
 * `settle` is implemented here because it needs an event loop.
 */
#define _GNU_SOURCE
#include "sl0ppty.h"

#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "app.h"
#include "cmd.h"

#define MAX_PANES 64

static int64_t ms_now(void) {
  struct timespec ts;
  clock_gettime(CLOCK_MONOTONIC, &ts);
  return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}

/* Pump every pane until none has produced anything for `quiet` ms. */
static void settle(app_t *a, int quiet) {
  int64_t last = ms_now();
  int64_t deadline = last + 3000;
  while (!app_should_quit(a) && ms_now() < deadline) {
    int fds[MAX_PANES];
    size_t n = app_fds(a, fds, MAX_PANES);
    if (n == 0) break;
    struct pollfd pfds[MAX_PANES];
    for (size_t i = 0; i < n; i++)
      pfds[i] = (struct pollfd){.fd = fds[i], .events = POLLIN};
    if (poll(pfds, (nfds_t)n, 10) > 0) {
      for (size_t i = 0; i < n; i++)
        if (pfds[i].revents) {
          app_pump_fd(a, pfds[i].fd);
          last = ms_now();
        }
    }
    app_reap(a);
    if (ms_now() - last >= quiet) break;
  }
}

int run_headless(const char *const argv[], uint16_t cols, uint16_t rows,
                 int idle_ms, bool script, const char *layout) {
  app_t *a = app_new(argv, cols, rows);
  if (!a) {
    fprintf(stderr, "sl0ppty: cannot spawn pane\n");
    return 1;
  }
  if (layout) {
    char err[256] = {0};
    if (!app_apply_layout_file(a, layout, true, err, sizeof err))
      fprintf(stderr, "sl0ppty: %s: %s\n", layout, err[0] ? err : "bad layout");
  }
  app_resize(a, cols, rows);
  screen_t s;
  screen_init(&s, cols, rows);
  input_parser_t *in = input_new();

  if (!script) { /* one-shot: run it, let it settle, print the screen */
    settle(a, idle_ms);
    app_compose(a, &s);
    char *dump = screen_dump(&s);
    fputs(dump, stdout);
    free(dump);
    goto done;
  }

  /* Paint once before accepting input: a click resolves against what was
   * painted, and in a real session a frame always precedes the pointer. */
  app_compose(a, &s);

  char line[4096];
  bool quit = false;
  while (!quit && fgets(line, sizeof line, stdin)) {
    line[strcspn(line, "\r\n")] = 0;
    if (!line[0]) continue;

    if (strncmp(line, "settle", 6) == 0) {
      settle(a, line[6] == ' ' ? atoi(line + 7) : idle_ms);
      continue;
    }

    char *reply = cmd_exec(a, &s, in, line, &quit);
    if (!reply) {
      fprintf(stderr, "sl0ppty: unknown command: %s\n", line);
      continue;
    }
    /* Only commands that answer print a line, so the harness stays in step. */
    if (*reply) {
      fputs(reply, stdout);
      fputc('\n', stdout);
      fflush(stdout);
    }
    free(reply);
  }

done:
  input_free(in);
  screen_free(&s);
  app_free(a);
  return 0;
}