1<?php
2
3/**
4 * Provides access to the command-line console. Instead of reading from or
5 * writing to stdin/stdout/stderr directly, this class provides a richer API
6 * including support for ANSI color and formatting, convenience methods for
7 * prompting the user, and the ability to interact with stdin/stdout/stderr
8 * in some other process instead of this one.
9 *
10 * @task  construct   Construction
11 * @task  interface   Interfacing with the User
12 * @task  internal    Internals
13 */
14final class PhutilConsole extends Phobject {
15
16  private static $console;
17
18  private $server;
19  private $channel;
20  private $messages = array();
21
22  private $flushing = false;
23  private $disabledTypes;
24
25
26/* -(  Console Construction  )----------------------------------------------- */
27
28
29  /**
30   * Use @{method:newLocalConsole} or @{method:newRemoteConsole} to construct
31   * new consoles.
32   *
33   * @task construct
34   */
35  private function __construct() {
36    $this->disabledTypes = new PhutilArrayWithDefaultValue();
37  }
38
39
40  /**
41   * Get the current console. If there's no active console, a new local console
42   * is created (see @{method:newLocalConsole} for details). You can change the
43   * active console with @{method:setConsole}.
44   *
45   * @return PhutilConsole  Active console.
46   * @task construct
47   */
48  public static function getConsole() {
49    if (empty(self::$console)) {
50      self::setConsole(self::newLocalConsole());
51    }
52    return self::$console;
53  }
54
55
56  /**
57   * Set the active console.
58   *
59   * @param PhutilConsole
60   * @return void
61   * @task construct
62   */
63  public static function setConsole(PhutilConsole $console) {
64    self::$console = $console;
65  }
66
67
68  /**
69   * Create a new console attached to stdin/stdout/stderr of this process.
70   * This is how consoles normally work -- for instance, writing output with
71   * @{method:writeOut} prints directly to stdout. If you don't create a
72   * console explicitly, a new local console is created for you.
73   *
74   * @return PhutilConsole A new console which operates on the pipes of this
75   *                       process.
76   * @task construct
77   */
78  public static function newLocalConsole() {
79    return self::newConsoleForServer(new PhutilConsoleServer());
80  }
81
82
83  public static function newConsoleForServer(PhutilConsoleServer $server) {
84    $console = new PhutilConsole();
85    $console->server = $server;
86    return $console;
87  }
88
89
90  public static function newRemoteConsole() {
91    $io_channel = new PhutilSocketChannel(
92      fopen('php://stdin', 'r'),
93      fopen('php://stdout', 'w'));
94    $protocol_channel = new PhutilPHPObjectProtocolChannel($io_channel);
95
96    $console = new PhutilConsole();
97    $console->channel = $protocol_channel;
98
99    return $console;
100  }
101
102
103/* -(  Interfacing with the User  )------------------------------------------ */
104
105
106  public function confirm($prompt, $default = false) {
107    $message = id(new PhutilConsoleMessage())
108      ->setType(PhutilConsoleMessage::TYPE_CONFIRM)
109      ->setData(
110        array(
111          'prompt'  => $prompt,
112          'default' => $default,
113        ));
114
115    $this->writeMessage($message);
116    $response = $this->waitForMessage();
117
118    return $response->getData();
119  }
120
121  public function prompt($prompt, $history = '') {
122    $message = id(new PhutilConsoleMessage())
123      ->setType(PhutilConsoleMessage::TYPE_PROMPT)
124      ->setData(
125        array(
126          'prompt'  => $prompt,
127          'history' => $history,
128        ));
129
130    $this->writeMessage($message);
131    $response = $this->waitForMessage();
132
133    return $response->getData();
134  }
135
136  public function sendMessage($data) {
137    $message = id(new PhutilConsoleMessage())->setData($data);
138    return $this->writeMessage($message);
139  }
140
141  public function writeOut($pattern /* , ... */) {
142    $args = func_get_args();
143    return $this->writeTextMessage(PhutilConsoleMessage::TYPE_OUT, $args);
144  }
145
146  public function writeErr($pattern /* , ... */) {
147    $args = func_get_args();
148    return $this->writeTextMessage(PhutilConsoleMessage::TYPE_ERR, $args);
149  }
150
151  public function writeLog($pattern /* , ... */) {
152    $args = func_get_args();
153    return $this->writeTextMessage(PhutilConsoleMessage::TYPE_LOG, $args);
154  }
155
156  public function beginRedirectOut() {
157    // We need as small buffer as possible. 0 means infinite, 1 means 4096 in
158    // PHP < 5.4.0.
159    ob_start(array($this, 'redirectOutCallback'), 2);
160    $this->flushing = true;
161  }
162
163  public function endRedirectOut() {
164    $this->flushing = false;
165    ob_end_flush();
166  }
167
168
169/* -(  Internals  )---------------------------------------------------------- */
170
171  // Must be public because it is called from output buffering.
172  public function redirectOutCallback($string) {
173    if (strlen($string)) {
174      $this->flushing = false;
175      $this->writeOut('%s', $string);
176      $this->flushing = true;
177    }
178    return '';
179  }
180
181  private function writeTextMessage($type, array $argv) {
182
183    $message = id(new PhutilConsoleMessage())
184      ->setType($type)
185      ->setData($argv);
186
187    $this->writeMessage($message);
188
189    return $this;
190  }
191
192  private function writeMessage(PhutilConsoleMessage $message) {
193    if ($this->disabledTypes[$message->getType()]) {
194      return $this;
195    }
196
197    if ($this->flushing) {
198      ob_flush();
199    }
200    if ($this->channel) {
201      $this->channel->write($message);
202      $this->channel->flush();
203    } else {
204      $response = $this->server->handleMessage($message);
205      if ($response) {
206        $this->messages[] = $response;
207      }
208    }
209    return $this;
210  }
211
212  private function waitForMessage() {
213    if ($this->channel) {
214      $message = $this->channel->waitForMessage();
215    } else if ($this->messages) {
216      $message = array_shift($this->messages);
217    } else {
218      throw new Exception(
219        pht(
220          '%s called with no messages!',
221          __FUNCTION__.'()'));
222    }
223
224    return $message;
225  }
226
227  public function getServer() {
228    return $this->server;
229  }
230
231  private function disableMessageType($type) {
232    $this->disabledTypes[$type] += 1;
233    return $this;
234  }
235
236  private function enableMessageType($type) {
237    if ($this->disabledTypes[$type] == 0) {
238      throw new Exception(pht("Message type '%s' is already enabled!", $type));
239    }
240    $this->disabledTypes[$type] -= 1;
241    return $this;
242  }
243
244  public function disableOut() {
245    return $this->disableMessageType(PhutilConsoleMessage::TYPE_OUT);
246  }
247
248  public function enableOut() {
249    return $this->enableMessageType(PhutilConsoleMessage::TYPE_OUT);
250  }
251
252  public function isLogEnabled() {
253    $message = id(new PhutilConsoleMessage())
254      ->setType(PhutilConsoleMessage::TYPE_ENABLED)
255      ->setData(
256        array(
257          'which' => PhutilConsoleMessage::TYPE_LOG,
258        ));
259
260    $this->writeMessage($message);
261    $response = $this->waitForMessage();
262
263    return $response->getData();
264  }
265
266  public function isErrATTY() {
267    $message = id(new PhutilConsoleMessage())
268      ->setType(PhutilConsoleMessage::TYPE_TTY)
269      ->setData(
270        array(
271          'which' => PhutilConsoleMessage::TYPE_ERR,
272        ));
273
274    $this->writeMessage($message);
275    $response = $this->waitForMessage();
276
277    return $response->getData();
278  }
279
280  public function getErrCols() {
281    $message = id(new PhutilConsoleMessage())
282      ->setType(PhutilConsoleMessage::TYPE_COLS)
283      ->setData(
284        array(
285          'which' => PhutilConsoleMessage::TYPE_ERR,
286        ));
287
288    $this->writeMessage($message);
289    $response = $this->waitForMessage();
290
291    return $response->getData();
292  }
293
294
295}
296